- Add UI components: confirm-delete-dialog, empty-table-row, list-pagination, pagination, status-badge - Add form-fields directory for reusable form field components - Add hooks: use-action-mutation, use-action-query for server action integration - Add action-utils lib for action state helpers - Update a11y components, charts, global-search, onboarding-gate, question components - Update UI components: chip-nav, filter-bar, page-header, stat-card, stat-item, switch, table - Update hooks: use-action-with-toast, use-aria-live, use-debounce, use-local-storage, use-media-query, use-permission - Update lib: a11y, ai, audit-logger, auth-guard, bcrypt-utils, change-logger, download, excel, file-storage, http-utils, login-logger, password-policy, password-security-service, permissions, rate-limit, role-utils, search-params, session, storage-provider - Update types: action-state, permissions - Update i18n messages (en, zh-CN) for dashboard, diagnostic, grades, lesson-preparation, settings
167 lines
5.0 KiB
TypeScript
167 lines
5.0 KiB
TypeScript
"use client"
|
||
|
||
import type { ReactNode } from "react"
|
||
import { Bar, BarChart, CartesianGrid, Legend, XAxis, YAxis, Cell } from "recharts"
|
||
|
||
import {
|
||
ChartContainer,
|
||
ChartTooltip,
|
||
ChartTooltipContent,
|
||
type ChartConfig,
|
||
} from "@/shared/components/ui/chart"
|
||
import { cn } from "@/shared/lib/utils"
|
||
|
||
/**
|
||
* 柱状图:统一的 BarChart 配置(CartesianGrid + XAxis + YAxis + ChartTooltip + Bar)。
|
||
*
|
||
* 覆盖以下重复模式:
|
||
* - GradeDistributionChart(单 Bar + Cell 分桶着色)
|
||
* - ClassComparisonChart(多 Bar + Legend)
|
||
*
|
||
* 默认配置:
|
||
* - CartesianGrid: vertical=false, strokeDasharray="4 4", strokeOpacity=0.4
|
||
* - XAxis: tickLine=false, axisLine=false, tickMargin=8, 默认截断到 8 字符
|
||
* - YAxis: tickLine=false, axisLine=false
|
||
* - Bar: radius=[4,4,0,0]
|
||
*/
|
||
export interface BarSeries {
|
||
/** 数据字段名 */
|
||
dataKey: string
|
||
/** 图例名称 */
|
||
name: string
|
||
/** 颜色(CSS 变量或 hsl 值) */
|
||
color: string
|
||
/** 圆角(默认 [4, 4, 0, 0]) */
|
||
radius?: [number, number, number, number]
|
||
}
|
||
|
||
interface SimpleBarChartProps {
|
||
/** 图表数据 */
|
||
data: Array<Record<string, string | number>>
|
||
/** 柱系列配置(单条或多条) */
|
||
bars: BarSeries[]
|
||
/** X 轴数据字段名 */
|
||
xKey: string
|
||
/** Y 轴定义域(如 [0, 100];不传则不设置 domain) */
|
||
yDomain?: [number, number]
|
||
/** Y 轴是否允许小数(默认 true) */
|
||
yAllowDecimals?: boolean
|
||
/** Y 轴刻度格式化(如百分比) */
|
||
yTickFormatter?: (value: number) => string
|
||
/** X 轴刻度格式化(默认 "default"=截断到 8 字符;设为 null 则不格式化;传函数则自定义) */
|
||
xTickFormatter?: ((value: string) => string) | "default" | null
|
||
/** X 轴截断长度(默认 8) */
|
||
xTruncateLength?: number
|
||
/** Y 轴宽度(默认 36) */
|
||
yWidth?: number
|
||
/** 图表高度类名(默认 "h-[280px]") */
|
||
heightClassName?: string
|
||
/** 图表 margin */
|
||
margin?: { left: number; right: number; top: number; bottom: number }
|
||
/** 是否显示 Legend(多 Bar 时建议 true) */
|
||
showLegend?: boolean
|
||
/** Tooltip 宽度类名(默认 "w-[200px]") */
|
||
tooltipClassName?: string
|
||
/** 自定义 Tooltip formatter(用于自定义 tooltip 内容) */
|
||
tooltipFormatter?: (payload: unknown) => ReactNode
|
||
/** 按数据项着色的映射(key = xKey 值, value = 颜色);用于单 Bar 分桶着色 */
|
||
cellColors?: Record<string, string>
|
||
/** 自定义 SVG defs(如 patterns、gradients),渲染在 BarChart 内部 */
|
||
defs?: ReactNode
|
||
/** 容器额外类名 */
|
||
className?: string
|
||
}
|
||
|
||
const DEFAULT_X_TRUNCATE_LENGTH = 8
|
||
|
||
function makeXTickFormatter(truncateLength: number) {
|
||
return (value: string): string =>
|
||
value.length > truncateLength ? `${value.slice(0, truncateLength)}...` : value
|
||
}
|
||
|
||
export function SimpleBarChart({
|
||
data,
|
||
bars,
|
||
xKey,
|
||
yDomain,
|
||
yAllowDecimals = true,
|
||
yTickFormatter,
|
||
xTickFormatter = "default",
|
||
xTruncateLength = DEFAULT_X_TRUNCATE_LENGTH,
|
||
yWidth = 36,
|
||
heightClassName = "h-[280px]",
|
||
margin = { left: 8, right: 8, top: 8, bottom: 8 },
|
||
showLegend = false,
|
||
tooltipClassName = "w-[200px]",
|
||
tooltipFormatter,
|
||
cellColors,
|
||
defs,
|
||
className,
|
||
}: SimpleBarChartProps) {
|
||
const chartConfig: ChartConfig = {}
|
||
for (const b of bars) {
|
||
chartConfig[b.dataKey] = {
|
||
label: b.name,
|
||
color: b.color,
|
||
}
|
||
}
|
||
|
||
const resolvedXTickFormatter =
|
||
xTickFormatter === null
|
||
? undefined
|
||
: xTickFormatter === "default"
|
||
? makeXTickFormatter(xTruncateLength)
|
||
: xTickFormatter
|
||
|
||
const hasCellColors = !!cellColors && bars.length === 1
|
||
|
||
return (
|
||
<ChartContainer config={chartConfig} className={cn(heightClassName, "w-full", className)}>
|
||
<BarChart data={data} margin={margin}>
|
||
{defs}
|
||
<CartesianGrid vertical={false} strokeDasharray="4 4" strokeOpacity={0.4} />
|
||
<XAxis
|
||
dataKey={xKey}
|
||
tickLine={false}
|
||
axisLine={false}
|
||
tickMargin={8}
|
||
tickFormatter={resolvedXTickFormatter}
|
||
/>
|
||
<YAxis
|
||
domain={yDomain}
|
||
allowDecimals={yAllowDecimals}
|
||
tickLine={false}
|
||
axisLine={false}
|
||
tickFormatter={yTickFormatter}
|
||
width={yWidth}
|
||
/>
|
||
<ChartTooltip
|
||
content={
|
||
tooltipFormatter ? (
|
||
<ChartTooltipContent className={tooltipClassName} formatter={tooltipFormatter} />
|
||
) : (
|
||
<ChartTooltipContent className={tooltipClassName} />
|
||
)
|
||
}
|
||
/>
|
||
{showLegend ? <Legend /> : null}
|
||
{bars.map((b) => (
|
||
<Bar
|
||
key={b.dataKey}
|
||
dataKey={b.dataKey}
|
||
fill={`var(--color-${b.dataKey})`}
|
||
radius={b.radius ?? [4, 4, 0, 0]}
|
||
>
|
||
{hasCellColors && cellColors
|
||
? data.map((entry) => {
|
||
const cellKey = String(entry[xKey])
|
||
return <Cell key={cellKey} fill={cellColors[cellKey]} />
|
||
})
|
||
: null}
|
||
</Bar>
|
||
))}
|
||
</BarChart>
|
||
</ChartContainer>
|
||
)
|
||
}
|