Files
Letters/exam/exam.js
T
biss b97a2faaff
Vercel Deploy / deploy (push) Successful in 55s
成绩查询
2026-06-21 14:03:06 +08:00

229 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[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 = `<div class="empty-state">${escapeHtml(message)}</div>`;
}
function renderError(message) {
resultsShell.innerHTML = `<div class="error-state">${escapeHtml(message)}</div>`;
}
function renderLoading(message) {
resultsShell.innerHTML = `<div class="loading-state">${escapeHtml(message)}</div>`;
}
function getSelectedExam() {
return loadedExams.find(exam => exam.id === examSelect.value) || null;
}
async function loadExams() {
examSelect.innerHTML = '<option value="">正在加载考试项目...</option>';
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 = '<option value="">暂无已发布考试项目</option>';
renderEmpty('暂无已发布考试项目,请先在 Supabase 的 exam_exams 表中新增考试,或执行 exam/schema.sql 中的示例插入语句。');
return;
}
examSelect.innerHTML = [
'<option value="">请选择考试项目</option>',
...loadedExams.map(exam => (
`<option value="${escapeHtml(exam.id)}">${escapeHtml(exam.exam_name)}${escapeHtml(formatDate(exam.exam_date))}</option>`
))
].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 `
<tr>
<td>${escapeHtml(item.item_name)}</td>
<td><span class="${scoreClass}">${escapeHtml(formatScore(item.score))}</span></td>
<td>${escapeHtml(fullScore)}</td>
</tr>
`;
}).join('');
resultsShell.innerHTML = `
<div class="candidate-card">
<div>
<h3 class="candidate-name">${escapeHtml(result.student_name)}</h3>
<div class="candidate-meta">
<span>准考证号:${escapeHtml(result.admission_no)}</span>
<span>班级:${escapeHtml(result.class_name || '-')}</span>
</div>
</div>
<span class="exam-id-pill">独立成绩单</span>
</div>
<div class="exam-list">
<article class="exam-block">
<div class="exam-block-header">
<div>
<h3 class="exam-title">${escapeHtml(exam.exam_name)}</h3>
<div class="exam-date">考试日期:${escapeHtml(formatDate(exam.exam_date))}</div>
</div>
<div class="exam-total">
本场总分<br><strong>${escapeHtml(formatScore(total))}</strong>
</div>
</div>
<div class="table-wrapper">
<table class="score-table">
<thead>
<tr>
<th>成绩项目</th>
<th>得分</th>
<th>满分</th>
</tr>
</thead>
<tbody>${rows || '<tr><td colspan="3">暂无成绩项目</td></tr>'}</tbody>
</table>
</div>
<p class="exam-remark">备注:${escapeHtml(result.remark || '无')}</p>
</article>
</div>
`;
}
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 = '<option value="">考试项目加载失败</option>';
renderError(`考试项目加载失败:${error.message}`);
}
}
searchButton.addEventListener('click', handleSearch);
[examSelect, nameInput, admissionInput].forEach(input => {
input.addEventListener('keydown', event => {
if (event.key === 'Enter') handleSearch();
});
});
init();