成绩查询
Vercel Deploy / deploy (push) Successful in 55s

This commit is contained in:
2026-06-21 14:03:06 +08:00 Unverified
parent 1111372dc9
commit b97a2faaff
4 changed files with 585 additions and 0 deletions
+228
View File
@@ -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 => ({
'&': '&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();