ykt-wxapp/pages/case/service-record-detail.vue

395 lines
12 KiB
Vue

<template>
<view class="page">
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/service-record-detail/service-record-detail.vue -->
<view class="body">
<scroll-view scroll-y class="scroll">
<view class="card">
<view class="section-title">执行日期</view>
<picker mode="date" @change="pickDate" :disabled="true">
<view class="picker-box">
<uni-icons type="calendar" size="18" color="#666" class="mr" />
<view class="picker-text" :class="{ muted: !date }">{{ date || '请选择服务日期' }}</view>
</view>
</picker>
<view class="section-title">执行时间</view>
<picker mode="time" @change="pickTime" :disabled="Boolean(recordId)">
<view class="picker-box">
<uni-icons type="calendar" size="18" color="#666" class="mr" />
<view class="picker-text" :class="{ muted: !time }">{{ time || '请选择执行时间' }}</view>
</view>
</picker>
<view class="section-title">服务类型</view>
<picker mode="selector" :range="typeOptions" range-key="label" @change="pickType" :disabled="Boolean(recordId)">
<view class="picker-box between">
<view class="picker-text" :class="{ muted: !currentType?.value }">
{{ currentType?.value ? currentType.label : '请选择服务类型' }}
</view>
<uni-icons type="arrowright" size="16" color="#999" />
</view>
</picker>
<view class="section-title">所属团队</view>
<picker mode="selector" :range="teamOptions" range-key="label" @change="pickTeam" :disabled="Boolean(recordId)">
<view class="picker-box between">
<view class="picker-text" :class="{ muted: !currentTeam?.value }">
{{ currentTeam?.value ? currentTeam.label : '请选择所属团队' }}
</view>
<uni-icons type="arrowright" size="16" color="#999" />
</view>
</picker>
<view class="section-title">服务内容</view>
<view class="textarea-box">
<textarea v-model="form.taskContent" class="textarea tall" maxlength="1000" placeholder="请输入服务小结内容" />
<view class="counter">{{ (form.taskContent || '').length }}/1000</view>
</view>
</view>
<view style="height: 120px;"></view>
</scroll-view>
<view class="footer">
<button class="btn plain" @click="cancel">取消</button>
<button class="btn primary" @click="save">保存</button>
</view>
</view>
<view v-if="recordId" class="delete-fab" @click="remove">
<uni-icons type="trash" size="22" color="#ff4d4f" />
</view>
</view>
</template>
<script setup>
import { 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 { getServiceTypeOptions } from '@/utils/service-type-const';
const archiveId = ref('');
const mode = ref('add');
const recordId = ref('');
const typeOptions = getServiceTypeOptions({ excludeCustomerUpdate: true });
const currentType = ref(null);
const teamOptions = ref([]);
const currentTeam = ref(null);
const date = ref('');
const time = ref('');
const form = reactive({
taskContent: '',
});
const customerId = ref('');
const customerName = ref('');
const customerUserId = ref('');
const accountStore = useAccountStore();
const { account, doctorInfo } = storeToRefs(accountStore);
const { getDoctorInfo } = accountStore;
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') || {};
const teamId = String(t.teamId || '') || '';
const name = String(t.name || t.teamName || '') || '';
return teamId ? { value: teamId, label: name || teamId } : null;
}
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 : [];
teamOptions.value = 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) return null;
return { label: String(name || teamId), value: String(teamId) };
})
.filter(Boolean);
}
onLoad((options) => {
archiveId.value = options?.archiveId ? String(options.archiveId) : '';
recordId.value = options?.id ? String(options.id) : '';
mode.value = options?.mode ? String(options.mode) : 'add';
const seed = uni.getStorageSync('service-record-detail') || null;
customerId.value = String(seed?.customerId || archiveId.value || '') || '';
customerName.value = String(seed?.customerName || '') || '';
customerUserId.value = String(seed?.customerUserId || '') || '';
const executionTime = seed?.executionTime && dayjs(seed.executionTime).isValid() ? dayjs(seed.executionTime) : null;
date.value = executionTime ? executionTime.format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD');
time.value = executionTime ? executionTime.format('HH:mm') : '';
form.taskContent = String(seed?.taskContent || '') || '';
const seedType = String(seed?.eventType || '') || '';
currentType.value = typeOptions.find((i) => i.value === seedType) || null;
const seedTeamId = String(seed?.executeTeamId || '') || '';
const seedTeamName = String(seed?.executeTeamName || '') || '';
if (seedTeamId) currentTeam.value = { value: seedTeamId, label: seedTeamName || seedTeamId };
else currentTeam.value = getCurrentTeam();
loadTeams().then(() => {
if (seedTeamId) currentTeam.value = teamOptions.value.find((t) => t.value === seedTeamId) || currentTeam.value;
else {
const cur = getCurrentTeam();
if (cur) currentTeam.value = teamOptions.value.find((t) => t.value === cur.value) || cur;
}
});
});
function pickType(e) {
currentType.value = typeOptions[e.detail.value] || null;
}
function pickTeam(e) {
currentTeam.value = teamOptions.value[e.detail.value] || null;
}
function pickDate(e) {
date.value = e.detail.value || '';
}
function pickTime(e) {
time.value = e.detail.value || '';
}
function cancel() {
uni.navigateBack();
}
function save() {
if (!customerId.value) return toast('缺少 customerId');
if (!date.value) return uni.showToast({ title: '请选择服务日期', icon: 'none' });
if (!time.value) return uni.showToast({ title: '请选择执行时间', icon: 'none' });
if (!currentType.value?.value) return uni.showToast({ title: '请选择服务类型', icon: 'none' });
if (!currentTeam.value?.value) return uni.showToast({ title: '请选择所属团队', icon: 'none' });
if (!String(form.taskContent || '').trim()) return uni.showToast({ title: '请输入服务内容', icon: 'none' });
const executionTime = dayjs(`${date.value} ${time.value}`).isValid() ? dayjs(`${date.value} ${time.value}`).valueOf() : Date.now();
submit(executionTime);
}
async function submit(executionTime) {
await ensureDoctor();
const corpId = getCorpId();
const userId = getUserId();
if (!corpId || !userId) {
toast('缺少用户/团队信息');
return;
}
const params = {
taskContent: form.taskContent,
executionTime,
eventType: currentType.value.value,
executeTeamId: currentTeam.value.value,
executeTeamName: currentTeam.value.label,
};
let res;
if (recordId.value) {
res = await api('updateServiceRecord', {
corpId,
id: recordId.value,
params: {
...params,
updateUserId: userId,
},
});
} else {
res = await api('addServiceRecord', {
...params,
corpId,
creatorUserId: userId,
executorUserId: userId,
customerId: customerId.value,
customerName: customerName.value,
customerUserId: customerUserId.value,
});
}
if (!res?.success) {
toast(res?.message || (recordId.value ? '修改失败' : '新增失败'));
return;
}
uni.$emit('archive-detail:service-record-changed');
uni.showToast({ title: '保存成功', icon: 'success' });
setTimeout(() => uni.navigateBack(), 300);
}
function remove() {
uni.showModal({
title: '提示',
content: '确定删除当前记录?',
success: async (res) => {
if (!res.confirm) return;
await ensureDoctor();
const corpId = getCorpId();
if (!corpId || !recordId.value) return;
const resp = await api('removeServiceRecord', { corpId, id: recordId.value });
if (!resp?.success) {
toast(resp?.message || '删除失败');
return;
}
uni.$emit('archive-detail:service-record-changed');
uni.showToast({ title: '已删除', icon: 'success' });
setTimeout(() => uni.navigateBack(), 300);
},
});
}
</script>
<style scoped>
.page {
min-height: 100vh;
background: #f5f6f8;
padding-bottom: calc(76px + env(safe-area-inset-bottom));
}
.body {
height: 100vh;
display: flex;
flex-direction: column;
}
.scroll {
flex: 1;
}
.card {
background: #fff;
padding: 14px;
margin: 10px 14px 0;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 12px 0 10px;
}
.section-title:first-child {
margin-top: 0;
}
.picker-box {
display: flex;
align-items: center;
padding: 12px 12px;
border: 1px solid #e6e6e6;
border-radius: 6px;
background: #fff;
}
.picker-box.between {
justify-content: space-between;
}
.mr {
margin-right: 10px;
}
.picker-text {
font-size: 14px;
color: #333;
}
.picker-text.muted {
color: #999;
}
.textarea-box {
border: 1px solid #e6e6e6;
border-radius: 8px;
padding: 10px;
}
.textarea {
width: 100%;
height: 90px;
font-size: 14px;
box-sizing: border-box;
}
.textarea.tall {
height: 120px;
}
.counter {
margin-top: 6px;
text-align: right;
font-size: 12px;
color: #999;
}
.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;
}
.delete-fab {
position: fixed;
right: 16px;
bottom: calc(96px + env(safe-area-inset-bottom));
width: 52px;
height: 52px;
border-radius: 26px;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 10px 18px rgba(0, 0, 0, 0.12);
z-index: 30;
}
</style>