You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

435 lines
20 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>“光联万物 创芯未来”首届沪苏协同新兴产业科创大赛报名系统</title>
<link rel="stylesheet" href="../assets/bootstrap.min.css" />
<link rel="stylesheet" href="../assets/prototype.css" />
</head>
<body class="prototype-page admin-desktop-page">
<div class="container-fluid p-3 p-md-4">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
<h5 class="mb-0 section-title">综合看板</h5>
</div>
<div class="row g-3 mb-1" id="kpiGrid"></div>
<div class="row g-3 mt-1 report-main-grid">
<div class="col-lg-6">
<div class="d-flex flex-column gap-3 h-100">
<div class="card report-section-card h-100">
<div class="card-body">
<h6 class="report-section-title">赛道报名结构</h6>
<div id="trackBars"></div>
</div>
</div>
<div class="card report-section-card h-100">
<div class="card-body">
<h6 class="report-section-title">学历结构</h6>
<div id="degreeBars"></div>
</div>
</div>
<div class="card report-section-card h-100">
<div class="card-body">
<h6 class="report-section-title">院校活跃度 TOP5</h6>
<div class="table-responsive">
<table class="table table-sm align-middle mb-0 report-table" id="schoolTopTable">
<thead>
<tr>
<th>排名</th>
<th>院校</th>
<th>报名数</th>
<th>通过率</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="d-flex flex-column gap-3 h-100">
<div class="card report-section-card h-100">
<div class="card-body">
<h6 class="report-section-title">地区分布(国家)</h6>
<div id="countryBars"></div>
<hr class="my-4 report-split-line" />
<h6 class="report-section-title">地区分布(省份TOP6)</h6>
<div id="provinceBars"></div>
</div>
</div>
<div class="card report-section-card h-100">
<div class="card-body">
<h6 class="report-section-title">城市活跃度 TOP5</h6>
<div class="table-responsive">
<table class="table table-sm align-middle mb-0 report-table" id="cityTopTable">
<thead>
<tr>
<th>排名</th>
<th>城市</th>
<th>报名数</th>
<th>通过率</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row g-3 mt-1">
<div class="col-lg-5">
<div class="card report-section-card h-100">
<div class="card-body">
<h6 class="report-section-title mb-2">赛道评审均分排行</h6>
<div class="table-responsive">
<table class="table table-sm align-middle mb-0 report-table" id="trackScoreRankTable">
<thead>
<tr>
<th>排名</th>
<th>赛道</th>
<th>均分</th>
<th>项目数</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-lg-7">
<div class="card report-section-card h-100">
<div class="card-body">
<div id="trackTop5Groups" class="d-flex flex-column gap-3"></div>
</div>
</div>
</div>
</div>
</div>
<script src="../assets/bootstrap.bundle.min.js"></script>
<script>
(function () {
const seedReportData = [
{ id: "P2026-0001", session: "上海赛区", track: "场景与应用", status: "待评审", degree: "硕士", country: "中国", province: "江苏省", city: "南京市", companyName: "江苏智享科技有限公司", submittedAt: "2026-04-27 10:25", reviewedDays: null },
{ id: "P2026-0002", session: "苏州赛区", track: "材料与设备", status: "已通过", degree: "本科", country: "中国", province: "浙江省", city: "杭州市", companyName: "杭州文旅创新有限公司", submittedAt: "2026-04-26 16:08", reviewedDays: 2 },
{ id: "P2026-0003", session: "深圳赛区", track: "制造与封装", status: "已驳回", degree: "本科", country: "中国", province: "湖北省", city: "武汉市", companyName: "", submittedAt: "2026-04-25 09:40", reviewedDays: 3 },
{ id: "P2026-0004", session: "上海赛区", track: "材料与设备", status: "待评审", degree: "硕士", country: "中国", province: "江苏省", city: "苏州市", companyName: "", submittedAt: "2026-04-24 14:20", reviewedDays: null },
{ id: "P2026-0005", session: "苏州赛区", track: "制造与封装", status: "已通过", degree: "博士", country: "中国", province: "广东省", city: "广州市", companyName: "银发健康科技有限公司", submittedAt: "2026-04-23 11:05", reviewedDays: 2 },
{ id: "P2026-0006", session: "深圳赛区", track: "芯片与组件", status: "待评审", degree: "本科", country: "中国", province: "福建省", city: "厦门市", companyName: "厦门绿色材料有限公司", submittedAt: "2026-04-22 09:30", reviewedDays: null },
{ id: "P2026-0007", session: "上海赛区", track: "制造与封装", status: "已通过", degree: "硕士", country: "中国", province: "江苏省", city: "南京市", companyName: "", submittedAt: "2026-04-21 16:45", reviewedDays: 1 },
{ id: "P2026-0008", session: "苏州赛区", track: "芯片与组件", status: "已驳回", degree: "大专", country: "中国", province: "河南省", city: "郑州市", companyName: "", submittedAt: "2026-04-20 10:10", reviewedDays: 4 },
{ id: "P2026-0009", session: "深圳赛区", track: "场景与应用", status: "已通过", degree: "硕士", country: "中国", province: "上海市", city: "上海市", companyName: "上海灵动AI科技有限公司", submittedAt: "2026-04-19 13:20", reviewedDays: 2 },
{ id: "P2026-0010", session: "上海赛区", track: "材料与设备", status: "待评审", degree: "本科", country: "中国", province: "四川省", city: "成都市", companyName: "成都文创消费运营中心", submittedAt: "2026-04-18 18:10", reviewedDays: null },
{ id: "P2026-0011", session: "苏州赛区", track: "制造与封装", status: "已通过", degree: "本科", country: "海外", province: "", city: "新加坡", companyName: "SeaWellness Pte. Ltd.", submittedAt: "2026-04-17 08:50", reviewedDays: 3 },
{ id: "P2026-0012", session: "深圳赛区", track: "芯片与组件", status: "待评审", degree: "硕士", country: "中国", province: "山东省", city: "青岛市", companyName: "", submittedAt: "2026-04-16 15:35", reviewedDays: null },
{ id: "P2026-0013", session: "上海赛区", track: "场景与应用", status: "已驳回", degree: "本科", country: "中国", province: "江苏省", city: "无锡市", companyName: "", submittedAt: "2026-04-15 12:26", reviewedDays: 5 },
{ id: "P2026-0014", session: "苏州赛区", track: "材料与设备", status: "已通过", degree: "硕士", country: "中国", province: "浙江省", city: "宁波市", companyName: "宁波文旅科技有限公司", submittedAt: "2026-04-14 09:12", reviewedDays: 2 },
{ id: "P2026-0015", session: "深圳赛区", track: "制造与封装", status: "待评审", degree: "博士", country: "中国", province: "北京市", city: "北京市", companyName: "京创生命科技有限公司", submittedAt: "2026-04-13 19:45", reviewedDays: null },
{ id: "P2026-0016", session: "上海赛区", track: "场景与应用", status: "已通过", degree: "本科", country: "海外", province: "", city: "东京", companyName: "AI Retail Lab", submittedAt: "2026-04-12 11:30", reviewedDays: 2 }
];
// 按原有样本结构扩展为 360 条模拟项目,所有看板统计共用这一数据源。
const reportData = seedReportData.flatMap((item, index) => {
const copies = index < 8 ? 23 : 22;
return Array.from({ length: copies }, (_, copyIndex) => ({
...item,
id: item.id + "-" + String(copyIndex + 1).padStart(2, "0"),
sourceId: item.id
}));
});
const els = {
kpiGrid: document.getElementById("kpiGrid"),
trackBars: document.getElementById("trackBars"),
countryBars: document.getElementById("countryBars"),
provinceBars: document.getElementById("provinceBars"),
degreeBars: document.getElementById("degreeBars"),
schoolTopBody: document.querySelector("#schoolTopTable tbody"),
cityTopBody: document.querySelector("#cityTopTable tbody"),
trackScoreRankBody: document.querySelector("#trackScoreRankTable tbody"),
trackTop5Groups: document.getElementById("trackTop5Groups")
};
const reviewScores = {
"P2026-0001": [99, 98, 97, 96],
"P2026-0002": [99, 100, 98, 97],
"P2026-0003": [80, 78, 82, 79],
"P2026-0004": [92, 90, 91, 89],
"P2026-0005": [95, 94, 96, 95],
"P2026-0006": [88, 87, 86, 89],
"P2026-0007": [93, 92, 94, 93],
"P2026-0008": [76, 74, 75, 77],
"P2026-0009": [97, 98, 96, 97],
"P2026-0010": [90, 89, 91, 90],
"P2026-0011": [91, 92, 90, 91],
"P2026-0012": [85, 84, 86, 85],
"P2026-0013": [79, 80, 78, 77],
"P2026-0014": [94, 95, 93, 94],
"P2026-0015": [89, 90, 88, 87],
"P2026-0016": [96, 97, 95, 96]
};
function countBy(arr, keyGetter) {
return arr.reduce((acc, item) => {
const key = keyGetter(item) || "未填写";
acc[key] = (acc[key] || 0) + 1;
return acc;
}, {});
}
function toSortedEntries(obj, limit) {
const entries = Object.entries(obj).sort((a, b) => b[1] - a[1]);
return typeof limit === "number" ? entries.slice(0, limit) : entries;
}
function barGroupHTML(entries, total, emptyText) {
if (!entries.length) return '<p class="text-secondary small mb-0">' + emptyText + "</p>";
return entries.map(([label, count]) => {
const pct = total ? (count / total) * 100 : 0;
return (
'<div class="report-bar-row">' +
'<div class="report-bar-head"><span class="report-bar-label">' + label + '</span><span class="report-bar-meta">' + count + "(" + pct.toFixed(1) + '%)</span></div>' +
'<div class="progress report-progress"><div class="progress-bar" role="progressbar" style="width:' + pct.toFixed(1) + '%"></div></div>' +
"</div>"
);
}).join("");
}
function avgReviewDays(list) {
const reviewed = list.filter((x) => x.reviewedDays !== null && x.reviewedDays !== undefined);
if (!reviewed.length) return "-";
const v = reviewed.reduce((sum, item) => sum + item.reviewedDays, 0) / reviewed.length;
return v.toFixed(1) + " 天";
}
function getFilteredData() {
return reportData;
}
function renderKpis(list) {
const total = list.length;
const approved = list.filter((x) => x.status === "已通过").length;
const pending = list.filter((x) => x.status === "待评审").length;
const passRate = total ? (approved / total) * 100 : 0;
const sessionCounts = countBy(list, (x) => x.session);
const cards = [
{ label: "项目总数", value: total, type: "total" },
{ label: "上海赛区报名", value: sessionCounts["上海赛区"] || 0, type: "session" },
{ label: "苏州赛区报名", value: sessionCounts["苏州赛区"] || 0, type: "session" },
{ label: "深圳赛区报名", value: sessionCounts["深圳赛区"] || 0, type: "session" },
{ label: "待评审数量", value: pending },
{ label: "通过率", value: passRate.toFixed(1) + "%" }
];
els.kpiGrid.innerHTML = cards.map((item) => {
const modifier = item.type ? " report-kpi-card--" + item.type : "";
return (
'<div class="col-12 col-md-6 col-lg-2">' +
'<div class="report-kpi-card' + modifier + '">' +
'<div class="report-kpi-label">' + item.label + "</div>" +
'<div class="report-kpi-value">' + item.value + "</div>" +
"</div>" +
"</div>"
);
}).join("");
}
function renderCityTop(list) {
const groups = {};
list.forEach((item) => {
const city = item.city || "海外";
if (!groups[city]) groups[city] = { total: 0, pass: 0 };
groups[city].total += 1;
if (item.status === "已通过") groups[city].pass += 1;
});
const top = Object.entries(groups).sort((a, b) => b[1].total - a[1].total).slice(0, 5);
if (!top.length) {
els.cityTopBody.innerHTML = '<tr><td colspan="4" class="text-center text-secondary py-3">暂无数据</td></tr>';
return;
}
els.cityTopBody.innerHTML = top.map(([city, v], idx) => {
const rate = v.total ? ((v.pass / v.total) * 100).toFixed(1) + "%" : "-";
return "<tr><td>" + (idx + 1) + "</td><td>" + city + "</td><td>" + v.total + "</td><td>" + rate + "</td></tr>";
}).join("");
}
function getSchoolName(item) {
const schoolMap = {
"P2026-0001": "南京大学",
"P2026-0002": "浙江大学",
"P2026-0003": "华中科技大学",
"P2026-0004": "苏州大学",
"P2026-0005": "中山大学",
"P2026-0006": "厦门大学",
"P2026-0007": "东南大学",
"P2026-0008": "郑州大学",
"P2026-0009": "复旦大学",
"P2026-0010": "四川大学",
"P2026-0011": "新加坡国立大学",
"P2026-0012": "中国海洋大学",
"P2026-0013": "江南大学",
"P2026-0014": "宁波大学",
"P2026-0015": "清华大学",
"P2026-0016": "东京大学"
};
return schoolMap[item.sourceId || item.id] || "未填写";
}
function renderSchoolTop(list) {
const groups = {};
list.forEach((item) => {
const school = getSchoolName(item);
if (!groups[school]) groups[school] = { total: 0, pass: 0 };
groups[school].total += 1;
if (item.status === "已通过") groups[school].pass += 1;
});
const top = Object.entries(groups).sort((a, b) => b[1].total - a[1].total).slice(0, 5);
if (!top.length) {
els.schoolTopBody.innerHTML = '<tr><td colspan="4" class="text-center text-secondary py-3">暂无数据</td></tr>';
return;
}
els.schoolTopBody.innerHTML = top.map(([school, v], idx) => {
const rate = v.total ? ((v.pass / v.total) * 100).toFixed(1) + "%" : "-";
return "<tr><td>" + (idx + 1) + "</td><td>" + school + "</td><td>" + v.total + "</td><td>" + rate + "</td></tr>";
}).join("");
}
function getProjectAvgScore(item) {
const scores = reviewScores[item.sourceId || item.id] || [];
if (!scores.length) return null;
return scores.reduce((sum, n) => sum + n, 0) / scores.length;
}
function renderTrackScoreRank(list) {
const groups = {};
list.forEach((item) => {
const avg = getProjectAvgScore(item);
if (avg === null) return;
if (!groups[item.track]) groups[item.track] = [];
groups[item.track].push(avg);
});
const ranks = Object.entries(groups)
.map(([track, arr]) => ({
track,
avg: arr.reduce((s, n) => s + n, 0) / arr.length,
count: arr.length
}))
.sort((a, b) => b.avg - a.avg);
if (!ranks.length) {
els.trackScoreRankBody.innerHTML = '<tr><td colspan="4" class="text-center text-secondary py-3">暂无评审数据</td></tr>';
return;
}
els.trackScoreRankBody.innerHTML = ranks
.map((r, idx) => {
return "<tr><td>" + (idx + 1) + "</td><td>" + r.track + "</td><td>" + r.avg.toFixed(2) + "</td><td>" + r.count + "</td></tr>";
})
.join("");
}
function renderTrackTop5(list) {
const projectNameMap = {
"P2026-0001": "高速硅光芯片研发平台",
"P2026-0002": "高密度光模块封装方案",
"P2026-0003": "智能光纤传感系统",
"P2026-0004": "数字文旅导览",
"P2026-0005": "银发助餐平台",
"P2026-0006": "绿色包装新材料",
"P2026-0007": "社区健康驿站",
"P2026-0008": "夜经济市集运营",
"P2026-0009": "灵动AI消费助手",
"P2026-0010": "商旅文创运营中台",
"P2026-0011": "海外制造与封装服务",
"P2026-0012": "海洋功能消费品",
"P2026-0013": "零售数字人导购",
"P2026-0014": "港城夜游文旅项目",
"P2026-0015": "智慧生命健康站",
"P2026-0016": "AI Retail Lab"
};
const byTrack = {};
list.forEach((item) => {
const avg = getProjectAvgScore(item);
if (avg === null) return;
if (!byTrack[item.track]) byTrack[item.track] = [];
const sourceId = item.sourceId || item.id;
byTrack[item.track].push({ id: item.id, name: projectNameMap[sourceId] || item.id, avg });
});
const tracks = Object.keys(byTrack);
if (!tracks.length) {
els.trackTop5Groups.innerHTML = '<p class="text-secondary small mb-0">暂无评审数据</p>';
return;
}
const cards = tracks
.sort()
.map((track) => {
const rows = byTrack[track]
.sort((a, b) => b.avg - a.avg)
.slice(0, 3)
.map((item, idx) => {
return (
'<li class="top5-item-row">' +
'<span class="top5-item-rank">' + (idx + 1) + "</span>" +
'<span class="top5-item-name">' + item.name + "</span>" +
'<span class="top5-item-score">' + item.avg.toFixed(2) + "</span>" +
"</li>"
);
})
.join("");
return (
'<div class="col-12 col-md-6">' +
'<div class="top5-track-card h-100">' +
'<div class="top5-track-title">' + track + " TOP3</div>" +
'<ul class="top5-track-list">' + rows + "</ul>" +
"</div>" +
"</div>"
);
})
.join("");
els.trackTop5Groups.innerHTML = '<div class="row g-2">' + cards + "</div>";
}
function render() {
const list = getFilteredData();
const total = list.length;
renderKpis(list);
els.trackBars.innerHTML = barGroupHTML(
toSortedEntries(countBy(list, (x) => x.track)),
total,
"当前筛选条件下暂无赛道数据。"
);
els.countryBars.innerHTML = barGroupHTML(
toSortedEntries(countBy(list, (x) => x.country)),
total,
"当前筛选条件下暂无国家数据。"
);
els.provinceBars.innerHTML = barGroupHTML(
toSortedEntries(countBy(list.filter((x) => x.country === "中国"), (x) => x.province), 6),
total,
"当前筛选条件下暂无省份数据。"
);
els.degreeBars.innerHTML = barGroupHTML(
toSortedEntries(countBy(list, (x) => x.degree)),
total,
"当前筛选条件下暂无学历数据。"
);
renderSchoolTop(list);
renderCityTop(list);
renderTrackScoreRank(list);
renderTrackTop5(list);
}
render();
})();
</script>
</body>
</html>