uniapp-app/pages_app/patientMsg/patientListPerf.vue
2026-03-17 14:49:50 +08:00

431 lines
11 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="content">
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="患者列表压测"
@clickLeft="goBack"
color="#8B2316"
:border="false"
backgroundColor="#eee"
>
<template #right>
<view class="nav-right">
<uni-icons type="search" size="24" color="#8B2316"></uni-icons>
</view>
</template>
</uni-nav-bar>
</view>
<view class="patient-list">
<view class="perf-toolbar">
<view class="perf-btn primary" @click="loadRealData">加载真实数据</view>
<view class="perf-btn" @click="loadRealData(true)">强制刷新</view>
<view class="perf-btn" @click="clearData">清空</view>
</view>
<view class="perf-tip">当前数据量: {{ patientList.length }}分组: {{ patientGroups.length }}当前字母: {{ activeLetter || '-' }}</view>
<view class="listbox patient-building" v-if="patientDataLoading && !loadFinish">
<text class="patient-building-text">正在生成并整理模拟数据...</text>
</view>
<view class="listbox" v-show="patientList.length > 0">
<scroll-view
class="groups-scroll"
scroll-y="true"
:scroll-into-view="scrollIntoViewId"
:scroll-with-animation="true"
@scroll="onGroupsScroll"
v-show="loadFinish"
>
<view class="special-actions">
<view class="action-item">
<view class="action-icon new-patient"></view>
<text class="action-text">新的患者(模拟)</text>
<uni-icons type="right" size="20" color="#999"></uni-icons>
</view>
</view>
<view class="group-section" v-for="group in patientGroups" :key="group.letter" :id="`anchor-${group.letter}`">
<view class="group-header">{{ group.letter }}</view>
<view class="patient-item" v-for="item in group.items" :key="item.uuid || item.id">
<view class="patient-avatar avatar-placeholder"></view>
<view class="patient-info">
<text class="patient-name">{{ item.nickname || item.realName }}</text>
</view>
<view class="patient-status">
<text class="follow-date">随访于{{ item.joinDateYMD }}</text>
</view>
</view>
</view>
</scroll-view>
<view class="letter-index" v-if="loadFinish && indexList.length > 0">
<view class="letter-item" v-for="letter in indexList" :key="letter" :class="{ active: activeLetter === letter }" @click="onLetterTap(letter)">
{{ letter }}
</view>
</view>
</view>
<view class="listbox" v-show="patientList.length === 0 && !patientDataLoading">
<empty></empty>
</view>
</view>
</view>
</template>
<script setup>
import { nextTick, ref, shallowRef } from 'vue';
import { onBackPress } from '@dcloudio/uni-app';
import dayjs from 'dayjs';
import pinyin from 'pinyin';
import api from '@/api/api.js';
const patientList = ref([]);
const patientDataLoading = ref(false);
const loadFinish = ref(false);
const firstLetterCache = new Map();
const LETTER_ORDER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
const GROUP_BUILD_CHUNK_SIZE = 180;
const indexList = ref([]);
const patientGroups = shallowRef([]);
const allPatientGroups = shallowRef([]);
const activeLetter = ref('');
const scrollIntoViewId = ref('');
const groupScrollRanges = ref([]);
const sortLetter = (a, b) => {
if (a === '#') return 1;
if (b === '#') return -1;
const ai = LETTER_ORDER.indexOf(a);
const bi = LETTER_ORDER.indexOf(b);
if (ai === -1 && bi === -1) return String(a).localeCompare(String(b));
if (ai === -1) return 1;
if (bi === -1) return -1;
return ai - bi;
};
const formatYMD = (input) => {
if (!input) return '';
const d = dayjs(input);
return d.isValid() ? d.format('YYYY-MM-DD') : '';
};
const getFirstLetter = (name = '') => {
const firstChar = String(name).trim().charAt(0);
if (!firstChar) return '#';
if (/^[A-Za-z]$/.test(firstChar)) return firstChar.toUpperCase();
if (firstLetterCache.has(firstChar)) return firstLetterCache.get(firstChar);
try {
const py = pinyin(firstChar, { style: pinyin.STYLE_NORMAL })?.[0]?.[0] || '';
const letter = py ? py.charAt(0).toUpperCase() : '#';
firstLetterCache.set(firstChar, letter);
return letter;
} catch (error) {
return '#';
}
};
const rebuildIndexList = () => {
indexList.value = allPatientGroups.value
.map(g => String(g?.letter || '').toUpperCase())
.filter((letter, index, arr) => (letter === '#' || /^[A-Z]$/.test(letter)) && arr.indexOf(letter) === index)
.sort(sortLetter);
activeLetter.value = indexList.value[0] || '';
};
const onLetterTap = (letter) => {
if (!letter) return;
activeLetter.value = letter;
const targetId = `anchor-${letter}`;
if (scrollIntoViewId.value === targetId) {
scrollIntoViewId.value = '';
nextTick(() => {
scrollIntoViewId.value = targetId;
});
return;
}
scrollIntoViewId.value = targetId;
};
const rpxToPx = (rpx) => {
const { screenWidth } = uni.getSystemInfoSync();
return Math.round((screenWidth / 750) * rpx);
};
const buildGroupScrollRanges = () => {
const actionHeight = rpxToPx(120); // action-item 高度近似
const headerHeight = rpxToPx(80); // group-header 高度近似
const itemHeight = rpxToPx(150); // patient-item 高度近似
let cursor = actionHeight;
groupScrollRanges.value = patientGroups.value.map(group => {
const groupHeight = headerHeight + (group.items?.length || 0) * itemHeight;
const range = {
letter: group.letter,
start: cursor,
end: cursor + groupHeight
};
cursor += groupHeight;
return range;
});
};
const onGroupsScroll = (e) => {
const top = Number(e?.detail?.scrollTop || 0);
if (!groupScrollRanges.value.length) return;
let current = groupScrollRanges.value[0].letter;
for (let i = 0; i < groupScrollRanges.value.length; i++) {
const range = groupScrollRanges.value[i];
if (top >= range.start) {
current = range.letter;
} else {
break;
}
}
activeLetter.value = current;
};
const waitForNextFrame = () => new Promise(resolve => setTimeout(resolve, 0));
const buildGroupsFromPatients = async () => {
const map = new Map();
const source = patientList.value || [];
for (let i = 0; i < source.length; i++) {
const p = source[i] || {};
const normalized = {
...p,
joinDateYMD: formatYMD(p.join_date)
};
const first = getFirstLetter(normalized.nickname || normalized.realName);
const letter = /^[A-Z]$/.test(first) ? first : '#';
if (!map.has(letter)) map.set(letter, []);
map.get(letter).push(normalized);
if (i > 0 && i % GROUP_BUILD_CHUNK_SIZE === 0) {
await waitForNextFrame();
}
}
const letters = Array.from(map.keys()).sort(sortLetter);
allPatientGroups.value = letters.map(l => ({ letter: l, items: map.get(l) }));
patientGroups.value = allPatientGroups.value;
rebuildIndexList();
buildGroupScrollRanges();
};
const loadRealData = async (force = false) => {
if (patientDataLoading.value) return;
if (!force && patientList.value.length > 0) return;
patientDataLoading.value = true;
loadFinish.value = false;
try {
const t1 = Date.now();
const res = await api.patientListByGBK();
if (res?.code !== 1) {
uni.showToast({ title: '获取数据失败', icon: 'none' });
return;
}
patientList.value = res.data || [];
await buildGroupsFromPatients();
loadFinish.value = true;
const t2 = Date.now();
uni.showToast({
title: `加载完成 ${patientList.value.length}条/${t2 - t1}ms`,
icon: 'none',
duration: 1500
});
} finally {
patientDataLoading.value = false;
}
};
const clearData = () => {
patientList.value = [];
patientGroups.value = [];
allPatientGroups.value = [];
indexList.value = [];
groupScrollRanges.value = [];
activeLetter.value = '';
scrollIntoViewId.value = '';
loadFinish.value = false;
};
const goBack = () => {
plus.runtime.quit();
};
onBackPress(() => {
plus.runtime.quit();
return true;
});
</script>
<style lang="scss" scoped>
.content {
background-color: #f5f5f5;
height: 100vh;
overflow-y: hidden;
}
.nav-right {
display: flex;
align-items: center;
}
.patient-list {
width: 100%;
margin-top: calc(var(--status-bar-height) + 44px);
height: calc(100vh - var(--status-bar-height) - 44px);
overflow: hidden;
background-color: #fff;
display: flex;
flex-direction: column;
}
.perf-toolbar {
display: flex;
align-items: center;
padding: 16rpx 20rpx;
border-bottom: 1rpx solid #f0f0f0;
background: #fff;
position: sticky;
top: 0;
z-index: 5;
}
.perf-btn {
padding: 10rpx 16rpx;
background: #f2f2f2;
border-radius: 8rpx;
font-size: 24rpx;
color: #555;
margin-right: 12rpx;
}
.perf-btn.primary {
background: #8B2316;
color: #fff;
}
.perf-tip {
font-size: 24rpx;
color: #999;
padding: 8rpx 20rpx 14rpx;
}
.listbox {
flex: 1;
min-height: 0;
}
.patient-building {
display: flex;
align-items: center;
justify-content: center;
}
.patient-building-text {
font-size: 28rpx;
color: #999;
margin-top: 60rpx;
}
.groups-scroll {
height: 100%;
}
.special-actions {
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.action-item {
display: flex;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.action-icon {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.new-patient {
background-color: #8B2316;
}
.action-text {
flex: 1;
font-size: 32rpx;
color: #333;
}
.group-header {
padding: 20rpx 30rpx;
background-color: #e4e4e4;
font-size: 36rpx;
color: #666;
}
.patient-item {
display: flex;
align-items: center;
padding: 30rpx 80rpx 20rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
background-color: #fff;
}
.patient-avatar {
width: 90rpx;
height: 90rpx;
border-radius: 10rpx;
margin-right: 20rpx;
}
.avatar-placeholder {
position: relative;
background: #e0e0e0;
border-radius: 50%;
}
.avatar-placeholder::before {
content: '';
position: absolute;
width: 28rpx;
height: 28rpx;
border-radius: 50%;
background: #ffffff;
top: 16rpx;
left: 31rpx;
}
.avatar-placeholder::after {
content: '';
position: absolute;
width: 56rpx;
height: 30rpx;
border-radius: 28rpx 28rpx 12rpx 12rpx;
background: #ffffff;
left: 17rpx;
bottom: 14rpx;
}
.patient-info {
flex: 1;
}
.patient-name {
font-size: 32rpx;
color: #333;
font-weight: 500;
}
.patient-status {
display: flex;
align-items: flex-end;
flex-direction: column;
}
.follow-date {
margin-top: 8rpx;
font-size: 24rpx;
color: #666;
}
.letter-index {
position: fixed;
right: 10rpx;
top: 50%;
transform: translateY(-50%);
background-color: rgba(255, 255, 255, 0.9);
border-radius: 20rpx;
padding: 10rpx 5rpx;
z-index: 999;
}
.letter-item {
padding: 8rpx 12rpx;
font-size: 24rpx;
color: #666;
text-align: center;
border-radius: 10rpx;
}
.letter-item.active {
color: #8B2316;
font-weight: 600;
}
</style>