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:
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user