feat(miniprogram): 访客模式 + 长辈模式 + MCP 自动化脚本
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

访客模式:
- 未登录用户可见首页(轮播图+健康资讯+登录引导)和"我的"页面
- 健康和消息 tab 显示 GuestGuard 登录拦截
- 登录页增加"暂不登录,先看看"跳过入口
- 401 拦截器增加 hasToken 检查,避免访客被重定向到登录页
- 退出登录后 reLaunch 到首页而非登录页

长辈模式:
- 新增 stores/ui.ts 管理显示模式(标准/长辈)
- 长辈模式放大字体 ×1.3、间距 ×1.2、按钮加大
- "我的 → 账号 → 长辈模式"切换页
- 设置持久化到 Storage

修复:
- Health/Messages 页面 Hooks 顺序违规(条件 return 在 hooks 之间)
  导致访客模式下页面白屏,所有 hooks 移到条件判断之前

工程:
- scripts/mpsync.sh/ps1 自动清理残留 DevTools 进程
- project.config.json 默认关闭域名校验
This commit is contained in:
iven
2026-05-09 11:42:44 +08:00
parent 0c28969c3b
commit 085163ec7a
23 changed files with 1385 additions and 312 deletions

View File

@@ -1,51 +1,96 @@
import React from 'react';
import { View, Text } from '@tarojs/components';
import Taro, { useDidShow } from '@tarojs/taro';
import { useAuthStore } from '../../stores/auth';
import { usePointsStore } from '../../stores/points';
import { useUIStore } from '../../stores/ui';
import './index.scss';
const MENU_ITEMS = [
{ label: '就诊人管理', icon: '家', bg: 'pri-l' },
{ label: '我的报告', icon: '报', bg: 'acc-l' },
{ label: '健康记录', icon: '健', bg: 'pri-l' },
{ label: '诊断记录', icon: '诊', bg: 'acc-l' },
{ label: '我的随访', icon: '随', bg: 'pri-l' },
{ label: '我的预约', icon: '约', bg: 'acc-l' },
{ label: '用药记录', icon: '药', bg: 'pri-l' },
{ 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' },
interface MenuItem {
label: string;
icon: string;
bg: string;
color: string;
path: string;
isSwitchTab?: boolean;
}
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' },
{ 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', isSwitchTab: true },
{ label: '线下活动', icon: '活', bg: 'acc-l', color: 'acc', path: '/pages/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/device-sync/index' },
{ label: '设置', icon: '齿', bg: 'surface-alt', color: 'tx3', path: '/pages/pkg-profile/settings/index' },
],
},
];
const MENU_PATHS: Record<string, string> = {
'就诊人管理': '/pages/pkg-profile/family/index',
'我的报告': '/pages/pkg-profile/reports/index',
'健康记录': '/pages/pkg-profile/health-records/index',
'诊断记录': '/pages/pkg-profile/diagnoses/index',
'我的随访': '/pages/pkg-profile/followups/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/pkg-profile/settings/index',
};
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' },
],
},
];
export default function Profile() {
const { user, logout } = useAuthStore();
const { account: pointsAccount, checkinStatus: checkinInfo, refresh: refreshPoints } = usePointsStore();
const mode = useUIStore((s) => s.mode);
const modeClass = mode === 'elder' ? 'elder-mode' : '';
const isGuest = !user;
useDidShow(() => {
refreshPoints();
if (!isGuest) refreshPoints();
});
const handleMenuClick = (label: string) => {
const path = MENU_PATHS[label];
if (path) Taro.navigateTo({ url: path });
const handleMenuClick = (item: MenuItem) => {
if (item.isSwitchTab) {
Taro.switchTab({ url: item.path });
} else {
Taro.navigateTo({ url: item.path });
}
};
const handleLogout = () => {
@@ -59,54 +104,84 @@ export default function Profile() {
});
};
const groups = isGuest ? GUEST_GROUPS : LOGGED_IN_GROUPS;
return (
<View className='profile-page'>
<View className={`profile-page ${modeClass}`}>
{/* 用户信息卡片 */}
<View className='profile-user-card'>
<View className='profile-avatar'>
<Text className='profile-avatar-icon'>
{(user?.display_name || '访').charAt(0)}
</Text>
</View>
<View className='profile-user-info'>
<Text className='profile-name'>{user?.display_name || '未登录'}</Text>
<Text className='profile-phone'>{user?.phone || ''}</Text>
</View>
</View>
{/* 积分 + 打卡 — 两个独立卡片并排 */}
<View className='profile-stats-row'>
<View className='stat-card'>
<Text className='stat-value stat-pri'>{(pointsAccount?.balance ?? 0).toLocaleString()}</Text>
<Text className='stat-label'></Text>
</View>
<View className='stat-card'>
<Text className='stat-value stat-acc'>{checkinInfo?.consecutive_days ?? 0}</Text>
<Text className='stat-label'></Text>
</View>
</View>
{/* 菜单 */}
<View className='profile-menu'>
{MENU_ITEMS.map((item) => (
<View
className='menu-item'
key={item.label}
onClick={() => handleMenuClick(item.label)}
>
<View className={`menu-icon menu-icon--${item.bg}`}>
<Text className='menu-icon-text'>{item.icon}</Text>
</View>
<Text className='menu-label'>{item.label}</Text>
<Text className='menu-arrow'></Text>
{isGuest ? (
<View className='profile-user-card' onClick={() => Taro.navigateTo({ url: '/pages/login/index' })}>
<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>
</View>
) : (
<>
<View className='profile-user-card'>
<View className='profile-avatar'>
<Text className='profile-avatar-char'>{(user?.display_name || '访').charAt(0)}</Text>
</View>
<View className='profile-user-info'>
<Text className='profile-name'>{user?.display_name || '访客'}</Text>
<Text className='profile-phone'>
{user?.phone ? `${user.phone.slice(0, 3)}****${user.phone.slice(-4)}` : ''}
</Text>
</View>
<Text className='profile-arrow'></Text>
</View>
))}
</View>
{/* 退出登录 */}
<View className='profile-logout' onClick={handleLogout}>
<Text className='logout-text'>退</Text>
</View>
{/* 积分 + 打卡 */}
<View className='profile-stats-row'>
<View className='stat-card'>
<Text className='stat-value stat-pri'>{(pointsAccount?.balance ?? 0).toLocaleString()}</Text>
<Text className='stat-label'></Text>
</View>
<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>
</View>
</>
)}
{/* 分组菜单 */}
{groups.map((group) => (
<View className='menu-group' key={group.title}>
<Text className='menu-group-title'>{group.title}</Text>
<View className='menu-group-card'>
{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>
))}
</View>
</View>
))}
{/* 退出登录 / 登录 */}
{isGuest ? (
<View className='profile-logout' onClick={() => Taro.navigateTo({ url: '/pages/login/index' })}>
<Text className='logout-text logout-text--pri'></Text>
</View>
) : (
<View className='profile-logout' onClick={handleLogout}>
<Text className='logout-text'>退</Text>
</View>
)}
</View>
);
}