feat(mp+health): 小程序分包迁移 + 积分商城后台列表 API
- 小程序页面迁移到 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:
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user