fix(mp): 小程序页面优化 + E2E 测试报告更新

- 小程序各页面优化和修复
- 更新联调报告和 E2E 测试报告
- 更新 miniprogram wiki
This commit is contained in:
iven
2026-05-15 23:03:21 +08:00
parent ced1c0ad0c
commit c06e986090
24 changed files with 905 additions and 441 deletions

View File

@@ -57,7 +57,6 @@
font-weight: bold;
color: $tx;
line-height: 1.4;
display: block;
margin-bottom: 8px;
overflow: hidden;
text-overflow: ellipsis;

View File

@@ -4,6 +4,7 @@ import Taro, { useReachBottom } from '@tarojs/taro';
import { usePageData } from '@/hooks/usePageData';
import { listArticles, listCategories, Article, ArticleCategory } from '../../services/article';
import EmptyState from '../../components/EmptyState';
import ErrorState from '../../components/ErrorState';
import Loading from '../../components/Loading';
import { useElderClass } from '../../hooks/useElderClass';
import './index.scss';
@@ -14,6 +15,7 @@ export default function ArticleList() {
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [categories, setCategories] = useState<ArticleCategory[]>([]);
const [activeCategory, setActiveCategory] = useState<string | null>(null);
@@ -28,6 +30,7 @@ export default function ArticleList() {
const fetchData = useCallback(async (p: number, append = false, categoryId?: string | null) => {
setLoading(true);
setError(false);
try {
const cid = categoryId !== undefined ? categoryId : activeCategory;
const res = await listArticles({
@@ -39,6 +42,7 @@ export default function ArticleList() {
setTotal(res.total);
setPage(p);
} catch {
setError(true);
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
setLoading(false);
@@ -93,7 +97,9 @@ export default function ArticleList() {
)}
<View className='article-list'>
{articles.map((a) => (
{error ? (
<ErrorState onRetry={() => fetchData(1, false, null)} />
) : articles.map((a) => (
<View
className='article-card'
key={a.id}

View File

@@ -5,7 +5,9 @@ import { useAuthStore } from '../../stores/auth';
import { useElderClass } from '../../hooks/useElderClass';
import { findThreshold, inputVitalSign, type HealthThreshold } from '../../services/health';
import Loading from '../../components/Loading';
import ErrorState from '../../components/ErrorState';
import GuestGuard from '../../components/GuestGuard';
import SegmentTabs from '../../components/SegmentTabs';
import { useHealthData, VITAL_TABS, type VitalType } from './useHealthData';
import './index.scss';
@@ -28,8 +30,8 @@ export default function Health() {
const currentPatient = useAuthStore((s) => s.currentPatient);
const modeClass = useElderClass();
const {
user, todaySummary, loading, activeTab, trendData, trendLoading,
aiSuggestions, thresholds, handleTabChange, loadTrend, refreshToday,
user, todaySummary, loading, error, activeTab, trendData, trendLoading,
aiSuggestions, thresholds, handleTabChange, loadTrend, refreshToday, fetchData,
} = useHealthData();
const [systolic, setSystolic] = useState('');
@@ -44,6 +46,17 @@ export default function Health() {
return <GuestGuard title='请先登录' desc='登录后即可记录和查看健康数据' />;
}
if (error) {
return (
<View className={`health-page ${modeClass}`}>
<View className='health-header'>
<Text className='health-title'></Text>
</View>
<ErrorState onRetry={fetchData} />
</View>
);
}
const getWarnStatus = (type: VitalType): string | null => {
if (type === 'blood_pressure') {
const sys = parseFloat(systolic);
@@ -184,24 +197,7 @@ export default function Health() {
</View>
)}
<View className='vital-tabs'>
{VITAL_TABS.map((tab) => {
const hasData = tab.key === 'blood_pressure' ? !!todaySummary?.blood_pressure
: tab.key === 'heart_rate' ? !!todaySummary?.heart_rate
: tab.key === 'blood_sugar' ? !!todaySummary?.blood_sugar
: !!todaySummary?.weight;
return (
<View
key={tab.key}
className={`vital-tab ${activeTab === tab.key ? 'vital-tab-active' : ''}`}
onClick={() => handleTabChange(tab.key)}
>
<Text className='vital-tab-text'>{tab.label}</Text>
{!hasData && <View className='vital-tab-dot' />}
</View>
);
})}
</View>
<SegmentTabs tabs={VITAL_TABS} activeKey={activeTab} onChange={handleTabChange} variant="pill" />
<View className='input-section'>
{activeTab === 'blood_pressure' && (

View File

@@ -31,6 +31,7 @@ export function useHealthData() {
const [trendLoading, setTrendLoading] = useState(false);
const [aiSuggestions, setAiSuggestions] = useState<AiSuggestionItem[]>([]);
const [thresholds, setThresholds] = useState<HealthThreshold[]>(DEFAULT_THRESHOLDS);
const [error, setError] = useState(false);
const loadTrend = async (type: VitalType) => {
setTrendLoading(true);
@@ -60,12 +61,15 @@ export function useHealthData() {
};
const fetchData = async () => {
await Promise.allSettled([
setError(false);
const results = await Promise.allSettled([
refreshToday(),
loadTrend(activeTab),
loadAiSuggestions(),
getHealthThresholds().then((t) => { if (t.length > 0) setThresholds(t); }),
]);
const hasError = results.some((r) => r.status === 'rejected');
if (hasError) setError(true);
};
usePageData(fetchData, {
@@ -83,6 +87,7 @@ export function useHealthData() {
user,
todaySummary,
loading,
error,
activeTab,
trendData,
trendLoading,
@@ -92,5 +97,6 @@ export function useHealthData() {
loadTrend,
refreshToday,
fetchTrend,
fetchData,
};
}

View File

@@ -7,6 +7,9 @@ import './index.scss';
const IS_DEV = process.env.NODE_ENV !== 'production';
// 运行时检测是否在 DevTools 模拟器中(弥补编译时 IS_DEV 在 production 构建中为 false 的问题)
const IS_SIMULATOR = typeof __wxConfig !== 'undefined' && (__wxConfig as any).envVersion !== 'release';
export default function Login() {
const modeClass = useElderClass();
const [needBind, setNeedBind] = useState(false);
@@ -124,7 +127,7 @@ export default function Login() {
>
</Button>
{IS_DEV && (
{(IS_DEV || IS_SIMULATOR) && (
<Button className='login-btn login-btn--dev' onClick={handleDevQuickLogin} loading={loading}>
</Button>

View File

@@ -7,6 +7,7 @@ import type { PointsProduct } from '../../services/points';
import { useAuthStore } from '../../stores/auth';
import { usePointsStore } from '../../stores/points';
import Loading from '../../components/Loading';
import ErrorState from '../../components/ErrorState';
import { useElderClass } from '../../hooks/useElderClass';
import './index.scss';
@@ -37,11 +38,13 @@ export default function Mall() {
const [loading, setLoading] = useState(false);
const [checkinLoading, setCheckinLoading] = useState(false);
const [noProfile, setNoProfile] = useState(false);
const [error, setError] = useState(false);
const modeClass = useElderClass();
const fetchProducts = useCallback(
async (pageNum: number, type: string, isRefresh = false) => {
setLoading(true);
setError(false);
try {
const res = await listProducts({
page: pageNum,
@@ -57,6 +60,7 @@ export default function Mall() {
setTotal(res.total);
setPage(pageNum);
} catch {
setError(true);
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
setLoading(false);
@@ -188,7 +192,9 @@ export default function Mall() {
</View>
{/* 商品列表 */}
{products.length === 0 && !loading ? (
{error ? (
<ErrorState onRetry={() => loadAll()} />
) : products.length === 0 && !loading ? (
<View className='mall-empty-state'>
<View className='empty-icon'>
<Text className='empty-char'></Text>

View File

@@ -4,6 +4,7 @@ import Taro, { useReachBottom } from '@tarojs/taro';
import { listConsultations, ConsultationSession } from '../../services/consultation';
import { notificationService } from '../../services/notification';
import Loading from '../../components/Loading';
import ErrorState from '../../components/ErrorState';
import GuestGuard from '../../components/GuestGuard';
import { useAuthStore } from '../../stores/auth';
import { useElderClass } from '../../hooks/useElderClass';
@@ -36,11 +37,13 @@ export default function Messages() {
const [sessions, setSessions] = useState<ConsultationSession[]>([]);
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const loadData = useCallback(async (tab: MsgTab, pageNum: number = 1, isRefresh = false) => {
setLoading(true);
setError(false);
try {
if (tab === 'consultation') {
const res = await listConsultations({ page: pageNum, page_size: 20 });
@@ -63,11 +66,12 @@ export default function Messages() {
}
setPage(pageNum);
} catch {
setError(true);
if (isRefresh) {
if (tab === 'consultation') setSessions([]);
else setNotifications([]);
Taro.showToast({ title: '加载失败,下拉重试', icon: 'none' });
}
Taro.showToast({ title: '加载失败,下拉重试', icon: 'none' });
} finally {
setLoading(false);
}
@@ -139,6 +143,10 @@ export default function Messages() {
</View>
<View className='msg-content'>
{error ? (
<ErrorState onRetry={() => loadData(activeTab, 1, true)} />
) : (
<>
{/* 咨询列表 */}
{activeTab === 'consultation' && (
loading ? (
@@ -220,6 +228,8 @@ export default function Messages() {
</View>
)
)}
</>
)}
</View>
</View>
);

View File

@@ -4,7 +4,9 @@ import Taro from '@tarojs/taro';
import { usePageData } from '@/hooks/usePageData';
import { listAlerts, type Alert } from '@/services/doctor/alerts';
import Loading from '@/components/Loading';
import ErrorState from '@/components/ErrorState';
import EmptyState from '@/components/EmptyState';
import SegmentTabs from '@/components/SegmentTabs';
import { useElderClass } from '../../../hooks/useElderClass';
import { safeNavigateTo } from '@/utils/navigate';
import './index.scss';
@@ -34,6 +36,7 @@ export default function AlertList() {
const modeClass = useElderClass();
const [alerts, setAlerts] = useState<Alert[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [activeTab, setActiveTab] = useState('');
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
@@ -43,6 +46,7 @@ export default function AlertList() {
const loadAlerts = useCallback(async () => {
setLoading(true);
setError(false);
try {
const res = await listAlerts({
status: activeTab || undefined,
@@ -52,6 +56,7 @@ export default function AlertList() {
setAlerts(res.data || []);
setTotal(res.total || 0);
} catch {
setError(true);
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
setLoading(false);
@@ -91,6 +96,28 @@ export default function AlertList() {
if (loading && alerts.length === 0) return <Loading />;
if (error) {
return (
<ScrollView scrollY className={`alert-list-page ${modeClass}`}>
<View className='alert-list-header'>
<Text className='alert-list-title'></Text>
</View>
<View className='alert-tabs'>
{STATUS_TABS.map((tab) => (
<Text
key={tab.value}
className={`alert-tab ${activeTab === tab.value ? 'alert-tab--active' : ''}`}
onClick={() => handleTabChange(tab.value)}
>
{tab.label}
</Text>
))}
</View>
<ErrorState onRetry={loadAlerts} />
</ScrollView>
);
}
return (
<ScrollView scrollY className={`alert-list-page ${modeClass}`}>
<View className='alert-list-header'>
@@ -98,17 +125,7 @@ export default function AlertList() {
<Text className='alert-list-count'> {total} </Text>
</View>
<View className='alert-tabs'>
{STATUS_TABS.map((tab) => (
<Text
key={tab.value}
className={`alert-tab ${activeTab === tab.value ? 'alert-tab--active' : ''}`}
onClick={() => handleTabChange(tab.value)}
>
{tab.label}
</Text>
))}
</View>
<SegmentTabs tabs={STATUS_TABS.map(t => ({ key: t.value, label: t.label }))} activeKey={activeTab} onChange={handleTabChange} variant="pill" />
{alerts.length === 0 ? (
<EmptyState description='暂无告警' />

View File

@@ -5,10 +5,12 @@ import { usePageData } from '@/hooks/usePageData';
import { listDialysisRecords, type DialysisRecord } from '@/services/doctor/dialysis';
import { listPatients } from '@/services/doctor/patient';
import Loading from '@/components/Loading';
import ErrorState from '@/components/ErrorState';
import EmptyState from '@/components/EmptyState';
import { useElderClass } from '../../../hooks/useElderClass';
import { safeNavigateTo } from '@/utils/navigate';
import './index.scss';
import SegmentTabs from '@/components/SegmentTabs';
import './index.scss';;
const TABS = [
{ key: '', label: '全部' },
@@ -28,6 +30,7 @@ export default function DialysisList() {
const [activeTab, setActiveTab] = useState('');
const [records, setRecords] = useState<DialysisRecord[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const mountedRef = useRef(false);
@@ -35,6 +38,7 @@ export default function DialysisList() {
const loadRecords = useCallback(async (p: number) => {
if (!currentPatientId) return;
setLoading(true);
setError(false);
try {
const params: { page: number; page_size: number; status?: string } = { page: p, page_size: 20 };
if (activeTab) params.status = activeTab;
@@ -43,6 +47,7 @@ export default function DialysisList() {
setTotal(res.total || 0);
setPage(p);
} catch {
setError(true);
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
setLoading(false);
@@ -88,6 +93,7 @@ export default function DialysisList() {
// 服务端已按 activeTab 过滤,无需客户端二次筛选
if (loading && records.length === 0) return <Loading />;
if (error) return <ErrorState onRetry={() => loadRecords(1)} />;
return (
<ScrollView scrollY className={`dialysis-page ${modeClass}`}>
@@ -104,17 +110,7 @@ export default function DialysisList() {
</View>
)}
<View className='tabs'>
{TABS.map((t) => (
<View
key={t.key}
className={`tab ${activeTab === t.key ? 'tab--active' : ''}`}
onClick={() => handleTab(t.key)}
>
<Text className='tab-text'>{t.label}</Text>
</View>
))}
</View>
<SegmentTabs tabs={TABS} activeKey={activeTab} onChange={handleTab} variant="underline" />
{!currentPatientId ? (
<EmptyState text='请搜索并选择患者' />

View File

@@ -5,9 +5,11 @@ import { usePageData } from '@/hooks/usePageData';
import { listDialysisPrescriptions, type DialysisPrescription } from '@/services/doctor/dialysis';
import { listPatients } from '@/services/doctor/patient';
import Loading from '@/components/Loading';
import ErrorState from '@/components/ErrorState';
import EmptyState from '@/components/EmptyState';
import { useElderClass } from '../../../hooks/useElderClass';
import { safeNavigateTo } from '@/utils/navigate';
import SegmentTabs from '@/components/SegmentTabs';
import './index.scss';
const TABS = [
@@ -25,12 +27,14 @@ export default function PrescriptionList() {
const [activeTab, setActiveTab] = useState('');
const [prescriptions, setPrescriptions] = useState<DialysisPrescription[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const mountedRef = useRef(false);
const loadData = useCallback(async (p: number) => {
setLoading(true);
setError(false);
try {
const res = await listDialysisPrescriptions({
patient_id: currentPatientId || undefined,
@@ -42,6 +46,7 @@ export default function PrescriptionList() {
setTotal(res.total || 0);
setPage(p);
} catch {
setError(true);
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
setLoading(false);
@@ -78,6 +83,7 @@ export default function PrescriptionList() {
};
if (loading && prescriptions.length === 0) return <Loading />;
if (error) return <ErrorState onRetry={() => loadData(1)} />;
return (
<ScrollView scrollY className={`prescription-page ${modeClass}`}>
@@ -94,17 +100,7 @@ export default function PrescriptionList() {
</View>
)}
<View className='tabs'>
{TABS.map((t) => (
<View
key={t.key}
className={`tab ${activeTab === t.key ? 'tab--active' : ''}`}
onClick={() => { setActiveTab(t.key); setPage(1); }}
>
<Text className='tab-text'>{t.label}</Text>
</View>
))}
</View>
<SegmentTabs tabs={TABS} activeKey={activeTab} onChange={(key) => { setActiveTab(key); setPage(1); }} variant="underline" />
{prescriptions.length === 0 ? (
<EmptyState text='暂无透析处方' />

View File

@@ -5,6 +5,7 @@ import { usePageData } from '@/hooks/usePageData';
import { listLabReports, type LabReportItem } from '@/services/doctor/labReport';
import { listPatients } from '@/services/doctor/patient';
import Loading from '@/components/Loading';
import ErrorState from '@/components/ErrorState';
import EmptyState from '@/components/EmptyState';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss';
@@ -17,17 +18,20 @@ export default function ReportList() {
const [currentPatientId, setCurrentPatientId] = useState(patientId);
const [reports, setReports] = useState<LabReportItem[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [total, setTotal] = useState(0);
const mountedRef = useRef(false);
const loadReports = useCallback(async () => {
if (!currentPatientId) return;
setLoading(true);
setError(false);
try {
const res = await listLabReports(currentPatientId, { page: 1, page_size: 50 });
setReports(res.data || []);
setTotal(res.total || 0);
} catch {
setError(true);
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
setLoading(false);
@@ -65,6 +69,7 @@ export default function ReportList() {
const formatDate = (d: string) => new Date(d).toLocaleDateString('zh-CN');
if (loading && reports.length === 0) return <Loading />;
if (error) return <ErrorState onRetry={loadReports} />;
return (
<ScrollView scrollY className={`report-page ${modeClass}`}>

View File

@@ -10,6 +10,8 @@ import {
type ThreadResponse,
} from '@/services/action-inbox';
import Loading from '@/components/Loading';
import ErrorState from '@/components/ErrorState';
import SegmentTabs from '@/components/SegmentTabs';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss';
@@ -40,6 +42,7 @@ export default function ActionInboxPage() {
const [total, setTotal] = useState(0);
const [_page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [activeTab, setActiveTab] = useState('');
const [threadData, setThreadData] = useState<ThreadResponse | null>(null);
const [showDetail, setShowDetail] = useState(false);
@@ -50,6 +53,7 @@ export default function ActionInboxPage() {
if (loadingRef.current) return;
loadingRef.current = true;
setLoading(true);
setError(false);
try {
const resp = await listActionItems({
page: pageNum,
@@ -65,6 +69,7 @@ export default function ActionInboxPage() {
setTotal(resp.total);
setPage(pageNum);
} catch {
setError(true);
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
setLoading(false);
@@ -114,23 +119,11 @@ export default function ActionInboxPage() {
return (
<View className={`action-inbox-page ${modeClass}`}>
<View className="inbox-tabs">
{STATUS_TABS.map((tab) => (
<View
key={tab.key}
className={`inbox-tab ${activeTab === tab.key ? 'active' : ''}`}
onClick={() => handleTabChange(tab.key)}
>
<Text
className={`inbox-tab-text ${activeTab === tab.key ? 'active' : ''}`}
>
{tab.label}
</Text>
</View>
))}
</View>
<SegmentTabs tabs={STATUS_TABS} activeKey={activeTab} onChange={handleTabChange} variant="underline" />
{items.length === 0 && !loading ? (
{error ? (
<ErrorState onRetry={() => fetchItems(1, activeTab, true)} />
) : items.length === 0 && !loading ? (
<View className="inbox-empty">
<Text className="inbox-empty-text"></Text>
</View>

View File

@@ -4,11 +4,13 @@ import Taro from '@tarojs/taro';
import { usePageData } from '@/hooks/usePageData';
import { listSessions, type ConsultationSession } from '@/services/doctor/consultation';
import Loading from '@/components/Loading';
import ErrorState from '@/components/ErrorState';
import EmptyState from '@/components/EmptyState';
import { useElderClass } from '../../../hooks/useElderClass';
import { getStatusInlineStyle, getStatusLabel } from '@/utils/statusTag';
import { formatDateTime } from '@/utils/date';
import { safeNavigateTo } from '@/utils/navigate';
import SegmentTabs from '@/components/SegmentTabs';
import './index.scss';
const TABS = [
@@ -23,6 +25,7 @@ export default function ConsultationList() {
const [sessions, setSessions] = useState<ConsultationSession[]>([]);
const [activeTab, setActiveTab] = useState('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const mountedRef = useRef(false);
@@ -31,6 +34,7 @@ export default function ConsultationList() {
const loadSessions = useCallback(async () => {
setLoading(true);
setError(false);
try {
const res = await listSessions({
page,
@@ -40,6 +44,7 @@ export default function ConsultationList() {
setSessions(res.data || []);
setTotal(res.total || 0);
} catch {
setError(true);
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
setLoading(false);
@@ -67,20 +72,11 @@ export default function ConsultationList() {
};
if (loading && sessions.length === 0) return <Loading />;
if (error) return <ErrorState onRetry={loadSessions} />;
return (
<ScrollView scrollY className={`consultation-page ${modeClass}`}>
<View className='tabs'>
{TABS.map((t) => (
<View
key={t.key}
className={`tab ${activeTab === t.key ? 'tab--active' : ''}`}
onClick={() => handleTabChange(t.key)}
>
<Text>{t.label}</Text>
</View>
))}
</View>
<SegmentTabs tabs={TABS} activeKey={activeTab} onChange={handleTabChange} variant="underline" />
{sessions.length === 0 ? (
<EmptyState text='暂无咨询会话' />

View File

@@ -4,9 +4,11 @@ import Taro, { useRouter } from '@tarojs/taro';
import { usePageData } from '@/hooks/usePageData';
import { listFollowUpTasks, type FollowUpTask } from '@/services/doctor/followup';
import Loading from '@/components/Loading';
import ErrorState from '@/components/ErrorState';
import EmptyState from '@/components/EmptyState';
import { useElderClass } from '../../../hooks/useElderClass';
import { getStatusInlineStyle, getStatusLabel } from '@/utils/statusTag';
import SegmentTabs from '@/components/SegmentTabs';
import './index.scss';
const TABS = [
@@ -24,11 +26,13 @@ export default function FollowUpList() {
const [tasks, setTasks] = useState<FollowUpTask[]>([]);
const [activeTab, setActiveTab] = useState('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [total, setTotal] = useState(0);
const mountedRef = useRef(false);
const loadTasks = useCallback(async () => {
setLoading(true);
setError(false);
try {
const res = await listFollowUpTasks({
page: 1,
@@ -39,6 +43,7 @@ export default function FollowUpList() {
setTasks(res.data || []);
setTotal(res.total || 0);
} catch {
setError(true);
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
setLoading(false);
@@ -70,20 +75,11 @@ export default function FollowUpList() {
};
if (loading && tasks.length === 0) return <Loading />;
if (error) return <ErrorState onRetry={loadTasks} />;
return (
<ScrollView scrollY className={`followup-page ${modeClass}`}>
<View className='tabs'>
{TABS.map((t) => (
<View
key={t.key}
className={`tab ${activeTab === t.key ? 'tab--active' : ''}`}
onClick={() => setActiveTab(t.key)}
>
<Text>{t.label}</Text>
</View>
))}
</View>
<SegmentTabs tabs={TABS} activeKey={activeTab} onChange={(key) => setActiveTab(key)} variant="underline" />
<View className='task-count'>
<Text> {total} </Text>

View File

@@ -5,7 +5,7 @@
min-height: 100vh;
background: $bg;
padding: 32px;
padding-bottom: 120px;
padding-bottom: calc(160px + env(safe-area-inset-bottom));
&__header {
margin-bottom: 40px;

View File

@@ -5,6 +5,8 @@ import { usePageData } from '@/hooks/usePageData';
import { listPatientAlerts, type Alert } from '@/services/alert';
import { useAuthStore } from '@/stores/auth';
import Loading from '@/components/Loading';
import ErrorState from '@/components/ErrorState';
import SegmentTabs from '@/components/SegmentTabs';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss';
@@ -30,11 +32,13 @@ export default function PatientAlerts() {
const [page, setPage] = useState(1);
const [status, setStatus] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const fetchAlerts = useCallback(
async (pageNum: number, s: string, isRefresh = false) => {
if (!currentPatient) return;
setLoading(true);
setError(false);
try {
const res = await listPatientAlerts(currentPatient.id, {
page: pageNum,
@@ -50,6 +54,7 @@ export default function PatientAlerts() {
setTotal(res.total);
setPage(pageNum);
} catch {
setError(true);
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
setLoading(false);
@@ -74,35 +79,26 @@ export default function PatientAlerts() {
if (!currentPatient) {
return (
<View className={`alerts-page ${modeClass}`}>
<View className='alerts-empty'>
<Text className='alerts-empty-text'></Text>
<View className='alerts-empty-action' onClick={() => Taro.navigateTo({ url: '/pages/pkg-profile/family-add/index' })}>
<Text className='alerts-empty-action-text'></Text>
</View>
</View>
<ErrorState text='请先完善个人档案' onRetry={() => Taro.navigateTo({ url: '/pages/pkg-profile/family-add/index' })} />
</View>
);
}
if (error) {
return (
<View className={`alerts-page ${modeClass}`}>
<SegmentTabs tabs={STATUS_TABS} activeKey={status} onChange={handleTabChange} variant="pill" />
<ErrorState onRetry={() => fetchAlerts(1, status, true)} />
</View>
);
}
return (
<View className={`alerts-page ${modeClass}`}>
<View className='alerts-tabs'>
{STATUS_TABS.map((tab) => (
<View
key={tab.key}
className={`alerts-tab ${status === tab.key ? 'active' : ''}`}
onClick={() => handleTabChange(tab.key)}
>
<Text className={`alerts-tab-text ${status === tab.key ? 'active' : ''}`}>{tab.label}</Text>
</View>
))}
</View>
<SegmentTabs tabs={STATUS_TABS} activeKey={status} onChange={handleTabChange} variant="pill" />
{alerts.length === 0 && !loading ? (
<View className='alerts-empty'>
<Text className='alerts-empty-text'></Text>
<Text className='alerts-empty-hint'></Text>
</View>
<ErrorState text='暂无告警记录' hint='您的各项指标正常' />
) : (
<View className='alerts-list'>
{alerts.map((item) => {

View File

@@ -5,7 +5,9 @@ import { usePageData } from '@/hooks/usePageData';
import { useHealthStore } from '@/stores/health';
import TrendChart from '@/components/TrendChart';
import Loading from '@/components/Loading';
import ErrorState from '@/components/ErrorState';
import EmptyState from '@/components/EmptyState';
import SegmentTabs from '@/components/SegmentTabs';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss';
@@ -32,14 +34,17 @@ export default function Trend() {
const [range, setRange] = useState('7d');
const [points, setPoints] = useState<{ date: string; value: number }[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const getTrend = useHealthStore((s) => s.getTrend);
const fetchTrend = useCallback(async () => {
setLoading(true);
setError(false);
try {
const data = await getTrend(indicator, range);
setPoints(data);
} catch {
setError(true);
setPoints([]);
} finally {
setLoading(false);
@@ -72,25 +77,17 @@ export default function Trend() {
</View>
{/* 时间范围切换 */}
<View className='trange-wrap'>
{RANGE_OPTIONS.map((opt) => (
<View
key={opt.value}
className={`trange-tab ${range === opt.value ? 'trange-tab-active' : ''}`}
onClick={() => setRange(opt.value)}
>
<Text className={`trange-tab-text ${range === opt.value ? 'trange-tab-text-active' : ''}`}>
{opt.label}
</Text>
</View>
))}
</View>
<SegmentTabs tabs={RANGE_OPTIONS.map(o => ({ key: o.value, label: o.label }))} activeKey={range} onChange={setRange} variant="pill" />
{/* ECharts 折线图 */}
{loading ? (
<View className='trend-chart-card'>
<Loading />
</View>
) : error ? (
<View className='trend-chart-card'>
<ErrorState onRetry={fetchTrend} />
</View>
) : points.length === 0 ? (
<View className='trend-chart-card'>
<EmptyState text='暂无趋势数据' />

View File

@@ -5,7 +5,9 @@ import { usePageData } from '@/hooks/usePageData';
import { listMyOrders } from '../../../services/points';
import type { PointsOrder } from '../../../services/points';
import EmptyState from '../../../components/EmptyState';
import ErrorState from '../../../components/ErrorState';
import Loading from '../../../components/Loading';
import SegmentTabs from '../../../components/SegmentTabs';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss';
@@ -30,10 +32,12 @@ export default function MallOrders() {
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const fetchOrders = useCallback(
async (pageNum: number, status: string, isRefresh = false) => {
setLoading(true);
setError(false);
try {
const res = await listMyOrders({
page: pageNum,
@@ -51,6 +55,7 @@ export default function MallOrders() {
setTotal(res.total);
setPage(pageNum);
} catch {
setError(true);
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
setLoading(false);
@@ -108,20 +113,12 @@ export default function MallOrders() {
return (
<View className={`orders-page ${modeClass}`}>
{/* 状态筛选标签 */}
<View className='status-tabs'>
{STATUS_TABS.map((tab) => (
<View
key={tab.key}
className={`status-tab ${activeTab === tab.key ? 'active' : ''}`}
onClick={() => handleTabChange(tab.key)}
>
<Text className='status-tab-text'>{tab.label}</Text>
</View>
))}
</View>
<SegmentTabs tabs={STATUS_TABS} activeKey={activeTab} onChange={handleTabChange} variant="underline" />
{/* 订单列表 */}
{orders.length === 0 && !loading ? (
{error ? (
<ErrorState onRetry={() => fetchOrders(1, activeTab, true)} />
) : orders.length === 0 && !loading ? (
<EmptyState
icon=''
text='暂无订单'

View File

@@ -4,6 +4,7 @@ import Taro from '@tarojs/taro';
import { usePageData } from '@/hooks/usePageData';
import { listTasks, FollowUpTask } from '../../../services/followup';
import EmptyState from '../../../components/EmptyState';
import ErrorState from '../../../components/ErrorState';
import Loading from '../../../components/Loading';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss';
@@ -19,13 +20,16 @@ export default function MyFollowUps() {
const [activeTab, setActiveTab] = useState('pending');
const [tasks, setTasks] = useState<FollowUpTask[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const fetchTasks = useCallback(async (status: string) => {
setLoading(true);
setError(false);
try {
const res = await listTasks(status);
setTasks(res.data || []);
} catch {
setError(true);
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
setLoading(false);
@@ -70,34 +74,40 @@ export default function MyFollowUps() {
))}
</View>
<View className='task-list'>
{tasks.map((t) => (
<View
className='task-card'
key={t.id}
onClick={() => goToDetail(t.id)}
>
<View className='task-top'>
<Text className='task-name'>{t.follow_up_type}</Text>
<Text className={`task-status ${getStatusClass(t.status)}`}>
{getStatusLabel(t.status)}
</Text>
</View>
<Text className='task-desc'>{t.content_template}</Text>
<Text className='task-due'>: {t.planned_date}</Text>
{error ? (
<ErrorState onRetry={() => fetchTasks(activeTab)} />
) : (
<>
<View className='task-list'>
{tasks.map((t) => (
<View
className='task-card'
key={t.id}
onClick={() => goToDetail(t.id)}
>
<View className='task-top'>
<Text className='task-name'>{t.follow_up_type}</Text>
<Text className={`task-status ${getStatusClass(t.status)}`}>
{getStatusLabel(t.status)}
</Text>
</View>
<Text className='task-desc'>{t.content_template}</Text>
<Text className='task-due'>: {t.planned_date}</Text>
</View>
))}
</View>
))}
</View>
{tasks.length === 0 && !loading && (
<EmptyState text={`暂无${(() => {
const tab = TABS.find((t) => t.key === activeTab);
return tab ? tab.label : '';
})()}任务`} />
)}
{tasks.length === 0 && !loading && (
<EmptyState text={`暂无${(() => {
const tab = TABS.find((t) => t.key === activeTab);
return tab ? tab.label : '';
})()}任务`} />
)}
{loading && (
<Loading />
{loading && (
<Loading />
)}
</>
)}
</View>
);