Compare commits

18 Commits

Author SHA1 Message Date
iven
d6abf45e7e docs(health): 媒体库与轮播图实施计划 — 5 Chunk / 22 Task
Some checks failed
CI / security-audit (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
Chunk 1: 后端实体 + 迁移 + DTO (Task 1-7)
Chunk 2: 后端 Service 层 (Task 8-9)
Chunk 3: Handler + 路由 + 公开端点 + 签名URL (Task 10-14)
Chunk 4: 前端 API + MediaPicker + 页面 (Task 15-20)
Chunk 5: 小程序访客页改造 (Task 21-22)
2026-05-10 13:35:30 +08:00
iven
5c5c099fb2 docs(health): 设计规格评审修复 — 3 CRITICAL + 5 HIGH + 关键 MEDIUM
修复项:
- C1: 公开端点增加租户识别机制(X-Tenant-Id / query param)
- C2: 签名 URL 增加路径规范化 + HMAC 输入格式 + is_public 实时校验
- C3: crop 端点补全权限码 health.media.manage
- H2: secret_key 生产环境 panic 保护
- H3: 软删除 media_item 级联设置 banner inactive
- H4: 补全 health.banners.list 权限码
- H5: 公开路由注册到 public_routes + 菜单种子迁移
- M3: 公开文章返回专用 PublicArticleListItem DTO
- M4: 新增"首页推荐"分类种子迁移
2026-05-10 11:40:44 +08:00
iven
a12fe0e8a9 docs(health): 媒体库与轮播图管理设计规格 + UI 可视化方案
新增设计规格涵盖:
- media_folder / media_item / banner 三个实体
- 媒体库 API(CRUD + 上传 + 裁剪 + 文件夹管理)
- 轮播图管理 API(CRUD + 排序 + 定时上下架)
- 公开端点(签名 URL 机制)+ 公开/私有访问控制
- 管理后台 UI(方案 A 左树+网格)+ MediaPicker 组件
- 小程序访客页改造(动态轮播图 + 文章卡片列表)
2026-05-10 11:32:38 +08:00
iven
3c828bfc4a fix(miniprogram): 退出登录后刷新仍保持登录态
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
根因:logout 清除 storage 期间并发请求触发 tryRefreshToken 写回新 token
修复:添加 isLoggingOut 标记,logout 时先标记阻止 token 刷新竞态
2026-05-10 10:36:17 +08:00
iven
11101ac204 feat(auth): 微信登录自动分配 patient 角色 + 创建患者档案
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
- 新增迁移 m20260510_000133:为所有租户创建 patient 角色并分配 19 个权限
- wechat_service: bind_phone 自动 assign_patient_role + ensure_patient_record
- find_or_create_user_by_phone 新用户自动获得 patient 角色和患者档案
- 小程序 auth store: bindPhone 抛出异常而非静默返回 false
- 小程序登录页: 捕获绑定错误并显示可操作的对话框
2026-05-10 09:57:45 +08:00
iven
28bcdc4208 docs: 更新 wiki — Design Token 全面接入记录
Some checks failed
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
- index.md: 更新关键数字(716 提交)、新增 Design Token 指标行
- miniprogram.md: 新增 §1.1 Design Token 系统完整文档(10 级字号表、
  4 结构 token、使用规则、关怀模式说明)、更新变更记录
2026-05-09 23:58:09 +08:00
iven
890c132890 refactor(miniprogram): 全面接入 Design Token — 68 SCSS 文件 px→var(--tk-*)
Some checks failed
CI / security-audit (push) Has been cancelled
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
- 重写 tokens.scss:校准 10 级字号 + 4 结构 token 匹配实际设计值
- 更新 mixins.scss:4 个 mixin 引用 token 替代硬编码
- 68 SCSS 文件全面迁移:font-size px → var(--tk-font-*),辅助文字色 → var(--tk-text-secondary)
- 清理 12 个页面的本地 mixin 重复定义
- elder-mode.scss 从 530 行缩减至 ~120 行:删除所有字号/颜色覆写,仅保留结构布局
- Token 覆盖率:634 引用 / 仅 3 个特殊硬编码值(72px/80px/21px)

关怀模式通过 CSS 变量级联自动生效,消除"打地鼠"问题。
2026-05-09 23:53:07 +08:00
iven
257ca94a25 fix(miniprogram): 登录页尺寸过大 + 排除关怀模式
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
- 正常模式大幅缩减:标题 48→32px、按钮高 96→56px、按钮字 32→28px
  logo 128→96px、副标题 26→16px、顶部留白 160→100px
- 登录页不应用 elder-mode class(正常模式已足够大)
- 关怀模式覆写值同步调整:标题 38px、按钮高 64px、副标题 21px

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 22:45:08 +08:00
iven
7b5138a630 feat(miniprogram): 关怀模式全覆盖 — 58/58 页面 100% 接入
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
- 医生端 18 个页面全部接入(首页/待办/告警/咨询/透析/随访/
  患者/处方/报告及其详情和创建页)
- 商城子包 3 页面(商品详情/积分兑换/订单)
- 患者端剩余页面(AI报告/文章/活动/设备同步/登录/随访详情/
  报告详情/知情同意/诊断/透析处方/透析记录/家庭成员添加)
- 页面覆盖率:22/59 (37%) → 58/58 (100%)
- useElderClass hook 统一接入模式,零样板代码

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 22:34:44 +08:00
iven
e8ccee02d5 feat(miniprogram): 关怀模式 Phase 2 — Design Token + 15 页面批量接入
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
- 新建 useElderClass hook,替代每页 3 行样板代码
- 新建 CSS 自定义属性 Design Token 系统(tokens.scss)
  正常/关怀两套值:字号、间距、触控、布局参数
- 15 个页面批量接入关怀模式 class:
  TabBar: 商城页
  主流程: 预约列表/详情/创建、咨询详情
  子包: 体征录入/趋势/日常监测/告警、用药/档案/随访/报告/家庭/设置
- 新建 elder-toast 工具(关怀模式 3s + 触觉反馈)
- 页面覆盖率:4/59 → 22/59 (37%)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 22:17:58 +08:00
iven
4335f7e144 fix(miniprogram): 关怀模式非线性放大重构 + 3 页面接入
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
- elder-mode.scss: 等比×1.3改为非线性放大(标题×1.15/正文×1.35/辅助×1.55)
- 体征网格从2列改为1列,解决放大后溢出问题
- 行高从1.5提升到1.7,对比度$tx3→$tx2增强可读性
- 健康页/消息页/咨询页接入useUIStore关怀模式
- 共享组件(EmptyState/ErrorState/Loading/StepIndicator)适配关怀模式
- 触控区域统一提升到56px+

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 22:05:06 +08:00
iven
66329852b8 fix(miniprogram): useDidShow 恢复认证状态 + E2E 全系统测试报告
Some checks failed
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
- app.tsx: 将 restoreAuth/restoreUI 从 useEffect 改为 useDidShow,
  修复 reLaunch 后 Zustand store 未恢复导致访客模式的问题
- docs/qa/e2e-full-system-report.md: 三端 E2E 测试报告更新,
  原 BUG-1(Admin 随访管理 403)确认为误报,综合通过率 100% (64/64)
- tools/weapp-mcp/e2e-test.mjs: 小程序 E2E 基础导航测试脚本
- tools/weapp-mcp/e2e-interactive-test.mjs: 小程序 E2E 交互操作测试脚本

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 18:25:43 +08:00
iven
085163ec7a 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 默认关闭域名校验
2026-05-09 11:42:44 +08:00
iven
0c28969c3b docs: 小程序端 E2E 闭环测试报告
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
5 条业务闭环全部通过:体征上报、预约挂号、咨询消息、积分商城、患者信息
Web管理端和小程序端数据流通完整,发现1个LOW问题(analytics/batch 422)
2026-05-09 08:13:37 +08:00
iven
8490344d69 fix(ai): AI 配额摘要端点 500 错误修复
Some checks failed
CI / security-audit (push) Has been cancelled
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
get_usage_summary 中 get_tenant_config 和 get_monthly_token_usage 的
数据库错误直接传播为 AppError::Internal (500),当 ai_tenant_configs 表
为空或查询异常时导致整个端点不可用。

改为 unwrap_or 降级处理:config 缺失时使用默认配额,token 查询失败时归零,
确保端点始终返回有效数据而非 500。
2026-05-09 07:52:41 +08:00
iven
e4b19090b8 docs: V1 发布前 E2E 多角色验证报告
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
5 角色全链路测试(115 API 端点 + 浏览器 UI),93% 通过率,3 BUG 已修复
2026-05-09 02:29:15 +08:00
iven
07217336e7 fix(web): 运营仪表盘数据映射错误和浮点精度修复
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
- OperatorWorkbench: "今日活跃用户" 错误使用体征上报率数据源,改为 pointsStats.active_accounts
- OperatorWorkbench: AI 摘要体征上报率显示原始浮点数(22.413793...),改为保留两位小数
- OperatorWorkbench: "科普阅读量" fallback 错误回退到积分发放数据,移除错误 fallback
- Home.tsx: 运营角色 ROLE_STATS "内容发布" 数据源错误,修正为 patientStats
- Home.tsx: 移除未使用的 TodoList/AiInsightPanel import
- .lintstagedrc.js: 修复 Windows 平台 eslint 命令,使用函数式获取文件名列表
2026-05-09 02:27:38 +08:00
iven
19705e31bd chore(demo): V1 演示数据预置脚本
Some checks failed
CI / frontend-build (push) Has been cancelled
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / security-audit (push) Has been cancelled
幂等 SQL 脚本,一键预置:
- 张建国患者档案 + 3 条体征记录
- 2 份化验单(肌酐 88→102 趋势)
- 25 个背景患者(仪表盘数据)
- 随访模板 + 21 个随访任务
- 收缩压>=160 告警规则(场景5用)
- 3 篇 CKD 科普文章
2026-05-09 02:01:41 +08:00
161 changed files with 8427 additions and 1897 deletions

View File

@@ -3,10 +3,8 @@ module.exports = {
'cargo fmt --check --', 'cargo fmt --check --',
() => 'cargo clippy -p erp-health -p erp-server -- -D warnings', () => 'cargo clippy -p erp-health -p erp-server -- -D warnings',
], ],
'apps/web/src/**/*.{ts,tsx}': () => 'apps/web/src/**/*.{ts,tsx}': (filenames) =>
process.platform === 'win32' `npx eslint --fix ${filenames.join(' ')}`,
? 'pushd apps/web && npx eslint --fix src/ & popd'
: 'cd apps/web && npx eslint --fix src/',
'apps/web/src/**/*.test.{ts,tsx}': [ 'apps/web/src/**/*.test.{ts,tsx}': [
'cd apps/web && npx vitest run --reporter=verbose', 'cd apps/web && npx vitest run --reporter=verbose',
], ],

View File

@@ -5,7 +5,7 @@ export default defineConfig(async (merge) => {
const baseConfig = { const baseConfig = {
projectName: 'hms-miniprogram', projectName: 'hms-miniprogram',
date: '2026-4-23', date: '2026-4-23',
designWidth: 750, designWidth: 375,
deviceRatio: { 640: 2.34 / 2, 750: 1, 375: 2, 828: 1.81 / 2 }, deviceRatio: { 640: 2.34 / 2, 750: 1, 375: 2, 828: 1.81 / 2 },
sourceRoot: 'src', sourceRoot: 'src',
outputRoot: 'dist', outputRoot: 'dist',

View File

@@ -3,7 +3,7 @@
"miniprogramRoot": "dist/", "miniprogramRoot": "dist/",
"compileType": "miniprogram", "compileType": "miniprogram",
"setting": { "setting": {
"urlCheck": true, "urlCheck": false,
"automationAudits": true, "automationAudits": true,
"es6": false, "es6": false,
"enhance": false, "enhance": false,
@@ -11,6 +11,10 @@
"postcss": false, "postcss": false,
"minified": true, "minified": true,
"bundle": false, "bundle": false,
"minifyWXML": true "minifyWXML": true,
} "packNpmManually": false,
"packNpmRelationList": [],
"ignoreUploadUnusedFiles": true
},
"condition": {}
} }

View File

@@ -44,6 +44,7 @@ export default defineAppConfig({
'dialysis-records/index', 'dialysis-records/detail/index', 'dialysis-records/index', 'dialysis-records/detail/index',
'dialysis-prescriptions/index', 'dialysis-prescriptions/detail/index', 'dialysis-prescriptions/index', 'dialysis-prescriptions/detail/index',
'consents/index', 'health-records/index', 'diagnoses/index', 'consents/index', 'health-records/index', 'diagnoses/index',
'elder-mode/index',
], ],
}, },
{ {

View File

@@ -1,4 +1,6 @@
@import './styles/variables.scss'; @import './styles/variables.scss';
@import './styles/tokens.scss';
@import './styles/elder-mode.scss';
page { page {
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, 'PingFang SC', font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, 'PingFang SC',

View File

@@ -1,15 +1,21 @@
import { useEffect, PropsWithChildren } from 'react'; import { useEffect, PropsWithChildren } from 'react';
import Taro from '@tarojs/taro'; import Taro, { useDidShow } from '@tarojs/taro';
import ErrorBoundary from './components/ErrorBoundary'; import ErrorBoundary from './components/ErrorBoundary';
import { flushEvents } from './services/analytics'; import { flushEvents } from './services/analytics';
import { useAuthStore } from './stores/auth'; import { useAuthStore } from './stores/auth';
import { useUIStore } from './stores/ui';
import './app.scss'; import './app.scss';
function App({ children }: PropsWithChildren<Record<string, unknown>>) { function App({ children }: PropsWithChildren<Record<string, unknown>>) {
const restoreAuth = useAuthStore((s) => s.restore); const restoreAuth = useAuthStore((s) => s.restore);
const restoreUI = useUIStore((s) => s.restore);
useDidShow(() => {
restoreAuth();
restoreUI();
});
useEffect(() => { useEffect(() => {
restoreAuth();
const timer = setInterval(() => { const timer = setInterval(() => {
flushEvents(); flushEvents();
}, 30000); }, 30000);

View File

@@ -10,7 +10,7 @@
box-shadow: $shadow-sm; box-shadow: $shadow-sm;
.device-icon { .device-icon {
font-size: 48rpx; font-size: var(--tk-font-h2);
margin-right: 20rpx; margin-right: 20rpx;
} }
@@ -18,14 +18,14 @@
flex: 1; flex: 1;
.device-name { .device-name {
font-size: 28rpx; font-size: var(--tk-font-cap);
font-weight: 600; font-weight: 600;
color: $tx; color: $tx;
display: block; display: block;
} }
.device-status { .device-status {
font-size: 24rpx; font-size: var(--tk-font-micro);
margin-top: 4rpx; margin-top: 4rpx;
display: block; display: block;
@@ -34,7 +34,7 @@
} }
.last-sync { .last-sync {
font-size: 22rpx; font-size: var(--tk-font-micro);
color: $tx3; color: $tx3;
margin-top: 4rpx; margin-top: 4rpx;
display: block; display: block;
@@ -46,6 +46,6 @@
background: $pri; background: $pri;
color: #fff; color: #fff;
border-radius: $r-pill; border-radius: $r-pill;
font-size: 24rpx; font-size: var(--tk-font-micro);
} }
} }

View File

@@ -21,20 +21,20 @@
.empty-state-icon-char { .empty-state-icon-char {
font-family: Georgia, 'Times New Roman', serif; font-family: Georgia, 'Times New Roman', serif;
font-size: 48px; font-size: var(--tk-font-hero);
font-weight: 600; font-weight: 600;
color: $tx3; color: $tx3;
} }
.empty-state-text { .empty-state-text {
font-size: 30px; font-size: var(--tk-font-num);
color: $tx2; color: $tx2;
margin-bottom: 8px; margin-bottom: 8px;
} }
.empty-state-hint { .empty-state-hint {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: var(--tk-text-secondary);
margin-bottom: 32px; margin-bottom: 32px;
} }
@@ -45,6 +45,6 @@
} }
.empty-state-action-text { .empty-state-action-text {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: #fff; color: #fff;
} }

View File

@@ -9,12 +9,12 @@
} }
.error-state-icon { .error-state-icon {
font-size: 80px; font-size: 80px; /* hero icon — kept as-is */
margin-bottom: 24px; margin-bottom: 24px;
} }
.error-state-text { .error-state-text {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx2; color: $tx2;
margin-bottom: 32px; margin-bottom: 32px;
text-align: center; text-align: center;
@@ -27,6 +27,6 @@
} }
.error-state-retry-text { .error-state-retry-text {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: #fff; color: #fff;
} }

View File

@@ -0,0 +1,64 @@
@import '../../styles/variables.scss';
@import '../../styles/mixins.scss';
.guard-page {
min-height: 100vh;
background: $bg;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 24px;
}
.guard-card {
text-align: center;
padding: 40px 20px;
}
.guard-icon-wrap {
width: 80px;
height: 80px;
border-radius: 40px;
background: $surface-alt;
@include flex-center;
margin: 0 auto 20px;
}
.guard-icon {
font-size: var(--tk-font-num);
color: $tx3;
}
.guard-title {
font-size: var(--tk-font-body-sm);
font-weight: 600;
color: $tx;
display: block;
margin-bottom: 8px;
}
.guard-desc {
font-size: var(--tk-font-cap);
color: var(--tk-text-secondary);
display: block;
margin-bottom: 24px;
}
.guard-btn {
display: inline-block;
height: 48px;
padding: 0 32px;
background: $pri;
border-radius: $r-pill;
@include flex-center;
&:active {
opacity: 0.85;
}
}
.guard-btn-text {
font-size: var(--tk-font-body-sm);
font-weight: 600;
color: #fff;
}

View File

@@ -0,0 +1,28 @@
import { View, Text } from '@tarojs/components';
import { navigateToLogin } from '../../utils/navigate';
import './index.scss';
interface GuestGuardProps {
title: string;
desc?: string;
}
export default function GuestGuard({ title, desc }: GuestGuardProps) {
return (
<View className='guard-page'>
<View className='guard-card'>
<View className='guard-icon-wrap'>
<Text className='guard-icon'></Text>
</View>
<Text className='guard-title'>{title}</Text>
{desc && <Text className='guard-desc'>{desc}</Text>}
<View
className='guard-btn'
onClick={navigateToLogin}
>
<Text className='guard-btn-text'></Text>
</View>
</View>
</View>
);
}

View File

@@ -25,6 +25,6 @@
} }
.loading-state-text { .loading-state-text {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx3; color: var(--tk-text-secondary);
} }

View File

@@ -17,13 +17,13 @@
.progress-ring-percent { .progress-ring-percent {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 22px; font-size: var(--tk-font-body);
font-weight: bold; font-weight: bold;
line-height: 1; line-height: 1;
} }
.progress-ring-unit { .progress-ring-unit {
font-size: 12px; font-size: var(--tk-font-micro);
font-weight: 600; font-weight: 600;
line-height: 1; line-height: 1;
} }

View File

@@ -39,7 +39,7 @@
justify-content: center; justify-content: center;
background: $bd-l; background: $bd-l;
color: $tx3; color: $tx3;
font-size: 24px; font-size: var(--tk-font-h2);
transition: all 0.3s ease; transition: all 0.3s ease;
z-index: 1; z-index: 1;
@@ -55,8 +55,8 @@
} }
.step-label { .step-label {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: var(--tk-text-secondary);
margin-top: 8px; margin-top: 8px;
text-align: center; text-align: center;

View File

@@ -13,8 +13,8 @@
} }
.trend-chart-empty-text { .trend-chart-empty-text {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx3; color: var(--tk-text-secondary);
} }
.trend-chart-skeleton { .trend-chart-skeleton {

View File

@@ -14,13 +14,13 @@
} }
.week-arrow { .week-arrow {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $pri; color: $pri;
padding: 0 16px; padding: 0 16px;
} }
.week-label { .week-label {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx; color: $tx;
font-weight: bold; font-weight: bold;
} }
@@ -39,13 +39,13 @@
} }
.cell-weekday { .cell-weekday {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: var(--tk-text-secondary);
display: block; display: block;
} }
.cell-date { .cell-date {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx; color: $tx;
display: block; display: block;
margin-top: 4px; margin-top: 4px;

View File

@@ -0,0 +1,6 @@
import { useUIStore } from '../stores/ui';
export function useElderClass(): string {
const mode = useUIStore((s) => s.mode);
return mode === 'elder' ? 'elder-mode' : '';
}

View File

@@ -27,7 +27,7 @@
} }
.meta-item { .meta-item {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: $tx3;
} }
@@ -45,7 +45,7 @@
} }
p { p {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx; color: $tx;
line-height: 1.8; line-height: 1.8;
margin-bottom: 16px; margin-bottom: 16px;
@@ -57,7 +57,7 @@
} }
li { li {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx; color: $tx;
line-height: 1.8; line-height: 1.8;
margin-bottom: 8px; margin-bottom: 8px;
@@ -69,7 +69,7 @@
} }
.report-content { .report-content {
font-size: 28px; font-size: var(--tk-font-body-lg);
line-height: 1.8; line-height: 1.8;
color: $tx; color: $tx;
} }
@@ -78,8 +78,8 @@
display: block; display: block;
text-align: center; text-align: center;
padding: 120px 0; padding: 120px 0;
color: $tx3; color: var(--tk-text-secondary);
font-size: 28px; font-size: var(--tk-font-body-lg);
} }
.auto-badge { .auto-badge {
@@ -91,7 +91,7 @@
display: inline-block; display: inline-block;
padding: 4px 16px; padding: 4px 16px;
border-radius: 8px; border-radius: 8px;
font-size: 22px; font-size: var(--tk-font-body);
font-weight: 500; font-weight: 500;
background: #f0e6ff; background: #f0e6ff;
color: #7c3aed; color: #7c3aed;
@@ -106,7 +106,7 @@
} }
.trend-tip-text { .trend-tip-text {
font-size: 22px; font-size: var(--tk-font-body);
color: #92400e; color: #92400e;
line-height: 1.6; line-height: 1.6;
} }

View File

@@ -3,6 +3,7 @@ import { View, Text, RichText } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro'; import Taro, { useRouter } from '@tarojs/taro';
import { getAiAnalysisDetail, type AiAnalysisItem } from '@/services/ai-analysis'; import { getAiAnalysisDetail, type AiAnalysisItem } from '@/services/ai-analysis';
import Loading from '@/components/Loading'; import Loading from '@/components/Loading';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
const TYPE_LABELS: Record<string, string> = { const TYPE_LABELS: Record<string, string> = {
@@ -44,6 +45,7 @@ function markdownToHtml(md: string): string {
} }
export default function AiReportDetail() { export default function AiReportDetail() {
const modeClass = useElderClass();
const router = useRouter(); const router = useRouter();
const id = router.params.id || ''; const id = router.params.id || '';
@@ -63,7 +65,7 @@ export default function AiReportDetail() {
if (!analysis) { if (!analysis) {
return ( return (
<View className='detail-page'> <View className={`detail-page ${modeClass}`}>
<Text className='empty-text'></Text> <Text className='empty-text'></Text>
</View> </View>
); );
@@ -77,7 +79,7 @@ export default function AiReportDetail() {
const isAutoAnalysis = (analysis.result_metadata as Record<string, unknown>)?.auto_analysis === true; const isAutoAnalysis = (analysis.result_metadata as Record<string, unknown>)?.auto_analysis === true;
return ( return (
<View className='detail-page'> <View className={`detail-page ${modeClass}`}>
<View className='detail-card'> <View className='detail-card'>
<Text className='detail-type'>{TYPE_LABELS[analysis.analysis_type] || analysis.analysis_type}</Text> <Text className='detail-type'>{TYPE_LABELS[analysis.analysis_type] || analysis.analysis_type}</Text>
<View className='detail-meta'> <View className='detail-meta'>

View File

@@ -1,28 +1,5 @@
@import '../../../styles/variables.scss'; @import '../../../styles/variables.scss';
@import '../../../styles/mixins.scss';
@mixin serif-number {
font-family: 'Georgia', 'Times New Roman', serif;
font-variant-numeric: tabular-nums;
}
@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;
}
.ai-report-page { .ai-report-page {
min-height: 100vh; min-height: 100vh;
@@ -55,7 +32,7 @@
} }
.card-type { .card-type {
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: 500; font-weight: 500;
color: $tx; color: $tx;
} }
@@ -87,19 +64,19 @@
} }
.card-time { .card-time {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: $tx3;
} }
.card-model { .card-model {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: $tx3;
} }
.no-more { .no-more {
text-align: center; text-align: center;
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: var(--tk-text-secondary);
padding: 24px 0; padding: 24px 0;
display: block; display: block;
} }

View File

@@ -4,6 +4,7 @@ import Taro from '@tarojs/taro';
import { listAiAnalysis, type AiAnalysisItem } from '@/services/ai-analysis'; import { listAiAnalysis, type AiAnalysisItem } from '@/services/ai-analysis';
import Loading from '@/components/Loading'; import Loading from '@/components/Loading';
import EmptyState from '@/components/EmptyState'; import EmptyState from '@/components/EmptyState';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
const TYPE_LABELS: Record<string, string> = { const TYPE_LABELS: Record<string, string> = {
@@ -21,6 +22,7 @@ const STATUS_MAP: Record<string, { text: string; className: string }> = {
}; };
export default function AiReportList() { export default function AiReportList() {
const modeClass = useElderClass();
const [list, setList] = useState<AiAnalysisItem[]>([]); const [list, setList] = useState<AiAnalysisItem[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
@@ -60,14 +62,14 @@ export default function AiReportList() {
if (list.length === 0) { if (list.length === 0) {
return ( return (
<View className='ai-report-page'> <View className={`ai-report-page ${modeClass}`}>
<EmptyState text='暂无 AI 分析报告' /> <EmptyState text='暂无 AI 分析报告' />
</View> </View>
); );
} }
return ( return (
<View className='ai-report-page'> <View className={`ai-report-page ${modeClass}`}>
<View className='page-title'>AI </View> <View className='page-title'>AI </View>
<ScrollView scrollY className='report-scroll' onScrollToLower={loadMore}> <ScrollView scrollY className='report-scroll' onScrollToLower={loadMore}>
{list.map((item) => { {list.map((item) => {

View File

@@ -55,7 +55,7 @@
.dept-initial-text { .dept-initial-text {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 30px; font-size: var(--tk-font-num);
font-weight: bold; font-weight: bold;
color: $pri; color: $pri;
@@ -65,7 +65,7 @@
} }
.dept-label { .dept-label {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx; color: $tx;
font-weight: 500; font-weight: 500;
} }
@@ -77,7 +77,7 @@
.slot-section-title { .slot-section-title {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
margin-bottom: 16px; margin-bottom: 16px;
@@ -114,14 +114,14 @@
.slot-time { .slot-time {
@include serif-number; @include serif-number;
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
display: block; display: block;
} }
.slot-count { .slot-count {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: $tx3;
display: block; display: block;
margin-top: 6px; margin-top: 6px;
@@ -156,7 +156,7 @@
.confirm-icon-serif { .confirm-icon-serif {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 24px; font-size: var(--tk-font-h2);
font-weight: bold; font-weight: bold;
color: $pri; color: $pri;
} }
@@ -169,12 +169,12 @@
} }
.confirm-label { .confirm-label {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: $tx3;
} }
.confirm-value { .confirm-value {
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
} }
@@ -185,7 +185,7 @@
} }
.confirm-dept-text { .confirm-dept-text {
font-size: 22px; font-size: var(--tk-font-body);
font-weight: 500; font-weight: 500;
color: $pri; color: $pri;
} }
@@ -225,7 +225,7 @@
.doctor-avatar-text { .doctor-avatar-text {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 32px; font-size: var(--tk-font-num);
color: $pri; color: $pri;
font-weight: bold; font-weight: bold;
} }
@@ -238,18 +238,18 @@
} }
.doctor-name { .doctor-name {
font-size: 30px; font-size: var(--tk-font-num);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
} }
.doctor-title { .doctor-title {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx2; color: $tx2;
} }
.doctor-specialty { .doctor-specialty {
font-size: 22px; font-size: var(--tk-font-body);
color: $pri; color: $pri;
} }
@@ -263,7 +263,7 @@
} }
.doctor-check-text { .doctor-check-text {
font-size: 24px; font-size: var(--tk-font-h2);
color: white; color: white;
font-weight: bold; font-weight: bold;
} }
@@ -274,7 +274,7 @@
} }
.form-label { .form-label {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
margin-bottom: 12px; margin-bottom: 12px;
display: block; display: block;
@@ -284,7 +284,7 @@
background: $card; background: $card;
border-radius: $r-sm; border-radius: $r-sm;
padding: 24px 28px; padding: 24px 28px;
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx; color: $tx;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
@@ -298,8 +298,8 @@
} }
.empty-text { .empty-text {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx3; color: var(--tk-text-secondary);
} }
/* 底部操作栏 */ /* 底部操作栏 */
@@ -339,7 +339,7 @@
} }
.btn-text { .btn-text {
font-size: 30px; font-size: var(--tk-font-num);
font-weight: bold; font-weight: bold;
color: $tx2; color: $tx2;
} }

View File

@@ -7,6 +7,7 @@ import { TEMPLATE_IDS } from '@/services/wechat-templates';
import { trackEvent } from '@/services/analytics'; import { trackEvent } from '@/services/analytics';
import StepIndicator from '../../../components/StepIndicator'; import StepIndicator from '../../../components/StepIndicator';
import WeekCalendar from '../../../components/WeekCalendar'; import WeekCalendar from '../../../components/WeekCalendar';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
const DEPARTMENTS = [ const DEPARTMENTS = [
@@ -44,6 +45,7 @@ export default function AppointmentCreate() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [schedules, setSchedules] = useState<any[]>([]); const [schedules, setSchedules] = useState<any[]>([]);
const [timeSlots, setTimeSlots] = useState<TimeSlot[]>([]); const [timeSlots, setTimeSlots] = useState<TimeSlot[]>([]);
const modeClass = useElderClass();
const currentPatient = useAuthStore((s) => s.currentPatient); const currentPatient = useAuthStore((s) => s.currentPatient);
@@ -148,7 +150,7 @@ export default function AppointmentCreate() {
}; };
return ( return (
<View className='create-page'> <View className={`create-page ${modeClass}`}>
<StepIndicator <StepIndicator
steps={[{ label: '选科室' }, { label: '选医生' }, { label: '选时段' }]} steps={[{ label: '选科室' }, { label: '选医生' }, { label: '选时段' }]}
current={currentStep} current={currentStep}

View File

@@ -23,14 +23,14 @@
} }
.back-text { .back-text {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $pri; color: $pri;
font-weight: 500; font-weight: 500;
} }
.header-title { .header-title {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 34px; font-size: var(--tk-font-num-lg);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
} }
@@ -80,19 +80,19 @@
} }
.status-tag-text { .status-tag-text {
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: bold; font-weight: bold;
} }
.status-doctor { .status-doctor {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 36px; font-size: var(--tk-font-num-lg);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
} }
.status-dept { .status-dept {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
} }
@@ -130,7 +130,7 @@
.info-icon-serif { .info-icon-serif {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 22px; font-size: var(--tk-font-body);
color: $pri; color: $pri;
background: $pri-l; background: $pri-l;
width: 36px; width: 36px;
@@ -143,12 +143,12 @@
} }
.info-label { .info-label {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
} }
.info-value { .info-value {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx; color: $tx;
font-weight: 500; font-weight: 500;
} }
@@ -163,8 +163,8 @@
.info-id { .info-id {
@include serif-number; @include serif-number;
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: var(--tk-text-secondary);
word-break: break-all; word-break: break-all;
max-width: 400px; max-width: 400px;
text-align: right; text-align: right;
@@ -180,7 +180,7 @@
.tips-title { .tips-title {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: bold; font-weight: bold;
color: $wrn; color: $wrn;
margin-bottom: 12px; margin-bottom: 12px;
@@ -188,7 +188,7 @@
} }
.tips-text { .tips-text {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx2; color: $tx2;
line-height: 1.6; line-height: 1.6;
} }
@@ -219,7 +219,7 @@
} }
.cancel-text { .cancel-text {
font-size: 30px; font-size: var(--tk-font-num);
font-weight: bold; font-weight: bold;
color: $dan; color: $dan;
} }

View File

@@ -5,6 +5,7 @@ import { getAppointment, cancelAppointment } from '../../../services/appointment
import type { Appointment } from '../../../services/appointment'; import type { Appointment } from '../../../services/appointment';
import Loading from '../../../components/Loading'; import Loading from '../../../components/Loading';
import ErrorState from '../../../components/ErrorState'; import ErrorState from '../../../components/ErrorState';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
const STATUS_MAP: Record<string, { label: string; className: string }> = { const STATUS_MAP: Record<string, { label: string; className: string }> = {
@@ -22,6 +23,7 @@ export default function AppointmentDetail() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(false); const [error, setError] = useState(false);
const [cancelling, setCancelling] = useState(false); const [cancelling, setCancelling] = useState(false);
const modeClass = useElderClass();
useEffect(() => { useEffect(() => {
if (!id) return; if (!id) return;
@@ -65,7 +67,7 @@ export default function AppointmentDetail() {
if (loading) { if (loading) {
return ( return (
<View className='detail-page'> <View className={`detail-page ${modeClass}`}>
<View className='detail-header'> <View className='detail-header'>
<View className='back-btn' onClick={goBack}><Text className='back-text'></Text></View> <View className='back-btn' onClick={goBack}><Text className='back-text'></Text></View>
<Text className='header-title'></Text> <Text className='header-title'></Text>
@@ -78,7 +80,7 @@ export default function AppointmentDetail() {
if (error || !appointment) { if (error || !appointment) {
return ( return (
<View className='detail-page'> <View className={`detail-page ${modeClass}`}>
<View className='detail-header'> <View className='detail-header'>
<View className='back-btn' onClick={goBack}><Text className='back-text'></Text></View> <View className='back-btn' onClick={goBack}><Text className='back-text'></Text></View>
<Text className='header-title'></Text> <Text className='header-title'></Text>
@@ -90,7 +92,7 @@ export default function AppointmentDetail() {
} }
return ( return (
<View className='detail-page'> <View className={`detail-page ${modeClass}`}>
<View className='detail-header'> <View className='detail-header'>
<View className='back-btn' onClick={goBack}><Text className='back-text'></Text></View> <View className='back-btn' onClick={goBack}><Text className='back-text'></Text></View>
<Text className='header-title'></Text> <Text className='header-title'></Text>

View File

@@ -17,13 +17,13 @@
.page-title { .page-title {
@include section-title; @include section-title;
margin-bottom: 4px; margin-bottom: 4px;
font-size: 36px; font-size: var(--tk-font-num-lg);
} }
.page-subtitle { .page-subtitle {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: var(--tk-text-secondary);
letter-spacing: 1px; letter-spacing: 1px;
} }
@@ -67,7 +67,7 @@
.dept-initial-text { .dept-initial-text {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 32px; font-size: var(--tk-font-num);
font-weight: bold; font-weight: bold;
color: $pri; color: $pri;
} }
@@ -80,7 +80,7 @@
} }
.doctor-name { .doctor-name {
font-size: 30px; font-size: var(--tk-font-num);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
overflow: hidden; overflow: hidden;
@@ -93,7 +93,7 @@
} }
.dept-tag-text { .dept-tag-text {
font-size: 22px; font-size: var(--tk-font-body);
font-weight: 500; font-weight: 500;
color: $pri; color: $pri;
} }
@@ -124,7 +124,7 @@
} }
.status-tag-text { .status-tag-text {
font-size: 22px; font-size: var(--tk-font-body);
font-weight: 500; font-weight: 500;
} }
@@ -157,12 +157,12 @@
.info-icon-serif { .info-icon-serif {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 22px; font-size: var(--tk-font-body);
color: $tx2; color: $tx2;
} }
.info-text { .info-text {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
} }
@@ -186,7 +186,7 @@
} }
.fab-text { .fab-text {
font-size: 30px; font-size: var(--tk-font-num);
color: white; color: white;
font-weight: bold; font-weight: bold;
letter-spacing: 2px; letter-spacing: 2px;

View File

@@ -5,6 +5,7 @@ import { listAppointments } from '../../services/appointment';
import type { Appointment } from '../../services/appointment'; import type { Appointment } from '../../services/appointment';
import EmptyState from '../../components/EmptyState'; import EmptyState from '../../components/EmptyState';
import Loading from '../../components/Loading'; import Loading from '../../components/Loading';
import { useElderClass } from '../../hooks/useElderClass';
import './index.scss'; import './index.scss';
const STATUS_MAP: Record<string, { label: string; className: string }> = { const STATUS_MAP: Record<string, { label: string; className: string }> = {
@@ -30,6 +31,7 @@ export default function AppointmentList() {
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const loadingRef = useRef(false); const loadingRef = useRef(false);
const modeClass = useElderClass();
const fetchData = useCallback(async (pageNum: number, isRefresh = false) => { const fetchData = useCallback(async (pageNum: number, isRefresh = false) => {
if (loadingRef.current) return; if (loadingRef.current) return;
@@ -86,7 +88,7 @@ export default function AppointmentList() {
}; };
return ( return (
<View className='appointment-page'> <View className={`appointment-page ${modeClass}`}>
{/* 页面标题 */} {/* 页面标题 */}
<View className='page-header'> <View className='page-header'>
<Text className='page-title'></Text> <Text className='page-title'></Text>

View File

@@ -13,7 +13,7 @@
} }
.article-title { .article-title {
font-size: 38px; font-size: var(--tk-font-hero);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
display: block; display: block;
@@ -29,7 +29,7 @@
} }
.article-category { .article-category {
font-size: 22px; font-size: var(--tk-font-body);
color: $pri; color: $pri;
background: $pri-l; background: $pri-l;
padding: 4px 12px; padding: 4px 12px;
@@ -37,13 +37,13 @@
} }
.article-author { .article-author {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx2; color: $tx2;
} }
.article-date { .article-date {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: var(--tk-text-secondary);
} }
.article-summary { .article-summary {
@@ -53,7 +53,7 @@
} }
.summary-text { .summary-text {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
line-height: 1.6; line-height: 1.6;
} }
@@ -70,7 +70,7 @@
} }
p { p {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx; color: $tx;
line-height: 1.8; line-height: 1.8;
margin-bottom: 16px; margin-bottom: 16px;
@@ -93,6 +93,6 @@
.loading-text, .loading-text,
.empty-text { .empty-text {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx3; color: var(--tk-text-secondary);
} }

View File

@@ -3,9 +3,11 @@ import { View, Text, RichText } from '@tarojs/components';
import Taro, { useRouter, useShareAppMessage } from '@tarojs/taro'; import Taro, { useRouter, useShareAppMessage } from '@tarojs/taro';
import { getArticleDetail, Article } from '../../../services/article'; import { getArticleDetail, Article } from '../../../services/article';
import { trackEvent } from '@/services/analytics'; import { trackEvent } from '@/services/analytics';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
export default function ArticleDetail() { export default function ArticleDetail() {
const modeClass = useElderClass();
const router = useRouter(); const router = useRouter();
const id = router.params.id || ''; const id = router.params.id || '';
@@ -31,7 +33,7 @@ export default function ArticleDetail() {
if (loading) { if (loading) {
return ( return (
<View className='article-detail-page'> <View className={`article-detail-page ${modeClass}`}>
<View className='loading-state'> <View className='loading-state'>
<Text className='loading-text'>...</Text> <Text className='loading-text'>...</Text>
</View> </View>
@@ -41,7 +43,7 @@ export default function ArticleDetail() {
if (!article) { if (!article) {
return ( return (
<View className='article-detail-page'> <View className={`article-detail-page ${modeClass}`}>
<View className='empty-state'> <View className='empty-state'>
<Text className='empty-text'></Text> <Text className='empty-text'></Text>
</View> </View>
@@ -50,7 +52,7 @@ export default function ArticleDetail() {
} }
return ( return (
<View className='article-detail-page'> <View className={`article-detail-page ${modeClass}`}>
{/* 文章头部 */} {/* 文章头部 */}
<View className='article-header'> <View className='article-header'>
<Text className='article-title'>{article.title}</Text> <Text className='article-title'>{article.title}</Text>

View File

@@ -16,7 +16,7 @@
display: inline-block; display: inline-block;
padding: 12px 28px; padding: 12px 28px;
margin-right: 12px; margin-right: 12px;
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
background: $card; background: $card;
border-radius: 32px; border-radius: 32px;
@@ -52,7 +52,7 @@
} }
.article-card-title { .article-card-title {
font-size: 30px; font-size: var(--tk-font-num);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
line-height: 1.4; line-height: 1.4;
@@ -66,7 +66,7 @@
} }
.article-card-summary { .article-card-summary {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
line-height: 1.4; line-height: 1.4;
display: block; display: block;
@@ -83,7 +83,7 @@
} }
.article-card-tag { .article-card-tag {
font-size: 22px; font-size: var(--tk-font-body);
color: $pri; color: $pri;
background: $pri-l; background: $pri-l;
padding: 2px 12px; padding: 2px 12px;
@@ -91,7 +91,7 @@
} }
.article-card-date { .article-card-date {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: $tx3;
} }
@@ -116,8 +116,8 @@
} }
.empty-text { .empty-text {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx3; color: var(--tk-text-secondary);
} }
.loading-hint { .loading-hint {
@@ -126,6 +126,6 @@
} }
.loading-text { .loading-text {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: var(--tk-text-secondary);
} }

View File

@@ -4,9 +4,11 @@ import Taro, { useDidShow, usePullDownRefresh, useReachBottom } from '@tarojs/ta
import { listArticles, listCategories, Article, ArticleCategory } from '../../services/article'; import { listArticles, listCategories, Article, ArticleCategory } from '../../services/article';
import EmptyState from '../../components/EmptyState'; import EmptyState from '../../components/EmptyState';
import Loading from '../../components/Loading'; import Loading from '../../components/Loading';
import { useElderClass } from '../../hooks/useElderClass';
import './index.scss'; import './index.scss';
export default function ArticleList() { export default function ArticleList() {
const modeClass = useElderClass();
const [articles, setArticles] = useState<Article[]>([]); const [articles, setArticles] = useState<Article[]>([]);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
@@ -72,7 +74,7 @@ export default function ArticleList() {
}; };
return ( return (
<View className='article-page'> <View className={`article-page ${modeClass}`}>
{/* 分类筛选 */} {/* 分类筛选 */}
{categories.length > 0 && ( {categories.length > 0 && (
<ScrollView scrollX className='article-categories'> <ScrollView scrollX className='article-categories'>

View File

@@ -8,61 +8,108 @@
background: $bg; background: $bg;
} }
/* ─── 导航栏 ─── */
.chat-header { .chat-header {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
padding: 24px 32px; padding: 12px 16px;
background: $card; background: $card;
border-bottom: 1px solid $bd; border-bottom: 1px solid $bd-l;
flex-shrink: 0;
position: relative;
}
&__title { .chat-header__back {
font-family: 'Georgia', 'Times New Roman', serif; position: absolute;
font-size: 30px; left: 16px;
font-weight: 600; z-index: 1;
color: $tx;
&:active {
opacity: 0.7;
} }
}
&__status { .chat-header__back-text {
font-size: 24px; font-size: var(--tk-font-body-sm);
color: $pri;
}
.chat-header__center {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.chat-header__title {
font-size: var(--tk-font-body-sm);
font-weight: 600;
color: $tx;
}
.chat-header__status {
font-size: var(--tk-font-micro);
color: $acc;
margin-top: 2px;
&--closed {
color: $tx3; color: $tx3;
} }
} }
/* ─── 消息区 ─── */
.chat-messages { .chat-messages {
flex: 1; flex: 1;
padding: 24px; padding: 16px 16px 0;
overflow-y: auto; overflow-y: auto;
} }
.msg-row { .msg-row {
display: flex; display: flex;
margin-bottom: 20px; margin-bottom: 16px;
gap: 8px;
&--self { &--self {
justify-content: flex-end; justify-content: flex-end;
} }
} }
/* ─── 医生头像 ─── */
.msg-avatar {
width: 32px;
height: 32px;
border-radius: 16px;
background: $pri-l;
@include flex-center;
flex-shrink: 0;
}
.msg-avatar-char {
@include serif-number;
font-size: var(--tk-font-cap);
font-weight: 700;
color: $pri;
}
/* ─── 消息气泡 ─── */
.msg-bubble { .msg-bubble {
max-width: 70%; max-width: 70%;
padding: 20px 24px; padding: 12px 16px;
border-radius: $r-lg; box-shadow: $shadow-sm;
position: relative;
&--other { &--other {
background: $card; background: $card;
border-top-left-radius: $r-sm; border-radius: $r $r $r $r-xs;
} }
&--self { &--self {
background: $pri; background: $pri;
border-top-right-radius: $r-sm; border-radius: $r $r $r-xs $r;
} }
} }
.msg-text { .msg-text {
font-size: 28px; font-size: var(--tk-font-cap);
color: $tx; color: $tx;
display: block; display: block;
line-height: 1.6; line-height: 1.6;
@@ -76,88 +123,95 @@
.msg-date-divider { .msg-date-divider {
display: flex; display: flex;
justify-content: center; justify-content: center;
padding: 16px 0 12px; padding: 12px 0;
&__text { &__text {
font-size: 22px; font-size: var(--tk-font-micro);
color: #94A3B8; color: var(--tk-text-secondary);
background: #F1F5F9; background: $surface-alt;
padding: 4px 16px; padding: 2px 12px;
border-radius: 8px; border-radius: $r-pill;
} }
} }
.msg-image { .msg-image {
width: 320px; width: 200px;
border-radius: 12px; border-radius: $r-sm;
margin-top: 4px; margin-top: 4px;
} }
.msg-time { .msg-time {
font-size: 22px; font-size: var(--tk-font-micro);
color: $tx3; color: var(--tk-text-secondary);
display: block; display: block;
margin-top: 8px; margin-top: 4px;
text-align: right;
.msg-bubble--self & { .msg-bubble--self & {
color: rgba(255, 255, 255, 0.7); color: rgba(255, 255, 255, 0.7);
text-align: right;
} }
} }
.chat-empty { .chat-empty {
text-align: center; text-align: center;
padding: 120px 32px; padding: 80px 24px;
&__text { &__text {
font-size: 26px; font-size: var(--tk-font-cap);
color: $tx3; color: var(--tk-text-secondary);
} }
} }
/* ─── 输入栏 ─── */
.chat-input-bar { .chat-input-bar {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 16px 24px; gap: 10px;
padding: 10px 16px 38px;
background: $card; background: $card;
border-top: 1px solid $bd; border-top: 1px solid $bd-l;
padding-bottom: calc(16px + env(safe-area-inset-bottom)); flex-shrink: 0;
} }
.chat-input { .chat-input {
flex: 1; flex: 1;
height: 40px;
background: $bg; background: $bg;
border-radius: $r; border: 1.5px solid $bd;
padding: 16px 20px; border-radius: 20px;
font-size: 28px; padding: 0 14px;
margin-right: 16px; font-size: var(--tk-font-cap);
color: $tx;
} }
.chat-send-btn { .chat-send-btn {
width: 40px;
height: 40px;
border-radius: 20px;
background: $pri; background: $pri;
border-radius: $r; @include flex-center;
padding: 16px 28px;
flex-shrink: 0; flex-shrink: 0;
box-shadow: 0 2px 6px rgba(196, 98, 58, 0.3);
&--disabled { &--disabled {
opacity: 0.5; opacity: 0.5;
} }
}
&__text { .chat-send-btn__icon {
font-size: 28px; font-size: var(--tk-font-cap);
color: #fff; color: #fff;
font-weight: 500; font-weight: 600;
}
} }
.chat-closed-bar { .chat-closed-bar {
padding: 24px; padding: 16px;
text-align: center; text-align: center;
background: $card; background: $card;
border-top: 1px solid $bd; border-top: 1px solid $bd-l;
&__text { &__text {
font-size: 26px; font-size: var(--tk-font-cap);
color: $tx3; color: var(--tk-text-secondary);
} }
} }

View File

@@ -6,14 +6,14 @@ import {
listMessages, listMessages,
sendMessage, sendMessage,
markSessionRead, markSessionRead,
pollMessages,
type ConsultationSession, type ConsultationSession,
type ConsultationMessage, type ConsultationMessage,
} from '@/services/consultation'; } from '@/services/consultation';
import Loading from '@/components/Loading'; import Loading from '@/components/Loading';
import { useElderClass } from '@/hooks/useElderClass';
import './index.scss'; import './index.scss';
const POLL_INTERVAL = 8000;
export default function ConsultationDetail() { export default function ConsultationDetail() {
const router = useRouter(); const router = useRouter();
const sessionId = router.params.id || ''; const sessionId = router.params.id || '';
@@ -23,43 +23,35 @@ export default function ConsultationDetail() {
const [sending, setSending] = useState(false); const [sending, setSending] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const scrollViewRef = useRef(''); const scrollViewRef = useRef('');
const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null); const pollingRef = useRef(false);
const modeClass = useElderClass();
useEffect(() => { useEffect(() => {
if (sessionId) { if (sessionId) {
loadData(); loadData();
markRead(); markRead();
startPolling(); startLongPolling();
} }
return () => stopPolling(); return () => { pollingRef.current = false; };
}, [sessionId]); }, [sessionId]);
const startPolling = () => { useEffect(() => {
stopPolling(); if (session?.status === 'closed') {
pollTimerRef.current = setInterval(pollNewMessages, POLL_INTERVAL); pollingRef.current = false;
}
}, [session?.status]);
const startLongPolling = () => {
pollingRef.current = true;
longPoll();
}; };
const stopPolling = () => { const longPoll = async () => {
if (pollTimerRef.current) { if (!pollingRef.current) return;
clearInterval(pollTimerRef.current);
pollTimerRef.current = null;
}
};
const pollNewMessages = async () => {
if (!session || session.status === 'closed') {
stopPolling();
return;
}
try { try {
const lastId = messages.length > 0 ? messages[messages.length - 1].id : undefined; const lastId = messages.length > 0 ? messages[messages.length - 1].id : undefined;
const m = await listMessages(sessionId, { const newMsgs = await pollMessages(sessionId, lastId);
page: 1, if (newMsgs && newMsgs.length > 0) {
page_size: 50,
after_id: lastId,
});
const newMsgs = m.data || [];
if (newMsgs.length > 0) {
setMessages((prev) => { setMessages((prev) => {
const existing = new Set(prev.map((msg) => msg.id)); const existing = new Set(prev.map((msg) => msg.id));
const fresh = newMsgs.filter((msg) => !existing.has(msg.id)); const fresh = newMsgs.filter((msg) => !existing.has(msg.id));
@@ -67,7 +59,12 @@ export default function ConsultationDetail() {
}); });
scrollViewRef.current = `msg-${messages.length + newMsgs.length}`; scrollViewRef.current = `msg-${messages.length + newMsgs.length}`;
} }
} catch { /* 轮询失败静默忽略 */ } } catch {
// 超时或网络错误,静默重试
}
if (pollingRef.current) {
longPoll();
}
}; };
const loadData = async () => { const loadData = async () => {
@@ -80,7 +77,7 @@ export default function ConsultationDetail() {
setSession(s); setSession(s);
setMessages(m.data || []); setMessages(m.data || []);
scrollViewRef.current = `msg-${(m.data || []).length}`; scrollViewRef.current = `msg-${(m.data || []).length}`;
if (s.status === 'closed') stopPolling(); if (s.status === 'closed') pollingRef.current = false;
} catch { } catch {
Taro.showToast({ title: '加载失败', icon: 'none' }); Taro.showToast({ title: '加载失败', icon: 'none' });
} finally { } finally {
@@ -137,14 +134,24 @@ export default function ConsultationDetail() {
if (loading) return <Loading />; if (loading) return <Loading />;
const isOpen = session?.status !== 'closed'; const isOpen = session?.status !== 'closed';
const doctorInitial = (session?.subject || '医').charAt(0);
const statusLabel = session?.status === 'active' ? '进行中'
: session?.status === 'pending' ? '等待接诊'
: '已结束';
return ( return (
<View className='chat-page'> <View className={`chat-page ${modeClass}`}>
{/* 导航栏 — 对齐设计稿:返回 + 标题 + 副标题 */}
<View className='chat-header'> <View className='chat-header'>
<Text className='chat-header__title'>{session?.subject || '在线咨询'}</Text> <View className='chat-header__back' onClick={() => Taro.navigateBack()}>
{!isOpen && ( <Text className='chat-header__back-text'> </Text>
<Text className='chat-header__status'></Text> </View>
)} <View className='chat-header__center'>
<Text className='chat-header__title'>{session?.subject || '在线咨询'}</Text>
<Text className={`chat-header__status ${isOpen ? '' : 'chat-header__status--closed'}`}>
{statusLabel}
</Text>
</View>
</View> </View>
<ScrollView <ScrollView
@@ -164,6 +171,11 @@ export default function ConsultationDetail() {
</View> </View>
)} )}
<View id={`msg-${idx + 1}`} className={`msg-row ${isSelf ? 'msg-row--self' : ''}`}> <View id={`msg-${idx + 1}`} className={`msg-row ${isSelf ? 'msg-row--self' : ''}`}>
{!isSelf && (
<View className='msg-avatar'>
<Text className='msg-avatar-char'>{doctorInitial}</Text>
</View>
)}
<View className={`msg-bubble ${isSelf ? 'msg-bubble--self' : 'msg-bubble--other'}`}> <View className={`msg-bubble ${isSelf ? 'msg-bubble--self' : 'msg-bubble--other'}`}>
{isImageUrl(msg.content) ? ( {isImageUrl(msg.content) ? (
<Image <Image
@@ -203,7 +215,7 @@ export default function ConsultationDetail() {
className={`chat-send-btn ${(!inputText.trim() || sending) ? 'chat-send-btn--disabled' : ''}`} className={`chat-send-btn ${(!inputText.trim() || sending) ? 'chat-send-btn--disabled' : ''}`}
onClick={handleSend} onClick={handleSend}
> >
<Text className='chat-send-btn__text'>{sending ? '...' : '发送'}</Text> <Text className='chat-send-btn__icon'></Text>
</View> </View>
</View> </View>
) : ( ) : (

View File

@@ -6,26 +6,36 @@
background: $bg; background: $bg;
} }
/* ─── 页头 ─── */ .consultation-body {
.consultation-header { padding: 12px 24px 24px;
background: linear-gradient(135deg, $pri 0%, $pri-d 100%);
padding: 48px 32px 36px;
color: #fff;
}
.consultation-title {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 40px;
font-weight: bold;
color: #fff;
display: block;
margin-bottom: 8px;
} }
/* ─── 副标题 ─── */
.consultation-subtitle { .consultation-subtitle {
font-size: 24px; font-size: var(--tk-font-cap);
color: rgba(255, 255, 255, 0.75); color: var(--tk-text-secondary);
display: block; display: block;
margin-bottom: 20px;
}
/* ─── 发起咨询按钮 — 实心主色 ─── */
.consultation-create-btn {
height: 48px;
border-radius: $r;
background: $pri;
@include flex-center;
box-shadow: 0 2px 8px rgba(196, 98, 58, 0.25);
margin-bottom: 20px;
&:active {
opacity: 0.85;
}
}
.consultation-create-btn-text {
font-size: var(--tk-font-body-sm);
font-weight: 600;
color: #fff;
} }
/* ─── 居中容器 ─── */ /* ─── 居中容器 ─── */
@@ -37,7 +47,7 @@
} }
.consultation-error { .consultation-error {
font-size: 26px; font-size: var(--tk-font-cap);
color: $dan; color: $dan;
} }
@@ -47,51 +57,52 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 160px 40px; padding: 120px 40px;
} }
.empty-icon { .empty-icon {
width: 120px; width: 80px;
height: 120px; height: 80px;
border-radius: 50%; border-radius: 50%;
background: $pri-l; background: $pri-l;
@include flex-center; @include flex-center;
margin-bottom: 32px; margin-bottom: 20px;
} }
.empty-char { .empty-char {
font-family: 'Georgia', 'Times New Roman', serif; @include serif-number;
font-size: 52px; font-size: var(--tk-font-num);
font-weight: bold; font-weight: 700;
color: $pri; color: $pri;
line-height: 1;
} }
.empty-title { .empty-title {
font-size: 32px; font-size: var(--tk-font-body-sm);
font-weight: 600; font-weight: 600;
color: $tx; color: $tx;
margin-bottom: 12px; margin-bottom: 8px;
} }
.empty-hint { .empty-hint {
font-size: 26px; font-size: var(--tk-font-cap);
color: $tx3; color: var(--tk-text-secondary);
text-align: center; text-align: center;
} }
/* ─── 会话列表 ─── */ /* ─── 会话列表 ─── */
.session-list { .session-list {
padding: 20px 24px; display: flex;
flex-direction: column;
gap: 8px;
} }
.session-card { .session-card {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px;
background: $card; background: $card;
border-radius: $r; border-radius: $r;
padding: 24px; padding: 16px;
margin-bottom: 12px;
box-shadow: $shadow-sm; box-shadow: $shadow-sm;
&:active { &:active {
@@ -99,7 +110,27 @@
} }
} }
.session-main { .session-card-closed {
opacity: 0.6;
}
.session-avatar {
width: 36px;
height: 36px;
border-radius: 18px;
background: $pri-l;
@include flex-center;
flex-shrink: 0;
}
.session-avatar-char {
@include serif-number;
font-size: var(--tk-font-body-sm);
font-weight: 700;
color: $pri;
}
.session-body {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
@@ -108,56 +139,73 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 8px; margin-bottom: 6px;
} }
.session-subject { .session-subject {
font-size: 28px; font-size: var(--tk-font-cap);
color: $tx;
font-weight: 600; font-weight: 600;
color: $tx;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
flex: 1; flex: 1;
margin-right: 12px; margin-right: 8px;
}
.session-tag {
&.tag-ok { @include tag($acc-l, $acc); }
&.tag-warn { @include tag($wrn-l, $wrn); }
&.tag-default { @include tag($bd-l, $tx2); }
}
.session-message {
font-size: 26px;
color: $tx2;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 8px;
} }
.session-time { .session-time {
font-size: 22px; font-size: var(--tk-font-micro);
color: $tx3; color: var(--tk-text-secondary);
display: block; flex-shrink: 0;
}
.session-tag {
font-size: var(--tk-font-micro);
padding: 2px 6px;
border-radius: 4px;
font-weight: 500;
display: inline-block;
&.tag-ok { background: $acc-l; color: $acc; }
&.tag-warn { background: $wrn-l; color: $wrn; }
&.tag-default { background: $surface-alt; color: $tx3; }
}
.session-meta {
display: flex;
align-items: center;
margin-bottom: 4px;
}
.session-message-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.session-message {
font-size: var(--tk-font-cap);
color: $tx2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
margin-right: 8px;
} }
/* ─── 未读角标 ─── */ /* ─── 未读角标 ─── */
.session-badge { .session-badge {
background: $dan; background: $dan;
border-radius: $r-pill; border-radius: $r-pill;
min-width: 36px; min-width: 18px;
height: 36px; height: 18px;
@include flex-center; @include flex-center;
padding: 0 10px; padding: 0 5px;
margin-left: 12px;
flex-shrink: 0; flex-shrink: 0;
} }
.session-badge-text { .session-badge-text {
font-size: 22px; font-size: var(--tk-font-micro);
color: #fff; color: #fff;
font-weight: 600; font-weight: 600;
} }

View File

@@ -3,6 +3,7 @@ import { View, Text } from '@tarojs/components';
import Taro, { useDidShow, usePullDownRefresh, useReachBottom } 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 { useElderClass } from '../../hooks/useElderClass';
import './index.scss'; import './index.scss';
function getStatusTag(status: string) { function getStatusTag(status: string) {
@@ -33,6 +34,7 @@ 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 modeClass = useElderClass();
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const loadingRef = useRef(false); const loadingRef = useRef(false);
@@ -85,68 +87,84 @@ export default function Consultation() {
}; };
return ( return (
<View className='consultation-page'> <View className={`consultation-page ${modeClass}`}>
{/* 页头 */} <View className='consultation-body'>
<View className='consultation-header'> {/* 副标题 */}
<Text className='consultation-title'>线</Text>
<Text className='consultation-subtitle'></Text> <Text className='consultation-subtitle'></Text>
</View>
{/* 内容区 */} {/* 发起咨询按钮 — 实心主色 */}
{loading ? ( <View
<View className='consultation-center'> className='consultation-create-btn'
<Loading text='加载中...' /> onClick={() => Taro.navigateTo({ url: '/pages/consultation/create/index' })}
>
<Text className='consultation-create-btn-text'></Text>
</View> </View>
) : error ? (
<View className='consultation-center'> {/* 内容区 */}
<Text className='consultation-error'>{error}</Text> {loading ? (
</View> <View className='consultation-center'>
) : sessions.length === 0 ? ( <Loading text='加载中...' />
<View className='consultation-empty'>
<View className='empty-icon'>
<Text className='empty-char'></Text>
</View> </View>
<Text className='empty-title'></Text> ) : error ? (
<Text className='empty-hint'></Text> <View className='consultation-center'>
</View> <Text className='consultation-error'>{error}</Text>
) : ( </View>
<View className='session-list'> ) : sessions.length === 0 ? (
{sessions.map((session) => { <View className='consultation-empty'>
const tag = getStatusTag(session.status); <View className='empty-icon'>
return ( <Text className='empty-char'></Text>
<View </View>
key={session.id} <Text className='empty-title'></Text>
className='session-card' <Text className='empty-hint'></Text>
onClick={() => handleTapSession(session)} </View>
> ) : (
<View className='session-main'> <View className='session-list'>
<View className='session-top'> {sessions.map((session) => {
<Text className='session-subject'> const tag = getStatusTag(session.status);
{session.subject || '在线咨询'} const initial = (session.subject || '咨').charAt(0);
</Text> const isClosed = session.status === 'closed' || session.status === 'cancelled';
<Text className={`session-tag ${tag.cls}`}>{tag.label}</Text> return (
<View
key={session.id}
className={`session-card ${isClosed ? 'session-card-closed' : ''}`}
onClick={() => handleTapSession(session)}
>
<View className='session-avatar'>
<Text className='session-avatar-char'>{initial}</Text>
</View>
<View className='session-body'>
<View className='session-top'>
<Text className='session-subject'>
{session.subject || '在线咨询'}
</Text>
<Text className='session-time'>
{session.last_message_at
? formatTime(session.last_message_at)
: formatTime(session.created_at)}
</Text>
</View>
<View className='session-meta'>
<Text className={`session-tag ${tag.cls}`}>{tag.label}</Text>
</View>
<View className='session-message-row'>
<Text className='session-message'>
{session.last_message || '暂无消息'}
</Text>
{session.unread_count_patient > 0 && (
<View className='session-badge'>
<Text className='session-badge-text'>
{session.unread_count_patient > 99 ? '99+' : session.unread_count_patient}
</Text>
</View>
)}
</View>
</View> </View>
<Text className='session-message'>
{session.last_message || '暂无消息'}
</Text>
<Text className='session-time'>
{session.last_message_at
? formatTime(session.last_message_at)
: formatTime(session.created_at)}
</Text>
</View> </View>
{session.unread_count_patient > 0 && ( );
<View className='session-badge'> })}
<Text className='session-badge-text'> </View>
{session.unread_count_patient > 99 ? '99+' : session.unread_count_patient} )}
</Text> </View>
</View>
)}
</View>
);
})}
</View>
)}
</View> </View>
); );
} }

View File

@@ -1,34 +1,5 @@
@import '../../styles/variables.scss'; @import '../../styles/variables.scss';
@import '../../styles/mixins.scss';
@mixin serif-number {
font-family: 'Georgia', 'Times New Roman', serif;
font-variant-numeric: tabular-nums;
}
@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;
}
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.device-sync-page { .device-sync-page {
min-height: 100vh; min-height: 100vh;
@@ -43,9 +14,8 @@
} }
.sync-header-title { .sync-header-title {
font-family: 'Georgia', 'Times New Roman', serif; @include section-title;
font-size: 40px; color: $card;
font-weight: bold;
} }
.sync-section { .sync-section {
@@ -72,20 +42,17 @@
margin-bottom: 20px; margin-bottom: 20px;
color: $pri; color: $pri;
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 36px; font-size: var(--tk-font-num-lg);
font-weight: bold; font-weight: bold;
} }
.sync-hero-title { .sync-hero-title {
font-family: 'Georgia', 'Times New Roman', serif; @include section-title;
font-size: 34px;
font-weight: bold;
color: $tx;
margin-bottom: 8px; margin-bottom: 8px;
} }
.sync-hero-desc { .sync-hero-desc {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
} }
@@ -110,7 +77,7 @@
.sync-action-text { .sync-action-text {
color: $card; color: $card;
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: 500; font-weight: 500;
} }
@@ -120,7 +87,7 @@
.sync-section-title { .sync-section-title {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
margin-bottom: 12px; margin-bottom: 12px;
@@ -144,19 +111,19 @@
} }
.sync-device-name { .sync-device-name {
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: 500; font-weight: 500;
color: $tx; color: $tx;
} }
.sync-device-adapter { .sync-device-adapter {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: var(--tk-text-secondary);
margin-top: 4px; margin-top: 4px;
} }
.sync-device-rssi { .sync-device-rssi {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx2; color: $tx2;
} }
@@ -183,7 +150,7 @@
} }
.sync-status-text { .sync-status-text {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx; color: $tx;
} }
@@ -208,12 +175,12 @@
} }
.sync-reading-type { .sync-reading-type {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
} }
.sync-reading-value { .sync-reading-value {
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: bold; font-weight: bold;
color: $pri; color: $pri;
@include serif-number; @include serif-number;
@@ -222,8 +189,8 @@
.sync-readings-count { .sync-readings-count {
display: block; display: block;
margin-top: 12px; margin-top: 12px;
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: var(--tk-text-secondary);
text-align: center; text-align: center;
} }
@@ -240,7 +207,7 @@
} }
.sync-error-text { .sync-error-text {
font-size: 26px; font-size: var(--tk-font-h1);
color: $dan; color: $dan;
} }
@@ -250,7 +217,7 @@
} }
.sync-loading-text { .sync-loading-text {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx2; color: $tx2;
} }
@@ -273,20 +240,17 @@
@include flex-center; @include flex-center;
color: $acc; color: $acc;
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 36px; font-size: var(--tk-font-num-lg);
font-weight: bold; font-weight: bold;
margin-bottom: 16px; margin-bottom: 16px;
} }
.sync-result-title { .sync-result-title {
font-family: 'Georgia', 'Times New Roman', serif; @include section-title;
font-size: 34px;
font-weight: bold;
color: $tx;
margin-bottom: 8px; margin-bottom: 8px;
} }
.sync-result-count { .sync-result-count {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
} }

View File

@@ -10,6 +10,7 @@ import { DataSyncScheduler } from '@/services/ble/DataSyncScheduler';
import { uploadReadings } from '@/services/device-sync'; import { uploadReadings } from '@/services/device-sync';
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';
import type { BLEDevice, NormalizedReading } from '@/services/ble/types'; import type { BLEDevice, NormalizedReading } from '@/services/ble/types';
import { useElderClass } from '../../hooks/useElderClass';
import './index.scss'; import './index.scss';
const bleManager = new BLEManager({ scanTimeout: 10000, retryCount: 3 }); const bleManager = new BLEManager({ scanTimeout: 10000, retryCount: 3 });
@@ -21,6 +22,7 @@ bleManager.registerAdapter(CustomBandAdapter);
type PageState = 'idle' | 'scanning' | 'connecting' | 'connected' | 'syncing' | 'done' | 'error'; type PageState = 'idle' | 'scanning' | 'connecting' | 'connected' | 'syncing' | 'done' | 'error';
export default function DeviceSync() { export default function DeviceSync() {
const modeClass = useElderClass();
const { currentPatient } = useAuthStore(); const { currentPatient } = useAuthStore();
const router = useRouter(); const router = useRouter();
const returnTo = router.params.returnTo || ''; const returnTo = router.params.returnTo || '';
@@ -271,7 +273,7 @@ export default function DeviceSync() {
); );
return ( return (
<View className="device-sync-page"> <View className={`device-sync-page ${modeClass}`}>
<View className="sync-header"> <View className="sync-header">
<Text className="sync-header-title"></Text> <Text className="sync-header-title"></Text>
</View> </View>

View File

@@ -38,7 +38,7 @@
} }
.inbox-tab-text { .inbox-tab-text {
font-size: 14px; font-size: var(--tk-font-cap);
color: $tx2; color: $tx2;
} }
} }
@@ -64,20 +64,20 @@
.inbox-type-tag { .inbox-type-tag {
color: $card; color: $card;
font-size: 10px; font-size: var(--tk-font-micro);
padding: 2px 6px; padding: 2px 6px;
border-radius: 4px; border-radius: 4px;
flex-shrink: 0; flex-shrink: 0;
} }
.inbox-card-title { .inbox-card-title {
font-size: 14px; font-size: var(--tk-font-cap);
font-weight: 500; font-weight: 500;
color: $tx; color: $tx;
} }
.inbox-card-desc { .inbox-card-desc {
font-size: 12px; font-size: var(--tk-font-micro);
color: $tx3; color: $tx3;
} }
} }
@@ -87,7 +87,7 @@
padding: 80px 0; padding: 80px 0;
.inbox-empty-text { .inbox-empty-text {
font-size: 14px; font-size: var(--tk-font-cap);
color: $tx3; color: $tx3;
} }
} }
@@ -112,13 +112,13 @@
border-bottom: 1px solid $bd-l; border-bottom: 1px solid $bd-l;
.dialog-title { .dialog-title {
font-size: 16px; font-size: var(--tk-font-body-sm);
font-weight: 600; font-weight: 600;
color: $tx; color: $tx;
} }
.dialog-close { .dialog-close {
font-size: 13px; font-size: var(--tk-font-cap);
color: $tx3; color: $tx3;
} }
} }
@@ -128,7 +128,7 @@
} }
.dialog-patient { .dialog-patient {
font-size: 13px; font-size: var(--tk-font-cap);
color: $tx2; color: $tx2;
display: block; display: block;
margin-bottom: 12px; margin-bottom: 12px;
@@ -156,13 +156,13 @@
.thread-content { .thread-content {
.thread-label { .thread-label {
font-size: 13px; font-size: var(--tk-font-cap);
color: $tx; color: $tx;
display: block; display: block;
} }
.thread-time { .thread-time {
font-size: 11px; font-size: var(--tk-font-micro);
color: $tx3; color: $tx3;
} }
} }
@@ -178,7 +178,7 @@
text-align: center; text-align: center;
padding: 10px; padding: 10px;
border-radius: $r-sm; border-radius: $r-sm;
font-size: 14px; font-size: var(--tk-font-cap);
font-weight: 500; font-weight: 500;
&.primary { background: $pri; color: $card; } &.primary { background: $pri; color: $card; }

View File

@@ -8,6 +8,7 @@ import {
type ThreadResponse, type ThreadResponse,
} from '@/services/action-inbox'; } from '@/services/action-inbox';
import Loading from '@/components/Loading'; import Loading from '@/components/Loading';
import { useElderClass } from '../../../hooks/useElderClass';
import { getStatusInlineStyle, getStatusLabel } from '@/utils/statusTag'; import { getStatusInlineStyle, getStatusLabel } from '@/utils/statusTag';
import './index.scss'; import './index.scss';
@@ -33,6 +34,7 @@ const STATUS_TABS = [
]; ];
export default function ActionInboxPage() { export default function ActionInboxPage() {
const modeClass = useElderClass();
const [items, setItems] = useState<ActionItem[]>([]); const [items, setItems] = useState<ActionItem[]>([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [_page, setPage] = useState(1); const [_page, setPage] = useState(1);
@@ -118,7 +120,7 @@ export default function ActionInboxPage() {
}; };
return ( return (
<View className="action-inbox-page"> <View className={`action-inbox-page ${modeClass}`}>
<View className="inbox-tabs"> <View className="inbox-tabs">
{STATUS_TABS.map((tab) => ( {STATUS_TABS.map((tab) => (
<View <View

View File

@@ -18,13 +18,13 @@
} }
&__time { &__time {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: $tx3;
} }
} }
.detail-severity { .detail-severity {
font-size: 24px; font-size: var(--tk-font-h2);
font-weight: 600; font-weight: 600;
padding: 6px 16px; padding: 6px 16px;
border-radius: $r-sm; border-radius: $r-sm;
@@ -51,7 +51,7 @@
} }
.detail-status { .detail-status {
font-size: 24px; font-size: var(--tk-font-h2);
padding: 6px 16px; padding: 6px 16px;
border-radius: $r-sm; border-radius: $r-sm;
@@ -84,24 +84,24 @@
box-shadow: $shadow-sm; box-shadow: $shadow-sm;
&__label { &__label {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx2; color: $tx2;
margin-bottom: 8px; margin-bottom: 8px;
} }
&__value { &__value {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx; color: $tx;
word-break: break-all; word-break: break-all;
&--id { &--id {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: $tx3;
font-family: monospace; font-family: monospace;
} }
&--detail { &--detail {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx2; color: $tx2;
font-family: monospace; font-family: monospace;
line-height: 1.6; line-height: 1.6;
@@ -125,7 +125,7 @@
flex: 1; flex: 1;
height: 88px; height: 88px;
line-height: 88px; line-height: 88px;
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: 600; font-weight: 600;
border-radius: $r-lg; border-radius: $r-lg;
text-align: center; text-align: center;

View File

@@ -3,6 +3,7 @@ import { View, Text, ScrollView, Button } from '@tarojs/components';
import Taro from '@tarojs/taro'; import Taro 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 { useElderClass } from '../../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
const SEVERITY_MAP: Record<string, { label: string; className: string }> = { const SEVERITY_MAP: Record<string, { label: string; className: string }> = {
@@ -20,6 +21,7 @@ const STATUS_MAP: Record<string, { label: string; className: string }> = {
}; };
export default function AlertDetail() { export default function AlertDetail() {
const modeClass = useElderClass();
const [alert, setAlert] = useState<doctorApi.Alert | null>(null); const [alert, setAlert] = useState<doctorApi.Alert | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [actionLoading, setActionLoading] = useState(false); const [actionLoading, setActionLoading] = useState(false);
@@ -93,7 +95,7 @@ export default function AlertDetail() {
if (loading) return <Loading />; if (loading) return <Loading />;
if (!alert) { if (!alert) {
return ( return (
<View className='alert-detail-page'> <View className={`alert-detail-page ${modeClass}`}>
<Text></Text> <Text></Text>
</View> </View>
); );
@@ -105,7 +107,7 @@ export default function AlertDetail() {
const isAcknowledged = alert.status === 'acknowledged'; const isAcknowledged = alert.status === 'acknowledged';
return ( return (
<ScrollView scrollY className='alert-detail-page'> <ScrollView scrollY className={`alert-detail-page ${modeClass}`}>
{/* 顶部状态 */} {/* 顶部状态 */}
<View className='alert-detail-header'> <View className='alert-detail-header'>
<View className='alert-detail-header__tags'> <View className='alert-detail-header__tags'>

View File

@@ -16,13 +16,13 @@
} }
.alert-list-title { .alert-list-title {
font-size: 36px; font-size: var(--tk-font-num-lg);
font-weight: 600; font-weight: 600;
color: $tx; color: $tx;
} }
.alert-list-count { .alert-list-count {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx2; color: $tx2;
} }
@@ -36,7 +36,7 @@
padding: 10px 24px; padding: 10px 24px;
border-radius: $r-pill; border-radius: $r-pill;
background: $bd-l; background: $bd-l;
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx2; color: $tx2;
transition: all 0.2s; transition: all 0.2s;
@@ -79,7 +79,7 @@
} }
&__title { &__title {
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: 500; font-weight: 500;
color: $tx; color: $tx;
margin-bottom: 8px; margin-bottom: 8px;
@@ -92,13 +92,13 @@
} }
&__time { &__time {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: $tx3;
} }
} }
.alert-severity { .alert-severity {
font-size: 22px; font-size: var(--tk-font-body);
font-weight: 600; font-weight: 600;
padding: 4px 12px; padding: 4px 12px;
border-radius: $r-sm; border-radius: $r-sm;
@@ -125,7 +125,7 @@
} }
.alert-status { .alert-status {
font-size: 22px; font-size: var(--tk-font-body);
padding: 4px 12px; padding: 4px 12px;
border-radius: $r-sm; border-radius: $r-sm;
@@ -158,7 +158,7 @@
margin-top: 32px; margin-top: 32px;
&__btn { &__btn {
font-size: 26px; font-size: var(--tk-font-h1);
color: $pri; color: $pri;
padding: 12px 24px; padding: 12px 24px;
@@ -168,7 +168,7 @@
} }
&__info { &__info {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx2; color: $tx2;
} }
} }

View File

@@ -4,6 +4,7 @@ import Taro 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';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
const SEVERITY_MAP: Record<string, { label: string; className: string }> = { const SEVERITY_MAP: Record<string, { label: string; className: string }> = {
@@ -28,6 +29,7 @@ const STATUS_TABS = [
]; ];
export default function AlertList() { export default function AlertList() {
const modeClass = useElderClass();
const [alerts, setAlerts] = useState<doctorApi.Alert[]>([]); const [alerts, setAlerts] = useState<doctorApi.Alert[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState(''); const [activeTab, setActiveTab] = useState('');
@@ -81,7 +83,7 @@ export default function AlertList() {
if (loading && alerts.length === 0) return <Loading />; if (loading && alerts.length === 0) return <Loading />;
return ( return (
<ScrollView scrollY className='alert-list-page'> <ScrollView scrollY className={`alert-list-page ${modeClass}`}>
<View className='alert-list-header'> <View className='alert-list-header'>
<Text className='alert-list-title'></Text> <Text className='alert-list-title'></Text>
<Text className='alert-list-count'> {total} </Text> <Text className='alert-list-count'> {total} </Text>

View File

@@ -18,13 +18,13 @@
&__title { &__title {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 30px; font-size: var(--tk-font-num);
font-weight: 600; font-weight: 600;
color: $tx; color: $tx;
} }
&__close { &__close {
font-size: 26px; font-size: var(--tk-font-h1);
color: $dan; color: $dan;
padding: 8px 16px; padding: 8px 16px;
} }
@@ -63,7 +63,7 @@
} }
.msg-text { .msg-text {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx; color: $tx;
display: block; display: block;
line-height: 1.6; line-height: 1.6;
@@ -76,7 +76,7 @@
.msg-time { .msg-time {
@include serif-number; @include serif-number;
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: $tx3;
display: block; display: block;
margin-top: 8px; margin-top: 8px;
@@ -92,7 +92,7 @@
padding: 120px 32px; padding: 120px 32px;
&__text { &__text {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx3; color: $tx3;
} }
} }
@@ -111,7 +111,7 @@
background: $bd-l; background: $bd-l;
border-radius: $r; border-radius: $r;
padding: 16px 20px; padding: 16px 20px;
font-size: 28px; font-size: var(--tk-font-body-lg);
margin-right: 16px; margin-right: 16px;
} }
@@ -126,7 +126,7 @@
} }
&__text { &__text {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $card; color: $card;
font-weight: 500; font-weight: 500;
} }
@@ -139,7 +139,7 @@
border-top: 1px solid $bd; border-top: 1px solid $bd;
&__text { &__text {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx3; color: $tx3;
} }
} }

View File

@@ -3,56 +3,47 @@ import { View, Text, Input, ScrollView } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro'; import Taro, { useRouter } 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 { useElderClass } from '../../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
const POLL_INTERVAL = 8000;
export default function ConsultationDetail() { export default function ConsultationDetail() {
const router = useRouter(); const router = useRouter();
const sessionId = router.params.id || ''; const sessionId = router.params.id || '';
const modeClass = useElderClass();
const [session, setSession] = useState<doctorApi.ConsultationSession | null>(null); const [session, setSession] = useState<doctorApi.ConsultationSession | null>(null);
const [messages, setMessages] = useState<doctorApi.ConsultationMessage[]>([]); const [messages, setMessages] = useState<doctorApi.ConsultationMessage[]>([]);
const [inputText, setInputText] = useState(''); const [inputText, setInputText] = useState('');
const [sending, setSending] = useState(false); const [sending, setSending] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const scrollViewRef = useRef(''); const scrollViewRef = useRef('');
const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null); const pollingRef = useRef(false);
useEffect(() => { useEffect(() => {
if (sessionId) { if (sessionId) {
loadData(); loadData();
markRead(); markRead();
startPolling(); startLongPolling();
} }
return () => stopPolling(); return () => { pollingRef.current = false; };
}, [sessionId]); }, [sessionId]);
const startPolling = () => { useEffect(() => {
stopPolling(); if (session?.status === 'closed') {
pollTimerRef.current = setInterval(pollNewMessages, POLL_INTERVAL); pollingRef.current = false;
}
}, [session?.status]);
const startLongPolling = () => {
pollingRef.current = true;
longPoll();
}; };
const stopPolling = () => { const longPoll = async () => {
if (pollTimerRef.current) { if (!pollingRef.current) return;
clearInterval(pollTimerRef.current);
pollTimerRef.current = null;
}
};
const pollNewMessages = async () => {
if (!session || session.status === 'closed') {
stopPolling();
return;
}
try { try {
const lastId = messages.length > 0 ? messages[messages.length - 1].id : undefined; const lastId = messages.length > 0 ? messages[messages.length - 1].id : undefined;
const m = await doctorApi.listMessages(sessionId, { const newMsgs = await doctorApi.pollMessages(sessionId, lastId);
page: 1, if (newMsgs && newMsgs.length > 0) {
page_size: 50,
after_id: lastId,
});
const newMsgs = m.data || [];
if (newMsgs.length > 0) {
setMessages((prev) => { setMessages((prev) => {
const existing = new Set(prev.map((msg) => msg.id)); const existing = new Set(prev.map((msg) => msg.id));
const fresh = newMsgs.filter((msg) => !existing.has(msg.id)); const fresh = newMsgs.filter((msg) => !existing.has(msg.id));
@@ -60,7 +51,12 @@ export default function ConsultationDetail() {
}); });
scrollViewRef.current = `msg-${messages.length + newMsgs.length}`; scrollViewRef.current = `msg-${messages.length + newMsgs.length}`;
} }
} catch { /* 轮询失败静默忽略 */ } } catch {
// 超时或网络错误,静默重试
}
if (pollingRef.current) {
longPoll();
}
}; };
const loadData = async () => { const loadData = async () => {
@@ -73,7 +69,7 @@ export default function ConsultationDetail() {
setSession(s); setSession(s);
setMessages(m.data || []); setMessages(m.data || []);
scrollViewRef.current = `msg-${(m.data || []).length}`; scrollViewRef.current = `msg-${(m.data || []).length}`;
if (s.status === 'closed') stopPolling(); if (s.status === 'closed') pollingRef.current = false;
} catch { } catch {
Taro.showToast({ title: '加载失败', icon: 'none' }); Taro.showToast({ title: '加载失败', icon: 'none' });
} finally { } finally {
@@ -132,7 +128,7 @@ export default function ConsultationDetail() {
const isOpen = session?.status !== 'closed'; const isOpen = session?.status !== 'closed';
return ( return (
<View className='chat-page'> <View className={`chat-page ${modeClass}`}>
{/* Header */} {/* Header */}
<View className='chat-header'> <View className='chat-header'>
<Text className='chat-header__title'>{session?.subject || '在线咨询'}</Text> <Text className='chat-header__title'>{session?.subject || '在线咨询'}</Text>

View File

@@ -17,7 +17,7 @@
flex: 1; flex: 1;
text-align: center; text-align: center;
padding: 24px 0; padding: 24px 0;
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx2; color: $tx2;
position: relative; position: relative;
@@ -64,7 +64,7 @@
} }
&__subject { &__subject {
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: 600; font-weight: 600;
color: $tx; color: $tx;
flex: 1; flex: 1;
@@ -80,7 +80,7 @@
} }
&__status-text { &__status-text {
font-size: 22px; font-size: var(--tk-font-body);
font-weight: 500; font-weight: 500;
} }
@@ -96,12 +96,12 @@
} }
&__time { &__time {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: $tx3;
} }
&__preview { &__preview {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -123,7 +123,7 @@
&__badge-text { &__badge-text {
@include serif-number; @include serif-number;
font-size: 22px; font-size: var(--tk-font-body);
color: $card; color: $card;
font-weight: 600; font-weight: 600;
} }
@@ -137,7 +137,7 @@
padding: 24px; padding: 24px;
&__btn { &__btn {
font-size: 26px; font-size: var(--tk-font-h1);
color: $pri; color: $pri;
padding: 12px 24px; padding: 12px 24px;
@@ -147,7 +147,7 @@
} }
&__info { &__info {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx2; color: $tx2;
} }
} }

View File

@@ -4,6 +4,7 @@ import Taro 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';
import { useElderClass } from '../../../hooks/useElderClass';
import { getStatusInlineStyle, getStatusLabel } from '@/utils/statusTag'; import { getStatusInlineStyle, getStatusLabel } from '@/utils/statusTag';
import { formatDateTime } from '@/utils/date'; import { formatDateTime } from '@/utils/date';
import './index.scss'; import './index.scss';
@@ -16,6 +17,7 @@ const TABS = [
]; ];
export default function ConsultationList() { export default function ConsultationList() {
const modeClass = useElderClass();
const [sessions, setSessions] = useState<doctorApi.ConsultationSession[]>([]); const [sessions, setSessions] = useState<doctorApi.ConsultationSession[]>([]);
const [activeTab, setActiveTab] = useState(''); const [activeTab, setActiveTab] = useState('');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -58,7 +60,7 @@ export default function ConsultationList() {
if (loading && sessions.length === 0) return <Loading />; if (loading && sessions.length === 0) return <Loading />;
return ( return (
<ScrollView scrollY className='consultation-page'> <ScrollView scrollY className={`consultation-page ${modeClass}`}>
<View className='tabs'> <View className='tabs'>
{TABS.map((t) => ( {TABS.map((t) => (
<View <View

View File

@@ -1,4 +1,5 @@
@import '../../../../styles/variables.scss'; @import '../../../../styles/variables.scss';
@import '../../../../styles/mixins.scss';
.create-page { .create-page {
min-height: 100vh; min-height: 100vh;
@@ -16,7 +17,7 @@
} }
.section-title { .section-title {
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
display: block; display: block;
@@ -41,7 +42,7 @@
} }
.form-label { .form-label {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
flex-shrink: 0; flex-shrink: 0;
min-width: 140px; min-width: 140px;
@@ -50,12 +51,12 @@
.form-input { .form-input {
flex: 1; flex: 1;
text-align: right; text-align: right;
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx; color: $tx;
} }
.form-value { .form-value {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx; color: $tx;
&.placeholder { &.placeholder {
@@ -66,7 +67,7 @@
.form-textarea { .form-textarea {
width: 100%; width: 100%;
margin-top: 12px; margin-top: 12px;
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx; color: $tx;
min-height: 120px; min-height: 120px;
background: $bg; background: $bg;
@@ -91,7 +92,7 @@
} }
.submit-btn__text { .submit-btn__text {
font-size: 30px; font-size: var(--tk-font-num);
font-weight: bold; font-weight: bold;
color: #fff; color: #fff;
} }

View File

@@ -3,6 +3,7 @@ import { View, Text, Input, Textarea, Picker, ScrollView } from '@tarojs/compone
import Taro, { useRouter } from '@tarojs/taro'; import Taro, { useRouter } 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 { useElderClass } from '../../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
const DIALYSIS_TYPES = ['HD', 'HDF', 'HF']; const DIALYSIS_TYPES = ['HD', 'HDF', 'HF'];
@@ -55,6 +56,7 @@ export default function DialysisCreate() {
const version = router.params.version ? Number(router.params.version) : 0; const version = router.params.version ? Number(router.params.version) : 0;
const patientIdFromRoute = router.params.patientId || ''; const patientIdFromRoute = router.params.patientId || '';
const isEdit = !!id; const isEdit = !!id;
const modeClass = useElderClass();
const [form, setForm] = useState<FormState>({ ...initialForm, patient_id: patientIdFromRoute }); const [form, setForm] = useState<FormState>({ ...initialForm, patient_id: patientIdFromRoute });
const [loading, setLoading] = useState(isEdit); const [loading, setLoading] = useState(isEdit);
@@ -167,7 +169,7 @@ export default function DialysisCreate() {
); );
return ( return (
<ScrollView scrollY className='create-page'> <ScrollView scrollY className={`create-page ${modeClass}`}>
<View className='section'> <View className='section'>
<Text className='section-title'></Text> <Text className='section-title'></Text>
<View className='form-row'> <View className='form-row'>

View File

@@ -1,4 +1,5 @@
@import '../../../../styles/variables.scss'; @import '../../../../styles/variables.scss';
@import '../../../../styles/mixins.scss';
.dialysis-detail { .dialysis-detail {
min-height: 100vh; min-height: 100vh;
@@ -16,7 +17,7 @@
} }
.section-title { .section-title {
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
display: block; display: block;
@@ -31,7 +32,7 @@
} }
.record-header__title { .record-header__title {
font-size: 34px; font-size: var(--tk-font-num-lg);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
@@ -41,7 +42,7 @@
display: inline-block; display: inline-block;
padding: 4px 12px; padding: 4px 12px;
border-radius: 8px; border-radius: 8px;
font-size: 22px; font-size: var(--tk-font-body);
background: $bd-l; background: $bd-l;
color: $tx3; color: $tx3;
@@ -57,13 +58,13 @@
} }
.record-sub { .record-sub {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
display: block; display: block;
} }
.review-info { .review-info {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: $tx3;
display: block; display: block;
margin-top: 8px; margin-top: 8px;
@@ -82,12 +83,12 @@
} }
.detail-label { .detail-label {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
} }
.detail-value { .detail-value {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx; color: $tx;
text-align: right; text-align: right;
flex: 1; flex: 1;
@@ -98,7 +99,7 @@
.error-text { .error-text {
text-align: center; text-align: center;
padding: 120px 0; padding: 120px 0;
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx3; color: $tx3;
} }
@@ -150,6 +151,6 @@
} }
.action-btn__text { .action-btn__text {
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: 500; font-weight: 500;
} }

View File

@@ -3,11 +3,13 @@ import { View, Text, ScrollView } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro'; import Taro, { useRouter } 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 { useElderClass } from '../../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
export default function DialysisDetail() { export default function DialysisDetail() {
const router = useRouter(); const router = useRouter();
const id = router.params.id || ''; const id = router.params.id || '';
const modeClass = useElderClass();
const [record, setRecord] = useState<doctorApi.DialysisRecord | null>(null); const [record, setRecord] = useState<doctorApi.DialysisRecord | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
@@ -85,13 +87,13 @@ export default function DialysisDetail() {
}; };
if (loading) return <Loading />; if (loading) return <Loading />;
if (!record) return <View className='error-text'><Text></Text></View>; if (!record) return <View className={`error-text ${modeClass}`}><Text></Text></View>;
const canComplete = record.status === 'draft'; const canComplete = record.status === 'draft';
const canReview = record.status === 'completed'; const canReview = record.status === 'completed';
return ( return (
<ScrollView scrollY className='dialysis-detail'> <ScrollView scrollY className={`dialysis-detail ${modeClass}`}>
{/* 状态头部 */} {/* 状态头部 */}
<View className='section'> <View className='section'>
<View className='record-header'> <View className='record-header'>

View File

@@ -1,4 +1,5 @@
@import '../../../styles/variables.scss'; @import '../../../styles/variables.scss';
@import '../../../styles/mixins.scss';
.dialysis-page { .dialysis-page {
min-height: 100vh; min-height: 100vh;
@@ -15,7 +16,7 @@
background: $bg; background: $bg;
border-radius: $r-sm; border-radius: $r-sm;
padding: 16px 20px; padding: 16px 20px;
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx; color: $tx;
} }
@@ -52,7 +53,7 @@
} }
.tab-text { .tab-text {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
} }
@@ -61,7 +62,7 @@
} }
.record-count { .record-count {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: $tx3;
padding: 8px 0 16px; padding: 8px 0 16px;
} }
@@ -89,7 +90,7 @@
display: inline-block; display: inline-block;
padding: 4px 12px; padding: 4px 12px;
border-radius: 8px; border-radius: 8px;
font-size: 22px; font-size: var(--tk-font-body);
font-weight: 600; font-weight: 600;
background: $pri-l; background: $pri-l;
color: $pri-d; color: $pri-d;
@@ -109,7 +110,7 @@
display: inline-block; display: inline-block;
padding: 4px 12px; padding: 4px 12px;
border-radius: 8px; border-radius: 8px;
font-size: 22px; font-size: var(--tk-font-body);
background: $bd-l; background: $bd-l;
color: $tx3; color: $tx3;
@@ -131,13 +132,13 @@
} }
.record-card__date { .record-card__date {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx; color: $tx;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
.record-card__meta { .record-card__meta {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx2; color: $tx2;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
@@ -154,7 +155,7 @@
padding: 12px 24px; padding: 12px 24px;
background: $card; background: $card;
border-radius: $r-sm; border-radius: $r-sm;
font-size: 26px; font-size: var(--tk-font-h1);
color: $pri; color: $pri;
&--disabled { &--disabled {
@@ -163,7 +164,7 @@
} }
.page-info { .page-info {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx2; color: $tx2;
} }
@@ -187,7 +188,7 @@
} }
.fab-text { .fab-text {
font-size: 40px; font-size: var(--tk-font-hero);
color: #fff; color: #fff;
font-weight: bold; font-weight: bold;
} }

View File

@@ -4,6 +4,7 @@ import Taro, { useRouter } 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';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
const TABS = [ const TABS = [
@@ -18,6 +19,7 @@ const TYPE_MAP: Record<string, string> = { HD: 'HD', HDF: 'HDF', HF: 'HF' };
export default function DialysisList() { export default function DialysisList() {
const router = useRouter(); const router = useRouter();
const patientId = router.params.patientId || ''; const patientId = router.params.patientId || '';
const modeClass = useElderClass();
const [searchPatient, setSearchPatient] = useState(''); const [searchPatient, setSearchPatient] = useState('');
const [currentPatientId, setCurrentPatientId] = useState(patientId); const [currentPatientId, setCurrentPatientId] = useState(patientId);
const [activeTab, setActiveTab] = useState(''); const [activeTab, setActiveTab] = useState('');
@@ -72,7 +74,7 @@ export default function DialysisList() {
if (loading && records.length === 0) return <Loading />; if (loading && records.length === 0) return <Loading />;
return ( return (
<ScrollView scrollY className='dialysis-page'> <ScrollView scrollY className={`dialysis-page ${modeClass}`}>
{!patientId && ( {!patientId && (
<View className='search-bar'> <View className='search-bar'>
<Input <Input

View File

@@ -28,13 +28,13 @@
&__title { &__title {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 30px; font-size: var(--tk-font-num);
font-weight: 700; font-weight: 700;
color: $tx; color: $tx;
} }
&__status { &__status {
font-size: 24px; font-size: var(--tk-font-h2);
padding: 6px 16px; padding: 6px 16px;
border-radius: $r; border-radius: $r;
font-weight: 500; font-weight: 500;
@@ -60,12 +60,12 @@
} }
.info-label { .info-label {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: $tx3;
} }
.info-value { .info-value {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx; color: $tx;
font-weight: 500; font-weight: 500;
} }
@@ -77,14 +77,14 @@
border-radius: $r; border-radius: $r;
&__label { &__label {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx2; color: $tx2;
display: block; display: block;
margin-bottom: 8px; margin-bottom: 8px;
} }
&__text { &__text {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx; color: $tx;
line-height: 1.6; line-height: 1.6;
} }
@@ -99,14 +99,14 @@
} }
&__date { &__date {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: $tx3;
display: block; display: block;
margin-bottom: 8px; margin-bottom: 8px;
} }
&__text { &__text {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx; color: $tx;
display: block; display: block;
margin-bottom: 4px; margin-bottom: 4px;
@@ -121,7 +121,7 @@
border-radius: $r; border-radius: $r;
margin-bottom: 24px; margin-bottom: 24px;
color: $card; color: $card;
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: 500; font-weight: 500;
} }
@@ -130,7 +130,7 @@
} }
.form-label { .form-label {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
font-weight: 500; font-weight: 500;
display: block; display: block;
@@ -143,7 +143,7 @@
background: $bd-l; background: $bd-l;
border-radius: $r; border-radius: $r;
padding: 16px 20px; padding: 16px 20px;
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx; color: $tx;
box-sizing: border-box; box-sizing: border-box;
line-height: 1.6; line-height: 1.6;
@@ -154,7 +154,7 @@
padding: 16px 20px; padding: 16px 20px;
background: $bd-l; background: $bd-l;
border-radius: $r; border-radius: $r;
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx; color: $tx;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -171,7 +171,7 @@
} }
&__text { &__text {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $card; color: $card;
font-weight: 600; font-weight: 600;
} }
@@ -181,5 +181,5 @@
text-align: center; text-align: center;
padding: 80px 32px; padding: 80px 32px;
color: $tx3; color: $tx3;
font-size: 28px; font-size: var(--tk-font-body-lg);
} }

View File

@@ -3,6 +3,7 @@ import { View, Text, Textarea, ScrollView } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro'; import Taro, { useRouter } 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 { useElderClass } from '../../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
const STATUS_LABELS: Record<string, string> = { const STATUS_LABELS: Record<string, string> = {
@@ -16,6 +17,7 @@ const STATUS_LABELS: Record<string, string> = {
export default function FollowUpDetail() { export default function FollowUpDetail() {
const router = useRouter(); const router = useRouter();
const taskId = router.params.id || ''; const taskId = router.params.id || '';
const modeClass = useElderClass();
const [task, setTask] = useState<doctorApi.FollowUpTask | null>(null); const [task, setTask] = useState<doctorApi.FollowUpTask | null>(null);
const [records, setRecords] = useState<doctorApi.FollowUpRecord[]>([]); const [records, setRecords] = useState<doctorApi.FollowUpRecord[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -87,12 +89,12 @@ export default function FollowUpDetail() {
const formatDate = (d: string) => new Date(d).toLocaleDateString('zh-CN'); const formatDate = (d: string) => new Date(d).toLocaleDateString('zh-CN');
if (loading) return <Loading />; if (loading) return <Loading />;
if (!task) return <View className='error-text'><Text></Text></View>; if (!task) return <View className={`error-text ${modeClass}`}><Text></Text></View>;
const canSubmit = task.status === 'in_progress' || task.status === 'pending' || task.status === 'overdue'; const canSubmit = task.status === 'in_progress' || task.status === 'pending' || task.status === 'overdue';
return ( return (
<ScrollView scrollY className='followup-detail'> <ScrollView scrollY className={`followup-detail ${modeClass}`}>
<View className='section'> <View className='section'>
<View className='task-header'> <View className='task-header'>
<Text className='task-header__title'>访</Text> <Text className='task-header__title'>访</Text>

View File

@@ -18,7 +18,7 @@
.tab { .tab {
display: inline-block; display: inline-block;
padding: 24px 16px; padding: 24px 16px;
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
position: relative; position: relative;
flex-shrink: 0; flex-shrink: 0;
@@ -44,7 +44,7 @@
padding: 20px 28px; padding: 20px 28px;
text { text {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: $tx3;
} }
} }
@@ -75,19 +75,19 @@
&__type { &__type {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: 600; font-weight: 600;
color: $tx; color: $tx;
} }
&__status { &__status {
@include tag(transparent, $tx2); @include tag(transparent, $tx2);
font-size: 22px; font-size: var(--tk-font-body);
font-weight: 500; font-weight: 500;
} }
&__patient { &__patient {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
display: block; display: block;
margin-bottom: 8px; margin-bottom: 8px;
@@ -99,7 +99,7 @@
} }
&__date { &__date {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: $tx3;
} }
} }

View File

@@ -4,6 +4,7 @@ import Taro, { useRouter } 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';
import { useElderClass } from '../../../hooks/useElderClass';
import { getStatusInlineStyle, getStatusLabel } from '@/utils/statusTag'; import { getStatusInlineStyle, getStatusLabel } from '@/utils/statusTag';
import './index.scss'; import './index.scss';
@@ -18,6 +19,7 @@ const TABS = [
export default function FollowUpList() { export default function FollowUpList() {
const router = useRouter(); const router = useRouter();
const patientId = router.params.patientId || ''; const patientId = router.params.patientId || '';
const modeClass = useElderClass();
const [tasks, setTasks] = useState<doctorApi.FollowUpTask[]>([]); const [tasks, setTasks] = useState<doctorApi.FollowUpTask[]>([]);
const [activeTab, setActiveTab] = useState(''); const [activeTab, setActiveTab] = useState('');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -62,7 +64,7 @@ export default function FollowUpList() {
if (loading && tasks.length === 0) return <Loading />; if (loading && tasks.length === 0) return <Loading />;
return ( return (
<ScrollView scrollY className='followup-page'> <ScrollView scrollY className={`followup-page ${modeClass}`}>
<View className='tabs'> <View className='tabs'>
{TABS.map((t) => ( {TABS.map((t) => (
<View <View

View File

@@ -13,19 +13,18 @@
&__title { &__title {
@include section-title; @include section-title;
font-size: 40px;
margin-bottom: 12px; margin-bottom: 12px;
} }
&__greeting { &__greeting {
font-size: 28px; font-size: var(--tk-font-h2);
color: $tx2; color: $tx2;
display: block; display: block;
margin-bottom: 8px; margin-bottom: 8px;
} }
&__date { &__date {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: $tx3;
} }
@@ -48,19 +47,19 @@
text-align: center; text-align: center;
line-height: 36px; line-height: 36px;
font-weight: bold; font-weight: bold;
font-size: 22px; font-size: var(--tk-font-body);
margin-right: 12px; margin-right: 12px;
flex-shrink: 0; flex-shrink: 0;
} }
&__alert-text { &__alert-text {
flex: 1; flex: 1;
font-size: 26px; font-size: var(--tk-font-h1);
color: $dan; color: $dan;
} }
&__alert-link { &__alert-link {
font-size: 24px; font-size: var(--tk-font-h2);
color: $dan; color: $dan;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -73,7 +72,7 @@
background: $surface-alt; background: $surface-alt;
border-radius: $r; border-radius: $r;
padding: 16px 20px; padding: 16px 20px;
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx3; color: $tx3;
} }
@@ -113,14 +112,14 @@
background: $pri-l; background: $pri-l;
color: $pri; color: $pri;
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: 700; font-weight: 700;
margin-bottom: 8px; margin-bottom: 8px;
} }
&__card-num { &__card-num {
@include serif-number; @include serif-number;
font-size: 48px; font-size: var(--tk-font-hero);
font-weight: 700; font-weight: 700;
color: $tx; color: $tx;
display: block; display: block;
@@ -128,7 +127,7 @@
} }
&__card-label { &__card-label {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx2; color: $tx2;
} }
@@ -145,7 +144,7 @@
&__logout { &__logout {
color: $dan; color: $dan;
font-size: 28px; font-size: var(--tk-font-h2);
padding: 16px 48px; padding: 16px 48px;
display: inline-block; display: inline-block;
} }
@@ -172,7 +171,7 @@
background: $acc-l; background: $acc-l;
color: $acc; color: $acc;
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: 700; font-weight: 700;
} }
@@ -192,14 +191,14 @@
text-align: center; text-align: center;
background: $dan; background: $dan;
color: #fff; color: #fff;
font-size: 18px; font-size: var(--tk-font-body-sm);
font-weight: 700; font-weight: 700;
border-radius: $r-pill; border-radius: $r-pill;
padding: 0 6px; padding: 0 6px;
} }
&__label { &__label {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx2; color: $tx2;
display: block; display: block;
} }

View File

@@ -2,6 +2,7 @@ import { useState, useEffect, useMemo } 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 from '@tarojs/taro';
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';
import { useElderClass } from '../../hooks/useElderClass';
import * as doctorApi from '@/services/doctor'; import * as doctorApi from '@/services/doctor';
import Loading from '@/components/Loading'; import Loading from '@/components/Loading';
import './index.scss'; import './index.scss';
@@ -53,6 +54,7 @@ const ROLE_LABELS: Record<string, string> = {
export default function DoctorHome() { export default function DoctorHome() {
const { user, logout, roles } = useAuthStore(); const { user, logout, roles } = useAuthStore();
const modeClass = useElderClass();
const [dashboard, setDashboard] = useState<doctorApi.DoctorDashboard | null>(null); const [dashboard, setDashboard] = useState<doctorApi.DoctorDashboard | null>(null);
const [alertCount, setAlertCount] = useState(0); const [alertCount, setAlertCount] = useState(0);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -105,7 +107,7 @@ export default function DoctorHome() {
if (loading) return <Loading />; if (loading) return <Loading />;
return ( return (
<ScrollView scrollY className='doctor-home'> <ScrollView scrollY className={`doctor-home ${modeClass}`}>
<View className='doctor-home__header'> <View className='doctor-home__header'>
<Text className='doctor-home__title'></Text> <Text className='doctor-home__title'></Text>
<Text className='doctor-home__greeting'> <Text className='doctor-home__greeting'>

View File

@@ -33,12 +33,12 @@
} }
.info-label { .info-label {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: $tx3;
} }
.info-value { .info-value {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx; color: $tx;
font-weight: 500; font-weight: 500;
} }
@@ -51,7 +51,7 @@
} }
.warning-label { .warning-label {
font-size: 22px; font-size: var(--tk-font-body);
color: $wrn; color: $wrn;
font-weight: 600; font-weight: 600;
display: block; display: block;
@@ -59,7 +59,7 @@
} }
.warning-text { .warning-text {
font-size: 26px; font-size: var(--tk-font-h1);
color: $pri-d; color: $pri-d;
} }
@@ -68,14 +68,14 @@
} }
.info-block-label { .info-block-label {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: $tx3;
display: block; display: block;
margin-bottom: 8px; margin-bottom: 8px;
} }
.info-block-text { .info-block-text {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx; color: $tx;
line-height: 1.6; line-height: 1.6;
} }
@@ -96,7 +96,7 @@
.vital-value { .vital-value {
@include serif-number; @include serif-number;
font-size: 36px; font-size: var(--tk-font-num-lg);
font-weight: 700; font-weight: 700;
color: $pri; color: $pri;
display: block; display: block;
@@ -104,7 +104,7 @@
} }
.vital-label { .vital-label {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx2; color: $tx2;
} }
@@ -116,13 +116,13 @@
} }
.stat-label { .stat-label {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
} }
.stat-value { .stat-value {
@include serif-number; @include serif-number;
font-size: 26px; font-size: var(--tk-font-h1);
font-weight: 600; font-weight: 600;
color: $tx; color: $tx;
@@ -151,18 +151,18 @@
} }
&__type { &__type {
font-size: 26px; font-size: var(--tk-font-h1);
font-weight: 500; font-weight: 500;
color: $tx; color: $tx;
} }
&__date { &__date {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: $tx3;
} }
&__abnormal { &__abnormal {
font-size: 24px; font-size: var(--tk-font-h2);
color: $dan; color: $dan;
font-weight: 500; font-weight: 500;
} }
@@ -180,7 +180,7 @@
border-radius: $r; border-radius: $r;
background: $pri; background: $pri;
color: $card; color: $card;
font-size: 26px; font-size: var(--tk-font-h1);
font-weight: 500; font-weight: 500;
&:active { &:active {
@@ -196,5 +196,5 @@
text-align: center; text-align: center;
padding: 80px 32px; padding: 80px 32px;
color: $tx3; color: $tx3;
font-size: 28px; font-size: var(--tk-font-body-lg);
} }

View File

@@ -3,11 +3,13 @@ import { View, Text, ScrollView } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro'; import Taro, { useRouter } 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 { useElderClass } from '../../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
export default function PatientDetail() { export default function PatientDetail() {
const router = useRouter(); const router = useRouter();
const patientId = router.params.id || ''; const patientId = router.params.id || '';
const modeClass = useElderClass();
const [patient, setPatient] = useState<doctorApi.PatientDetail | null>(null); const [patient, setPatient] = useState<doctorApi.PatientDetail | null>(null);
const [summary, setSummary] = useState<doctorApi.HealthSummary | null>(null); const [summary, setSummary] = useState<doctorApi.HealthSummary | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -40,10 +42,10 @@ export default function PatientDetail() {
}; };
if (loading) return <Loading />; if (loading) return <Loading />;
if (!patient) return <View className='error-text'><Text></Text></View>; if (!patient) return <View className={`error-text ${modeClass}`}><Text></Text></View>;
return ( return (
<ScrollView scrollY className='patient-detail'> <ScrollView scrollY className={`patient-detail ${modeClass}`}>
{/* 基本信息 */} {/* 基本信息 */}
<View className='section'> <View className='section'>
<View className='section-header'> <View className='section-header'>

View File

@@ -15,7 +15,7 @@
background: $card; background: $card;
border-radius: $r; border-radius: $r;
padding: 20px 24px; padding: 20px 24px;
font-size: 28px; font-size: var(--tk-font-body-lg);
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
box-shadow: $shadow-sm; box-shadow: $shadow-sm;
@@ -33,7 +33,7 @@
padding: 10px 24px; padding: 10px 24px;
border-radius: $r-pill; border-radius: $r-pill;
background: $bd-l; background: $bd-l;
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx2; color: $tx2;
margin-right: 16px; margin-right: 16px;
@@ -47,7 +47,7 @@
margin-bottom: 16px; margin-bottom: 16px;
text { text {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: $tx3;
} }
} }
@@ -75,14 +75,14 @@
} }
&__name { &__name {
font-size: 30px; font-size: var(--tk-font-num);
font-weight: 600; font-weight: 600;
color: $tx; color: $tx;
margin-right: 16px; margin-right: 16px;
} }
&__meta { &__meta {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx2; color: $tx2;
} }
@@ -112,7 +112,7 @@
background: $pri-l; background: $pri-l;
&__text { &__text {
font-size: 22px; font-size: var(--tk-font-body);
} }
} }
@@ -124,7 +124,7 @@
margin-top: 32px; margin-top: 32px;
&__btn { &__btn {
font-size: 26px; font-size: var(--tk-font-h1);
color: $pri; color: $pri;
padding: 12px 24px; padding: 12px 24px;
@@ -134,7 +134,7 @@
} }
&__info { &__info {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx2; color: $tx2;
} }
} }

View File

@@ -4,9 +4,11 @@ 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';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
export default function PatientList() { export default function PatientList() {
const modeClass = useElderClass();
const [patients, setPatients] = useState<doctorApi.PatientItem[]>([]); const [patients, setPatients] = useState<doctorApi.PatientItem[]>([]);
const [tags, setTags] = useState<doctorApi.PatientTag[]>([]); const [tags, setTags] = useState<doctorApi.PatientTag[]>([]);
const [activeTag, setActiveTag] = useState<string>(''); const [activeTag, setActiveTag] = useState<string>('');
@@ -98,7 +100,7 @@ export default function PatientList() {
if (loading && patients.length === 0) return <Loading />; if (loading && patients.length === 0) return <Loading />;
return ( return (
<ScrollView scrollY className='patient-list-page'> <ScrollView scrollY className={`patient-list-page ${modeClass}`}>
<View className='search-bar'> <View className='search-bar'>
<Input <Input
className='search-input' className='search-input'

View File

@@ -1,4 +1,5 @@
@import '../../../../styles/variables.scss'; @import '../../../../styles/variables.scss';
@import '../../../../styles/mixins.scss';
.create-page { .create-page {
min-height: 100vh; min-height: 100vh;
@@ -16,7 +17,7 @@
} }
.section-title { .section-title {
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
display: block; display: block;
@@ -36,7 +37,7 @@
} }
.form-label { .form-label {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
flex-shrink: 0; flex-shrink: 0;
min-width: 140px; min-width: 140px;
@@ -45,12 +46,12 @@
.form-input { .form-input {
flex: 1; flex: 1;
text-align: right; text-align: right;
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx; color: $tx;
} }
.form-value { .form-value {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx; color: $tx;
&.placeholder { &.placeholder {
@@ -61,7 +62,7 @@
.form-textarea { .form-textarea {
width: 100%; width: 100%;
margin-top: 12px; margin-top: 12px;
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx; color: $tx;
min-height: 120px; min-height: 120px;
background: $bg; background: $bg;
@@ -86,7 +87,7 @@
} }
.submit-btn__text { .submit-btn__text {
font-size: 30px; font-size: var(--tk-font-num);
font-weight: bold; font-weight: bold;
color: #fff; color: #fff;
} }

View File

@@ -3,6 +3,7 @@ import { View, Text, Input, Textarea, Picker, ScrollView } from '@tarojs/compone
import Taro, { useRouter } from '@tarojs/taro'; import Taro, { useRouter } 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 { useElderClass } from '../../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
interface FormState { interface FormState {
@@ -50,6 +51,7 @@ const initialForm: FormState = {
export default function PrescriptionCreate() { export default function PrescriptionCreate() {
const router = useRouter(); const router = useRouter();
const patientId = router.params.patientId || ''; const patientId = router.params.patientId || '';
const modeClass = useElderClass();
const [form, setForm] = useState<FormState>(initialForm); const [form, setForm] = useState<FormState>(initialForm);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
@@ -114,7 +116,7 @@ export default function PrescriptionCreate() {
); );
return ( return (
<ScrollView scrollY className='create-page'> <ScrollView scrollY className={`create-page ${modeClass}`}>
{/* 透析器 */} {/* 透析器 */}
<View className='section'> <View className='section'>
<Text className='section-title'></Text> <Text className='section-title'></Text>

View File

@@ -1,4 +1,5 @@
@import '../../../../styles/variables.scss'; @import '../../../../styles/variables.scss';
@import '../../../../styles/mixins.scss';
.prescription-detail { .prescription-detail {
min-height: 100vh; min-height: 100vh;
@@ -16,7 +17,7 @@
} }
.section-title { .section-title {
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
display: block; display: block;
@@ -31,7 +32,7 @@
} }
.rx-header__title { .rx-header__title {
font-size: 34px; font-size: var(--tk-font-num-lg);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
} }
@@ -40,7 +41,7 @@
display: inline-block; display: inline-block;
padding: 4px 12px; padding: 4px 12px;
border-radius: 8px; border-radius: 8px;
font-size: 22px; font-size: var(--tk-font-body);
background: $bd-l; background: $bd-l;
color: $tx3; color: $tx3;
@@ -51,7 +52,7 @@
} }
.rx-sub { .rx-sub {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
display: block; display: block;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
@@ -69,12 +70,12 @@
} }
.detail-label { .detail-label {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
} }
.detail-value { .detail-value {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx; color: $tx;
text-align: right; text-align: right;
flex: 1; flex: 1;
@@ -83,7 +84,7 @@
} }
.notes-text { .notes-text {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx; color: $tx;
line-height: 1.6; line-height: 1.6;
} }
@@ -91,7 +92,7 @@
.error-text { .error-text {
text-align: center; text-align: center;
padding: 120px 0; padding: 120px 0;
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx3; color: $tx3;
} }
@@ -131,6 +132,6 @@
} }
.action-btn__text { .action-btn__text {
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: 500; font-weight: 500;
} }

View File

@@ -3,11 +3,13 @@ import { View, Text, ScrollView } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro'; import Taro, { useRouter } 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 { useElderClass } from '../../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
export default function PrescriptionDetail() { export default function PrescriptionDetail() {
const router = useRouter(); const router = useRouter();
const id = router.params.id || ''; const id = router.params.id || '';
const modeClass = useElderClass();
const [rx, setRx] = useState<doctorApi.DialysisPrescription | null>(null); const [rx, setRx] = useState<doctorApi.DialysisPrescription | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
@@ -76,10 +78,10 @@ export default function PrescriptionDetail() {
}; };
if (loading) return <Loading />; if (loading) return <Loading />;
if (!rx) return <View className='error-text'><Text></Text></View>; if (!rx) return <View className={`error-text ${modeClass}`}><Text></Text></View>;
return ( return (
<ScrollView scrollY className='prescription-detail'> <ScrollView scrollY className={`prescription-detail ${modeClass}`}>
{/* 状态头部 */} {/* 状态头部 */}
<View className='section'> <View className='section'>
<View className='rx-header'> <View className='rx-header'>

View File

@@ -1,4 +1,5 @@
@import '../../../styles/variables.scss'; @import '../../../styles/variables.scss';
@import '../../../styles/mixins.scss';
.prescription-page { .prescription-page {
min-height: 100vh; min-height: 100vh;
@@ -15,7 +16,7 @@
background: $bg; background: $bg;
border-radius: $r-sm; border-radius: $r-sm;
padding: 16px 20px; padding: 16px 20px;
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx; color: $tx;
} }
@@ -52,7 +53,7 @@
} }
.tab-text { .tab-text {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
} }
@@ -61,7 +62,7 @@
} }
.prescription-count { .prescription-count {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: $tx3;
padding: 8px 0 16px; padding: 8px 0 16px;
} }
@@ -86,7 +87,7 @@
} }
.prescription-card__model { .prescription-card__model {
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
} }
@@ -95,7 +96,7 @@
display: inline-block; display: inline-block;
padding: 4px 12px; padding: 4px 12px;
border-radius: 8px; border-radius: 8px;
font-size: 22px; font-size: var(--tk-font-body);
background: $bd-l; background: $bd-l;
color: $tx3; color: $tx3;
@@ -112,13 +113,13 @@
} }
.prescription-card__meta { .prescription-card__meta {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx2; color: $tx2;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
.prescription-card__date { .prescription-card__date {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: $tx3;
display: block; display: block;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
@@ -136,7 +137,7 @@
padding: 12px 24px; padding: 12px 24px;
background: $card; background: $card;
border-radius: $r-sm; border-radius: $r-sm;
font-size: 26px; font-size: var(--tk-font-h1);
color: $pri; color: $pri;
&--disabled { &--disabled {
@@ -145,7 +146,7 @@
} }
.page-info { .page-info {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx2; color: $tx2;
} }
@@ -169,7 +170,7 @@
} }
.fab-text { .fab-text {
font-size: 40px; font-size: var(--tk-font-hero);
color: #fff; color: #fff;
font-weight: bold; font-weight: bold;
} }

View File

@@ -4,6 +4,7 @@ import Taro, { useRouter } 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';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
const TABS = [ const TABS = [
@@ -15,6 +16,7 @@ const TABS = [
export default function PrescriptionList() { export default function PrescriptionList() {
const router = useRouter(); const router = useRouter();
const patientId = router.params.patientId || ''; const patientId = router.params.patientId || '';
const modeClass = useElderClass();
const [searchPatient, setSearchPatient] = useState(''); const [searchPatient, setSearchPatient] = useState('');
const [currentPatientId, setCurrentPatientId] = useState(patientId); const [currentPatientId, setCurrentPatientId] = useState(patientId);
const [activeTab, setActiveTab] = useState(''); const [activeTab, setActiveTab] = useState('');
@@ -66,7 +68,7 @@ export default function PrescriptionList() {
if (loading && prescriptions.length === 0) return <Loading />; if (loading && prescriptions.length === 0) return <Loading />;
return ( return (
<ScrollView scrollY className='prescription-page'> <ScrollView scrollY className={`prescription-page ${modeClass}`}>
{!patientId && ( {!patientId && (
<View className='search-bar'> <View className='search-bar'>
<Input <Input

View File

@@ -28,13 +28,13 @@
&__type { &__type {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 32px; font-size: var(--tk-font-num);
font-weight: 700; font-weight: 700;
color: $tx; color: $tx;
} }
&__status { &__status {
font-size: 24px; font-size: var(--tk-font-h2);
padding: 6px 16px; padding: 6px 16px;
border-radius: $r; border-radius: $r;
font-weight: 500; font-weight: 500;
@@ -45,13 +45,13 @@
} }
.report-date { .report-date {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
display: block; display: block;
} }
.review-info { .review-info {
font-size: 24px; font-size: var(--tk-font-h2);
color: $acc; color: $acc;
display: block; display: block;
margin-top: 8px; margin-top: 8px;
@@ -81,7 +81,7 @@
} }
.indicator-cell { .indicator-cell {
font-size: 24px; font-size: var(--tk-font-h2);
&--name { &--name {
flex: 2; flex: 2;
@@ -116,7 +116,7 @@
} }
.indicator-row--header & { .indicator-row--header & {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: $tx3;
font-weight: 400; font-weight: 400;
} }
@@ -129,7 +129,7 @@
} }
.notes-text { .notes-text {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx; color: $tx;
line-height: 1.6; line-height: 1.6;
} }
@@ -140,7 +140,7 @@
background: $bd-l; background: $bd-l;
border-radius: $r; border-radius: $r;
padding: 20px; padding: 20px;
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx; color: $tx;
box-sizing: border-box; box-sizing: border-box;
line-height: 1.6; line-height: 1.6;
@@ -158,7 +158,7 @@
} }
&__text { &__text {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $card; color: $card;
font-weight: 600; font-weight: 600;
} }
@@ -168,5 +168,5 @@
text-align: center; text-align: center;
padding: 80px 32px; padding: 80px 32px;
color: $tx3; color: $tx3;
font-size: 28px; font-size: var(--tk-font-body-lg);
} }

View File

@@ -3,12 +3,14 @@ import { View, Text, Textarea, ScrollView } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro'; import Taro, { useRouter } 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 { useElderClass } from '../../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
export default function ReportDetail() { export default function ReportDetail() {
const router = useRouter(); const router = useRouter();
const patientId = router.params.patientId || ''; const patientId = router.params.patientId || '';
const reportId = router.params.id || ''; const reportId = router.params.id || '';
const modeClass = useElderClass();
const [report, setReport] = useState<doctorApi.LabReportDetail | null>(null); const [report, setReport] = useState<doctorApi.LabReportDetail | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [doctorNotes, setDoctorNotes] = useState(''); const [doctorNotes, setDoctorNotes] = useState('');
@@ -51,10 +53,10 @@ export default function ReportDetail() {
const formatDate = (d: string) => new Date(d).toLocaleDateString('zh-CN'); const formatDate = (d: string) => new Date(d).toLocaleDateString('zh-CN');
if (loading) return <Loading />; if (loading) return <Loading />;
if (!report) return <View className='error-text'><Text></Text></View>; if (!report) return <View className={`error-text ${modeClass}`}><Text></Text></View>;
return ( return (
<ScrollView scrollY className='report-detail'> <ScrollView scrollY className={`report-detail ${modeClass}`}>
{/* 基本信息 */} {/* 基本信息 */}
<View className='section'> <View className='section'>
<View className='report-header'> <View className='report-header'>

View File

@@ -15,7 +15,7 @@
background: $card; background: $card;
border-radius: $r; border-radius: $r;
padding: 20px 24px; padding: 20px 24px;
font-size: 28px; font-size: var(--tk-font-body-lg);
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
box-shadow: $shadow-sm; box-shadow: $shadow-sm;
@@ -26,7 +26,7 @@
margin-bottom: 16px; margin-bottom: 16px;
text { text {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: $tx3;
} }
} }
@@ -56,13 +56,13 @@
&__type { &__type {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: 600; font-weight: 600;
color: $tx; color: $tx;
} }
&__date { &__date {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: $tx3;
} }
@@ -73,13 +73,13 @@
} }
&__abnormal { &__abnormal {
font-size: 26px; font-size: var(--tk-font-h1);
color: $dan; color: $dan;
font-weight: 600; font-weight: 600;
} }
&__normal { &__normal {
font-size: 26px; font-size: var(--tk-font-h1);
color: $acc; color: $acc;
} }

View File

@@ -4,11 +4,13 @@ import Taro, { useRouter } 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';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
export default function ReportList() { export default function ReportList() {
const router = useRouter(); const router = useRouter();
const patientId = router.params.patientId || ''; const patientId = router.params.patientId || '';
const modeClass = useElderClass();
const [searchPatient, setSearchPatient] = useState(''); const [searchPatient, setSearchPatient] = useState('');
const [currentPatientId, setCurrentPatientId] = useState(patientId); const [currentPatientId, setCurrentPatientId] = useState(patientId);
const [reports, setReports] = useState<doctorApi.LabReportItem[]>([]); const [reports, setReports] = useState<doctorApi.LabReportItem[]>([]);
@@ -55,7 +57,7 @@ export default function ReportList() {
if (loading && reports.length === 0) return <Loading />; if (loading && reports.length === 0) return <Loading />;
return ( return (
<ScrollView scrollY className='report-page'> <ScrollView scrollY className={`report-page ${modeClass}`}>
{!patientId && ( {!patientId && (
<View className='search-bar'> <View className='search-bar'>
<Input <Input

View File

@@ -1,34 +1,5 @@
@import '../../styles/variables.scss'; @import '../../styles/variables.scss';
@import '../../styles/mixins.scss';
@mixin serif-number {
font-family: 'Georgia', 'Times New Roman', serif;
font-variant-numeric: tabular-nums;
}
@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;
}
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.events-page { .events-page {
min-height: 100vh; min-height: 100vh;
@@ -43,14 +14,14 @@
&__title { &__title {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 40px; font-size: var(--tk-font-h1);
font-weight: bold; font-weight: bold;
display: block; display: block;
margin-bottom: 8px; margin-bottom: 8px;
} }
&__subtitle { &__subtitle {
font-size: 26px; font-size: var(--tk-font-h1);
opacity: 0.85; opacity: 0.85;
} }
} }
@@ -77,7 +48,7 @@
&__status { &__status {
@include tag($bd-l, $tx2); @include tag($bd-l, $tx2);
font-size: 22px; font-size: var(--tk-font-body);
} }
&__status--published { &__status--published {
@@ -97,7 +68,7 @@
} }
&__points { &__points {
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: bold; font-weight: bold;
color: $wrn; color: $wrn;
@include serif-number; @include serif-number;
@@ -105,7 +76,7 @@
&__title { &__title {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 30px; font-size: var(--tk-font-h1);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
display: block; display: block;
@@ -113,7 +84,7 @@
} }
&__desc { &__desc {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;
@@ -128,13 +99,13 @@
} }
&__date { &__date {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx2; color: $tx2;
} }
&__location { &__location {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: var(--tk-text-secondary);
} }
&__footer { &__footer {
@@ -146,8 +117,8 @@
} }
&__participants { &__participants {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: var(--tk-text-secondary);
@include serif-number; @include serif-number;
} }
@@ -161,7 +132,7 @@
} }
&-text { &-text {
font-size: 26px; font-size: var(--tk-font-h1);
color: $card; color: $card;
font-weight: 500; font-weight: 500;
} }

View File

@@ -4,6 +4,7 @@ import Taro from '@tarojs/taro';
import * as pointsApi from '@/services/points'; import * as pointsApi from '@/services/points';
import Loading from '@/components/Loading'; import Loading from '@/components/Loading';
import EmptyState from '@/components/EmptyState'; import EmptyState from '@/components/EmptyState';
import { useElderClass } from '../../hooks/useElderClass';
import './index.scss'; import './index.scss';
const STATUS_MAP: Record<string, { label: string; className: string }> = { const STATUS_MAP: Record<string, { label: string; className: string }> = {
@@ -14,6 +15,7 @@ const STATUS_MAP: Record<string, { label: string; className: string }> = {
}; };
export default function EventsPage() { export default function EventsPage() {
const modeClass = useElderClass();
const [events, setEvents] = useState<pointsApi.OfflineEvent[]>([]); const [events, setEvents] = useState<pointsApi.OfflineEvent[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [registering, setRegistering] = useState<string | null>(null); const [registering, setRegistering] = useState<string | null>(null);
@@ -59,7 +61,7 @@ export default function EventsPage() {
if (loading) return <Loading />; if (loading) return <Loading />;
return ( return (
<ScrollView scrollY className='events-page'> <ScrollView scrollY className={`events-page ${modeClass}`}>
<View className='events-header'> <View className='events-header'>
<Text className='events-header__title'>线</Text> <Text className='events-header__title'>线</Text>
<Text className='events-header__subtitle'></Text> <Text className='events-header__subtitle'></Text>

View File

@@ -18,7 +18,7 @@
.detail-title { .detail-title {
@include section-title; @include section-title;
font-size: 34px; font-size: var(--tk-font-num-lg);
margin-bottom: 20px; margin-bottom: 20px;
} }
@@ -34,12 +34,12 @@
} }
.detail-label { .detail-label {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
} }
.detail-value { .detail-value {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx; color: $tx;
&.status-completed { color: $acc; } &.status-completed { color: $acc; }
@@ -59,7 +59,7 @@
} }
.countdown-text { .countdown-text {
font-size: 24px; font-size: var(--tk-font-h2);
color: $wrn; color: $wrn;
font-weight: bold; font-weight: bold;
@@ -74,7 +74,7 @@
} }
.detail-desc-text { .detail-desc-text {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx; color: $tx;
line-height: 1.6; line-height: 1.6;
} }
@@ -94,7 +94,7 @@
.submit-textarea { .submit-textarea {
width: 100%; width: 100%;
min-height: 200px; min-height: 200px;
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx; color: $tx;
background: $bg; background: $bg;
border-radius: $r-sm; border-radius: $r-sm;
@@ -121,7 +121,7 @@
} }
.submit-btn-text { .submit-btn-text {
font-size: 30px; font-size: var(--tk-font-num);
color: #fff; color: #fff;
font-weight: 600; font-weight: 600;
} }
@@ -134,6 +134,6 @@
.loading-text, .loading-text,
.empty-text { .empty-text {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx3; color: var(--tk-text-secondary);
} }

View File

@@ -7,9 +7,11 @@ import { TEMPLATE_IDS } from '@/services/wechat-templates';
import { trackEvent } from '@/services/analytics'; import { trackEvent } from '@/services/analytics';
import Loading from '../../../components/Loading'; import Loading from '../../../components/Loading';
import ErrorState from '../../../components/ErrorState'; import ErrorState from '../../../components/ErrorState';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
export default function FollowUpDetail() { export default function FollowUpDetail() {
const modeClass = useElderClass();
const router = useRouter(); const router = useRouter();
const id = router.params.id || ''; const id = router.params.id || '';
@@ -82,7 +84,7 @@ export default function FollowUpDetail() {
if (loading) { if (loading) {
return ( return (
<View className='detail-page'> <View className={`detail-page ${modeClass}`}>
<Loading /> <Loading />
</View> </View>
); );
@@ -90,7 +92,7 @@ export default function FollowUpDetail() {
if (error || !task) { if (error || !task) {
return ( return (
<View className='detail-page'> <View className={`detail-page ${modeClass}`}>
<ErrorState text='任务不存在' /> <ErrorState text='任务不存在' />
</View> </View>
); );
@@ -99,7 +101,7 @@ export default function FollowUpDetail() {
const isCompleted = task.status === 'completed'; const isCompleted = task.status === 'completed';
return ( return (
<View className='detail-page'> <View className={`detail-page ${modeClass}`}>
<View className='detail-card'> <View className='detail-card'>
<Text className='detail-title'>{task.follow_up_type}</Text> <Text className='detail-title'>{task.follow_up_type}</Text>
<View className='detail-row'> <View className='detail-row'>

View File

@@ -10,11 +10,12 @@
/* ─── 页头 ─── */ /* ─── 页头 ─── */
.health-header { .health-header {
margin-bottom: 16px; margin-bottom: 20px;
} }
.health-title { .health-title {
font-size: 22px; @include serif-number;
font-size: var(--tk-font-h1);
font-weight: 700; font-weight: 700;
color: $tx; color: $tx;
} }
@@ -40,6 +41,7 @@
&.vital-tab-active { &.vital-tab-active {
background: $pri; background: $pri;
box-shadow: 0 2px 8px rgba(196, 98, 58, 0.25);
.vital-tab-text { .vital-tab-text {
color: #fff; color: #fff;
@@ -48,7 +50,7 @@
} }
.vital-tab-text { .vital-tab-text {
font-size: 15px; font-size: var(--tk-font-cap);
font-weight: 600; font-weight: 600;
color: $tx2; color: $tx2;
} }
@@ -65,11 +67,11 @@
/* ─── 录入区 ─── */ /* ─── 录入区 ─── */
.input-section { .input-section {
margin-bottom: 12px; margin-bottom: 20px;
background: $card; background: $card;
border-radius: $r; border-radius: $r;
padding: 16px; padding: 20px;
box-shadow: $shadow-sm; box-shadow: $shadow-md;
} }
.input-group { .input-group {
@@ -77,8 +79,8 @@
} }
.input-label { .input-label {
font-size: 13px; font-size: var(--tk-font-cap);
color: $tx3; color: var(--tk-text-secondary);
display: block; display: block;
margin-bottom: 4px; margin-bottom: 4px;
} }
@@ -90,7 +92,7 @@
border-radius: 12px; border-radius: 12px;
padding: 0 16px; padding: 0 16px;
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: 600; font-weight: 600;
color: $tx; color: $tx;
width: 100%; width: 100%;
@@ -98,13 +100,17 @@
} }
.input-ref { .input-ref {
font-size: 13px; font-size: var(--tk-font-cap);
color: $tx3; color: var(--tk-text-secondary);
display: block; display: block;
margin-top: 8px; margin-top: 8px;
margin-bottom: 4px; margin-bottom: 4px;
} }
.input-label--secondary {
margin-top: 20px;
}
/* ─── 血糖时段选择 ─── */ /* ─── 血糖时段选择 ─── */
.period-group { .period-group {
display: flex; display: flex;
@@ -133,7 +139,7 @@
} }
.period-btn-text { .period-btn-text {
font-size: 15px; font-size: var(--tk-font-cap);
font-weight: 600; font-weight: 600;
color: $tx2; color: $tx2;
} }
@@ -145,7 +151,8 @@
border-radius: 14px; border-radius: 14px;
background: $pri; background: $pri;
@include flex-center; @include flex-center;
margin-top: 12px; margin-top: 20px;
box-shadow: 0 2px 8px rgba(196, 98, 58, 0.25);
&:active { &:active {
opacity: 0.85; opacity: 0.85;
@@ -153,7 +160,7 @@
} }
.save-btn-text { .save-btn-text {
font-size: 17px; font-size: var(--tk-font-body-sm);
font-weight: 600; font-weight: 600;
color: #fff; color: #fff;
} }
@@ -176,7 +183,7 @@
} }
.trend-empty-text { .trend-empty-text {
font-size: 14px; font-size: var(--tk-font-cap);
color: $tx2; color: $tx2;
} }
@@ -195,6 +202,25 @@
border-radius: 12px; border-radius: 12px;
padding: 12px 8px; padding: 12px 8px;
gap: 0; gap: 0;
position: relative;
}
.trend-threshold-line {
position: absolute;
left: 8px;
right: 8px;
border-top: 1.5px dashed $wrn;
opacity: 0.6;
pointer-events: none;
}
.trend-threshold-label {
position: absolute;
right: 0;
top: -16px;
font-size: var(--tk-font-micro);
color: $wrn;
opacity: 0.8;
} }
.trend-bar-col { .trend-bar-col {
@@ -222,8 +248,8 @@
} }
.trend-bar-label { .trend-bar-label {
font-size: 11px; font-size: var(--tk-font-micro);
color: $tx3; color: var(--tk-text-secondary);
margin-top: 6px; margin-top: 6px;
} }
@@ -256,7 +282,7 @@
} }
.device-icon-text { .device-icon-text {
font-size: 22px; font-size: var(--tk-font-body);
} }
.device-info { .device-info {
@@ -265,21 +291,21 @@
} }
.device-name { .device-name {
font-size: 15px; font-size: var(--tk-font-cap);
font-weight: 500; font-weight: 500;
color: $tx; color: $tx;
display: block; display: block;
} }
.device-desc { .device-desc {
font-size: 13px; font-size: var(--tk-font-cap);
color: $acc; color: $acc;
display: block; display: block;
} }
.device-arrow { .device-arrow {
font-size: 14px; font-size: var(--tk-font-cap);
color: $tx3; color: var(--tk-text-secondary);
flex-shrink: 0; flex-shrink: 0;
} }
@@ -296,37 +322,38 @@
} }
.article-entry-text { .article-entry-text {
font-size: 15px; font-size: var(--tk-font-cap);
color: $tx; color: $tx;
font-weight: 500; font-weight: 500;
} }
/* ─── AI 建议卡片 ─── */ /* ─── AI 建议卡片 ─── */
.ai-suggestion-card { .ai-suggestion-card {
background: $card; background: $acc-l;
border-radius: $r; border-radius: $r;
padding: 16px; padding: 16px;
margin-bottom: 16px; margin-bottom: 20px;
box-shadow: $shadow-sm; box-shadow: none;
border-left: 4px solid $pri; border-left: 4px solid $acc;
} }
.ai-card-header { .ai-card-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 10px; margin-bottom: 8px;
} }
.ai-card-title { .ai-card-title {
font-size: 15px; font-size: var(--tk-font-cap);
font-weight: 600; font-weight: 600;
color: $tx; color: $acc;
} }
.ai-card-count { .ai-card-count {
font-size: 12px; font-size: var(--tk-font-micro);
color: $acc; color: $acc;
opacity: 0.7;
} }
.ai-suggestion-item { .ai-suggestion-item {
@@ -344,6 +371,7 @@
} }
.ai-suggestion-text { .ai-suggestion-text {
font-size: 13px; font-size: var(--tk-font-cap);
color: $tx2; color: $tx2;
line-height: 1.6;
} }

View File

@@ -3,9 +3,11 @@ import { View, Text, Input } from '@tarojs/components';
import Taro, { useDidShow, usePullDownRefresh } 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 { useElderClass } from '../../hooks/useElderClass';
import { inputVitalSign, getTrend, getHealthThresholds, findThreshold, DEFAULT_THRESHOLDS, type HealthThreshold } from '../../services/health'; import { inputVitalSign, getTrend, getHealthThresholds, findThreshold, DEFAULT_THRESHOLDS, type HealthThreshold } from '../../services/health';
import { listPendingSuggestions, type AiSuggestionItem } from '../../services/ai-analysis'; import { listPendingSuggestions, type AiSuggestionItem } from '../../services/ai-analysis';
import Loading from '../../components/Loading'; import Loading from '../../components/Loading';
import GuestGuard from '../../components/GuestGuard';
import './index.scss'; import './index.scss';
type VitalType = 'blood_pressure' | 'heart_rate' | 'blood_sugar' | 'weight'; type VitalType = 'blood_pressure' | 'heart_rate' | 'blood_sugar' | 'weight';
@@ -40,7 +42,8 @@ interface TrendPoint {
export default function Health() { export default function Health() {
const { todaySummary, loading, refreshToday, getTrend: fetchTrend } = useHealthStore(); const { todaySummary, loading, refreshToday, getTrend: fetchTrend } = useHealthStore();
const { currentPatient } = useAuthStore(); const { user, currentPatient } = useAuthStore();
const modeClass = useElderClass();
const [activeTab, setActiveTab] = useState<VitalType>('blood_pressure'); const [activeTab, setActiveTab] = useState<VitalType>('blood_pressure');
const [systolic, setSystolic] = useState(''); const [systolic, setSystolic] = useState('');
const [diastolic, setDiastolic] = useState(''); const [diastolic, setDiastolic] = useState('');
@@ -55,6 +58,7 @@ export default function Health() {
const [thresholds, setThresholds] = useState<HealthThreshold[]>(DEFAULT_THRESHOLDS); const [thresholds, setThresholds] = useState<HealthThreshold[]>(DEFAULT_THRESHOLDS);
useDidShow(() => { useDidShow(() => {
if (!user) return;
refreshToday(); refreshToday();
loadTrend(activeTab); loadTrend(activeTab);
loadAiSuggestions(); loadAiSuggestions();
@@ -62,11 +66,16 @@ export default function Health() {
}); });
usePullDownRefresh(() => { usePullDownRefresh(() => {
if (!user) return;
Promise.all([refreshToday(true), loadTrend(activeTab), loadAiSuggestions()]).finally(() => { Promise.all([refreshToday(true), loadTrend(activeTab), loadAiSuggestions()]).finally(() => {
Taro.stopPullDownRefresh(); Taro.stopPullDownRefresh();
}); });
}); });
if (!user) {
return <GuestGuard title='请先登录' desc='登录后即可记录和查看健康数据' />;
}
const loadAiSuggestions = async () => { const loadAiSuggestions = async () => {
try { try {
const items = await listPendingSuggestions(); const items = await listPendingSuggestions();
@@ -204,7 +213,7 @@ export default function Health() {
const dayLabels = ['日', '一', '二', '三', '四', '五', '六']; const dayLabels = ['日', '一', '二', '三', '四', '五', '六'];
return ( return (
<View className='health-page'> <View className={`health-page ${modeClass}`}>
{/* 页头 */} {/* 页头 */}
<View className='health-header'> <View className='health-header'>
<Text className='health-title'></Text> <Text className='health-title'></Text>
@@ -273,7 +282,7 @@ export default function Health() {
value={systolic} value={systolic}
onInput={(e) => setSystolic(e.detail.value)} onInput={(e) => setSystolic(e.detail.value)}
/> />
<Text className='input-label' style='margin-top:20px;'></Text> <Text className='input-label input-label--secondary'></Text>
<Input <Input
className='input-field' className='input-field'
type='number' type='number'
@@ -363,7 +372,7 @@ export default function Health() {
const tv = getThresholdValue(activeTab, thresholds)!; const tv = getThresholdValue(activeTab, thresholds)!;
const pct = Math.min(95, (tv / maxTrendValue) * 100); const pct = Math.min(95, (tv / maxTrendValue) * 100);
return ( return (
<View className='trend-threshold-line' style={`bottom:${12 + pct * 1.08}px;`}> <View className='trend-threshold-line' style={`bottom:${((12 + pct * 1.08) / 120 * 100).toFixed(1)}%;`}>
<Text className='trend-threshold-label'>{tv}</Text> <Text className='trend-threshold-label'>{tv}</Text>
</View> </View>
); );

View File

@@ -1,6 +1,10 @@
@import '../../styles/variables.scss'; @import '../../styles/variables.scss';
@import '../../styles/mixins.scss'; @import '../../styles/mixins.scss';
/* ═══════════════════════════════════════
登录后首页
═══════════════════════════════════════ */
.home-page { .home-page {
min-height: 100vh; min-height: 100vh;
background: $bg; background: $bg;
@@ -8,12 +12,12 @@
padding-bottom: calc(100px + env(safe-area-inset-bottom)); padding-bottom: calc(100px + env(safe-area-inset-bottom));
} }
/* ─── 区域 1问候 + 日期 + 铃铛 ─── */ /* ─── 问候区 ─── */
.greeting-section { .greeting-section {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
margin-bottom: 16px; margin-bottom: 24px;
} }
.greeting-left { .greeting-left {
@@ -21,7 +25,8 @@
} }
.greeting-text { .greeting-text {
font-size: 24px; @include serif-number;
font-size: var(--tk-font-h1);
font-weight: 700; font-weight: 700;
color: $tx; color: $tx;
display: block; display: block;
@@ -29,14 +34,16 @@
} }
.greeting-date { .greeting-date {
font-size: 14px; font-size: var(--tk-font-cap);
color: $tx3; color: var(--tk-text-secondary);
} }
.greeting-bell { .greeting-bell {
position: relative; position: relative;
width: 40px; width: 44px;
height: 40px; height: 44px;
border-radius: 22px;
background: $pri-l;
@include flex-center; @include flex-center;
flex-shrink: 0; flex-shrink: 0;
@@ -46,16 +53,27 @@
} }
.greeting-bell-icon { .greeting-bell-icon {
font-size: 22px; font-size: var(--tk-font-body-sm);
color: $pri-d;
} }
/* ─── 区域 2今日体征完成度 ─── */ .greeting-bell-dot {
position: absolute;
top: 6px;
right: 6px;
width: 8px;
height: 8px;
border-radius: 4px;
background: $dan;
}
/* ─── 今日体征进度 ─── */
.checkin-card { .checkin-card {
background: $card; background: $card;
border-radius: $r; border-radius: $r;
box-shadow: $shadow-sm; box-shadow: $shadow-md;
padding: 16px; padding: 20px;
margin-bottom: 12px; margin-bottom: 16px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 16px;
@@ -76,7 +94,7 @@
} }
.checkin-title { .checkin-title {
font-size: 16px; font-size: var(--tk-font-body-sm);
font-weight: 600; font-weight: 600;
color: $tx; color: $tx;
display: block; display: block;
@@ -90,7 +108,7 @@
} }
.capsule { .capsule {
font-size: 11px; font-size: var(--tk-font-micro);
padding: 3px 8px; padding: 3px 8px;
border-radius: $r-pill; border-radius: $r-pill;
font-weight: 500; font-weight: 500;
@@ -106,9 +124,9 @@
} }
} }
/* ─── 区域 3今日体征 2x2 ─── */ /* ─── 今日体征 2x2 ─── */
.vitals-section { .vitals-section {
margin-bottom: 12px; margin-bottom: 16px;
} }
.section-title { .section-title {
@@ -133,40 +151,39 @@
} }
.vital-label { .vital-label {
font-size: 13px; font-size: var(--tk-font-cap);
color: $tx2; color: $tx2;
display: block; display: block;
margin-bottom: 4px; margin-bottom: 6px;
} }
.vital-value-row { .vital-value-row {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
margin-bottom: 4px; margin-bottom: 6px;
} }
.vital-value { .vital-value {
@include serif-number; @include serif-number;
font-size: 32px; font-size: var(--tk-font-num);
font-weight: 700; font-weight: 700;
color: $tx; color: $tx;
line-height: 1.1; line-height: 1;
} }
.vital-unit { .vital-unit {
font-size: 12px; font-size: var(--tk-font-micro);
color: $tx3; color: var(--tk-text-secondary);
margin-left: 2px; margin-left: 3px;
} }
.vital-bottom { .vital-bottom {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0;
} }
.vital-tag { .vital-tag {
font-size: 12px; font-size: var(--tk-font-micro);
font-weight: 500; font-weight: 500;
padding: 2px 8px; padding: 2px 8px;
border-radius: $r-pill; border-radius: $r-pill;
@@ -189,103 +206,85 @@
} }
} }
/* ─── 区域 4今日待办 ─── */ /* ─── 智能提醒卡片 ─── */
.todo-section { .reminder-card {
margin-bottom: 12px; background: linear-gradient(135deg, $pri 0%, $pri-d 100%);
}
.todo-empty {
background: $card;
border-radius: $r; border-radius: $r;
padding: 24px; padding: 18px;
text-align: center; margin-bottom: 16px;
box-shadow: $shadow-sm; color: #fff;
} }
.todo-empty-text { .reminder-header {
font-size: 14px; display: flex;
color: $tx2; justify-content: space-between;
align-items: center;
margin-bottom: 10px;
} }
.todo-list { .reminder-title {
background: $card; font-size: var(--tk-font-cap);
border-radius: $r; font-weight: 600;
overflow: hidden; color: #fff;
box-shadow: $shadow-sm;
} }
.todo-item { .reminder-count {
font-size: var(--tk-font-micro);
opacity: 0.7;
color: #fff;
}
.reminder-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 8px;
padding: 12px 16px; padding: 8px 0;
border-bottom: 1px solid $bd;
&:last-child {
border-bottom: none;
}
&:active { &:active {
background: $bd-l; opacity: 0.8;
} }
} }
.todo-icon-wrap { .reminder-item-border {
width: 36px; border-top: 1px solid rgba(255, 255, 255, 0.15);
height: 36px; }
border-radius: 10px;
background: $pri-l; .reminder-tag {
@include flex-center; font-size: var(--tk-font-micro);
padding: 2px 6px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.2);
font-weight: 500;
color: #fff;
flex-shrink: 0; flex-shrink: 0;
} }
.todo-icon-char { .reminder-text {
font-size: 18px; font-size: var(--tk-font-cap);
font-weight: bold;
color: $pri;
}
.todo-info {
flex: 1; flex: 1;
min-width: 0; color: #fff;
}
.todo-title {
font-size: 15px;
color: $tx;
font-weight: 500;
display: block;
margin-bottom: 2px;
}
.todo-sub {
font-size: 13px;
color: $tx3;
display: block;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.todo-arrow { .reminder-arrow {
font-size: 14px; opacity: 0.5;
color: $tx3; color: #fff;
flex-shrink: 0; flex-shrink: 0;
} }
/* ─── 区域 5快捷操作 ─── */ /* ─── 快捷操作 ─── */
.action-section { .action-section {
display: flex; display: flex;
gap: 10px; gap: 10px;
margin-top: 16px; margin-top: 8px;
} }
.action-btn { .action-btn {
flex: 1; flex: 1;
height: 52px; height: 52px;
border-radius: 14px; border-radius: 14px;
font-size: 17px;
font-weight: 600;
@include flex-center; @include flex-center;
&:active { &:active {
@@ -296,6 +295,7 @@
.action-primary { .action-primary {
background: $pri; background: $pri;
color: #fff; color: #fff;
box-shadow: 0 2px 8px rgba(196, 98, 58, 0.25);
} }
.action-outline { .action-outline {
@@ -305,6 +305,169 @@
} }
.action-btn-text { .action-btn-text {
font-size: 17px; font-size: var(--tk-font-body-sm);
font-weight: 600; font-weight: 600;
} }
/* ═══════════════════════════════════════
访客首页
═══════════════════════════════════════ */
.guest-page {
min-height: 100vh;
background: $bg;
padding-bottom: calc(120px + env(safe-area-inset-bottom));
}
/* ─── 轮播图 ─── */
.guest-swiper {
width: 100%;
height: 360px;
}
.guest-slide {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.guest-slide-bg {
position: absolute;
inset: 0;
&--1 {
background: linear-gradient(135deg, $pri-d 0%, $pri 60%, $pri-l 100%);
}
}
.guest-slide:nth-child(2) .guest-slide-bg {
background: linear-gradient(135deg, $acc 0%, #3D5A40 60%, $acc-l 100%);
}
.guest-slide:nth-child(3) .guest-slide-bg {
background: linear-gradient(135deg, #8B6F4E 0%, $wrn 60%, $wrn-l 100%);
}
.guest-slide-content {
position: relative;
z-index: 1;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
padding: 40px 32px;
}
.guest-slide-title {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: var(--tk-font-h1);
font-weight: 700;
color: #FFFFFF;
display: block;
margin-bottom: 8px;
}
.guest-slide-desc {
font-size: var(--tk-font-body-sm);
color: rgba(255, 255, 255, 0.85);
display: block;
}
/* ─── 健康资讯 ─── */
.guest-section {
padding: 24px 24px 0;
}
.guest-section-title {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: var(--tk-font-body);
font-weight: bold;
color: $tx;
display: block;
margin-bottom: 16px;
}
.guest-articles {
display: flex;
flex-direction: column;
gap: 12px;
}
.guest-article-card {
background: $card;
border-radius: $r;
padding: 16px 18px;
box-shadow: $shadow-sm;
&:active {
opacity: 0.85;
}
}
.guest-article-title {
font-size: var(--tk-font-body-sm);
font-weight: 600;
color: $tx;
display: block;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.guest-article-summary {
font-size: var(--tk-font-cap);
color: var(--tk-text-secondary);
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.guest-empty {
padding: 40px 0;
text-align: center;
}
.guest-empty-text {
font-size: var(--tk-font-cap);
color: var(--tk-text-secondary);
}
/* ─── 底部登录引导 ─── */
.guest-login-prompt {
margin: 24px 24px 0;
background: $card;
border-radius: $r;
padding: 20px;
box-shadow: $shadow-md;
display: flex;
align-items: center;
gap: 16px;
}
.guest-login-text {
flex: 1;
font-size: var(--tk-font-cap);
color: $tx2;
}
.guest-login-btn {
height: 56px;
padding: 0 28px;
background: $pri;
border-radius: $r-pill;
@include flex-center;
flex-shrink: 0;
&:active {
opacity: 0.85;
}
}
.guest-login-btn-text {
font-size: var(--tk-font-h2);
font-weight: 600;
color: #fff;
}

View File

@@ -1,7 +1,9 @@
import { View, Text } from '@tarojs/components'; import { View, Text, Swiper, SwiperItem } from '@tarojs/components';
import { useState } from 'react'; import { useState, useCallback } from 'react';
import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro'; import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
import { useAuthStore } from '../../stores/auth'; import { useAuthStore } from '../../stores/auth';
import { useUIStore } from '../../stores/ui';
import { navigateToLogin } from '../../utils/navigate';
import { useHealthStore } from '../../stores/health'; import { useHealthStore } from '../../stores/health';
import ProgressRing from '../../components/ProgressRing'; import ProgressRing from '../../components/ProgressRing';
import Loading from '../../components/Loading'; import Loading from '../../components/Loading';
@@ -19,7 +21,77 @@ interface ReminderItem {
tag: string; tag: string;
} }
export default function Index() { // ─── 访客首页 ───
const CAROUSEL_SLIDES = [
{ id: 'slide-1', title: '专业血透中心', desc: '三甲级医护团队全程守护' },
{ id: 'slide-2', title: '智慧健康管理', desc: 'AI 驱动个性化健康方案' },
{ id: 'slide-3', title: '温馨就医环境', desc: '舒适安全的治疗体验' },
];
function GuestHome({ modeClass }: { modeClass: string }) {
return (
<View className={`guest-page ${modeClass}`}>
{/* 轮播图 */}
<Swiper
className='guest-swiper'
indicatorDots
indicatorColor='rgba(255,255,255,0.4)'
indicatorActiveColor='#FFFFFF'
autoplay
circular
interval={4000}
duration={500}
>
{CAROUSEL_SLIDES.map((slide) => (
<SwiperItem key={slide.id}>
<View className='guest-slide'>
<View className='guest-slide-bg guest-slide-bg--1' />
<View className='guest-slide-content'>
<Text className='guest-slide-title'>{slide.title}</Text>
<Text className='guest-slide-desc'>{slide.desc}</Text>
</View>
</View>
</SwiperItem>
))}
</Swiper>
{/* 功能亮点 */}
<View className='guest-section'>
<Text className='guest-section-title'></Text>
<View className='guest-articles'>
<View className='guest-article-card'>
<Text className='guest-article-title'></Text>
<Text className='guest-article-summary'></Text>
</View>
<View className='guest-article-card'>
<Text className='guest-article-title'></Text>
<Text className='guest-article-summary'>线</Text>
</View>
<View className='guest-article-card'>
<Text className='guest-article-title'>AI </Text>
<Text className='guest-article-summary'></Text>
</View>
</View>
</View>
{/* 底部登录引导 */}
<View className='guest-login-prompt'>
<Text className='guest-login-text'>使</Text>
<View
className='guest-login-btn'
onClick={navigateToLogin}
>
<Text className='guest-login-btn-text'></Text>
</View>
</View>
</View>
);
}
// ─── 登录后首页 ───
function HomeDashboard({ modeClass }: { modeClass: string }) {
const { user, currentPatient } = useAuthStore(); const { user, currentPatient } = useAuthStore();
const { todaySummary, loading, refreshToday } = useHealthStore(); const { todaySummary, loading, refreshToday } = useHealthStore();
const [reminders, setReminders] = useState<ReminderItem[]>([]); const [reminders, setReminders] = useState<ReminderItem[]>([]);
@@ -55,7 +127,6 @@ export default function Index() {
setRemindersLoading(true); setRemindersLoading(true);
try { try {
const items: ReminderItem[] = []; const items: ReminderItem[] = [];
const [apptRes, taskRes, suggestRes] = await Promise.allSettled([ const [apptRes, taskRes, suggestRes] = await Promise.allSettled([
appointmentApi.listAppointments(patientId, 1), appointmentApi.listAppointments(patientId, 1),
followupApi.listTasks(patientId, 'pending'), followupApi.listTasks(patientId, 'pending'),
@@ -64,15 +135,9 @@ export default function Index() {
if (suggestRes.status === 'fulfilled') { if (suggestRes.status === 'fulfilled') {
for (const s of suggestRes.value.data.slice(0, 1)) { for (const s of suggestRes.value.data.slice(0, 1)) {
items.push({ items.push({ id: s.id, text: buildSuggestionText(s), type: 'ai', tag: 'AI 建议' });
id: s.id,
text: buildSuggestionText(s),
type: 'ai',
tag: 'AI 建议',
});
} }
} }
if (apptRes.status === 'fulfilled') { if (apptRes.status === 'fulfilled') {
for (const a of apptRes.value.data.slice(0, 1)) { for (const a of apptRes.value.data.slice(0, 1)) {
if (a.status === 'pending' || a.status === 'confirmed') { if (a.status === 'pending' || a.status === 'confirmed') {
@@ -85,7 +150,6 @@ export default function Index() {
} }
} }
} }
if (taskRes.status === 'fulfilled') { if (taskRes.status === 'fulfilled') {
for (const t of taskRes.value.data.slice(0, 1)) { for (const t of taskRes.value.data.slice(0, 1)) {
items.push({ items.push({
@@ -96,7 +160,6 @@ export default function Index() {
}); });
} }
} }
setReminders(items.slice(0, 3)); setReminders(items.slice(0, 3));
} catch { } catch {
setReminders([]); setReminders([]);
@@ -110,12 +173,7 @@ export default function Index() {
const displayName = user?.display_name || currentPatient?.name || '访客'; const displayName = user?.display_name || currentPatient?.name || '访客';
const summary = todaySummary || {}; const summary = todaySummary || {};
const indicators = [ const indicators = [!!summary.blood_pressure, !!summary.heart_rate, !!summary.blood_sugar, !!summary.weight];
!!summary.blood_pressure,
!!summary.heart_rate,
!!summary.blood_sugar,
!!summary.weight,
];
const completedCount = indicators.filter(Boolean).length; const completedCount = indicators.filter(Boolean).length;
const progressPercent = Math.round((completedCount / 4) * 100); const progressPercent = Math.round((completedCount / 4) * 100);
@@ -140,7 +198,7 @@ export default function Index() {
}; };
return ( return (
<View className='home-page'> <View className={`home-page ${modeClass}`}>
{/* 问候区 */} {/* 问候区 */}
<View className='greeting-section'> <View className='greeting-section'>
<View className='greeting-left'> <View className='greeting-left'>
@@ -149,20 +207,14 @@ export default function Index() {
{new Date().toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'short' })} {new Date().toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'short' })}
</Text> </Text>
</View> </View>
<View <View className='greeting-bell' onClick={() => Taro.switchTab({ url: '/pages/messages/index' })}>
className='greeting-bell'
onClick={() => Taro.switchTab({ url: '/pages/messages/index' })}
>
<Text className='greeting-bell-icon'></Text> <Text className='greeting-bell-icon'></Text>
{unreadCount > 0 && <View className='greeting-bell-dot' />} {unreadCount > 0 && <View className='greeting-bell-dot' />}
</View> </View>
</View> </View>
{/* 今日体征进度 */} {/* 今日体征进度 */}
<View <View className='checkin-card' onClick={() => Taro.switchTab({ url: '/pages/health/index' })}>
className='checkin-card'
onClick={() => Taro.switchTab({ url: '/pages/health/index' })}
>
<View className='checkin-left'> <View className='checkin-left'>
<ProgressRing percent={progressPercent} /> <ProgressRing percent={progressPercent} />
</View> </View>
@@ -172,10 +224,7 @@ export default function Index() {
</Text> </Text>
<View className='checkin-capsules'> <View className='checkin-capsules'>
{indicatorCapsules.map((cap) => ( {indicatorCapsules.map((cap) => (
<Text <Text key={cap.label} className={`capsule ${cap.done ? 'capsule-done' : 'capsule-pending'}`}>
key={cap.label}
className={`capsule ${cap.done ? 'capsule-done' : 'capsule-pending'}`}
>
{cap.done ? '✓ ' : ''}{cap.label} {cap.done ? '✓ ' : ''}{cap.label}
</Text> </Text>
))} ))}
@@ -226,11 +275,8 @@ export default function Index() {
key={r.id} key={r.id}
className={`reminder-item ${i > 0 ? 'reminder-item-border' : ''}`} className={`reminder-item ${i > 0 ? 'reminder-item-border' : ''}`}
onClick={() => { onClick={() => {
if (r.type === 'appointment') { if (r.type === 'appointment') Taro.navigateTo({ url: '/pages/appointment/index' });
Taro.navigateTo({ url: '/pages/appointment/index' }); else if (r.type === 'followup') Taro.navigateTo({ url: `/pages/followup/detail/index?id=${r.id}` });
} else if (r.type === 'followup') {
Taro.navigateTo({ url: `/pages/followup/detail/index?id=${r.id}` });
}
}} }}
> >
<Text className='reminder-tag'>{r.tag}</Text> <Text className='reminder-tag'>{r.tag}</Text>
@@ -243,16 +289,10 @@ export default function Index() {
{/* 快捷操作 */} {/* 快捷操作 */}
<View className='action-section'> <View className='action-section'>
<View <View className='action-btn action-primary' onClick={() => Taro.switchTab({ url: '/pages/health/index' })}>
className='action-btn action-primary'
onClick={() => Taro.switchTab({ url: '/pages/health/index' })}
>
<Text className='action-btn-text'></Text> <Text className='action-btn-text'></Text>
</View> </View>
<View <View className='action-btn action-outline' onClick={() => Taro.navigateTo({ url: '/pages/appointment/create/index' })}>
className='action-btn action-outline'
onClick={() => Taro.navigateTo({ url: '/pages/appointment/create/index' })}
>
<Text className='action-btn-text'></Text> <Text className='action-btn-text'></Text>
</View> </View>
</View> </View>
@@ -260,12 +300,21 @@ export default function Index() {
); );
} }
// ─── 首页入口:根据登录状态切换 ───
export default function Index() {
const user = useAuthStore((s) => s.user);
const mode = useUIStore((s) => s.mode);
const modeClass = mode === 'elder' ? 'elder-mode' : '';
if (!user) {
return <GuestHome modeClass={modeClass} />;
}
return <HomeDashboard modeClass={modeClass} />;
}
function buildSuggestionText(s: AiSuggestionItem): string { function buildSuggestionText(s: AiSuggestionItem): string {
const riskMap: Record<string, string> = { const riskMap: Record<string, string> = { high: '高风险', medium: '中风险', low: '低风险' };
high: '高风险',
medium: '中风险',
low: '低风险',
};
const typeMap: Record<string, string> = { const typeMap: Record<string, string> = {
vital_sign_anomaly: '体征异常', vital_sign_anomaly: '体征异常',
lab_result_anomaly: '化验异常', lab_result_anomaly: '化验异常',

View File

@@ -8,19 +8,19 @@
} }
.legal-content { .legal-content {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx; color: $tx;
line-height: 1.8; line-height: 1.8;
h3 { h3 {
font-size: 34px; font-size: var(--tk-font-num-lg);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
margin-bottom: 12px; margin-bottom: 12px;
} }
h4 { h4 {
font-size: 30px; font-size: var(--tk-font-num);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
margin-top: 24px; margin-top: 24px;
@@ -28,7 +28,7 @@
} }
p { p {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
margin-bottom: 8px; margin-bottom: 8px;
line-height: 1.8; line-height: 1.8;
@@ -41,6 +41,6 @@
} }
.legal-footer-text { .legal-footer-text {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: var(--tk-text-secondary);
} }

View File

@@ -11,7 +11,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 160px 56px 80px; padding: 100px 40px 60px;
} }
/* ─── 品牌区 ─── */ /* ─── 品牌区 ─── */
@@ -19,22 +19,22 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
margin-bottom: 80px; margin-bottom: 48px;
} }
.login-logo { .login-logo {
width: 128px; width: 96px;
height: 128px; height: 96px;
border-radius: $r-lg; border-radius: $r-lg;
background: $pri; background: $pri;
@include flex-center; @include flex-center;
margin-bottom: 36px; margin-bottom: 24px;
box-shadow: 0 8px 24px rgba($pri, 0.3); box-shadow: 0 8px 24px rgba($pri, 0.3);
} }
.login-logo-mark { .login-logo-mark {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 64px; font-size: var(--tk-font-hero);
color: #fff; color: #fff;
font-weight: bold; font-weight: bold;
line-height: 1; line-height: 1;
@@ -42,14 +42,14 @@
.login-title { .login-title {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 48px; font-size: var(--tk-font-num);
color: $tx; color: $tx;
font-weight: bold; font-weight: bold;
margin-bottom: 12px; margin-bottom: 8px;
} }
.login-subtitle { .login-subtitle {
font-size: 26px; font-size: var(--tk-font-body-sm);
color: $tx2; color: $tx2;
letter-spacing: 0.05em; letter-spacing: 0.05em;
} }
@@ -57,7 +57,7 @@
/* ─── 装饰线 ─── */ /* ─── 装饰线 ─── */
.login-divider { .login-divider {
width: 48px; width: 48px;
margin-bottom: 64px; margin-bottom: 40px;
} }
.login-divider-line { .login-divider-line {
@@ -74,16 +74,18 @@
.login-btn { .login-btn {
width: 100%; width: 100%;
height: 96px; height: $btn-primary-h;
background: $pri; background: $pri;
color: #fff; color: #fff;
font-size: 32px; font-size: var(--tk-font-body-lg);
font-weight: 600; font-weight: 600;
border-radius: $r; border-radius: $r;
border: none; border: none;
@include flex-center; @include flex-center;
letter-spacing: 0.04em; letter-spacing: 0.04em;
box-shadow: 0 4px 16px rgba($pri, 0.25); box-shadow: 0 4px 16px rgba($pri, 0.25);
padding: 0;
line-height: 1;
&::after { &::after {
border: none; border: none;
@@ -98,14 +100,14 @@
.agreement-row { .agreement-row {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
margin-top: 40px; margin-top: 28px;
gap: 12px; gap: 10px;
width: 100%; width: 100%;
} }
.agreement-check { .agreement-check {
width: 32px; width: 28px;
height: 32px; height: 28px;
border: 2px solid $bd; border: 2px solid $bd;
border-radius: $r-sm; border-radius: $r-sm;
@include flex-center; @include flex-center;
@@ -120,14 +122,14 @@
} }
.agreement-check-mark { .agreement-check-mark {
font-size: 22px; font-size: var(--tk-font-body-sm);
color: #fff; color: #fff;
font-weight: bold; font-weight: bold;
line-height: 1; line-height: 1;
} }
.agreement-text { .agreement-text {
font-size: 22px; font-size: var(--tk-font-cap);
color: $tx2; color: $tx2;
line-height: 1.7; line-height: 1.7;
} }
@@ -136,3 +138,16 @@
color: $pri; color: $pri;
font-weight: 500; font-weight: 500;
} }
/* ─── 暂不登录 ─── */
.skip-row {
width: 100%;
text-align: center;
margin-top: 24px;
}
.skip-btn {
font-size: var(--tk-font-body-sm);
color: var(--tk-text-secondary);
padding: 8px 16px;
}

View File

@@ -2,13 +2,18 @@ import { useState } from 'react';
import { View, Text, Button, ScrollView } from '@tarojs/components'; import { View, Text, Button, ScrollView } from '@tarojs/components';
import Taro from '@tarojs/taro'; import Taro from '@tarojs/taro';
import { useAuthStore } from '../../stores/auth'; import { useAuthStore } from '../../stores/auth';
import { useElderClass } from '../../hooks/useElderClass';
import './index.scss'; import './index.scss';
export default function Login() { export default function Login() {
const modeClass = useElderClass();
const [needBind, setNeedBind] = useState(false); const [needBind, setNeedBind] = useState(false);
const [agreed, setAgreed] = useState(false); const [agreed, setAgreed] = useState(false);
const { login, bindPhone, loading, isMedicalStaff } = useAuthStore(); const { login, bindPhone, loading, isMedicalStaff } = useAuthStore();
// 登录页不应用关怀模式(正常模式尺寸已足够大)
const loginClass = '';
const navigateAfterLogin = () => { const navigateAfterLogin = () => {
if (isMedicalStaff()) { if (isMedicalStaff()) {
Taro.redirectTo({ url: '/pages/doctor/index' }); Taro.redirectTo({ url: '/pages/doctor/index' });
@@ -47,16 +52,29 @@ export default function Login() {
return; return;
} }
const { encryptedData, iv } = e.detail; const { encryptedData, iv } = e.detail;
const success = await bindPhone(encryptedData, iv); try {
if (success) { const success = await bindPhone(encryptedData, iv);
navigateAfterLogin(); if (success) {
} else { navigateAfterLogin();
Taro.showToast({ title: '绑定失败,请重试', icon: 'none' }); }
} catch (err: any) {
const msg = err?.message || '绑定失败';
Taro.showModal({
title: '绑定手机号失败',
content: msg,
confirmText: '重新登录',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
setNeedBind(false);
}
},
});
} }
}; };
return ( return (
<ScrollView scrollY className='login-scroll'> <ScrollView scrollY className={`login-scroll ${loginClass}`}>
<View className='login-page'> <View className='login-page'>
{/* 品牌区 */} {/* 品牌区 */}
<View className='login-brand'> <View className='login-brand'>
@@ -102,6 +120,13 @@ export default function Login() {
<Text className='agreement-link' onClick={() => Taro.navigateTo({ url: '/pages/legal/privacy-policy' })}></Text> <Text className='agreement-link' onClick={() => Taro.navigateTo({ url: '/pages/legal/privacy-policy' })}></Text>
</Text> </Text>
</View> </View>
{/* 暂不登录 */}
<View className='skip-row'>
<Text className='skip-btn' onClick={() => Taro.reLaunch({ url: '/pages/index/index' })}>
</Text>
</View>
</View> </View>
</ScrollView> </ScrollView>
); );

View File

@@ -28,7 +28,7 @@
} }
.points-label { .points-label {
font-size: 26px; font-size: var(--tk-font-h1);
color: rgba(255, 255, 255, 0.85); color: rgba(255, 255, 255, 0.85);
} }
@@ -50,7 +50,7 @@
} }
.checkin-btn-text { .checkin-btn-text {
font-size: 24px; font-size: var(--tk-font-h2);
color: #fff; color: #fff;
font-weight: 600; font-weight: 600;
} }
@@ -61,7 +61,7 @@
.points-balance { .points-balance {
@include serif-number; @include serif-number;
font-size: 72px; font-size: 72px; /* kept as-is: special display value */
font-weight: bold; font-weight: bold;
color: #fff; color: #fff;
display: block; display: block;
@@ -71,7 +71,7 @@
} }
.points-streak { .points-streak {
font-size: 22px; font-size: var(--tk-font-body);
color: rgba(255, 255, 255, 0.7); color: rgba(255, 255, 255, 0.7);
display: block; display: block;
} }
@@ -102,7 +102,7 @@
} }
.type-tab-text { .type-tab-text {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx2; color: $tx2;
&.active { &.active {
@@ -142,7 +142,7 @@
.product-image-char { .product-image-char {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 56px; font-size: var(--tk-font-hero);
font-weight: bold; font-weight: bold;
color: $pri; color: $pri;
line-height: 1; line-height: 1;
@@ -156,7 +156,7 @@
} }
.product-name { .product-name {
font-size: 26px; font-size: var(--tk-font-h1);
font-weight: 600; font-weight: 600;
color: $tx; color: $tx;
display: block; display: block;
@@ -180,20 +180,20 @@
.product-points-char { .product-points-char {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 22px; font-size: var(--tk-font-body);
font-weight: bold; font-weight: bold;
color: $wrn; color: $wrn;
} }
.product-points-value { .product-points-value {
@include serif-number; @include serif-number;
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: bold; font-weight: bold;
color: $wrn; color: $wrn;
} }
.product-stock { .product-stock {
font-size: 22px; font-size: var(--tk-font-body);
padding: 2px 10px; padding: 2px 10px;
border-radius: $r-sm; border-radius: $r-sm;
@@ -212,7 +212,7 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 160px 40px; padding: 100px 40px;
} }
.empty-icon { .empty-icon {
@@ -226,22 +226,22 @@
.empty-char { .empty-char {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 52px; font-size: var(--tk-font-hero);
font-weight: bold; font-weight: bold;
color: $pri; color: $pri;
line-height: 1; line-height: 1;
} }
.empty-title { .empty-title {
font-size: 32px; font-size: var(--tk-font-body-lg);
font-weight: 600; font-weight: 600;
color: $tx; color: $tx;
margin-bottom: 12px; margin-bottom: 12px;
} }
.empty-hint { .empty-hint {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx3; color: var(--tk-text-secondary);
text-align: center; text-align: center;
margin-bottom: 24px; margin-bottom: 24px;
} }
@@ -257,7 +257,7 @@
} }
.empty-action-text { .empty-action-text {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: #fff; color: #fff;
font-weight: 600; font-weight: 600;
} }

View File

@@ -6,6 +6,7 @@ import type { PointsProduct } from '../../services/points';
import { useAuthStore } from '../../stores/auth'; import { useAuthStore } from '../../stores/auth';
import { usePointsStore } from '../../stores/points'; import { usePointsStore } from '../../stores/points';
import Loading from '../../components/Loading'; import Loading from '../../components/Loading';
import { useElderClass } from '../../hooks/useElderClass';
import './index.scss'; import './index.scss';
const PRODUCT_TYPE_TABS = [ const PRODUCT_TYPE_TABS = [
@@ -32,6 +33,7 @@ export default function Mall() {
const [checkinLoading, setCheckinLoading] = useState(false); const [checkinLoading, setCheckinLoading] = useState(false);
const [noProfile, setNoProfile] = useState(false); const [noProfile, setNoProfile] = useState(false);
const loadingRef = useRef(false); const loadingRef = useRef(false);
const modeClass = useElderClass();
const fetchProducts = useCallback( const fetchProducts = useCallback(
async (pageNum: number, type: string, isRefresh = false) => { async (pageNum: number, type: string, isRefresh = false) => {
@@ -132,7 +134,7 @@ export default function Mall() {
if (noProfile) { if (noProfile) {
return ( return (
<View className='mall-page'> <View className={`mall-page ${modeClass}`}>
<View className='mall-empty-state'> <View className='mall-empty-state'>
<View className='empty-icon'> <View className='empty-icon'>
<Text className='empty-char'></Text> <Text className='empty-char'></Text>
@@ -148,7 +150,7 @@ export default function Mall() {
} }
return ( return (
<View className='mall-page'> <View className={`mall-page ${modeClass}`}>
{/* 积分余额卡片 */} {/* 积分余额卡片 */}
<View className='mall-header'> <View className='mall-header'>
<View className='points-card'> <View className='points-card'>

View File

@@ -4,70 +4,86 @@
.messages-page { .messages-page {
min-height: 100vh; min-height: 100vh;
background: $bg; background: $bg;
padding-bottom: calc(120px + env(safe-area-inset-bottom)); padding: 20px 24px 100px;
padding-bottom: calc(100px + env(safe-area-inset-bottom));
} }
/* ─── 页头 ─── */ /* ─── 页头 ─── */
.messages-header { .messages-header {
padding: 24px 32px 8px; margin-bottom: 20px;
} }
.messages-title { .messages-title {
font-family: 'Georgia', 'Times New Roman', serif; @include serif-number;
font-size: 36px; font-size: var(--tk-font-h1);
font-weight: bold; font-weight: 700;
color: $tx; color: $tx;
} }
/* ─── Tab 切换 ─── */ /* ─── 分段控件 Tab ─── */
.msg-tabs { .msg-segment {
display: flex; display: flex;
padding: 16px 24px 0;
gap: 0; gap: 0;
background: $surface-alt;
border-radius: $r-sm;
padding: 3px;
margin-bottom: 12px;
} }
.msg-tab { .msg-segment-tab {
flex: 1; flex: 1;
height: $tab-h; height: 40px;
border-radius: $r-xs;
@include flex-center; @include flex-center;
position: relative;
&:active { &:active {
opacity: 0.85; opacity: 0.85;
} }
} }
.msg-tab-text { .msg-segment-active {
font-size: 28px; background: $card;
font-weight: 600; box-shadow: $shadow-sm;
color: $tx2;
}
.msg-tab-active .msg-tab-text { .msg-segment-text {
color: $pri; color: $tx;
}
.msg-tab-indicator {
padding: 0 24px;
height: 3px;
background: $bd-l;
margin-bottom: 16px;
}
.msg-tab-bar {
width: 50%;
height: 3px;
background: $pri;
border-radius: 2px;
transition: transform 0.2s;
&.msg-tab-bar-right {
transform: translateX(100%);
} }
} }
.msg-segment-text {
font-size: var(--tk-font-cap);
font-weight: 600;
color: $tx3;
}
.msg-segment-badge {
position: absolute;
top: 4px;
right: 12px;
min-width: 16px;
height: 16px;
border-radius: 8px;
background: $dan;
@include flex-center;
padding: 0 4px;
}
.msg-segment-badge-text {
font-size: var(--tk-font-micro);
color: #fff;
font-weight: 600;
}
/* ─── 内容区 ─── */ /* ─── 内容区 ─── */
.msg-content { .msg-content {
padding: 0 24px; // wrapper
}
.msg-list {
display: flex;
flex-direction: column;
gap: 8px;
} }
.msg-empty { .msg-empty {
@@ -79,18 +95,18 @@
} }
.msg-empty-text { .msg-empty-text {
font-size: 26px; font-size: var(--tk-font-cap);
color: $tx2; color: $tx2;
} }
/* ─── 咨询卡片 ─── */ /* ─── 咨询卡片 ─── */
.consult-card { .consult-card {
display: flex; display: flex;
gap: 12px;
align-items: center; align-items: center;
background: $card; background: $card;
border-radius: $r; border-radius: $r;
padding: 24px; padding: 16px;
margin-bottom: 12px;
box-shadow: $shadow-sm; box-shadow: $shadow-sm;
&:active { &:active {
@@ -98,53 +114,84 @@
} }
} }
.consult-info { .consult-card-muted {
opacity: 0.65;
}
.consult-avatar {
width: 44px;
height: 44px;
border-radius: 22px;
background: $surface-alt;
@include flex-center;
flex-shrink: 0;
}
.consult-avatar-active {
background: $pri-l;
}
.consult-avatar-char {
@include serif-number;
font-size: var(--tk-font-body-sm);
font-weight: 700;
color: $tx3;
}
.consult-avatar-active .consult-avatar-char {
color: $pri;
}
.consult-body {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.consult-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
&:last-child {
margin-bottom: 0;
}
}
.consult-doctor { .consult-doctor {
font-size: 28px; font-size: var(--tk-font-cap);
font-weight: 600; font-weight: 600;
color: $tx; color: $tx;
display: block;
margin-bottom: 6px;
}
.consult-preview {
font-size: 24px;
color: $tx2;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.consult-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
flex-shrink: 0;
margin-left: 16px;
} }
.consult-time { .consult-time {
font-size: 22px; font-size: var(--tk-font-micro);
color: var(--tk-text-secondary);
flex-shrink: 0;
}
.consult-preview {
font-size: var(--tk-font-cap);
color: $tx2; color: $tx2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
margin-right: 8px;
} }
.consult-badge { .consult-badge {
min-width: 24px; min-width: 18px;
height: 24px; height: 18px;
border-radius: 12px; border-radius: 9px;
background: $dan; background: $dan;
@include flex-center; @include flex-center;
padding: 0 6px; padding: 0 4px;
flex-shrink: 0;
} }
.consult-badge-text { .consult-badge-text {
font-size: 18px; font-size: var(--tk-font-micro);
color: #fff; color: #fff;
font-weight: 600; font-weight: 600;
} }
@@ -152,43 +199,72 @@
/* ─── 通知卡片 ─── */ /* ─── 通知卡片 ─── */
.notify-card { .notify-card {
display: flex; display: flex;
align-items: center; gap: 12px;
align-items: flex-start;
background: $card; background: $card;
border-radius: $r; border-radius: $r;
padding: 24px; padding: 16px;
margin-bottom: 12px;
box-shadow: $shadow-sm; box-shadow: $shadow-sm;
&:active {
opacity: 0.85;
}
} }
.notify-info { .notify-card-muted {
opacity: 0.65;
}
.notify-icon {
width: 36px;
height: 36px;
border-radius: $r-sm;
@include flex-center;
flex-shrink: 0;
}
.notify-icon-char {
@include serif-number;
font-size: var(--tk-font-body-sm);
font-weight: 700;
}
.notify-body {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.notify-title { .notify-row {
font-size: 28px; display: flex;
font-weight: 600; justify-content: space-between;
color: $tx; align-items: center;
display: block; margin-bottom: 4px;
margin-bottom: 6px;
} }
.notify-desc { .notify-title {
font-size: 24px; font-size: var(--tk-font-cap);
color: $tx2; font-weight: 400;
display: block; color: $tx;
overflow: hidden; }
text-overflow: ellipsis;
white-space: nowrap; .notify-title-bold {
font-weight: 600;
} }
.notify-time { .notify-time {
font-size: 22px; font-size: var(--tk-font-micro);
color: $tx2; color: var(--tk-text-secondary);
flex-shrink: 0; flex-shrink: 0;
margin-left: 16px; margin-left: 8px;
}
.notify-desc {
font-size: var(--tk-font-cap);
color: $tx2;
line-height: 1.5;
}
.notify-dot {
width: 8px;
height: 8px;
border-radius: 4px;
background: $pri;
flex-shrink: 0;
margin-top: 6px;
} }

View File

@@ -4,6 +4,9 @@ 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';
import GuestGuard from '../../components/GuestGuard';
import { useAuthStore } from '../../stores/auth';
import { useElderClass } from '../../hooks/useElderClass';
import './index.scss'; import './index.scss';
type MsgTab = 'consultation' | 'notification'; type MsgTab = 'consultation' | 'notification';
@@ -14,9 +17,20 @@ interface NotificationItem {
desc: string; desc: string;
time: string; time: string;
type: string; type: string;
read?: boolean;
} }
const NOTIFY_ICONS: Record<string, { icon: string; bg: string; color: string }> = {
appointment: { icon: '约', bg: '#F0DDD4', color: '#C4623A' },
alert: { icon: '警', bg: '#FFF3E0', color: '#C4873A' },
followup: { icon: '随', bg: '#E8F0E8', color: '#5B7A5E' },
points: { icon: '分', bg: '#F0DDD4', color: '#C4623A' },
report: { icon: '报', bg: '#E8F0E8', color: '#5B7A5E' },
};
export default function Messages() { export default function Messages() {
const user = useAuthStore((s) => s.user);
const modeClass = useElderClass();
const [activeTab, setActiveTab] = useState<MsgTab>('consultation'); const [activeTab, setActiveTab] = useState<MsgTab>('consultation');
const [sessions, setSessions] = useState<ConsultationSession[]>([]); const [sessions, setSessions] = useState<ConsultationSession[]>([]);
const [notifications, setNotifications] = useState<NotificationItem[]>([]); const [notifications, setNotifications] = useState<NotificationItem[]>([]);
@@ -25,10 +39,6 @@ export default function Messages() {
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const loadingRef = useRef(false); const loadingRef = useRef(false);
useDidShow(() => {
loadData(activeTab, 1, true);
});
const loadData = async (tab: MsgTab, pageNum: number = 1, isRefresh = false) => { const loadData = async (tab: MsgTab, pageNum: number = 1, isRefresh = false) => {
if (loadingRef.current) return; if (loadingRef.current) return;
loadingRef.current = true; loadingRef.current = true;
@@ -65,6 +75,10 @@ export default function Messages() {
} }
}; };
useDidShow(() => {
if (user) loadData(activeTab, 1, true);
});
const handleTabChange = (tab: MsgTab) => { const handleTabChange = (tab: MsgTab) => {
setActiveTab(tab); setActiveTab(tab);
loadData(tab, 1, true); loadData(tab, 1, true);
@@ -89,94 +103,123 @@ export default function Messages() {
return dateStr.slice(0, 10); return dateStr.slice(0, 10);
}; };
if (!user) {
return <GuestGuard title='请先登录' desc='登录后即可查看消息和通知' />;
}
const unreadConsultCount = sessions.filter((s) => s.unread_count_patient > 0).length;
return ( return (
<View className='messages-page'> <View className={`messages-page ${modeClass}`}>
{/* 页头 */} {/* 页头 */}
<View className='messages-header'> <View className='messages-header'>
<Text className='messages-title'></Text> <Text className='messages-title'></Text>
</View> </View>
{/* Tab 切换 */} {/* 分段控件 Tab */}
<View className='msg-tabs'> <View className='msg-segment'>
<View <View
className={`msg-tab ${activeTab === 'consultation' ? 'msg-tab-active' : ''}`} className={`msg-segment-tab ${activeTab === 'consultation' ? 'msg-segment-active' : ''}`}
onClick={() => handleTabChange('consultation')} onClick={() => handleTabChange('consultation')}
> >
<Text className='msg-tab-text'></Text> <Text className='msg-segment-text'></Text>
{unreadConsultCount > 0 && (
<View className='msg-segment-badge'>
<Text className='msg-segment-badge-text'>{unreadConsultCount}</Text>
</View>
)}
</View> </View>
<View <View
className={`msg-tab ${activeTab === 'notification' ? 'msg-tab-active' : ''}`} className={`msg-segment-tab ${activeTab === 'notification' ? 'msg-segment-active' : ''}`}
onClick={() => handleTabChange('notification')} onClick={() => handleTabChange('notification')}
> >
<Text className='msg-tab-text'></Text> <Text className='msg-segment-text'></Text>
</View> </View>
</View> </View>
<View className='msg-tab-indicator'>
<View className={`msg-tab-bar ${activeTab === 'notification' ? 'msg-tab-bar-right' : ''}`} />
</View>
{/* 咨询列表 */} <View className='msg-content'>
{activeTab === 'consultation' && ( {/* 咨询列表 */}
<View className='msg-content'> {activeTab === 'consultation' && (
{loading ? ( loading ? (
<Loading /> <Loading />
) : sessions.length === 0 ? ( ) : sessions.length === 0 ? (
<View className='msg-empty'> <View className='msg-empty'>
<Text className='msg-empty-text'></Text> <Text className='msg-empty-text'></Text>
</View> </View>
) : ( ) : (
sessions.map((session) => ( <View className='msg-list'>
<View {sessions.map((session) => {
key={session.id} const doctorName = session.last_message?.slice(0, 1) || '医';
className='consult-card' const hasUnread = session.unread_count_patient > 0;
onClick={() => Taro.navigateTo({ url: `/pages/consultation/detail/index?id=${session.id}` })} return (
> <View
<View className='consult-info'> key={session.id}
<Text className='consult-doctor'> className={`consult-card ${hasUnread ? '' : 'consult-card-muted'}`}
{session.consultation_type === 'online' ? '在线咨询' : '门诊咨询'} onClick={() => Taro.navigateTo({ url: `/pages/consultation/detail/index?id=${session.id}` })}
</Text> >
<Text className='consult-preview'> <View className={`consult-avatar ${hasUnread ? 'consult-avatar-active' : ''}`}>
{session.last_message || session.subject || '暂无消息'} <Text className='consult-avatar-char'>{doctorName}</Text>
</Text>
</View>
<View className='consult-meta'>
<Text className='consult-time'>{formatTime(session.last_message_at)}</Text>
{session.unread_count_patient > 0 && (
<View className='consult-badge'>
<Text className='consult-badge-text'>
{session.unread_count_patient > 99 ? '99+' : session.unread_count_patient}
</Text>
</View> </View>
)} <View className='consult-body'>
</View> <View className='consult-row'>
</View> <Text className='consult-doctor'>
)) {session.consultation_type === 'online' ? '在线咨询' : '门诊咨询'}
)} </Text>
</View> <Text className='consult-time'>{formatTime(session.last_message_at)}</Text>
)} </View>
<View className='consult-row'>
<Text className='consult-preview'>
{session.last_message || session.subject || '暂无消息'}
</Text>
{hasUnread && (
<View className='consult-badge'>
<Text className='consult-badge-text'>
{session.unread_count_patient > 99 ? '99+' : session.unread_count_patient}
</Text>
</View>
)}
</View>
</View>
</View>
);
})}
</View>
)
)}
{/* 通知列表 */} {/* 通知列表 */}
{activeTab === 'notification' && ( {activeTab === 'notification' && (
<View className='msg-content'> loading ? (
{loading ? (
<Loading /> <Loading />
) : notifications.length === 0 ? ( ) : notifications.length === 0 ? (
<View className='msg-empty'> <View className='msg-empty'>
<Text className='msg-empty-text'></Text> <Text className='msg-empty-text'></Text>
</View> </View>
) : ( ) : (
notifications.map((n) => ( <View className='msg-list'>
<View key={n.id} className='notify-card'> {notifications.map((n) => {
<View className='notify-info'> const cfg = NOTIFY_ICONS[n.type] || NOTIFY_ICONS.report;
<Text className='notify-title'>{n.title}</Text> const isUnread = !n.read;
<Text className='notify-desc'>{n.desc}</Text> return (
</View> <View key={n.id} className={`notify-card ${isUnread ? '' : 'notify-card-muted'}`}>
<Text className='notify-time'>{n.time}</Text> <View className='notify-icon' style={`background:${cfg.bg};`}>
</View> <Text className='notify-icon-char' style={`color:${cfg.color};`}>{cfg.icon}</Text>
)) </View>
)} <View className='notify-body'>
</View> <View className='notify-row'>
)} <Text className={`notify-title ${isUnread ? 'notify-title-bold' : ''}`}>{n.title}</Text>
<Text className='notify-time'>{n.time}</Text>
</View>
<Text className='notify-desc'>{n.desc}</Text>
</View>
{isUnread && <View className='notify-dot' />}
</View>
);
})}
</View>
)
)}
</View>
</View> </View>
); );
} }

View File

@@ -26,7 +26,7 @@
} }
.alerts-tab-text { .alerts-tab-text {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
} }
@@ -75,12 +75,12 @@
} }
.alert-badge-text { .alert-badge-text {
font-size: 22px; font-size: var(--tk-font-body);
font-weight: 600; font-weight: 600;
} }
.alert-badge.sev-info .alert-badge-text { .alert-badge.sev-info .alert-badge-text {
color: $tx3; color: var(--tk-text-secondary);
} }
.alert-badge.sev-warning .alert-badge-text { .alert-badge.sev-warning .alert-badge-text {
@@ -96,12 +96,12 @@
} }
.alert-time { .alert-time {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: var(--tk-text-secondary);
} }
.alert-title { .alert-title {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx; color: $tx;
line-height: 1.5; line-height: 1.5;
} }
@@ -115,14 +115,14 @@
} }
.alerts-empty-text { .alerts-empty-text {
font-size: 30px; font-size: var(--tk-font-num);
color: $tx3; color: var(--tk-text-secondary);
margin-bottom: 16px; margin-bottom: 16px;
} }
.alerts-empty-hint { .alerts-empty-hint {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx3; color: var(--tk-text-secondary);
} }
.alerts-empty-action { .alerts-empty-action {
@@ -134,5 +134,5 @@
.alerts-empty-action-text { .alerts-empty-action-text {
color: $card; color: $card;
font-size: 28px; font-size: var(--tk-font-body-lg);
} }

View File

@@ -4,6 +4,7 @@ import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
import { listPatientAlerts, type Alert } from '@/services/alert'; import { listPatientAlerts, type Alert } from '@/services/alert';
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';
import Loading from '@/components/Loading'; import Loading from '@/components/Loading';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
const SEVERITY_MAP: Record<string, { label: string; className: string }> = { const SEVERITY_MAP: Record<string, { label: string; className: string }> = {
@@ -21,6 +22,7 @@ const STATUS_TABS = [
]; ];
export default function PatientAlerts() { export default function PatientAlerts() {
const modeClass = useElderClass();
const { currentPatient } = useAuthStore(); const { currentPatient } = useAuthStore();
const [alerts, setAlerts] = useState<Alert[]>([]); const [alerts, setAlerts] = useState<Alert[]>([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
@@ -74,7 +76,7 @@ export default function PatientAlerts() {
if (!currentPatient) { if (!currentPatient) {
return ( return (
<View className='alerts-page'> <View className={`alerts-page ${modeClass}`}>
<View className='alerts-empty'> <View className='alerts-empty'>
<Text className='alerts-empty-text'></Text> <Text className='alerts-empty-text'></Text>
<View className='alerts-empty-action' onClick={() => Taro.navigateTo({ url: '/pages/pkg-profile/family-add/index' })}> <View className='alerts-empty-action' onClick={() => Taro.navigateTo({ url: '/pages/pkg-profile/family-add/index' })}>
@@ -86,7 +88,7 @@ export default function PatientAlerts() {
} }
return ( return (
<View className='alerts-page'> <View className={`alerts-page ${modeClass}`}>
<View className='alerts-tabs'> <View className='alerts-tabs'>
{STATUS_TABS.map((tab) => ( {STATUS_TABS.map((tab) => (
<View <View

View File

@@ -26,20 +26,20 @@
.dm-hero-icon-text { .dm-hero-icon-text {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 40px; font-size: var(--tk-font-hero);
font-weight: bold; font-weight: bold;
color: $pri; color: $pri;
} }
.dm-hero-title { .dm-hero-title {
@include section-title; @include section-title;
font-size: 36px; font-size: var(--tk-font-num-lg);
margin-bottom: 8px; margin-bottom: 8px;
} }
.dm-hero-sub { .dm-hero-sub {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: var(--tk-text-secondary);
} }
/* ── card (standalone, used for date picker) ── */ /* ── card (standalone, used for date picker) ── */
@@ -60,14 +60,14 @@
.dm-card-title { .dm-card-title {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
} }
.dm-card-badge { .dm-card-badge {
@include tag($acc-l, $acc); @include tag($acc-l, $acc);
font-size: 22px; font-size: var(--tk-font-body);
margin-left: auto; margin-left: auto;
} }
@@ -82,7 +82,7 @@
} }
.dm-date-value { .dm-date-value {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $pri; color: $pri;
@include serif-number; @include serif-number;
font-weight: bold; font-weight: bold;
@@ -90,8 +90,8 @@
.dm-date-arrow { .dm-date-arrow {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: var(--tk-text-secondary);
transform: rotate(180deg); transform: rotate(180deg);
display: inline-block; display: inline-block;
} }
@@ -118,14 +118,14 @@
.dm-group-title { .dm-group-title {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: 600; font-weight: 600;
color: $tx; color: $tx;
} }
.dm-group-arrow { .dm-group-arrow {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: var(--tk-text-secondary);
transition: transform 0.2s ease; transition: transform 0.2s ease;
display: inline-block; display: inline-block;
} }
@@ -163,7 +163,7 @@
} }
.dm-field-label { .dm-field-label {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx2; color: $tx2;
display: block; display: block;
margin-bottom: 8px; margin-bottom: 8px;
@@ -185,14 +185,14 @@
.dm-bp-slash { .dm-bp-slash {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 36px; font-size: var(--tk-font-num-lg);
color: $tx3; color: var(--tk-text-secondary);
font-weight: 300; font-weight: 300;
} }
.dm-field-unit { .dm-field-unit {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: var(--tk-text-secondary);
display: block; display: block;
margin-top: 10px; margin-top: 10px;
font-style: italic; font-style: italic;
@@ -210,8 +210,8 @@
} }
.dm-unit-inline { .dm-unit-inline {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx3; color: var(--tk-text-secondary);
white-space: nowrap; white-space: nowrap;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -221,7 +221,7 @@
background: $bg; background: $bg;
border-radius: $r-sm; border-radius: $r-sm;
padding: 20px 24px; padding: 20px 24px;
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx; color: $tx;
@include serif-number; @include serif-number;
box-sizing: border-box; box-sizing: border-box;
@@ -238,7 +238,7 @@
} }
.dm-field-warning { .dm-field-warning {
font-size: 22px; font-size: var(--tk-font-body);
color: $wrn; color: $wrn;
margin-top: 8px; margin-top: 8px;
display: block; display: block;
@@ -269,7 +269,7 @@
} }
.dm-submit-text { .dm-submit-text {
font-size: 32px; font-size: var(--tk-font-num);
color: white; color: white;
font-weight: bold; font-weight: bold;
letter-spacing: 2px; letter-spacing: 2px;
@@ -283,6 +283,6 @@
} }
.dm-reset-text { .dm-reset-text {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx3; color: var(--tk-text-secondary);
} }

View File

@@ -8,6 +8,7 @@ import { useHealthStore } from '@/stores/health';
import { usePointsStore } from '@/stores/points'; import { usePointsStore } from '@/stores/points';
import { clearRequestCache } from '@/services/request'; import { clearRequestCache } from '@/services/request';
import { trackEvent } from '@/services/analytics'; import { trackEvent } from '@/services/analytics';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
const bpSchema = z.number().min(30, '血压值不能低于30').max(300, '血压值不能高于300').optional(); const bpSchema = z.number().min(30, '血压值不能低于30').max(300, '血压值不能高于300').optional();
@@ -58,6 +59,7 @@ const FIELD_LABELS: Record<string, string> = {
}; };
export default function DailyMonitoring() { export default function DailyMonitoring() {
const modeClass = useElderClass();
const { currentPatient } = useAuthStore(); const { currentPatient } = useAuthStore();
const today = formatDate(new Date()); const today = formatDate(new Date());
@@ -258,7 +260,7 @@ export default function DailyMonitoring() {
const bloodSugarAbnormal = checkAbnormal(bloodSugar, 'bloodSugar'); const bloodSugarAbnormal = checkAbnormal(bloodSugar, 'bloodSugar');
return ( return (
<View className='dm-page'> <View className={`dm-page ${modeClass}`}>
{/* 页面标题 */} {/* 页面标题 */}
<View className='dm-hero'> <View className='dm-hero'>
<View className='dm-hero-icon'> <View className='dm-hero-icon'>

View File

@@ -26,20 +26,20 @@
.input-hero-icon-text { .input-hero-icon-text {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 40px; font-size: var(--tk-font-hero);
font-weight: bold; font-weight: bold;
color: $pri; color: $pri;
} }
.input-hero-title { .input-hero-title {
@include section-title; @include section-title;
font-size: 36px; font-size: var(--tk-font-num-lg);
margin-bottom: 8px; margin-bottom: 8px;
} }
.input-hero-sub { .input-hero-sub {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: var(--tk-text-secondary);
} }
/* ── sync entry ── */ /* ── sync entry ── */
@@ -60,14 +60,14 @@
} }
.input-sync-entry-text { .input-sync-entry-text {
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: 600; font-weight: 600;
color: $pri; color: $pri;
} }
.input-sync-entry-hint { .input-sync-entry-hint {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: var(--tk-text-secondary);
} }
/* ── card ── */ /* ── card ── */
@@ -96,14 +96,14 @@
.input-card-indicator-char { .input-card-indicator-char {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 24px; font-size: var(--tk-font-h2);
font-weight: bold; font-weight: bold;
color: $acc; color: $acc;
} }
.input-card-label { .input-card-label {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
} }
@@ -119,15 +119,15 @@
} }
.input-picker-value { .input-picker-value {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx; color: $tx;
@include serif-number; @include serif-number;
} }
.input-picker-arrow { .input-picker-arrow {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: var(--tk-text-secondary);
transform: rotate(180deg); transform: rotate(180deg);
display: inline-block; display: inline-block;
} }
@@ -135,7 +135,7 @@
/* ── section title ── */ /* ── section title ── */
.input-section-title { .input-section-title {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
margin-bottom: 16px; margin-bottom: 16px;
@@ -154,7 +154,7 @@
} }
.input-field-label { .input-field-label {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx2; color: $tx2;
display: block; display: block;
margin-bottom: 8px; margin-bottom: 8px;
@@ -176,8 +176,8 @@
.input-bp-slash { .input-bp-slash {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 36px; font-size: var(--tk-font-num-lg);
color: $tx3; color: var(--tk-text-secondary);
font-weight: 300; font-weight: 300;
} }
@@ -186,7 +186,7 @@
background: $bg; background: $bg;
border-radius: $r-sm; border-radius: $r-sm;
padding: 20px 24px; padding: 20px 24px;
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx; color: $tx;
@include serif-number; @include serif-number;
box-sizing: border-box; box-sizing: border-box;
@@ -197,8 +197,8 @@
} }
.input-field-unit { .input-field-unit {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: var(--tk-text-secondary);
display: block; display: block;
margin-top: 10px; margin-top: 10px;
font-style: italic; font-style: italic;
@@ -225,7 +225,7 @@
} }
.input-submit-text { .input-submit-text {
font-size: 32px; font-size: var(--tk-font-num);
color: white; color: white;
font-weight: bold; font-weight: bold;
letter-spacing: 2px; letter-spacing: 2px;

View File

@@ -8,6 +8,7 @@ import { useHealthStore } from '@/stores/health';
import { usePointsStore } from '@/stores/points'; import { usePointsStore } from '@/stores/points';
import { clearRequestCache } from '@/services/request'; import { clearRequestCache } from '@/services/request';
import { trackEvent } from '@/services/analytics'; import { trackEvent } from '@/services/analytics';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
const INDICATORS = [ const INDICATORS = [
@@ -56,6 +57,7 @@ function getWarnForIndicator(
} }
export default function HealthInput() { export default function HealthInput() {
const modeClass = useElderClass();
const [indicatorIdx, setIndicatorIdx] = useState(0); const [indicatorIdx, setIndicatorIdx] = useState(0);
const [thresholds, setThresholds] = useState<HealthThreshold[]>(DEFAULT_THRESHOLDS); const [thresholds, setThresholds] = useState<HealthThreshold[]>(DEFAULT_THRESHOLDS);
const [value, setValue] = useState(''); const [value, setValue] = useState('');
@@ -155,7 +157,7 @@ export default function HealthInput() {
const indicatorInitial = INDICATORS[indicatorIdx].label.charAt(0); const indicatorInitial = INDICATORS[indicatorIdx].label.charAt(0);
return ( return (
<View className='input-page'> <View className={`input-page ${modeClass}`}>
{/* 页面标题 */} {/* 页面标题 */}
<View className='input-hero'> <View className='input-hero'>
<View className='input-hero-icon'> <View className='input-hero-icon'>

View File

@@ -26,14 +26,14 @@
.trend-hero-icon-text { .trend-hero-icon-text {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 40px; font-size: var(--tk-font-hero);
font-weight: bold; font-weight: bold;
color: $pri; color: $pri;
} }
.trend-hero-title { .trend-hero-title {
@include section-title; @include section-title;
font-size: 36px; font-size: var(--tk-font-num-lg);
margin-bottom: 0; margin-bottom: 0;
} }
@@ -59,7 +59,7 @@
} }
.trange-tab-text { .trange-tab-text {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx2; color: $tx2;
font-weight: 500; font-weight: 500;
} }
@@ -91,13 +91,13 @@
} }
.trend-ref-label { .trend-ref-label {
font-size: 24px; font-size: var(--tk-font-h2);
color: $acc; color: $acc;
font-weight: bold; font-weight: bold;
} }
.trend-ref-value { .trend-ref-value {
font-size: 26px; font-size: var(--tk-font-h1);
color: $acc; color: $acc;
@include serif-number; @include serif-number;
font-weight: 500; font-weight: 500;
@@ -110,7 +110,7 @@
.trend-list-title { .trend-list-title {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
display: block; display: block;
@@ -146,7 +146,7 @@
} }
.trend-item-date { .trend-item-date {
font-size: 26px; font-size: var(--tk-font-h1);
color: $tx2; color: $tx2;
} }
@@ -159,7 +159,7 @@
} }
.trend-item-value { .trend-item-value {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $pri; color: $pri;
@include serif-number; @include serif-number;
font-weight: bold; font-weight: bold;

View File

@@ -3,6 +3,7 @@ import { View, Text } from '@tarojs/components';
import { useRouter } from '@tarojs/taro'; import { useRouter } from '@tarojs/taro';
import { useHealthStore } from '@/stores/health'; import { useHealthStore } from '@/stores/health';
import TrendChart from '@/components/TrendChart'; import TrendChart from '@/components/TrendChart';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
const RANGE_OPTIONS = [ const RANGE_OPTIONS = [
@@ -22,6 +23,7 @@ const INDICATOR_META: Record<string, { label: string; unit: string; refMin?: num
}; };
export default function Trend() { export default function Trend() {
const modeClass = useElderClass();
const router = useRouter(); const router = useRouter();
const indicator = router.params.indicator || 'heart_rate'; const indicator = router.params.indicator || 'heart_rate';
const [range, setRange] = useState('7d'); const [range, setRange] = useState('7d');
@@ -41,7 +43,7 @@ export default function Trend() {
}; };
return ( return (
<View className='trend-page'> <View className={`trend-page ${modeClass}`}>
{/* 页面标题 */} {/* 页面标题 */}
<View className='trend-hero'> <View className='trend-hero'>
<View className='trend-hero-icon'> <View className='trend-hero-icon'>

View File

@@ -1,34 +1,5 @@
@import '../../../styles/variables.scss'; @import '../../../styles/variables.scss';
@import '../../../styles/mixins.scss';
@mixin serif-number {
font-family: 'Georgia', 'Times New Roman', serif;
font-variant-numeric: tabular-nums;
}
@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;
}
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.detail-page { .detail-page {
min-height: 100vh; min-height: 100vh;
@@ -46,7 +17,7 @@
} }
.balance-label { .balance-label {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx2; color: $tx2;
display: block; display: block;
margin-bottom: 8px; margin-bottom: 8px;
@@ -54,7 +25,7 @@
.balance-value { .balance-value {
@include serif-number; @include serif-number;
font-size: 60px; font-size: var(--tk-font-hero);
font-weight: bold; font-weight: bold;
color: $pri; color: $pri;
display: block; display: block;
@@ -79,7 +50,7 @@
.stat-value { .stat-value {
@include serif-number; @include serif-number;
font-size: 30px; font-size: var(--tk-font-num);
font-weight: bold; font-weight: bold;
margin-bottom: 4px; margin-bottom: 4px;
@@ -97,7 +68,7 @@
} }
.stat-label { .stat-label {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: $tx3;
} }
@@ -135,7 +106,7 @@
} }
.type-tab-text { .type-tab-text {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx3; color: $tx3;
.type-tab.active & { .type-tab.active & {
@@ -182,7 +153,7 @@
.tx-badge-text { .tx-badge-text {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 28px; font-size: var(--tk-font-body-lg);
font-weight: bold; font-weight: bold;
.tx-badge-earn & { .tx-badge-earn & {
@@ -204,7 +175,7 @@
} }
.tx-desc { .tx-desc {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx; color: $tx;
display: block; display: block;
margin-bottom: 4px; margin-bottom: 4px;
@@ -214,7 +185,7 @@
} }
.tx-date { .tx-date {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: $tx3;
display: block; display: block;
} }
@@ -229,7 +200,7 @@
.tx-amount { .tx-amount {
@include serif-number; @include serif-number;
font-size: 32px; font-size: var(--tk-font-num);
font-weight: bold; font-weight: bold;
margin-bottom: 4px; margin-bottom: 4px;
@@ -244,6 +215,6 @@
.tx-remaining { .tx-remaining {
@include serif-number; @include serif-number;
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: $tx3;
} }

View File

@@ -6,6 +6,7 @@ import type { PointsTransaction } from '../../../services/points';
import { usePointsStore } from '../../../stores/points'; import { usePointsStore } from '../../../stores/points';
import EmptyState from '../../../components/EmptyState'; import EmptyState from '../../../components/EmptyState';
import Loading from '../../../components/Loading'; import Loading from '../../../components/Loading';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
const TYPE_TABS = [ const TYPE_TABS = [
@@ -15,6 +16,7 @@ const TYPE_TABS = [
]; ];
export default function PointsDetail() { export default function PointsDetail() {
const modeClass = useElderClass();
const { account, refresh: refreshPoints } = usePointsStore(); const { account, refresh: refreshPoints } = usePointsStore();
const [transactions, setTransactions] = useState<PointsTransaction[]>([]); const [transactions, setTransactions] = useState<PointsTransaction[]>([]);
const [activeTab, setActiveTab] = useState(''); const [activeTab, setActiveTab] = useState('');
@@ -112,7 +114,7 @@ export default function PointsDetail() {
const balance = account?.balance ?? 0; const balance = account?.balance ?? 0;
return ( return (
<View className='detail-page'> <View className={`detail-page ${modeClass}`}>
{/* 余额卡片 */} {/* 余额卡片 */}
<View className='balance-card'> <View className='balance-card'>
<Text className='balance-label'></Text> <Text className='balance-label'></Text>

View File

@@ -1,34 +1,5 @@
@import '../../../styles/variables.scss'; @import '../../../styles/variables.scss';
@import '../../../styles/mixins.scss';
@mixin serif-number {
font-family: 'Georgia', 'Times New Roman', serif;
font-variant-numeric: tabular-nums;
}
@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;
}
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.exchange-page { .exchange-page {
min-height: 100vh; min-height: 100vh;
@@ -58,7 +29,7 @@
.product-icon-char { .product-icon-char {
font-family: 'Georgia', 'Times New Roman', serif; font-family: 'Georgia', 'Times New Roman', serif;
font-size: 52px; font-size: var(--tk-font-hero);
font-weight: bold; font-weight: bold;
color: #FFFFFF; color: #FFFFFF;
} }
@@ -69,7 +40,7 @@
} }
.product-name { .product-name {
font-size: 32px; font-size: var(--tk-font-num);
font-weight: bold; font-weight: bold;
color: $tx; color: $tx;
display: block; display: block;
@@ -113,19 +84,19 @@
} }
.detail-label { .detail-label {
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx2; color: $tx2;
} }
.detail-value { .detail-value {
@include serif-number; @include serif-number;
font-size: 28px; font-size: var(--tk-font-body-lg);
color: $tx; color: $tx;
font-weight: bold; font-weight: bold;
&.detail-cost { &.detail-cost {
color: $pri; color: $pri;
font-size: 34px; font-size: var(--tk-font-num-lg);
} }
&.detail-sufficient { &.detail-sufficient {
@@ -148,12 +119,12 @@
.notice-title { .notice-title {
@include section-title; @include section-title;
font-size: 28px; font-size: var(--tk-font-body-lg);
margin-bottom: 12px; margin-bottom: 12px;
} }
.notice-text { .notice-text {
font-size: 24px; font-size: var(--tk-font-h2);
color: $tx3; color: $tx3;
display: block; display: block;
line-height: 1.7; line-height: 1.7;
@@ -182,19 +153,19 @@
} }
.footer-cost-label { .footer-cost-label {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx3; color: $tx3;
} }
.footer-cost-num { .footer-cost-num {
@include serif-number; @include serif-number;
font-size: 38px; font-size: var(--tk-font-num-lg);
font-weight: bold; font-weight: bold;
color: $pri; color: $pri;
} }
.footer-cost-unit { .footer-cost-unit {
font-size: 22px; font-size: var(--tk-font-body);
color: $tx2; color: $tx2;
margin-left: 4px; margin-left: 4px;
} }
@@ -212,7 +183,7 @@
} }
.confirm-btn-text { .confirm-btn-text {
font-size: 30px; font-size: var(--tk-font-num);
color: white; color: white;
font-weight: bold; font-weight: bold;
} }

View File

@@ -8,6 +8,7 @@ import {
import type { PointsProduct } from '../../../services/points'; import type { PointsProduct } from '../../../services/points';
import { usePointsStore } from '../../../stores/points'; import { usePointsStore } from '../../../stores/points';
import Loading from '../../../components/Loading'; import Loading from '../../../components/Loading';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss'; import './index.scss';
const TYPE_INITIAL: Record<string, string> = { const TYPE_INITIAL: Record<string, string> = {
@@ -29,6 +30,7 @@ const TYPE_COLOR: Record<string, string> = {
}; };
export default function ExchangeConfirm() { export default function ExchangeConfirm() {
const modeClass = useElderClass();
const [product, setProduct] = useState<PointsProduct | null>(null); const [product, setProduct] = useState<PointsProduct | null>(null);
const { account, refresh: refreshPoints } = usePointsStore(); const { account, refresh: refreshPoints } = usePointsStore();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -119,7 +121,7 @@ export default function ExchangeConfirm() {
if (loading) { if (loading) {
return ( return (
<View className='exchange-page'> <View className={`exchange-page ${modeClass}`}>
<Loading /> <Loading />
</View> </View>
); );
@@ -131,7 +133,7 @@ export default function ExchangeConfirm() {
const typeColor = TYPE_COLOR[productType] || '#C4623A'; const typeColor = TYPE_COLOR[productType] || '#C4623A';
return ( return (
<View className='exchange-page'> <View className={`exchange-page ${modeClass}`}>
{/* 商品预览卡片 */} {/* 商品预览卡片 */}
<View className='product-card'> <View className='product-card'>
<View <View

Some files were not shown because too many files have changed in this diff Show More