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

540 lines
14 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

<template>
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/new-followup/new-followup.vuewxapp仅新增待办任务 -->
<view class="page">
<view class="card">
<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="selectExecutor">
<view class="left">
<view class="label">处理人</view>
<view class="req">*</view>
</view>
<view class="right">
<view class="value" :class="{ muted: !form.executorName }">
{{ form.executorName ? `${form.executorName}${form.executeTeamName ? `(${form.executeTeamName})` : ''}` : '请选择处理人' }}
</view>
<uni-icons type="arrowright" size="16" color="#999" />
</view>
</view>
<view class="row clickable" @click="selectType">
<view class="left">
<view class="label">类型</view>
<view class="req">*</view>
</view>
<view class="right">
<view class="value" :class="{ muted: !form.eventType }">{{ eventTypeLabel || '请选择类型' }}</view>
<uni-icons type="arrowright" size="16" color="#999" />
</view>
</view>
<view class="block">
<view class="block-title">目的</view>
<view class="textarea-box">
<textarea v-model="form.taskContent" class="textarea" placeholder="请输入文字提醒" maxlength="200" />
<view class="counter">{{ (form.taskContent || '').length }}/200</view>
</view>
</view>
</view>
<view class="footer">
<button class="btn plain" @click="cancel">取消</button>
<button class="btn primary" @click="save">保存</button>
</view>
<uni-popup ref="typePopup" type="bottom" :mask-click="true">
<view class="picker-sheet">
<view class="picker-title">选择类型</view>
<scroll-view scroll-y class="picker-body">
<view
v-for="t in eventTypeList"
:key="t.value"
class="picker-item"
:class="{ active: form.eventType === t.value }"
@click="pickType(t.value)"
>
<view class="picker-item-text">{{ t.label }}</view>
</view>
</scroll-view>
<view class="picker-actions">
<button class="btn plain" @click="closeTypePicker">取消</button>
<button class="btn primary" @click="closeTypePicker">确定</button>
</view>
</view>
</uni-popup>
<uni-popup ref="executorPopup" type="bottom" :mask-click="true">
<view class="picker-sheet">
<view class="picker-title">选择处理人本团队</view>
<scroll-view scroll-y class="picker-body">
<view
v-for="m in teamMembers"
:key="String(m?.userid || '')"
class="picker-item"
:class="{ active: form.executorUserId && String(m?.userid || '') === form.executorUserId }"
@click="pickExecutor(m)"
>
<view class="picker-item-text">{{ String(m?.anotherName || m?.name || m?.userid || '') }}</view>
</view>
</scroll-view>
<view class="picker-actions">
<button class="btn plain" @click="closeExecutorPicker">取消</button>
<button class="btn primary" @click="closeExecutorPicker">确定</button>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { computed, reactive, ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import dayjs from 'dayjs';
import { storeToRefs } from 'pinia';
import api from '@/utils/api';
import useAccountStore from '@/store/account';
import { toast } from '@/utils/widget';
import { getTodoEventTypeLabel, getTodoEventTypeOptions } from '@/utils/todo-const';
const archiveId = ref('');
const archiveName = ref('');
const customerData = ref({});
const eventTypeList = getTodoEventTypeOptions();
const accountStore = useAccountStore();
const { account, doctorInfo } = storeToRefs(accountStore);
const { getDoctorInfo } = accountStore;
const teamMembers = ref([]);
const form = reactive({
planExecutionTime: '',
executeTeamId: '', // teamId
executeTeamName: '', // teamName
executorUserId: '', // userid
executorName: '', // anotherName
eventType: '',
taskContent: '',
});
const eventTypeLabel = computed(() => getTodoEventTypeLabel(form.eventType));
onLoad((options) => {
resetForm();
archiveId.value = options?.archiveId ? String(options.archiveId) : '';
const c = uni.getStorageSync('new-followup-customer');
if (c && typeof c === 'object') {
archiveId.value = archiveId.value || String(c._id || '');
archiveName.value = String(c.name || '');
customerData.value = c;
}
if (!archiveId.value) {
uni.showToast({ title: '缺少 archiveId', icon: 'none' });
setTimeout(() => uni.navigateBack(), 300);
return;
}
const cached = uni.getStorageSync('ykt_case_archive_detail');
if (cached && typeof cached === 'object' && String(cached?._id || '') === archiveId.value) {
customerData.value = cached;
archiveName.value = archiveName.value || String(cached.name || '');
}
if (!form.planExecutionTime) form.planExecutionTime = dayjs().add(1, 'day').format('YYYY-MM-DD');
initDefaultExecutor();
});
function resetForm(keepCustomer = false) {
form.planExecutionTime = '';
form.executeTeamId = '';
form.executeTeamName = '';
form.executorUserId = '';
form.executorName = '';
form.eventType = '';
form.taskContent = '';
if (!keepCustomer) {
archiveId.value = '';
archiveName.value = '';
customerData.value = {};
}
teamMembers.value = [];
uni.setStorageSync('select-mamagement-plan', '');
}
async function ensureDoctor() {
if (doctorInfo.value) return;
if (!account.value?.openid) return;
try {
await getDoctorInfo();
} catch {
// ignore
}
}
function getUserId() {
const d = doctorInfo.value || {};
const a = account.value || {};
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || '') || '';
}
function getCorpId() {
const t = uni.getStorageSync('ykt_case_current_team') || {};
const a = account.value || {};
const d = doctorInfo.value || {};
return String(t.corpId || a.corpId || d.corpId || '') || '';
}
async function loadTeamMembers(teamId) {
const corpId = getCorpId();
if (!corpId || !teamId) return;
const res = await api('getTeamData', { corpId, teamId });
if (!res?.success) {
teamMembers.value = [];
return;
}
const t = res?.data && typeof res.data === 'object' ? res.data : {};
teamMembers.value = Array.isArray(t.memberList) ? t.memberList : [];
}
async function initDefaultExecutor() {
await ensureDoctor();
const userId = getUserId();
const name = String(doctorInfo.value?.anotherName || doctorInfo.value?.name || '');
const currentTeam = uni.getStorageSync('ykt_case_current_team') || {};
const teamId = String(currentTeam.teamId || '');
const teamName = String(currentTeam.name || '');
if (teamId) {
form.executeTeamId = teamId;
form.executeTeamName = teamName;
await loadTeamMembers(teamId);
}
if (userId) {
form.executorUserId = userId;
form.executorName = name || '我';
}
}
function changeDate(e) {
const date = String(e.detail.value || '');
if (dayjs().startOf('day').isAfter(dayjs(date))) {
uni.showToast({ title: '请选择有效的回访日期', icon: 'none' });
return;
}
form.planExecutionTime = date;
}
function selectType() {
typePopup.value?.open?.();
}
function selectExecutor() {
if (!teamMembers.value.length) return toast('当前团队暂无可选成员');
executorPopup.value?.open?.();
}
function cancel() {
uni.showModal({
title: '确认取消',
content: '确定要取消新建回访任务吗?',
success: (res) => res.confirm && uni.navigateBack(),
});
}
async function save() {
if (!form.planExecutionTime) return uni.showToast({ title: '请选择回访日期', icon: 'none' });
if (!form.executeTeamId) return uni.showToast({ title: '请选择处理人', icon: 'none' });
if (!form.executorUserId) return uni.showToast({ title: '请选择处理人', icon: 'none' });
if (!form.eventType) return uni.showToast({ title: '请选择类型', icon: 'none' });
if (!String(form.taskContent || '').trim()) return uni.showToast({ title: '请输入目的', icon: 'none' });
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 params = {
corpId,
customerId,
customerName,
customerUserId,
executeTeamId: form.executeTeamId,
executeTeamName: form.executeTeamName,
creatorUserId: userId,
userId: form.executorUserId || userId,
taskList: [
{
enableSend: false,
eventType: form.eventType,
executeMethod: 'todo',
executorUserId: form.executorUserId || userId,
planExecutionTime: form.planExecutionTime,
sendContent: '',
taskContent: form.taskContent,
taskId: `${Date.now()}_${Math.random().toString(16).slice(2)}`,
fileList: [],
},
],
};
const res = await api('executeManagementPlanTodo', params);
if (!res?.success) {
toast(res?.message || '保存失败');
return;
}
uni.$emit('archive-detail:followup-changed');
toast('保存成功');
setTimeout(() => uni.navigateBack(), 300);
}
const typePopup = ref(null);
function pickType(v) {
form.eventType = String(v || '');
}
function closeTypePicker() {
typePopup.value?.close?.();
}
const executorPopup = ref(null);
function pickExecutor(m) {
const id = String(m?.userid || '');
if (!id) return;
form.executorUserId = id;
form.executorName = String(m?.anotherName || m?.name || m?.userid || '') || '';
}
function closeExecutorPicker() {
executorPopup.value?.close?.();
}
</script>
<style scoped>
.page {
min-height: 100vh;
background: #f5f6f8;
padding-bottom: calc(76px + env(safe-area-inset-bottom));
}
.card {
background: #fff;
margin: 10px 14px 0;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
}
.row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 14px;
border-bottom: 1px solid #f2f2f2;
}
.row.clickable:active {
background: #fafafa;
}
.left {
display: flex;
align-items: center;
}
.label {
font-size: 14px;
color: #666;
}
.req {
margin-left: 6px;
color: #ff4d4f;
font-size: 14px;
}
.right {
display: flex;
align-items: center;
gap: 10px;
}
.value {
font-size: 14px;
color: #333;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.value.muted {
color: #999;
}
.block {
padding: 14px 14px;
border-bottom: 1px solid #f2f2f2;
}
.block:last-child {
border-bottom: none;
}
.block-title {
font-size: 14px;
color: #666;
margin-bottom: 10px;
}
.picker-sheet {
background: #fff;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
overflow: hidden;
}
.picker-title {
text-align: center;
font-size: 16px;
font-weight: 600;
padding: 14px;
border-bottom: 1px solid #f0f0f0;
}
.picker-body {
max-height: 60vh;
}
.picker-item {
padding: 14px;
border-bottom: 1px solid #f2f2f2;
}
.picker-item.active {
background: #f2f6ff;
}
.picker-item-text {
font-size: 14px;
color: #333;
}
.picker-actions {
padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
display: flex;
gap: 12px;
background: #fff;
}
.textarea-box {
border: 1px solid #e6e6e6;
border-radius: 8px;
padding: 10px;
}
.textarea {
width: 100%;
min-height: 90px;
font-size: 14px;
box-sizing: border-box;
}
.counter {
margin-top: 6px;
text-align: right;
font-size: 12px;
color: #999;
}
.toggle-row {
display: flex;
align-items: center;
gap: 10px;
}
.pill {
padding: 8px 12px;
border-radius: 999px;
background: #eaecef;
font-size: 12px;
color: #333;
}
.pill.active {
background: #4f6ef7;
color: #fff;
}
.info {
margin-left: auto;
width: 18px;
height: 18px;
border-radius: 9px;
background: #cfd3dc;
color: #fff;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
display: flex;
gap: 12px;
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
}
.btn {
flex: 1;
height: 44px;
line-height: 44px;
border-radius: 6px;
font-size: 15px;
}
.btn::after {
border: none;
}
.btn.plain {
background: #fff;
color: #4f6ef7;
border: 1px solid #4f6ef7;
}
.btn.primary {
background: #4f6ef7;
color: #fff;
}
.modal {
width: 320px;
background: #fff;
border-radius: 8px;
overflow: hidden;
}
.modal-title {
font-size: 16px;
font-weight: 600;
text-align: center;
padding: 14px 12px;
color: #333;
border-bottom: 1px solid #f0f0f0;
}
.modal-body {
padding: 14px;
}
.modal-text {
font-size: 14px;
color: #666;
line-height: 20px;
margin-bottom: 10px;
}
.modal-actions {
padding: 12px 14px 14px;
}
.modal-btn {
height: 40px;
line-height: 40px;
text-align: center;
border-radius: 4px;
font-size: 14px;
}
.modal-btn.save {
background: #4f6ef7;
color: #fff;
}
</style>