From b97a2faaff0463bc37edbbcab3c85c7ad04f1abc Mon Sep 17 00:00:00 2001 From: biss Date: Sun, 21 Jun 2026 14:03:06 +0800 Subject: [PATCH] =?UTF-8?q?=E6=88=90=E7=BB=A9=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- exam/exam.js | 228 ++++++++++++++++++++++++++++++++++++++++++++++++ exam/index.html | 224 +++++++++++++++++++++++++++++++++++++++++++++++ exam/schema.sql | 123 ++++++++++++++++++++++++++ index.html | 10 +++ 4 files changed, 585 insertions(+) create mode 100644 exam/exam.js create mode 100644 exam/index.html create mode 100644 exam/schema.sql diff --git a/exam/exam.js b/exam/exam.js new file mode 100644 index 0000000..4949c93 --- /dev/null +++ b/exam/exam.js @@ -0,0 +1,228 @@ +const SUPABASE_URL = 'https://chixssrphfgxvqqigkzo.supabase.co'; +const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNoaXhzc3JwaGZneHZxcWlna3pvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzQ2OTE0OTEsImV4cCI6MjA5MDI2NzQ5MX0.Az_Ew2J2zdOMcSV0UNAjBS-LPqGpqhsaN4IyZ5R7iqU'; +const sbClient = supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY); + +const examSelect = document.getElementById('examSelect'); +const nameInput = document.getElementById('studentName'); +const admissionInput = document.getElementById('admissionNo'); +const searchButton = document.getElementById('searchBtn'); +const resultsShell = document.getElementById('searchResults'); + +let loadedExams = []; + +function escapeHtml(value) { + return String(value ?? '').replace(/[&<>"']/g, char => ({ + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }[char])); +} + +function normalize(value) { + return String(value ?? '').trim(); +} + +function formatDate(value) { + if (!value) return '-'; + const date = new Date(`${value}T00:00:00`); + if (Number.isNaN(date.getTime())) return value; + return date.toLocaleDateString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }); +} + +function formatScore(value) { + const number = Number(value || 0); + return Number.isInteger(number) ? String(number) : number.toFixed(2); +} + +function getScoreTotal(items) { + return (items || []).reduce((sum, item) => sum + Number(item.score || 0), 0); +} + +function renderEmpty(message) { + resultsShell.innerHTML = `
${escapeHtml(message)}
`; +} + +function renderError(message) { + resultsShell.innerHTML = `
${escapeHtml(message)}
`; +} + +function renderLoading(message) { + resultsShell.innerHTML = `
${escapeHtml(message)}
`; +} + +function getSelectedExam() { + return loadedExams.find(exam => exam.id === examSelect.value) || null; +} + +async function loadExams() { + examSelect.innerHTML = ''; + + const { data, error } = await sbClient + .from('exam_exams') + .select('id, exam_code, exam_name, exam_date') + .eq('is_published', true) + .order('exam_date', { ascending: false }); + + if (error) throw error; + + loadedExams = data || []; + + if (loadedExams.length === 0) { + examSelect.innerHTML = ''; + renderEmpty('暂无已发布考试项目,请先在 Supabase 的 exam_exams 表中新增考试,或执行 exam/schema.sql 中的示例插入语句。'); + return; + } + + examSelect.innerHTML = [ + '', + ...loadedExams.map(exam => ( + `` + )) + ].join(''); +} + +function renderResult(result, exam) { + const items = [...(result.exam_score_items || [])].sort((a, b) => + Number(a.sort_order || 0) - Number(b.sort_order || 0) || + String(a.item_name || '').localeCompare(String(b.item_name || ''), 'zh-CN') + ); + const total = getScoreTotal(items); + const rows = items.map(item => { + const score = Number(item.score || 0); + const scoreClass = score < 0 ? 'score-value negative' : 'score-value'; + const fullScore = item.full_score === null || item.full_score === undefined ? '-' : formatScore(item.full_score); + + return ` + + ${escapeHtml(item.item_name)} + ${escapeHtml(formatScore(item.score))} + ${escapeHtml(fullScore)} + + `; + }).join(''); + + resultsShell.innerHTML = ` +
+
+

${escapeHtml(result.student_name)}

+
+ 准考证号:${escapeHtml(result.admission_no)} + 班级:${escapeHtml(result.class_name || '-')} +
+
+ 独立成绩单 +
+
+
+
+
+

${escapeHtml(exam.exam_name)}

+
考试日期:${escapeHtml(formatDate(exam.exam_date))}
+
+
+ 本场总分
${escapeHtml(formatScore(total))} +
+
+
+ + + + + + + + + ${rows || ''} +
成绩项目得分满分
暂无成绩项目
+
+

备注:${escapeHtml(result.remark || '无')}

+
+
+ `; +} + +async function handleSearch() { + const exam = getSelectedExam(); + const studentName = normalize(nameInput.value); + const admissionNo = normalize(admissionInput.value); + + if (!exam || !studentName || !admissionNo) { + renderEmpty('请同时选择考试项目,并输入姓名和准考证号。'); + return; + } + + renderLoading('正在连接成绩数据库并执行查询...'); + + try { + const { data, error } = await sbClient + .from('exam_results') + .select(` + id, + student_name, + admission_no, + class_name, + remark, + exam_score_items ( + id, + item_name, + score, + full_score, + sort_order + ) + `) + .eq('exam_id', exam.id) + .eq('student_name', studentName) + .eq('admission_no', admissionNo) + .maybeSingle(); + + if (error) throw error; + + if (!data) { + renderEmpty('未查询到该考试项目下的匹配成绩,请核对考试项目、姓名和准考证号。'); + return; + } + + renderResult(data, exam); + } catch (error) { + renderError(`查询失败:${error.message}`); + } +} + +function applyQueryParams() { + const params = new URLSearchParams(window.location.search); + const presetExam = params.get('examId') || params.get('exam') || ''; + const presetName = params.get('name') || ''; + const presetAdmissionNo = params.get('admissionNo') || params.get('ticket') || ''; + + if (presetExam) { + const matchedExam = loadedExams.find(exam => exam.id === presetExam || exam.exam_code === presetExam); + if (matchedExam) examSelect.value = matchedExam.id; + } + if (presetName) nameInput.value = presetName; + if (presetAdmissionNo) admissionInput.value = presetAdmissionNo; + if (examSelect.value && presetName && presetAdmissionNo) handleSearch(); +} + +async function init() { + try { + await loadExams(); + applyQueryParams(); + } catch (error) { + examSelect.innerHTML = ''; + renderError(`考试项目加载失败:${error.message}`); + } +} + +searchButton.addEventListener('click', handleSearch); +[examSelect, nameInput, admissionInput].forEach(input => { + input.addEventListener('keydown', event => { + if (event.key === 'Enter') handleSearch(); + }); +}); +init(); diff --git a/exam/index.html b/exam/index.html new file mode 100644 index 0000000..f2a00d7 --- /dev/null +++ b/exam/index.html @@ -0,0 +1,224 @@ + + + + + + 成绩查询系统 + + + + + + + +
+ + +
+
+
+

查询条件

+
考试项目、姓名和准考证号必须同时匹配
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ +
+
+
+

查询结果

+
只展示当前所选考试项目的成绩
+
+
+
+
请选择考试项目,并输入姓名和准考证号后开始查询。
+
+
+
+ + + + + + + diff --git a/exam/schema.sql b/exam/schema.sql new file mode 100644 index 0000000..6a30058 --- /dev/null +++ b/exam/schema.sql @@ -0,0 +1,123 @@ +-- PostgreSQL / Supabase schema for the exam result query module. +-- Each exam is independent. Scores are stored under one exam only and do +-- not affect any other exam. + +create extension if not exists pgcrypto; + +create table if not exists public.exam_exams ( + id uuid primary key default gen_random_uuid(), + exam_code text not null unique, + exam_name text not null, + exam_date date, + is_published boolean not null default true, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +create table if not exists public.exam_results ( + id uuid primary key default gen_random_uuid(), + exam_id uuid not null references public.exam_exams(id) on delete cascade, + student_name text not null, + admission_no text not null, + class_name text, + remark text, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + constraint exam_results_one_candidate_per_exam unique (exam_id, admission_no, student_name) +); + +create table if not exists public.exam_score_items ( + id uuid primary key default gen_random_uuid(), + result_id uuid not null references public.exam_results(id) on delete cascade, + item_name text not null, + score numeric(10, 2) not null, + full_score numeric(10, 2), + sort_order integer not null default 0, + created_at timestamptz not null default now() +); + +create index if not exists exam_results_lookup_idx + on public.exam_results (exam_id, student_name, admission_no); + +create unique index if not exists exam_score_items_unique_item_idx + on public.exam_score_items (result_id, item_name); + +create index if not exists exam_score_items_result_idx + on public.exam_score_items (result_id, sort_order); + +grant select on public.exam_exams to anon, authenticated; +grant select on public.exam_results to anon, authenticated; +grant select on public.exam_score_items to anon, authenticated; + +alter table public.exam_exams enable row level security; +alter table public.exam_results enable row level security; +alter table public.exam_score_items enable row level security; + +drop policy if exists "Public can read published exams" on public.exam_exams; +create policy "Public can read published exams" + on public.exam_exams + for select + to anon, authenticated + using (is_published = true); + +drop policy if exists "Public can read published exam results" on public.exam_results; +create policy "Public can read published exam results" + on public.exam_results + for select + to anon, authenticated + using ( + exists ( + select 1 + from public.exam_exams + where public.exam_exams.id = exam_results.exam_id + and public.exam_exams.is_published = true + ) + ); + +drop policy if exists "Public can read published score items" on public.exam_score_items; +create policy "Public can read published score items" + on public.exam_score_items + for select + to anon, authenticated + using ( + exists ( + select 1 + from public.exam_results + join public.exam_exams on public.exam_exams.id = public.exam_results.exam_id + where public.exam_results.id = exam_score_items.result_id + and public.exam_exams.is_published = true + ) + ); + +insert into public.exam_exams (exam_code, exam_name, exam_date) +values + ('2026-spring-midterm', '2026 年春季期中考试', '2026-04-18'), + ('2026-spring-monthly-01', '2026 年春季第一次月考', '2026-03-12') +on conflict (exam_code) do nothing; + +with target_exam as ( + select id from public.exam_exams where exam_code = '2026-spring-midterm' +), +inserted_result as ( + insert into public.exam_results (exam_id, student_name, admission_no, class_name, remark) + select id, '张三', 'EX20260001', '高一 1 班', '本场考试独立计分,实验规范扣分仅影响本场期中考试。' + from target_exam + on conflict (exam_id, admission_no, student_name) do update + set class_name = excluded.class_name, + remark = excluded.remark, + updated_at = now() + returning id +) +insert into public.exam_score_items (result_id, item_name, score, full_score, sort_order) +select id, item_name, score, full_score, sort_order +from inserted_result, +(values + ('语文', 108, 150, 10), + ('数学', 126, 150, 20), + ('英语', 119, 150, 30), + ('实验规范扣分', -2, null, 40) +) as item_rows(item_name, score, full_score, sort_order) +on conflict (result_id, item_name) do update + set score = excluded.score, + full_score = excluded.full_score, + sort_order = excluded.sort_order; diff --git a/index.html b/index.html index 8611146..fe4ef83 100644 --- a/index.html +++ b/index.html @@ -52,6 +52,16 @@ + + +
Module 04
+

成绩查询

+

按姓名和准考证号核验身份,集中展示多场考试、成绩项目、负分扣分和备注说明。

+ +