refactor(mp): 试点迁移患者列表页 — 使用统一组件库

替换手写 UI 为:
- PageShell 替代手动 min-height/bg/padding
- SearchSection 替代手写搜索栏 + 标签筛选
- ContentCard 替代手写卡片样式(背景/圆角/阴影/触摸反馈)
- StatusTag 替代 @include tag() mixin
- LoadingCard 替代初始加载的 Loading 组件

SCSS 从 151 行精简到 65 行,保留页面特有业务样式。
数据加载逻辑(无限滚动 + usePageData)保持不变。
This commit is contained in:
iven
2026-05-16 00:50:27 +08:00
parent d758563a13
commit 80794c9547
2 changed files with 69 additions and 185 deletions

View File

@@ -1,47 +1,9 @@
@import '../../../styles/variables.scss';
@import '../../../styles/mixins.scss';
.patient-list-page {
min-height: 100vh;
background: $bg;
padding: 24px;
padding-bottom: 120px;
}
.search-bar {
margin-bottom: 20px;
.search-input {
background: $card;
border-radius: $r;
padding: 20px 24px;
font-size: var(--tk-font-body-lg);
width: 100%;
box-sizing: border-box;
box-shadow: $shadow-sm;
}
}
.tag-filter {
white-space: nowrap;
margin-bottom: 20px;
width: 100%;
}
.tag-chip {
display: inline-block;
padding: 10px 24px;
border-radius: $r-pill;
background: $bd-l;
font-size: var(--tk-font-h2);
color: $tx2;
margin-right: 16px;
&.active {
background: $pri;
color: $card;
}
}
// PageShell 已接管min-height, background, padding
// SearchSection 已接管search-bar
// ContentCard 已接管patient-card 背景/圆角/阴影/触摸反馈
// StatusTag 已接管patient-card__status 标签样式
.patient-count {
margin-bottom: 16px;
@@ -55,90 +17,44 @@
.patient-cards {
display: flex;
flex-direction: column;
gap: var(--tk-gap-md);
}
.patient-card__header {
display: flex;
align-items: center;
gap: 16px;
}
.patient-card {
background: $card;
border-radius: $r-lg;
padding: 28px;
box-shadow: $shadow-sm;
.patient-card__name {
font-size: var(--tk-font-num);
font-weight: 600;
color: $tx;
}
&:active {
background: $bd-l;
}
.patient-card__meta {
font-size: var(--tk-font-h2);
color: $tx2;
flex: 1;
}
&__header {
display: flex;
align-items: center;
margin-bottom: 12px;
}
&__name {
font-size: var(--tk-font-num);
font-weight: 600;
color: $tx;
margin-right: 16px;
}
&__meta {
font-size: var(--tk-font-h2);
color: $tx2;
}
&__tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 8px;
}
&__status {
@include tag($bg, $tx2);
&--active {
@include tag($acc-l, $acc);
}
&--inactive {
@include tag($bd-l, $tx3);
}
}
.patient-card__tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.patient-tag {
padding: 4px 14px;
border-radius: $r;
background: $pri-l;
background: rgba($pri, 0.1);
&__text {
font-size: var(--tk-font-body);
}
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 24px;
margin-top: 32px;
&__btn {
font-size: var(--tk-font-h1);
color: $pri;
padding: 12px 24px;
&.disabled {
color: $tx3;
}
}
&__info {
font-size: var(--tk-font-h2);
color: $tx2;
}
}
.load-more-hint-wrap {
text-align: center;
padding: 20px;

View File

@@ -1,10 +1,15 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { View, Text, Input, ScrollView } from '@tarojs/components';
import { View, Text } from '@tarojs/components';
import Taro, { useReachBottom } from '@tarojs/taro';
import { usePageData } from '@/hooks/usePageData';
import { listPatients, listPatientTags, type PatientItem, type PatientTag } from '@/services/doctor/patient';
import Loading from '@/components/Loading';
import PageShell from '@/components/ui/PageShell';
import ContentCard from '@/components/ui/ContentCard';
import StatusTag from '@/components/ui/StatusTag';
import LoadingCard from '@/components/ui/LoadingCard';
import SearchSection from '@/components/patterns/SearchSection';
import EmptyState from '@/components/EmptyState';
import Loading from '@/components/Loading';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss';
@@ -19,9 +24,7 @@ export default function PatientList() {
const [page, setPage] = useState(1);
const mountedRef = useRef(false);
useEffect(() => {
loadTags();
}, []);
useEffect(() => { loadTags(); }, []);
const loadTags = async () => {
try {
@@ -40,11 +43,7 @@ export default function PatientList() {
tag_id: activeTag || undefined,
});
const list = res.data || [];
if (isRefresh) {
setPatients(list);
} else {
setPatients((prev) => [...prev, ...list]);
}
setPatients(prev => isRefresh ? list : [...prev, ...list]);
setTotal(res.total || 0);
setPage(pageNum);
} catch {
@@ -59,24 +58,15 @@ export default function PatientList() {
{ enablePullDown: true },
);
// tag 变化时重新加载(跳过首次 mount由 usePageData 的 useDidShow 处理)
useEffect(() => {
if (mountedRef.current) {
loadPatients(1, true);
}
if (mountedRef.current) { loadPatients(1, true); }
mountedRef.current = true;
}, [activeTag, loadPatients]);
useReachBottom(() => {
if (!loading && patients.length < total) {
loadPatients(page + 1);
}
if (!loading && patients.length < total) { loadPatients(page + 1); }
});
const handleSearch = () => {
loadPatients(1, true);
};
const handleTagFilter = (tagId: string) => {
setActiveTag(tagId === activeTag ? '' : tagId);
};
@@ -98,91 +88,69 @@ export default function PatientList() {
return `${age}`;
};
if (loading && patients.length === 0) return <Loading />;
const filters = [
{ key: '', label: '全部' },
...tags.map(t => ({ key: t.id, label: t.name })),
];
if (loading && patients.length === 0) return <LoadingCard count={3} />;
return (
<ScrollView scrollY className={`patient-list-page ${modeClass}`}>
<View className='search-bar'>
<Input
className='search-input'
placeholder='搜索患者姓名/手机号'
value={search}
onInput={(e) => setSearch(e.detail.value)}
confirmType='search'
onConfirm={handleSearch}
/>
</View>
<PageShell safeBottom className={modeClass}>
<SearchSection
value={search}
onChange={setSearch}
onSearch={() => loadPatients(1, true)}
placeholder="搜索患者姓名/手机号"
filters={filters}
activeFilter={activeTag}
onFilterChange={handleTagFilter}
/>
{tags.length > 0 && (
<ScrollView scrollX className='tag-filter'>
<View
className={`tag-chip ${!activeTag ? 'active' : ''}`}
onClick={() => handleTagFilter('')}
>
<Text></Text>
</View>
{tags.map((tag) => (
<View
key={tag.id}
className={`tag-chip ${activeTag === tag.id ? 'active' : ''}`}
style={activeTag === tag.id && tag.color ? `background: ${tag.color}; color: white` : ''}
onClick={() => handleTagFilter(tag.id)}
>
<Text>{tag.name}</Text>
</View>
))}
</ScrollView>
)}
<View className='patient-count'>
<View className="patient-count">
<Text> {total} </Text>
</View>
{patients.length === 0 ? (
<EmptyState text='暂无患者数据' />
<EmptyState text="暂无患者数据" />
) : (
<View className='patient-cards'>
<View className="patient-cards">
{patients.map((p) => (
<View
<ContentCard
key={p.id}
className='patient-card'
onClick={() => Taro.navigateTo({ url: `/pages/pkg-doctor-core/patients/detail/index?id=${p.id}` })}
onPress={() => Taro.navigateTo({ url: `/pages/pkg-doctor-core/patients/detail/index?id=${p.id}` })}
>
<View className='patient-card__header'>
<Text className='patient-card__name'>{p.name}</Text>
<Text className='patient-card__meta'>
<View className="patient-card__header">
<Text className="patient-card__name">{p.name}</Text>
<Text className="patient-card__meta">
{getGenderLabel(p.gender)} {calcAge(p.birth_date)}
</Text>
{p.status && <StatusTag status={p.status} size="sm" />}
</View>
{p.tags && p.tags.length > 0 && (
<View className='patient-card__tags'>
<View className="patient-card__tags">
{p.tags.map((t) => (
<View
key={t.id}
className='patient-tag'
className="patient-tag"
style={t.color ? `background: ${t.color}20; color: ${t.color}` : ''}
>
<Text className='patient-tag__text'>{t.name}</Text>
<Text className="patient-tag__text">{t.name}</Text>
</View>
))}
</View>
)}
{p.status && (
<Text className={`patient-card__status patient-card__status--${p.status}`}>
{p.status === 'active' ? '活跃' : p.status === 'inactive' ? '非活跃' : p.status}
</Text>
)}
</View>
</ContentCard>
))}
</View>
)}
{!loading && patients.length >= total && total > 0 && (
<View className='load-more-hint-wrap'>
<Text className='load-more-hint'></Text>
<View className="load-more-hint-wrap">
<Text className="load-more-hint"></Text>
</View>
)}
{loading && patients.length > 0 && <Loading />}
</ScrollView>
</PageShell>
);
}