ykt-wxapp/pages/case/archive-detail.vue

932 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="page">
<view class="card">
<view class="header">
<view class="avatar">
<image v-if="archive.avatar" class="avatar-img" :src="archive.avatar" mode="aspectFill" />
</view>
<view class="header-main">
<view class="name-row">
<text class="name">{{ archive.name || '-' }}</text>
<text v-if="sexOrAge" class="meta">{{ sexOrAge }}</text>
</view>
<view v-if="archive.mobile" class="sub-line">{{ archive.mobile }}</view>
<view v-if="idRows.length" class="id-rows">
<view v-for="row in idRows" :key="row.label" class="id-row">
<text class="id-label">{{ row.label }}</text>
<text class="id-value">{{ row.value }}</text>
</view>
</view>
<view v-if="createText" class="create-row">
<text class="create-text">{{ createText }}</text>
</view>
</view>
<view class="header-right" @click="goEdit">
<uni-icons type="right" size="18" color="#bbb" />
</view>
</view>
<view class="cells">
<!-- 联系电话 -->
<view class="info-row border-bottom">
<text class="input-label">联系电话</text>
<view class="info-right">
<template v-if="archive.mobile">
<view class="phone-area" @click="makeCall">
<uni-icons type="phone-filled" size="18" color="#4f6ef7" class="mr-4" />
<text class="phone-text">{{ archive.mobile }}</text>
</view>
<view class="edit-icon" @click.stop="openContact">
<uni-icons type="compose" size="20" color="#4f6ef7" />
</view>
</template>
<view v-else class="empty-click" @click="openContact">
<text class="placeholder">添加联系电话</text>
</view>
</view>
</view>
<!-- 备注 -->
<view class="info-block border-bottom">
<view class="block-header">
<text class="input-label">备注</text>
<view class="edit-icon" @click.stop="openNotes">
<uni-icons type="compose" size="20" color="#4f6ef7" />
</view>
</view>
<view class="block-content">
<text v-if="archive.notes" class="note-content">{{ archive.notes }}</text>
<view v-else class="empty-click" @click="openNotes">
<text class="placeholder">添加备注</text>
</view>
</view>
</view>
<!-- 分组 -->
<view class="info-block">
<view class="block-header">
<text class="input-label">分组</text>
<view class="edit-icon" @click.stop="openGroups">
<uni-icons type="compose" size="20" color="#4f6ef7" />
</view>
</view>
<view class="block-content">
<view v-if="archive.groups && archive.groups.length" class="tags-wrap">
<view v-for="tag in archive.groups" :key="tag" class="tag-item">
{{ tag }}
</view>
</view>
<view v-else class="empty-click" @click="openGroups">
<text class="placeholder">添加分组</text>
</view>
</view>
</view>
</view>
</view>
<view class="tabs">
<view
v-for="t in tabs"
:key="t.key"
class="tab"
:class="{ active: currentTab === t.key }"
@click="currentTab = t.key"
>
{{ t.title }}
</view>
</view>
<view class="content">
<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">
<button class="bind-btn" @click="bindWechat">
<uni-icons type="email" size="18" color="#fff" />
<text class="bind-text">关联患者微信</text>
</button>
</view>
<!-- 修改联系电话居中弹窗 -->
<uni-popup ref="contactPopup" type="center">
<view class="modal">
<view class="modal-title">修改联系电话</view>
<view class="modal-body">
<input class="modal-input" v-model="contactInput" type="number" placeholder="请输入联系电话" />
</view>
<view class="modal-actions">
<view class="modal-btn cancel" @click="closeContact">取消</view>
<view class="modal-btn save" @click="saveContact">保存</view>
</view>
</view>
</uni-popup>
<!-- 修改备注底部弹层 -->
<uni-popup ref="notesPopup" type="bottom">
<view class="sheet">
<view class="sheet-header">
<view class="sheet-header-left" />
<view class="sheet-title">修改备注</view>
<view class="sheet-close" @click="closeNotes">
<uni-icons type="closeempty" size="18" color="#333" />
</view>
</view>
<view class="sheet-body">
<textarea
v-model="notesInput"
class="notes-textarea"
placeholder="请输入备注"
:maxlength="100"
/>
<view class="counter">{{ notesInput.length }}/100</view>
</view>
<view class="sheet-footer">
<button class="primary-btn" @click="saveNotes">保存</button>
</view>
</view>
</uni-popup>
<!-- 选择分组底部弹层 -->
<uni-popup ref="groupPopup" type="bottom">
<view class="sheet">
<view class="sheet-header">
<view class="sheet-close" @click="closeGroups">
<uni-icons type="closeempty" size="18" color="#333" />
</view>
<view class="sheet-title">选择分组</view>
<view class="sheet-link" @click="openAddGroup">添加分组</view>
</view>
<view class="group-list">
<view v-for="g in groupOptions" :key="g" class="group-item" @click="toggleGroup(g)">
<uni-icons
:type="selectedGroupMap[g] ? 'checkbox-filled' : 'checkbox'"
size="20"
:color="selectedGroupMap[g] ? '#4f6ef7' : '#c7c7c7'"
/>
<text class="group-name">{{ g }}</text>
</view>
</view>
<view class="sheet-footer">
<button class="primary-btn" @click="saveGroups">保存</button>
</view>
</view>
</uni-popup>
<!-- 添加新分组居中弹窗 -->
<uni-popup ref="addGroupPopup" type="center">
<view class="modal">
<view class="modal-title">添加新分组</view>
<view class="modal-body">
<input class="modal-input" v-model="newGroupName" placeholder="请输入分组名称" />
</view>
<view class="modal-actions">
<view class="modal-btn cancel" @click="closeAddGroup">取消</view>
<view class="modal-btn save" @click="saveAddGroup">保存</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { computed, getCurrentInstance, ref } from 'vue';
import { onLoad, onPullDownRefresh, onReachBottom, onReady, 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 tabs = [
{ key: 'visitRecord', title: '病历记录' },
{ key: 'followUp', title: '回访任务' },
{ key: 'serviceRecord', title: '服务记录' }
];
const currentTab = ref('visitRecord');
const reachBottomTime = ref(0);
const archiveId = ref('');
const tabsScrollTop = ref(0);
const instanceProxy = getCurrentInstance()?.proxy;
const archive = ref({
name: '',
sex: '',
age: '',
avatar: '',
mobile: '',
outpatientNo: '',
inpatientNo: '',
medicalRecordNo: '',
createTime: '',
creator: '',
createdByDoctor: true,
hasBindWechat: false,
notes: '',
groups: [],
groupOptions: ['高血压', '糖尿病', '高血脂']
});
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 = {
...archive.value,
...cached,
groups: Array.isArray(cached.groups) ? cached.groups : archive.value.groups,
groupOptions: Array.isArray(cached.groupOptions) ? cached.groupOptions : archive.value.groupOptions
};
}
if (!archive.value.mobile) {
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);
});
function measureTabsTop() {
const q = instanceProxy ? uni.createSelectorQuery().in(instanceProxy) : uni.createSelectorQuery();
q.select('.tabs').boundingClientRect();
q.selectViewport().scrollOffset();
q.exec((res) => {
const rect = res && res[0];
const viewport = res && res[1];
if (!rect || !viewport) return;
tabsScrollTop.value = (rect.top || 0) + (viewport.scrollTop || 0);
});
}
onReady(() => {
setTimeout(measureTabsTop, 30);
});
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,
};
}
setTimeout(measureTabsTop, 30);
});
onPullDownRefresh(() => {
uni.pageScrollTo({
scrollTop: tabsScrollTop.value || 0,
duration: 0,
});
setTimeout(() => uni.stopPullDownRefresh(), 150);
});
onReachBottom(() => {
reachBottomTime.value = Date.now();
});
const sexOrAge = computed(() => {
const sex = archive.value.sex ? String(archive.value.sex) : '';
const age = archive.value.age || archive.value.age === 0 ? `${archive.value.age}` : '';
if (sex && age) return `${sex} ${age}`;
return sex || age;
});
const idRows = computed(() => {
const rows = [];
if (archive.value.outpatientNo) rows.push({ label: '门诊号', value: String(archive.value.outpatientNo) });
if (archive.value.inpatientNo) rows.push({ label: '住院号', value: String(archive.value.inpatientNo) });
if (archive.value.medicalRecordNo) rows.push({ label: '病案号', value: String(archive.value.medicalRecordNo) });
return rows;
});
const createText = computed(() => {
const time = archive.value.createTime ? String(archive.value.createTime) : '';
const creator = archive.value.creator ? String(archive.value.creator) : '';
if (time && creator) return `${time} ${creator}创建`;
if (time) return `${time} 创建`;
return '';
});
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 ? '备注' : '添加备注'));
// const groupTitle = computed(() => (Array.isArray(archive.value.groups) && archive.value.groups.length ? '分组' : '添加分组'));
const saveToStorage = () => {
uni.setStorageSync(STORAGE_KEY, { ...archive.value });
};
const goEdit = () => {
uni.navigateTo({ url: `/pages/case/archive-edit?archiveId=${encodeURIComponent(archiveId.value || '')}` });
};
const bindWechat = () => {
uni.showToast({ title: '关联患者微信功能待接入', icon: 'none' });
};
const makeCall = () => {
if (archive.value.mobile) {
uni.makePhoneCall({ phoneNumber: archive.value.mobile });
}
};
// ===== 联系电话 =====
const contactPopup = ref(null);
const contactInput = ref('');
const openContact = () => {
contactInput.value = archive.value.mobile ? String(archive.value.mobile) : '';
contactPopup.value?.open();
};
const closeContact = () => {
contactPopup.value?.close();
};
const saveContact = () => {
const v = String(contactInput.value || '').trim();
if (!v) {
uni.showToast({ title: '请输入联系电话', icon: 'none' });
return;
}
archive.value.mobile = v;
saveToStorage();
closeContact();
};
// ===== 备注 =====
const notesPopup = ref(null);
const notesInput = ref('');
const openNotes = () => {
notesInput.value = archive.value.notes ? String(archive.value.notes) : '';
notesPopup.value?.open();
};
const closeNotes = () => {
notesPopup.value?.close();
};
const saveNotes = () => {
archive.value.notes = String(notesInput.value || '').trim();
saveToStorage();
closeNotes();
};
// ===== 分组 =====
const groupPopup = ref(null);
const addGroupPopup = ref(null);
const groupOptions = computed(() => (Array.isArray(archive.value.groupOptions) ? archive.value.groupOptions : []));
const selectedGroupMap = ref({});
const syncSelectedMapFromArchive = () => {
const current = Array.isArray(archive.value.groups) ? archive.value.groups : [];
selectedGroupMap.value = current.reduce((acc, name) => {
acc[name] = true;
return acc;
}, {});
};
const openGroups = () => {
syncSelectedMapFromArchive();
groupPopup.value?.open();
};
const closeGroups = () => {
groupPopup.value?.close();
};
const toggleGroup = (name) => {
selectedGroupMap.value[name] = !selectedGroupMap.value[name];
};
const saveGroups = () => {
archive.value.groups = Object.keys(selectedGroupMap.value).filter(k => selectedGroupMap.value[k]);
saveToStorage();
closeGroups();
};
// 添加新分组
const newGroupName = ref('');
const openAddGroup = () => {
newGroupName.value = '';
addGroupPopup.value?.open();
};
const closeAddGroup = () => {
addGroupPopup.value?.close();
};
const saveAddGroup = () => {
const name = String(newGroupName.value || '').trim();
if (!name) {
uni.showToast({ title: '请输入分组名称', icon: 'none' });
return;
}
const exists = groupOptions.value.some(g => String(g).trim() === name);
if (exists) {
uni.showToast({ title: '该分组已存在', icon: 'none' });
return;
}
archive.value.groupOptions = [...groupOptions.value, name];
selectedGroupMap.value[name] = true;
saveToStorage();
closeAddGroup();
};
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: #f5f6f8;
padding-bottom: calc(80px + env(safe-area-inset-bottom));
}
.card {
background: #fff;
}
.header {
display: flex;
align-items: flex-start;
padding: 14px 14px 10px;
border-bottom: 1px solid #f2f2f2;
}
.avatar {
width: 56px;
height: 56px;
border-radius: 6px;
border: 1px solid #e8e8e8;
background: #fafafa;
overflow: hidden;
flex-shrink: 0;
}
.avatar-img {
width: 56px;
height: 56px;
}
.header-main {
flex: 1;
min-width: 0;
padding: 0 10px;
}
.name-row {
display: flex;
align-items: center;
gap: 8px;
padding-top: 2px;
}
.name {
font-size: 18px;
font-weight: 600;
color: #1f1f1f;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.meta {
font-size: 13px;
color: #666;
}
.sub-line {
margin-top: 6px;
font-size: 12px;
color: #666;
}
.id-rows {
margin-top: 6px;
}
.id-row {
display: flex;
align-items: center;
font-size: 12px;
color: #666;
line-height: 18px;
}
.id-label {
flex-shrink: 0;
}
.id-value {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.create-row {
margin-top: 6px;
}
.create-text {
font-size: 12px;
color: #999;
}
.header-right {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 6px;
}
.cells {
background: #fff;
padding: 0 14px;
}
.border-bottom {
border-bottom: 1px solid #f2f2f2;
}
.info-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0;
min-height: 24px;
}
.info-block {
padding: 16px 0;
}
.block-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.block-content {
min-height: 20px;
}
.input-label {
font-size: 15px;
color: #666;
}
.info-right {
display: flex;
align-items: center;
}
.phone-area {
display: flex;
align-items: center;
margin-right: 12px;
}
.mr-4 {
margin-right: 4px;
}
.phone-text {
font-size: 16px;
color: #4f6ef7;
font-weight: 500;
}
.edit-icon {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
}
.placeholder {
font-size: 14px;
color: #999;
}
.note-content {
font-size: 14px;
color: #333;
line-height: 1.5;
word-break: break-all;
}
.tags-wrap {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tag-item {
height: 24px;
line-height: 22px;
padding: 0 10px;
border: 1px solid #4f6ef7;
border-radius: 12px;
font-size: 12px;
color: #4f6ef7;
box-sizing: border-box;
}
.empty-click {
/* To ensure empty area is clickable */
display: inline-block;
}
.tabs {
margin-top: 10px;
background: #fff;
display: flex;
border-top: 1px solid #f2f2f2;
border-bottom: 1px solid #f2f2f2;
position: sticky;
top: 0;
z-index: 30;
}
.tab {
flex: 1;
text-align: center;
height: 44px;
line-height: 44px;
font-size: 14px;
color: #333;
position: relative;
}
.tab.active {
color: #4f6ef7;
font-weight: 600;
}
.tab.active::after {
content: '';
position: absolute;
left: 50%;
bottom: 0;
width: 32px;
height: 3px;
background: #4f6ef7;
transform: translateX(-50%);
border-radius: 2px;
}
.content {
padding: 0;
}
.empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.empty-img {
width: 160px;
height: 160px;
opacity: 0.9;
}
.empty-text {
margin-top: 10px;
font-size: 13px;
color: #9aa0a6;
}
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
}
.bind-btn {
width: 100%;
height: 44px;
background: #4f6ef7;
color: #fff;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 15px;
}
.bind-text {
color: #fff;
}
/* ===== 弹窗样式(居中) ===== */
.modal {
width: 320px;
background: #fff;
border-radius: 8px;
overflow: hidden;
}
.modal-title {
font-size: 16px;
font-weight: 600;
text-align: center;
padding: 14px 12px;
color: #333;
border-bottom: 1px solid #f0f0f0;
}
.modal-body {
padding: 14px 14px 8px;
}
.modal-input {
width: 100%;
height: 40px;
border: 1px solid #e6e6e6;
border-radius: 4px;
padding: 0 10px;
font-size: 14px;
box-sizing: border-box;
}
.modal-actions {
display: flex;
gap: 12px;
padding: 12px 14px 14px;
}
.modal-btn {
flex: 1;
height: 40px;
line-height: 40px;
text-align: center;
border-radius: 4px;
font-size: 14px;
}
.modal-btn.cancel {
border: 1px solid #4f6ef7;
color: #4f6ef7;
background: #fff;
}
.modal-btn.save {
background: #4f6ef7;
color: #fff;
}
/* ===== 底部弹层样式 ===== */
.sheet {
background: #fff;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
overflow: hidden;
}
.sheet-header {
height: 48px;
display: flex;
align-items: center;
padding: 0 14px;
border-bottom: 1px solid #f0f0f0;
}
.sheet-title {
flex: 1;
text-align: center;
font-size: 16px;
font-weight: 600;
color: #333;
}
.sheet-close {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.sheet-header-left {
width: 24px;
}
.sheet-link {
min-width: 60px;
text-align: right;
font-size: 14px;
color: #4f6ef7;
}
.sheet-body {
padding: 14px;
}
.notes-textarea {
width: 100%;
height: 140px;
border: 1px solid #e6e6e6;
border-radius: 4px;
padding: 10px;
font-size: 14px;
box-sizing: border-box;
}
.counter {
text-align: right;
margin-top: 8px;
font-size: 12px;
color: #999;
}
.group-list {
padding: 8px 14px 14px;
max-height: 55vh;
overflow: auto;
}
.group-item {
display: flex;
align-items: center;
padding: 12px 0;
}
.group-name {
margin-left: 10px;
font-size: 14px;
color: #333;
}
.sheet-footer {
padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
}
.primary-btn {
width: 100%;
height: 44px;
background: #4f6ef7;
color: #fff;
border-radius: 4px;
font-size: 15px;
line-height: 44px;
}
</style>