Files
hms/apps/miniprogram/src/pages/profile/index.tsx
iven f11dd59382 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
2026-05-23 12:27:14 +08:00

212 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}