feat: 添加病历详情页面

This commit is contained in:
Jafeng 2026-01-22 15:54:15 +08:00
parent dea8db16a4
commit bc01ba97ca
20 changed files with 3275 additions and 11 deletions

View File

@ -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_WX_APP_ID=wx93af55767423938e

View 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>

View 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>

View 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>

View 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);
}

View 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>

View File

@ -60,6 +60,30 @@
"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",
"style": {
@ -118,4 +142,4 @@
]
},
"uniIdRouter": {}
}
}

View File

@ -102,10 +102,26 @@
</view>
<view class="content">
<view class="empty">
<image class="empty-img" src="/static/empty.svg" mode="aspectFit" />
<text class="empty-text">暂无记录</text>
</view>
<HealthProfileTab
v-if="currentTab === 'visitRecord'"
:data="archive"
: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 v-if="showBindWechat" class="footer">
@ -202,7 +218,12 @@
<script setup>
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';
@ -213,6 +234,8 @@ const tabs = [
];
const currentTab = ref('visitRecord');
const reachBottomTime = ref(0);
const archiveId = ref('');
const archive = ref({
name: '',
@ -232,7 +255,9 @@ const archive = ref({
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);
if (cached && typeof cached === 'object') {
archive.value = {
@ -247,6 +272,27 @@ onLoad(() => {
const mobiles = cached && Array.isArray(cached.mobiles) ? cached.mobiles : [];
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(() => {
@ -273,6 +319,7 @@ const createText = computed(() => {
});
const showBindWechat = computed(() => Boolean(archive.value.createdByDoctor && !archive.value.hasBindWechat));
const floatingBottom = computed(() => (showBindWechat.value ? 90 : 16));
// const contactTitle = computed(() => (archive.value.mobile ? '' : ''));
// const notesTitle = computed(() => (archive.value.notes ? '' : ''));
@ -283,7 +330,7 @@ const saveToStorage = () => {
};
const goEdit = () => {
uni.showToast({ title: '档案编辑待接入', icon: 'none' });
uni.navigateTo({ url: `/pages/case/archive-edit?archiveId=${encodeURIComponent(archiveId.value || '')}` });
};
const bindWechat = () => {
@ -650,7 +697,7 @@ const saveAddGroup = () => {
}
.content {
padding: 28px 0;
padding: 0;
}
.empty {

View 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>

View 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>

View 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>

View 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>

View 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
View 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
View 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
View 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

View 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

View 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

View 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

View File

@ -1,7 +1,7 @@
// 引入腾讯IM SDK
import TIM from 'tim-wx-sdk'
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__;