feat: 添加病历详情页面
@ -1,3 +1,3 @@
|
|||||||
MP_API_BASE_URL=http://192.168.60.2:8080
|
MP_API_BASE_URL=http://localhost:8080
|
||||||
MP_CACHE_PREFIX=development
|
MP_CACHE_PREFIX=development
|
||||||
MP_WX_APP_ID=wx93af55767423938e
|
MP_WX_APP_ID=wx93af55767423938e
|
||||||
|
|||||||
410
components/archive-detail/customer-profile-tab.vue
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/customer-detail/customer-profile.vue -->
|
||||||
|
<view class="wrap">
|
||||||
|
<view class="sub-tabs">
|
||||||
|
<view
|
||||||
|
v-for="t in anchors"
|
||||||
|
:key="t.value"
|
||||||
|
class="sub-tab"
|
||||||
|
:class="{ active: activeAnchor === t.value }"
|
||||||
|
@click="scrollToAnchor(t.value)"
|
||||||
|
>
|
||||||
|
{{ t.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card">
|
||||||
|
<view id="anchor-base" class="section-title" @click="startEdit">
|
||||||
|
<text>基本信息</text>
|
||||||
|
<image class="pen" src="/static/icons/icon-pen.svg" />
|
||||||
|
</view>
|
||||||
|
<view class="rows">
|
||||||
|
<view class="row">
|
||||||
|
<view class="label">姓名</view>
|
||||||
|
<view v-if="!editing" class="val">{{ data.name || '-' }}</view>
|
||||||
|
<input v-else v-model="draft.name" class="input" placeholder="请输入姓名" />
|
||||||
|
</view>
|
||||||
|
<view class="row">
|
||||||
|
<view class="label">性别</view>
|
||||||
|
<view v-if="!editing" class="val">{{ data.sex || '-' }}</view>
|
||||||
|
<picker v-else mode="selector" :range="sexOptions" @change="pickSex">
|
||||||
|
<view class="picker">{{ draft.sex || '请选择' }}</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
<view class="row">
|
||||||
|
<view class="label">年龄</view>
|
||||||
|
<view v-if="!editing" class="val">{{ data.age || '-' }}</view>
|
||||||
|
<input v-else v-model="draft.age" class="input" type="number" placeholder="请输入年龄" />
|
||||||
|
</view>
|
||||||
|
<view class="row">
|
||||||
|
<view class="label">联系电话</view>
|
||||||
|
<view v-if="!editing" class="val link" @click="call(data.mobile)">{{ data.mobile || '-' }}</view>
|
||||||
|
<input v-else v-model="draft.mobile" class="input" type="number" placeholder="请输入联系电话" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card">
|
||||||
|
<view id="anchor-internal" class="section-title" @click="startEdit">
|
||||||
|
<text>内部信息</text>
|
||||||
|
<image class="pen" src="/static/icons/icon-pen.svg" />
|
||||||
|
</view>
|
||||||
|
<view class="rows">
|
||||||
|
<view class="row" @click="openTransferRecord">
|
||||||
|
<view class="label">院内来源</view>
|
||||||
|
<view class="val link">
|
||||||
|
{{ data.creator || '点击查看' }}
|
||||||
|
<uni-icons type="arrowright" size="14" color="#4f6ef7" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="row">
|
||||||
|
<view class="label">备注</view>
|
||||||
|
<view v-if="!editing" class="val">{{ data.notes || '-' }}</view>
|
||||||
|
<input v-else v-model="draft.notes" class="input" placeholder="请输入备注" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card">
|
||||||
|
<view id="anchor-behavior" class="section-title">
|
||||||
|
行为画像
|
||||||
|
</view>
|
||||||
|
<view class="rows">
|
||||||
|
<view class="row">
|
||||||
|
<view class="label">门诊号</view>
|
||||||
|
<view class="val">{{ data.outpatientNo || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="row">
|
||||||
|
<view class="label">住院号</view>
|
||||||
|
<view class="val">{{ data.inpatientNo || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="row">
|
||||||
|
<view class="label">病案号</view>
|
||||||
|
<view class="val">{{ data.medicalRecordNo || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="editing" class="bottom-bar" :style="{ bottom: `${floatingBottom}px` }">
|
||||||
|
<button class="btn plain" @click="cancel">取消</button>
|
||||||
|
<button class="btn primary" @click="save">保存</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<uni-popup ref="transferPopupRef" type="bottom" :mask-click="true">
|
||||||
|
<view class="popup">
|
||||||
|
<view class="popup-title">
|
||||||
|
<view class="popup-title-text">院内流转记录(mock)</view>
|
||||||
|
<view class="popup-close" @click="closeTransferRecord">
|
||||||
|
<uni-icons type="closeempty" size="18" color="#666" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<scroll-view scroll-y class="popup-body">
|
||||||
|
<view class="timeline">
|
||||||
|
<view class="line"></view>
|
||||||
|
<view v-for="r in transferRecords" :key="r._id" class="item">
|
||||||
|
<view class="dot"></view>
|
||||||
|
<view class="content">
|
||||||
|
<view class="time">{{ r.time }}</view>
|
||||||
|
<view class="card2">
|
||||||
|
<view class="trow"><text class="tlabel">转入团队:</text>{{ r.team }}</view>
|
||||||
|
<view class="trow"><text class="tlabel">转入方式:</text>{{ r.type }}</view>
|
||||||
|
<view class="trow"><text class="tlabel">操作人:</text>{{ r.user }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</uni-popup>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref, watch } from 'vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: { type: Object, default: () => ({}) },
|
||||||
|
floatingBottom: { type: Number, default: 16 },
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['save']);
|
||||||
|
|
||||||
|
const anchors = [
|
||||||
|
{ label: '基本信息', value: 'base' },
|
||||||
|
{ label: '内部信息', value: 'internal' },
|
||||||
|
{ label: '行为画像', value: 'behavior' },
|
||||||
|
];
|
||||||
|
const activeAnchor = ref('base');
|
||||||
|
|
||||||
|
const editing = ref(false);
|
||||||
|
const sexOptions = ['男', '女'];
|
||||||
|
const draft = reactive({
|
||||||
|
name: '',
|
||||||
|
sex: '',
|
||||||
|
age: '',
|
||||||
|
mobile: '',
|
||||||
|
notes: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
() => {
|
||||||
|
if (!editing.value) syncDraft();
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
function syncDraft() {
|
||||||
|
draft.name = props.data?.name || '';
|
||||||
|
draft.sex = props.data?.sex || '';
|
||||||
|
draft.age = props.data?.age || '';
|
||||||
|
draft.mobile = props.data?.mobile || '';
|
||||||
|
draft.notes = props.data?.notes || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function startEdit() {
|
||||||
|
if (editing.value) return;
|
||||||
|
editing.value = true;
|
||||||
|
syncDraft();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
editing.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
emit('save', {
|
||||||
|
name: draft.name,
|
||||||
|
sex: draft.sex,
|
||||||
|
age: draft.age,
|
||||||
|
mobile: draft.mobile,
|
||||||
|
notes: draft.notes,
|
||||||
|
});
|
||||||
|
editing.value = false;
|
||||||
|
uni.showToast({ title: '保存成功', icon: 'success' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickSex(e) {
|
||||||
|
draft.sex = sexOptions[e.detail.value] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function call(mobile) {
|
||||||
|
if (!mobile) return;
|
||||||
|
uni.makePhoneCall({ phoneNumber: String(mobile) });
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToAnchor(key) {
|
||||||
|
activeAnchor.value = key;
|
||||||
|
const selector = `#anchor-${key}`;
|
||||||
|
const query = uni.createSelectorQuery();
|
||||||
|
query.select(selector).boundingClientRect();
|
||||||
|
query.selectViewport().scrollOffset();
|
||||||
|
query.exec((res) => {
|
||||||
|
const rect = res[0];
|
||||||
|
const scroll = res[1];
|
||||||
|
if (!rect || !scroll) return;
|
||||||
|
uni.pageScrollTo({ scrollTop: rect.top + scroll.scrollTop - 60, duration: 200 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const transferPopupRef = ref(null);
|
||||||
|
const transferRecords = ref([]);
|
||||||
|
|
||||||
|
function openTransferRecord() {
|
||||||
|
transferRecords.value = [
|
||||||
|
{ _id: 't1', time: dayjs().subtract(120, 'day').format('YYYY-MM-DD HH:mm'), team: '口腔一科(示例)', type: '系统建档', user: '系统' },
|
||||||
|
{ _id: 't2', time: dayjs().subtract(30, 'day').format('YYYY-MM-DD HH:mm'), team: '正畸团队(示例)', type: '团队流转', user: '管理员A' },
|
||||||
|
];
|
||||||
|
transferPopupRef.value?.open?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTransferRecord() {
|
||||||
|
transferPopupRef.value?.close?.();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.wrap {
|
||||||
|
padding: 12px 0 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-tabs {
|
||||||
|
display: flex;
|
||||||
|
background: #f5f6f8;
|
||||||
|
border-bottom: 1px solid #f2f2f2;
|
||||||
|
}
|
||||||
|
.sub-tab {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.sub-tab.active {
|
||||||
|
color: #4f6ef7;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 14px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 1px solid #f2f2f2;
|
||||||
|
}
|
||||||
|
.pen {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
.rows {
|
||||||
|
padding: 2px 14px;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f6f6f6;
|
||||||
|
}
|
||||||
|
.row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
width: 90px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.val {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.val.link {
|
||||||
|
color: #4f6ef7;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.input,
|
||||||
|
.picker {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: #fff;
|
||||||
|
padding: 12px 14px;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
z-index: 30;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup {
|
||||||
|
background: #fff;
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.popup-title {
|
||||||
|
position: relative;
|
||||||
|
padding: 14px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.popup-title-text {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.popup-close {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.popup-body {
|
||||||
|
max-height: 70vh;
|
||||||
|
}
|
||||||
|
.timeline {
|
||||||
|
position: relative;
|
||||||
|
padding: 14px 14px 18px;
|
||||||
|
}
|
||||||
|
.line {
|
||||||
|
position: absolute;
|
||||||
|
left: 22px;
|
||||||
|
top: 18px;
|
||||||
|
bottom: 18px;
|
||||||
|
width: 2px;
|
||||||
|
background: #4f6ef7;
|
||||||
|
}
|
||||||
|
.item {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 32px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.dot {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
top: 6px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #4f6ef7;
|
||||||
|
}
|
||||||
|
.time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.card2 {
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.trow {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
.tlabel {
|
||||||
|
color: #666;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
623
components/archive-detail/follow-up-manage-tab.vue
Normal file
@ -0,0 +1,623 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/customer-detail/followup-manage/followup-manage.vue -->
|
||||||
|
<view class="wrap">
|
||||||
|
<view class="top">
|
||||||
|
<view class="top-row">
|
||||||
|
<view class="my" @click="toggleMy">
|
||||||
|
<image :src="`/static/checkbox${query.isMy ? '-checked' : ''}.svg`" class="checkbox" />
|
||||||
|
<view class="my-text">我的</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="status-scroll">
|
||||||
|
<scroll-view scroll-x>
|
||||||
|
<view class="status-tabs">
|
||||||
|
<view
|
||||||
|
v-for="t in statusTabs"
|
||||||
|
:key="t.value"
|
||||||
|
class="status-tab"
|
||||||
|
:class="{ active: query.status === t.value }"
|
||||||
|
@click="toggleStatus(t.value)"
|
||||||
|
>
|
||||||
|
{{ t.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="filter-btn" @click="openFilter">
|
||||||
|
<image class="filter-icon" :src="`/static/icons/icon-filter${filtered ? '-active' : ''}.svg`" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="total">共<text class="total-num">{{ total }}</text>条</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="list">
|
||||||
|
<view v-for="i in list" :key="i._id" class="card" @click="toDetail(i)">
|
||||||
|
<view class="head">
|
||||||
|
<view class="date">计划日期: <text class="date-val">{{ i.planDate }}</text></view>
|
||||||
|
<view class="executor truncate">{{ i.executorName }}<text v-if="i.executeTeamName">({{ i.executeTeamName }})</text></view>
|
||||||
|
</view>
|
||||||
|
<view class="body">
|
||||||
|
<view class="title-row">
|
||||||
|
<view class="type">{{ i.eventTypeLabel }}</view>
|
||||||
|
<view class="status" :class="`st-${i.status}`">{{ i.eventStatusLabel }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="content">{{ i.taskContent || '暂无内容' }}</view>
|
||||||
|
<view v-if="i.status === 'treated'" class="result">【处理结果】 {{ i.result || '' }}</view>
|
||||||
|
<view class="footer">创建: {{ i.createTimeStr }} {{ i.creatorName }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="list.length === 0" class="empty">暂无数据</view>
|
||||||
|
<uni-load-more v-if="list.length" :status="moreStatus" :contentText="loadMoreText" @clickLoadMore="getMore" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="fab" :style="{ bottom: `${floatingBottom}px` }" @click="add">
|
||||||
|
<uni-icons type="plusempty" size="24" color="#fff" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 筛选弹窗(简化 mock) -->
|
||||||
|
<uni-popup ref="filterPopupRef" type="bottom" :mask-click="true">
|
||||||
|
<view class="popup">
|
||||||
|
<view class="popup-title">
|
||||||
|
<view class="popup-title-text">全部筛选</view>
|
||||||
|
<view class="popup-close" @click="closeFilter(true)">
|
||||||
|
<uni-icons type="closeempty" size="18" color="#666" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<scroll-view scroll-y class="popup-body">
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-title">任务类型</view>
|
||||||
|
<view class="chip-wrap">
|
||||||
|
<view
|
||||||
|
v-for="t in typeOptions"
|
||||||
|
:key="t.value"
|
||||||
|
class="chip"
|
||||||
|
:class="{ active: typeSelectedMap[t.value] }"
|
||||||
|
@click="toggleType(t.value)"
|
||||||
|
>
|
||||||
|
{{ t.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-title">所属团队</view>
|
||||||
|
<picker mode="selector" :range="teamOptions" range-key="label" @change="pickTeam">
|
||||||
|
<view class="select-row">
|
||||||
|
<view class="select-text" :class="{ muted: teamPicked.value === 'ALL' }">{{ teamPicked.label }}</view>
|
||||||
|
<uni-icons type="right" size="16" color="#999" />
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-title">计划日期</view>
|
||||||
|
<view class="range-row">
|
||||||
|
<picker mode="date" @change="pickStart">
|
||||||
|
<view class="range-pill" :class="{ muted: !planRange[0] }">{{ planRange[0] || '开始日期' }}</view>
|
||||||
|
</picker>
|
||||||
|
<view class="sep">-</view>
|
||||||
|
<picker mode="date" @change="pickEnd">
|
||||||
|
<view class="range-pill" :class="{ muted: !planRange[1] }">{{ planRange[1] || '结束日期' }}</view>
|
||||||
|
</picker>
|
||||||
|
<view class="clear" @click="clearPlanRange">
|
||||||
|
<uni-icons type="closeempty" size="16" color="#999" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view class="actions">
|
||||||
|
<button class="btn plain" @click="resetFilter">重置</button>
|
||||||
|
<button class="btn primary" @click="confirmFilter">确定</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</uni-popup>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
|
||||||
|
import { ensureSeed, queryFollowups } from './mock';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: { type: Object, default: () => ({}) },
|
||||||
|
archiveId: { type: String, default: '' },
|
||||||
|
reachBottomTime: { type: [String, Number], default: '' },
|
||||||
|
floatingBottom: { type: Number, default: 16 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusTabs = [
|
||||||
|
{ label: '全部', value: 'all' },
|
||||||
|
{ label: '待处理', value: 'processing' },
|
||||||
|
{ label: '未开始', value: 'notStart' },
|
||||||
|
{ label: '已完成', value: 'treated' },
|
||||||
|
{ label: '已取消', value: 'cancelled' },
|
||||||
|
{ label: '已过期', value: 'expired' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const typeOptions = [
|
||||||
|
{ label: '回访', value: 'followup' },
|
||||||
|
{ label: '复诊提醒', value: 'revisit' },
|
||||||
|
{ label: '问卷', value: 'questionnaire' },
|
||||||
|
{ label: '其他', value: 'other' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const teamOptions = [
|
||||||
|
{ label: '全部', value: 'ALL' },
|
||||||
|
{ label: '口腔一科(示例)', value: 'team_1' },
|
||||||
|
{ label: '正畸团队(示例)', value: 'team_2' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const query = reactive({
|
||||||
|
isMy: false,
|
||||||
|
status: 'all',
|
||||||
|
eventTypes: [],
|
||||||
|
teamId: 'ALL',
|
||||||
|
planRange: ['', ''],
|
||||||
|
});
|
||||||
|
|
||||||
|
const list = ref([]);
|
||||||
|
const total = ref(0);
|
||||||
|
|
||||||
|
const page = ref(1);
|
||||||
|
const pageSize = 10;
|
||||||
|
const pages = ref(1);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const moreStatus = computed(() => {
|
||||||
|
if (loading.value) return 'loading';
|
||||||
|
return page.value <= pages.value ? 'more' : 'no-more';
|
||||||
|
});
|
||||||
|
const loadMoreText = {
|
||||||
|
contentdown: '点击加载更多',
|
||||||
|
contentrefresh: '加载中...',
|
||||||
|
contentnomore: '没有更多了',
|
||||||
|
};
|
||||||
|
|
||||||
|
const filtered = ref(false);
|
||||||
|
const typeSelectedMap = computed(() => {
|
||||||
|
const s = new Set(query.eventTypes || []);
|
||||||
|
return typeOptions.reduce((acc, cur) => ((acc[cur.value] = s.has(cur.value)), acc), {});
|
||||||
|
});
|
||||||
|
|
||||||
|
function resetList() {
|
||||||
|
page.value = 1;
|
||||||
|
pages.value = 1;
|
||||||
|
list.value = [];
|
||||||
|
getMore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMore() {
|
||||||
|
if (!props.archiveId) return;
|
||||||
|
if (loading.value) return;
|
||||||
|
if (page.value > pages.value) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const { list: arr, total: t, pages: p } = queryFollowups({
|
||||||
|
archiveId: props.archiveId,
|
||||||
|
page: page.value,
|
||||||
|
pageSize,
|
||||||
|
status: query.status,
|
||||||
|
isMy: query.isMy,
|
||||||
|
eventTypes: query.eventTypes,
|
||||||
|
teamId: query.teamId,
|
||||||
|
planRange: query.planRange,
|
||||||
|
});
|
||||||
|
total.value = t;
|
||||||
|
pages.value = p;
|
||||||
|
list.value = page.value === 1 ? arr : [...list.value, ...arr];
|
||||||
|
page.value += 1;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMy() {
|
||||||
|
query.isMy = !query.isMy;
|
||||||
|
resetList();
|
||||||
|
}
|
||||||
|
function toggleStatus(v) {
|
||||||
|
query.status = v;
|
||||||
|
resetList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function add() {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['+新增任务', '+回访记录'],
|
||||||
|
success: ({ tapIndex }) => {
|
||||||
|
if (tapIndex === 0) {
|
||||||
|
uni.navigateTo({ url: `/pages/case/followup-detail?archiveId=${encodeURIComponent(props.archiveId)}&mode=add` });
|
||||||
|
} else if (tapIndex === 1) {
|
||||||
|
uni.navigateTo({ url: `/pages/case/followup-detail?archiveId=${encodeURIComponent(props.archiveId)}&mode=record` });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDetail(todo) {
|
||||||
|
uni.navigateTo({ url: `/pages/case/followup-detail?archiveId=${encodeURIComponent(props.archiveId)}&mode=edit&id=${encodeURIComponent(todo._id)}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- filter popup ----
|
||||||
|
const filterPopupRef = ref(null);
|
||||||
|
const state = ref(null);
|
||||||
|
const teamPicked = ref(teamOptions[0]);
|
||||||
|
const planRange = ref(['', '']);
|
||||||
|
|
||||||
|
function openFilter() {
|
||||||
|
state.value = {
|
||||||
|
query: { ...query, eventTypes: [...(query.eventTypes || [])], planRange: [...(query.planRange || ['', ''])] },
|
||||||
|
team: { ...teamPicked.value },
|
||||||
|
range: [...planRange.value],
|
||||||
|
};
|
||||||
|
planRange.value = [...(query.planRange || ['', ''])];
|
||||||
|
teamPicked.value = teamOptions.find((i) => i.value === query.teamId) || teamOptions[0];
|
||||||
|
filterPopupRef.value?.open?.();
|
||||||
|
}
|
||||||
|
function closeFilter(revert) {
|
||||||
|
if (revert && state.value) {
|
||||||
|
Object.assign(query, state.value.query);
|
||||||
|
planRange.value = state.value.range;
|
||||||
|
teamPicked.value = state.value.team;
|
||||||
|
}
|
||||||
|
filterPopupRef.value?.close?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleType(v) {
|
||||||
|
const set = new Set(query.eventTypes || []);
|
||||||
|
if (set.has(v)) set.delete(v);
|
||||||
|
else set.add(v);
|
||||||
|
query.eventTypes = Array.from(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickTeam(e) {
|
||||||
|
teamPicked.value = teamOptions[e.detail.value] || teamOptions[0];
|
||||||
|
query.teamId = teamPicked.value.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickStart(e) {
|
||||||
|
planRange.value = [e.detail.value || '', planRange.value[1] || ''];
|
||||||
|
query.planRange = [...planRange.value];
|
||||||
|
}
|
||||||
|
function pickEnd(e) {
|
||||||
|
planRange.value = [planRange.value[0] || '', e.detail.value || ''];
|
||||||
|
query.planRange = [...planRange.value];
|
||||||
|
}
|
||||||
|
function clearPlanRange() {
|
||||||
|
planRange.value = ['', ''];
|
||||||
|
query.planRange = ['', ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFilter() {
|
||||||
|
query.eventTypes = [];
|
||||||
|
query.teamId = 'ALL';
|
||||||
|
teamPicked.value = teamOptions[0];
|
||||||
|
clearPlanRange();
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmFilter() {
|
||||||
|
filtered.value = Boolean((query.eventTypes && query.eventTypes.length) || query.teamId !== 'ALL' || query.planRange[0] || query.planRange[1]);
|
||||||
|
closeFilter(false);
|
||||||
|
resetList();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
ensureSeed(props.archiveId, props.data);
|
||||||
|
resetList();
|
||||||
|
uni.$on('archive-detail:followup-changed', resetList);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
uni.$off('archive-detail:followup-changed', resetList);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.reachBottomTime,
|
||||||
|
() => getMore()
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.wrap {
|
||||||
|
padding: 0 0 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top {
|
||||||
|
background: #f5f6f8;
|
||||||
|
padding: 10px 14px 8px;
|
||||||
|
border-bottom: 1px solid #f2f2f2;
|
||||||
|
}
|
||||||
|
.top-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.my {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.checkbox {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
.my-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.status-scroll {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.status-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
.status-tab {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-right: 8px;
|
||||||
|
background: #eaecef;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.status-tab.active {
|
||||||
|
background: #dbe6ff;
|
||||||
|
color: #4f6ef7;
|
||||||
|
}
|
||||||
|
.filter-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
.filter-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
.total {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.total-num {
|
||||||
|
color: #ff4d4f;
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
padding: 0 14px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.head {
|
||||||
|
padding: 12px 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid #f2f2f2;
|
||||||
|
}
|
||||||
|
.date {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.date-val {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.executor {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.truncate {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.body {
|
||||||
|
padding: 12px 12px;
|
||||||
|
}
|
||||||
|
.title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.type {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f1f1f;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.st-processing {
|
||||||
|
background: #ffe5e5;
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
.st-notStart {
|
||||||
|
background: #dbe6ff;
|
||||||
|
color: #4f6ef7;
|
||||||
|
}
|
||||||
|
.st-treated {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
.st-cancelled,
|
||||||
|
.st-expired {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
.result {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
padding: 120px 0;
|
||||||
|
text-align: center;
|
||||||
|
color: #9aa0a6;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab {
|
||||||
|
position: fixed;
|
||||||
|
right: 16px;
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
border-radius: 26px;
|
||||||
|
background: #4f6ef7;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 10px 18px rgba(79, 110, 247, 0.35);
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup {
|
||||||
|
background: #fff;
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.popup-title {
|
||||||
|
position: relative;
|
||||||
|
padding: 14px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.popup-title-text {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.popup-close {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.popup-body {
|
||||||
|
max-height: 70vh;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.chip-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.chip {
|
||||||
|
width: 110px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.chip.active {
|
||||||
|
background: #4f6ef7;
|
||||||
|
border-color: #4f6ef7;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.select-row {
|
||||||
|
height: 40px;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.select-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
max-width: 250px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.select-text.muted {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.range-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.range-pill {
|
||||||
|
width: 110px;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.range-pill.muted {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.sep {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.clear {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.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>
|
||||||
311
components/archive-detail/health-profile-tab.vue
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/customer-detail/health-profile/health-profile.vue -->
|
||||||
|
<view class="wrap">
|
||||||
|
<view class="filters">
|
||||||
|
<picker mode="selector" :range="typeRange" range-key="name" @change="pickType">
|
||||||
|
<view class="filter-pill">
|
||||||
|
<view class="pill-text" :class="{ muted: currentType.value === 'ALL' }">{{ currentType.value === 'ALL' ? '档案类型' : currentType.name }}</view>
|
||||||
|
<uni-icons type="arrowdown" size="12" color="#666" />
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
|
||||||
|
<view class="filter-pill">
|
||||||
|
<picker mode="date" @change="pickDate">
|
||||||
|
<view class="pill-text" :class="{ muted: !date }">{{ date || '时间筛选' }}</view>
|
||||||
|
</picker>
|
||||||
|
<view class="pill-icon" @click.stop="clearDate">
|
||||||
|
<uni-icons v-if="date" type="closeempty" size="14" color="#666" />
|
||||||
|
<uni-icons v-else type="arrowdown" size="12" color="#666" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="list">
|
||||||
|
<view v-for="r in records" :key="r._id" class="card record" @click="edit(r)">
|
||||||
|
<view class="record-head">
|
||||||
|
<view class="record-date">{{ r.dateStr }}</view>
|
||||||
|
<view class="record-tag" :class="tagClass[r.templateType] || 'bg-blue'">{{ r.tempName }}</view>
|
||||||
|
<view v-if="r.corpName === '其他'" class="record-tag bg-rose">外院</view>
|
||||||
|
</view>
|
||||||
|
<view class="record-body">
|
||||||
|
<view v-for="row in buildSummaryRows(r)" :key="row.k" class="kv">
|
||||||
|
<view class="k">{{ row.k }}:</view>
|
||||||
|
<view class="v">{{ row.v }}</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="!buildSummaryRows(r).length" class="kv">
|
||||||
|
<view class="k">摘要:</view>
|
||||||
|
<view class="v">{{ r.summary || '暂无内容' }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="record-foot">
|
||||||
|
<view class="foot-left">创建时间:{{ r.createTimeStr }}</view>
|
||||||
|
<view class="foot-right">记录人:{{ r.creatorName || '—' }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="records.length === 0" class="empty">暂无数据</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="fab" :style="{ bottom: `${floatingBottom}px` }" @click="add">
|
||||||
|
<uni-icons type="plusempty" size="24" color="#fff" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { ensureSeed, getVisitRecordTemplates, queryVisitRecords } from './mock';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: { type: Object, default: () => ({}) },
|
||||||
|
archiveId: { type: String, default: '' },
|
||||||
|
floatingBottom: { type: Number, default: 16 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const date = ref('');
|
||||||
|
|
||||||
|
const templates = ref(getVisitRecordTemplates());
|
||||||
|
|
||||||
|
const typeRange = computed(() => [{ name: '全部', value: 'ALL' }, ...templates.value.map((t) => ({ name: t.name, value: t.templateType }))]);
|
||||||
|
const currentType = ref({ name: '全部', value: 'ALL' });
|
||||||
|
|
||||||
|
const records = ref([]);
|
||||||
|
|
||||||
|
function refreshList() {
|
||||||
|
if (!props.archiveId) return;
|
||||||
|
records.value = queryVisitRecords({
|
||||||
|
archiveId: props.archiveId,
|
||||||
|
medicalType: currentType.value.value,
|
||||||
|
date: date.value,
|
||||||
|
}).map((r) => ({
|
||||||
|
...r,
|
||||||
|
createTimeStr: r.createTime ? dayjs(r.createTime).format('YYYY-MM-DD') : '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagClass = {
|
||||||
|
outpatient: 'bg-amber',
|
||||||
|
inhospital: 'bg-teal',
|
||||||
|
physicalExaminationTemplate: 'bg-green',
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildSummaryRows(r) {
|
||||||
|
const rows = [];
|
||||||
|
if (r.templateType === 'outpatient') {
|
||||||
|
if (r.corpName) rows.push({ k: '机构', v: r.corpName });
|
||||||
|
if (r.deptName) rows.push({ k: '科室', v: r.deptName });
|
||||||
|
if (r.doctor) rows.push({ k: '医生', v: r.doctor });
|
||||||
|
if (r.diagnosisName) rows.push({ k: '诊断', v: r.diagnosisName });
|
||||||
|
if (r.summary) rows.push({ k: '摘要', v: r.summary });
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
if (r.templateType === 'inhospital') {
|
||||||
|
if (r.corpName) rows.push({ k: '机构', v: r.corpName });
|
||||||
|
if (r.diagnosisName) rows.push({ k: '诊断', v: r.diagnosisName });
|
||||||
|
if (r.summary) rows.push({ k: '摘要', v: r.summary });
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
if (r.templateType === 'physicalExaminationTemplate') {
|
||||||
|
if (r.corpName) rows.push({ k: '机构', v: r.corpName });
|
||||||
|
if (r.inspectPakageName) rows.push({ k: '套餐', v: r.inspectPakageName });
|
||||||
|
if (r.positiveFind) rows.push({ k: '阳性', v: r.positiveFind });
|
||||||
|
if (r.summary) rows.push({ k: '摘要', v: r.summary });
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
if (r.summary) rows.push({ k: '摘要', v: r.summary });
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickType(e) {
|
||||||
|
currentType.value = typeRange.value[e.detail.value] || { name: '全部', value: 'ALL' };
|
||||||
|
refreshList();
|
||||||
|
}
|
||||||
|
function pickDate(e) {
|
||||||
|
date.value = e.detail.value || '';
|
||||||
|
refreshList();
|
||||||
|
}
|
||||||
|
function clearDate() {
|
||||||
|
if (!date.value) return;
|
||||||
|
date.value = '';
|
||||||
|
refreshList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function add() {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: templates.value.map((i) => i.name),
|
||||||
|
success: ({ tapIndex }) => {
|
||||||
|
const t = templates.value[tapIndex];
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/case/visit-record-detail?archiveId=${encodeURIComponent(props.archiveId)}&type=${encodeURIComponent(t.templateType)}&name=${encodeURIComponent(props.data?.name || '')}`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit(record) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/case/visit-record-detail?archiveId=${encodeURIComponent(props.archiveId)}&id=${encodeURIComponent(record._id)}&type=${encodeURIComponent(record.templateType || record.medicalType || '')}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
ensureSeed(props.archiveId, props.data);
|
||||||
|
refreshList();
|
||||||
|
uni.$on('archive-detail:visit-record-changed', refreshList);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
uni.$off('archive-detail:visit-record-changed', refreshList);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.wrap {
|
||||||
|
padding: 12px 0 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #f5f6f8;
|
||||||
|
border-bottom: 1px solid #f2f2f2;
|
||||||
|
}
|
||||||
|
.filter-pill {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 110px;
|
||||||
|
}
|
||||||
|
.pill-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
max-width: 140px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.pill-text.muted {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.pill-icon {
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
padding: 0 14px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
.record {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.record-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 12px 10px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.record-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f1f1f;
|
||||||
|
}
|
||||||
|
.record-date {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.record-body {
|
||||||
|
padding: 0 12px 12px;
|
||||||
|
}
|
||||||
|
.kv {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
.k {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.v {
|
||||||
|
flex: 1;
|
||||||
|
color: #333;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.record-foot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 12px;
|
||||||
|
border-top: 1px solid #f2f2f2;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.foot-left {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.foot-right {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-tag {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #fff;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.bg-blue {
|
||||||
|
background: #4f6ef7;
|
||||||
|
}
|
||||||
|
.bg-amber {
|
||||||
|
background: #d97706;
|
||||||
|
}
|
||||||
|
.bg-teal {
|
||||||
|
background: #0f766e;
|
||||||
|
}
|
||||||
|
.bg-green {
|
||||||
|
background: #16a34a;
|
||||||
|
}
|
||||||
|
.bg-rose {
|
||||||
|
background: #f43f5e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
padding: 120px 0;
|
||||||
|
text-align: center;
|
||||||
|
color: #9aa0a6;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab {
|
||||||
|
position: fixed;
|
||||||
|
right: 16px;
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
border-radius: 26px;
|
||||||
|
background: #4f6ef7;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 10px 18px rgba(79, 110, 247, 0.35);
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
333
components/archive-detail/mock.js
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
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', required: true, format: 'YYYY-MM-DD' },
|
||||||
|
{ title: 'corpName', name: '就诊机构', type: 'input', required: false, wordLimit: 30, inputType: 'text' },
|
||||||
|
{ title: 'deptName', name: '科室', type: 'input', required: false, wordLimit: 30, inputType: 'text' },
|
||||||
|
{ title: 'doctor', name: '医生', type: 'input', required: false, wordLimit: 30, inputType: 'text' },
|
||||||
|
{ title: 'diagnosisName', name: '诊断', type: 'textarea', required: false, wordLimit: 200 },
|
||||||
|
{ title: 'summary', name: '摘要', type: 'textarea', required: false, wordLimit: 200 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
templateType: 'inhospital',
|
||||||
|
templateName: '住院记录',
|
||||||
|
templateList: [
|
||||||
|
{ title: 'inhospitalDate', name: '入院日期', type: 'date', required: true, format: 'YYYY-MM-DD' },
|
||||||
|
{ title: 'corpName', name: '住院机构', type: 'input', required: false, wordLimit: 30, inputType: 'text' },
|
||||||
|
{ title: 'diagnosisName', name: '诊断', type: 'textarea', required: false, wordLimit: 200 },
|
||||||
|
{ title: 'summary', name: '摘要', type: 'textarea', required: false, wordLimit: 200 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
templateType: 'physicalExaminationTemplate',
|
||||||
|
templateName: '体检记录',
|
||||||
|
templateList: [
|
||||||
|
{ title: 'inspectDate', name: '体检日期', type: 'date', required: true, format: 'YYYY-MM-DD' },
|
||||||
|
{ title: 'corpName', name: '体检机构', type: 'input', required: false, wordLimit: 30, inputType: 'text' },
|
||||||
|
{ title: 'inspectPakageName', name: '体检套餐', type: 'input', required: false, wordLimit: 50, inputType: 'text' },
|
||||||
|
{ title: 'positiveFind', name: '阳性发现', type: 'textarea', required: false, wordLimit: 300 },
|
||||||
|
{ title: 'summary', name: '摘要', type: 'textarea', required: false, wordLimit: 200 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
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',
|
||||||
|
sortTime: now - 1000 * 60 * 60 * 24 * 2,
|
||||||
|
visitTime: dayjs(now - 1000 * 60 * 60 * 24 * 2).format('YYYY-MM-DD'),
|
||||||
|
corpName: '某某医院',
|
||||||
|
deptName: '口腔科',
|
||||||
|
doctor: '李医生',
|
||||||
|
diagnosisName: '牙列不齐(mock)',
|
||||||
|
summary: '初诊:拍片、取模,制定治疗方案。',
|
||||||
|
createTime: now - 1000 * 60 * 60 * 24 * 2,
|
||||||
|
creatorName: '李医生',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: uid('mr'),
|
||||||
|
medicalType: 'inhospital',
|
||||||
|
tempName: '住院记录',
|
||||||
|
templateType: 'inhospital',
|
||||||
|
sortTime: now - 1000 * 60 * 60 * 24 * 15,
|
||||||
|
inhospitalDate: dayjs(now - 1000 * 60 * 60 * 24 * 15).format('YYYY-MM-DD'),
|
||||||
|
corpName: '某某医院',
|
||||||
|
diagnosisName: '术后复查(mock)',
|
||||||
|
summary: '复诊:术后复查,恢复良好。',
|
||||||
|
createTime: now - 1000 * 60 * 60 * 24 * 15,
|
||||||
|
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 queryVisitRecords({ archiveId, medicalType = 'ALL', date = '' }) {
|
||||||
|
const db = getDb();
|
||||||
|
const list = Array.isArray(db.visitRecordsByArchiveId[archiveId]) ? db.visitRecordsByArchiveId[archiveId] : [];
|
||||||
|
|
||||||
|
const withDate = list.map((i) => ({
|
||||||
|
...i,
|
||||||
|
dateStr: i.sortTime ? dayjs(i.sortTime).format('YYYY-MM-DD') : '',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const matchType = medicalType === 'ALL' ? withDate : withDate.filter((i) => i.medicalType === medicalType);
|
||||||
|
const matchDate = date ? matchType.filter((i) => i.dateStr === date) : matchType;
|
||||||
|
return matchDate.sort((a, b) => (b.sortTime || 0) - (a.sortTime || 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
460
components/archive-detail/service-info-tab.vue
Normal file
@ -0,0 +1,460 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/customer-detail/service-info/service-info.vue -->
|
||||||
|
<view class="wrap">
|
||||||
|
<view class="filters">
|
||||||
|
<picker mode="selector" :range="typeList" range-key="label" @change="pickType">
|
||||||
|
<view class="filter-pill">
|
||||||
|
<view class="pill-text" :class="{ muted: currentType.value === 'ALL' }">{{ currentType.value === 'ALL' ? '服务类型' : currentType.label }}</view>
|
||||||
|
<uni-icons type="arrowdown" size="12" color="#666" />
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
|
||||||
|
<uni-datetime-picker v-model="dateRange" type="daterange" rangeSeparator="-" @change="changeDates">
|
||||||
|
<view class="filter-pill wide">
|
||||||
|
<view class="pill-text" :class="{ muted: !dateRange.length }">
|
||||||
|
{{ dateRange.length ? dateRange.join('~') : '服务时间' }}
|
||||||
|
</view>
|
||||||
|
<view class="pill-icon" @click.stop="clearDates">
|
||||||
|
<uni-icons v-if="dateRange.length" type="closeempty" size="14" color="#666" />
|
||||||
|
<uni-icons v-else type="arrowdown" size="12" color="#666" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</uni-datetime-picker>
|
||||||
|
|
||||||
|
<picker mode="selector" :range="teamList" range-key="label" @change="pickTeam">
|
||||||
|
<view class="filter-pill">
|
||||||
|
<view class="pill-text">{{ currentTeam.label }}</view>
|
||||||
|
<uni-icons type="arrowdown" size="12" color="#666" />
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="timeline">
|
||||||
|
<view v-for="(i, idx) in list" :key="i._id" class="cell" @click="toggleExpand(i)">
|
||||||
|
<view class="head">
|
||||||
|
<view class="dot"></view>
|
||||||
|
<view class="time">{{ i.timeStr }}</view>
|
||||||
|
<view v-if="i.hasFile" class="file-link" @click.stop="viewFile(i)">
|
||||||
|
{{ i.fileType === 'article' ? '查看文章' : '查看问卷' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta">
|
||||||
|
<view class="tag">{{ i.typeStr }}</view>
|
||||||
|
<view class="meta-text">{{ i.executorName }}</view>
|
||||||
|
<view class="meta-text truncate">{{ i.executeTeamName }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="body">
|
||||||
|
<view class="content" :class="{ clamp: !expandMap[i._id] }">
|
||||||
|
{{ i.taskContent || '暂无内容' }}<text v-if="i.result">(处理结果: {{ i.result }})</text>
|
||||||
|
</view>
|
||||||
|
<image class="pen" src="/static/icons/icon-pen.svg" @click.stop="edit(i)" />
|
||||||
|
</view>
|
||||||
|
<view v-if="idx < list.length - 1" class="line"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="list.length === 0" class="empty">暂无数据</view>
|
||||||
|
<uni-load-more
|
||||||
|
v-if="list.length"
|
||||||
|
:status="moreStatus"
|
||||||
|
:contentText="loadMoreText"
|
||||||
|
@clickLoadMore="getMore"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="fab" :style="{ bottom: `${floatingBottom}px` }" @click="add">
|
||||||
|
<uni-icons type="plusempty" size="24" color="#fff" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<uni-popup ref="filePopupRef" type="bottom" :mask-click="true">
|
||||||
|
<view class="popup">
|
||||||
|
<view class="popup-title">
|
||||||
|
<view class="popup-title-text">{{ fileTitle }}</view>
|
||||||
|
<view class="popup-close" @click="closeFilePopup">
|
||||||
|
<uni-icons type="closeempty" size="18" color="#666" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="popup-body2">
|
||||||
|
<view class="desc">{{ fileDesc }}</view>
|
||||||
|
<button class="btn primary" @click="copyFile">{{ fileAction }}</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</uni-popup>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { ensureSeed, queryServiceRecords } from './mock';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: { type: Object, default: () => ({}) },
|
||||||
|
archiveId: { type: String, default: '' },
|
||||||
|
reachBottomTime: { type: [String, Number], default: '' },
|
||||||
|
floatingBottom: { type: Number, default: 16 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const typeList = [
|
||||||
|
{ label: '全部', value: 'ALL' },
|
||||||
|
{ label: '电话回访', value: 'phone' },
|
||||||
|
{ label: '短信提醒', value: 'sms' },
|
||||||
|
{ label: '问卷', value: 'questionnaire' },
|
||||||
|
{ label: '文章', value: 'article' },
|
||||||
|
];
|
||||||
|
const teamList = [
|
||||||
|
{ label: '全部', value: 'ALL' },
|
||||||
|
{ label: '口腔一科(示例)', value: 'team_1' },
|
||||||
|
{ label: '正畸团队(示例)', value: 'team_2' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const currentType = ref(typeList[0]);
|
||||||
|
const currentTeam = ref(teamList[0]);
|
||||||
|
const dateRange = ref([]);
|
||||||
|
|
||||||
|
const page = ref(1);
|
||||||
|
const pageSize = 10;
|
||||||
|
const pages = ref(1);
|
||||||
|
const loading = ref(false);
|
||||||
|
const list = ref([]);
|
||||||
|
const expandMap = ref({});
|
||||||
|
|
||||||
|
const moreStatus = computed(() => {
|
||||||
|
if (loading.value) return 'loading';
|
||||||
|
return page.value <= pages.value ? 'more' : 'no-more';
|
||||||
|
});
|
||||||
|
const loadMoreText = {
|
||||||
|
contentdown: '点击加载更多',
|
||||||
|
contentrefresh: '加载中...',
|
||||||
|
contentnomore: '没有更多了',
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapRow(i) {
|
||||||
|
const hasFile = Boolean(i.pannedEventSendFile);
|
||||||
|
const fileType = i.pannedEventSendFile?.type === 'article' ? 'article' : i.pannedEventSendFile?.type === 'questionnaire' ? 'questionnaire' : '';
|
||||||
|
return {
|
||||||
|
...i,
|
||||||
|
hasFile,
|
||||||
|
fileType,
|
||||||
|
timeStr: i.executionTime ? dayjs(i.executionTime).format('YYYY-MM-DD HH:mm') : '--',
|
||||||
|
typeStr: typeList.find((t) => t.value === i.eventType)?.label || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
page.value = 1;
|
||||||
|
pages.value = 1;
|
||||||
|
list.value = [];
|
||||||
|
getMore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMore() {
|
||||||
|
if (!props.archiveId) return;
|
||||||
|
if (loading.value) return;
|
||||||
|
if (page.value > pages.value) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const { list: arr, pages: p } = queryServiceRecords({
|
||||||
|
archiveId: props.archiveId,
|
||||||
|
page: page.value,
|
||||||
|
pageSize,
|
||||||
|
eventType: currentType.value.value,
|
||||||
|
teamId: currentTeam.value.value,
|
||||||
|
dateRange: dateRange.value,
|
||||||
|
});
|
||||||
|
pages.value = p;
|
||||||
|
const mapped = arr.map(mapRow);
|
||||||
|
list.value = page.value === 1 ? mapped : [...list.value, ...mapped];
|
||||||
|
page.value += 1;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleExpand(r) {
|
||||||
|
expandMap.value[r._id] = !expandMap.value[r._id];
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickType(e) {
|
||||||
|
currentType.value = typeList[e.detail.value] || typeList[0];
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
function pickTeam(e) {
|
||||||
|
currentTeam.value = teamList[e.detail.value] || teamList[0];
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeDates() {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
function clearDates() {
|
||||||
|
if (!dateRange.value.length) return;
|
||||||
|
dateRange.value = [];
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
function add() {
|
||||||
|
uni.navigateTo({ url: `/pages/case/service-record-detail?archiveId=${encodeURIComponent(props.archiveId)}&mode=add` });
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit(record) {
|
||||||
|
uni.navigateTo({ url: `/pages/case/service-record-detail?archiveId=${encodeURIComponent(props.archiveId)}&mode=edit&id=${encodeURIComponent(record._id)}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePopupRef = ref(null);
|
||||||
|
const fileTitle = ref('');
|
||||||
|
const fileDesc = ref('');
|
||||||
|
const fileAction = ref('');
|
||||||
|
const fileCopyText = ref('');
|
||||||
|
|
||||||
|
function viewFile(record) {
|
||||||
|
if (!record.hasFile) return;
|
||||||
|
if (record.fileType === 'article') {
|
||||||
|
fileTitle.value = '查看文章';
|
||||||
|
fileDesc.value = record.pannedEventSendFile?.url || '';
|
||||||
|
fileAction.value = '复制链接';
|
||||||
|
fileCopyText.value = record.pannedEventSendFile?.url || '';
|
||||||
|
} else {
|
||||||
|
fileTitle.value = '查看问卷';
|
||||||
|
fileDesc.value = `问卷ID: ${record.pannedEventSendFile?.surveryId || '--'}`;
|
||||||
|
fileAction.value = '复制问卷ID';
|
||||||
|
fileCopyText.value = record.pannedEventSendFile?.surveryId || '';
|
||||||
|
}
|
||||||
|
filePopupRef.value?.open?.();
|
||||||
|
}
|
||||||
|
function closeFilePopup() {
|
||||||
|
filePopupRef.value?.close?.();
|
||||||
|
}
|
||||||
|
function copyFile() {
|
||||||
|
if (!fileCopyText.value) return;
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: fileCopyText.value,
|
||||||
|
success: () => uni.showToast({ title: '已复制', icon: 'success' }),
|
||||||
|
});
|
||||||
|
closeFilePopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
ensureSeed(props.archiveId, props.data);
|
||||||
|
reset();
|
||||||
|
uni.$on('archive-detail:service-record-changed', reset);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
uni.$off('archive-detail:service-record-changed', reset);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.reachBottomTime,
|
||||||
|
() => getMore()
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.wrap {
|
||||||
|
padding: 12px 0 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #f5f6f8;
|
||||||
|
border-bottom: 1px solid #f2f2f2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.filter-pill {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 110px;
|
||||||
|
}
|
||||||
|
.filter-pill.wide {
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
.pill-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
max-width: 190px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.pill-text.muted {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.pill-icon {
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
background: #fff;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 12px 0 70px;
|
||||||
|
}
|
||||||
|
.cell {
|
||||||
|
padding: 0 14px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.head {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 44px;
|
||||||
|
padding-left: 18px;
|
||||||
|
}
|
||||||
|
.dot {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #4f6ef7;
|
||||||
|
}
|
||||||
|
.time {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f1f1f;
|
||||||
|
}
|
||||||
|
.file-link {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #4f6ef7;
|
||||||
|
}
|
||||||
|
.meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding-left: 18px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.tag {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #4f6ef7;
|
||||||
|
border: 1px solid #4f6ef7;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
.meta-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.truncate {
|
||||||
|
max-width: 160px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.body {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-left: 18px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 18px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.content.clamp {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.pen {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.line {
|
||||||
|
position: absolute;
|
||||||
|
left: 18px;
|
||||||
|
top: 34px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 2px;
|
||||||
|
background: #4f6ef7;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.empty {
|
||||||
|
padding: 120px 0;
|
||||||
|
text-align: center;
|
||||||
|
color: #9aa0a6;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab {
|
||||||
|
position: fixed;
|
||||||
|
right: 16px;
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
border-radius: 26px;
|
||||||
|
background: #4f6ef7;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 10px 18px rgba(79, 110, 247, 0.35);
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup {
|
||||||
|
background: #fff;
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.popup-title {
|
||||||
|
position: relative;
|
||||||
|
padding: 14px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.popup-title-text {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.popup-close {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.popup-body2 {
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 20px;
|
||||||
|
word-break: break-all;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.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>
|
||||||
24
pages.json
@ -60,6 +60,30 @@
|
|||||||
"navigationBarTitleText": "档案详情"
|
"navigationBarTitleText": "档案详情"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/case/archive-edit",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "档案编辑"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/case/visit-record-detail",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "健康档案"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/case/service-record-detail",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "服务记录"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/case/followup-detail",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "回访详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/work/work",
|
"path": "pages/work/work",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
@ -102,10 +102,26 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="content">
|
<view class="content">
|
||||||
<view class="empty">
|
<HealthProfileTab
|
||||||
<image class="empty-img" src="/static/empty.svg" mode="aspectFit" />
|
v-if="currentTab === 'visitRecord'"
|
||||||
<text class="empty-text">暂无记录</text>
|
:data="archive"
|
||||||
</view>
|
:archiveId="archiveId"
|
||||||
|
:floatingBottom="floatingBottom"
|
||||||
|
/>
|
||||||
|
<ServiceInfoTab
|
||||||
|
v-else-if="currentTab === 'serviceRecord'"
|
||||||
|
:data="archive"
|
||||||
|
:archiveId="archiveId"
|
||||||
|
:reachBottomTime="reachBottomTime"
|
||||||
|
:floatingBottom="floatingBottom"
|
||||||
|
/>
|
||||||
|
<FollowUpManageTab
|
||||||
|
v-else
|
||||||
|
:data="archive"
|
||||||
|
:archiveId="archiveId"
|
||||||
|
:reachBottomTime="reachBottomTime"
|
||||||
|
:floatingBottom="floatingBottom"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="showBindWechat" class="footer">
|
<view v-if="showBindWechat" class="footer">
|
||||||
@ -202,7 +218,12 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { onLoad } from '@dcloudio/uni-app';
|
import { onLoad, onReachBottom, onShow } from '@dcloudio/uni-app';
|
||||||
|
|
||||||
|
import HealthProfileTab from '@/components/archive-detail/health-profile-tab.vue';
|
||||||
|
import ServiceInfoTab from '@/components/archive-detail/service-info-tab.vue';
|
||||||
|
import FollowUpManageTab from '@/components/archive-detail/follow-up-manage-tab.vue';
|
||||||
|
import { ensureSeed } from '@/components/archive-detail/mock';
|
||||||
|
|
||||||
const STORAGE_KEY = 'ykt_case_archive_detail';
|
const STORAGE_KEY = 'ykt_case_archive_detail';
|
||||||
|
|
||||||
@ -213,6 +234,8 @@ const tabs = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const currentTab = ref('visitRecord');
|
const currentTab = ref('visitRecord');
|
||||||
|
const reachBottomTime = ref(0);
|
||||||
|
const archiveId = ref('');
|
||||||
|
|
||||||
const archive = ref({
|
const archive = ref({
|
||||||
name: '',
|
name: '',
|
||||||
@ -232,7 +255,9 @@ const archive = ref({
|
|||||||
groupOptions: ['高血压', '糖尿病', '高血脂']
|
groupOptions: ['高血压', '糖尿病', '高血脂']
|
||||||
});
|
});
|
||||||
|
|
||||||
onLoad(() => {
|
onLoad((options) => {
|
||||||
|
archiveId.value = options?.id ? String(options.id) : String(archive.value.medicalRecordNo || archive.value.outpatientNo || archive.value.inpatientNo || '');
|
||||||
|
|
||||||
const cached = uni.getStorageSync(STORAGE_KEY);
|
const cached = uni.getStorageSync(STORAGE_KEY);
|
||||||
if (cached && typeof cached === 'object') {
|
if (cached && typeof cached === 'object') {
|
||||||
archive.value = {
|
archive.value = {
|
||||||
@ -247,6 +272,27 @@ onLoad(() => {
|
|||||||
const mobiles = cached && Array.isArray(cached.mobiles) ? cached.mobiles : [];
|
const mobiles = cached && Array.isArray(cached.mobiles) ? cached.mobiles : [];
|
||||||
if (mobiles.length) archive.value.mobile = String(mobiles[0]);
|
if (mobiles.length) archive.value.mobile = String(mobiles[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!archiveId.value) {
|
||||||
|
archiveId.value = String(archive.value.medicalRecordNo || archive.value.outpatientNo || archive.value.inpatientNo || `mock_${Date.now()}`);
|
||||||
|
}
|
||||||
|
ensureSeed(archiveId.value, archive.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
const cached = uni.getStorageSync(STORAGE_KEY);
|
||||||
|
if (cached && typeof cached === 'object') {
|
||||||
|
archive.value = {
|
||||||
|
...archive.value,
|
||||||
|
...cached,
|
||||||
|
groups: Array.isArray(cached.groups) ? cached.groups : archive.value.groups,
|
||||||
|
groupOptions: Array.isArray(cached.groupOptions) ? cached.groupOptions : archive.value.groupOptions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onReachBottom(() => {
|
||||||
|
reachBottomTime.value = Date.now();
|
||||||
});
|
});
|
||||||
|
|
||||||
const sexOrAge = computed(() => {
|
const sexOrAge = computed(() => {
|
||||||
@ -273,6 +319,7 @@ const createText = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const showBindWechat = computed(() => Boolean(archive.value.createdByDoctor && !archive.value.hasBindWechat));
|
const showBindWechat = computed(() => Boolean(archive.value.createdByDoctor && !archive.value.hasBindWechat));
|
||||||
|
const floatingBottom = computed(() => (showBindWechat.value ? 90 : 16));
|
||||||
|
|
||||||
// const contactTitle = computed(() => (archive.value.mobile ? '联系方式' : '添加联系电话'));
|
// const contactTitle = computed(() => (archive.value.mobile ? '联系方式' : '添加联系电话'));
|
||||||
// const notesTitle = computed(() => (archive.value.notes ? '备注' : '添加备注'));
|
// const notesTitle = computed(() => (archive.value.notes ? '备注' : '添加备注'));
|
||||||
@ -283,7 +330,7 @@ const saveToStorage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const goEdit = () => {
|
const goEdit = () => {
|
||||||
uni.showToast({ title: '档案编辑待接入', icon: 'none' });
|
uni.navigateTo({ url: `/pages/case/archive-edit?archiveId=${encodeURIComponent(archiveId.value || '')}` });
|
||||||
};
|
};
|
||||||
|
|
||||||
const bindWechat = () => {
|
const bindWechat = () => {
|
||||||
@ -650,7 +697,7 @@ const saveAddGroup = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding: 28px 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
|
|||||||
68
pages/case/archive-edit.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<CustomerProfileTab :data="archive" :floatingBottom="16" @save="savePatch" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||||
|
import CustomerProfileTab from '@/components/archive-detail/customer-profile-tab.vue';
|
||||||
|
import { ensureSeed } from '@/components/archive-detail/mock';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'ykt_case_archive_detail';
|
||||||
|
|
||||||
|
const archiveId = ref('');
|
||||||
|
const archive = ref({
|
||||||
|
name: '',
|
||||||
|
sex: '',
|
||||||
|
age: '',
|
||||||
|
avatar: '',
|
||||||
|
mobile: '',
|
||||||
|
outpatientNo: '',
|
||||||
|
inpatientNo: '',
|
||||||
|
medicalRecordNo: '',
|
||||||
|
createTime: '',
|
||||||
|
creator: '',
|
||||||
|
createdByDoctor: true,
|
||||||
|
hasBindWechat: false,
|
||||||
|
notes: '',
|
||||||
|
groups: [],
|
||||||
|
groupOptions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadFromStorage() {
|
||||||
|
const cached = uni.getStorageSync(STORAGE_KEY);
|
||||||
|
if (cached && typeof cached === 'object') {
|
||||||
|
archive.value = {
|
||||||
|
...archive.value,
|
||||||
|
...cached,
|
||||||
|
groups: Array.isArray(cached.groups) ? cached.groups : archive.value.groups,
|
||||||
|
groupOptions: Array.isArray(cached.groupOptions) ? cached.groupOptions : archive.value.groupOptions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!archiveId.value) {
|
||||||
|
archiveId.value = String(archive.value.medicalRecordNo || archive.value.outpatientNo || archive.value.inpatientNo || `mock_${Date.now()}`);
|
||||||
|
}
|
||||||
|
ensureSeed(archiveId.value, archive.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function savePatch(patch) {
|
||||||
|
archive.value = { ...archive.value, ...(patch && typeof patch === 'object' ? patch : {}) };
|
||||||
|
uni.setStorageSync(STORAGE_KEY, { ...archive.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad((options) => {
|
||||||
|
archiveId.value = options?.archiveId ? String(options.archiveId) : '';
|
||||||
|
loadFromStorage();
|
||||||
|
});
|
||||||
|
|
||||||
|
onShow(() => loadFromStorage());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f6f8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
430
pages/case/followup-detail.vue
Normal file
@ -0,0 +1,430 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/followup-detail/followup-detail.vue(简化移植:去除微信会话/员工选择/接口) -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="title-row">
|
||||||
|
<view class="title">{{ currentType.label }}</view>
|
||||||
|
<view class="tag" :class="`st-${currentStatus.value}`">{{ currentStatus.label }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="desc">{{ form.taskContent || '暂无任务内容' }}</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<view class="info">计划执行时间:{{ planDate || '--' }}</view>
|
||||||
|
<view class="info">执行人:{{ form.executorName || '--' }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card">
|
||||||
|
<view class="section-title">回访方式</view>
|
||||||
|
<view class="method-row" @click="toggleMethod('phone')">
|
||||||
|
<image class="radio" :src="`/static/circle${todoMethod === 'phone' ? 'd' : ''}.svg`" />
|
||||||
|
<view class="method-label">电话</view>
|
||||||
|
<view class="method-input" @click.stop="pickPhone">
|
||||||
|
<view class="method-value" :class="{ muted: !phone }">{{ phone || '选择号码' }}</view>
|
||||||
|
<uni-icons type="arrowright" size="16" color="#999" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="method-row" @click="toggleMethod('wechat')">
|
||||||
|
<image class="radio" :src="`/static/circle${todoMethod === 'wechat' ? 'd' : ''}.svg`" />
|
||||||
|
<view class="method-label">微信</view>
|
||||||
|
<view class="method-note">(mock:不接入会话)</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card">
|
||||||
|
<view class="section-title">回访结果</view>
|
||||||
|
<view class="textarea-box">
|
||||||
|
<textarea v-model="form.result" class="textarea tall" placeholder="请填写回访结果" maxlength="500" />
|
||||||
|
<view class="counter">{{ (form.result || '').length }}/500</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="footer">
|
||||||
|
<button class="btn plain" @click="cancel">返回</button>
|
||||||
|
<button v-if="canEdit" class="btn plain danger" @click="cancelTask">取消任务</button>
|
||||||
|
<button v-if="canEdit" class="btn primary" @click="markDone">设为完成</button>
|
||||||
|
<button v-else class="btn primary" @click="save">保存</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="taskId" class="delete-fab" @click="remove">
|
||||||
|
<uni-icons type="trash" size="22" color="#ff4d4f" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, reactive, ref } from 'vue';
|
||||||
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { ensureSeed, getFollowup, removeFollowup, upsertFollowup } from '@/components/archive-detail/mock';
|
||||||
|
|
||||||
|
const archiveId = ref('');
|
||||||
|
const mode = ref('add');
|
||||||
|
const taskId = ref('');
|
||||||
|
|
||||||
|
const typeOptions = [
|
||||||
|
{ label: '回访', value: 'followup' },
|
||||||
|
{ label: '复诊提醒', value: 'revisit' },
|
||||||
|
{ label: '问卷', value: 'questionnaire' },
|
||||||
|
{ label: '其他', value: 'other' },
|
||||||
|
];
|
||||||
|
const currentType = ref(typeOptions[0]);
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: '待处理', value: 'processing' },
|
||||||
|
{ label: '未开始', value: 'notStart' },
|
||||||
|
{ label: '已完成', value: 'treated' },
|
||||||
|
{ label: '已取消', value: 'cancelled' },
|
||||||
|
{ label: '已过期', value: 'expired' },
|
||||||
|
];
|
||||||
|
const currentStatus = ref(statusOptions[0]);
|
||||||
|
|
||||||
|
const teamOptions = [
|
||||||
|
{ label: '口腔一科(示例)', value: 'team_1' },
|
||||||
|
{ label: '正畸团队(示例)', value: 'team_2' },
|
||||||
|
];
|
||||||
|
const currentTeam = ref(teamOptions[0]);
|
||||||
|
|
||||||
|
const planDate = ref('');
|
||||||
|
const form = reactive({
|
||||||
|
executorName: '',
|
||||||
|
taskContent: '',
|
||||||
|
result: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const todoMethod = ref('phone'); // phone | wechat
|
||||||
|
const phone = ref('');
|
||||||
|
const mobiles = computed(() => {
|
||||||
|
const m = String(form.executorName || '').trim();
|
||||||
|
// mock:用本地缓存里的手机号 + 默认号码
|
||||||
|
const arr = [];
|
||||||
|
const cached = uni.getStorageSync('ykt_case_archive_detail');
|
||||||
|
if (cached && typeof cached === 'object' && cached.mobile) arr.push(String(cached.mobile));
|
||||||
|
if (!arr.includes('13800000000')) arr.push('13800000000');
|
||||||
|
return arr;
|
||||||
|
});
|
||||||
|
|
||||||
|
const canEdit = computed(() => ['processing', 'notStart'].includes(currentStatus.value.value));
|
||||||
|
|
||||||
|
onLoad((options) => {
|
||||||
|
archiveId.value = options?.archiveId ? String(options.archiveId) : '';
|
||||||
|
mode.value = options?.mode ? String(options.mode) : 'add';
|
||||||
|
taskId.value = options?.id ? String(options.id) : '';
|
||||||
|
|
||||||
|
ensureSeed(archiveId.value, {});
|
||||||
|
|
||||||
|
if (taskId.value) {
|
||||||
|
const task = getFollowup({ archiveId: archiveId.value, id: taskId.value });
|
||||||
|
if (task) {
|
||||||
|
currentType.value = typeOptions.find((i) => i.value === task.eventType) || typeOptions[0];
|
||||||
|
currentStatus.value = statusOptions.find((i) => i.value === task.status) || statusOptions[0];
|
||||||
|
currentTeam.value = teamOptions.find((i) => i.value === task.executeTeamId) || teamOptions[0];
|
||||||
|
planDate.value = task.planDate || '';
|
||||||
|
form.executorName = task.executorName || '';
|
||||||
|
form.taskContent = task.taskContent || '';
|
||||||
|
form.result = task.result || '';
|
||||||
|
if (typeof task.todoMethod === 'string') {
|
||||||
|
if (task.todoMethod.startsWith('phone')) {
|
||||||
|
todoMethod.value = 'phone';
|
||||||
|
const parts = task.todoMethod.split(':');
|
||||||
|
phone.value = parts[1] || '';
|
||||||
|
} else if (task.todoMethod === 'wechat') {
|
||||||
|
todoMethod.value = 'wechat';
|
||||||
|
phone.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
planDate.value = dayjs().format('YYYY-MM-DD');
|
||||||
|
});
|
||||||
|
|
||||||
|
function pickType(e) {
|
||||||
|
currentType.value = typeOptions[e.detail.value] || typeOptions[0];
|
||||||
|
}
|
||||||
|
function pickStatus(e) {
|
||||||
|
currentStatus.value = statusOptions[e.detail.value] || statusOptions[0];
|
||||||
|
}
|
||||||
|
function pickTeam(e) {
|
||||||
|
currentTeam.value = teamOptions[e.detail.value] || teamOptions[0];
|
||||||
|
}
|
||||||
|
function pickPlanDate(e) {
|
||||||
|
planDate.value = e.detail.value || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
uni.navigateBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
if (!archiveId.value) {
|
||||||
|
uni.showToast({ title: '缺少 archiveId', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const plannedExecutionTime = planDate.value ? dayjs(planDate.value).valueOf() : Date.now();
|
||||||
|
upsertFollowup({
|
||||||
|
archiveId: archiveId.value,
|
||||||
|
followup: {
|
||||||
|
_id: taskId.value || '',
|
||||||
|
plannedExecutionTime,
|
||||||
|
status: currentStatus.value.value,
|
||||||
|
eventStatusLabel: currentStatus.value.label,
|
||||||
|
eventType: currentType.value.value,
|
||||||
|
eventTypeLabel: currentType.value.label,
|
||||||
|
executeTeamId: currentTeam.value.value,
|
||||||
|
executeTeamName: currentTeam.value.label,
|
||||||
|
executorName: form.executorName || '李医生',
|
||||||
|
creatorName: '管理员A',
|
||||||
|
taskContent: form.taskContent,
|
||||||
|
result: form.result,
|
||||||
|
todoMethod: todoMethod.value === 'phone' ? (phone.value ? `phone:${phone.value}` : 'phone') : 'wechat',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
uni.$emit('archive-detail:followup-changed');
|
||||||
|
uni.showToast({ title: '保存成功', icon: 'success' });
|
||||||
|
setTimeout(() => uni.navigateBack(), 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMethod(v) {
|
||||||
|
if (!canEdit.value) return;
|
||||||
|
todoMethod.value = v;
|
||||||
|
if (v !== 'phone') phone.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickPhone() {
|
||||||
|
if (!canEdit.value) return;
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: mobiles.value,
|
||||||
|
success: ({ tapIndex }) => {
|
||||||
|
phone.value = mobiles.value[tapIndex] || '';
|
||||||
|
todoMethod.value = 'phone';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function markDone() {
|
||||||
|
if (!canEdit.value) return;
|
||||||
|
if (!['phone', 'wechat'].includes(todoMethod.value)) return uni.showToast({ title: '请选择回访方式', icon: 'none' });
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定完成该回访任务吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (!res.confirm) return;
|
||||||
|
currentStatus.value = statusOptions.find((i) => i.value === 'treated') || currentStatus.value;
|
||||||
|
save();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelTask() {
|
||||||
|
if (!canEdit.value) return;
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定取消该回访任务吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (!res.confirm) return;
|
||||||
|
currentStatus.value = statusOptions.find((i) => i.value === 'cancelled') || currentStatus.value;
|
||||||
|
save();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定删除当前记录?',
|
||||||
|
success: (res) => {
|
||||||
|
if (!res.confirm) return;
|
||||||
|
removeFollowup({ archiveId: archiveId.value, id: taskId.value });
|
||||||
|
uni.$emit('archive-detail:followup-changed');
|
||||||
|
uni.showToast({ title: '已删除', icon: 'success' });
|
||||||
|
setTimeout(() => uni.navigateBack(), 300);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f6f8;
|
||||||
|
padding-bottom: calc(76px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
margin: 10px 14px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 14px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.tag {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.st-processing {
|
||||||
|
background: #ffe5e5;
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
.st-notStart {
|
||||||
|
background: #dbe6ff;
|
||||||
|
color: #4f6ef7;
|
||||||
|
}
|
||||||
|
.st-treated {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
.st-cancelled,
|
||||||
|
.st-expired {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
.info-row {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.method-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
.radio {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.method-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.method-note {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.method-input {
|
||||||
|
margin-left: auto;
|
||||||
|
width: 220px;
|
||||||
|
height: 36px;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.method-value {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
max-width: 180px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.method-value.muted {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-box {
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 140px;
|
||||||
|
font-size: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.textarea.tall {
|
||||||
|
height: 140px;
|
||||||
|
}
|
||||||
|
.counter {
|
||||||
|
margin-top: 6px;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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.plain.danger {
|
||||||
|
color: #ff4d4f;
|
||||||
|
border-color: #ff4d4f;
|
||||||
|
}
|
||||||
|
.btn.primary {
|
||||||
|
background: #4f6ef7;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-fab {
|
||||||
|
position: fixed;
|
||||||
|
right: 16px;
|
||||||
|
bottom: calc(96px + env(safe-area-inset-bottom));
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
border-radius: 26px;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 10px 18px rgba(0, 0, 0, 0.12);
|
||||||
|
z-index: 30;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
314
pages/case/service-record-detail.vue
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/service-record-detail/service-record-detail.vue -->
|
||||||
|
<view class="body">
|
||||||
|
<scroll-view scroll-y class="scroll">
|
||||||
|
<view class="card">
|
||||||
|
<view class="section-title">执行日期</view>
|
||||||
|
<picker mode="date" @change="pickDate">
|
||||||
|
<view class="picker-box">
|
||||||
|
<uni-icons type="calendar" size="18" color="#666" class="mr" />
|
||||||
|
<view class="picker-text" :class="{ muted: !date }">{{ date || '请选择服务日期' }}</view>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
|
||||||
|
<view class="section-title">执行时间</view>
|
||||||
|
<picker mode="time" @change="pickTime">
|
||||||
|
<view class="picker-box">
|
||||||
|
<uni-icons type="calendar" size="18" color="#666" class="mr" />
|
||||||
|
<view class="picker-text" :class="{ muted: !time }">{{ time || '请选择执行时间' }}</view>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
|
||||||
|
<view class="section-title">服务类型</view>
|
||||||
|
<picker mode="selector" :range="typeOptions" range-key="label" @change="pickType" :disabled="Boolean(recordId)">
|
||||||
|
<view class="picker-box between">
|
||||||
|
<view class="picker-text" :class="{ muted: !currentType.value }">
|
||||||
|
{{ currentType.value ? currentType.label : '请选择服务类型' }}
|
||||||
|
</view>
|
||||||
|
<uni-icons type="arrowright" size="16" color="#999" />
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
|
||||||
|
<view class="section-title">所属团队</view>
|
||||||
|
<picker mode="selector" :range="teamOptions" range-key="label" @change="pickTeam" :disabled="Boolean(recordId)">
|
||||||
|
<view class="picker-box between">
|
||||||
|
<view class="picker-text" :class="{ muted: !currentTeam.value }">
|
||||||
|
{{ currentTeam.value ? currentTeam.label : '请选择所属团队' }}
|
||||||
|
</view>
|
||||||
|
<uni-icons type="arrowright" size="16" color="#999" />
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
|
||||||
|
<view class="section-title">服务内容</view>
|
||||||
|
<view class="textarea-box">
|
||||||
|
<textarea v-model="form.taskContent" class="textarea tall" maxlength="1000" placeholder="请输入服务小结内容" />
|
||||||
|
<view class="counter">{{ (form.taskContent || '').length }}/1000</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section-title">处理结果</view>
|
||||||
|
<view class="textarea-box">
|
||||||
|
<textarea v-model="form.result" class="textarea" maxlength="500" placeholder="请输入处理结果(可选)" />
|
||||||
|
<view class="counter">{{ (form.result || '').length }}/500</view>
|
||||||
|
</view>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<view v-if="recordId" class="delete-fab" @click="remove">
|
||||||
|
<uni-icons type="trash" size="22" color="#ff4d4f" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { ensureSeed, getServiceRecord, removeServiceRecord, upsertServiceRecord } from '@/components/archive-detail/mock';
|
||||||
|
|
||||||
|
const archiveId = ref('');
|
||||||
|
const mode = ref('add');
|
||||||
|
const recordId = ref('');
|
||||||
|
|
||||||
|
const typeOptions = [
|
||||||
|
{ label: '电话回访', value: 'phone' },
|
||||||
|
{ label: '短信提醒', value: 'sms' },
|
||||||
|
{ label: '问卷', value: 'questionnaire' },
|
||||||
|
{ label: '文章', value: 'article' },
|
||||||
|
];
|
||||||
|
const currentType = ref(typeOptions[0]);
|
||||||
|
|
||||||
|
const teamOptions = [
|
||||||
|
{ label: '口腔一科(示例)', value: 'team_1' },
|
||||||
|
{ label: '正畸团队(示例)', value: 'team_2' },
|
||||||
|
];
|
||||||
|
const currentTeam = ref(teamOptions[0]);
|
||||||
|
|
||||||
|
const date = ref('');
|
||||||
|
const time = ref('');
|
||||||
|
const form = reactive({
|
||||||
|
executorName: '',
|
||||||
|
taskContent: '',
|
||||||
|
result: '',
|
||||||
|
articleUrl: '',
|
||||||
|
surveryId: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
onLoad((options) => {
|
||||||
|
archiveId.value = options?.archiveId ? String(options.archiveId) : '';
|
||||||
|
recordId.value = options?.id ? String(options.id) : '';
|
||||||
|
|
||||||
|
ensureSeed(archiveId.value, {});
|
||||||
|
|
||||||
|
if (recordId.value) {
|
||||||
|
const record = getServiceRecord({ archiveId: archiveId.value, id: recordId.value });
|
||||||
|
if (record) {
|
||||||
|
currentType.value = typeOptions.find((i) => i.value === record.eventType) || typeOptions[0];
|
||||||
|
currentTeam.value = teamOptions.find((i) => i.value === record.executeTeamId) || teamOptions[0];
|
||||||
|
date.value = record.executionTime ? dayjs(record.executionTime).format('YYYY-MM-DD') : '';
|
||||||
|
time.value = record.executionTime ? dayjs(record.executionTime).format('HH:mm') : '';
|
||||||
|
form.executorName = record.executorName || '';
|
||||||
|
form.taskContent = record.taskContent || '';
|
||||||
|
form.result = record.result || '';
|
||||||
|
form.articleUrl = record.pannedEventSendFile?.type === 'article' ? record.pannedEventSendFile.url || '' : '';
|
||||||
|
form.surveryId = record.pannedEventSendFile?.type === 'questionnaire' ? record.pannedEventSendFile.surveryId || '' : '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
date.value = dayjs().format('YYYY-MM-DD');
|
||||||
|
});
|
||||||
|
|
||||||
|
function pickType(e) {
|
||||||
|
currentType.value = typeOptions[e.detail.value] || typeOptions[0];
|
||||||
|
}
|
||||||
|
function pickTeam(e) {
|
||||||
|
currentTeam.value = teamOptions[e.detail.value] || teamOptions[0];
|
||||||
|
}
|
||||||
|
function pickDate(e) {
|
||||||
|
date.value = e.detail.value || '';
|
||||||
|
}
|
||||||
|
function pickTime(e) {
|
||||||
|
time.value = e.detail.value || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
uni.navigateBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
if (!archiveId.value) {
|
||||||
|
uni.showToast({ title: '缺少 archiveId', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!date.value) return uni.showToast({ title: '请选择服务日期', icon: 'none' });
|
||||||
|
if (!time.value) return uni.showToast({ title: '请选择执行时间', icon: 'none' });
|
||||||
|
if (!currentType.value?.value) return uni.showToast({ title: '请选择服务类型', icon: 'none' });
|
||||||
|
if (!currentTeam.value?.value) return uni.showToast({ title: '请选择所属团队', icon: 'none' });
|
||||||
|
if (!String(form.taskContent || '').trim()) return uni.showToast({ title: '请输入服务内容', icon: 'none' });
|
||||||
|
|
||||||
|
const executionTime = dayjs(`${date.value} ${time.value}`).isValid() ? dayjs(`${date.value} ${time.value}`).valueOf() : Date.now();
|
||||||
|
const pannedEventSendFile =
|
||||||
|
currentType.value.value === 'article'
|
||||||
|
? (form.articleUrl ? { type: 'article', url: form.articleUrl } : null)
|
||||||
|
: currentType.value.value === 'questionnaire'
|
||||||
|
? (form.surveryId ? { type: 'questionnaire', surveryId: form.surveryId } : null)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
upsertServiceRecord({
|
||||||
|
archiveId: archiveId.value,
|
||||||
|
record: {
|
||||||
|
_id: recordId.value || '',
|
||||||
|
eventType: currentType.value.value,
|
||||||
|
executionTime,
|
||||||
|
executeTeamId: currentTeam.value.value,
|
||||||
|
executeTeamName: currentTeam.value.label,
|
||||||
|
executorName: form.executorName,
|
||||||
|
taskContent: form.taskContent,
|
||||||
|
result: form.result,
|
||||||
|
pannedEventSendFile,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
uni.$emit('archive-detail:service-record-changed');
|
||||||
|
uni.showToast({ title: '保存成功', icon: 'success' });
|
||||||
|
setTimeout(() => uni.navigateBack(), 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定删除当前记录?',
|
||||||
|
success: (res) => {
|
||||||
|
if (!res.confirm) return;
|
||||||
|
removeServiceRecord({ archiveId: archiveId.value, id: recordId.value });
|
||||||
|
uni.$emit('archive-detail:service-record-changed');
|
||||||
|
uni.showToast({ title: '已删除', icon: 'success' });
|
||||||
|
setTimeout(() => uni.navigateBack(), 300);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f6f8;
|
||||||
|
padding-bottom: calc(76px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
.body {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.scroll {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
padding: 14px;
|
||||||
|
margin: 10px 14px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 12px 0 10px;
|
||||||
|
}
|
||||||
|
.section-title:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.picker-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 12px;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.picker-box.between {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.mr {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.picker-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.picker-text.muted {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.textarea-box {
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 90px;
|
||||||
|
font-size: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.textarea.tall {
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
.counter {
|
||||||
|
margin-top: 6px;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-fab {
|
||||||
|
position: fixed;
|
||||||
|
right: 16px;
|
||||||
|
bottom: calc(96px + env(safe-area-inset-bottom));
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
border-radius: 26px;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 10px 18px rgba(0, 0, 0, 0.12);
|
||||||
|
z-index: 30;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
215
pages/case/visit-record-detail.vue
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/visit-record-detail/visit-record-detail.vue -->
|
||||||
|
<view class="body">
|
||||||
|
<scroll-view scroll-y class="scroll">
|
||||||
|
<view class="header">
|
||||||
|
<view class="header-title">{{ template?.templateName || '健康档案' }}</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-wrap">
|
||||||
|
<FormTemplate v-if="template" ref="formRef" :items="showItems" :form="forms" @change="onChange" />
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<view v-if="recordId" class="delete-fab" @click="remove">
|
||||||
|
<uni-icons type="trash" size="22" color="#ff4d4f" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, reactive, ref } from 'vue';
|
||||||
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import FormTemplate from '@/components/form-template/index.vue';
|
||||||
|
import { ensureSeed, getVisitRecord, getVisitRecordTemplate, removeVisitRecord, upsertVisitRecord } from '@/components/archive-detail/mock';
|
||||||
|
|
||||||
|
const archiveId = ref('');
|
||||||
|
const recordId = ref('');
|
||||||
|
const templateType = ref('');
|
||||||
|
|
||||||
|
const template = computed(() => getVisitRecordTemplate(templateType.value));
|
||||||
|
const detail = ref({});
|
||||||
|
const form = reactive({});
|
||||||
|
const forms = computed(() => ({ ...detail.value, ...form }));
|
||||||
|
const showItems = computed(() => {
|
||||||
|
const list = template.value?.templateList || [];
|
||||||
|
// referenceField 兼容(与 mobile 一致)
|
||||||
|
return list.filter((i) => {
|
||||||
|
if (i && typeof i.referenceField === 'string') {
|
||||||
|
return forms.value[i.referenceField] === i.referenceValue;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRef = ref(null);
|
||||||
|
|
||||||
|
onLoad((options) => {
|
||||||
|
archiveId.value = options?.archiveId ? String(options.archiveId) : '';
|
||||||
|
recordId.value = options?.id ? String(options.id) : '';
|
||||||
|
templateType.value = options?.type ? String(options.type) : '';
|
||||||
|
|
||||||
|
ensureSeed(archiveId.value, { name: options?.name ? String(options.name) : '' });
|
||||||
|
|
||||||
|
if (recordId.value) {
|
||||||
|
const record = getVisitRecord({ archiveId: archiveId.value, id: recordId.value });
|
||||||
|
if (record) {
|
||||||
|
templateType.value = record.templateType || record.medicalType || templateType.value;
|
||||||
|
detail.value = record;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!templateType.value) templateType.value = 'outpatient';
|
||||||
|
});
|
||||||
|
|
||||||
|
function onChange({ title, value }) {
|
||||||
|
form[title] = value;
|
||||||
|
const item = showItems.value.find((i) => i.title === title);
|
||||||
|
if (!item) return;
|
||||||
|
// 关联字段变化时清理被关联字段(与 mobile 行为一致)
|
||||||
|
const relat = (template.value?.templateList || []).filter((i) => i.referenceField === title);
|
||||||
|
relat.forEach((i) => (form[i.title] = ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
uni.navigateBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
if (!archiveId.value) {
|
||||||
|
uni.showToast({ title: '缺少 archiveId', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (formRef.value?.verify && !formRef.value.verify()) return;
|
||||||
|
|
||||||
|
// sortTime:尽量用模板中的日期字段
|
||||||
|
const timeTitle =
|
||||||
|
templateType.value === 'outpatient'
|
||||||
|
? 'visitTime'
|
||||||
|
: templateType.value === 'inhospital'
|
||||||
|
? 'inhospitalDate'
|
||||||
|
: templateType.value === 'physicalExaminationTemplate'
|
||||||
|
? 'inspectDate'
|
||||||
|
: '';
|
||||||
|
const timeValue = timeTitle ? forms.value[timeTitle] : '';
|
||||||
|
const sortTime = timeValue && dayjs(timeValue).isValid() ? dayjs(timeValue).valueOf() : Date.now();
|
||||||
|
|
||||||
|
upsertVisitRecord({
|
||||||
|
archiveId: archiveId.value,
|
||||||
|
record: {
|
||||||
|
_id: recordId.value || '',
|
||||||
|
medicalType: templateType.value,
|
||||||
|
templateType: templateType.value,
|
||||||
|
tempName: template.value?.templateName || '健康档案',
|
||||||
|
sortTime,
|
||||||
|
...form,
|
||||||
|
createTime: detail.value?.createTime || Date.now(),
|
||||||
|
creatorName: detail.value?.creatorName || '我',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
uni.$emit('archive-detail:visit-record-changed');
|
||||||
|
uni.showToast({ title: '保存成功', icon: 'success' });
|
||||||
|
setTimeout(() => uni.navigateBack(), 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定删除当前记录?',
|
||||||
|
success: (res) => {
|
||||||
|
if (!res.confirm) return;
|
||||||
|
removeVisitRecord({ archiveId: archiveId.value, id: recordId.value });
|
||||||
|
uni.$emit('archive-detail:visit-record-changed');
|
||||||
|
uni.showToast({ title: '已删除', icon: 'success' });
|
||||||
|
setTimeout(() => uni.navigateBack(), 300);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f6f8;
|
||||||
|
padding-bottom: calc(76px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
.body {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.scroll {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
.header-title {
|
||||||
|
padding: 14px 14px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.form-wrap {
|
||||||
|
background: #fff;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-fab {
|
||||||
|
position: fixed;
|
||||||
|
right: 16px;
|
||||||
|
bottom: calc(96px + env(safe-area-inset-bottom));
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
border-radius: 26px;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 10px 18px rgba(0, 0, 0, 0.12);
|
||||||
|
z-index: 30;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1
static/checkbox-checked.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1758103034282" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4491" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M816 96a128 128 0 0 1 128 128v592a128 128 0 0 1-128 128H224a128 128 0 0 1-128-128V224a128 128 0 0 1 128-128h592z m-69.632 182.464l-333.152 333.12-146.24-146.24-66.624 66.624 212.896 212.896 399.744-399.776-66.624-66.624z" fill="#0074ff" p-id="4492"></path></svg>
|
||||||
|
After Width: | Height: | Size: 595 B |
1
static/checkbox.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1758103134600" class="icon" viewBox="0 0 1097 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7841" xmlns:xlink="http://www.w3.org/1999/xlink" width="214.2578125" height="200"><path d="M865.426286 0h-614.4c-112.64 0-204.8 92.16-204.8 204.8v614.4c0 112.64 92.16 204.8 204.8 204.8h614.4c112.64 0 204.8-92.16 204.8-204.8V204.8c0-112.64-92.16-204.8-204.8-204.8m0 54.857143c82.651429 0 149.942857 67.291429 149.942857 149.942857v614.4c0 82.651429-67.291429 149.942857-149.942857 149.942857h-614.4c-82.651429 0-149.942857-67.291429-149.942857-149.942857V204.8c0-82.651429 67.291429-149.942857 149.942857-149.942857h614.4" fill="#8a8a8a" p-id="7842"></path></svg>
|
||||||
|
After Width: | Height: | Size: 812 B |
1
static/circle.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1758619545835" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4908" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M1024 512C1024 229.234759 794.765241 0 512 0S0 229.234759 0 512s229.234759 512 512 512 512-229.234759 512-512zM70.62069 512C70.62069 268.235034 268.235034 70.62069 512 70.62069s441.37931 197.614345 441.37931 441.37931-197.614345 441.37931-441.37931 441.37931S70.62069 755.764966 70.62069 512z" fill="#707070" p-id="4909"></path></svg>
|
||||||
|
After Width: | Height: | Size: 667 B |
1
static/circled.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1758619535183" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4703" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M204.8 512a307.2 307.2 0 1 0 307.2-307.2 307.2 307.2 0 0 0-307.2 307.2z m-64 0A371.2 371.2 0 1 0 512 140.8 371.2 371.2 0 0 0 140.8 512zM76.8 512a435.2 435.2 0 1 1 435.2 435.2A435.2 435.2 0 0 1 76.8 512z" fill="#0074ff" p-id="4704"></path></svg>
|
||||||
|
After Width: | Height: | Size: 577 B |
12
static/icons/icon-filter-active.svg
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg t="1716882323134" class="icon" viewBox="0 0 1031 1024" version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" p-id="2608" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
width="201.3671875" height="200">
|
||||||
|
<path
|
||||||
|
d="M544.24 870.052a24.08 24.08 0 0 1-24.053-24.054V434.555c0-5.755 2.063-11.32 5.816-15.678l182.513-212.265H213.545l167.578 213.074a24.151 24.151 0 0 1 5.145 14.869v245.637l72.458 72.453a23.849 23.849 0 0 1 6.887 17.024 23.916 23.916 0 0 1-7.173 16.958c-4.532 4.464-10.527 6.922-16.881 6.922s-12.355-2.453-16.88-6.922l-79.463-79.468a23.92 23.92 0 0 1-7.045-17.014V442.88L145.126 197.422a24.08 24.08 0 0 1 4.025-33.777 24.151 24.151 0 0 1 14.868-5.145h596.885a23.884 23.884 0 0 1 17.003 7.05 23.875 23.875 0 0 1 7.04 17.014 24.054 24.054 0 0 1-5.811 15.672L568.284 443.47v402.524a24.074 24.074 0 0 1-24.043 24.059z"
|
||||||
|
fill="#007aff" p-id="2609"></path>
|
||||||
|
<path
|
||||||
|
d="M660.951 805.99a23.885 23.885 0 0 1-16.998-7.055 23.905 23.905 0 0 1-7.03-17.019 24.084 24.084 0 0 1 24.033-24.033h186.117a24.084 24.084 0 0 1 24.028 24.074 24.084 24.084 0 0 1-24.033 24.033h0.005-186.122z m0-150.553a24.074 24.074 0 0 1-24.028-24.07 24.09 24.09 0 0 1 24.033-24.038h186.117a24.084 24.084 0 0 1 24.028 24.075 24.09 24.09 0 0 1-24.033 24.033h0.005-186.122z m0-148.823c-13.184-0.17-23.839-11.095-23.67-24.356a23.9 23.9 0 0 1 23.747-23.747h186.045c13.261 0 24.049 10.788 24.049 24.054s-10.788 24.054-24.049 24.054H660.951z"
|
||||||
|
fill="#007aff" p-id="2610"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
12
static/icons/icon-filter.svg
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg t="1716882323134" class="icon" viewBox="0 0 1031 1024" version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" p-id="2608" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
width="201.3671875" height="200">
|
||||||
|
<path
|
||||||
|
d="M544.24 870.052a24.08 24.08 0 0 1-24.053-24.054V434.555c0-5.755 2.063-11.32 5.816-15.678l182.513-212.265H213.545l167.578 213.074a24.151 24.151 0 0 1 5.145 14.869v245.637l72.458 72.453a23.849 23.849 0 0 1 6.887 17.024 23.916 23.916 0 0 1-7.173 16.958c-4.532 4.464-10.527 6.922-16.881 6.922s-12.355-2.453-16.88-6.922l-79.463-79.468a23.92 23.92 0 0 1-7.045-17.014V442.88L145.126 197.422a24.08 24.08 0 0 1 4.025-33.777 24.151 24.151 0 0 1 14.868-5.145h596.885a23.884 23.884 0 0 1 17.003 7.05 23.875 23.875 0 0 1 7.04 17.014 24.054 24.054 0 0 1-5.811 15.672L568.284 443.47v402.524a24.074 24.074 0 0 1-24.043 24.059z"
|
||||||
|
fill="#2C2C2C" p-id="2609"></path>
|
||||||
|
<path
|
||||||
|
d="M660.951 805.99a23.885 23.885 0 0 1-16.998-7.055 23.905 23.905 0 0 1-7.03-17.019 24.084 24.084 0 0 1 24.033-24.033h186.117a24.084 24.084 0 0 1 24.028 24.074 24.084 24.084 0 0 1-24.033 24.033h0.005-186.122z m0-150.553a24.074 24.074 0 0 1-24.028-24.07 24.09 24.09 0 0 1 24.033-24.038h186.117a24.084 24.084 0 0 1 24.028 24.075 24.09 24.09 0 0 1-24.033 24.033h0.005-186.122z m0-148.823c-13.184-0.17-23.839-11.095-23.67-24.356a23.9 23.9 0 0 1 23.747-23.747h186.045c13.261 0 24.049 10.788 24.049 24.054s-10.788 24.054-24.049 24.054H660.951z"
|
||||||
|
fill="#2C2C2C" p-id="2610"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
1
static/icons/icon-pen.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1705374788985" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4258" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M471.14752 747.8784l336.2048-441.80992c16.4352-21.59616 12.19072-52.50048-9.4464-68.89984l-120.90368-91.58144c-21.64224-16.39424-52.62336-12.16512-69.04832 9.43104l-336.22528 441.78944 199.41888 151.07072z m-271.94368-169.28256c0.82944-1.52064 1.78176-2.9952 2.86208-4.40832l350.75584-460.93312c39.51104-51.92192 114.00704-62.09536 166.03648-22.67136l120.91392 91.58144c52.03968 39.424 62.2336 113.7408 22.71744 165.65248l-352.31232 462.97088c-0.82432 1.0752-1.69984 2.0992-2.62144 3.06176a34.62144 34.62144 0 0 1-16.49664 14.30016l-305.7408 126.66368a34.68288 34.68288 0 0 1-34.69824-4.77184 34.48832 34.48832 0 0 1-12.74368-32.55808l51.2-319.49312a34.42688 34.42688 0 0 1 10.12736-19.39456z m49.09568 87.168l-32.29696 201.55392L408.8832 787.4048l-160.58368-121.64096z" fill="#007aff" p-id="4259"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -1,7 +1,7 @@
|
|||||||
// 引入腾讯IM SDK
|
// 引入腾讯IM SDK
|
||||||
import TIM from 'tim-wx-sdk'
|
import TIM from 'tim-wx-sdk'
|
||||||
import TIMUploadPlugin from 'tim-upload-plugin'
|
import TIMUploadPlugin from 'tim-upload-plugin'
|
||||||
import { getUserSig, sendSystemMessage, getChatRecordsByGroupId } from '../api/corp/im.js'
|
// import { getUserSig, sendSystemMessage, getChatRecordsByGroupId } from '../api/corp/im.js'
|
||||||
|
|
||||||
const env = __VITE_ENV__;
|
const env = __VITE_ENV__;
|
||||||
|
|
||||||
|
|||||||