ykt-wxapp/pages/case/components/archive-detail/follow-up-manage-tab.vue
2026-02-09 17:09:44 +08:00

1315 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/customer-detail/followup-manage/followup-manage.vue -->
<view class="wrap">
<view class="top">
<view class="top-row">
<view class="my" @click="toggleMy">
<image
:src="`/static/checkbox${query.isMy ? '-checked' : ''}.svg`"
class="checkbox"
/>
<view class="my-text">我的</view>
</view>
<view class="status-scroll">
<scroll-view scroll-x>
<view class="status-tabs">
<view
v-for="t in statusTabs"
:key="t.value"
class="status-tab"
:class="{ active: query.status === t.value }"
@click="toggleStatus(t.value)"
>
{{ t.label }}
</view>
</view>
</scroll-view>
</view>
<view class="filter-btn" @click="openFilter">
<image
class="filter-icon"
:src="`/static/icons/icon-filter${filtered ? '-active' : ''}.svg`"
/>
</view>
</view>
<view class="total"
><text class="total-num">{{ total }}</text
></view
>
</view>
<view class="list">
<view v-for="i in list" :key="i._id" class="card" @click="toDetail(i)">
<view class="head">
<view class="date"
>计划日期: <text class="date-val">{{ i.planDate }}</text></view
>
<view class="executor truncate"
>{{ i.executorName
}}<text v-if="i.executeTeamName"
>{{ i.executeTeamName }}</text
></view
>
</view>
<view class="body">
<view class="title-row">
<view class="type">{{ i.eventTypeLabel }}</view>
<view class="status" :class="`st-${i.status}`">{{
i.eventStatusLabel
}}</view>
</view>
<view class="content">{{ i.taskContent || "暂无内容" }}</view>
<view
v-if="i.sendContent || (i.fileList && i.fileList.length > 0)"
class="send-content-wrapper"
>
<view class="send-content-section">
<view class="send-content-label">发送内容:</view>
<view class="send-content-body">
<view v-if="i.sendContent" class="send-text">{{
i.sendContent
}}</view>
<view
v-if="i.fileList && i.fileList.length > 0"
class="file-list"
:class="{ 'no-send-text': !i.sendContent }"
>
<view
v-for="(file, idx) in i.fileList"
:key="idx"
class="file-item"
:class="`file-type-${file.type}`"
>
<view class="file-name">{{
file.file?.name || file.name
}}</view>
</view>
</view>
</view>
</view>
<button
v-if="canShowSendButton(i)"
class="action-btn send-btn"
@click.stop="goChatAndSend(i)"
>
发送
</button>
</view>
<view v-if="i.status === 'treated'" class="result"
>处理结果 {{ i.result || "" }}</view
>
<view class="footer-row">
<view class="footer"
>创建: {{ i.createTimeStr }} {{ i.creatorName }}</view
>
</view>
</view>
</view>
<view v-if="list.length === 0" class="empty">暂无数据</view>
<uni-load-more
v-if="list.length"
:status="moreStatus"
:contentText="loadMoreText"
@clickLoadMore="getMore"
/>
</view>
<view class="fab" :style="{ bottom: `${floatingBottom}px` }" @click="add">
<uni-icons type="plusempty" size="24" color="#fff" />
</view>
<!-- 筛选弹窗简化 mock -->
<uni-popup ref="filterPopupRef" type="bottom" :mask-click="true">
<view class="popup">
<view class="popup-title">
<view class="popup-title-text">全部筛选</view>
<view class="popup-close" @click="closeFilter(true)">
<uni-icons type="closeempty" size="18" color="#666" />
</view>
</view>
<scroll-view scroll-y class="popup-body">
<view class="section">
<view class="section-title">任务状态</view>
<view class="chip-wrap">
<view
v-for="t in statusTabs"
:key="t.value"
class="chip"
:class="{ active: query.status === t.value }"
@click="query.status = t.value"
>
{{ t.label }}
</view>
</view>
</view>
<view class="section">
<view class="section-title">任务类型</view>
<view class="chip-wrap">
<view
v-for="t in typeOptions"
:key="t.value"
class="chip"
:class="{ active: typeSelectedMap[t.value] }"
@click="toggleType(t.value)"
>
{{ t.label }}
</view>
</view>
</view>
<view class="section">
<view class="section-title">所属团队</view>
<picker
mode="selector"
:range="teamOptions"
range-key="label"
@change="pickTeam"
>
<view class="select-row">
<view
class="select-text"
:class="{ muted: teamPicked.value === 'ALL' }"
>{{ teamPicked.label }}</view
>
<uni-icons type="right" size="16" color="#999" />
</view>
</picker>
</view>
<view class="section">
<view class="section-title">计划日期</view>
<view class="range-row">
<picker mode="date" @change="pickStart">
<view class="range-pill" :class="{ muted: !planRange[0] }">{{
planRange[0] || "开始日期"
}}</view>
</picker>
<view class="sep">-</view>
<picker mode="date" @change="pickEnd">
<view class="range-pill" :class="{ muted: !planRange[1] }">{{
planRange[1] || "结束日期"
}}</view>
</picker>
<view class="clear" @click="clearPlanRange">
<uni-icons type="closeempty" size="16" color="#999" />
</view>
</view>
</view>
</scroll-view>
<view class="actions">
<button class="btn plain" @click="resetFilter">重置</button>
<button class="btn primary" @click="confirmFilter">确定</button>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { computed, onMounted, onUnmounted, reactive, ref, watch } from "vue";
import { storeToRefs } from "pinia";
import dayjs from "dayjs";
import api from "@/utils/api";
import useAccountStore from "@/store/account";
import { toast } from "@/utils/widget";
import {
getTodoEventTypeLabel,
getTodoEventTypeOptions,
} from "@/utils/todo-const";
const props = defineProps({
data: { type: Object, default: () => ({}) },
archiveId: { type: String, default: "" },
reachBottomTime: { type: [String, Number], default: "" },
floatingBottom: { type: Number, default: 16 },
fromChat: { type: Boolean, default: false },
});
const accountStore = useAccountStore();
const { account, doctorInfo } = storeToRefs(accountStore);
const { getDoctorInfo } = accountStore;
function getUserId() {
const d = doctorInfo.value || {};
const a = account.value || {};
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || "") || "";
}
function getCorpId() {
const team = uni.getStorageSync("ykt_case_current_team") || {};
const d = doctorInfo.value || {};
const a = account.value || {};
return String(d.corpId || a.corpId || team.corpId || "") || "";
}
function getCurrentTeamId() {
const team = uni.getStorageSync("ykt_case_current_team") || {};
return String(team?.teamId || team?._id || team?.id || "") || "";
}
function normalizeGroupId(v) {
const s = String(v || "").trim();
if (!s) return "";
return s.startsWith("GROUP") ? s.slice(5) : s;
}
const statusTabs = [
{ label: "全部", value: "all" },
{ label: "待处理", value: "processing" },
{ label: "未开始", value: "notStart" },
{ label: "已完成", value: "treated" },
{ label: "已取消", value: "cancelled" },
{ label: "已过期", value: "expired" },
];
const typeOptions = [
{ label: "全部", value: "all" },
...getTodoEventTypeOptions(),
];
const teamOptions = ref([{ label: "全部", value: "ALL" }]);
const query = reactive({
isMy: false,
status: "all",
eventTypes: [],
teamId: "ALL",
planRange: ["", ""],
});
const list = ref([]);
const total = ref(0);
const chatGroupId = ref("");
const currentChatGroupId = computed(() => normalizeGroupId(chatGroupId.value || ""));
const PENDING_FOLLOWUP_SEND_STORAGE_KEY = "ykt_followup_pending_send";
const page = ref(1);
const pageSize = 10;
const pages = ref(1);
const loading = ref(false);
const userNameMap = ref({});
const moreStatus = computed(() => {
if (loading.value) return "loading";
return page.value <= pages.value ? "more" : "no-more";
});
const loadMoreText = {
contentdown: "点击加载更多",
contentrefresh: "加载中...",
contentnomore: "没有更多了",
};
const filtered = ref(false);
const typeSelectedMap = computed(() => {
const s = new Set(query.eventTypes || []);
return typeOptions.reduce((acc, cur) => {
if (cur.value === "all") acc[cur.value] = s.size === 0;
else acc[cur.value] = s.has(cur.value);
return acc;
}, {});
});
async function ensureDoctor() {
if (doctorInfo.value) return;
if (!account.value?.openid) return;
try {
await getDoctorInfo();
} catch {
// ignore
}
}
function statusLabelFromStatus(status) {
const map = {
processing: "待处理",
notStart: "未开始",
treated: "已完成",
cancelled: "已取消",
expired: "已过期",
};
return map[status] || "未知";
}
function getStatus(todo) {
const endOfToday = dayjs().endOf("day").valueOf();
const startOfToday = dayjs().startOf("day").valueOf();
const plannedExecutionTime = Number(todo?.plannedExecutionTime || 0) || 0;
const expireTime = Number(todo?.expireTime || 0) || 0;
const eventStatus = String(todo?.eventStatus || "");
if (eventStatus === "treated") return "treated";
if (eventStatus === "closed") return "cancelled";
if (eventStatus === "expire") return "expired";
if (eventStatus === "untreated") {
if (expireTime && expireTime < startOfToday) return "expired";
if (plannedExecutionTime >= endOfToday) return "notStart";
if (
plannedExecutionTime <= startOfToday &&
(!expireTime || expireTime >= endOfToday)
)
return "processing";
return "processing";
}
return "processing";
}
function eventTypeLabel(eventType) {
return getTodoEventTypeLabel(eventType);
}
function resolveUserName(userId) {
const id = String(userId || "");
if (!id) return "";
const map = userNameMap.value || {};
return String(map[id] || "") || id;
}
function formatTodo(todo) {
const status = getStatus(todo);
const plannedExecutionTime = todo?.plannedExecutionTime;
const createTime = todo?.createTime;
return {
...todo,
status,
eventStatusLabel: statusLabelFromStatus(status),
eventTypeLabel: eventTypeLabel(todo?.eventType),
planDate:
plannedExecutionTime && dayjs(plannedExecutionTime).isValid()
? dayjs(plannedExecutionTime).format("YYYY-MM-DD")
: "",
createTimeStr:
createTime && dayjs(createTime).isValid()
? dayjs(createTime).format("YYYY-MM-DD HH:mm")
: "",
executorName: resolveUserName(todo?.executorUserId),
creatorName: resolveUserName(todo?.creatorUserId),
};
}
function resetList() {
page.value = 1;
pages.value = 1;
list.value = [];
getMore();
}
async function getMore() {
if (!props.archiveId) return;
if (loading.value) return;
if (page.value > pages.value) return;
loading.value = true;
try {
await ensureDoctor();
const corpId = getCorpId();
const userId = getUserId();
if (!corpId) {
toast("缺少 corpId请先完成登录/团队选择");
return;
}
const params = {
corpId,
customerId: String(props.archiveId),
page: page.value,
pageSize,
};
if (query.status !== "all") params.statusList = [query.status];
if (query.isMy) params.executorUserId = userId;
if (Array.isArray(query.eventTypes) && query.eventTypes.length)
params.eventType = query.eventTypes;
if (query.teamId && query.teamId !== "ALL") params.teamId = query.teamId;
if (query.planRange?.[0]) params.startDate = query.planRange[0];
if (query.planRange?.[1]) params.endDate = query.planRange[1];
const res = await api("getCustomerTodos", params);
if (!res?.success) {
toast(res?.message || "获取回访任务失败");
return;
}
const arr = Array.isArray(res.data) ? res.data : [];
const next = arr.map(formatTodo);
total.value = typeof res.total === "number" ? res.total : 0;
pages.value = Math.ceil(total.value / pageSize) || 0;
list.value = page.value === 1 ? next : [...list.value, ...next];
page.value += 1;
} finally {
loading.value = false;
}
}
function toggleMy() {
query.isMy = !query.isMy;
resetList();
}
function toggleStatus(v) {
query.status = v;
resetList();
}
async function loadTeams() {
await ensureDoctor();
const corpId = getCorpId();
const userId = getUserId();
if (!corpId || !userId) return;
const res = await api("getTeamBymember", { corpId, corpUserId: userId });
if (!res?.success) return;
const list = Array.isArray(res?.data)
? res.data
: Array.isArray(res?.data?.data)
? res.data.data
: [];
const normalized = list
.map((raw) => {
if (!raw || typeof raw !== "object") return null;
const teamId = raw.teamId || raw.id || raw._id || "";
const name = raw.name || raw.teamName || raw.team || "";
if (!teamId || !name) return null;
return { label: String(name), value: String(teamId) };
})
.filter(Boolean);
teamOptions.value = [{ label: "全部", value: "ALL" }, ...normalized];
}
function add() {
uni.showActionSheet({
itemList: ["+新增任务", "+使用模板", "+回访记录"],
success: ({ tapIndex }) => {
if (tapIndex === 0) {
uni.setStorageSync("new-followup-customer", {
_id: props.archiveId,
name: props.data?.name || "",
});
uni.navigateTo({
url: `/pages/case/new-followup?archiveId=${encodeURIComponent(
props.archiveId
)}`,
});
} else if (tapIndex === 1) {
uni.setStorageSync("new-followup-plan-customer", {
_id: props.archiveId,
name: props.data?.name || "",
});
uni.setStorageSync("select-mamagement-plan", "");
uni.navigateTo({
url: `/pages/case/plan-list?archiveId=${encodeURIComponent(
props.archiveId
)}`,
});
} else if (tapIndex === 2) {
uni.setStorageSync("new-followup-record-customer", {
_id: props.archiveId,
name: props.data?.name || "",
});
uni.navigateTo({
url: `/pages/case/new-followup-record?archiveId=${encodeURIComponent(
props.archiveId
)}`,
});
}
},
});
}
function toDetail(todo) {
uni.navigateTo({
url: `/pages/case/followup-detail?archiveId=${encodeURIComponent(
props.archiveId
)}&mode=edit&id=${encodeURIComponent(todo._id)}`,
});
}
function hasSendContent(todo) {
return Boolean(todo?.sendContent) || (Array.isArray(todo?.fileList) && todo.fileList.length > 0);
}
function isExecutorMe(todo) {
const me = String(getUserId() || "");
const executor = String(todo?.executorUserId || "");
if (!me || !executor) return false;
return me === executor;
}
function canShowSendButton(todo) {
if (!hasSendContent(todo)) return false;
if (!isExecutorMe(todo)) return false;
// 当前患者无会话则不展示
return Boolean(currentChatGroupId.value);
}
function buildFollowUpMessages(todo) {
const messages = [];
if (todo?.sendContent) {
messages.push({ type: "text", content: String(todo.sendContent) });
}
if (Array.isArray(todo?.fileList)) {
for (const file of todo.fileList) {
const outerType = String(file?.type || "");
let innerFile = file?.file;
if (typeof innerFile === "string") {
try {
innerFile = JSON.parse(innerFile);
} catch {
// ignore
}
}
innerFile = innerFile && typeof innerFile === "object" ? innerFile : null;
const innerType = String(innerFile?.type || "");
const outerUrl = String(file?.URL || file?.url || "");
const innerUrl = String(innerFile?.url || "");
// 兼容 followup-detail.vue 的判定方式
let fileType = "";
if (outerType === "image" || innerType.includes("image")) fileType = "image";
else if (innerType === "article") fileType = "article";
else if (innerType === "questionnaire") fileType = "questionnaire";
else fileType = outerType;
const url = fileType === "article" || fileType === "questionnaire" ? (innerUrl || outerUrl) : (outerUrl || innerUrl);
if (fileType === "image" && url) {
messages.push({
type: "image",
content: url,
name: innerFile?.name || file?.name || "图片",
});
continue;
}
if (fileType === "article") {
const fallbackArticleId = String(innerFile?._id || file?._id || innerFile?.articleId || file?.articleId || "") || "";
const extractedId = extractIdFromUrl(url);
const articleId = String(extractedId || fallbackArticleId || "");
// url 兜底:如果后端没给文章跳转链接,则用 articleId+corpId 拼接
let articleUrl = String(url || "");
if (!articleUrl && articleId) {
const corpId = getCorpId();
articleUrl = `${__VITE_ENV__?.MP_PATIENT_PAGE_BASE_URL || ""}pages/article/index?id=${encodeURIComponent(
articleId
)}&corpId=${encodeURIComponent(corpId || "")}`;
}
// 没有 id 和 url 时,跳过
if (!articleId && !articleUrl) continue;
messages.push({
type: "article",
content: {
_id: articleId,
title: innerFile?.name || file?.name || "宣教文章",
url: articleUrl,
subtitle: innerFile?.subtitle || "",
cover: innerFile?.cover || file?.URL || "",
articleId: articleId,
},
});
continue;
}
if (fileType === "questionnaire") {
const surveryId = innerFile?.surveryId || file?.surveryId;
if (!surveryId) continue;
const surveyId = String(innerFile?._id || file?._id || surveryId || "");
messages.push({
type: "questionnaire",
content: {
_id: surveyId,
name: innerFile?.name || file?.name || "问卷",
surveryId,
url: String(url || ""),
createBy: innerFile?.createBy,
},
});
}
}
}
return messages;
}
async function goChatAndSend(todo) {
if (!canShowSendButton(todo)) return;
if (!props.archiveId) return;
let gid = normalizeGroupId(currentChatGroupId.value || "");
if (!gid) {
await refreshChatRoom();
gid = normalizeGroupId(currentChatGroupId.value || "");
}
if (!gid) {
toast("暂无可进入的会话");
return;
}
const messages = buildFollowUpMessages(todo);
if (!messages.length) {
console.warn("[followup] buildFollowUpMessages empty:", {
sendContent: todo?.sendContent,
fileList: todo?.fileList,
});
toast("发送内容解析失败");
return;
}
const conversationID = `GROUP${gid}`;
uni.setStorageSync(PENDING_FOLLOWUP_SEND_STORAGE_KEY, {
createdAt: Date.now(),
groupId: gid,
conversationID,
messages,
context: {
userId: getUserId(),
customerId: props.archiveId,
customerName: props.data?.name || "",
corpId: getCorpId(),
env: __VITE_ENV__,
},
});
uni.navigateTo({
url: `/pages/message/index?conversationID=${encodeURIComponent(
conversationID
)}&groupID=${encodeURIComponent(gid)}&fromCase=true&pendingFollowUpSend=1`,
});
}
/**
* 从 URL 中提取 id 参数
* @param {string} url - 完整的 URL
* @returns {string} 提取出的 id 值
*/
function extractIdFromUrl(url) {
if (!url) return "";
try {
// 处理格式: https://www.youcan365.com/patientDeploy/#/pages/article/index?id=267epkhd3xbklcnbf0f45gzp1769567841991&corpId=...
const urlObj = new URL(url);
const id = urlObj.searchParams.get("id");
if (id) return id;
// 备用方案:使用正则表达式提取
const match = url.match(/[?&]id=([^&]+)/);
return match ? decodeURIComponent(match[1]) : "";
} catch (error) {
console.error("解析 URL 失败:", error);
// 备用方案:使用正则表达式提取
const match = url.match(/[?&]id=([^&]+)/);
return match ? decodeURIComponent(match[1]) : "";
}
}
const isRefreshingChatRoom = ref(false);
let lastRefreshChatRoomAt = 0;
function parseAnyTimeMs(v) {
if (v === null || v === undefined) return 0;
if (typeof v === "number") return v;
const s = String(v).trim();
if (!s) return 0;
if (/^\d{10,13}$/.test(s)) return Number(s.length === 10 ? `${s}000` : s);
const d = dayjs(s);
return d.isValid() ? d.valueOf() : 0;
}
async function refreshChatRoom() {
const customerId = String(props.archiveId || "");
if (!customerId) return;
if (isRefreshingChatRoom.value) return;
const now = Date.now();
if (now - lastRefreshChatRoomAt < 5000) return;
lastRefreshChatRoomAt = now;
isRefreshingChatRoom.value = true;
try {
await ensureDoctor();
const corpId = getCorpId();
const teamId = getCurrentTeamId();
const baseQuery = {
corpId,
customerId,
page: 1,
pageSize: 50,
};
const queryWithTeam = teamId ? { ...baseQuery, teamId } : baseQuery;
let detailRes = await api("getGroupList", queryWithTeam, false);
let details = Array.isArray(detailRes?.data?.list) ? detailRes.data.list : [];
if (!details.length && teamId) {
detailRes = await api("getGroupList", baseQuery, false);
details = Array.isArray(detailRes?.data?.list) ? detailRes.data.list : [];
}
if (!detailRes?.success || !details.length) {
chatGroupId.value = "";
return;
}
const currentTeamId = getCurrentTeamId();
const detailsForCurrentTeam = currentTeamId
? details.filter((g) => String(g?.teamId || g?.team?._id || g?.team?.teamId || "") === currentTeamId)
: [];
const candidates = detailsForCurrentTeam.length ? detailsForCurrentTeam : details;
const statusRank = (s) => (s === "processing" ? 3 : s === "pending" ? 2 : 1);
candidates.sort((a, b) => {
const ra = statusRank(String(a?.orderStatus || ""));
const rb = statusRank(String(b?.orderStatus || ""));
if (rb !== ra) return rb - ra;
const ta = parseAnyTimeMs(a?.updatedAt) || parseAnyTimeMs(a?.createdAt);
const tb = parseAnyTimeMs(b?.updatedAt) || parseAnyTimeMs(b?.createdAt);
return tb - ta;
});
const best = candidates[0] || {};
const gid = normalizeGroupId(best.groupId || best.groupID || best.group_id || "");
chatGroupId.value = gid ? String(gid) : "";
} catch (e) {
// ignore
} finally {
isRefreshingChatRoom.value = false;
}
}
// ---- filter popup ----
const filterPopupRef = ref(null);
const state = ref(null);
const teamPicked = ref(teamOptions.value[0]);
const planRange = ref(["", ""]);
function openFilter() {
state.value = {
query: {
...query,
eventTypes: [...(query.eventTypes || [])],
planRange: [...(query.planRange || ["", ""])],
},
team: { ...teamPicked.value },
range: [...planRange.value],
};
planRange.value = [...(query.planRange || ["", ""])];
teamPicked.value =
teamOptions.value.find((i) => i.value === query.teamId) ||
teamOptions.value[0];
filterPopupRef.value?.open?.();
}
function closeFilter(revert) {
if (revert && state.value) {
Object.assign(query, state.value.query);
planRange.value = state.value.range;
teamPicked.value = state.value.team;
}
filterPopupRef.value?.close?.();
}
function toggleType(v) {
if (v === "all") {
query.eventTypes = [];
return;
}
const set = new Set(query.eventTypes || []);
if (set.has(v)) set.delete(v);
else set.add(v);
query.eventTypes = Array.from(set);
}
function pickTeam(e) {
teamPicked.value = teamOptions.value[e.detail.value] || teamOptions.value[0];
query.teamId = teamPicked.value.value;
}
function pickStart(e) {
planRange.value = [e.detail.value || "", planRange.value[1] || ""];
query.planRange = [...planRange.value];
}
function pickEnd(e) {
planRange.value = [planRange.value[0] || "", e.detail.value || ""];
query.planRange = [...planRange.value];
}
function clearPlanRange() {
planRange.value = ["", ""];
query.planRange = ["", ""];
}
function resetFilter() {
query.eventTypes = [];
query.teamId = "ALL";
teamPicked.value = teamOptions.value[0];
clearPlanRange();
}
function confirmFilter() {
filtered.value = Boolean(
(query.eventTypes && query.eventTypes.length) ||
query.teamId !== "ALL" ||
query.planRange[0] ||
query.planRange[1]
);
closeFilter(false);
resetList();
}
onMounted(() => {
const userId = getUserId();
const name = String(
doctorInfo.value?.anotherName || doctorInfo.value?.name || ""
);
if (userId && name)
userNameMap.value = { ...(userNameMap.value || {}), [userId]: name };
loadTeams();
refreshChatRoom();
resetList();
uni.$on("archive-detail:followup-changed", resetList);
});
onUnmounted(() => {
uni.$off("archive-detail:followup-changed", resetList);
});
watch(
() => props.reachBottomTime,
() => getMore()
);
</script>
<style scoped>
.wrap {
padding: 0 0 96px;
}
.top {
background: #f5f6f8;
padding: 10px 14px 8px;
border-bottom: 1px solid #f2f2f2;
}
.top-row {
display: flex;
align-items: center;
}
.my {
display: flex;
align-items: center;
flex-shrink: 0;
}
.checkbox {
width: 18px;
height: 18px;
margin-right: 6px;
}
.my-text {
font-size: 13px;
color: #333;
}
.status-scroll {
flex: 1;
margin-left: 10px;
}
.status-tabs {
display: flex;
flex-wrap: nowrap;
}
.status-tab {
flex-shrink: 0;
padding: 8px 10px;
font-size: 12px;
border-radius: 6px;
margin-right: 8px;
background: #eaecef;
color: #333;
}
.status-tab.active {
background: #dbe6ff;
color: #0877f1;
}
.filter-btn {
flex-shrink: 0;
padding-left: 10px;
}
.filter-icon {
width: 20px;
height: 20px;
}
.total {
margin-top: 6px;
font-size: 12px;
color: #666;
}
.total-num {
color: #ff4d4f;
margin: 0 4px;
}
.list {
padding: 0 14px;
}
.card {
background: #fff;
border-radius: 8px;
margin-top: 10px;
overflow: hidden;
}
.head {
padding: 12px 12px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f2f2f2;
}
.date {
font-size: 13px;
color: #666;
flex-shrink: 0;
margin-right: 10px;
}
.date-val {
color: #333;
}
.executor {
flex: 1;
text-align: right;
font-size: 13px;
color: #333;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.body {
padding: 12px 12px;
}
.title-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.type {
font-size: 15px;
font-weight: 600;
color: #1f1f1f;
}
.status {
font-size: 12px;
padding: 6px 10px;
border-radius: 6px;
}
.st-processing {
background: #ffe5e5;
color: #ff4d4f;
}
.st-notStart {
background: #dbe6ff;
color: #0877f1;
}
.st-treated {
background: #dcfce7;
color: #16a34a;
}
.st-cancelled,
.st-expired {
background: #f3f4f6;
color: #666;
}
.content {
margin-top: 10px;
font-size: 13px;
color: #666;
line-height: 18px;
}
.send-content-wrapper {
margin-top: 10px;
display: flex;
align-items: flex-start;
gap: 10px;
}
.send-content-section {
flex: 1;
padding: 10px;
background: #f9f9f9;
border-radius: 6px;
display: flex;
align-items: flex-start;
gap: 10px;
}
.send-content-label {
font-size: 13px;
color: #666;
flex-shrink: 0;
font-weight: 500;
white-space: nowrap;
}
.send-content-body {
flex: 1;
min-width: 0;
}
.send-text {
font-size: 13px;
color: #333;
line-height: 18px;
word-break: break-word;
}
.file-list {
margin-top: 6px;
}
.file-list.no-send-text {
margin-top: 0;
}
.file-item {
display: flex;
align-items: center;
gap: 6px;
}
.file-item + .file-item {
margin-top: 4px;
}
.file-icon {
font-size: 14px;
flex-shrink: 0;
}
.file-name {
font-size: 12px;
color: #0877f1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
width: 330rpx;
}
.file-type-image .file-name {
color: #0877f1;
}
.file-type-article .file-name {
color: #16a34a;
}
.file-type-questionnaire .file-name {
color: #f59e0b;
}
.result {
margin-top: 10px;
font-size: 13px;
color: #666;
}
.footer-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #f2f2f2;
gap: 10px;
}
.footer {
font-size: 12px;
color: #999;
flex: 1;
}
.footer-row .send-btn {
flex: 0 0 auto;
width: 60px;
height: 28px;
line-height: 28px;
padding: 0;
margin: 0;
}
.card-actions {
display: flex;
gap: 10px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #f2f2f2;
}
.action-btn {
flex: 1;
height: 32px;
line-height: 32px;
border-radius: 6px;
font-size: 12px;
border: none;
background: #f5f6f8;
color: #333;
}
.action-btn::after {
border: none;
}
.detail-btn {
background: #f5f6f8;
color: #333;
}
.send-btn {
flex: 0 0 auto;
width: 60px;
background: #0877f1;
color: #fff;
}
.empty {
padding: 120px 0;
text-align: center;
color: #9aa0a6;
font-size: 13px;
}
.fab {
position: fixed;
right: 16px;
width: 52px;
height: 52px;
border-radius: 26px;
background: #0877f1;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 10px 18px rgba(79, 110, 247, 0.35);
z-index: 20;
}
.popup {
background: #fff;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
overflow: hidden;
}
.popup-title {
position: relative;
padding: 14px;
border-bottom: 1px solid #f0f0f0;
}
.popup-title-text {
text-align: center;
font-size: 16px;
font-weight: 600;
color: #333;
}
.popup-close {
position: absolute;
right: 12px;
top: 0;
height: 100%;
display: flex;
align-items: center;
}
.popup-body {
max-height: 70vh;
}
.section {
padding: 14px;
}
.section-title {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 10px;
}
.chip-wrap {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.chip {
width: 110px;
text-align: center;
padding: 8px 0;
border: 1px solid #e6e6e6;
border-radius: 8px;
font-size: 12px;
color: #333;
}
.chip.active {
background: #0877f1;
border-color: #0877f1;
color: #fff;
}
.select-row {
height: 40px;
border: 1px solid #e6e6e6;
border-radius: 8px;
padding: 0 10px;
display: flex;
align-items: center;
justify-content: space-between;
}
.select-text {
font-size: 12px;
color: #333;
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.select-text.muted {
color: #999;
}
.range-row {
display: flex;
align-items: center;
gap: 8px;
}
.range-pill {
width: 110px;
height: 40px;
line-height: 40px;
text-align: center;
border: 1px solid #e6e6e6;
border-radius: 8px;
font-size: 12px;
color: #333;
}
.range-pill.muted {
color: #999;
}
.sep {
color: #999;
}
.clear {
padding: 6px;
}
.actions {
padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
display: flex;
gap: 12px;
}
.btn {
flex: 1;
height: 44px;
line-height: 44px;
border-radius: 6px;
font-size: 15px;
}
.btn::after {
border: none;
}
.btn.plain {
background: #fff;
color: #0877f1;
border: 1px solid #0877f1;
}
.btn.primary {
background: #0877f1;
color: #fff;
}
</style>