uniapp-app/pages_app/patientGroup/patientGroup.vue
2026-03-13 17:13:09 +08:00

658 lines
15 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="patient-group-page">
<!-- 头部 -->
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="患者分组"
@clickLeft="goBack"
color="#8B2316"
:border="false"
backgroundColor="#eeeeee"
>
<template #right>
<view class="save-text" @click="addGroup">新建</view>
</template>
</uni-nav-bar>
</view>
<!-- 筛选排序栏 -->
<view class="filter-sort-bar">
<view class="sort-section" @click="toggleGroupSort">
<text class="sort-label" :class="{ 'is-selected': groupSortSelected }">{{ groupSortTitle }}</text>
<view class="imgbox">
<up-image :src="groupSortIcon" width="26rpx" height="26rpx" ></up-image>
</view>
</view>
<view class="divider"></view>
<view class="current-sort" @click="toggleInnerSort">
<text class="sort-text" :class="{ 'is-selected': innerSortSelected }">{{ innerSortTitle }}</text>
<view class="imgbox">
<up-image :src="innerSortIcon" width="26rpx" height="26rpx" ></up-image>
</view>
</view>
</view>
<!-- 分组排序弹窗 -->
<view v-if="showGroupSort" class="popup-panel">
<view class="popup-item" :class="{ active: group_sort==0 }" @click.stop="chooseGroupSort('letter')">
<text class="item-text">按首字母</text>
<uni-icons v-if="selectedGroupSort==='letter'" type="checkmarkempty" color="#8B2316" size="22"></uni-icons>
</view>
<view class="popup-divider"></view>
<view class="popup-item" :class="{ active:group_sort==1}" @click.stop="chooseGroupSort('count')">
<text class="item-text">分组人数</text>
<uni-icons v-if="selectedGroupSort==='count'" type="checkmarkempty" color="#8B2316" size="22"></uni-icons>
</view>
</view>
<view v-if="showGroupSort" class="popup-mask" @click="closeGroupSort"></view>
<!-- 组内排序弹窗 -->
<view v-if="showInnerSort" class="popup-panel">
<view class="popup-item" :class="{ active:list_sort==0 }" @click.stop="chooseInnerSort('letter')">
<text class="item-text">按首字母</text>
<uni-icons v-if="selectedInnerSort==='letter'" type="checkmarkempty" color="#8B2316" size="22"></uni-icons>
</view>
<view class="popup-divider"></view>
<view class="popup-item" :class="{ active:list_sort==1 }" @click.stop="chooseInnerSort('count')">
<text class="item-text">随访时间</text>
<uni-icons v-if="selectedInnerSort==='count'" type="checkmarkempty" color="#8B2316" size="22"></uni-icons>
</view>
</view>
<view v-if="showInnerSort" class="popup-mask" @click="closeInnerSort"></view>
<!-- 患者列表 -->
<scroll-view class="patient-list-section" scroll-y="true" >
<!-- 分组循环渲染 -->
<view class="groupcell" v-for="(group, gi) in groups" :key="group.uuid || gi" style="flex-direction: row;">
<view class="section-title" @click="toggleGroup(gi)">
<view class="left">
<view class="imgbox" :class="{ 'active':openGroups[gi] }">
<up-image v-if="!openGroups[gi]" :src="groupRightImg" width="19rpx" height="32rpx" ></up-image>
<up-image v-else :src="groupDownImg" width="32rpx" height="19rpx" ></up-image>
</view>
<view class="title">{{ group.name || '未命名分组' }} | {{ group.patientNum || (group.patientList ? group.patientList.length : 0) }}</view>
</view>
<view class="right-edit" @click.stop="editGroup(group)">
<text class="edit-text">编辑</text>
</view>
</view>
<view class="patient-list" v-if="openGroups[gi]">
<view class="patient-item" v-for="(patient, index) in getVisiblePatients(group, gi)" :key="patient.uuid || index" @click="goPatientDetail(patient.uuid)">
<view class="patient-avatar">
<up-image :src="patient._avatarLoadError ? defaultImg : patient._photoUrl" width="80rpx" height="80rpx" mode="aspectFill" @error="handlePatientAvatarError(patient)"></up-image>
</view>
<view class="patient-info">
<view class="patient-name">{{ patient._displayName }}</view>
<view class="follow-up-time">随访于{{ patient._formattedJoinDate }}</view>
</view>
</view>
<view v-if="isGroupRendering(group, gi)" class="rendering-tip">加载中...</view>
</view>
</view>
<empty v-if="groups.length == 0" :emptyDesc="'暂无分组'" />
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed, onMounted,nextTick } from 'vue';
import { onShow,onBackPress,onLoad } from "@dcloudio/uni-app";
import upImg from "@/static/triangle_green_theme.png"
import downImg from "@/static/triangle_normal.png"
import groupRightImg from "@/static/groupright_big.png"
import groupDownImg from "@/static/groupup_big.png"
import normalImg from "@/static/triangle_normal.png"
import api from '@/api/api.js';
import docUrl from '@/utils/docUrl'
import empty from "@/components/empty/empty.vue"
import dayjs from 'dayjs'
import navTo from '@/utils/navTo.js'
import defaultImg from "@/static/default.png"
const list_sort = ref(0);
const group_sort = ref(0);
const groupSortSelected = ref(false);
const innerSortSelected = ref(false);
const from = ref('');
onLoad((options) => {
if(options.from){
from.value = options.from;
}
});
onShow(() => {
uni.showLoading({
title: '加载中...',
mask: true
});
fetchGroupList();
});
const goPatientDetail = (uuid) => {
navTo({
url: `/pages_app/patientDetail/patientDetail?uuid=${uuid}`
})
}
onBackPress(() => {
if(!from.value){
uni.navigateBack();
return true;
}
});
// 分组数据与展开状态
const groups = ref([]);
const openGroups = ref({});
const groupRenderCount = ref({});
const INITIAL_RENDER_COUNT = 20;
const BATCH_RENDER_COUNT = 20;
const formatYMD = (val) => {
if (!val) return '';
const d = dayjs(val);
return d.isValid() ? d.format('YYYY-MM-DD') : '';
};
// 排序栏标题/图标(初始化为默认文案+灰色图标,选择后展示具体文案+主题图标)
const groupSortTitle = computed(() => {
if (!groupSortSelected.value) return '分组排序';
return group_sort.value === 0 ? '按首字母' : '分组人数';
});
const innerSortTitle = computed(() => {
if (!innerSortSelected.value) return '组内排序';
return list_sort.value === 0 ? '按首字母' : '随访时间';
});
const groupSortIcon = computed(() => groupSortSelected.value ? upImg : normalImg);
const innerSortIcon = computed(() => innerSortSelected.value ? upImg : normalImg);
const fetchGroupList = async () => {
const res = await api.groupList({
list_sort:list_sort.value,
page:1,
pageSize:10,
group_sort:group_sort.value
});
if(res.code === 200){
groups.value = Array.isArray(res.data) ? preprocessGroups(res.data) : [];
// 初始化展开状态:默认收起
openGroups.value = {};
groupRenderCount.value = {};
groups.value.forEach((_, idx) => openGroups.value[idx] = false);
nextTick(() => {
uni.hideLoading();
});
}
};
const toggleGroup = (idx) => {
openGroups.value[idx] = !openGroups.value[idx];
if (openGroups.value[idx]) {
startProgressiveRender(idx);
}
};
const preprocessGroups = (rawGroups) => {
return rawGroups.map((group) => {
const list = Array.isArray(group.patientList) ? group.patientList : [];
list.forEach((patient) => {
patient._formattedJoinDate = formatYMD(patient.join_date);
patient._displayName = patient.nickname || patient.realName ;
patient._photoUrl = patient.photo ? `${docUrl}${patient.photo}` : defaultImg;
});
return {
...group,
patientList: list
};
});
};
const handlePatientAvatarError = (patient) => {
if (!patient) return;
patient._avatarLoadError = true;
};
const startProgressiveRender = (idx) => {
const group = groups.value[idx];
if (!group) return;
const total = (group.patientList || []).length;
if (!total) {
groupRenderCount.value[idx] = 0;
return;
}
groupRenderCount.value[idx] = Math.min(INITIAL_RENDER_COUNT, total);
if (total <= INITIAL_RENDER_COUNT) return;
const loadNextBatch = () => {
if (!openGroups.value[idx]) return;
const current = groupRenderCount.value[idx] || 0;
if (current >= total) return;
groupRenderCount.value[idx] = Math.min(current + BATCH_RENDER_COUNT, total);
if (groupRenderCount.value[idx] < total) {
setTimeout(loadNextBatch, 16);
}
};
setTimeout(loadNextBatch, 16);
};
const getVisiblePatients = (group, idx) => {
const list = group?.patientList || [];
const renderCount = groupRenderCount.value[idx] || 0;
if (renderCount >= list.length) return list;
return list.slice(0, renderCount);
};
const isGroupRendering = (group, idx) => {
const total = (group?.patientList || []).length;
const rendered = groupRenderCount.value[idx] || 0;
return total > 0 && rendered < total;
};
const editGroup = (group) => {
navTo({
url: `/pages_app/groupEdit/groupEdit?uuid=${group.uuid}`
})
};
// 分组排序弹窗状态
const showGroupSort = ref(false);
const selectedGroupSort = ref('letter'); // letter | count
// 组内排序弹窗状态
const showInnerSort = ref(false);
const selectedInnerSort = ref('letter');
// 待分组展开/收起
const showPending = ref(false);
const toggleGroupSort = () => {
showGroupSort.value = !showGroupSort.value;
if (showGroupSort.value) showInnerSort.value = false;
};
const closeGroupSort = () => {
showGroupSort.value = false;
};
const chooseGroupSort = (type) => {
selectedGroupSort.value = type;
// 修改接口字段:按首字母=0分组人数=1
group_sort.value = type === 'letter' ? 0 : 1;
groupSortSelected.value = true;
closeGroupSort();
fetchGroupList();
};
const addGroup = () => {
navTo({
url: `/pages_app/groupEdit/groupEdit`
})
}
const toggleInnerSort = () => {
showInnerSort.value = !showInnerSort.value;
if (showInnerSort.value) showGroupSort.value = false;
};
const closeInnerSort = () => {
showInnerSort.value = false;
};
const chooseInnerSort = (type) => {
selectedInnerSort.value = type;
// 组内排序:按首字母=0随访时间=1
list_sort.value = type === 'letter' ? 0 : 1;
innerSortSelected.value = true;
closeInnerSort();
fetchGroupList();
};
// 方法
const goBack = () => {
if(!from.value){
plus.runtime.quit();
}else{
uni.navigateBack();
}
};
const createNew = () => {
uni.showToast({
title: '跳转到新建分组页面',
icon: 'none'
});
// 这里可以跳转到新建分组页面
};
</script>
<style lang="scss" scoped>
.patient-group-page {
min-height: 100vh;
background-color: #f5f5f5;
.groupcell{
flex-direction: row!important;
}
}
/* 弹窗样式 */
.popup-mask {
position: fixed;
top: 220rpx; /* 位于筛选栏下方 */
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.3);
z-index: 8;
}
.popup-panel {
position: fixed;
top: calc(var(--status-bar-height) + 44px + 82rpx); /* 紧贴筛选栏 */
left: 0;
right: 0;
background: #fff;
z-index: 10;
box-shadow: 0 6rpx 20rpx rgba(0,0,0,0.08);
}
.popup-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 30rpx;
font-size: 30rpx;
color: #333;
}
.popup-divider {
height: 2rpx;
background: #eaeaea;
}
.popup-item.active .item-text {
color: #8B2316;
}
.save-text{
color:#8B2316;
font-size: 30rpx;
display: flex;
align-items: center;
justify-content: center;
}
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10rpx 30rpx;
background-color: #ffffff;
font-size: 24rpx;
color: #333;
.status-left {
display: flex;
align-items: center;
gap: 20rpx;
.time {
font-weight: 500;
}
.app-icons {
display: flex;
gap: 10rpx;
.app-icon {
width: 24rpx;
height: 24rpx;
border-radius: 4rpx;
&.weibo {
background-color: #ff8200;
}
&.taobao {
background-color: #ff6a00;
}
&.xiaohongshu {
background-color: #ff2442;
}
}
}
}
.status-right {
display: flex;
align-items: center;
gap: 15rpx;
.network {
color: #666;
}
.signal-icon {
width: 32rpx;
height: 20rpx;
background-color: #333;
border-radius: 2rpx;
}
.battery-text {
font-size: 20rpx;
color: #333;
}
.battery {
width: 40rpx;
height: 20rpx;
border: 2rpx solid #333;
border-radius: 4rpx;
background-color: #4caf50;
position: relative;
&::after {
content: '';
position: absolute;
right: -6rpx;
top: 6rpx;
width: 4rpx;
height: 8rpx;
background-color: #333;
border-radius: 0 2rpx 2rpx 0;
}
}
}
}
.header {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
background-color: #ffffff;
border-bottom: 1rpx solid #f0f0f0;
.back-btn {
padding: 10rpx;
}
.title {
flex: 1;
text-align: center;
font-size: 36rpx;
font-weight: normal;
color: #ff0000;
margin-right: 60rpx; // 为了保持标题居中
}
.new-btn {
padding: 10rpx;
.new-text {
font-size: 28rpx;
color: #ff0000;
}
}
}
.filter-sort-bar {
display: flex;
align-items: center;
justify-content:center;
padding: 20rpx 30rpx;
background-color: #ffff;
position: fixed;
top: calc(var(--status-bar-height) + 44px);
left: 0;
right: 0;
z-index: 9;
border-bottom: 1rpx solid #f0f0f0;
.imgbox{
width: 32rpx;
margin-top: -10rpx;
}
.sort-section {
display: flex;
align-items: center;
gap: 10rpx;
.sort-label {
font-size: 28rpx;
color: #999;
&.is-selected {
color: #8B2316;
}
}
.sort-icon.down {
width: 0;
height: 0;
border-left: 8rpx solid transparent;
border-right: 8rpx solid transparent;
border-top: 12rpx solid #999;
}
}
.divider {
width: 2rpx;
height: 40rpx;
background-color: #e0e0e0;
margin: 0 80rpx;
}
.current-sort {
display: flex;
align-items: center;
gap: 10rpx;
.sort-text {
font-size: 28rpx;
color: #999;
&.is-selected {
color: #8B2316;
}
}
.sort-icon.up {
width: 0;
height: 0;
border-left: 8rpx solid transparent;
border-right: 8rpx solid transparent;
border-bottom: 12rpx solid #999;
&.active {
border-bottom-color: #ff0000;
}
}
}
}
.patient-list-section {
top: calc(var(--status-bar-height) + 44px + 100rpx);
width:100%;
bottom:0;
position: fixed;
background-color: #ffffff;
.section-title {
padding: 30rpx 30rpx 20rpx;
font-size: 32rpx;
width: 100%;
box-sizing: border-box;
font-weight: normal;
display: flex;
color: #333;
border-bottom: 1rpx solid #f0f0f0;
align-items: center;
justify-content: space-between;
.left{
display:flex;
align-items:center;
}
.imgbox{
&.active{
margin-top: -20rpx;
}
margin-right: 8rpx;
}
.right-edit{
display:flex;
align-items:center;
gap: 8rpx;
.edit-text{ font-size: 28rpx; color:#8B2316; }
}
}
.patient-list {
// 滚动视图样式
&::-webkit-scrollbar {
display: none;
}
.patient-item {
display: flex;
align-items: center;
gap: 20rpx;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.patient-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 12rpx;
overflow: hidden;
}
.patient-info {
flex: 1;
.patient-name {
font-size: 32rpx;
color: #333;
margin-top: 24rpx;
}
.follow-up-time {
font-size: 24rpx;
color: #999;
display: flex;
justify-content: flex-end;
}
}
}
}
.rendering-tip {
padding: 16rpx 30rpx 24rpx;
font-size: 24rpx;
color: #999;
text-align: center;
}
}
</style>