Merge commit 'f552ad2b4e333631f5e7fbd0f3f089e93e15e6b0' into dev-wdb

This commit is contained in:
wangdongbo 2026-02-09 09:17:55 +08:00
commit 0dfb05fe97
13 changed files with 1426 additions and 850 deletions

View File

@ -2,18 +2,12 @@
<view class="page"> <view class="page">
<view class="card"> <view class="card">
<view class="header"> <view class="header">
<view class="avatar">
<image v-if="archive.avatar" class="avatar-img" :src="archive.avatar" mode="aspectFill" />
</view>
<view class="header-main"> <view class="header-main">
<view class="name-row"> <view class="name-row">
<text class="name">{{ archive.name || '-' }}</text> <text class="name">{{ archive.name || '-' }}</text>
<text v-if="sexOrAge" class="meta">{{ sexOrAge }}</text> <text v-if="sexOrAge" class="meta">{{ sexOrAge }}</text>
</view> </view>
<view v-if="archive.mobile" class="sub-line">{{ archive.mobile }}</view>
<view v-if="idRows.length" class="id-rows"> <view v-if="idRows.length" class="id-rows">
<view v-for="row in idRows" :key="row.label" class="id-row"> <view v-for="row in idRows" :key="row.label" class="id-row">
<text class="id-label">{{ row.label }}</text> <text class="id-label">{{ row.label }}</text>
@ -296,7 +290,7 @@ function normalizeArchiveFromApi(raw) {
name: r.name || '', name: r.name || '',
sex: r.sex || r.gender || '', sex: r.sex || r.gender || '',
age: r.age ?? '', age: r.age ?? '',
avatar: r.avatar || '', avatar: r.avatar || r.avatarUrl || r.headImgUrl || r.headimgurl || r.headImageUrl || r.profilePhoto || r.photoUrl || r.photo || '',
mobile: r.mobile || r.phone1 || r.phone || '', mobile: r.mobile || r.phone1 || r.phone || '',
outpatientNo: r.outpatientNo || '', outpatientNo: r.outpatientNo || '',
inpatientNo: r.inpatientNo || '', inpatientNo: r.inpatientNo || '',
@ -778,25 +772,10 @@ const saveAddGroup = async () => {
border-bottom: 2rpx solid #f2f2f2; border-bottom: 2rpx solid #f2f2f2;
} }
.avatar {
width: 112rpx;
height: 112rpx;
border-radius: 12rpx;
border: 2rpx solid #e8e8e8;
background: #fafafa;
overflow: hidden;
flex-shrink: 0;
}
.avatar-img {
width: 112rpx;
height: 112rpx;
}
.header-main { .header-main {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
padding: 0 20rpx; padding: 0 20rpx 0 0;
} }
.name-row { .name-row {

View File

@ -1,37 +1,18 @@
<template> <template>
<view class="transfer-container"> <view class="transfer-container">
<view class="content"> <view class="content">
<view class="section-title">选择新负责团队</view> <view class="tips">处理中...</view>
<view class="selector-item" @click="selectTeam">
<text :class="team ? '' : 'placeholder'">{{ team ? team.name : '请选择团队' }}</text>
<uni-icons type="arrowdown" size="16" color="#999" />
</view>
<template v-if="team">
<view class="section-title">选择责任人</view>
<view class="selector-item" @click="selectUser">
<text :class="userId ? '' : 'placeholder'">{{ userLabel || '请选择责任人' }}</text>
<uni-icons type="arrowdown" size="16" color="#999" />
</view>
</template>
<view class="tips">客户将与本团队解除服务关系本团队成员将没有权限查询到客户档案</view>
</view>
<view class="footer">
<button class="btn plain" @click="cancel">取消</button>
<button class="btn primary" @click="save">保存</button>
</view> </view>
</view> </view>
</template> </template>
<script setup> <script setup>
import { computed, ref } from 'vue'; import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app'; import { onLoad } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
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 { confirm as uniConfirm, hideLoading, loading as showLoading, toast } from '@/utils/widget';
const CURRENT_TEAM_STORAGE_KEY = 'ykt_case_current_team'; const CURRENT_TEAM_STORAGE_KEY = 'ykt_case_current_team';
const NEED_RELOAD_STORAGE_KEY = 'ykt_case_need_reload'; const NEED_RELOAD_STORAGE_KEY = 'ykt_case_need_reload';
@ -44,15 +25,6 @@ const { getDoctorInfo } = accountStore;
const customerIds = ref([]); const customerIds = ref([]);
const currentTeam = ref(null); const currentTeam = ref(null);
const teams = ref([]); const teams = ref([]);
const team = ref(null);
const teamMembers = ref([]);
const userId = ref('');
const userLabel = computed(() => {
const list = Array.isArray(teamMembers.value) ? teamMembers.value : [];
const found = list.find((m) => String(m?.userid || '') === String(userId.value));
return found ? String(found.anotherName || found.name || found.userid || '') : '';
});
function getUserId() { function getUserId() {
const d = doctorInfo.value || {}; const d = doctorInfo.value || {};
@ -71,6 +43,21 @@ function getCurrentTeamId() {
return String(currentTeam.value?.teamId || '') || ''; return String(currentTeam.value?.teamId || '') || '';
} }
function showActionSheet(itemList = [], title = '') {
return new Promise((resolve, reject) => {
if (!Array.isArray(itemList) || itemList.length === 0) {
reject(new Error('empty'));
return;
}
uni.showActionSheet({
title,
itemList,
success: ({ tapIndex }) => resolve(tapIndex),
fail: () => reject(new Error('cancel')),
});
});
}
function normalizeTeam(raw) { function normalizeTeam(raw) {
if (!raw || typeof raw !== 'object') return null; if (!raw || typeof raw !== 'object') return null;
const teamId = raw.teamId || raw.id || raw._id || ''; const teamId = raw.teamId || raw.id || raw._id || '';
@ -94,114 +81,174 @@ async function loadTeams() {
await ensureDoctor(); await ensureDoctor();
const corpId = getCorpId(); const corpId = getCorpId();
const userId = getUserId(); const userId = getUserId();
if (!corpId || !userId) return; if (!corpId || !userId) return [];
const res = await api('getTeamBymember', { corpId, corpUserId: userId }); const res = await api('getTeamBymember', { corpId, corpUserId: userId });
if (!res?.success) { if (!res?.success) {
toast(res?.message || '获取团队失败'); toast(res?.message || '获取团队失败');
return; return [];
} }
const list = Array.isArray(res?.data) ? res.data : Array.isArray(res?.data?.data) ? res.data.data : []; const list = Array.isArray(res?.data) ? res.data : Array.isArray(res?.data?.data) ? res.data.data : [];
teams.value = list.map(normalizeTeam).filter(Boolean); teams.value = list.map(normalizeTeam).filter(Boolean);
return teams.value;
} }
async function loadTeamMembers(teamId) { async function fetchTeamMembers(teamId) {
const corpId = getCorpId(); const corpId = getCorpId();
if (!teamId) return; if (!teamId) return [];
const res = await api('getTeamData', { corpId, teamId }); const res = await api('getTeamData', { corpId, teamId });
if (!res?.success) { if (!res?.success) {
toast(res?.message || '获取团队成员失败'); toast(res?.message || '获取团队成员失败');
teamMembers.value = []; return [];
return;
} }
const t = res?.data && typeof res.data === 'object' ? res.data : {}; const t = res?.data && typeof res.data === 'object' ? res.data : {};
teamMembers.value = Array.isArray(t.memberList) ? t.memberList : []; return Array.isArray(t.memberList) ? t.memberList : [];
} }
const selectTeam = async () => { async function transferToCustomerPool() {
if (!teams.value.length) await loadTeams(); try {
const currentId = getCurrentTeamId(); await uniConfirm('客户将与本团队解除服务关系,本团队成员将没有权限查询到客户档案。');
const candidates = teams.value.filter((t) => t.teamId !== currentId); } catch (e) {
if (!candidates.length) { return false;
toast('暂无可选团队');
return;
} }
uni.showActionSheet({
itemList: candidates.map((t) => t.name),
success: async (res) => {
team.value = candidates[res.tapIndex] || null;
userId.value = '';
teamMembers.value = [];
if (team.value) await loadTeamMembers(team.value.teamId);
},
});
};
const selectUser = () => {
const list = Array.isArray(teamMembers.value) ? teamMembers.value : [];
if (!list.length) {
toast('当前团队暂无可选成员');
return;
}
const labels = list.map((m) => String(m?.anotherName || m?.name || m?.userid || ''));
uni.showActionSheet({
itemList: labels,
success: (res) => {
const picked = list[res.tapIndex];
userId.value = picked?.userid ? String(picked.userid) : '';
},
});
};
const cancel = () => {
uni.navigateBack();
};
const save = async () => {
if (!team.value) return toast('请选择团队');
if (!userId.value) return toast('请选择负责人');
const corpId = getCorpId(); const corpId = getCorpId();
const currentTeamId = getCurrentTeamId(); const currentTeamId = getCurrentTeamId();
const creatorUserId = getUserId(); const creatorUserId = getUserId();
if (!corpId || !currentTeamId || !creatorUserId) return toast('缺少用户/团队信息'); if (!corpId || !currentTeamId || !creatorUserId) {
toast('缺少用户/团队信息');
return false;
}
loading('保存中...'); showLoading('保存中...');
try { try {
const res = await api('transferCustomers', { const res = await api('transferCustomers', {
corpId, corpId,
customerIds: customerIds.value, customerIds: customerIds.value,
currentTeamId, currentTeamId,
targetTeamId: team.value.teamId, operationType: 'transferToCustomerPool',
targetUserId: userId.value, creatorUserId,
});
if (!res?.success) {
toast(res?.message || '操作失败');
return false;
}
toast('操作成功');
return true;
} catch (e) {
toast('操作失败');
return false;
} finally {
hideLoading();
}
}
async function transferToOtherTeam() {
const corpId = getCorpId();
const currentTeamId = getCurrentTeamId();
const creatorUserId = getUserId();
if (!corpId || !currentTeamId || !creatorUserId) {
toast('缺少用户/团队信息');
return false;
}
if (!teams.value.length) await loadTeams();
const candidates = teams.value.filter((t) => String(t?.teamId || '') !== String(currentTeamId));
if (!candidates.length) {
toast('暂无可选团队');
return false;
}
let teamIndex;
try {
teamIndex = await showActionSheet(candidates.map((t) => t.name || ''), '选择新负责团队');
} catch (e) {
return false;
}
const pickedTeam = candidates[teamIndex];
if (!pickedTeam?.teamId) {
toast('团队信息异常');
return false;
}
const members = await fetchTeamMembers(pickedTeam.teamId);
if (!members.length) {
toast('当前团队暂无可选成员');
return false;
}
let userIndex;
try {
userIndex = await showActionSheet(
members.map((m) => String(m?.anotherName || m?.name || m?.userid || '')),
'选择责任人'
);
} catch (e) {
return false;
}
const targetUserId = String(members[userIndex]?.userid || '') || '';
if (!targetUserId) {
toast('责任人信息异常');
return false;
}
showLoading('保存中...');
try {
const res = await api('transferCustomers', {
corpId,
customerIds: customerIds.value,
currentTeamId,
targetTeamId: String(pickedTeam.teamId),
targetUserId,
operationType: 'transferToOtherTeam', operationType: 'transferToOtherTeam',
creatorUserId, creatorUserId,
}); });
if (!res?.success) { if (!res?.success) {
toast(res?.message || '操作失败'); toast(res?.message || '操作失败');
return; return false;
} }
toast('操作成功'); toast('操作成功');
uni.removeStorageSync(BATCH_CUSTOMER_IDS_KEY); return true;
uni.setStorageSync(NEED_RELOAD_STORAGE_KEY, 1);
uni.navigateBack();
} catch (e) { } catch (e) {
toast('操作失败'); toast('操作失败');
return false;
} finally { } finally {
hideLoading(); hideLoading();
} }
}; }
onLoad(async () => { async function runFlow(options = {}) {
let mode = options?.mode === 'pool' ? 'pool' : options?.mode === 'team' ? 'team' : '';
if (!mode) {
let pick;
try {
pick = await showActionSheet(['转移给其他团队', '转移至客户公共池']);
} catch (e) {
return false;
}
mode = pick === 0 ? 'team' : 'pool';
}
return mode === 'team' ? await transferToOtherTeam() : await transferToCustomerPool();
}
onLoad(async (options = {}) => {
customerIds.value = Array.isArray(uni.getStorageSync(BATCH_CUSTOMER_IDS_KEY)) customerIds.value = Array.isArray(uni.getStorageSync(BATCH_CUSTOMER_IDS_KEY))
? uni.getStorageSync(BATCH_CUSTOMER_IDS_KEY).map(String).filter(Boolean) ? uni.getStorageSync(BATCH_CUSTOMER_IDS_KEY).map(String).filter(Boolean)
: []; : [];
currentTeam.value = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY) || null; currentTeam.value = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY) || null;
if (!customerIds.value.length) { if (!customerIds.value.length) {
toast('未选择客户'); toast('未选择客户');
setTimeout(() => uni.navigateBack(), 200); setTimeout(() => uni.navigateBack(), 200);
return; return;
} }
await loadTeams();
const ok = await runFlow(options);
if (ok) {
uni.removeStorageSync(BATCH_CUSTOMER_IDS_KEY);
uni.setStorageSync(NEED_RELOAD_STORAGE_KEY, 1);
}
setTimeout(() => uni.navigateBack(), 50);
}); });
</script> </script>

View File

@ -40,13 +40,6 @@
<image class="pen" src="/static/icons/icon-pen.svg" /> <image class="pen" src="/static/icons/icon-pen.svg" />
</view> </view>
<view class="rows"> <view class="rows">
<view class="row" @click="openTransferRecord">
<view class="label">院内来源</view>
<view class="val link">
{{ latestTransferRecord?.executeTeamName || '点击查看' }}
<uni-icons type="arrowright" size="14" color="#0877F1" />
</view>
</view>
<form-template <form-template
v-if="editing && effectiveInternalItems.length" v-if="editing && effectiveInternalItems.length"
ref="internalFormRef" ref="internalFormRef"
@ -148,7 +141,15 @@ const fallbackInternalItems = [
]; ];
const effectiveBaseItems = computed(() => (Array.isArray(props.baseItems) && props.baseItems.length ? props.baseItems : fallbackBaseItems)); const effectiveBaseItems = computed(() => (Array.isArray(props.baseItems) && props.baseItems.length ? props.baseItems : fallbackBaseItems));
const effectiveInternalItems = computed(() => (Array.isArray(props.internalItems) && props.internalItems.length ? props.internalItems : fallbackInternalItems)); function isInHospitalSourceItem(item) {
const title = String(item?.title || '');
const name = String(item?.name || '');
return title === 'inHospitalSource' || title === 'hospitalSource' || name.includes('院内来源');
}
const effectiveInternalItems = computed(() => {
const raw = Array.isArray(props.internalItems) && props.internalItems.length ? props.internalItems : fallbackInternalItems;
return raw.filter((i) => i && !isInHospitalSourceItem(i));
});
const filterRule = { const filterRule = {
reference(formModel) { reference(formModel) {

View File

@ -49,18 +49,28 @@
<view v-if="records.length === 0" class="empty">暂无数据</view> <view v-if="records.length === 0" class="empty">暂无数据</view>
</view> </view>
<view class="fab" :style="{ bottom: `${floatingBottom}px` }" @click="add"> <picker
<uni-icons type="plusempty" size="24" color="#fff" /> class="fab-picker"
</view> mode="selector"
:range="selectableTemplates"
range-key="name"
:disabled="fabPickerDisabled"
:style="{ bottom: `${floatingBottom}px` }"
@change="pickAddType"
>
<view class="fab" :class="{ 'fab--disabled': selectableTemplates.length === 0 }" @tap="onFabTap">
<uni-icons type="plusempty" size="24" color="#fff" />
</view>
</picker>
</view> </view>
</template> </template>
<script setup> <script setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'; import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { VISIT_RECORD_TEMPLATES } from './templates';
import api from '@/utils/api'; import api from '@/utils/api';
import { loading, hideLoading } from '@/utils/widget'; import { loading, hideLoading, toast } from '@/utils/widget';
import { normalizeTemplate } from '../../utils/template';
const props = defineProps({ const props = defineProps({
data: { type: Object, default: () => ({}) }, data: { type: Object, default: () => ({}) },
@ -68,7 +78,16 @@ const props = defineProps({
floatingBottom: { type: Number, default: 16 }, floatingBottom: { type: Number, default: 16 },
}); });
const templates = ref(VISIT_RECORD_TEMPLATES.map(t => ({ name: t.templateName, templateType: t.templateType }))); const FALLBACK_TEMPLATE_TYPES = ['outpatient', 'inhospital', 'preConsultation', 'physicalExaminationTemplate'];
const templates = ref([]);
const selectableTemplates = computed(() => templates.value.filter((i) => i && i.templateType && typeof i.name === 'string' && i.name.trim()));
const useActionSheet = computed(() => selectableTemplates.value.length > 0 && selectableTemplates.value.length <= 6);
const fabPickerDisabled = computed(() => selectableTemplates.value.length === 0 || useActionSheet.value);
const templateMap = computed(() => templates.value.reduce((m, t) => {
if (t?.templateType) m[String(t.templateType)] = t;
return m;
}, {}));
const availableTypes = computed(() => (templates.value.length ? templates.value.map((i) => i.templateType) : FALLBACK_TEMPLATE_TYPES));
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' });
@ -108,6 +127,50 @@ function getCorpId() {
return team?.corpId ? String(team.corpId) : ''; return team?.corpId ? String(team.corpId) : '';
} }
const loadedCorpId = ref('');
async function loadVisitTemplates() {
const corpId = getCorpId();
if (!corpId) return;
if (loadedCorpId.value === corpId && templates.value.length) return;
loadedCorpId.value = corpId;
try {
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 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;
const detailRes = await api('getTemplateListByTemptype', { corpId, templateTypeList: typeList });
const detail = detailRes?.data && Array.isArray(detailRes.data?.data) ? detailRes.data.data : Array.isArray(detailRes?.data) ? detailRes.data : [];
const temps = Array.isArray(detail) ? detail : [];
const byType = temps.reduce((m, t) => {
const k = t?.templateType ? String(t.templateType) : '';
if (k) m[k] = t;
return m;
}, {});
const ordered = typeList.map((t) => byType[String(t)]).filter(Boolean);
const next = ordered
.map((t) => {
const temp = normalizeTemplate(t);
const name = String(temp?.name || temp?.templateName || temp?.templateTypeName || '') || String(temp?.templateType || '');
return {
templateType: String(temp?.templateType || ''),
name,
service: temp?.service || {},
templateList: Array.isArray(temp?.templateList) ? temp.templateList : [],
};
})
.filter((i) => i && i.templateType);
if (next.length) templates.value = next;
} catch (e) {
console.error('loadVisitTemplates error:', e);
}
}
const userNameMap = ref({}); const userNameMap = ref({});
const loadedTeamId = ref(''); const loadedTeamId = ref('');
function resolveUserName(userId) { function resolveUserName(userId) {
@ -138,6 +201,8 @@ async function loadTeamMembers() {
} }
function getSortTimeTitle(templateType) { function getSortTimeTitle(templateType) {
const t = templateMap.value[String(templateType || '')] || {};
if (t?.service?.timeTitle) return String(t.service.timeTitle);
if (templateType === 'outpatient') return 'visitTime'; if (templateType === 'outpatient') return 'visitTime';
if (templateType === 'inhospital') return 'inhosDate'; if (templateType === 'inhospital') return 'inhosDate';
if (templateType === 'preConsultation') return 'consultDate'; if (templateType === 'preConsultation') return 'consultDate';
@ -172,8 +237,8 @@ function formatPositiveFind(v, { withOpinion = false } = {}) {
} }
function getTemplateName(type) { function getTemplateName(type) {
const t = VISIT_RECORD_TEMPLATES.find((i) => i && i.templateType === type); const t = templateMap.value[String(type || '')];
return t?.templateName ? String(t.templateName) : ''; return t?.name ? String(t.name) : '';
} }
function toDateStr(sortTime) { function toDateStr(sortTime) {
@ -193,6 +258,7 @@ async function refreshList() {
const corpId = getCorpId(); const corpId = getCorpId();
if (!corpId) return; if (!corpId) return;
await loadVisitTemplates();
loadTeamMembers(); loadTeamMembers();
loading('加载中...'); loading('加载中...');
@ -201,7 +267,7 @@ async function refreshList() {
// //
params.medicalType = params.medicalType =
currentType.value.value === 'ALL' ? templates.value.map((i) => i.templateType) : currentType.value.value; currentType.value.value === 'ALL' ? availableTypes.value : currentType.value.value;
// //
if (Array.isArray(dateRange.value) && dateRange.value.length === 2 && dateRange.value[0] && dateRange.value[1]) { if (Array.isArray(dateRange.value) && dateRange.value.length === 2 && dateRange.value[0] && dateRange.value[1]) {
@ -329,18 +395,58 @@ function previewFiles(r, idx) {
uni.previewImage({ urls, current: urls[idx] }); uni.previewImage({ urls, current: urls[idx] });
} }
function add() { function goAdd(t) {
if (!t?.templateType) return;
uni.navigateTo({
url: `/pages/case/visit-record-detail?archiveId=${encodeURIComponent(props.archiveId)}&type=${encodeURIComponent(t.templateType)}&name=${encodeURIComponent(props.data?.name || '')}`,
});
}
function pickAddType(e) {
if (!props.archiveId) return toast('缺少档案信息');
const idx = Number(e?.detail?.value ?? -1);
const t = selectableTemplates.value[idx];
if (!t) return;
goAdd(t);
}
function showAddActionSheet() {
if (!props.archiveId) return toast('缺少档案信息');
const list = selectableTemplates.value;
if (!list.length) return toast('暂无可用病历模板');
uni.showActionSheet({ uni.showActionSheet({
itemList: templates.value.map((i) => i.name), itemList: list.map((i) => i.name),
success: ({ tapIndex }) => { success: ({ tapIndex }) => {
const t = templates.value[tapIndex]; const t = list[tapIndex];
uni.navigateTo({ if (!t) return;
url: `/pages/case/visit-record-detail?archiveId=${encodeURIComponent(props.archiveId)}&type=${encodeURIComponent(t.templateType)}&name=${encodeURIComponent(props.data?.name || '')}`, goAdd(t);
}); },
fail: (e) => {
//
const errMsg = String(e?.errMsg || '');
if (errMsg && errMsg.includes('cancel')) return;
if (errMsg) console.warn('[health-profile-tab] showActionSheet fail:', errMsg);
}, },
}); });
} }
async function onFabTap() {
if (!props.archiveId) return toast('缺少档案信息');
//
if (!templates.value.length) {
loading('加载模板...');
try {
await loadVisitTemplates();
} finally {
hideLoading();
}
}
// <=6 actionSheet>6 picker
if (useActionSheet.value) showAddActionSheet();
}
function edit(record) { function edit(record) {
const type = String(record?.medicalType || record?.templateType || '') || ''; const type = String(record?.medicalType || record?.templateType || '') || '';
uni.navigateTo({ uni.navigateTo({
@ -349,6 +455,8 @@ function edit(record) {
} }
onMounted(() => { onMounted(() => {
// + picker
loadVisitTemplates();
// archiveId watch // archiveId watch
refreshList(); refreshList();
uni.$on('archive-detail:visit-record-changed', refreshList); uni.$on('archive-detail:visit-record-changed', refreshList);
@ -533,9 +641,14 @@ watch(
font-size: 26rpx; font-size: 26rpx;
} }
.fab { .fab-picker {
position: fixed; position: fixed;
right: 32rpx; right: 32rpx;
width: 104rpx;
height: 104rpx;
z-index: 20;
}
.fab {
width: 104rpx; width: 104rpx;
height: 104rpx; height: 104rpx;
border-radius: 52rpx; border-radius: 52rpx;
@ -544,6 +657,8 @@ watch(
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: 0 20rpx 36rpx rgba(79, 110, 247, 0.35); box-shadow: 0 20rpx 36rpx rgba(79, 110, 247, 0.35);
z-index: 20; }
.fab--disabled {
opacity: 0.5;
} }
</style> </style>

View File

@ -40,8 +40,8 @@
</view> </view>
<view class="meta"> <view class="meta">
<view class="tag">{{ i.typeStr }}</view> <view class="tag">{{ i.typeStr }}</view>
<view class="meta-text">{{ i.executorName }}</view> <view class="meta-text">{{ executorText(i) }}</view>
<view class="meta-text truncate">{{ i.executeTeamName }}</view> <view class="meta-text truncate">{{ executeTeamText(i) }}</view>
</view> </view>
<view class="body"> <view class="body">
<view class="content" :class="{ clamp: !expandMap[i._id] }"> <view class="content" :class="{ clamp: !expandMap[i._id] }">
@ -79,6 +79,31 @@
</view> </view>
</view> </view>
</uni-popup> </uni-popup>
<!-- 编辑服务内容 -->
<uni-popup ref="editPopupRef" type="bottom" :mask-click="true" @maskClick="closeEditPopup">
<view class="edit-sheet">
<view class="edit-header">
<view class="edit-header-left" />
<view class="edit-title">修改服务内容</view>
<view class="edit-close" @click="closeEditPopup">
<uni-icons type="closeempty" size="18" color="#333" />
</view>
</view>
<view class="edit-body">
<textarea
v-model="editContent"
class="edit-textarea"
placeholder="请输入服务内容"
:maxlength="1000"
/>
<view class="counter">{{ (editContent || '').length }}/1000</view>
</view>
<view class="edit-footer">
<button class="btn primary" @click="saveEdit">保存</button>
</view>
</view>
</uni-popup>
</view> </view>
</template> </template>
@ -130,7 +155,8 @@ async function ensureDoctor() {
function getUserId() { function getUserId() {
const d = doctorInfo.value || {}; const d = doctorInfo.value || {};
const a = account.value || {}; const a = account.value || {};
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || '') || ''; const t = uni.getStorageSync('ykt_case_current_team') || {};
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || t.userId || t.userid || t.corpUserId || '') || '';
} }
function getCorpId() { function getCorpId() {
@ -145,6 +171,58 @@ function getCurrentTeamId() {
return String(t.teamId || '') || ''; return String(t.teamId || '') || '';
} }
function normalizeName(v) {
const s = v === 0 ? '0' : v ? String(v) : '';
const trimmed = s.trim();
if (!trimmed) return '';
if (['-', '—', '--'].includes(trimmed)) return '';
return trimmed;
}
function getExecutorId(r) {
const row = r && typeof r === 'object' ? r : {};
return String(
row.executorUserId ||
row.executorId ||
row.executor ||
row.creatorUserId ||
row.creator ||
row.updateUserId ||
''
) || '';
}
function executorText(r) {
const row = r && typeof r === 'object' ? r : {};
const fromRow = normalizeName(row.executorName || row.executorUserName || row.creatorName || row.updateUserName || '');
if (fromRow) return fromRow;
const uid = getExecutorId(row);
const mapped = normalizeName(resolveUserName(uid));
return mapped || (uid ? uid : '--');
}
function getExecuteTeamId(r) {
const row = r && typeof r === 'object' ? r : {};
return String(row.executeTeamId || row.teamId || row.teamID || '') || '';
}
function resolveTeamName(teamId) {
const tid = String(teamId || '') || '';
if (!tid) return '';
const list = teamList.value || [];
const hit = list.find((i) => i && i.value === tid);
return hit?.label ? String(hit.label) : '';
}
function executeTeamText(r) {
const row = r && typeof r === 'object' ? r : {};
const fromRow = normalizeName(row.executeTeamName || row.teamName || '');
if (fromRow) return fromRow;
const tid = getExecuteTeamId(row) || getCurrentTeamId();
const mapped = normalizeName(resolveTeamName(tid));
return mapped || (tid ? tid : '--');
}
function resolveUserName(userId) { function resolveUserName(userId) {
const id = String(userId || ''); const id = String(userId || '');
if (!id) return ''; if (!id) return '';
@ -181,9 +259,12 @@ function displayTaskContent(r) {
return formatTaskContent(String(r?.taskContent || '')); return formatTaskContent(String(r?.taskContent || ''));
} }
const loadedTeamMemberIds = new Set();
async function loadTeamMembers(teamId) { async function loadTeamMembers(teamId) {
const tid = String(teamId || '') || ''; const tid = String(teamId || '') || '';
if (!tid) return; if (!tid) return;
if (loadedTeamMemberIds.has(tid)) return;
loadedTeamMemberIds.add(tid);
await ensureDoctor(); await ensureDoctor();
const corpId = getCorpId(); const corpId = getCorpId();
if (!corpId) return; if (!corpId) return;
@ -216,6 +297,8 @@ function mapRow(i) {
return { return {
...i, ...i,
_id: String(i?._id || i?.id || ''), _id: String(i?._id || i?.id || ''),
executorUserId: getExecutorId(i),
executeTeamId: getExecuteTeamId(i),
hasFile, hasFile,
fileType, fileType,
timeStr: i.executionTime ? dayjs(i.executionTime).format('YYYY-MM-DD HH:mm') : '--', timeStr: i.executionTime ? dayjs(i.executionTime).format('YYYY-MM-DD HH:mm') : '--',
@ -288,6 +371,10 @@ async function getMore() {
const mapped = arr.map(mapRow).filter((i) => i && i._id); const mapped = arr.map(mapRow).filter((i) => i && i._id);
list.value = page.value === 1 ? mapped : [...list.value, ...mapped]; list.value = page.value === 1 ? mapped : [...list.value, ...mapped];
page.value += 1; page.value += 1;
//
const teamIds = mapped.map((i) => i.executeTeamId).filter(Boolean);
Array.from(new Set(teamIds)).forEach((tid) => loadTeamMembers(tid));
} finally { } finally {
loading.value = false; loading.value = false;
} }
@ -330,21 +417,10 @@ function add() {
} }
function edit(record) { function edit(record) {
const archive = props.data || {}; if (!record?._id) return;
const customerUserId = String(archive.externalUserId || archive.customerUserId || '') || ''; editingRecord.value = record;
uni.setStorageSync('service-record-detail', { editContent.value = String(record?.taskContent || '') || '';
customerId: String(props.archiveId), editPopupRef.value?.open?.();
customerName: String(archive.name || ''),
customerUserId,
id: String(record?._id || ''),
executionTime: record?.executionTime || 0,
executeTeamId: String(record?.executeTeamId || ''),
executeTeamName: String(record?.executeTeamName || ''),
eventType: String(record?.eventType || ''),
taskContent: String(record?.taskContent || ''),
pannedEventSendFile: record?.pannedEventSendFile || null,
});
uni.navigateTo({ url: `/pages/case/service-record-detail?archiveId=${encodeURIComponent(props.archiveId)}&mode=edit&id=${encodeURIComponent(record._id)}` });
} }
const filePopupRef = ref(null); const filePopupRef = ref(null);
@ -380,6 +456,45 @@ function copyFile() {
closeFilePopup(); closeFilePopup();
} }
const editPopupRef = ref(null);
const editingRecord = ref(null);
const editContent = ref('');
function closeEditPopup() {
editPopupRef.value?.close?.();
editingRecord.value = null;
}
async function saveEdit() {
const r = editingRecord.value;
if (!r?._id) return;
if (!String(editContent.value || '').trim()) return toast('请输入服务内容');
await ensureDoctor();
const corpId = getCorpId();
const userId = getUserId();
if (!corpId || !userId) return toast('缺少用户信息');
const res = await api('updateServiceRecord', {
corpId,
id: String(r._id),
params: {
taskContent: String(editContent.value || ''),
updateUserId: userId,
},
});
if (!res?.success) return toast(res?.message || '修改失败');
//
const idx = list.value.findIndex((i) => i && i._id === String(r._id));
if (idx > -1) list.value[idx] = { ...list.value[idx], taskContent: String(editContent.value || '') };
uni.$emit('archive-detail:service-record-changed');
uni.showToast({ title: '保存成功', icon: 'success' });
closeEditPopup();
}
async function loadTeams() { async function loadTeams() {
await ensureDoctor(); await ensureDoctor();
const corpId = getCorpId(); const corpId = getCorpId();
@ -640,4 +755,56 @@ watch(
background: #0877F1; background: #0877F1;
color: #fff; color: #fff;
} }
.edit-sheet {
background: #fff;
border-top-left-radius: 20rpx;
border-top-right-radius: 20rpx;
overflow: hidden;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
}
.edit-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.edit-header-left {
width: 36rpx;
height: 36rpx;
}
.edit-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.edit-close {
width: 36rpx;
height: 36rpx;
display: flex;
align-items: center;
justify-content: center;
}
.edit-body {
padding: 28rpx 28rpx 0;
}
.edit-textarea {
width: 100%;
height: 260rpx;
padding: 20rpx;
border: 2rpx solid #e5e7eb;
border-radius: 12rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.counter {
margin-top: 12rpx;
text-align: right;
font-size: 24rpx;
color: #999;
}
.edit-footer {
padding: 24rpx 28rpx 0;
}
</style> </style>

View File

@ -1,473 +1,277 @@
<template> <template>
<view class="medical-case-form"> <view class="page">
<view class="form-container"> <view class="body">
<!-- 动态渲染表单字段 --> <scroll-view scroll-y class="scroll">
<view <view class="header">
v-for="field in currentFields" <view class="header-title">{{ pageTitle }}</view>
:key="field.key" </view>
class="form-item"
:class="{ required: field.required }"
>
<view class="item-label">{{ field.label }}</view>
<!-- 日期选择器 --> <view class="form-wrap">
<picker <FormTemplate v-if="temp" ref="formRef" :items="showItems" :form="form" @change="onChange" />
v-if="field.type === 'date'" </view>
mode="date"
:value="formData[field.key]"
@change="onDateChange(field.key, $event)"
:disabled="!isEditing"
>
<view class="picker-value">
{{ formData[field.key] || "暂无" }}
</view>
</picker>
<!-- 多行文本 --> <view style="height: 240rpx;"></view>
<textarea </scroll-view>
v-else-if="field.type === 'textarea'"
class="item-textarea"
v-model="formData[field.key]"
placeholder="请输入"
:disabled="!isEditing"
/>
<!-- 单行文本 --> <view class="footer">
<input <button class="btn plain" @click="handleRegenerate">重新生成</button>
v-else <button class="btn primary" @click="handleSave">保存至档案</button>
class="item-input"
v-model="formData[field.key]"
placeholder="暂无"
:disabled="!isEditing"
/>
</view>
</view>
<view class="footer-buttons">
<view class="btn-regenerate" @click="handleRegenerate">
<text class="btn-text">重新生成</text>
</view>
<view class="btn-save" @click="handleSave">
<text class="btn-text">保存至档案</text>
</view> </view>
</view> </view>
</view> </view>
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted } from "vue"; import { computed, reactive, ref } from 'vue';
import { storeToRefs } from "pinia"; import { onLoad } from '@dcloudio/uni-app';
import useAccountStore from "@/store/account"; import dayjs from 'dayjs';
import api from "@/utils/api.js"; import { storeToRefs } from 'pinia';
const caseType = ref(""); import FormTemplate from '@/components/form-template/index.vue';
const formData = ref({}); import api from '@/utils/api.js';
const isEditing = ref(true); import useAccountStore from '@/store/account';
const customerId = ref(""); import { toast, confirm, loading as uniLoading, hideLoading } from '@/utils/widget';
const groupId = ref(""); import { normalizeVisitRecordFormData } from './utils/visit-record';
import { normalizeTemplate, unwrapTemplateResponse } from './utils/template';
const caseType = ref('');
const customerId = ref('');
const groupId = ref('');
const accountStore = useAccountStore(); const accountStore = useAccountStore();
const { doctorInfo } = storeToRefs(accountStore); const { account, doctorInfo } = storeToRefs(accountStore);
// const { getDoctorInfo } = accountStore;
const CASE_TYPE_NAMES = {
outpatient: "门诊病历",
inhospital: "住院病历",
physicalExaminationTemplate: "体检记录",
preConsultation: "预问诊记录",
};
//
const FIELD_LABELS = {
//
visitTime: "就诊日期",
chiefComplaint: "主诉",
medicalHistorySummary: "病史概要",
examination: "检查",
diagnosisName: "门诊诊断",
//
inhosDate: "入院日期",
operation: "手术记录",
operationDate: "手术日期",
treatmentPlan: "治疗方案",
//
inspectTime: "体检日期",
inspectSummary: "体检小结",
positiveFind: "阳性发现及处理意见",
//
presentIllnessHistory: "现病史",
pastMedicalHistory: "既往史",
};
//
const FIELD_CONFIG = {
outpatient: [
{
key: "visitTime",
label: FIELD_LABELS.visitTime,
type: "date",
required: false,
},
{
key: "diagnosisName",
label: FIELD_LABELS.diagnosisName,
type: "textarea",
required: false,
},
{
key: "chiefComplaint",
label: FIELD_LABELS.chiefComplaint,
type: "textarea",
required: false,
},
{
key: "medicalHistorySummary",
label: FIELD_LABELS.medicalHistorySummary,
type: "textarea",
required: false,
},
{
key: "examination",
label: FIELD_LABELS.examination,
type: "textarea",
required: false,
},
{
key: "treatmentPlan",
label: "治疗方案",
type: "textarea",
required: false,
},
],
inhospital: [
{
key: "inhosDate",
label: FIELD_LABELS.inhosDate,
type: "date",
required: false,
},
{
key: "diagnosisName",
label: "住院主诊断",
type: "textarea",
required: false,
},
{
key: "operation",
label: FIELD_LABELS.operation,
type: "textarea",
required: false,
},
{
key: "operationDate",
label: FIELD_LABELS.operationDate,
type: "date",
required: false,
},
{
key: "treatmentPlan",
label: FIELD_LABELS.treatmentPlan,
type: "textarea",
required: false,
},
{
key: "chiefComplaint",
label: FIELD_LABELS.chiefComplaint,
type: "textarea",
required: false,
},
{
key: "medicalHistorySummary",
label: FIELD_LABELS.medicalHistorySummary,
type: "textarea",
required: false,
},
{
key: "examination",
label: FIELD_LABELS.examination,
type: "textarea",
required: false,
},
{
key: "treatmentPlan",
label: "治疗方案",
type: "textarea",
required: false,
},
],
physicalExaminationTemplate: [
{
key: "inspectTime",
label: FIELD_LABELS.inspectTime,
type: "date",
required: false,
},
{
key: "inspectSummary",
label: FIELD_LABELS.inspectSummary,
type: "textarea",
required: false,
},
{
key: "positiveFind",
label: FIELD_LABELS.positiveFind,
type: "textarea",
required: false,
},
],
preConsultation: [
{
key: "chiefComplaint",
label: FIELD_LABELS.chiefComplaint,
type: "textarea",
required: false,
},
{
key: "presentIllnessHistory",
label: FIELD_LABELS.presentIllnessHistory,
type: "textarea",
required: false,
},
{
key: "pastMedicalHistory",
label: FIELD_LABELS.pastMedicalHistory,
type: "textarea",
required: false,
},
],
};
//
const currentFields = computed(() => {
return FIELD_CONFIG[caseType.value] || [];
});
onMounted(() => {
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const options = currentPage.options;
caseType.value = options.caseType || "";
customerId.value = options.patientId || "";
groupId.value = options.groupId || "";
// options
if (options.formData) {
try {
formData.value = JSON.parse(decodeURIComponent(options.formData));
} catch (e) {
console.error("解析表单数据失败:", e);
}
}
//
const title = CASE_TYPE_NAMES[caseType.value]
? `添加${CASE_TYPE_NAMES[caseType.value]}`
: "添加病历";
uni.setNavigationBarTitle({ title });
});
const onDateChange = (field, event) => {
formData.value[field] = event.detail.value;
};
const handleRegenerate = () => {
uni.showModal({
title: "提示",
content: "确定要重新生成吗?当前编辑的内容将被覆盖",
success: (res) => {
if (res.confirm) {
//
uni.navigateBack({
success: () => {
uni.$emit("regenerateMedicalCase", {
caseType: caseType.value,
customerId: customerId.value,
groupId: groupId.value,
});
},
});
}
},
});
};
const handleSave = async () => {
//
const requiredFields = getRequiredFields();
const missingFields = requiredFields.filter(
(field) => !formData.value[field.key]
);
if (missingFields.length > 0) {
uni.showToast({
title: `请填写${missingFields[0].label}`,
icon: "none",
});
return;
}
async function ensureDoctor() {
if (doctorInfo.value) return;
if (!account.value?.openid) return;
try { try {
uni.showLoading({ title: "保存中..." }); await getDoctorInfo();
const result = await api("addMedicalRecord", { } catch {
medicalType: caseType.value, // ignore
memberId: customerId.value,
creator: doctorInfo.value.userid,
...formData.value,
});
uni.hideLoading();
if (result.success) {
uni.showToast({
title: "保存成功",
icon: "success",
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
} else {
uni.showToast({
title: result.message || "保存失败",
icon: "none",
});
}
} catch (error) {
uni.hideLoading();
console.error("保存病历失败:", error);
uni.showToast({
title: "保存失败,请重试",
icon: "none",
});
}
};
const getRequiredFields = () => {
return currentFields.value.filter((field) => field.required);
};
</script>
<style scoped lang="scss">
.medical-case-form {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 120rpx;
.form-container {
background-color: #ffffff;
padding: 32rpx;
.form-item {
margin-bottom: 32rpx;
&.required .item-label::before {
content: "*";
color: #ff4d4f;
margin-right: 8rpx;
}
.item-label {
font-size: 28rpx;
color: #333333;
margin-bottom: 16rpx;
}
.item-input,
.picker-value {
width: 100%;
height: 80rpx;
padding: 0 24rpx;
background-color: #f8f9fa;
border-radius: 8rpx;
font-size: 28rpx;
color: #333333;
&[disabled] {
color: #999999;
}
}
.picker-value {
display: flex;
align-items: center;
color: #999999;
}
.item-textarea {
width: 100%;
min-height: 100rpx;
padding: 20rpx 24rpx;
background-color: #f8f9fa;
border-radius: 8rpx;
font-size: 28rpx;
color: #333333;
height: 100px;
&[disabled] {
color: #999999;
}
}
}
.tips-box {
margin-top: 32rpx;
padding: 24rpx;
background-color: #fffbe6;
border-radius: 8rpx;
.tips-text {
display: block;
font-size: 24rpx;
color: #666666;
line-height: 1.6;
margin-bottom: 8rpx;
&:last-child {
margin-bottom: 0;
}
}
}
}
.footer-buttons {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
display: flex;
gap: 24rpx;
padding: 24rpx 32rpx;
background-color: #ffffff;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
.btn-regenerate,
.btn-save {
flex: 1;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 44rpx;
.btn-text {
font-size: 32rpx;
font-weight: 500;
}
}
.btn-regenerate {
background-color: #ffffff;
border: 2rpx solid #1890ff;
.btn-text {
color: #1890ff;
}
}
.btn-save {
background-color: #1890ff;
.btn-text {
color: #ffffff;
}
}
} }
} }
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 temp = ref(null);
const titleText = computed(() => {
const t = temp.value || {};
return String(t.name || t.templateName || t.templateTypeName || '').trim();
});
const pageTitle = computed(() => {
const name = titleText.value;
return name ? `添加${name}` : '添加病历';
});
const form = reactive({});
const forms = computed(() => form);
const showItems = computed(() => {
const list = temp.value?.templateList || [];
return list.filter((i) => {
if (i?.type === 'files') return false;
if (i && typeof i.referenceField === 'string') {
return forms.value[i.referenceField] === i.referenceValue;
}
return true;
});
});
const formRef = ref(null);
function onChange({ title, value }) {
form[title] = value;
const item = showItems.value.find((i) => i.title === title);
if (!item) return;
const relat = (temp.value?.templateList || []).filter((i) => i.referenceField === title);
relat.forEach((i) => (form[i.title] = ''));
}
function setDefaultDates() {
const timeKey = temp.value?.service?.timeTitle || '';
if (timeKey && !form[timeKey]) form[timeKey] = dayjs().format('YYYY-MM-DD');
}
function applyInitialFormData(raw) {
const normalized = normalizeVisitRecordFormData(caseType.value, raw);
Object.assign(form, normalized);
setDefaultDates();
}
async function loadTemplate(t) {
const corpId = getCorpId();
if (!corpId) return null;
try {
const res = await api('getCurrentTemplate', { corpId, templateType: t });
if (!res?.success) {
toast(res?.message || '获取模板失败');
return null;
}
const raw = unwrapTemplateResponse(res);
return normalizeTemplate(raw);
} catch (e) {
console.error('loadTemplate error:', e);
toast('获取模板失败');
return null;
}
}
onLoad(async (options) => {
caseType.value = options?.caseType || options?.type || '';
customerId.value = options?.patientId || options?.memberId || '';
groupId.value = options?.groupId || '';
if (!caseType.value) caseType.value = 'outpatient';
temp.value = await loadTemplate(caseType.value);
if (temp.value?.templateType) caseType.value = String(temp.value.templateType);
if (options?.formData) {
try {
const parsed = JSON.parse(decodeURIComponent(options.formData));
applyInitialFormData(parsed);
} catch (e) {
console.error('解析表单数据失败:', e);
applyInitialFormData({});
}
} else {
applyInitialFormData({});
}
uni.setNavigationBarTitle({ title: pageTitle.value });
});
async function handleRegenerate() {
try {
await confirm('确定要重新生成吗?当前编辑的内容将被覆盖');
} catch {
return;
}
uni.navigateBack({
success: () => {
uni.$emit('regenerateMedicalCase', {
caseType: caseType.value,
customerId: customerId.value,
groupId: groupId.value,
});
},
});
}
async function handleSave() {
if (!customerId.value) return toast('缺少患者信息');
await ensureDoctor();
const corpId = getCorpId();
const userId = getUserId();
if (!corpId || !userId) return toast('缺少用户信息');
if (formRef.value?.verify && !formRef.value.verify()) return;
const params = {
...form,
corpId,
memberId: customerId.value,
medicalType: caseType.value,
creator: userId,
};
// /diagnosisName
if ((caseType.value === 'outpatient' || caseType.value === 'inhospital') && form.diagnosis && !form.diagnosisName) {
params.diagnosisName = form.diagnosis;
}
const sortTimeKey = temp.value?.service?.timeTitle || '';
if (sortTimeKey && form[sortTimeKey] && dayjs(form[sortTimeKey]).isValid()) {
params.sortTime = dayjs(form[sortTimeKey]).valueOf();
} else {
params.sortTime = Date.now();
}
uniLoading('保存中...');
try {
const res = await api('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('保存病历失败:', error);
toast('保存失败,请重试');
}
}
</script>
<style scoped>
.page {
min-height: 100vh;
background: #f5f6f8;
padding-bottom: calc(152rpx + env(safe-area-inset-bottom));
}
.body {
height: 100vh;
display: flex;
flex-direction: column;
}
.scroll {
flex: 1;
}
.header {
background: #fff;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
}
.header-title {
padding: 28rpx 28rpx;
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.form-wrap {
background: #fff;
margin-top: 20rpx;
padding: 8rpx 0;
}
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
padding: 24rpx 28rpx calc(24rpx + env(safe-area-inset-bottom));
display: flex;
gap: 24rpx;
box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
}
.btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
border-radius: 12rpx;
font-size: 30rpx;
}
.btn::after {
border: none;
}
.btn.plain {
background: #fff;
color: #0877F1;
border: 2rpx solid #0877F1;
}
.btn.primary {
background: #0877F1;
color: #fff;
}
</style> </style>

View File

@ -267,6 +267,12 @@ function isTagItem(i) {
return title === 'tagIds' || title === 'tag' || type === 'tag' || name.includes('标签'); return title === 'tagIds' || title === 'tag' || type === 'tag' || name.includes('标签');
} }
function isInHospitalSourceItem(i) {
const title = String(i?.title || '');
const name = String(i?.name || '');
return title === 'inHospitalSource' || title === 'hospitalSource' || name.includes('院内来源');
}
async function loadInternalTemplate() { async function loadInternalTemplate() {
const corpId = String(account.value?.corpId || doctorInfo.value?.corpId || '') || ''; const corpId = String(account.value?.corpId || doctorInfo.value?.corpId || '') || '';
if (!corpId) return; if (!corpId) return;
@ -304,6 +310,7 @@ async function loadInternalTemplate() {
items.value = list items.value = list
.filter((i) => i && i.fieldStatus !== 'disable') .filter((i) => i && i.fieldStatus !== 'disable')
.filter((i) => i.operateType !== 'onlyRead') .filter((i) => i.operateType !== 'onlyRead')
.filter((i) => !isInHospitalSourceItem(i))
.map(normalizeTemplateItem); .map(normalizeTemplateItem);
const debugStage = items.value.find(isStageItem); const debugStage = items.value.find(isStageItem);
@ -392,6 +399,20 @@ function buildPayload(base, inner) {
return { payload, team }; return { payload, team };
} }
function getCreatedCustomerId(res) {
const root = res && typeof res === 'object' ? res : {};
const d = root.data && typeof root.data === 'object' ? root.data : {};
return String(
d._id ||
d.id ||
d.customerId ||
d.memberId ||
root.customerId ||
root.memberId ||
''
) || '';
}
async function save() { async function save() {
if (formRef.value?.verify && !formRef.value.verify()) return; if (formRef.value?.verify && !formRef.value.verify()) return;
@ -420,10 +441,23 @@ async function save() {
} }
toast('新增成功'); toast('新增成功');
const createdId = getCreatedCustomerId(res);
uni.removeStorageSync(BASE_KEY); uni.removeStorageSync(BASE_KEY);
uni.removeStorageSync(INNER_KEY); uni.removeStorageSync(INNER_KEY);
uni.setStorageSync(NEED_RELOAD_STORAGE_KEY, 1); uni.setStorageSync(NEED_RELOAD_STORAGE_KEY, 1);
uni.navigateBack({ delta: 2 }); uni.navigateBack({
delta: 2,
success: () => {
if (!createdId) return;
setTimeout(() => {
uni.navigateTo({ url: `/pages/case/archive-detail?id=${encodeURIComponent(createdId)}` });
}, 30);
},
fail: () => {
if (!createdId) return;
uni.redirectTo({ url: `/pages/case/archive-detail?id=${encodeURIComponent(createdId)}` });
},
});
} catch (e) { } catch (e) {
hideLoading(); hideLoading();
toast('新增失败'); toast('新增失败');

View File

@ -1,7 +1,7 @@
<template> <template>
<view class="page"> <view class="page">
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/service-record-detail/service-record-detail.vue --> <!-- Mobile 来源: ykt-management-mobile/src/pages/customer/service-record-detail/service-record-detail.vue -->
<view class="body"> <view v-if="!recordId" class="body">
<scroll-view scroll-y class="scroll"> <scroll-view scroll-y class="scroll">
<view class="card"> <view class="card">
<view class="section-title">执行日期</view> <view class="section-title">执行日期</view>
@ -31,12 +31,11 @@
</picker> </picker>
<view class="section-title">所属团队</view> <view class="section-title">所属团队</view>
<picker mode="selector" :range="teamOptions" range-key="label" @change="pickTeam" :disabled="Boolean(recordId)"> <picker mode="selector" :range="teamOptions" range-key="label" :disabled="true">
<view class="picker-box between"> <view class="picker-box">
<view class="picker-text" :class="{ muted: !currentTeam?.value }"> <view class="picker-text" :class="{ muted: !currentTeam?.value }">
{{ currentTeam?.value ? currentTeam.label : '请选择所属团队' }} {{ currentTeam?.value ? currentTeam.label : '请选择所属团队' }}
</view> </view>
<uni-icons type="arrowright" size="16" color="#999" />
</view> </view>
</picker> </picker>
@ -55,14 +54,37 @@
</view> </view>
</view> </view>
<view v-if="recordId" class="delete-fab" @click="remove"> <!-- 编辑仅支持修改服务内容底部弹层 -->
<uni-icons type="trash" size="22" color="#ff4d4f" /> <uni-popup v-else ref="editPopupRef" type="bottom" :mask-click="true" @maskClick="closeEdit">
</view> <view class="sheet">
<view class="sheet-header">
<view class="sheet-header-left" />
<view class="sheet-title">修改服务内容</view>
<view class="sheet-close" @click="closeEdit">
<uni-icons type="closeempty" size="18" color="#333" />
</view>
</view>
<view class="sheet-body">
<textarea
v-model="form.taskContent"
class="sheet-textarea"
placeholder="请输入服务内容"
:maxlength="1000"
/>
<view class="counter">{{ (form.taskContent || '').length }}/1000</view>
</view>
<view class="sheet-footer">
<button class="primary-btn" @click="saveEdit">保存</button>
</view>
</view>
</uni-popup>
</view> </view>
</template> </template>
<script setup> <script setup>
import { reactive, ref } from 'vue'; import { nextTick, 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 { storeToRefs } from 'pinia';
@ -107,7 +129,8 @@ async function ensureDoctor() {
function getUserId() { function getUserId() {
const d = doctorInfo.value || {}; const d = doctorInfo.value || {};
const a = account.value || {}; const a = account.value || {};
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || '') || ''; const t = uni.getStorageSync('ykt_case_current_team') || {};
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || t.userId || t.userid || t.corpUserId || '') || '';
} }
function getCorpId() { function getCorpId() {
@ -124,25 +147,6 @@ function getCurrentTeam() {
return teamId ? { value: teamId, label: name || teamId } : null; return teamId ? { value: teamId, label: name || teamId } : null;
} }
async function loadTeams() {
await ensureDoctor();
const corpId = getCorpId();
const userId = getUserId();
if (!corpId || !userId) return;
const res = await api('getTeamBymember', { corpId, corpUserId: userId });
if (!res?.success) return;
const list = Array.isArray(res?.data) ? res.data : Array.isArray(res?.data?.data) ? res.data.data : [];
teamOptions.value = list
.map((raw) => {
if (!raw || typeof raw !== 'object') return null;
const teamId = raw.teamId || raw.id || raw._id || '';
const name = raw.name || raw.teamName || raw.team || '';
if (!teamId) return null;
return { label: String(name || teamId), value: String(teamId) };
})
.filter(Boolean);
}
onLoad((options) => { onLoad((options) => {
archiveId.value = options?.archiveId ? String(options.archiveId) : ''; archiveId.value = options?.archiveId ? String(options.archiveId) : '';
recordId.value = options?.id ? String(options.id) : ''; recordId.value = options?.id ? String(options.id) : '';
@ -162,26 +166,22 @@ onLoad((options) => {
const seedType = String(seed?.eventType || '') || ''; const seedType = String(seed?.eventType || '') || '';
currentType.value = typeOptions.find((i) => i.value === seedType) || null; currentType.value = typeOptions.find((i) => i.value === seedType) || null;
const seedTeamId = String(seed?.executeTeamId || '') || ''; //
const seedTeamName = String(seed?.executeTeamName || '') || ''; const cur = getCurrentTeam();
if (seedTeamId) currentTeam.value = { value: seedTeamId, label: seedTeamName || seedTeamId }; currentTeam.value = cur;
else currentTeam.value = getCurrentTeam(); teamOptions.value = cur ? [cur] : [];
loadTeams().then(() => { if (recordId.value) {
if (seedTeamId) currentTeam.value = teamOptions.value.find((t) => t.value === seedTeamId) || currentTeam.value; uni.setNavigationBarTitle({ title: '修改服务内容' });
else { nextTick(() => editPopupRef.value?.open?.());
const cur = getCurrentTeam(); } else {
if (cur) currentTeam.value = teamOptions.value.find((t) => t.value === cur.value) || cur; uni.setNavigationBarTitle({ title: '新建服务记录' });
} }
});
}); });
function pickType(e) { function pickType(e) {
currentType.value = typeOptions[e.detail.value] || null; currentType.value = typeOptions[e.detail.value] || null;
} }
function pickTeam(e) {
currentTeam.value = teamOptions.value[e.detail.value] || null;
}
function pickDate(e) { function pickDate(e) {
date.value = e.detail.value || ''; date.value = e.detail.value || '';
} }
@ -205,6 +205,45 @@ function save() {
submit(executionTime); submit(executionTime);
} }
const editPopupRef = ref(null);
function closeEdit() {
editPopupRef.value?.close?.();
setTimeout(() => uni.navigateBack(), 200);
}
function saveEdit() {
if (!String(form.taskContent || '').trim()) return uni.showToast({ title: '请输入服务内容', icon: 'none' });
submitEdit();
}
async function submitEdit() {
await ensureDoctor();
const corpId = getCorpId();
const userId = getUserId();
if (!corpId || !userId) {
toast('缺少用户/团队信息');
return;
}
if (!recordId.value) return;
const res = await api('updateServiceRecord', {
corpId,
id: recordId.value,
params: {
taskContent: form.taskContent,
updateUserId: userId,
},
});
if (!res?.success) {
toast(res?.message || '修改失败');
return;
}
uni.$emit('archive-detail:service-record-changed');
uni.showToast({ title: '保存成功', icon: 'success' });
setTimeout(() => uni.navigateBack(), 300);
}
async function submit(executionTime) { async function submit(executionTime) {
await ensureDoctor(); await ensureDoctor();
const corpId = getCorpId(); const corpId = getCorpId();
@ -252,27 +291,6 @@ async function submit(executionTime) {
uni.showToast({ title: '保存成功', icon: 'success' }); uni.showToast({ title: '保存成功', icon: 'success' });
setTimeout(() => uni.navigateBack(), 300); setTimeout(() => uni.navigateBack(), 300);
} }
function remove() {
uni.showModal({
title: '提示',
content: '确定删除当前记录?',
success: async (res) => {
if (!res.confirm) return;
await ensureDoctor();
const corpId = getCorpId();
if (!corpId || !recordId.value) return;
const resp = await api('removeServiceRecord', { corpId, id: recordId.value });
if (!resp?.success) {
toast(resp?.message || '删除失败');
return;
}
uni.$emit('archive-detail:service-record-changed');
uni.showToast({ title: '已删除', icon: 'success' });
setTimeout(() => uni.navigateBack(), 300);
},
});
}
</script> </script>
<style scoped> <style scoped>
@ -377,18 +395,60 @@ function remove() {
color: #fff; color: #fff;
} }
.delete-fab { .sheet {
position: fixed;
right: 16px;
bottom: calc(96px + env(safe-area-inset-bottom));
width: 52px;
height: 52px;
border-radius: 26px;
background: #fff; background: #fff;
border-top-left-radius: 14px;
border-top-right-radius: 14px;
padding-bottom: calc(14px + env(safe-area-inset-bottom));
}
.sheet-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid #f0f0f0;
}
.sheet-header-left {
width: 24px;
height: 24px;
}
.sheet-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
.sheet-close {
width: 24px;
height: 24px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: 0 10px 18px rgba(0, 0, 0, 0.12); }
z-index: 30; .sheet-body {
padding: 14px 16px 0;
}
.sheet-textarea {
width: 100%;
height: 180px;
padding: 12px;
border: 1px solid #e6e6e6;
border-radius: 8px;
font-size: 14px;
box-sizing: border-box;
}
.sheet-footer {
padding: 12px 16px 0;
}
.primary-btn {
width: 100%;
height: 44px;
line-height: 44px;
border-radius: 6px;
background: #0877F1;
color: #fff;
font-size: 15px;
}
.primary-btn::after {
border: none;
} }
</style> </style>

View File

@ -0,0 +1,95 @@
function normalizeOptions(options) {
if (!Array.isArray(options)) return [];
if (!options.length) return [];
if (typeof options[0] === 'string') return options.filter((i) => typeof i === 'string');
if (typeof options[0] === 'object') {
return options
.map((i) => {
const label = i?.label ?? i?.name ?? i?.text ?? i?.title ?? '';
const value = i?.value ?? i?.id ?? i?.key ?? label;
if (!label && (value === undefined || value === null || value === '')) return null;
return { label: String(label || value), value: String(value) };
})
.filter(Boolean);
}
return [];
}
export function normalizeTemplateItem(item) {
const next = { ...(item || {}) };
if (next.operateType === 'custom') next.operateType = 'formCell';
const originalType = next.type;
const customTypeMap = {
customerSource: 'select',
customerStage: 'select',
tag: 'multiSelectAndOther',
reference: 'input',
selectWwuser: 'select',
files: 'files',
corpProject: 'select',
diagnosis: 'textarea',
BMI: 'input',
bloodPressure: 'textarea',
selfMultipleDiseases: 'textarea',
};
if (originalType && customTypeMap[originalType]) {
next.__originType = originalType;
next.type = customTypeMap[originalType];
}
const aliasTypeMap = {
text: 'input',
string: 'input',
number: 'input',
integer: 'input',
int: 'input',
};
if (next.type && aliasTypeMap[next.type]) {
next.type = aliasTypeMap[next.type];
if (!next.inputType && (originalType === 'number' || originalType === 'integer' || originalType === 'int')) next.inputType = 'number';
}
const rawRange = next.range || next.options || next.optionList || next.values || [];
const range = normalizeOptions(rawRange);
if (next.type === 'select' || next.type === 'selectAndOther' || next.type === 'selectAndImage') {
next.range = range;
} else if (next.type === 'radio') {
// wxapp 目前 radio 组件只支持字符串列表;模板如为对象选项则降级为 select
if (range.length && typeof range[0] === 'object') {
next.type = 'select';
next.range = range;
} else {
next.range = Array.isArray(rawRange) ? rawRange : [];
}
}
if (!next.operateType) next.operateType = 'formCell';
next.required = Boolean(next.required);
if (next.type === 'input' && (next.wordLimit === undefined || next.wordLimit === null || next.wordLimit === '')) next.wordLimit = 20;
if (next.type === 'textarea' && (next.wordLimit === undefined || next.wordLimit === null || next.wordLimit === '')) next.wordLimit = 200;
return next;
}
export function unwrapTemplateResponse(res) {
const d = res?.data;
if (d && typeof d === 'object') {
if (d.data && typeof d.data === 'object') return d.data;
return d;
}
return res && typeof res === 'object' ? res : {};
}
export function normalizeTemplate(temp) {
const t = temp && typeof temp === 'object' ? { ...temp } : {};
const list = Array.isArray(t.templateList) ? t.templateList : [];
t.templateList = list
.filter((i) => i && i.fieldStatus !== 'disable')
.filter((i) => i.operateType !== 'onlyRead')
.map(normalizeTemplateItem);
return t;
}

View File

@ -0,0 +1,40 @@
function isEmpty(v) {
if (v === null || v === undefined) return true;
if (Array.isArray(v)) return v.length === 0;
if (typeof v === 'string') return v.trim() === '';
return false;
}
const ALIAS_MAP = {
outpatient: {
diagnosisName: 'diagnosis',
medicalHistorySummary: 'medicalHistory',
},
inhospital: {
diagnosisName: 'diagnosis',
medicalHistorySummary: 'medicalHistory',
operationDate: 'surgeryDate',
operation: 'surgeryName',
},
physicalExaminationTemplate: {
inspectTime: 'inspectDate',
inspectSummary: 'summary',
},
preConsultation: {
presentIllnessHistory: 'presentIllness',
pastMedicalHistory: 'pastHistory',
},
};
export function normalizeVisitRecordFormData(templateType, raw) {
const input = raw && typeof raw === 'object' ? raw : {};
const out = { ...input };
const map = ALIAS_MAP[String(templateType || '')] || {};
Object.entries(map).forEach(([from, to]) => {
if (isEmpty(out[to]) && !isEmpty(out[from])) out[to] = out[from];
});
return out;
}

View File

@ -3,12 +3,8 @@
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/visit-record-detail/visit-record-detail.vue --> <!-- Mobile 来源: ykt-management-mobile/src/pages/customer/visit-record-detail/visit-record-detail.vue -->
<view class="body"> <view class="body">
<scroll-view scroll-y class="scroll"> <scroll-view scroll-y class="scroll">
<view class="header">
<view class="header-title">{{ template?.templateName || '健康档案' }}</view>
</view>
<view class="form-wrap"> <view class="form-wrap">
<FormTemplate v-if="template" ref="formRef" :items="showItems" :form="forms" @change="onChange" /> <FormTemplate v-if="temp" ref="formRef" :items="showItems" :form="forms" @change="onChange" />
</view> </view>
<!-- 附件上传FormTemplate 不支持 files单独实现 --> <!-- 附件上传FormTemplate 不支持 files单独实现 -->
@ -19,7 +15,8 @@
</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 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 class="upload-remove" @click.stop="removeFile(idx)">×</view>
</view> </view>
<view class="upload-add" @click="addFiles"> <view class="upload-add" @click="addFiles">
@ -49,10 +46,12 @@ import { onLoad } from '@dcloudio/uni-app';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import FormTemplate from '@/components/form-template/index.vue'; import FormTemplate from '@/components/form-template/index.vue';
import { getVisitRecordTemplate } from './components/archive-detail/templates';
import api from '@/utils/api'; import api from '@/utils/api';
import { uploadFile } from '@/utils/file';
import useAccountStore from '@/store/account'; import useAccountStore from '@/store/account';
import { toast, confirm, loading as uniLoading, hideLoading } from '@/utils/widget'; import { toast, confirm, loading as uniLoading, hideLoading } from '@/utils/widget';
import { normalizeVisitRecordFormData } from './utils/visit-record';
import { normalizeTemplate, unwrapTemplateResponse } from './utils/template';
const accountStore = useAccountStore(); const accountStore = useAccountStore();
const { account, doctorInfo } = storeToRefs(accountStore); const { account, doctorInfo } = storeToRefs(accountStore);
@ -84,14 +83,17 @@ function getCorpId() {
const memberId = ref(''); 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 temp = ref(null);
const titleText = computed(() => {
const t = temp.value || {};
return String(t.name || t.templateName || t.templateTypeName || '').trim();
});
const detail = ref({}); const detail = ref({});
const form = reactive({}); const form = reactive({});
const forms = computed(() => ({ ...detail.value, ...form })); const forms = computed(() => ({ ...detail.value, ...form }));
const showItems = computed(() => { const showItems = computed(() => {
const list = template.value?.templateList || []; const list = temp.value?.templateList || [];
// referenceField mobile // referenceField mobile
return list.filter((i) => { return list.filter((i) => {
if (i?.type === 'files') return false; if (i?.type === 'files') return false;
@ -103,7 +105,7 @@ const showItems = computed(() => {
}); });
const hasFilesField = computed(() => { const hasFilesField = computed(() => {
const list = template.value?.templateList || []; const list = temp.value?.templateList || [];
return list.some((i) => i && (i.type === 'files' || i.title === 'files')); return list.some((i) => i && (i.type === 'files' || i.title === 'files'));
}); });
@ -119,30 +121,43 @@ function ensureFilesField() {
form.files = []; form.files = [];
} }
async function loadTemplate(t) {
const corpId = getCorpId();
if (!corpId) return null;
try {
const res = await api('getCurrentTemplate', { corpId, templateType: t });
if (!res?.success) {
toast(res?.message || '获取模板失败');
return null;
}
const raw = unwrapTemplateResponse(res);
return normalizeTemplate(raw);
} catch (e) {
console.error('loadTemplate error:', e);
toast('获取模板失败');
return null;
}
}
onLoad(async (options) => { onLoad(async (options) => {
memberId.value = options?.memberId || options?.archiveId || ''; memberId.value = options?.memberId || options?.archiveId || '';
recordId.value = options?.id || ''; recordId.value = options?.id || '';
templateType.value = options?.type || ''; templateType.value = options?.type || '';
customerName.value = decodeURIComponent(options?.customerName || '');
if (recordId.value) { if (recordId.value) {
await getDetail(); await getDetail();
} else { } else {
if (!templateType.value) templateType.value = 'outpatient'; if (!templateType.value) templateType.value = 'outpatient';
temp.value = await loadTemplate(templateType.value);
if (temp.value?.templateType) templateType.value = String(temp.value.templateType);
ensureFilesField(); ensureFilesField();
//
if (templateType.value === 'outpatient') { //
form.visitTime = dayjs().format('YYYY-MM-DD'); const timeKey = temp.value?.service?.timeTitle || '';
} if (timeKey && !form[timeKey]) form[timeKey] = 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 (titleText.value) uni.setNavigationBarTitle({ title: titleText.value });
}); });
async function getDetail() { async function getDetail() {
@ -162,14 +177,14 @@ async function getDetail() {
const record = res?.record || res?.data?.record || null; const record = res?.record || res?.data?.record || null;
if (res?.success && record) { if (res?.success && record) {
templateType.value = record.templateType || record.medicalType || templateType.value; templateType.value = record.templateType || record.medicalType || templateType.value;
detail.value = normalizeVisitRecordFormData(templateType.value, record);
// wxapp 使 diagnosis diagnosisName
if ((record.medicalType === 'outpatient' || record.medicalType === 'inhospital') && !record.diagnosis && record.diagnosisName) {
record.diagnosis = record.diagnosisName;
}
detail.value = record;
ensureFilesField(); ensureFilesField();
// templateType
if (!temp.value || temp.value?.templateType !== templateType.value) {
temp.value = await loadTemplate(templateType.value);
if (temp.value?.templateType) templateType.value = String(temp.value.templateType);
if (titleText.value) uni.setNavigationBarTitle({ title: titleText.value });
}
} else { } else {
toast(res.message || '加载失败'); toast(res.message || '加载失败');
} }
@ -185,7 +200,7 @@ function onChange({ title, value }) {
const item = showItems.value.find((i) => i.title === title); const item = showItems.value.find((i) => i.title === title);
if (!item) return; if (!item) return;
// mobile // mobile
const relat = (template.value?.templateList || []).filter((i) => i.referenceField === title); const relat = (temp.value?.templateList || []).filter((i) => i.referenceField === title);
relat.forEach((i) => (form[i.title] = '')); relat.forEach((i) => (form[i.title] = ''));
} }
@ -224,7 +239,7 @@ async function save() {
} }
// sortTime使 // sortTime使
const sortTimeKey = template.value?.service?.timeTitle || ''; const sortTimeKey = temp.value?.service?.timeTitle || '';
if (sortTimeKey && form[sortTimeKey] && dayjs(form[sortTimeKey]).isValid()) { if (sortTimeKey && form[sortTimeKey] && dayjs(form[sortTimeKey]).isValid()) {
params.sortTime = dayjs(form[sortTimeKey]).valueOf(); params.sortTime = dayjs(form[sortTimeKey]).valueOf();
} else { } else {
@ -252,65 +267,84 @@ async function save() {
} }
} }
function remove() { async function remove() {
confirm('确定删除当前记录?', async () => { try {
if (!memberId.value || !recordId.value) return toast('缺少必要信息'); await confirm('确定删除当前记录?');
await ensureDoctor(); } catch {
const corpId = getCorpId(); return;
if (!corpId) return toast('缺少必要信息'); }
uniLoading('删除中...'); if (!memberId.value || !recordId.value) return toast('缺少必要信息');
try { await ensureDoctor();
const res = await api('removeMedicalRecord', { const corpId = getCorpId();
corpId, if (!corpId) return toast('缺少必要信息');
memberId: memberId.value, uniLoading('删除中...');
medicalType: templateType.value, try {
_id: recordId.value, const res = await api('removeMedicalRecord', {
}); corpId,
hideLoading(); memberId: memberId.value,
if (res.success) { medicalType: templateType.value,
uni.$emit('archive-detail:visit-record-changed'); _id: recordId.value,
toast(res.message || '已删除'); });
setTimeout(() => uni.navigateBack(), 300); hideLoading();
} else { if (res.success) {
toast(res.message || '删除失败'); uni.$emit('archive-detail:visit-record-changed');
} toast(res.message || '已删除');
} catch (error) { setTimeout(() => uni.navigateBack(), 300);
hideLoading(); } else {
console.error('remove error:', error); toast(res.message || '删除失败');
toast('删除失败');
} }
}); } catch (error) {
hideLoading();
console.error('remove error:', error);
toast('删除失败');
}
} }
function addFiles() { function isPdfUrl(url) {
const fileConfig = template.value?.templateList?.find(i => i.type === 'files'); const u = String(url || '').toLowerCase();
const maxSize = fileConfig?.maxSize || 5; // MB return u.includes('.pdf') || u.startsWith('data:application/pdf');
const accept = fileConfig?.accept || 'pdf'; }
uni.chooseMessageFile({ async function addFiles() {
count: 9, const fileConfig = temp.value?.templateList?.find((i) => i && (i.type === 'files' || i.title === 'files')) || {};
type: 'file', const maxSize = Number(fileConfig?.maxSize || 5) || 5; // MB
extension: [accept], const accept = String(fileConfig?.accept || 'pdf') || 'pdf';
success: (res) => {
const files = Array.isArray(res.tempFiles) ? res.tempFiles : [];
const maxBytes = maxSize * 1024 * 1024;
// const chooseRes = await new Promise((resolve) => {
const invalidFiles = files.filter(f => f.size > maxBytes); uni.chooseMessageFile({
if (invalidFiles.length > 0) { count: 9,
toast(`文件大小不能超过${maxSize}M`); type: 'file',
return; extension: accept ? [accept] : undefined,
} success: (res) => resolve(res),
fail: () => resolve(null),
const next = files.map((f) => ({ });
url: f.path,
name: f.name || '',
size: f.size
}));
const cur = Array.isArray(forms.value.files) ? forms.value.files : [];
form.files = [...cur, ...next];
},
}); });
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) { function removeFile(idx) {
@ -319,8 +353,32 @@ function removeFile(idx) {
} }
function previewFile(idx) { function previewFile(idx) {
const urls = fileList.value.map((i) => i.url); const f = fileList.value[idx];
uni.previewImage({ urls, current: urls[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> </script>
@ -393,6 +451,17 @@ function previewFile(idx) {
width: 100%; width: 100%;
height: 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 { .upload-remove {
position: absolute; position: absolute;
right: 0; right: 0;

View File

@ -30,13 +30,8 @@
<view class="tabs-area"> <view class="tabs-area">
<scroll-view scroll-x class="tabs-scroll" :show-scrollbar="false"> <scroll-view scroll-x class="tabs-scroll" :show-scrollbar="false">
<view class="tabs-container"> <view class="tabs-container">
<view <view v-for="tab in tabs" :key="tab.key" class="tab-item" :class="{ active: currentTabKey === tab.key }"
v-for="tab in tabs" @click="onTabClick(tab)">
:key="tab.key"
class="tab-item"
:class="{ active: currentTabKey === tab.key }"
@click="onTabClick(tab)"
>
{{ tab.label }} {{ tab.label }}
</view> </view>
</view> </view>
@ -47,32 +42,24 @@
<!-- Main Content --> <!-- Main Content -->
<view class="content-body"> <view class="content-body">
<!-- Patient List --> <!-- Patient List -->
<scroll-view <scroll-view scroll-y class="patient-list" :scroll-into-view="scrollIntoId" :scroll-with-animation="true"
scroll-y lower-threshold="80" @scrolltolower="loadMore">
class="patient-list"
:scroll-into-view="scrollIntoId"
:scroll-with-animation="true"
lower-threshold="80"
@scrolltolower="loadMore"
>
<view v-for="group in patientList" :key="group.letter" :id="letterToDomId(group.letter)"> <view v-for="group in patientList" :key="group.letter" :id="letterToDomId(group.letter)">
<view class="group-title">{{ group.letter }}</view> <view class="group-title">{{ group.letter }}</view>
<view v-for="(patient, pIndex) in group.data" :key="pIndex" class="patient-card" @click="handlePatientClick(patient)"> <view v-for="(patient, pIndex) in group.data" :key="pIndex" class="patient-card"
@click="handlePatientClick(patient)">
<!-- Checkbox for Batch Mode --> <!-- Checkbox for Batch Mode -->
<view v-if="isBatchMode" class="checkbox-area"> <view v-if="isBatchMode" class="checkbox-area">
<uni-icons <uni-icons :type="selectedItems.includes(getSelectId(patient)) ? 'checkbox-filled' : 'checkbox'" size="24"
:type="selectedItems.includes(getSelectId(patient)) ? 'checkbox-filled' : 'checkbox'" :color="selectedItems.includes(getSelectId(patient)) ? '#007aff' : '#ccc'"></uni-icons>
size="24"
:color="selectedItems.includes(getSelectId(patient)) ? '#007aff' : '#ccc'"
></uni-icons>
</view> </view>
<view class="card-content"> <view class="card-content">
<!-- Row 1 --> <!-- Row 1 -->
<view class="card-row-top"> <view class="card-row-top">
<view class="patient-info"> <view class="patient-info">
<text class="patient-name">{{ patient.name }}</text> <text class="patient-name">{{ patient.name }}</text>
<text class="patient-meta">{{ patient.gender }}/{{ patient.age }}</text> <text class="patient-meta">{{ patient.gender }}{{ patient.age ? '/' + patient.age + '岁' : '' }}</text>
</view> </view>
<view class="patient-tags"> <view class="patient-tags">
<view v-for="(tag, tIndex) in resolveGroupTags(patient)" :key="tIndex" class="tag"> <view v-for="(tag, tIndex) in resolveGroupTags(patient)" :key="tIndex" class="tag">
@ -81,11 +68,20 @@
</view> </view>
</view> </view>
<!-- Row 2 / 3 --> <!-- Row 2 -->
<view v-if="currentTabKey === 'new'" class="card-row-middle">
<text v-if="patient.record" class="record-text record-ellipsis">
{{ patient.record.type }} / {{ patient.record.date }} / {{ patient.record.diagnosis }}
</text>
<text v-else class="no-record">暂无病历记录</text>
</view>
<!-- Row 3 -->
<view class="card-row-bottom"> <view class="card-row-bottom">
<template v-if="currentTabKey === 'new'"> <!-- New Patient Tab --> <template v-if="currentTabKey === 'new'"> <!-- New Patient Tab -->
<text class="record-text"> <text class="record-text">
{{ patient.createTime || '-' }} / {{ resolveCreatorName(patient) || '-' }} {{ patient.createTime || '-' }} {{ resolveCreatorName(patient) ? resolveCreatorName(patient) +
'创建' : '-' }}
</text> </text>
</template> </template>
<template v-else> <template v-else>
@ -104,12 +100,7 @@
<!-- Sidebar Index --> <!-- Sidebar Index -->
<view v-if="!isBatchMode" class="sidebar-index"> <view v-if="!isBatchMode" class="sidebar-index">
<view <view v-for="letter in indexList" :key="letter" class="index-item" @click="scrollToLetter(letter)">
v-for="letter in indexList"
:key="letter"
class="index-item"
@click="scrollToLetter(letter)"
>
{{ letter }} {{ letter }}
</view> </view>
</view> </view>
@ -120,9 +111,7 @@
<view class="left-action" @click="handleSelectAll"> <view class="left-action" @click="handleSelectAll">
<uni-icons <uni-icons
:type="selectedItems.length > 0 && selectedItems.length === patientList.flatMap(g => g.data).length ? 'checkbox-filled' : 'checkbox'" :type="selectedItems.length > 0 && selectedItems.length === patientList.flatMap(g => g.data).length ? 'checkbox-filled' : 'checkbox'"
size="24" size="24" color="#666"></uni-icons>
color="#666"
></uni-icons>
<text class="footer-text">全选 ({{ selectedItems.length }})</text> <text class="footer-text">全选 ({{ selectedItems.length }})</text>
</view> </view>
<view class="right-actions"> <view class="right-actions">
@ -142,7 +131,7 @@ import dayjs from 'dayjs';
import api from '@/utils/api'; import api from '@/utils/api';
import useAccountStore from '@/store/account'; import useAccountStore from '@/store/account';
import { toast } from '@/utils/widget'; import { confirm as uniConfirm, hideLoading, loading as showLoading, toast } from '@/utils/widget';
// State // State
const teams = ref([]); const teams = ref([]);
@ -202,7 +191,7 @@ const accountStore = useAccountStore();
const { account, doctorInfo } = storeToRefs(accountStore); const { account, doctorInfo } = storeToRefs(accountStore);
const { getDoctorInfo } = accountStore; const { getDoctorInfo } = accountStore;
const teamDisplay = computed(() => `${currentTeam.value?.name || ''}(${managedArchiveCountAllTeams.value})`); const teamDisplay = computed(() => `${currentTeam.value?.name || ''}`);
function asArray(value) { function asArray(value) {
return Array.isArray(value) ? value : []; return Array.isArray(value) ? value : [];
@ -234,6 +223,19 @@ function getTeamId() {
return String(currentTeam.value?.teamId || '') || ''; return String(currentTeam.value?.teamId || '') || '';
} }
function hasCompleteUserInfo() {
const userId = getUserId();
const corpId = String(account.value?.corpId || doctorInfo.value?.corpId || '') || '';
const anotherName = String(doctorInfo.value?.anotherName || '').trim();
return Boolean(corpId && userId && anotherName);
}
function ensureUserInfoForFeature() {
if (hasCompleteUserInfo()) return true;
toast('请先完善个人信息再使用该功能');
return false;
}
async function loadTeamMembers() { async function loadTeamMembers() {
const corpId = getCorpId(); const corpId = getCorpId();
const teamId = getTeamId(); const teamId = getTeamId();
@ -246,10 +248,10 @@ async function loadTeamMembers() {
const members = Array.isArray(t.memberList) ? t.memberList : []; const members = Array.isArray(t.memberList) ? t.memberList : [];
// Update map // Update map
members.forEach(m => { members.forEach(m => {
const uid = String(m?.userid || ''); const uid = String(m?.userid || '');
if (uid) { if (uid) {
userNameMap.value[uid] = String(m?.anotherName || m?.name || m?.userid || '') || uid; userNameMap.value[uid] = String(m?.anotherName || m?.name || m?.userid || '') || uid;
} }
}); });
} catch (e) { } catch (e) {
console.error('获取团队成员失败', e); console.error('获取团队成员失败', e);
@ -535,11 +537,11 @@ async function reload(reset = true) {
managedArchiveCountAllTeams.value = managedArchiveCountAllTeams.value =
Number( Number(
payload.totalAllTeams || payload.totalAllTeams ||
payload.totalAllTeam || payload.totalAllTeam ||
payload.totalAllTeamsCount || payload.totalAllTeamsCount ||
managedArchiveCountAllTeams.value || managedArchiveCountAllTeams.value ||
totalFromApi.value || totalFromApi.value ||
0 0
) || (totalFromApi.value || 0); ) || (totalFromApi.value || 0);
} }
@ -579,7 +581,7 @@ const patientList = computed(() => {
.filter((p) => Number(p?.createTimeTs || 0) >= sevenDaysAgo) .filter((p) => Number(p?.createTimeTs || 0) >= sevenDaysAgo)
.slice() .slice()
.sort((a, b) => Number(b?.createTimeTs || 0) - Number(a?.createTimeTs || 0)); .sort((a, b) => Number(b?.createTimeTs || 0) - Number(a?.createTimeTs || 0));
return [{ letter: '最近新增', data: flatList }]; return [{ letter: '最近7天新增', data: flatList }];
} }
return groupByLetter(all); return groupByLetter(all);
@ -587,7 +589,8 @@ const patientList = computed(() => {
const indexList = computed(() => { const indexList = computed(() => {
if (currentTab.value.kind === 'new') return []; // No index bar for new patient if (currentTab.value.kind === 'new') return []; // No index bar for new patient
return 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').filter(l => patientList.value.some(g => g.letter === l)); const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#'.split('').filter(l => patientList.value.some(g => g.letter === l));
return letters;
}); });
const totalPatients = computed(() => { const totalPatients = computed(() => {
@ -638,6 +641,7 @@ const goToSearch = () => {
const goToGroupManage = () => { const goToGroupManage = () => {
if (checkBatchMode()) return; if (checkBatchMode()) return;
if (!ensureUserInfoForFeature()) return;
uni.navigateTo({ uni.navigateTo({
url: '/pages/case/group-manage' url: '/pages/case/group-manage'
}); });
@ -777,14 +781,149 @@ const cancelBatch = () => {
selectedItems.value = []; selectedItems.value = [];
}; };
const handleTransfer = () => { function showActionSheet(itemList = [], title = '') {
return new Promise((resolve, reject) => {
if (!Array.isArray(itemList) || itemList.length === 0) {
reject(new Error('empty'));
return;
}
uni.showActionSheet({
title,
itemList,
success: ({ tapIndex }) => resolve(tapIndex),
fail: () => reject(new Error('cancel')),
});
});
}
async function fetchTeamMembersByTeamId(teamId) {
const corpId = getCorpId();
if (!corpId || !teamId) return [];
const res = await api('getTeamData', { corpId, teamId });
if (!res?.success) return [];
const t = res?.data && typeof res.data === 'object' ? res.data : {};
return Array.isArray(t.memberList) ? t.memberList : [];
}
async function transferToCustomerPool(customerIds) {
try {
await uniConfirm('客户将与本团队解除服务关系,本团队成员将没有权限查询到客户档案。');
} catch (e) {
return;
}
const corpId = getCorpId();
const currentTeamId = getTeamId();
const creatorUserId = getUserId();
if (!corpId || !currentTeamId || !creatorUserId) return toast('缺少用户/团队信息');
showLoading('保存中...');
try {
const res = await api('transferCustomers', {
corpId,
customerIds,
currentTeamId,
operationType: 'transferToCustomerPool',
creatorUserId,
});
if (!res?.success) {
toast(res?.message || '操作失败');
return;
}
toast('操作成功');
cancelBatch();
await reload(true);
} catch (e) {
toast('操作失败');
} finally {
hideLoading();
}
}
async function transferToOtherTeam(customerIds) {
const corpId = getCorpId();
const currentTeamId = getTeamId();
const creatorUserId = getUserId();
if (!corpId || !currentTeamId || !creatorUserId) return toast('缺少用户/团队信息');
if (!teams.value.length) await loadTeams();
const candidates = teams.value.filter((t) => String(t?.teamId || '') !== String(currentTeamId));
if (!candidates.length) return toast('暂无可选团队');
let teamIndex;
try {
teamIndex = await showActionSheet(candidates.map((t) => t.name || ''), '选择新负责团队');
} catch (e) {
return;
}
const pickedTeam = candidates[teamIndex];
if (!pickedTeam?.teamId) return toast('团队信息异常');
let members = [];
try {
members = await fetchTeamMembersByTeamId(pickedTeam.teamId);
} catch (e) {
members = [];
}
if (!members.length) return toast('当前团队暂无可选成员');
const labels = members.map((m) => String(m?.anotherName || m?.name || m?.userid || ''));
let userIndex;
try {
userIndex = await showActionSheet(labels, '选择责任人');
} catch (e) {
return;
}
const pickedUser = members[userIndex] || null;
const targetUserId = String(pickedUser?.userid || '') || '';
if (!targetUserId) return toast('责任人信息异常');
showLoading('保存中...');
try {
const res = await api('transferCustomers', {
corpId,
customerIds,
currentTeamId,
targetTeamId: String(pickedTeam.teamId),
targetUserId,
operationType: 'transferToOtherTeam',
creatorUserId,
});
if (!res?.success) {
toast(res?.message || '操作失败');
return;
}
toast('操作成功');
cancelBatch();
await reload(true);
} catch (e) {
toast('操作失败');
} finally {
hideLoading();
}
}
const handleTransfer = async () => {
if (selectedItems.value.length === 0) { if (selectedItems.value.length === 0) {
uni.showToast({ title: '请选择患者', icon: 'none' }); uni.showToast({ title: '请选择患者', icon: 'none' });
return; return;
} }
uni.setStorageSync(BATCH_CUSTOMER_IDS_KEY, selectedItems.value.slice());
// Navigate to Transfer Page let tapIndex;
uni.navigateTo({ url: '/pages/case/batch-transfer' }); try {
tapIndex = await showActionSheet(['转移给其他团队', '转移至客户公共池']);
} catch (e) {
return;
}
const customerIds = selectedItems.value.slice().map(String).filter(Boolean);
if (!customerIds.length) return toast('请选择患者');
if (tapIndex === 0) {
await transferToOtherTeam(customerIds);
} else if (tapIndex === 1) {
await transferToCustomerPool(customerIds);
}
}; };
const handleShare = () => { const handleShare = () => {
@ -817,6 +956,8 @@ function onTabClick(tab) {
if (checkBatchMode()) return; if (checkBatchMode()) return;
if (!tab || !tab.key) return; if (!tab || !tab.key) return;
if (currentTabKey.value === tab.key) return; if (currentTabKey.value === tab.key) return;
if (tab.kind === 'group' && !ensureUserInfoForFeature()) return;
currentTabKey.value = tab.key; currentTabKey.value = tab.key;
} }
@ -953,7 +1094,7 @@ onShow(async () => {
color: #666; color: #666;
white-space: nowrap; white-space: nowrap;
flex-shrink: 0; flex-shrink: 0;
min-width: 100rpx; min-width: 70rpx;
text-align: right; text-align: right;
} }
} }
@ -1035,6 +1176,27 @@ onShow(async () => {
} }
} }
.card-row-middle {
font-size: 28rpx;
margin-bottom: 12rpx;
.record-text {
color: #666;
}
.record-ellipsis {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.no-record {
color: #bdc3c7;
}
}
.card-row-bottom { .card-row-bottom {
font-size: 28rpx; font-size: 28rpx;
@ -1067,12 +1229,13 @@ onShow(async () => {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0 30rpx; padding: 0 30rpx;
box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.05); box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
z-index: 99; z-index: 99;
.left-action { .left-action {
display: flex; display: flex;
align-items: center; align-items: center;
.footer-text { .footer-text {
margin-left: 10rpx; margin-left: 10rpx;
font-size: 28rpx; font-size: 28rpx;

View File

@ -10,6 +10,8 @@ const urlsConfig = {
getTeamData: 'getTeamData', getTeamData: 'getTeamData',
getTeamBymember: 'getTeamBymember', getTeamBymember: 'getTeamBymember',
getCurrentTemplate: 'getCurrentTemplate', getCurrentTemplate: 'getCurrentTemplate',
getTemplateGroup: 'getTemplateGroup',
getTemplateListByTemptype: 'getTemplateListByTemptype',
wxAppLogin: 'wxAppLogin', wxAppLogin: 'wxAppLogin',
getDeptList: 'getRealDeptList', getDeptList: 'getRealDeptList',
getHospitalList: 'getRealHospital', getHospitalList: 'getRealHospital',