diff --git a/app/server/db.mjs b/app/server/db.mjs index 98f09e0..fb58fb5 100644 --- a/app/server/db.mjs +++ b/app/server/db.mjs @@ -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++; } diff --git a/app/src/lib/api.ts b/app/src/lib/api.ts index 720659f..b0902e5 100644 --- a/app/src/lib/api.ts +++ b/app/src/lib/api.ts @@ -123,7 +123,7 @@ export const gradesOverview = () => call('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 }>('gpa_settings_get', {}); +export const gpaSettingsUpsert = (weights: Record) => call<{ ok: true; weights: Record }>('gpa_settings_upsert', { weights }); + // ---- 个人信息 / 简历 ---- export const profileGet = () => call<{ content: { info?: Partial; resume?: ResumeConfig } }>('profile_get', {}); export const profileUpsert = (content: { info?: Partial; resume?: ResumeConfig }) => diff --git a/app/src/lib/types.ts b/app/src/lib/types.ts index 05ed9c2..5008c2a 100644 --- a/app/src/lib/types.ts +++ b/app/src/lib/types.ts @@ -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; } // ---------- 个人信息 ---------- diff --git a/app/src/pages/grades/GradesPage.tsx b/app/src/pages/grades/GradesPage.tsx index 6e6b047..556c737 100644 --- a/app/src/pages/grades/GradesPage.tsx +++ b/app/src/pages/grades/GradesPage.tsx @@ -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): 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() { {/* 成绩梯度设置 */} - + {/* 学年学科成绩 */}
@@ -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 }) { const qc = useQueryClient(); const toast = useToast(); const [adding, setAdding] = useState(null); @@ -178,7 +189,7 @@ function BandEditor({ bands }: { bands: GradeBand[] }) { 各校规则不同:每档可设定级与绩点,「自动绩点」按此计算} + extra={绩点按档内线性折算(起~顶),各校规则不同可修改} />
@@ -196,12 +207,35 @@ function BandEditor({ bands }: { bands: GradeBand[] }) { onCancel={() => setAdding(null)} /> ) : ( - )}
+
+
+ 课程权重系数 + 课程学分绩点 = 绩点 × 权重 × 学分 +
+
+ {Object.entries(weights ?? {}).map(([cat, w]) => ( +
+ {cat} + { + const next = { ...(weights ?? {}), [cat]: Number(e.target.value) || 1 }; + void gpaSettingsUpsert(next) + .then(() => qc.invalidateQueries({ queryKey: ['grades'] })) + .catch((err: Error) => toast('error', err.message)); + }} /> +
+ ))} +
+
+ 公式:平均学分绩点 = Σ(课程绩点 × 权重系数 × 学分) ÷ Σ(学分) · 各校规则不同,可直接修改 +
+
— set({ max: Number(e.target.value) })} /> 绩点 - set({ gp: e.target.value === '' ? null : Number(e.target.value) })} /> + set({ gp: e.target.value === '' ? null : Number(e.target.value) })} /> + ~ + set({ gpTop: e.target.value === '' ? null : Number(e.target.value) })} />
{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 }) {
- {['课程', '学分', '分数', '等级', '绩点', ''].map(h => ( + {['课程', '学分', '分数', '类别', '等级', '绩点', ''].map(h => ( ))} @@ -334,6 +370,7 @@ function SemesterCard({ sem }: { sem: SemesterGrades }) { + @@ -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 }) { + +
{h}
{c.score != null ? {c.score} : 未录入} {c.category ?? '—'} {c.band ? {c.band.label} : —} setName(e.target.value)} /> setCredit(e.target.value)} /> setScore(e.target.value)} /> — setGp(e.target.value)} /> @@ -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 setName(e.target.value)} /> setCredit(e.target.value)} /> setScore(e.target.value)} /> 保存后按梯度定级 setGp(e.target.value)} /> @@ -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 }) { setScore(e.target.value)} /> setGp(e.target.value)} /> + setScore(e.target.value)} /> setGp(e.target.value)} /> + without a
');let b=n.getAttribute("formaction")||v.getAttribute("action");if(o=b?va(b,r):null,s=n.getAttribute("formmethod")||v.getAttribute("method")||Js,f=ld(n.getAttribute("formenctype"))||ld(v.getAttribute("enctype"))||Fs,m=new FormData(v,n),!bS()){let{name:p,type:x,value:g}=n;if(x==="image"){let _=p?`${p}.`:"";m.append(`${_}x`,"0"),m.append(`${_}y`,"0")}else p&&m.append(p,g)}}else{if(io(n))throw new Error('Cannot submit element that is not ,