feat(auth,mp): 患者登录流程优化 — 智能合并 + 角色冻结 + 页面冻结
- 智能合并:微信注册时用手机号盲索引匹配已有患者档案,避免重复建 档(AuthState 添加 PiiCrypto + ensure_patient_record 增加盲索引查询) - 角色冻结:小程序仅允许患者角色登录,医护角色被拦截 (auth_service.rs 添加反向拦截 + 登录页移除 credential login 表单) - 页面冻结:10 个非核心页面替换为 FrozenPage 占位组件(用药/知情同意 /透析/家属/诊断/事件),移除 profile 导航入口,移除医生端预加载 - 医生端代码保留,仅隐藏入口,后续可零成本恢复 讨论记录:docs/discussions/2026-05-23-account-registration-login-flow.md
This commit is contained in:
@@ -84,7 +84,7 @@ export default defineAppConfig({
|
||||
preloadRule: {
|
||||
'pages/index/index': {
|
||||
network: 'all',
|
||||
packages: ['pages/pkg-health', 'pages/pkg-doctor-core', 'pages/article'],
|
||||
packages: ['pages/pkg-health', 'pages/article'],
|
||||
},
|
||||
'pages/health/index': {
|
||||
network: 'all',
|
||||
|
||||
48
apps/miniprogram/src/components/FrozenPage/index.scss
Normal file
48
apps/miniprogram/src/components/FrozenPage/index.scss
Normal file
@@ -0,0 +1,48 @@
|
||||
@import '../../styles/variables.scss';
|
||||
@import '../../styles/mixins.scss';
|
||||
|
||||
.frozen-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.frozen-page-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.frozen-page-title {
|
||||
font-size: var(--tk-font-h3);
|
||||
font-weight: 600;
|
||||
color: $tx;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.frozen-page-desc {
|
||||
font-size: var(--tk-font-body);
|
||||
color: $tx3;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.frozen-page-btn {
|
||||
height: 44px;
|
||||
padding: 0 32px;
|
||||
border-radius: $r;
|
||||
background: var(--tk-pri);
|
||||
@include flex-center;
|
||||
|
||||
&:active {
|
||||
opacity: var(--tk-touch-feedback-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
.frozen-page-btn-text {
|
||||
font-size: var(--tk-font-body);
|
||||
font-weight: 500;
|
||||
color: $white;
|
||||
}
|
||||
22
apps/miniprogram/src/components/FrozenPage/index.tsx
Normal file
22
apps/miniprogram/src/components/FrozenPage/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import PageShell from '@/components/ui/PageShell';
|
||||
import './index.scss';
|
||||
|
||||
export default function FrozenPage() {
|
||||
return (
|
||||
<PageShell scroll={false}>
|
||||
<View className="frozen-page">
|
||||
<Text className="frozen-page-icon">🚧</Text>
|
||||
<Text className="frozen-page-title">功能即将上线</Text>
|
||||
<Text className="frozen-page-desc">我们正在努力准备中,敬请期待</Text>
|
||||
<View
|
||||
className="frozen-page-btn"
|
||||
onClick={() => Taro.navigateBack({ delta: 1 }).catch(() => Taro.switchTab({ url: '/pages/index/index' }))}
|
||||
>
|
||||
<Text className="frozen-page-btn-text">返回</Text>
|
||||
</View>
|
||||
</View>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
@@ -50,78 +50,6 @@
|
||||
color: $tx3;
|
||||
}
|
||||
|
||||
/* ─── 输入框 ─── */
|
||||
.login-field {
|
||||
height: 56px;
|
||||
background: $card;
|
||||
border: 1.5px solid $bd;
|
||||
border-radius: $r;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.login-input {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
font-size: var(--tk-font-body);
|
||||
color: $tx;
|
||||
}
|
||||
|
||||
.login-placeholder {
|
||||
color: $tx3;
|
||||
font-size: var(--tk-font-body);
|
||||
}
|
||||
|
||||
.login-eye {
|
||||
font-size: var(--tk-font-body-sm);
|
||||
color: var(--tk-pri);
|
||||
font-weight: 500;
|
||||
padding: 6px 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ─── 登录按钮 ─── */
|
||||
.login-submit {
|
||||
height: 54px;
|
||||
border-radius: $r;
|
||||
background: var(--tk-pri);
|
||||
@include flex-center;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 4px 16px rgba($pri, 0.3);
|
||||
|
||||
&:active {
|
||||
opacity: var(--tk-touch-feedback-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
.login-submit-text {
|
||||
font-size: var(--tk-font-body-lg);
|
||||
font-weight: 600;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
/* ─── 分隔线 ─── */
|
||||
.login-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.login-divider-line {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: $bd-l;
|
||||
}
|
||||
|
||||
.login-divider-text {
|
||||
font-size: var(--tk-font-cap);
|
||||
color: $tx3;
|
||||
}
|
||||
|
||||
/* ─── 微信登录 ─── */
|
||||
.login-wechat-btn {
|
||||
height: 54px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { View, Text, Input, Button } from '@tarojs/components';
|
||||
import { View, Text, Button } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { safeNavigateTo } from '@/utils/navigate';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
@@ -11,9 +11,6 @@ const IS_DEV = process.env.NODE_ENV !== 'production';
|
||||
const IS_SIMULATOR = typeof __wxConfig !== 'undefined' && (__wxConfig as Record<string, unknown>)?.envVersion === 'develop';
|
||||
|
||||
export default function Login() {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [agreed, setAgreed] = useState(false);
|
||||
const [needBind, setNeedBind] = useState(false);
|
||||
|
||||
@@ -25,12 +22,9 @@ export default function Login() {
|
||||
|
||||
const navigateAfterLogin = () => {
|
||||
if (isMedicalStaff()) {
|
||||
// 使用 redirectTo 替代 reLaunch 避免分包加载超时
|
||||
// redirectTo 只替换当前页面,不销毁整个页栈,分包预加载不会被中断
|
||||
Taro.redirectTo({
|
||||
url: '/pages/pkg-doctor-core/index',
|
||||
fail: () => {
|
||||
// fallback: 先跳首页再 redirectTo
|
||||
Taro.switchTab({
|
||||
url: '/pages/index/index',
|
||||
success: () => {
|
||||
@@ -46,32 +40,6 @@ export default function Login() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCredentialLogin = async () => {
|
||||
if (!username.trim()) {
|
||||
Taro.showToast({ title: '请输入账号', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (!password.trim()) {
|
||||
Taro.showToast({ title: '请输入密码', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (!agreed) {
|
||||
Taro.showToast({ title: '请先阅读并同意用户协议', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const success = await credentialLogin(username.trim(), password);
|
||||
if (success) {
|
||||
navigateAfterLogin();
|
||||
} else {
|
||||
Taro.showToast({ title: '账号或密码错误', icon: 'none' });
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[login] 登录失败:', err);
|
||||
Taro.showToast({ title: '登录失败,请重试', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleWechatLogin = async () => {
|
||||
if (!agreed) {
|
||||
Taro.showToast({ title: '请先阅读并同意用户协议', icon: 'none' });
|
||||
@@ -144,49 +112,6 @@ export default function Login() {
|
||||
|
||||
{!needBind ? (
|
||||
<>
|
||||
{/* 账号输入 */}
|
||||
<View className="login-field">
|
||||
<Input
|
||||
className="login-input"
|
||||
type="text"
|
||||
placeholder="请输入账号"
|
||||
placeholderClass="login-placeholder"
|
||||
value={username}
|
||||
onInput={(e) => setUsername(e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 密码输入 */}
|
||||
<View className="login-field">
|
||||
<Input
|
||||
className="login-input"
|
||||
type="safe-password"
|
||||
password={!showPassword}
|
||||
placeholder="请输入密码"
|
||||
placeholderClass="login-placeholder"
|
||||
value={password}
|
||||
onInput={(e) => setPassword(e.detail.value)}
|
||||
/>
|
||||
<Text
|
||||
className="login-eye"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? '隐藏' : '显示'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 登录按钮 */}
|
||||
<View className="login-submit" onClick={handleCredentialLogin}>
|
||||
<Text className="login-submit-text">{loading ? '登录中...' : '登录'}</Text>
|
||||
</View>
|
||||
|
||||
{/* 分隔线 */}
|
||||
<View className="login-divider">
|
||||
<View className="login-divider-line" />
|
||||
<Text className="login-divider-text">或</Text>
|
||||
<View className="login-divider-line" />
|
||||
</View>
|
||||
|
||||
{/* 微信一键登录 */}
|
||||
<View className="login-wechat-btn" onClick={handleWechatLogin}>
|
||||
<Text className="login-wechat-icon">微</Text>
|
||||
|
||||
@@ -1,131 +1,5 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useReachBottom } from '@tarojs/taro';
|
||||
import { usePageData } from '@/hooks/usePageData';
|
||||
import { getCachedPatientId } from '@/services/request';
|
||||
import { listConsents, revokeConsent } from '@/services/consent';
|
||||
import type { Consent } from '@/services/consent';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import Loading from '@/components/Loading';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
import PageShell from '@/components/ui/PageShell';
|
||||
import './index.scss';
|
||||
import FrozenPage from '@/components/FrozenPage';
|
||||
|
||||
const CONSENT_TYPE_MAP: Record<string, string> = {
|
||||
data_processing: '数据处理同意',
|
||||
health_data_collection: '健康数据采集',
|
||||
research_use: '科研使用',
|
||||
third_party_share: '第三方共享',
|
||||
genetic_testing: '基因检测',
|
||||
telemedicine: '远程医疗',
|
||||
};
|
||||
|
||||
const STATUS_MAP: Record<string, { label: string; cls: string }> = {
|
||||
granted: { label: '已签署', cls: 'granted' },
|
||||
revoked: { label: '已撤回', cls: 'revoked' },
|
||||
};
|
||||
|
||||
export default function ConsentList() {
|
||||
const modeClass = useElderClass();
|
||||
const [consents, setConsents] = useState<Consent[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [revoking, setRevoking] = useState<string | null>(null);
|
||||
const [hasPatient, setHasPatient] = useState(true);
|
||||
|
||||
const fetchData = useCallback(async (p: number, append = false) => {
|
||||
const patientId = getCachedPatientId();
|
||||
if (!patientId) {
|
||||
setConsents([]);
|
||||
setHasPatient(false);
|
||||
return;
|
||||
}
|
||||
setHasPatient(true);
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await listConsents(patientId, { page: p, page_size: 20 });
|
||||
const list = res.data || [];
|
||||
setConsents(append ? (prev) => [...prev, ...list] : list);
|
||||
setTotal(res.total);
|
||||
setPage(p);
|
||||
} catch (err) {
|
||||
console.warn('[consent] 加载失败:', err);
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
usePageData(async () => { await fetchData(1); }, { throttleMs: 10000, enablePullDown: true });
|
||||
|
||||
useReachBottom(() => {
|
||||
if (!loading && consents.length < total) {
|
||||
fetchData(page + 1, true);
|
||||
}
|
||||
});
|
||||
|
||||
const handleRevoke = async (consent: Consent) => {
|
||||
const { confirm } = await Taro.showModal({
|
||||
title: '确认撤回',
|
||||
content: `确定要撤回「${CONSENT_TYPE_MAP[consent.consent_type] || consent.consent_type}」的同意吗?`,
|
||||
});
|
||||
if (!confirm) return;
|
||||
setRevoking(consent.id);
|
||||
try {
|
||||
const updated = await revokeConsent(consent.id, consent.version);
|
||||
setConsents((prev) => prev.map((c) => c.id === updated.id ? updated : c));
|
||||
Taro.showToast({ title: '已撤回', icon: 'success' });
|
||||
} catch (err) {
|
||||
console.warn('[consent] 撤回失败:', err);
|
||||
Taro.showToast({ title: '撤回失败', icon: 'none' });
|
||||
} finally {
|
||||
setRevoking(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageShell className={modeClass}>
|
||||
<Text className='page-title'>知情同意</Text>
|
||||
|
||||
<View className='consent-list'>
|
||||
{consents.map((c) => {
|
||||
const si = STATUS_MAP[c.status] || { label: c.status, cls: '' };
|
||||
const typeName = CONSENT_TYPE_MAP[c.consent_type] || c.consent_type;
|
||||
return (
|
||||
<View className='consent-card' key={c.id}>
|
||||
<View className='consent-card__header'>
|
||||
<Text className='consent-card__type'>{typeName}</Text>
|
||||
<Text className={`status-tag ${si.cls}`}>{si.label}</Text>
|
||||
</View>
|
||||
<Text className='consent-card__scope'>范围: {c.consent_scope}</Text>
|
||||
{c.granted_at && (
|
||||
<Text className='consent-card__date'>签署时间: {c.granted_at}</Text>
|
||||
)}
|
||||
{c.revoked_at && (
|
||||
<Text className='consent-card__date'>撤回时间: {c.revoked_at}</Text>
|
||||
)}
|
||||
{c.expiry_date && (
|
||||
<Text className='consent-card__expiry'>有效期至: {c.expiry_date}</Text>
|
||||
)}
|
||||
{c.status === 'granted' && (
|
||||
<View
|
||||
className={`revoke-btn ${revoking === c.id ? 'revoke-btn--disabled' : ''}`}
|
||||
onClick={() => handleRevoke(c)}
|
||||
>
|
||||
<Text className='revoke-btn__text'>{revoking === c.id ? '处理中...' : '撤回同意'}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{consents.length === 0 && !loading && (
|
||||
<EmptyState text={hasPatient ? '暂无知情同意记录' : '请先在就诊人管理中选择就诊人'} />
|
||||
)}
|
||||
|
||||
{loading && <Loading />}
|
||||
</PageShell>
|
||||
);
|
||||
export default function ConsentsPage() {
|
||||
return <FrozenPage />;
|
||||
}
|
||||
|
||||
@@ -1,102 +1,5 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useReachBottom } from '@tarojs/taro';
|
||||
import { usePageData } from '@/hooks/usePageData';
|
||||
import { getCachedPatientId } from '@/services/request';
|
||||
import { listDiagnoses, Diagnosis } from '../../../services/health-record';
|
||||
import EmptyState from '../../../components/EmptyState';
|
||||
import Loading from '../../../components/Loading';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
import PageShell from '@/components/ui/PageShell';
|
||||
import './index.scss';
|
||||
import FrozenPage from '@/components/FrozenPage';
|
||||
|
||||
const TYPE_MAP: Record<string, { label: string; cls: string }> = {
|
||||
primary: { label: '主要', cls: 'primary' },
|
||||
secondary: { label: '次要', cls: 'secondary' },
|
||||
comorbid: { label: '合并症', cls: 'comorbid' },
|
||||
};
|
||||
|
||||
const STATUS_MAP: Record<string, { label: string; cls: string }> = {
|
||||
active: { label: '活动', cls: 'active' },
|
||||
resolved: { label: '已解决', cls: 'resolved' },
|
||||
chronic: { label: '慢性', cls: 'chronic' },
|
||||
};
|
||||
|
||||
export default function Diagnoses() {
|
||||
const modeClass = useElderClass();
|
||||
const [records, setRecords] = useState<Diagnosis[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasPatient, setHasPatient] = useState(true);
|
||||
|
||||
const fetchData = useCallback(async (p: number, append = false) => {
|
||||
const patientId = getCachedPatientId();
|
||||
if (!patientId) {
|
||||
setRecords([]);
|
||||
setHasPatient(false);
|
||||
return;
|
||||
}
|
||||
setHasPatient(true);
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await listDiagnoses(patientId, { page: p, page_size: 20 });
|
||||
const list = res.data || [];
|
||||
setRecords(append ? (prev) => [...prev, ...list] : list);
|
||||
setTotal(res.total);
|
||||
setPage(p);
|
||||
} catch (err) {
|
||||
console.warn('[diagnosis] 加载失败:', err);
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
usePageData(async () => { await fetchData(1); }, { throttleMs: 10000, enablePullDown: true });
|
||||
|
||||
useReachBottom(() => {
|
||||
if (!loading && records.length < total) {
|
||||
fetchData(page + 1, true);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<PageShell className={modeClass}>
|
||||
<Text className='page-title'>诊断记录</Text>
|
||||
|
||||
<View className='diagnosis-list'>
|
||||
{records.map((d) => {
|
||||
const typeInfo = TYPE_MAP[d.diagnosis_type] || { label: d.diagnosis_type, cls: '' };
|
||||
const statusInfo = STATUS_MAP[d.status] || { label: d.status, cls: '' };
|
||||
return (
|
||||
<View className='diagnosis-card' key={d.id}>
|
||||
<View className='diagnosis-card__header'>
|
||||
<Text className='diagnosis-card__name'>{d.diagnosis_name}</Text>
|
||||
<Text className={`diagnosis-card__status ${statusInfo.cls}`}>
|
||||
{statusInfo.label}
|
||||
</Text>
|
||||
</View>
|
||||
<View className='diagnosis-card__meta'>
|
||||
<Text className={`diagnosis-card__type ${typeInfo.cls}`}>
|
||||
{typeInfo.label}
|
||||
</Text>
|
||||
<Text className='diagnosis-card__code'>{d.icd_code}</Text>
|
||||
</View>
|
||||
<Text className='diagnosis-card__date'>诊断日期:{d.diagnosed_date}</Text>
|
||||
{d.notes && (
|
||||
<Text className='diagnosis-card__notes'>{d.notes}</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{records.length === 0 && !loading && (
|
||||
<EmptyState text={hasPatient ? '暂无诊断记录' : '请先在就诊人管理中选择就诊人'} />
|
||||
)}
|
||||
|
||||
{loading && <Loading />}
|
||||
</PageShell>
|
||||
);
|
||||
export default function DiagnosesPage() {
|
||||
return <FrozenPage />;
|
||||
}
|
||||
|
||||
@@ -1,123 +1,5 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import { usePageData } from '@/hooks/usePageData';
|
||||
import { getDialysisPrescription } from '@/services/dialysis';
|
||||
import type { DialysisPrescription } from '@/services/dialysis';
|
||||
import Loading from '@/components/Loading';
|
||||
import PageShell from '@/components/ui/PageShell';
|
||||
import ContentCard from '@/components/ui/ContentCard';
|
||||
import { useElderClass } from '../../../../hooks/useElderClass';
|
||||
import './index.scss';
|
||||
import FrozenPage from '@/components/FrozenPage';
|
||||
|
||||
const STATUS_MAP: Record<string, { label: string; cls: string }> = {
|
||||
active: { label: '生效中', cls: 'active' },
|
||||
inactive: { label: '已停用', cls: 'inactive' },
|
||||
expired: { label: '已过期', cls: 'expired' },
|
||||
};
|
||||
|
||||
export default function DialysisPrescriptionDetail() {
|
||||
const modeClass = useElderClass();
|
||||
const router = useRouter();
|
||||
const id = router.params.id || '';
|
||||
const [rx, setRx] = useState<DialysisPrescription | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetchDetail = useCallback(async () => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getDialysisPrescription(id);
|
||||
setRx(data);
|
||||
} catch (err) {
|
||||
console.warn('[prescription] 加载失败:', err);
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
usePageData(fetchDetail, { throttleMs: 60000 });
|
||||
|
||||
if (loading) return <PageShell className={modeClass}><Loading /></PageShell>;
|
||||
if (!rx) return <PageShell className={modeClass}><View className='empty-state'><Text className='empty-text'>处方不存在</Text></View></PageShell>;
|
||||
|
||||
const si = STATUS_MAP[rx.status] || { label: rx.status, cls: '' };
|
||||
|
||||
const Row = ({ label, value }: { label: string; value?: string | number | null }) => {
|
||||
if (value == null) return null;
|
||||
return (
|
||||
<View className='detail-row'>
|
||||
<Text className='detail-label'>{label}</Text>
|
||||
<Text className='detail-value'>{value}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageShell className={modeClass}>
|
||||
{/* 状态头部 */}
|
||||
<ContentCard>
|
||||
<View className='header-row'>
|
||||
<Text className='detail-title'>{rx.dialyzer_model || '透析处方'}</Text>
|
||||
<Text className={`status-tag ${si.cls}`}>{si.label}</Text>
|
||||
</View>
|
||||
{(rx.effective_from || rx.effective_to) && (
|
||||
<Text className='header-sub'>{rx.effective_from || '...'} ~ {rx.effective_to || '...'}</Text>
|
||||
)}
|
||||
</ContentCard>
|
||||
|
||||
{/* 基本参数 */}
|
||||
<ContentCard>
|
||||
<Text className='section-title'>基本参数</Text>
|
||||
<Row label='透析器型号' value={rx.dialyzer_model} />
|
||||
<Row label='膜面积' value={rx.membrane_area != null ? `${rx.membrane_area} m²` : null} />
|
||||
<Row label='血流速' value={rx.blood_flow_rate != null ? `${rx.blood_flow_rate} ml/min` : null} />
|
||||
<Row label='透析液流量' value={rx.dialysate_flow_rate != null ? `${rx.dialysate_flow_rate} ml/min` : null} />
|
||||
<Row label='频率' value={rx.frequency_per_week != null ? `${rx.frequency_per_week} 次/周` : null} />
|
||||
<Row label='每次时长' value={rx.duration_minutes != null ? `${rx.duration_minutes} 分钟` : null} />
|
||||
</ContentCard>
|
||||
|
||||
{/* 透析液配比 */}
|
||||
<ContentCard>
|
||||
<Text className='section-title'>透析液配比</Text>
|
||||
<Row label='钾浓度' value={rx.dialysate_potassium != null ? `${rx.dialysate_potassium} mmol/L` : null} />
|
||||
<Row label='钙浓度' value={rx.dialysate_calcium != null ? `${rx.dialysate_calcium} mmol/L` : null} />
|
||||
<Row label='碳酸氢盐浓度' value={rx.dialysate_bicarbonate != null ? `${rx.dialysate_bicarbonate} mmol/L` : null} />
|
||||
</ContentCard>
|
||||
|
||||
{/* 抗凝方案 */}
|
||||
<ContentCard>
|
||||
<Text className='section-title'>抗凝方案</Text>
|
||||
<Row label='抗凝类型' value={rx.anticoagulation_type} />
|
||||
<Row label='抗凝剂量' value={rx.anticoagulation_dose} />
|
||||
</ContentCard>
|
||||
|
||||
{/* 血管通路 */}
|
||||
{(rx.vascular_access_type || rx.vascular_access_location) && (
|
||||
<ContentCard>
|
||||
<Text className='section-title'>血管通路</Text>
|
||||
<Row label='通路类型' value={rx.vascular_access_type} />
|
||||
<Row label='通路位置' value={rx.vascular_access_location} />
|
||||
</ContentCard>
|
||||
)}
|
||||
|
||||
{/* 超滤与干体重 */}
|
||||
{(rx.target_ultrafiltration_ml != null || rx.target_dry_weight != null) && (
|
||||
<ContentCard>
|
||||
<Text className='section-title'>超滤目标</Text>
|
||||
<Row label='目标超滤量' value={rx.target_ultrafiltration_ml != null ? `${rx.target_ultrafiltration_ml} ml` : null} />
|
||||
<Row label='目标干体重' value={rx.target_dry_weight != null ? `${rx.target_dry_weight} kg` : null} />
|
||||
</ContentCard>
|
||||
)}
|
||||
|
||||
{/* 备注 */}
|
||||
{rx.notes && (
|
||||
<ContentCard>
|
||||
<Text className='section-title'>备注</Text>
|
||||
<Text className='notes-text'>{rx.notes}</Text>
|
||||
</ContentCard>
|
||||
)}
|
||||
</PageShell>
|
||||
);
|
||||
export default function DialysisPrescriptionDetailPage() {
|
||||
return <FrozenPage />;
|
||||
}
|
||||
|
||||
@@ -1,104 +1,5 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useReachBottom } from '@tarojs/taro';
|
||||
import { safeNavigateTo } from '@/utils/navigate';
|
||||
import { usePageData } from '@/hooks/usePageData';
|
||||
import { getCachedPatientId } from '@/services/request';
|
||||
import { listDialysisPrescriptions } from '@/services/dialysis';
|
||||
import type { DialysisPrescription } from '@/services/dialysis';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import Loading from '@/components/Loading';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
import PageShell from '@/components/ui/PageShell';
|
||||
import './index.scss';
|
||||
import FrozenPage from '@/components/FrozenPage';
|
||||
|
||||
const STATUS_MAP: Record<string, { label: string; cls: string }> = {
|
||||
active: { label: '生效中', cls: 'active' },
|
||||
inactive: { label: '已停用', cls: 'inactive' },
|
||||
expired: { label: '已过期', cls: 'expired' },
|
||||
};
|
||||
|
||||
export default function DialysisPrescriptionList() {
|
||||
const modeClass = useElderClass();
|
||||
const [prescriptions, setPrescriptions] = useState<DialysisPrescription[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasPatient, setHasPatient] = useState(true);
|
||||
|
||||
const fetchData = useCallback(async (p: number, append = false) => {
|
||||
const patientId = getCachedPatientId();
|
||||
if (!patientId) {
|
||||
setPrescriptions([]);
|
||||
setHasPatient(false);
|
||||
return;
|
||||
}
|
||||
setHasPatient(true);
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await listDialysisPrescriptions({ patient_id: patientId, page: p, page_size: 20 });
|
||||
const list = res.data || [];
|
||||
setPrescriptions(append ? (prev) => [...prev, ...list] : list);
|
||||
setTotal(res.total);
|
||||
setPage(p);
|
||||
} catch (err) {
|
||||
console.warn('[prescription] 加载失败:', err);
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
usePageData(async () => { await fetchData(1); }, { throttleMs: 10000, enablePullDown: true });
|
||||
|
||||
useReachBottom(() => {
|
||||
if (!loading && prescriptions.length < total) {
|
||||
fetchData(page + 1, true);
|
||||
}
|
||||
});
|
||||
|
||||
const statusInfo = (s: string) => STATUS_MAP[s] || { label: s, cls: '' };
|
||||
|
||||
return (
|
||||
<PageShell className={modeClass}>
|
||||
<Text className='page-title'>透析处方</Text>
|
||||
|
||||
<View className='prescription-list'>
|
||||
{prescriptions.map((p) => {
|
||||
const si = statusInfo(p.status);
|
||||
return (
|
||||
<View
|
||||
className='prescription-card'
|
||||
key={p.id}
|
||||
onClick={() => safeNavigateTo(`/pages/pkg-profile/dialysis-prescriptions/detail/index?id=${p.id}`)}
|
||||
>
|
||||
<View className='prescription-card-top'>
|
||||
<Text className='prescription-model'>{p.dialyzer_model || '未指定型号'}</Text>
|
||||
<Text className={`status-tag ${si.cls}`}>{si.label}</Text>
|
||||
</View>
|
||||
<View className='prescription-meta'>
|
||||
{p.frequency_per_week != null && (
|
||||
<Text className='meta-item'>{p.frequency_per_week}次/周</Text>
|
||||
)}
|
||||
{p.duration_minutes != null && (
|
||||
<Text className='meta-item'>每次{p.duration_minutes}分钟</Text>
|
||||
)}
|
||||
</View>
|
||||
{(p.effective_from || p.effective_to) && (
|
||||
<Text className='prescription-date'>
|
||||
{p.effective_from || '...'} ~ {p.effective_to || '...'}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{prescriptions.length === 0 && !loading && (
|
||||
<EmptyState text={hasPatient ? '暂无透析处方' : '请先在就诊人管理中选择就诊人'} />
|
||||
)}
|
||||
|
||||
{loading && <Loading />}
|
||||
</PageShell>
|
||||
);
|
||||
export default function DialysisPrescriptionsPage() {
|
||||
return <FrozenPage />;
|
||||
}
|
||||
|
||||
@@ -1,125 +1,5 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import { usePageData } from '@/hooks/usePageData';
|
||||
import { getDialysisRecord } from '@/services/dialysis';
|
||||
import type { DialysisRecord } from '@/services/dialysis';
|
||||
import Loading from '@/components/Loading';
|
||||
import PageShell from '@/components/ui/PageShell';
|
||||
import ContentCard from '@/components/ui/ContentCard';
|
||||
import { useElderClass } from '../../../../hooks/useElderClass';
|
||||
import './index.scss';
|
||||
import FrozenPage from '@/components/FrozenPage';
|
||||
|
||||
const STATUS_MAP: Record<string, { label: string; cls: string }> = {
|
||||
draft: { label: '草稿', cls: 'draft' },
|
||||
completed: { label: '已完成', cls: 'completed' },
|
||||
reviewed: { label: '已审核', cls: 'reviewed' },
|
||||
};
|
||||
|
||||
const TYPE_MAP: Record<string, string> = {
|
||||
HD: '血液透析',
|
||||
HDF: '血液透析滤过',
|
||||
HF: '血液滤过',
|
||||
};
|
||||
|
||||
export default function DialysisRecordDetail() {
|
||||
const modeClass = useElderClass();
|
||||
const router = useRouter();
|
||||
const id = router.params.id || '';
|
||||
const [record, setRecord] = useState<DialysisRecord | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetchDetail = useCallback(async () => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getDialysisRecord(id);
|
||||
setRecord(data);
|
||||
} catch (err) {
|
||||
console.warn('[dialysis] 加载失败:', err);
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
usePageData(fetchDetail, { throttleMs: 60000 });
|
||||
|
||||
if (loading) return <PageShell className={modeClass}><Loading /></PageShell>;
|
||||
if (!record) return <PageShell className={modeClass}><View className='empty-state'><Text className='empty-text'>记录不存在</Text></View></PageShell>;
|
||||
|
||||
const si = STATUS_MAP[record.status] || { label: record.status, cls: '' };
|
||||
|
||||
return (
|
||||
<PageShell className={modeClass}>
|
||||
{/* 状态头部 */}
|
||||
<ContentCard>
|
||||
<View className='header-row'>
|
||||
<Text className='detail-title'>{record.dialysis_date}</Text>
|
||||
<Text className={`status-tag ${si.cls}`}>{si.label}</Text>
|
||||
</View>
|
||||
<Text className='header-sub'>{TYPE_MAP[record.dialysis_type] || record.dialysis_type}</Text>
|
||||
{record.reviewed_at && <Text className='review-info'>审核时间 {record.reviewed_at}</Text>}
|
||||
</ContentCard>
|
||||
|
||||
{/* 基本信息 */}
|
||||
<ContentCard>
|
||||
<Text className='section-title'>基本信息</Text>
|
||||
{record.start_time && (
|
||||
<View className='detail-row'><Text className='detail-label'>开始时间</Text><Text className='detail-value'>{record.start_time}</Text></View>
|
||||
)}
|
||||
{record.end_time && (
|
||||
<View className='detail-row'><Text className='detail-label'>结束时间</Text><Text className='detail-value'>{record.end_time}</Text></View>
|
||||
)}
|
||||
{record.dialysis_duration != null && (
|
||||
<View className='detail-row'><Text className='detail-label'>透析时长</Text><Text className='detail-value'>{record.dialysis_duration} 分钟</Text></View>
|
||||
)}
|
||||
{record.blood_flow_rate != null && (
|
||||
<View className='detail-row'><Text className='detail-label'>血流速</Text><Text className='detail-value'>{record.blood_flow_rate} ml/min</Text></View>
|
||||
)}
|
||||
{record.ultrafiltration_volume != null && (
|
||||
<View className='detail-row'><Text className='detail-label'>超滤量</Text><Text className='detail-value'>{record.ultrafiltration_volume} ml</Text></View>
|
||||
)}
|
||||
</ContentCard>
|
||||
|
||||
{/* 体重与血压 */}
|
||||
<ContentCard>
|
||||
<Text className='section-title'>体重与血压</Text>
|
||||
{record.dry_weight != null && (
|
||||
<View className='detail-row'><Text className='detail-label'>干体重</Text><Text className='detail-value'>{record.dry_weight} kg</Text></View>
|
||||
)}
|
||||
{record.pre_weight != null && (
|
||||
<View className='detail-row'><Text className='detail-label'>透前体重</Text><Text className='detail-value'>{record.pre_weight} kg</Text></View>
|
||||
)}
|
||||
{record.post_weight != null && (
|
||||
<View className='detail-row'><Text className='detail-label'>透后体重</Text><Text className='detail-value'>{record.post_weight} kg</Text></View>
|
||||
)}
|
||||
{record.pre_bp_systolic != null && record.pre_bp_diastolic != null && (
|
||||
<View className='detail-row'><Text className='detail-label'>透前血压</Text><Text className='detail-value'>{record.pre_bp_systolic}/{record.pre_bp_diastolic} mmHg</Text></View>
|
||||
)}
|
||||
{record.post_bp_systolic != null && record.post_bp_diastolic != null && (
|
||||
<View className='detail-row'><Text className='detail-label'>透后血压</Text><Text className='detail-value'>{record.post_bp_systolic}/{record.post_bp_diastolic} mmHg</Text></View>
|
||||
)}
|
||||
{record.pre_heart_rate != null && (
|
||||
<View className='detail-row'><Text className='detail-label'>透前心率</Text><Text className='detail-value'>{record.pre_heart_rate} bpm</Text></View>
|
||||
)}
|
||||
{record.post_heart_rate != null && (
|
||||
<View className='detail-row'><Text className='detail-label'>透后心率</Text><Text className='detail-value'>{record.post_heart_rate} bpm</Text></View>
|
||||
)}
|
||||
</ContentCard>
|
||||
|
||||
{/* 症状与并发症 */}
|
||||
{(record.symptoms || record.complication_notes) && (
|
||||
<ContentCard>
|
||||
<Text className='section-title'>症状与并发症</Text>
|
||||
{record.symptoms && Object.keys(record.symptoms).length > 0 && (
|
||||
<View className='detail-row'><Text className='detail-label'>症状</Text><Text className='detail-value'>{JSON.stringify(record.symptoms)}</Text></View>
|
||||
)}
|
||||
{record.complication_notes && (
|
||||
<View className='detail-row'><Text className='detail-label'>并发症备注</Text><Text className='detail-value'>{record.complication_notes}</Text></View>
|
||||
)}
|
||||
</ContentCard>
|
||||
)}
|
||||
</PageShell>
|
||||
);
|
||||
export default function DialysisRecordDetailPage() {
|
||||
return <FrozenPage />;
|
||||
}
|
||||
|
||||
@@ -1,109 +1,5 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useReachBottom } from '@tarojs/taro';
|
||||
import { safeNavigateTo } from '@/utils/navigate';
|
||||
import { usePageData } from '@/hooks/usePageData';
|
||||
import { getCachedPatientId } from '@/services/request';
|
||||
import { listDialysisRecords } from '@/services/dialysis';
|
||||
import type { DialysisRecord } from '@/services/dialysis';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import Loading from '@/components/Loading';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
import PageShell from '@/components/ui/PageShell';
|
||||
import './index.scss';
|
||||
import FrozenPage from '@/components/FrozenPage';
|
||||
|
||||
const TYPE_MAP: Record<string, { label: string; cls: string }> = {
|
||||
HD: { label: 'HD', cls: 'hd' },
|
||||
HDF: { label: 'HDF', cls: 'hdf' },
|
||||
HF: { label: 'HF', cls: 'hf' },
|
||||
};
|
||||
|
||||
const STATUS_MAP: Record<string, { label: string; cls: string }> = {
|
||||
draft: { label: '草稿', cls: 'draft' },
|
||||
completed: { label: '已完成', cls: 'completed' },
|
||||
reviewed: { label: '已审核', cls: 'reviewed' },
|
||||
};
|
||||
|
||||
export default function DialysisRecordList() {
|
||||
const modeClass = useElderClass();
|
||||
const [records, setRecords] = useState<DialysisRecord[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasPatient, setHasPatient] = useState(true);
|
||||
|
||||
const fetchData = useCallback(async (p: number, append = false) => {
|
||||
const patientId = getCachedPatientId();
|
||||
if (!patientId) {
|
||||
setRecords([]);
|
||||
setHasPatient(false);
|
||||
return;
|
||||
}
|
||||
setHasPatient(true);
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await listDialysisRecords(patientId, { page: p, page_size: 20 });
|
||||
const list = res.data || [];
|
||||
setRecords(append ? (prev) => [...prev, ...list] : list);
|
||||
setTotal(res.total);
|
||||
setPage(p);
|
||||
} catch (err) {
|
||||
console.warn('[dialysis] 加载失败:', err);
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
usePageData(async () => { await fetchData(1); }, { throttleMs: 10000, enablePullDown: true });
|
||||
|
||||
useReachBottom(() => {
|
||||
if (!loading && records.length < total) {
|
||||
fetchData(page + 1, true);
|
||||
}
|
||||
});
|
||||
|
||||
const typeInfo = (t: string) => TYPE_MAP[t] || { label: t, cls: '' };
|
||||
const statusInfo = (s: string) => STATUS_MAP[s] || { label: s, cls: '' };
|
||||
|
||||
return (
|
||||
<PageShell className={modeClass}>
|
||||
<Text className='page-title'>透析记录</Text>
|
||||
|
||||
<View className='record-list'>
|
||||
{records.map((r) => {
|
||||
const ti = typeInfo(r.dialysis_type);
|
||||
const si = statusInfo(r.status);
|
||||
return (
|
||||
<View
|
||||
className='record-card'
|
||||
key={r.id}
|
||||
onClick={() => safeNavigateTo(`/pages/pkg-profile/dialysis-records/detail/index?id=${r.id}`)}
|
||||
>
|
||||
<View className='record-card-top'>
|
||||
<Text className={`type-tag ${ti.cls}`}>{ti.label}</Text>
|
||||
<Text className={`status-tag ${si.cls}`}>{si.label}</Text>
|
||||
</View>
|
||||
<Text className='record-date'>{r.dialysis_date}</Text>
|
||||
{(r.pre_weight || r.post_weight) && (
|
||||
<View className='weight-row'>
|
||||
{r.pre_weight && <Text className='weight-item'>透前 {r.pre_weight}kg</Text>}
|
||||
{r.post_weight && <Text className='weight-item'>透后 {r.post_weight}kg</Text>}
|
||||
</View>
|
||||
)}
|
||||
{r.dialysis_duration && (
|
||||
<Text className='record-meta'>时长 {r.dialysis_duration}分钟</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{records.length === 0 && !loading && (
|
||||
<EmptyState text={hasPatient ? '暂无透析记录' : '请先在就诊人管理中选择就诊人'} />
|
||||
)}
|
||||
|
||||
{loading && <Loading />}
|
||||
</PageShell>
|
||||
);
|
||||
export default function DialysisRecordsPage() {
|
||||
return <FrozenPage />;
|
||||
}
|
||||
|
||||
@@ -1,118 +1,5 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import * as pointsApi from '@/services/points';
|
||||
import Loading from '@/components/Loading';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import { useElderClass } from '@/hooks/useElderClass';
|
||||
import { usePageData } from '@/hooks/usePageData';
|
||||
import PageShell from '@/components/ui/PageShell';
|
||||
import './index.scss';
|
||||
|
||||
const STATUS_MAP: Record<string, { label: string; className: string }> = {
|
||||
published: { label: '报名中', className: 'event-card__status--published' },
|
||||
ongoing: { label: '进行中', className: 'event-card__status--ongoing' },
|
||||
completed: { label: '已结束', className: 'event-card__status--completed' },
|
||||
cancelled: { label: '已取消', className: 'event-card__status--cancelled' },
|
||||
};
|
||||
import FrozenPage from '@/components/FrozenPage';
|
||||
|
||||
export default function EventsPage() {
|
||||
const modeClass = useElderClass();
|
||||
const [events, setEvents] = useState<pointsApi.OfflineEvent[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [registering, setRegistering] = useState<string | null>(null);
|
||||
|
||||
const loadEvents = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await pointsApi.listOfflineEvents({ page: 1, page_size: 50, status: 'published' });
|
||||
setEvents(res.data || []);
|
||||
} catch (err) {
|
||||
console.warn('[event] 加载失败:', err);
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
usePageData(loadEvents, { throttleMs: 10000, enablePullDown: true });
|
||||
|
||||
const handleRegister = async (event: pointsApi.OfflineEvent) => {
|
||||
setRegistering(event.id);
|
||||
try {
|
||||
await pointsApi.registerEvent(event.id);
|
||||
Taro.showToast({ title: '报名成功', icon: 'success' });
|
||||
loadEvents();
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : '报名失败';
|
||||
Taro.showToast({ title: msg.substring(0, 20), icon: 'none' });
|
||||
} finally {
|
||||
setRegistering(null);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (d: string) => {
|
||||
return new Date(d).toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) return <Loading />;
|
||||
|
||||
return (
|
||||
<PageShell className={modeClass}>
|
||||
<View className='events-header'>
|
||||
<Text className='events-header__title'>线下活动</Text>
|
||||
<Text className='events-header__subtitle'>参加活动赢取积分</Text>
|
||||
</View>
|
||||
|
||||
{events.length === 0 ? (
|
||||
<EmptyState text='暂无可报名的活动' />
|
||||
) : (
|
||||
<View className='event-list'>
|
||||
{events.map((event) => {
|
||||
const st = STATUS_MAP[event.status] || { label: event.status, className: '' };
|
||||
const isFull = event.max_participants != null && event.current_participants >= event.max_participants;
|
||||
const isRegistering = registering === event.id;
|
||||
|
||||
return (
|
||||
<View key={event.id} className='event-card'>
|
||||
<View className='event-card__header'>
|
||||
<View className={`event-card__status ${st.className}`}>
|
||||
<Text>{st.label}</Text>
|
||||
</View>
|
||||
<Text className='event-card__points'>+{event.points_reward} 积分</Text>
|
||||
</View>
|
||||
<Text className='event-card__title'>{event.title}</Text>
|
||||
{event.description && (
|
||||
<Text className='event-card__desc'>{event.description}</Text>
|
||||
)}
|
||||
<View className='event-card__info'>
|
||||
<Text className='event-card__date'>{formatDate(event.event_date)}</Text>
|
||||
{event.location && (
|
||||
<Text className='event-card__location'>{event.location}</Text>
|
||||
)}
|
||||
</View>
|
||||
<View className='event-card__footer'>
|
||||
<Text className='event-card__participants'>
|
||||
{event.current_participants}{event.max_participants ? `/${event.max_participants}` : ''} 人已报名
|
||||
</Text>
|
||||
<View
|
||||
className={`event-card__btn ${isFull ? 'event-card__btn--disabled' : ''}`}
|
||||
onClick={() => !isFull && !isRegistering && handleRegister(event)}
|
||||
>
|
||||
<Text className='event-card__btn-text'>
|
||||
{isRegistering ? '报名中...' : isFull ? '已满' : '立即报名'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</PageShell>
|
||||
);
|
||||
return <FrozenPage />;
|
||||
}
|
||||
|
||||
@@ -1,179 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { View, Text, Input, Picker } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import { createPatient, updatePatient, Patient } from '../../../services/patient';
|
||||
import { secureGet, secureRemove } from '@/utils/secure-storage';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
import { useSafeTimeout } from '@/hooks/useSafeTimeout';
|
||||
import PageShell from '@/components/ui/PageShell';
|
||||
import './index.scss';
|
||||
import FrozenPage from '@/components/FrozenPage';
|
||||
|
||||
const RELATION_OPTIONS = ['本人', '配偶', '父母', '子女', '其他'];
|
||||
const GENDER_OPTIONS = ['男', '女'];
|
||||
|
||||
export default function FamilyAdd() {
|
||||
const modeClass = useElderClass();
|
||||
const router = useRouter();
|
||||
const editId = router.params.id || '';
|
||||
const rawEdit = secureGet('edit_patient');
|
||||
const editData: Patient | null = rawEdit ? JSON.parse(rawEdit) : null;
|
||||
|
||||
const [name, setName] = useState(editData?.name || '');
|
||||
const [relationIdx, setRelationIdx] = useState(
|
||||
editData?.relation ? RELATION_OPTIONS.indexOf(editData.relation) : 0
|
||||
);
|
||||
const [genderIdx, setGenderIdx] = useState(
|
||||
editData?.gender === 'female' ? 1 : 0
|
||||
);
|
||||
const [birthDate, setBirthDate] = useState(editData?.birth_date || '');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const { safeSetTimeout } = useSafeTimeout();
|
||||
|
||||
useEffect(() => {
|
||||
return () => { secureRemove('edit_patient'); };
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!name.trim()) {
|
||||
Taro.showToast({ title: '请输入姓名', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
Taro.showLoading({ title: '提交中...' });
|
||||
try {
|
||||
if (editId && editData) {
|
||||
await updatePatient(editId, {
|
||||
name: name.trim(),
|
||||
gender: GENDER_OPTIONS[genderIdx] === '男' ? 'male' : 'female',
|
||||
birth_date: birthDate || undefined,
|
||||
relation: RELATION_OPTIONS[relationIdx],
|
||||
}, editData.version);
|
||||
Taro.hideLoading();
|
||||
Taro.showToast({ title: '修改成功', icon: 'success' });
|
||||
} else {
|
||||
await createPatient({
|
||||
name: name.trim(),
|
||||
gender: GENDER_OPTIONS[genderIdx] === '男' ? 'male' : 'female',
|
||||
birth_date: birthDate || undefined,
|
||||
});
|
||||
Taro.hideLoading();
|
||||
Taro.showToast({ title: '添加成功', icon: 'success' });
|
||||
}
|
||||
safeSetTimeout(() => Taro.navigateBack(), 1000);
|
||||
} catch (err) {
|
||||
console.warn('[family] 操作失败:', err);
|
||||
Taro.hideLoading();
|
||||
Taro.showToast({ title: editId ? '修改失败' : '添加失败', icon: 'none' });
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageShell padding="md" safeBottom={false} scroll={false} className={`family-add-page ${modeClass}`}>
|
||||
<Text className='family-add-title'>{editId ? '编辑就诊人' : '添加就诊人'}</Text>
|
||||
|
||||
{/* 提示卡片 */}
|
||||
<View className='family-add-tip'>
|
||||
<Text className='family-add-tip-title'>完善个人信息</Text>
|
||||
<Text className='family-add-tip-desc'>
|
||||
完善信息后即可使用积分商城、签到等功能。请填写真实信息。
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 表单 */}
|
||||
<View className='form-card'>
|
||||
<View className='form-item'>
|
||||
<Text className='form-label'>姓名<Text className='form-required'>*</Text></Text>
|
||||
<View className='form-input-wrap'>
|
||||
<Input
|
||||
className='form-input'
|
||||
placeholder='请输入真实姓名'
|
||||
placeholderClass='form-placeholder'
|
||||
value={name}
|
||||
onInput={(e) => setName(e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='form-item'>
|
||||
<Text className='form-label'>关系<Text className='form-required'>*</Text></Text>
|
||||
<Picker
|
||||
mode='selector'
|
||||
range={RELATION_OPTIONS}
|
||||
value={relationIdx}
|
||||
onChange={(e) => setRelationIdx(Number(e.detail.value))}
|
||||
>
|
||||
<View className='form-picker-wrap'>
|
||||
<Text className='form-picker-text'>{RELATION_OPTIONS[relationIdx]}</Text>
|
||||
<Text className='form-picker-arrow'>›</Text>
|
||||
</View>
|
||||
</Picker>
|
||||
</View>
|
||||
|
||||
<View className='form-item'>
|
||||
<Text className='form-label'>性别<Text className='form-required'>*</Text></Text>
|
||||
<Picker
|
||||
mode='selector'
|
||||
range={GENDER_OPTIONS}
|
||||
value={genderIdx}
|
||||
onChange={(e) => setGenderIdx(Number(e.detail.value))}
|
||||
>
|
||||
<View className='form-picker-wrap'>
|
||||
<Text className='form-picker-text'>{GENDER_OPTIONS[genderIdx]}</Text>
|
||||
<Text className='form-picker-arrow'>›</Text>
|
||||
</View>
|
||||
</Picker>
|
||||
</View>
|
||||
|
||||
<View className='form-item'>
|
||||
<Text className='form-label'>出生日期<Text className='form-required'>*</Text></Text>
|
||||
<Picker
|
||||
mode='date'
|
||||
value={birthDate || '2000-01-01'}
|
||||
onChange={(e) => setBirthDate(e.detail.value)}
|
||||
>
|
||||
<View className='form-picker-wrap'>
|
||||
<Text className={`form-picker-text ${!birthDate ? 'placeholder' : ''}`}>
|
||||
{birthDate || '请选择'}
|
||||
</Text>
|
||||
<Text className='form-picker-arrow'>›</Text>
|
||||
</View>
|
||||
</Picker>
|
||||
</View>
|
||||
|
||||
<View className='form-item'>
|
||||
<Text className='form-label'>手机号</Text>
|
||||
<View className='form-input-wrap'>
|
||||
<Input
|
||||
className='form-input'
|
||||
placeholder='选填,用于接收通知'
|
||||
placeholderClass='form-placeholder'
|
||||
type='number'
|
||||
maxlength={11}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='form-item'>
|
||||
<Text className='form-label'>身份证号</Text>
|
||||
<View className='form-input-wrap'>
|
||||
<Input
|
||||
className='form-input'
|
||||
placeholder='选填,用于医保对接'
|
||||
placeholderClass='form-placeholder'
|
||||
maxlength={18}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
className={`submit-btn ${submitting ? 'disabled' : ''}`}
|
||||
onClick={submitting ? undefined : handleSubmit}
|
||||
>
|
||||
<Text className='submit-text'>{submitting ? '提交中...' : editId ? '保存修改' : '确认添加'}</Text>
|
||||
</View>
|
||||
</PageShell>
|
||||
);
|
||||
export default function FamilyAddPage() {
|
||||
return <FrozenPage />;
|
||||
}
|
||||
|
||||
@@ -1,131 +1,5 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { safeNavigateTo } from '@/utils/navigate';
|
||||
import { secureSet } from '@/utils/secure-storage';
|
||||
import { usePageData } from '@/hooks/usePageData';
|
||||
import { listPatients, Patient } from '../../../services/patient';
|
||||
import { useAuthStore } from '../../../stores/auth';
|
||||
import EmptyState from '../../../components/EmptyState';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
import PageShell from '@/components/ui/PageShell';
|
||||
import './index.scss';
|
||||
import FrozenPage from '@/components/FrozenPage';
|
||||
|
||||
const RELATION_CLASS: Record<string, string> = {
|
||||
'本人': 'self',
|
||||
'配偶': 'spouse',
|
||||
'父母': 'parent',
|
||||
'子女': 'child',
|
||||
'其他': 'other',
|
||||
};
|
||||
|
||||
function getRelationClass(relation: string): string {
|
||||
return RELATION_CLASS[relation] || 'other';
|
||||
}
|
||||
|
||||
export default function FamilyList() {
|
||||
const modeClass = useElderClass();
|
||||
const [patients, setPatients] = useState<Patient[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const currentPatient = useAuthStore((s) => s.currentPatient);
|
||||
const setCurrentPatient = useAuthStore((s) => s.setCurrentPatient);
|
||||
|
||||
const fetchPatients = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await listPatients();
|
||||
setPatients(res.data || []);
|
||||
} catch (err) {
|
||||
console.warn('[family] 加载失败:', err);
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
usePageData(fetchPatients, { throttleMs: 10000 });
|
||||
|
||||
const handleSelect = (patient: Patient) => {
|
||||
setCurrentPatient({
|
||||
id: patient.id,
|
||||
name: patient.name,
|
||||
gender: patient.gender,
|
||||
birth_date: patient.birth_date,
|
||||
relation: patient.relation || '本人',
|
||||
});
|
||||
Taro.showToast({ title: `已切换为 ${patient.name}`, icon: 'success' });
|
||||
};
|
||||
|
||||
const goToAdd = () => {
|
||||
safeNavigateTo('/pages/pkg-profile/family-add/index');
|
||||
};
|
||||
|
||||
const goToEdit = (patient: Patient) => {
|
||||
secureSet('edit_patient', JSON.stringify(patient));
|
||||
safeNavigateTo(`/pages/pkg-profile/family-add/index?id=${patient.id}`);
|
||||
};
|
||||
|
||||
const genderText = (g?: string) => {
|
||||
if (g === 'male') return '男';
|
||||
if (g === 'female') return '女';
|
||||
return '未知';
|
||||
};
|
||||
|
||||
const birthYear = (d?: string) => {
|
||||
if (!d) return '';
|
||||
return d.slice(0, 4) + '年';
|
||||
};
|
||||
|
||||
return (
|
||||
<PageShell padding="md" safeBottom={false} scroll={false} className={`family-page ${modeClass}`}>
|
||||
<Text className='family-page-title'>就诊人管理</Text>
|
||||
<Text className='family-hint'>完善信息后即可使用积分商城、签到等功能。可添加多位家庭成员。</Text>
|
||||
|
||||
<View className='family-list'>
|
||||
{patients.map((p) => {
|
||||
const isActive = currentPatient?.id === p.id;
|
||||
const relClass = getRelationClass(p.relation || '本人');
|
||||
return (
|
||||
<View
|
||||
className={`family-item ${isActive ? 'active' : ''}`}
|
||||
key={p.id}
|
||||
onClick={() => handleSelect(p)}
|
||||
>
|
||||
<View className={`family-avatar family-avatar--${relClass}`}>
|
||||
<Text className='family-avatar-text'>{p.name.charAt(0)}</Text>
|
||||
</View>
|
||||
<View className='family-info'>
|
||||
<View className='family-name-row'>
|
||||
<Text className='family-name'>{p.name}</Text>
|
||||
{isActive && <Text className='family-current-tag'>当前</Text>}
|
||||
</View>
|
||||
<View className='family-meta'>
|
||||
<Text className={`family-relation-tag family-relation-tag--${relClass}`}>
|
||||
{p.relation || '本人'}
|
||||
</Text>
|
||||
<Text>{genderText(p.gender)}</Text>
|
||||
{birthYear(p.birth_date) && <Text>{birthYear(p.birth_date)}</Text>}
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
className='family-edit'
|
||||
onClick={(e) => { e.stopPropagation(); goToEdit(p); }}
|
||||
>
|
||||
<Text className='family-edit-text'>编辑</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{patients.length === 0 && !loading && (
|
||||
<EmptyState text='暂无就诊人' />
|
||||
)}
|
||||
|
||||
<View className='family-add-btn' onClick={goToAdd}>
|
||||
<Text className='family-add-icon'>+</Text>
|
||||
<Text className='family-add-text'>添加就诊人</Text>
|
||||
</View>
|
||||
</PageShell>
|
||||
);
|
||||
export default function FamilyPage() {
|
||||
return <FrozenPage />;
|
||||
}
|
||||
|
||||
@@ -1,217 +1,5 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { View, Text, Input, Picker } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { usePageData } from '@/hooks/usePageData';
|
||||
import { getCachedPatientId } from '@/services/request';
|
||||
import { requestSubscribe } from '@/services/wechat-templates';
|
||||
import EmptyState from '../../../components/EmptyState';
|
||||
import {
|
||||
listReminders,
|
||||
createReminder,
|
||||
updateReminder,
|
||||
deleteReminder,
|
||||
type MedicationReminder,
|
||||
} from '../../../services/medication-reminder';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
import PageShell from '@/components/ui/PageShell';
|
||||
import './index.scss';
|
||||
import FrozenPage from '@/components/FrozenPage';
|
||||
|
||||
export default function MedicationReminder() {
|
||||
const modeClass = useElderClass();
|
||||
const [reminders, setReminders] = useState<MedicationReminder[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [formName, setFormName] = useState('');
|
||||
const [formDosage, setFormDosage] = useState('');
|
||||
const [formTime, setFormTime] = useState('08:00');
|
||||
|
||||
const fetchReminders = useCallback(async () => {
|
||||
try {
|
||||
const res = await listReminders();
|
||||
setReminders(res.data ?? []);
|
||||
} catch (err) {
|
||||
console.warn('[medication] 加载失败:', err);
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
usePageData(
|
||||
async () => {
|
||||
await fetchReminders();
|
||||
// 请求用药提醒推送订阅
|
||||
requestSubscribe('MEDICATION_REMINDER');
|
||||
},
|
||||
{ throttleMs: 5000, enablePullDown: true },
|
||||
);
|
||||
|
||||
const handleToggle = async (r: MedicationReminder) => {
|
||||
try {
|
||||
await updateReminder(r.id, {
|
||||
is_active: !r.is_active,
|
||||
version: r.version,
|
||||
});
|
||||
fetchReminders();
|
||||
} catch (err) {
|
||||
console.warn('[medication] 操作失败:', err);
|
||||
Taro.showToast({ title: '操作失败', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (r: MedicationReminder) => {
|
||||
Taro.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个提醒吗?',
|
||||
}).then(async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await deleteReminder(r.id, r.version);
|
||||
Taro.showToast({ title: '已删除', icon: 'success' });
|
||||
fetchReminders();
|
||||
} catch (err) {
|
||||
console.warn('[medication] 删除失败:', err);
|
||||
Taro.showToast({ title: '删除失败', icon: 'none' });
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleAdd = async () => {
|
||||
if (!formName.trim()) {
|
||||
Taro.showToast({ title: '请输入药品名称', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const patientId = getCachedPatientId();
|
||||
if (!patientId) {
|
||||
Taro.showToast({ title: '请先绑定患者档案', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await createReminder({
|
||||
patient_id: patientId,
|
||||
medication_name: formName.trim(),
|
||||
dosage: formDosage.trim() || undefined,
|
||||
reminder_times: [formTime],
|
||||
is_active: true,
|
||||
});
|
||||
setFormName('');
|
||||
setFormDosage('');
|
||||
setFormTime('08:00');
|
||||
setShowForm(false);
|
||||
Taro.showToast({ title: '添加成功', icon: 'success' });
|
||||
fetchReminders();
|
||||
} catch (err) {
|
||||
console.warn('[medication] 添加失败:', err);
|
||||
Taro.showToast({ title: '添加失败', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
const nameInitial = (name: string) => {
|
||||
return name ? name.charAt(0) : '药';
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<PageShell className={modeClass}>
|
||||
<Text className='page-title'>用药提醒</Text>
|
||||
<View className='medication-loading'>
|
||||
<Text className='medication-loading-text'>加载中...</Text>
|
||||
</View>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageShell className={modeClass}>
|
||||
<Text className='page-title'>用药提醒</Text>
|
||||
|
||||
<View className='reminder-list'>
|
||||
{reminders.map((r) => (
|
||||
<View className={`reminder-card ${!r.is_active ? 'disabled' : ''}`} key={r.id}>
|
||||
<View className='reminder-avatar'>
|
||||
<Text className='reminder-avatar-text'>{nameInitial(r.medication_name)}</Text>
|
||||
</View>
|
||||
<View className='reminder-info'>
|
||||
<Text className='reminder-name'>{r.medication_name}</Text>
|
||||
<Text className='reminder-dosage'>
|
||||
{r.dosage || '-'} | {r.reminder_times?.join(', ') || '-'}
|
||||
</Text>
|
||||
</View>
|
||||
<View className='reminder-actions'>
|
||||
<View
|
||||
className={`toggle ${r.is_active ? 'on' : 'off'}`}
|
||||
onClick={() => handleToggle(r)}
|
||||
>
|
||||
<View className='toggle-dot' />
|
||||
</View>
|
||||
<Text
|
||||
className='delete-btn'
|
||||
onClick={() => handleDelete(r)}
|
||||
>
|
||||
删除
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{reminders.length === 0 && (
|
||||
<EmptyState text='暂无用药提醒' />
|
||||
)}
|
||||
|
||||
{showForm && (
|
||||
<View className='form-card'>
|
||||
<Text className='form-card-title'>添加提醒</Text>
|
||||
<View className='form-item'>
|
||||
<Text className='form-label'>药品名称</Text>
|
||||
<Input
|
||||
className='form-input'
|
||||
placeholder='请输入药品名称'
|
||||
placeholderClass='form-placeholder'
|
||||
value={formName}
|
||||
onInput={(e) => setFormName(e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
<View className='form-item'>
|
||||
<Text className='form-label'>剂量</Text>
|
||||
<Input
|
||||
className='form-input'
|
||||
placeholder='如: 1片、10ml'
|
||||
placeholderClass='form-placeholder'
|
||||
value={formDosage}
|
||||
onInput={(e) => setFormDosage(e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
<View className='form-item'>
|
||||
<Text className='form-label'>提醒时间</Text>
|
||||
<Picker
|
||||
mode='time'
|
||||
value={formTime}
|
||||
onChange={(e) => setFormTime(e.detail.value)}
|
||||
>
|
||||
<View className='time-picker-wrap'>
|
||||
<Text className='time-value'>{formTime}</Text>
|
||||
<Text className='time-modify'>修改</Text>
|
||||
</View>
|
||||
</Picker>
|
||||
</View>
|
||||
<View className='form-actions'>
|
||||
<View className='form-cancel' onClick={() => setShowForm(false)}>
|
||||
<Text className='form-cancel-text'>取消</Text>
|
||||
</View>
|
||||
<View className='form-confirm' onClick={handleAdd}>
|
||||
<Text className='form-confirm-text'>确认</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{!showForm && (
|
||||
<View className='add-btn' onClick={() => setShowForm(true)}>
|
||||
<Text className='add-text'>添加提醒</Text>
|
||||
</View>
|
||||
)}
|
||||
</PageShell>
|
||||
);
|
||||
export default function MedicationPage() {
|
||||
return <FrozenPage />;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ interface MenuItem {
|
||||
bg: string;
|
||||
color: string;
|
||||
path: string;
|
||||
isSwitchTab?: boolean;
|
||||
}
|
||||
|
||||
interface MenuGroup {
|
||||
@@ -29,43 +28,15 @@ interface MenuGroup {
|
||||
|
||||
const LOGGED_IN_GROUPS: MenuGroup[] = [
|
||||
{
|
||||
title: '健康管理',
|
||||
title: '健康档案',
|
||||
items: [
|
||||
{ label: '健康记录', icon: '健', bg: 'pri-l', color: 'pri', path: '/pages/pkg-profile/health-records/index' },
|
||||
{ label: '健康档案', icon: '健', bg: 'pri-l', color: 'pri', path: '/pages/pkg-profile/health-records/index' },
|
||||
{ label: '我的报告', icon: '报', bg: 'acc-l', color: 'acc', path: '/pages/pkg-profile/reports/index' },
|
||||
{ label: 'AI 分析', icon: '智', bg: 'pri-l', color: 'pri', path: '/pages/ai-report/list/index' },
|
||||
{ label: '诊断记录', icon: '诊', bg: 'acc-l', color: 'acc', path: '/pages/pkg-profile/diagnoses/index' },
|
||||
{ label: '用药记录', icon: '药', bg: 'pri-l', color: 'pri', path: '/pages/pkg-profile/medication/index' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '就诊服务',
|
||||
items: [
|
||||
{ label: '我的预约', icon: '约', bg: 'pri-l', color: 'pri', path: '/pages/appointment/index' },
|
||||
{ label: '我的随访', icon: '随', bg: 'acc-l', color: 'acc', path: '/pages/pkg-profile/followups/index' },
|
||||
{ label: '在线咨询', icon: '问', bg: 'pri-l', color: 'pri', path: '/pages/consultation/index' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '透析管理',
|
||||
items: [
|
||||
{ label: '透析记录', icon: '透', bg: 'pri-l', color: 'pri', path: '/pages/pkg-profile/dialysis-records/index' },
|
||||
{ label: '透析处方', icon: '方', bg: 'acc-l', color: 'acc', path: '/pages/pkg-profile/dialysis-prescriptions/index' },
|
||||
{ label: '知情同意', icon: '知', bg: 'pri-l', color: 'pri', path: '/pages/pkg-profile/consents/index' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '生活服务',
|
||||
items: [
|
||||
{ label: '积分商城', icon: '礼', bg: 'pri-l', color: 'pri', path: '/pages/mall/index' },
|
||||
{ label: '线下活动', icon: '活', bg: 'acc-l', color: 'acc', path: '/pages/pkg-profile/events/index' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '账号',
|
||||
items: [
|
||||
{ label: '就诊人管理', icon: '家', bg: 'pri-l', color: 'pri', path: '/pages/pkg-profile/family/index' },
|
||||
{ label: '长辈模式', icon: '老', bg: 'acc-l', color: 'acc', path: '/pages/pkg-profile/elder-mode/index' },
|
||||
{ label: '设备同步', icon: '设', bg: 'surface-alt', color: 'tx3', path: '/pages/pkg-health/device-sync/index' },
|
||||
{ label: '设置', icon: '齿', bg: 'surface-alt', color: 'tx3', path: '/pages/pkg-profile/settings/index' },
|
||||
],
|
||||
@@ -76,7 +47,6 @@ const GUEST_GROUPS: MenuGroup[] = [
|
||||
{
|
||||
title: '设置',
|
||||
items: [
|
||||
{ label: '长辈模式', icon: '老', bg: 'acc-l', color: 'acc', path: '/pages/pkg-profile/elder-mode/index' },
|
||||
{ label: '设置', icon: '齿', bg: 'surface-alt', color: 'tx3', path: '/pages/pkg-profile/settings/index' },
|
||||
],
|
||||
},
|
||||
@@ -101,9 +71,9 @@ export default function Profile() {
|
||||
await refreshPoints();
|
||||
setPointsLoading(false);
|
||||
try {
|
||||
const res = await notificationService.list<{ total?: number; data?: { read?: boolean }[] }>({ page: 1, page_size: 50 });
|
||||
const items = (res as { data?: { read?: boolean }[] })?.data || [];
|
||||
setUnreadCount(items.filter((n) => !n.read).length);
|
||||
const res = await notificationService.getUnreadCount();
|
||||
const count = (res as { data?: { count?: number } })?.data?.count ?? 0;
|
||||
setUnreadCount(count);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}, [isGuest, refreshPoints]);
|
||||
@@ -111,11 +81,7 @@ export default function Profile() {
|
||||
usePageData(fetchPoints, { throttleMs: 5000 });
|
||||
|
||||
const handleMenuClick = (item: MenuItem) => {
|
||||
if (item.isSwitchTab) {
|
||||
Taro.switchTab({ url: item.path });
|
||||
} else {
|
||||
safeNavigateTo(item.path);
|
||||
}
|
||||
safeNavigateTo(item.path);
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
|
||||
Reference in New Issue
Block a user