Merge commit '1018707e25082fb5b29b265ed55a9ea3499c44bf' into dev-wdb

This commit is contained in:
wangdongbo 2026-02-09 15:41:03 +08:00
commit 5c040e3f55
16 changed files with 451 additions and 354 deletions

View File

@ -11,6 +11,11 @@
top: 0;
}
.form-cell-required {
margin-left: 4rpx;
color: #ff4d4f;
}
.form-content__wrapper {
width: 100%;
display: flex;
@ -58,4 +63,4 @@
.form-row__content {
width: 0;
flex-grow: 1;
}
}

View File

@ -1,7 +1,7 @@
<template>
<view class="form-row" @click="handleClick">
<view class="form-row__label">
{{ name }}<text v-if="required" class="form-cell--required"></text>
{{ name }}<text v-if="required" class="form-cell-required">*</text>
</view>
<view class="form-row__content">
<slot></slot>

View File

@ -1,6 +1,8 @@
<template>
<view class="files-wrap">
<view class="files-label" :class="required ? 'form-cell--required' : ''">{{ name }}</view>
<view class="files-label">
{{ name }}<text v-if="required" class="form-cell-required">*</text>
</view>
<view class="grid">
<view v-for="(f, idx) in files" :key="idx" class="item" @click="preview(idx)">
<image class="thumb" :src="f.url" mode="aspectFill" />
@ -15,7 +17,7 @@
<script setup>
import { computed } from 'vue';
import { chooseAndUploadImage } from '@/utils/file';
import { chooseAndUploadImage, normalizeFileUrl } from '@/utils/file';
import { toast } from '@/utils/widget';
const emits = defineEmits(['change']);
@ -38,13 +40,13 @@ const files = computed(() => {
if (Array.isArray(v)) {
return v
.map((i) => {
if (typeof i === 'string') return { url: i };
if (i && typeof i === 'object' && i.url) return { url: String(i.url) };
if (typeof i === 'string') return { url: normalizeFileUrl(i) };
if (i && typeof i === 'object' && i.url) return { url: normalizeFileUrl(String(i.url)) };
return null;
})
.filter(Boolean);
}
if (typeof v === 'string' && v) return [{ url: v }];
if (typeof v === 'string' && v) return [{ url: normalizeFileUrl(v) }];
return [];
});
@ -141,4 +143,3 @@ async function add() {
line-height: 56rpx;
}
</style>

View File

@ -1,6 +1,8 @@
<template>
<view class="multi-wrap">
<view class="label" :class="required ? 'form-cell--required' : ''">{{ name }}</view>
<view class="label">
{{ name }}<text v-if="required" class="form-cell-required">*</text>
</view>
<view class="options" :class="hasOtherSelected ? 'with-other' : ''">
<view
v-for="opt in displayOptions"
@ -165,4 +167,3 @@ function onOtherInput(e) {
box-sizing: border-box;
}
</style>

View File

@ -1,6 +1,8 @@
<template>
<view class="form-row">
<view class="form-row__label" :class="required ? 'form-cell--required' : ''">{{ name }}</view>
<view class="form-row__label">
{{ name }}<text v-if="required" class="form-cell-required">*</text>
</view>
<view class="form-row__content runtime">
<input :disabled="disableChange" class="num" type="number" :value="year" @input="onInput($event, 'year')" />
<text class="unit"></text>
@ -86,4 +88,3 @@ function onInput(e, key) {
color: #333;
}
</style>

View File

@ -1,6 +1,8 @@
<template>
<view class="form-row">
<view class="form-row__label" :class="required ? 'form-cell--required' : ''">{{ name }}</view>
<view class="form-row__label">
{{ name }}<text v-if="required" class="form-cell-required">*</text>
</view>
<view class="form-row__content content">
<input
:disabled="disableChange"

View File

@ -1,7 +1,7 @@
<template>
<view class="textarea-row">
<view class="form-row__label">
{{ name }}<text v-if="required" class="form-cell--required"></text>
{{ name }}<text v-if="required" class="form-cell-required">*</text>
</view>
<view class="mt-10">
<textarea
@ -94,6 +94,8 @@ function change(e) {
</script>
<style lang="scss" scoped>
@import '../cell-style.css';
.textarea-row {
padding: 24rpx 30rpx;
border-bottom: 1px solid #eee;

View File

@ -7,7 +7,7 @@
@change="change"
/>
<form-diagnosis-picker
v-else-if="attrs.title === 'diagnosis' || attrs.title === 'diagnosisName'"
v-else-if="attrs.type === 'diagnosis' || attrs.__originType === 'diagnosis' || attrs.title === 'diagnosis' || attrs.title === 'diagnosisName'"
v-bind="attrs"
:form="form"
:disableChange="disableChange"

View File

@ -71,6 +71,8 @@ import dayjs from 'dayjs';
import api from '@/utils/api';
import { loading, hideLoading, toast } from '@/utils/widget';
import { normalizeTemplate } from '../../utils/template';
import { normalizeVisitRecordFormData } from '../../utils/visit-record';
import { normalizeFileUrl } from '@/utils/file';
const props = defineProps({
data: { type: Object, default: () => ({}) },
@ -128,16 +130,25 @@ function getCorpId() {
}
const loadedCorpId = ref('');
let loadVisitTemplatesPromise = null;
let loadVisitTemplatesCorpId = '';
async function loadVisitTemplates() {
const corpId = getCorpId();
if (!corpId) return;
if (loadedCorpId.value === corpId && templates.value.length) return;
loadedCorpId.value = corpId;
if (loadVisitTemplatesPromise && loadVisitTemplatesCorpId === corpId) return loadVisitTemplatesPromise;
try {
loadVisitTemplatesCorpId = corpId;
loadVisitTemplatesPromise = (async () => {
const groupRes = await api('getTemplateGroup', { corpId, parentType: 'medicalRecord' });
const group = groupRes?.data && Array.isArray(groupRes.data?.data) ? groupRes.data.data : Array.isArray(groupRes?.data) ? groupRes.data : [];
const list = Array.isArray(group) ? group : [];
const groupNameMap = list.reduce((m, i) => {
const t = i?.templateType ? String(i.templateType) : '';
const name = i?.name ? String(i.name) : '';
if (t && name) m[t] = name;
return m;
}, {});
const enabled = list.filter((i) => i && i.templateType !== 'healthTemplate' && i.templateStatus !== 'disable');
const typeList = enabled.map((i) => String(i.templateType || '')).filter(Boolean);
if (!typeList.length) return;
@ -155,9 +166,10 @@ async function loadVisitTemplates() {
const next = ordered
.map((t) => {
const temp = normalizeTemplate(t);
const name = String(temp?.name || temp?.templateName || temp?.templateTypeName || '') || String(temp?.templateType || '');
const rawType = String(temp?.templateType || '');
const name = String(groupNameMap[rawType] || temp?.name || temp?.templateName || temp?.templateTypeName || '') || rawType;
return {
templateType: String(temp?.templateType || ''),
templateType: rawType,
name,
service: temp?.service || {},
templateList: Array.isArray(temp?.templateList) ? temp.templateList : [],
@ -165,10 +177,22 @@ async function loadVisitTemplates() {
})
.filter((i) => i && i.templateType);
if (next.length) templates.value = next;
} catch (e) {
console.error('loadVisitTemplates error:', e);
}
if (next.length) {
templates.value = next;
loadedCorpId.value = corpId;
}
})()
.catch((e) => {
console.error('loadVisitTemplates error:', e);
})
.finally(() => {
if (loadVisitTemplatesCorpId === corpId) {
loadVisitTemplatesPromise = null;
loadVisitTemplatesCorpId = '';
}
});
return loadVisitTemplatesPromise;
}
const userNameMap = ref({});
@ -201,12 +225,14 @@ async function loadTeamMembers() {
}
function getSortTimeTitle(templateType) {
const t = templateMap.value[String(templateType || '')] || {};
const rawType = String(templateType || '');
const t = templateMap.value[rawType] || {};
if (t?.service?.timeTitle) return String(t.service.timeTitle);
if (templateType === 'outpatient') return 'visitTime';
if (templateType === 'inhospital') return 'inhosDate';
if (templateType === 'preConsultation') return 'consultDate';
if (templateType === 'physicalExaminationTemplate') return 'inspectDate';
const ui = normalizeMedicalType(rawType);
if (ui === 'outpatient') return 'visitTime';
if (ui === 'inhospital') return 'inhosDate';
if (ui === 'preConsultation') return 'consultDate';
if (ui === 'physicalExaminationTemplate') return 'inspectDate';
return '';
}
@ -242,15 +268,48 @@ function getTemplateName(type) {
}
function toDateStr(sortTime) {
if (!sortTime) return '';
const d = dayjs(sortTime);
return d.isValid() ? d.format('YYYY-MM-DD') : '';
return formatAnyDate(sortTime, 'YYYY-MM-DD');
}
function normalizeMedicalType(raw) {
const s = String(raw || '').trim();
if (!s) return '';
const lower = s.toLowerCase();
if (lower.includes('preconsult') || (lower.includes('pre') && lower.includes('consult'))) return 'preConsultation';
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 'preConsultation';
if (lower === 'physicalexaminationtemplate' || lower === 'physicalexamination' || lower === 'physical_examination') return 'physicalExaminationTemplate';
if (s === 'outPatient') return 'outpatient';
if (s === 'inHospital') return 'inhospital';
if (s === 'preConsultation') return 'preConsultation';
if (s === 'physicalExaminationTemplate') return 'physicalExaminationTemplate';
return s;
}
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) : '';
}
function toDateTimeStr(ts) {
if (!ts) return '';
const d = dayjs(ts);
return d.isValid() ? d.format('YYYY-MM-DD HH:mm') : '';
return formatAnyDate(ts, 'YYYY-MM-DD HH:mm');
}
async function refreshList() {
@ -285,21 +344,31 @@ async function refreshList() {
? res.list
: Array.isArray(res?.data?.list)
? res.data.list
: Array.isArray(res?.data?.data)
? res.data.data
: Array.isArray(res?.data?.data?.list)
? res.data.data.list
: Array.isArray(res?.data)
? res.data
: [];
if (list.length) {
const mapped = list.map((r) => {
const t = String(r?.medicalType || r?.templateType || '') || '';
const timeTitle = getSortTimeTitle(t);
const dateStr = timeTitle ? normalizeText(r?.[timeTitle]) : '';
const rawType = String(r?.medicalType || r?.templateType || '') || '';
const uiType = normalizeMedicalType(rawType);
const normalized = normalizeVisitRecordFormData(uiType, r);
const timeTitle = getSortTimeTitle(rawType);
const rawTime = timeTitle ? (normalized?.[timeTitle] ?? r?.[timeTitle]) : '';
const dateStr = rawTime ? formatAnyDate(rawTime, 'YYYY-MM-DD') : '';
return {
...r,
templateType: t,
...normalized,
medicalType: rawType,
templateType: uiType,
rawTemplateType: rawType,
dateStr: dateStr || toDateStr(r?.sortTime),
createDateStr: r?.createTime ? dayjs(r.createTime).format('YYYY-MM-DD') : '',
createDateStr: r?.createTime ? formatAnyDate(r.createTime, 'YYYY-MM-DD') : '',
createTimeStr: toDateTimeStr(r?.createTime),
tempName: r?.tempName || getTemplateName(t) || '病历',
tempName: r?.tempName || getTemplateName(rawType) || '病历',
};
});
@ -324,9 +393,21 @@ const tagClass = {
physicalExaminationTemplate: 'bg-green',
};
function resolveRecordType(r) {
if (!r) return '';
const direct = normalizeMedicalType(r.uiType || r.templateType || r.rawTemplateType || r.medicalType || '');
if (direct) return direct;
// fallback by known fields
if (r.inspectDate || r.positiveFind || r.inspectSummary) return 'physicalExaminationTemplate';
if (r.inhosDate || r.surgeryName || r.surgeryDate || r.operationDate) return 'inhospital';
if (r.visitTime || r.disposePlan || r.treatmentPlan) return 'outpatient';
if (r.consultDate || r.presentIllness || r.presentIllnessHistory || r.pastHistory) return 'preConsultation';
return '';
}
function getDiagnosis(r) {
if (!r) return '--';
const t = r.templateType || r.medicalType;
const t = resolveRecordType(r);
if (t === 'preConsultation') return normalizeText(r.chiefComplaint) || normalizeText(r.summary) || '--';
if (t === 'physicalExaminationTemplate') return formatPositiveFind(r.positiveFind) || normalizeText(r.summary) || '--';
if (t === 'outpatient' || t === 'inhospital') return normalizeText(r.diagnosisName || r.diagnosis) || normalizeText(r.summary) || '--';
@ -339,7 +420,7 @@ function firstLine(v) {
}
function getDisplayLines(r) {
const t = r?.templateType || r?.medicalType;
const t = resolveRecordType(r);
if (t === 'outpatient') {
return [{ label: '门诊诊断:', value: firstLine(r.diagnosisName || r.diagnosis) }];
}
@ -350,12 +431,12 @@ function getDisplayLines(r) {
return lines;
}
if (t === 'physicalExaminationTemplate') {
return [{ label: '体检小结:', value: firstLine(r.summary) }];
return [{ label: '体检小结:', value: firstLine(r.summary || r.inspectSummary) }];
}
if (t === 'preConsultation') {
const lines = [
{ label: '主诉:', value: firstLine(r.chiefComplaint) },
{ label: '现病史:', value: firstLine(r.presentIllness) },
{ label: '现病史:', value: firstLine(r.presentIllness || r.presentIllnessHistory) },
];
const past = normalizeText(r.pastHistory);
if (past) lines.push({ label: '既往史:', value: past });
@ -386,7 +467,11 @@ function pickTimeRange(val) {
function getFiles(r) {
const arr = r?.files;
return Array.isArray(arr) ? arr.filter((i) => i && i.url) : [];
return Array.isArray(arr)
? arr
.filter((i) => i && i.url)
.map((i) => ({ ...i, url: normalizeFileUrl(i.url) }))
: [];
}
function previewFiles(r, idx) {
@ -455,10 +540,6 @@ function edit(record) {
}
onMounted(() => {
// + picker
loadVisitTemplates();
// archiveId watch
refreshList();
uni.$on('archive-detail:visit-record-changed', refreshList);
});

View File

@ -8,9 +8,9 @@ export const VISIT_RECORD_TEMPLATES = [
{ title: 'chiefComplaint', name: '主诉', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200, placeholder: '请输入或说话录音转录问题', rows: 1, autoHeight: true },
{ title: 'medicalHistory', name: '病史概要', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 500, placeholder: '请简述患者情况,量键既往病史、用药史等', rows: 3, autoHeight: true },
{ title: 'examination', name: '检查', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 500, placeholder: '请填写关键项目或异常结果描述', rows: 1, autoHeight: true },
{ title: 'diagnosis', name: '诊断', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200, placeholder: '请填写诊断名称', supportVoice: true, rows: 1, autoHeight: true },
{ title: 'diagnosis', name: '诊断', type: 'diagnosis', operateType: 'formCell', required: false, wordLimit: 200, placeholder: '请选择诊断', supportVoice: true, rows: 1, autoHeight: true },
{ title: 'treatmentPlan', name: '治疗方案', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 1000, placeholder: '请简述治疗方案及效果', rows: 3, autoHeight: true },
{ title: 'files', name: '文件上传', type: 'files', required: false, desc: '(支持≤5M文件pdf文件格式)', maxSize: 5, accept: 'pdf' },
{ title: 'files', name: '文件上传', type: 'files', required: false, desc: '(支持拍摄/相册选择图片,≤5M)', maxSize: 5, accept: 'image' },
],
},
{
@ -22,11 +22,11 @@ export const VISIT_RECORD_TEMPLATES = [
{ title: 'chiefComplaint', name: '主诉', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200, placeholder: '请输入病症与转诊问题', rows: 1, autoHeight: true },
{ title: 'medicalHistory', name: '病史概要', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 500, placeholder: '请简述患者情况,量键既往病史、用药史等症状', rows: 3, autoHeight: true },
{ title: 'examination', name: '检查', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 500, placeholder: '请填写关键项目或异常结果描述', rows: 1, autoHeight: true },
{ title: 'diagnosis', name: '住院主诊断', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200, placeholder: '请填写诊断名称', rows: 1, autoHeight: true },
{ title: 'diagnosis', name: '住院主诊断', type: 'diagnosis', operateType: 'formCell', required: false, wordLimit: 200, placeholder: '请选择诊断', rows: 1, autoHeight: true },
{ title: 'surgeryDate', name: '手术日期', type: 'date', operateType: 'formCell', required: false, format: 'YYYY-MM-DD', placeholder: '请选择手术日期' },
{ title: 'surgeryName', name: '手术名称', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 100, placeholder: '请填写手术名称', rows: 1, autoHeight: true },
{ title: 'treatmentPlan', name: '治疗方案', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 1000, placeholder: '请简述治疗方案及效果', rows: 3, autoHeight: true },
{ title: 'files', name: '文件上传', type: 'files', required: false, desc: '(支持≤5M文件pdf文件格式)', maxSize: 5, accept: 'pdf' },
{ title: 'files', name: '文件上传', type: 'files', required: false, desc: '(支持拍摄/相册选择图片,≤5M)', maxSize: 5, accept: 'image' },
],
},
{
@ -37,7 +37,7 @@ export const VISIT_RECORD_TEMPLATES = [
{ title: 'chiefComplaint', name: '主诉', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200, placeholder: '请输入症状及转诊问题', rows: 1, autoHeight: true },
{ title: 'presentIllness', name: '现病史', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 800, placeholder: '请述述发病的过程、发展、诊疗经过及当前病情', rows: 3, autoHeight: true },
{ title: 'pastHistory', name: '既往史', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 800, placeholder: '请填写既往疾病、手术/外伤史、药物/食物过敏史', rows: 3, autoHeight: true },
{ title: 'files', name: '文件上传', type: 'files', required: false, desc: '(支持≤5M文件pdf文件格式)', maxSize: 5, accept: 'pdf' },
{ title: 'files', name: '文件上传', type: 'files', required: false, desc: '(支持拍摄/相册选择图片,≤5M)', maxSize: 5, accept: 'image' },
],
},
{
@ -48,7 +48,7 @@ export const VISIT_RECORD_TEMPLATES = [
{ title: 'inspectDate', name: '体检日期', type: 'date', operateType: 'formCell', required: false, format: 'YYYY-MM-DD', placeholder: '请选择体检日期' },
{ title: 'summary', name: '体检小结', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200, placeholder: '请填写本次体检的小结', rows: 3, autoHeight: true },
{ title: 'positiveFind', name: '阳性发现及处理意见', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 500, placeholder: '可参照医客通现有模式', rows: 3, refNote: '可参照医客通现有模式', autoHeight: true },
{ title: 'files', name: '文件上传', type: 'files', required: false, desc: '(支持≤5M文件pdf文件格式)', maxSize: 5, accept: 'pdf' },
{ title: 'files', name: '文件上传', type: 'files', required: false, desc: '(支持拍摄/相册选择图片,≤5M)', maxSize: 5, accept: 'image' },
],
},
];

View File

@ -29,7 +29,7 @@ export function normalizeTemplateItem(item) {
selectWwuser: 'select',
files: 'files',
corpProject: 'select',
diagnosis: 'textarea',
diagnosis: 'diagnosis',
BMI: 'input',
bloodPressure: 'textarea',
selfMultipleDiseases: 'textarea',
@ -92,4 +92,3 @@ export function normalizeTemplate(temp) {
.map(normalizeTemplateItem);
return t;
}

View File

@ -7,24 +7,6 @@
<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>
@ -47,7 +29,6 @@ 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';
@ -92,11 +73,34 @@ const titleText = computed(() => {
const detail = ref({});
const form = reactive({});
const forms = computed(() => ({ ...detail.value, ...form }));
const HIDDEN_FIELD_NAMES = {
outpatient: ['就诊机构', '就诊科室', '机构名称', '责任医生'],
inhospital: ['就诊机构', '机构名称'],
};
const HIDDEN_FIELD_TITLES = {
// systemFieldName/title
// - : corp
// - : deptName
// - : corpName
// - : doctor
outpatient: ['corp', 'deptName', 'corpName', 'doctor'],
// - : corp
// - : corpName
inhospital: ['corp', 'corpName'],
};
function shouldHideField(item) {
const t = String(templateType.value || '');
const name = item?.name ? String(item.name).trim() : '';
const title = item?.title ? String(item.title).trim() : '';
const hiddenNames = HIDDEN_FIELD_NAMES[t] || [];
const hiddenTitles = HIDDEN_FIELD_TITLES[t] || [];
return (name && hiddenNames.includes(name)) || (title && hiddenTitles.includes(title));
}
const showItems = computed(() => {
const list = temp.value?.templateList || [];
// referenceField mobile
return list.filter((i) => {
if (i?.type === 'files') return false;
if (shouldHideField(i)) return false;
if (i && typeof i.referenceField === 'string') {
return forms.value[i.referenceField] === i.referenceValue;
}
@ -104,16 +108,7 @@ const showItems = computed(() => {
});
});
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;
@ -300,86 +295,6 @@ async function remove() {
}
}
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>
@ -412,82 +327,6 @@ function previewFile(idx) {
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;

View File

@ -12,10 +12,10 @@
<view class="value">{{ typeLabel }}</view>
</view>
<view class="row">
<view class="label">就诊日期</view>
<view class="label">{{ visitDateLabel }}</view>
<view class="value">{{ visitDate || '--' }}</view>
</view>
<view class="row">
<view v-if="showDiagnosisRow" class="row">
<view class="label">诊断</view>
<view class="value">{{ diagnosisText }}</view>
</view>
@ -30,7 +30,7 @@
<view class="p">{{ s.value }}</view>
</view>
<view class="section">
<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)">
@ -55,24 +55,70 @@ 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 archiveId = ref('');
const id = ref('');
const medicalType = ref('');
const rawType = ref('');
const record = ref({});
const temp = ref(null);
const files = computed(() => {
const arr = record.value?.files;
return Array.isArray(arr) ? arr.filter((i) => i && i.url) : [];
return Array.isArray(arr)
? arr
.filter((i) => i && i.url)
.map((i) => ({ ...i, url: normalizeFileUrl(i.url) }))
: [];
});
const templateType = computed(() => record.value?.templateType || record.value?.medicalType || '');
function normalizeMedicalType(raw) {
const s = String(raw || '').trim();
if (!s) return '';
const lower = s.toLowerCase();
if (lower.includes('preconsult') || (lower.includes('pre') && lower.includes('consult'))) return 'preConsultation';
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 'preConsultation';
if (lower === 'physicalexaminationtemplate' || lower === 'physicalexamination' || lower === 'physical_examination') return 'physicalExaminationTemplate';
if (s === 'outPatient') return 'outpatient';
if (s === 'inHospital') return 'inhospital';
if (s === 'preConsultation') return 'preConsultation';
if (s === 'physicalExaminationTemplate') return 'physicalExaminationTemplate';
return s;
}
const typeLabel = computed(() => record.value?.tempName || getVisitRecordTemplate(templateType.value || medicalType.value)?.templateName || '病历');
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 === 'preConsultation') return 'consultDate';
if (t === 'physicalExaminationTemplate') return 'inspectDate';
return '';
}
function getDefaultTimeName(t) {
if (t === 'outpatient') return '就诊日期';
if (t === 'inhospital') return '入院日期';
if (t === 'preConsultation') 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) : '';
}
@ -101,68 +147,139 @@ function getCorpId() {
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;
if (t === 'outpatient') return record.value?.visitTime || '';
if (t === 'inhospital') return record.value?.inhosDate || '';
if (t === 'preConsultation') return record.value?.consultDate || '';
if (t === 'physicalExaminationTemplate') return record.value?.inspectDate || '';
return record.value?.dateStr || '';
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 (t === 'preConsultation') return normalizeText(record.value?.chiefComplaint) || normalizeText(record.value?.summary) || '--';
if (t === 'physicalExaminationTemplate') return formatPositiveFind(record.value?.positiveFind) || normalizeText(record.value?.summary) || '--';
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 push = (title, value) => {
const v = value === 0 ? '0' : value ? String(value) : '';
if (!v.trim()) return;
return { title, value: v };
};
const hiddenKeys = new Set(t === 'outpatient'
? ['corp', 'deptName', 'corpName', 'doctor']
: t === 'inhospital'
? ['corp', 'corpName']
: []);
const list = [];
if (t === 'outpatient') {
const corp = push('就诊机构', record.value?.corpName);
const dept = push('科室', record.value?.deptName);
const doctor = push('医生', record.value?.doctor);
const treatment = push('治疗方案', record.value?.treatmentPlan);
const dispose = push('处置计划', record.value?.disposePlan);
const summary = push('备注/摘要', record.value?.summary);
[corp, dept, doctor, treatment, dispose, summary].forEach((i) => i && list.push(i));
return list;
}
if (t === 'inhospital') {
const corp = push('住院机构', record.value?.corpName);
const summary = push('摘要', record.value?.summary);
[corp, summary].forEach((i) => i && list.push(i));
return list;
}
if (t === 'preConsultation') {
const illness = push('现病史', record.value?.presentIllness);
const past = push('既往史', record.value?.pastHistory);
const allergy = push('过敏史', record.value?.allergyHistory);
const summary = push('摘要', record.value?.summary);
[illness, past, allergy, summary].forEach((i) => i && list.push(i));
return list;
}
if (t === 'physicalExaminationTemplate') {
const corp = push('体检机构', record.value?.corpName);
const pkg = push('体检套餐', record.value?.inspectPakageName);
const positive = push('阳性发现及处理意见', formatPositiveFind(record.value?.positiveFind, { withOpinion: true }));
const summary = push('摘要', record.value?.summary);
[corp, pkg, positive, summary].forEach((i) => i && list.push(i));
return list;
}
const summary = push('摘要', record.value?.summary);
if (summary) list.push(summary);
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) : '';
@ -206,13 +323,11 @@ onLoad(async (opt) => {
setTimeout(() => uni.navigateBack(), 300);
return;
}
// wxapp 使 diagnosis diagnosisName
if ((r.medicalType === 'outpatient' || r.medicalType === 'inhospital') && !r.diagnosis && r.diagnosisName) {
r.diagnosis = r.diagnosisName;
}
record.value = r;
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 || '病历详情') });
} catch (error) {
hideLoading();

View File

@ -705,40 +705,63 @@ const toggleBatchMode = () => {
const handleCreate = () => {
if (checkBatchMode()) return;
// 100 ->
if (managedArchiveCountAllTeams.value >= 100) {
uni.showModal({
title: '提示',
content: '当前管理档案数已达 100 个,无法继续新增。如需提升档案管理数,请联系客服处理。',
cancelText: '知道了',
confirmText: '添加客服',
success: (res) => {
if (res.confirm) {
openAddCustomerServiceEntry();
}
}
});
return;
}
const rawMax = doctorInfo.value?.maxCustomerArchive;
const hasMaxField = rawMax !== undefined && rawMax !== null && String(rawMax).trim() !== '';
const maxCustomerArchive = hasMaxField ? Number(rawMax) : NaN;
// + 10
if (!isVerified.value && managedArchiveCountAllTeams.value >= 10) {
if (verifyStatus.value === 'verifying') {
toast('信息认证中,请耐心等待!');
// maxCustomerArchive
// -1 = 沿10/100
if (hasMaxField && Number.isFinite(maxCustomerArchive)) {
if (maxCustomerArchive !== -1 && managedArchiveCountAllTeams.value >= maxCustomerArchive) {
uni.showModal({
title: '提示',
content: `当前管理档案数已达上限 ${maxCustomerArchive} 个,无法继续新增。如需提升档案管理数,请联系客服处理。`,
cancelText: '知道了',
confirmText: '添加客服',
success: (res) => {
if (res.confirm) {
openAddCustomerServiceEntry();
}
}
});
return;
}
uni.showModal({
title: '提示',
content: '当前管理档案数已达上限 10 个,完成认证即可升级至 100 个。',
cancelText: '暂不认证',
confirmText: '去认证',
success: (res) => {
if (res.confirm) {
startVerifyFlow();
} else {
// 100 ->
if (managedArchiveCountAllTeams.value >= 100) {
uni.showModal({
title: '提示',
content: '当前管理档案数已达 100 个,无法继续新增。如需提升档案管理数,请联系客服处理。',
cancelText: '知道了',
confirmText: '添加客服',
success: (res) => {
if (res.confirm) {
openAddCustomerServiceEntry();
}
}
});
return;
}
// + 10
if (!isVerified.value && managedArchiveCountAllTeams.value >= 10) {
if (verifyStatus.value === 'verifying') {
toast('信息认证中,请耐心等待!');
return;
}
});
return;
uni.showModal({
title: '提示',
content: '当前管理档案数已达上限 10 个,完成认证即可升级至 100 个。',
cancelText: '暂不认证',
confirmText: '去认证',
success: (res) => {
if (res.confirm) {
startVerifyFlow();
}
}
});
return;
}
}
//

View File

@ -9,7 +9,7 @@
<view class="label">{{ item.label }}</view>
<uni-icons :type="selectedMap[item.label] ? 'checkmarkempty' : ''" size="22" color="#007aff" />
</view>
<view v-if="showList.length === 0" class="empty">暂无诊断数据</view>
<view v-if="showList.length === 0" class="empty">{{ keyword.trim() ? '暂无诊断数据' : '请输入关键词搜索' }}</view>
<view style="height: 240rpx;"></view>
</scroll-view>
@ -31,6 +31,7 @@ const list = ref([]);
const selections = ref([]);
const mult = ref(false);
const eventName = ref('change-diagnosis');
let lastQueryId = 0;
const selectedMap = computed(() => selections.value.reduce((m, i) => ((m[i] = true), m), {}));
@ -38,22 +39,8 @@ function normalizeText(v) {
return v ? String(v) : '';
}
const fullMatched = computed(() => {
const value = String(keyword.value || '').trim();
if (!value) return null;
return { label: value, value, key: `full_${value}` };
});
const showList = computed(() => {
const base = Array.isArray(list.value) ? list.value : [];
const arr = [];
if (fullMatched.value) arr.push(fullMatched.value);
base.forEach((i) => {
if (!i || !i.label) return;
if (fullMatched.value && i.label === fullMatched.value.label) return;
arr.push(i);
});
return arr;
return Array.isArray(list.value) ? list.value : [];
});
let timer = null;
@ -89,11 +76,25 @@ onLoad((opt) => {
async function query() {
if (!ready.value) return;
const value = String(keyword.value || '').trim();
if (!value) {
list.value = [];
return;
}
const queryId = ++lastQueryId;
uni.showLoading({ title: '加载中...' });
try {
uni.showLoading({ title: '加载中...' });
const res = await api('getDisease', { diseaseName: value });
const arr = Array.isArray(res?.data?.data) ? res.data.data : [];
if (queryId !== lastQueryId) return;
const arr =
Array.isArray(res?.data?.data) ? res.data.data
: Array.isArray(res?.data?.list) ? res.data.list
: Array.isArray(res?.data?.data?.data) ? res.data.data.data
: Array.isArray(res?.data) ? res.data
: Array.isArray(res?.list) ? res.list
: Array.isArray(res?.data?.data?.list) ? res.data.data.list
: [];
list.value = arr
.map((i) => {
const label = normalizeText(i?.diseaseName);
@ -103,9 +104,9 @@ async function query() {
})
.filter(Boolean);
} catch (e) {
list.value = [];
if (queryId === lastQueryId) list.value = [];
} finally {
uni.hideLoading();
if (queryId === lastQueryId) uni.hideLoading();
}
}

View File

@ -1,10 +1,39 @@
const env = __VITE_ENV__;
function joinBaseAndPath(base, path) {
const b = String(base || '').replace(/\/+$/, '');
const p = String(path || '');
if (!p) return b;
if (/^https?:\/\//i.test(p)) return p;
return `${b}/${p.replace(/^\/+/, '')}`;
}
// 兼容历史数据:某些链接缺少域名后的 /
export function normalizeFileUrl(url) {
const u = String(url || '').trim();
if (!u) return '';
if (/^https?:\/\//i.test(u)) {
const afterProtocolIndex = u.indexOf('://') + 3;
const firstSlashIndex = u.indexOf('/', afterProtocolIndex);
if (firstSlashIndex > 0) {
const prefix = u.slice(0, afterProtocolIndex);
const host = u.slice(afterProtocolIndex, firstSlashIndex);
const path = u.slice(firstSlashIndex);
if (host.toLowerCase().endsWith('uploads') && !path.toLowerCase().startsWith('/uploads/')) {
const fixedHost = host.slice(0, -'uploads'.length);
const normalizedPath = `/uploads${path.startsWith('/') ? '' : '/'}${path.replace(/^\/+/, '')}`;
return `${prefix}${fixedHost}${normalizedPath}`;
}
}
}
return u.replace(/^(https?:\/\/[^/]+)(uploads\/)/i, '$1/$2');
}
export async function uploadFile(tempFilePath) {
try {
const res = await new Promise((resolve, reject) => {
uni.uploadFile({
url: `${env.MP_API_BASE_URL}/upload`,
url: joinBaseAndPath(env.MP_API_BASE_URL, 'upload'),
filePath: tempFilePath,
name: 'file',
success: (resp) => resolve(resp),
@ -14,7 +43,7 @@ export async function uploadFile(tempFilePath) {
const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
if (data && data.success && data.filePath) {
return `${env.MP_API_BASE_URL}${data.filePath}`;
return normalizeFileUrl(joinBaseAndPath(env.MP_API_BASE_URL, data.filePath));
}
} catch (e) {
console.log('upload file error:', e);
@ -58,5 +87,3 @@ export async function chooseAndUploadImage(options = {}) {
return null;
}
}