- 智能合并:微信注册时用手机号盲索引匹配已有患者档案,避免重复建 档(AuthState 添加 PiiCrypto + ensure_patient_record 增加盲索引查询) - 角色冻结:小程序仅允许患者角色登录,医护角色被拦截 (auth_service.rs 添加反向拦截 + 登录页移除 credential login 表单) - 页面冻结:10 个非核心页面替换为 FrozenPage 占位组件(用药/知情同意 /透析/家属/诊断/事件),移除 profile 导航入口,移除医生端预加载 - 医生端代码保留,仅隐藏入口,后续可零成本恢复 讨论记录:docs/discussions/2026-05-23-account-registration-login-flow.md
212 lines
7.5 KiB
TypeScript
212 lines
7.5 KiB
TypeScript
import { View, Text } from '@tarojs/components';
|
||
import Taro from '@tarojs/taro';
|
||
import { safeNavigateTo } from '@/utils/navigate';
|
||
import { useState, useCallback } from 'react';
|
||
import { useAuthStore } from '../../stores/auth';
|
||
import { usePointsStore } from '../../stores/points';
|
||
import { useUIStore } from '../../stores/ui';
|
||
import { navigateToLogin } from '../../utils/navigate';
|
||
import { usePageData } from '@/hooks/usePageData';
|
||
import { notificationService } from '@/services/notification';
|
||
import Loading from '../../components/Loading';
|
||
import PageShell from '@/components/ui/PageShell';
|
||
import ContentCard from '@/components/ui/ContentCard';
|
||
import './index.scss';
|
||
|
||
interface MenuItem {
|
||
label: string;
|
||
icon: string;
|
||
bg: string;
|
||
color: string;
|
||
path: string;
|
||
}
|
||
|
||
interface MenuGroup {
|
||
title: string;
|
||
items: MenuItem[];
|
||
}
|
||
|
||
const LOGGED_IN_GROUPS: MenuGroup[] = [
|
||
{
|
||
title: '健康档案',
|
||
items: [
|
||
{ 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' },
|
||
],
|
||
},
|
||
{
|
||
title: '账号',
|
||
items: [
|
||
{ 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' },
|
||
],
|
||
},
|
||
];
|
||
|
||
const GUEST_GROUPS: MenuGroup[] = [
|
||
{
|
||
title: '设置',
|
||
items: [
|
||
{ label: '设置', icon: '齿', bg: 'surface-alt', color: 'tx3', path: '/pages/pkg-profile/settings/index' },
|
||
],
|
||
},
|
||
];
|
||
|
||
export default function Profile() {
|
||
const user = useAuthStore((s) => s.user);
|
||
const logout = useAuthStore((s) => s.logout);
|
||
const pointsAccount = usePointsStore((s) => s.account);
|
||
const checkinInfo = usePointsStore((s) => s.checkinStatus);
|
||
const refreshPoints = usePointsStore((s) => s.refresh);
|
||
const mode = useUIStore((s) => s.mode);
|
||
const modeClass = mode === 'elder' ? 'elder-mode' : '';
|
||
const isGuest = !user;
|
||
const groups = isGuest ? GUEST_GROUPS : LOGGED_IN_GROUPS;
|
||
const [pointsLoading, setPointsLoading] = useState(false);
|
||
const [unreadCount, setUnreadCount] = useState(0);
|
||
|
||
const fetchPoints = useCallback(async () => {
|
||
if (!isGuest) {
|
||
setPointsLoading(true);
|
||
await refreshPoints();
|
||
setPointsLoading(false);
|
||
try {
|
||
const res = await notificationService.getUnreadCount();
|
||
const count = (res as { data?: { count?: number } })?.data?.count ?? 0;
|
||
setUnreadCount(count);
|
||
} catch { /* ignore */ }
|
||
}
|
||
}, [isGuest, refreshPoints]);
|
||
|
||
usePageData(fetchPoints, { throttleMs: 5000 });
|
||
|
||
const handleMenuClick = (item: MenuItem) => {
|
||
safeNavigateTo(item.path);
|
||
};
|
||
|
||
const handleLogout = () => {
|
||
Taro.showModal({
|
||
title: '退出登录',
|
||
content: '确定要退出登录吗?',
|
||
}).then((res) => {
|
||
if (res.confirm) {
|
||
logout();
|
||
}
|
||
});
|
||
};
|
||
|
||
const displayName = user?.display_name || user?.username || (user?.phone ? `${user.phone.slice(-4)}` : '') || '用户';
|
||
const displayInitial = (user?.display_name || user?.username || '用').charAt(0);
|
||
|
||
return (
|
||
<PageShell padding="md" safeBottom={false} scroll={false} className={`profile-page ${modeClass}`}>
|
||
{/* 用户信息卡片 */}
|
||
{isGuest ? (
|
||
<ContentCard variant="elevated" onPress={navigateToLogin} className="profile-user-card">
|
||
<View className='profile-avatar profile-avatar--guest'>
|
||
<Text className='profile-avatar-char'>?</Text>
|
||
</View>
|
||
<View className='profile-user-info'>
|
||
<Text className='profile-name'>未登录</Text>
|
||
<Text className='profile-phone'>点击登录,开启健康管理之旅</Text>
|
||
</View>
|
||
<Text className='profile-arrow'>›</Text>
|
||
</ContentCard>
|
||
) : (
|
||
<>
|
||
<ContentCard variant="elevated" className="profile-user-card">
|
||
<View className='profile-avatar'>
|
||
<Text className='profile-avatar-char'>{displayInitial}</Text>
|
||
</View>
|
||
<View className='profile-user-info'>
|
||
<Text className='profile-name'>{displayName}</Text>
|
||
<Text className='profile-phone'>
|
||
{user?.phone ? `${user.phone.slice(0, 3)}****${user.phone.slice(-4)}` : ''}
|
||
</Text>
|
||
</View>
|
||
<Text className='profile-arrow'>›</Text>
|
||
</ContentCard>
|
||
|
||
{/* 积分 + 打卡 */}
|
||
{pointsLoading ? (
|
||
<Loading />
|
||
) : (
|
||
<View className='profile-stats-row'>
|
||
<ContentCard padding="sm" margin="none">
|
||
<View className='stat-card'>
|
||
<Text className='stat-value stat-pri'>{(pointsAccount?.balance ?? 0).toLocaleString()}</Text>
|
||
<Text className='stat-label'>健康积分</Text>
|
||
</View>
|
||
</ContentCard>
|
||
<ContentCard padding="sm" margin="none">
|
||
<View className='stat-card'>
|
||
<Text className='stat-value stat-acc'>{checkinInfo?.consecutive_days ?? 0}<Text className='stat-unit'>天</Text></Text>
|
||
<Text className='stat-label'>连续打卡</Text>
|
||
</View>
|
||
</ContentCard>
|
||
</View>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
{/* 消息通知入口 */}
|
||
{!isGuest && (
|
||
<View className='menu-group'>
|
||
<ContentCard
|
||
padding="none"
|
||
margin="none"
|
||
onPress={() => safeNavigateTo('/pages/pkg-profile/notifications/index')}
|
||
>
|
||
<View className='menu-item'>
|
||
<View className='menu-icon menu-icon--pri-l'>
|
||
<Text className='menu-icon-text menu-icon-text--pri'>讯</Text>
|
||
</View>
|
||
<Text className='menu-label'>消息通知</Text>
|
||
{unreadCount > 0 && (
|
||
<View className='menu-badge'>
|
||
<Text className='menu-badge-text'>{unreadCount > 99 ? '99+' : unreadCount}</Text>
|
||
</View>
|
||
)}
|
||
<Text className='menu-arrow'>›</Text>
|
||
</View>
|
||
</ContentCard>
|
||
</View>
|
||
)}
|
||
|
||
{/* 分组菜单 */}
|
||
{groups.map((group) => (
|
||
<View className='menu-group' key={group.title}>
|
||
<Text className='menu-group-title'>{group.title}</Text>
|
||
<ContentCard padding="none" margin="none">
|
||
{group.items.map((item, idx) => (
|
||
<View
|
||
className='menu-item'
|
||
key={item.label}
|
||
onClick={() => handleMenuClick(item)}
|
||
>
|
||
<View className={`menu-icon menu-icon--${item.bg}`}>
|
||
<Text className={`menu-icon-text menu-icon-text--${item.color}`}>{item.icon}</Text>
|
||
</View>
|
||
<Text className='menu-label'>{item.label}</Text>
|
||
{idx < group.items.length - 1 && <View className='menu-divider' />}
|
||
<Text className='menu-arrow'>›</Text>
|
||
</View>
|
||
))}
|
||
</ContentCard>
|
||
</View>
|
||
))}
|
||
|
||
{/* 退出登录 / 登录 */}
|
||
{isGuest ? (
|
||
<View className='profile-logout' onClick={navigateToLogin}>
|
||
<Text className='logout-text logout-text--pri'>登录账号</Text>
|
||
</View>
|
||
) : (
|
||
<View className='profile-logout' onClick={handleLogout}>
|
||
<Text className='logout-text'>退出登录</Text>
|
||
</View>
|
||
)}
|
||
</PageShell>
|
||
);
|
||
}
|