Initial commit: secondary school grade archive system.
Add FastAPI/React app with Docker deployment, Ubuntu one-click install, and docs for junior/senior high score tracking and mistake bank. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
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 <div style={{ textAlign: 'center', padding: 40, color: '#999' }}>暂无成绩数据</div>
|
||||
}
|
||||
|
||||
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 = `<strong>${p.exam_date}</strong> (${typeLabel})<br/>占比: ${p.ratio_percent}%`
|
||||
if (p.title) html += `<br/>${p.title}`
|
||||
if (p.delta_percent !== null) {
|
||||
const sign = p.delta_percent > 0 ? '+' : ''
|
||||
html += `<br/>较上次: ${sign}${p.delta_percent}%`
|
||||
if (p.is_volatile) html += ' <span style="color:#fa8c16">[大幅波动]</span>'
|
||||
}
|
||||
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 (
|
||||
<div>
|
||||
<ReactECharts option={option} style={{ height: 400, width: '100%' }} notMerge />
|
||||
<p style={{ color: '#888', fontSize: 12, marginTop: 8 }}>
|
||||
波动阈值: {(threshold * 100).toFixed(0)}%,超过此变化幅度将高亮显示
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user