ykt-wxapp/pages/case/visit-record-detail.vue
2026-02-06 18:04:34 +08:00

537 lines
14 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">
<!-- 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="form-wrap">
<FormTemplate v-if="temp" 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 v-if="!isPdfUrl(f.url)" class="upload-thumb" :src="f.url" mode="aspectFill" />
<view v-else class="upload-pdf">PDF</view>
<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 api from '@/utils/api';
import { uploadFile } from '@/utils/file';
import useAccountStore from '@/store/account';
import { toast, confirm, loading as uniLoading, hideLoading } from '@/utils/widget';
import { normalizeVisitRecordFormData } from './utils/visit-record';
import { normalizeTemplate, unwrapTemplateResponse } from './utils/template';
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 temp = ref(null);
const titleText = computed(() => {
const t = temp.value || {};
return String(t.name || t.templateName || t.templateTypeName || '').trim();
});
const detail = ref({});
const form = reactive({});
const forms = computed(() => ({ ...detail.value, ...form }));
const showItems = computed(() => {
const list = temp.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 = temp.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 = [];
}
async function loadTemplate(t) {
const corpId = getCorpId();
if (!corpId) return null;
try {
const res = await api('getCurrentTemplate', { corpId, templateType: t });
if (!res?.success) {
toast(res?.message || '获取模板失败');
return null;
}
const raw = unwrapTemplateResponse(res);
return normalizeTemplate(raw);
} catch (e) {
console.error('loadTemplate error:', e);
toast('获取模板失败');
return null;
}
}
onLoad(async (options) => {
memberId.value = options?.memberId || options?.archiveId || '';
recordId.value = options?.id || '';
templateType.value = options?.type || '';
if (recordId.value) {
await getDetail();
} else {
if (!templateType.value) templateType.value = 'outpatient';
temp.value = await loadTemplate(templateType.value);
if (temp.value?.templateType) templateType.value = String(temp.value.templateType);
ensureFilesField();
// 默认填充模板时间字段
const timeKey = temp.value?.service?.timeTitle || '';
if (timeKey && !form[timeKey]) form[timeKey] = dayjs().format('YYYY-MM-DD');
}
if (titleText.value) uni.setNavigationBarTitle({ title: titleText.value });
});
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;
detail.value = normalizeVisitRecordFormData(templateType.value, record);
ensureFilesField();
// 详情可能返回真实 templateType与模板保持一致
if (!temp.value || temp.value?.templateType !== templateType.value) {
temp.value = await loadTemplate(templateType.value);
if (temp.value?.templateType) templateType.value = String(temp.value.templateType);
if (titleText.value) uni.setNavigationBarTitle({ title: titleText.value });
}
} 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 = (temp.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 = temp.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('保存失败');
}
}
async function remove() {
try {
await confirm('确定删除当前记录?');
} catch {
return;
}
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 isPdfUrl(url) {
const u = String(url || '').toLowerCase();
return u.includes('.pdf') || u.startsWith('data:application/pdf');
}
async function addFiles() {
const fileConfig = temp.value?.templateList?.find((i) => i && (i.type === 'files' || i.title === 'files')) || {};
const maxSize = Number(fileConfig?.maxSize || 5) || 5; // MB
const accept = String(fileConfig?.accept || 'pdf') || 'pdf';
const chooseRes = await new Promise((resolve) => {
uni.chooseMessageFile({
count: 9,
type: 'file',
extension: accept ? [accept] : undefined,
success: (res) => resolve(res),
fail: () => resolve(null),
});
});
const files = Array.isArray(chooseRes?.tempFiles) ? chooseRes.tempFiles : [];
if (!files.length) return;
const maxBytes = maxSize * 1024 * 1024;
const invalidFiles = files.filter((f) => f && f.size > maxBytes);
if (invalidFiles.length > 0) {
toast(`文件大小不能超过${maxSize}M`);
return;
}
const cur = Array.isArray(forms.value.files) ? forms.value.files : [];
uniLoading('上传中...');
try {
const uploaded = [];
for (const f of files) {
const url = await uploadFile(f.path);
if (!url) {
toast('上传失败');
continue;
}
uploaded.push({ url, name: f.name || '', size: f.size });
}
form.files = [...cur, ...uploaded];
} finally {
hideLoading();
}
}
function removeFile(idx) {
const cur = Array.isArray(forms.value.files) ? forms.value.files : [];
form.files = cur.filter((_, i) => i !== idx);
}
function previewFile(idx) {
const f = fileList.value[idx];
const url = f?.url ? String(f.url) : '';
if (!url) return;
if (!isPdfUrl(url)) {
const urls = fileList.value.map((i) => i.url);
uni.previewImage({ urls, current: url });
return;
}
uniLoading('打开中...');
uni.downloadFile({
url,
success: (res) => {
hideLoading();
const filePath = res?.tempFilePath;
if (!filePath) return toast('打开失败');
uni.openDocument({
filePath,
showMenu: true,
fail: () => toast('打开失败'),
});
},
fail: () => {
hideLoading();
toast('打开失败');
},
});
}
</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-pdf {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 34rpx;
font-weight: 700;
color: #0877F1;
background: #eef6ff;
}
.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: #0877F1;
border: 2rpx solid #0877F1;
}
.btn.primary {
background: #0877F1;
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>