ykt-wxapp/pages/case/components/archive-detail/follow-up-manage-tab.vue
2026-02-10 15:47:35 +08:00

1221 lines
29 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="fromChat && isExecutor(i)"
class="action-btn send-btn"
:class="{ loading: sendingFollowUp }"
:disabled="sendingFollowUp"
@click.stop="sendFollowUp(i)"
>
{{ sendingFollowUp ? "发送中..." : "发送" }}
</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 isExecutor(todo) {
const currentUserId = getUserId();
const executorUserId = String(todo?.executorUserId || "");
return currentUserId && executorUserId && currentUserId === executorUserId;
}
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 sendingFollowUp = ref(false);
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 refreshChatRoom() {
if (!props.fromChat) return;
const pending = uni.getStorageSync(PENDING_FOLLOWUP_SEND_STORAGE_KEY);
if (pending && typeof pending === "object") {
chatGroupId.value = String(pending.chatGroupId || pending.groupId || "");
}
}
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;
}
// 对齐管理端同接口返回结构:{ success, data: { data: [], total } }
const payload = res?.data;
const arr = payload && Array.isArray(payload.data)
? payload.data
: Array.isArray(payload)
? payload
: [];
const next = arr.map(formatTodo);
total.value = payload && typeof payload.total === "number" ? payload.total : 0;
pages.value = Math.ceil(total.value / pageSize) || 0;
list.value = page.value === 1 ? next : [...list.value, ...next];
page.value += 1;
} catch (e) {
console.error("getCustomerTodos failed:", e);
toast("获取回访任务失败");
} 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)}`,
});
}
async function sendFollowUp(todo) {
if (sendingFollowUp.value) {
toast("正在发送中,请稍候...");
return;
}
if (!todo.sendContent && (!todo.fileList || todo.fileList.length === 0)) {
toast("没有发送内容");
return;
}
sendingFollowUp.value = true;
try {
const messages = [];
// 1. 发送文字内容
if (todo.sendContent) {
messages.push({
type: "text",
content: todo.sendContent,
});
}
console.log("==============>fileList", todo.fileList);
// 2. 处理文件列表(图片、宣教文章、问卷)
if (Array.isArray(todo.fileList)) {
for (const file of todo.fileList) {
if (file.type === "image" && file.URL) {
// 发送图片
messages.push({
type: "image",
content: file.URL,
name: file.file?.name || file.name || "图片",
});
} else if (file.file.type === "article" && file.file?.url) {
// 发送宣教文章 - 从 URL 中解析 id
const articleId = extractIdFromUrl(file.file.url);
messages.push({
type: "article",
content: {
_id: articleId,
title: file.file?.name || "宣教文章",
url: file.file?.url || file.URL,
subtitle: file.file?.subtitle || "",
cover: file.file?.cover || "",
articleId: articleId,
},
});
} else if (
file.file.type === "questionnaire" &&
(file.file?.url || file.URL)
) {
// 发送问卷 - 从 URL 中解析 surveryId
const surveryUrl = file.file?.url || file.URL;
const surveryId = extractSurveryIdFromUrl(surveryUrl);
messages.push({
type: "questionnaire",
content: {
_id: surveryId,
name: file.file?.name || file.name || "问卷",
surveryId: surveryId,
url: surveryUrl,
},
});
}
}
}
// 调用统一的消息发送处理函数
const success = await handleFollowUpMessages(messages, {
userId: getUserId(),
customerId: props.archiveId,
customerName: props.data?.name || "",
corpId: getCorpId(),
env: __VITE_ENV__,
});
if (success) {
toast("消息已发送");
uni.navigateBack();
}
} finally {
sendingFollowUp.value = false;
}
}
/**
* 从 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]) : "";
}
}
/**
* 从问卷 URL 中提取 surveryId 参数
* @param {string} url - 完整的 URL格式如: https://www.youcan365.com/patientDeploy/#/pages/survery/fill?corpId=wwe3fb2faa52cf9dfb&surveryId=9ji5kg2oa9x52oyg9w4rj5k81769510562099
* @returns {string} 提取出的 surveryId 值
*/
function extractSurveryIdFromUrl(url) {
if (!url) return "";
try {
// 使用正则表达式提取 surveryId 参数
// 处理格式: ?surveryId=xxx 或 &surveryId=xxx
const match = url.match(/[?&]surveryId=([^&]+)/);
return match ? decodeURIComponent(match[1]) : "";
} catch (error) {
console.error("解析问卷 URL 失败:", error);
return "";
}
}
// ---- 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;
}
.send-btn.loading {
opacity: 0.6;
}
.send-btn:disabled {
opacity: 0.6;
}
.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>