ykt-wxapp/pages/case/case.vue

724 lines
20 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="container">
<!-- Header -->
<view class="header">
<view class="team-selector" @click="toggleTeamPopup">
<text class="team-name">{{ teamDisplay }}</text>
<uni-icons type="loop" size="18" color="#333" class="team-icon"></uni-icons>
</view>
<view class="header-actions">
<view class="action-item" @click="goToSearch">
<uni-icons type="search" size="22" color="#333"></uni-icons>
<text class="action-text">搜索</text>
</view>
<view class="action-item" @click="toggleBatchMode">
<uni-icons type="checkbox" size="22" color="#333"></uni-icons>
<text class="action-text">批量</text>
</view>
<view class="action-item" @click="goToGroupManage">
<uni-icons type="staff" size="22" color="#333"></uni-icons>
<text class="action-text">分组</text>
</view>
<view class="action-item" @click="handleCreate">
<uni-icons type="plusempty" size="22" color="#333"></uni-icons>
<text class="action-text">新增</text>
</view>
</view>
</view>
<!-- Tabs and Count -->
<view class="tabs-area">
<scroll-view scroll-x class="tabs-scroll" :show-scrollbar="false">
<view class="tabs-container">
<view
v-for="(tab, index) in tabs"
:key="index"
class="tab-item"
:class="{ active: currentTab === index }"
@click="currentTab = index"
>
{{ tab }}
</view>
</view>
</scroll-view>
<view class="total-count-inline">{{ totalPatients }}</view>
</view>
<!-- Main Content -->
<view class="content-body">
<!-- Patient List -->
<scroll-view
scroll-y
class="patient-list"
:scroll-into-view="scrollIntoId"
:scroll-with-animation="true"
>
<view v-for="group in patientList" :key="group.letter" :id="'letter-' + group.letter">
<view class="group-title">{{ group.letter }}</view>
<view v-for="(patient, pIndex) in group.data" :key="pIndex" class="patient-card" @click="isBatchMode ? toggleSelect(patient) : null">
<!-- Checkbox for Batch Mode -->
<view v-if="isBatchMode" class="checkbox-area">
<uni-icons
:type="selectedItems.includes(patient.phone) ? 'checkbox-filled' : 'checkbox'"
size="24"
:color="selectedItems.includes(patient.phone) ? '#007aff' : '#ccc'"
></uni-icons>
</view>
<view class="card-content">
<!-- Row 1 -->
<view class="card-row-top">
<view class="patient-info">
<text class="patient-name">{{ patient.name }}</text>
<text class="patient-meta">{{ patient.gender }}/{{ patient.age }}</text>
</view>
<view class="patient-tags">
<view v-for="(tag, tIndex) in patient.tags" :key="tIndex" class="tag">
{{ tag }}
</view>
</view>
</view>
<!-- Row 2 / 3 -->
<view class="card-row-bottom">
<template v-if="currentTab === 1"> <!-- New Patient Tab -->
<text class="record-text">
{{ patient.createTime || '-' }} / {{ patient.creator || '-' }}
</text>
</template>
<template v-else>
<text v-if="patient.record" class="record-text">
{{ patient.record.type }} / {{ patient.record.date }} / {{ patient.record.diagnosis }}
</text>
<text v-else class="no-record">暂无病历记录</text>
</template>
</view>
</view>
</view>
</view>
<!-- Bottom padding for tabbar -->
<view style="height: 100px;"></view> <!-- Increased padding -->
</scroll-view>
<!-- Sidebar Index -->
<view v-if="!isBatchMode" class="sidebar-index">
<view
v-for="letter in indexList"
:key="letter"
class="index-item"
@click="scrollToLetter(letter)"
>
{{ letter }}
</view>
</view>
</view>
<!-- Batch Actions Footer -->
<view v-if="isBatchMode" class="batch-footer">
<view class="left-action" @click="handleSelectAll">
<uni-icons
:type="selectedItems.length > 0 && selectedItems.length === patientList.flatMap(g => g.data).length ? 'checkbox-filled' : 'checkbox'"
size="24"
color="#666"
></uni-icons>
<text class="footer-text">全选 ({{ selectedItems.length }})</text>
</view>
<view class="right-actions">
<button class="footer-btn plain" @click="cancelBatch">取消</button>
<button class="footer-btn primary" @click="handleTransfer">转移</button>
<button class="footer-btn primary" @click="handleShare">共享</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
// State
const currentTeam = ref('张敏西服务团队');
const currentTab = ref(0);
const scrollIntoId = ref('');
const tabs = ['全部', '新患者', '糖尿病', '高血压', '冠心病', '慢阻肺'];
const isBatchMode = ref(false);
const selectedItems = ref([]); // Stores patient phone or unique ID
// 新增流程所需状态(后续接接口替换)
const managedArchiveCountAllTeams = ref(10); // 在管档案数(所有团队)
const isVerified = ref(true); // 是否已认证
const hasVerifyFailedHistory = ref(false); // 是否有历史认证失败
const verifyFailedReason = ref('资料不完整,请补充营业执照/资质证明后重新提交。');
const teamDisplay = computed(() => `${currentTeam.value}(${managedArchiveCountAllTeams.value})`);
// Mock Data
const allPatients = [
{
letter: 'A',
data: [
{
name: '安乐', gender: '男', age: 45, tags: ['糖尿病'],
record: { type: '门诊', date: '2026.1.10', diagnosis: '2型糖尿病' },
createTime: '2026.1.19 14:30', creator: '李医生', phone: '13888888888', hospitalId: '1001'
},
{
name: '奥利奥', gender: '女', age: 22, tags: [], record: null,
createTime: '2026.1.15 09:00', creator: '王医生', phone: '13999999999', hospitalId: '1002'
}
]
},
{
letter: 'L',
data: [
{
name: '李珊珊', gender: '女', age: 37, tags: ['糖尿病', '高血压'],
record: { type: '门诊', date: '2026.1.10', diagnosis: '急性上呼吸道感染' },
createTime: '2026.1.10 10:20', creator: '张医生', phone: '13666666666', hospitalId: '1003'
},
{
name: '李珊珊', gender: '女', age: 37, tags: [],
record: { type: '住院', date: '2026.1.10', diagnosis: '急性上呼吸道感染' },
createTime: '2025.12.30 11:00', creator: '张医生', phone: '13666666667', hospitalId: '10031'
},
{
name: '李某某', gender: '女', age: 37, tags: [], record: null,
createTime: '2025.12.01 08:30', creator: '系统导入', phone: '13555555555', hospitalId: '1004'
},
{
name: '李四', gender: '男', age: 50, tags: ['高血压'], record: null,
createTime: '2026.1.18 16:45', creator: '管理员', phone: '13444444444', hospitalId: '1005'
}
]
},
{
letter: 'Z',
data: [
{
name: '张三', gender: '男', age: 28, tags: [], record: null,
createTime: '2026.1.19 10:00', creator: '赵医生', phone: '13333333333', hospitalId: '1006'
},
{
name: '张敏', gender: '女', age: 32, tags: ['高血压'],
record: { type: '门诊', date: '2025.12.15', diagnosis: '高血压' },
createTime: '2025.11.20 15:15', creator: '孙医生', phone: '13222222222', hospitalId: '1007'
}
]
}
];
// Computed
const patientList = computed(() => {
let list = allPatients;
// New Patient Filter (Last 7 days)
if (currentTab.value === 1) {
// Current date logic: 2026-01-20
const now = new Date('2026-01-20').getTime();
const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000;
let flatList = [];
list.forEach(group => {
group.data.forEach(p => {
if (p.createTime) {
// Parse format 2026.1.19 14:30
const timeStr = p.createTime.replace(/\./g, '/'); // safari compatibility often needs /, but for calculation standardizing
const pTime = new Date(timeStr).getTime();
if (pTime >= sevenDaysAgo) {
flatList.push({ ...p, _ts: pTime });
}
}
});
});
// Sort by create time desc
flatList.sort((a, b) => b._ts - a._ts);
// Re-group or just show one group?
// Usually grouped list maintains grouping or creates a "Search Result" group.
// Requirement says "New Patient show...". Usually lists are still grouped by alphabet or just a flat list.
// The requirement "Sorted from late to early" for creation time implies order is important, creating letter groups breaks time order.
// So for "New Patient" tab, we should probably output a single group or regoup by date?
// Requirement 1.2 "New Patient card... order late to early". This strongly implies creating a single list sorted by time.
// But the template expects `group.letter` and `group.data`.
// I will return a single group named 'Recent' or similar, or just empty letter to hide header.
// The template has `<view class="group-title">{{ group.letter }}</view>`. If letter is empty, it shows empty line.
// I will return [{ letter: '新患者', data: flatList }]
return [{ letter: '最近新增', data: flatList }];
}
// Tab Filtering (Mock logic for other tabs)
if (currentTab.value > 1) {
const tabName = tabs[currentTab.value];
list = list.map(group => ({
...group,
data: group.data.filter(p => p.tags.includes(tabName))
})).filter(group => group.data.length > 0);
}
return list;
});
const indexList = computed(() => {
if (currentTab.value === 1) return []; // No index bar for new patient
return 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').filter(l => patientList.value.some(g => g.letter === l));
});
const totalPatients = computed(() => {
let count = 0;
patientList.value.forEach(g => count += g.data.length);
return count;
});
// Methods
const checkBatchMode = () => {
if (isBatchMode.value) {
uni.showToast({ title: '请先完成当前批量设置或点击底部“取消”按钮退出', icon: 'none' });
return true;
}
return false;
};
const scrollToLetter = (letter) => {
if (currentTab.value === 1) return;
scrollIntoId.value = 'letter-' + letter;
};
const toggleTeamPopup = () => {
if (checkBatchMode()) return;
uni.showActionSheet({
itemList: ['张敏西服务团队', '李医生团队', '王医生团队'],
success: function (res) {
const teams = ['张敏西服务团队', '李医生团队', '王医生团队'];
currentTeam.value = teams[res.tapIndex];
}
});
};
const goToSearch = () => {
if (checkBatchMode()) return;
uni.navigateTo({
url: '/pages/case/search'
});
};
const goToGroupManage = () => {
if (checkBatchMode()) return;
uni.navigateTo({
url: '/pages/case/group-manage'
});
};
const toggleBatchMode = () => {
if (isBatchMode.value) {
// Already in batch mode, do nothing or prompt?
// Prompt says "click Other operations... prompt please finish".
// Clicking "Batch" itself while in batch mode: user usually expects toggle off or nothing.
// Based on "click Cancel button to exit", I'll assume clicking existing batch button doesn't exit.
return;
}
isBatchMode.value = true;
selectedItems.value = [];
};
const handleCreate = () => {
if (checkBatchMode()) return;
// 100上限无法继续新增 -> 引导联系客服(预留入口)
if (managedArchiveCountAllTeams.value >= 100) {
uni.showModal({
title: '提示',
content: '当前管理档案数已达 100 个,无法继续新增。如需提升档案管理数,请联系客服处理。',
cancelText: '知道了',
confirmText: '添加客服',
success: (res) => {
if (res.confirm) {
openAddCustomerServiceEntry();
}
}
});
return;
}
// 未认证 + 达到10上限提示去认证
if (!isVerified.value && managedArchiveCountAllTeams.value >= 10) {
uni.showModal({
title: '提示',
content: '当前管理档案数已达上限 10 个,完成认证即可升级至 100 个。',
cancelText: '暂不认证',
confirmText: '去认证',
success: (res) => {
if (res.confirm) {
startVerifyFlow();
}
}
});
return;
}
// 未达上限:显示新增入口
uni.showActionSheet({
itemList: ['邀请患者建档', '我帮患者建档'],
success: (res) => {
if (res.tapIndex === 0) {
openInvitePatientEntry();
} else if (res.tapIndex === 1) {
openCreatePatientEntry();
}
}
});
};
// 新增流程:认证分支
const startVerifyFlow = () => {
// 有历史失败记录 -> 展示失败原因 & 重新认证
if (hasVerifyFailedHistory.value) {
uni.showModal({
title: '提示',
content: `您有历史认证未通过记录。失败原因为:\n\n${verifyFailedReason.value}`,
cancelText: '取消',
confirmText: '重新认证',
success: (res) => {
if (res.confirm) {
openVerifyEntry();
}
}
});
return;
}
// 正常去认证
openVerifyEntry();
};
// ===== 预留入口(后续对接真实页面/接口) =====
const openVerifyEntry = () => {
uni.showToast({ title: '认证功能待接入', icon: 'none' });
};
const openAddCustomerServiceEntry = () => {
uni.showToast({ title: '添加客服功能待接入', icon: 'none' });
};
const openInvitePatientEntry = () => {
uni.navigateTo({ url: '/pages/case/patient-invite' });
};
const openCreatePatientEntry = () => {
uni.navigateTo({ url: '/pages/case/patient-create' });
};
// Batch Operations
const toggleSelect = (patient) => {
if (!isBatchMode.value) return; // Should not happen if click handler is correct
const id = patient.phone; // Using phone as unique ID for mock
const index = selectedItems.value.indexOf(id);
if (index > -1) {
selectedItems.value.splice(index, 1);
} else {
selectedItems.value.push(id);
}
};
const handleSelectAll = () => {
// Flatten current list
const currentList = patientList.value.flatMap(group => group.data);
if (selectedItems.value.length === currentList.length) {
selectedItems.value = []; // Unselect All
} else {
selectedItems.value = currentList.map(p => p.phone);
}
};
const cancelBatch = () => {
isBatchMode.value = false;
selectedItems.value = [];
};
const handleTransfer = () => {
if (selectedItems.value.length === 0) {
uni.showToast({ title: '请选择患者', icon: 'none' });
return;
}
// Navigate to Transfer Page
uni.navigateTo({ url: '/pages/case/batch-transfer' });
};
const handleShare = () => {
if (selectedItems.value.length === 0) {
uni.showToast({ title: '请选择患者', icon: 'none' });
return;
}
// Navigate to Share Page
uni.navigateTo({ url: '/pages/case/batch-share' });
};
</script>
<style lang="scss" scoped>
.container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #fff;
padding-bottom: 0; // Default
// Padding for batch footer
/* &.is-batch {
padding-bottom: 50px;
} */
// We can't use &.is-batch because scoped style and root element is tricky depending on uni-app version/style
// Instead we handle it in content-body or separate view
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
.team-selector {
display: flex;
align-items: center;
font-size: 18px;
font-weight: bold;
color: #333;
.team-name {
margin-right: 5px;
}
}
.header-actions {
display: flex;
gap: 15px;
.action-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.action-text {
font-size: 10px;
color: #333;
margin-top: 2px;
}
}
}
}
.tabs-area {
display: flex;
align-items: center;
background-color: #f5f7fa;
border-bottom: 1px solid #eee;
padding-right: 15px; // Padding for the count
.tabs-scroll {
flex: 1;
white-space: nowrap;
overflow: hidden;
.tabs-container {
display: flex;
padding: 10px 15px;
.tab-item {
padding: 5px 15px;
margin-right: 10px;
font-size: 14px;
color: #666;
background-color: #fff;
border-radius: 4px;
flex-shrink: 0;
&.active {
color: #5d8aff;
background-color: #e6f0ff;
font-weight: bold;
}
}
}
}
.total-count-inline {
font-size: 12px;
color: #666;
white-space: nowrap;
flex-shrink: 0;
min-width: 50px;
text-align: right;
}
}
.content-body {
flex: 1;
display: flex;
position: relative;
overflow: hidden;
}
.patient-list {
flex: 1;
height: 100%;
background-color: #f7f8fa;
}
.group-title {
padding: 5px 15px;
font-size: 14px;
color: #333;
}
.patient-card {
display: flex;
background-color: #fff;
padding: 15px;
margin-bottom: 1px; // Separator line
border-bottom: 1px solid #f0f0f0;
.checkbox-area {
display: flex;
align-items: center;
margin-right: 10px;
}
.card-content {
flex: 1;
}
.card-row-top {
display: flex;
align-items: center;
margin-bottom: 8px;
flex-wrap: wrap;
.patient-info {
display: flex;
align-items: flex-end;
margin-right: 10px;
.patient-name {
font-size: 18px;
font-weight: bold;
color: #333;
margin-right: 8px;
}
.patient-meta {
font-size: 12px;
color: #999;
margin-bottom: 2px;
}
}
.patient-tags {
display: flex;
gap: 5px;
.tag {
font-size: 10px;
color: #5d8aff;
border: 1px solid #5d8aff;
padding: 0 4px;
border-radius: 8px;
height: 16px;
line-height: 14px;
}
}
}
.card-row-bottom {
font-size: 14px;
.record-text {
color: #666;
}
.no-record {
color: #bdc3c7;
}
}
}
.batch-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 50px;
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
z-index: 99;
.left-action {
display: flex;
align-items: center;
.footer-text {
margin-left: 5px;
font-size: 14px;
color: #333;
}
}
.right-actions {
display: flex;
gap: 10px;
.footer-btn {
font-size: 14px;
padding: 0 15px;
height: 32px;
line-height: 32px;
margin: 0;
border-radius: 4px;
&.plain {
border: 1px solid #ddd;
background-color: #fff;
color: #666;
}
&.primary {
background-color: #5d8aff;
color: #fff;
border: none;
}
&::after {
border: none;
}
}
}
}
.sidebar-index {
width: 20px;
position: absolute;
right: 0;
top: 20px;
bottom: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: transparent;
z-index: 10;
.index-item {
font-size: 10px;
color: #555;
padding: 2px 0;
width: 20px;
text-align: center;
font-weight: 500;
}
}
</style>