import ReactECharts from 'echarts-for-react' import type { TrendPoint } from '../types' import { EXAM_TYPE_LABELS } from '../types' interface Props { points: TrendPoint[] subjectName: string threshold: number } const COLORS = { up: '#52c41a', down: '#ff4d4f', flat: '#8c8c8c', volatile: '#fa8c16', } export default function TrendChart({ points, subjectName, threshold }: Props) { if (points.length === 0) { return
暂无成绩数据
} const dates = points.map((p) => p.exam_date) const values = points.map((p) => p.ratio_percent) const lineSeries = points.slice(1).map((point, i) => { let color = COLORS.flat if (point.direction === 'up') color = COLORS.up if (point.direction === 'down') color = COLORS.down return { type: 'line' as const, data: dates.map((_, idx) => (idx === i || idx === i + 1 ? values[idx] : null)), connectNulls: false, showSymbol: false, lineStyle: { width: 3, color }, tooltip: { show: false }, silent: true, } }) const markPoints = points .map((point, i) => ({ point, i })) .filter(({ point }) => point.is_volatile) .map(({ i }) => ({ coord: [dates[i], values[i]], symbol: 'circle', symbolSize: 18, itemStyle: { color: COLORS.volatile, borderColor: '#fff', borderWidth: 2, }, label: { show: false }, })) const option = { title: { text: `${subjectName} 成绩占比趋势`, left: 'center', textStyle: { fontSize: 16 }, }, tooltip: { trigger: 'axis', formatter: (params: Array<{ dataIndex: number; value: number }>) => { const idx = params[0]?.dataIndex ?? 0 const p = points[idx] if (!p) return '' const typeLabel = EXAM_TYPE_LABELS[p.exam_type] let html = `${p.exam_date} (${typeLabel})
占比: ${p.ratio_percent}%` if (p.title) html += `
${p.title}` if (p.delta_percent !== null) { const sign = p.delta_percent > 0 ? '+' : '' html += `
较上次: ${sign}${p.delta_percent}%` if (p.is_volatile) html += ' [大幅波动]' } return html }, }, grid: { left: 50, right: 30, top: 60, bottom: 50 }, xAxis: { type: 'category', data: dates, axisLabel: { rotate: 30 }, }, yAxis: { type: 'value', name: '占比 (%)', min: 0, max: 100, }, series: [ { type: 'line', data: values, symbol: 'circle', symbolSize: (_val: number, params: { dataIndex: number }) => points[params.dataIndex]?.is_volatile ? 14 : 8, itemStyle: { color: (params: { dataIndex: number }) => { const p = points[params.dataIndex] if (p?.is_volatile) return COLORS.volatile if (p?.direction === 'up') return COLORS.up if (p?.direction === 'down') return COLORS.down return '#1677ff' }, }, lineStyle: { opacity: 0 }, markPoint: markPoints.length ? { data: markPoints } : undefined, z: 10, }, ...lineSeries, ], legend: { bottom: 0, data: [ { name: '上升', itemStyle: { color: COLORS.up } }, { name: '下降', itemStyle: { color: COLORS.down } }, { name: '大幅波动', itemStyle: { color: COLORS.volatile } }, ], }, } return (

波动阈值: {(threshold * 100).toFixed(0)}%,超过此变化幅度将高亮显示

) }