diff --git a/apps/miniprogram/src/app.config.ts b/apps/miniprogram/src/app.config.ts index e27d769..9d1373a 100644 --- a/apps/miniprogram/src/app.config.ts +++ b/apps/miniprogram/src/app.config.ts @@ -38,7 +38,7 @@ export default defineAppConfig({ }, { root: 'pages/pkg-mall', - pages: ['exchange/index', 'orders/index', 'detail/index'], + pages: ['exchange/index', 'orders/index', 'detail/index', 'product/index'], }, { root: 'pages/pkg-profile', diff --git a/apps/miniprogram/src/components/ui/TabFilter/index.scss b/apps/miniprogram/src/components/ui/TabFilter/index.scss index d09ea79..ebfcf4f 100644 --- a/apps/miniprogram/src/components/ui/TabFilter/index.scss +++ b/apps/miniprogram/src/components/ui/TabFilter/index.scss @@ -28,22 +28,26 @@ } } - // Pill型 — 文章分类 + // Pill型 — 筛选标签(医生端 ActionInbox / FollowUpList) &--pill { flex-wrap: wrap; gap: var(--tk-gap-xs); .tab-filter__item { height: 32px; - padding: 0 var(--tk-gap-lg); - border-radius: $r-pill; - background: $surface-alt; + padding: 0 16px; + border-radius: $r-lg; + background: $card; + border: 1px solid $bd; display: flex; align-items: center; justify-content: center; + transition: all 0.2s; &--active { background: var(--tk-pri); + border-color: var(--tk-pri); + box-shadow: var(--tk-shadow-tab); .tab-filter__text { color: $white; diff --git a/apps/miniprogram/src/pages/index/index.scss b/apps/miniprogram/src/pages/index/index.scss index ace2c8a..81b43f7 100644 --- a/apps/miniprogram/src/pages/index/index.scss +++ b/apps/miniprogram/src/pages/index/index.scss @@ -298,7 +298,7 @@ } /* ═══════════════════════════════════════ - 访客首页 + 访客首页 — 对齐原型 docs/design/mp-14-guest-home.html ═══════════════════════════════════════ */ .guest-page { @@ -308,7 +308,7 @@ /* ─── 轮播图 ─── */ .guest-swiper { width: 100%; - height: 400px; + height: 200px; } .guest-slide { @@ -340,124 +340,162 @@ display: flex; flex-direction: column; justify-content: center; - padding: var(--tk-gap-2xl) var(--tk-gap-xl); + padding: 0 var(--tk-gap-xl); } .guest-slide-title { font-family: 'Georgia', 'Times New Roman', serif; - font-size: var(--tk-font-h1); + font-size: 24px; font-weight: 700; color: $white; display: block; - margin-bottom: var(--tk-gap-xs); + margin-bottom: 6px; } .guest-slide-desc { font-size: var(--tk-font-body-sm); - color: rgba(255, 255, 255, 0.85); + color: rgba(255, 255, 255, 0.8); display: block; } /* ─── 健康资讯 ─── */ .guest-section { - padding: var(--tk-gap-lg) var(--tk-gap-lg) 0; + padding: var(--tk-gap-lg) var(--tk-page-padding) 0; +} + +.guest-section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--tk-gap-md); } .guest-section-title { font-family: 'Georgia', 'Times New Roman', serif; - font-size: var(--tk-font-body); - font-weight: bold; + font-size: var(--tk-font-nav); + font-weight: 700; color: $tx; - display: block; - margin-bottom: var(--tk-gap-md); +} + +.guest-section-more { + font-size: var(--tk-font-cap); + color: $tx3; + + &:active { + opacity: var(--tk-touch-feedback-opacity); + } } .guest-articles { display: flex; flex-direction: column; - gap: var(--tk-gap-sm); + gap: 10px; } +// 文章卡片 — 左图标 + 右标题/日期 .guest-article-card { - overflow: hidden; + background: $card; + border-radius: $r; + padding: var(--tk-gap-md); + box-shadow: $shadow-sm; display: flex; + align-items: center; + gap: 14px; &:active { opacity: var(--tk-touch-feedback-opacity); } } -.guest-article-cover { - width: 100px; - height: 80px; +.guest-article-icon { + width: 64px; + height: 64px; + border-radius: $r-sm; + display: flex; + align-items: center; + justify-content: center; flex-shrink: 0; + + .guest-article-card--pri & { background: $pri-l; } + .guest-article-card--acc & { background: $acc-l; } + .guest-article-card--wrn & { background: $wrn-l; } +} + +.guest-article-icon-char { + font-size: 22px; + line-height: 1; + + .guest-article-card--pri & { color: $pri; } + .guest-article-card--acc & { color: $acc; } + .guest-article-card--wrn & { color: $wrn; } } .guest-article-body { - padding: var(--tk-gap-sm); flex: 1; min-width: 0; } .guest-article-title { - font-size: var(--tk-font-body-sm); + font-size: 15px; font-weight: 600; color: $tx; display: block; - margin-bottom: var(--tk-gap-2xs); + margin-bottom: 4px; + line-height: 1.4; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.guest-article-summary { - font-size: var(--tk-font-cap); - color: var(--tk-text-secondary); +.guest-article-date { + font-size: var(--tk-font-micro); + color: $tx3; display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } -.guest-empty { - padding: var(--tk-gap-2xl) 0; +/* ─── 底部注册引导卡片 ─── */ +.guest-cta-card { + margin: var(--tk-gap-lg) var(--tk-page-padding); + background: $card; + border-radius: $r; + padding: var(--tk-section-gap); + box-shadow: $shadow-sm; text-align: center; } -.guest-empty-text { - font-size: var(--tk-font-cap); - color: var(--tk-text-secondary); +.guest-cta-title { + font-family: 'Georgia', 'Times New Roman', serif; + font-size: var(--tk-font-body); + font-weight: 700; + color: $tx; + display: block; + margin-bottom: 6px; } -/* ─── 底部登录引导 ─── */ -.guest-login-prompt { - margin: var(--tk-gap-lg) var(--tk-gap-lg) 0; - display: flex; - align-items: center; - gap: var(--tk-gap-md); -} - -.guest-login-text { - flex: 1; +.guest-cta-desc { font-size: var(--tk-font-cap); color: $tx2; + display: block; + margin-bottom: var(--tk-gap-md); + line-height: 1.5; } -.guest-login-btn { - height: var(--tk-input-height); - padding: 0 var(--tk-card-padding-lg); +.guest-cta-btn { + height: 48px; + border-radius: 24px; background: var(--tk-pri); - border-radius: $r-pill; - @include flex-center; - flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + box-shadow: var(--tk-shadow-btn); &:active { opacity: var(--tk-touch-feedback-opacity); } } -.guest-login-btn-text { - font-size: var(--tk-font-h2); +.guest-cta-btn-text { + font-size: var(--tk-font-body); font-weight: 600; color: $white; } diff --git a/apps/miniprogram/src/pages/index/index.tsx b/apps/miniprogram/src/pages/index/index.tsx index 747df2a..676a424 100644 --- a/apps/miniprogram/src/pages/index/index.tsx +++ b/apps/miniprogram/src/pages/index/index.tsx @@ -85,6 +85,15 @@ function GuestHome({ modeClass }: { modeClass: string }) { const slides = banners.length > 0 ? banners : FALLBACK_SLIDES; + const ARTICLE_ICONS = ['♥', '◇', '✦']; + const ARTICLE_COLORS = ['pri', 'acc', 'wrn'] as const; + + const formatDate = (dateStr?: string) => { + if (!dateStr) return ''; + const d = new Date(dateStr); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; + }; + return ( + {/* 健康资讯 */} - 健康资讯 + + 健康资讯 + Taro.switchTab({ url: '/pages/health/index' })} + > + 查看全部 › + + + {articles.length > 0 ? ( - {articles.map((article) => ( - ( + safeNavigateTo(`/pages/article/detail/index?id=${article.id}`)} - activeFeedback="opacity" - padding="none" + className={`guest-article-card guest-article-card--${ARTICLE_COLORS[i % 3]}`} + onClick={() => safeNavigateTo(`/pages/article/detail/index?id=${article.id}`)} > - {article.cover_image && ( - - )} + + {ARTICLE_ICONS[i % 3]} + {article.title} - - {article.summary || '点击查看详情'} - + {formatDate(article.published_at)} - + ))} ) : ( - + + 健康数据管理 - 记录并追踪您的体征数据 + 记录并追踪您的体征数据 - - + + + - 智能预约排班 - 在线预约透析和治疗 + 积分商城 + 签到赚积分,好礼兑不停 - - + + + - AI 健康分析 - 个性化健康趋势解读 + 专业科普文章 + 权威健康知识推送 - + )} - - 登录后即可使用完整健康管理服务 - - 立即登录 + {/* 底部注册引导 */} + + 加入我们 + 注册后即可使用签到、积分商城等全部功能 + + 注册 / 登录 - + ); } diff --git a/apps/miniprogram/src/pages/login/index.tsx b/apps/miniprogram/src/pages/login/index.tsx index a4b9b81..4d09a2e 100644 --- a/apps/miniprogram/src/pages/login/index.tsx +++ b/apps/miniprogram/src/pages/login/index.tsx @@ -23,7 +23,22 @@ export default function Login() { const navigateAfterLogin = () => { if (isMedicalStaff()) { - Taro.reLaunch({ url: '/pages/pkg-doctor-core/index' }); + // 使用 redirectTo 替代 reLaunch 避免分包加载超时 + // redirectTo 只替换当前页面,不销毁整个页栈,分包预加载不会被中断 + Taro.redirectTo({ + url: '/pages/pkg-doctor-core/index', + fail: () => { + // fallback: 先跳首页再 redirectTo + Taro.switchTab({ + url: '/pages/index/index', + success: () => { + setTimeout(() => { + Taro.navigateTo({ url: '/pages/pkg-doctor-core/index' }); + }, 500); + }, + }); + }, + }); } else { Taro.switchTab({ url: '/pages/index/index' }); } diff --git a/apps/miniprogram/src/pages/mall/index.scss b/apps/miniprogram/src/pages/mall/index.scss index b40599e..ba95e76 100644 --- a/apps/miniprogram/src/pages/mall/index.scss +++ b/apps/miniprogram/src/pages/mall/index.scss @@ -1,40 +1,68 @@ @import '../../styles/variables.scss'; @import '../../styles/mixins.scss'; +// 积分商城 — 对齐原型 docs/design/mp-05-mall.html + .mall-page { padding-bottom: calc(var(--tk-tabbar-space) + env(safe-area-inset-bottom)); } -/* ─── 积分余额卡片 ─── */ +/* ─── 积分卡片(渐变背景) ─── */ .mall-header { background: linear-gradient(135deg, var(--tk-pri) 0%, var(--tk-pri-d) 100%); - padding: var(--tk-gap-2xl) var(--tk-gap-xl) var(--tk-gap-xl); + padding: var(--tk-gap-xl) var(--tk-page-padding) var(--tk-gap-xl); + position: relative; + overflow: hidden; + + // 装饰圆 + &::before { + content: ''; + position: absolute; + top: -20px; + right: -20px; + width: 100px; + height: 100px; + border-radius: 50px; + background: rgba(255, 255, 255, 0.08); + } + &::after { + content: ''; + position: absolute; + bottom: -30px; + right: 40px; + width: 80px; + height: 80px; + border-radius: 40px; + background: rgba(255, 255, 255, 0.05); + } } .points-card { - background: rgba(255, 255, 255, 0.15); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: $r-lg; - padding: var(--tk-gap-xl); + position: relative; + z-index: 1; } .points-top { display: flex; justify-content: space-between; align-items: center; - margin-bottom: var(--tk-gap-md); + margin-bottom: var(--tk-gap-sm); } .points-label { - font-size: var(--tk-font-h1); - color: rgba(255, 255, 255, 0.85); + font-size: var(--tk-font-cap); + color: rgba(255, 255, 255, 0.7); + letter-spacing: 1px; } .checkin-btn { - background: rgba(255, 255, 255, 0.25); + background: rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.4); - padding: var(--tk-gap-sm) var(--tk-card-padding-lg); + padding: 6px 14px; border-radius: $r-pill; + display: flex; + align-items: center; + gap: 4px; transition: all 0.2s; &:active { @@ -48,9 +76,9 @@ } .checkin-btn-text { - font-size: var(--tk-font-h2); + font-size: var(--tk-font-cap); color: $white; - font-weight: 600; + font-weight: 500; } .checkin-btn.checked .checkin-btn-text { @@ -59,8 +87,8 @@ .points-balance { @include serif-number; - font-size: var(--tk-font-display); - font-weight: bold; + font-size: 42px; + font-weight: 700; color: $white; display: block; margin-bottom: var(--tk-gap-xs); @@ -69,44 +97,105 @@ } .points-streak { - font-size: var(--tk-font-body); + font-size: var(--tk-font-cap); color: rgba(255, 255, 255, 0.7); display: block; } -/* ─── 商品类型切换 ─── */ +/* ─── 快捷操作 ─── */ +.mall-actions { + display: flex; + justify-content: space-around; + padding: var(--tk-section-gap) var(--tk-page-padding); +} + +.mall-action { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + + &:active { + opacity: var(--tk-touch-feedback-opacity); + } +} + +.mall-action-icon { + width: 52px; + height: 52px; + border-radius: 26px; + display: flex; + align-items: center; + justify-content: center; + + &--checkin { + background: $acc; + box-shadow: 0 4px 12px rgba(91, 122, 94, 0.3); + } + &--task { + background: $pri; + box-shadow: 0 4px 12px rgba(196, 98, 58, 0.3); + } + &--history { + background: $wrn; + box-shadow: 0 4px 12px rgba(196, 135, 58, 0.3); + } +} + +.mall-action-icon-text { + font-size: 22px; + color: $white; + line-height: 1; +} + +.mall-action-label { + font-size: var(--tk-font-micro); + color: $tx2; + font-weight: 500; +} + +/* ─── 分类标签(Pill) ─── */ .type-tabs { display: flex; - padding: var(--tk-section-gap) var(--tk-page-padding) 0; + gap: 10px; + padding: 0 var(--tk-page-padding) var(--tk-section-gap); + overflow-x: auto; + + &::-webkit-scrollbar { + display: none; + } } .type-tab { - flex: 1; - text-align: center; - padding: var(--tk-gap-md) 0; - position: relative; - min-height: 48px; + padding: 7px 18px; + border-radius: $r-pill; + font-size: var(--tk-font-body-sm); + font-weight: 400; + background: $surface-alt; + color: $tx2; + white-space: nowrap; + flex-shrink: 0; + transition: all 0.2s; - &.active::after { - content: ''; - position: absolute; - bottom: 0; - left: 50%; - transform: translateX(-50%); - width: 48px; - height: 4px; + &:active { + opacity: var(--tk-touch-feedback-opacity); + } + + &.active { background: var(--tk-pri); - border-radius: $r-xs; + color: $white; + font-weight: 600; + box-shadow: var(--tk-shadow-tab); } } .type-tab-text { - font-size: var(--tk-font-body-lg); - color: $tx2; + font-size: inherit; + color: inherit; + font-weight: inherit; &.active { - color: var(--tk-pri); - font-weight: 600; + color: inherit; } } @@ -114,12 +203,15 @@ .product-grid { display: grid; grid-template-columns: 1fr 1fr; - gap: var(--tk-gap-md); - padding: var(--tk-section-gap) var(--tk-page-padding); + gap: var(--tk-gap-sm); + padding: 0 var(--tk-page-padding) var(--tk-gap-lg); } .product-card { + background: $card; + border-radius: $r-sm; overflow: hidden; + box-shadow: $shadow-sm; &:active { opacity: var(--tk-touch-feedback-opacity); @@ -128,19 +220,20 @@ .product-image { width: 100%; - height: 200px; + aspect-ratio: 1; @include flex-center; + position: relative; - &.type-physical { background: var(--tk-pri-l); } + &.type-physical { background: $pri-l; } &.type-service { background: $acc-l; } &.type-privilege { background: $wrn-l; } } .product-image-char { font-family: 'Georgia', 'Times New Roman', serif; - font-size: var(--tk-font-hero); - font-weight: bold; - color: var(--tk-pri); + font-size: 32px; + font-weight: 700; + color: $pri; line-height: 1; .type-service & { color: $acc; } @@ -148,57 +241,83 @@ } .product-info { - padding: var(--tk-section-gap); + padding: 10px var(--tk-gap-sm) 14px; } .product-name { - font-size: var(--tk-font-h1); + font-size: var(--tk-font-body-sm); font-weight: 600; color: $tx; - display: block; - margin-bottom: var(--tk-gap-sm); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + line-height: 1.4; + height: 40px; + margin-bottom: 8px; } .product-bottom { display: flex; - justify-content: space-between; - align-items: center; + align-items: baseline; + gap: 6px; } .product-points { display: flex; - align-items: center; - gap: var(--tk-gap-2xs); + align-items: baseline; + gap: 2px; } .product-points-char { - font-family: 'Georgia', 'Times New Roman', serif; - font-size: var(--tk-font-body); - font-weight: bold; - color: $wrn; + @include serif-number; + font-size: 18px; + font-weight: 700; + color: $pri; } .product-points-value { - @include serif-number; - font-size: var(--tk-font-body-lg); - font-weight: bold; - color: $wrn; + font-size: var(--tk-font-micro); + color: $pri; + font-weight: 500; +} + +.product-price { + font-size: var(--tk-font-micro); + color: $tx3; + text-decoration: line-through; } .product-stock { - font-size: var(--tk-font-body); - padding: 2px var(--tk-gap-sm); - border-radius: $r-sm; + font-size: var(--tk-font-micro); + padding: 2px 6px; + border-radius: $r-xs; &.out { - @include tag($bd-l, $tx3); + background: $bd-l; + color: $tx3; } &.low { - @include tag($dan-l, $dan); + background: $dan-l; + color: $dan; } } +.product-tag { + position: absolute; + top: 8px; + left: 8px; + font-size: 10px; + font-weight: 600; + padding: 2px 8px; + border-radius: 6px; + color: $white; + + &--hot { + background: $dan; + } + &--new { + background: $acc; + } +} diff --git a/apps/miniprogram/src/pages/mall/index.tsx b/apps/miniprogram/src/pages/mall/index.tsx index 41202a6..fc0fb45 100644 --- a/apps/miniprogram/src/pages/mall/index.tsx +++ b/apps/miniprogram/src/pages/mall/index.tsx @@ -12,14 +12,13 @@ import ErrorState from '../../components/ErrorState'; import EmptyState from '../../components/EmptyState'; import { useElderClass } from '../../hooks/useElderClass'; import PageShell from '@/components/ui/PageShell'; -import ContentCard from '@/components/ui/ContentCard'; import './index.scss'; const PRODUCT_TYPE_TABS = [ { key: '', label: '全部' }, - { key: 'physical', label: '实物', char: '物' }, - { key: 'service', label: '服务券', char: '券' }, - { key: 'privilege', label: '权益', char: '权' }, + { key: 'physical', label: '实物' }, + { key: 'service', label: '服务券' }, + { key: 'privilege', label: '权益' }, ]; const TYPE_BG: Record = { @@ -77,7 +76,6 @@ export default function Mall() { async (type?: string) => { const t = type !== undefined ? type : productType; if (!currentPatient) { - // 先尝试从服务端加载患者列表 await loadPatients(); const updated = useAuthStore.getState().currentPatient; if (!updated) { @@ -133,7 +131,7 @@ export default function Mall() { Taro.showToast({ title: '已兑完', icon: 'none' }); return; } - safeNavigateTo(`/pages/pkg-mall/exchange/index?product_id=${item.id}`); + safeNavigateTo(`/pages/pkg-mall/product/index?product_id=${item.id}`); }; const balance = account?.balance ?? 0; @@ -158,7 +156,7 @@ export default function Mall() { - 当前积分 + 我的积分 + {/* 快捷操作 */} + + + + + + 签到打卡 + + + + + + 积分任务 + + safeNavigateTo('/pages/pkg-mall/orders/index')}> + + + + 兑换记录 + + + {/* 商品类型切换 */} {PRODUCT_TYPE_TABS.map((tab) => ( @@ -200,7 +220,11 @@ export default function Mall() { ) : ( {products.map((item) => ( - handleProductClick(item)} activeFeedback="opacity" padding="none"> + handleProductClick(item)} + > {item.product_type === 'physical' ? '物' : item.product_type === 'service' ? '券' : '权'} @@ -210,8 +234,8 @@ export default function Mall() { {item.name} - P - {item.points_cost} + {item.points_cost} + 积分 {item.stock <= 0 ? ( 已兑完 @@ -220,7 +244,7 @@ export default function Mall() { ) : null} - + ))} {loading && } {!loading && products.length >= total && total > 0 && ( diff --git a/apps/miniprogram/src/pages/pkg-doctor-core/action-inbox/index.scss b/apps/miniprogram/src/pages/pkg-doctor-core/action-inbox/index.scss index 870e121..9b5d3d9 100644 --- a/apps/miniprogram/src/pages/pkg-doctor-core/action-inbox/index.scss +++ b/apps/miniprogram/src/pages/pkg-doctor-core/action-inbox/index.scss @@ -1,64 +1,102 @@ @import '../../../styles/variables.scss'; @import '../../../styles/mixins.scss'; -// PageShell 已接管:min-height, background -// ContentCard 已接管:inbox-card 背景/圆角/阴影/触摸反馈 +.inbox-page { + height: 100vh; + background: $bg; + display: flex; + flex-direction: column; -.inbox-list { - height: calc(100vh - 50px); - padding: var(--tk-gap-sm); + &__filter { + padding: 12px 20px; + } + + &__list { + flex: 1; + padding: 0 20px 20px; + } } .inbox-card { - margin-bottom: var(--tk-gap-sm); + margin-bottom: 10px !important; - .inbox-card-header { + &__row { display: flex; align-items: center; - gap: var(--tk-gap-xs); - margin-bottom: var(--tk-gap-2xs); + gap: 12px; } - .inbox-type-tag { - color: $card; - font-size: var(--tk-font-micro); - padding: 2px var(--tk-gap-2xs); - border-radius: $r-xs; - flex-shrink: 0; - - &--ai { - background: var(--tk-pri); - } - - &--alert { - background: $dan; - } - - &--followup { - background: $acc; - } - - &--anomaly { - background: $wrn; - } - - &--default { - background: $tx3; - } + &__body { + flex: 1; + min-width: 0; } - .inbox-card-title { - font-size: var(--tk-font-cap); - font-weight: 500; + &__name-row { + display: flex; + align-items: center; + gap: 6px; + } + + &__patient { + font-size: 15px; + font-weight: 600; color: $tx; } - .inbox-card-desc { - font-size: var(--tk-font-micro); + &__urgent { + display: inline-block; + padding: 1px 6px; + border-radius: 4px; + font-size: 10px; + font-weight: 600; + color: $card; + background: $dan; + line-height: 1.6; + } + + &__desc { + font-size: 13px; + color: $tx2; + margin-top: 3px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: block; + } + + &__time { + font-size: 11px; + color: $tx3; + flex-shrink: 0; + text-align: right; + min-width: 48px; + } + + &__arrow { + flex-shrink: 0; + font-size: 20px; color: $tx3; } } +// ── 类型标签(对齐原型:color/bg 配对,非白字实底)── +.inbox-type-tag { + display: inline-block; + padding: 2px 8px; + border-radius: 6px; + font-size: 11px; + font-weight: 600; + line-height: 1.6; + flex-shrink: 0; + + &--pri { color: $doc-pri; background: $doc-pri-l; } + &--dan { color: $dan; background: $dan-l; } + &--acc { color: $acc; background: $acc-l; } + &--wrn { color: $wrn; background: $wrn-l; } + &--default { color: $tx3; background: $surface-alt; } +} + +// ── 半屏详情弹窗 ── .half-screen-dialog { position: fixed; bottom: 0; @@ -75,37 +113,27 @@ display: flex; justify-content: space-between; align-items: center; - padding: var(--tk-gap-md) var(--tk-section-gap); + padding: 16px 20px; border-bottom: 1px solid $bd-l; - .dialog-title { - font-size: var(--tk-font-body-sm); - font-weight: 600; - color: $tx; - } - - .dialog-close { - font-size: var(--tk-font-cap); - color: $tx3; - } + .dialog-title { font-size: 14px; font-weight: 600; color: $tx; } + .dialog-close { font-size: 13px; color: $tx3; } } - .dialog-body { - padding: var(--tk-gap-md) var(--tk-section-gap); - } + .dialog-body { padding: 16px 20px; } .dialog-patient { - font-size: var(--tk-font-cap); + font-size: 13px; color: $tx2; display: block; - margin-bottom: var(--tk-gap-sm); + margin-bottom: 12px; } .thread-item { display: flex; align-items: flex-start; - gap: var(--tk-gap-sm); - padding: var(--tk-gap-2xs) 0; + gap: 12px; + padding: 4px 0; } .thread-dot { @@ -122,30 +150,22 @@ } .thread-content { - .thread-label { - font-size: var(--tk-font-cap); - color: $tx; - display: block; - } - - .thread-time { - font-size: var(--tk-font-micro); - color: $tx3; - } + .thread-label { font-size: 13px; color: $tx; display: block; } + .thread-time { font-size: 11px; color: $tx3; } } .dialog-actions { display: flex; - gap: var(--tk-gap-xs); - padding: var(--tk-gap-sm) var(--tk-section-gap) var(--tk-section-gap); + gap: 8px; + padding: 12px 20px 20px; border-top: 1px solid $bd-l; .action-btn { flex: 1; text-align: center; - padding: var(--tk-gap-sm); + padding: 12px; border-radius: $r-sm; - font-size: var(--tk-font-cap); + font-size: 13px; font-weight: 500; &.primary { background: var(--tk-pri); color: $card; } diff --git a/apps/miniprogram/src/pages/pkg-doctor-core/action-inbox/index.tsx b/apps/miniprogram/src/pages/pkg-doctor-core/action-inbox/index.tsx index 849a67a..c9c6ed4 100644 --- a/apps/miniprogram/src/pages/pkg-doctor-core/action-inbox/index.tsx +++ b/apps/miniprogram/src/pages/pkg-doctor-core/action-inbox/index.tsx @@ -2,7 +2,6 @@ import React, { useState, useCallback, useRef } from 'react'; import { View, Text, ScrollView } from '@tarojs/components'; import Taro from '@tarojs/taro'; import { usePageData } from '@/hooks/usePageData'; -import { api } from '@/services/request'; import { listActionItems, getActionThread, @@ -12,47 +11,54 @@ import { import Loading from '@/components/Loading'; import ErrorState from '@/components/ErrorState'; import EmptyState from '@/components/EmptyState'; -import SegmentTabs from '@/components/SegmentTabs'; -import PageShell from '@/components/ui/PageShell'; +import PageHeader from '@/components/patterns/PageHeader'; +import TabFilter from '@/components/ui/TabFilter'; import ContentCard from '@/components/ui/ContentCard'; import { useDoctorClass } from '@/hooks/useDoctorClass'; import './index.scss'; -const TYPE_LABEL: Record = { - ai_suggestion: 'AI建议', - alert: '告警', - followup: '随访', - data_anomaly: '异常', +const TYPE_CONFIG: Record = { + data_anomaly: { label: '异常', colorCls: 'inbox-type-tag--dan' }, + followup: { label: '随访', colorCls: 'inbox-type-tag--acc' }, + ai_suggestion: { label: '咨询', colorCls: 'inbox-type-tag--pri' }, + alert: { label: '告警', colorCls: 'inbox-type-tag--dan' }, }; -const TYPE_CLS: Record = { - ai_suggestion: 'inbox-type-tag--ai', - alert: 'inbox-type-tag--alert', - followup: 'inbox-type-tag--followup', - data_anomaly: 'inbox-type-tag--anomaly', +const FILTER_TABS = ['全部', '异常', '随访', '咨询']; + +const FILTER_MAP: Record = { + 0: undefined, + 1: 'data_anomaly', + 2: 'followup', + 3: 'ai_suggestion', }; -const STATUS_TABS = [ - { key: '', label: '全部' }, - { key: 'pending', label: '待处理' }, - { key: 'in_progress', label: '进行中' }, - { key: 'completed', label: '已完成' }, -]; +function formatTimeAgo(dateStr: string): string { + const now = Date.now(); + const then = new Date(dateStr).getTime(); + const diff = now - then; + const minutes = Math.floor(diff / 60000); + if (minutes < 60) return `${minutes}分钟前`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}小时前`; + const days = Math.floor(hours / 24); + if (days === 1) return '昨天'; + if (days < 7) return `${days}天前`; + return new Date(dateStr).toLocaleDateString('zh-CN'); +} export default function ActionInboxPage() { const modeClass = useDoctorClass(); const [items, setItems] = useState([]); - const [total, setTotal] = useState(0); - const [_page, setPage] = useState(1); const [loading, setLoading] = useState(false); const [error, setError] = useState(false); - const [activeTab, setActiveTab] = useState(''); + const [activeFilter, setActiveFilter] = useState(0); const [threadData, setThreadData] = useState(null); const [showDetail, setShowDetail] = useState(false); const loadingRef = useRef(false); const fetchItems = useCallback( - async (pageNum: number, status: string, isRefresh = false) => { + async (pageNum: number, typeFilter: string | undefined, isRefresh = false) => { if (loadingRef.current) return; loadingRef.current = true; setLoading(true); @@ -61,7 +67,7 @@ export default function ActionInboxPage() { const resp = await listActionItems({ page: pageNum, page_size: 20, - status: status || undefined, + type: typeFilter, }); const list = resp.data || []; if (isRefresh) { @@ -69,8 +75,6 @@ export default function ActionInboxPage() { } else { setItems((prev) => [...prev, ...list]); } - setTotal(resp.total); - setPage(pageNum); } catch { setError(true); Taro.showToast({ title: '加载失败', icon: 'none' }); @@ -84,15 +88,14 @@ export default function ActionInboxPage() { usePageData( useCallback(async () => { - Taro.setNavigationBarTitle({ title: '待办事项' }); - await fetchItems(1, activeTab, true); - }, [fetchItems, activeTab]), + await fetchItems(1, FILTER_MAP[activeFilter], true); + }, [fetchItems, activeFilter]), { throttleMs: 10000, enablePullDown: true }, ); - const handleTabChange = (key: string) => { - setActiveTab(key); - fetchItems(1, key, true); + const handleFilterChange = (index: number) => { + setActiveFilter(index); + fetchItems(1, FILTER_MAP[index], true); }; const handleItemClick = async (item: ActionItem) => { @@ -111,90 +114,98 @@ export default function ActionInboxPage() { }) => { if (!action.api_endpoint || !threadData) return; try { + const { api } = await import('@/services/request'); await api.post(action.api_endpoint, { action: action.key }); Taro.showToast({ title: '操作成功', icon: 'success' }); setShowDetail(false); - fetchItems(1, activeTab, true); + fetchItems(1, FILTER_MAP[activeFilter], true); } catch { Taro.showToast({ title: '操作失败', icon: 'none' }); } }; + const getTypeConfig = (type: string) => + TYPE_CONFIG[type] || { label: '未知', colorCls: 'inbox-type-tag--default' }; + return ( - - + + + + + + {error ? ( - fetchItems(1, activeTab, true)} /> + fetchItems(1, FILTER_MAP[activeFilter], true)} /> ) : items.length === 0 && !loading ? ( - + ) : ( - - {items.map((item) => ( - handleItemClick(item)} - > - - - {TYPE_LABEL[item.action_type] || '未知'} - - {item.title} - - - {item.patient_name} ·{' '} - {new Date(item.created_at).toLocaleDateString('zh-CN')} - - - ))} + + {items.map((item) => { + const cfg = getTypeConfig(item.action_type); + const isUrgent = item.priority === 'urgent' || item.priority === 'high'; + return ( + handleItemClick(item)} + > + + {/* 类型标签 — 原型:color/bg 配对 */} + + {cfg.label} + + {/* 内容区 */} + + + {item.patient_name || '未知患者'} + {isUrgent && 紧急} + + {item.title} + + {/* 时间 */} + {formatTimeAgo(item.created_at)} + {/* 箭头 */} + + + + ); + })} {loading && } - {!loading && items.length >= total && total > 0 && ( - - )} )} {showDetail && threadData && ( - - {threadData.action_item.title} - - setShowDetail(false)} - > - 收起 - + {threadData.action_item.title} + setShowDetail(false)}>收起 {threadData.action_item.patient_name} ·{' '} - {threadData.action_item.priority === 'urgent' - ? '紧急' - : threadData.action_item.priority === 'high' - ? '高' - : '中'} + {threadData.action_item.priority === 'urgent' ? '紧急' + : threadData.action_item.priority === 'high' ? '高' : '中'} - - {threadData.thread.map((evt, idx) => ( - - - - {evt.label} - {evt.timestamp && ( - - {new Date(evt.timestamp).toLocaleDateString('zh-CN')} - - )} - + {threadData.thread.map((evt, idx) => ( + + + + {evt.label} + {evt.timestamp && ( + + {new Date(evt.timestamp).toLocaleDateString('zh-CN')} + + )} - ))} - + + ))} {threadData.available_actions.length > 0 && ( @@ -211,6 +222,6 @@ export default function ActionInboxPage() { )} )} - + ); } diff --git a/apps/miniprogram/src/pages/pkg-doctor-core/consultation/index.scss b/apps/miniprogram/src/pages/pkg-doctor-core/consultation/index.scss index 8343834..009aa3a 100644 --- a/apps/miniprogram/src/pages/pkg-doctor-core/consultation/index.scss +++ b/apps/miniprogram/src/pages/pkg-doctor-core/consultation/index.scss @@ -1,76 +1,88 @@ @import '../../../styles/variables.scss'; @import '../../../styles/mixins.scss'; -// PageShell 已接管:min-height, background, padding -// SearchSection 已接管:标签筛选栏 -// ContentCard 已接管:session-card 背景/圆角/阴影/触摸反馈 -// StatusTag 已接管:会话状态标签 -// PaginationBar 已接管:分页控件 - -.session-list { +.consult-page { + height: 100vh; + background: $bg; display: flex; flex-direction: column; - gap: var(--tk-gap-md); + + &__tabs { + padding: 12px 20px 0; + } + + &__list { + flex: 1; + padding: 12px 20px 20px; + } } -.session-card__top { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: var(--tk-gap-sm); -} +.consult-card { + margin-bottom: 10px !important; -.session-card__subject { - font-size: var(--tk-font-body-lg); - font-weight: 600; - color: $tx; - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - margin-right: var(--tk-gap-md); -} + &__row { + display: flex; + align-items: center; + gap: 12px; + } -.session-card__info { - display: flex; - align-items: center; - gap: var(--tk-gap-md); - margin-bottom: var(--tk-gap-xs); -} + &__body { + flex: 1; + min-width: 0; + } -.session-card__type { - @include tag(var(--tk-pri-l), var(--tk-pri)); -} + &__top { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 4px; + } -.session-card__time { - font-size: var(--tk-font-h2); - color: $tx3; -} + &__name { + font-size: 15px; + font-weight: 600; + color: $tx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } -.session-card__preview { - font-size: var(--tk-font-h1); - color: $tx2; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - display: block; -} + &__time { + font-size: 12px; + color: $tx3; + flex-shrink: 0; + margin-left: 8px; + } -.session-card__badge { - position: absolute; - top: var(--tk-section-gap); - right: var(--tk-section-gap); - min-width: 36px; - height: 36px; - background: $dan; - border-radius: $r-pill; - @include flex-center; - padding: 0 8px; -} + &__msg { + font-size: 13px; + color: $tx2; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: block; + margin-top: 4px; + } -.session-card__badge-text { - @include serif-number; - font-size: var(--tk-font-body); - color: $card; - font-weight: 600; + &__badge { + min-width: 20px; + height: 20px; + background: $dan; + border-radius: $r-pill; + @include flex-center; + padding: 0 6px; + flex-shrink: 0; + } + + &__badge-text { + font-size: 11px; + color: $card; + font-weight: 700; + } + + &__arrow { + flex-shrink: 0; + font-size: 20px; + color: $tx3; + } } diff --git a/apps/miniprogram/src/pages/pkg-doctor-core/consultation/index.tsx b/apps/miniprogram/src/pages/pkg-doctor-core/consultation/index.tsx index 0904411..1e04bf1 100644 --- a/apps/miniprogram/src/pages/pkg-doctor-core/consultation/index.tsx +++ b/apps/miniprogram/src/pages/pkg-doctor-core/consultation/index.tsx @@ -1,41 +1,33 @@ import { useState, useEffect, useCallback, useRef } from 'react'; -import { View, Text } from '@tarojs/components'; +import { View, Text, ScrollView } from '@tarojs/components'; import Taro from '@tarojs/taro'; import { usePageData } from '@/hooks/usePageData'; import { listSessions, type ConsultationSession } from '@/services/doctor/consultation'; -import PageShell from '@/components/ui/PageShell'; +import PageHeader from '@/components/patterns/PageHeader'; +import SegmentTabs from '@/components/SegmentTabs'; import ContentCard from '@/components/ui/ContentCard'; -import StatusTag from '@/components/ui/StatusTag'; +import AvatarCircle from '@/components/ui/AvatarCircle'; import LoadingCard from '@/components/ui/LoadingCard'; -import PaginationBar from '@/components/patterns/PaginationBar'; -import SearchSection from '@/components/patterns/SearchSection'; import ErrorState from '@/components/ErrorState'; import EmptyState from '@/components/EmptyState'; import { useDoctorClass } from '@/hooks/useDoctorClass'; -import { formatDateTime } from '@/utils/date'; import { safeNavigateTo } from '@/utils/navigate'; +import { formatDateTime } from '@/utils/date'; import './index.scss'; -const TABS = [ - { key: '', label: '全部' }, +const STATUS_TABS = [ { key: 'active', label: '进行中' }, - { key: 'waiting', label: '等待中' }, - { key: 'closed', label: '已关闭' }, + { key: 'closed', label: '已结束' }, ]; -const STATUS_COLOR_MAP: Record = { - active: 'success', - waiting: 'warning', - closed: 'default', -}; +const AVATAR_COLORS: Array<'pri' | 'acc' | 'wrn' | 'dan'> = ['pri', 'acc', 'wrn', 'dan']; export default function ConsultationList() { const modeClass = useDoctorClass(); const [sessions, setSessions] = useState([]); - const [activeTab, setActiveTab] = useState(''); + const [activeTab, setActiveTab] = useState('active'); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); - const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const mountedRef = useRef(false); @@ -49,7 +41,6 @@ export default function ConsultationList() { status: activeTab || undefined, }); setSessions(res.data || []); - setTotal(res.total || 0); } catch { setError(true); Taro.showToast({ title: '加载失败', icon: 'none' }); @@ -72,6 +63,8 @@ export default function ConsultationList() { setPage(1); }; + const getAvatarColor = (index: number) => AVATAR_COLORS[index % AVATAR_COLORS.length]; + const formatTime = (dateStr?: string | null) => { if (!dateStr) return ''; return formatDateTime(dateStr); @@ -81,57 +74,57 @@ export default function ConsultationList() { if (error) return ; return ( - - {}} - filters={TABS} - activeFilter={activeTab} - onFilterChange={handleTabChange} - /> + + + + + + {sessions.length === 0 ? ( ) : ( - - {sessions.map((s) => ( + + {sessions.map((s, idx) => ( safeNavigateTo(`/pages/pkg-doctor-core/consultation/detail/index?id=${s.id}`)} > - - {s.subject || '在线咨询'} - + - - - - {s.consultation_type === 'text' ? '图文' : s.consultation_type === 'video' ? '视频' : '咨询'} - - {formatTime(s.last_message_at)} - - {s.last_message && ( - {s.last_message} - )} - {(s.unread_count_doctor ?? 0) > 0 && ( - - {s.unread_count_doctor} + + + {s.patient_name || '未知患者'} + {formatTime(s.last_message_at)} + + + {s.last_message || (s.consultation_type === 'text' ? '图文咨询' : '视频咨询')} + - )} + {(s.unread_count_doctor ?? 0) > 0 && ( + + {s.unread_count_doctor} + + )} + {!((s.unread_count_doctor ?? 0) > 0) && ( + + )} + ))} - + )} - - - + ); } diff --git a/apps/miniprogram/src/pages/pkg-doctor-core/followup/index.scss b/apps/miniprogram/src/pages/pkg-doctor-core/followup/index.scss index 6f8f764..fad9ded 100644 --- a/apps/miniprogram/src/pages/pkg-doctor-core/followup/index.scss +++ b/apps/miniprogram/src/pages/pkg-doctor-core/followup/index.scss @@ -1,51 +1,90 @@ @import '../../../styles/variables.scss'; +@import '../../../styles/mixins.scss'; -// PageShell 已接管:min-height, background, padding -// SearchSection 已接管:标签筛选栏 -// ContentCard 已接管:task-card 背景/圆角/阴影/触摸反馈 -// StatusTag 已接管:任务状态标签 +.followup-page { + height: 100vh; + background: $bg; + display: flex; + flex-direction: column; -.task-count { - margin-bottom: var(--tk-gap-md); + &__filter { + padding: 12px 20px; + } - text { - font-size: var(--tk-font-h2); - color: $tx3; + &__list { + flex: 1; + padding: 0 20px 20px; } } -.task-list { - display: flex; - flex-direction: column; - gap: var(--tk-gap-md); -} +.followup-card { + margin-bottom: 10px !important; -.task-card__header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: var(--tk-gap-sm); -} + &__header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 8px; + } -.task-card__type { - font-size: var(--tk-font-body-lg); - font-weight: 600; - color: $tx; -} + &__top-left { + flex: 1; + min-width: 0; + } -.task-card__patient { - font-size: var(--tk-font-h1); - color: $tx2; - display: block; - margin-bottom: var(--tk-gap-xs); -} + &__name-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; + } -.task-card__footer { - display: flex; - justify-content: space-between; -} + &__patient { + font-size: 16px; + font-weight: 600; + color: $tx; + } -.task-card__date { - font-size: var(--tk-font-h2); - color: $tx3; + &__status-tag { + display: inline-block; + padding: 2px 8px; + border-radius: 6px; + font-size: 11px; + font-weight: 600; + line-height: 1.6; + } + + &__type { + font-size: 13px; + color: $tx3; + display: block; + } + + &__date { + font-size: 13px; + color: $tx2; + font-weight: 500; + flex-shrink: 0; + margin-left: 12px; + } + + &__data-row { + display: flex; + align-items: center; + justify-content: space-between; + background: $bg; + border-radius: $r-sm; + padding: 10px 12px; + } + + &__data-label { + font-size: 12px; + color: $tx3; + } + + &__data-value { + font-size: 14px; + font-weight: 600; + color: $tx; + } } diff --git a/apps/miniprogram/src/pages/pkg-doctor-core/followup/index.tsx b/apps/miniprogram/src/pages/pkg-doctor-core/followup/index.tsx index 2af621c..bf63322 100644 --- a/apps/miniprogram/src/pages/pkg-doctor-core/followup/index.tsx +++ b/apps/miniprogram/src/pages/pkg-doctor-core/followup/index.tsx @@ -1,43 +1,40 @@ import { useState, useEffect, useCallback, useRef } from 'react'; -import { View, Text } from '@tarojs/components'; -import Taro, { useRouter } from '@tarojs/taro'; +import { View, Text, ScrollView } from '@tarojs/components'; +import Taro from '@tarojs/taro'; import { safeNavigateTo } from '@/utils/navigate'; import { usePageData } from '@/hooks/usePageData'; import { listFollowUpTasks, type FollowUpTask } from '@/services/doctor/followup'; -import PageShell from '@/components/ui/PageShell'; +import PageHeader from '@/components/patterns/PageHeader'; +import TabFilter from '@/components/ui/TabFilter'; import ContentCard from '@/components/ui/ContentCard'; -import StatusTag from '@/components/ui/StatusTag'; import LoadingCard from '@/components/ui/LoadingCard'; -import SearchSection from '@/components/patterns/SearchSection'; import ErrorState from '@/components/ErrorState'; import EmptyState from '@/components/EmptyState'; import { useDoctorClass } from '@/hooks/useDoctorClass'; import './index.scss'; -const TABS = [ - { key: '', label: '全部' }, - { key: 'pending', label: '待处理' }, - { key: 'in_progress', label: '进行中' }, - { key: 'completed', label: '已完成' }, - { key: 'overdue', label: '已逾期' }, -]; +const FILTER_TABS = ['待随访', '已完成', '已过期']; -const STATUS_COLOR_MAP: Record = { - pending: 'warning', - in_progress: 'info', - completed: 'success', - overdue: 'error', +const FILTER_MAP: Record = { + 0: 'pending', + 1: 'completed', + 2: 'overdue', +}; + +const STATUS_STYLE: Record = { + pending: { color: '#3A6B8C', bg: '#D4E5F0' }, + in_progress: { color: '#3A6B8C', bg: '#D4E5F0' }, + completed: { color: '#5B7A5E', bg: '#E8F0E8' }, + overdue: { color: '#B54A4A', bg: '#FDEAEA' }, }; export default function FollowUpList() { - const router = useRouter(); - const patientId = router.params.patientId || ''; const modeClass = useDoctorClass(); const [tasks, setTasks] = useState([]); - const [activeTab, setActiveTab] = useState(''); + const [activeFilter, setActiveFilter] = useState(0); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); - const [total, setTotal] = useState(0); + const [, setTotal] = useState(0); const mountedRef = useRef(false); const loadTasks = useCallback(async () => { @@ -47,8 +44,7 @@ export default function FollowUpList() { const res = await listFollowUpTasks({ page: 1, page_size: 50, - status: activeTab || undefined, - patient_id: patientId || undefined, + status: FILTER_MAP[activeFilter], }); setTasks(res.data || []); setTotal(res.total || 0); @@ -58,7 +54,7 @@ export default function FollowUpList() { } finally { setLoading(false); } - }, [activeTab, patientId]); + }, [activeFilter]); const { trigger } = usePageData(loadTasks); @@ -67,10 +63,15 @@ export default function FollowUpList() { trigger(); } mountedRef.current = true; - }, [activeTab, patientId, trigger]); + }, [activeFilter, trigger]); + + const handleFilterChange = (index: number) => { + setActiveFilter(index); + }; const formatDate = (dateStr: string) => { - return new Date(dateStr).toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' }); + const d = new Date(dateStr); + return `${d.getMonth() + 1}月${d.getDate()}日`; }; const getTypeLabel = (type: string) => { @@ -83,48 +84,72 @@ export default function FollowUpList() { return map[type] || type; }; + const getStatusLabel = (status: string) => { + const map: Record = { + pending: '待随访', + in_progress: '进行中', + completed: '已完成', + overdue: '已过期', + }; + return map[status] || status; + }; + if (loading && tasks.length === 0) return ; if (error) return ; return ( - - {}} - filters={TABS} - activeFilter={activeTab} - onFilterChange={(key) => setActiveTab(key)} - /> + + - - 共 {total} 项任务 + + {tasks.length === 0 ? ( ) : ( - - {tasks.map((task) => ( - safeNavigateTo(`/pages/pkg-doctor-core/followup/detail/index?id=${task.id}`)} - > - - {getTypeLabel(task.follow_up_type)} - - - {task.patient_name || '未知患者'} - - 计划日期: {formatDate(task.planned_date)} - - - ))} - + + {tasks.map((task) => { + const statusStyle = STATUS_STYLE[task.status] || STATUS_STYLE.pending; + return ( + safeNavigateTo(`/pages/pkg-doctor-core/followup/detail/index?id=${task.id}`)} + > + {/* 上部:患者名 + 标签 | 日期 */} + + + + {task.patient_name || '未知患者'} + + {getStatusLabel(task.status)} + + + {getTypeLabel(task.follow_up_type)} + + {formatDate(task.planned_date)} + + + {/* 下部:最近数据行(对齐原型 bg 底色行) */} + + 计划日期 + {formatDate(task.planned_date)} + + + ); + })} + )} - + ); } diff --git a/apps/miniprogram/src/pages/pkg-doctor-core/index.scss b/apps/miniprogram/src/pages/pkg-doctor-core/index.scss index fd21781..e235cff 100644 --- a/apps/miniprogram/src/pages/pkg-doctor-core/index.scss +++ b/apps/miniprogram/src/pages/pkg-doctor-core/index.scss @@ -1,204 +1,86 @@ @import '../../styles/variables.scss'; @import '../../styles/mixins.scss'; -// PageShell 已接管:min-height, background, padding - .doctor-home { + height: 100vh; + background: $bg; + display: flex; + flex-direction: column; + &__scroll { + flex: 1; + } + + &__content { + padding: 16px 20px 80px; + } + + // ── 头部(对齐原型)── &__header { - margin-bottom: var(--tk-gap-2xl); + margin-bottom: 16px; } &__title { - @include section-title; - margin-bottom: var(--tk-gap-sm); - } - - &__greeting { - font-size: var(--tk-font-h2); - color: $tx2; - display: block; - margin-bottom: var(--tk-gap-xs); - } - - &__date { - font-size: var(--tk-font-h2); - color: $tx3; - } - - &__alert { - display: flex; - align-items: center; - margin: var(--tk-gap-md) var(--tk-page-padding); - padding: var(--tk-gap-md) var(--tk-section-gap); - background: $dan-l; - border-radius: $r; - border-left: 4px solid $dan; - } - - &__alert-icon { - width: 36px; - height: 36px; - border-radius: 50%; - background: $dan; - color: $white; - text-align: center; - line-height: 36px; - font-weight: bold; - font-size: var(--tk-font-body); - margin-right: var(--tk-gap-sm); - flex-shrink: 0; - } - - &__alert-text { - flex: 1; - font-size: var(--tk-font-h1); - color: $dan; - } - - &__alert-link { - font-size: var(--tk-font-h2); - color: $dan; - flex-shrink: 0; - } - - &__search { - margin: 0 var(--tk-page-padding) var(--tk-gap-md); - } - - &__search-input { - background: $surface-alt; - border-radius: $r; - padding: var(--tk-gap-md) var(--tk-section-gap); - font-size: var(--tk-font-h1); - color: $tx3; - } - - &__section { - margin-bottom: var(--tk-gap-2xl); - } - - &__section-title { - @include section-title; - } - - &__grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--tk-section-gap); - } - - &__card { - background: $card; - border-radius: $r-lg; - padding: var(--tk-card-padding-lg) var(--tk-card-padding); - text-align: center; - box-shadow: $shadow-md; - transition: transform 0.15s; - - &:active { - opacity: var(--tk-touch-feedback-opacity); - } - } - - &__card-initial { - display: inline-flex; - @include flex-center; - width: 56px; - height: 56px; - border-radius: $r; - background: var(--tk-pri-l); - color: var(--tk-pri); - font-family: 'Georgia', 'Times New Roman', serif; - font-size: var(--tk-font-body-lg); - font-weight: 700; - margin-bottom: var(--tk-gap-xs); - } - - &__card-num { - @include serif-number; - font-size: var(--tk-font-hero); + font-family: Georgia, 'Times New Roman', serif; + font-size: 26px; font-weight: 700; color: $tx; display: block; - margin-bottom: var(--tk-gap-xs); + margin-bottom: 4px; } - &__card-label { - font-size: var(--tk-font-h2); + &__date { + font-size: 14px; + color: $tx3; + } + + // ── 小节标题(对齐原型:13px fontWeight600)── + &__section-label { + display: block; + font-size: 13px; + font-weight: 600; color: $tx2; + margin-bottom: 14px; + font-family: -apple-system, 'PingFang SC', sans-serif; } - &__quick-actions { + // ── 今日概览统计网格(对齐原型:子卡片有 bg 背景)── + &__stat-grid { display: grid; - grid-template-columns: repeat(4, 1fr); - gap: var(--tk-section-gap); + grid-template-columns: 1fr 1fr; + gap: 12px; } - &__footer { - margin-top: 60px; + &__stat-item { + background: $bg; + border-radius: $r-sm; + padding: 14px 12px; text-align: center; - padding-bottom: env(safe-area-inset-bottom); } - &__logout { - color: $dan; - font-size: var(--tk-font-h2); - padding: var(--tk-gap-md) var(--tk-gap-2xl); - display: inline-block; - } -} - -.quick-action { - flex: 1; - background: $card; - border-radius: $r-lg; - padding: var(--tk-card-padding-lg) var(--tk-section-gap); - text-align: center; - box-shadow: $shadow-md; - - &:active { - opacity: var(--tk-touch-feedback-opacity); - } - - &__initial { - display: inline-flex; - @include flex-center; - width: 56px; - height: 56px; - border-radius: $r; - background: $acc-l; - color: $acc; - font-family: 'Georgia', 'Times New Roman', serif; - font-size: var(--tk-font-body-lg); + &__stat-value { + font-family: Georgia, 'Times New Roman', serif; + font-size: 28px; font-weight: 700; + line-height: 1.1; + + &--wrn { color: $wrn; } + &--pri { color: $doc-pri; } + &--acc { color: $acc; } + &--dan { color: $dan; } } - &__icon-wrap { - position: relative; - display: inline-flex; - margin-bottom: var(--tk-gap-xs); - } - - &__badge { - position: absolute; - top: -6px; - right: -12px; - min-width: 32px; - height: 32px; - line-height: 32px; - text-align: center; - background: $dan; - color: $white; - font-size: var(--tk-font-body-sm); - font-weight: 700; - border-radius: $r-pill; - padding: 0 6px; - } - - &__label { - font-size: var(--tk-font-h2); - color: $tx2; + &__stat-label { + font-size: 12px; + color: $tx3; + margin-top: 4px; display: block; } + + // ── 快捷操作(对齐原型:space-between)── + &__shortcuts { + display: flex; + justify-content: space-between; + margin-bottom: 16px; + } } diff --git a/apps/miniprogram/src/pages/pkg-doctor-core/index.tsx b/apps/miniprogram/src/pages/pkg-doctor-core/index.tsx index 78b4063..5645eed 100644 --- a/apps/miniprogram/src/pages/pkg-doctor-core/index.tsx +++ b/apps/miniprogram/src/pages/pkg-doctor-core/index.tsx @@ -1,67 +1,48 @@ import { useState, useMemo, useCallback } from 'react'; -import { View, Text, Input } from '@tarojs/components'; -import Taro from '@tarojs/taro'; +import { View, Text, ScrollView } from '@tarojs/components'; import { safeNavigateTo } from '@/utils/navigate'; import { useAuthStore } from '@/stores/auth'; import { useDoctorClass } from '@/hooks/useDoctorClass'; import { usePageData } from '@/hooks/usePageData'; import { getDashboard, type DoctorDashboard } from '@/services/doctor/dashboard'; import Loading from '@/components/Loading'; -import PageShell from '@/components/ui/PageShell'; +import ContentCard from '@/components/ui/ContentCard'; +import ShortcutButton from '@/components/ui/ShortcutButton'; +import TodoAlert from '@/components/ui/TodoAlert'; import './index.scss'; -interface CardConfig { +interface StatItem { key: keyof DoctorDashboard; label: string; - initial: string; + color: string; +} + +interface ShortcutItem { + icon: string; + label: string; + color: 'pri' | 'acc' | 'wrn' | 'dan'; route: string; roles?: string[]; } -const ALL_CARDS: CardConfig[] = [ - { key: 'total_patients', label: '我的患者', initial: '患', route: '/pages/pkg-doctor-core/patients/index' }, - { key: 'unread_messages', label: '未读消息', initial: '消', route: '/pages/pkg-doctor-core/consultation/index' }, - { key: 'pending_follow_ups', label: '待处理随访', initial: '随', route: '/pages/pkg-doctor-core/followup/index', roles: ['doctor', 'nurse', 'health_manager'] }, - { key: 'today_consultations', label: '今日咨询', initial: '诊', route: '/pages/pkg-doctor-core/consultation/index', roles: ['doctor', 'health_manager'] }, +const STATS: StatItem[] = [ + { key: 'pending_follow_ups', label: '待处理', color: 'wrn' }, + { key: 'today_consultations', label: '咨询中', color: 'pri' }, + { key: 'today_appointments', label: '今日患者', color: 'acc' }, + { key: 'unread_messages', label: '随访到期', color: 'dan' }, ]; -const ALL_HEALTH_CARDS: CardConfig[] = [ - { key: 'pending_lab_review', label: '待审化验', initial: '化', route: '/pages/pkg-doctor-clinical/report/index', roles: ['doctor'] }, - { key: 'today_appointments', label: '今日预约', initial: '约', route: '/pages/pkg-doctor-core/patients/index' }, +const SHORTCUTS: ShortcutItem[] = [ + { icon: '👤', label: '患者管理', color: 'pri', route: '/pages/pkg-doctor-core/patients/index' }, + { icon: '💬', label: '在线咨询', color: 'acc', route: '/pages/pkg-doctor-core/consultation/index' }, + { icon: '📋', label: '随访管理', color: 'wrn', route: '/pages/pkg-doctor-core/followup/index', roles: ['doctor', 'nurse', 'health_manager'] }, + { icon: '🩺', label: '透析管理', color: 'dan', route: '/pages/pkg-doctor-clinical/dialysis/index', roles: ['doctor'] }, ]; -interface QuickAction { - label: string; - initial: string; - route: string; - roles: string[]; -} - -const ALL_QUICK_ACTIONS: QuickAction[] = [ - { label: '化验审核', initial: '审', route: '/pages/pkg-doctor-clinical/report/index', roles: ['doctor'] }, - { label: '患者查询', initial: '查', route: '/pages/pkg-doctor-core/patients/index', roles: ['doctor', 'nurse', 'health_manager'] }, - { label: '随访记录', initial: '随', route: '/pages/pkg-doctor-core/followup/index', roles: ['doctor', 'nurse', 'health_manager'] }, - { label: '告警中心', initial: '警', route: '/pages/pkg-doctor-clinical/alerts/index', roles: ['doctor', 'nurse', 'health_manager'] }, - { label: '透析管理', initial: '透', route: '/pages/pkg-doctor-clinical/dialysis/index', roles: ['doctor'] }, - { label: '处方管理', initial: '方', route: '/pages/pkg-doctor-clinical/prescription/index', roles: ['doctor'] }, - { label: '行动收件箱', initial: '行', route: '/pages/pkg-doctor-core/action-inbox/index', roles: ['doctor', 'nurse', 'health_manager'] }, -]; - -const ROLE_LABELS: Record = { - doctor: '医生', - nurse: '护士', - health_manager: '健康管理师', - admin: '管理员', - operator: '运营', -}; - export default function DoctorHome() { - const user = useAuthStore((s) => s.user); - const logout = useAuthStore((s) => s.logout); const roles = useAuthStore((s) => s.roles); const modeClass = useDoctorClass(); const [dashboard, setDashboard] = useState(null); - const [alertCount, setAlertCount] = useState(0); const [loading, setLoading] = useState(true); const hasRole = (allowed: string[] | undefined) => { @@ -69,23 +50,14 @@ export default function DoctorHome() { return roles.some((r) => r === 'admin' || allowed.includes(r)); }; - const cards = useMemo(() => ALL_CARDS.filter((c) => hasRole(c.roles)), [roles]); - const healthCards = useMemo(() => ALL_HEALTH_CARDS.filter((c) => hasRole(c.roles)), [roles]); - const quickActions = useMemo(() => ALL_QUICK_ACTIONS.filter((a) => hasRole(a.roles)), [roles]); - - const roleLabel = useMemo(() => { - const primary = roles.find((r) => r !== 'admin'); - return primary ? (ROLE_LABELS[primary] || primary) : '医护'; - }, [roles]); + const shortcuts = useMemo(() => SHORTCUTS.filter((s) => hasRole(s.roles)), [roles]); const loadDashboard = useCallback(async () => { try { const data = await getDashboard(); setDashboard(data); - const count = (data as Record)?.abnormal_vital_count; - setAlertCount(typeof count === 'number' ? count : 0); } catch { - // 静默失败,显示占位 + // 静默失败 } finally { setLoading(false); } @@ -93,107 +65,78 @@ export default function DoctorHome() { usePageData(loadDashboard, { throttleMs: 10000 }); - const handleCardClick = (card: CardConfig) => { - safeNavigateTo(card.route); - }; - - const handleLogout = () => { - logout(); - }; - const getValue = (key: keyof DoctorDashboard): number | string => { if (!dashboard) return '-'; return dashboard[key] ?? 0; }; + const today = new Date(); + const dateStr = `${today.getFullYear()}年${today.getMonth() + 1}月${today.getDate()}日 ${ + ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'][today.getDay()] + }`; + if (loading) return ; return ( - - - 医护工作台 - - {user?.display_name || user?.username || roleLabel},您好 - - - {new Date().toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'long' })} - - + + + + {/* 问候区 — 对齐原型:标题 + 日期 */} + + 工作台 + {dateStr} + - {alertCount > 0 && ( - - ! - {alertCount} 位患者体征异常 - safeNavigateTo('/pages/pkg-doctor-clinical/alerts/index')}>查看 → - - )} - - - safeNavigateTo('/pages/pkg-doctor-core/patients/index')} - /> - - - - 工作概览 - - {cards.map((card) => ( - handleCardClick(card)} - > - {card.initial} - {getValue(card.key)} - {card.label} + {/* 今日概览 — 原型:卡片内含子网格 */} + + 今日概览 + + {STATS.map((stat) => ( + + + {getValue(stat.key)} + + {stat.label} + + ))} - ))} - - + - {healthCards.length > 0 && ( - 健康审核 - - {healthCards.map((card) => ( - handleCardClick(card)} - > - {card.initial} - {getValue(card.key)} - {card.label} - - ))} - - )} + {/* 快捷操作 — 原型:space-between 均分 */} + + {shortcuts.map((item) => ( + safeNavigateTo(item.route)} + /> + ))} + - - 快捷操作 - - {quickActions.map((action) => ( - safeNavigateTo(action.route)} - > - - {action.initial} - {action.label === '告警中心' && alertCount > 0 && ( - {alertCount > 99 ? '99+' : alertCount} - )} - - {action.label} - - ))} + {/* 待办提醒 — 原型:无 SectionTitle,直接小标题 + 警告卡片 */} + 待办提醒 + {dashboard && dashboard.pending_follow_ups > 0 && ( + safeNavigateTo('/pages/pkg-doctor-core/followup/index')} + /> + )} + {dashboard && dashboard.today_consultations > 0 && ( + safeNavigateTo('/pages/pkg-doctor-core/consultation/index')} + /> + )} - - - - 退出登录 - - + + ); } diff --git a/apps/miniprogram/src/pages/pkg-doctor-core/patients/index.scss b/apps/miniprogram/src/pages/pkg-doctor-core/patients/index.scss index 4acf542..d8841c9 100644 --- a/apps/miniprogram/src/pages/pkg-doctor-core/patients/index.scss +++ b/apps/miniprogram/src/pages/pkg-doctor-core/patients/index.scss @@ -1,66 +1,123 @@ @import '../../../styles/variables.scss'; -// PageShell 已接管:min-height, background, padding -// SearchSection 已接管:search-bar -// ContentCard 已接管:patient-card 背景/圆角/阴影/触摸反馈 -// StatusTag 已接管:patient-card__status 标签样式 - -.patient-count { - margin-bottom: var(--tk-gap-md); - - text { - font-size: var(--tk-font-h2); - color: $tx3; - } -} - -.patient-cards { +.patient-page { + height: 100vh; + background: $bg; display: flex; flex-direction: column; - gap: var(--tk-gap-md); -} -.patient-card__header { - display: flex; - align-items: center; - gap: var(--tk-gap-md); -} + &__search { + padding: 12px 20px; + } -.patient-card__name { - font-size: var(--tk-font-num); - font-weight: 600; - color: $tx; -} + &__count { + padding: 0 20px; + margin-bottom: 12px; -.patient-card__meta { - font-size: var(--tk-font-h2); - color: $tx2; - flex: 1; -} + text { + font-size: 13px; + color: $tx3; + } + } -.patient-card__tags { - display: flex; - flex-wrap: wrap; - gap: var(--tk-gap-xs); - margin-top: var(--tk-gap-sm); -} + &__list { + flex: 1; + padding: 0 20px 20px; + } -.patient-tag { - padding: var(--tk-gap-2xs) 14px; - border-radius: $r; - background: rgba($pri, 0.1); + &__hint { + text-align: center; + padding: 20px; - &__text { - font-size: var(--tk-font-body); + text { + font-size: 13px; + color: #78716C; + } } } -.load-more-hint-wrap { - text-align: center; - padding: var(--tk-section-gap); +// ── 搜索栏(对齐原型 §3.3 SearchBar)── +.search-bar { + display: flex; + align-items: center; + gap: 10px; + background: $card; + border-radius: 12px; + height: 42px; + padding: 0 14px; + border: 1px solid $bd; + + &__icon { + font-size: 14px; + flex-shrink: 0; + } + + &__input { + flex: 1; + font-size: 14px; + color: #2D2A26; + height: 100%; + } + + &__placeholder { + color: #78716C; + font-size: 14px; + } } -.load-more-hint { - font-size: var(--tk-font-h2); - color: $tx3; +// ── 患者卡片(对齐原型:诊断 $doc-pri 色 + 最近访问日期)── +.patient-card { + margin-bottom: 10px !important; + + &__row { + display: flex; + align-items: center; + gap: 12px; + } + + &__body { + flex: 1; + min-width: 0; + } + + &__top { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; + } + + &__name { + font-size: 15px; + font-weight: 600; + color: #2D2A26; + } + + &__meta { + font-size: 12px; + color: #78716C; + } + + &__diagnosis { + font-size: 13px; + color: $doc-pri; + margin-top: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: block; + } + + &__last-visit { + font-size: 12px; + color: $tx3; + margin-top: 3px; + display: block; + } + + &__arrow { + flex-shrink: 0; + font-size: 20px; + color: #78716C; + } } diff --git a/apps/miniprogram/src/pages/pkg-doctor-core/patients/index.tsx b/apps/miniprogram/src/pages/pkg-doctor-core/patients/index.tsx index 5bd86d0..6920d58 100644 --- a/apps/miniprogram/src/pages/pkg-doctor-core/patients/index.tsx +++ b/apps/miniprogram/src/pages/pkg-doctor-core/patients/index.tsx @@ -1,38 +1,29 @@ import { useState, useEffect, useCallback, useRef } from 'react'; -import { View, Text } from '@tarojs/components'; +import { View, Text, Input, ScrollView } from '@tarojs/components'; import Taro, { useReachBottom } from '@tarojs/taro'; import { safeNavigateTo } from '@/utils/navigate'; import { usePageData } from '@/hooks/usePageData'; -import { listPatients, listPatientTags, type PatientItem, type PatientTag } from '@/services/doctor/patient'; -import PageShell from '@/components/ui/PageShell'; +import { listPatients, type PatientItem } from '@/services/doctor/patient'; +import PageHeader from '@/components/patterns/PageHeader'; import ContentCard from '@/components/ui/ContentCard'; -import StatusTag from '@/components/ui/StatusTag'; +import AvatarCircle from '@/components/ui/AvatarCircle'; import LoadingCard from '@/components/ui/LoadingCard'; -import SearchSection from '@/components/patterns/SearchSection'; import EmptyState from '@/components/EmptyState'; import Loading from '@/components/Loading'; import { useDoctorClass } from '@/hooks/useDoctorClass'; import './index.scss'; +const AVATAR_COLORS: Array<'pri' | 'acc' | 'wrn' | 'dan'> = ['pri', 'acc', 'wrn', 'dan']; + export default function PatientList() { const modeClass = useDoctorClass(); const [patients, setPatients] = useState([]); - const [tags, setTags] = useState([]); - const [activeTag, setActiveTag] = useState(''); const [search, setSearch] = useState(''); const [loading, setLoading] = useState(true); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const mountedRef = useRef(false); - - useEffect(() => { loadTags(); }, []); - - const loadTags = async () => { - try { - const res = await listPatientTags(); - setTags(res.data || []); - } catch { /* ignore */ } - }; + const debounceRef = useRef>(); const loadPatients = useCallback(async (pageNum: number, isRefresh = false) => { if (isRefresh) setLoading(true); @@ -41,7 +32,6 @@ export default function PatientList() { page: pageNum, page_size: 20, search: search || undefined, - tag_id: activeTag || undefined, }); const list = res.data || []; setPatients(prev => isRefresh ? list : [...prev, ...list]); @@ -52,7 +42,7 @@ export default function PatientList() { } finally { setLoading(false); } - }, [search, activeTag]); + }, [search]); usePageData( useCallback(() => loadPatients(1, true), [loadPatients]), @@ -62,16 +52,22 @@ export default function PatientList() { useEffect(() => { if (mountedRef.current) { loadPatients(1, true); } mountedRef.current = true; - }, [activeTag, loadPatients]); + }, [search, loadPatients]); useReachBottom(() => { if (!loading && patients.length < total) { loadPatients(page + 1); } }); - const handleTagFilter = (tagId: string) => { - setActiveTag(tagId === activeTag ? '' : tagId); + const handleSearchInput = (val: string) => { + setSearch(val); + if (debounceRef.current) clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => { + loadPatients(1, true); + }, 300); }; + const getAvatarColor = (index: number) => AVATAR_COLORS[index % AVATAR_COLORS.length]; + const getGenderLabel = (gender?: string) => { if (!gender) return ''; return gender === 'male' ? '男' : gender === 'female' ? '女' : gender; @@ -89,69 +85,90 @@ export default function PatientList() { return `${age}岁`; }; - const filters = [ - { key: '', label: '全部' }, - ...tags.map(t => ({ key: t.id, label: t.name })), - ]; + const formatLastVisit = (dateStr?: string) => { + if (!dateStr) return ''; + const d = new Date(dateStr); + return `${d.getMonth() + 1}月${d.getDate()}日`; + }; + + // 用 tag 名称组合为诊断摘要 + const getDiagnosis = (p: PatientItem) => { + if (p.tags && p.tags.length > 0) { + return p.tags.map(t => t.name).join(' · '); + } + return ''; + }; if (loading && patients.length === 0) return ; return ( - - loadPatients(1, true)} - placeholder="搜索患者姓名/手机号" - filters={filters} - activeFilter={activeTag} - onFilterChange={handleTagFilter} - /> + + - + + + 🔍 + handleSearchInput(e.detail.value)} + placeholder="搜索患者姓名" + placeholderClass="search-bar__placeholder" + confirmType="search" + /> + + + + 共 {total} 位患者 {patients.length === 0 ? ( ) : ( - - {patients.map((p) => ( - safeNavigateTo(`/pages/pkg-doctor-core/patients/detail/index?id=${p.id}`)} - > - - {p.name} - - {getGenderLabel(p.gender)} {calcAge(p.birth_date)} - - {p.status && } - - {p.tags && p.tags.length > 0 && ( - - {p.tags.map((t) => ( - - {t.name} + + {patients.map((p, idx) => { + const diagnosis = getDiagnosis(p); + return ( + safeNavigateTo(`/pages/pkg-doctor-core/patients/detail/index?id=${p.id}`)} + > + + + + + {p.name} + + {calcAge(p.birth_date)} · {getGenderLabel(p.gender)} + - ))} + {diagnosis && ( + {diagnosis} + )} + {p.last_visit_date && ( + 最近 {formatLastVisit(p.last_visit_date)} + )} + + - )} - - ))} - + + ); + })} + {!loading && patients.length >= total && total > 0 && ( + + 没有更多了 + + )} + {loading && patients.length > 0 && } + )} - - {!loading && patients.length >= total && total > 0 && ( - - 没有更多了 - - )} - {loading && patients.length > 0 && } - + ); } diff --git a/apps/miniprogram/src/pages/pkg-mall/exchange/index.scss b/apps/miniprogram/src/pages/pkg-mall/exchange/index.scss index dfd5406..eacf1bb 100644 --- a/apps/miniprogram/src/pages/pkg-mall/exchange/index.scss +++ b/apps/miniprogram/src/pages/pkg-mall/exchange/index.scss @@ -1,200 +1,201 @@ @import '../../../styles/variables.scss'; @import '../../../styles/mixins.scss'; -// PageShell 已接管:min-height, background, padding +// 兑换确认 — 对齐原型 docs/design/mp-10-mall-pkg.html → 屏幕2 + .exchange-page { - padding-bottom: 140px; + padding-bottom: var(--tk-gap-xl); } -/* ===== 商品预览 ===== */ -.product-card { +// 商品预览卡片 +.exchange-product-card { display: flex; - align-items: center; - padding: var(--tk-gap-xl) var(--tk-gap-lg); + gap: var(--tk-gap-sm); + padding: 14px; background: $card; - margin: var(--tk-section-gap) var(--tk-gap-lg) var(--tk-gap-md); - border-radius: $r-lg; + border-radius: $r; + margin-bottom: var(--tk-gap-md); box-shadow: $shadow-sm; } -.product-icon-wrap { - width: 128px; - height: 128px; - border-radius: $r; - @include flex-center; - margin-right: var(--tk-gap-lg); +.exchange-product-icon { + width: 72px; + height: 72px; + border-radius: $r-sm; + display: flex; + align-items: center; + justify-content: center; flex-shrink: 0; - &--physical { - background: $acc; - } - - &--service { - background: var(--tk-pri); - } - - &--privilege { - background: var(--tk-pri-d); - } + &--physical { background: $pri-l; } + &--service { background: $acc-l; } + &--privilege { background: $wrn-l; } } -.product-icon-char { - font-family: 'Georgia', 'Times New Roman', serif; - font-size: var(--tk-font-hero); - font-weight: bold; - color: $white; +.exchange-product-icon-char { + font-size: 24px; + font-weight: 700; + color: $pri; + + .exchange-product-icon--service & { color: $acc; } + .exchange-product-icon--privilege & { color: $wrn; } } -.product-meta { +.exchange-product-meta { flex: 1; min-width: 0; } -.product-name { - font-size: var(--tk-font-num); - font-weight: bold; +.exchange-product-name { + font-family: 'Georgia', 'Times New Roman', serif; + font-size: 15px; + font-weight: 700; color: $tx; display: block; - margin-bottom: var(--tk-gap-sm); + margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.product-type-tag { - @include tag(var(--tk-pri-l), var(--tk-pri-d)); +.exchange-product-points { + font-size: var(--tk-font-cap); + color: $pri; + font-weight: 600; + display: block; } -/* ===== 兑换明细 ===== */ -.detail-section { - padding: 0 var(--tk-gap-lg); - margin-bottom: var(--tk-gap-md); +.exchange-product-qty { + font-size: var(--tk-font-micro); + color: $tx3; + margin-top: 2px; + display: block; } -.detail-section-title { - @include section-title; -} - -.detail-card { +// 收货信息卡片 +.exchange-address-card { background: $card; border-radius: $r; + padding: var(--tk-gap-md); + margin-bottom: var(--tk-gap-md); box-shadow: $shadow-sm; - padding: 0 var(--tk-gap-lg); } -.detail-row { +.exchange-address-header { display: flex; justify-content: space-between; align-items: center; - padding: var(--tk-gap-lg) 0; - border-bottom: 1px solid $bd-l; - - &.last { - border-bottom: none; - } + margin-bottom: 10px; } -.detail-label { - font-size: var(--tk-font-body-lg); - color: $tx2; -} - -.detail-value { - @include serif-number; - font-size: var(--tk-font-body-lg); +.exchange-address-title { + font-size: var(--tk-font-body-sm); + font-weight: 600; color: $tx; - font-weight: bold; - - &.detail-cost { - color: var(--tk-pri); - font-size: var(--tk-font-num-lg); - } - - &.detail-sufficient { - color: $acc; - } - - &.detail-insufficient { - color: $dan; - } } -/* ===== 温馨提示 ===== */ -.notice-section { +.exchange-address-edit { + font-size: var(--tk-font-micro); + color: $pri; + font-weight: 500; +} + +.exchange-address-name { + font-size: var(--tk-font-cap); + color: $tx; + font-weight: 500; + display: block; + margin-bottom: 2px; +} + +.exchange-address-detail { + font-size: var(--tk-font-micro); + color: $tx3; + line-height: 1.5; + display: block; +} + +// 兑换明细卡片 +.exchange-detail-card { background: $card; - padding: var(--tk-gap-lg); - margin: 0 var(--tk-gap-lg); border-radius: $r; + padding: var(--tk-gap-md); + margin-bottom: var(--tk-gap-lg); box-shadow: $shadow-sm; } -.notice-title { - @include section-title; - font-size: var(--tk-font-body-lg); +.exchange-detail-title { + font-size: var(--tk-font-body-sm); + font-weight: 600; + color: $tx; + display: block; margin-bottom: var(--tk-gap-sm); } -.notice-text { - font-size: var(--tk-font-h2); - color: $tx3; - display: block; - line-height: 1.7; - margin-bottom: var(--tk-gap-2xs); -} - -/* ===== 底部操作栏 ===== */ -.exchange-footer { - position: fixed; - bottom: 0; - left: 0; - right: 0; +.exchange-detail-row { display: flex; - align-items: center; - padding: var(--tk-gap-md) var(--tk-gap-lg); - padding-bottom: calc(var(--tk-gap-md) + env(safe-area-inset-bottom)); - background: $card; - box-shadow: 0 -2px 12px rgba($tx, 0.06); - z-index: 10; -} + justify-content: space-between; + padding-bottom: 8px; + margin-bottom: 8px; + border-bottom: 1px dashed $bd-l; -.footer-cost { - flex: 1; - display: flex; - flex-direction: column; -} - -.footer-cost-label { - font-size: var(--tk-font-body); - color: $tx3; -} - -.footer-cost-num { - @include serif-number; - font-size: var(--tk-font-num-lg); - font-weight: bold; - color: var(--tk-pri); -} - -.footer-cost-unit { - font-size: var(--tk-font-body); - color: $tx2; - margin-left: var(--tk-gap-2xs); -} - -.confirm-btn { - background: var(--tk-pri); - padding: var(--tk-section-gap) var(--tk-gap-2xl); - border-radius: $r-pill; - transition: opacity 0.2s; - - &.disabled { - background: $bd; - opacity: 0.7; + &.last { + border-bottom: none; + padding-bottom: 0; + margin-bottom: 0; } } -.confirm-btn-text { - font-size: var(--tk-font-num); - color: $white; - font-weight: bold; +.exchange-detail-label { + font-size: var(--tk-font-cap); + color: $tx3; +} + +.exchange-detail-value { + font-size: var(--tk-font-cap); + color: $tx; + font-weight: 400; + + &.exchange-detail-cost { + color: $pri; + font-weight: 600; + } + + &.sufficient { + color: $acc; + font-weight: 600; + } + + &.insufficient { + color: $dan; + font-weight: 600; + } +} + +// 确认兑换按钮 +.exchange-confirm-btn { + height: 50px; + background: var(--tk-pri); + border-radius: $r; + display: flex; + align-items: center; + justify-content: center; + box-shadow: var(--tk-shadow-btn); + + &.disabled { + background: $bd; + box-shadow: none; + opacity: 0.7; + } + + &:active:not(.disabled) { + opacity: var(--tk-touch-feedback-opacity); + } +} + +.exchange-confirm-text { + font-family: 'Georgia', 'Times New Roman', serif; + font-size: var(--tk-font-body); + color: $white; + font-weight: 700; } diff --git a/apps/miniprogram/src/pages/pkg-mall/exchange/index.tsx b/apps/miniprogram/src/pages/pkg-mall/exchange/index.tsx index 209bab4..5bbf675 100644 --- a/apps/miniprogram/src/pages/pkg-mall/exchange/index.tsx +++ b/apps/miniprogram/src/pages/pkg-mall/exchange/index.tsx @@ -3,33 +3,29 @@ import { View, Text } from '@tarojs/components'; import Taro from '@tarojs/taro'; import { usePageData } from '@/hooks/usePageData'; import { + getProduct, listProducts, exchangeProduct, } from '../../../services/points'; import type { PointsProduct } from '../../../services/points'; import { usePointsStore } from '../../../stores/points'; +import { useAuthStore } from '../../../stores/auth'; import Loading from '../../../components/Loading'; import { useElderClass } from '../../../hooks/useElderClass'; import { useSafeTimeout } from '@/hooks/useSafeTimeout'; import PageShell from '@/components/ui/PageShell'; import './index.scss'; -const TYPE_INITIAL: Record = { +const TYPE_CHAR: Record = { physical: '物', service: '券', privilege: '权', }; -const TYPE_LABEL: Record = { - physical: '实物商品', - service: '服务券', - privilege: '权益卡', -}; - const TYPE_CLASS: Record = { - physical: 'product-icon-wrap--physical', - service: 'product-icon-wrap--service', - privilege: 'product-icon-wrap--privilege', + physical: 'physical', + service: 'service', + privilege: 'privilege', }; export default function ExchangeConfirm() { @@ -37,6 +33,7 @@ export default function ExchangeConfirm() { const [product, setProduct] = useState(null); const account = usePointsStore((s) => s.account); const refreshPoints = usePointsStore((s) => s.refresh); + const currentPatient = useAuthStore((s) => s.currentPatient); const [loading, setLoading] = useState(true); const [submitting, setSubmitting] = useState(false); const { safeSetTimeout } = useSafeTimeout(); @@ -52,17 +49,21 @@ export default function ExchangeConfirm() { setLoading(true); try { - const [productRes] = await Promise.all([ - listProducts({ page: 1, page_size: 100 }), - refreshPoints(), - ]); - const found = productRes.data.find((p) => p.id === productId); + // 先尝试单商品接口,降级到列表查找 + let found: PointsProduct | null = null; + try { + found = await getProduct(productId); + } catch { + const productRes = await listProducts({ page: 1, page_size: 100 }); + found = productRes.data.find((p) => p.id === productId) || null; + } if (!found) { Taro.showToast({ title: '商品不存在', icon: 'none' }); safeSetTimeout(() => Taro.navigateBack(), 1500); return; } setProduct(found); + await refreshPoints(); } catch { Taro.showToast({ title: '加载失败', icon: 'none' }); safeSetTimeout(() => Taro.navigateBack(), 1500); @@ -82,6 +83,11 @@ export default function ExchangeConfirm() { const balance = account?.balance ?? 0; const cost = product?.points_cost ?? 0; const insufficient = balance < cost; + const remaining = balance - cost; + const productType = product?.product_type || 'physical'; + const isService = productType === 'service'; + const typeChar = TYPE_CHAR[productType] || '礼'; + const typeCls = TYPE_CLASS[productType] || 'physical'; const handleConfirm = useCallback(async () => { if (!product || submitting) return; @@ -103,17 +109,19 @@ export default function ExchangeConfirm() { Taro.showToast({ title: '兑换成功', icon: 'success', duration: 2000 }); safeSetTimeout(() => { - Taro.showModal({ - title: '兑换成功', - content: `核销码: ${order.qr_code}\n请凭此码到前台核销`, - showCancel: false, - confirmText: '查看订单', - success: () => { - Taro.redirectTo({ - url: `/pages/pkg-mall/orders/index`, - }); - }, - }); + if (isService && order.qr_code) { + Taro.showModal({ + title: '兑换成功', + content: `核销码: ${order.qr_code}\n请凭此码到前台核销`, + showCancel: false, + confirmText: '查看订单', + success: () => { + Taro.redirectTo({ url: '/pages/pkg-mall/orders/index' }); + }, + }); + } else { + Taro.redirectTo({ url: '/pages/pkg-mall/orders/index' }); + } }, 2000); } catch (err) { const msg = err instanceof Error ? err.message : '兑换失败'; @@ -125,7 +133,7 @@ export default function ExchangeConfirm() { } finally { setSubmitting(false); } - }, [product, submitting, insufficient, cost]); + }, [product, submitting, insufficient, cost, isService]); if (loading) { return ( @@ -135,88 +143,72 @@ export default function ExchangeConfirm() { ); } - const productType = product?.product_type || 'physical'; - const initial = TYPE_INITIAL[productType] || '礼'; - const typeLabel = TYPE_LABEL[productType] || '商品'; - const iconCls = TYPE_CLASS[productType] || 'product-icon-wrap--service'; - return ( - + {/* 商品预览卡片 */} - - - {initial} + + + {typeChar} - - {product?.name || ''} - {typeLabel} + + {product?.name || ''} + {cost.toLocaleString()} 积分 + ×1 + {/* 收货信息(实体商品) */} + {!isService && currentPatient && ( + + + 收货信息 + 修改地址 › + + + {currentPatient.name} + + 请前往个人中心完善收货地址 + + )} + {/* 兑换明细 */} - - 兑换明细 - - - 所需积分 - {cost.toLocaleString()} - - - 当前余额 - - {balance.toLocaleString()} - - - {insufficient && ( - - 差额 - - -{(cost - balance).toLocaleString()} - - - )} - - 库存 - - {product && product.stock > 0 ? `剩余 ${product.stock} 件` : '已兑完'} - - + + 兑换明细 + + 商品积分 + {cost.toLocaleString()} - - - {/* 温馨提示 */} - - 温馨提示 - - 兑换成功后将生成核销码,请凭核销码到前台核销领取。 - - 积分一经兑换不可退回。 - - - {/* 底部操作 */} - - - 合计 - {cost.toLocaleString()} - 积分 + + {isService ? '核销方式' : '运费'} + {isService ? '到院核销' : '¥0.00'} - - - {submitting - ? '兑换中...' - : insufficient - ? '积分不足' - : (product?.stock ?? 0) <= 0 - ? '已兑完' - : '确认兑换'} + + 应扣积分 + {cost.toLocaleString()} + + + 剩余积分 + = 0 ? 'sufficient' : 'insufficient'}`}> + {remaining.toLocaleString()} + + {/* 确认兑换按钮 */} + + + {submitting + ? '兑换中...' + : insufficient + ? '积分不足' + : (product?.stock ?? 0) <= 0 + ? '已兑完' + : '确认兑换'} + + ); } diff --git a/apps/miniprogram/src/pages/pkg-mall/orders/index.scss b/apps/miniprogram/src/pages/pkg-mall/orders/index.scss index 0747fa2..d182bdb 100644 --- a/apps/miniprogram/src/pages/pkg-mall/orders/index.scss +++ b/apps/miniprogram/src/pages/pkg-mall/orders/index.scss @@ -1,114 +1,184 @@ @import '../../../styles/variables.scss'; @import '../../../styles/mixins.scss'; -// PageShell 已接管:min-height, background, safe-bottom -// ContentCard 已接管:order-card 背景/圆角/阴影 +// 订单列表 — 对齐原型 docs/design/mp-10-mall-pkg.html → 屏幕3 -/* ===== 订单列表 ===== */ -.order-list { - padding: 0 var(--tk-gap-lg); +.orders-page { + padding-bottom: env(safe-area-inset-bottom); } +// 状态筛选 Tab +.orders-tabs { + display: flex; + padding: 0; + background: $card; + border-bottom: 1px solid $bd-l; +} + +.orders-tab { + flex: 1; + text-align: center; + padding: var(--tk-gap-sm) 0; + position: relative; + min-height: 48px; + display: flex; + align-items: center; + justify-content: center; + border-bottom: 2px solid transparent; + + &.active { + border-bottom-color: var(--tk-pri); + } + + &:active { + opacity: var(--tk-touch-feedback-opacity); + } +} + +.orders-tab-text { + font-size: var(--tk-font-cap); + color: $tx3; + + .orders-tab.active & { + color: var(--tk-pri); + font-weight: 600; + } +} + +// 订单列表 +.order-list { + padding: var(--tk-gap-md); + display: flex; + flex-direction: column; + gap: var(--tk-gap-sm); +} + +// 订单卡片 .order-card { - margin-bottom: var(--tk-gap-md); - overflow: hidden; + padding: var(--tk-gap-md); + background: $card; + border-radius: $r; + box-shadow: $shadow-sm; } .order-header { display: flex; justify-content: space-between; align-items: center; - padding: var(--tk-gap-lg) var(--tk-gap-lg) var(--tk-gap-md); - border-bottom: 1px solid $bd-l; + margin-bottom: 10px; } -.order-product { - font-family: 'Georgia', 'Times New Roman', serif; - font-size: var(--tk-font-body-lg); - font-weight: bold; - color: $tx; +.order-id { + font-size: var(--tk-font-micro); + color: $tx3; +} + +// 状态标签 +.order-status-tag { + padding: 2px 8px; + border-radius: 6px; + + &--pending { background: $wrn-l; } + &--approved { background: $pri-l; } + &--shipped { background: $acc-l; } + &--completed { background: $surface-alt; } + &--verified { background: $acc-l; } + &--cancelled { background: $dan-l; } + &--expired { background: $surface-alt; } +} + +.order-status-text { + font-size: var(--tk-font-micro); + font-weight: 600; + + .order-status-tag--pending & { color: $wrn; } + .order-status-tag--approved & { color: $pri; } + .order-status-tag--shipped & { color: $acc; } + .order-status-tag--completed & { color: $tx3; } + .order-status-tag--verified & { color: $acc; } + .order-status-tag--cancelled & { color: $dan; } + .order-status-tag--expired & { color: $tx3; } +} + +// 订单主体 +.order-body { + // layout handled by children +} + +.order-main { + display: flex; + justify-content: space-between; + align-items: center; +} + +.order-product-info { flex: 1; + min-width: 0; +} + +.order-product-name { + font-size: var(--tk-font-body-sm); + font-weight: 600; + color: $tx; + display: block; + margin-bottom: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.order-status-tag { - padding: var(--tk-gap-2xs) var(--tk-gap-md); - border-radius: $r-pill; - margin-left: var(--tk-gap-sm); - flex-shrink: 0; - - &--pending { - @include tag($wrn-l, $wrn); - } - - &--verified { - @include tag($acc-l, $acc); - } - - &--cancelled { - @include tag($dan-l, $dan); - } - - &--expired { - @include tag($bd-l, $tx3); - } -} - -.order-status-text { - font-size: var(--tk-font-body); - font-weight: bold; -} - -.order-body { - padding: var(--tk-gap-md) var(--tk-gap-lg) var(--tk-section-gap); -} - -.order-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: var(--tk-gap-xs) 0; -} - -.order-row-label { - font-size: var(--tk-font-h1); +.order-date { + font-size: var(--tk-font-micro); color: $tx3; + display: block; } -.order-row-value { +.order-points { + display: flex; + align-items: baseline; + gap: 2px; + flex-shrink: 0; + margin-left: var(--tk-gap-sm); +} + +.order-points-value { @include serif-number; - font-size: var(--tk-font-h1); - color: $tx; - - &.order-cost { - color: var(--tk-pri); - font-weight: bold; - } + font-size: var(--tk-font-body); + font-weight: 700; + color: $pri; } -/* ===== 核销码 ===== */ +.order-points-unit { + font-size: var(--tk-font-micro); + color: $pri; + font-weight: 400; +} + +// 核销码 .order-qrcode { display: flex; align-items: center; - padding: var(--tk-gap-md); + padding: var(--tk-gap-sm); margin-top: var(--tk-gap-sm); - background: var(--tk-pri-l); + background: $pri-l; border-radius: $r-sm; + + &:active { + opacity: var(--tk-touch-feedback-opacity); + } } .qrcode-label { - font-size: var(--tk-font-h2); + font-size: var(--tk-font-cap); color: $tx3; margin-right: var(--tk-gap-xs); } .qrcode-value { @include serif-number; - font-size: var(--tk-font-h2); - color: var(--tk-pri-d); - font-weight: bold; + font-size: var(--tk-font-cap); + color: $pri-d; + font-weight: 700; flex: 1; overflow: hidden; text-overflow: ellipsis; @@ -116,8 +186,7 @@ } .qrcode-tap { - font-size: var(--tk-font-body); - color: var(--tk-pri); - margin-left: var(--tk-gap-xs); + font-size: var(--tk-font-cap); + color: $pri; flex-shrink: 0; } diff --git a/apps/miniprogram/src/pages/pkg-mall/orders/index.tsx b/apps/miniprogram/src/pages/pkg-mall/orders/index.tsx index 710eb53..199143c 100644 --- a/apps/miniprogram/src/pages/pkg-mall/orders/index.tsx +++ b/apps/miniprogram/src/pages/pkg-mall/orders/index.tsx @@ -7,21 +7,22 @@ import type { PointsOrder } from '../../../services/points'; import EmptyState from '../../../components/EmptyState'; import ErrorState from '../../../components/ErrorState'; import Loading from '../../../components/Loading'; -import SegmentTabs from '../../../components/SegmentTabs'; -import PageShell from '@/components/ui/PageShell'; -import ContentCard from '@/components/ui/ContentCard'; import { useElderClass } from '../../../hooks/useElderClass'; +import PageShell from '@/components/ui/PageShell'; import './index.scss'; const STATUS_TABS = [ { key: '', label: '全部' }, - { key: 'pending', label: '待核销' }, - { key: 'verified', label: '已核销' }, - { key: 'expired', label: '已过期' }, + { key: 'pending', label: '待处理' }, + { key: 'shipped', label: '已发货' }, + { key: 'verified', label: '已完成' }, ]; const STATUS_CONFIG: Record = { - pending: { label: '待核销', cls: 'order-status-tag--pending' }, + pending: { label: '待处理', cls: 'order-status-tag--pending' }, + approved: { label: '已审核', cls: 'order-status-tag--approved' }, + shipped: { label: '已发货', cls: 'order-status-tag--shipped' }, + completed: { label: '已完成', cls: 'order-status-tag--completed' }, verified: { label: '已核销', cls: 'order-status-tag--verified' }, cancelled: { label: '已取消', cls: 'order-status-tag--cancelled' }, expired: { label: '已过期', cls: 'order-status-tag--expired' }, @@ -76,7 +77,7 @@ export default function MallOrders() { usePageData( useCallback(async () => { - Taro.setNavigationBarTitle({ title: '我的订单' }); + Taro.setNavigationBarTitle({ title: '兑换记录' }); await loadAll(); }, [loadAll]), { throttleMs: 10000, enablePullDown: true }, @@ -103,19 +104,29 @@ export default function MallOrders() { }; const getStatusConfig = (status: string) => { - return STATUS_CONFIG[status] || { label: status, cls: 'order-status-tag--expired' }; + return STATUS_CONFIG[status] || { label: status, cls: 'order-status-tag--pending' }; }; const formatDate = (dateStr: string) => { if (!dateStr) return ''; const d = new Date(dateStr); - return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; }; return ( - + {/* 状态筛选标签 */} - + + {STATUS_TABS.map((tab) => ( + handleTabChange(tab.key)} + > + {tab.label} + + ))} + {/* 订单列表 */} {error ? ( @@ -133,30 +144,25 @@ export default function MallOrders() { {orders.map((order) => { const statusCfg = getStatusConfig(order.status); return ( - + - 商品 {order.product_id.slice(0, 8)} - + 订单号 {order.id.slice(0, 12).toUpperCase()} + {statusCfg.label} - - - 消耗积分 - - {order.points_cost.toLocaleString()} - + + + 商品 {order.product_id.slice(0, 8)} + {formatDate(order.created_at)} + + + {order.points_cost.toLocaleString()} + 积分 + - - 兑换时间 - - {formatDate(order.created_at)} - - - {order.status === 'pending' && ( + {order.status === 'pending' && order.qr_code && ( handleShowQrCode(order.qr_code)}> 核销码 {order.qr_code} @@ -164,7 +170,7 @@ export default function MallOrders() { )} - + ); })} {loading && } diff --git a/apps/miniprogram/src/pages/pkg-profile/family-add/index.scss b/apps/miniprogram/src/pages/pkg-profile/family-add/index.scss index 3e5eea6..df78935 100644 --- a/apps/miniprogram/src/pages/pkg-profile/family-add/index.scss +++ b/apps/miniprogram/src/pages/pkg-profile/family-add/index.scss @@ -1,28 +1,56 @@ @import '../../../styles/variables.scss'; -@import '../../../styles/mixins.scss'; -// PageShell 已接管:min-height, background, padding +// 就诊人建档/编辑 — 对齐原型 docs/design/mp-13-family-profile.html → FamilyAdd + .family-add-page { - padding-bottom: 160px; + padding-bottom: 120px; } -.page-title { - @include section-title; +.family-add-title { + font-family: 'Georgia', 'Times New Roman', serif; + font-size: var(--tk-font-h1); + font-weight: bold; + color: $tx; + margin-bottom: var(--tk-section-gap); + display: block; padding-left: var(--tk-gap-2xs); } +// 提示卡片 +.family-add-tip { + background: $pri-l; + border-radius: $r; + padding: 14px var(--tk-gap-md); + margin-bottom: var(--tk-section-gap); + border-left: 4px solid $pri; +} + +.family-add-tip-title { + font-size: var(--tk-font-body-sm); + font-weight: 600; + color: $pri; + margin-bottom: 4px; + display: block; +} + +.family-add-tip-desc { + font-size: var(--tk-font-cap); + color: $tx2; + line-height: 1.6; + display: block; +} + +// 表单卡片 .form-card { background: $card; border-radius: $r; - padding: var(--tk-gap-2xs) var(--tk-card-padding-lg); + overflow: hidden; box-shadow: $shadow-sm; } +// 表单项 — 垂直布局(标签在上,输入在下) .form-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--tk-card-padding-lg) 0; + padding: 14px var(--tk-gap-md); border-bottom: 1px solid $bd-l; &:last-child { @@ -31,39 +59,60 @@ } .form-label { - font-family: 'Georgia', 'Times New Roman', serif; - font-size: var(--tk-font-body-lg); - color: $tx; - flex-shrink: 0; - width: 140px; + font-size: var(--tk-font-cap); + color: $tx3; font-weight: 500; + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 2px; +} + +.form-required { + color: $dan; +} + +// 带边框的输入容器 +.form-input-wrap { + height: 44px; + background: $bg; + border: 1.5px solid $bd; + border-radius: $r-sm; + display: flex; + align-items: center; + padding: 0 14px; } .form-input { flex: 1; - font-size: var(--tk-font-body-lg); + font-size: 15px; color: $tx; - text-align: right; border: none; background: transparent; outline: none; + height: 100%; } .form-placeholder { + font-size: 15px; color: $tx3; } -.form-picker { +// 选择器容器 +.form-picker-wrap { + height: 44px; + background: $bg; + border: 1.5px solid $bd; + border-radius: $r-sm; display: flex; align-items: center; - flex: 1; - justify-content: flex-end; + justify-content: space-between; + padding: 0 14px; } .form-picker-text { - font-size: var(--tk-font-body-lg); + font-size: 15px; color: $tx; - margin-right: var(--tk-gap-sm); &.placeholder { color: $tx3; @@ -71,30 +120,33 @@ } .form-picker-arrow { - font-size: var(--tk-font-h2); + font-size: var(--tk-font-body-sm); color: $tx3; - font-family: 'Georgia', 'Times New Roman', serif; } +// 提交按钮 .submit-btn { - position: fixed; - bottom: 0; - left: 0; - right: 0; + margin-top: var(--tk-gap-lg); + height: var(--tk-btn-primary-h); + border-radius: $r; background: var(--tk-pri); - padding: var(--tk-card-padding-lg); - text-align: center; - box-shadow: 0 -2px 12px rgba($pri, 0.15); + display: flex; + align-items: center; + justify-content: center; + box-shadow: var(--tk-shadow-btn); &.disabled { opacity: 0.5; } + + &:active { + opacity: var(--tk-touch-feedback-opacity); + } } .submit-text { - font-family: 'Georgia', 'Times New Roman', serif; - font-size: var(--tk-font-num); + font-size: 17px; + font-weight: 600; color: $white; - font-weight: bold; letter-spacing: 2px; } diff --git a/apps/miniprogram/src/pages/pkg-profile/family-add/index.tsx b/apps/miniprogram/src/pages/pkg-profile/family-add/index.tsx index 54d1bb1..e6444f7 100644 --- a/apps/miniprogram/src/pages/pkg-profile/family-add/index.tsx +++ b/apps/miniprogram/src/pages/pkg-profile/family-add/index.tsx @@ -67,66 +67,102 @@ export default function FamilyAdd() { }; return ( - - {editId ? '编辑就诊人' : '添加就诊人'} + + {editId ? '编辑就诊人' : '添加就诊人'} + {/* 提示卡片 */} + + 完善个人信息 + + 完善信息后即可使用积分商城、签到等功能。请填写真实信息。 + + + + {/* 表单 */} - 姓名 - setName(e.detail.value)} - /> + 姓名* + + setName(e.detail.value)} + /> + - 关系 + 关系* setRelationIdx(Number(e.detail.value))} > - + {RELATION_OPTIONS[relationIdx]} - {'>'} + - 性别 + 性别* setGenderIdx(Number(e.detail.value))} > - + {GENDER_OPTIONS[genderIdx]} - {'>'} + - 出生日期 + 出生日期* setBirthDate(e.detail.value)} > - + {birthDate || '请选择'} - {'>'} + + + + 手机号 + + + + + + + 身份证号 + + + + = { + '本人': 'self', + '配偶': 'spouse', + '父母': 'parent', + '子女': 'child', + '其他': 'other', +}; + +function getRelationClass(relation: string): string { + return RELATION_CLASS[relation] || 'other'; +} + export default function FamilyList() { const modeClass = useElderClass(); const [patients, setPatients] = useState([]); @@ -57,25 +69,28 @@ export default function FamilyList() { return '未知'; }; - const relationInitial = (relation: string) => { - return relation ? relation.charAt(0) : '本'; + const birthYear = (d?: string) => { + if (!d) return ''; + return d.slice(0, 4) + '年'; }; return ( - + 就诊人管理 + 完善信息后即可使用积分商城、签到等功能。可添加多位家庭成员。 {patients.map((p) => { const isActive = currentPatient?.id === p.id; + const relClass = getRelationClass(p.relation || '本人'); return ( handleSelect(p)} > - - {relationInitial(p.relation || '本人')} + + {p.name.charAt(0)} @@ -83,8 +98,11 @@ export default function FamilyList() { {isActive && 当前} - {p.relation || '本人'} - {genderText(p.gender)} + + {p.relation || '本人'} + + {genderText(p.gender)} + {birthYear(p.birth_date) && {birthYear(p.birth_date)}} + + 添加就诊人 diff --git a/apps/miniprogram/src/services/points.ts b/apps/miniprogram/src/services/points.ts index a89125a..a60928a 100644 --- a/apps/miniprogram/src/services/points.ts +++ b/apps/miniprogram/src/services/points.ts @@ -56,6 +56,10 @@ export async function listProducts(params?: { return api.get('/health/points/products', params); } +export async function getProduct(productId: string) { + return api.get(`/health/points/products/${productId}`); +} + // ===== 兑换订单 ===== export interface PointsOrder {