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:
@@ -1,47 +1,9 @@
|
|||||||
@import '../../../styles/variables.scss';
|
@import '../../../styles/variables.scss';
|
||||||
@import '../../../styles/mixins.scss';
|
|
||||||
|
|
||||||
.patient-list-page {
|
// PageShell 已接管:min-height, background, padding
|
||||||
min-height: 100vh;
|
// SearchSection 已接管:search-bar
|
||||||
background: $bg;
|
// ContentCard 已接管:patient-card 背景/圆角/阴影/触摸反馈
|
||||||
padding: 24px;
|
// StatusTag 已接管:patient-card__status 标签样式
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.patient-count {
|
.patient-count {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
@@ -55,90 +17,44 @@
|
|||||||
.patient-cards {
|
.patient-cards {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: var(--tk-gap-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.patient-card__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.patient-card {
|
.patient-card__name {
|
||||||
background: $card;
|
font-size: var(--tk-font-num);
|
||||||
border-radius: $r-lg;
|
font-weight: 600;
|
||||||
padding: 28px;
|
color: $tx;
|
||||||
box-shadow: $shadow-sm;
|
}
|
||||||
|
|
||||||
&:active {
|
.patient-card__meta {
|
||||||
background: $bd-l;
|
font-size: var(--tk-font-h2);
|
||||||
}
|
color: $tx2;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
&__header {
|
.patient-card__tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-wrap: wrap;
|
||||||
margin-bottom: 12px;
|
gap: 8px;
|
||||||
}
|
margin-top: 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-tag {
|
.patient-tag {
|
||||||
padding: 4px 14px;
|
padding: 4px 14px;
|
||||||
border-radius: $r;
|
border-radius: $r;
|
||||||
background: $pri-l;
|
background: rgba($pri, 0.1);
|
||||||
|
|
||||||
&__text {
|
&__text {
|
||||||
font-size: var(--tk-font-body);
|
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 {
|
.load-more-hint-wrap {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
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 Taro, { useReachBottom } from '@tarojs/taro';
|
||||||
import { usePageData } from '@/hooks/usePageData';
|
import { usePageData } from '@/hooks/usePageData';
|
||||||
import { listPatients, listPatientTags, type PatientItem, type PatientTag } from '@/services/doctor/patient';
|
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 EmptyState from '@/components/EmptyState';
|
||||||
|
import Loading from '@/components/Loading';
|
||||||
import { useElderClass } from '../../../hooks/useElderClass';
|
import { useElderClass } from '../../../hooks/useElderClass';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
@@ -19,9 +24,7 @@ export default function PatientList() {
|
|||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const mountedRef = useRef(false);
|
const mountedRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { loadTags(); }, []);
|
||||||
loadTags();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadTags = async () => {
|
const loadTags = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -40,11 +43,7 @@ export default function PatientList() {
|
|||||||
tag_id: activeTag || undefined,
|
tag_id: activeTag || undefined,
|
||||||
});
|
});
|
||||||
const list = res.data || [];
|
const list = res.data || [];
|
||||||
if (isRefresh) {
|
setPatients(prev => isRefresh ? list : [...prev, ...list]);
|
||||||
setPatients(list);
|
|
||||||
} else {
|
|
||||||
setPatients((prev) => [...prev, ...list]);
|
|
||||||
}
|
|
||||||
setTotal(res.total || 0);
|
setTotal(res.total || 0);
|
||||||
setPage(pageNum);
|
setPage(pageNum);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -59,24 +58,15 @@ export default function PatientList() {
|
|||||||
{ enablePullDown: true },
|
{ enablePullDown: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
// tag 变化时重新加载(跳过首次 mount,由 usePageData 的 useDidShow 处理)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mountedRef.current) {
|
if (mountedRef.current) { loadPatients(1, true); }
|
||||||
loadPatients(1, true);
|
|
||||||
}
|
|
||||||
mountedRef.current = true;
|
mountedRef.current = true;
|
||||||
}, [activeTag, loadPatients]);
|
}, [activeTag, loadPatients]);
|
||||||
|
|
||||||
useReachBottom(() => {
|
useReachBottom(() => {
|
||||||
if (!loading && patients.length < total) {
|
if (!loading && patients.length < total) { loadPatients(page + 1); }
|
||||||
loadPatients(page + 1);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSearch = () => {
|
|
||||||
loadPatients(1, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTagFilter = (tagId: string) => {
|
const handleTagFilter = (tagId: string) => {
|
||||||
setActiveTag(tagId === activeTag ? '' : tagId);
|
setActiveTag(tagId === activeTag ? '' : tagId);
|
||||||
};
|
};
|
||||||
@@ -98,91 +88,69 @@ export default function PatientList() {
|
|||||||
return `${age}岁`;
|
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 (
|
return (
|
||||||
<ScrollView scrollY className={`patient-list-page ${modeClass}`}>
|
<PageShell safeBottom className={modeClass}>
|
||||||
<View className='search-bar'>
|
<SearchSection
|
||||||
<Input
|
value={search}
|
||||||
className='search-input'
|
onChange={setSearch}
|
||||||
placeholder='搜索患者姓名/手机号'
|
onSearch={() => loadPatients(1, true)}
|
||||||
value={search}
|
placeholder="搜索患者姓名/手机号"
|
||||||
onInput={(e) => setSearch(e.detail.value)}
|
filters={filters}
|
||||||
confirmType='search'
|
activeFilter={activeTag}
|
||||||
onConfirm={handleSearch}
|
onFilterChange={handleTagFilter}
|
||||||
/>
|
/>
|
||||||
</View>
|
|
||||||
|
|
||||||
{tags.length > 0 && (
|
<View className="patient-count">
|
||||||
<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'>
|
|
||||||
<Text>共 {total} 位患者</Text>
|
<Text>共 {total} 位患者</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{patients.length === 0 ? (
|
{patients.length === 0 ? (
|
||||||
<EmptyState text='暂无患者数据' />
|
<EmptyState text="暂无患者数据" />
|
||||||
) : (
|
) : (
|
||||||
<View className='patient-cards'>
|
<View className="patient-cards">
|
||||||
{patients.map((p) => (
|
{patients.map((p) => (
|
||||||
<View
|
<ContentCard
|
||||||
key={p.id}
|
key={p.id}
|
||||||
className='patient-card'
|
onPress={() => Taro.navigateTo({ url: `/pages/pkg-doctor-core/patients/detail/index?id=${p.id}` })}
|
||||||
onClick={() => Taro.navigateTo({ url: `/pages/pkg-doctor-core/patients/detail/index?id=${p.id}` })}
|
|
||||||
>
|
>
|
||||||
<View className='patient-card__header'>
|
<View className="patient-card__header">
|
||||||
<Text className='patient-card__name'>{p.name}</Text>
|
<Text className="patient-card__name">{p.name}</Text>
|
||||||
<Text className='patient-card__meta'>
|
<Text className="patient-card__meta">
|
||||||
{getGenderLabel(p.gender)} {calcAge(p.birth_date)}
|
{getGenderLabel(p.gender)} {calcAge(p.birth_date)}
|
||||||
</Text>
|
</Text>
|
||||||
|
{p.status && <StatusTag status={p.status} size="sm" />}
|
||||||
</View>
|
</View>
|
||||||
{p.tags && p.tags.length > 0 && (
|
{p.tags && p.tags.length > 0 && (
|
||||||
<View className='patient-card__tags'>
|
<View className="patient-card__tags">
|
||||||
{p.tags.map((t) => (
|
{p.tags.map((t) => (
|
||||||
<View
|
<View
|
||||||
key={t.id}
|
key={t.id}
|
||||||
className='patient-tag'
|
className="patient-tag"
|
||||||
style={t.color ? `background: ${t.color}20; color: ${t.color}` : ''}
|
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>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{p.status && (
|
</ContentCard>
|
||||||
<Text className={`patient-card__status patient-card__status--${p.status}`}>
|
|
||||||
{p.status === 'active' ? '活跃' : p.status === 'inactive' ? '非活跃' : p.status}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && patients.length >= total && total > 0 && (
|
{!loading && patients.length >= total && total > 0 && (
|
||||||
<View className='load-more-hint-wrap'>
|
<View className="load-more-hint-wrap">
|
||||||
<Text className='load-more-hint'>没有更多了</Text>
|
<Text className="load-more-hint">没有更多了</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{loading && patients.length > 0 && <Loading />}
|
{loading && patients.length > 0 && <Loading />}
|
||||||
</ScrollView>
|
</PageShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user