2026-01-22 15:54:15 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<view class="page">
|
2026-01-26 15:39:14 +08:00
|
|
|
|
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/followup-detail/followup-detail.vue -->
|
|
|
|
|
|
<scroll-view scroll-y class="scroll">
|
|
|
|
|
|
<view class="section">
|
|
|
|
|
|
<view class="head-row">
|
|
|
|
|
|
<view class="head-title">{{ currentType.label }}</view>
|
|
|
|
|
|
<view class="head-tag" :class="`tag-${currentStatus.value}`">{{ currentStatus.label }}</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="head-content">{{ form.taskContent || '暂无任务内容' }}</view>
|
2026-01-22 15:54:15 +08:00
|
|
|
|
</view>
|
2026-01-26 15:39:14 +08:00
|
|
|
|
|
|
|
|
|
|
<view v-if="todo?.sendContent" class="section border-top">
|
|
|
|
|
|
<view class="sub-label">发送内容 :</view>
|
|
|
|
|
|
<view class="sub-content">{{ todo.sendContent }}</view>
|
|
|
|
|
|
<view v-for="(f, idx) in showFileList" :key="idx" class="file-line">
|
|
|
|
|
|
<text class="file-type">{{ f.typeStr }}</text>
|
|
|
|
|
|
<text class="file-name">{{ f.name }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="section border-top kv">
|
|
|
|
|
|
<view class="kv-left">计划执行时间:{{ planDate || '--' }}</view>
|
|
|
|
|
|
<view class="kv-right">{{ executorDisplay || '--' }}</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="section border-top kv">
|
|
|
|
|
|
<view class="kv-left">客户:</view>
|
|
|
|
|
|
<view class="kv-right link">{{ customerDisplay || '--' }}</view>
|
2026-01-22 15:54:15 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
2026-01-26 15:39:14 +08:00
|
|
|
|
<view class="section mt-20">
|
|
|
|
|
|
<view class="section-title">回访方式</view>
|
|
|
|
|
|
<view class="method-row" @click="toggleMethod('phone')">
|
|
|
|
|
|
<image class="radio" :src="`/static/circle${todoMethod === 'phone' ? 'd' : ''}.svg`" />
|
|
|
|
|
|
<view class="method-label">电话</view>
|
|
|
|
|
|
<view class="method-select" @click.stop="pickPhone">
|
|
|
|
|
|
<view class="method-value" :class="{ muted: !phone }">{{ phone || '请选择号码' }}</view>
|
|
|
|
|
|
<uni-icons type="arrowright" size="16" color="#999" />
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="method-row" @click="toggleMethod('wechat')">
|
|
|
|
|
|
<image class="radio" :src="`/static/circle${todoMethod === 'wechat' ? 'd' : ''}.svg`" />
|
|
|
|
|
|
<view class="method-label">微信</view>
|
2026-01-22 15:54:15 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2026-01-26 15:39:14 +08:00
|
|
|
|
|
|
|
|
|
|
<view class="section mt-20">
|
|
|
|
|
|
<view class="section-title">回访结果</view>
|
|
|
|
|
|
<view class="result-box">
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
v-model="form.result"
|
|
|
|
|
|
class="result-textarea"
|
|
|
|
|
|
placeholder="请填写回访结果"
|
|
|
|
|
|
maxlength="500"
|
|
|
|
|
|
:disabled="!(editable || canEditResult)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</view>
|
2026-01-22 15:54:15 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
2026-01-26 15:39:14 +08:00
|
|
|
|
<view class="section mt-20 meta">
|
|
|
|
|
|
<view class="meta-row">
|
|
|
|
|
|
<text class="meta-key">创建人 :</text>
|
|
|
|
|
|
<text class="meta-val">{{ creatorDisplay || '--' }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="meta-row">
|
|
|
|
|
|
<text class="meta-key">创建时间 :</text>
|
|
|
|
|
|
<text class="meta-val">{{ createTimeStr || '--' }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view v-if="endTimeStr" class="meta-row">
|
|
|
|
|
|
<text class="meta-key">执行时间 :</text>
|
|
|
|
|
|
<text class="meta-val">{{ endTimeStr }}</text>
|
|
|
|
|
|
</view>
|
2026-01-22 15:54:15 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
2026-01-26 15:39:14 +08:00
|
|
|
|
<view style="height: 160rpx" />
|
|
|
|
|
|
</scroll-view>
|
|
|
|
|
|
|
|
|
|
|
|
<view v-if="editable || canEditResult" class="footer">
|
|
|
|
|
|
<button v-if="editable" class="footer-btn plain danger" @click="cancelTask">取消任务</button>
|
|
|
|
|
|
<button v-if="editable" class="footer-btn primary" @click="markDone">设为完成</button>
|
|
|
|
|
|
<button v-else class="footer-btn primary" @click="save">保存</button>
|
2026-01-22 15:54:15 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
2026-01-26 15:39:14 +08:00
|
|
|
|
<view v-if="taskId && canRemove" class="delete-fab" @click="remove">
|
2026-01-22 15:54:15 +08:00
|
|
|
|
<uni-icons type="trash" size="22" color="#ff4d4f" />
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { computed, reactive, ref } from 'vue';
|
|
|
|
|
|
import { onLoad } from '@dcloudio/uni-app';
|
|
|
|
|
|
import dayjs from 'dayjs';
|
2026-01-26 15:39:14 +08:00
|
|
|
|
import { storeToRefs } from 'pinia';
|
|
|
|
|
|
import api from '@/utils/api';
|
|
|
|
|
|
import useAccountStore from '@/store/account';
|
|
|
|
|
|
import { toast } from '@/utils/widget';
|
|
|
|
|
|
import { getTodoEventTypeLabel } from '@/utils/todo-const';
|
2026-01-22 15:54:15 +08:00
|
|
|
|
|
|
|
|
|
|
const archiveId = ref('');
|
|
|
|
|
|
const mode = ref('add');
|
|
|
|
|
|
const taskId = ref('');
|
|
|
|
|
|
|
2026-01-26 15:39:14 +08:00
|
|
|
|
const todo = ref(null);
|
|
|
|
|
|
const currentType = computed(() => ({ label: getTodoEventTypeLabel(todo.value?.eventType), value: String(todo.value?.eventType || '') }));
|
|
|
|
|
|
const customer = ref(null);
|
|
|
|
|
|
const userNameMap = ref({});
|
|
|
|
|
|
|
|
|
|
|
|
function statusLabelFromStatus(status) {
|
|
|
|
|
|
const map = {
|
|
|
|
|
|
processing: '待处理',
|
|
|
|
|
|
notStart: '未开始',
|
|
|
|
|
|
treated: '已完成',
|
|
|
|
|
|
cancelled: '已取消',
|
|
|
|
|
|
expired: '已过期',
|
|
|
|
|
|
};
|
|
|
|
|
|
return map[status] || '未知';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getStatus(t) {
|
|
|
|
|
|
const endOfToday = dayjs().endOf('day').valueOf();
|
|
|
|
|
|
const startOfToday = dayjs().startOf('day').valueOf();
|
|
|
|
|
|
const plannedExecutionTime = Number(t?.plannedExecutionTime || 0) || 0;
|
|
|
|
|
|
const expireTime = Number(t?.expireTime || 0) || 0;
|
|
|
|
|
|
const eventStatus = String(t?.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';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const currentStatus = computed(() => {
|
|
|
|
|
|
const status = getStatus(todo.value);
|
|
|
|
|
|
return { value: status, label: statusLabelFromStatus(status) };
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const planDate = computed(() => {
|
|
|
|
|
|
const v = todo.value?.plannedExecutionTime;
|
|
|
|
|
|
return v && dayjs(v).isValid() ? dayjs(v).format('YYYY-MM-DD') : '';
|
|
|
|
|
|
});
|
|
|
|
|
|
const createTimeStr = computed(() => {
|
|
|
|
|
|
const v = todo.value?.createTime;
|
|
|
|
|
|
return v && dayjs(v).isValid() ? dayjs(v).format('YYYY-MM-DD HH:mm') : '';
|
|
|
|
|
|
});
|
|
|
|
|
|
const endTimeStr = computed(() => {
|
|
|
|
|
|
const v = todo.value?.endTime;
|
|
|
|
|
|
return v && dayjs(v).isValid() ? dayjs(v).format('YYYY-MM-DD HH:mm') : '';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const showFileList = computed(() => {
|
|
|
|
|
|
const list = Array.isArray(todo.value?.fileList) ? todo.value.fileList : [];
|
|
|
|
|
|
return list.map((i) => {
|
|
|
|
|
|
const type = String(i?.type || '');
|
|
|
|
|
|
const fileType = i?.file && typeof i.file.type === 'string' ? i.file.type : '';
|
|
|
|
|
|
let typeStr = '';
|
|
|
|
|
|
if (type === 'video' || fileType.includes('video')) typeStr = '【视频】';
|
|
|
|
|
|
else if (type === 'image' || fileType.includes('image')) typeStr = '【图片】';
|
|
|
|
|
|
else if (fileType === 'article') typeStr = '【文章】';
|
|
|
|
|
|
else if (fileType === 'questionnaire') typeStr = '【问卷】';
|
|
|
|
|
|
else if (type === 'link') typeStr = '【链接】';
|
|
|
|
|
|
const name = i?.file?.name ? String(i.file.name) : '';
|
|
|
|
|
|
return { typeStr, name };
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const accountStore = useAccountStore();
|
|
|
|
|
|
const { account, doctorInfo } = storeToRefs(accountStore);
|
|
|
|
|
|
const { getDoctorInfo } = accountStore;
|
|
|
|
|
|
|
2026-01-22 15:54:15 +08:00
|
|
|
|
const form = reactive({
|
|
|
|
|
|
executorName: '',
|
|
|
|
|
|
taskContent: '',
|
|
|
|
|
|
result: '',
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const todoMethod = ref('phone'); // phone | wechat
|
|
|
|
|
|
const phone = ref('');
|
|
|
|
|
|
const mobiles = computed(() => {
|
|
|
|
|
|
const arr = [];
|
|
|
|
|
|
const cached = uni.getStorageSync('ykt_case_archive_detail');
|
2026-01-26 15:39:14 +08:00
|
|
|
|
if (cached && typeof cached === 'object') {
|
|
|
|
|
|
if (cached.mobile) arr.push(String(cached.mobile));
|
|
|
|
|
|
if (Array.isArray(cached.mobiles)) arr.push(...cached.mobiles.map(String));
|
|
|
|
|
|
}
|
|
|
|
|
|
const cleaned = arr.map((i) => String(i || '').trim()).filter(Boolean);
|
|
|
|
|
|
if (!cleaned.includes('13800000000')) cleaned.push('13800000000');
|
|
|
|
|
|
return Array.from(new Set(cleaned));
|
2026-01-22 15:54:15 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-26 15:39:14 +08:00
|
|
|
|
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 t = uni.getStorageSync('ykt_case_current_team') || {};
|
|
|
|
|
|
const a = account.value || {};
|
|
|
|
|
|
const d = doctorInfo.value || {};
|
|
|
|
|
|
return String(t.corpId || a.corpId || d.corpId || '') || '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function ensureDoctor() {
|
|
|
|
|
|
if (doctorInfo.value) return;
|
|
|
|
|
|
if (!account.value?.openid) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await getDoctorInfo();
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveUserName(userId) {
|
|
|
|
|
|
const id = String(userId || '');
|
|
|
|
|
|
if (!id) return '';
|
|
|
|
|
|
const map = userNameMap.value || {};
|
|
|
|
|
|
return String(map[id] || '') || id;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadUserNameMap(teamId) {
|
|
|
|
|
|
if (!teamId) return;
|
|
|
|
|
|
const corpId = getCorpId();
|
|
|
|
|
|
if (!corpId) return;
|
|
|
|
|
|
const res = await api('getTeamData', { corpId, teamId });
|
|
|
|
|
|
if (!res?.success) return;
|
|
|
|
|
|
const t = res?.data && typeof res.data === 'object' ? res.data : {};
|
|
|
|
|
|
const list = Array.isArray(t.memberList) ? t.memberList : [];
|
|
|
|
|
|
const map = list.reduce((acc, cur) => {
|
|
|
|
|
|
const id = cur?.userid ? String(cur.userid) : '';
|
|
|
|
|
|
if (!id) return acc;
|
|
|
|
|
|
acc[id] = String(cur?.anotherName || cur?.name || cur?.userid || '');
|
|
|
|
|
|
return acc;
|
|
|
|
|
|
}, {});
|
|
|
|
|
|
userNameMap.value = { ...(userNameMap.value || {}), ...map };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const isSelf = computed(() => {
|
|
|
|
|
|
const id = getUserId();
|
|
|
|
|
|
return Boolean(id && todo.value && String(todo.value.executorUserId || '') === id);
|
|
|
|
|
|
});
|
|
|
|
|
|
const editable = computed(() => isSelf.value && ['notStart', 'processing'].includes(currentStatus.value.value));
|
|
|
|
|
|
const canEditResult = computed(() => isSelf.value && ['treated', 'cancelled'].includes(currentStatus.value.value));
|
|
|
|
|
|
|
|
|
|
|
|
const canRemove = computed(() => {
|
|
|
|
|
|
const t = todo.value;
|
|
|
|
|
|
if (!t) return false;
|
|
|
|
|
|
const userId = getUserId();
|
|
|
|
|
|
return String(t.creatorUserId || '') === userId;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const executorDisplay = computed(() => {
|
|
|
|
|
|
const t = todo.value || {};
|
|
|
|
|
|
const name = resolveUserName(t.executorUserId);
|
|
|
|
|
|
const teamName = String(t.executeTeamName || '');
|
|
|
|
|
|
return teamName ? `${name}(${teamName})` : name;
|
|
|
|
|
|
});
|
|
|
|
|
|
const creatorDisplay = computed(() => resolveUserName(todo.value?.creatorUserId));
|
|
|
|
|
|
const customerDisplay = computed(() => {
|
|
|
|
|
|
const t = todo.value || {};
|
|
|
|
|
|
const c = customer.value || {};
|
|
|
|
|
|
return String(t.customerName || c.name || '');
|
|
|
|
|
|
});
|
2026-01-22 15:54:15 +08:00
|
|
|
|
|
|
|
|
|
|
onLoad((options) => {
|
|
|
|
|
|
archiveId.value = options?.archiveId ? String(options.archiveId) : '';
|
|
|
|
|
|
mode.value = options?.mode ? String(options.mode) : 'add';
|
|
|
|
|
|
taskId.value = options?.id ? String(options.id) : '';
|
|
|
|
|
|
|
2026-01-26 15:39:14 +08:00
|
|
|
|
const cached = uni.getStorageSync('ykt_case_archive_detail');
|
|
|
|
|
|
if (cached && typeof cached === 'object') customer.value = cached;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
|
2026-01-26 15:39:14 +08:00
|
|
|
|
if (!taskId.value) {
|
|
|
|
|
|
toast('缺少回访任务 id');
|
|
|
|
|
|
setTimeout(() => uni.navigateBack(), 300);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
getTodo();
|
2026-01-22 15:54:15 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-26 15:39:14 +08:00
|
|
|
|
function parseTodoMethod(value) {
|
|
|
|
|
|
if (typeof value !== 'string') return { todoMethod: '', phone: '' };
|
|
|
|
|
|
if (value.startsWith('phone')) {
|
|
|
|
|
|
const [, num] = value.split(':');
|
|
|
|
|
|
return { todoMethod: 'phone', phone: num || '' };
|
|
|
|
|
|
}
|
|
|
|
|
|
if (value === 'wechat') return { todoMethod: 'wechat', phone: '' };
|
|
|
|
|
|
return { todoMethod: '', phone: '' };
|
2026-01-22 15:54:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function cancel() {
|
|
|
|
|
|
uni.navigateBack();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 15:39:14 +08:00
|
|
|
|
function buildTodoMethodValue() {
|
|
|
|
|
|
if (todoMethod.value === 'phone') return phone.value ? `phone:${phone.value}` : 'phone';
|
|
|
|
|
|
if (todoMethod.value === 'wechat') return 'wechat';
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function getTodo() {
|
|
|
|
|
|
await ensureDoctor();
|
|
|
|
|
|
const corpId = getCorpId();
|
|
|
|
|
|
if (!corpId) {
|
|
|
|
|
|
toast('缺少 corpId,请先完成登录/团队选择');
|
2026-01-22 15:54:15 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-01-26 15:39:14 +08:00
|
|
|
|
const res = await api('getTodoById', { corpId, id: taskId.value });
|
|
|
|
|
|
if (!res?.success) {
|
|
|
|
|
|
toast(res?.message || '获取回访任务失败');
|
|
|
|
|
|
todo.value = null;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
todo.value = res.data && typeof res.data === 'object' ? res.data : null;
|
|
|
|
|
|
await loadUserNameMap(String(todo.value?.executeTeamId || ''));
|
|
|
|
|
|
|
|
|
|
|
|
const parsed = parseTodoMethod(todo.value?.todoMethod);
|
|
|
|
|
|
if (parsed.todoMethod) {
|
|
|
|
|
|
todoMethod.value = parsed.todoMethod;
|
|
|
|
|
|
phone.value = parsed.phone || '';
|
|
|
|
|
|
} else if (editable.value) {
|
|
|
|
|
|
todoMethod.value = 'phone';
|
|
|
|
|
|
phone.value = mobiles.value[0] || '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
form.executorName = resolveUserName(todo.value?.executorUserId);
|
|
|
|
|
|
form.taskContent = String(todo.value?.taskContent || '') || '';
|
|
|
|
|
|
form.result = String(todo.value?.result || '') || '';
|
2026-01-22 15:54:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleMethod(v) {
|
2026-01-26 15:39:14 +08:00
|
|
|
|
if (!editable.value) return;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
todoMethod.value = v;
|
|
|
|
|
|
if (v !== 'phone') phone.value = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function pickPhone() {
|
2026-01-26 15:39:14 +08:00
|
|
|
|
if (!editable.value) return;
|
|
|
|
|
|
if (todoMethod.value !== 'phone') return;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
uni.showActionSheet({
|
|
|
|
|
|
itemList: mobiles.value,
|
|
|
|
|
|
success: ({ tapIndex }) => {
|
|
|
|
|
|
phone.value = mobiles.value[tapIndex] || '';
|
|
|
|
|
|
todoMethod.value = 'phone';
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function markDone() {
|
2026-01-26 15:39:14 +08:00
|
|
|
|
if (!editable.value) return;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
if (!['phone', 'wechat'].includes(todoMethod.value)) return uni.showToast({ title: '请选择回访方式', icon: 'none' });
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '提示',
|
|
|
|
|
|
content: '确定完成该回访任务吗?',
|
2026-01-26 15:39:14 +08:00
|
|
|
|
success: async (res) => {
|
2026-01-22 15:54:15 +08:00
|
|
|
|
if (!res.confirm) return;
|
2026-01-26 15:39:14 +08:00
|
|
|
|
await ensureDoctor();
|
|
|
|
|
|
const corpId = getCorpId();
|
|
|
|
|
|
const userId = getUserId();
|
|
|
|
|
|
const methodValue = buildTodoMethodValue();
|
|
|
|
|
|
const result = String(form.result || '').trim() || '已完成';
|
|
|
|
|
|
const r = await api('setTodoStatus', { corpId, id: taskId.value, eventStatus: 'treated', result, userId, todoMethod: methodValue });
|
|
|
|
|
|
if (!r?.success) return toast(r?.message || '操作失败');
|
|
|
|
|
|
toast('操作成功');
|
|
|
|
|
|
uni.$emit('archive-detail:followup-changed');
|
|
|
|
|
|
setTimeout(() => uni.navigateBack(), 300);
|
2026-01-22 15:54:15 +08:00
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function cancelTask() {
|
2026-01-26 15:39:14 +08:00
|
|
|
|
if (!editable.value) return;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '提示',
|
|
|
|
|
|
content: '确定取消该回访任务吗?',
|
2026-01-26 15:39:14 +08:00
|
|
|
|
success: async (res) => {
|
2026-01-22 15:54:15 +08:00
|
|
|
|
if (!res.confirm) return;
|
2026-01-26 15:39:14 +08:00
|
|
|
|
await ensureDoctor();
|
|
|
|
|
|
const corpId = getCorpId();
|
|
|
|
|
|
const userId = getUserId();
|
|
|
|
|
|
const methodValue = buildTodoMethodValue();
|
|
|
|
|
|
const result = String(form.result || '').trim() || '已取消';
|
|
|
|
|
|
const r = await api('setTodoStatus', { corpId, id: taskId.value, eventStatus: 'closed', result, userId, todoMethod: methodValue });
|
|
|
|
|
|
if (!r?.success) return toast(r?.message || '操作失败');
|
|
|
|
|
|
toast('取消成功');
|
|
|
|
|
|
uni.$emit('archive-detail:followup-changed');
|
|
|
|
|
|
setTimeout(() => uni.navigateBack(), 300);
|
2026-01-22 15:54:15 +08:00
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 15:39:14 +08:00
|
|
|
|
async function save() {
|
|
|
|
|
|
if (!canEditResult.value) return;
|
|
|
|
|
|
if (String(form.result || '').trim() === '') return toast('请填写回访结果');
|
|
|
|
|
|
await ensureDoctor();
|
|
|
|
|
|
const corpId = getCorpId();
|
|
|
|
|
|
if (!corpId) return toast('缺少 corpId');
|
|
|
|
|
|
const methodValue = buildTodoMethodValue();
|
|
|
|
|
|
const res = await api('updateTaskTodoResult', { corpId, id: taskId.value, result: String(form.result || ''), todoMethod: methodValue });
|
|
|
|
|
|
if (!res?.success) return toast(res?.message || '操作失败');
|
|
|
|
|
|
toast('操作成功');
|
|
|
|
|
|
uni.$emit('archive-detail:followup-changed');
|
|
|
|
|
|
setTimeout(() => uni.navigateBack(), 300);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 15:54:15 +08:00
|
|
|
|
function remove() {
|
2026-01-26 15:39:14 +08:00
|
|
|
|
if (!canRemove.value) return;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '提示',
|
|
|
|
|
|
content: '确定删除当前记录?',
|
2026-01-26 15:39:14 +08:00
|
|
|
|
success: async (res) => {
|
2026-01-22 15:54:15 +08:00
|
|
|
|
if (!res.confirm) return;
|
2026-01-26 15:39:14 +08:00
|
|
|
|
await ensureDoctor();
|
|
|
|
|
|
const corpId = getCorpId();
|
|
|
|
|
|
const userId = getUserId();
|
|
|
|
|
|
const r = await api('removeTodo', { corpId, userId, id: taskId.value });
|
|
|
|
|
|
if (!r?.success) return toast(r?.message || '删除失败');
|
2026-01-22 15:54:15 +08:00
|
|
|
|
uni.$emit('archive-detail:followup-changed');
|
2026-01-26 15:39:14 +08:00
|
|
|
|
toast('已删除');
|
2026-01-22 15:54:15 +08:00
|
|
|
|
setTimeout(() => uni.navigateBack(), 300);
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.page {
|
2026-01-26 15:39:14 +08:00
|
|
|
|
height: 100vh;
|
|
|
|
|
|
background: #f6f7f8;
|
|
|
|
|
|
}
|
|
|
|
|
|
.scroll {
|
|
|
|
|
|
height: 100vh;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
}
|
2026-01-26 15:39:14 +08:00
|
|
|
|
.section {
|
2026-01-22 15:54:15 +08:00
|
|
|
|
background: #fff;
|
2026-01-26 15:39:14 +08:00
|
|
|
|
padding: 24rpx 30rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.border-top {
|
|
|
|
|
|
border-top: 1px solid #e5e7eb;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 15:39:14 +08:00
|
|
|
|
.head-row {
|
2026-01-22 15:54:15 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
}
|
2026-01-26 15:39:14 +08:00
|
|
|
|
.head-title {
|
|
|
|
|
|
font-size: 32rpx;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
font-weight: 600;
|
2026-01-26 15:39:14 +08:00
|
|
|
|
color: #111827;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
}
|
2026-01-26 15:39:14 +08:00
|
|
|
|
.head-tag {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
padding: 12rpx 20rpx;
|
|
|
|
|
|
border-radius: 16rpx;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
background: #f3f4f6;
|
2026-01-26 15:39:14 +08:00
|
|
|
|
color: #6b7280;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
}
|
2026-01-26 15:39:14 +08:00
|
|
|
|
.tag-processing {
|
|
|
|
|
|
background: #fee2e2;
|
|
|
|
|
|
color: #ef4444;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
}
|
2026-01-26 15:39:14 +08:00
|
|
|
|
.tag-notStart {
|
|
|
|
|
|
background: #dbeafe;
|
|
|
|
|
|
color: #2563eb;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
}
|
2026-01-26 15:39:14 +08:00
|
|
|
|
.tag-treated {
|
2026-01-22 15:54:15 +08:00
|
|
|
|
background: #dcfce7;
|
|
|
|
|
|
color: #16a34a;
|
|
|
|
|
|
}
|
2026-01-26 15:39:14 +08:00
|
|
|
|
.tag-cancelled,
|
|
|
|
|
|
.tag-expired {
|
2026-01-22 15:54:15 +08:00
|
|
|
|
background: #f3f4f6;
|
2026-01-26 15:39:14 +08:00
|
|
|
|
color: #6b7280;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
}
|
2026-01-26 15:39:14 +08:00
|
|
|
|
.head-content {
|
|
|
|
|
|
margin-top: 24rpx;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
line-height: 44rpx;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
}
|
2026-01-26 15:39:14 +08:00
|
|
|
|
|
|
|
|
|
|
.sub-label {
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
}
|
|
|
|
|
|
.sub-content {
|
|
|
|
|
|
margin-top: 24rpx;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
line-height: 44rpx;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
}
|
2026-01-26 15:39:14 +08:00
|
|
|
|
.file-line {
|
|
|
|
|
|
margin-top: 12rpx;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
line-height: 40rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.file-type {
|
|
|
|
|
|
color: #111827;
|
|
|
|
|
|
margin-right: 12rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.file-name {
|
|
|
|
|
|
color: #2563eb;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-title {
|
2026-01-26 15:39:14 +08:00
|
|
|
|
font-size: 28rpx;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
font-weight: 600;
|
2026-01-26 15:39:14 +08:00
|
|
|
|
color: #111827;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
.method-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-01-26 15:39:14 +08:00
|
|
|
|
margin-top: 24rpx;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
.radio {
|
2026-01-26 15:39:14 +08:00
|
|
|
|
width: 32rpx;
|
|
|
|
|
|
height: 32rpx;
|
|
|
|
|
|
margin-right: 20rpx;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
.method-label {
|
2026-01-26 15:39:14 +08:00
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #111827;
|
|
|
|
|
|
margin-right: 20rpx;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
}
|
2026-01-26 15:39:14 +08:00
|
|
|
|
.method-select {
|
2026-01-22 15:54:15 +08:00
|
|
|
|
margin-left: auto;
|
2026-01-26 15:39:14 +08:00
|
|
|
|
width: 400rpx;
|
|
|
|
|
|
height: 60rpx;
|
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
|
border-radius: 16rpx;
|
|
|
|
|
|
padding: 12rpx 20rpx;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
}
|
|
|
|
|
|
.method-value {
|
2026-01-26 15:39:14 +08:00
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #111827;
|
|
|
|
|
|
max-width: 320rpx;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.method-value.muted {
|
2026-01-26 15:39:14 +08:00
|
|
|
|
color: #9ca3af;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kv {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
}
|
|
|
|
|
|
.kv-left {
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
}
|
|
|
|
|
|
.kv-right {
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #111827;
|
|
|
|
|
|
max-width: 70%;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.kv-right.link {
|
|
|
|
|
|
color: #2563eb;
|
|
|
|
|
|
text-decoration: underline;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 15:39:14 +08:00
|
|
|
|
.mt-20 {
|
|
|
|
|
|
margin-top: 20rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.result-box {
|
|
|
|
|
|
margin-top: 24rpx;
|
|
|
|
|
|
padding: 20rpx;
|
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
|
border-radius: 16rpx;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
}
|
2026-01-26 15:39:14 +08:00
|
|
|
|
.result-textarea {
|
2026-01-22 15:54:15 +08:00
|
|
|
|
width: 100%;
|
2026-01-26 15:39:14 +08:00
|
|
|
|
min-height: 160rpx;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
line-height: 48rpx;
|
|
|
|
|
|
color: #111827;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
2026-01-26 15:39:14 +08:00
|
|
|
|
.meta {
|
|
|
|
|
|
font-size: 28rpx;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
}
|
2026-01-26 15:39:14 +08:00
|
|
|
|
.meta-row {
|
|
|
|
|
|
margin-top: 24rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.meta-row:first-child {
|
|
|
|
|
|
margin-top: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.meta-key {
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
margin-right: 12rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.meta-val {
|
|
|
|
|
|
color: #6b7280;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.footer {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
display: flex;
|
2026-01-26 15:39:14 +08:00
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
gap: 20rpx;
|
|
|
|
|
|
padding: 30rpx;
|
|
|
|
|
|
box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
|
|
|
|
|
|
}
|
|
|
|
|
|
.footer-btn {
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
min-width: 260rpx;
|
|
|
|
|
|
height: 80rpx;
|
|
|
|
|
|
line-height: 80rpx;
|
|
|
|
|
|
border-radius: 16rpx;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.footer-btn::after {
|
2026-01-22 15:54:15 +08:00
|
|
|
|
border: none;
|
|
|
|
|
|
}
|
2026-01-26 15:39:14 +08:00
|
|
|
|
.footer-btn.plain {
|
2026-01-22 15:54:15 +08:00
|
|
|
|
background: #fff;
|
2026-01-26 15:39:14 +08:00
|
|
|
|
color: #2563eb;
|
|
|
|
|
|
border: 1px solid #2563eb;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
}
|
2026-01-26 15:39:14 +08:00
|
|
|
|
.footer-btn.plain.danger {
|
|
|
|
|
|
color: #ef4444;
|
|
|
|
|
|
border-color: #ef4444;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
}
|
2026-01-26 15:39:14 +08:00
|
|
|
|
.footer-btn.primary {
|
|
|
|
|
|
background: #2563eb;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.delete-fab {
|
|
|
|
|
|
position: fixed;
|
2026-01-26 15:39:14 +08:00
|
|
|
|
right: 32rpx;
|
|
|
|
|
|
bottom: calc(200rpx + env(safe-area-inset-bottom));
|
|
|
|
|
|
width: 104rpx;
|
|
|
|
|
|
height: 104rpx;
|
|
|
|
|
|
border-radius: 52rpx;
|
2026-01-22 15:54:15 +08:00
|
|
|
|
background: #fff;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-01-26 15:39:14 +08:00
|
|
|
|
box-shadow: 0 10rpx 18rpx rgba(0, 0, 0, 0.12);
|
2026-01-22 15:54:15 +08:00
|
|
|
|
z-index: 30;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|