/* 共用元件 — components.jsx */ /* ---------- 圖示(簡潔線性 icon) ---------- */ function Icon({ name, size = 18, stroke = 2 }) { const common = { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: stroke, strokeLinecap: "round", strokeLinejoin: "round" }; const paths = { chart: <>, user: <>, pencil: <>, upload: <>, spark: <>, users: <>, plus: <>, trash: <>, check: , chevron: , logout: <>, search: <>, file: <>, sheet: <>, calendar: <>, arrowUp: <>, arrowDown: <>, info: <>, lock: <>, mail: <>, award: <> }; return {paths[name] || null}; } /* ---------- 卡片 ---------- */ function Card({ children, style, pad = true, className }) { return (
{children}
); } /* ---------- 統計卡 ---------- */ function StatCard({ icon, label, value, suffix, tone, hint }) { return (
{label} {icon && }
{value}{suffix && {suffix}}
{hint &&
{hint}
}
); } /* ---------- 評量類型徽章 ---------- */ const TYPE_COLORS = { "作業": "type-a", "小考": "type-b", "課堂表現": "type-c", "分組活動": "type-d" }; function TypeBadge({ type }) { return {type}; } /* ---------- 成績色塊(紅黃綠) ---------- */ function ScoreChip({ score, size = "md" }) { const lvl = window.APPDATA.gradeLevel(score); return ( {score} ); } /* ---------- 風險燈號 ---------- */ function RiskLight({ level, size = 14 }) { return ; } /* ---------- 按鈕 ---------- */ function Button({ children, onClick, variant = "primary", icon, size, disabled, type, full }) { return ( ); } /* ---------- 區段標題 ---------- */ function SectionTitle({ icon, children, right }) { return (
{icon && } {children}
{right &&
{right}
}
); } /* ---------- 篩選 chip ---------- */ function FilterChip({ active, onClick, children }) { return ( ); } /* ---------- 頭像 ---------- */ function Avatar({ name, size = 36 }) { const ch = (name || "?").slice(0, 1); return ( {ch} ); } /* ---------- 小型長條分佈圖 ---------- */ function DistBar({ buckets }) { const max = Math.max(1, ...buckets.map(b => b.count)); return (
{buckets.map((b, i) => (
{b.count}
{b.label}
))}
); } /* ---------- 折線(學期趨勢,簡易 SVG) ---------- */ function TrendLine({ points, height = 90 }) { if (!points.length) return null; const w = 100, h = 100, pad = 8; const xs = points.map((_, i) => pad + (i / Math.max(1, points.length - 1)) * (w - pad * 2)); const ys = points.map(p => h - pad - (p / 100) * (h - pad * 2)); const d = xs.map((x, i) => (i === 0 ? "M" : "L") + x.toFixed(1) + " " + ys[i].toFixed(1)).join(" "); const area = d + " L" + xs[xs.length - 1].toFixed(1) + " " + (h - pad) + " L" + xs[0].toFixed(1) + " " + (h - pad) + " Z"; return ( {xs.map((x, i) => ( ))} ); } Object.assign(window, { Icon, Card, StatCard, TypeBadge, ScoreChip, RiskLight, Button, SectionTitle, FilterChip, Avatar, DistBar, TrendLine, TYPE_COLORS });