feat:接入病历接口

This commit is contained in:
Jafeng 2026-01-27 16:46:36 +08:00
parent 8e72ec6d24
commit 2fc385f994
17 changed files with 1319 additions and 591 deletions

View File

@ -1,4 +1,4 @@
MP_API_BASE_URL=http://localhost:8080 MP_API_BASE_URL=http://192.168.137.1:8080
MP_CACHE_PREFIX=development MP_CACHE_PREFIX=development
MP_WX_APP_ID=wx93af55767423938e MP_WX_APP_ID=wx93af55767423938e
MP_CORP_ID=wwe3fb2faa52cf9dfb MP_CORP_ID=wwe3fb2faa52cf9dfb

View File

@ -30,13 +30,9 @@
</view> </view>
<view class="record-body"> <view class="record-body">
<view class="line"> <view v-for="(l, idx) in getDisplayLines(r)" :key="`${r._id}_${idx}`" class="line">
<text class="line-label">诊断</text> <text class="line-label">{{ l.label }}</text>
<text class="line-value">{{ getDiagnosis(r) }}</text> <text class="line-value">{{ l.value }}</text>
</view>
<view v-if="r.templateType === 'inhospital' && r.surgeryName" class="line">
<text class="line-label">手术</text>
<text class="line-value">{{ r.surgeryName }}</text>
</view> </view>
<view v-if="getFiles(r).length" class="thumbs"> <view v-if="getFiles(r).length" class="thumbs">
@ -47,8 +43,7 @@
</view> </view>
</view> </view>
<view class="record-foot"> <view class="record-foot">
<view class="foot-left">创建时间{{ r.createDateStr || '' }}</view> <view class="foot-left">{{ getCreateFooter(r) }}</view>
<view class="foot-right">创建人{{ r.creatorName || '—' }}</view>
</view> </view>
</view> </view>
<view v-if="records.length === 0" class="empty">暂无数据</view> <view v-if="records.length === 0" class="empty">暂无数据</view>
@ -61,8 +56,11 @@
</template> </template>
<script setup> <script setup>
import { computed, onMounted, onUnmounted, ref } from 'vue'; import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { ensureSeed, getCurrentTeamId, getVisitRecordTemplates, queryVisitRecords } from './mock'; import dayjs from 'dayjs';
import { VISIT_RECORD_TEMPLATES } from './templates';
import api from '@/utils/api';
import { loading, hideLoading } from '@/utils/widget';
const props = defineProps({ const props = defineProps({
data: { type: Object, default: () => ({}) }, data: { type: Object, default: () => ({}) },
@ -70,7 +68,7 @@ const props = defineProps({
floatingBottom: { type: Number, default: 16 }, floatingBottom: { type: Number, default: 16 },
}); });
const templates = ref(getVisitRecordTemplates()); const templates = ref(VISIT_RECORD_TEMPLATES.map(t => ({ name: t.templateName, templateType: t.templateType })));
const typeRange = computed(() => [{ name: '全部', value: 'ALL' }, ...templates.value.map((t) => ({ name: t.name, value: t.templateType }))]); const typeRange = computed(() => [{ name: '全部', value: 'ALL' }, ...templates.value.map((t) => ({ name: t.name, value: t.templateType }))]);
const currentType = ref({ name: '全部', value: 'ALL' }); const currentType = ref({ name: '全部', value: 'ALL' });
@ -85,6 +83,11 @@ const timeRangeOptions = [
]; ];
const currentTimeRange = ref(timeRangeOptions[0]); const currentTimeRange = ref(timeRangeOptions[0]);
function getCurrentTeamId() {
const team = uni.getStorageSync('ykt_case_current_team') || {};
return team?.teamId ? String(team.teamId) : '';
}
const teamId = ref(getCurrentTeamId()); const teamId = ref(getCurrentTeamId());
const showShareTip = computed(() => { const showShareTip = computed(() => {
if (props.data && typeof props.data.shareAllTeams === 'boolean') return props.data.shareAllTeams; if (props.data && typeof props.data.shareAllTeams === 'boolean') return props.data.shareAllTeams;
@ -100,15 +103,162 @@ const shareAllTeamsForQuery = computed(() => {
return true; return true;
}); });
function refreshList() { function getCorpId() {
const team = uni.getStorageSync('ykt_case_current_team') || {};
return team?.corpId ? String(team.corpId) : '';
}
const userNameMap = ref({});
const loadedTeamId = ref('');
function resolveUserName(userId) {
const id = String(userId || '');
if (!id) return '';
const map = userNameMap.value || {};
return String(map[id] || '') || id;
}
async function loadTeamMembers() {
const team = uni.getStorageSync('ykt_case_current_team') || {};
const teamId = team?.teamId ? String(team.teamId) : '';
const corpId = getCorpId();
if (!teamId || !corpId) return;
if (loadedTeamId.value === teamId && Object.keys(userNameMap.value || {}).length > 0) return;
loadedTeamId.value = teamId;
const res = await api('getTeamData', { corpId, teamId });
if (!res?.success) return;
const t = res?.data && typeof res.data === 'object' ? res.data : {};
const members = Array.isArray(t.memberList) ? t.memberList : [];
userNameMap.value = members.reduce((acc, m) => {
const uid = String(m?.userid || '');
if (!uid) return acc;
acc[uid] = String(m?.anotherName || m?.name || m?.userid || '') || uid;
return acc;
}, {});
}
function getSortTimeTitle(templateType) {
if (templateType === 'outpatient') return 'visitTime';
if (templateType === 'inhospital') return 'inhosDate';
if (templateType === 'preConsultation') return 'consultDate';
if (templateType === 'physicalExaminationTemplate') return 'inspectDate';
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';
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('');
}
return normalizeText(v);
}
function getTemplateName(type) {
const t = VISIT_RECORD_TEMPLATES.find((i) => i && i.templateType === type);
return t?.templateName ? String(t.templateName) : '';
}
function toDateStr(sortTime) {
if (!sortTime) return '';
const d = dayjs(sortTime);
return d.isValid() ? d.format('YYYY-MM-DD') : '';
}
function toDateTimeStr(ts) {
if (!ts) return '';
const d = dayjs(ts);
return d.isValid() ? d.format('YYYY-MM-DD HH:mm') : '';
}
async function refreshList() {
if (!props.archiveId) return; if (!props.archiveId) return;
records.value = queryVisitRecords({
archiveId: props.archiveId, const corpId = getCorpId();
medicalType: currentType.value.value, if (!corpId) return;
timeRange: currentTimeRange.value.value, loadTeamMembers();
teamId: teamId.value,
shareAllTeams: shareAllTeamsForQuery.value, loading('加载中...');
}); try {
const params = { memberId: props.archiveId, corpId };
//
params.medicalType =
currentType.value.value === 'ALL' ? templates.value.map((i) => i.templateType) : currentType.value.value;
//
if (currentTimeRange.value.value !== 'ALL') {
const now = Date.now();
if (currentTimeRange.value.value === 'today') {
const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).getTime();
params.startTime = todayStart;
params.endTime = now;
} else if (currentTimeRange.value.value === '7d') {
params.startTime = now - 7 * 24 * 60 * 60 * 1000;
params.endTime = now;
} else if (currentTimeRange.value.value === '30d') {
params.startTime = now - 30 * 24 * 60 * 60 * 1000;
params.endTime = now;
}
}
const res = await api('getCustomerMedicalRecord', params);
hideLoading();
//
// - { success, message, list: [] }
// - { success, message, data: { list: [] } }
const list = Array.isArray(res?.list)
? res.list
: Array.isArray(res?.data?.list)
? res.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]) : '';
return {
...r,
templateType: t,
dateStr: dateStr || toDateStr(r?.sortTime),
createDateStr: r?.createTime ? dayjs(r.createTime).format('YYYY-MM-DD') : '',
createTimeStr: toDateTimeStr(r?.createTime),
tempName: r?.tempName || getTemplateName(t) || '病历',
};
});
//
records.value = !shareAllTeamsForQuery.value && teamId.value
? mapped.filter((i) => String(i?.teamId || '') === String(teamId.value))
: mapped;
} else {
records.value = [];
}
} catch (error) {
hideLoading();
console.error('获取病历记录失败:', error);
records.value = [];
}
} }
const tagClass = { const tagClass = {
@ -120,8 +270,53 @@ const tagClass = {
function getDiagnosis(r) { function getDiagnosis(r) {
if (!r) return '--'; if (!r) return '--';
if (r.templateType === 'preConsultation') return r.chiefComplaint || r.summary || '--'; const t = r.templateType || r.medicalType;
return r.diagnosisName || r.summary || '--'; 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) || '--';
return normalizeText(r.diagnosisName || r.diagnosis || r.summary) || '--';
}
function firstLine(v) {
const s = normalizeText(v);
return s || '--';
}
function getDisplayLines(r) {
const t = r?.templateType || r?.medicalType;
if (t === 'outpatient') {
return [{ label: '门诊诊断:', value: firstLine(r.diagnosisName || r.diagnosis) }];
}
if (t === 'inhospital') {
const lines = [{ label: '入院诊断:', value: firstLine(r.diagnosisName || r.diagnosis) }];
const surgery = normalizeText(r.surgeryName);
if (surgery) lines.push({ label: '手术名称:', value: surgery });
return lines;
}
if (t === 'physicalExaminationTemplate') {
return [{ label: '体检小结:', value: firstLine(r.summary) }];
}
if (t === 'preConsultation') {
const lines = [
{ label: '主诉:', value: firstLine(r.chiefComplaint) },
{ label: '现病史:', value: firstLine(r.presentIllness) },
];
const past = normalizeText(r.pastHistory);
if (past) lines.push({ label: '既往史:', value: past });
return lines;
}
return [{ label: '摘要:', value: firstLine(r.summary) }];
}
function getCreateFooter(r) {
const time = r?.createTimeStr || r?.createDateStr || '';
const byCustomer = r?.ignore === 'checkIn';
if (byCustomer) return time ? `${time} 患者自建` : '患者自建';
const creatorId = String(r?.creator || '');
if (!creatorId) return time ? `创建时间:${time}` : '';
const name = resolveUserName(creatorId);
return time ? `${time} ${name}代建` : `${name}代建`;
} }
function pickType(e) { function pickType(e) {
@ -157,13 +352,14 @@ function add() {
} }
function edit(record) { function edit(record) {
const type = String(record?.medicalType || record?.templateType || '') || '';
uni.navigateTo({ uni.navigateTo({
url: `/pages/case/visit-record-view?archiveId=${encodeURIComponent(props.archiveId)}&id=${encodeURIComponent(record._id)}`, url: `/pages/case/visit-record-view?archiveId=${encodeURIComponent(props.archiveId)}&id=${encodeURIComponent(record._id)}&type=${encodeURIComponent(type)}`,
}); });
} }
onMounted(() => { onMounted(() => {
ensureSeed(props.archiveId, props.data); // archiveId watch
refreshList(); refreshList();
uni.$on('archive-detail:visit-record-changed', refreshList); uni.$on('archive-detail:visit-record-changed', refreshList);
}); });
@ -171,6 +367,16 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
uni.$off('archive-detail:visit-record-changed', refreshList); uni.$off('archive-detail:visit-record-changed', refreshList);
}); });
watch(
() => props.archiveId,
(v, old) => {
const next = v ? String(v) : '';
const prev = old ? String(old) : '';
if (next && next !== prev) refreshList();
},
{ immediate: true }
);
</script> </script>
<style scoped> <style scoped>
@ -265,9 +471,7 @@ onUnmounted(() => {
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
display: -webkit-box; white-space: nowrap;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
} }
.thumbs { .thumbs {
@ -306,13 +510,6 @@ onUnmounted(() => {
flex-shrink: 0; flex-shrink: 0;
margin-right: 10px; margin-right: 10px;
} }
.foot-right {
flex: 1;
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.record-tag { .record-tag {
font-size: 12px; font-size: 12px;

View File

@ -1,410 +0,0 @@
import dayjs from 'dayjs';
const DB_KEY = 'ykt_case_archive_detail_mockdb_v1';
export const VISIT_RECORD_TEMPLATES = [
{
templateType: 'outpatient',
templateName: '门诊记录',
templateList: [
{ title: 'visitTime', name: '就诊日期', type: 'date', operateType: 'formCell', required: true, format: 'YYYY-MM-DD' },
{ title: 'corpName', name: '就诊机构', type: 'input', operateType: 'formCell', required: false, wordLimit: 30, inputType: 'text' },
{ title: 'deptName', name: '科室', type: 'input', operateType: 'formCell', required: false, wordLimit: 30, inputType: 'text' },
{ title: 'doctor', name: '医生', type: 'input', operateType: 'formCell', required: false, wordLimit: 30, inputType: 'text' },
{ title: 'diagnosisName', name: '门诊诊断', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200 },
{ title: 'treatmentPlan', name: '治疗方案', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 1000 },
{ title: 'disposePlan', name: '处置计划', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 1000 },
{ title: 'summary', name: '备注/摘要', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200 },
{ title: 'files', name: '文件上传', type: 'files', required: false },
],
},
{
templateType: 'inhospital',
templateName: '住院记录',
templateList: [
{ title: 'inhosDate', name: '入院日期', type: 'date', operateType: 'formCell', required: true, format: 'YYYY-MM-DD' },
{ title: 'corpName', name: '住院机构', type: 'input', operateType: 'formCell', required: false, wordLimit: 30, inputType: 'text' },
{ title: 'diagnosisName', name: '入院诊断', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200 },
{ title: 'surgeryName', name: '手术名称', type: 'input', operateType: 'formCell', required: false, wordLimit: 50, inputType: 'text' },
{ title: 'summary', name: '摘要', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200 },
{ title: 'files', name: '文件上传', type: 'files', required: false },
],
},
{
templateType: 'preConsultation',
templateName: '预问诊记录',
templateList: [
{ title: 'consultDate', name: '问诊日期', type: 'date', operateType: 'formCell', required: true, format: 'YYYY-MM-DD' },
{ title: 'chiefComplaint', name: '主诉', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 300 },
{ title: 'presentIllness', name: '现病史', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 800 },
{ title: 'pastHistory', name: '既往史', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 800 },
{ title: 'allergyHistory', name: '过敏史', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 300 },
{ title: 'summary', name: '摘要', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200 },
{ title: 'files', name: '文件上传', type: 'files', required: false },
],
},
{
templateType: 'physicalExaminationTemplate',
templateName: '体检记录',
templateList: [
{ title: 'inspectDate', name: '体检日期', type: 'date', operateType: 'formCell', required: true, format: 'YYYY-MM-DD' },
{ title: 'corpName', name: '体检机构', type: 'input', operateType: 'formCell', required: false, wordLimit: 30, inputType: 'text' },
{ title: 'inspectPakageName', name: '体检套餐', type: 'input', operateType: 'formCell', required: false, wordLimit: 50, inputType: 'text' },
{ title: 'positiveFind', name: '阳性发现', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 300 },
{ title: 'summary', name: '摘要', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200 },
{ title: 'files', name: '文件上传', type: 'files', required: false },
],
},
];
export function getVisitRecordTemplates() {
return VISIT_RECORD_TEMPLATES.map((i) => ({ templateType: i.templateType, name: i.templateName, templateList: i.templateList }));
}
export function getVisitRecordTemplate(templateType) {
return VISIT_RECORD_TEMPLATES.find((i) => i.templateType === templateType) || null;
}
function safeParse(json) {
try {
return JSON.parse(json);
} catch {
return null;
}
}
function getDb() {
const raw = uni.getStorageSync(DB_KEY);
const db = raw && typeof raw === 'string' ? safeParse(raw) : raw;
const next = db && typeof db === 'object' ? db : {};
next.visitRecordsByArchiveId = next.visitRecordsByArchiveId || {};
next.serviceRecordsByArchiveId = next.serviceRecordsByArchiveId || {};
next.followupsByArchiveId = next.followupsByArchiveId || {};
return next;
}
function setDb(db) {
uni.setStorageSync(DB_KEY, JSON.stringify(db));
}
function uid(prefix) {
return `${prefix}_${Date.now()}_${Math.random().toString(16).slice(2)}`;
}
export function ensureSeed(archiveId, archive) {
if (!archiveId) return;
const db = getDb();
if (!Array.isArray(db.visitRecordsByArchiveId[archiveId]) || db.visitRecordsByArchiveId[archiveId].length === 0) {
const now = Date.now();
db.visitRecordsByArchiveId[archiveId] = [
{
_id: uid('mr'),
medicalType: 'outpatient',
tempName: '门诊记录',
templateType: 'outpatient',
teamId: 'team_1',
sortTime: now - 1000 * 60 * 60 * 24 * 2,
visitTime: dayjs(now - 1000 * 60 * 60 * 24 * 2).format('YYYY-MM-DD'),
corpName: '某某医院',
deptName: '呼吸内科',
doctor: '李医生',
diagnosisName: '急性上呼吸道感染mock',
treatmentPlan: '建议1对症处理退热2多饮水休息3如出现呼吸困难及时就医。',
disposePlan: '建议1继续对症治疗2监测体温与血压3如3天无缓解或加重立即复诊。',
summary: '初诊:对症处理并随访。',
files: [{ url: '/static/tabbar/home.png', name: '示例图片1' }],
createTime: now - 1000 * 60 * 60 * 24 * 2,
creatorName: '李珊珊',
},
{
_id: uid('mr'),
medicalType: 'inhospital',
tempName: '住院记录',
templateType: 'inhospital',
teamId: 'team_2',
sortTime: now - 1000 * 60 * 60 * 24 * 15,
inhosDate: dayjs(now - 1000 * 60 * 60 * 24 * 15).format('YYYY-MM-DD'),
corpName: '某某医院',
diagnosisName: '术后复查mock',
surgeryName: '阑尾切除术',
summary: '复诊:术后复查,恢复良好。',
files: [],
createTime: now - 1000 * 60 * 60 * 24 * 15,
creatorName: '王护士',
},
{
_id: uid('mr'),
medicalType: 'preConsultation',
tempName: '预问诊记录',
templateType: 'preConsultation',
teamId: 'team_1',
sortTime: now - 1000 * 60 * 60 * 6,
consultDate: dayjs(now - 1000 * 60 * 60 * 6).format('YYYY-MM-DD'),
chiefComplaint: '咽痛、流涕 2 天mock',
presentIllness: '近2天受凉后出现咽痛、流涕体温最高 38.2℃。',
pastHistory: '既往体健。',
allergyHistory: '无明确过敏史。',
summary: '建议对症处理,必要时线下就医。',
files: [],
createTime: now - 1000 * 60 * 60 * 6,
creatorName: '李珊珊',
},
];
}
if (!Array.isArray(db.serviceRecordsByArchiveId[archiveId]) || db.serviceRecordsByArchiveId[archiveId].length === 0) {
const now = Date.now();
db.serviceRecordsByArchiveId[archiveId] = Array.from({ length: 18 }).map((_, idx) => {
const eventType =
idx % 5 === 0 ? 'questionnaire' : idx % 4 === 0 ? 'article' : idx % 3 === 0 ? 'sms' : 'phone';
const executionTime = now - 1000 * 60 * 60 * (idx * 6 + 3);
const executeTeamId = idx % 2 === 0 ? 'team_1' : 'team_2';
const executeTeamName = executeTeamId === 'team_1' ? '口腔一科(示例)' : '正畸团队(示例)';
return {
_id: uid('sr'),
eventType,
taskContent: `服务内容示例 #${idx + 1}:这里是任务描述,支持长文本展开收起。`,
result: idx % 7 === 0 ? '已联系患者,已确认到院时间。' : '',
executorName: idx % 2 === 0 ? '李医生' : '王护士',
executeTeamId,
executeTeamName,
executionTime,
pannedEventSendFile:
eventType === 'article'
? { type: 'article', url: 'https://example.com/article/1' }
: eventType === 'questionnaire'
? { type: 'questionnaire', surveryId: 'q_1' }
: null,
archiveName: archive?.name || '',
};
});
}
if (!Array.isArray(db.followupsByArchiveId[archiveId]) || db.followupsByArchiveId[archiveId].length === 0) {
const now = Date.now();
db.followupsByArchiveId[archiveId] = Array.from({ length: 22 }).map((_, idx) => {
const plannedExecutionTime = now + 1000 * 60 * 60 * 24 * ((idx % 9) - 2);
const createTime = now - 1000 * 60 * 60 * (idx * 5 + 2);
const statusPool = ['processing', 'notStart', 'treated', 'cancelled', 'expired'];
const status = statusPool[idx % statusPool.length];
const eventTypePool = ['followup', 'revisit', 'questionnaire', 'other'];
const eventType = eventTypePool[idx % eventTypePool.length];
const executeTeamId = idx % 2 === 0 ? 'team_1' : 'team_2';
const executeTeamName = executeTeamId === 'team_1' ? '口腔一科(示例)' : '正畸团队(示例)';
return {
_id: uid('td'),
plannedExecutionTime,
planDate: dayjs(plannedExecutionTime).format('YYYY-MM-DD'),
createTime,
createTimeStr: dayjs(createTime).format('YYYY-MM-DD HH:mm'),
executorName: idx % 2 === 0 ? '李医生' : '王护士',
executeTeamId,
executeTeamName,
creatorName: idx % 3 === 0 ? '系统' : '管理员A',
status,
eventType,
eventTypeLabel:
eventType === 'followup' ? '回访' : eventType === 'revisit' ? '复诊提醒' : eventType === 'questionnaire' ? '问卷' : '其他',
eventStatusLabel:
status === 'processing'
? '待处理'
: status === 'notStart'
? '未开始'
: status === 'treated'
? '已完成'
: status === 'cancelled'
? '已取消'
: '已过期',
taskContent: `回访任务示例 #${idx + 1}:电话回访/提醒到院等。`,
result: status === 'treated' ? '已完成回访,患者反馈良好。' : '',
archiveName: archive?.name || '',
};
});
}
setDb(db);
}
export function getCurrentTeamId() {
const v = uni.getStorageSync('ykt_mock_current_team_id');
return v ? String(v) : 'team_1';
}
function getSortTimeTitle(templateType) {
if (templateType === 'outpatient') return 'visitTime';
if (templateType === 'inhospital') return 'inhosDate';
if (templateType === 'preConsultation') return 'consultDate';
if (templateType === 'physicalExaminationTemplate') return 'inspectDate';
return '';
}
export function queryVisitRecords({ archiveId, medicalType = 'ALL', timeRange = 'ALL', teamId = '', shareAllTeams = false }) {
const db = getDb();
const list = Array.isArray(db.visitRecordsByArchiveId[archiveId]) ? db.visitRecordsByArchiveId[archiveId] : [];
const withDate = list.map((i) => {
const type = i.templateType || i.medicalType || '';
const timeTitle = getSortTimeTitle(type);
const rawDate = timeTitle ? i[timeTitle] : '';
const fallback = i.sortTime ? dayjs(i.sortTime).format('YYYY-MM-DD') : '';
const date = rawDate || fallback;
return {
...i,
dateStr: date ? String(date) : '',
date: date ? String(date) : '',
createDateStr: i.createTime ? dayjs(i.createTime).format('YYYY-MM-DD') : '',
};
});
let filtered = [...withDate];
if (medicalType !== 'ALL') filtered = filtered.filter((i) => (i.medicalType || i.templateType) === medicalType);
if (!shareAllTeams && teamId) filtered = filtered.filter((i) => !i.teamId || i.teamId === teamId);
if (timeRange && timeRange !== 'ALL') {
const days = timeRange === 'today' ? 0 : timeRange === '7d' ? 7 : timeRange === '30d' ? 30 : null;
if (days !== null) {
const start = days === 0 ? dayjs().startOf('day') : dayjs().subtract(days, 'day').startOf('day');
const startMs = start.valueOf();
filtered = filtered.filter((i) => (i.sortTime || 0) >= startMs);
}
}
filtered.sort((a, b) => (b.sortTime || 0) - (a.sortTime || 0));
return filtered;
}
export function getVisitRecord({ archiveId, id }) {
const db = getDb();
const list = Array.isArray(db.visitRecordsByArchiveId[archiveId]) ? db.visitRecordsByArchiveId[archiveId] : [];
return list.find((i) => i._id === id) || null;
}
export function upsertVisitRecord({ archiveId, record }) {
const db = getDb();
const list = Array.isArray(db.visitRecordsByArchiveId[archiveId]) ? db.visitRecordsByArchiveId[archiveId] : [];
const next = { ...record };
if (!next._id) next._id = uid('mr');
if (!next.sortTime) next.sortTime = Date.now();
if (!next.createTime) next.createTime = Date.now();
const idx = list.findIndex((i) => i._id === next._id);
if (idx >= 0) list[idx] = { ...list[idx], ...next };
else list.unshift(next);
db.visitRecordsByArchiveId[archiveId] = list;
setDb(db);
return next;
}
export function removeVisitRecord({ archiveId, id }) {
const db = getDb();
const list = Array.isArray(db.visitRecordsByArchiveId[archiveId]) ? db.visitRecordsByArchiveId[archiveId] : [];
db.visitRecordsByArchiveId[archiveId] = list.filter((i) => i._id !== id);
setDb(db);
}
export function queryServiceRecords({ archiveId, page = 1, pageSize = 10, eventType = 'ALL', teamId = 'ALL', dateRange = [] }) {
const db = getDb();
const list = Array.isArray(db.serviceRecordsByArchiveId[archiveId]) ? db.serviceRecordsByArchiveId[archiveId] : [];
let filtered = [...list];
if (eventType !== 'ALL') filtered = filtered.filter((i) => i.eventType === eventType);
if (teamId !== 'ALL') filtered = filtered.filter((i) => i.executeTeamId === teamId);
if (Array.isArray(dateRange) && dateRange.length === 2 && dateRange[0] && dateRange[1]) {
filtered = filtered.filter((i) => {
const d = i.executionTime ? dayjs(i.executionTime).format('YYYY-MM-DD') : '';
return d >= dateRange[0] && d <= dateRange[1];
});
}
filtered.sort((a, b) => (b.executionTime || 0) - (a.executionTime || 0));
const total = filtered.length;
const pages = Math.ceil(total / pageSize) || 1;
const start = (page - 1) * pageSize;
const slice = filtered.slice(start, start + pageSize);
return { list: slice, total, pages };
}
export function getServiceRecord({ archiveId, id }) {
const db = getDb();
const list = Array.isArray(db.serviceRecordsByArchiveId[archiveId]) ? db.serviceRecordsByArchiveId[archiveId] : [];
return list.find((i) => i._id === id) || null;
}
export function upsertServiceRecord({ archiveId, record }) {
const db = getDb();
const list = Array.isArray(db.serviceRecordsByArchiveId[archiveId]) ? db.serviceRecordsByArchiveId[archiveId] : [];
const next = { ...record };
if (!next._id) next._id = uid('sr');
if (!next.executionTime) next.executionTime = Date.now();
const idx = list.findIndex((i) => i._id === next._id);
if (idx >= 0) list[idx] = { ...list[idx], ...next };
else list.unshift(next);
db.serviceRecordsByArchiveId[archiveId] = list;
setDb(db);
return next;
}
export function removeServiceRecord({ archiveId, id }) {
const db = getDb();
const list = Array.isArray(db.serviceRecordsByArchiveId[archiveId]) ? db.serviceRecordsByArchiveId[archiveId] : [];
db.serviceRecordsByArchiveId[archiveId] = list.filter((i) => i._id !== id);
setDb(db);
}
export function queryFollowups({ archiveId, page = 1, pageSize = 10, status = 'all', isMy = false, eventTypes = [], teamId = 'ALL', planRange = ['', ''] }) {
const db = getDb();
const list = Array.isArray(db.followupsByArchiveId[archiveId]) ? db.followupsByArchiveId[archiveId] : [];
let filtered = [...list];
if (status !== 'all') filtered = filtered.filter((i) => i.status === status);
if (isMy) filtered = filtered.filter((i) => i.executorName === '李医生');
if (Array.isArray(eventTypes) && eventTypes.length) filtered = filtered.filter((i) => eventTypes.includes(i.eventType));
if (teamId !== 'ALL') filtered = filtered.filter((i) => i.executeTeamId === teamId);
if (planRange && (planRange[0] || planRange[1])) {
if (planRange[0]) filtered = filtered.filter((i) => i.planDate >= planRange[0]);
if (planRange[1]) filtered = filtered.filter((i) => i.planDate <= planRange[1]);
}
filtered.sort((a, b) => (b.plannedExecutionTime || 0) - (a.plannedExecutionTime || 0));
const total = filtered.length;
const pages = Math.ceil(total / pageSize) || 1;
const start = (page - 1) * pageSize;
const slice = filtered.slice(start, start + pageSize);
return { list: slice, total, pages };
}
export function getFollowup({ archiveId, id }) {
const db = getDb();
const list = Array.isArray(db.followupsByArchiveId[archiveId]) ? db.followupsByArchiveId[archiveId] : [];
return list.find((i) => i._id === id) || null;
}
export function upsertFollowup({ archiveId, followup }) {
const db = getDb();
const list = Array.isArray(db.followupsByArchiveId[archiveId]) ? db.followupsByArchiveId[archiveId] : [];
const next = { ...followup };
if (!next._id) next._id = uid('td');
if (!next.plannedExecutionTime) next.plannedExecutionTime = Date.now();
next.planDate = dayjs(next.plannedExecutionTime).format('YYYY-MM-DD');
if (!next.createTime) next.createTime = Date.now();
next.createTimeStr = dayjs(next.createTime).format('YYYY-MM-DD HH:mm');
const idx = list.findIndex((i) => i._id === next._id);
if (idx >= 0) list[idx] = { ...list[idx], ...next };
else list.unshift(next);
db.followupsByArchiveId[archiveId] = list;
setDb(db);
return next;
}
export function removeFollowup({ archiveId, id }) {
const db = getDb();
const list = Array.isArray(db.followupsByArchiveId[archiveId]) ? db.followupsByArchiveId[archiveId] : [];
db.followupsByArchiveId[archiveId] = list.filter((i) => i._id !== id);
setDb(db);
}

View File

@ -2,14 +2,14 @@
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/customer-detail/service-info/service-info.vue --> <!-- Mobile 来源: ykt-management-mobile/src/pages/customer/customer-detail/service-info/service-info.vue -->
<view class="wrap"> <view class="wrap">
<view class="filters"> <view class="filters">
<picker mode="selector" :range="typeList" range-key="label" @change="pickType"> <picker class="filter-item" mode="selector" :range="typeList" range-key="label" @change="pickType">
<view class="filter-pill"> <view class="filter-pill">
<view class="pill-text" :class="{ muted: currentType.value === 'ALL' }">{{ currentType.value === 'ALL' ? '服务类型' : currentType.label }}</view> <view class="pill-text" :class="{ muted: currentType.value === 'ALL' }">{{ currentType.value === 'ALL' ? '服务类型' : currentType.label }}</view>
<uni-icons type="arrowdown" size="12" color="#666" /> <uni-icons type="arrowdown" size="12" color="#666" />
</view> </view>
</picker> </picker>
<uni-datetime-picker v-model="dateRange" type="daterange" rangeSeparator="-" @change="changeDates"> <uni-datetime-picker class="filter-item wide-item" v-model="dateRange" type="daterange" rangeSeparator="-" @change="changeDates">
<view class="filter-pill wide"> <view class="filter-pill wide">
<view class="pill-text" :class="{ muted: !dateRange.length }"> <view class="pill-text" :class="{ muted: !dateRange.length }">
{{ dateRange.length ? dateRange.join('~') : '服务时间' }} {{ dateRange.length ? dateRange.join('~') : '服务时间' }}
@ -21,7 +21,7 @@
</view> </view>
</uni-datetime-picker> </uni-datetime-picker>
<picker mode="selector" :range="teamList" range-key="label" @change="pickTeam"> <picker class="filter-item" mode="selector" :range="teamList" range-key="label" @change="pickTeam">
<view class="filter-pill"> <view class="filter-pill">
<view class="pill-text">{{ currentTeam.label }}</view> <view class="pill-text">{{ currentTeam.label }}</view>
<uni-icons type="arrowdown" size="12" color="#666" /> <uni-icons type="arrowdown" size="12" color="#666" />
@ -401,56 +401,56 @@ watch(
} }
.filters { .filters {
padding: 6px 14px; padding: 10px 14px;
background: #f5f6f8; background: #fff;
border-bottom: 1px solid #f2f2f2; border-bottom: 1px solid #f0f0f0;
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: nowrap; gap: 12px;
} }
.filters :deep(picker) { .filter-item {
display: block;
flex: 0 0 auto;
width: 100px;
}
.filters :deep(uni-datetime-picker) {
display: block; display: block;
flex: 1; flex: 1;
min-width: 0; min-width: 0;
margin: 0 12px;
} }
.filter-item.wide-item {
flex: 1.4;
}
/* Removed old deep selectors */
.filter-pill { .filter-pill {
background: #fff; background: #f7f8fa;
border: 1px solid #e6e6e6; border: 1px solid #e5e7eb;
border-radius: 8px; border-radius: 6px;
padding: 8px 10px; padding: 10px 12px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; gap: 8px;
min-width: 0;
width: 100%; width: 100%;
}
.filter-pill.wide {
min-width: 0; min-width: 0;
box-sizing: border-box; /* Ensure padding doesn't overflow width */
} }
.pill-text { .pill-text {
font-size: 13px; font-size: 13px;
color: #333; color: #333;
flex: 1; flex: 1;
min-width: 0; min-width: 0;
margin-right: 8px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} font-weight: 500;
.filter-pill :deep(uni-icons) {
flex-shrink: 0;
} }
.pill-text.muted { .pill-text.muted {
color: #999; color: #999;
font-weight: 400;
} }
.pill-icon { .pill-icon {
padding-left: 8px; flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.filter-pill :deep(uni-icons) {
flex-shrink: 0;
} }
.timeline { .timeline {

View File

@ -0,0 +1,62 @@
export const VISIT_RECORD_TEMPLATES = [
{
templateType: 'outpatient',
templateName: '门诊记录',
service: { timeTitle: 'visitTime', timeName: '就诊日期' },
templateList: [
{ title: 'visitTime', name: '就诊日期', type: 'date', operateType: 'formCell', required: false, format: 'YYYY-MM-DD', placeholder: '请选择就诊日期' },
{ 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: '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' },
],
},
{
templateType: 'inhospital',
templateName: '住院记录',
service: { timeTitle: 'inhosDate', timeName: '入院日期' },
templateList: [
{ title: 'inhosDate', name: '入院日期', type: 'date', operateType: 'formCell', required: false, format: 'YYYY-MM-DD', placeholder: '请选择入院日期' },
{ 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: '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' },
],
},
{
templateType: 'preConsultation',
templateName: '预问诊记录',
service: { timeTitle: 'consultDate', timeName: '问诊日期' },
templateList: [
{ 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' },
],
},
{
templateType: 'physicalExaminationTemplate',
templateName: '体检记录',
service: { timeTitle: 'inspectDate', timeName: '体检日期' },
templateList: [
{ 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' },
],
},
];
export function getVisitRecordTemplates() {
return VISIT_RECORD_TEMPLATES.map((i) => ({ templateType: i.templateType, name: i.templateName, templateList: i.templateList }));
}
export function getVisitRecordTemplate(templateType) {
return VISIT_RECORD_TEMPLATES.find((i) => i.templateType === templateType) || null;
}

View File

@ -0,0 +1,103 @@
<template>
<view class="row" @click="open">
<view class="left">
<text class="label">{{ name }}</text>
<text v-if="required" class="required">*</text>
</view>
<view class="right">
<view class="value" :class="{ muted: !displayText }">{{ displayText || placeholder }}</view>
<uni-icons type="arrowright" size="16" color="#999" />
</view>
</view>
</template>
<script setup>
import { computed, onUnmounted, ref } from 'vue';
const emits = defineEmits(['change']);
const props = defineProps({
form: { type: Object, default: () => ({}) },
name: { default: '' },
required: { type: Boolean, default: false },
title: { default: '' },
disableChange: { type: Boolean, default: false },
placeholder: { default: '' },
mult: { type: Boolean, default: false },
});
const placeholder = computed(() => (props.placeholder ? String(props.placeholder) : `请选择${props.name || ''}`));
const rawValue = computed(() => props.form?.[props.title]);
const displayText = computed(() => {
const v = rawValue.value;
if (Array.isArray(v)) return v.filter((i) => i !== null && i !== undefined && String(i).trim()).join('');
return v ? String(v) : '';
});
const activeEventName = ref('');
function clearListener() {
if (activeEventName.value) uni.$off(activeEventName.value);
activeEventName.value = '';
}
onUnmounted(clearListener);
function open() {
if (props.disableChange) return;
clearListener();
activeEventName.value = `diagnosis-change_${Date.now()}`;
uni.$on(activeEventName.value, (data) => {
emits('change', { title: props.title, value: data });
});
if (props.mult) {
const cur = Array.isArray(rawValue.value) ? rawValue.value : [];
uni.setStorageSync('diagnosis-list-selection', cur);
}
uni.navigateTo({
url: `/pages/library/diagnosis-list?eventName=${encodeURIComponent(activeEventName.value)}&mult=${props.mult ? 'YES' : 'NO'}&value=${encodeURIComponent(displayText.value || '')}`,
});
}
</script>
<style scoped>
.row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px;
border-bottom: 1px solid #eee;
}
.left {
display: flex;
align-items: center;
gap: 6px;
}
.label {
font-size: 14px;
color: #111827;
font-weight: 700;
}
.required {
color: #ff4d4f;
font-size: 14px;
}
.right {
display: flex;
align-items: center;
gap: 8px;
min-width: 120px;
justify-content: flex-end;
}
.value {
font-size: 14px;
color: #111827;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.muted {
color: #9aa0a6;
}
</style>

View File

@ -0,0 +1,168 @@
<template>
<view class="wrap">
<view class="head" @click="add">
<view class="head-left">
<text class="label">{{ name }}</text>
<text v-if="required" class="required">*</text>
</view>
<uni-icons type="plusempty" size="20" color="#4f6ef7" />
</view>
<view v-if="value.length" class="list">
<uni-swipe-action>
<uni-swipe-action-item v-for="(i, idx) in value" :key="idx">
<view class="item">
<view class="item-title">{{ idx + 1 }}{{ i.category || '' }}</view>
<view class="item-sub">{{ i.opinion || '' }}</view>
</view>
<template #right>
<view class="actions">
<view class="action edit" @click.stop="edit(i, idx)">编辑</view>
<view class="action del" @click.stop="remove(idx)">删除</view>
</view>
</template>
</uni-swipe-action-item>
</uni-swipe-action>
</view>
<view v-else class="empty">暂无内容点击右侧 + 添加</view>
</view>
</template>
<script setup>
import { computed, onUnmounted, ref } from 'vue';
import { confirm, toast } from '@/utils/widget';
const emits = defineEmits(['change']);
const props = defineProps({
form: { type: Object, default: () => ({}) },
name: { default: '' },
required: { type: Boolean, default: false },
title: { default: '' },
disableChange: { type: Boolean, default: false },
});
const value = computed(() => {
const arr = props.form?.[props.title];
return Array.isArray(arr) ? arr.filter((i) => i && (i.category || i.opinion)) : [];
});
const activeEventName = ref('');
function clearListener() {
if (activeEventName.value) uni.$off(activeEventName.value);
activeEventName.value = '';
}
onUnmounted(clearListener);
function emitChange(next) {
emits('change', { title: props.title, value: next });
}
function add() {
if (props.disableChange) return;
uni.setStorageSync('current-positive-find', { category: '', opinion: '' });
clearListener();
activeEventName.value = `positive-find-change_${Date.now()}`;
uni.$on(activeEventName.value, (data) => {
const list = value.value.map((i) => ({ category: i.category, opinion: i.opinion }));
list.push({ category: String(data?.category || ''), opinion: String(data?.opinion || '') });
emitChange(list);
});
uni.navigateTo({
url: `/pages/others/edit-positive-find?eventName=${encodeURIComponent(activeEventName.value)}&title=${encodeURIComponent(props.name || '阳性发现')}`,
});
}
function edit(item, idx) {
if (props.disableChange) return;
uni.setStorageSync('current-positive-find', { category: item?.category || '', opinion: item?.opinion || '' });
clearListener();
activeEventName.value = `positive-find-change_${Date.now()}`;
uni.$on(activeEventName.value, (data) => {
const list = value.value.map((i) => ({ category: i.category, opinion: i.opinion }));
list[idx] = { category: String(data?.category || ''), opinion: String(data?.opinion || '') };
emitChange(list);
});
uni.navigateTo({
url: `/pages/others/edit-positive-find?eventName=${encodeURIComponent(activeEventName.value)}&title=${encodeURIComponent(props.name || '阳性发现')}`,
});
}
function remove(idx) {
if (props.disableChange) return;
confirm('确定删除吗?', () => {
const list = value.value.map((i) => ({ category: i.category, opinion: i.opinion }));
list.splice(idx, 1);
emitChange(list);
toast('已删除');
});
}
</script>
<style scoped>
.wrap {
padding: 12px 14px;
border-bottom: 1px solid #eee;
}
.head {
display: flex;
align-items: center;
justify-content: space-between;
}
.head-left {
display: flex;
align-items: center;
gap: 6px;
}
.label {
font-size: 14px;
font-weight: 700;
color: #111827;
}
.required {
color: #ff4d4f;
font-size: 14px;
}
.list {
margin-top: 10px;
}
.item {
padding: 10px 0;
}
.item-title {
font-size: 14px;
color: #111827;
font-weight: 600;
}
.item-sub {
margin-top: 6px;
font-size: 13px;
color: #6b7280;
white-space: pre-wrap;
}
.actions {
display: flex;
height: 100%;
align-items: stretch;
}
.action {
width: 70px;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
color: #fff;
}
.action.edit {
background: #4f6ef7;
}
.action.del {
background: #ff4d4f;
}
.empty {
margin-top: 10px;
font-size: 13px;
color: #9aa0a6;
}
</style>

View File

@ -4,8 +4,16 @@
{{ name }}<text v-if="required" class="form-cell--required"></text> {{ name }}<text v-if="required" class="form-cell--required"></text>
</view> </view>
<view class="mt-10"> <view class="mt-10">
<textarea :disabled="disableChange" :value="value" class="form-textarea" :placeholder="placeholder" <textarea
placeholder-class="form__placeholder" :maxlength="wordLimit" @input="change($event)" /> :disabled="disableChange"
:value="value"
class="form-textarea"
:style="textareaStyle"
:placeholder="placeholder"
placeholder-class="form__placeholder"
:maxlength="wordLimit"
:auto-height="autoHeight"
@input="change($event)" />
<view v-if="wordLimit > 0" class="form-textarea__count"> <view v-if="wordLimit > 0" class="form-textarea__count">
{{ value && value.length ? value.length : 0 }} / {{ wordLimit }} {{ value && value.length ? value.length : 0 }} / {{ wordLimit }}
</view> </view>
@ -38,6 +46,14 @@ const props = defineProps({
wordLimit: { wordLimit: {
type: [Number, String], type: [Number, String],
default: 100 default: 100
},
autoHeight: {
type: Boolean,
default: false
},
rows: {
type: [Number, String],
default: 3
} }
}) })
@ -56,6 +72,13 @@ const wordLimit = computed(() => {
return 100 return 100
}) })
const textareaStyle = computed(() => {
const rowCount = typeof props.rows === 'number' ? props.rows : (Number(props.rows) || 3);
// 42rpx28rpx + 14rpx
const minHeight = rowCount * 42;
return `min-height: ${minHeight}rpx;`;
})
function change(e) { function change(e) {
emits('change', { emits('change', {
title: props.title, title: props.title,

View File

@ -1,6 +1,20 @@
<template> <template>
<form-positive-find
v-if="attrs.title === 'positiveFind'"
v-bind="attrs"
:form="form"
:disableChange="disableChange"
@change="change"
/>
<form-diagnosis-picker
v-else-if="attrs.title === 'diagnosis' || attrs.title === 'diagnosisName'"
v-bind="attrs"
:form="form"
:disableChange="disableChange"
@change="change"
/>
<form-surgical-history <form-surgical-history
v-if="attrs.title === 'surgicalHistory'" v-else-if="attrs.title === 'surgicalHistory'"
v-bind="attrs" v-bind="attrs"
:form="form" :form="form"
:disableChange="disableChange" :disableChange="disableChange"
@ -54,6 +68,8 @@ import formRunTime from './form-run-time.vue';
import formFiles from './form-files.vue'; import formFiles from './form-files.vue';
import formSelectImage from './form-select-image.vue'; import formSelectImage from './form-select-image.vue';
import formSurgicalHistory from './form-surgical-history.vue'; import formSurgicalHistory from './form-surgical-history.vue';
import formPositiveFind from './form-positive-find.vue';
import formDiagnosisPicker from './form-diagnosis-picker.vue';
defineProps({ defineProps({
form: { form: {

View File

@ -140,6 +140,18 @@
"navigationBarTitleText": "执行回访计划" "navigationBarTitleText": "执行回访计划"
} }
}, },
{
"path": "pages/library/diagnosis-list",
"style": {
"navigationBarTitleText": "诊断"
}
},
{
"path": "pages/others/edit-positive-find",
"style": {
"navigationBarTitleText": "阳性发现"
}
},
{ {
"path": "pages/work/work", "path": "pages/work/work",
"style": { "style": {

View File

@ -95,7 +95,7 @@
:key="t.key" :key="t.key"
class="tab" class="tab"
:class="{ active: currentTab === t.key }" :class="{ active: currentTab === t.key }"
@click="currentTab = t.key" @click="switchTab(t.key)"
> >
{{ t.title }} {{ t.title }}
</view> </view>
@ -224,7 +224,6 @@ import { storeToRefs } from 'pinia';
import HealthProfileTab from '@/components/archive-detail/health-profile-tab.vue'; import HealthProfileTab from '@/components/archive-detail/health-profile-tab.vue';
import ServiceInfoTab from '@/components/archive-detail/service-info-tab.vue'; import ServiceInfoTab from '@/components/archive-detail/service-info-tab.vue';
import FollowUpManageTab from '@/components/archive-detail/follow-up-manage-tab.vue'; import FollowUpManageTab from '@/components/archive-detail/follow-up-manage-tab.vue';
import { ensureSeed } from '@/components/archive-detail/mock';
import api from '@/utils/api'; import api from '@/utils/api';
import useAccountStore from '@/store/account'; import useAccountStore from '@/store/account';
import { hideLoading, loading, toast } from '@/utils/widget'; import { hideLoading, loading, toast } from '@/utils/widget';
@ -245,6 +244,12 @@ const archiveId = ref('');
const tabsScrollTop = ref(0); const tabsScrollTop = ref(0);
const instanceProxy = getCurrentInstance()?.proxy; const instanceProxy = getCurrentInstance()?.proxy;
function switchTab(key) {
currentTab.value = key;
// tab tab
uni.pageScrollTo({ scrollTop: 0, duration: 0 });
}
const archive = ref({ const archive = ref({
name: '', name: '',
sex: '', sex: '',
@ -438,7 +443,6 @@ onLoad((options) => {
if (!archiveId.value) { if (!archiveId.value) {
archiveId.value = String(archive.value.medicalRecordNo || archive.value.outpatientNo || archive.value.inpatientNo || `mock_${Date.now()}`); archiveId.value = String(archive.value.medicalRecordNo || archive.value.outpatientNo || archive.value.inpatientNo || `mock_${Date.now()}`);
} }
ensureSeed(archiveId.value, archive.value);
// //
fetchArchive(); fetchArchive();
@ -502,7 +506,8 @@ const idRows = computed(() => {
const createText = computed(() => { const createText = computed(() => {
const time = archive.value.createTime ? String(archive.value.createTime) : ''; const time = archive.value.createTime ? String(archive.value.createTime) : '';
const creator = archive.value.creator ? String(archive.value.creator) : ''; const rawCreator = archive.value.creator ? String(archive.value.creator) : '';
const creator = ['-', '—', '--'].includes(rawCreator.trim()) ? '' : rawCreator.trim();
if (time && creator) return `${time} ${creator}创建`; if (time && creator) return `${time} ${creator}创建`;
if (time) return `${time} 创建`; if (time) return `${time} 创建`;
return ''; return '';

View File

@ -13,7 +13,10 @@
<!-- 附件上传FormTemplate 不支持 files单独实现 --> <!-- 附件上传FormTemplate 不支持 files单独实现 -->
<view v-if="hasFilesField" class="upload-wrap"> <view v-if="hasFilesField" class="upload-wrap">
<view class="upload-title">文件上传</view> <view class="upload-row">
<view class="upload-label">文件上传</view>
<view class="upload-desc">支持5M文件pdf文件格式</view>
</view>
<view class="upload-grid"> <view class="upload-grid">
<view v-for="(f, idx) in fileList" :key="idx" class="upload-item" @click="previewFile(idx)"> <view v-for="(f, idx) in fileList" :key="idx" class="upload-item" @click="previewFile(idx)">
<image class="upload-thumb" :src="f.url" mode="aspectFill" /> <image class="upload-thumb" :src="f.url" mode="aspectFill" />
@ -44,12 +47,44 @@
import { computed, reactive, ref } from 'vue'; import { computed, reactive, ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app'; import { onLoad } from '@dcloudio/uni-app';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { storeToRefs } from 'pinia';
import FormTemplate from '@/components/form-template/index.vue'; import FormTemplate from '@/components/form-template/index.vue';
import { ensureSeed, getCurrentTeamId, getVisitRecord, getVisitRecordTemplate, removeVisitRecord, upsertVisitRecord } from '@/components/archive-detail/mock'; 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 archiveId = 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 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 recordId = ref('');
const templateType = ref(''); const templateType = ref('');
const customerName = ref('');
const template = computed(() => getVisitRecordTemplate(templateType.value)); const template = computed(() => getVisitRecordTemplate(templateType.value));
const detail = ref({}); const detail = ref({});
@ -84,27 +119,67 @@ function ensureFilesField() {
form.files = []; form.files = [];
} }
onLoad((options) => { onLoad(async (options) => {
archiveId.value = options?.archiveId ? String(options.archiveId) : ''; memberId.value = options?.memberId || options?.archiveId || '';
recordId.value = options?.id ? String(options.id) : ''; recordId.value = options?.id || '';
templateType.value = options?.type ? String(options.type) : ''; templateType.value = options?.type || '';
customerName.value = decodeURIComponent(options?.customerName || '');
ensureSeed(archiveId.value, { name: options?.name ? String(options.name) : '' });
if (recordId.value) { if (recordId.value) {
const record = getVisitRecord({ archiveId: archiveId.value, id: recordId.value }); await getDetail();
if (record) { } else {
templateType.value = record.templateType || record.medicalType || templateType.value; if (!templateType.value) templateType.value = 'outpatient';
detail.value = record; ensureFilesField();
ensureFilesField(); //
return; 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');
} }
} }
if (!templateType.value) templateType.value = 'outpatient';
ensureFilesField();
}); });
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 }) { function onChange({ title, value }) {
form[title] = value; form[title] = value;
const item = showItems.value.find((i) => i.title === title); const item = showItems.value.find((i) => i.title === title);
@ -118,67 +193,120 @@ function cancel() {
uni.navigateBack(); uni.navigateBack();
} }
function save() { async function save() {
if (!archiveId.value) { if (!memberId.value) {
uni.showToast({ title: '缺少 archiveId', icon: 'none' }); toast('缺少患者信息');
return; return;
} }
await ensureDoctor();
const corpId = getCorpId();
const userId = getUserId();
if (!corpId || !userId) return toast('缺少用户信息');
if (formRef.value?.verify && !formRef.value.verify()) return; if (formRef.value?.verify && !formRef.value.verify()) return;
// sortTime const params = {
const timeTitle = ...form,
templateType.value === 'outpatient' corpId,
? 'visitTime' memberId: memberId.value,
: templateType.value === 'inhospital' medicalType: templateType.value,
? 'inhosDate' };
: templateType.value === 'preConsultation'
? 'consultDate'
: templateType.value === 'physicalExaminationTemplate'
? 'inspectDate'
: '';
const timeValue = timeTitle ? forms.value[timeTitle] : '';
const sortTime = timeValue && dayjs(timeValue).isValid() ? dayjs(timeValue).valueOf() : Date.now();
upsertVisitRecord({ // / mobile diagnosisName
archiveId: archiveId.value, if ((templateType.value === 'outpatient' || templateType.value === 'inhospital') && form.diagnosis && !form.diagnosisName) {
record: { params.diagnosisName = form.diagnosis;
_id: recordId.value || '', }
medicalType: templateType.value,
templateType: templateType.value, if (recordId.value) {
tempName: template.value?.templateName || '健康档案', params._id = recordId.value;
sortTime, params.userId = userId;
teamId: detail.value?.teamId || getCurrentTeamId(), } else {
...form, params.creator = userId;
files: fileList.value, }
createTime: detail.value?.createTime || Date.now(),
creatorName: detail.value?.creatorName || '我', // sortTime使
}, const sortTimeKey = template.value?.service?.timeTitle || '';
}); if (sortTimeKey && form[sortTimeKey] && dayjs(form[sortTimeKey]).isValid()) {
uni.$emit('archive-detail:visit-record-changed'); params.sortTime = dayjs(form[sortTimeKey]).valueOf();
uni.showToast({ title: '保存成功', icon: 'success' }); } else {
setTimeout(() => uni.navigateBack(), 300); 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() { function remove() {
uni.showModal({ confirm('确定删除当前记录?', async () => {
title: '提示', if (!memberId.value || !recordId.value) return toast('缺少必要信息');
content: '确定删除当前记录?', await ensureDoctor();
success: (res) => { const corpId = getCorpId();
if (!res.confirm) return; if (!corpId) return toast('缺少必要信息');
removeVisitRecord({ archiveId: archiveId.value, id: recordId.value }); uniLoading('删除中...');
uni.$emit('archive-detail:visit-record-changed'); try {
uni.showToast({ title: '已删除', icon: 'success' }); const res = await api('removeMedicalRecord', {
setTimeout(() => uni.navigateBack(), 300); 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() { function addFiles() {
uni.chooseImage({ 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, count: 9,
type: 'file',
extension: [accept],
success: (res) => { success: (res) => {
const paths = Array.isArray(res.tempFilePaths) ? res.tempFilePaths : []; const files = Array.isArray(res.tempFiles) ? res.tempFiles : [];
const next = paths.map((p) => ({ url: p, name: '' })); 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 : []; const cur = Array.isArray(forms.value.files) ? forms.value.files : [];
form.files = [...cur, ...next]; form.files = [...cur, ...next];
}, },
@ -228,57 +356,68 @@ function previewFile(idx) {
.upload-wrap { .upload-wrap {
background: #fff; background: #fff;
margin: 10px 14px 0; padding: 24rpx 30rpx;
border-radius: 8px; border-bottom: 1px solid #eee;
padding: 14px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
} }
.upload-title { .upload-row {
font-size: 14px; display: flex;
font-weight: 700; align-items: baseline;
margin-bottom: 18rpx;
}
.upload-label {
font-size: 28rpx;
line-height: 42rpx;
color: #111827; color: #111827;
margin-bottom: 10px; flex-shrink: 0;
}
.upload-desc {
font-size: 24rpx;
color: #9ca3af;
margin-left: 8rpx;
} }
.upload-grid { .upload-grid {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 10px; gap: 18rpx;
} }
.upload-item { .upload-item {
width: 90px; width: 180rpx;
height: 70px; height: 140rpx;
position: relative; position: relative;
border: 1px solid #d1d5db; border: 1px solid #e5e7eb;
border-radius: 8rpx;
overflow: hidden;
background: #f9fafb; background: #f9fafb;
} }
.upload-thumb { .upload-thumb {
width: 90px; width: 100%;
height: 70px; height: 100%;
} }
.upload-remove { .upload-remove {
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
width: 20px; width: 36rpx;
height: 20px; height: 36rpx;
line-height: 20px; line-height: 36rpx;
text-align: center; text-align: center;
background: rgba(0, 0, 0, 0.55); background: rgba(0, 0, 0, 0.55);
color: #fff; color: #fff;
font-size: 14px; font-size: 28rpx;
} }
.upload-add { .upload-add {
width: 90px; width: 180rpx;
height: 70px; height: 140rpx;
border: 1px dashed #c7c7c7; border: 1px dashed #d1d5db;
border-radius: 8rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #666; color: #9ca3af;
} }
.plus { .plus {
font-size: 26px; font-size: 52rpx;
line-height: 26px; line-height: 52rpx;
} }
.footer { .footer {
position: fixed; position: fixed;

View File

@ -52,10 +52,13 @@
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app'; import { onLoad } from '@dcloudio/uni-app';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { getVisitRecord, removeVisitRecord } from '@/components/archive-detail/mock'; import api from '@/utils/api';
import { loading, hideLoading, toast } from '@/utils/widget';
import { getVisitRecordTemplate } from '@/components/archive-detail/templates';
const archiveId = ref(''); const archiveId = ref('');
const id = ref(''); const id = ref('');
const medicalType = ref('');
const record = ref({}); const record = ref({});
const files = computed(() => { const files = computed(() => {
@ -65,7 +68,38 @@ const files = computed(() => {
const templateType = computed(() => record.value?.templateType || record.value?.medicalType || ''); const templateType = computed(() => record.value?.templateType || record.value?.medicalType || '');
const typeLabel = computed(() => record.value?.tempName || '病历'); const typeLabel = computed(() => record.value?.tempName || getVisitRecordTemplate(templateType.value || medicalType.value)?.templateName || '病历');
function normalizeText(v) {
if (Array.isArray(v)) return v.filter((i) => i !== null && i !== undefined && String(i).trim()).join('');
if (v === 0) return '0';
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) : '';
}
const visitDate = computed(() => { const visitDate = computed(() => {
const t = templateType.value; const t = templateType.value;
@ -78,9 +112,10 @@ const visitDate = computed(() => {
const diagnosisText = computed(() => { const diagnosisText = computed(() => {
const t = templateType.value; const t = templateType.value;
if (t === 'preConsultation') return record.value?.chiefComplaint || record.value?.summary || '--'; if (t === 'preConsultation') return normalizeText(record.value?.chiefComplaint) || normalizeText(record.value?.summary) || '--';
if (t === 'physicalExaminationTemplate') return record.value?.positiveFind || record.value?.summary || '--'; if (t === 'physicalExaminationTemplate') return formatPositiveFind(record.value?.positiveFind) || normalizeText(record.value?.summary) || '--';
return record.value?.diagnosisName || record.value?.summary || '--'; 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 sections = computed(() => { const sections = computed(() => {
@ -118,7 +153,7 @@ const sections = computed(() => {
if (t === 'physicalExaminationTemplate') { if (t === 'physicalExaminationTemplate') {
const corp = push('体检机构', record.value?.corpName); const corp = push('体检机构', record.value?.corpName);
const pkg = push('体检套餐', record.value?.inspectPakageName); const pkg = push('体检套餐', record.value?.inspectPakageName);
const positive = push('阳性发现', record.value?.positiveFind); const positive = push('阳性发现及处理意见', formatPositiveFind(record.value?.positiveFind, { withOpinion: true }));
const summary = push('摘要', record.value?.summary); const summary = push('摘要', record.value?.summary);
[corp, pkg, positive, summary].forEach((i) => i && list.push(i)); [corp, pkg, positive, summary].forEach((i) => i && list.push(i));
return list; return list;
@ -130,26 +165,61 @@ const sections = computed(() => {
const topText = computed(() => { const topText = computed(() => {
const time = record.value?.createTime ? dayjs(record.value.createTime).format('YYYY-MM-DD HH:mm') : ''; const time = record.value?.createTime ? dayjs(record.value.createTime).format('YYYY-MM-DD HH:mm') : '';
const name = record.value?.creatorName ? String(record.value.creatorName) : ''; const rawName = record.value?.creatorName ? String(record.value.creatorName) : '';
return `${time || '--'} ${name || '--'}创建`; 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 || '--'}`;
}); });
onLoad((opt) => { onLoad(async (opt) => {
archiveId.value = opt?.archiveId ? String(opt.archiveId) : ''; archiveId.value = opt?.archiveId ? String(opt.archiveId) : '';
id.value = opt?.id ? String(opt.id) : ''; id.value = opt?.id ? String(opt.id) : '';
if (!archiveId.value || !id.value) { medicalType.value = opt?.type ? String(opt.type) : '';
uni.showToast({ title: '参数缺失', icon: 'none' }); if (!archiveId.value || !id.value || !medicalType.value) {
toast('参数缺失');
setTimeout(() => uni.navigateBack(), 300); setTimeout(() => uni.navigateBack(), 300);
return; return;
} }
const r = getVisitRecord({ archiveId: archiveId.value, id: id.value });
if (!r) { // 使 API
uni.showToast({ title: '记录不存在', icon: 'none' }); loading('加载中...');
try {
const corpId = getCorpId();
if (!corpId) {
hideLoading();
toast('缺少 corpId');
setTimeout(() => uni.navigateBack(), 300);
return;
}
const res = await api('getMedicalRecordById', {
_id: id.value,
corpId,
memberId: archiveId.value,
medicalType: medicalType.value,
});
hideLoading();
const r = res?.record || res?.data?.record || null;
if (!res?.success || !r) {
toast('记录不存在');
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;
uni.setNavigationBarTitle({ title: String(typeLabel.value || '病历详情') });
} catch (error) {
hideLoading();
console.error('获取病历记录失败:', error);
toast('加载失败');
setTimeout(() => uni.navigateBack(), 300); setTimeout(() => uni.navigateBack(), 300);
return;
} }
record.value = r;
uni.setNavigationBarTitle({ title: r?.tempName ? String(r.tempName) : '病历详情' });
}); });
function preview(idx) { function preview(idx) {
@ -168,12 +238,26 @@ function remove() {
uni.showModal({ uni.showModal({
title: '提示', title: '提示',
content: '确定删除当前记录?', content: '确定删除当前记录?',
success: (res) => { success: async (res) => {
if (!res.confirm) return; if (!res.confirm) return;
removeVisitRecord({ archiveId: archiveId.value, id: id.value });
uni.$emit('archive-detail:visit-record-changed'); loading('删除中...');
uni.showToast({ title: '已删除', icon: 'success' }); try {
setTimeout(() => uni.navigateBack(), 300); 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('删除失败');
}
}, },
}); });
} }

View File

@ -0,0 +1,185 @@
<template>
<view class="page">
<view class="top">
<uni-easyinput v-model="keyword" prefixIcon="search" placeholder="请搜索诊断名称" @input="onInput" />
</view>
<scroll-view scroll-y class="scroll">
<view v-for="item in showList" :key="item.key" class="row" @click="toggle(item)">
<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 style="height: 120px;"></view>
</scroll-view>
<view class="footer">
<button class="btn primary" @click="save">确定</button>
</view>
</view>
</template>
<script setup>
import { computed, onUnmounted, ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import api from '@/utils/api';
import { toast } from '@/utils/widget';
const ready = ref(false);
const keyword = ref('');
const list = ref([]);
const selections = ref([]);
const mult = ref(false);
const eventName = ref('change-diagnosis');
const selectedMap = computed(() => selections.value.reduce((m, i) => ((m[i] = true), m), {}));
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;
});
let timer = null;
function onInput() {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
query();
}, 600);
}
onUnmounted(() => {
if (timer) clearTimeout(timer);
});
onLoad((opt) => {
eventName.value = opt?.eventName ? String(opt.eventName) : 'change-diagnosis';
mult.value = String(opt?.mult || '') === 'YES';
if (mult.value) {
const data = uni.getStorageSync('diagnosis-list-selection');
selections.value = Array.isArray(data) ? data : [];
uni.removeStorageSync('diagnosis-list-selection');
} else {
const v = opt?.value ? String(opt.value) : '';
selections.value = v ? [v] : [];
}
ready.value = true;
query();
});
async function query() {
if (!ready.value) return;
const value = String(keyword.value || '').trim();
uni.showLoading({ title: '加载中...' });
try {
const res = await api('getDisease', { diseaseName: value });
const arr = Array.isArray(res?.data?.data) ? res.data.data : [];
list.value = arr
.map((i) => {
const label = normalizeText(i?.diseaseName);
const code = normalizeText(i?.code);
if (!label) return null;
return { label, value: code || label, key: code || label };
})
.filter(Boolean);
} catch (e) {
list.value = [];
} finally {
uni.hideLoading();
}
}
function toggle(item) {
if (!item || !item.label) return;
const index = selections.value.findIndex((i) => i === item.label);
if (index >= 0) {
selections.value.splice(index, 1);
return;
}
if (mult.value) selections.value.push(item.label);
else selections.value = [item.label];
}
function save() {
if (selections.value.length === 0) return toast('请选择');
if (mult.value) uni.$emit(eventName.value, selections.value);
else uni.$emit(eventName.value, selections.value[0]);
uni.navigateBack();
}
</script>
<style scoped>
.page {
min-height: 100vh;
background: #fff;
padding-bottom: calc(76px + env(safe-area-inset-bottom));
}
.top {
padding: 12px 14px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
}
.scroll {
height: calc(100vh - 140px);
}
.row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 14px;
border-bottom: 1px solid #f2f2f2;
}
.label {
font-size: 14px;
color: #111827;
margin-right: 10px;
}
.empty {
padding: 60px 0;
text-align: center;
color: #9aa0a6;
font-size: 13px;
}
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
}
.btn {
width: 100%;
height: 44px;
line-height: 44px;
border-radius: 6px;
font-size: 15px;
}
.btn::after {
border: none;
}
.btn.primary {
background: #4f6ef7;
color: #fff;
}
</style>

View File

@ -0,0 +1,128 @@
<template>
<view class="page">
<scroll-view scroll-y class="scroll">
<view class="section">
<view class="title">阳性发现</view>
<textarea
v-model="category"
class="textarea"
placeholder="请输入阳性发现"
placeholder-class="placeholder"
:maxlength="200"
/>
</view>
<view class="section">
<view class="title">处理意见</view>
<textarea
v-model="opinion"
class="textarea"
placeholder="请输入处理意见"
placeholder-class="placeholder"
:maxlength="500"
/>
</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>
</template>
<script setup>
import { onLoad } from '@dcloudio/uni-app';
import { ref } from 'vue';
import { toast } from '@/utils/widget';
const category = ref('');
const opinion = ref('');
const eventName = ref('');
onLoad((opt) => {
eventName.value = String(opt?.eventName || '');
if (opt?.title) uni.setNavigationBarTitle({ title: String(opt.title) });
const data = uni.getStorageSync('current-positive-find') || {};
category.value = typeof data?.category === 'string' ? data.category : '';
opinion.value = typeof data?.opinion === 'string' ? data.opinion : '';
});
function cancel() {
uni.navigateBack();
}
function save() {
if (!String(category.value || '').trim()) return toast('请输入阳性发现');
if (!String(opinion.value || '').trim()) return toast('请输入处理意见');
if (eventName.value) {
uni.$emit(eventName.value, { category: String(category.value).trim(), opinion: String(opinion.value).trim() });
}
uni.navigateBack();
}
</script>
<style scoped>
.page {
min-height: 100vh;
background: #fff;
padding-bottom: calc(76px + env(safe-area-inset-bottom));
}
.scroll {
height: 100vh;
}
.section {
padding: 16px 14px 0;
}
.title {
font-size: 15px;
font-weight: 700;
color: #111827;
margin-bottom: 10px;
}
.textarea {
width: 100%;
min-height: 120px;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 10px;
box-sizing: border-box;
font-size: 14px;
color: #111827;
}
.placeholder {
color: #9aa0a6;
}
.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;
}
</style>

View File

@ -93,6 +93,14 @@ export default [
path: 'pages/case/plan-execute', path: 'pages/case/plan-execute',
meta: { title: '执行回访计划', login: false }, meta: { title: '执行回访计划', login: false },
}, },
{
path: 'pages/library/diagnosis-list',
meta: { title: '诊断', login: false },
},
{
path: 'pages/others/edit-positive-find',
meta: { title: '阳性发现', login: false },
},
{ {
path: 'pages/work/work', path: 'pages/work/work',
meta: { title: '工作台', login: false } meta: { title: '工作台', login: false }

View File

@ -16,7 +16,9 @@ const urlsConfig = {
}, },
knowledgeBase: { knowledgeBase: {
getArticleByIds: 'getArticleByIds' getArticleByIds: 'getArticleByIds',
// 诊断库(对齐 ykt-management-mobile/src/api/knowledgeBase.js
getDisease: 'getDisease',
}, },
member: { member: {
addCustomer: 'add', addCustomer: 'add',
@ -36,6 +38,12 @@ const urlsConfig = {
searchCorpCustomer: 'searchCorpCustomer', searchCorpCustomer: 'searchCorpCustomer',
searchCorpCustomerWithFollowTime: 'searchCorpCustomerWithFollowTime', searchCorpCustomerWithFollowTime: 'searchCorpCustomerWithFollowTime',
unbindMiniAppArchive: 'unbindMiniAppArchive', unbindMiniAppArchive: 'unbindMiniAppArchive',
// 健康档案相关接口(对齐 ykt-management-mobile/src/api/member.js
addMedicalRecord: 'addMedicalRecord',
getMedicalRecordById: 'getMedicalRecordById',
updateMedicalRecord: 'updateMedicalRecord',
removeMedicalRecord: 'removeMedicalRecord',
getCustomerMedicalRecord: 'getCustomerMedicalRecord',
}, },
wecom: { wecom: {
addContactWay: 'addContactWay' addContactWay: 'addContactWay'