diff --git a/apps/miniprogram/config/index.ts b/apps/miniprogram/config/index.ts index 8cf0fe8..6182532 100644 --- a/apps/miniprogram/config/index.ts +++ b/apps/miniprogram/config/index.ts @@ -27,6 +27,9 @@ export default defineConfig(async (merge) => { mini: { compile: { exclude: [], + include: [ + require.resolve('zod').replace(/[\\/]index\.cjs$/, ''), + ], }, postcss: { pxtransform: { enable: true, config: {} }, diff --git a/apps/miniprogram/e2e/check-readiness.ts b/apps/miniprogram/e2e/check-readiness.ts index 4d5b7ce..92a58e3 100644 --- a/apps/miniprogram/e2e/check-readiness.ts +++ b/apps/miniprogram/e2e/check-readiness.ts @@ -13,6 +13,6 @@ async function check(url: string, label: string) { export default async function setup() { const apiBase = process.env.E2E_API_URL || 'http://localhost:3000'; - await check(`${apiBase}/health/live`, '后端 API'); + await check(`${apiBase}/api/v1/health`, '后端 API'); console.log('✅ 小程序 E2E 环境就绪'); } diff --git a/apps/miniprogram/src/components/TrendChart/index.tsx b/apps/miniprogram/src/components/TrendChart/index.tsx index b03f097..3c16ee5 100644 --- a/apps/miniprogram/src/components/TrendChart/index.tsx +++ b/apps/miniprogram/src/components/TrendChart/index.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useRef, useCallback } from 'react'; -import { View, Text } from '@tarojs/components'; -import EcCanvas from '../EcCanvas'; -import type { EcCanvasRef } from '../EcCanvas'; +import { Canvas, View, Text } from '@tarojs/components'; +import Taro from '@tarojs/taro'; import './index.scss'; interface TrendChartProps { @@ -12,6 +11,24 @@ interface TrendChartProps { height?: number; } +const DPR = Taro.getSystemInfoSync().pixelRatio || 2; + +function drawLine( + ctx: CanvasRenderingContext2D, + points: { x: number; y: number }[], +) { + if (points.length < 2) return; + ctx.beginPath(); + ctx.moveTo(points[0].x, points[0].y); + for (let i = 1; i < points.length; i++) { + const prev = points[i - 1]; + const curr = points[i]; + const cpx = (prev.x + curr.x) / 2; + ctx.bezierCurveTo(cpx, prev.y, cpx, curr.y, curr.x, curr.y); + } + ctx.stroke(); +} + export default React.memo(function TrendChart({ data, referenceMin, @@ -19,97 +36,132 @@ export default React.memo(function TrendChart({ unit = '', height = 500, }: TrendChartProps) { - const chartRef = useRef(null); + const canvasRef = useRef(null); - const getOption = useCallback(() => { - if (!data || data.length === 0) return null; + const draw = useCallback(() => { + const node = canvasRef.current; + if (!node || !data || data.length === 0) return; - const series: any[] = []; - const markArea: any = {}; + const w = node.width / DPR; + const h = node.height / DPR; + const ctx = node.getContext('2d'); + if (!ctx) return; + ctx.clearRect(0, 0, node.width, node.height); + ctx.save(); + ctx.scale(DPR, DPR); + + const pad = { left: 45, right: 15, top: 20, bottom: 30 }; + const cw = w - pad.left - pad.right; + const ch = h - pad.top - pad.bottom; + + const values = data.map((d) => d.value); + let yMin = Math.min(...values); + let yMax = Math.max(...values); + if (referenceMin != null) yMin = Math.min(yMin, referenceMin); + if (referenceMax != null) yMax = Math.max(yMax, referenceMax); + const yRange = yMax - yMin || 1; + const yPad = yRange * 0.1; + yMin -= yPad; + yMax += yPad; + const yTotal = yMax - yMin; + + const toX = (i: number) => pad.left + (i / Math.max(data.length - 1, 1)) * cw; + const toY = (v: number) => pad.top + ch - ((v - yMin) / yTotal) * ch; + + // Reference band if (referenceMin != null && referenceMax != null) { - markArea.data = [ - [ - { - yAxis: referenceMin, - itemStyle: { color: 'rgba(5,150,105,0.08)' }, - }, - { yAxis: referenceMax }, - ], - ]; + const ry1 = toY(referenceMax); + const ry2 = toY(referenceMin); + ctx.fillStyle = 'rgba(5,150,105,0.08)'; + ctx.fillRect(pad.left, ry1, cw, ry2 - ry1); } - series.push({ - type: 'line', - data: data.map((d) => d.value), - smooth: true, - symbol: 'circle', - symbolSize: 6, - lineStyle: { color: '#0891B2', width: 2 }, - itemStyle: { color: '#0891B2' }, - areaStyle: { - color: { - type: 'linear', - x: 0, - y: 0, - x2: 0, - y2: 1, - colorStops: [ - { offset: 0, color: 'rgba(8,145,178,0.15)' }, - { offset: 1, color: 'rgba(8,145,178,0.01)' }, - ], - }, - }, - markArea: markArea.data - ? { silent: true, data: markArea.data } - : undefined, - markPoint: - referenceMin != null && referenceMax != null - ? { - data: data - .filter((d) => d.value < referenceMin || d.value > referenceMax) - .map((d) => ({ - coord: [data.indexOf(d), d.value], - itemStyle: { color: '#DC2626' }, - symbolSize: 12, - })), - } - : undefined, - }); + // Grid lines + ctx.strokeStyle = '#F3F4F6'; + ctx.lineWidth = 1; + const gridLines = 4; + for (let i = 0; i <= gridLines; i++) { + const gy = pad.top + (ch / gridLines) * i; + ctx.beginPath(); + ctx.moveTo(pad.left, gy); + ctx.lineTo(pad.left + cw, gy); + ctx.stroke(); + } - return { - grid: { left: 45, right: 15, top: 20, bottom: 30 }, - xAxis: { - type: 'category', - data: data.map((d) => d.date.slice(5)), - axisLabel: { fontSize: 10, color: '#94A3B8' }, - axisLine: { lineStyle: { color: '#E5E7EB' } }, - }, - yAxis: { - type: 'value', - axisLabel: { fontSize: 10, color: '#94A3B8' }, - splitLine: { lineStyle: { color: '#F3F4F6' } }, - }, - tooltip: { - trigger: 'axis', - formatter: (params: any) => { - const p = params[0]; - const idx = p.dataIndex; - return `${data[idx]?.date || ''}\n${p.value}${unit ? ' ' + unit : ''}`; - }, - }, - series, - }; - }, [data, referenceMin, referenceMax, unit]); + // Y-axis labels + ctx.fillStyle = '#94A3B8'; + ctx.font = '10px sans-serif'; + ctx.textAlign = 'right'; + for (let i = 0; i <= gridLines; i++) { + const val = yMax - (yTotal / gridLines) * i; + const gy = pad.top + (ch / gridLines) * i; + ctx.fillText(val.toFixed(1), pad.left - 6, gy + 3); + } + + // X-axis labels + ctx.textAlign = 'center'; + const step = Math.max(1, Math.floor(data.length / 5)); + for (let i = 0; i < data.length; i += step) { + const lx = toX(i); + ctx.fillText(data[i].date.slice(5), lx, h - 8); + } + + // Area fill + const chartPoints = data.map((d, i) => ({ x: toX(i), y: toY(d.value) })); + ctx.beginPath(); + ctx.moveTo(chartPoints[0].x, toY(yMin)); + ctx.lineTo(chartPoints[0].x, chartPoints[0].y); + for (let i = 1; i < chartPoints.length; i++) { + const prev = chartPoints[i - 1]; + const curr = chartPoints[i]; + const cpx = (prev.x + curr.x) / 2; + ctx.bezierCurveTo(cpx, prev.y, cpx, curr.y, curr.x, curr.y); + } + ctx.lineTo(chartPoints[chartPoints.length - 1].x, toY(yMin)); + ctx.closePath(); + const grad = ctx.createLinearGradient(0, pad.top, 0, pad.top + ch); + grad.addColorStop(0, 'rgba(8,145,178,0.15)'); + grad.addColorStop(1, 'rgba(8,145,178,0.01)'); + ctx.fillStyle = grad; + ctx.fill(); + + // Line + ctx.strokeStyle = '#0891B2'; + ctx.lineWidth = 2; + drawLine(ctx, chartPoints); + + // Data points + for (let i = 0; i < data.length; i++) { + const d = data[i]; + const isAbnormal = + (referenceMin != null && d.value < referenceMin) || + (referenceMax != null && d.value > referenceMax); + ctx.beginPath(); + ctx.arc(chartPoints[i].x, chartPoints[i].y, isAbnormal ? 5 : 3, 0, Math.PI * 2); + ctx.fillStyle = isAbnormal ? '#DC2626' : '#0891B2'; + ctx.fill(); + } + + ctx.restore(); + }, [data, referenceMin, referenceMax]); useEffect(() => { - if (chartRef.current && data && data.length > 0) { - const option = getOption(); - if (option) { - chartRef.current.setOption(option); - } - } - }, [data, getOption]); + const query = Taro.createSelectorQuery(); + query + .select('#trend-chart-canvas') + .node() + .exec((res) => { + const node = res[0]?.node; + if (!node) return; + canvasRef.current = node; + const sysInfo = Taro.getSystemInfoSync(); + const canvasW = (sysInfo.windowWidth * 750) / sysInfo.windowWidth; + node.width = sysInfo.windowWidth * DPR; + node.height = ((height / 750) * sysInfo.windowWidth) * DPR; + draw(); + }); + }, [draw, height]); if (!data || data.length === 0) { return ( @@ -121,7 +173,11 @@ export default React.memo(function TrendChart({ return ( - + ); }); diff --git a/apps/miniprogram/src/pages/health/index.tsx b/apps/miniprogram/src/pages/health/index.tsx index 0f98e55..5ae2e0e 100644 --- a/apps/miniprogram/src/pages/health/index.tsx +++ b/apps/miniprogram/src/pages/health/index.tsx @@ -73,15 +73,15 @@ export default function Health() { }; const goToInput = () => { - Taro.navigateTo({ url: '/pages/health/input/index' }); + Taro.navigateTo({ url: '/pages/pkg-health/input/index' }); }; const goToDailyMonitoring = () => { - Taro.navigateTo({ url: '/pages/health/daily-monitoring/index' }); + Taro.navigateTo({ url: '/pages/pkg-health/daily-monitoring/index' }); }; const goToTrend = (indicator: string) => { - Taro.navigateTo({ url: `/pages/health/trend/index?indicator=${indicator}` }); + Taro.navigateTo({ url: `/pages/pkg-health/trend/index?indicator=${indicator}` }); }; const goToMall = () => { diff --git a/apps/miniprogram/src/pages/health/input/index.scss b/apps/miniprogram/src/pages/health/input/index.scss deleted file mode 100644 index a7415ee..0000000 --- a/apps/miniprogram/src/pages/health/input/index.scss +++ /dev/null @@ -1,204 +0,0 @@ -@import '../../../styles/variables.scss'; -@import '../../../styles/mixins.scss'; - -.input-page { - min-height: 100vh; - background: $bg; - padding: 0 0 60px; -} - -/* ── hero ── */ -.input-hero { - padding: 48px 32px 36px; - display: flex; - flex-direction: column; - align-items: center; -} - -.input-hero-icon { - @include flex-center; - width: 88px; - height: 88px; - border-radius: $r-lg; - background: $pri-l; - margin-bottom: 20px; -} - -.input-hero-icon-text { - font-family: 'Georgia', 'Times New Roman', serif; - font-size: 40px; - font-weight: bold; - color: $pri; -} - -.input-hero-title { - @include section-title; - font-size: 36px; - margin-bottom: 8px; -} - -.input-hero-sub { - font-size: 24px; - color: $tx3; -} - -/* ── card ── */ -.input-card { - background: $card; - border-radius: $r; - box-shadow: $shadow-md; - padding: 28px; - margin: 0 24px 20px; -} - -.input-card-header { - display: flex; - align-items: center; - gap: 14px; - margin-bottom: 20px; -} - -.input-card-indicator { - @include flex-center; - width: 44px; - height: 44px; - border-radius: $r-sm; - background: $acc-l; -} - -.input-card-indicator-char { - font-family: 'Georgia', 'Times New Roman', serif; - font-size: 24px; - font-weight: bold; - color: $acc; -} - -.input-card-label { - font-family: 'Georgia', 'Times New Roman', serif; - font-size: 28px; - font-weight: bold; - color: $tx; -} - -/* ── picker ── */ -.input-picker-row { - display: flex; - justify-content: space-between; - align-items: center; - background: $bg; - border-radius: $r-sm; - padding: 22px 24px; -} - -.input-picker-value { - font-size: 28px; - color: $tx; - @include serif-number; -} - -.input-picker-arrow { - font-family: 'Georgia', 'Times New Roman', serif; - font-size: 22px; - color: $tx3; - transform: rotate(180deg); - display: inline-block; -} - -/* ── section title ── */ -.input-section-title { - font-family: 'Georgia', 'Times New Roman', serif; - font-size: 28px; - font-weight: bold; - color: $tx; - margin-bottom: 16px; - display: block; -} - -/* ── blood pressure group ── */ -.input-bp-group { - display: flex; - align-items: flex-end; - gap: 12px; -} - -.input-bp-field { - flex: 1; -} - -.input-field-label { - font-size: 22px; - color: $tx2; - display: block; - margin-bottom: 8px; -} - -.input-bp-divider { - display: flex; - flex-direction: column; - align-items: center; - padding-bottom: 20px; - gap: 6px; -} - -.input-bp-line { - width: 16px; - height: 1px; - background: $bd; -} - -.input-bp-slash { - font-family: 'Georgia', 'Times New Roman', serif; - font-size: 36px; - color: $tx3; - font-weight: 300; -} - -/* ── input field ── */ -.input-field-box { - background: $bg; - border-radius: $r-sm; - padding: 20px 24px; - font-size: 28px; - color: $tx; - @include serif-number; - box-sizing: border-box; -} - -.input-field-full { - width: 100%; -} - -.input-field-unit { - font-size: 22px; - color: $tx3; - display: block; - margin-top: 10px; - font-style: italic; -} - -/* ── submit ── */ -.input-submit { - background: $pri; - border-radius: $r; - padding: 26px; - text-align: center; - margin: 48px 24px 0; - box-shadow: $shadow-md; - transition: opacity 0.2s; - - &:active { - opacity: 0.85; - } -} - -.input-submit-disabled { - opacity: 0.5; - box-shadow: none; -} - -.input-submit-text { - font-size: 32px; - color: white; - font-weight: bold; - letter-spacing: 2px; -} diff --git a/apps/miniprogram/src/pages/health/input/index.tsx b/apps/miniprogram/src/pages/health/input/index.tsx deleted file mode 100644 index 0223bbb..0000000 --- a/apps/miniprogram/src/pages/health/input/index.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import { useState } from 'react'; -import { View, Text, Input, Picker } from '@tarojs/components'; -import Taro from '@tarojs/taro'; -import { z } from 'zod'; -import { inputVitalSign } from '../../../services/health'; -import { useAuthStore } from '../../../stores/auth'; -import { useHealthStore } from '@/stores/health'; -import { usePointsStore } from '@/stores/points'; -import { clearRequestCache } from '@/services/request'; -import { trackEvent } from '@/services/analytics'; -import './index.scss'; - -const INDICATORS = [ - { value: 'blood_pressure', label: '血压 (mmHg)' }, - { value: 'heart_rate', label: '心率 (bpm)' }, - { value: 'blood_sugar_fasting', label: '空腹血糖 (mmol/L)' }, - { value: 'blood_sugar_postprandial', label: '餐后血糖 (mmol/L)' }, - { value: 'weight', label: '体重 (kg)' }, - { value: 'temperature', label: '体温 (℃)' }, -]; - -const vitalSignSchema = z.object({ - indicator_type: z.enum(['blood_pressure', 'heart_rate', 'blood_sugar_fasting', 'blood_sugar_postprandial', 'weight', 'temperature']), - value: z.number().positive({ message: '请输入有效数值' }), - extra: z.object({ - systolic: z.number().min(60, '收缩压过低').max(250, '收缩压过高,请及时就医').optional(), - diastolic: z.number().min(40, '舒张压过低').max(150, '舒张压过高,请及时就医').optional(), - }).optional(), - note: z.string().max(200, '备注不能超过200字').optional(), -}); - -const WARN_THRESHOLDS: Record = { - blood_pressure: { max: 180, warning: '收缩压偏高,建议及时就医' }, - heart_rate: { max: 120, min: 50, warning: '心率异常,请注意休息' }, - blood_sugar_fasting: { max: 11.0, warning: '血糖偏高,建议就医检查' }, -}; - -export default function HealthInput() { - const [indicatorIdx, setIndicatorIdx] = useState(0); - const [value, setValue] = useState(''); - const [systolic, setSystolic] = useState(''); - const [diastolic, setDiastolic] = useState(''); - const [note, setNote] = useState(''); - const [submitting, setSubmitting] = useState(false); - const { currentPatient } = useAuthStore(); - const { clearCache } = useHealthStore(); - - const handleSubmit = async () => { - if (!currentPatient) { - Taro.showToast({ title: '请先选择就诊人', icon: 'none' }); - return; - } - - const currentIndicator = INDICATORS[indicatorIdx].value; - - if (currentIndicator === 'blood_pressure') { - if (!systolic || !diastolic) { - Taro.showToast({ title: '请填写收缩压和舒张压', icon: 'none' }); - return; - } - } else { - if (!value) { - Taro.showToast({ title: '请输入数值', icon: 'none' }); - return; - } - } - - const input = currentIndicator === 'blood_pressure' - ? { indicator_type: 'blood_pressure' as const, value: parseFloat(systolic), extra: { systolic: parseFloat(systolic), diastolic: parseFloat(diastolic) } } - : { indicator_type: currentIndicator as any, value: parseFloat(value) }; - - const result = vitalSignSchema.safeParse(input); - if (!result.success) { - Taro.showToast({ title: result.error.issues[0].message, icon: 'none' }); - return; - } - - const threshold = WARN_THRESHOLDS[currentIndicator]; - if (threshold) { - const val = input.value; - if ((threshold.max && val > threshold.max) || (threshold.min && val < threshold.min)) { - await Taro.showModal({ title: '健康提示', content: threshold.warning, showCancel: false }); - } - } - - setSubmitting(true); - try { - await inputVitalSign(currentPatient.id, { - ...input, - note: note || undefined, - }); - clearCache(); - clearRequestCache('/health/'); - usePointsStore.getState().invalidate(); - Taro.showToast({ title: '录入成功', icon: 'success' }); - trackEvent('health_data_input', { type: currentIndicator }); - setTimeout(() => Taro.navigateBack(), 1000); - } catch (e: unknown) { - const msg = e instanceof Error ? e.message : '录入失败'; - Taro.showToast({ title: msg, icon: 'none' }); - } finally { - setSubmitting(false); - } - }; - - const indicatorInitial = INDICATORS[indicatorIdx].label.charAt(0); - - return ( - - {/* 页面标题 */} - - - - - 体征录入 - 记录今日健康数据 - - - {/* 指标类型选择 */} - - - - {indicatorInitial} - - 指标类型 - - i.label)} - value={indicatorIdx} - onChange={(e) => setIndicatorIdx(Number(e.detail.value))} - > - - {INDICATORS[indicatorIdx].label} - V - - - - - {/* 数值输入 */} - {INDICATORS[indicatorIdx].value === 'blood_pressure' ? ( - - 血压数值 - - - 收缩压 - setSystolic(e.detail.value)} - /> - - - - / - - - - 舒张压 - setDiastolic(e.detail.value)} - /> - - - mmHg - - ) : ( - - 检测数值 - setValue(e.detail.value)} - /> - - {INDICATORS[indicatorIdx].label.match(/\((.+)\)/)?.[1] || ''} - - - )} - - {/* 备注 */} - - 备注 - setNote(e.detail.value)} - /> - - - {/* 提交 */} - - {submitting ? '提交中...' : '提交录入'} - - - ); -} diff --git a/apps/miniprogram/src/pages/mall/index.tsx b/apps/miniprogram/src/pages/mall/index.tsx index cfd7957..85c8249 100644 --- a/apps/miniprogram/src/pages/mall/index.tsx +++ b/apps/miniprogram/src/pages/mall/index.tsx @@ -120,7 +120,7 @@ export default function Mall() { Taro.showToast({ title: '已兑完', icon: 'none' }); return; } - Taro.navigateTo({ url: `/pages/mall/exchange/index?product_id=${item.id}` }); + Taro.navigateTo({ url: `/pages/pkg-mall/exchange/index?product_id=${item.id}` }); }; const balance = account?.balance ?? 0; @@ -134,7 +134,7 @@ export default function Mall() { 请先完善个人档案 建档后即可使用积分商城、签到等功能 - Taro.navigateTo({ url: '/pages/profile/family-add/index' })}> + Taro.navigateTo({ url: '/pages/pkg-profile/family-add/index' })}> 去建档 diff --git a/apps/miniprogram/src/pages/health/daily-monitoring/index.scss b/apps/miniprogram/src/pages/pkg-health/daily-monitoring/index.scss similarity index 100% rename from apps/miniprogram/src/pages/health/daily-monitoring/index.scss rename to apps/miniprogram/src/pages/pkg-health/daily-monitoring/index.scss diff --git a/apps/miniprogram/src/pages/health/daily-monitoring/index.tsx b/apps/miniprogram/src/pages/pkg-health/daily-monitoring/index.tsx similarity index 100% rename from apps/miniprogram/src/pages/health/daily-monitoring/index.tsx rename to apps/miniprogram/src/pages/pkg-health/daily-monitoring/index.tsx diff --git a/apps/miniprogram/src/pages/health/trend/index.scss b/apps/miniprogram/src/pages/pkg-health/trend/index.scss similarity index 100% rename from apps/miniprogram/src/pages/health/trend/index.scss rename to apps/miniprogram/src/pages/pkg-health/trend/index.scss diff --git a/apps/miniprogram/src/pages/health/trend/index.tsx b/apps/miniprogram/src/pages/pkg-health/trend/index.tsx similarity index 100% rename from apps/miniprogram/src/pages/health/trend/index.tsx rename to apps/miniprogram/src/pages/pkg-health/trend/index.tsx diff --git a/apps/miniprogram/src/pages/mall/detail/index.scss b/apps/miniprogram/src/pages/pkg-mall/detail/index.scss similarity index 100% rename from apps/miniprogram/src/pages/mall/detail/index.scss rename to apps/miniprogram/src/pages/pkg-mall/detail/index.scss diff --git a/apps/miniprogram/src/pages/mall/detail/index.tsx b/apps/miniprogram/src/pages/pkg-mall/detail/index.tsx similarity index 100% rename from apps/miniprogram/src/pages/mall/detail/index.tsx rename to apps/miniprogram/src/pages/pkg-mall/detail/index.tsx diff --git a/apps/miniprogram/src/pages/mall/exchange/index.scss b/apps/miniprogram/src/pages/pkg-mall/exchange/index.scss similarity index 100% rename from apps/miniprogram/src/pages/mall/exchange/index.scss rename to apps/miniprogram/src/pages/pkg-mall/exchange/index.scss diff --git a/apps/miniprogram/src/pages/mall/exchange/index.tsx b/apps/miniprogram/src/pages/pkg-mall/exchange/index.tsx similarity index 99% rename from apps/miniprogram/src/pages/mall/exchange/index.tsx rename to apps/miniprogram/src/pages/pkg-mall/exchange/index.tsx index d3f29b7..e594e33 100644 --- a/apps/miniprogram/src/pages/mall/exchange/index.tsx +++ b/apps/miniprogram/src/pages/pkg-mall/exchange/index.tsx @@ -100,7 +100,7 @@ export default function ExchangeConfirm() { confirmText: '查看订单', success: () => { Taro.navigateTo({ - url: `/pages/mall/orders/index`, + url: `/pages/pkg-mall/orders/index`, }); }, }); diff --git a/apps/miniprogram/src/pages/mall/orders/index.scss b/apps/miniprogram/src/pages/pkg-mall/orders/index.scss similarity index 100% rename from apps/miniprogram/src/pages/mall/orders/index.scss rename to apps/miniprogram/src/pages/pkg-mall/orders/index.scss diff --git a/apps/miniprogram/src/pages/mall/orders/index.tsx b/apps/miniprogram/src/pages/pkg-mall/orders/index.tsx similarity index 100% rename from apps/miniprogram/src/pages/mall/orders/index.tsx rename to apps/miniprogram/src/pages/pkg-mall/orders/index.tsx diff --git a/apps/miniprogram/src/pages/profile/family-add/index.scss b/apps/miniprogram/src/pages/pkg-profile/family-add/index.scss similarity index 100% rename from apps/miniprogram/src/pages/profile/family-add/index.scss rename to apps/miniprogram/src/pages/pkg-profile/family-add/index.scss diff --git a/apps/miniprogram/src/pages/profile/family-add/index.tsx b/apps/miniprogram/src/pages/pkg-profile/family-add/index.tsx similarity index 100% rename from apps/miniprogram/src/pages/profile/family-add/index.tsx rename to apps/miniprogram/src/pages/pkg-profile/family-add/index.tsx diff --git a/apps/miniprogram/src/pages/profile/family/index.scss b/apps/miniprogram/src/pages/pkg-profile/family/index.scss similarity index 100% rename from apps/miniprogram/src/pages/profile/family/index.scss rename to apps/miniprogram/src/pages/pkg-profile/family/index.scss diff --git a/apps/miniprogram/src/pages/profile/family/index.tsx b/apps/miniprogram/src/pages/pkg-profile/family/index.tsx similarity index 95% rename from apps/miniprogram/src/pages/profile/family/index.tsx rename to apps/miniprogram/src/pages/pkg-profile/family/index.tsx index 83b8359..4651529 100644 --- a/apps/miniprogram/src/pages/profile/family/index.tsx +++ b/apps/miniprogram/src/pages/pkg-profile/family/index.tsx @@ -39,12 +39,12 @@ export default function FamilyList() { }; const goToAdd = () => { - Taro.navigateTo({ url: '/pages/profile/family-add/index' }); + Taro.navigateTo({ url: '/pages/pkg-profile/family-add/index' }); }; const goToEdit = (patient: Patient) => { Taro.setStorageSync('edit_patient', patient); - Taro.navigateTo({ url: `/pages/profile/family-add/index?id=${patient.id}` }); + Taro.navigateTo({ url: `/pages/pkg-profile/family-add/index?id=${patient.id}` }); }; const genderText = (g?: string) => { diff --git a/apps/miniprogram/src/pages/profile/followups/index.scss b/apps/miniprogram/src/pages/pkg-profile/followups/index.scss similarity index 100% rename from apps/miniprogram/src/pages/profile/followups/index.scss rename to apps/miniprogram/src/pages/pkg-profile/followups/index.scss diff --git a/apps/miniprogram/src/pages/profile/followups/index.tsx b/apps/miniprogram/src/pages/pkg-profile/followups/index.tsx similarity index 100% rename from apps/miniprogram/src/pages/profile/followups/index.tsx rename to apps/miniprogram/src/pages/pkg-profile/followups/index.tsx diff --git a/apps/miniprogram/src/pages/profile/medication/index.scss b/apps/miniprogram/src/pages/pkg-profile/medication/index.scss similarity index 100% rename from apps/miniprogram/src/pages/profile/medication/index.scss rename to apps/miniprogram/src/pages/pkg-profile/medication/index.scss diff --git a/apps/miniprogram/src/pages/profile/medication/index.tsx b/apps/miniprogram/src/pages/pkg-profile/medication/index.tsx similarity index 100% rename from apps/miniprogram/src/pages/profile/medication/index.tsx rename to apps/miniprogram/src/pages/pkg-profile/medication/index.tsx diff --git a/apps/miniprogram/src/pages/profile/reports/index.scss b/apps/miniprogram/src/pages/pkg-profile/reports/index.scss similarity index 100% rename from apps/miniprogram/src/pages/profile/reports/index.scss rename to apps/miniprogram/src/pages/pkg-profile/reports/index.scss diff --git a/apps/miniprogram/src/pages/profile/reports/index.tsx b/apps/miniprogram/src/pages/pkg-profile/reports/index.tsx similarity index 100% rename from apps/miniprogram/src/pages/profile/reports/index.tsx rename to apps/miniprogram/src/pages/pkg-profile/reports/index.tsx diff --git a/apps/miniprogram/src/pages/profile/settings/index.scss b/apps/miniprogram/src/pages/pkg-profile/settings/index.scss similarity index 100% rename from apps/miniprogram/src/pages/profile/settings/index.scss rename to apps/miniprogram/src/pages/pkg-profile/settings/index.scss diff --git a/apps/miniprogram/src/pages/profile/settings/index.tsx b/apps/miniprogram/src/pages/pkg-profile/settings/index.tsx similarity index 100% rename from apps/miniprogram/src/pages/profile/settings/index.tsx rename to apps/miniprogram/src/pages/pkg-profile/settings/index.tsx diff --git a/apps/miniprogram/src/pages/profile/index.tsx b/apps/miniprogram/src/pages/profile/index.tsx index d76e786..be87b6b 100644 --- a/apps/miniprogram/src/pages/profile/index.tsx +++ b/apps/miniprogram/src/pages/profile/index.tsx @@ -6,13 +6,13 @@ import { usePointsStore } from '../../stores/points'; import './index.scss'; const MENU_ITEMS = [ - { label: '我的订单', char: '单', path: '/pages/mall/orders/index' }, - { label: '积分明细', char: '明', path: '/pages/mall/detail/index' }, - { label: '就诊人管理', char: '人', path: '/pages/profile/family/index' }, - { label: '我的报告', char: '报', path: '/pages/profile/reports/index' }, - { label: '我的随访', char: '随', path: '/pages/profile/followups/index' }, - { label: '用药提醒', char: '药', path: '/pages/profile/medication/index' }, - { label: '设置', char: '设', path: '/pages/profile/settings/index' }, + { label: '我的订单', char: '单', path: '/pages/pkg-mall/orders/index' }, + { label: '积分明细', char: '明', path: '/pages/pkg-mall/detail/index' }, + { label: '就诊人管理', char: '人', path: '/pages/pkg-profile/family/index' }, + { label: '我的报告', char: '报', path: '/pages/pkg-profile/reports/index' }, + { label: '我的随访', char: '随', path: '/pages/pkg-profile/followups/index' }, + { label: '用药提醒', char: '药', path: '/pages/pkg-profile/medication/index' }, + { label: '设置', char: '设', path: '/pages/pkg-profile/settings/index' }, ]; export default function Profile() { @@ -53,7 +53,7 @@ export default function Profile() { {/* 积分余额 */} Taro.navigateTo({ url: '/pages/mall/detail/index' })} + onClick={() => Taro.navigateTo({ url: '/pages/pkg-mall/detail/index' })} > {(pointsAccount?.balance ?? 0).toLocaleString()} diff --git a/crates/erp-health/src/handler/points_handler.rs b/crates/erp-health/src/handler/points_handler.rs index 32fcb67..75fba16 100644 --- a/crates/erp-health/src/handler/points_handler.rs +++ b/crates/erp-health/src/handler/points_handler.rs @@ -23,6 +23,11 @@ pub struct ProductTypeParam { pub product_type: Option, } +#[derive(Debug, Deserialize, IntoParams)] +pub struct AdminProductFilter { + pub is_active: Option, +} + // --------------------------------------------------------------------------- // 患者端:积分账户 + 打卡 // --------------------------------------------------------------------------- @@ -259,6 +264,24 @@ where HealthState: FromRef, S: Clone + Send + Sync + 'static, Ok(Json(ApiResponse::ok(()))) } +pub async fn admin_list_products( + State(state): State, + Extension(ctx): Extension, + Query(params): Query, + Query(page): Query, + Query(filter): Query, +) -> Result>>, AppError> +where HealthState: FromRef, S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "health.points.list")?; + let p = page.page.unwrap_or(1); + let ps = page.page_size.unwrap_or(20); + let result = points_service::admin_list_products( + &state, ctx.tenant_id, params.product_type, filter.is_active, p, ps, + ).await?; + Ok(Json(ApiResponse::ok(result))) +} + pub async fn admin_create_product( State(state): State, Extension(ctx): Extension, diff --git a/crates/erp-health/src/service/points_service.rs b/crates/erp-health/src/service/points_service.rs index 3086848..e9c262a 100644 --- a/crates/erp-health/src/service/points_service.rs +++ b/crates/erp-health/src/service/points_service.rs @@ -589,6 +589,50 @@ pub async fn list_products( Ok(PaginatedResponse { data, total, page, page_size: limit, total_pages }) } +/// 管理端商品列表 — 不过滤 is_active,显示全部商品 +pub async fn admin_list_products( + state: &HealthState, + tenant_id: Uuid, + product_type: Option, + is_active: Option, + page: u64, + page_size: u64, +) -> HealthResult> { + let limit = page_size.min(100); + let offset = page.saturating_sub(1) * limit; + + let mut query = points_product::Entity::find() + .filter(points_product::Column::TenantId.eq(tenant_id)) + .filter(points_product::Column::DeletedAt.is_null()); + + if let Some(ref pt) = product_type { + query = query.filter(points_product::Column::ProductType.eq(pt.as_str())); + } + if let Some(active) = is_active { + query = query.filter(points_product::Column::IsActive.eq(active)); + } + + let total = query.clone().count(&state.db).await?; + let models = query + .order_by_asc(points_product::Column::SortOrder) + .order_by_desc(points_product::Column::CreatedAt) + .offset(offset) + .limit(limit) + .all(&state.db) + .await?; + + let total_pages = total.div_ceil(limit.max(1)); + let data = models.into_iter().map(|m| PointsProductResp { + id: m.id, name: m.name, product_type: m.product_type, + points_cost: m.points_cost, stock: m.stock, + image_url: m.image_url, description: m.description, + is_active: m.is_active, sort_order: m.sort_order, + created_at: m.created_at, updated_at: m.updated_at, version: m.version, + }).collect(); + + Ok(PaginatedResponse { data, total, page, page_size: limit, total_pages }) +} + pub async fn get_product( state: &HealthState, tenant_id: Uuid,