+228
@@ -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 = `<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();
|
||||
Reference in New Issue
Block a user