ykt-wxapp/pages/case/plan-execute.vue
2026-01-26 15:39:14 +08:00

401 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<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>