2025-09-19 17:37:19 +08:00

715 lines
17 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">
<!-- 头部 -->
<uni-nav-bar
left-icon="left"
title="患者分组"
@clickLeft="goBack"
fixed
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#eeeeee"
>
<template #right>
<view class="save-text" @click="confirmGroup">确定({{ totalSelectedCount }})</view>
</template>
</uni-nav-bar>
<!-- 筛选排序栏 -->
<view class="filter-sort-bar">
<view class="sort-section" @click="toggleGroupSort">
<text class="sort-label">{{ groupSortTitle }}</text>
<view class="imgbox">
<up-image :src="upImg" width="26rpx" height="26rpx" ></up-image>
</view>
</view>
<view class="divider"></view>
<view class="current-sort" @click="toggleInnerSort">
<text class="sort-text">{{ innerSortTitle }}</text>
<view class="imgbox">
<up-image :src="upImg" 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" :style="{ height: scrollViewHeight }">
<!-- 分组循环渲染 -->
<view class="groupcell" v-for="(group, gi) in groups" :key="group.uuid || gi">
<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-checkbox" @click.stop="toggleGroupSelection(gi)" v-if="group.patientList.length > 0">
<view class="checkbox" :class="{ 'checked': isGroupSelected(gi) }">
<uni-icons v-if="isGroupSelected(gi)" type="checkmarkempty" color="#8B2316" size="16"></uni-icons>
</view>
</view>
</view>
<view class="patient-list" v-if="openGroups[gi]">
<view class="patient-item" v-for="(patient, index) in (group.patientList || [])" :key="patient.uuid || index">
<view class="patient-avatar">
<up-image :src="docUrl + patient.photo" width="80rpx" height="80rpx" mode="aspectFill"></up-image>
</view>
<view class="patient-checkbox" @click.stop="togglePatientSelection(gi, patient, index)">
<view class="checkbox" :class="{ 'checked': isPatientSelected(gi, patient, index) }">
<uni-icons v-if="isPatientSelected(gi, patient, index)" type="checkmarkempty" color="#8B2316" size="16"></uni-icons>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="bottom-bar">
<view class="clear-btn" @click="clearSelection">清空选择</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { onShow,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 api from '@/api/api.js';
import docUrl from '@/utils/docUrl'
import dayjs from 'dayjs'
import navTo from '@/utils/navTo.js'
const list_sort = ref(0);
const group_sort = ref(0);
// 分组数据与展开状态
const groups = ref([]);
const openGroups = ref({});
// 选中的分组状态
const selectedGroups = ref({});
// 选中的患者状态以分组索引为key值为Set或索引Map
const selectedPatients = ref({});
const formatYMD = (val) => {
if (!val) return '';
const d = dayjs(val);
return d.isValid() ? d.format('YYYY-MM-DD') : '';
};
// 排序栏标题
const groupSortTitle = computed(() => group_sort.value === 0 ? '按首字母' : '分组人数');
const innerSortTitle = computed(() => list_sort.value === 0 ? '按首字母' : '随访时间');
// 计算选中的分组数量
const selectedGroupCount = computed(() => {
return Object.values(selectedGroups.value).filter(Boolean).length;
});
// 计算选中的患者数量(去重)
const getPatientUniqueId = (p) => {
return (p && (p.uuid || p.patientUuid || p.id || p.userId)) || '';
};
const selectedPatientCount = computed(() => {
const idSet = new Set();
groups.value.forEach((group, gi) => {
const map = selectedPatients.value[gi] || {};
(group.patientList || []).forEach((patient, pi) => {
if (map[pi]) {
const uid = getPatientUniqueId(patient);
if (uid) idSet.add(uid);
}
});
});
return idSet.size;
});
// 合计数量:只计算去重后的患者数量
const totalSelectedCount = computed(() => selectedPatientCount.value);
onShow(() => {
fetchGroupList();
});
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) ? res.data : [];
// 初始化展开状态:默认收起
openGroups.value = {};
groups.value.forEach((group, idx) => {
openGroups.value[idx] = false;
// 初始化患者选择Map避免未定义
if (!selectedPatients.value[idx]) selectedPatients.value[idx] = {};
// 若组选中但患者为空,则保持组选中;否则按患者是否全选同步组选中状态
updateGroupSelectionByPatients(idx);
});
}
};
const toggleGroup = (idx) => {
openGroups.value[idx] = !openGroups.value[idx];
};
const toggleGroupSelection = (idx) => {
const group = groups.value[idx] || {};
const patients = group.patientList || [];
const willSelect = !isGroupSelected(idx);
// 设置组选中状态
selectedGroups.value[idx] = willSelect;
// 同步该组下的患者选中状态
if (!selectedPatients.value[idx]) selectedPatients.value[idx] = {};
patients.forEach((_, pi) => {
selectedPatients.value[idx][pi] = willSelect;
});
};
const isPatientSelected = (gi, patient, index) => {
const groupSelectedMap = selectedPatients.value[gi] || {};
return !!groupSelectedMap[index];
};
const togglePatientSelection = (gi, patient, index) => {
if (!selectedPatients.value[gi]) selectedPatients.value[gi] = {};
selectedPatients.value[gi][index] = !selectedPatients.value[gi][index];
// 若组内所有患者都被勾选,则自动勾选组;否则取消组勾选
updateGroupSelectionByPatients(gi);
};
const isGroupSelected = (gi) => {
// 优先若显式设置了组选中状态且为true直接认为选中
if (selectedGroups.value[gi]) return true;
// 否则根据患者是否全选判断
const group = groups.value[gi] || {};
const patients = group.patientList || [];
if (patients.length === 0) return !!selectedGroups.value[gi];
const map = selectedPatients.value[gi] || {};
return patients.every((_, pi) => !!map[pi]);
};
const updateGroupSelectionByPatients = (gi) => {
const group = groups.value[gi] || {};
const patients = group.patientList || [];
const map = selectedPatients.value[gi] || {};
if (patients.length === 0) return;
selectedGroups.value[gi] = patients.every((_, pi) => !!map[pi]);
};
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;
closeGroupSort();
fetchGroupList();
};
const confirmGroup = () => {
// 收集已选患者,并做去重
const idSet = new Set();
const dedupPatients = [];
groups.value.forEach((group, gi) => {
const map = selectedPatients.value[gi] || {};
(group.patientList || []).forEach((patient, pi) => {
if (map[pi]) {
const uid = getPatientUniqueId(patient);
if (uid && !idSet.has(uid)) {
idSet.add(uid);
dedupPatients.push(patient);
}
}
});
});
if (dedupPatients.length === 0) {
uni.showToast({ title: '请至少选择1个患者', icon: 'none' });
return;
}
try {
uni.navigateTo({
url: '/pages_chat/groupSend/groupSend',
success: (res) => {
try {
uni.$emit('selectedChatPatients', { patients: dedupPatients });
} catch (emitErr) {
console.error('传递已选患者失败', emitErr);
uni.showToast({ title: '传递失败,请重试', icon: 'none' });
}
}
});
} catch (e) {
console.error('跳转失败', e);
uni.showToast({ title: '跳转失败,请重试', icon: 'none' });
}
}
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;
closeInnerSort();
fetchGroupList();
};
// 方法
const goBack = () => {
uni.navigateBack();
};
const createNew = () => {
uni.showToast({
title: '跳转到新建分组页面',
icon: 'none'
});
// 这里可以跳转到新建分组页面
};
// 清空选择
const clearSelection = () => {
selectedGroups.value = {};
selectedPatients.value = {};
};
</script>
<style lang="scss" scoped>
.patient-group-page {
min-height: 100vh;
background-color: #f5f5f5;
}
/* 弹窗样式 */
.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: 220rpx; /* 紧贴筛选栏 */
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{
margin-top: -20rpx;
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: 140rpx;
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: #333;
}
.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: #333;
}
.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: 240rpx;
width:100%;
bottom:120rpx; // 预留底部操作栏高度
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-checkbox{
display:flex;
align-items:center;
.checkbox{
width: 40rpx;
height: 40rpx;
border: 2rpx solid #ddd;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
transition: all 0.3s ease;
&.checked{
border-color: #8B2316;
}
}
}
}
.patient-list {
// 滚动视图样式
&::-webkit-scrollbar {
display: none;
}
.patient-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
padding: 30rpx 40rpx;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.patient-checkbox{
margin-right: 8rpx;
.checkbox{
width: 40rpx;
height: 40rpx;
border: 2rpx solid #ddd;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
transition: all 0.3s ease;
&.checked{ border-color: #8B2316; }
}
}
.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;
}
}
}
}
}
.bottom-bar{
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
box-shadow: 0 -6rpx 20rpx rgba(0,0,0,0.06);
padding: 20rpx 30rpx env(safe-area-inset-bottom);
display: flex;
justify-content: flex-end;
.align-right{justify-content:flex-end;}
.clear-btn{
width: 100%;
margin-bottom: 20rpx;
height: 80rpx;
line-height: 80rpx;
text-align: center;
border-radius: 12rpx;
border: 2rpx solid #8B2316;
color: #8B2316;
font-size: 28rpx;
}
}
</style>