更新功能
Vercel Deploy / deploy (push) Successful in 1m7s

This commit is contained in:
2026-04-30 19:33:01 +08:00
Unverified
parent 290f1df7fa
commit 3185abfef4
2 changed files with 272 additions and 15 deletions
+124 -13
View File
@@ -20,7 +20,7 @@
<p>支持按证书编号、持有人姓名与发证日期进行检索,也可以直接扫码快速定位证书记录。</p>
<div class="hero-meta">
<span class="meta-chip">数据范围:2024 年 10 月至今</span>
<span class="meta-chip">查询方式:文本检索 / 二维码扫描</span>
<span class="meta-chip">查询方式:文本检索 / 条形码扫描</span>
</div>
</div>
<aside class="stat-card">
@@ -44,8 +44,8 @@
<div class="form-group">
<label class="form-label" for="searchTerm">编号 / 姓名</label>
<div class="input-with-action">
<input type="text" id="searchTerm" class="tech-input" placeholder="输入编号或姓名,或点击右侧扫码">
<button type="button" class="icon-action" id="startScanBtn" title="扫码查询" aria-label="扫码查询">
<input type="text" id="searchTerm" class="tech-input" placeholder="输入编号、姓名或奖项名,或点击右侧扫条形码">
<button type="button" class="icon-action" id="startScanBtn" title="扫条形码查询" aria-label="扫条形码查询">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7V5a2 2 0 0 1 2-2h2"></path><path d="M17 3h2a2 2 0 0 1 2 2v2"></path><path d="M21 17v2a2 2 0 0 1-2 2h-2"></path><path d="M7 21H5a2 2 0 0 1-2-2v-2"></path><rect x="7" y="7" width="10" height="10"></rect></svg>
</button>
</div>
@@ -80,7 +80,7 @@
<div id="reader"></div>
<div class="scanner-line"></div>
</div>
<div class="scanner-text">请将二维码置于识别框内</div>
<div class="scanner-text">请将条形码置于识别框内</div>
<button class="tech-button secondary" id="stopScanBtn" type="button">退出扫描</button>
</div>
@@ -101,10 +101,24 @@
const config = {
fps: 15,
qrbox: { width: 300, height: 180 },
qrbox: { width: 320, height: 140 },
aspectRatio: 1.0
};
if (typeof Html5QrcodeSupportedFormats !== 'undefined') {
config.formatsToSupport = [
Html5QrcodeSupportedFormats.CODE_128,
Html5QrcodeSupportedFormats.CODE_39,
Html5QrcodeSupportedFormats.CODE_93,
Html5QrcodeSupportedFormats.EAN_13,
Html5QrcodeSupportedFormats.EAN_8,
Html5QrcodeSupportedFormats.UPC_A,
Html5QrcodeSupportedFormats.UPC_E,
Html5QrcodeSupportedFormats.ITF,
Html5QrcodeSupportedFormats.CODABAR
];
}
try {
await html5QrCode.start(
{ facingMode: "environment" },
@@ -138,7 +152,7 @@
const display = document.getElementById('resultsArea');
if (!term) {
alert("请输入证书编号持有人姓名。");
alert("请输入证书编号持有人姓名或奖项名。");
return;
}
@@ -146,7 +160,7 @@
try {
let query = sbClient.from('certificates').select('*');
query = query.or(`cert_number.ilike.%${term}%,holder_name.ilike.%${term}%`);
query = query.or(`cert_number.ilike.%${term}%,holder_name.ilike.%${term}%,honor_title.ilike.%${term}%`);
if (start) query = query.gte('issue_date', start);
if (end) query = query.lte('issue_date', end);
@@ -154,13 +168,105 @@
const { data, error } = await query.order('issue_date', { ascending: false });
if (error) throw error;
renderResults(data);
const awardRecipientCounts = await loadAwardRecipientCounts(data);
renderResults(data, awardRecipientCounts);
} catch (err) {
display.innerHTML = `<div class="error-state">检索失败:${err.message}</div>`;
}
}
function renderResults(data) {
async function loadAwardRecipientCounts(data) {
const awardTitles = [...new Set((data || []).map(item => item.honor_title).filter(Boolean))];
if (awardTitles.length === 0) return {};
const countPairs = await Promise.all(awardTitles.map(async title => {
const { count, error } = await sbClient
.from('certificates')
.select('id', { count: 'exact', head: true })
.eq('honor_title', title);
if (error) throw error;
return [title, count || 0];
}));
return Object.fromEntries(countPairs);
}
function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, char => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[char]));
}
function renderAwardStats(data, awardRecipientCounts) {
const holderCounts = (data || []).reduce((counts, item) => {
const holderName = item.holder_name || '未填写';
counts[holderName] = (counts[holderName] || 0) + 1;
return counts;
}, {});
const holderStats = Object.entries(holderCounts)
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0], 'zh-CN'));
const maxHolderCount = Math.max(...holderStats.map(([, count]) => count), 1);
const awardStats = [...new Set((data || []).map(item => item.honor_title).filter(Boolean))]
.map(title => ({
title,
count: awardRecipientCounts[title] || 0
}))
.sort((a, b) => b.count - a.count || a.title.localeCompare(b.title, 'zh-CN'));
const holderBars = holderStats.map(([name, count]) => `
<div class="chart-row">
<div class="chart-row-label" title="${escapeHtml(name)}">${escapeHtml(name)}</div>
<div class="chart-bar-track">
<div class="chart-bar-fill" style="width: ${Math.max((count / maxHolderCount) * 100, 6)}%;"></div>
</div>
<div class="chart-row-value">${count} 项</div>
</div>
`).join('');
const awardChips = awardStats.map(item => `
<div class="award-count-chip">
<span class="award-count-title" title="${escapeHtml(item.title)}">${escapeHtml(item.title)}</span>
<strong>${item.count}</strong>
<span>人获得</span>
</div>
`).join('');
return `
<div class="results-summary">
<div class="summary-stat-tile">
<span>查询结果</span>
<strong>${data.length}</strong>
</div>
<div class="summary-stat-tile">
<span>涉及人员</span>
<strong>${holderStats.length}</strong>
</div>
<div class="summary-stat-tile">
<span>奖项种类</span>
<strong>${awardStats.length}</strong>
</div>
</div>
<div class="stats-grid">
<section class="stats-panel">
<div class="stats-panel-title">个人奖项数量</div>
<div class="bar-chart">${holderBars}</div>
</section>
<section class="stats-panel">
<div class="stats-panel-title">查询奖项获得人数</div>
<div class="award-count-grid">${awardChips || '<div class="muted-note">暂无奖项名称</div>'}</div>
</section>
</div>
`;
}
function renderResults(data, awardRecipientCounts = {}) {
const display = document.getElementById('resultsArea');
if (!data || data.length === 0) {
display.innerHTML = '<div class="empty-state">未找到匹配的证书记录。</div>';
@@ -168,6 +274,7 @@
}
let html = `
${renderAwardStats(data, awardRecipientCounts)}
<div class="table-wrapper">
<table class="result-table">
<thead>
@@ -185,10 +292,10 @@
data.forEach(item => {
html += `
<tr>
<td class="table-highlight">${item.cert_number || '-'}</td>
<td>${item.holder_name || '-'}</td>
<td>${item.honor_title || '-'}</td>
<td>${item.issue_date || '-'}</td>
<td class="table-highlight">${escapeHtml(item.cert_number || '-')}</td>
<td>${escapeHtml(item.holder_name || '-')}</td>
<td>${escapeHtml(item.honor_title || '-')}</td>
<td>${escapeHtml(item.issue_date || '-')}</td>
<td><button class="tech-link-button secondary" type="button" onclick="goToDetail('${item.id}')">查看详情</button></td>
</tr>
`;
@@ -211,6 +318,10 @@
params.get('cert_number') ||
params.get('certNumber') ||
params.get('number') ||
params.get('awardName') ||
params.get('honor_title') ||
params.get('honorTitle') ||
params.get('award') ||
params.get('q') ||
'';
+148 -2
View File
@@ -329,6 +329,141 @@ a {
overflow-x: auto;
}
.results-summary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
padding: 16px;
border-bottom: 1px solid rgba(103, 213, 255, 0.12);
}
.summary-stat-tile {
min-height: 86px;
display: flex;
flex-direction: column;
justify-content: center;
gap: 8px;
padding: 14px 16px;
border-radius: var(--radius-md);
border: 1px solid rgba(103, 213, 255, 0.14);
background: rgba(255, 255, 255, 0.035);
}
.summary-stat-tile span,
.stats-panel-title,
.muted-note {
color: var(--text-secondary);
}
.summary-stat-tile span {
font-size: 13px;
letter-spacing: 0.08em;
}
.summary-stat-tile strong {
font-family: var(--font-display);
font-size: 30px;
color: var(--accent);
line-height: 1;
}
.stats-grid {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
gap: 16px;
padding: 0 16px 16px;
}
.stats-panel {
min-width: 0;
padding: 16px;
border-radius: var(--radius-md);
border: 1px solid rgba(103, 213, 255, 0.14);
background: rgba(255, 255, 255, 0.025);
}
.stats-panel-title {
margin-bottom: 14px;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.08em;
}
.bar-chart {
display: grid;
gap: 12px;
}
.chart-row {
display: grid;
grid-template-columns: minmax(88px, 0.35fr) minmax(120px, 1fr) 56px;
gap: 12px;
align-items: center;
}
.chart-row-label,
.award-count-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chart-row-label {
color: var(--text-primary);
font-weight: 600;
}
.chart-bar-track {
height: 12px;
overflow: hidden;
border-radius: 999px;
background: rgba(103, 213, 255, 0.1);
}
.chart-bar-fill {
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, var(--accent), var(--success));
box-shadow: 0 0 18px rgba(103, 213, 255, 0.25);
}
.chart-row-value {
color: var(--accent);
font-weight: 700;
text-align: right;
}
.award-count-grid {
display: grid;
gap: 10px;
}
.award-count-chip {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
gap: 8px;
align-items: center;
padding: 12px 14px;
border-radius: var(--radius-sm);
border: 1px solid rgba(147, 175, 209, 0.14);
background: rgba(3, 8, 18, 0.36);
}
.award-count-chip strong {
color: var(--success);
font-family: var(--font-display);
}
.award-count-chip span:last-child {
color: var(--text-secondary);
font-size: 13px;
}
.muted-note {
padding: 12px 0;
text-align: center;
}
.result-table {
width: 100%;
min-width: 720px;
@@ -436,8 +571,9 @@ a {
.scanner-frame {
position: relative;
width: min(88vw, 360px);
height: min(88vw, 360px);
width: min(92vw, 520px);
height: min(54vw, 240px);
min-height: 180px;
border-radius: 28px;
overflow: hidden;
border: 1px solid rgba(103, 213, 255, 0.28);
@@ -877,6 +1013,16 @@ a {
grid-template-columns: 1fr;
}
.results-summary,
.stats-grid {
grid-template-columns: 1fr;
}
.chart-row {
grid-template-columns: minmax(74px, 0.4fr) minmax(90px, 1fr) 52px;
gap: 8px;
}
.result-table {
min-width: 620px;
}