feat:回访接口接入
This commit is contained in:
parent
da7397ce7e
commit
70d0f5e496
@ -67,6 +67,21 @@
|
||||
</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">
|
||||
@ -119,7 +134,12 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
|
||||
import { ensureSeed, queryFollowups } from './mock';
|
||||
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: () => ({}) },
|
||||
@ -128,6 +148,10 @@ const props = defineProps({
|
||||
floatingBottom: { type: Number, default: 16 },
|
||||
});
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const { account, doctorInfo } = storeToRefs(accountStore);
|
||||
const { getDoctorInfo } = accountStore;
|
||||
|
||||
const statusTabs = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '待处理', value: 'processing' },
|
||||
@ -137,18 +161,9 @@ const statusTabs = [
|
||||
{ label: '已过期', value: 'expired' },
|
||||
];
|
||||
|
||||
const typeOptions = [
|
||||
{ label: '回访', value: 'followup' },
|
||||
{ label: '复诊提醒', value: 'revisit' },
|
||||
{ label: '问卷', value: 'questionnaire' },
|
||||
{ label: '其他', value: 'other' },
|
||||
];
|
||||
const typeOptions = [{ label: '全部', value: 'all' }, ...getTodoEventTypeOptions()];
|
||||
|
||||
const teamOptions = [
|
||||
{ label: '全部', value: 'ALL' },
|
||||
{ label: '口腔一科(示例)', value: 'team_1' },
|
||||
{ label: '正畸团队(示例)', value: 'team_2' },
|
||||
];
|
||||
const teamOptions = ref([{ label: '全部', value: 'ALL' }]);
|
||||
|
||||
const query = reactive({
|
||||
isMy: false,
|
||||
@ -166,6 +181,8 @@ 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';
|
||||
@ -179,9 +196,94 @@ const loadMoreText = {
|
||||
const filtered = ref(false);
|
||||
const typeSelectedMap = computed(() => {
|
||||
const s = new Set(query.eventTypes || []);
|
||||
return typeOptions.reduce((acc, cur) => ((acc[cur.value] = s.has(cur.value)), acc), {});
|
||||
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;
|
||||
}, {});
|
||||
});
|
||||
|
||||
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 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;
|
||||
@ -189,26 +291,47 @@ function resetList() {
|
||||
getMore();
|
||||
}
|
||||
|
||||
function getMore() {
|
||||
async function getMore() {
|
||||
if (!props.archiveId) return;
|
||||
if (loading.value) return;
|
||||
if (page.value > pages.value) return;
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const { list: arr, total: t, pages: p } = queryFollowups({
|
||||
archiveId: props.archiveId,
|
||||
await ensureDoctor();
|
||||
|
||||
const corpId = getCorpId();
|
||||
const userId = getUserId();
|
||||
if (!corpId) {
|
||||
toast('缺少 corpId,请先完成登录/团队选择');
|
||||
return;
|
||||
}
|
||||
|
||||
const params = {
|
||||
corpId,
|
||||
customerId: String(props.archiveId),
|
||||
page: page.value,
|
||||
pageSize,
|
||||
status: query.status,
|
||||
isMy: query.isMy,
|
||||
eventTypes: query.eventTypes,
|
||||
teamId: query.teamId,
|
||||
planRange: query.planRange,
|
||||
});
|
||||
total.value = t;
|
||||
pages.value = p;
|
||||
list.value = page.value === 1 ? arr : [...list.value, ...arr];
|
||||
};
|
||||
|
||||
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;
|
||||
@ -224,6 +347,26 @@ function toggleStatus(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: ['+新增任务', '+使用模板', '+回访记录'],
|
||||
@ -250,7 +393,7 @@ function toDetail(todo) {
|
||||
// ---- filter popup ----
|
||||
const filterPopupRef = ref(null);
|
||||
const state = ref(null);
|
||||
const teamPicked = ref(teamOptions[0]);
|
||||
const teamPicked = ref(teamOptions.value[0]);
|
||||
const planRange = ref(['', '']);
|
||||
|
||||
function openFilter() {
|
||||
@ -260,7 +403,7 @@ function openFilter() {
|
||||
range: [...planRange.value],
|
||||
};
|
||||
planRange.value = [...(query.planRange || ['', ''])];
|
||||
teamPicked.value = teamOptions.find((i) => i.value === query.teamId) || teamOptions[0];
|
||||
teamPicked.value = teamOptions.value.find((i) => i.value === query.teamId) || teamOptions.value[0];
|
||||
filterPopupRef.value?.open?.();
|
||||
}
|
||||
function closeFilter(revert) {
|
||||
@ -273,6 +416,10 @@ function closeFilter(revert) {
|
||||
}
|
||||
|
||||
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);
|
||||
@ -280,7 +427,7 @@ function toggleType(v) {
|
||||
}
|
||||
|
||||
function pickTeam(e) {
|
||||
teamPicked.value = teamOptions[e.detail.value] || teamOptions[0];
|
||||
teamPicked.value = teamOptions.value[e.detail.value] || teamOptions.value[0];
|
||||
query.teamId = teamPicked.value.value;
|
||||
}
|
||||
|
||||
@ -300,7 +447,7 @@ function clearPlanRange() {
|
||||
function resetFilter() {
|
||||
query.eventTypes = [];
|
||||
query.teamId = 'ALL';
|
||||
teamPicked.value = teamOptions[0];
|
||||
teamPicked.value = teamOptions.value[0];
|
||||
clearPlanRange();
|
||||
}
|
||||
|
||||
@ -311,7 +458,10 @@ function confirmFilter() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
ensureSeed(props.archiveId, props.data);
|
||||
const userId = getUserId();
|
||||
const name = String(doctorInfo.value?.anotherName || doctorInfo.value?.name || '');
|
||||
if (userId && name) userNameMap.value = { ...(userNameMap.value || {}), [userId]: name };
|
||||
loadTeams();
|
||||
resetList();
|
||||
uni.$on('archive-detail:followup-changed', resetList);
|
||||
});
|
||||
|
||||
54
components/manage-plan/node-list.vue
Normal file
54
components/manage-plan/node-list.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<view class="node" :class="{ last }">
|
||||
<view class="node-index">{{ index }}</view>
|
||||
<view v-if="!last" class="node-line" />
|
||||
<view class="node-body">
|
||||
<slot />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
index: { type: Number, default: 1 },
|
||||
last: { type: Boolean, default: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.node {
|
||||
position: relative;
|
||||
padding-left: 60rpx;
|
||||
margin-right: 30rpx;
|
||||
}
|
||||
.node-index {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
left: 30rpx;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
line-height: 40rpx;
|
||||
transform: translateX(-50%);
|
||||
border-radius: 50%;
|
||||
background: #4f6ef7;
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
text-align: center;
|
||||
z-index: 3;
|
||||
}
|
||||
.node-line {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
left: 30rpx;
|
||||
width: 2rpx;
|
||||
height: 100%;
|
||||
transform: translateX(-50%);
|
||||
background: #4f6ef7;
|
||||
z-index: 2;
|
||||
}
|
||||
.node-body {
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
}
|
||||
</style>
|
||||
|
||||
113
components/manage-plan/plan-node-item.vue
Normal file
113
components/manage-plan/plan-node-item.vue
Normal file
@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<view class="card" :class="classnames">
|
||||
<view class="block">
|
||||
<view class="label">待办类型:</view>
|
||||
<view class="value">{{ getTodoEventTypeLabel(item?.eventType) }}</view>
|
||||
</view>
|
||||
<view class="block">
|
||||
<view class="label">任务内容:</view>
|
||||
<view
|
||||
v-if="item?.taskContent"
|
||||
class="value"
|
||||
:class="{ clamp2: foldContent }"
|
||||
@click="foldContent = !foldContent"
|
||||
>
|
||||
{{ item.taskContent }}
|
||||
</view>
|
||||
<view v-else class="value muted">暂无内容</view>
|
||||
</view>
|
||||
<view class="block">
|
||||
<view class="label">向客户发送:</view>
|
||||
<view
|
||||
v-if="item?.sendContent"
|
||||
class="send"
|
||||
:class="{ clamp1: foldSendContent }"
|
||||
@click="foldSendContent = !foldSendContent"
|
||||
>
|
||||
<text class="muted">【提醒】</text>
|
||||
<text>{{ item.sendContent }}</text>
|
||||
</view>
|
||||
<view v-if="sendFile" class="send">
|
||||
<text class="muted">【{{ sendFile.fileLabel }}】</text>
|
||||
<text class="link">{{ sendFile.name }}</text>
|
||||
</view>
|
||||
<view v-if="!item?.sendContent && !sendFile" class="send muted">暂无发送内容</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { getTodoEventTypeLabel } from '@/utils/todo-const';
|
||||
|
||||
const props = defineProps({
|
||||
item: { type: Object, default: () => ({}) },
|
||||
classnames: { type: String, default: '' },
|
||||
});
|
||||
|
||||
const foldContent = ref(true);
|
||||
const foldSendContent = ref(true);
|
||||
|
||||
const sendFile = computed(() => {
|
||||
const file = props.item?.pannedEventSendFile;
|
||||
if (!file || typeof file !== 'object') return null;
|
||||
if (file.type === 'questionnaire' || file.type === 'article') {
|
||||
return {
|
||||
...file,
|
||||
fileLabel: file.type === 'questionnaire' ? '问卷' : '文章',
|
||||
name: String(file.name || file.title || file.url || ''),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card {
|
||||
font-size: 28rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.block {
|
||||
padding: 20rpx;
|
||||
border-bottom: 1px solid #eef0f2;
|
||||
}
|
||||
.block:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.label {
|
||||
margin-bottom: 20rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
.value {
|
||||
color: #111827;
|
||||
line-height: 44rpx;
|
||||
word-break: break-all;
|
||||
}
|
||||
.send {
|
||||
margin-top: 20rpx;
|
||||
color: #111827;
|
||||
line-height: 44rpx;
|
||||
word-break: break-all;
|
||||
}
|
||||
.muted {
|
||||
color: #9ca3af;
|
||||
}
|
||||
.link {
|
||||
color: #4f6ef7;
|
||||
}
|
||||
.clamp2 {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.clamp1 {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
</style>
|
||||
|
||||
128
components/manage-plan/plan-node-list.vue
Normal file
128
components/manage-plan/plan-node-list.vue
Normal file
@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<view>
|
||||
<node-list v-for="(g, idx) in taskShowList" :key="g.id" :index="idx + 1" :last="idx === taskShowList.length - 1">
|
||||
<view class="group-head">
|
||||
<view class="group-title">{{ g.title }}</view>
|
||||
<view class="group-toggle" @click.stop="toggle(g.id)">
|
||||
<view class="toggle-text">{{ shrinkMap[g.id] ? '展开' : '收起' }}</view>
|
||||
<uni-icons class="toggle-icon" size="12" :type="shrinkMap[g.id] ? 'arrowup' : 'arrowdown'" color="#6b7280" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!shrinkMap[g.id]" class="group-body">
|
||||
<plan-node-item v-for="(t, tIdx) in g.list" :key="String(t.taskId || tIdx)" :item="t" :classnames="tIdx ? 'mt-20' : ''" />
|
||||
</view>
|
||||
</node-list>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import nodeList from './node-list.vue';
|
||||
import planNodeItem from './plan-node-item.vue';
|
||||
|
||||
const props = defineProps({
|
||||
taskList: { type: Array, default: () => [] },
|
||||
planExecutionTime: { type: String, default: '' },
|
||||
});
|
||||
|
||||
const shrinkMap = ref({});
|
||||
|
||||
function toggle(id) {
|
||||
shrinkMap.value[id] = !shrinkMap.value[id];
|
||||
}
|
||||
|
||||
function timeTypeTransform(timeType) {
|
||||
if (!timeType || timeType === 'day') return '天';
|
||||
if (timeType === 'week') return '周';
|
||||
if (timeType === 'month') return '月';
|
||||
if (timeType === 'year') return '年';
|
||||
return String(timeType);
|
||||
}
|
||||
|
||||
function normalizedDays(taskTime, timeType) {
|
||||
const t = typeof taskTime === 'number' ? taskTime : 0;
|
||||
if (timeType === 'week') return t * 7;
|
||||
if (timeType === 'month') return t * 30;
|
||||
if (timeType === 'year') return t * 365;
|
||||
return t;
|
||||
}
|
||||
|
||||
function dayTransform(baseDate, taskTime, timeType) {
|
||||
if (!baseDate) return '';
|
||||
if (!dayjs(baseDate).isValid()) return '';
|
||||
const t = typeof taskTime === 'number' ? taskTime : 0;
|
||||
const unit = ['day', 'week', 'month', 'year'].includes(String(timeType || '')) ? String(timeType) : 'day';
|
||||
return dayjs(baseDate).add(t, unit).format('YYYY-MM-DD');
|
||||
}
|
||||
|
||||
const taskShowList = computed(() => {
|
||||
if (!Array.isArray(props.taskList) || props.taskList.length === 0) return [];
|
||||
|
||||
const grouped = props.taskList.reduce((acc, task) => {
|
||||
const obj = task && typeof task === 'object' ? task : {};
|
||||
const timeKey = normalizedDays(obj.taskTime, obj.timeType);
|
||||
const key = String(timeKey);
|
||||
if (!acc[key]) acc[key] = [];
|
||||
acc[key].push(obj);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const baseDate = props.planExecutionTime && dayjs(props.planExecutionTime).isValid() ? props.planExecutionTime : '';
|
||||
return Object.entries(grouped)
|
||||
.sort((a, b) => Number(a[0]) - Number(b[0]))
|
||||
.map(([key, list]) => {
|
||||
const first = list[0] || {};
|
||||
const taskTime = typeof first.taskTime === 'number' ? first.taskTime : 0;
|
||||
const unit = timeTypeTransform(first.timeType);
|
||||
const isToday = (first.timeType === 'day' || !first.timeType) && taskTime === 0;
|
||||
const offsetText = isToday ? '当天' : `${taskTime}${unit}`;
|
||||
const dateText = baseDate ? dayTransform(baseDate, taskTime, first.timeType) : '';
|
||||
return {
|
||||
id: key,
|
||||
title: `计划开始后: ${offsetText}${dateText ? `(${dateText})` : ''}`,
|
||||
list,
|
||||
};
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.group-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 0 20rpx;
|
||||
}
|
||||
.group-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-left: 20rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.group-toggle {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
.toggle-text {
|
||||
font-size: 28rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
.toggle-icon {
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
.group-body {
|
||||
padding-bottom: 20rpx;
|
||||
}
|
||||
.mt-20 {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
18
pages.json
18
pages.json
@ -19,12 +19,6 @@
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
// {
|
||||
// "path": "pages/message/index",
|
||||
// "style": {
|
||||
// "navigationBarTitleText": "消息"
|
||||
// }
|
||||
// },
|
||||
{
|
||||
"path": "pages/message/index",
|
||||
"style": {
|
||||
@ -134,6 +128,18 @@
|
||||
"navigationBarTitleText": "回访计划"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/case/plan-preview",
|
||||
"style": {
|
||||
"navigationBarTitleText": "回访计划详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/case/plan-execute",
|
||||
"style": {
|
||||
"navigationBarTitleText": "执行回访计划"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/work/work",
|
||||
"style": {
|
||||
|
||||
@ -1,51 +1,87 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/followup-detail/followup-detail.vue(简化移植:去除微信会话/员工选择/接口) -->
|
||||
<view class="card">
|
||||
<view class="title-row">
|
||||
<view class="title">{{ currentType.label }}</view>
|
||||
<view class="tag" :class="`st-${currentStatus.value}`">{{ currentStatus.label }}</view>
|
||||
<!-- 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>
|
||||
</view>
|
||||
<view class="desc">{{ form.taskContent || '暂无任务内容' }}</view>
|
||||
<view class="info-row">
|
||||
<view class="info">计划执行时间:{{ planDate || '--' }}</view>
|
||||
<view class="info">执行人:{{ form.executorName || '--' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<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-input" @click.stop="pickPhone">
|
||||
<view class="method-value" :class="{ muted: !phone }">{{ phone || '选择号码' }}</view>
|
||||
<uni-icons type="arrowright" size="16" color="#999" />
|
||||
<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="method-row" @click="toggleMethod('wechat')">
|
||||
<image class="radio" :src="`/static/circle${todoMethod === 'wechat' ? 'd' : ''}.svg`" />
|
||||
<view class="method-label">微信</view>
|
||||
<view class="method-note">(mock:不接入会话)</view>
|
||||
|
||||
<view class="section border-top kv">
|
||||
<view class="kv-left">计划执行时间:{{ planDate || '--' }}</view>
|
||||
<view class="kv-right">{{ executorDisplay || '--' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="section-title">回访结果</view>
|
||||
<view class="textarea-box">
|
||||
<textarea v-model="form.result" class="textarea tall" placeholder="请填写回访结果" maxlength="500" />
|
||||
<view class="counter">{{ (form.result || '').length }}/500</view>
|
||||
<view class="section border-top kv">
|
||||
<view class="kv-left">客户:</view>
|
||||
<view class="kv-right link">{{ customerDisplay || '--' }}</view>
|
||||
</view>
|
||||
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<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>
|
||||
</view>
|
||||
|
||||
<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>
|
||||
</view>
|
||||
|
||||
<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>
|
||||
</view>
|
||||
|
||||
<view class="footer">
|
||||
<button class="btn plain" @click="cancel">返回</button>
|
||||
<button v-if="canEdit" class="btn plain danger" @click="cancelTask">取消任务</button>
|
||||
<button v-if="canEdit" class="btn primary" @click="markDone">设为完成</button>
|
||||
<button v-else class="btn primary" @click="save">保存</button>
|
||||
</view>
|
||||
|
||||
<view v-if="taskId" class="delete-fab" @click="remove">
|
||||
<view v-if="taskId && canRemove" class="delete-fab" @click="remove">
|
||||
<uni-icons type="trash" size="22" color="#ff4d4f" />
|
||||
</view>
|
||||
</view>
|
||||
@ -55,36 +91,90 @@
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import dayjs from 'dayjs';
|
||||
import { ensureSeed, getFollowup, removeFollowup, upsertFollowup } from '@/components/archive-detail/mock';
|
||||
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';
|
||||
|
||||
const archiveId = ref('');
|
||||
const mode = ref('add');
|
||||
const taskId = ref('');
|
||||
|
||||
const typeOptions = [
|
||||
{ label: '回访', value: 'followup' },
|
||||
{ label: '复诊提醒', value: 'revisit' },
|
||||
{ label: '问卷', value: 'questionnaire' },
|
||||
{ label: '其他', value: 'other' },
|
||||
];
|
||||
const currentType = ref(typeOptions[0]);
|
||||
const todo = ref(null);
|
||||
const currentType = computed(() => ({ label: getTodoEventTypeLabel(todo.value?.eventType), value: String(todo.value?.eventType || '') }));
|
||||
const customer = ref(null);
|
||||
const userNameMap = ref({});
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '待处理', value: 'processing' },
|
||||
{ label: '未开始', value: 'notStart' },
|
||||
{ label: '已完成', value: 'treated' },
|
||||
{ label: '已取消', value: 'cancelled' },
|
||||
{ label: '已过期', value: 'expired' },
|
||||
];
|
||||
const currentStatus = ref(statusOptions[0]);
|
||||
function statusLabelFromStatus(status) {
|
||||
const map = {
|
||||
processing: '待处理',
|
||||
notStart: '未开始',
|
||||
treated: '已完成',
|
||||
cancelled: '已取消',
|
||||
expired: '已过期',
|
||||
};
|
||||
return map[status] || '未知';
|
||||
}
|
||||
|
||||
const teamOptions = [
|
||||
{ label: '口腔一科(示例)', value: 'team_1' },
|
||||
{ label: '正畸团队(示例)', value: 'team_2' },
|
||||
];
|
||||
const currentTeam = ref(teamOptions[0]);
|
||||
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;
|
||||
|
||||
const planDate = ref('');
|
||||
const form = reactive({
|
||||
executorName: '',
|
||||
taskContent: '',
|
||||
@ -94,105 +184,166 @@ const form = reactive({
|
||||
const todoMethod = ref('phone'); // phone | wechat
|
||||
const phone = ref('');
|
||||
const mobiles = computed(() => {
|
||||
const m = String(form.executorName || '').trim();
|
||||
// mock:用本地缓存里的手机号 + 默认号码
|
||||
const arr = [];
|
||||
const cached = uni.getStorageSync('ykt_case_archive_detail');
|
||||
if (cached && typeof cached === 'object' && cached.mobile) arr.push(String(cached.mobile));
|
||||
if (!arr.includes('13800000000')) arr.push('13800000000');
|
||||
return arr;
|
||||
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));
|
||||
});
|
||||
|
||||
const canEdit = computed(() => ['processing', 'notStart'].includes(currentStatus.value.value));
|
||||
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 || '');
|
||||
});
|
||||
|
||||
onLoad((options) => {
|
||||
archiveId.value = options?.archiveId ? String(options.archiveId) : '';
|
||||
mode.value = options?.mode ? String(options.mode) : 'add';
|
||||
taskId.value = options?.id ? String(options.id) : '';
|
||||
|
||||
ensureSeed(archiveId.value, {});
|
||||
const cached = uni.getStorageSync('ykt_case_archive_detail');
|
||||
if (cached && typeof cached === 'object') customer.value = cached;
|
||||
|
||||
if (taskId.value) {
|
||||
const task = getFollowup({ archiveId: archiveId.value, id: taskId.value });
|
||||
if (task) {
|
||||
currentType.value = typeOptions.find((i) => i.value === task.eventType) || typeOptions[0];
|
||||
currentStatus.value = statusOptions.find((i) => i.value === task.status) || statusOptions[0];
|
||||
currentTeam.value = teamOptions.find((i) => i.value === task.executeTeamId) || teamOptions[0];
|
||||
planDate.value = task.planDate || '';
|
||||
form.executorName = task.executorName || '';
|
||||
form.taskContent = task.taskContent || '';
|
||||
form.result = task.result || '';
|
||||
if (typeof task.todoMethod === 'string') {
|
||||
if (task.todoMethod.startsWith('phone')) {
|
||||
todoMethod.value = 'phone';
|
||||
const parts = task.todoMethod.split(':');
|
||||
phone.value = parts[1] || '';
|
||||
} else if (task.todoMethod === 'wechat') {
|
||||
todoMethod.value = 'wechat';
|
||||
phone.value = '';
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!taskId.value) {
|
||||
toast('缺少回访任务 id');
|
||||
setTimeout(() => uni.navigateBack(), 300);
|
||||
return;
|
||||
}
|
||||
|
||||
planDate.value = dayjs().format('YYYY-MM-DD');
|
||||
getTodo();
|
||||
});
|
||||
|
||||
function pickType(e) {
|
||||
currentType.value = typeOptions[e.detail.value] || typeOptions[0];
|
||||
}
|
||||
function pickStatus(e) {
|
||||
currentStatus.value = statusOptions[e.detail.value] || statusOptions[0];
|
||||
}
|
||||
function pickTeam(e) {
|
||||
currentTeam.value = teamOptions[e.detail.value] || teamOptions[0];
|
||||
}
|
||||
function pickPlanDate(e) {
|
||||
planDate.value = e.detail.value || '';
|
||||
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: '' };
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
uni.navigateBack();
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (!archiveId.value) {
|
||||
uni.showToast({ title: '缺少 archiveId', icon: 'none' });
|
||||
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,请先完成登录/团队选择');
|
||||
return;
|
||||
}
|
||||
const plannedExecutionTime = planDate.value ? dayjs(planDate.value).valueOf() : Date.now();
|
||||
upsertFollowup({
|
||||
archiveId: archiveId.value,
|
||||
followup: {
|
||||
_id: taskId.value || '',
|
||||
plannedExecutionTime,
|
||||
status: currentStatus.value.value,
|
||||
eventStatusLabel: currentStatus.value.label,
|
||||
eventType: currentType.value.value,
|
||||
eventTypeLabel: currentType.value.label,
|
||||
executeTeamId: currentTeam.value.value,
|
||||
executeTeamName: currentTeam.value.label,
|
||||
executorName: form.executorName || '李医生',
|
||||
creatorName: '管理员A',
|
||||
taskContent: form.taskContent,
|
||||
result: form.result,
|
||||
todoMethod: todoMethod.value === 'phone' ? (phone.value ? `phone:${phone.value}` : 'phone') : 'wechat',
|
||||
},
|
||||
});
|
||||
uni.$emit('archive-detail:followup-changed');
|
||||
uni.showToast({ title: '保存成功', icon: 'success' });
|
||||
setTimeout(() => uni.navigateBack(), 300);
|
||||
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 || '') || '';
|
||||
}
|
||||
|
||||
function toggleMethod(v) {
|
||||
if (!canEdit.value) return;
|
||||
if (!editable.value) return;
|
||||
todoMethod.value = v;
|
||||
if (v !== 'phone') phone.value = '';
|
||||
}
|
||||
|
||||
function pickPhone() {
|
||||
if (!canEdit.value) return;
|
||||
if (!editable.value) return;
|
||||
if (todoMethod.value !== 'phone') return;
|
||||
uni.showActionSheet({
|
||||
itemList: mobiles.value,
|
||||
success: ({ tapIndex }) => {
|
||||
@ -203,41 +354,76 @@ function pickPhone() {
|
||||
}
|
||||
|
||||
function markDone() {
|
||||
if (!canEdit.value) return;
|
||||
if (!editable.value) return;
|
||||
if (!['phone', 'wechat'].includes(todoMethod.value)) return uni.showToast({ title: '请选择回访方式', icon: 'none' });
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定完成该回访任务吗?',
|
||||
success: (res) => {
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return;
|
||||
currentStatus.value = statusOptions.find((i) => i.value === 'treated') || currentStatus.value;
|
||||
save();
|
||||
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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function cancelTask() {
|
||||
if (!canEdit.value) return;
|
||||
if (!editable.value) return;
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定取消该回访任务吗?',
|
||||
success: (res) => {
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return;
|
||||
currentStatus.value = statusOptions.find((i) => i.value === 'cancelled') || currentStatus.value;
|
||||
save();
|
||||
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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function remove() {
|
||||
if (!canRemove.value) return;
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定删除当前记录?',
|
||||
success: (res) => {
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return;
|
||||
removeFollowup({ archiveId: archiveId.value, id: taskId.value });
|
||||
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 || '删除失败');
|
||||
uni.$emit('archive-detail:followup-changed');
|
||||
uni.showToast({ title: '已删除', icon: 'success' });
|
||||
toast('已删除');
|
||||
setTimeout(() => uni.navigateBack(), 300);
|
||||
},
|
||||
});
|
||||
@ -246,136 +432,183 @@ function remove() {
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6f8;
|
||||
padding-bottom: calc(76px + env(safe-area-inset-bottom));
|
||||
height: 100vh;
|
||||
background: #f6f7f8;
|
||||
}
|
||||
.card {
|
||||
.scroll {
|
||||
height: 100vh;
|
||||
}
|
||||
.section {
|
||||
background: #fff;
|
||||
margin: 10px 14px 0;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||||
padding: 24rpx 30rpx;
|
||||
}
|
||||
.border-top {
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
.head-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.title {
|
||||
font-size: 16px;
|
||||
.head-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: #111827;
|
||||
}
|
||||
.tag {
|
||||
font-size: 12px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
.head-tag {
|
||||
flex-shrink: 0;
|
||||
font-size: 24rpx;
|
||||
padding: 12rpx 20rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #f3f4f6;
|
||||
color: #666;
|
||||
color: #6b7280;
|
||||
}
|
||||
.st-processing {
|
||||
background: #ffe5e5;
|
||||
color: #ff4d4f;
|
||||
.tag-processing {
|
||||
background: #fee2e2;
|
||||
color: #ef4444;
|
||||
}
|
||||
.st-notStart {
|
||||
background: #dbe6ff;
|
||||
color: #4f6ef7;
|
||||
.tag-notStart {
|
||||
background: #dbeafe;
|
||||
color: #2563eb;
|
||||
}
|
||||
.st-treated {
|
||||
.tag-treated {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
.st-cancelled,
|
||||
.st-expired {
|
||||
.tag-cancelled,
|
||||
.tag-expired {
|
||||
background: #f3f4f6;
|
||||
color: #666;
|
||||
color: #6b7280;
|
||||
}
|
||||
.desc {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 20px;
|
||||
.head-content {
|
||||
margin-top: 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #6b7280;
|
||||
line-height: 44rpx;
|
||||
}
|
||||
.info-row {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
|
||||
.sub-label {
|
||||
font-size: 28rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
.info {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
.sub-content {
|
||||
margin-top: 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #6b7280;
|
||||
line-height: 44rpx;
|
||||
}
|
||||
.file-line {
|
||||
margin-top: 12rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 40rpx;
|
||||
}
|
||||
.file-type {
|
||||
color: #111827;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
.file-name {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
color: #111827;
|
||||
}
|
||||
.method-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
.radio {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 10px;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
.method-label {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-right: 10px;
|
||||
font-size: 28rpx;
|
||||
color: #111827;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
.method-note {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.method-input {
|
||||
.method-select {
|
||||
margin-left: auto;
|
||||
width: 220px;
|
||||
height: 36px;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 6px;
|
||||
padding: 0 10px;
|
||||
width: 400rpx;
|
||||
height: 60rpx;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 16rpx;
|
||||
padding: 12rpx 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.method-value {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
max-width: 180px;
|
||||
font-size: 28rpx;
|
||||
color: #111827;
|
||||
max-width: 320rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.method-value.muted {
|
||||
color: #999;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.textarea-box {
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
.kv {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.textarea {
|
||||
.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;
|
||||
}
|
||||
|
||||
.mt-20 {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
.result-box {
|
||||
margin-top: 24rpx;
|
||||
padding: 20rpx;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
.result-textarea {
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
font-size: 14px;
|
||||
min-height: 160rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 48rpx;
|
||||
color: #111827;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.textarea.tall {
|
||||
height: 140px;
|
||||
.meta {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.counter {
|
||||
margin-top: 6px;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
.meta-row {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
.meta-row:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.meta-key {
|
||||
color: #6b7280;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
.meta-val {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.footer {
|
||||
@ -384,47 +617,49 @@ function remove() {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #fff;
|
||||
padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
|
||||
justify-content: flex-end;
|
||||
gap: 20rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.btn {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
.footer-btn {
|
||||
flex: 0 0 auto;
|
||||
min-width: 260rpx;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.btn::after {
|
||||
.footer-btn::after {
|
||||
border: none;
|
||||
}
|
||||
.btn.plain {
|
||||
.footer-btn.plain {
|
||||
background: #fff;
|
||||
color: #4f6ef7;
|
||||
border: 1px solid #4f6ef7;
|
||||
color: #2563eb;
|
||||
border: 1px solid #2563eb;
|
||||
}
|
||||
.btn.plain.danger {
|
||||
color: #ff4d4f;
|
||||
border-color: #ff4d4f;
|
||||
.footer-btn.plain.danger {
|
||||
color: #ef4444;
|
||||
border-color: #ef4444;
|
||||
}
|
||||
.btn.primary {
|
||||
background: #4f6ef7;
|
||||
.footer-btn.primary {
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.delete-fab {
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
bottom: calc(96px + env(safe-area-inset-bottom));
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 26px;
|
||||
right: 32rpx;
|
||||
bottom: calc(200rpx + env(safe-area-inset-bottom));
|
||||
width: 104rpx;
|
||||
height: 104rpx;
|
||||
border-radius: 52rpx;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 10px 18px rgba(0, 0, 0, 0.12);
|
||||
box-shadow: 0 10rpx 18rpx rgba(0, 0, 0, 0.12);
|
||||
z-index: 30;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -57,6 +57,27 @@
|
||||
<button class="btn plain" @click="cancel">取消</button>
|
||||
<button class="btn primary" @click="save">保存</button>
|
||||
</view>
|
||||
|
||||
<uni-popup ref="typePopup" type="bottom" :mask-click="true">
|
||||
<view class="picker-sheet">
|
||||
<view class="picker-title">选择回访类型</view>
|
||||
<scroll-view scroll-y class="picker-body">
|
||||
<view
|
||||
v-for="t in eventTypeList"
|
||||
:key="t.value"
|
||||
class="picker-item"
|
||||
:class="{ active: form.eventType === t.value }"
|
||||
@click="pickType(t.value)"
|
||||
>
|
||||
<view class="picker-item-text">{{ t.label }}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="picker-actions">
|
||||
<button class="btn plain" @click="closeTypePicker">取消</button>
|
||||
<button class="btn primary" @click="closeTypePicker">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -64,11 +85,22 @@
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import dayjs from 'dayjs';
|
||||
import { ensureSeed, upsertFollowup } from '@/components/archive-detail/mock';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import api from '@/utils/api';
|
||||
import useAccountStore from '@/store/account';
|
||||
import { toast } from '@/utils/widget';
|
||||
import { getTodoEventTypeLabel, getTodoEventTypeOptions } from '@/utils/todo-const';
|
||||
|
||||
const archiveId = ref('');
|
||||
const archiveName = ref('');
|
||||
const archiveMobile = ref('');
|
||||
const customerData = ref({});
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const { account, doctorInfo } = storeToRefs(accountStore);
|
||||
const { getDoctorInfo } = accountStore;
|
||||
|
||||
const teams = ref([]);
|
||||
|
||||
const form = reactive({
|
||||
plannedExecutionTime: '',
|
||||
@ -80,18 +112,8 @@ const form = reactive({
|
||||
result: '',
|
||||
});
|
||||
|
||||
const eventTypeList = [
|
||||
{ label: '回访', value: 'followup' },
|
||||
{ label: '复诊提醒', value: 'revisit' },
|
||||
{ label: '问卷', value: 'questionnaire' },
|
||||
{ label: '其他', value: 'other' },
|
||||
];
|
||||
const eventTypeLabel = computed(() => eventTypeList.find((i) => i.value === form.eventType)?.label || '');
|
||||
|
||||
const teamOptions = [
|
||||
{ label: '口腔一科(示例)', value: 'team_1' },
|
||||
{ label: '正畸团队(示例)', value: 'team_2' },
|
||||
];
|
||||
const eventTypeList = getTodoEventTypeOptions();
|
||||
const eventTypeLabel = computed(() => getTodoEventTypeLabel(form.eventType));
|
||||
|
||||
const mobiles = computed(() => {
|
||||
const arr = [];
|
||||
@ -106,19 +128,84 @@ onLoad((options) => {
|
||||
if (c && typeof c === 'object') {
|
||||
archiveId.value = archiveId.value || String(c._id || '');
|
||||
archiveName.value = String(c.name || '');
|
||||
customerData.value = c;
|
||||
}
|
||||
const cached = uni.getStorageSync('ykt_case_archive_detail');
|
||||
if (cached && typeof cached === 'object') archiveMobile.value = String(cached.mobile || '');
|
||||
if (cached && typeof cached === 'object' && String(cached?._id || '') === archiveId.value) {
|
||||
archiveMobile.value = String(cached.mobile || '');
|
||||
customerData.value = cached;
|
||||
archiveName.value = archiveName.value || String(cached.name || '');
|
||||
}
|
||||
|
||||
if (!archiveId.value) {
|
||||
uni.showToast({ title: '缺少 archiveId', icon: 'none' });
|
||||
setTimeout(() => uni.navigateBack(), 300);
|
||||
return;
|
||||
}
|
||||
ensureSeed(archiveId.value, { name: archiveName.value });
|
||||
form.plannedExecutionTime = dayjs().format('YYYY-MM-DD');
|
||||
initDefaultTeam();
|
||||
});
|
||||
|
||||
async function ensureDoctor() {
|
||||
if (doctorInfo.value) return;
|
||||
if (!account.value?.openid) return;
|
||||
try {
|
||||
await getDoctorInfo();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
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 || '') || '';
|
||||
}
|
||||
|
||||
function normalizeTeam(raw) {
|
||||
if (!raw || typeof raw !== 'object') return null;
|
||||
const teamId = raw.teamId || raw.id || raw._id || '';
|
||||
const name = raw.name || raw.teamName || raw.team || '';
|
||||
const corpId = raw.corpId || raw.corpID || '';
|
||||
if (!teamId || !name) return null;
|
||||
return { teamId: String(teamId), name: String(name), corpId: corpId ? String(corpId) : '' };
|
||||
}
|
||||
|
||||
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 : [];
|
||||
teams.value = list.map(normalizeTeam).filter(Boolean);
|
||||
}
|
||||
|
||||
async function initDefaultTeam() {
|
||||
await ensureDoctor();
|
||||
const currentTeam = uni.getStorageSync('ykt_case_current_team') || {};
|
||||
const teamId = String(currentTeam.teamId || '');
|
||||
const teamName = String(currentTeam.name || '');
|
||||
if (teamId) {
|
||||
form.teamId = teamId;
|
||||
form.teamName = teamName;
|
||||
} else {
|
||||
await loadTeams();
|
||||
if (teams.value[0]) {
|
||||
form.teamId = teams.value[0].teamId;
|
||||
form.teamName = teams.value[0].name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function changeDate(e) {
|
||||
form.plannedExecutionTime = e.detail.value || '';
|
||||
}
|
||||
@ -143,23 +230,26 @@ function selectMethod(method) {
|
||||
}
|
||||
|
||||
function selectType() {
|
||||
uni.showActionSheet({
|
||||
itemList: eventTypeList.map((i) => i.label),
|
||||
success: ({ tapIndex }) => {
|
||||
form.eventType = eventTypeList[tapIndex]?.value || '';
|
||||
},
|
||||
});
|
||||
typePopup.value?.open?.();
|
||||
}
|
||||
|
||||
function selectTeam() {
|
||||
uni.showActionSheet({
|
||||
itemList: teamOptions.map((i) => i.label),
|
||||
success: ({ tapIndex }) => {
|
||||
const t = teamOptions[tapIndex];
|
||||
form.teamId = t.value;
|
||||
form.teamName = t.label;
|
||||
},
|
||||
});
|
||||
(async () => {
|
||||
if (!teams.value.length) await loadTeams();
|
||||
if (!teams.value.length) {
|
||||
toast('暂无可选团队');
|
||||
return;
|
||||
}
|
||||
uni.showActionSheet({
|
||||
itemList: teams.value.map((t) => t.name),
|
||||
success: ({ tapIndex }) => {
|
||||
const t = teams.value[tapIndex];
|
||||
if (!t) return;
|
||||
form.teamId = t.teamId;
|
||||
form.teamName = t.name;
|
||||
},
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
@ -170,40 +260,64 @@ function cancel() {
|
||||
});
|
||||
}
|
||||
|
||||
function save() {
|
||||
async function save() {
|
||||
if (!form.plannedExecutionTime) return uni.showToast({ title: '请选择回访日期', icon: 'none' });
|
||||
if (!form.todoMethod) return uni.showToast({ title: '请选择回访方式', icon: 'none' });
|
||||
if (!form.eventType) return uni.showToast({ title: '请选择回访类型', icon: 'none' });
|
||||
if (!form.teamId) return uni.showToast({ title: '请选择所在团队', icon: 'none' });
|
||||
if (!String(form.result || '').trim()) return uni.showToast({ title: '请输入回访结果', icon: 'none' });
|
||||
|
||||
await ensureDoctor();
|
||||
const corpId = getCorpId();
|
||||
const userId = getUserId();
|
||||
if (!corpId || !userId) {
|
||||
toast('缺少用户/团队信息,请先完成登录与团队选择');
|
||||
return;
|
||||
}
|
||||
|
||||
const customer = customerData.value && typeof customerData.value === 'object' ? customerData.value : {};
|
||||
const customerId = String(customer._id || archiveId.value || '');
|
||||
const customerName = String(customer.name || archiveName.value || '');
|
||||
const customerUserId = String(customer.externalUserId || customer.customerUserId || '') || '';
|
||||
|
||||
const phoneValue = form.phoneNumber ? `phone:${form.phoneNumber}` : 'phone';
|
||||
const plannedExecutionTime = dayjs(form.plannedExecutionTime).valueOf();
|
||||
|
||||
upsertFollowup({
|
||||
archiveId: archiveId.value,
|
||||
followup: {
|
||||
plannedExecutionTime,
|
||||
endTime: plannedExecutionTime,
|
||||
status: 'treated',
|
||||
eventStatusLabel: '已完成',
|
||||
eventType: form.eventType,
|
||||
eventTypeLabel: eventTypeLabel.value || '回访',
|
||||
executeTeamId: form.teamId,
|
||||
executeTeamName: form.teamName,
|
||||
executorName: '我',
|
||||
creatorName: '我',
|
||||
taskContent: '',
|
||||
result: form.result,
|
||||
todoMethod: form.todoMethod === 'phone' ? phoneValue : form.todoMethod,
|
||||
createTime: Date.now(),
|
||||
},
|
||||
});
|
||||
const params = {
|
||||
corpId,
|
||||
eventStatus: 'treated',
|
||||
customerId,
|
||||
customerName,
|
||||
customerUserId,
|
||||
executeTeamId: form.teamId,
|
||||
executeTeamName: form.teamName,
|
||||
executorUserId: userId,
|
||||
creatorUserId: userId,
|
||||
taskContent: '',
|
||||
result: String(form.result || ''),
|
||||
todoMethod: form.todoMethod === 'phone' ? phoneValue : form.todoMethod,
|
||||
plannedExecutionTime,
|
||||
endTime: plannedExecutionTime,
|
||||
eventType: form.eventType,
|
||||
};
|
||||
|
||||
const res = await api('createEvents', { params });
|
||||
if (!res?.success) {
|
||||
toast(res?.message || '保存失败');
|
||||
return;
|
||||
}
|
||||
uni.$emit('archive-detail:followup-changed');
|
||||
uni.showToast({ title: '保存成功', icon: 'success' });
|
||||
toast('保存成功');
|
||||
setTimeout(() => uni.navigateBack(), 300);
|
||||
}
|
||||
|
||||
const typePopup = ref(null);
|
||||
function pickType(v) {
|
||||
form.eventType = String(v || '');
|
||||
}
|
||||
function closeTypePicker() {
|
||||
typePopup.value?.close?.();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -340,5 +454,38 @@ function save() {
|
||||
background: #4f6ef7;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
.picker-sheet {
|
||||
background: #fff;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.picker-title {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
padding: 14px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.picker-body {
|
||||
max-height: 60vh;
|
||||
}
|
||||
.picker-item {
|
||||
padding: 14px;
|
||||
border-bottom: 1px solid #f2f2f2;
|
||||
}
|
||||
.picker-item.active {
|
||||
background: #f2f6ff;
|
||||
}
|
||||
.picker-item-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
.picker-actions {
|
||||
padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/new-followup/new-followup.vue(简化移植:去除员工组件/群发附件/接口) -->
|
||||
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/new-followup/new-followup.vue(wxapp:仅新增“待办”任务) -->
|
||||
<view class="page">
|
||||
<view class="card">
|
||||
<picker mode="date" :value="form.planExecutionTime" @change="changeDate">
|
||||
@ -46,22 +46,6 @@
|
||||
<view class="counter">{{ (form.taskContent || '').length }}/200</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="block">
|
||||
<view class="block-title">跟进方式</view>
|
||||
<view class="toggle-row">
|
||||
<view class="pill" :class="{ active: form.executeMethod === 'todo' }" @click="form.executeMethod = 'todo'">待办</view>
|
||||
<view class="pill" :class="{ active: form.executeMethod === 'groupMessage' }" @click="form.executeMethod = 'groupMessage'">群发</view>
|
||||
<view class="info" @click="showInfo">i</view>
|
||||
</view>
|
||||
<view v-if="form.executeMethod === 'groupMessage'" class="block">
|
||||
<view class="block-title">发送内容</view>
|
||||
<view class="textarea-box">
|
||||
<textarea v-model="form.sendContent" class="textarea" placeholder="请输入群发内容(mock)" maxlength="500" />
|
||||
<view class="counter">{{ (form.sendContent || '').length }}/500</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="footer">
|
||||
@ -69,15 +53,44 @@
|
||||
<button class="btn primary" @click="save">保存</button>
|
||||
</view>
|
||||
|
||||
<uni-popup ref="infoPopup" type="center">
|
||||
<view class="modal">
|
||||
<view class="modal-title">跟进方式说明</view>
|
||||
<view class="modal-body">
|
||||
<view class="modal-text">待办:生成待办单,需员工手动进行处理</view>
|
||||
<view class="modal-text">群发:生成群发单,员工可批量进行处理(wxapp mock 不接入群发)</view>
|
||||
<uni-popup ref="typePopup" type="bottom" :mask-click="true">
|
||||
<view class="picker-sheet">
|
||||
<view class="picker-title">选择类型</view>
|
||||
<scroll-view scroll-y class="picker-body">
|
||||
<view
|
||||
v-for="t in eventTypeList"
|
||||
:key="t.value"
|
||||
class="picker-item"
|
||||
:class="{ active: form.eventType === t.value }"
|
||||
@click="pickType(t.value)"
|
||||
>
|
||||
<view class="picker-item-text">{{ t.label }}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="picker-actions">
|
||||
<button class="btn plain" @click="closeTypePicker">取消</button>
|
||||
<button class="btn primary" @click="closeTypePicker">确定</button>
|
||||
</view>
|
||||
<view class="modal-actions">
|
||||
<view class="modal-btn save" @click="closeInfo">关闭</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
|
||||
<uni-popup ref="executorPopup" type="bottom" :mask-click="true">
|
||||
<view class="picker-sheet">
|
||||
<view class="picker-title">选择处理人(本团队)</view>
|
||||
<scroll-view scroll-y class="picker-body">
|
||||
<view
|
||||
v-for="m in teamMembers"
|
||||
:key="String(m?.userid || '')"
|
||||
class="picker-item"
|
||||
:class="{ active: form.executorUserId && String(m?.userid || '') === form.executorUserId }"
|
||||
@click="pickExecutor(m)"
|
||||
>
|
||||
<view class="picker-item-text">{{ String(m?.anotherName || m?.name || m?.userid || '') }}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="picker-actions">
|
||||
<button class="btn plain" @click="closeExecutorPicker">取消</button>
|
||||
<button class="btn primary" @click="closeExecutorPicker">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
@ -88,42 +101,44 @@
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import dayjs from 'dayjs';
|
||||
import { ensureSeed, upsertFollowup } from '@/components/archive-detail/mock';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import api from '@/utils/api';
|
||||
import useAccountStore from '@/store/account';
|
||||
import { toast } from '@/utils/widget';
|
||||
import { getTodoEventTypeLabel, getTodoEventTypeOptions } from '@/utils/todo-const';
|
||||
|
||||
const archiveId = ref('');
|
||||
const archiveName = ref('');
|
||||
const customerData = ref({});
|
||||
|
||||
const eventTypeList = [
|
||||
{ label: '回访', value: 'followup' },
|
||||
{ label: '复诊提醒', value: 'revisit' },
|
||||
{ label: '问卷', value: 'questionnaire' },
|
||||
{ label: '其他', value: 'other' },
|
||||
];
|
||||
const eventTypeList = getTodoEventTypeOptions();
|
||||
|
||||
const teamOptions = [
|
||||
{ label: '口腔一科(示例)', value: 'team_1' },
|
||||
{ label: '正畸团队(示例)', value: 'team_2' },
|
||||
];
|
||||
const accountStore = useAccountStore();
|
||||
const { account, doctorInfo } = storeToRefs(accountStore);
|
||||
const { getDoctorInfo } = accountStore;
|
||||
|
||||
const teamMembers = ref([]);
|
||||
|
||||
const form = reactive({
|
||||
planExecutionTime: '',
|
||||
executeTeamId: '',
|
||||
executeTeamName: '',
|
||||
executorName: '',
|
||||
executeTeamId: '', // teamId
|
||||
executeTeamName: '', // teamName
|
||||
executorUserId: '', // userid
|
||||
executorName: '', // anotherName
|
||||
eventType: '',
|
||||
taskContent: '',
|
||||
executeMethod: 'todo', // todo | groupMessage
|
||||
sendContent: '',
|
||||
});
|
||||
|
||||
const eventTypeLabel = computed(() => eventTypeList.find((i) => i.value === form.eventType)?.label || '');
|
||||
const eventTypeLabel = computed(() => getTodoEventTypeLabel(form.eventType));
|
||||
|
||||
onLoad((options) => {
|
||||
resetForm();
|
||||
archiveId.value = options?.archiveId ? String(options.archiveId) : '';
|
||||
const c = uni.getStorageSync('new-followup-customer');
|
||||
if (c && typeof c === 'object') {
|
||||
archiveId.value = archiveId.value || String(c._id || '');
|
||||
archiveName.value = String(c.name || '');
|
||||
customerData.value = c;
|
||||
}
|
||||
if (!archiveId.value) {
|
||||
uni.showToast({ title: '缺少 archiveId', icon: 'none' });
|
||||
@ -131,19 +146,87 @@ onLoad((options) => {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureSeed(archiveId.value, { name: archiveName.value });
|
||||
|
||||
// 使用模板:从 plan-list 选择后写入 select-mamagement-plan
|
||||
const plan = uni.getStorageSync('select-mamagement-plan');
|
||||
if (plan && typeof plan === 'object' && plan.planName) {
|
||||
form.eventType = plan.eventType || 'followup';
|
||||
form.taskContent = plan.taskContent || `执行回访计划:${plan.planName}`;
|
||||
uni.setNavigationBarTitle({ title: '使用模板新增任务' });
|
||||
const cached = uni.getStorageSync('ykt_case_archive_detail');
|
||||
if (cached && typeof cached === 'object' && String(cached?._id || '') === archiveId.value) {
|
||||
customerData.value = cached;
|
||||
archiveName.value = archiveName.value || String(cached.name || '');
|
||||
}
|
||||
|
||||
if (!form.planExecutionTime) form.planExecutionTime = dayjs().add(1, 'day').format('YYYY-MM-DD');
|
||||
|
||||
initDefaultExecutor();
|
||||
});
|
||||
|
||||
function resetForm(keepCustomer = false) {
|
||||
form.planExecutionTime = '';
|
||||
form.executeTeamId = '';
|
||||
form.executeTeamName = '';
|
||||
form.executorUserId = '';
|
||||
form.executorName = '';
|
||||
form.eventType = '';
|
||||
form.taskContent = '';
|
||||
if (!keepCustomer) {
|
||||
archiveId.value = '';
|
||||
archiveName.value = '';
|
||||
customerData.value = {};
|
||||
}
|
||||
teamMembers.value = [];
|
||||
uni.setStorageSync('select-mamagement-plan', '');
|
||||
}
|
||||
|
||||
async function ensureDoctor() {
|
||||
if (doctorInfo.value) return;
|
||||
if (!account.value?.openid) return;
|
||||
try {
|
||||
await getDoctorInfo();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
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 loadTeamMembers(teamId) {
|
||||
const corpId = getCorpId();
|
||||
if (!corpId || !teamId) return;
|
||||
const res = await api('getTeamData', { corpId, teamId });
|
||||
if (!res?.success) {
|
||||
teamMembers.value = [];
|
||||
return;
|
||||
}
|
||||
const t = res?.data && typeof res.data === 'object' ? res.data : {};
|
||||
teamMembers.value = Array.isArray(t.memberList) ? t.memberList : [];
|
||||
}
|
||||
|
||||
async function initDefaultExecutor() {
|
||||
await ensureDoctor();
|
||||
const userId = getUserId();
|
||||
const name = String(doctorInfo.value?.anotherName || doctorInfo.value?.name || '');
|
||||
const currentTeam = uni.getStorageSync('ykt_case_current_team') || {};
|
||||
const teamId = String(currentTeam.teamId || '');
|
||||
const teamName = String(currentTeam.name || '');
|
||||
if (teamId) {
|
||||
form.executeTeamId = teamId;
|
||||
form.executeTeamName = teamName;
|
||||
await loadTeamMembers(teamId);
|
||||
}
|
||||
if (userId) {
|
||||
form.executorUserId = userId;
|
||||
form.executorName = name || '我';
|
||||
}
|
||||
}
|
||||
|
||||
function changeDate(e) {
|
||||
const date = String(e.detail.value || '');
|
||||
if (dayjs().startOf('day').isAfter(dayjs(date))) {
|
||||
@ -154,25 +237,12 @@ function changeDate(e) {
|
||||
}
|
||||
|
||||
function selectType() {
|
||||
uni.showActionSheet({
|
||||
itemList: eventTypeList.map((i) => i.label),
|
||||
success: ({ tapIndex }) => {
|
||||
form.eventType = eventTypeList[tapIndex]?.value || '';
|
||||
},
|
||||
});
|
||||
typePopup.value?.open?.();
|
||||
}
|
||||
|
||||
function selectExecutor() {
|
||||
// wxapp 先用 mock:选择团队 + 固定执行人
|
||||
uni.showActionSheet({
|
||||
itemList: teamOptions.map((i) => i.label),
|
||||
success: ({ tapIndex }) => {
|
||||
const t = teamOptions[tapIndex];
|
||||
form.executeTeamId = t.value;
|
||||
form.executeTeamName = t.label;
|
||||
form.executorName = '李医生';
|
||||
},
|
||||
});
|
||||
if (!teamMembers.value.length) return toast('当前团队暂无可选成员');
|
||||
executorPopup.value?.open?.();
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
@ -183,47 +253,76 @@ function cancel() {
|
||||
});
|
||||
}
|
||||
|
||||
function save() {
|
||||
async function save() {
|
||||
if (!form.planExecutionTime) return uni.showToast({ title: '请选择回访日期', icon: 'none' });
|
||||
if (!form.executorName) return uni.showToast({ title: '请选择处理人', icon: 'none' });
|
||||
if (!form.executeTeamId) return uni.showToast({ title: '请选择处理人', icon: 'none' });
|
||||
if (!form.executorUserId) return uni.showToast({ title: '请选择处理人', icon: 'none' });
|
||||
if (!form.eventType) return uni.showToast({ title: '请选择类型', icon: 'none' });
|
||||
if (!String(form.taskContent || '').trim()) return uni.showToast({ title: '请输入目的', icon: 'none' });
|
||||
if (form.executeMethod === 'groupMessage' && !String(form.sendContent || '').trim()) {
|
||||
return uni.showToast({ title: '请输入发送内容', icon: 'none' });
|
||||
|
||||
await ensureDoctor();
|
||||
const corpId = getCorpId();
|
||||
const userId = getUserId();
|
||||
if (!corpId || !userId) {
|
||||
toast('缺少用户/团队信息,请先完成登录与团队选择');
|
||||
return;
|
||||
}
|
||||
const customer = customerData.value && typeof customerData.value === 'object' ? customerData.value : {};
|
||||
const customerId = String(customer._id || archiveId.value || '');
|
||||
const customerName = String(customer.name || archiveName.value || '');
|
||||
const customerUserId = String(customer.externalUserId || customer.customerUserId || '') || '';
|
||||
|
||||
const plannedExecutionTime = dayjs(form.planExecutionTime).valueOf();
|
||||
upsertFollowup({
|
||||
archiveId: archiveId.value,
|
||||
followup: {
|
||||
plannedExecutionTime,
|
||||
status: 'processing',
|
||||
eventStatusLabel: '待处理',
|
||||
eventType: form.eventType,
|
||||
eventTypeLabel: eventTypeLabel.value || '回访',
|
||||
executeTeamId: form.executeTeamId || 'team_1',
|
||||
executeTeamName: form.executeTeamName || '口腔一科(示例)',
|
||||
executorName: form.executorName,
|
||||
creatorName: '我',
|
||||
taskContent: form.taskContent,
|
||||
result: '',
|
||||
executeMethod: form.executeMethod,
|
||||
sendContent: form.executeMethod === 'groupMessage' ? form.sendContent : '',
|
||||
createTime: Date.now(),
|
||||
},
|
||||
});
|
||||
const params = {
|
||||
corpId,
|
||||
customerId,
|
||||
customerName,
|
||||
customerUserId,
|
||||
executeTeamId: form.executeTeamId,
|
||||
executeTeamName: form.executeTeamName,
|
||||
creatorUserId: userId,
|
||||
userId: form.executorUserId || userId,
|
||||
taskList: [
|
||||
{
|
||||
enableSend: false,
|
||||
eventType: form.eventType,
|
||||
executeMethod: 'todo',
|
||||
executorUserId: form.executorUserId || userId,
|
||||
planExecutionTime: form.planExecutionTime,
|
||||
sendContent: '',
|
||||
taskContent: form.taskContent,
|
||||
taskId: `${Date.now()}_${Math.random().toString(16).slice(2)}`,
|
||||
fileList: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const res = await api('executeManagementPlanTodo', params);
|
||||
if (!res?.success) {
|
||||
toast(res?.message || '保存失败');
|
||||
return;
|
||||
}
|
||||
uni.$emit('archive-detail:followup-changed');
|
||||
uni.showToast({ title: '保存成功', icon: 'success' });
|
||||
toast('保存成功');
|
||||
setTimeout(() => uni.navigateBack(), 300);
|
||||
}
|
||||
|
||||
const infoPopup = ref(null);
|
||||
function showInfo() {
|
||||
infoPopup.value?.open?.();
|
||||
const typePopup = ref(null);
|
||||
function pickType(v) {
|
||||
form.eventType = String(v || '');
|
||||
}
|
||||
function closeInfo() {
|
||||
infoPopup.value?.close?.();
|
||||
function closeTypePicker() {
|
||||
typePopup.value?.close?.();
|
||||
}
|
||||
|
||||
const executorPopup = ref(null);
|
||||
function pickExecutor(m) {
|
||||
const id = String(m?.userid || '');
|
||||
if (!id) return;
|
||||
form.executorUserId = id;
|
||||
form.executorName = String(m?.anotherName || m?.name || m?.userid || '') || '';
|
||||
}
|
||||
function closeExecutorPicker() {
|
||||
executorPopup.value?.close?.();
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -291,6 +390,39 @@ function closeInfo() {
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.picker-sheet {
|
||||
background: #fff;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.picker-title {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
padding: 14px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.picker-body {
|
||||
max-height: 60vh;
|
||||
}
|
||||
.picker-item {
|
||||
padding: 14px;
|
||||
border-bottom: 1px solid #f2f2f2;
|
||||
}
|
||||
.picker-item.active {
|
||||
background: #f2f6ff;
|
||||
}
|
||||
.picker-item-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
.picker-actions {
|
||||
padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
.textarea-box {
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 8px;
|
||||
@ -405,4 +537,3 @@ function closeInfo() {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
400
pages/case/plan-execute.vue
Normal file
400
pages/case/plan-execute.vue
Normal file
@ -0,0 +1,400 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view scroll-y class="scroll">
|
||||
<view class="section header">
|
||||
<view class="header-title">{{ plan.planName || '回访计划' }}</view>
|
||||
</view>
|
||||
<view class="section border-top">
|
||||
<view class="sub">应用范围: {{ plan.planDetail || '无' }}</view>
|
||||
</view>
|
||||
|
||||
<view class="section mt-20">
|
||||
<picker mode="date" :value="form.planExecutionTime" @change="changeDate">
|
||||
<view class="row clickable">
|
||||
<view class="left">
|
||||
<view class="label">开始时间</view>
|
||||
<view class="req">*</view>
|
||||
</view>
|
||||
<view class="right">
|
||||
<view class="value" :class="{ muted: !form.planExecutionTime }">{{ form.planExecutionTime || '请选择开始时间' }}</view>
|
||||
<uni-icons type="arrowright" size="16" color="#999" />
|
||||
</view>
|
||||
</view>
|
||||
</picker>
|
||||
|
||||
<view class="row clickable" @click="openExecutor">
|
||||
<view class="left">
|
||||
<view class="label">处理人</view>
|
||||
<view class="req">*</view>
|
||||
</view>
|
||||
<view class="right">
|
||||
<view class="value" :class="{ muted: !form.executorName }">{{ form.executorName || '请选择处理人' }}</view>
|
||||
<uni-icons type="arrowright" size="16" color="#999" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section mt-20">
|
||||
<view v-if="taskList.length === 0" class="empty">暂无任务</view>
|
||||
<plan-node-list v-else :taskList="taskList" :planExecutionTime="form.planExecutionTime" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="footer">
|
||||
<button class="btn plain" @click="cancel">取消</button>
|
||||
<button class="btn primary" @click="confirm">确定</button>
|
||||
</view>
|
||||
|
||||
<uni-popup ref="executorPopup" type="bottom" :mask-click="true">
|
||||
<view class="picker-sheet">
|
||||
<view class="picker-title">选择处理人(本团队)</view>
|
||||
<scroll-view scroll-y class="picker-body">
|
||||
<view
|
||||
v-for="m in teamMembers"
|
||||
:key="String(m?.userid || '')"
|
||||
class="picker-item"
|
||||
:class="{ active: form.executorUserId && String(m?.userid || '') === form.executorUserId }"
|
||||
@click="pickExecutor(m)"
|
||||
>
|
||||
<view class="picker-item-text">{{ String(m?.anotherName || m?.name || m?.userid || '') }}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="picker-actions">
|
||||
<button class="btn plain" @click="closeExecutor">取消</button>
|
||||
<button class="btn primary" @click="closeExecutor">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import dayjs from 'dayjs';
|
||||
import api from '@/utils/api';
|
||||
import useAccountStore from '@/store/account';
|
||||
import { toast } from '@/utils/widget';
|
||||
import planNodeList from '@/components/manage-plan/plan-node-list.vue';
|
||||
|
||||
const archiveId = ref('');
|
||||
const plan = ref({});
|
||||
const customer = ref({});
|
||||
const teamMembers = ref([]);
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const { account, doctorInfo } = storeToRefs(accountStore);
|
||||
const { getDoctorInfo } = accountStore;
|
||||
|
||||
const form = reactive({
|
||||
planExecutionTime: '',
|
||||
executorUserId: '',
|
||||
executorName: '',
|
||||
});
|
||||
|
||||
const taskList = computed(() => {
|
||||
const list = Array.isArray(plan.value?.taskList) ? plan.value.taskList : [];
|
||||
return list.map((t) => (t && typeof t === 'object' ? { ...t } : {}));
|
||||
});
|
||||
|
||||
const previewTasks = computed(() => {
|
||||
const base = form.planExecutionTime && dayjs(form.planExecutionTime).isValid() ? dayjs(form.planExecutionTime) : null;
|
||||
const list = taskList.value;
|
||||
const next = list
|
||||
.map((t) => {
|
||||
if (!base) return { ...t, planExecutionTime: '' };
|
||||
const taskTime = typeof t.taskTime === 'number' ? t.taskTime : 0;
|
||||
const timeType = ['day', 'week', 'month', 'year'].includes(String(t.timeType || '')) ? String(t.timeType) : 'day';
|
||||
return { ...t, planExecutionTime: base.add(taskTime, timeType).format('YYYY-MM-DD') };
|
||||
})
|
||||
.sort((a, b) => String(a.planExecutionTime || '').localeCompare(String(b.planExecutionTime || '')));
|
||||
return next;
|
||||
});
|
||||
|
||||
onLoad((options) => {
|
||||
archiveId.value = options?.archiveId ? String(options.archiveId) : '';
|
||||
const rawPlan = uni.getStorageSync('select-mamagement-plan');
|
||||
plan.value = rawPlan && typeof rawPlan === 'object' ? rawPlan : {};
|
||||
const rawCustomer = uni.getStorageSync('new-followup-plan-customer');
|
||||
customer.value = rawCustomer && typeof rawCustomer === 'object' ? rawCustomer : {};
|
||||
const cached = uni.getStorageSync('ykt_case_archive_detail');
|
||||
if (cached && typeof cached === 'object' && String(cached?._id || '') === String(customer.value?._id || archiveId.value || '')) {
|
||||
customer.value = { ...cached, ...customer.value };
|
||||
}
|
||||
if (!form.planExecutionTime) form.planExecutionTime = dayjs().format('YYYY-MM-DD');
|
||||
initTeamMembers();
|
||||
});
|
||||
|
||||
async function ensureDoctor() {
|
||||
if (doctorInfo.value) return;
|
||||
if (!account.value?.openid) return;
|
||||
try {
|
||||
await getDoctorInfo();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
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 || '') || '';
|
||||
}
|
||||
|
||||
function getCurrentTeam() {
|
||||
const t = uni.getStorageSync('ykt_case_current_team') || {};
|
||||
return { teamId: String(t.teamId || ''), name: String(t.name || '') };
|
||||
}
|
||||
|
||||
async function initTeamMembers() {
|
||||
await ensureDoctor();
|
||||
const corpId = getCorpId();
|
||||
const { teamId } = getCurrentTeam();
|
||||
if (!corpId || !teamId) return;
|
||||
const res = await api('getTeamData', { corpId, teamId });
|
||||
if (!res?.success) {
|
||||
teamMembers.value = [];
|
||||
return;
|
||||
}
|
||||
const t = res?.data && typeof res.data === 'object' ? res.data : {};
|
||||
teamMembers.value = Array.isArray(t.memberList) ? t.memberList : [];
|
||||
|
||||
const userId = getUserId();
|
||||
const me = teamMembers.value.find((m) => String(m?.userid || '') === userId);
|
||||
if (me) {
|
||||
form.executorUserId = userId;
|
||||
form.executorName = String(me?.anotherName || me?.name || me?.userid || '') || '';
|
||||
}
|
||||
}
|
||||
|
||||
function changeDate(e) {
|
||||
const date = String(e.detail.value || '');
|
||||
if (dayjs().startOf('day').isAfter(dayjs(date))) {
|
||||
toast('请选择有效的开始时间');
|
||||
return;
|
||||
}
|
||||
form.planExecutionTime = date;
|
||||
}
|
||||
|
||||
const executorPopup = ref(null);
|
||||
function openExecutor() {
|
||||
if (!teamMembers.value.length) return toast('当前团队暂无可选成员');
|
||||
executorPopup.value?.open?.();
|
||||
}
|
||||
function pickExecutor(m) {
|
||||
const id = String(m?.userid || '');
|
||||
if (!id) return;
|
||||
form.executorUserId = id;
|
||||
form.executorName = String(m?.anotherName || m?.name || m?.userid || '') || '';
|
||||
}
|
||||
function closeExecutor() {
|
||||
executorPopup.value?.close?.();
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
uni.navigateBack();
|
||||
}
|
||||
|
||||
async function confirm() {
|
||||
if (!form.planExecutionTime) return toast('请选择开始时间');
|
||||
if (!form.executorUserId) return toast('请选择处理人');
|
||||
const tasks = previewTasks.value;
|
||||
if (!tasks.length) return toast('任务不能为空');
|
||||
|
||||
await ensureDoctor();
|
||||
const corpId = getCorpId();
|
||||
const creatorUserId = getUserId();
|
||||
const { teamId: executeTeamId, name: executeTeamName } = getCurrentTeam();
|
||||
if (!corpId || !creatorUserId || !executeTeamId) return toast('缺少用户/团队信息');
|
||||
|
||||
const c = customer.value && typeof customer.value === 'object' ? customer.value : {};
|
||||
const customerId = String(c._id || archiveId.value || '');
|
||||
const cached = uni.getStorageSync('ykt_case_archive_detail');
|
||||
const cachedName = cached && typeof cached === 'object' ? String(cached.name || '') : '';
|
||||
const customerName = String(c.name || '') || cachedName;
|
||||
const customerUserId = String(c.externalUserId || c.customerUserId || '') || '';
|
||||
|
||||
const payload = {
|
||||
corpId,
|
||||
customerId,
|
||||
customerName,
|
||||
customerUserId,
|
||||
executeTeamId,
|
||||
executeTeamName,
|
||||
planId: String(plan.value?.planId || ''),
|
||||
planName: String(plan.value?.planName || ''),
|
||||
userId: creatorUserId,
|
||||
creatorUserId,
|
||||
taskList: tasks.map((t) => ({
|
||||
taskId: String(t.taskId || `${Date.now()}_${Math.random().toString(16).slice(2)}`),
|
||||
eventType: String(t.eventType || ''),
|
||||
taskContent: String(t.taskContent || ''),
|
||||
planExecutionTime: String(t.planExecutionTime || ''),
|
||||
executorUserId: form.executorUserId,
|
||||
executeMethod: String(t.executeMethod || 'todo'),
|
||||
enableSend: typeof t.enableSend === 'boolean' ? t.enableSend : false,
|
||||
sendContent: String(t.sendContent || ''),
|
||||
fileList: Array.isArray(t.fileList) ? t.fileList : [],
|
||||
})),
|
||||
};
|
||||
|
||||
const res = await api('executeManagementPlanTodo', payload);
|
||||
if (!res?.success) return toast(res?.message || '执行失败');
|
||||
toast('执行成功');
|
||||
uni.$emit('archive-detail:followup-changed');
|
||||
uni.setStorageSync('select-mamagement-plan', '');
|
||||
uni.setStorageSync('preview-mamagement-plan', '');
|
||||
uni.navigateBack({ delta: 2 });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f6f7f8;
|
||||
}
|
||||
.scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
.section {
|
||||
background: #fff;
|
||||
padding: 24rpx 30rpx;
|
||||
}
|
||||
.section.header {
|
||||
padding: 24rpx 30rpx;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
.section.border-top {
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
.mt-20 {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
.header-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
.sub {
|
||||
font-size: 28rpx;
|
||||
color: #6b7280;
|
||||
line-height: 48rpx;
|
||||
}
|
||||
.empty {
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
color: #9ca3af;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28rpx 0;
|
||||
border-bottom: 1px solid #eef0f2;
|
||||
}
|
||||
.row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
.req {
|
||||
margin-left: 8rpx;
|
||||
font-size: 28rpx;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.value {
|
||||
font-size: 28rpx;
|
||||
color: #111827;
|
||||
max-width: 420rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.value.muted {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background: #fff;
|
||||
padding: 24rpx 30rpx calc(24rpx + env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
.btn::after {
|
||||
border: none;
|
||||
}
|
||||
.btn.plain {
|
||||
background: #fff;
|
||||
color: #4f6ef7;
|
||||
border: 1px solid #4f6ef7;
|
||||
}
|
||||
.btn.primary {
|
||||
background: #4f6ef7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.picker-sheet {
|
||||
background: #fff;
|
||||
border-top-left-radius: 20rpx;
|
||||
border-top-right-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.picker-title {
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
padding: 28rpx;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.picker-body {
|
||||
max-height: 60vh;
|
||||
}
|
||||
.picker-item {
|
||||
padding: 28rpx;
|
||||
border-bottom: 1px solid #f2f2f2;
|
||||
}
|
||||
.picker-item.active {
|
||||
background: #f2f6ff;
|
||||
}
|
||||
.picker-item-text {
|
||||
font-size: 28rpx;
|
||||
color: #111827;
|
||||
}
|
||||
.picker-actions {
|
||||
padding: 24rpx 30rpx calc(24rpx + env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<!-- Mobile 来源: ykt-management-mobile/src/pages/manage-plan/plan-list/plan-list.vue(wxapp mock:仅选择模板并回到 new-followup) -->
|
||||
<!-- Mobile 来源: ykt-management-mobile/src/pages/manage-plan/plan-list/plan-list.vue -->
|
||||
<view class="page">
|
||||
<view v-if="list.length === 0" class="empty">
|
||||
<view v-if="loading" class="loading">加载中...</view>
|
||||
<view v-else-if="list.length === 0" class="empty">
|
||||
<image class="empty-img" src="/static/empty.svg" mode="aspectFit" />
|
||||
<view class="empty-text">暂无回访计划</view>
|
||||
</view>
|
||||
@ -26,43 +27,117 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
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';
|
||||
|
||||
const archiveId = ref('');
|
||||
const loading = ref(false);
|
||||
|
||||
const list = ref([
|
||||
{
|
||||
id: 'p1',
|
||||
planName: '复诊提醒模板',
|
||||
planType: 'corp',
|
||||
planDetail: '适用于复诊提醒人群',
|
||||
eventType: 'revisit',
|
||||
taskContent: '请于本周内完成复诊预约与提醒。',
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
planName: '随访回访模板',
|
||||
planType: 'team',
|
||||
planDetail: '适用于普通随访',
|
||||
eventType: 'followup',
|
||||
taskContent: '请电话回访患者,确认恢复情况并记录结果。',
|
||||
},
|
||||
]);
|
||||
const list = ref([]);
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const { account, doctorInfo } = storeToRefs(accountStore);
|
||||
const { getDoctorInfo } = accountStore;
|
||||
|
||||
onLoad((options) => {
|
||||
archiveId.value = options?.archiveId ? String(options.archiveId) : '';
|
||||
loadList();
|
||||
});
|
||||
|
||||
async function ensureDoctor() {
|
||||
if (doctorInfo.value) return;
|
||||
if (!account.value?.openid) return;
|
||||
try {
|
||||
await getDoctorInfo();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
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 || '') || '';
|
||||
}
|
||||
|
||||
function getTeamId() {
|
||||
const t = uni.getStorageSync('ykt_case_current_team') || {};
|
||||
return String(t.teamId || '') || '';
|
||||
}
|
||||
|
||||
function normalizePlan(raw) {
|
||||
if (!raw || typeof raw !== 'object') return null;
|
||||
const id = String(raw.planId || raw._id || '');
|
||||
if (!id) return null;
|
||||
const taskList = Array.isArray(raw.taskList) ? raw.taskList : [];
|
||||
const first = taskList[0] && typeof taskList[0] === 'object' ? taskList[0] : {};
|
||||
const eventType = String(first.eventType || '');
|
||||
const taskContent = String(first.taskContent || '');
|
||||
return {
|
||||
...raw,
|
||||
id,
|
||||
planName: String(raw.planName || ''),
|
||||
planType: String(raw.planType || ''),
|
||||
planDetail: String(raw.planDetail || ''),
|
||||
firstEventType: eventType,
|
||||
firstEventTypeLabel: eventType ? getTodoEventTypeLabel(eventType) : '',
|
||||
taskContent,
|
||||
taskCount: taskList.length,
|
||||
taskList,
|
||||
};
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
await ensureDoctor();
|
||||
const corpId = getCorpId();
|
||||
const userId = getUserId();
|
||||
const teamId = getTeamId();
|
||||
if (!corpId || !userId || !teamId) {
|
||||
list.value = [];
|
||||
toast('缺少用户/团队信息');
|
||||
return;
|
||||
}
|
||||
const res = await api('getManagementPlan', {
|
||||
corpId,
|
||||
userId,
|
||||
planType: 'team',
|
||||
teamId,
|
||||
page: 1,
|
||||
pageSize: 999,
|
||||
});
|
||||
if (!res?.success) {
|
||||
list.value = [];
|
||||
toast(res?.message || '获取回访计划失败');
|
||||
return;
|
||||
}
|
||||
const arr = Array.isArray(res.data) ? res.data : [];
|
||||
list.value = arr.filter((p) => p && p.planStatus === true).map(normalizePlan).filter(Boolean);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function select(plan) {
|
||||
uni.setStorageSync('select-mamagement-plan', plan);
|
||||
uni.navigateTo({ url: `/pages/case/new-followup?archiveId=${encodeURIComponent(archiveId.value)}&fromPlan=1` });
|
||||
uni.navigateTo({ url: `/pages/case/plan-execute?archiveId=${encodeURIComponent(archiveId.value)}` });
|
||||
}
|
||||
|
||||
function preview(plan) {
|
||||
uni.showModal({
|
||||
title: plan.planName || '回访计划',
|
||||
content: plan.taskContent || plan.planDetail || '',
|
||||
showCancel: false,
|
||||
});
|
||||
uni.setStorageSync('preview-mamagement-plan', plan);
|
||||
uni.navigateTo({ url: `/pages/case/plan-preview?archiveId=${encodeURIComponent(archiveId.value)}` });
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -74,6 +149,11 @@ function preview(plan) {
|
||||
.scroll {
|
||||
height: 100vh;
|
||||
}
|
||||
.loading {
|
||||
padding: 16px;
|
||||
font-size: 13px;
|
||||
color: #9aa0a6;
|
||||
}
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -147,4 +227,3 @@ function preview(plan) {
|
||||
color: #9aa0a6;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
104
pages/case/plan-preview.vue
Normal file
104
pages/case/plan-preview.vue
Normal file
@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view scroll-y class="scroll">
|
||||
<view class="section header">
|
||||
<view class="header-title">{{ plan.planName || '回访计划' }}</view>
|
||||
</view>
|
||||
<view class="section border-top">
|
||||
<view class="sub">应用范围: {{ plan.planDetail || '无' }}</view>
|
||||
</view>
|
||||
|
||||
<view class="section mt-20">
|
||||
<view v-if="tasks.length === 0" class="empty">暂无任务</view>
|
||||
<plan-node-list v-else class="node-list" :taskList="tasks" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="footer">
|
||||
<button class="footer-btn" @click="back">返回</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import planNodeList from '@/components/manage-plan/plan-node-list.vue';
|
||||
|
||||
const plan = ref({});
|
||||
|
||||
const tasks = computed(() => {
|
||||
const list = Array.isArray(plan.value?.taskList) ? plan.value.taskList : [];
|
||||
return list.map((i) => (i && typeof i === 'object' ? i : {}));
|
||||
});
|
||||
|
||||
onLoad(() => {
|
||||
const p = uni.getStorageSync('preview-mamagement-plan');
|
||||
if (p && typeof p === 'object') plan.value = p;
|
||||
});
|
||||
|
||||
function back() {
|
||||
uni.navigateBack();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f6f7f8;
|
||||
}
|
||||
.scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
.section {
|
||||
background: #fff;
|
||||
padding: 24rpx 30rpx;
|
||||
}
|
||||
.section.header {
|
||||
padding: 24rpx 30rpx;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
.section.border-top {
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
.mt-20 {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
.header-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
.empty {
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
color: #9ca3af;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.sub {
|
||||
font-size: 28rpx;
|
||||
color: #6b7280;
|
||||
line-height: 48rpx;
|
||||
}
|
||||
.footer {
|
||||
background: #fff;
|
||||
padding: 30rpx 30rpx calc(30rpx + env(safe-area-inset-bottom));
|
||||
box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.footer-btn {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 28rpx;
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
.footer-btn::after {
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
@ -14,7 +14,8 @@ export default [
|
||||
},
|
||||
{
|
||||
path: 'pages/message/index',
|
||||
meta: { title: '消息', login: false },
|
||||
meta: { title: '聊天', login: false },
|
||||
style: { enablePullDownRefresh: false },
|
||||
},
|
||||
{
|
||||
path: 'pages/case/case',
|
||||
@ -84,6 +85,14 @@ export default [
|
||||
path: 'pages/case/plan-list',
|
||||
meta: { title: '回访计划', login: false },
|
||||
},
|
||||
{
|
||||
path: 'pages/case/plan-preview',
|
||||
meta: { title: '回访计划详情', login: false },
|
||||
},
|
||||
{
|
||||
path: 'pages/case/plan-execute',
|
||||
meta: { title: '执行回访计划', login: false },
|
||||
},
|
||||
{
|
||||
path: 'pages/work/work',
|
||||
meta: { title: '工作台', login: false }
|
||||
@ -97,4 +106,3 @@ export default [
|
||||
meta: { title: '选择科室', login: false }
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@ -1,6 +1,13 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function stripJsonComments(content) {
|
||||
// Remove /* */ comments first, then // comments.
|
||||
return String(content || '')
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.replace(/^\s*\/\/.*$/gm, '');
|
||||
}
|
||||
|
||||
// 读取 routes 配置并转换为 pages.json
|
||||
function generatePagesJson() {
|
||||
// 读取 routes/index.js
|
||||
@ -41,7 +48,7 @@ function generatePagesJson() {
|
||||
|
||||
if (fs.existsSync(pagesJsonPath)) {
|
||||
const content = fs.readFileSync(pagesJsonPath, 'utf-8');
|
||||
pagesJson = JSON.parse(content);
|
||||
pagesJson = JSON.parse(stripJsonComments(content));
|
||||
}
|
||||
|
||||
// 转换 routes 为 pages 格式
|
||||
|
||||
13
utils/api.js
13
utils/api.js
@ -44,6 +44,19 @@ const urlsConfig = {
|
||||
getUserSig: 'getUserSig',
|
||||
sendSystemMessage: "sendSystemMessage",
|
||||
getChatRecordsByGroupId: "getChatRecordsByGroupId"
|
||||
},
|
||||
todo: {
|
||||
getCustomerTodos: 'getCustomerTodos',
|
||||
getTodoById: 'getTodoById',
|
||||
setTodoStatus: 'setTodoStatus',
|
||||
updateTaskTodoResult: 'updateTaskTodoResult',
|
||||
updateEvent: 'updateEvent',
|
||||
removeTodo: 'removeTodo',
|
||||
createEvents: 'createEvents',
|
||||
executeManagementPlanTodo: 'executeManagementPlanTodo',
|
||||
getManagementPlan: 'getManagementPlan',
|
||||
getManagementPlanById: 'getManagementPlanById',
|
||||
getNextFollowUpTime: 'getNextFollowUpTime',
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
30
utils/todo-const.js
Normal file
30
utils/todo-const.js
Normal file
@ -0,0 +1,30 @@
|
||||
export const TODO_EVENT_TYPE_LABELS = {
|
||||
followUpNoShow: '未到院回访',
|
||||
followUpNoDeal: '未成交回访',
|
||||
followUp: '诊后回访',
|
||||
followUpPostSurgery: '术后回访',
|
||||
followUpPostTreatment: '治疗后回访',
|
||||
appointmentReminder: '就诊提醒',
|
||||
followUpReminder: '复诊提醒',
|
||||
medicationReminder: '用药提醒',
|
||||
serviceSummary: '咨询服务',
|
||||
eventNotification: '活动通知',
|
||||
ContentReminder: '宣教发送',
|
||||
questionnaire: '问卷调查',
|
||||
followUpComplaint: '投诉回访',
|
||||
followUpActivity: '活动回访',
|
||||
other: '其他',
|
||||
};
|
||||
|
||||
export function getTodoEventTypeLabel(value) {
|
||||
const v = String(value || '');
|
||||
return TODO_EVENT_TYPE_LABELS[v] || v || '其他';
|
||||
}
|
||||
|
||||
export function getTodoEventTypeOptions() {
|
||||
return Object.keys(TODO_EVENT_TYPE_LABELS).map((key) => ({
|
||||
value: key,
|
||||
label: TODO_EVENT_TYPE_LABELS[key],
|
||||
}));
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user