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 {