ykt-wxapp/pages/case/visit-record-detail.vue
2026-01-28 20:01:28 +08:00

468 lines
12 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>
<view class="page">
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/visit-record-detail/visit-record-detail.vue -->
<view class="body">
<scroll-view scroll-y class="scroll">
<view class="header">
<view class="header-title">{{ template?.templateName || '健康档案' }}</view>
</view>
<view class="form-wrap">
<FormTemplate v-if="template" ref="formRef" :items="showItems" :form="forms" @change="onChange" />
</view>
<!-- 附件上传FormTemplate 不支持 files单独实现 -->
<view v-if="hasFilesField" class="upload-wrap">
<view class="upload-row">
<view class="upload-label">文件上传</view>
<view class="upload-desc">支持5M文件pdf文件格式</view>
</view>
<view class="upload-grid">
<view v-for="(f, idx) in fileList" :key="idx" class="upload-item" @click="previewFile(idx)">
<image class="upload-thumb" :src="f.url" mode="aspectFill" />
<view class="upload-remove" @click.stop="removeFile(idx)">×</view>
</view>
<view class="upload-add" @click="addFiles">
<view class="plus">+</view>
</view>
</view>
</view>
<view style="height: 240rpx;"></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 { computed, reactive, ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import dayjs from 'dayjs';
import { storeToRefs } from 'pinia';
import FormTemplate from '@/components/form-template/index.vue';
import { getVisitRecordTemplate } from '@/components/archive-detail/templates';
import api from '@/utils/api';
import useAccountStore from '@/store/account';
import { toast, confirm, loading as uniLoading, hideLoading } from '@/utils/widget';
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 d = doctorInfo.value || {};
const a = account.value || {};
const t = uni.getStorageSync('ykt_case_current_team') || {};
return String(d.corpId || a.corpId || t.corpId || '') || '';
}
const memberId = ref('');
const recordId = ref('');
const templateType = ref('');
const customerName = ref('');
const template = computed(() => getVisitRecordTemplate(templateType.value));
const detail = ref({});
const form = reactive({});
const forms = computed(() => ({ ...detail.value, ...form }));
const showItems = computed(() => {
const list = template.value?.templateList || [];
// referenceField 兼容(与 mobile 一致)
return list.filter((i) => {
if (i?.type === 'files') return false;
if (i && typeof i.referenceField === 'string') {
return forms.value[i.referenceField] === i.referenceValue;
}
return true;
});
});
const hasFilesField = computed(() => {
const list = template.value?.templateList || [];
return list.some((i) => i && (i.type === 'files' || i.title === 'files'));
});
const formRef = ref(null);
const fileList = computed(() => {
const arr = forms.value?.files;
return Array.isArray(arr) ? arr.filter((i) => i && i.url) : [];
});
function ensureFilesField() {
if (form.files !== undefined) return;
if (detail.value && detail.value.files !== undefined) return;
form.files = [];
}
onLoad(async (options) => {
memberId.value = options?.memberId || options?.archiveId || '';
recordId.value = options?.id || '';
templateType.value = options?.type || '';
customerName.value = decodeURIComponent(options?.customerName || '');
if (recordId.value) {
await getDetail();
} else {
if (!templateType.value) templateType.value = 'outpatient';
ensureFilesField();
// 门诊记录默认今日日期
if (templateType.value === 'outpatient') {
form.visitTime = dayjs().format('YYYY-MM-DD');
}
// 住院记录默认今日日期
if (templateType.value === 'inhospital') {
form.inhosDate = dayjs().format('YYYY-MM-DD');
}
// 体检记录默认今日日期
if (templateType.value === 'physicalExaminationTemplate') {
form.inspectDate = dayjs().format('YYYY-MM-DD');
}
}
});
async function getDetail() {
if (!recordId.value || !memberId.value) return;
await ensureDoctor();
const corpId = getCorpId();
if (!corpId) return;
uniLoading('加载中...');
try {
const res = await api('getMedicalRecordById', {
_id: recordId.value,
corpId,
memberId: memberId.value,
medicalType: templateType.value,
});
hideLoading();
const record = res?.record || res?.data?.record || null;
if (res?.success && record) {
templateType.value = record.templateType || record.medicalType || templateType.value;
// 兼容模板字段wxapp 使用 diagnosis但接口通常返回 diagnosisName
if ((record.medicalType === 'outpatient' || record.medicalType === 'inhospital') && !record.diagnosis && record.diagnosisName) {
record.diagnosis = record.diagnosisName;
}
detail.value = record;
ensureFilesField();
} else {
toast(res.message || '加载失败');
}
} catch (error) {
hideLoading();
console.error('getDetail error:', error);
toast('加载失败');
}
}
function onChange({ title, value }) {
form[title] = value;
const item = showItems.value.find((i) => i.title === title);
if (!item) return;
// 关联字段变化时清理被关联字段(与 mobile 行为一致)
const relat = (template.value?.templateList || []).filter((i) => i.referenceField === title);
relat.forEach((i) => (form[i.title] = ''));
}
function cancel() {
uni.navigateBack();
}
async function save() {
if (!memberId.value) {
toast('缺少患者信息');
return;
}
await ensureDoctor();
const corpId = getCorpId();
const userId = getUserId();
if (!corpId || !userId) return toast('缺少用户信息');
if (formRef.value?.verify && !formRef.value.verify()) return;
const params = {
...form,
corpId,
memberId: memberId.value,
medicalType: templateType.value,
};
// 门诊/住院:与 mobile 字段对齐diagnosisName
if ((templateType.value === 'outpatient' || templateType.value === 'inhospital') && form.diagnosis && !form.diagnosisName) {
params.diagnosisName = form.diagnosis;
}
if (recordId.value) {
params._id = recordId.value;
params.userId = userId;
} else {
params.creator = userId;
}
// sortTime使用模板中的时间字段
const sortTimeKey = template.value?.service?.timeTitle || '';
if (sortTimeKey && form[sortTimeKey] && dayjs(form[sortTimeKey]).isValid()) {
params.sortTime = dayjs(form[sortTimeKey]).valueOf();
} else {
params.sortTime = Date.now();
}
uniLoading('保存中...');
try {
const res = await api(
recordId.value ? 'updateMedicalRecord' : 'addMedicalRecord',
params
);
hideLoading();
if (res.success) {
uni.$emit('archive-detail:visit-record-changed');
toast(res.message || '保存成功');
setTimeout(() => uni.navigateBack(), 300);
} else {
toast(res.message || '保存失败');
}
} catch (error) {
hideLoading();
console.error('save error:', error);
toast('保存失败');
}
}
function remove() {
confirm('确定删除当前记录?', async () => {
if (!memberId.value || !recordId.value) return toast('缺少必要信息');
await ensureDoctor();
const corpId = getCorpId();
if (!corpId) return toast('缺少必要信息');
uniLoading('删除中...');
try {
const res = await api('removeMedicalRecord', {
corpId,
memberId: memberId.value,
medicalType: templateType.value,
_id: recordId.value,
});
hideLoading();
if (res.success) {
uni.$emit('archive-detail:visit-record-changed');
toast(res.message || '已删除');
setTimeout(() => uni.navigateBack(), 300);
} else {
toast(res.message || '删除失败');
}
} catch (error) {
hideLoading();
console.error('remove error:', error);
toast('删除失败');
}
});
}
function addFiles() {
const fileConfig = template.value?.templateList?.find(i => i.type === 'files');
const maxSize = fileConfig?.maxSize || 5; // MB
const accept = fileConfig?.accept || 'pdf';
uni.chooseMessageFile({
count: 9,
type: 'file',
extension: [accept],
success: (res) => {
const files = Array.isArray(res.tempFiles) ? res.tempFiles : [];
const maxBytes = maxSize * 1024 * 1024;
// 验证文件大小
const invalidFiles = files.filter(f => f.size > maxBytes);
if (invalidFiles.length > 0) {
toast(`文件大小不能超过${maxSize}M`);
return;
}
const next = files.map((f) => ({
url: f.path,
name: f.name || '',
size: f.size
}));
const cur = Array.isArray(forms.value.files) ? forms.value.files : [];
form.files = [...cur, ...next];
},
});
}
function removeFile(idx) {
const cur = Array.isArray(forms.value.files) ? forms.value.files : [];
form.files = cur.filter((_, i) => i !== idx);
}
function previewFile(idx) {
const urls = fileList.value.map((i) => i.url);
uni.previewImage({ urls, current: urls[idx] });
}
</script>
<style scoped>
.page {
min-height: 100vh;
background: #f5f6f8;
padding-bottom: calc(152rpx + env(safe-area-inset-bottom));
}
.body {
height: 100vh;
display: flex;
flex-direction: column;
}
.scroll {
flex: 1;
}
.header {
background: #fff;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
}
.header-title {
padding: 28rpx 28rpx;
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.form-wrap {
background: #fff;
margin-top: 20rpx;
padding: 8rpx 0;
}
.upload-wrap {
background: #fff;
padding: 24rpx 30rpx;
border-bottom: 2rpx solid #eee;
}
.upload-row {
display: flex;
align-items: baseline;
margin-bottom: 18rpx;
}
.upload-label {
font-size: 28rpx;
line-height: 42rpx;
color: #111827;
flex-shrink: 0;
}
.upload-desc {
font-size: 24rpx;
color: #9ca3af;
margin-left: 8rpx;
}
.upload-grid {
display: flex;
flex-wrap: wrap;
gap: 18rpx;
}
.upload-item {
width: 180rpx;
height: 140rpx;
position: relative;
border: 2rpx solid #e5e7eb;
border-radius: 8rpx;
overflow: hidden;
background: #f9fafb;
}
.upload-thumb {
width: 100%;
height: 100%;
}
.upload-remove {
position: absolute;
right: 0;
top: 0;
width: 36rpx;
height: 36rpx;
line-height: 36rpx;
text-align: center;
background: rgba(0, 0, 0, 0.55);
color: #fff;
font-size: 28rpx;
}
.upload-add {
width: 180rpx;
height: 140rpx;
border: 2rpx dashed #d1d5db;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
color: #9ca3af;
}
.plus {
font-size: 52rpx;
line-height: 52rpx;
}
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
padding: 24rpx 28rpx 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: 88rpx;
line-height: 88rpx;
border-radius: 12rpx;
font-size: 30rpx;
}
.btn::after {
border: none;
}
.btn.plain {
background: #fff;
color: #4f6ef7;
border: 2rpx solid #4f6ef7;
}
.btn.primary {
background: #4f6ef7;
color: #fff;
}
.delete-fab {
position: fixed;
right: 32rpx;
bottom: calc(192rpx + env(safe-area-inset-bottom));
width: 104rpx;
height: 104rpx;
border-radius: 52rpx;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 20rpx 36rpx rgba(0, 0, 0, 0.12);
z-index: 30;
}
</style>