+124
-13
@@ -20,7 +20,7 @@
|
|||||||
<p>支持按证书编号、持有人姓名与发证日期进行检索,也可以直接扫码快速定位证书记录。</p>
|
<p>支持按证书编号、持有人姓名与发证日期进行检索,也可以直接扫码快速定位证书记录。</p>
|
||||||
<div class="hero-meta">
|
<div class="hero-meta">
|
||||||
<span class="meta-chip">数据范围:2024 年 10 月至今</span>
|
<span class="meta-chip">数据范围:2024 年 10 月至今</span>
|
||||||
<span class="meta-chip">查询方式:文本检索 / 二维码扫描</span>
|
<span class="meta-chip">查询方式:文本检索 / 条形码扫描</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<aside class="stat-card">
|
<aside class="stat-card">
|
||||||
@@ -44,8 +44,8 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="searchTerm">编号 / 姓名</label>
|
<label class="form-label" for="searchTerm">编号 / 姓名</label>
|
||||||
<div class="input-with-action">
|
<div class="input-with-action">
|
||||||
<input type="text" id="searchTerm" class="tech-input" placeholder="输入编号或姓名,或点击右侧扫码">
|
<input type="text" id="searchTerm" class="tech-input" placeholder="输入编号、姓名或奖项名,或点击右侧扫条形码">
|
||||||
<button type="button" class="icon-action" id="startScanBtn" title="扫码查询" aria-label="扫码查询">
|
<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>
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
<div id="reader"></div>
|
<div id="reader"></div>
|
||||||
<div class="scanner-line"></div>
|
<div class="scanner-line"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="scanner-text">请将二维码置于识别框内</div>
|
<div class="scanner-text">请将条形码置于识别框内</div>
|
||||||
<button class="tech-button secondary" id="stopScanBtn" type="button">退出扫描</button>
|
<button class="tech-button secondary" id="stopScanBtn" type="button">退出扫描</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -101,10 +101,24 @@
|
|||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
fps: 15,
|
fps: 15,
|
||||||
qrbox: { width: 300, height: 180 },
|
qrbox: { width: 320, height: 140 },
|
||||||
aspectRatio: 1.0
|
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 {
|
try {
|
||||||
await html5QrCode.start(
|
await html5QrCode.start(
|
||||||
{ facingMode: "environment" },
|
{ facingMode: "environment" },
|
||||||
@@ -138,7 +152,7 @@
|
|||||||
const display = document.getElementById('resultsArea');
|
const display = document.getElementById('resultsArea');
|
||||||
|
|
||||||
if (!term) {
|
if (!term) {
|
||||||
alert("请输入证书编号或持有人姓名。");
|
alert("请输入证书编号、持有人姓名或奖项名。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +160,7 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let query = sbClient.from('certificates').select('*');
|
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 (start) query = query.gte('issue_date', start);
|
||||||
if (end) query = query.lte('issue_date', end);
|
if (end) query = query.lte('issue_date', end);
|
||||||
@@ -154,13 +168,105 @@
|
|||||||
const { data, error } = await query.order('issue_date', { ascending: false });
|
const { data, error } = await query.order('issue_date', { ascending: false });
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
renderResults(data);
|
const awardRecipientCounts = await loadAwardRecipientCounts(data);
|
||||||
|
renderResults(data, awardRecipientCounts);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
display.innerHTML = `<div class="error-state">检索失败:${err.message}</div>`;
|
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 => ({
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
}[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');
|
const display = document.getElementById('resultsArea');
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
display.innerHTML = '<div class="empty-state">未找到匹配的证书记录。</div>';
|
display.innerHTML = '<div class="empty-state">未找到匹配的证书记录。</div>';
|
||||||
@@ -168,6 +274,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let html = `
|
let html = `
|
||||||
|
${renderAwardStats(data, awardRecipientCounts)}
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table class="result-table">
|
<table class="result-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -185,10 +292,10 @@
|
|||||||
data.forEach(item => {
|
data.forEach(item => {
|
||||||
html += `
|
html += `
|
||||||
<tr>
|
<tr>
|
||||||
<td class="table-highlight">${item.cert_number || '-'}</td>
|
<td class="table-highlight">${escapeHtml(item.cert_number || '-')}</td>
|
||||||
<td>${item.holder_name || '-'}</td>
|
<td>${escapeHtml(item.holder_name || '-')}</td>
|
||||||
<td>${item.honor_title || '-'}</td>
|
<td>${escapeHtml(item.honor_title || '-')}</td>
|
||||||
<td>${item.issue_date || '-'}</td>
|
<td>${escapeHtml(item.issue_date || '-')}</td>
|
||||||
<td><button class="tech-link-button secondary" type="button" onclick="goToDetail('${item.id}')">查看详情</button></td>
|
<td><button class="tech-link-button secondary" type="button" onclick="goToDetail('${item.id}')">查看详情</button></td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
@@ -211,6 +318,10 @@
|
|||||||
params.get('cert_number') ||
|
params.get('cert_number') ||
|
||||||
params.get('certNumber') ||
|
params.get('certNumber') ||
|
||||||
params.get('number') ||
|
params.get('number') ||
|
||||||
|
params.get('awardName') ||
|
||||||
|
params.get('honor_title') ||
|
||||||
|
params.get('honorTitle') ||
|
||||||
|
params.get('award') ||
|
||||||
params.get('q') ||
|
params.get('q') ||
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
|||||||
+148
-2
@@ -329,6 +329,141 @@ a {
|
|||||||
overflow-x: auto;
|
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 {
|
.result-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 720px;
|
min-width: 720px;
|
||||||
@@ -436,8 +571,9 @@ a {
|
|||||||
|
|
||||||
.scanner-frame {
|
.scanner-frame {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: min(88vw, 360px);
|
width: min(92vw, 520px);
|
||||||
height: min(88vw, 360px);
|
height: min(54vw, 240px);
|
||||||
|
min-height: 180px;
|
||||||
border-radius: 28px;
|
border-radius: 28px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid rgba(103, 213, 255, 0.28);
|
border: 1px solid rgba(103, 213, 255, 0.28);
|
||||||
@@ -877,6 +1013,16 @@ a {
|
|||||||
grid-template-columns: 1fr;
|
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 {
|
.result-table {
|
||||||
min-width: 620px;
|
min-width: 620px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user