feat(mp+health): 小程序分包迁移 + 积分商城后台列表 API
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

- 小程序页面迁移到 pkg-health/pkg-mall/pkg-profile 分包目录
- 删除旧 pages/health/input、pages/mall/detail 等旧路径
- 导航路径更新为分包路径(/pages/pkg-mall/exchange/index 等)
- TrendChart 组件优化
- 后台添加 admin_list_products API(支持查看已下架商品)
- config/index.ts 添加 defineConstants 环境变量
- mp e2e check-readiness 路径修正
This commit is contained in:
iven
2026-04-29 07:29:49 +08:00
parent 9015a2b85e
commit cb6f5cc651
32 changed files with 229 additions and 516 deletions

View File

@@ -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<EcCanvasRef>(null);
const canvasRef = useRef<any>(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 (
<View className='trend-chart' style={{ height: `${height}rpx` }}>
<EcCanvas canvasId='trend-chart-canvas' ref={chartRef} height={height} />
<Canvas
type='2d'
id='trend-chart-canvas'
style={{ width: '100%', height: '100%' }}
/>
</View>
);
});