feat:新增病历列表页面
This commit is contained in:
parent
e838f8af15
commit
594913404b
42
pages.json
42
pages.json
@ -12,6 +12,48 @@
|
|||||||
"navigationBarTitleText": "病例"
|
"navigationBarTitleText": "病例"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/case/search",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "搜索患者"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/case/group-manage",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "分组管理"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/case/batch-transfer",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "转移客户给其他团队"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/case/batch-share",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "共享客户"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/case/patient-invite",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "邀请患者"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/case/patient-create",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "新增患者"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/case/patient-inner-info",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "内部信息"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/work/work",
|
"path": "pages/work/work",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
173
pages/case/batch-share.vue
Normal file
173
pages/case/batch-share.vue
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
<template>
|
||||||
|
<view class="share-container">
|
||||||
|
<view class="content">
|
||||||
|
<view class="section-title">选择共享团队</view>
|
||||||
|
<view class="selector-item" @click="selectTeam">
|
||||||
|
<text :class="team ? '' : 'placeholder'">{{ team ? team.name : "请选择团队" }}</text>
|
||||||
|
<uni-icons type="arrowdown" size="16" color="#999"></uni-icons>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="tips">共享客户:表示客户档案共享多个团队可见,多个团队可同时为该客户服务。</view>
|
||||||
|
|
||||||
|
<template v-if="team">
|
||||||
|
<view class="section-title">选择责任人</view>
|
||||||
|
<view class="selector-item" @click="selectUser">
|
||||||
|
<text :class="user ? '' : 'placeholder'">{{ user ? user.name : "请选择责任人" }}</text>
|
||||||
|
<uni-icons type="arrowdown" size="16" color="#999"></uni-icons>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="footer">
|
||||||
|
<button class="btn plain" @click="cancel">取消</button>
|
||||||
|
<button class="btn primary" @click="save">保存</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const team = ref(null);
|
||||||
|
const user = ref(null);
|
||||||
|
|
||||||
|
// Mock Data
|
||||||
|
const teams = [
|
||||||
|
{ id: 1, name: '李医生团队' },
|
||||||
|
{ id: 2, name: '王医生团队' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const users = [
|
||||||
|
{ id: 101, name: '张医生' },
|
||||||
|
{ id: 102, name: '李医生' },
|
||||||
|
{ id: 103, name: '王医生' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectTeam = () => {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: teams.map(t => t.name),
|
||||||
|
success: (res) => {
|
||||||
|
team.value = teams[res.tapIndex];
|
||||||
|
user.value = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectUser = () => {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: users.map(u => u.name),
|
||||||
|
success: (res) => {
|
||||||
|
user.value = users[res.tapIndex];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
uni.navigateBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
if (!team.value) {
|
||||||
|
uni.showToast({ title: '请选择团队', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!user.value) {
|
||||||
|
uni.showToast({ title: '请选择负责人', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showLoading({ title: '保存中' });
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showToast({ title: '操作成功' });
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateBack();
|
||||||
|
}, 1500);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.share-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #f7f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-item {
|
||||||
|
background-color: #fff;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 15px;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
&.placeholder {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
margin-top: 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 15px 20px 30px;
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
&.plain {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #666;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background-color: #5d8aff;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
174
pages/case/batch-transfer.vue
Normal file
174
pages/case/batch-transfer.vue
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<template>
|
||||||
|
<view class="transfer-container">
|
||||||
|
<view class="content">
|
||||||
|
<view class="section-title">选择新负责团队</view>
|
||||||
|
<view class="selector-item" @click="selectTeam">
|
||||||
|
<text :class="team ? '' : 'placeholder'">{{ team ? team.name : "请选择选择团队" }}</text>
|
||||||
|
<uni-icons type="arrowdown" size="16" color="#999"></uni-icons>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-if="team">
|
||||||
|
<view class="section-title">选择责任人</view>
|
||||||
|
<view class="selector-item" @click="selectUser">
|
||||||
|
<text :class="user ? '' : 'placeholder'">{{ user ? user.name : "请选择责任人" }}</text>
|
||||||
|
<uni-icons type="arrowdown" size="16" color="#999"></uni-icons>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="tips">客户将与本团队解除服务关系,本团队成员将没有权限查询到客户档案。</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="footer">
|
||||||
|
<button class="btn plain" @click="cancel">取消</button>
|
||||||
|
<button class="btn primary" @click="save">保存</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const team = ref(null);
|
||||||
|
const user = ref(null);
|
||||||
|
|
||||||
|
// Mock Data
|
||||||
|
const teams = [
|
||||||
|
{ id: 1, name: '张敏西服务团队' },
|
||||||
|
{ id: 2, name: '李医生团队' },
|
||||||
|
{ id: 3, name: '王医生团队' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const users = [
|
||||||
|
{ id: 101, name: '张医生' },
|
||||||
|
{ id: 102, name: '李医生' },
|
||||||
|
{ id: 103, name: '王医生' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectTeam = () => {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: teams.map(t => t.name),
|
||||||
|
success: (res) => {
|
||||||
|
team.value = teams[res.tapIndex];
|
||||||
|
user.value = null; // Reset user when team changes
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectUser = () => {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: users.map(u => u.name),
|
||||||
|
success: (res) => {
|
||||||
|
user.value = users[res.tapIndex];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
uni.navigateBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
if (!team.value) {
|
||||||
|
uni.showToast({ title: '请选择团队', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!user.value) {
|
||||||
|
uni.showToast({ title: '请选择责任人', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showLoading({ title: '保存中' });
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showToast({ title: '操作成功' });
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateBack();
|
||||||
|
}, 1500);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.transfer-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #f7f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-item {
|
||||||
|
background-color: #fff;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 15px;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
&.placeholder {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
margin-top: 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 15px 20px 30px;
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
&.plain {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #666;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background-color: #5d8aff;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,8 +1,724 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>case</div>
|
<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>
|
||||||
<script>
|
<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>
|
</script>
|
||||||
|
|
||||||
<style>
|
<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>
|
</style>
|
||||||
239
pages/case/group-manage.vue
Normal file
239
pages/case/group-manage.vue
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
<template>
|
||||||
|
<view class="manage-container">
|
||||||
|
<view class="group-list">
|
||||||
|
<view v-for="(item, index) in groups" :key="index" class="group-item">
|
||||||
|
<view class="left-action" @click="handleDelete(index)">
|
||||||
|
<uni-icons type="minus-filled" size="24" color="#ff4d4f"></uni-icons>
|
||||||
|
</view>
|
||||||
|
<view class="group-name">{{ item.name }}</view>
|
||||||
|
<view class="right-actions">
|
||||||
|
<uni-icons type="compose" size="24" color="#5d8aff" class="icon-edit" @click="handleEdit(item, index)"></uni-icons>
|
||||||
|
<uni-icons type="bars" size="24" color="#5d8aff" class="icon-drag"></uni-icons>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Bottom Button -->
|
||||||
|
<view class="footer">
|
||||||
|
<button class="add-btn" @click="handleAdd">添加新分组</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Dialog -->
|
||||||
|
<view v-if="showDialog" class="dialog-mask">
|
||||||
|
<view class="dialog-content">
|
||||||
|
<view class="dialog-header">{{ dialogTitle }}</view>
|
||||||
|
<view class="dialog-body">
|
||||||
|
<input class="dialog-input" type="text" v-model="inputValue" placeholder="请输入分组名称" />
|
||||||
|
</view>
|
||||||
|
<view class="dialog-footer">
|
||||||
|
<button class="dialog-btn cancel" @click="closeDialog">取消</button>
|
||||||
|
<button class="dialog-btn confirm" @click="handleSave">保存</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
// State
|
||||||
|
const groups = ref([
|
||||||
|
{ id: 1, name: '糖尿病' },
|
||||||
|
{ id: 2, name: '高血压' },
|
||||||
|
{ id: 3, name: '高血脂' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const showDialog = ref(false);
|
||||||
|
const dialogMode = ref('add'); // 'add' or 'edit'
|
||||||
|
const inputValue = ref('');
|
||||||
|
const editingIndex = ref(-1);
|
||||||
|
|
||||||
|
const dialogTitle = ref('添加新分组');
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const handleAdd = () => {
|
||||||
|
dialogMode.value = 'add';
|
||||||
|
dialogTitle.value = '添加新分组';
|
||||||
|
inputValue.value = '';
|
||||||
|
showDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (item, index) => {
|
||||||
|
dialogMode.value = 'edit';
|
||||||
|
dialogTitle.value = '编辑分组名称';
|
||||||
|
inputValue.value = item.name;
|
||||||
|
editingIndex.value = index;
|
||||||
|
showDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (index) => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定要删除该分组吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
groups.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
showDialog.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!inputValue.value.trim()) {
|
||||||
|
uni.showToast({ title: '请输入分组名称', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dialogMode.value === 'add') {
|
||||||
|
groups.value.push({
|
||||||
|
id: Date.now(),
|
||||||
|
name: inputValue.value
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
groups.value[editingIndex.value].name = inputValue.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDialog();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.manage-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #fff;
|
||||||
|
padding-bottom: 80px; // Space for footer
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-list {
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
|
||||||
|
.left-action {
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
.icon-edit, .icon-drag {
|
||||||
|
padding: 5px; // Increase tap area
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 15px 20px 30px; // Safe area padding
|
||||||
|
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
background-color: #5d8aff;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom Dialog Styles
|
||||||
|
.dialog-mask {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 999;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content {
|
||||||
|
width: 280px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
padding: 20px 0 10px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body {
|
||||||
|
padding: 10px 20px 20px;
|
||||||
|
|
||||||
|
.dialog-input {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
.dialog-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
&.cancel {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #666;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.confirm {
|
||||||
|
background-color: #5d8aff;
|
||||||
|
color: #fff;
|
||||||
|
border: none; // Remove border for confirm button usually
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
132
pages/case/patient-create.vue
Normal file
132
pages/case/patient-create.vue
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<view class="body">
|
||||||
|
<scroll-view scroll-y class="scroll">
|
||||||
|
<view class="form-wrap">
|
||||||
|
<form-template ref="formRef" :items="baseItems" :form="form" :rule="rules" @change="onChange" />
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
|
||||||
|
<view class="footer">
|
||||||
|
<button class="primary" @click="next">下一步</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
|
import FormTemplate from '@/components/form-template/index.vue';
|
||||||
|
import validate from '@/utils/validate';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'patient-create-base';
|
||||||
|
|
||||||
|
const formRef = ref(null);
|
||||||
|
const form = reactive({
|
||||||
|
name: '',
|
||||||
|
gender: '',
|
||||||
|
age: '',
|
||||||
|
mobile: '',
|
||||||
|
birthday: '',
|
||||||
|
idType: '',
|
||||||
|
idNo: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const baseItems = [
|
||||||
|
{ title: 'name', name: '姓名', type: 'input', operateType: 'formCell', required: true, wordLimit: 20, inputType: 'text' },
|
||||||
|
{ title: 'gender', name: '性别', type: 'select', operateType: 'formCell', required: false, range: ['男', '女'] },
|
||||||
|
{ title: 'age', name: '年龄', type: 'input', operateType: 'formCell', required: false, wordLimit: 3, inputType: 'number' },
|
||||||
|
{ title: 'mobile', name: '手机号', type: 'input', operateType: 'formCell', required: false, wordLimit: 11, inputType: 'number' },
|
||||||
|
{ title: 'birthday', name: '出生日期', type: 'date', operateType: 'formCell', required: false },
|
||||||
|
{ title: 'idType', name: '证件类型', type: 'select', operateType: 'formCell', required: false, range: ['身份证', '护照', '港澳台通行证', '其他'] },
|
||||||
|
{ title: 'idNo', name: '证件号', type: 'input', operateType: 'formCell', required: false, wordLimit: 30, inputType: 'text' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
idNo(value) {
|
||||||
|
if (!value) return true;
|
||||||
|
if (form.idType === '身份证') {
|
||||||
|
const [ok, msg] = validate.isChinaId(value);
|
||||||
|
if (!ok) return msg || '证件号格式不正确';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onLoad(() => {
|
||||||
|
const cached = uni.getStorageSync(STORAGE_KEY);
|
||||||
|
if (cached && typeof cached === 'object') {
|
||||||
|
Object.assign(form, cached);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function onChange({ title, value }) {
|
||||||
|
form[title] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function next() {
|
||||||
|
if (!formRef.value?.verify?.()) return;
|
||||||
|
uni.setStorageSync(STORAGE_KEY, { ...form });
|
||||||
|
uni.navigateTo({ url: '/pages/case/patient-inner-info' });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
height: 100vh;
|
||||||
|
background: #f6f6f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-wrap {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-note {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 92px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: #f2df52;
|
||||||
|
color: #000;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: #fff;
|
||||||
|
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
|
||||||
|
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
line-height: 48px;
|
||||||
|
background: #5d8aff;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
144
pages/case/patient-inner-info.vue
Normal file
144
pages/case/patient-inner-info.vue
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<view class="body">
|
||||||
|
<scroll-view scroll-y class="scroll">
|
||||||
|
<view class="form-wrap">
|
||||||
|
<form-template v-if="items.length" ref="formRef" :items="items" :form="form" :rule="rules" @change="onChange" />
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view class="footer">
|
||||||
|
<button class="btn plain" @click="cancel">取消</button>
|
||||||
|
<button class="btn primary" @click="save">完成</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
|
import FormTemplate from '@/components/form-template/index.vue';
|
||||||
|
|
||||||
|
const BASE_KEY = 'patient-create-base';
|
||||||
|
const INNER_KEY = 'patient-create-inner';
|
||||||
|
|
||||||
|
const formRef = ref(null);
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
customerSource: '',
|
||||||
|
firstVisitDate: '',
|
||||||
|
diseaseTag: '',
|
||||||
|
remark: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ title: 'customerSource', name: '患者来源', type: 'select', operateType: 'formCell', required: false, range: ['线上咨询', '同事推荐', '客户推荐', '其他'] },
|
||||||
|
{ title: 'firstVisitDate', name: '首次就诊日期', type: 'date', operateType: 'formCell', required: false },
|
||||||
|
{ title: 'diseaseTag', name: '慢病标签', type: 'select', operateType: 'formCell', required: false, range: ['糖尿病', '高血压', '冠心病', '慢阻肺'] },
|
||||||
|
{ title: 'remark', name: '备注', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const rules = {};
|
||||||
|
|
||||||
|
onLoad(() => {
|
||||||
|
const base = uni.getStorageSync(BASE_KEY);
|
||||||
|
if (!base || typeof base !== 'object') {
|
||||||
|
uni.showToast({ title: '请先填写基础信息', icon: 'none' });
|
||||||
|
uni.navigateBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = uni.getStorageSync(INNER_KEY);
|
||||||
|
if (cached && typeof cached === 'object') {
|
||||||
|
Object.assign(form, cached);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function onChange({ title, value }) {
|
||||||
|
form[title] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
uni.navigateBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
if (formRef.value?.verify && !formRef.value.verify()) return;
|
||||||
|
|
||||||
|
const base = uni.getStorageSync(BASE_KEY) || {};
|
||||||
|
const payload = { ...base, ...form };
|
||||||
|
|
||||||
|
uni.setStorageSync(INNER_KEY, { ...form });
|
||||||
|
|
||||||
|
// 这里后续对接真实新增接口:payload 即最终提交数据
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '已完成新增(mock)。后续将对接真实建档接口。',
|
||||||
|
showCancel: false,
|
||||||
|
success: () => {
|
||||||
|
uni.removeStorageSync(BASE_KEY);
|
||||||
|
uni.removeStorageSync(INNER_KEY);
|
||||||
|
// 返回到病例列表
|
||||||
|
uni.navigateBack({ delta: 2 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
height: 100vh;
|
||||||
|
background: #f6f6f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.form-wrap {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: #fff;
|
||||||
|
padding: 12px 16px 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: #666;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.primary {
|
||||||
|
background: #5d8aff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
225
pages/case/patient-invite.vue
Normal file
225
pages/case/patient-invite.vue
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<view class="content">
|
||||||
|
<view class="code-row">
|
||||||
|
<text class="code-text">{{ displayTeamCode }}</text>
|
||||||
|
<view class="help" @click="showHelp">
|
||||||
|
<uni-icons type="help-filled" size="18" color="#5d8aff"></uni-icons>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="qr-card">
|
||||||
|
<image class="qr-image" :src="qrImageUrl" mode="aspectFit" @click="previewQr" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="tips">
|
||||||
|
<text class="tips-title">微信扫一扫上面的二维码</text>
|
||||||
|
<text class="tips-sub">进入团队首页,即可发起线上咨询、建档授权等服务</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="footer">
|
||||||
|
<button class="footer-btn outline" @click="saveQr">保存图片</button>
|
||||||
|
|
||||||
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
|
<button class="footer-btn primary" open-type="share">分享到微信</button>
|
||||||
|
<!-- #endif -->
|
||||||
|
|
||||||
|
<!-- #ifndef MP-WEIXIN -->
|
||||||
|
<button class="footer-btn primary" @click="shareFallback">分享到微信</button>
|
||||||
|
<!-- #endif -->
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { onLoad, onShareAppMessage } from '@dcloudio/uni-app';
|
||||||
|
|
||||||
|
const teamCode = ref('133****3356');
|
||||||
|
const qrImageUrl = ref('');
|
||||||
|
|
||||||
|
const displayTeamCode = computed(() => `${teamCode.value}服务团队码`);
|
||||||
|
|
||||||
|
const buildQrUrl = (text) => {
|
||||||
|
// 仅用于演示UI:实际项目建议由后端返回可扫码的二维码图片地址
|
||||||
|
const encoded = encodeURIComponent(text);
|
||||||
|
return `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encoded}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
onLoad((query) => {
|
||||||
|
if (query?.teamCode) teamCode.value = String(query.teamCode);
|
||||||
|
if (query?.qrUrl) {
|
||||||
|
qrImageUrl.value = String(query.qrUrl);
|
||||||
|
} else {
|
||||||
|
qrImageUrl.value = buildQrUrl(`TEAM_CODE:${teamCode.value}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const showHelp = () => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '服务团队码',
|
||||||
|
content: '患者微信扫一扫二维码进入团队首页,可发起线上咨询、建档授权等服务。',
|
||||||
|
showCancel: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const previewQr = () => {
|
||||||
|
if (!qrImageUrl.value) return;
|
||||||
|
uni.previewImage({ urls: [qrImageUrl.value] });
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveQr = () => {
|
||||||
|
if (!qrImageUrl.value) {
|
||||||
|
uni.showToast({ title: '二维码生成中,请稍后', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showLoading({ title: '保存中...' });
|
||||||
|
uni.downloadFile({
|
||||||
|
url: qrImageUrl.value,
|
||||||
|
success: (res) => {
|
||||||
|
const filePath = res.tempFilePath;
|
||||||
|
uni.saveImageToPhotosAlbum({
|
||||||
|
filePath,
|
||||||
|
success: () => {
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showToast({ title: '已保存到相册', icon: 'success' });
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showModal({
|
||||||
|
title: '保存失败',
|
||||||
|
content: '请在系统设置中允许保存到相册后重试。',
|
||||||
|
confirmText: '去设置',
|
||||||
|
cancelText: '取消',
|
||||||
|
success: (r) => {
|
||||||
|
if (r.confirm) {
|
||||||
|
uni.openSetting?.({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showToast({ title: '下载二维码失败', icon: 'none' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const shareFallback = () => {
|
||||||
|
uni.showToast({ title: '请使用右上角菜单分享', icon: 'none' });
|
||||||
|
};
|
||||||
|
|
||||||
|
onShareAppMessage(() => {
|
||||||
|
return {
|
||||||
|
title: '邀请您加入服务团队',
|
||||||
|
path: `/pages/case/patient-invite?teamCode=${encodeURIComponent(teamCode.value)}`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #ffffff;
|
||||||
|
padding-bottom: calc(76px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 22px 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-text {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help {
|
||||||
|
margin-left: 8px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-card {
|
||||||
|
margin: 18px auto 0;
|
||||||
|
width: 280px;
|
||||||
|
height: 280px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-image {
|
||||||
|
width: 260px;
|
||||||
|
height: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
margin-top: 16px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #3a3a3a;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-sub {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: #fff;
|
||||||
|
padding: 10px 16px calc(10px + env(safe-area-inset-bottom));
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-btn::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-btn.outline {
|
||||||
|
background: #fff;
|
||||||
|
color: #5d8aff;
|
||||||
|
border: 1px solid #5d8aff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-btn.primary {
|
||||||
|
background: #5d8aff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
283
pages/case/search.vue
Normal file
283
pages/case/search.vue
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
<template>
|
||||||
|
<view class="search-container">
|
||||||
|
<!-- Search Header -->
|
||||||
|
<view class="search-header">
|
||||||
|
<view class="search-bar">
|
||||||
|
<uni-icons type="search" size="18" color="#999" class="search-icon"></uni-icons>
|
||||||
|
<input
|
||||||
|
class="search-input"
|
||||||
|
placeholder="搜索患者名称/手机号/院内ID号"
|
||||||
|
v-model="searchQuery"
|
||||||
|
confirm-type="search"
|
||||||
|
focus
|
||||||
|
@input="handleSearch"
|
||||||
|
/>
|
||||||
|
<uni-icons v-if="searchQuery" type="clear" size="18" color="#ccc" @click="clearSearch" class="clear-icon"></uni-icons>
|
||||||
|
</view>
|
||||||
|
<text class="cancel-btn" @click="goBack">取消</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Search Results -->
|
||||||
|
<scroll-view v-if="searchQuery" scroll-y class="search-results">
|
||||||
|
<view v-if="searchResults.length === 0" class="empty-state">
|
||||||
|
<text class="empty-text">暂无搜索结果</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else>
|
||||||
|
<view v-for="(patient, index) in searchResults" :key="index" class="patient-card">
|
||||||
|
<!-- 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 -->
|
||||||
|
<view class="card-row-bottom">
|
||||||
|
<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>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- History or Suggestions (when no search) -->
|
||||||
|
<view v-else class="search-tips">
|
||||||
|
<text class="tips-text">输入患者名称、手机号或院内ID号进行搜索</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
|
// State
|
||||||
|
const searchQuery = ref('');
|
||||||
|
|
||||||
|
// Mock all patients data (same as case.vue)
|
||||||
|
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: '13666666666', hospitalId: '1003'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 searchResults = computed(() => {
|
||||||
|
if (!searchQuery.value) return [];
|
||||||
|
|
||||||
|
const q = searchQuery.value.toLowerCase();
|
||||||
|
let results = [];
|
||||||
|
|
||||||
|
allPatients.forEach(group => {
|
||||||
|
group.data.forEach(p => {
|
||||||
|
if (p.name.includes(q) ||
|
||||||
|
(p.phone && p.phone.includes(q)) ||
|
||||||
|
(p.hospitalId && p.hospitalId.includes(q))) {
|
||||||
|
results.push(p);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const handleSearch = () => {
|
||||||
|
// Search logic handled by computed property
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearSearch = () => {
|
||||||
|
searchQuery.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
uni.navigateBack();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.search-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #f7f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 15px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 36px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 12px;
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-icon {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #5d8aff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 80px 20px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-tips {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.tips-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.patient-card {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 1px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
x
Reference in New Issue
Block a user