feat(grades): gpa formula system - linear per-band gp (start~top), course category weights (editable), formula display, seed aligned to school standard
This commit is contained in:
+54
-26
@@ -317,12 +317,12 @@ function ensureBuiltin(db) {
|
||||
['维度:兴趣生活', '兴趣生活', '#db2777', 40]
|
||||
]);
|
||||
|
||||
seed('grade_band', ['label', 'color', 'min', 'max', 'sort', 'uuid', 'hlc', 'origin'], [
|
||||
['梯度:优秀', '优秀', '#16803c', 90, 100, 10],
|
||||
['梯度:良好', '良好', '#0891b2', 80, 90, 20],
|
||||
['梯度:中等', '中等', '#d97706', 70, 80, 30],
|
||||
['梯度:及格', '及格', '#ea580c', 60, 70, 40],
|
||||
['梯度:不及格', '不及格', '#dc2626', 0, 60, 50]
|
||||
seed('grade_band', ['label', 'color', 'min', 'max', 'sort', 'gp', 'gp_top', 'uuid', 'hlc', 'origin'], [
|
||||
['梯度:优秀', '优秀', '#16803c', 90, 101, 10, 4.0, 5.0],
|
||||
['梯度:良好', '良好', '#0891b2', 80, 90, 20, 3.0, 3.9],
|
||||
['梯度:中等', '中等', '#d97706', 70, 80, 30, 2.0, 2.9],
|
||||
['梯度:及格', '及格', '#ea580c', 60, 70, 40, 1.0, 1.9],
|
||||
['梯度:不及格', '不及格', '#dc2626', 0, 60, 50, 0, 0]
|
||||
]);
|
||||
|
||||
seed('metric_def', ['key', 'label', 'unit', 'agg', 'section', 'uuid', 'hlc', 'origin'], [
|
||||
@@ -338,9 +338,16 @@ function openDb(dbPath) {
|
||||
d.exec(fs.readFileSync(path.join(__dirname, '..', 'schema.sql'), 'utf8'));
|
||||
// 个人信息表兜底(确保任何挂载形态下都存在)
|
||||
d.exec("CREATE TABLE IF NOT EXISTS profile (id INTEGER PRIMARY KEY AUTOINCREMENT, uuid TEXT, content TEXT NOT NULL DEFAULT '{}', deleted_at TEXT, origin TEXT, hlc TEXT)");
|
||||
// 绩点公式:梯度带携带每档绩点(gp),各校规则不同可自行修改
|
||||
// 绩点公式:梯度带携带每档绩点(gp 起)与满档绩点(gp_top,档内线性折算),各校规则不同可自行修改
|
||||
const hasGp = d.prepare("SELECT COUNT(*) AS c FROM pragma_table_info('grade_band') WHERE name='gp'").get().c;
|
||||
if (Number(hasGp) === 0) d.exec("ALTER TABLE grade_band ADD COLUMN gp REAL");
|
||||
const hasGpTop = d.prepare("SELECT COUNT(*) AS c FROM pragma_table_info('grade_band') WHERE name='gp_top'").get().c;
|
||||
if (Number(hasGpTop) === 0) d.exec("ALTER TABLE grade_band ADD COLUMN gp_top REAL");
|
||||
// 课程类别(权重系数)
|
||||
const hasCat = d.prepare("SELECT COUNT(*) AS c FROM pragma_table_info('course_grade') WHERE name='category'").get().c;
|
||||
if (Number(hasCat) === 0) d.exec("ALTER TABLE course_grade ADD COLUMN category TEXT");
|
||||
// 应用设置(绩点权重等)
|
||||
d.exec("CREATE TABLE IF NOT EXISTS app_settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
|
||||
migrateSync(d);
|
||||
ensureBuiltin(d);
|
||||
alignBuiltinUuids(d);
|
||||
@@ -417,20 +424,20 @@ export const DEMO_PROFILE = {
|
||||
// 学期: { term, gpa, rank, total, courses: [课程名, 学分, 分数, 绩点][] }
|
||||
export const DEMO_SEMESTERS = [
|
||||
{ term: '2023-2024-1', gpa: 3.42, rank: 38, total: 156, courses: [
|
||||
['高等数学(上)', 5, 78, 3.0], ['大学英语(一)', 3, 85, 3.7], ['程序设计基础', 4, 91, 4.0],
|
||||
['思想道德与法治', 2, 88, 3.9], ['体育(一)', 1, 82, 3.5]
|
||||
['高等数学(上)', 5, 78, 3.0, '必修课'], ['大学英语(一)', 3, 85, 3.7, '必修课'], ['程序设计基础', 4, 91, 4.0, '必修课'],
|
||||
['思想道德与法治', 2, 88, 3.9, '必修课'], ['体育(一)', 1, 82, 3.5, '必修课']
|
||||
]},
|
||||
{ term: '2023-2024-2', gpa: 3.61, rank: 29, total: 156, courses: [
|
||||
['高等数学(下)', 5, 84, 3.7], ['线性代数', 3, 76, 2.9], ['数据结构', 4, 90, 4.0],
|
||||
['大学英语(二)', 3, 87, 3.8], ['中国近现代史纲要', 2, 84, 3.5], ['体育(二)', 1, 86, 3.8]
|
||||
['高等数学(下)', 5, 84, 3.7, '必修课'], ['线性代数', 3, 76, 2.9, '必修课'], ['数据结构', 4, 90, 4.0, '必修课'],
|
||||
['大学英语(二)', 3, 87, 3.8, '必修课'], ['中国近现代史纲要', 2, 84, 3.5, '必修课'], ['体育(二)', 1, 86, 3.8, '必修课']
|
||||
]},
|
||||
{ term: '2024-2025-1', gpa: 3.78, rank: 18, total: 156, courses: [
|
||||
['概率论与数理统计', 3, 88, 3.9], ['操作系统', 4, 92, 4.0], ['计算机网络', 4, 86, 3.8],
|
||||
['数据库原理', 3, 94, 4.0], ['马克思主义基本原理', 2, 81, 3.3]
|
||||
['概率论与数理统计', 3, 88, 3.9, '必修课'], ['操作系统', 4, 92, 4.0, '必修课'], ['计算机网络', 4, 86, 3.8, '必修课'],
|
||||
['数据库原理', 3, 94, 4.0, '必修课'], ['马克思主义基本原理', 2, 81, 3.3, '必修课']
|
||||
]},
|
||||
{ term: '2024-2025-2', gpa: 3.85, rank: 12, total: 156, courses: [
|
||||
['软件工程', 3, 90, 4.0], ['信息安全导论', 3, 95, 4.0], ['密码学基础', 3, 89, 3.9],
|
||||
['Web 应用开发', 2, 96, 4.0], ['数字逻辑', 3, 79, 3.1], ['体育(四)', 1, 90, 4.0]
|
||||
['软件工程', 3, 90, 4.0, '必修课'], ['信息安全导论', 3, 95, 4.0, '必修课'], ['密码学基础', 3, 89, 3.9, '必修课'],
|
||||
['Web 应用开发', 2, 96, 4.0, '专业选修课'], ['数字逻辑', 3, 79, 3.1, '必修课'], ['体育(四)', 1, 90, 4.0, '必修课']
|
||||
]}
|
||||
];
|
||||
|
||||
@@ -892,7 +899,7 @@ function listFootprints(d, query) {
|
||||
if (score == null) return null;
|
||||
let hit = bands.find(b => score >= b.min && score < b.max);
|
||||
if (!hit) hit = bands.find(b => score >= b.min && score <= b.max);
|
||||
return hit ? { id: hit.id, label: hit.label, color: hit.color, gp: hit.gp ?? null } : null;
|
||||
return hit ? { id: hit.id, label: hit.label, color: hit.color, gp: hit.gp ?? null, gpTop: hit.gp_top ?? null } : null;
|
||||
};
|
||||
const semesters = d.prepare('SELECT * FROM semester WHERE ' + DEL + ' ORDER BY term DESC').all();
|
||||
const activeSemIds = new Set(semesters.map(s => s.id));
|
||||
@@ -922,9 +929,15 @@ function listFootprints(d, query) {
|
||||
.filter(s => s.gpa != null)
|
||||
.sort((a, b) => a.term.localeCompare(b.term))
|
||||
.map(s => ({ term: s.term, gpa: s.gpa, avg: s.stats.avg }));
|
||||
let weights = { 必修课: 1.2, 专业选修课: 1.1, 人文素质选修课: 1.0, 其他: 1.0 };
|
||||
try {
|
||||
const wr = d.prepare("SELECT value FROM app_settings WHERE key='gpa_formula'").get();
|
||||
if (wr) weights = { ...weights, ...JSON.parse(wr.value).weights };
|
||||
} catch {}
|
||||
return {
|
||||
semesters,
|
||||
bands,
|
||||
weights,
|
||||
overall: {
|
||||
avg: overallAvg,
|
||||
credits: Math.round(courses.reduce((a, c) => a + (c.credit || 0), 0) * 10) / 10,
|
||||
@@ -982,14 +995,14 @@ function listFootprints(d, query) {
|
||||
withTx(d, () => {
|
||||
if (id) {
|
||||
const h = hlcNext(d);
|
||||
d.prepare('UPDATE course_grade SET semester_id=?, course_name=?, credit=?, score=?, grade_point=?, hlc=? WHERE id=?')
|
||||
.run(p.semester_id, name, p.credit ?? 0, p.score ?? null, p.grade_point ?? null, h, id);
|
||||
d.prepare('UPDATE course_grade SET semester_id=?, course_name=?, credit=?, score=?, grade_point=?, category=?, hlc=? WHERE id=?')
|
||||
.run(p.semester_id, name, p.credit ?? 0, p.score ?? null, p.grade_point ?? null, p.category ?? null, h, id);
|
||||
touchOplog(d, 'course_grade', getUuid(d, 'course_grade', id), 'upsert', h);
|
||||
} else {
|
||||
const h = hlcNext(d);
|
||||
const uuid = uuidv7();
|
||||
id = Number(d.prepare('INSERT INTO course_grade (semester_id, course_name, credit, score, grade_point, uuid, hlc, origin) VALUES (?,?,?,?,?,?,?,?)')
|
||||
.run(p.semester_id, name, p.credit ?? 0, p.score ?? null, p.grade_point ?? null, uuid, h, getDeviceId(d)).lastInsertRowid);
|
||||
id = Number(d.prepare('INSERT INTO course_grade (semester_id, course_name, credit, score, grade_point, category, uuid, hlc, origin) VALUES (?,?,?,?,?,?,?,?,?)')
|
||||
.run(p.semester_id, name, p.credit ?? 0, p.score ?? null, p.grade_point ?? null, p.category ?? null, uuid, h, getDeviceId(d)).lastInsertRowid);
|
||||
touchOplog(d, 'course_grade', uuid, 'upsert', h);
|
||||
}
|
||||
});
|
||||
@@ -1010,14 +1023,14 @@ function listFootprints(d, query) {
|
||||
withTx(d, () => {
|
||||
if (id) {
|
||||
const h = hlcNext(d);
|
||||
d.prepare('UPDATE grade_band SET label=?, color=?, min=?, max=?, sort=?, gp=?, hlc=? WHERE id=?')
|
||||
.run(label, p.color ?? '#64748b', p.min, p.max, p.sort ?? 100, p.gp ?? null, h, id);
|
||||
d.prepare('UPDATE grade_band SET label=?, color=?, min=?, max=?, sort=?, gp=?, gp_top=?, hlc=? WHERE id=?')
|
||||
.run(label, p.color ?? '#64748b', p.min, p.max, p.sort ?? 100, p.gp ?? null, p.gpTop ?? null, h, id);
|
||||
touchOplog(d, 'grade_band', getUuid(d, 'grade_band', id), 'upsert', h);
|
||||
} else {
|
||||
const h = hlcNext(d);
|
||||
const uuid = uuidv7();
|
||||
id = Number(d.prepare('INSERT INTO grade_band (label, color, min, max, sort, gp, uuid, hlc, origin) VALUES (?,?,?,?,?,?,?,?,?)')
|
||||
.run(label, p.color ?? '#64748b', p.min, p.max, p.sort ?? 100, p.gp ?? null, uuid, h, getDeviceId(d)).lastInsertRowid);
|
||||
id = Number(d.prepare('INSERT INTO grade_band (label, color, min, max, sort, gp, gp_top, uuid, hlc, origin) VALUES (?,?,?,?,?,?,?,?,?,?)')
|
||||
.run(label, p.color ?? '#64748b', p.min, p.max, p.sort ?? 100, p.gp ?? null, p.gpTop ?? null, uuid, h, getDeviceId(d)).lastInsertRowid);
|
||||
touchOplog(d, 'grade_band', uuid, 'upsert', h);
|
||||
}
|
||||
});
|
||||
@@ -1125,6 +1138,21 @@ function listFootprints(d, query) {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
case 'gpa_settings_get': {
|
||||
const row = d.prepare("SELECT value FROM app_settings WHERE key='gpa_formula'").get();
|
||||
let weights = { 必修课: 1.2, 专业选修课: 1.1, 人文素质选修课: 1.0, 其他: 1.0 };
|
||||
if (row) { try { weights = { ...weights, ...JSON.parse(row.value).weights }; } catch {} }
|
||||
return { weights };
|
||||
}
|
||||
|
||||
case 'gpa_settings_upsert': {
|
||||
const p = payload;
|
||||
const weights = p.weights ?? {};
|
||||
d.prepare("INSERT INTO app_settings (key, value) VALUES ('gpa_formula', ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value")
|
||||
.run(JSON.stringify({ weights }));
|
||||
return { ok: true, weights };
|
||||
}
|
||||
|
||||
case 'profile_get': {
|
||||
// 个人信息(单行 JSON):供个人信息页与简历生成使用
|
||||
const row = d.prepare('SELECT * FROM profile WHERE deleted_at IS NULL ORDER BY id DESC LIMIT 1').get();
|
||||
@@ -1209,8 +1237,8 @@ function listFootprints(d, query) {
|
||||
for (const s of DEMO_SEMESTERS) {
|
||||
const r = handleCommand('semester_upsert', { term: s.term, gpa: s.gpa, rank: s.rank, total_students: s.total });
|
||||
const sid = r.item.id;
|
||||
for (const [name, credit, score, gp] of s.courses) {
|
||||
handleCommand('course_upsert', { semester_id: sid, course_name: name, credit, score, grade_point: gp });
|
||||
for (const [name, credit, score, gp, category] of s.courses) {
|
||||
handleCommand('course_upsert', { semester_id: sid, course_name: name, credit, score, grade_point: gp, category });
|
||||
}
|
||||
sems++;
|
||||
}
|
||||
|
||||
+5
-1
@@ -123,7 +123,7 @@ export const gradesOverview = () => call<GradesOverview>('grades_overview');
|
||||
export const semesterUpsert = (p: { id?: number; term: string; gpa?: number | null; rank?: number | null; total_students?: number | null }) =>
|
||||
call<{ item: { id: number } }>('semester_upsert', p);
|
||||
export const semesterDelete = (id: number) => call<{ ok: true }>('semester_delete', { id });
|
||||
export const courseUpsert = (p: { id?: number; semester_id: number; course_name: string; credit: number; score?: number | null; grade_point?: number | null }) =>
|
||||
export const courseUpsert = (p: { id?: number; semester_id: number; course_name: string; credit: number; score?: number | null; grade_point?: number | null; category?: string | null }) =>
|
||||
call<{ item: { id: number } }>('course_upsert', p);
|
||||
export const courseDelete = (id: number) => call<{ ok: true }>('course_delete', { id });
|
||||
export const bandUpsert = (p: GradeBand) => call<{ item: { id: number } }>('band_upsert', p);
|
||||
@@ -218,6 +218,10 @@ export const adminDeleteUser = (userId: number | string, baseUrl?: string) =>
|
||||
export const adminDeleteInvite = (id: number, baseUrl?: string) =>
|
||||
adminCall<{ deleted: number }>('/admin/invites/delete', { id }, baseUrl);
|
||||
|
||||
// ---- 绩点公式设置 ----
|
||||
export const gpaSettingsGet = () => call<{ weights: Record<string, number> }>('gpa_settings_get', {});
|
||||
export const gpaSettingsUpsert = (weights: Record<string, number>) => call<{ ok: true; weights: Record<string, number> }>('gpa_settings_upsert', { weights });
|
||||
|
||||
// ---- 个人信息 / 简历 ----
|
||||
export const profileGet = () => call<{ content: { info?: Partial<ProfileInfo>; resume?: ResumeConfig } }>('profile_get', {});
|
||||
export const profileUpsert = (content: { info?: Partial<ProfileInfo>; resume?: ResumeConfig }) =>
|
||||
|
||||
@@ -196,6 +196,7 @@ export interface GradeBand {
|
||||
max: number;
|
||||
sort: number;
|
||||
gp?: number | null;
|
||||
gpTop?: number | null;
|
||||
}
|
||||
|
||||
export interface BandInfo { id: number; label: string; color: string; }
|
||||
@@ -207,6 +208,7 @@ export interface CourseGrade {
|
||||
credit: number;
|
||||
score?: number | null;
|
||||
grade_point?: number | null;
|
||||
category?: string | null;
|
||||
band?: BandInfo | null;
|
||||
}
|
||||
|
||||
@@ -230,6 +232,7 @@ export interface GradesOverview {
|
||||
gpa_trend: { term: string; gpa: number; avg: number | null }[];
|
||||
tier_dist: { label: string; color: string; count: number }[];
|
||||
};
|
||||
weights?: Record<string, number>;
|
||||
}
|
||||
|
||||
// ---------- 个人信息 ----------
|
||||
|
||||
@@ -5,9 +5,9 @@ import { BarChart, LineChart, PieChart } from 'echarts/charts';
|
||||
import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import { Plus, Trash2, Pencil, Calculator, GraduationCap } from 'lucide-react';
|
||||
import { bandDelete, bandUpsert, courseDelete, courseUpsert, gradesOverview, semesterDelete, semesterUpsert } from '@/lib/api';
|
||||
import { bandDelete, bandUpsert, courseDelete, courseUpsert, gpaSettingsUpsert, gradesOverview, semesterDelete, semesterUpsert } from '@/lib/api';
|
||||
import type { CourseGrade, GradeBand, SemesterGrades } from '@/lib/types';
|
||||
import { Badge, Button, Card, CardBody, CardHeader, Confirm, Dialog, Empty, Field, Input, useToast } from '@/components/ui';
|
||||
import { Badge, Button, Card, CardBody, CardHeader, Confirm, Dialog, Empty, Field, Input, Select, useToast } from '@/components/ui';
|
||||
import { useChart, inkColor, useThemeTick } from '@/hooks/useChart';
|
||||
|
||||
echarts.use([BarChart, LineChart, PieChart, GridComponent, TooltipComponent, LegendComponent, CanvasRenderer]);
|
||||
@@ -17,20 +17,31 @@ function termYear(term: string): string {
|
||||
return m ? m[1] + '~' + m[2] + ' 学年' : '其他学期';
|
||||
}
|
||||
|
||||
function autoGpa(courses: CourseGrade[], bands?: { min: number; max: number; gp?: number | null }[]): number | null {
|
||||
// 按梯度公式补算缺失绩点:分数落在哪一档 → 该档绩点(各校规则可在梯度设置中修改)
|
||||
function autoGpa(courses: CourseGrade[], bands?: { min: number; max: number; gp?: number | null; gpTop?: number | null }[], weights?: Record<string, number>): number | null {
|
||||
// 绩点公式(各校规则不同,梯度设置中可改):
|
||||
// 课程绩点 = 档内线性折算(分数落在哪档 → gp(档首)~gpTop(档顶) 插值;未填 gpTop 则取固定档绩点)
|
||||
// 课程学分绩点 = 课程绩点 × 课程权重系数 × 课程学分
|
||||
// 平均学分绩点 GPA = Σ(课程学分绩点) ÷ Σ(课程学分)
|
||||
const gpOf = (c: CourseGrade): number | null => {
|
||||
if (c.grade_point != null) return c.grade_point;
|
||||
if (bands && c.score != null) {
|
||||
const hit = bands.find(b => c.score! >= b.min && (c.score! < b.max || (b.max >= 100 && c.score! <= b.max)));
|
||||
if (hit && hit.gp != null) return hit.gp;
|
||||
const b = bands.find(x => c.score! >= x.min && (c.score! < x.max || (x.max >= 100 && c.score! <= x.max)));
|
||||
if (b && b.gp != null) {
|
||||
if (b.gpTop != null) {
|
||||
const topScore = b.max >= 100 ? b.max : b.max - 1;
|
||||
const slope = (b.gpTop - b.gp) / Math.max(1, topScore - b.min);
|
||||
return Math.round((b.gp + (c.score! - b.min) * slope) * 100) / 100;
|
||||
}
|
||||
return b.gp;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const wOf = (c: CourseGrade): number => (weights ? (weights[c.category ?? '其他'] ?? weights['其他'] ?? 1) : 1);
|
||||
const withGp = courses.map(c => ({ ...c, grade_point: gpOf(c) })).filter(c => c.grade_point != null && c.credit > 0);
|
||||
if (!withGp.length) return null;
|
||||
const credits = withGp.reduce((a, c) => a + c.credit, 0);
|
||||
const weighted = withGp.reduce((a, c) => a + (c.grade_point as number) * c.credit, 0);
|
||||
const weighted = withGp.reduce((a, c) => a + (c.grade_point as number) * wOf(c) * c.credit, 0);
|
||||
return Math.round((weighted / credits) * 100) / 100;
|
||||
}
|
||||
|
||||
@@ -105,7 +116,7 @@ export function GradesPage() {
|
||||
</div>
|
||||
|
||||
{/* 成绩梯度设置 */}
|
||||
<BandEditor bands={s?.bands ?? []} />
|
||||
<BandEditor bands={s?.bands ?? []} weights={s?.weights} />
|
||||
|
||||
{/* 学年学科成绩 */}
|
||||
<div className="mb-3 mt-5 flex items-center justify-between">
|
||||
@@ -155,7 +166,7 @@ function MetricCard({ label, value }: { label: string; value: number | string })
|
||||
}
|
||||
|
||||
// ---------- 梯度设置 ----------
|
||||
function BandEditor({ bands }: { bands: GradeBand[] }) {
|
||||
function BandEditor({ bands, weights }: { bands: GradeBand[]; weights?: Record<string, number> }) {
|
||||
const qc = useQueryClient();
|
||||
const toast = useToast();
|
||||
const [adding, setAdding] = useState<GradeBand | null>(null);
|
||||
@@ -178,7 +189,7 @@ function BandEditor({ bands }: { bands: GradeBand[] }) {
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="成绩梯度"
|
||||
extra={<span className="text-[11px] text-ink-3">各校规则不同:每档可设定级与绩点,「自动绩点」按此计算</span>}
|
||||
extra={<span className="text-[11px] text-ink-3">绩点按档内线性折算(起~顶),各校规则不同可修改</span>}
|
||||
/>
|
||||
<CardBody className="p-0">
|
||||
<div className="divide-y divide-line">
|
||||
@@ -196,12 +207,35 @@ function BandEditor({ bands }: { bands: GradeBand[] }) {
|
||||
onCancel={() => setAdding(null)}
|
||||
/>
|
||||
) : (
|
||||
<Button size="sm" variant="secondary" onClick={() => setAdding({ id: 0, label: '', color: '#3056d3', min: 0, max: 60, gp: null, sort: (bands.at(-1)?.sort ?? 0) + 10 })}>
|
||||
<Button size="sm" variant="secondary" onClick={() => setAdding({ id: 0, label: '', color: '#3056d3', min: 0, max: 60, gp: null, gpTop: null, sort: (bands.at(-1)?.sort ?? 0) + 10 })}>
|
||||
<Plus size={13} /> 添加等级
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardBody>
|
||||
<div className="border-t border-line px-4 py-3">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-[12px] font-semibold text-ink-2">课程权重系数</span>
|
||||
<span className="text-[11px] text-ink-3">课程学分绩点 = 绩点 × 权重 × 学分</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Object.entries(weights ?? {}).map(([cat, w]) => (
|
||||
<div key={cat} className="flex items-center gap-1.5 rounded-md border border-line bg-surface-2 px-2.5 py-1">
|
||||
<span className="text-[11.5px] text-ink-2">{cat}</span>
|
||||
<Input type="number" step="0.1" className="num h-6 w-[56px] border-0 bg-transparent p-0"
|
||||
value={String(w)} onChange={e => {
|
||||
const next = { ...(weights ?? {}), [cat]: Number(e.target.value) || 1 };
|
||||
void gpaSettingsUpsert(next)
|
||||
.then(() => qc.invalidateQueries({ queryKey: ['grades'] }))
|
||||
.catch((err: Error) => toast('error', err.message));
|
||||
}} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-2 text-[11px] text-ink-3">
|
||||
公式:平均学分绩点 = Σ(课程绩点 × 权重系数 × 学分) ÷ Σ(学分) · 各校规则不同,可直接修改
|
||||
</div>
|
||||
</div>
|
||||
<Confirm
|
||||
open={!!deleting}
|
||||
title="删除等级"
|
||||
@@ -243,7 +277,9 @@ function BandRow({ band, onSave, onDelete, onChange, onCancel, isNew }: {
|
||||
<span className="text-[11.5px] text-ink-3">—</span>
|
||||
<Input type="number" className="num h-6 w-[64px] border-0 bg-transparent p-0" value={String(value.max)} onChange={e => set({ max: Number(e.target.value) })} />
|
||||
<span className="ml-1 text-[11px] text-ink-3">绩点</span>
|
||||
<Input type="number" step="0.1" className="num h-6 w-[56px] border-0 bg-transparent p-0" value={value.gp != null ? String(value.gp) : ''} placeholder="—" onChange={e => set({ gp: e.target.value === '' ? null : Number(e.target.value) })} />
|
||||
<Input type="number" step="0.1" className="num h-6 w-[52px] border-0 bg-transparent p-0 text-right" value={value.gp != null ? String(value.gp) : ''} placeholder="起" onChange={e => set({ gp: e.target.value === '' ? null : Number(e.target.value) })} />
|
||||
<span className="text-[11.5px] text-ink-3">~</span>
|
||||
<Input type="number" step="0.1" className="num h-6 w-[52px] border-0 bg-transparent p-0" value={value.gpTop != null ? String(value.gpTop) : ''} placeholder="顶" onChange={e => set({ gpTop: e.target.value === '' ? null : Number(e.target.value) })} />
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 sm:ml-auto">
|
||||
{isNew ? (
|
||||
@@ -283,7 +319,7 @@ function SemesterCard({ sem }: { sem: SemesterGrades }) {
|
||||
|
||||
const autoGpaMut = useMutation({
|
||||
mutationFn: () => {
|
||||
const gpa = autoGpa(sem.courses, overviewQ.data?.bands);
|
||||
const gpa = autoGpa(sem.courses, overviewQ.data?.bands, overviewQ.data?.weights);
|
||||
if (gpa == null) throw new Error('请先为课程填写绩点');
|
||||
return semesterUpsert({ id: sem.id, term: sem.term, gpa, rank: sem.rank ?? null, total_students: sem.total_students ?? null });
|
||||
},
|
||||
@@ -318,7 +354,7 @@ function SemesterCard({ sem }: { sem: SemesterGrades }) {
|
||||
<div className="-mx-1 overflow-x-auto px-1"><table className="w-full border-collapse text-[13px]">
|
||||
<thead>
|
||||
<tr>
|
||||
{['课程', '学分', '分数', '等级', '绩点', ''].map(h => (
|
||||
{['课程', '学分', '分数', '类别', '等级', '绩点', ''].map(h => (
|
||||
<th key={h} className="border-b border-line bg-surface-2 px-3 py-2 text-left text-[12px] font-semibold text-ink-3">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
@@ -334,6 +370,7 @@ function SemesterCard({ sem }: { sem: SemesterGrades }) {
|
||||
<td className="num border-b border-line px-3 py-2">
|
||||
{c.score != null ? <span className="font-medium">{c.score}</span> : <span className="text-ink-3">未录入</span>}
|
||||
</td>
|
||||
<td className="border-b border-line px-3 py-2 text-[12px] text-ink-3">{c.category ?? '—'}</td>
|
||||
<td className="border-b border-line px-3 py-2">
|
||||
{c.band ? <Badge color={c.band.color}>{c.band.label}</Badge> : <span className="text-ink-3">—</span>}
|
||||
</td>
|
||||
@@ -410,6 +447,7 @@ function CourseAddRow({ sem }: { sem: SemesterGrades }) {
|
||||
const [credit, setCredit] = useState('');
|
||||
const [score, setScore] = useState('');
|
||||
const [gp, setGp] = useState('');
|
||||
const [category, setCategory] = useState('必修课');
|
||||
|
||||
const addMut = useMutation({
|
||||
mutationFn: () => courseUpsert({
|
||||
@@ -428,6 +466,7 @@ function CourseAddRow({ sem }: { sem: SemesterGrades }) {
|
||||
<td className="px-3 py-2"><Input className="h-7 w-[160px]" placeholder="课程名称" value={name} onChange={e => setName(e.target.value)} /></td>
|
||||
<td className="px-3 py-2"><Input type="number" step="0.5" className="num h-7 w-[64px]" placeholder="0" value={credit} onChange={e => setCredit(e.target.value)} /></td>
|
||||
<td className="px-3 py-2"><Input type="number" className="num h-7 w-[72px]" placeholder="0-100" value={score} onChange={e => setScore(e.target.value)} /></td>
|
||||
<td className="px-3 py-2"><Select className="h-7 w-[110px]" value={category} onChange={e => setCategory(e.target.value)} options={['必修课', '专业选修课', '人文素质选修课', '其他'].map(o => ({ value: o, label: o }))} /></td>
|
||||
<td className="px-3 py-2 text-ink-3">—</td>
|
||||
<td className="px-3 py-2"><Input type="number" step="0.1" className="num h-7 w-[64px]" placeholder="选填" value={gp} onChange={e => setGp(e.target.value)} /></td>
|
||||
<td className="px-3 py-2">
|
||||
@@ -446,11 +485,12 @@ function CourseEditRow({ sem, course, onDone }: { sem: SemesterGrades; course: C
|
||||
const [credit, setCredit] = useState(String(course.credit));
|
||||
const [score, setScore] = useState(course.score != null ? String(course.score) : '');
|
||||
const [gp, setGp] = useState(course.grade_point != null ? String(course.grade_point) : '');
|
||||
const [category, setCategory] = useState(course.category ?? '必修课');
|
||||
|
||||
const saveMut = useMutation({
|
||||
mutationFn: () => courseUpsert({
|
||||
id: course.id, semester_id: sem.id, course_name: name, credit: Number(credit) || 0,
|
||||
score: score === '' ? null : Number(score), grade_point: gp === '' ? null : Number(gp)
|
||||
score: score === '' ? null : Number(score), grade_point: gp === '' ? null : Number(gp), category
|
||||
}),
|
||||
onSuccess: () => { void qc.invalidateQueries({ queryKey: ['grades'] }); onDone(); },
|
||||
onError: (e: Error) => toast('error', e.message)
|
||||
@@ -461,6 +501,7 @@ function CourseEditRow({ sem, course, onDone }: { sem: SemesterGrades; course: C
|
||||
<td className="border-b border-line px-3 py-2"><Input className="h-7 w-[160px]" value={name} onChange={e => setName(e.target.value)} /></td>
|
||||
<td className="border-b border-line px-3 py-2"><Input type="number" step="0.5" className="num h-7 w-[64px]" value={credit} onChange={e => setCredit(e.target.value)} /></td>
|
||||
<td className="border-b border-line px-3 py-2"><Input type="number" className="num h-7 w-[72px]" value={score} onChange={e => setScore(e.target.value)} /></td>
|
||||
<td className="border-b border-line px-3 py-2"><Select className="h-7 w-[110px]" value={category} onChange={e => setCategory(e.target.value)} options={['必修课', '专业选修课', '人文素质选修课', '其他'].map(o => ({ value: o, label: o }))} /></td>
|
||||
<td className="border-b border-line px-3 py-2 text-[12px] text-ink-3">保存后按梯度定级</td>
|
||||
<td className="border-b border-line px-3 py-2"><Input type="number" step="0.1" className="num h-7 w-[64px]" value={gp} onChange={e => setGp(e.target.value)} /></td>
|
||||
<td className="border-b border-line px-3 py-2">
|
||||
@@ -482,10 +523,11 @@ function MobileCourseAdd({ sem }: { sem: SemesterGrades }) {
|
||||
const [credit, setCredit] = useState('');
|
||||
const [score, setScore] = useState('');
|
||||
const [gp, setGp] = useState('');
|
||||
const [category, setCategory] = useState('必修课');
|
||||
const saveMut = useMutation({
|
||||
mutationFn: () => courseUpsert({
|
||||
semester_id: sem.id, course_name: name, credit: Number(credit) || 0,
|
||||
score: score === '' ? null : Number(score), grade_point: gp === '' ? null : Number(gp)
|
||||
score: score === '' ? null : Number(score), grade_point: gp === '' ? null : Number(gp), category
|
||||
}),
|
||||
onSuccess: () => {
|
||||
setName(''); setCredit(''); setScore(''); setGp('');
|
||||
@@ -502,6 +544,7 @@ function MobileCourseAdd({ sem }: { sem: SemesterGrades }) {
|
||||
<Input type="number" placeholder="分数" value={score} onChange={e => setScore(e.target.value)} />
|
||||
<Input type="number" step="0.1" placeholder="绩点" value={gp} onChange={e => setGp(e.target.value)} />
|
||||
</div>
|
||||
<Select className="mb-2" value={category} onChange={e => setCategory(e.target.value)} options={['必修课', '专业选修课', '人文素质选修课', '其他'].map(o => ({ value: o, label: o }))} />
|
||||
<Button size="sm" variant="primary" disabled={!name.trim() || saveMut.isPending} onClick={() => saveMut.mutate()}>
|
||||
<Plus size={13} /> 添加课程
|
||||
</Button>
|
||||
@@ -516,10 +559,11 @@ function MobileCourseEdit({ sem, course, onDone }: { sem: SemesterGrades; course
|
||||
const [credit, setCredit] = useState(String(course.credit));
|
||||
const [score, setScore] = useState(course.score != null ? String(course.score) : '');
|
||||
const [gp, setGp] = useState(course.grade_point != null ? String(course.grade_point) : '');
|
||||
const [category, setCategory] = useState(course.category ?? '必修课');
|
||||
const saveMut = useMutation({
|
||||
mutationFn: () => courseUpsert({
|
||||
id: course.id, semester_id: sem.id, course_name: name, credit: Number(credit) || 0,
|
||||
score: score === '' ? null : Number(score), grade_point: gp === '' ? null : Number(gp)
|
||||
score: score === '' ? null : Number(score), grade_point: gp === '' ? null : Number(gp), category
|
||||
}),
|
||||
onSuccess: () => { void qc.invalidateQueries({ queryKey: ['grades'] }); onDone(); },
|
||||
onError: (e: Error) => toast('error', e.message)
|
||||
@@ -532,6 +576,7 @@ function MobileCourseEdit({ sem, course, onDone }: { sem: SemesterGrades; course
|
||||
<Input type="number" placeholder="分数" value={score} onChange={e => setScore(e.target.value)} />
|
||||
<Input type="number" step="0.1" placeholder="绩点" value={gp} onChange={e => setGp(e.target.value)} />
|
||||
</div>
|
||||
<Select className="mb-2" value={category} onChange={e => setCategory(e.target.value)} options={['必修课', '专业选修课', '人文素质选修课', '其他'].map(o => ({ value: o, label: o }))} />
|
||||
<div className="flex gap-1.5">
|
||||
<Button size="sm" variant="primary" onClick={() => saveMut.mutate()}>保存</Button>
|
||||
<Button size="sm" variant="ghost" onClick={onDone}>取消</Button>
|
||||
|
||||
+4
-4
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{r as g,f as v,j as t,S as j,t as S}from"./index-T-NGjlrZ.js";import{C as c,a as d,b as x}from"./card-C5ifMTlY.js";import{u as N,i as u,a as m}from"./useChart-dAkYJqaV.js";import{u as k,i as w,a as C,b as L,c as R,d as $,e as z,f as T}from"./echarts-BIWefrIc.js";k([w,C,L,R,$,z,T]);function q(){const[l,n]=g.useState(""),i=N(),e=v({queryKey:["stats",l||void 0],queryFn:()=>S(l?Number(l):void 0)}).data,p=u("2"),r=u("3"),h=m(()=>({grid:{left:34,right:12,top:14,bottom:24},tooltip:{trigger:"axis"},xAxis:{type:"category",data:(e==null?void 0:e.monthly.map(a=>a.ym.slice(2)))??[],axisLine:{lineStyle:{color:r}},axisTick:{show:!1},axisLabel:{color:r,fontSize:10}},yAxis:{type:"value",splitLine:{lineStyle:{color:"rgba(128,138,155,0.15)"}},axisLabel:{color:r,fontSize:10}},series:[{type:"bar",data:(e==null?void 0:e.monthly.map(a=>a.count))??[],barMaxWidth:18,itemStyle:{color:"#3056d3",borderRadius:[3,3,0,0]}}]}),[e,i]),b=m(()=>{const a=((e==null?void 0:e.by_type)??[]).filter(s=>s.count>0);return{tooltip:{trigger:"item"},legend:{orient:"vertical",right:4,top:"middle",textStyle:{color:p,fontSize:11},itemWidth:10,itemHeight:10},series:[{type:"pie",radius:["48%","72%"],center:["34%","50%"],label:{show:!1},data:a.map(s=>({name:s.label,value:s.count,itemStyle:{color:s.color||"#64748b"}}))}]}},[e,i]),y=m(()=>{const a=((e==null?void 0:e.by_level)??[]).filter(s=>s.count>0).slice().reverse();return{grid:{left:64,right:24,top:8,bottom:22},tooltip:{trigger:"axis"},xAxis:{type:"value",splitLine:{lineStyle:{color:"rgba(128,138,155,0.15)"}},axisLabel:{color:r,fontSize:10}},yAxis:{type:"category",data:a.map(s=>s.label),axisLine:{show:!1},axisTick:{show:!1},axisLabel:{color:p,fontSize:11}},series:[{type:"bar",data:a.map(s=>({value:s.count,itemStyle:{color:s.color||"#3056d3",borderRadius:[0,3,3,0]}})),barMaxWidth:14}]}},[e,i]),f=((e==null?void 0:e.metrics)??[]).slice(0,3);return t.jsxs("div",{className:"mx-auto max-w-[1080px] px-3 py-4 sm:px-6 sm:py-5",children:[t.jsxs("div",{className:"mb-4 flex flex-wrap items-center justify-between gap-3",children:[t.jsxs("div",{className:"flex flex-wrap gap-3",children:[t.jsx(o,{label:"总记录",value:(e==null?void 0:e.total)??0}),t.jsx(o,{label:l?l+" 年新增":"本年新增",value:(e==null?void 0:e.total_this_year)??0}),t.jsx(o,{label:"附件总数",value:(e==null?void 0:e.attachment_total)??0}),f.map(a=>t.jsx(o,{label:a.label,value:a.value,unit:a.unit??""},a.key))]}),t.jsx(j,{className:"w-[120px]",value:l,onChange:a=>n(a.target.value),options:[{value:"",label:"全部年份"},...((e==null?void 0:e.years)??[]).map(a=>({value:String(a),label:a+" 年"}))]})]}),!(e!=null&&e.metrics.length)&&t.jsx("div",{className:"mb-4 rounded-md border border-dashed border-line px-4 py-2 text-[12px] text-ink-3",children:"提示:在「统计」配置中注册指标(如志愿时长、读书本数),带单位的数值字段即可在此量化展示。"}),t.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-4",children:[t.jsxs(c,{children:[t.jsx(d,{title:"近 12 个月活跃度"}),t.jsx(x,{children:t.jsx("div",{ref:h,className:"h-[220px]"})})]}),t.jsxs(c,{children:[t.jsx(d,{title:"类型分布"}),t.jsx(x,{children:t.jsx("div",{ref:b,className:"h-[220px]"})})]}),t.jsxs(c,{className:"lg:col-span-2",children:[t.jsx(d,{title:"级别分布"}),t.jsx(x,{children:t.jsx("div",{ref:y,className:"h-[200px]"})})]})]})]})}function o({label:l,value:n,unit:i}){return t.jsxs("div",{className:"min-w-[120px] rounded-lg border border-line bg-surface px-4 py-3 shadow-card",children:[t.jsx("div",{className:"text-[12px] text-ink-3",children:l}),t.jsxs("div",{className:"num mt-0.5 text-[22px] font-semibold leading-tight text-ink-1",children:[n,i&&t.jsx("span",{className:"ml-1 text-[12px] font-normal text-ink-3",children:i})]})]})}export{q as DashboardPage};
|
||||
import{r as g,f as v,j as t,S as j,t as S}from"./index-DTwrm6HB.js";import{C as c,a as d,b as x}from"./card-CHSa56zc.js";import{u as N,i as u,a as m}from"./useChart-Bs5kYKHg.js";import{u as k,i as w,a as C,b as L,c as R,d as $,e as z,f as T}from"./echarts-BIWefrIc.js";k([w,C,L,R,$,z,T]);function q(){const[l,n]=g.useState(""),i=N(),e=v({queryKey:["stats",l||void 0],queryFn:()=>S(l?Number(l):void 0)}).data,p=u("2"),r=u("3"),h=m(()=>({grid:{left:34,right:12,top:14,bottom:24},tooltip:{trigger:"axis"},xAxis:{type:"category",data:(e==null?void 0:e.monthly.map(a=>a.ym.slice(2)))??[],axisLine:{lineStyle:{color:r}},axisTick:{show:!1},axisLabel:{color:r,fontSize:10}},yAxis:{type:"value",splitLine:{lineStyle:{color:"rgba(128,138,155,0.15)"}},axisLabel:{color:r,fontSize:10}},series:[{type:"bar",data:(e==null?void 0:e.monthly.map(a=>a.count))??[],barMaxWidth:18,itemStyle:{color:"#3056d3",borderRadius:[3,3,0,0]}}]}),[e,i]),b=m(()=>{const a=((e==null?void 0:e.by_type)??[]).filter(s=>s.count>0);return{tooltip:{trigger:"item"},legend:{orient:"vertical",right:4,top:"middle",textStyle:{color:p,fontSize:11},itemWidth:10,itemHeight:10},series:[{type:"pie",radius:["48%","72%"],center:["34%","50%"],label:{show:!1},data:a.map(s=>({name:s.label,value:s.count,itemStyle:{color:s.color||"#64748b"}}))}]}},[e,i]),y=m(()=>{const a=((e==null?void 0:e.by_level)??[]).filter(s=>s.count>0).slice().reverse();return{grid:{left:64,right:24,top:8,bottom:22},tooltip:{trigger:"axis"},xAxis:{type:"value",splitLine:{lineStyle:{color:"rgba(128,138,155,0.15)"}},axisLabel:{color:r,fontSize:10}},yAxis:{type:"category",data:a.map(s=>s.label),axisLine:{show:!1},axisTick:{show:!1},axisLabel:{color:p,fontSize:11}},series:[{type:"bar",data:a.map(s=>({value:s.count,itemStyle:{color:s.color||"#3056d3",borderRadius:[0,3,3,0]}})),barMaxWidth:14}]}},[e,i]),f=((e==null?void 0:e.metrics)??[]).slice(0,3);return t.jsxs("div",{className:"mx-auto max-w-[1080px] px-3 py-4 sm:px-6 sm:py-5",children:[t.jsxs("div",{className:"mb-4 flex flex-wrap items-center justify-between gap-3",children:[t.jsxs("div",{className:"flex flex-wrap gap-3",children:[t.jsx(o,{label:"总记录",value:(e==null?void 0:e.total)??0}),t.jsx(o,{label:l?l+" 年新增":"本年新增",value:(e==null?void 0:e.total_this_year)??0}),t.jsx(o,{label:"附件总数",value:(e==null?void 0:e.attachment_total)??0}),f.map(a=>t.jsx(o,{label:a.label,value:a.value,unit:a.unit??""},a.key))]}),t.jsx(j,{className:"w-[120px]",value:l,onChange:a=>n(a.target.value),options:[{value:"",label:"全部年份"},...((e==null?void 0:e.years)??[]).map(a=>({value:String(a),label:a+" 年"}))]})]}),!(e!=null&&e.metrics.length)&&t.jsx("div",{className:"mb-4 rounded-md border border-dashed border-line px-4 py-2 text-[12px] text-ink-3",children:"提示:在「统计」配置中注册指标(如志愿时长、读书本数),带单位的数值字段即可在此量化展示。"}),t.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-4",children:[t.jsxs(c,{children:[t.jsx(d,{title:"近 12 个月活跃度"}),t.jsx(x,{children:t.jsx("div",{ref:h,className:"h-[220px]"})})]}),t.jsxs(c,{children:[t.jsx(d,{title:"类型分布"}),t.jsx(x,{children:t.jsx("div",{ref:b,className:"h-[220px]"})})]}),t.jsxs(c,{className:"lg:col-span-2",children:[t.jsx(d,{title:"级别分布"}),t.jsx(x,{children:t.jsx("div",{ref:y,className:"h-[200px]"})})]})]})]})}function o({label:l,value:n,unit:i}){return t.jsxs("div",{className:"min-w-[120px] rounded-lg border border-line bg-surface px-4 py-3 shadow-card",children:[t.jsx("div",{className:"text-[12px] text-ink-3",children:l}),t.jsxs("div",{className:"num mt-0.5 text-[22px] font-semibold leading-tight text-ink-1",children:[n,i&&t.jsx("span",{className:"ml-1 text-[12px] font-normal text-ink-3",children:i})]})]})}export{q as DashboardPage};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as fe,j as e,a as N,r as s,b as he,u as ve,d as ge,e as je,f as L,g as ye,I as be,S as k,B as w,P as Q,h as te,L as se,G as ae,R as Ne,i as ne,k as ke,l as we,T as Ce,E as le,m as Ee,n as Se,o as _e,C as ze,p as qe,q as Le,s as Re,t as De,v as Ie}from"./index-T-NGjlrZ.js";/**
|
||||
import{c as fe,j as e,a as N,r as s,b as he,u as ve,d as ge,e as je,f as L,g as ye,I as be,S as k,B as w,P as Q,h as te,L as se,G as ae,R as Ne,i as ne,k as ke,l as we,T as Ce,E as le,m as Ee,n as Se,o as _e,C as ze,p as qe,q as Le,s as Re,t as De,v as Ie}from"./index-DTwrm6HB.js";/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as L,d as we,f as se,r as T,j as e,B as R,ag as Ce,ae as D,H as B,S as ne,ac as de,P as $,F as Se,a8 as _e,T as ie,I as V,af as ze,v as Re}from"./index-T-NGjlrZ.js";import{C as ae,a as le,b as re}from"./card-C5ifMTlY.js";import{R as Pe}from"./refresh-cw-DXuKsZdS.js";/**
|
||||
import{c as L,d as we,f as se,r as T,j as e,B as R,ah as Ce,af as D,J as B,S as ne,ad as de,P as $,H as Se,a9 as _e,T as ie,I as V,ag as ze,v as Re}from"./index-DTwrm6HB.js";import{C as ae,a as le,b as re}from"./card-CHSa56zc.js";import{R as Pe}from"./refresh-cw-D2g9hAc-.js";/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{j as s,a}from"./index-T-NGjlrZ.js";function i({className:r,...e}){return s.jsx("div",{className:a("rounded-lg border border-line bg-surface shadow-card",r),...e})}function t({title:r,extra:e,className:n}){return s.jsxs("div",{className:a("flex min-h-11 flex-wrap items-center justify-between gap-2 border-b border-line px-4 py-1.5",n),children:[s.jsx("h3",{className:"text-[13px] font-semibold text-ink-1",children:r}),e&&s.jsx("div",{className:"flex flex-wrap items-center gap-2",children:e})]})}function c({className:r,...e}){return s.jsx("div",{className:a("p-4",r),...e})}export{i as C,t as a,c as b};
|
||||
import{j as s,a}from"./index-DTwrm6HB.js";function i({className:r,...e}){return s.jsx("div",{className:a("rounded-lg border border-line bg-surface shadow-card",r),...e})}function t({title:r,extra:e,className:n}){return s.jsxs("div",{className:a("flex min-h-11 flex-wrap items-center justify-between gap-2 border-b border-line px-4 py-1.5",n),children:[s.jsx("h3",{className:"text-[13px] font-semibold text-ink-1",children:r}),e&&s.jsx("div",{className:"flex flex-wrap items-center gap-2",children:e})]})}function c({className:r,...e}){return s.jsx("div",{className:a("p-4",r),...e})}export{i as C,t as a,c as b};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+10
-10
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as e}from"./index-T-NGjlrZ.js";/**
|
||||
import{c as e}from"./index-DTwrm6HB.js";/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{r}from"./index-T-NGjlrZ.js";import{g as f}from"./echarts-BIWefrIc.js";function p(){const[n,o]=r.useState(0);return r.useEffect(()=>{const s=()=>o(t=>t+1);return window.addEventListener("fp-theme-change",s),()=>window.removeEventListener("fp-theme-change",s)},[]),n}function d(n){return getComputedStyle(document.documentElement).getPropertyValue("--text-"+n).trim()||"#454e5c"}function h(n,o){const s=r.useRef(null),[t,i]=r.useState(null),u=r.useRef(n);return u.current=n,r.useEffect(()=>{if(!t)return;const e=f(t);s.current=e,e.setOption(u.current(),{notMerge:!0});const c=new ResizeObserver(()=>e.resize());return c.observe(t),()=>{c.disconnect(),e.dispose(),s.current=null}},[t]),r.useEffect(()=>{var e;(e=s.current)==null||e.setOption(u.current(),{notMerge:!0})},[t,...o]),e=>i(e)}export{h as a,d as i,p as u};
|
||||
import{r}from"./index-DTwrm6HB.js";import{g as f}from"./echarts-BIWefrIc.js";function p(){const[n,o]=r.useState(0);return r.useEffect(()=>{const s=()=>o(t=>t+1);return window.addEventListener("fp-theme-change",s),()=>window.removeEventListener("fp-theme-change",s)},[]),n}function d(n){return getComputedStyle(document.documentElement).getPropertyValue("--text-"+n).trim()||"#454e5c"}function h(n,o){const s=r.useRef(null),[t,i]=r.useState(null),u=r.useRef(n);return u.current=n,r.useEffect(()=>{if(!t)return;const e=f(t);s.current=e,e.setOption(u.current(),{notMerge:!0});const c=new ResizeObserver(()=>e.resize());return c.observe(t),()=>{c.disconnect(),e.dispose(),s.current=null}},[t]),r.useEffect(()=>{var e;(e=s.current)==null||e.setOption(u.current(),{notMerge:!0})},[t,...o]),e=>i(e)}export{h as a,d as i,p as u};
|
||||
@@ -9,8 +9,8 @@
|
||||
<link rel="icon" href="/icons/pwa-192.png" />
|
||||
<link rel="apple-touch-icon" href="/icons/pwa-192.png" />
|
||||
<title>足迹 Footprints</title>
|
||||
<script type="module" crossorigin src="/assets/index-T-NGjlrZ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B4Ww6089.css">
|
||||
<script type="module" crossorigin src="/assets/index-DTwrm6HB.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CRSzz9iX.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user