fix(miniprogram): 多角色找茬模式发现并修复 16 个问题
P0 Bug: - 健康 AI 建议幽灵路径 pkg-appointment → appointment/create - 血糖 indicator_type 始终 blood_sugar,不区分空腹/餐后 - 商城订单页 switchTab 跳转非 TabBar 页面 P1 设计系统: - Profile/Index 页 emoji 图标替换为衬线首字 - Profile 硬编码颜色替换为 SCSS 变量 class - alerts/action-inbox 两个页面全面接入设计系统 - ai-report/detail 删除重复 mixin 定义 - ErrorBoundary 添加重试按钮移除 emoji - 新增 $r-xs: 8px 圆角变量 P1 导航/交互: - Profile 补充 4 个缺失菜单(透析/知情同意/用药/活动) - Settings 隐私政策改为跳转实际页面 - 全局启用 enablePullDownRefresh - 首页/健康页添加下拉刷新 - 咨询/消息列表添加分页加载更多 - 医生端患者列表改为上拉加载 - 首页/健康页间距统一为 24px
This commit is contained in:
@@ -88,5 +88,6 @@ export default defineAppConfig({
|
|||||||
navigationBarBackgroundColor: '#FFFFFF',
|
navigationBarBackgroundColor: '#FFFFFF',
|
||||||
navigationBarTitleText: '健康管理',
|
navigationBarTitleText: '健康管理',
|
||||||
navigationBarTextStyle: 'black',
|
navigationBarTextStyle: 'black',
|
||||||
|
enablePullDownRefresh: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,13 +23,25 @@ export default class ErrorBoundary extends Component<Props, State> {
|
|||||||
console.error('[ErrorBoundary]', error, info.componentStack);
|
console.error('[ErrorBoundary]', error, info.componentStack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleRetry = () => {
|
||||||
|
this.setState({ hasError: false });
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.hasError) {
|
if (this.state.hasError) {
|
||||||
return (
|
return (
|
||||||
<View style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '60vh', padding: '40px' }}>
|
<View style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '60vh', padding: '40px 24px' }}>
|
||||||
<Text style={{ fontSize: '48px', marginBottom: '20px' }}>😵</Text>
|
<View style={{ width: '64px', height: '64px', borderRadius: '32px', background: '#F0DDD4', display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: '20px' }}>
|
||||||
<Text style={{ fontSize: '32px', color: '#134E4A', marginBottom: '12px' }}>页面出了点问题</Text>
|
<Text style={{ fontFamily: 'Georgia, serif', fontSize: '28px', fontWeight: 600, color: '#8B3E1F' }}>!</Text>
|
||||||
<Text style={{ fontSize: '24px', color: '#94A3B8', marginBottom: '24px' }}>请返回重试</Text>
|
</View>
|
||||||
|
<Text style={{ fontSize: '32px', color: '#2D2A26', marginBottom: '12px', fontWeight: 600 }}>页面出了点问题</Text>
|
||||||
|
<Text style={{ fontSize: '24px', color: '#78716C', marginBottom: '32px' }}>请返回重试</Text>
|
||||||
|
<View
|
||||||
|
onClick={this.handleRetry}
|
||||||
|
style={{ background: '#C4623A', borderRadius: '12px', padding: '14px 48px' }}
|
||||||
|
>
|
||||||
|
<Text style={{ color: '#FFFFFF', fontSize: '28px' }}>重新加载</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,5 @@
|
|||||||
@import '../../../styles/variables.scss';
|
@import '../../../styles/variables.scss';
|
||||||
|
@import '../../../styles/mixins.scss';
|
||||||
@mixin section-title {
|
|
||||||
font-family: 'Georgia', 'Times New Roman', serif;
|
|
||||||
font-size: 30px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: $tx;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin tag($bg, $color) {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 500;
|
|
||||||
background: $bg;
|
|
||||||
color: $color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-page {
|
.detail-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
import { View, Text } from '@tarojs/components';
|
import { View, Text } from '@tarojs/components';
|
||||||
import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
|
import Taro, { useDidShow, usePullDownRefresh, useReachBottom } from '@tarojs/taro';
|
||||||
import { listConsultations, ConsultationSession } from '@/services/consultation';
|
import { listConsultations, ConsultationSession } from '@/services/consultation';
|
||||||
import Loading from '../../components/Loading';
|
import Loading from '../../components/Loading';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
@@ -33,32 +33,51 @@ export default function Consultation() {
|
|||||||
const [sessions, setSessions] = useState<ConsultationSession[]>([]);
|
const [sessions, setSessions] = useState<ConsultationSession[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const loadingRef = useRef(false);
|
||||||
|
|
||||||
const loadSessions = async () => {
|
const loadSessions = async (pageNum: number, isRefresh = false) => {
|
||||||
setLoading(true);
|
if (loadingRef.current) return;
|
||||||
|
loadingRef.current = true;
|
||||||
|
if (isRefresh) setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const resp = await listConsultations({ page: 1, page_size: 20 });
|
const resp = await listConsultations({ page: pageNum, page_size: 20 });
|
||||||
setSessions(resp.data || []);
|
const list = resp.data || [];
|
||||||
|
if (isRefresh) {
|
||||||
|
setSessions(list);
|
||||||
|
} else {
|
||||||
|
setSessions((prev) => [...prev, ...list]);
|
||||||
|
}
|
||||||
|
setTotal(resp.total || 0);
|
||||||
|
setPage(pageNum);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const msg = e instanceof Error ? e.message : '加载失败';
|
const msg = e instanceof Error ? e.message : '加载失败';
|
||||||
setError(msg);
|
setError(msg);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
loadingRef.current = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useDidShow(() => {
|
useDidShow(() => {
|
||||||
Taro.setNavigationBarTitle({ title: '在线咨询' });
|
Taro.setNavigationBarTitle({ title: '在线咨询' });
|
||||||
loadSessions();
|
loadSessions(1, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
usePullDownRefresh(() => {
|
usePullDownRefresh(() => {
|
||||||
loadSessions().finally(() => {
|
loadSessions(1, true).finally(() => {
|
||||||
Taro.stopPullDownRefresh();
|
Taro.stopPullDownRefresh();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useReachBottom(() => {
|
||||||
|
if (!loading && sessions.length < total) {
|
||||||
|
loadSessions(page + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const handleTapSession = (session: ConsultationSession) => {
|
const handleTapSession = (session: ConsultationSession) => {
|
||||||
Taro.navigateTo({ url: `/pages/consultation/detail/index?id=${session.id}` });
|
Taro.navigateTo({ url: `/pages/consultation/detail/index?id=${session.id}` });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
|
@import '../../../styles/variables.scss';
|
||||||
|
@import '../../../styles/mixins.scss';
|
||||||
|
|
||||||
.action-inbox-page {
|
.action-inbox-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: #f5f5f5;
|
background: $bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inbox-tabs {
|
.inbox-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: white;
|
background: $card;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 1px solid $bd;
|
||||||
|
|
||||||
.inbox-tab {
|
.inbox-tab {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -16,7 +19,7 @@
|
|||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
.inbox-tab-text {
|
.inbox-tab-text {
|
||||||
color: #C4623A;
|
color: $pri;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
@@ -27,7 +30,7 @@
|
|||||||
left: 30%;
|
left: 30%;
|
||||||
right: 30%;
|
right: 30%;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
background: #C4623A;
|
background: $pri;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,7 +39,7 @@
|
|||||||
|
|
||||||
.inbox-tab-text {
|
.inbox-tab-text {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #666;
|
color: $tx2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,10 +49,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.inbox-card {
|
.inbox-card {
|
||||||
background: white;
|
background: $card;
|
||||||
border-radius: 12px;
|
border-radius: $r-sm;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
box-shadow: $shadow-sm;
|
||||||
|
|
||||||
.inbox-card-header {
|
.inbox-card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -59,7 +63,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.inbox-type-tag {
|
.inbox-type-tag {
|
||||||
color: white;
|
color: $card;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -69,11 +73,12 @@
|
|||||||
.inbox-card-title {
|
.inbox-card-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
color: $tx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inbox-card-desc {
|
.inbox-card-desc {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #999;
|
color: $tx3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,37 +88,38 @@
|
|||||||
|
|
||||||
.inbox-empty-text {
|
.inbox-empty-text {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #999;
|
color: $tx3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 半屏弹窗
|
|
||||||
.half-screen-dialog {
|
.half-screen-dialog {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
max-height: 70vh;
|
max-height: 70vh;
|
||||||
background: white;
|
background: $card;
|
||||||
border-radius: 16px 16px 0 0;
|
border-radius: $r-lg $r-lg 0 0;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
box-shadow: $shadow-lg;
|
||||||
|
|
||||||
.dialog-header {
|
.dialog-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid $bd-l;
|
||||||
|
|
||||||
.dialog-title {
|
.dialog-title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: $tx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-close {
|
.dialog-close {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #999;
|
color: $tx3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +129,7 @@
|
|||||||
|
|
||||||
.dialog-patient {
|
.dialog-patient {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #666;
|
color: $tx2;
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
@@ -151,12 +157,13 @@
|
|||||||
.thread-content {
|
.thread-content {
|
||||||
.thread-label {
|
.thread-label {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
color: $tx;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thread-time {
|
.thread-time {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #999;
|
color: $tx3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,19 +171,19 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 12px 20px 20px;
|
padding: 12px 20px 20px;
|
||||||
border-top: 1px solid #f0f0f0;
|
border-top: 1px solid $bd-l;
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 8px;
|
border-radius: $r-sm;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
&.primary { background: #C4623A; color: white; }
|
&.primary { background: $pri; color: $card; }
|
||||||
&.danger { background: #ff4d4f; color: white; }
|
&.danger { background: $dan; color: $card; }
|
||||||
&.default { background: #f5f5f5; color: #666; }
|
&.default { background: $surface-alt; color: $tx2; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { View, Text, Input, ScrollView } from '@tarojs/components';
|
import { View, Text, Input, ScrollView } from '@tarojs/components';
|
||||||
import Taro from '@tarojs/taro';
|
import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro';
|
||||||
import * as doctorApi from '@/services/doctor';
|
import * as doctorApi from '@/services/doctor';
|
||||||
import Loading from '@/components/Loading';
|
import Loading from '@/components/Loading';
|
||||||
import EmptyState from '@/components/EmptyState';
|
import EmptyState from '@/components/EmptyState';
|
||||||
@@ -14,14 +14,15 @@ export default function PatientList() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
const loadingRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadTags();
|
loadTags();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadPatients();
|
loadPatients(1, true);
|
||||||
}, [page, activeTag]);
|
}, [activeTag]);
|
||||||
|
|
||||||
const loadTags = async () => {
|
const loadTags = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -30,32 +31,51 @@ export default function PatientList() {
|
|||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadPatients = async () => {
|
const loadPatients = async (pageNum: number, isRefresh = false) => {
|
||||||
setLoading(true);
|
if (loadingRef.current) return;
|
||||||
|
loadingRef.current = true;
|
||||||
|
if (isRefresh) setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await doctorApi.listPatients({
|
const res = await doctorApi.listPatients({
|
||||||
page,
|
page: pageNum,
|
||||||
page_size: 20,
|
page_size: 20,
|
||||||
search: search || undefined,
|
search: search || undefined,
|
||||||
tag_id: activeTag || undefined,
|
tag_id: activeTag || undefined,
|
||||||
});
|
});
|
||||||
setPatients(res.data || []);
|
const list = res.data || [];
|
||||||
|
if (isRefresh) {
|
||||||
|
setPatients(list);
|
||||||
|
} else {
|
||||||
|
setPatients((prev) => [...prev, ...list]);
|
||||||
|
}
|
||||||
setTotal(res.total || 0);
|
setTotal(res.total || 0);
|
||||||
|
setPage(pageNum);
|
||||||
} catch {
|
} catch {
|
||||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
loadingRef.current = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
usePullDownRefresh(() => {
|
||||||
|
loadPatients(1, true).finally(() => {
|
||||||
|
Taro.stopPullDownRefresh();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
useReachBottom(() => {
|
||||||
|
if (!loading && patients.length < total) {
|
||||||
|
loadPatients(page + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
setPage(1);
|
loadPatients(1, true);
|
||||||
loadPatients();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTagFilter = (tagId: string) => {
|
const handleTagFilter = (tagId: string) => {
|
||||||
setActiveTag(tagId === activeTag ? '' : tagId);
|
setActiveTag(tagId === activeTag ? '' : tagId);
|
||||||
setPage(1);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getGenderLabel = (gender?: string) => {
|
const getGenderLabel = (gender?: string) => {
|
||||||
@@ -156,23 +176,12 @@ export default function PatientList() {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{total > 20 && (
|
{!loading && patients.length >= total && total > 0 && (
|
||||||
<View className='pagination'>
|
<View style={{ textAlign: 'center', padding: '20px' }}>
|
||||||
<Text
|
<Text style={{ fontSize: '24px', color: '#78716C' }}>没有更多了</Text>
|
||||||
className={`pagination__btn ${page <= 1 ? 'disabled' : ''}`}
|
|
||||||
onClick={() => page > 1 && setPage(page - 1)}
|
|
||||||
>
|
|
||||||
上一页
|
|
||||||
</Text>
|
|
||||||
<Text className='pagination__info'>{page} / {totalPages}</Text>
|
|
||||||
<Text
|
|
||||||
className={`pagination__btn ${page >= totalPages ? 'disabled' : ''}`}
|
|
||||||
onClick={() => page < totalPages && setPage(page + 1)}
|
|
||||||
>
|
|
||||||
下一页
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
{loading && patients.length > 0 && <Loading />}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
.health-page {
|
.health-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: $bg;
|
background: $bg;
|
||||||
padding: 20px 16px 100px;
|
padding: 20px 24px 100px;
|
||||||
padding-bottom: calc(100px + env(safe-area-inset-bottom));
|
padding-bottom: calc(100px + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { View, Text, Input } from '@tarojs/components';
|
import { View, Text, Input } from '@tarojs/components';
|
||||||
import Taro, { useDidShow } from '@tarojs/taro';
|
import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
|
||||||
import { useHealthStore } from '../../stores/health';
|
import { useHealthStore } from '../../stores/health';
|
||||||
import { useAuthStore } from '../../stores/auth';
|
import { useAuthStore } from '../../stores/auth';
|
||||||
import { inputVitalSign, getTrend, getHealthThresholds, findThreshold, DEFAULT_THRESHOLDS, type HealthThreshold } from '../../services/health';
|
import { inputVitalSign, getTrend, getHealthThresholds, findThreshold, DEFAULT_THRESHOLDS, type HealthThreshold } from '../../services/health';
|
||||||
@@ -61,6 +61,12 @@ export default function Health() {
|
|||||||
getHealthThresholds().then((t) => { if (t.length > 0) setThresholds(t); });
|
getHealthThresholds().then((t) => { if (t.length > 0) setThresholds(t); });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
usePullDownRefresh(() => {
|
||||||
|
Promise.all([refreshToday(true), loadTrend(activeTab), loadAiSuggestions()]).finally(() => {
|
||||||
|
Taro.stopPullDownRefresh();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const loadAiSuggestions = async () => {
|
const loadAiSuggestions = async () => {
|
||||||
try {
|
try {
|
||||||
const items = await listPendingSuggestions();
|
const items = await listPendingSuggestions();
|
||||||
@@ -164,7 +170,8 @@ export default function Health() {
|
|||||||
case 'blood_sugar': {
|
case 'blood_sugar': {
|
||||||
const val = parseFloat(sugarVal);
|
const val = parseFloat(sugarVal);
|
||||||
if (!val) { Taro.showToast({ title: '请填写血糖值', icon: 'none' }); return; }
|
if (!val) { Taro.showToast({ title: '请填写血糖值', icon: 'none' }); return; }
|
||||||
await inputVitalSign(patientId, { indicator_type: 'blood_sugar', value: val });
|
const bsType = sugarPeriod === 'fasting' ? 'blood_sugar_fasting' : 'blood_sugar_postprandial';
|
||||||
|
await inputVitalSign(patientId, { indicator_type: bsType, value: val });
|
||||||
setSugarVal('');
|
setSugarVal('');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -201,7 +208,7 @@ export default function Health() {
|
|||||||
<View className='ai-suggestion-card' onClick={() => {
|
<View className='ai-suggestion-card' onClick={() => {
|
||||||
const first = aiSuggestions[0];
|
const first = aiSuggestions[0];
|
||||||
if (first?.suggestion_type === 'appointment') {
|
if (first?.suggestion_type === 'appointment') {
|
||||||
Taro.navigateTo({ url: `/pages/pkg-appointment/create/index?patientId=${first.patient_id}` });
|
Taro.navigateTo({ url: `/pages/appointment/create/index` });
|
||||||
} else if (first?.suggestion_type === 'followup') {
|
} else if (first?.suggestion_type === 'followup') {
|
||||||
Taro.navigateTo({ url: '/pages/pkg-profile/followups/index' });
|
Taro.navigateTo({ url: '/pages/pkg-profile/followups/index' });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
.home-page {
|
.home-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: $bg;
|
background: $bg;
|
||||||
padding: 20px 16px 100px;
|
padding: 20px 24px 100px;
|
||||||
padding-bottom: calc(100px + env(safe-area-inset-bottom));
|
padding-bottom: calc(100px + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { View, Text } from '@tarojs/components';
|
import { View, Text } from '@tarojs/components';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Taro, { useDidShow } from '@tarojs/taro';
|
import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
|
||||||
import { useAuthStore } from '../../stores/auth';
|
import { useAuthStore } from '../../stores/auth';
|
||||||
import { useHealthStore } from '../../stores/health';
|
import { useHealthStore } from '../../stores/health';
|
||||||
import ProgressRing from '../../components/ProgressRing';
|
import ProgressRing from '../../components/ProgressRing';
|
||||||
@@ -30,6 +30,12 @@ export default function Index() {
|
|||||||
trackPageView('home');
|
trackPageView('home');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
usePullDownRefresh(() => {
|
||||||
|
Promise.all([refreshToday(true), loadUpcoming()]).finally(() => {
|
||||||
|
Taro.stopPullDownRefresh();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const loadUpcoming = async () => {
|
const loadUpcoming = async () => {
|
||||||
const patientId = useAuthStore.getState().currentPatient?.id;
|
const patientId = useAuthStore.getState().currentPatient?.id;
|
||||||
if (!patientId) return;
|
if (!patientId) return;
|
||||||
@@ -121,7 +127,7 @@ export default function Index() {
|
|||||||
className='greeting-bell'
|
className='greeting-bell'
|
||||||
onClick={() => Taro.switchTab({ url: '/pages/messages/index' })}
|
onClick={() => Taro.switchTab({ url: '/pages/messages/index' })}
|
||||||
>
|
>
|
||||||
<Text className='greeting-bell-icon'>🔔</Text>
|
<Text className='greeting-bell-icon'>消</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
import { View, Text } from '@tarojs/components';
|
import { View, Text } from '@tarojs/components';
|
||||||
import Taro, { useDidShow } from '@tarojs/taro';
|
import Taro, { useDidShow, useReachBottom } from '@tarojs/taro';
|
||||||
import { listConsultations, ConsultationSession } from '../../services/consultation';
|
import { listConsultations, ConsultationSession } from '../../services/consultation';
|
||||||
import { notificationService } from '../../services/notification';
|
import { notificationService } from '../../services/notification';
|
||||||
import Loading from '../../components/Loading';
|
import Loading from '../../components/Loading';
|
||||||
@@ -21,34 +21,62 @@ export default function Messages() {
|
|||||||
const [sessions, setSessions] = useState<ConsultationSession[]>([]);
|
const [sessions, setSessions] = useState<ConsultationSession[]>([]);
|
||||||
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
|
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const loadingRef = useRef(false);
|
||||||
|
|
||||||
useDidShow(() => {
|
useDidShow(() => {
|
||||||
loadData(activeTab);
|
loadData(activeTab, 1, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
const loadData = async (tab: MsgTab) => {
|
const loadData = async (tab: MsgTab, pageNum: number = 1, isRefresh = false) => {
|
||||||
|
if (loadingRef.current) return;
|
||||||
|
loadingRef.current = true;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
if (tab === 'consultation') {
|
if (tab === 'consultation') {
|
||||||
const res = await listConsultations({ page: 1, page_size: 20 });
|
const res = await listConsultations({ page: pageNum, page_size: 20 });
|
||||||
setSessions(res.data || []);
|
const list = res.data || [];
|
||||||
|
if (isRefresh) {
|
||||||
|
setSessions(list);
|
||||||
|
} else {
|
||||||
|
setSessions((prev) => [...prev, ...list]);
|
||||||
|
}
|
||||||
|
setTotal(res.total || 0);
|
||||||
} else {
|
} else {
|
||||||
const res = await notificationService.list<{ data: unknown[] }>({ page: 1, page_size: 20 });
|
const res = await notificationService.list<{ data: unknown[]; total?: number }>({ page: pageNum, page_size: 20 });
|
||||||
setNotifications((res as { data?: unknown[] })?.data || []);
|
const list = (res as { data?: unknown[] })?.data || [];
|
||||||
|
if (isRefresh) {
|
||||||
|
setNotifications(list as NotificationItem[]);
|
||||||
|
} else {
|
||||||
|
setNotifications((prev) => [...prev, ...(list as NotificationItem[])]);
|
||||||
|
}
|
||||||
|
setTotal((res as { total?: number })?.total || 0);
|
||||||
}
|
}
|
||||||
|
setPage(pageNum);
|
||||||
} catch {
|
} catch {
|
||||||
if (tab === 'consultation') setSessions([]);
|
if (isRefresh) {
|
||||||
else setNotifications([]);
|
if (tab === 'consultation') setSessions([]);
|
||||||
|
else setNotifications([]);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
loadingRef.current = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTabChange = (tab: MsgTab) => {
|
const handleTabChange = (tab: MsgTab) => {
|
||||||
setActiveTab(tab);
|
setActiveTab(tab);
|
||||||
loadData(tab);
|
loadData(tab, 1, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useReachBottom(() => {
|
||||||
|
const currentList = activeTab === 'consultation' ? sessions : notifications;
|
||||||
|
if (!loading && currentList.length < total) {
|
||||||
|
loadData(activeTab, page + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const formatTime = (dateStr: string | null) => {
|
const formatTime = (dateStr: string | null) => {
|
||||||
if (!dateStr) return '';
|
if (!dateStr) return '';
|
||||||
const d = new Date(dateStr);
|
const d = new Date(dateStr);
|
||||||
|
|||||||
@@ -1,34 +1,37 @@
|
|||||||
|
@import '../../../styles/variables.scss';
|
||||||
|
@import '../../../styles/mixins.scss';
|
||||||
|
|
||||||
.alerts-page {
|
.alerts-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: #f5f5f5;
|
background: $bg;
|
||||||
padding-bottom: env(safe-area-inset-bottom);
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
.alerts-tabs {
|
.alerts-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: #fff;
|
background: $card;
|
||||||
padding: 20px 16px;
|
padding: 20px 16px;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 1px solid $bd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alerts-tab {
|
.alerts-tab {
|
||||||
padding: 8px 20px;
|
padding: 8px 20px;
|
||||||
border-radius: 20px;
|
border-radius: $r-pill;
|
||||||
background: #f0f0f0;
|
background: $surface-alt;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alerts-tab.active {
|
.alerts-tab.active {
|
||||||
background: #C4623A;
|
background: $pri;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alerts-tab-text {
|
.alerts-tab-text {
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
color: #666;
|
color: $tx2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alerts-tab-text.active {
|
.alerts-tab-text.active {
|
||||||
color: #fff;
|
color: $card;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alerts-list {
|
.alerts-list {
|
||||||
@@ -36,10 +39,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alert-card {
|
.alert-card {
|
||||||
background: #fff;
|
background: $card;
|
||||||
border-radius: 16px;
|
border-radius: $r;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
box-shadow: $shadow-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-header {
|
.alert-header {
|
||||||
@@ -51,7 +55,7 @@
|
|||||||
|
|
||||||
.alert-badge {
|
.alert-badge {
|
||||||
padding: 4px 16px;
|
padding: 4px 16px;
|
||||||
border-radius: 8px;
|
border-radius: $r-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-badge.sev-info {
|
.alert-badge.sev-info {
|
||||||
@@ -88,17 +92,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alert-badge.sev-urgent .alert-badge-text {
|
.alert-badge.sev-urgent .alert-badge-text {
|
||||||
color: #fff;
|
color: $card;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-time {
|
.alert-time {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
color: #999;
|
color: $tx3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-title {
|
.alert-title {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
color: #333;
|
color: $tx;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,23 +116,23 @@
|
|||||||
|
|
||||||
.alerts-empty-text {
|
.alerts-empty-text {
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
color: #999;
|
color: $tx3;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alerts-empty-hint {
|
.alerts-empty-hint {
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
color: #bbb;
|
color: $tx3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alerts-empty-action {
|
.alerts-empty-action {
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
padding: 16px 48px;
|
padding: 16px 48px;
|
||||||
background: #C4623A;
|
background: $pri;
|
||||||
border-radius: 32px;
|
border-radius: $r-pill;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alerts-empty-action-text {
|
.alerts-empty-action-text {
|
||||||
color: #fff;
|
color: $card;
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export default function MallOrders() {
|
|||||||
text='暂无订单'
|
text='暂无订单'
|
||||||
hint='去商城兑换心仪商品吧'
|
hint='去商城兑换心仪商品吧'
|
||||||
actionText='去商城'
|
actionText='去商城'
|
||||||
onAction={() => Taro.switchTab({ url: '/pages/mall/index' })}
|
onAction={() => Taro.redirectTo({ url: '/pages/mall/index' })}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<View className='order-list'>
|
<View className='order-list'>
|
||||||
|
|||||||
@@ -40,11 +40,7 @@ export default function Settings() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePrivacy = () => {
|
const handlePrivacy = () => {
|
||||||
Taro.showModal({
|
Taro.navigateTo({ url: '/pages/legal/privacy-policy' });
|
||||||
title: '隐私政策',
|
|
||||||
content: '我们重视您的隐私保护。所有健康数据均经过加密存储,仅用于为您提供健康管理服务,不会向第三方分享。',
|
|
||||||
showCancel: false,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
|
|||||||
@@ -118,10 +118,25 @@
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@include flex-center;
|
@include flex-center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&.menu-icon--pri-l {
|
||||||
|
background: $pri-l;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.menu-icon--acc-l {
|
||||||
|
background: $acc-l;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.menu-icon--surface-alt {
|
||||||
|
background: $surface-alt;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-icon-text {
|
.menu-icon-text {
|
||||||
font-size: 22px;
|
font-family: Georgia, Times New Roman, serif;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $pri-d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-label {
|
.menu-label {
|
||||||
|
|||||||
@@ -6,14 +6,18 @@ import { usePointsStore } from '../../stores/points';
|
|||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
const MENU_ITEMS = [
|
const MENU_ITEMS = [
|
||||||
{ label: '就诊人管理', icon: '👨👩👧', bg: '#F0DDD4' },
|
{ label: '就诊人管理', icon: '家', bg: 'pri-l' },
|
||||||
{ label: '我的报告', icon: '📄', bg: '#E8F0E8' },
|
{ label: '我的报告', icon: '报', bg: 'acc-l' },
|
||||||
{ label: '健康记录', icon: '📝', bg: '#E8F0F8' },
|
{ label: '健康记录', icon: '健', bg: 'pri-l' },
|
||||||
{ label: '诊断记录', icon: '📋', bg: '#E8F0E8' },
|
{ label: '诊断记录', icon: '诊', bg: 'acc-l' },
|
||||||
{ label: '我的随访', icon: '🏥', bg: '#F3E8F8' },
|
{ label: '我的随访', icon: '随', bg: 'pri-l' },
|
||||||
{ label: '我的预约', icon: '📅', bg: '#E8F0F8' },
|
{ label: '我的预约', icon: '约', bg: 'acc-l' },
|
||||||
{ label: '在线咨询', icon: '💬', bg: '#E8F0E8' },
|
{ label: '用药记录', icon: '药', bg: 'pri-l' },
|
||||||
{ label: '设置', icon: '⚙️', bg: '#f0f0f0' },
|
{ label: '透析记录', icon: '透', bg: 'acc-l' },
|
||||||
|
{ label: '知情同意', icon: '知', bg: 'pri-l' },
|
||||||
|
{ label: '线下活动', icon: '活', bg: 'acc-l' },
|
||||||
|
{ label: '在线咨询', icon: '问', bg: 'pri-l' },
|
||||||
|
{ label: '设置', icon: '设', bg: 'surface-alt' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const MENU_PATHS: Record<string, string> = {
|
const MENU_PATHS: Record<string, string> = {
|
||||||
@@ -23,6 +27,10 @@ const MENU_PATHS: Record<string, string> = {
|
|||||||
'诊断记录': '/pages/pkg-profile/diagnoses/index',
|
'诊断记录': '/pages/pkg-profile/diagnoses/index',
|
||||||
'我的随访': '/pages/pkg-profile/followups/index',
|
'我的随访': '/pages/pkg-profile/followups/index',
|
||||||
'我的预约': '/pages/appointment/index',
|
'我的预约': '/pages/appointment/index',
|
||||||
|
'用药记录': '/pages/pkg-profile/medication/index',
|
||||||
|
'透析记录': '/pages/pkg-profile/dialysis-records/index',
|
||||||
|
'知情同意': '/pages/pkg-profile/consents/index',
|
||||||
|
'线下活动': '/pages/events/index',
|
||||||
'在线咨询': '/pages/consultation/index',
|
'在线咨询': '/pages/consultation/index',
|
||||||
'设置': '/pages/pkg-profile/settings/index',
|
'设置': '/pages/pkg-profile/settings/index',
|
||||||
};
|
};
|
||||||
@@ -56,7 +64,9 @@ export default function Profile() {
|
|||||||
{/* 用户信息卡片 */}
|
{/* 用户信息卡片 */}
|
||||||
<View className='profile-user-card'>
|
<View className='profile-user-card'>
|
||||||
<View className='profile-avatar'>
|
<View className='profile-avatar'>
|
||||||
<Text className='profile-avatar-icon'>👤</Text>
|
<Text className='profile-avatar-icon'>
|
||||||
|
{(user?.display_name || '访').charAt(0)}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className='profile-user-info'>
|
<View className='profile-user-info'>
|
||||||
<Text className='profile-name'>{user?.display_name || '未登录'}</Text>
|
<Text className='profile-name'>{user?.display_name || '未登录'}</Text>
|
||||||
@@ -84,7 +94,7 @@ export default function Profile() {
|
|||||||
key={item.label}
|
key={item.label}
|
||||||
onClick={() => handleMenuClick(item.label)}
|
onClick={() => handleMenuClick(item.label)}
|
||||||
>
|
>
|
||||||
<View className='menu-icon' style={`background:${item.bg};`}>
|
<View className={`menu-icon menu-icon--${item.bg}`}>
|
||||||
<Text className='menu-icon-text'>{item.icon}</Text>
|
<Text className='menu-icon-text'>{item.icon}</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text className='menu-label'>{item.label}</Text>
|
<Text className='menu-label'>{item.label}</Text>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ $wrn-l: #FFF3E0; // 警告浅
|
|||||||
// ─── 圆角 ───
|
// ─── 圆角 ───
|
||||||
$r: 16px;
|
$r: 16px;
|
||||||
$r-sm: 12px;
|
$r-sm: 12px;
|
||||||
|
$r-xs: 8px;
|
||||||
$r-lg: 20px;
|
$r-lg: 20px;
|
||||||
$r-pill: 999px;
|
$r-pill: 999px;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user