import type { ReactNode } from "react"; import { ArrowLeft, ChevronDown } from "lucide-react"; import { Button } from "~/components/ui/button"; import { cn } from "~/lib/utils"; import type { AbnormalLevel, ModelGrade, ModelStatus } from "./modelData"; export function OperationsPageHeader({ title, description, actions, onBack, }: { title: string; description: string; actions?: ReactNode; onBack?: () => void; }) { return (
{onBack && ( )}
A CARD MODEL OPERATIONS

{title}

{description}

{actions &&
{actions}
}
); } const gradeClasses: Record = { A: "bg-success-soft text-success-strong hover:bg-success-soft/80", B: "bg-warning-soft text-warning hover:bg-warning-soft/80", C: "bg-danger-soft text-danger hover:bg-danger-soft/80", }; export function GradeBadge({ grade, onClick, suffix, }: { grade: ModelGrade; onClick?: () => void; suffix?: string; }) { const label = `${grade}${suffix ? ` ${suffix}` : ""}`; if (onClick) { return ( ); } return ( {label} ); } const abnormalClasses: Record = { 三级: "bg-danger-soft text-danger", 二级: "bg-warning-soft text-warning", 一级: "bg-brand-soft text-primary", 正常: "bg-success-soft text-success-strong", }; export function AbnormalBadge({ level }: { level: AbnormalLevel }) { return ( {level} ); } const statusClasses: Record = { 正常: "bg-success-soft text-success-strong", 陪跑: "bg-brand-soft text-primary", 陪跑结束: "bg-muted text-muted-foreground", 下线: "bg-muted text-muted-foreground", }; export function StatusBadge({ status }: { status: ModelStatus }) { return ( {status} ); } export function FilterSelect({ label, value, options, onChange, allLabel = "全部", className, }: { label: string; value: string; options: Array; onChange: (value: string) => void; allLabel?: string; className?: string; }) { return ( ); } export function MultiSelectFilter({ label, values, options, onChange, allLabel = "全部", className, }: { label: string; values: string[]; options: Array; onChange: (values: string[]) => void; allLabel?: string; className?: string; }) { const normalized = options.map((option) => typeof option === "string" ? { label: option, value: option } : option); const selectedLabels = normalized.filter((option) => values.includes(option.value)).map((option) => option.label); const summary = !selectedLabels.length ? allLabel : selectedLabels.length <= 2 ? selectedLabels.join("、") : `已选 ${selectedLabels.length} 项`; const toggle = (value: string) => { onChange(values.includes(value) ? values.filter((item) => item !== value) : [...values, value]); }; return (
{label}
{summary}
{values.length ? `已选 ${values.length} 项` : allLabel} {values.length > 0 && }
{normalized.map((option) => ( ))}
); } type Threshold = { value: number; label: string; tone?: "warning" | "danger"; }; export function MetricLineChart({ months, values, name, unit = "%", thresholds = [], comparison, }: { months: string[]; values: number[]; name: string; unit?: string; thresholds?: Threshold[]; comparison?: { name: string; values: number[]; tone?: "success" | "warning" }; }) { const width = 640; const height = 190; const left = 48; const right = 20; const top = 18; const bottom = 34; const plotWidth = width - left - right; const plotHeight = height - top - bottom; const allValues = [...values, ...(comparison?.values ?? []), ...thresholds.map((item) => item.value)]; const rawMin = Math.min(...allValues); const rawMax = Math.max(...allValues); const padding = Math.max(1, (rawMax - rawMin) * 0.16); const min = Math.max(0, rawMin - padding); const max = rawMax + padding; const range = Math.max(1, max - min); const x = (index: number) => left + (months.length <= 1 ? plotWidth / 2 : index * plotWidth / (months.length - 1)); const y = (value: number) => top + (max - value) / range * plotHeight; const path = values.map((value, index) => `${index === 0 ? "M" : "L"}${x(index)},${y(value)}`).join(" "); const comparisonPath = comparison?.values.map((value, index) => `${index === 0 ? "M" : "L"}${x(index)},${y(value)}`).join(" ") ?? ""; const ticks = Array.from({ length: 4 }, (_, index) => max - range * index / 3); return (
{ticks.map((tick) => ( {tick.toFixed(1)}{unit} ))} {thresholds.map((threshold) => ( {threshold.label} ))} {values.map((value, index) => ( ))} {comparison && } {comparison?.values.map((value, index) => ( ))} {months.map((month, index) => ( {month} ))}
{name} {comparison && <>{comparison.name}}
); } export function SortingComboChart({ bins, counts, badRates, }: { bins: readonly string[]; counts: readonly number[]; badRates: readonly number[]; }) { const width = 760; const height = 300; const left = 56; const right = 54; const top = 22; const bottom = 52; const plotWidth = width - left - right; const plotHeight = height - top - bottom; const maxCount = Math.ceil(Math.max(...counts) / 1000) * 1000; const maxRate = 12; const step = plotWidth / bins.length; const barWidth = step * 0.34; const x = (index: number) => left + step * index + step / 2; const countY = (value: number) => top + (1 - value / maxCount) * plotHeight; const rateY = (value: number) => top + (1 - value / maxRate) * plotHeight; const linePath = badRates.map((value, index) => `${index === 0 ? "M" : "L"}${x(index)},${rateY(value)}`).join(" "); const rateTicks = [0, 2, 4, 6, 8, 10, 12]; return (
{rateTicks.map((tick) => ( {tick.toFixed(0)}% {Math.round(tick / maxRate * maxCount)} ))} {counts.map((count, index) => ( {count} {bins[index]} ))} {badRates.map((rate, index) => ( {rate.toFixed(2)}% ))}
客户数 坏客户占比
); }