feat(miniprogram): 关怀模式 Phase 2 — Design Token + 15 页面批量接入
- 新建 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:
@@ -1,4 +1,5 @@
|
||||
@import './styles/variables.scss';
|
||||
@import './styles/tokens.scss';
|
||||
@import './styles/elder-mode.scss';
|
||||
|
||||
page {
|
||||
|
||||
6
apps/miniprogram/src/hooks/useElderClass.ts
Normal file
6
apps/miniprogram/src/hooks/useElderClass.ts
Normal 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' : '';
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'>
|
||||
<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>
|
||||
{!isOpen && (
|
||||
<Text className='chat-header__status'>已结束</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>
|
||||
) : (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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('');
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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[]>([]);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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'>
|
||||
|
||||
79
apps/miniprogram/src/styles/tokens.scss
Normal file
79
apps/miniprogram/src/styles/tokens.scss
Normal 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;
|
||||
}
|
||||
23
apps/miniprogram/src/utils/elder-toast.ts
Normal file
23
apps/miniprogram/src/utils/elder-toast.ts
Normal 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' });
|
||||
}
|
||||
Reference in New Issue
Block a user