From 616e0a153930f994570c9e6bd1c7871c5dd98074 Mon Sep 17 00:00:00 2001 From: iven Date: Wed, 13 May 2026 23:26:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(mp):=20=E5=B0=8F=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=AE=8C=E5=96=84=20=E2=80=94=20=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=B1=82=E6=89=A9=E5=B1=95=20+=20=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 actionInbox 服务层(待办事项列表/线程查询) - consultation 服务扩展(会话详情/发送消息) - 多页面代码优化(profile/messages/health/article) - 新增 navigate 工具函数 --- apps/miniprogram/src/app.tsx | 16 +++++ .../src/pages/article/detail/index.tsx | 7 +- .../doctor/consultation/detail/index.tsx | 2 +- apps/miniprogram/src/pages/index/index.tsx | 14 ++-- apps/miniprogram/src/pages/messages/index.tsx | 17 ++--- apps/miniprogram/src/pages/profile/index.tsx | 8 ++- apps/miniprogram/src/services/article.ts | 5 ++ apps/miniprogram/src/services/consultation.ts | 11 ++- .../src/services/doctor/actionInbox.ts | 68 +++++++++++++++++++ .../src/services/doctor/consultation.ts | 16 ++++- 10 files changed, 141 insertions(+), 23 deletions(-) create mode 100644 apps/miniprogram/src/services/doctor/actionInbox.ts diff --git a/apps/miniprogram/src/app.tsx b/apps/miniprogram/src/app.tsx index a51e4d7..96c3428 100644 --- a/apps/miniprogram/src/app.tsx +++ b/apps/miniprogram/src/app.tsx @@ -10,11 +10,27 @@ function App({ children }: PropsWithChildren>) { const restoreAuth = useAuthStore((s) => s.restore); const restoreUI = useUIStore((s) => s.restore); + // 首次 mount 时立即恢复认证状态(优先于 useDidShow) + useEffect(() => { + restoreAuth(); + restoreUI(); + }, []); + useDidShow(() => { restoreAuth(); restoreUI(); }); + // 暴露全局 bridge 供 MCP/自动化测试调用 + useEffect(() => { + (globalThis as any).__hms = { + restoreAuth: () => { restoreAuth(); return useAuthStore.getState(); }, + restoreUI, + getAuthState: () => useAuthStore.getState(), + }; + return () => { delete (globalThis as any).__hms; }; + }, [restoreAuth, restoreUI]); + useEffect(() => { const timer = setInterval(() => { flushEvents(); diff --git a/apps/miniprogram/src/pages/article/detail/index.tsx b/apps/miniprogram/src/pages/article/detail/index.tsx index 6243768..21fc56d 100644 --- a/apps/miniprogram/src/pages/article/detail/index.tsx +++ b/apps/miniprogram/src/pages/article/detail/index.tsx @@ -1,9 +1,10 @@ import React, { useState, useEffect } from 'react'; import { View, Text, RichText } from '@tarojs/components'; import Taro, { useRouter, useShareAppMessage } from '@tarojs/taro'; -import { getArticleDetail, Article } from '../../../services/article'; +import { getArticleDetail, getPublicArticleDetail, Article } from '../../../services/article'; import { trackEvent } from '@/services/analytics'; import { useElderClass } from '../../../hooks/useElderClass'; +import { useAuthStore } from '../../../stores/auth'; import './index.scss'; export default function ArticleDetail() { @@ -25,7 +26,9 @@ export default function ArticleDetail() { useEffect(() => { if (!id) return; setLoading(true); - getArticleDetail(id) + const user = useAuthStore.getState().user; + const fetcher = user ? getArticleDetail(id) : getPublicArticleDetail(id); + fetcher .then((data) => setArticle(data)) .catch(() => Taro.showToast({ title: '加载失败', icon: 'none' })) .finally(() => setLoading(false)); diff --git a/apps/miniprogram/src/pages/doctor/consultation/detail/index.tsx b/apps/miniprogram/src/pages/doctor/consultation/detail/index.tsx index 0e49d2e..cf96f81 100644 --- a/apps/miniprogram/src/pages/doctor/consultation/detail/index.tsx +++ b/apps/miniprogram/src/pages/doctor/consultation/detail/index.tsx @@ -107,7 +107,7 @@ export default function ConsultationDetail() { success: async (res) => { if (res.confirm) { try { - await doctorApi.closeSession(sessionId); + await doctorApi.closeSession(sessionId, session?.version ?? 0); Taro.showToast({ title: '已关闭', icon: 'success' }); loadData(); } catch { diff --git a/apps/miniprogram/src/pages/index/index.tsx b/apps/miniprogram/src/pages/index/index.tsx index bbe77ed..6443de7 100644 --- a/apps/miniprogram/src/pages/index/index.tsx +++ b/apps/miniprogram/src/pages/index/index.tsx @@ -69,13 +69,12 @@ function GuestHome({ modeClass }: { modeClass: string }) { ]); if (bannerData.status === 'fulfilled' && bannerData.value?.length > 0) { - const baseUrl = process.env.TARO_APP_API_URL || 'http://localhost:3000/api/v1'; - const fileBase = baseUrl.replace(/\/api\/v1$/, ''); + const apiBase = process.env.TARO_APP_API_URL || 'http://localhost:3000/api/v1'; const withLocal = await Promise.all( bannerData.value.map(async (b) => { if (!b.image_url) return b; try { - const fullUrl = b.image_url.startsWith('http') ? b.image_url : `${fileBase}${b.image_url}`; + const fullUrl = b.image_url.startsWith('http') ? b.image_url : `${apiBase}${b.image_url}`; const res = await Taro.downloadFile({ url: fullUrl }); if (res.tempFilePath) { return { ...b, local_path: res.tempFilePath }; @@ -94,6 +93,7 @@ function GuestHome({ modeClass }: { modeClass: string }) { } } catch { setBanners(FALLBACK_SLIDES); + Taro.showToast({ title: '内容加载失败', icon: 'none' }); } }; @@ -135,7 +135,11 @@ function GuestHome({ modeClass }: { modeClass: string }) { {articles.length > 0 ? ( {articles.map((article) => ( - + Taro.navigateTo({ url: `/pages/article/detail/index?id=${article.id}` })} + > {article.cover_image && ( )} @@ -261,7 +265,7 @@ function HomeDashboard({ modeClass }: { modeClass: string }) { const hour = new Date().getHours(); const greeting = hour < 12 ? '上午好' : hour < 18 ? '下午好' : '晚上好'; - const displayName = user?.display_name || currentPatient?.name || '访客'; + const displayName = user?.display_name || currentPatient?.name || user?.username || (user?.phone ? `${user.phone.slice(-4)}` : '') || '用户'; const summary = todaySummary || {}; const indicators = [!!summary.blood_pressure, !!summary.heart_rate, !!summary.blood_sugar, !!summary.weight]; diff --git a/apps/miniprogram/src/pages/messages/index.tsx b/apps/miniprogram/src/pages/messages/index.tsx index 5f1a87b..2713de7 100644 --- a/apps/miniprogram/src/pages/messages/index.tsx +++ b/apps/miniprogram/src/pages/messages/index.tsx @@ -20,12 +20,12 @@ interface NotificationItem { read?: boolean; } -const NOTIFY_ICONS: Record = { - 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' }, +const NOTIFY_ICONS: Record = { + appointment: { icon: '约', cls: 'notify-type-appointment' }, + alert: { icon: '警', cls: 'notify-type-alert' }, + followup: { icon: '随', cls: 'notify-type-followup' }, + points: { icon: '分', cls: 'notify-type-points' }, + report: { icon: '报', cls: 'notify-type-report' }, }; export default function Messages() { @@ -68,6 +68,7 @@ export default function Messages() { if (isRefresh) { if (tab === 'consultation') setSessions([]); else setNotifications([]); + Taro.showToast({ title: '加载失败,下拉重试', icon: 'none' }); } } finally { setLoading(false); @@ -202,8 +203,8 @@ export default function Messages() { const isUnread = !n.read; return ( - - {cfg.icon} + + {cfg.icon} diff --git a/apps/miniprogram/src/pages/profile/index.tsx b/apps/miniprogram/src/pages/profile/index.tsx index 78c46f1..21a3396 100644 --- a/apps/miniprogram/src/pages/profile/index.tsx +++ b/apps/miniprogram/src/pages/profile/index.tsx @@ -81,6 +81,7 @@ export default function Profile() { const mode = useUIStore((s) => s.mode); const modeClass = mode === 'elder' ? 'elder-mode' : ''; const isGuest = !user; + const groups = isGuest ? GUEST_GROUPS : LOGGED_IN_GROUPS; useDidShow(() => { if (!isGuest) refreshPoints(); @@ -105,7 +106,8 @@ export default function Profile() { }); }; - const groups = isGuest ? GUEST_GROUPS : LOGGED_IN_GROUPS; + const displayName = user?.display_name || user?.username || (user?.phone ? `${user.phone.slice(-4)}` : '') || '用户'; + const displayInitial = (user?.display_name || user?.username || '用').charAt(0); return ( @@ -125,10 +127,10 @@ export default function Profile() { <> - {(user?.display_name || '访').charAt(0)} + {displayInitial} - {user?.display_name || '访客'} + {displayName} {user?.phone ? `${user.phone.slice(0, 3)}****${user.phone.slice(-4)}` : ''} diff --git a/apps/miniprogram/src/services/article.ts b/apps/miniprogram/src/services/article.ts index 1f1a168..da9a41d 100644 --- a/apps/miniprogram/src/services/article.ts +++ b/apps/miniprogram/src/services/article.ts @@ -60,6 +60,11 @@ export async function getArticleDetail(id: string) { return api.get
(`/health/articles/${id}`); } +/** 公开文章详情(无需认证) */ +export async function getPublicArticleDetail(id: string) { + return api.get
(`/public/articles/${id}`); +} + export async function listCategories() { return api.get('/health/article-categories'); } diff --git a/apps/miniprogram/src/services/consultation.ts b/apps/miniprogram/src/services/consultation.ts index 8e3b178..6557158 100644 --- a/apps/miniprogram/src/services/consultation.ts +++ b/apps/miniprogram/src/services/consultation.ts @@ -1,4 +1,4 @@ -import { api } from './request'; +import { api, requestWithTimeout } from './request'; export interface ConsultationSession { id: string; @@ -60,3 +60,12 @@ export async function sendMessage(sessionId: string, content: string, contentTyp export async function markSessionRead(sessionId: string) { return api.put(`/health/consultation-sessions/${sessionId}/read`); } + +export async function pollMessages(sessionId: string, afterId?: string) { + const params = new URLSearchParams(); + if (afterId) params.set('after_id', afterId); + params.set('timeout', '25'); + const query = params.toString(); + const path = `/health/consultation-sessions/${sessionId}/messages/poll${query ? '?' + query : ''}`; + return requestWithTimeout('GET', path, undefined, 30000); +} diff --git a/apps/miniprogram/src/services/doctor/actionInbox.ts b/apps/miniprogram/src/services/doctor/actionInbox.ts new file mode 100644 index 0000000..efd44be --- /dev/null +++ b/apps/miniprogram/src/services/doctor/actionInbox.ts @@ -0,0 +1,68 @@ +import { api } from '../request'; +import type { ActionItem, ThreadResponse } from '../action-inbox'; + +interface WorkbenchStats { + pending: number; + in_progress: number; + completed_today: number; + overdue: number; +} + +interface NursePatientSummary { + patient_id: string; + patient_name: string; + bed_number?: string; + primary_diagnosis?: string; + care_plan_status?: string; + open_action_count: number; +} + +interface TeamOverview { + team_name: string; + members: { + user_id: string; + user_name: string; + role: string; + active_tasks: number; + }[]; +} + +interface PaginatedData { + data: ActionItem[]; + total: number; +} + +export async function listActionItems(params?: { + status?: string; + type?: string; + page?: number; + page_size?: number; + assigned_to_me?: boolean; + patient_id?: string; +}) { + return api.get( + '/health/action-inbox', + params as Record, + ); +} + +export async function getActionThread(sourceRef: string) { + return api.get( + `/health/action-inbox/${encodeURIComponent(sourceRef)}/thread`, + ); +} + +export async function getWorkbenchStats(assignedToMe?: boolean) { + return api.get( + '/health/action-inbox/stats', + assignedToMe !== undefined ? { assigned_to_me: assignedToMe } : undefined, + ); +} + +export async function getTeamOverview() { + return api.get('/health/action-inbox/team'); +} + +export async function getMyPatients() { + return api.get('/health/action-inbox/my-patients'); +} diff --git a/apps/miniprogram/src/services/doctor/consultation.ts b/apps/miniprogram/src/services/doctor/consultation.ts index df0d0a2..6896adb 100644 --- a/apps/miniprogram/src/services/doctor/consultation.ts +++ b/apps/miniprogram/src/services/doctor/consultation.ts @@ -1,4 +1,4 @@ -import { api } from '../request'; +import { api, requestWithTimeout } from '../request'; // ── Consultation (doctor view) ───────────────────── @@ -14,6 +14,7 @@ export interface ConsultationSession { last_message_at: string | null; unread_count_doctor?: number; created_at: string; + version: number; } export interface ConsultationMessage { @@ -60,8 +61,17 @@ export async function markSessionRead(sessionId: string) { return api.put(`/health/consultation-sessions/${sessionId}/read`); } -export async function closeSession(sessionId: string) { - return api.put(`/health/consultation-sessions/${sessionId}/close`); +export async function closeSession(sessionId: string, version: number) { + return api.put(`/health/consultation-sessions/${sessionId}/close`, { version }); +} + +export async function pollMessages(sessionId: string, afterId?: string) { + const params = new URLSearchParams(); + if (afterId) params.set('after_id', afterId); + params.set('timeout', '25'); + const query = params.toString(); + const path = `/health/consultation-sessions/${sessionId}/messages/poll${query ? '?' + query : ''}`; + return requestWithTimeout('GET', path, undefined, 30000); } export interface ConsultationStats {