feat:回访接口接入
This commit is contained in:
parent
da7397ce7e
commit
70d0f5e496
@ -67,6 +67,21 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<scroll-view scroll-y class="popup-body">
|
<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">
|
||||||
<view class="section-title">任务类型</view>
|
<view class="section-title">任务类型</view>
|
||||||
<view class="chip-wrap">
|
<view class="chip-wrap">
|
||||||
@ -119,7 +134,12 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
|
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({
|
const props = defineProps({
|
||||||
data: { type: Object, default: () => ({}) },
|
data: { type: Object, default: () => ({}) },
|
||||||
@ -128,6 +148,10 @@ const props = defineProps({
|
|||||||
floatingBottom: { type: Number, default: 16 },
|
floatingBottom: { type: Number, default: 16 },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const accountStore = useAccountStore();
|
||||||
|
const { account, doctorInfo } = storeToRefs(accountStore);
|
||||||
|
const { getDoctorInfo } = accountStore;
|
||||||
|
|
||||||
const statusTabs = [
|
const statusTabs = [
|
||||||
{ label: '全部', value: 'all' },
|
{ label: '全部', value: 'all' },
|
||||||
{ label: '待处理', value: 'processing' },
|
{ label: '待处理', value: 'processing' },
|
||||||
@ -137,18 +161,9 @@ const statusTabs = [
|
|||||||
{ label: '已过期', value: 'expired' },
|
{ label: '已过期', value: 'expired' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const typeOptions = [
|
const typeOptions = [{ label: '全部', value: 'all' }, ...getTodoEventTypeOptions()];
|
||||||
{ label: '回访', value: 'followup' },
|
|
||||||
{ label: '复诊提醒', value: 'revisit' },
|
|
||||||
{ label: '问卷', value: 'questionnaire' },
|
|
||||||
{ label: '其他', value: 'other' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const teamOptions = [
|
const teamOptions = ref([{ label: '全部', value: 'ALL' }]);
|
||||||
{ label: '全部', value: 'ALL' },
|
|
||||||
{ label: '口腔一科(示例)', value: 'team_1' },
|
|
||||||
{ label: '正畸团队(示例)', value: 'team_2' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const query = reactive({
|
const query = reactive({
|
||||||
isMy: false,
|
isMy: false,
|
||||||
@ -166,6 +181,8 @@ const pageSize = 10;
|
|||||||
const pages = ref(1);
|
const pages = ref(1);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const userNameMap = ref({});
|
||||||
|
|
||||||
const moreStatus = computed(() => {
|
const moreStatus = computed(() => {
|
||||||
if (loading.value) return 'loading';
|
if (loading.value) return 'loading';
|
||||||
return page.value <= pages.value ? 'more' : 'no-more';
|
return page.value <= pages.value ? 'more' : 'no-more';
|
||||||
@ -179,9 +196,94 @@ const loadMoreText = {
|
|||||||
const filtered = ref(false);
|
const filtered = ref(false);
|
||||||
const typeSelectedMap = computed(() => {
|
const typeSelectedMap = computed(() => {
|
||||||
const s = new Set(query.eventTypes || []);
|
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() {
|
function resetList() {
|
||||||
page.value = 1;
|
page.value = 1;
|
||||||
pages.value = 1;
|
pages.value = 1;
|
||||||
@ -189,26 +291,47 @@ function resetList() {
|
|||||||
getMore();
|
getMore();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMore() {
|
async function getMore() {
|
||||||
if (!props.archiveId) return;
|
if (!props.archiveId) return;
|
||||||
if (loading.value) return;
|
if (loading.value) return;
|
||||||
if (page.value > pages.value) return;
|
if (page.value > pages.value) return;
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const { list: arr, total: t, pages: p } = queryFollowups({
|
await ensureDoctor();
|
||||||
archiveId: props.archiveId,
|
|
||||||
|
const corpId = getCorpId();
|
||||||
|
const userId = getUserId();
|
||||||
|
if (!corpId) {
|
||||||
|
toast('缺少 corpId,请先完成登录/团队选择');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
corpId,
|
||||||
|
customerId: String(props.archiveId),
|
||||||
page: page.value,
|
page: page.value,
|
||||||
pageSize,
|
pageSize,
|
||||||
status: query.status,
|
};
|
||||||
isMy: query.isMy,
|
|
||||||
eventTypes: query.eventTypes,
|
if (query.status !== 'all') params.statusList = [query.status];
|
||||||
teamId: query.teamId,
|
if (query.isMy) params.executorUserId = userId;
|
||||||
planRange: query.planRange,
|
if (Array.isArray(query.eventTypes) && query.eventTypes.length) params.eventType = query.eventTypes;
|
||||||
});
|
if (query.teamId && query.teamId !== 'ALL') params.teamId = query.teamId;
|
||||||
total.value = t;
|
if (query.planRange?.[0]) params.startDate = query.planRange[0];
|
||||||
pages.value = p;
|
if (query.planRange?.[1]) params.endDate = query.planRange[1];
|
||||||
list.value = page.value === 1 ? arr : [...list.value, ...arr];
|
|
||||||
|
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;
|
page.value += 1;
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@ -224,6 +347,26 @@ function toggleStatus(v) {
|
|||||||
resetList();
|
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() {
|
function add() {
|
||||||
uni.showActionSheet({
|
uni.showActionSheet({
|
||||||
itemList: ['+新增任务', '+使用模板', '+回访记录'],
|
itemList: ['+新增任务', '+使用模板', '+回访记录'],
|
||||||
@ -250,7 +393,7 @@ function toDetail(todo) {
|
|||||||
// ---- filter popup ----
|
// ---- filter popup ----
|
||||||
const filterPopupRef = ref(null);
|
const filterPopupRef = ref(null);
|
||||||
const state = ref(null);
|
const state = ref(null);
|
||||||
const teamPicked = ref(teamOptions[0]);
|
const teamPicked = ref(teamOptions.value[0]);
|
||||||
const planRange = ref(['', '']);
|
const planRange = ref(['', '']);
|
||||||
|
|
||||||
function openFilter() {
|
function openFilter() {
|
||||||
@ -260,7 +403,7 @@ function openFilter() {
|
|||||||
range: [...planRange.value],
|
range: [...planRange.value],
|
||||||
};
|
};
|
||||||
planRange.value = [...(query.planRange || ['', ''])];
|
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?.();
|
filterPopupRef.value?.open?.();
|
||||||
}
|
}
|
||||||
function closeFilter(revert) {
|
function closeFilter(revert) {
|
||||||
@ -273,6 +416,10 @@ function closeFilter(revert) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleType(v) {
|
function toggleType(v) {
|
||||||
|
if (v === 'all') {
|
||||||
|
query.eventTypes = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
const set = new Set(query.eventTypes || []);
|
const set = new Set(query.eventTypes || []);
|
||||||
if (set.has(v)) set.delete(v);
|
if (set.has(v)) set.delete(v);
|
||||||
else set.add(v);
|
else set.add(v);
|
||||||
@ -280,7 +427,7 @@ function toggleType(v) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pickTeam(e) {
|
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;
|
query.teamId = teamPicked.value.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,7 +447,7 @@ function clearPlanRange() {
|
|||||||
function resetFilter() {
|
function resetFilter() {
|
||||||
query.eventTypes = [];
|
query.eventTypes = [];
|
||||||
query.teamId = 'ALL';
|
query.teamId = 'ALL';
|
||||||
teamPicked.value = teamOptions[0];
|
teamPicked.value = teamOptions.value[0];
|
||||||
clearPlanRange();
|
clearPlanRange();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -311,7 +458,10 @@ function confirmFilter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
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();
|
resetList();
|
||||||
uni.$on('archive-detail:followup-changed', 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"
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// "path": "pages/message/index",
|
|
||||||
// "style": {
|
|
||||||
// "navigationBarTitleText": "消息"
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
"path": "pages/message/index",
|
"path": "pages/message/index",
|
||||||
"style": {
|
"style": {
|
||||||
@ -134,6 +128,18 @@
|
|||||||
"navigationBarTitleText": "回访计划"
|
"navigationBarTitleText": "回访计划"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/case/plan-preview",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "回访计划详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/case/plan-execute",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "执行回访计划"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/work/work",
|
"path": "pages/work/work",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
@ -1,51 +1,87 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page">
|
<view class="page">
|
||||||
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/followup-detail/followup-detail.vue(简化移植:去除微信会话/员工选择/接口) -->
|
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/followup-detail/followup-detail.vue -->
|
||||||
<view class="card">
|
<scroll-view scroll-y class="scroll">
|
||||||
<view class="title-row">
|
<view class="section">
|
||||||
<view class="title">{{ currentType.label }}</view>
|
<view class="head-row">
|
||||||
<view class="tag" :class="`st-${currentStatus.value}`">{{ currentStatus.label }}</view>
|
<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>
|
||||||
<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 v-if="todo?.sendContent" class="section border-top">
|
||||||
<view class="section-title">回访方式</view>
|
<view class="sub-label">发送内容 :</view>
|
||||||
<view class="method-row" @click="toggleMethod('phone')">
|
<view class="sub-content">{{ todo.sendContent }}</view>
|
||||||
<image class="radio" :src="`/static/circle${todoMethod === 'phone' ? 'd' : ''}.svg`" />
|
<view v-for="(f, idx) in showFileList" :key="idx" class="file-line">
|
||||||
<view class="method-label">电话</view>
|
<text class="file-type">{{ f.typeStr }}</text>
|
||||||
<view class="method-input" @click.stop="pickPhone">
|
<text class="file-name">{{ f.name }}</text>
|
||||||
<view class="method-value" :class="{ muted: !phone }">{{ phone || '选择号码' }}</view>
|
|
||||||
<uni-icons type="arrowright" size="16" color="#999" />
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="method-row" @click="toggleMethod('wechat')">
|
|
||||||
<image class="radio" :src="`/static/circle${todoMethod === 'wechat' ? 'd' : ''}.svg`" />
|
<view class="section border-top kv">
|
||||||
<view class="method-label">微信</view>
|
<view class="kv-left">计划执行时间:{{ planDate || '--' }}</view>
|
||||||
<view class="method-note">(mock:不接入会话)</view>
|
<view class="kv-right">{{ executorDisplay || '--' }}</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
<view class="section border-top kv">
|
||||||
|
<view class="kv-left">客户:</view>
|
||||||
<view class="card">
|
<view class="kv-right link">{{ customerDisplay || '--' }}</view>
|
||||||
<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>
|
</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>
|
||||||
|
|
||||||
<view class="footer">
|
<view v-if="taskId && canRemove" class="delete-fab" @click="remove">
|
||||||
<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">
|
|
||||||
<uni-icons type="trash" size="22" color="#ff4d4f" />
|
<uni-icons type="trash" size="22" color="#ff4d4f" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@ -55,36 +91,90 @@
|
|||||||
import { computed, reactive, ref } from 'vue';
|
import { computed, reactive, ref } from 'vue';
|
||||||
import { onLoad } from '@dcloudio/uni-app';
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
import dayjs from 'dayjs';
|
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 archiveId = ref('');
|
||||||
const mode = ref('add');
|
const mode = ref('add');
|
||||||
const taskId = ref('');
|
const taskId = ref('');
|
||||||
|
|
||||||
const typeOptions = [
|
const todo = ref(null);
|
||||||
{ label: '回访', value: 'followup' },
|
const currentType = computed(() => ({ label: getTodoEventTypeLabel(todo.value?.eventType), value: String(todo.value?.eventType || '') }));
|
||||||
{ label: '复诊提醒', value: 'revisit' },
|
const customer = ref(null);
|
||||||
{ label: '问卷', value: 'questionnaire' },
|
const userNameMap = ref({});
|
||||||
{ label: '其他', value: 'other' },
|
|
||||||
];
|
|
||||||
const currentType = ref(typeOptions[0]);
|
|
||||||
|
|
||||||
const statusOptions = [
|
function statusLabelFromStatus(status) {
|
||||||
{ label: '待处理', value: 'processing' },
|
const map = {
|
||||||
{ label: '未开始', value: 'notStart' },
|
processing: '待处理',
|
||||||
{ label: '已完成', value: 'treated' },
|
notStart: '未开始',
|
||||||
{ label: '已取消', value: 'cancelled' },
|
treated: '已完成',
|
||||||
{ label: '已过期', value: 'expired' },
|
cancelled: '已取消',
|
||||||
];
|
expired: '已过期',
|
||||||
const currentStatus = ref(statusOptions[0]);
|
};
|
||||||
|
return map[status] || '未知';
|
||||||
|
}
|
||||||
|
|
||||||
const teamOptions = [
|
function getStatus(t) {
|
||||||
{ label: '口腔一科(示例)', value: 'team_1' },
|
const endOfToday = dayjs().endOf('day').valueOf();
|
||||||
{ label: '正畸团队(示例)', value: 'team_2' },
|
const startOfToday = dayjs().startOf('day').valueOf();
|
||||||
];
|
const plannedExecutionTime = Number(t?.plannedExecutionTime || 0) || 0;
|
||||||
const currentTeam = ref(teamOptions[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({
|
const form = reactive({
|
||||||
executorName: '',
|
executorName: '',
|
||||||
taskContent: '',
|
taskContent: '',
|
||||||
@ -94,105 +184,166 @@ const form = reactive({
|
|||||||
const todoMethod = ref('phone'); // phone | wechat
|
const todoMethod = ref('phone'); // phone | wechat
|
||||||
const phone = ref('');
|
const phone = ref('');
|
||||||
const mobiles = computed(() => {
|
const mobiles = computed(() => {
|
||||||
const m = String(form.executorName || '').trim();
|
|
||||||
// mock:用本地缓存里的手机号 + 默认号码
|
|
||||||
const arr = [];
|
const arr = [];
|
||||||
const cached = uni.getStorageSync('ykt_case_archive_detail');
|
const cached = uni.getStorageSync('ykt_case_archive_detail');
|
||||||
if (cached && typeof cached === 'object' && cached.mobile) arr.push(String(cached.mobile));
|
if (cached && typeof cached === 'object') {
|
||||||
if (!arr.includes('13800000000')) arr.push('13800000000');
|
if (cached.mobile) arr.push(String(cached.mobile));
|
||||||
return arr;
|
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) => {
|
onLoad((options) => {
|
||||||
archiveId.value = options?.archiveId ? String(options.archiveId) : '';
|
archiveId.value = options?.archiveId ? String(options.archiveId) : '';
|
||||||
mode.value = options?.mode ? String(options.mode) : 'add';
|
mode.value = options?.mode ? String(options.mode) : 'add';
|
||||||
taskId.value = options?.id ? String(options.id) : '';
|
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) {
|
if (!taskId.value) {
|
||||||
const task = getFollowup({ archiveId: archiveId.value, id: taskId.value });
|
toast('缺少回访任务 id');
|
||||||
if (task) {
|
setTimeout(() => uni.navigateBack(), 300);
|
||||||
currentType.value = typeOptions.find((i) => i.value === task.eventType) || typeOptions[0];
|
return;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
getTodo();
|
||||||
planDate.value = dayjs().format('YYYY-MM-DD');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function pickType(e) {
|
function parseTodoMethod(value) {
|
||||||
currentType.value = typeOptions[e.detail.value] || typeOptions[0];
|
if (typeof value !== 'string') return { todoMethod: '', phone: '' };
|
||||||
}
|
if (value.startsWith('phone')) {
|
||||||
function pickStatus(e) {
|
const [, num] = value.split(':');
|
||||||
currentStatus.value = statusOptions[e.detail.value] || statusOptions[0];
|
return { todoMethod: 'phone', phone: num || '' };
|
||||||
}
|
}
|
||||||
function pickTeam(e) {
|
if (value === 'wechat') return { todoMethod: 'wechat', phone: '' };
|
||||||
currentTeam.value = teamOptions[e.detail.value] || teamOptions[0];
|
return { todoMethod: '', phone: '' };
|
||||||
}
|
|
||||||
function pickPlanDate(e) {
|
|
||||||
planDate.value = e.detail.value || '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancel() {
|
function cancel() {
|
||||||
uni.navigateBack();
|
uni.navigateBack();
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function buildTodoMethodValue() {
|
||||||
if (!archiveId.value) {
|
if (todoMethod.value === 'phone') return phone.value ? `phone:${phone.value}` : 'phone';
|
||||||
uni.showToast({ title: '缺少 archiveId', icon: 'none' });
|
if (todoMethod.value === 'wechat') return 'wechat';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTodo() {
|
||||||
|
await ensureDoctor();
|
||||||
|
const corpId = getCorpId();
|
||||||
|
if (!corpId) {
|
||||||
|
toast('缺少 corpId,请先完成登录/团队选择');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const plannedExecutionTime = planDate.value ? dayjs(planDate.value).valueOf() : Date.now();
|
const res = await api('getTodoById', { corpId, id: taskId.value });
|
||||||
upsertFollowup({
|
if (!res?.success) {
|
||||||
archiveId: archiveId.value,
|
toast(res?.message || '获取回访任务失败');
|
||||||
followup: {
|
todo.value = null;
|
||||||
_id: taskId.value || '',
|
return;
|
||||||
plannedExecutionTime,
|
}
|
||||||
status: currentStatus.value.value,
|
todo.value = res.data && typeof res.data === 'object' ? res.data : null;
|
||||||
eventStatusLabel: currentStatus.value.label,
|
await loadUserNameMap(String(todo.value?.executeTeamId || ''));
|
||||||
eventType: currentType.value.value,
|
|
||||||
eventTypeLabel: currentType.value.label,
|
const parsed = parseTodoMethod(todo.value?.todoMethod);
|
||||||
executeTeamId: currentTeam.value.value,
|
if (parsed.todoMethod) {
|
||||||
executeTeamName: currentTeam.value.label,
|
todoMethod.value = parsed.todoMethod;
|
||||||
executorName: form.executorName || '李医生',
|
phone.value = parsed.phone || '';
|
||||||
creatorName: '管理员A',
|
} else if (editable.value) {
|
||||||
taskContent: form.taskContent,
|
todoMethod.value = 'phone';
|
||||||
result: form.result,
|
phone.value = mobiles.value[0] || '';
|
||||||
todoMethod: todoMethod.value === 'phone' ? (phone.value ? `phone:${phone.value}` : 'phone') : 'wechat',
|
}
|
||||||
},
|
|
||||||
});
|
form.executorName = resolveUserName(todo.value?.executorUserId);
|
||||||
uni.$emit('archive-detail:followup-changed');
|
form.taskContent = String(todo.value?.taskContent || '') || '';
|
||||||
uni.showToast({ title: '保存成功', icon: 'success' });
|
form.result = String(todo.value?.result || '') || '';
|
||||||
setTimeout(() => uni.navigateBack(), 300);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMethod(v) {
|
function toggleMethod(v) {
|
||||||
if (!canEdit.value) return;
|
if (!editable.value) return;
|
||||||
todoMethod.value = v;
|
todoMethod.value = v;
|
||||||
if (v !== 'phone') phone.value = '';
|
if (v !== 'phone') phone.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickPhone() {
|
function pickPhone() {
|
||||||
if (!canEdit.value) return;
|
if (!editable.value) return;
|
||||||
|
if (todoMethod.value !== 'phone') return;
|
||||||
uni.showActionSheet({
|
uni.showActionSheet({
|
||||||
itemList: mobiles.value,
|
itemList: mobiles.value,
|
||||||
success: ({ tapIndex }) => {
|
success: ({ tapIndex }) => {
|
||||||
@ -203,41 +354,76 @@ function pickPhone() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function markDone() {
|
function markDone() {
|
||||||
if (!canEdit.value) return;
|
if (!editable.value) return;
|
||||||
if (!['phone', 'wechat'].includes(todoMethod.value)) return uni.showToast({ title: '请选择回访方式', icon: 'none' });
|
if (!['phone', 'wechat'].includes(todoMethod.value)) return uni.showToast({ title: '请选择回访方式', icon: 'none' });
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '提示',
|
title: '提示',
|
||||||
content: '确定完成该回访任务吗?',
|
content: '确定完成该回访任务吗?',
|
||||||
success: (res) => {
|
success: async (res) => {
|
||||||
if (!res.confirm) return;
|
if (!res.confirm) return;
|
||||||
currentStatus.value = statusOptions.find((i) => i.value === 'treated') || currentStatus.value;
|
await ensureDoctor();
|
||||||
save();
|
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() {
|
function cancelTask() {
|
||||||
if (!canEdit.value) return;
|
if (!editable.value) return;
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '提示',
|
title: '提示',
|
||||||
content: '确定取消该回访任务吗?',
|
content: '确定取消该回访任务吗?',
|
||||||
success: (res) => {
|
success: async (res) => {
|
||||||
if (!res.confirm) return;
|
if (!res.confirm) return;
|
||||||
currentStatus.value = statusOptions.find((i) => i.value === 'cancelled') || currentStatus.value;
|
await ensureDoctor();
|
||||||
save();
|
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() {
|
function remove() {
|
||||||
|
if (!canRemove.value) return;
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '提示',
|
title: '提示',
|
||||||
content: '确定删除当前记录?',
|
content: '确定删除当前记录?',
|
||||||
success: (res) => {
|
success: async (res) => {
|
||||||
if (!res.confirm) return;
|
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.$emit('archive-detail:followup-changed');
|
||||||
uni.showToast({ title: '已删除', icon: 'success' });
|
toast('已删除');
|
||||||
setTimeout(() => uni.navigateBack(), 300);
|
setTimeout(() => uni.navigateBack(), 300);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -246,136 +432,183 @@ function remove() {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.page {
|
||||||
min-height: 100vh;
|
height: 100vh;
|
||||||
background: #f5f6f8;
|
background: #f6f7f8;
|
||||||
padding-bottom: calc(76px + env(safe-area-inset-bottom));
|
|
||||||
}
|
}
|
||||||
.card {
|
.scroll {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
margin: 10px 14px 0;
|
padding: 24rpx 30rpx;
|
||||||
border-radius: 8px;
|
}
|
||||||
padding: 14px;
|
.border-top {
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
border-top: 1px solid #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-row {
|
.head-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
.title {
|
.head-title {
|
||||||
font-size: 16px;
|
font-size: 32rpx;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #111827;
|
||||||
}
|
}
|
||||||
.tag {
|
.head-tag {
|
||||||
font-size: 12px;
|
flex-shrink: 0;
|
||||||
padding: 6px 10px;
|
font-size: 24rpx;
|
||||||
border-radius: 6px;
|
padding: 12rpx 20rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
background: #f3f4f6;
|
background: #f3f4f6;
|
||||||
color: #666;
|
color: #6b7280;
|
||||||
}
|
}
|
||||||
.st-processing {
|
.tag-processing {
|
||||||
background: #ffe5e5;
|
background: #fee2e2;
|
||||||
color: #ff4d4f;
|
color: #ef4444;
|
||||||
}
|
}
|
||||||
.st-notStart {
|
.tag-notStart {
|
||||||
background: #dbe6ff;
|
background: #dbeafe;
|
||||||
color: #4f6ef7;
|
color: #2563eb;
|
||||||
}
|
}
|
||||||
.st-treated {
|
.tag-treated {
|
||||||
background: #dcfce7;
|
background: #dcfce7;
|
||||||
color: #16a34a;
|
color: #16a34a;
|
||||||
}
|
}
|
||||||
.st-cancelled,
|
.tag-cancelled,
|
||||||
.st-expired {
|
.tag-expired {
|
||||||
background: #f3f4f6;
|
background: #f3f4f6;
|
||||||
color: #666;
|
color: #6b7280;
|
||||||
}
|
}
|
||||||
.desc {
|
.head-content {
|
||||||
margin-top: 10px;
|
margin-top: 24rpx;
|
||||||
font-size: 14px;
|
font-size: 28rpx;
|
||||||
color: #666;
|
color: #6b7280;
|
||||||
line-height: 20px;
|
line-height: 44rpx;
|
||||||
}
|
}
|
||||||
.info-row {
|
|
||||||
margin-top: 10px;
|
.sub-label {
|
||||||
display: flex;
|
font-size: 28rpx;
|
||||||
justify-content: space-between;
|
color: #6b7280;
|
||||||
gap: 10px;
|
|
||||||
}
|
}
|
||||||
.info {
|
.sub-content {
|
||||||
font-size: 12px;
|
margin-top: 24rpx;
|
||||||
color: #999;
|
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 {
|
.section-title {
|
||||||
font-size: 15px;
|
font-size: 28rpx;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #111827;
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
.method-row {
|
.method-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px 0;
|
margin-top: 24rpx;
|
||||||
}
|
}
|
||||||
.radio {
|
.radio {
|
||||||
width: 16px;
|
width: 32rpx;
|
||||||
height: 16px;
|
height: 32rpx;
|
||||||
margin-right: 10px;
|
margin-right: 20rpx;
|
||||||
}
|
}
|
||||||
.method-label {
|
.method-label {
|
||||||
font-size: 14px;
|
font-size: 28rpx;
|
||||||
color: #333;
|
color: #111827;
|
||||||
margin-right: 10px;
|
margin-right: 20rpx;
|
||||||
}
|
}
|
||||||
.method-note {
|
.method-select {
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
.method-input {
|
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
width: 220px;
|
width: 400rpx;
|
||||||
height: 36px;
|
height: 60rpx;
|
||||||
border: 1px solid #e6e6e6;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 6px;
|
border-radius: 16rpx;
|
||||||
padding: 0 10px;
|
padding: 12rpx 20rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
.method-value {
|
.method-value {
|
||||||
font-size: 13px;
|
font-size: 28rpx;
|
||||||
color: #333;
|
color: #111827;
|
||||||
max-width: 180px;
|
max-width: 320rpx;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.method-value.muted {
|
.method-value.muted {
|
||||||
color: #999;
|
color: #9ca3af;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textarea-box {
|
.kv {
|
||||||
border: 1px solid #e6e6e6;
|
display: flex;
|
||||||
border-radius: 8px;
|
align-items: center;
|
||||||
padding: 10px;
|
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%;
|
width: 100%;
|
||||||
height: 140px;
|
min-height: 160rpx;
|
||||||
font-size: 14px;
|
font-size: 28rpx;
|
||||||
|
line-height: 48rpx;
|
||||||
|
color: #111827;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.textarea.tall {
|
.meta {
|
||||||
height: 140px;
|
font-size: 28rpx;
|
||||||
}
|
}
|
||||||
.counter {
|
.meta-row {
|
||||||
margin-top: 6px;
|
margin-top: 24rpx;
|
||||||
text-align: right;
|
}
|
||||||
font-size: 12px;
|
.meta-row:first-child {
|
||||||
color: #999;
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.meta-key {
|
||||||
|
color: #6b7280;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
}
|
||||||
|
.meta-val {
|
||||||
|
color: #6b7280;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
@ -384,47 +617,49 @@ function remove() {
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
justify-content: flex-end;
|
||||||
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
|
gap: 20rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
.btn {
|
.footer-btn {
|
||||||
flex: 1;
|
flex: 0 0 auto;
|
||||||
height: 44px;
|
min-width: 260rpx;
|
||||||
line-height: 44px;
|
height: 80rpx;
|
||||||
border-radius: 6px;
|
line-height: 80rpx;
|
||||||
font-size: 15px;
|
border-radius: 16rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
}
|
}
|
||||||
.btn::after {
|
.footer-btn::after {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
.btn.plain {
|
.footer-btn.plain {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #4f6ef7;
|
color: #2563eb;
|
||||||
border: 1px solid #4f6ef7;
|
border: 1px solid #2563eb;
|
||||||
}
|
}
|
||||||
.btn.plain.danger {
|
.footer-btn.plain.danger {
|
||||||
color: #ff4d4f;
|
color: #ef4444;
|
||||||
border-color: #ff4d4f;
|
border-color: #ef4444;
|
||||||
}
|
}
|
||||||
.btn.primary {
|
.footer-btn.primary {
|
||||||
background: #4f6ef7;
|
background: #2563eb;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-fab {
|
.delete-fab {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 16px;
|
right: 32rpx;
|
||||||
bottom: calc(96px + env(safe-area-inset-bottom));
|
bottom: calc(200rpx + env(safe-area-inset-bottom));
|
||||||
width: 52px;
|
width: 104rpx;
|
||||||
height: 52px;
|
height: 104rpx;
|
||||||
border-radius: 26px;
|
border-radius: 52rpx;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: 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;
|
z-index: 30;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -57,6 +57,27 @@
|
|||||||
<button class="btn plain" @click="cancel">取消</button>
|
<button class="btn plain" @click="cancel">取消</button>
|
||||||
<button class="btn primary" @click="save">保存</button>
|
<button class="btn primary" @click="save">保存</button>
|
||||||
</view>
|
</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>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -64,11 +85,22 @@
|
|||||||
import { computed, reactive, ref } from 'vue';
|
import { computed, reactive, ref } from 'vue';
|
||||||
import { onLoad } from '@dcloudio/uni-app';
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
import dayjs from 'dayjs';
|
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 archiveId = ref('');
|
||||||
const archiveName = ref('');
|
const archiveName = ref('');
|
||||||
const archiveMobile = ref('');
|
const archiveMobile = ref('');
|
||||||
|
const customerData = ref({});
|
||||||
|
|
||||||
|
const accountStore = useAccountStore();
|
||||||
|
const { account, doctorInfo } = storeToRefs(accountStore);
|
||||||
|
const { getDoctorInfo } = accountStore;
|
||||||
|
|
||||||
|
const teams = ref([]);
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
plannedExecutionTime: '',
|
plannedExecutionTime: '',
|
||||||
@ -80,18 +112,8 @@ const form = reactive({
|
|||||||
result: '',
|
result: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const eventTypeList = [
|
const eventTypeList = getTodoEventTypeOptions();
|
||||||
{ label: '回访', value: 'followup' },
|
const eventTypeLabel = computed(() => getTodoEventTypeLabel(form.eventType));
|
||||||
{ 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 mobiles = computed(() => {
|
const mobiles = computed(() => {
|
||||||
const arr = [];
|
const arr = [];
|
||||||
@ -106,19 +128,84 @@ onLoad((options) => {
|
|||||||
if (c && typeof c === 'object') {
|
if (c && typeof c === 'object') {
|
||||||
archiveId.value = archiveId.value || String(c._id || '');
|
archiveId.value = archiveId.value || String(c._id || '');
|
||||||
archiveName.value = String(c.name || '');
|
archiveName.value = String(c.name || '');
|
||||||
|
customerData.value = c;
|
||||||
}
|
}
|
||||||
const cached = uni.getStorageSync('ykt_case_archive_detail');
|
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) {
|
if (!archiveId.value) {
|
||||||
uni.showToast({ title: '缺少 archiveId', icon: 'none' });
|
uni.showToast({ title: '缺少 archiveId', icon: 'none' });
|
||||||
setTimeout(() => uni.navigateBack(), 300);
|
setTimeout(() => uni.navigateBack(), 300);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ensureSeed(archiveId.value, { name: archiveName.value });
|
|
||||||
form.plannedExecutionTime = dayjs().format('YYYY-MM-DD');
|
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) {
|
function changeDate(e) {
|
||||||
form.plannedExecutionTime = e.detail.value || '';
|
form.plannedExecutionTime = e.detail.value || '';
|
||||||
}
|
}
|
||||||
@ -143,23 +230,26 @@ function selectMethod(method) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectType() {
|
function selectType() {
|
||||||
uni.showActionSheet({
|
typePopup.value?.open?.();
|
||||||
itemList: eventTypeList.map((i) => i.label),
|
|
||||||
success: ({ tapIndex }) => {
|
|
||||||
form.eventType = eventTypeList[tapIndex]?.value || '';
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectTeam() {
|
function selectTeam() {
|
||||||
uni.showActionSheet({
|
(async () => {
|
||||||
itemList: teamOptions.map((i) => i.label),
|
if (!teams.value.length) await loadTeams();
|
||||||
success: ({ tapIndex }) => {
|
if (!teams.value.length) {
|
||||||
const t = teamOptions[tapIndex];
|
toast('暂无可选团队');
|
||||||
form.teamId = t.value;
|
return;
|
||||||
form.teamName = t.label;
|
}
|
||||||
},
|
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() {
|
function cancel() {
|
||||||
@ -170,40 +260,64 @@ function cancel() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
async function save() {
|
||||||
if (!form.plannedExecutionTime) return uni.showToast({ title: '请选择回访日期', icon: 'none' });
|
if (!form.plannedExecutionTime) return uni.showToast({ title: '请选择回访日期', icon: 'none' });
|
||||||
if (!form.todoMethod) 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.eventType) return uni.showToast({ title: '请选择回访类型', icon: 'none' });
|
||||||
if (!form.teamId) 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' });
|
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 phoneValue = form.phoneNumber ? `phone:${form.phoneNumber}` : 'phone';
|
||||||
const plannedExecutionTime = dayjs(form.plannedExecutionTime).valueOf();
|
const plannedExecutionTime = dayjs(form.plannedExecutionTime).valueOf();
|
||||||
|
|
||||||
upsertFollowup({
|
const params = {
|
||||||
archiveId: archiveId.value,
|
corpId,
|
||||||
followup: {
|
eventStatus: 'treated',
|
||||||
plannedExecutionTime,
|
customerId,
|
||||||
endTime: plannedExecutionTime,
|
customerName,
|
||||||
status: 'treated',
|
customerUserId,
|
||||||
eventStatusLabel: '已完成',
|
executeTeamId: form.teamId,
|
||||||
eventType: form.eventType,
|
executeTeamName: form.teamName,
|
||||||
eventTypeLabel: eventTypeLabel.value || '回访',
|
executorUserId: userId,
|
||||||
executeTeamId: form.teamId,
|
creatorUserId: userId,
|
||||||
executeTeamName: form.teamName,
|
taskContent: '',
|
||||||
executorName: '我',
|
result: String(form.result || ''),
|
||||||
creatorName: '我',
|
todoMethod: form.todoMethod === 'phone' ? phoneValue : form.todoMethod,
|
||||||
taskContent: '',
|
plannedExecutionTime,
|
||||||
result: form.result,
|
endTime: plannedExecutionTime,
|
||||||
todoMethod: form.todoMethod === 'phone' ? phoneValue : form.todoMethod,
|
eventType: form.eventType,
|
||||||
createTime: Date.now(),
|
};
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const res = await api('createEvents', { params });
|
||||||
|
if (!res?.success) {
|
||||||
|
toast(res?.message || '保存失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
uni.$emit('archive-detail:followup-changed');
|
uni.$emit('archive-detail:followup-changed');
|
||||||
uni.showToast({ title: '保存成功', icon: 'success' });
|
toast('保存成功');
|
||||||
setTimeout(() => uni.navigateBack(), 300);
|
setTimeout(() => uni.navigateBack(), 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const typePopup = ref(null);
|
||||||
|
function pickType(v) {
|
||||||
|
form.eventType = String(v || '');
|
||||||
|
}
|
||||||
|
function closeTypePicker() {
|
||||||
|
typePopup.value?.close?.();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -340,5 +454,38 @@ function save() {
|
|||||||
background: #4f6ef7;
|
background: #4f6ef7;
|
||||||
color: #fff;
|
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>
|
<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="page">
|
||||||
<view class="card">
|
<view class="card">
|
||||||
<picker mode="date" :value="form.planExecutionTime" @change="changeDate">
|
<picker mode="date" :value="form.planExecutionTime" @change="changeDate">
|
||||||
@ -46,22 +46,6 @@
|
|||||||
<view class="counter">{{ (form.taskContent || '').length }}/200</view>
|
<view class="counter">{{ (form.taskContent || '').length }}/200</view>
|
||||||
</view>
|
</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>
|
||||||
|
|
||||||
<view class="footer">
|
<view class="footer">
|
||||||
@ -69,15 +53,44 @@
|
|||||||
<button class="btn primary" @click="save">保存</button>
|
<button class="btn primary" @click="save">保存</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<uni-popup ref="infoPopup" type="center">
|
<uni-popup ref="typePopup" type="bottom" :mask-click="true">
|
||||||
<view class="modal">
|
<view class="picker-sheet">
|
||||||
<view class="modal-title">跟进方式说明</view>
|
<view class="picker-title">选择类型</view>
|
||||||
<view class="modal-body">
|
<scroll-view scroll-y class="picker-body">
|
||||||
<view class="modal-text">待办:生成待办单,需员工手动进行处理</view>
|
<view
|
||||||
<view class="modal-text">群发:生成群发单,员工可批量进行处理(wxapp mock 不接入群发)</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>
|
||||||
<view class="modal-actions">
|
</view>
|
||||||
<view class="modal-btn save" @click="closeInfo">关闭</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>
|
||||||
</view>
|
</view>
|
||||||
</uni-popup>
|
</uni-popup>
|
||||||
@ -88,42 +101,44 @@
|
|||||||
import { computed, reactive, ref } from 'vue';
|
import { computed, reactive, ref } from 'vue';
|
||||||
import { onLoad } from '@dcloudio/uni-app';
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
import dayjs from 'dayjs';
|
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 archiveId = ref('');
|
||||||
const archiveName = ref('');
|
const archiveName = ref('');
|
||||||
|
const customerData = ref({});
|
||||||
|
|
||||||
const eventTypeList = [
|
const eventTypeList = getTodoEventTypeOptions();
|
||||||
{ label: '回访', value: 'followup' },
|
|
||||||
{ label: '复诊提醒', value: 'revisit' },
|
|
||||||
{ label: '问卷', value: 'questionnaire' },
|
|
||||||
{ label: '其他', value: 'other' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const teamOptions = [
|
const accountStore = useAccountStore();
|
||||||
{ label: '口腔一科(示例)', value: 'team_1' },
|
const { account, doctorInfo } = storeToRefs(accountStore);
|
||||||
{ label: '正畸团队(示例)', value: 'team_2' },
|
const { getDoctorInfo } = accountStore;
|
||||||
];
|
|
||||||
|
const teamMembers = ref([]);
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
planExecutionTime: '',
|
planExecutionTime: '',
|
||||||
executeTeamId: '',
|
executeTeamId: '', // teamId
|
||||||
executeTeamName: '',
|
executeTeamName: '', // teamName
|
||||||
executorName: '',
|
executorUserId: '', // userid
|
||||||
|
executorName: '', // anotherName
|
||||||
eventType: '',
|
eventType: '',
|
||||||
taskContent: '',
|
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) => {
|
onLoad((options) => {
|
||||||
|
resetForm();
|
||||||
archiveId.value = options?.archiveId ? String(options.archiveId) : '';
|
archiveId.value = options?.archiveId ? String(options.archiveId) : '';
|
||||||
const c = uni.getStorageSync('new-followup-customer');
|
const c = uni.getStorageSync('new-followup-customer');
|
||||||
if (c && typeof c === 'object') {
|
if (c && typeof c === 'object') {
|
||||||
archiveId.value = archiveId.value || String(c._id || '');
|
archiveId.value = archiveId.value || String(c._id || '');
|
||||||
archiveName.value = String(c.name || '');
|
archiveName.value = String(c.name || '');
|
||||||
|
customerData.value = c;
|
||||||
}
|
}
|
||||||
if (!archiveId.value) {
|
if (!archiveId.value) {
|
||||||
uni.showToast({ title: '缺少 archiveId', icon: 'none' });
|
uni.showToast({ title: '缺少 archiveId', icon: 'none' });
|
||||||
@ -131,19 +146,87 @@ onLoad((options) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureSeed(archiveId.value, { name: archiveName.value });
|
const cached = uni.getStorageSync('ykt_case_archive_detail');
|
||||||
|
if (cached && typeof cached === 'object' && String(cached?._id || '') === archiveId.value) {
|
||||||
// 使用模板:从 plan-list 选择后写入 select-mamagement-plan
|
customerData.value = cached;
|
||||||
const plan = uni.getStorageSync('select-mamagement-plan');
|
archiveName.value = archiveName.value || String(cached.name || '');
|
||||||
if (plan && typeof plan === 'object' && plan.planName) {
|
|
||||||
form.eventType = plan.eventType || 'followup';
|
|
||||||
form.taskContent = plan.taskContent || `执行回访计划:${plan.planName}`;
|
|
||||||
uni.setNavigationBarTitle({ title: '使用模板新增任务' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!form.planExecutionTime) form.planExecutionTime = dayjs().add(1, 'day').format('YYYY-MM-DD');
|
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) {
|
function changeDate(e) {
|
||||||
const date = String(e.detail.value || '');
|
const date = String(e.detail.value || '');
|
||||||
if (dayjs().startOf('day').isAfter(dayjs(date))) {
|
if (dayjs().startOf('day').isAfter(dayjs(date))) {
|
||||||
@ -154,25 +237,12 @@ function changeDate(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectType() {
|
function selectType() {
|
||||||
uni.showActionSheet({
|
typePopup.value?.open?.();
|
||||||
itemList: eventTypeList.map((i) => i.label),
|
|
||||||
success: ({ tapIndex }) => {
|
|
||||||
form.eventType = eventTypeList[tapIndex]?.value || '';
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectExecutor() {
|
function selectExecutor() {
|
||||||
// wxapp 先用 mock:选择团队 + 固定执行人
|
if (!teamMembers.value.length) return toast('当前团队暂无可选成员');
|
||||||
uni.showActionSheet({
|
executorPopup.value?.open?.();
|
||||||
itemList: teamOptions.map((i) => i.label),
|
|
||||||
success: ({ tapIndex }) => {
|
|
||||||
const t = teamOptions[tapIndex];
|
|
||||||
form.executeTeamId = t.value;
|
|
||||||
form.executeTeamName = t.label;
|
|
||||||
form.executorName = '李医生';
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancel() {
|
function cancel() {
|
||||||
@ -183,47 +253,76 @@ function cancel() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
async function save() {
|
||||||
if (!form.planExecutionTime) return uni.showToast({ title: '请选择回访日期', icon: 'none' });
|
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 (!form.eventType) return uni.showToast({ title: '请选择类型', icon: 'none' });
|
||||||
if (!String(form.taskContent || '').trim()) 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();
|
const params = {
|
||||||
upsertFollowup({
|
corpId,
|
||||||
archiveId: archiveId.value,
|
customerId,
|
||||||
followup: {
|
customerName,
|
||||||
plannedExecutionTime,
|
customerUserId,
|
||||||
status: 'processing',
|
executeTeamId: form.executeTeamId,
|
||||||
eventStatusLabel: '待处理',
|
executeTeamName: form.executeTeamName,
|
||||||
eventType: form.eventType,
|
creatorUserId: userId,
|
||||||
eventTypeLabel: eventTypeLabel.value || '回访',
|
userId: form.executorUserId || userId,
|
||||||
executeTeamId: form.executeTeamId || 'team_1',
|
taskList: [
|
||||||
executeTeamName: form.executeTeamName || '口腔一科(示例)',
|
{
|
||||||
executorName: form.executorName,
|
enableSend: false,
|
||||||
creatorName: '我',
|
eventType: form.eventType,
|
||||||
taskContent: form.taskContent,
|
executeMethod: 'todo',
|
||||||
result: '',
|
executorUserId: form.executorUserId || userId,
|
||||||
executeMethod: form.executeMethod,
|
planExecutionTime: form.planExecutionTime,
|
||||||
sendContent: form.executeMethod === 'groupMessage' ? form.sendContent : '',
|
sendContent: '',
|
||||||
createTime: Date.now(),
|
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.$emit('archive-detail:followup-changed');
|
||||||
uni.showToast({ title: '保存成功', icon: 'success' });
|
toast('保存成功');
|
||||||
setTimeout(() => uni.navigateBack(), 300);
|
setTimeout(() => uni.navigateBack(), 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
const infoPopup = ref(null);
|
const typePopup = ref(null);
|
||||||
function showInfo() {
|
function pickType(v) {
|
||||||
infoPopup.value?.open?.();
|
form.eventType = String(v || '');
|
||||||
}
|
}
|
||||||
function closeInfo() {
|
function closeTypePicker() {
|
||||||
infoPopup.value?.close?.();
|
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>
|
</script>
|
||||||
|
|
||||||
@ -291,6 +390,39 @@ function closeInfo() {
|
|||||||
color: #666;
|
color: #666;
|
||||||
margin-bottom: 10px;
|
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 {
|
.textarea-box {
|
||||||
border: 1px solid #e6e6e6;
|
border: 1px solid #e6e6e6;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@ -405,4 +537,3 @@ function closeInfo() {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
</style>
|
</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>
|
<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 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" />
|
<image class="empty-img" src="/static/empty.svg" mode="aspectFit" />
|
||||||
<view class="empty-text">暂无回访计划</view>
|
<view class="empty-text">暂无回访计划</view>
|
||||||
</view>
|
</view>
|
||||||
@ -26,43 +27,117 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { onLoad } from '@dcloudio/uni-app';
|
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 archiveId = ref('');
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
const list = ref([
|
const list = ref([]);
|
||||||
{
|
|
||||||
id: 'p1',
|
const accountStore = useAccountStore();
|
||||||
planName: '复诊提醒模板',
|
const { account, doctorInfo } = storeToRefs(accountStore);
|
||||||
planType: 'corp',
|
const { getDoctorInfo } = accountStore;
|
||||||
planDetail: '适用于复诊提醒人群',
|
|
||||||
eventType: 'revisit',
|
|
||||||
taskContent: '请于本周内完成复诊预约与提醒。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'p2',
|
|
||||||
planName: '随访回访模板',
|
|
||||||
planType: 'team',
|
|
||||||
planDetail: '适用于普通随访',
|
|
||||||
eventType: 'followup',
|
|
||||||
taskContent: '请电话回访患者,确认恢复情况并记录结果。',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
archiveId.value = options?.archiveId ? String(options.archiveId) : '';
|
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) {
|
function select(plan) {
|
||||||
uni.setStorageSync('select-mamagement-plan', 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) {
|
function preview(plan) {
|
||||||
uni.showModal({
|
uni.setStorageSync('preview-mamagement-plan', plan);
|
||||||
title: plan.planName || '回访计划',
|
uni.navigateTo({ url: `/pages/case/plan-preview?archiveId=${encodeURIComponent(archiveId.value)}` });
|
||||||
content: plan.taskContent || plan.planDetail || '',
|
|
||||||
showCancel: false,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -74,6 +149,11 @@ function preview(plan) {
|
|||||||
.scroll {
|
.scroll {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
.loading {
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #9aa0a6;
|
||||||
|
}
|
||||||
.item {
|
.item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -147,4 +227,3 @@ function preview(plan) {
|
|||||||
color: #9aa0a6;
|
color: #9aa0a6;
|
||||||
}
|
}
|
||||||
</style>
|
</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',
|
path: 'pages/message/index',
|
||||||
meta: { title: '消息', login: false },
|
meta: { title: '聊天', login: false },
|
||||||
|
style: { enablePullDownRefresh: false },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'pages/case/case',
|
path: 'pages/case/case',
|
||||||
@ -84,6 +85,14 @@ export default [
|
|||||||
path: 'pages/case/plan-list',
|
path: 'pages/case/plan-list',
|
||||||
meta: { title: '回访计划', login: false },
|
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',
|
path: 'pages/work/work',
|
||||||
meta: { title: '工作台', login: false }
|
meta: { title: '工作台', login: false }
|
||||||
@ -97,4 +106,3 @@ export default [
|
|||||||
meta: { title: '选择科室', login: false }
|
meta: { title: '选择科室', login: false }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,13 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
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
|
// 读取 routes 配置并转换为 pages.json
|
||||||
function generatePagesJson() {
|
function generatePagesJson() {
|
||||||
// 读取 routes/index.js
|
// 读取 routes/index.js
|
||||||
@ -41,7 +48,7 @@ function generatePagesJson() {
|
|||||||
|
|
||||||
if (fs.existsSync(pagesJsonPath)) {
|
if (fs.existsSync(pagesJsonPath)) {
|
||||||
const content = fs.readFileSync(pagesJsonPath, 'utf-8');
|
const content = fs.readFileSync(pagesJsonPath, 'utf-8');
|
||||||
pagesJson = JSON.parse(content);
|
pagesJson = JSON.parse(stripJsonComments(content));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换 routes 为 pages 格式
|
// 转换 routes 为 pages 格式
|
||||||
|
|||||||
13
utils/api.js
13
utils/api.js
@ -44,6 +44,19 @@ const urlsConfig = {
|
|||||||
getUserSig: 'getUserSig',
|
getUserSig: 'getUserSig',
|
||||||
sendSystemMessage: "sendSystemMessage",
|
sendSystemMessage: "sendSystemMessage",
|
||||||
getChatRecordsByGroupId: "getChatRecordsByGroupId"
|
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