ykt-wxapp/pages/case/visit-record-view.vue
2026-02-11 17:45:09 +08:00

529 lines
16 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" :style="{ height: scrollHeight + 'px' }">
<view class="topbar">
<view class="topbar-text">{{ topText }}</view>
</view>
<view class="content">
<view class="section">
<view class="row">
<view class="label">病历类型</view>
<view class="value">{{ typeLabel }}</view>
</view>
<view class="row">
<view class="label">{{ visitDateLabel }}</view>
<view class="value">{{ visitDate || '--' }}</view>
</view>
<view v-if="showDiagnosisRow" class="row">
<view class="label">诊断</view>
<view class="value">{{ diagnosisText }}</view>
</view>
<view v-if="templateType === 'inhospital' && record.surgeryName" class="row">
<view class="label">手术名称</view>
<view class="value">{{ record.surgeryName }}</view>
</view>
</view>
<view v-for="(s, idx) in sections" :key="idx" class="section">
<view class="h2">{{ s.title }}</view>
<view class="p">{{ s.value }}</view>
</view>
<view v-if="showFilesSection" class="section">
<view class="h2">文件上传</view>
<view v-if="files.length" class="files">
<view v-for="(f, idx) in files" :key="idx" class="file" @click="preview(idx)">
<image class="thumb" :src="f.url" mode="aspectFill" />
</view>
</view>
<view v-else class="files-empty">暂无附件</view>
</view>
</view>
<view class="scroll-spacer" />
</scroll-view>
<view class="footer">
<button class="btn danger" @click="remove">删除</button>
<button class="btn primary" @click="edit">编辑</button>
</view>
</view>
</template>
<script setup>
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import dayjs from 'dayjs';
import api from '@/utils/api';
import { loading, hideLoading, toast } from '@/utils/widget';
import { getVisitRecordTemplate } from './components/archive-detail/templates';
import { normalizeVisitRecordFormData } from './utils/visit-record';
import { normalizeTemplate, unwrapTemplateResponse } from './utils/template';
import { normalizeFileUrl } from '@/utils/file';
const scrollHeight = ref(0);
const archiveId = ref('');
const id = ref('');
const medicalType = ref('');
const rawType = ref('');
const record = ref({});
const temp = ref(null);
const needReload = ref(false);
let recordChangedHandler = null;
const files = computed(() => {
const arr = record.value?.files;
return Array.isArray(arr)
? arr
.filter((i) => i && i.url)
.map((i) => ({ ...i, url: normalizeFileUrl(i.url) }))
: [];
});
function normalizeMedicalType(raw) {
const s = String(raw || '').trim();
if (!s) return '';
const lower = s.toLowerCase();
if (lower.includes('preconsultationrecord')) return 'preConsultationRecord';
if (lower.includes('preconsult') || (lower.includes('pre') && lower.includes('consult'))) return 'preConsultationRecord';
if (lower === 'outpatient' || lower === 'out_patient' || lower === 'out-patient') return 'outpatient';
if (lower === 'inhospital' || lower === 'in_hospital' || lower === 'in-hospital' || lower === 'inpatient') return 'inhospital';
if (lower === 'preconsultation' || lower === 'pre_consultation' || lower === 'pre-consultation') return 'preConsultationRecord';
if (lower === 'physicalexaminationtemplate' || lower === 'physicalexamination' || lower === 'physical_examination') return 'physicalExaminationTemplate';
if (s === 'outPatient') return 'outpatient';
if (s === 'inHospital') return 'inhospital';
if (s === 'preConsultation') return 'preConsultationRecord';
if (s === 'preConsultationRecord') return 'preConsultationRecord';
if (s === 'physicalExaminationTemplate') return 'physicalExaminationTemplate';
return s;
}
const templateType = computed(() => normalizeMedicalType(rawType.value || medicalType.value || record.value?.templateType || record.value?.medicalType || ''));
const typeLabel = computed(() => record.value?.tempName || temp.value?.name || getVisitRecordTemplate(templateType.value || medicalType.value)?.templateName || '病历');
function getDefaultTimeTitle(t) {
if (t === 'outpatient') return 'visitTime';
if (t === 'inhospital') return 'inhosDate';
if (t === 'preConsultationRecord') return 'consultationDate';
if (t === 'physicalExaminationTemplate') return 'inspectDate';
return '';
}
function getDefaultTimeName(t) {
if (t === 'outpatient') return '就诊日期';
if (t === 'inhospital') return '入院日期';
if (t === 'preConsultationRecord') return '问诊日期';
if (t === 'physicalExaminationTemplate') return '体检日期';
return '日期';
}
function normalizeText(v) {
if (Array.isArray(v)) return v.filter((i) => i !== null && i !== undefined && String(i).trim()).join('');
if (v === 0) return '0';
if (v && typeof v === 'object') {
const o = v;
const candidate = o.label ?? o.name ?? o.text ?? o.title ?? o.value ?? o.code ?? '';
return candidate ? String(candidate) : '';
}
return v ? String(v) : '';
}
function formatPositiveFind(v, { withOpinion = false } = {}) {
if (Array.isArray(v)) {
const list = v
.map((i) => (i && typeof i === 'object' ? { category: i.category, opinion: i.opinion } : null))
.filter((i) => i && (i.category || i.opinion));
if (!list.length) return '';
if (!withOpinion) return list.map((i) => String(i.category || '').trim()).filter(Boolean).join('');
return list
.map((i) => {
const c = String(i.category || '').trim();
const o = String(i.opinion || '').trim();
if (c && o) return `${c}${o}`;
return c || o;
})
.filter(Boolean)
.join('\n');
}
return normalizeText(v);
}
function getCorpId() {
const team = uni.getStorageSync('ykt_case_current_team') || {};
return team?.corpId ? String(team.corpId) : '';
}
function parseAnyTimeMs(v) {
if (v === null || v === undefined) return 0;
if (typeof v === 'number') {
// 10位秒级时间戳
if (v > 1e9 && v < 1e12) return v * 1000;
return v;
}
const s = String(v).trim();
if (!s) return 0;
if (/^\d{10,13}$/.test(s)) return Number(s.length === 10 ? `${s}000` : s);
const d = dayjs(s);
return d.isValid() ? d.valueOf() : 0;
}
function formatAnyDate(v, fmt = 'YYYY-MM-DD') {
const ms = parseAnyTimeMs(v);
if (!ms) return '';
const d = dayjs(ms);
return d.isValid() ? d.format(fmt) : '';
}
const visitDate = computed(() => {
const t = templateType.value;
const timeTitle = temp.value?.service?.timeTitle || getDefaultTimeTitle(t);
const raw = timeTitle ? record.value?.[timeTitle] : (record.value?.dateStr ?? record.value?.sortTime ?? '');
return formatAnyDate(raw) || normalizeText(raw) || '';
});
const visitDateLabel = computed(() => {
const t = templateType.value;
return String(temp.value?.service?.timeName || getDefaultTimeName(t) || '日期');
});
const showDiagnosisRow = computed(() => {
const list = Array.isArray(temp.value?.templateList)
? temp.value.templateList
: Array.isArray(getVisitRecordTemplate(templateType.value)?.templateList)
? getVisitRecordTemplate(templateType.value).templateList
: [];
return list.some((i) => i && (i.title === 'diagnosis' || i.title === 'diagnosisName'));
});
const diagnosisText = computed(() => {
const t = templateType.value;
if (!showDiagnosisRow.value) return '--';
if (t === 'outpatient' || t === 'inhospital') return normalizeText(record.value?.diagnosisName || record.value?.diagnosis) || normalizeText(record.value?.summary) || '--';
return normalizeText(record.value?.diagnosisName || record.value?.diagnosis || record.value?.summary) || '--';
});
const showFilesSection = computed(() => {
if (files.value.length) return true;
const list = Array.isArray(temp.value?.templateList)
? temp.value.templateList
: Array.isArray(getVisitRecordTemplate(templateType.value)?.templateList)
? getVisitRecordTemplate(templateType.value).templateList
: [];
return list.some((i) => i && (i.type === 'files' || i.title === 'files'));
});
const sections = computed(() => {
const t = templateType.value;
const hiddenKeys = new Set(t === 'outpatient'
? ['corp', 'deptName', 'corpName', 'doctor']
: t === 'inhospital'
? ['corp', 'corpName']
: t === 'physicalExaminationTemplate'
? ['inspectPakageName']
: []);
const list = [];
const pushedNames = new Set();
const pushRow = (name, value) => {
const v = normalizeText(value);
if (!v.trim()) return;
if (pushedNames.has(name)) return;
pushedNames.add(name);
list.push({ title: name, value: v });
};
const resolveOptionLabel = (item, candidate) => {
const range = Array.isArray(item?.range) ? item.range : [];
if (!range.length) return normalizeText(candidate);
const isObjectRange = range[0] && typeof range[0] === 'object';
const toLabel = (v) => {
const s = normalizeText(v);
if (!s) return '';
if (!isObjectRange) return s;
const found = range.find((opt) => opt && typeof opt === 'object' && String(opt.value) === String(s));
return found ? String(found.label ?? found.value ?? s) : s;
};
if (Array.isArray(candidate)) return candidate.map(toLabel).filter((i) => String(i).trim()).join('');
return toLabel(candidate);
};
const templateList = Array.isArray(temp.value?.templateList)
? temp.value.templateList
: Array.isArray(getVisitRecordTemplate(t)?.templateList)
? getVisitRecordTemplate(t).templateList
: [];
const timeTitle = temp.value?.service?.timeTitle || getDefaultTimeTitle(t);
templateList.forEach((item) => {
const key = item?.title ? String(item.title) : '';
if (!key) return;
if (key === 'files') return;
if (key === 'diagnosis' || key === 'diagnosisName') return;
if (timeTitle && key === timeTitle) return;
if (hiddenKeys.has(key)) return;
const raw = record.value?.[key];
const display = key === 'positiveFind'
? formatPositiveFind(raw, { withOpinion: true })
: item?.type === 'date'
? (formatAnyDate(raw) || normalizeText(raw))
: resolveOptionLabel(item, raw);
pushRow(String(item?.name || key), display);
});
return list;
});
async function loadTemplate(t) {
const corpId = getCorpId();
if (!corpId || !t) return null;
try {
const res = await api('getCurrentTemplate', { corpId, templateType: t });
if (!res?.success) return null;
const raw = unwrapTemplateResponse(res);
return normalizeTemplate(raw);
} catch (e) {
return null;
}
}
const topText = computed(() => {
const time = record.value?.createTime ? dayjs(record.value.createTime).format('YYYY-MM-DD HH:mm') : '';
const rawName = record.value?.creatorName ? String(record.value.creatorName) : '';
const cleanName = ['-', '—', '--'].includes(rawName.trim()) ? '' : rawName.trim();
const byCustomer = record.value?.ignore === 'checkIn';
const suffix = byCustomer ? '患者自建' : cleanName ? `${cleanName}代建` : record.value?.creator ? '员工代建' : '';
return suffix ? `${time || '--'} ${suffix}` : `${time || '--'}`;
});
async function fetchRecord({ silent = false } = {}) {
if (!archiveId.value || !id.value || !medicalType.value) return false;
const corpId = getCorpId();
if (!corpId) {
if (!silent) toast('缺少 corpId');
return false;
}
if (!silent) loading('加载中...');
try {
const res = await api('getMedicalRecordById', {
_id: id.value,
corpId,
memberId: archiveId.value,
medicalType: medicalType.value,
});
const r = res?.record || res?.data?.record || null;
if (!res?.success || !r) return false;
const raw = String(r?.templateType || r?.medicalType || medicalType.value || '');
rawType.value = raw;
const ui = normalizeMedicalType(raw);
record.value = normalizeVisitRecordFormData(ui, r);
temp.value = await loadTemplate(raw);
uni.setNavigationBarTitle({ title: String(typeLabel.value || '病历详情') });
return true;
} catch (error) {
console.error('获取病历记录失败:', error);
if (!silent) toast('加载失败');
return false;
} finally {
if (!silent) hideLoading();
}
}
onLoad(async (opt) => {
try {
const { windowHeight } = uni.getSystemInfoSync();
scrollHeight.value = windowHeight || 0;
} catch {
scrollHeight.value = 0;
}
archiveId.value = opt?.archiveId ? String(opt.archiveId) : '';
id.value = opt?.id ? String(opt.id) : '';
medicalType.value = opt?.type ? String(opt.type) : '';
if (!archiveId.value || !id.value || !medicalType.value) {
toast('参数缺失');
setTimeout(() => uni.navigateBack(), 300);
return;
}
const ok = await fetchRecord();
if (!ok) {
toast('记录不存在');
setTimeout(() => uni.navigateBack(), 300);
}
});
onMounted(() => {
recordChangedHandler = () => {
needReload.value = true;
};
// 页面在编辑页返回前可能收到事件:先标记,回到页面再刷新
uni.$on('archive-detail:visit-record-changed', recordChangedHandler);
});
onUnmounted(() => {
if (recordChangedHandler) uni.$off('archive-detail:visit-record-changed', recordChangedHandler);
recordChangedHandler = null;
});
onShow(async () => {
if (!needReload.value) return;
needReload.value = false;
await fetchRecord({ silent: true });
});
function preview(idx) {
const urls = files.value.map((i) => i.url);
uni.previewImage({ urls, current: urls[idx] });
}
function edit() {
const type = record.value?.templateType || record.value?.medicalType || '';
uni.navigateTo({
url: `/pages/case/visit-record-detail?archiveId=${encodeURIComponent(archiveId.value)}&id=${encodeURIComponent(id.value)}&type=${encodeURIComponent(type)}`,
});
}
function remove() {
uni.showModal({
title: '提示',
content: '确定删除当前记录?',
success: async (res) => {
if (!res.confirm) return;
loading('删除中...');
try {
const corpId = getCorpId();
if (!corpId) {
hideLoading();
return toast('缺少 corpId');
}
await api('removeMedicalRecord', { corpId, memberId: archiveId.value, medicalType: medicalType.value, _id: id.value });
hideLoading();
uni.$emit('archive-detail:visit-record-changed');
toast('已删除');
setTimeout(() => uni.navigateBack(), 300);
} catch (error) {
hideLoading();
console.error('删除病历记录失败:', error);
toast('删除失败');
}
},
});
}
</script>
<style scoped>
.page {
height: 100vh;
overflow: hidden;
background: #fff;
}
.scroll {
width: 100%;
}
.topbar {
background: #5d6df0;
padding: 20rpx 28rpx;
}
.topbar-text {
color: #fff;
font-size: 28rpx;
text-align: center;
}
.content {
padding: 28rpx 28rpx 0;
}
.section {
margin-bottom: 28rpx;
}
.row {
display: flex;
padding: 20rpx 0;
}
.label {
width: 180rpx;
font-size: 28rpx;
font-weight: 600;
color: #111827;
}
.value {
flex: 1;
font-size: 28rpx;
color: #111827;
word-break: break-all;
}
.h2 {
font-size: 28rpx;
font-weight: 700;
color: #111827;
padding: 16rpx 0;
}
.p {
font-size: 28rpx;
color: #111827;
line-height: 40rpx;
white-space: pre-wrap;
}
.files {
display: flex;
gap: 20rpx;
flex-wrap: wrap;
}
.file {
width: 180rpx;
height: 140rpx;
border: 2rpx solid #d1d5db;
background: #f9fafb;
}
.thumb {
width: 180rpx;
height: 140rpx;
}
.files-empty {
font-size: 26rpx;
color: #9aa0a6;
padding: 16rpx 0;
}
.scroll-spacer {
height: calc(152rpx + env(safe-area-inset-bottom));
}
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
padding: 24rpx 28rpx calc(24rpx + env(safe-area-inset-bottom));
display: flex;
justify-content: flex-end;
gap: 28rpx;
box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
}
.btn {
width: 240rpx;
height: 88rpx;
line-height: 88rpx;
border-radius: 12rpx;
font-size: 30rpx;
}
.btn::after {
border: none;
}
.btn.danger {
background: #fff;
color: #ff4d4f;
border: 2rpx solid #ff4d4f;
}
.btn.primary {
background: #0877F1;
color: #fff;
}
</style>