fix: stabilize zhihu export and dashboard flow
This commit is contained in:
637
resources/zhihu-hotlist-echarts.html
Normal file
637
resources/zhihu-hotlist-echarts.html
Normal file
@@ -0,0 +1,637 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>知乎热榜图表驾驶舱</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #06111f;
|
||||
--bg-2: #0a1f37;
|
||||
--panel: rgba(8, 25, 42, 0.88);
|
||||
--panel-strong: rgba(10, 32, 55, 0.95);
|
||||
--line: rgba(101, 187, 255, 0.18);
|
||||
--line-strong: rgba(236, 186, 81, 0.26);
|
||||
--text: #eef6ff;
|
||||
--muted: #8ea6c2;
|
||||
--accent: #62d0ff;
|
||||
--accent-2: #ecba51;
|
||||
--accent-3: #6df0c2;
|
||||
--danger: #ff8b7e;
|
||||
--shadow: 0 20px 48px rgba(0, 0, 0, 0.34);
|
||||
--font-heading: "DIN Alternate", "Bahnschrift", "Microsoft YaHei UI", sans-serif;
|
||||
--font-body: "Segoe UI Variable Text", "Microsoft YaHei", "PingFang SC", sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
background:
|
||||
radial-gradient(circle at 16% 10%, rgba(98, 208, 255, 0.18), transparent 22%),
|
||||
radial-gradient(circle at 86% 12%, rgba(236, 186, 81, 0.14), transparent 18%),
|
||||
linear-gradient(145deg, var(--bg) 0%, var(--bg-2) 42%, #030910 100%);
|
||||
color: var(--text);
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background-image:
|
||||
linear-gradient(rgba(101, 187, 255, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(101, 187, 255, 0.05) 1px, transparent 1px);
|
||||
background-size: 44px 44px;
|
||||
mask-image: radial-gradient(circle at center, black 34%, rgba(0, 0, 0, 0.22) 88%, transparent 100%);
|
||||
}
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
padding: 18px;
|
||||
display: grid;
|
||||
grid-template-rows: auto auto 1fr auto;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.01)),
|
||||
linear-gradient(145deg, rgba(9, 30, 51, 0.97), rgba(6, 20, 34, 0.92));
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 22px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.panel::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 18px;
|
||||
right: 18px;
|
||||
top: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, var(--accent), var(--accent-2), transparent);
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 18px 24px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 360px;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
color: var(--accent);
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-family: var(--font-heading);
|
||||
font-size: 38px;
|
||||
line-height: 1.08;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
#snapshot-meta {
|
||||
margin: 10px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.hero-notes {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.note-card {
|
||||
padding: 14px 16px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(135deg, rgba(98, 208, 255, 0.08), rgba(236, 186, 81, 0.08));
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.note-card strong {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.note-card span {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.metric {
|
||||
padding: 18px 18px 16px;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
margin-top: 10px;
|
||||
font-family: var(--font-heading);
|
||||
font-size: 34px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.metric-sub {
|
||||
margin-top: 8px;
|
||||
color: var(--accent);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.charts {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1fr 0.95fr;
|
||||
grid-template-rows: 360px 320px;
|
||||
gap: 14px;
|
||||
grid-template-areas:
|
||||
"bar top pie"
|
||||
"bubble table table";
|
||||
}
|
||||
|
||||
.chart-panel {
|
||||
padding: 14px 16px 12px;
|
||||
}
|
||||
|
||||
.bar-panel { grid-area: bar; }
|
||||
.top-panel { grid-area: top; }
|
||||
.pie-panel { grid-area: pie; }
|
||||
.bubble-panel { grid-area: bubble; }
|
||||
.table-panel { grid-area: table; padding: 14px 16px 10px; }
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.section-head h2 {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-family: var(--font-heading);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.section-head span {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: calc(100% - 42px);
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
height: calc(100% - 42px);
|
||||
overflow: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
background: rgba(6, 19, 32, 0.96);
|
||||
padding: 10px 8px;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
border-bottom: 1px solid var(--line-strong);
|
||||
}
|
||||
|
||||
tbody td {
|
||||
padding: 11px 8px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
font-size: 13px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(odd) {
|
||||
background: rgba(255, 255, 255, 0.016);
|
||||
}
|
||||
|
||||
.rank {
|
||||
font-family: var(--font-heading);
|
||||
color: var(--accent-2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.heat {
|
||||
color: var(--accent-3);
|
||||
font-family: var(--font-heading);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(98, 208, 255, 0.12);
|
||||
color: var(--accent);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 10px 16px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1440px) {
|
||||
.hero {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.metrics {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.charts {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 320px 320px 320px 320px 420px;
|
||||
grid-template-areas:
|
||||
"bar"
|
||||
"top"
|
||||
"pie"
|
||||
"bubble"
|
||||
"table";
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.page {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.metrics {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<section class="panel hero">
|
||||
<div>
|
||||
<div class="eyebrow">Zhihu Hotlist Visual Command Center</div>
|
||||
<h1>知乎热榜图表驾驶舱</h1>
|
||||
<p id="snapshot-meta">由 sgClaw screen_html_export 生成的本地静态展示页</p>
|
||||
</div>
|
||||
<div class="hero-notes">
|
||||
<div class="note-card">
|
||||
<strong>图表表达</strong>
|
||||
<span>同一份热榜数据同时映射为分类热度、头部热点、结构占比和热度散点,适合现场讲解图表能力。</span>
|
||||
</div>
|
||||
<div class="note-card">
|
||||
<strong>演示建议</strong>
|
||||
<span id="lead-summary">优先讲解榜首热点、分类分布与热度层级,再向下展开全量榜单细节。</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="metrics">
|
||||
<article class="panel metric">
|
||||
<div class="metric-label">热榜条目数</div>
|
||||
<div id="metric-total" class="metric-value">0</div>
|
||||
<div class="metric-sub">Tracked items</div>
|
||||
</article>
|
||||
<article class="panel metric">
|
||||
<div class="metric-label">主题分类数</div>
|
||||
<div id="metric-categories" class="metric-value">0</div>
|
||||
<div class="metric-sub">Topic groups</div>
|
||||
</article>
|
||||
<article class="panel metric">
|
||||
<div class="metric-label">累计热度</div>
|
||||
<div id="metric-heat" class="metric-value">0</div>
|
||||
<div class="metric-sub">Total heat</div>
|
||||
</article>
|
||||
<article class="panel metric">
|
||||
<div class="metric-label">头部峰值</div>
|
||||
<div id="metric-peak" class="metric-value">0</div>
|
||||
<div class="metric-sub">Peak topic heat</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="charts">
|
||||
<section class="panel chart-panel bar-panel">
|
||||
<div class="section-head">
|
||||
<h2>分类总热度</h2>
|
||||
<span>横向对比</span>
|
||||
</div>
|
||||
<div id="bar-chart" class="chart"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel chart-panel top-panel">
|
||||
<div class="section-head">
|
||||
<h2>Top10 热点</h2>
|
||||
<span>柱状排行</span>
|
||||
</div>
|
||||
<div id="top-chart" class="chart"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel chart-panel pie-panel">
|
||||
<div class="section-head">
|
||||
<h2>分类占比</h2>
|
||||
<span>环形结构</span>
|
||||
</div>
|
||||
<div id="pie-chart" class="chart"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel chart-panel bubble-panel">
|
||||
<div class="section-head">
|
||||
<h2>热度分层</h2>
|
||||
<span>散点气泡</span>
|
||||
</div>
|
||||
<div id="bubble-chart" class="chart"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel table-panel">
|
||||
<div class="section-head">
|
||||
<h2>热榜明细</h2>
|
||||
<span id="table-note">按原始顺序保留</span>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>排名</th>
|
||||
<th>标题</th>
|
||||
<th>分类</th>
|
||||
<th>热度</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="table-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="panel footer">
|
||||
本页由 `screen_html_export` 生成,适合在系统浏览器中直接打开进行展示。
|
||||
</section>
|
||||
</div>
|
||||
<script>
|
||||
const defaultPayload = {
|
||||
"snapshot_id": "template-snapshot",
|
||||
"generated_at_ms": 0,
|
||||
"categories": [],
|
||||
"table": []
|
||||
}
|
||||
|
||||
const themeMeta = {
|
||||
title: "知乎热榜图表驾驶舱",
|
||||
renderer: "screen_html_export"
|
||||
};
|
||||
|
||||
const chartColors = ["#62d0ff", "#ecba51", "#6df0c2", "#7f8cff", "#ff8b7e", "#9fcbff", "#58a6ff"];
|
||||
const charts = {};
|
||||
|
||||
function formatNumber(value) {
|
||||
return new Intl.NumberFormat("zh-CN").format(Number(value || 0));
|
||||
}
|
||||
|
||||
function getTotalHeat(categories) {
|
||||
return (categories || []).reduce((sum, item) => sum + Number(item.total_heat || 0), 0);
|
||||
}
|
||||
|
||||
function getPeakHeat(table) {
|
||||
return (table || []).reduce((max, row) => Math.max(max, Number(row.heat_value || 0)), 0);
|
||||
}
|
||||
|
||||
function buildLeadSummary(table, categories) {
|
||||
const top = (table || [])[0];
|
||||
const category = (categories || []).slice().sort((a, b) => (b.total_heat || 0) - (a.total_heat || 0))[0];
|
||||
const parts = [];
|
||||
if (top) {
|
||||
parts.push(`榜首是“${top.title}”`);
|
||||
}
|
||||
if (category) {
|
||||
parts.push(`主导分类为“${category.category_label}”`);
|
||||
}
|
||||
parts.push(`共覆盖 ${(table || []).length} 条热点`);
|
||||
return parts.join(",");
|
||||
}
|
||||
|
||||
function ensureCharts() {
|
||||
if (!window.echarts) {
|
||||
return;
|
||||
}
|
||||
charts.bar = charts.bar || echarts.init(document.getElementById("bar-chart"));
|
||||
charts.top = charts.top || echarts.init(document.getElementById("top-chart"));
|
||||
charts.pie = charts.pie || echarts.init(document.getElementById("pie-chart"));
|
||||
charts.bubble = charts.bubble || echarts.init(document.getElementById("bubble-chart"));
|
||||
}
|
||||
|
||||
function renderBarChart(categories) {
|
||||
const sorted = (categories || []).slice().sort((a, b) => Number(a.total_heat || 0) - Number(b.total_heat || 0));
|
||||
charts.bar.setOption({
|
||||
animationDuration: 700,
|
||||
grid: {left: 90, right: 18, top: 10, bottom: 20},
|
||||
xAxis: {
|
||||
type: "value",
|
||||
axisLabel: {color: "#8ea6c2"},
|
||||
splitLine: {lineStyle: {color: "rgba(255,255,255,0.06)"}}
|
||||
},
|
||||
yAxis: {
|
||||
type: "category",
|
||||
data: sorted.map((item) => item.category_label),
|
||||
axisLabel: {color: "#eef6ff"},
|
||||
axisLine: {lineStyle: {color: "rgba(255,255,255,0.1)"}}
|
||||
},
|
||||
tooltip: {trigger: "axis", axisPointer: {type: "shadow"}},
|
||||
series: [{
|
||||
type: "bar",
|
||||
data: sorted.map((item, index) => ({
|
||||
value: Number(item.total_heat || 0),
|
||||
itemStyle: {color: chartColors[index % chartColors.length], borderRadius: [0, 8, 8, 0]}
|
||||
})),
|
||||
label: {show: true, position: "right", color: "#dfeeff"}
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
function renderTopChart(table) {
|
||||
const top = (table || []).slice(0, 10);
|
||||
charts.top.setOption({
|
||||
animationDuration: 700,
|
||||
grid: {left: 42, right: 12, top: 26, bottom: 46},
|
||||
tooltip: {trigger: "axis", axisPointer: {type: "shadow"}},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: top.map((row) => `#${row.rank}`),
|
||||
axisLabel: {color: "#8ea6c2"},
|
||||
axisLine: {lineStyle: {color: "rgba(255,255,255,0.1)"}}
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
axisLabel: {color: "#8ea6c2"},
|
||||
splitLine: {lineStyle: {color: "rgba(255,255,255,0.06)"}}
|
||||
},
|
||||
series: [{
|
||||
type: "bar",
|
||||
data: top.map((row, index) => ({
|
||||
value: Number(row.heat_value || 0),
|
||||
itemStyle: {color: chartColors[index % chartColors.length], borderRadius: [8, 8, 0, 0]}
|
||||
})),
|
||||
label: {show: true, position: "top", color: "#eef6ff", formatter: ({dataIndex}) => top[dataIndex].heat_text}
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
function renderPieChart(categories) {
|
||||
charts.pie.setOption({
|
||||
animationDuration: 700,
|
||||
color: chartColors,
|
||||
tooltip: {trigger: "item"},
|
||||
legend: {
|
||||
bottom: 2,
|
||||
textStyle: {color: "#8ea6c2", fontSize: 11},
|
||||
itemWidth: 12,
|
||||
itemHeight: 8
|
||||
},
|
||||
series: [{
|
||||
type: "pie",
|
||||
radius: ["44%", "72%"],
|
||||
center: ["50%", "44%"],
|
||||
itemStyle: {borderColor: "#081a2c", borderWidth: 2},
|
||||
label: {
|
||||
color: "#eef6ff",
|
||||
formatter: "{b}\n{d}%"
|
||||
},
|
||||
data: (categories || []).map((item) => ({
|
||||
name: item.category_label,
|
||||
value: Number(item.total_heat || 0)
|
||||
}))
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
function renderBubbleChart(table) {
|
||||
const top = (table || []).slice(0, 12);
|
||||
charts.bubble.setOption({
|
||||
animationDuration: 700,
|
||||
color: chartColors,
|
||||
grid: {left: 44, right: 18, top: 16, bottom: 36},
|
||||
xAxis: {
|
||||
type: "value",
|
||||
name: "排名",
|
||||
inverse: true,
|
||||
min: 0,
|
||||
max: Math.max(...top.map((row) => Number(row.rank || 0)), 10) + 1,
|
||||
nameTextStyle: {color: "#8ea6c2"},
|
||||
axisLabel: {color: "#8ea6c2"},
|
||||
splitLine: {lineStyle: {color: "rgba(255,255,255,0.06)"}}
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
name: "热度值",
|
||||
nameTextStyle: {color: "#8ea6c2"},
|
||||
axisLabel: {color: "#8ea6c2"},
|
||||
splitLine: {lineStyle: {color: "rgba(255,255,255,0.06)"}}
|
||||
},
|
||||
tooltip: {
|
||||
formatter: (params) => {
|
||||
const row = params.data.raw;
|
||||
return `${row.title}<br/>排名 #${row.rank}<br/>热度 ${row.heat_text}<br/>分类 ${row.category_label}`;
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
type: "scatter",
|
||||
symbolSize: (value) => Math.max(18, Math.min(56, value[2] / 80000)),
|
||||
data: top.map((row, index) => ({
|
||||
value: [Number(row.rank || 0), Number(row.heat_value || 0), Number(row.heat_value || 0)],
|
||||
raw: row,
|
||||
itemStyle: {color: chartColors[index % chartColors.length], opacity: 0.82}
|
||||
}))
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
function renderTable(table) {
|
||||
document.getElementById("table-body").innerHTML = (table || []).map((row) => `
|
||||
<tr>
|
||||
<td class="rank">#${row.rank}</td>
|
||||
<td>${row.title}</td>
|
||||
<td><span class="tag">${row.category_label}</span></td>
|
||||
<td class="heat">${row.heat_text}</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
function render(payload) {
|
||||
const data = payload || defaultPayload;
|
||||
const categories = data.categories || [];
|
||||
const table = data.table || [];
|
||||
|
||||
document.title = themeMeta.title;
|
||||
document.getElementById("snapshot-meta").textContent =
|
||||
`${data.snapshot_id} | 生成时间 ${new Date(data.generated_at_ms || 0).toLocaleString()}`;
|
||||
document.getElementById("metric-total").textContent = formatNumber(table.length);
|
||||
document.getElementById("metric-categories").textContent = formatNumber(categories.length);
|
||||
document.getElementById("metric-heat").textContent = formatNumber(getTotalHeat(categories));
|
||||
document.getElementById("metric-peak").textContent = formatNumber(getPeakHeat(table));
|
||||
document.getElementById("lead-summary").textContent = buildLeadSummary(table, categories);
|
||||
document.getElementById("table-note").textContent =
|
||||
table.length > 0 ? `当前展示 ${table.length} 条热点` : "暂无热榜数据";
|
||||
|
||||
renderTable(table);
|
||||
ensureCharts();
|
||||
if (window.echarts) {
|
||||
renderBarChart(categories);
|
||||
renderTopChart(table);
|
||||
renderPieChart(categories);
|
||||
renderBubbleChart(table);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
Object.values(charts).forEach((chart) => chart && chart.resize());
|
||||
});
|
||||
|
||||
render(defaultPayload);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user