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>
This commit is contained in:
iven
2026-05-09 22:17:58 +08:00
parent 4335f7e144
commit e8ccee02d5
22 changed files with 209 additions and 63 deletions

View File

@@ -1,4 +1,5 @@
@import './styles/variables.scss';
@import './styles/tokens.scss';
@import './styles/elder-mode.scss';
page {

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

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ import { View, Text } from '@tarojs/components';
import Taro, { useDidShow, usePullDownRefresh, useReachBottom } from '@tarojs/taro';
import { listConsultations, ConsultationSession } from '@/services/consultation';
import Loading from '../../components/Loading';
import { useUIStore } from '../../stores/ui';
import { useElderClass } from '../../hooks/useElderClass';
import './index.scss';
function getStatusTag(status: string) {
@@ -34,8 +34,7 @@ export default function Consultation() {
const [sessions, setSessions] = useState<ConsultationSession[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const { mode } = useUIStore();
const modeClass = mode === 'elder' ? 'elder-mode' : '';
const modeClass = useElderClass();
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const loadingRef = useRef(false);

View File

@@ -3,7 +3,7 @@ import { View, Text, Input } from '@tarojs/components';
import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
import { useHealthStore } from '../../stores/health';
import { useAuthStore } from '../../stores/auth';
import { useUIStore } from '../../stores/ui';
import { useElderClass } from '../../hooks/useElderClass';
import { inputVitalSign, getTrend, getHealthThresholds, findThreshold, DEFAULT_THRESHOLDS, type HealthThreshold } from '../../services/health';
import { listPendingSuggestions, type AiSuggestionItem } from '../../services/ai-analysis';
import Loading from '../../components/Loading';
@@ -43,8 +43,7 @@ interface TrendPoint {
export default function Health() {
const { todaySummary, loading, refreshToday, getTrend: fetchTrend } = useHealthStore();
const { user, currentPatient } = useAuthStore();
const { mode } = useUIStore();
const modeClass = mode === 'elder' ? 'elder-mode' : '';
const modeClass = useElderClass();
const [activeTab, setActiveTab] = useState<VitalType>('blood_pressure');
const [systolic, setSystolic] = useState('');
const [diastolic, setDiastolic] = useState('');

View File

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

View File

@@ -6,7 +6,7 @@ import { notificationService } from '../../services/notification';
import Loading from '../../components/Loading';
import GuestGuard from '../../components/GuestGuard';
import { useAuthStore } from '../../stores/auth';
import { useUIStore } from '../../stores/ui';
import { useElderClass } from '../../hooks/useElderClass';
import './index.scss';
type MsgTab = 'consultation' | 'notification';
@@ -30,8 +30,7 @@ const NOTIFY_ICONS: Record<string, { icon: string; bg: string; color: string }>
export default function Messages() {
const user = useAuthStore((s) => s.user);
const { mode } = useUIStore();
const modeClass = mode === 'elder' ? 'elder-mode' : '';
const modeClass = useElderClass();
const [activeTab, setActiveTab] = useState<MsgTab>('consultation');
const [sessions, setSessions] = useState<ConsultationSession[]>([]);
const [notifications, setNotifications] = useState<NotificationItem[]>([]);

View File

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

View File

@@ -8,6 +8,7 @@ import { useHealthStore } from '@/stores/health';
import { usePointsStore } from '@/stores/points';
import { clearRequestCache } from '@/services/request';
import { trackEvent } from '@/services/analytics';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss';
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() {
const modeClass = useElderClass();
const { currentPatient } = useAuthStore();
const today = formatDate(new Date());
@@ -258,7 +260,7 @@ export default function DailyMonitoring() {
const bloodSugarAbnormal = checkAbnormal(bloodSugar, 'bloodSugar');
return (
<View className='dm-page'>
<View className={`dm-page ${modeClass}`}>
{/* 页面标题 */}
<View className='dm-hero'>
<View className='dm-hero-icon'>

View File

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

View File

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

View File

@@ -4,9 +4,11 @@ import Taro, { useDidShow } from '@tarojs/taro';
import { listPatients, Patient } from '../../../services/patient';
import { useAuthStore } from '../../../stores/auth';
import EmptyState from '../../../components/EmptyState';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss';
export default function FamilyList() {
const modeClass = useElderClass();
const [patients, setPatients] = useState<Patient[]>([]);
const [loading, setLoading] = useState(false);
const { currentPatient, setCurrentPatient } = useAuthStore();
@@ -58,7 +60,7 @@ export default function FamilyList() {
};
return (
<View className='family-page'>
<View className={`family-page ${modeClass}`}>
<Text className='family-page-title'></Text>
<View className='family-list'>

View File

@@ -4,6 +4,7 @@ import Taro, { useDidShow } from '@tarojs/taro';
import { listTasks, FollowUpTask } from '../../../services/followup';
import EmptyState from '../../../components/EmptyState';
import Loading from '../../../components/Loading';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss';
const TABS = [
@@ -13,6 +14,7 @@ const TABS = [
];
export default function MyFollowUps() {
const modeClass = useElderClass();
const [activeTab, setActiveTab] = useState('pending');
const [tasks, setTasks] = useState<FollowUpTask[]>([]);
const [loading, setLoading] = useState(false);
@@ -55,7 +57,7 @@ export default function MyFollowUps() {
};
return (
<View className='my-followups-page'>
<View className={`my-followups-page ${modeClass}`}>
<View className='tab-bar'>
{TABS.map((tab) => (
<View

View File

@@ -4,6 +4,7 @@ import Taro, { useDidShow, usePullDownRefresh, useReachBottom } from '@tarojs/ta
import { listHealthRecords, HealthRecord } from '../../../services/health-record';
import EmptyState from '../../../components/EmptyState';
import Loading from '../../../components/Loading';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss';
const TYPE_MAP: Record<string, string> = {
@@ -13,6 +14,7 @@ const TYPE_MAP: Record<string, string> = {
};
export default function HealthRecords() {
const modeClass = useElderClass();
const [records, setRecords] = useState<HealthRecord[]>([]);
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
@@ -55,7 +57,7 @@ export default function HealthRecords() {
});
return (
<View className='health-records-page'>
<View className={`health-records-page ${modeClass}`}>
<Text className='page-title'></Text>
<View className='record-list'>

View File

@@ -9,9 +9,11 @@ import {
deleteReminder,
type MedicationReminder,
} from '../../../services/medication-reminder';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss';
export default function MedicationReminder() {
const modeClass = useElderClass();
const [reminders, setReminders] = useState<MedicationReminder[]>([]);
const [loading, setLoading] = useState(true);
const [showForm, setShowForm] = useState(false);
@@ -96,7 +98,7 @@ export default function MedicationReminder() {
if (loading) {
return (
<View className='medication-page'>
<View className={`medication-page ${modeClass}`}>
<Text className='page-title'></Text>
<View style={{ padding: '40px 0', textAlign: 'center' }}>
<Text style={{ color: '#94A3B8', fontSize: '28px' }}>...</Text>
@@ -106,7 +108,7 @@ export default function MedicationReminder() {
}
return (
<View className='medication-page'>
<View className={`medication-page ${modeClass}`}>
<Text className='page-title'></Text>
<View className='reminder-list'>

View File

@@ -4,9 +4,11 @@ import Taro, { useDidShow, usePullDownRefresh, useReachBottom } from '@tarojs/ta
import { listReports, LabReport } from '../../../services/report';
import EmptyState from '../../../components/EmptyState';
import Loading from '../../../components/Loading';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss';
export default function MyReports() {
const modeClass = useElderClass();
const [reports, setReports] = useState<LabReport[]>([]);
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
@@ -65,7 +67,7 @@ export default function MyReports() {
};
return (
<View className='my-reports-page'>
<View className={`my-reports-page ${modeClass}`}>
<Text className='page-title'></Text>
<View className='report-list'>

View File

@@ -2,9 +2,11 @@ import React from 'react';
import { View, Text } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { useAuthStore } from '../../../stores/auth';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss';
export default function Settings() {
const modeClass = useElderClass();
const { logout } = useAuthStore();
const handleClearCache = () => {
@@ -55,7 +57,7 @@ export default function Settings() {
};
return (
<View className='settings-page'>
<View className={`settings-page ${modeClass}`}>
<Text className='page-title'></Text>
<View className='settings-group'>

View File

@@ -0,0 +1,79 @@
// Design Token — CSS 自定义属性
// 正常模式值定义在 :root关怀模式通过 .elder-mode 覆盖
// 页面样式应引用 var(--tk-*) 而非硬编码 px 值
// ═══════════════════════════════════════
// 正常模式 Token
// ═══════════════════════════════════════
page {
// ─── 字号 ───
--tk-font-hero: 48px; // 登录标题等超大字
--tk-font-h1: 26px; // 页面标题
--tk-font-h2: 22px; // 区块标题
--tk-font-body: 15px; // 正文 / 列表项
--tk-font-body-lg: 17px; // 大正文 / 按钮文字
--tk-font-cap: 13px; // 辅助说明 / 次要文字
--tk-font-micro: 11px; // 标签 / 角标 / 最小文字
// ─── 数值型字号 ───
--tk-font-value: 30px; // 体征数值
--tk-font-value-lg: 28px; // 统计数值
// ─── 间距 ───
--tk-gap-xs: 6px;
--tk-gap-sm: 8px;
--tk-gap-md: 10px;
--tk-gap-lg: 16px;
--tk-gap-xl: 20px;
--tk-gap-2xl: 24px;
// ─── 内边距 ───
--tk-pad-page: 20px 24px; // 页面内边距
--tk-pad-card: 16px; // 卡片内边距
--tk-pad-section: 20px; // 区块内边距
// ─── 触控 ───
--tk-touch-min: 44px;
--tk-btn-h: 52px;
--tk-btn-primary-h: 56px;
// ─── 布局 ───
--tk-grid-cols: 2;
--tk-line-height: 1.5;
}
// ═══════════════════════════════════════
// 关怀模式 Token非线性放大
// ═══════════════════════════════════════
.elder-mode {
// 字号:标题 ×1.15 / 正文 ×1.35 / 辅助 ×1.55
--tk-font-hero: 56px;
--tk-font-h1: 30px;
--tk-font-h2: 26px;
--tk-font-body: 20px;
--tk-font-body-lg: 22px;
--tk-font-cap: 18px;
--tk-font-micro: 17px;
--tk-font-value: 34px;
--tk-font-value-lg: 34px;
// 间距×1.4(大于字号放大倍率,增加呼吸空间)
--tk-gap-xs: 8px;
--tk-gap-sm: 12px;
--tk-gap-md: 14px;
--tk-gap-lg: 22px;
--tk-gap-xl: 28px;
--tk-gap-2xl: 32px;
--tk-pad-page: 28px 32px;
--tk-pad-card: 20px;
--tk-pad-section: 28px;
--tk-touch-min: 56px;
--tk-btn-h: 60px;
--tk-btn-primary-h: 64px;
--tk-grid-cols: 1;
--tk-line-height: 1.7;
}

View File

@@ -0,0 +1,23 @@
import Taro from '@tarojs/taro';
import { useUIStore } from '../stores/ui';
/**
* 关怀模式自适应 Toast
* - 正常模式:默认 1500ms
* - 关怀模式:默认 3000ms + 触觉反馈
*/
export function showToast(options: {
title: string;
icon?: 'success' | 'error' | 'loading' | 'none';
duration?: number;
mask?: boolean;
}) {
const { mode } = useUIStore.getState();
const duration = options.duration ?? (mode === 'elder' ? 3000 : 1500);
if (mode === 'elder') {
try { Taro.vibrateShort({ type: 'light' }); } catch { /* 不支持时静默 */ }
}
Taro.showToast({ ...options, duration, icon: options.icon ?? 'none' });
}