From 4be28de3ce3c0461a668cc1421395ed923346235 Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 16 May 2026 22:38:21 +0800 Subject: [PATCH] =?UTF-8?q?fix(health):=20=E4=BF=AE=E5=A4=8D=E6=82=A3?= =?UTF-8?q?=E8=80=85=E7=AB=AF=E5=92=A8=E8=AF=A2=E6=9D=83=E9=99=90+?= =?UTF-8?q?=E8=81=8A=E5=A4=A9=E9=A1=B5UI+SVG=E6=A8=A1=E6=9D=BF=E8=AD=A6?= =?UTF-8?q?=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - consultation_handler: create_message/mark_session_read 从 .manage 降为 .list, 患者端只有 list 权限,导致发送消息和标记已读 403 - consultation.ts: 同步后端 DTO doctor_name/patient_name 等缺失字段 - messages/index.tsx: 咨询卡片显示医生姓名替代 consultation_type - consultation/index.tsx: 同步显示 doctor_name - pkg-consultation/detail: 按原型重写聊天页(医生头像+在线状态+非对称气泡+药丸输入栏) - ProgressRing: SVG 替换为 conic-gradient 纯 CSS,消除 tmpl_0_svg 模板警告 - usePageData: stopPullDownRefresh 加 try-catch 防止 DevTools fd race --- .../src/components/ui/ProgressRing/index.scss | 27 +- .../src/components/ui/ProgressRing/index.tsx | 43 +-- apps/miniprogram/src/hooks/usePageData.ts | 2 +- .../src/pages/consultation/index.tsx | 2 +- .../miniprogram/src/pages/messages/index.scss | 7 + apps/miniprogram/src/pages/messages/index.tsx | 12 +- .../pages/pkg-consultation/detail/index.scss | 304 ++++++++++-------- .../pages/pkg-consultation/detail/index.tsx | 80 ++--- apps/miniprogram/src/services/consultation.ts | 5 + .../src/handler/consultation_handler.rs | 4 +- 10 files changed, 278 insertions(+), 208 deletions(-) diff --git a/apps/miniprogram/src/components/ui/ProgressRing/index.scss b/apps/miniprogram/src/components/ui/ProgressRing/index.scss index 62b4f08..f40057a 100644 --- a/apps/miniprogram/src/components/ui/ProgressRing/index.scss +++ b/apps/miniprogram/src/components/ui/ProgressRing/index.scss @@ -4,9 +4,30 @@ position: relative; flex-shrink: 0; - &__center { - position: absolute; - inset: 0; + &--sm { + width: 64px; + height: 64px; + } + + &--lg { + width: 80px; + height: 80px; + } + + &__track { + width: 100%; + height: 100%; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + } + + &__inner { + width: calc(100% - 8px); + height: calc(100% - 8px); + border-radius: 50%; + background: $card; display: flex; align-items: center; justify-content: center; diff --git a/apps/miniprogram/src/components/ui/ProgressRing/index.tsx b/apps/miniprogram/src/components/ui/ProgressRing/index.tsx index 736836b..3167fd2 100644 --- a/apps/miniprogram/src/components/ui/ProgressRing/index.tsx +++ b/apps/miniprogram/src/components/ui/ProgressRing/index.tsx @@ -15,39 +15,24 @@ const ProgressRing: React.FC = ({ label, className = '', }) => { - const px = size === 'sm' ? 64 : 80; - const r = (px / 2) - 4; - const circumference = 2 * Math.PI * r; - const offset = circumference * (1 - Math.min(progress, 1)); + const pct = Math.round(Math.min(progress, 1) * 100); + const deg = (pct / 100) * 360; const cls = ['progress-ring', `progress-ring--${size}`, className].filter(Boolean).join(' '); return ( - - - - - - - {label ? ( - {label} - ) : ( - {Math.round(progress * 100)}% - )} + + + + {label ? ( + {label} + ) : ( + {pct}% + )} + ); diff --git a/apps/miniprogram/src/hooks/usePageData.ts b/apps/miniprogram/src/hooks/usePageData.ts index 0962831..34812f4 100644 --- a/apps/miniprogram/src/hooks/usePageData.ts +++ b/apps/miniprogram/src/hooks/usePageData.ts @@ -67,7 +67,7 @@ export function usePageData( try { await refresh(); } finally { - Taro.stopPullDownRefresh(); + try { Taro.stopPullDownRefresh(); } catch { /* DevTools fd race */ } } }); diff --git a/apps/miniprogram/src/pages/consultation/index.tsx b/apps/miniprogram/src/pages/consultation/index.tsx index 6301ca8..ef90f82 100644 --- a/apps/miniprogram/src/pages/consultation/index.tsx +++ b/apps/miniprogram/src/pages/consultation/index.tsx @@ -161,7 +161,7 @@ export default function Consultation() { - {session.subject || '在线咨询'} + {session.doctor_name || session.subject || '在线咨询'} {session.last_message_at diff --git a/apps/miniprogram/src/pages/messages/index.scss b/apps/miniprogram/src/pages/messages/index.scss index 4f73c1e..b9e17cf 100644 --- a/apps/miniprogram/src/pages/messages/index.scss +++ b/apps/miniprogram/src/pages/messages/index.scss @@ -143,6 +143,13 @@ color: $tx; } +.consult-type-tag { + font-size: var(--tk-font-micro); + font-weight: 400; + color: $tx3; + margin-left: 6px; +} + .consult-time { font-size: var(--tk-font-micro); color: var(--tk-text-secondary); diff --git a/apps/miniprogram/src/pages/messages/index.tsx b/apps/miniprogram/src/pages/messages/index.tsx index e10cf25..80f6b91 100644 --- a/apps/miniprogram/src/pages/messages/index.tsx +++ b/apps/miniprogram/src/pages/messages/index.tsx @@ -159,7 +159,8 @@ export default function Messages() { ) : ( {sessions.map((session) => { - const doctorName = session.last_message?.slice(0, 1) || '医'; + const displayName = session.doctor_name || '在线咨询'; + const avatarChar = session.doctor_name?.charAt(0) || '咨'; const hasUnread = session.unread_count_patient > 0; return ( - {doctorName} + {avatarChar} - {session.consultation_type === 'online' ? '在线咨询' : '门诊咨询'} + {displayName} + {session.consultation_type && ( + + {session.consultation_type === 'online' ? '在线' : '门诊'} + + )} {formatTime(session.last_message_at)} diff --git a/apps/miniprogram/src/pages/pkg-consultation/detail/index.scss b/apps/miniprogram/src/pages/pkg-consultation/detail/index.scss index d5c8d30..a4c22c3 100644 --- a/apps/miniprogram/src/pages/pkg-consultation/detail/index.scss +++ b/apps/miniprogram/src/pages/pkg-consultation/detail/index.scss @@ -9,223 +9,263 @@ } /* ─── 导航栏 ─── */ -.chat-header { +.chat-nav { display: flex; align-items: center; - padding: var(--tk-gap-sm) var(--tk-gap-md); + justify-content: space-between; + padding: 16px 20px 12px; background: $card; border-bottom: 1px solid $bd-l; flex-shrink: 0; - position: relative; } -.chat-header__back { - position: absolute; - left: 16px; - z-index: 1; +.chat-nav__back { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; &:active { opacity: var(--tk-touch-feedback-opacity); } } -.chat-header__back-text { - font-size: var(--tk-font-body-sm); - color: var(--tk-pri); +.chat-nav__back-icon { + font-size: 24px; + font-weight: 300; + color: $tx; + line-height: 1; } -.chat-header__center { - flex: 1; +.chat-nav__center { display: flex; flex-direction: column; align-items: center; } -.chat-header__title { - font-size: var(--tk-font-body-sm); - font-weight: 600; +.chat-nav__title { + font-family: Georgia, 'Times New Roman', serif; + font-size: 17px; + font-weight: 700; color: $tx; } -.chat-header__status { - font-size: var(--tk-font-micro); - color: $acc; +.chat-nav__online { + display: flex; + align-items: center; + gap: 4px; margin-top: 2px; +} - &--closed { - color: $tx3; +.chat-nav__dot { + width: 6px; + height: 6px; + border-radius: 3px; + background: $acc; +} + +.chat-nav__online-text { + font-size: 11px; + color: $acc; +} + +.chat-nav__offline-text { + font-size: 11px; + color: $tx3; + margin-top: 2px; +} + +.chat-nav__more { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + + &:active { + opacity: var(--tk-touch-feedback-opacity); } } -/* ─── 消息区 ─── */ -.chat-messages { - flex: 1; - padding: var(--tk-gap-md) var(--tk-gap-md) 0; - overflow-y: auto; +.chat-nav__more-icon { + font-size: 18px; + font-weight: 700; + color: $tx3; + letter-spacing: 1px; } -.msg-row { +/* ─── 消息区域 ─── */ +.chat-body { + flex: 1; + padding: 16px 20px; +} + +/* ─── 日期分隔 ─── */ +.chat-date-pill { display: flex; - margin-bottom: var(--tk-gap-md); - gap: var(--tk-gap-xs); + justify-content: center; + margin: 8px 0 12px; + + &__text { + font-size: 11px; + color: $tx3; + background: $surface-alt; + padding: 3px 12px; + border-radius: 10px; + } +} + +/* ─── 消息行 ─── */ +.chat-msg { + display: flex; + align-items: flex-start; + margin-bottom: 8px; &--self { justify-content: flex-end; } + + &--doctor { + gap: 10px; + } } /* ─── 医生头像 ─── */ -.msg-avatar { - width: 32px; - height: 32px; - border-radius: $r; - background: var(--tk-pri-l); - @include flex-center; +.chat-msg__avatar { + width: 36px; + height: 36px; + border-radius: 18px; + background: $pri; + display: flex; + align-items: center; + justify-content: center; flex-shrink: 0; } -.msg-avatar-char { - @include serif-number; - font-size: var(--tk-font-cap); - font-weight: 700; - color: var(--tk-pri); +.chat-msg__avatar-char { + color: $white; + font-size: 15px; + font-weight: 600; } /* ─── 消息气泡 ─── */ -.msg-bubble { - max-width: 70%; - padding: var(--tk-gap-sm) var(--tk-gap-md); - box-shadow: $shadow-sm; +.chat-msg__bubble { + max-width: 72%; + padding: 10px 14px; - &--other { + &--doctor { background: $card; - border-radius: $r $r $r $r-xs; + border-radius: 4px 16px 16px 16px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); } &--self { - background: var(--tk-pri); - border-radius: $r $r $r-xs $r; + background: $pri-l; + border-radius: 16px 4px 16px 16px; } } -.msg-text { - font-size: var(--tk-font-cap); +.chat-msg__text { + font-size: 15px; color: $tx; - display: block; line-height: 1.6; word-break: break-all; - - .msg-bubble--self & { - color: $white; - } } -.msg-date-divider { - display: flex; - justify-content: center; - padding: var(--tk-gap-sm) 0; - - &__text { - font-size: var(--tk-font-micro); - color: var(--tk-text-secondary); - background: $surface-alt; - padding: 2px 12px; - border-radius: $r-pill; - } -} - -.msg-truncated-hint { - display: flex; - justify-content: center; - padding: var(--tk-gap-sm) 0; - - &__text { - font-size: var(--tk-font-micro); - color: var(--tk-text-secondary); - background: $surface-alt; - padding: 2px 12px; - border-radius: $r-pill; - } -} - -.msg-image { +.chat-msg__image { width: 200px; - border-radius: $r-sm; - margin-top: var(--tk-gap-2xs); -} - -.msg-time { - font-size: var(--tk-font-micro); - color: var(--tk-text-secondary); - display: block; - margin-top: var(--tk-gap-2xs); - - .msg-bubble--self & { - color: rgba(255, 255, 255, 0.7); - text-align: right; - } + border-radius: 12px; } +/* ─── 空状态 ─── */ .chat-empty { text-align: center; - padding: 80px var(--tk-page-padding); + padding: 80px 20px; &__text { - font-size: var(--tk-font-cap); - color: var(--tk-text-secondary); + font-size: 14px; + color: $tx3; } } -/* ─── 输入栏 ─── */ -.chat-input-bar { +/* ─── 底部输入栏 ─── */ +.chat-bar { display: flex; align-items: center; gap: 10px; - padding: 10px 16px 38px; + padding: 10px 16px; + padding-bottom: calc(10px + env(safe-area-inset-bottom)); background: $card; border-top: 1px solid $bd-l; flex-shrink: 0; + + &--closed { + justify-content: center; + } } -.chat-input { +.chat-bar__add { + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + &:active { + opacity: var(--tk-touch-feedback-opacity); + } +} + +.chat-bar__add-icon { + font-size: 22px; + color: $tx3; + font-weight: 300; +} + +.chat-bar__input { flex: 1; - height: 48px; - background: $bg; - border: 1.5px solid $bd; - border-radius: $r-lg; + height: 40px; + background: $surface-alt; + border: none; + border-radius: 20px; padding: 0 14px; - font-size: var(--tk-font-cap); + font-size: 14px; color: $tx; } -.chat-send-btn { - width: 48px; - height: 48px; - border-radius: $r-lg; - background: var(--tk-pri); - @include flex-center; +.chat-bar__placeholder { + color: $tx3; + font-size: 14px; +} + +.chat-bar__send { + width: 40px; + height: 40px; + border-radius: 20px; + background: $pri; + display: flex; + align-items: center; + justify-content: center; flex-shrink: 0; - box-shadow: 0 2px 6px rgba($pri, 0.3); &--disabled { opacity: 0.5; } -} -.chat-send-btn__icon { - font-size: var(--tk-font-cap); - color: $white; - font-weight: 600; -} - -.chat-closed-bar { - padding: var(--tk-gap-md); - text-align: center; - background: $card; - border-top: 1px solid $bd-l; - - &__text { - font-size: var(--tk-font-cap); - color: var(--tk-text-secondary); + &:active:not(&--disabled) { + opacity: var(--tk-touch-feedback-opacity); } } + +.chat-bar__send-icon { + color: $white; + font-size: 20px; + font-weight: 700; +} + +.chat-bar__closed-text { + font-size: 14px; + color: $tx3; +} diff --git a/apps/miniprogram/src/pages/pkg-consultation/detail/index.tsx b/apps/miniprogram/src/pages/pkg-consultation/detail/index.tsx index 95deda7..aab2620 100644 --- a/apps/miniprogram/src/pages/pkg-consultation/detail/index.tsx +++ b/apps/miniprogram/src/pages/pkg-consultation/detail/index.tsx @@ -105,11 +105,6 @@ export default function ConsultationDetail() { } }; - const formatTime = (dateStr: string) => { - const d = new Date(dateStr); - return d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); - }; - const getDateLabel = (dateStr: string): string => { const d = new Date(dateStr); const today = new Date(); @@ -128,42 +123,49 @@ export default function ConsultationDetail() { const isImageUrl = (url: string) => /\.(jpg|jpeg|png|gif|webp)(\?.*)?$/i.test(url); - // 渲染层面的消息数量上限,防止长对话 DOM 节点过多 const hiddenCount = Math.max(0, messages.length - MAX_RENDER_MESSAGES); const renderMessages = hiddenCount > 0 ? messages.slice(-MAX_RENDER_MESSAGES) : messages; if (loading) return ; const isOpen = session?.status !== 'closed'; - const doctorInitial = (session?.subject || '医').charAt(0); - const statusLabel = session?.status === 'active' ? '进行中' - : session?.status === 'pending' ? '等待接诊' - : '已结束'; + const doctorName = session?.doctor_name || '医生'; + const avatarChar = doctorName.charAt(0); + const isOnline = session?.status === 'active'; return ( - {/* 导航栏 — 对齐设计稿:返回 + 标题 + 副标题 */} - - Taro.navigateBack()}> - ‹ 返回 + {/* 导航栏 */} + + Taro.navigateBack()}> + - - {session?.subject || '在线咨询'} - - {statusLabel} - + + {doctorName} + {isOnline ? ( + + + 在线 + + ) : ( + 离线 + )} + + + ··· + {/* 消息区域 */} {hiddenCount > 0 && ( - - 已隐藏较早的 {hiddenCount} 条消息 + + 已隐藏较早的 {hiddenCount} 条消息 )} {renderMessages.map((msg, idx) => { @@ -172,28 +174,27 @@ export default function ConsultationDetail() { return ( {showDateDivider && ( - - {getDateLabel(msg.created_at)} + + {getDateLabel(msg.created_at)} )} - + {!isSelf && ( - - {doctorInitial} + + {avatarChar} )} - + {isImageUrl(msg.content) ? ( Taro.previewImage({ urls: [msg.content], current: msg.content })} /> ) : ( - {msg.content} + {msg.content} )} - {formatTime(msg.created_at)} @@ -206,11 +207,16 @@ export default function ConsultationDetail() { )} + {/* 底部输入栏 */} {isOpen ? ( - + + + + + setInputText(e.detail.value)} confirmType='send' @@ -218,15 +224,15 @@ export default function ConsultationDetail() { disabled={sending} /> - + ) : ( - - 会话已关闭 + + 会话已关闭 )} diff --git a/apps/miniprogram/src/services/consultation.ts b/apps/miniprogram/src/services/consultation.ts index 6557158..515a228 100644 --- a/apps/miniprogram/src/services/consultation.ts +++ b/apps/miniprogram/src/services/consultation.ts @@ -4,13 +4,18 @@ export interface ConsultationSession { id: string; patient_id: string; doctor_id: string | null; + patient_name?: string | null; + doctor_name?: string | null; consultation_type: string; status: string; subject?: string | null; last_message?: string | null; last_message_at: string | null; unread_count_patient: number; + unread_count_doctor?: number; created_at: string; + updated_at?: string; + version?: number; } export interface ConsultationMessage { diff --git a/crates/erp-health/src/handler/consultation_handler.rs b/crates/erp-health/src/handler/consultation_handler.rs index 3efcbed..a5eb788 100644 --- a/crates/erp-health/src/handler/consultation_handler.rs +++ b/crates/erp-health/src/handler/consultation_handler.rs @@ -191,7 +191,7 @@ where HealthState: FromRef, S: Clone + Send + Sync + 'static, { - require_permission(&ctx, "health.consultation.manage")?; + require_permission(&ctx, "health.consultation.list")?; // 从 JWT 身份推导 sender_role,不信任客户端输入 let is_doctor = crate::entity::doctor_profile::Entity::find() .filter(crate::entity::doctor_profile::Column::UserId.eq(ctx.user_id)) @@ -262,7 +262,7 @@ where HealthState: FromRef, S: Clone + Send + Sync + 'static, { - require_permission(&ctx, "health.consultation.manage")?; + require_permission(&ctx, "health.consultation.list")?; let is_doctor = crate::entity::doctor_profile::Entity::find() .filter(crate::entity::doctor_profile::Column::UserId.eq(ctx.user_id)) .filter(crate::entity::doctor_profile::Column::TenantId.eq(ctx.tenant_id))