2445 lines
74 KiB
HTML
2445 lines
74 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN" data-theme="gov_blue_gold">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>知乎热榜主题分类分析大屏</title>
|
||
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
|
||
<style>
|
||
:root {
|
||
--bg: #071b34;
|
||
--bg-alt: #0e2a4d;
|
||
--bg-glow: rgba(62, 199, 255, 0.16);
|
||
--panel: rgba(9, 28, 53, 0.92);
|
||
--panel-strong: rgba(14, 42, 77, 0.97);
|
||
--panel-soft: rgba(12, 36, 68, 0.78);
|
||
--line: rgba(62, 199, 255, 0.2);
|
||
--line-strong: rgba(216, 179, 106, 0.34);
|
||
--text: #eaf4ff;
|
||
--muted: #8faecc;
|
||
--accent: #d8b36a;
|
||
--accent-2: #3ec7ff;
|
||
--accent-3: #5b8cff;
|
||
--success: #74c69d;
|
||
--danger: #e05a47;
|
||
--warning: #d79a43;
|
||
--shadow: 0 18px 40px rgba(0, 0, 0, 0.28);
|
||
--table-header: rgba(8, 25, 48, 0.98);
|
||
--panel-radius: 18px;
|
||
--font-main: "Segoe UI Variable Text", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||
--font-number: "Bahnschrift", "DIN Alternate", "Segoe UI", sans-serif;
|
||
}
|
||
|
||
html[data-theme="gov_blue_gold"] {
|
||
--bg: #071b34;
|
||
--bg-alt: #0e2a4d;
|
||
--bg-glow: rgba(62, 199, 255, 0.18);
|
||
--panel: rgba(9, 28, 53, 0.92);
|
||
--panel-strong: rgba(14, 42, 77, 0.97);
|
||
--panel-soft: rgba(12, 36, 68, 0.78);
|
||
--line: rgba(62, 199, 255, 0.2);
|
||
--line-strong: rgba(216, 179, 106, 0.34);
|
||
--text: #eaf4ff;
|
||
--muted: #8faecc;
|
||
--accent: #d8b36a;
|
||
--accent-2: #3ec7ff;
|
||
--accent-3: #5b8cff;
|
||
--success: #74c69d;
|
||
--danger: #e05a47;
|
||
--warning: #d79a43;
|
||
--table-header: rgba(8, 25, 48, 0.98);
|
||
}
|
||
|
||
html[data-theme="tech_cyan_blue"] {
|
||
--bg: #061a2d;
|
||
--bg-alt: #0b2844;
|
||
--bg-glow: rgba(34, 211, 238, 0.2);
|
||
--panel: rgba(6, 26, 45, 0.92);
|
||
--panel-strong: rgba(11, 40, 68, 0.97);
|
||
--panel-soft: rgba(9, 34, 58, 0.82);
|
||
--line: rgba(34, 211, 238, 0.23);
|
||
--line-strong: rgba(91, 140, 255, 0.34);
|
||
--text: #e6f7ff;
|
||
--muted: #93b7d8;
|
||
--accent: #22d3ee;
|
||
--accent-2: #5b8cff;
|
||
--accent-3: #99f6ff;
|
||
--success: #5eead4;
|
||
--danger: #fb7185;
|
||
--warning: #60a5fa;
|
||
--table-header: rgba(4, 21, 38, 0.98);
|
||
}
|
||
|
||
html[data-theme="industry_ink_green"] {
|
||
--bg: #0a1d1d;
|
||
--bg-alt: #113536;
|
||
--bg-glow: rgba(116, 198, 157, 0.18);
|
||
--panel: rgba(10, 29, 29, 0.92);
|
||
--panel-strong: rgba(17, 53, 54, 0.97);
|
||
--panel-soft: rgba(14, 45, 45, 0.82);
|
||
--line: rgba(116, 198, 157, 0.22);
|
||
--line-strong: rgba(214, 181, 110, 0.34);
|
||
--text: #edfdf7;
|
||
--muted: #99bdb7;
|
||
--accent: #d6b56e;
|
||
--accent-2: #74c69d;
|
||
--accent-3: #34d399;
|
||
--success: #74c69d;
|
||
--danger: #f87171;
|
||
--warning: #d6b56e;
|
||
--table-header: rgba(9, 26, 26, 0.98);
|
||
}
|
||
|
||
html[data-theme="meeting_red_gold"] {
|
||
--bg: #2c1111;
|
||
--bg-alt: #4b1e1e;
|
||
--bg-glow: rgba(224, 90, 71, 0.16);
|
||
--panel: rgba(44, 17, 17, 0.92);
|
||
--panel-strong: rgba(75, 30, 30, 0.97);
|
||
--panel-soft: rgba(58, 23, 23, 0.82);
|
||
--line: rgba(224, 90, 71, 0.22);
|
||
--line-strong: rgba(215, 154, 67, 0.36);
|
||
--text: #fff0ea;
|
||
--muted: #d8b4ad;
|
||
--accent: #d79a43;
|
||
--accent-2: #e05a47;
|
||
--accent-3: #f59e0b;
|
||
--success: #fca5a5;
|
||
--danger: #fb7185;
|
||
--warning: #d79a43;
|
||
--table-header: rgba(37, 14, 14, 0.98);
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
html,
|
||
body {
|
||
margin: 0;
|
||
height: 100%;
|
||
}
|
||
|
||
body {
|
||
overflow: hidden;
|
||
color: var(--text);
|
||
font-family: var(--font-main);
|
||
background:
|
||
radial-gradient(circle at 18% 16%, var(--bg-glow), transparent 24%),
|
||
radial-gradient(circle at 82% 12%, rgba(216, 179, 106, 0.12), transparent 22%),
|
||
linear-gradient(135deg, var(--bg) 0%, var(--bg-alt) 52%, var(--bg) 100%);
|
||
transition: background 0.35s ease, color 0.35s ease;
|
||
}
|
||
|
||
body::before,
|
||
body::after {
|
||
content: "";
|
||
position: fixed;
|
||
inset: 0;
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
}
|
||
|
||
body::before {
|
||
background-image:
|
||
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
|
||
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
|
||
background-size: 36px 36px;
|
||
mask-image: linear-gradient(180deg, rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0.18));
|
||
}
|
||
|
||
body::after {
|
||
background:
|
||
linear-gradient(90deg, transparent 0, rgba(255, 255, 255, 0.03) 50%, transparent 100%),
|
||
linear-gradient(180deg, transparent 0, rgba(255, 255, 255, 0.03) 50%, transparent 100%);
|
||
mix-blend-mode: screen;
|
||
opacity: 0.35;
|
||
}
|
||
|
||
button,
|
||
textarea {
|
||
font: inherit;
|
||
}
|
||
|
||
button {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.screen {
|
||
position: relative;
|
||
z-index: 1;
|
||
height: 100vh;
|
||
padding: 20px 24px 14px;
|
||
display: grid;
|
||
/* V2 height contract: header / KPI / charts / table / footer */
|
||
grid-template-rows: 92px 140px 380px minmax(0, 1fr) 44px;
|
||
gap: 14px;
|
||
}
|
||
|
||
.panel,
|
||
.screen-header,
|
||
.screen-footer,
|
||
.kpi-card,
|
||
.data-drawer {
|
||
position: relative;
|
||
overflow: hidden;
|
||
background: linear-gradient(180deg, var(--panel-strong) 0%, var(--panel) 100%);
|
||
border: 1px solid var(--line);
|
||
box-shadow: var(--shadow);
|
||
transition: background 0.35s ease, border-color 0.35s ease, color 0.35s ease;
|
||
}
|
||
|
||
.panel::before,
|
||
.screen-header::before,
|
||
.screen-footer::before,
|
||
.kpi-card::before,
|
||
.data-drawer::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
border: 1px solid rgba(255, 255, 255, 0.04);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.panel::after,
|
||
.screen-header::after,
|
||
.screen-footer::after,
|
||
.kpi-card::after,
|
||
.data-drawer::after {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 12px;
|
||
border: 1px solid rgba(255, 255, 255, 0.03);
|
||
clip-path: polygon(10px 0, calc(100% - 10px) 0, 100% 10px, 100% calc(100% - 10px), calc(100% - 10px) 100%, 10px 100%, 0 calc(100% - 10px), 0 10px);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.screen-header {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1.35fr) minmax(420px, 1.15fr);
|
||
gap: 18px;
|
||
align-items: center;
|
||
padding: 0 24px;
|
||
border-radius: 18px;
|
||
}
|
||
|
||
.header-main {
|
||
min-width: 0;
|
||
}
|
||
|
||
.header-eyebrow {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 6px 12px;
|
||
border-radius: 999px;
|
||
font-size: 12px;
|
||
letter-spacing: 0.18em;
|
||
color: var(--accent);
|
||
background: rgba(255, 255, 255, 0.04);
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.header-eyebrow::before {
|
||
content: "";
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: var(--accent-2);
|
||
box-shadow: 0 0 10px rgba(62, 199, 255, 0.4);
|
||
}
|
||
|
||
.header-title {
|
||
margin: 10px 0 6px;
|
||
font-size: 34px;
|
||
line-height: 1.1;
|
||
letter-spacing: 0.02em;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.header-subtitle {
|
||
margin: 0;
|
||
color: var(--muted);
|
||
font-size: 14px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.header-side {
|
||
display: grid;
|
||
justify-items: end;
|
||
gap: 12px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.header-chip-row {
|
||
display: flex;
|
||
align-items: stretch;
|
||
justify-content: flex-end;
|
||
flex-wrap: nowrap;
|
||
gap: 14px;
|
||
width: 100%;
|
||
min-width: 0;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
-webkit-overflow-scrolling: touch;
|
||
scrollbar-gutter: stable both-edges;
|
||
padding-bottom: 4px;
|
||
}
|
||
|
||
/* Ensure horizontal scroll works (no forced shrink-to-fit that overflows header). */
|
||
.header-chip-row > * {
|
||
flex: 0 0 auto;
|
||
min-width: 0;
|
||
}
|
||
|
||
.chip-group {
|
||
display: flex;
|
||
align-items: stretch;
|
||
gap: 10px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.chip-group-meta {
|
||
flex: 1 1 auto;
|
||
justify-content: flex-end;
|
||
min-width: max-content;
|
||
}
|
||
|
||
.chip-group-actions {
|
||
flex: 0 0 auto;
|
||
justify-content: flex-end;
|
||
min-width: max-content;
|
||
}
|
||
|
||
.meta-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-height: 52px;
|
||
padding: 10px 14px;
|
||
border-radius: 14px;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
color: var(--muted);
|
||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.075) 0%, rgba(255, 255, 255, 0.035) 100%);
|
||
font-size: 12px;
|
||
min-width: 0;
|
||
max-width: 100%;
|
||
flex: 0 1 auto;
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||
}
|
||
|
||
.meta-chip strong {
|
||
color: var(--text);
|
||
font-weight: 600;
|
||
font-family: var(--font-number);
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
.meta-chip.status-chip {
|
||
border-color: rgba(116, 198, 157, 0.32);
|
||
color: rgba(234, 244, 255, 0.92);
|
||
background: linear-gradient(180deg, rgba(116, 198, 157, 0.16) 0%, rgba(116, 198, 157, 0.08) 100%);
|
||
}
|
||
|
||
.meta-chip.status-chip strong {
|
||
color: var(--success);
|
||
font-family: var(--font-main);
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.status-dot {
|
||
width: 9px;
|
||
height: 9px;
|
||
border-radius: 50%;
|
||
background: var(--success);
|
||
box-shadow: 0 0 0 4px rgba(116, 198, 157, 0.12), 0 0 10px rgba(116, 198, 157, 0.28);
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.meta-chip.action-chip {
|
||
padding: 8px 12px;
|
||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.065) 0%, rgba(255, 255, 255, 0.028) 100%);
|
||
}
|
||
|
||
.meta-chip.action-chip .theme-switcher {
|
||
gap: 8px;
|
||
}
|
||
|
||
#snapshotId {
|
||
display: inline-block;
|
||
min-width: 0;
|
||
max-width: 18ch;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.theme-switcher {
|
||
display: flex;
|
||
flex-wrap: nowrap;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
min-width: max-content;
|
||
}
|
||
|
||
.theme-btn,
|
||
.drawer-toggle,
|
||
.action-btn {
|
||
border: 1px solid var(--line);
|
||
border-radius: 999px;
|
||
color: var(--text);
|
||
background: rgba(255, 255, 255, 0.04);
|
||
transition: transform 0.2s ease, background 0.25s ease, border-color 0.25s ease, color 0.25s ease, box-shadow 0.25s ease;
|
||
}
|
||
|
||
.theme-btn {
|
||
padding: 8px 12px;
|
||
font-size: 12px;
|
||
min-width: 78px;
|
||
}
|
||
|
||
.theme-btn.active {
|
||
color: #05121d;
|
||
border-color: transparent;
|
||
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%);
|
||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
|
||
}
|
||
|
||
.drawer-toggle,
|
||
.action-btn {
|
||
padding: 10px 18px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.drawer-toggle.primary,
|
||
.action-btn.primary {
|
||
color: #05121d;
|
||
border-color: transparent;
|
||
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%);
|
||
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.18);
|
||
}
|
||
|
||
.action-btn.secondary {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.action-btn.ghost {
|
||
background: transparent;
|
||
}
|
||
|
||
.theme-btn:hover,
|
||
.drawer-toggle:hover,
|
||
.action-btn:hover {
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.theme-btn:focus-visible,
|
||
.drawer-toggle:focus-visible,
|
||
.action-btn:focus-visible,
|
||
textarea:focus-visible {
|
||
outline: 2px solid var(--accent-2);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
/*
|
||
KPI track is fixed (140px) in the V2 screen grid contract, so the KPI layout
|
||
must be deterministic and never wrap.
|
||
|
||
Spec: 2 large + 3 small cards in one row.
|
||
We use a 5-column grid where the first two columns are wider.
|
||
*/
|
||
.kpi-grid {
|
||
display: grid;
|
||
grid-template-columns: 2fr 2fr 1.2fr 1.2fr 1.2fr;
|
||
gap: 14px;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.kpi-card {
|
||
border-radius: 16px;
|
||
padding: 18px 20px;
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) auto;
|
||
grid-template-rows: auto auto auto;
|
||
gap: 8px 12px;
|
||
min-width: 0;
|
||
grid-column: auto;
|
||
align-content: start;
|
||
}
|
||
|
||
/* V2 KPI tiers (kept for typography differences, not layout spans). */
|
||
.kpi-card.kpi-card-large {
|
||
min-height: 124px;
|
||
align-content: center;
|
||
}
|
||
|
||
.kpi-card.kpi-card-small {
|
||
min-height: 0;
|
||
}
|
||
|
||
.kpi-label,
|
||
.kpi-sub,
|
||
.kpi-value {
|
||
grid-column: 1 / 2;
|
||
min-width: 0;
|
||
}
|
||
|
||
.kpi-badge-slot {
|
||
grid-column: 2 / 3;
|
||
grid-row: 1 / 2;
|
||
justify-self: end;
|
||
align-self: start;
|
||
min-width: 0;
|
||
min-height: 28px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.kpi-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
max-width: 120px;
|
||
min-height: 28px;
|
||
padding: 0 10px;
|
||
border-radius: 999px;
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
background: rgba(255, 255, 255, 0.05);
|
||
color: var(--text);
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.kpi-badge.is-muted {
|
||
color: var(--muted);
|
||
}
|
||
|
||
.kpi-badge.has-tone {
|
||
border-color: currentColor;
|
||
background: transparent;
|
||
}
|
||
|
||
.kpi-badge-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
flex: 0 0 auto;
|
||
box-shadow: 0 0 10px currentColor;
|
||
}
|
||
|
||
.kpi-value {
|
||
font-family: var(--font-number);
|
||
font-size: clamp(20px, 2vw, 24px);
|
||
line-height: 1.15;
|
||
font-weight: 700;
|
||
color: var(--text);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.kpi-card.kpi-card-large .kpi-value {
|
||
font-size: clamp(30px, 2.6vw, 36px);
|
||
}
|
||
|
||
/* 最高热话题:2-line clamp */
|
||
.kpi-card.kpi-card-small .kpi-value.long {
|
||
font-size: clamp(20px, 1.75vw, 24px);
|
||
white-space: normal;
|
||
display: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 2;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.kpi-sub {
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.kpi-label {
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
letter-spacing: 0.14em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
/* KPI typography handled by V2 tier rules above. */
|
||
|
||
.kpi-status-normal { color: var(--success); }
|
||
.kpi-status-warning { color: var(--warning); }
|
||
.kpi-status-danger { color: var(--danger); }
|
||
|
||
.main-grid {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 360px;
|
||
gap: 14px;
|
||
min-height: 0;
|
||
}
|
||
|
||
.panel {
|
||
border-radius: var(--panel-radius);
|
||
padding: 18px 18px 16px;
|
||
min-height: 0;
|
||
}
|
||
|
||
.panel-head {
|
||
position: relative;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-end;
|
||
gap: 12px;
|
||
margin-bottom: 14px;
|
||
padding: 0 0 14px 14px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.panel-head::before {
|
||
content: "";
|
||
position: absolute;
|
||
left: 0;
|
||
top: 2px;
|
||
width: 4px;
|
||
height: 22px;
|
||
border-radius: 999px;
|
||
background: linear-gradient(180deg, var(--accent) 0%, var(--accent-2) 100%);
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.panel-head::after {
|
||
content: "";
|
||
position: absolute;
|
||
right: 0;
|
||
top: 0;
|
||
width: 84px;
|
||
height: 24px;
|
||
border-radius: 999px;
|
||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||
background: rgba(255, 255, 255, 0.025);
|
||
opacity: 0.85;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.panel-title-wrap {
|
||
min-width: 0;
|
||
}
|
||
|
||
.panel-kicker {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 6px;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.18em;
|
||
text-transform: uppercase;
|
||
color: var(--accent);
|
||
}
|
||
|
||
.panel-kicker::before {
|
||
content: "";
|
||
width: 22px;
|
||
height: 1px;
|
||
background: linear-gradient(90deg, var(--accent) 0%, rgba(216, 179, 106, 0.08) 100%);
|
||
}
|
||
|
||
.panel-title {
|
||
margin: 0;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.03em;
|
||
}
|
||
|
||
.panel-desc {
|
||
margin: 6px 0 0;
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.chart-box {
|
||
height: calc(100% - 58px);
|
||
min-height: 280px;
|
||
width: 100%;
|
||
}
|
||
|
||
.insights-panel {
|
||
display: grid;
|
||
grid-template-rows: auto minmax(0, 1fr);
|
||
gap: 12px;
|
||
}
|
||
|
||
.insights-body {
|
||
display: grid;
|
||
grid-template-rows: auto auto 1fr;
|
||
gap: 12px;
|
||
min-height: 0;
|
||
}
|
||
|
||
.insight-section {
|
||
position: relative;
|
||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.045) 0%, rgba(255, 255, 255, 0.025) 100%);
|
||
border-radius: 14px;
|
||
padding: 14px 14px 14px 16px;
|
||
}
|
||
|
||
.insight-section::before {
|
||
content: "";
|
||
position: absolute;
|
||
left: 0;
|
||
top: 14px;
|
||
bottom: 14px;
|
||
width: 3px;
|
||
border-radius: 999px;
|
||
background: linear-gradient(180deg, var(--accent) 0%, rgba(255, 255, 255, 0.1) 100%);
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.insight-section h3 {
|
||
margin: 0 0 10px;
|
||
font-size: 14px;
|
||
letter-spacing: 0.08em;
|
||
color: var(--accent);
|
||
}
|
||
|
||
.insight-list,
|
||
.conclusion-list,
|
||
.category-pills {
|
||
display: grid;
|
||
gap: 10px;
|
||
}
|
||
|
||
.insight-item {
|
||
display: grid;
|
||
grid-template-columns: 36px minmax(0, 1fr);
|
||
gap: 12px;
|
||
align-items: start;
|
||
}
|
||
|
||
.insight-rank {
|
||
width: 36px;
|
||
height: 36px;
|
||
display: grid;
|
||
place-items: center;
|
||
border-radius: 12px;
|
||
font-family: var(--font-number);
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||
background: rgba(255, 255, 255, 0.06);
|
||
color: var(--text);
|
||
}
|
||
|
||
.medal-gold { color: #ffe5a3; border-color: rgba(255, 215, 128, 0.4); background: rgba(255, 215, 128, 0.08); }
|
||
.medal-silver { color: #dbe7f6; border-color: rgba(207, 219, 232, 0.35); background: rgba(207, 219, 232, 0.08); }
|
||
.medal-bronze { color: #e6c49a; border-color: rgba(201, 145, 89, 0.35); background: rgba(201, 145, 89, 0.08); }
|
||
|
||
.insight-title {
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.insight-meta {
|
||
margin-top: 4px;
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.conclusion-item {
|
||
padding-left: 14px;
|
||
position: relative;
|
||
font-size: 13px;
|
||
color: var(--text);
|
||
line-height: 1.65;
|
||
}
|
||
|
||
.conclusion-item::before {
|
||
content: "";
|
||
position: absolute;
|
||
left: 0;
|
||
top: 8px;
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
background: var(--accent-2);
|
||
box-shadow: none;
|
||
}
|
||
|
||
.insight-empty,
|
||
.chart-empty,
|
||
.table-empty {
|
||
display: grid;
|
||
place-items: center;
|
||
text-align: center;
|
||
color: var(--muted);
|
||
border-radius: 14px;
|
||
border: 1px dashed rgba(255, 255, 255, 0.14);
|
||
background: rgba(255, 255, 255, 0.03);
|
||
min-height: 120px;
|
||
padding: 16px;
|
||
}
|
||
|
||
.table-panel {
|
||
display: grid;
|
||
grid-template-rows: auto auto minmax(0, 1fr);
|
||
gap: 12px;
|
||
min-height: 0;
|
||
}
|
||
|
||
.category-pills {
|
||
display: flex;
|
||
flex-wrap: nowrap;
|
||
gap: 10px;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
padding-bottom: 4px;
|
||
scrollbar-width: thin;
|
||
}
|
||
|
||
.category-pill,
|
||
.category-tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
min-width: max-content;
|
||
border-radius: 14px;
|
||
padding: 10px 14px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
background: rgba(255, 255, 255, 0.06);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.swatch {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 50%;
|
||
flex: 0 0 auto;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.table-wrap {
|
||
min-height: 0;
|
||
overflow: auto;
|
||
border-radius: 14px;
|
||
border: 1px solid rgba(255, 255, 255, 0.09);
|
||
background: rgba(2, 10, 22, 0.24);
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
min-width: 1020px;
|
||
border-collapse: separate;
|
||
border-spacing: 0;
|
||
}
|
||
|
||
thead th {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 2;
|
||
padding: 14px 16px;
|
||
text-align: left;
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
background: var(--table-header);
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||
letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
white-space: nowrap;
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
tbody td {
|
||
padding: 14px 16px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||
font-size: 13px;
|
||
vertical-align: top;
|
||
}
|
||
|
||
tbody tr:hover {
|
||
background: rgba(255, 255, 255, 0.03);
|
||
}
|
||
|
||
/* weak highlight for Top3 only */
|
||
tbody tr.top-1 { background: rgba(255, 215, 128, 0.038); }
|
||
tbody tr.top-2 { background: rgba(207, 219, 232, 0.032); }
|
||
tbody tr.top-3 { background: rgba(201, 145, 89, 0.032); }
|
||
|
||
.rank-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 40px;
|
||
height: 30px;
|
||
padding: 0 12px;
|
||
border-radius: 999px;
|
||
font-family: var(--font-number);
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.topic-cell {
|
||
display: grid;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.topic-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
line-height: 1.55;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.topic-url {
|
||
color: var(--accent-2);
|
||
font-size: 12px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
text-decoration: none;
|
||
display: inline-block;
|
||
max-width: 100%;
|
||
}
|
||
|
||
.topic-url:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.heat-value {
|
||
font-family: var(--font-number);
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
color: var(--accent);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.muted {
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.screen-footer {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) auto;
|
||
align-items: center;
|
||
gap: 14px;
|
||
padding: 0 18px;
|
||
border-radius: 14px;
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
}
|
||
|
||
#footerNote {
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.footer-meta {
|
||
display: flex;
|
||
gap: 18px;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.drawer-backdrop {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 10;
|
||
background: rgba(2, 8, 18, 0.38);
|
||
opacity: 0;
|
||
visibility: hidden;
|
||
transition: opacity 0.25s ease, visibility 0.25s ease;
|
||
}
|
||
|
||
.drawer-backdrop.open {
|
||
opacity: 1;
|
||
visibility: visible;
|
||
}
|
||
|
||
.data-drawer {
|
||
position: fixed;
|
||
left: 16px;
|
||
right: 16px;
|
||
bottom: 16px;
|
||
height: min(420px, 36vh);
|
||
z-index: 11;
|
||
border-radius: 20px;
|
||
transform: translateY(calc(100% + 24px));
|
||
transition: transform 0.28s ease;
|
||
padding: 18px;
|
||
display: grid;
|
||
grid-template-rows: auto 1fr auto auto;
|
||
gap: 12px;
|
||
}
|
||
|
||
.data-drawer.open {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.drawer-head {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 14px;
|
||
}
|
||
|
||
.drawer-title {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.drawer-note {
|
||
margin: 4px 0 0;
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.drawer-close {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 50%;
|
||
border: 1px solid var(--line);
|
||
background: rgba(255, 255, 255, 0.04);
|
||
color: var(--text);
|
||
font-size: 16px;
|
||
}
|
||
|
||
textarea {
|
||
width: 100%;
|
||
height: 100%;
|
||
min-height: 0;
|
||
resize: none;
|
||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||
border-radius: 14px;
|
||
background: rgba(2, 12, 24, 0.6);
|
||
color: var(--text);
|
||
padding: 14px 16px;
|
||
font: 13px/1.6 "Consolas", "Cascadia Code", "SFMono-Regular", monospace;
|
||
}
|
||
|
||
.drawer-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
|
||
.feedback {
|
||
padding: 12px 14px;
|
||
border-radius: 14px;
|
||
font-size: 13px;
|
||
color: var(--success);
|
||
background: rgba(116, 198, 157, 0.09);
|
||
border: 1px solid rgba(116, 198, 157, 0.2);
|
||
}
|
||
|
||
.feedback.error {
|
||
color: #ffb4ab;
|
||
background: rgba(224, 90, 71, 0.1);
|
||
border-color: rgba(224, 90, 71, 0.22);
|
||
}
|
||
|
||
@media (max-width: 1600px) {
|
||
body {
|
||
height: auto;
|
||
min-height: 100%;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.screen {
|
||
height: auto;
|
||
min-height: 1080px;
|
||
padding: 16px;
|
||
gap: 12px;
|
||
grid-template-rows: auto auto auto auto auto;
|
||
}
|
||
|
||
.screen-header {
|
||
grid-template-columns: 1fr;
|
||
align-items: start;
|
||
padding: 18px;
|
||
}
|
||
|
||
.header-side {
|
||
justify-items: start;
|
||
}
|
||
|
||
.header-chip-row {
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.chip-group {
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.header-chip-row,
|
||
.chip-group-meta,
|
||
.chip-group-actions,
|
||
.theme-switcher {
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.kpi-grid {
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
}
|
||
|
||
.kpi-card,
|
||
.kpi-card.kpi-card-large,
|
||
.kpi-card.kpi-card-small {
|
||
grid-column: auto;
|
||
}
|
||
|
||
.main-grid {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.insights-panel {
|
||
grid-column: 1 / -1;
|
||
}
|
||
}
|
||
|
||
@media (max-height: 900px) {
|
||
body {
|
||
height: auto;
|
||
min-height: 100%;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.screen {
|
||
height: auto;
|
||
min-height: 1080px;
|
||
grid-template-rows: auto auto auto auto auto;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1280px), (max-height: 900px) {
|
||
.kpi-grid {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.kpi-card,
|
||
.kpi-card.kpi-card-large,
|
||
.kpi-card.kpi-card-small {
|
||
grid-column: span 1;
|
||
}
|
||
|
||
.main-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.chart-box {
|
||
height: 320px;
|
||
}
|
||
|
||
.data-drawer {
|
||
left: 12px;
|
||
right: 12px;
|
||
height: min(500px, 56vh);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 720px) {
|
||
.screen {
|
||
padding: 12px;
|
||
}
|
||
|
||
.kpi-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.kpi-card,
|
||
.kpi-card.kpi-card-large,
|
||
.kpi-card.kpi-card-small {
|
||
grid-column: span 1;
|
||
}
|
||
|
||
.header-title {
|
||
font-size: 28px;
|
||
}
|
||
|
||
.data-drawer {
|
||
left: 8px;
|
||
right: 8px;
|
||
bottom: 8px;
|
||
}
|
||
|
||
.drawer-actions,
|
||
.theme-switcher,
|
||
.footer-meta,
|
||
.header-chip-row,
|
||
.chip-group {
|
||
gap: 8px;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="screen">
|
||
<header class="screen-header">
|
||
<div class="header-main">
|
||
<div class="header-eyebrow">Zhihu Hotlist Big Screen</div>
|
||
<h1 class="header-title">知乎热榜主题分类分析大屏</h1>
|
||
<p class="header-subtitle">基于知乎热榜快照,按主题分类汇总热度与互动指标,适配国企汇报场景的一页式监测大屏。</p>
|
||
</div>
|
||
<div class="header-side">
|
||
<div class="header-chip-row">
|
||
<div class="chip-group chip-group-meta">
|
||
<div class="meta-chip" id="metaCurrentTime">
|
||
<span>当前时间</span>
|
||
<strong id="currentTime">-</strong>
|
||
</div>
|
||
<div class="meta-chip" id="metaSnapshot">
|
||
<span>快照</span>
|
||
<strong id="snapshotId">demo-snapshot</strong>
|
||
</div>
|
||
<div class="meta-chip" id="metaGeneratedAt">
|
||
<span>生成时间</span>
|
||
<strong id="generatedAt">-</strong>
|
||
</div>
|
||
<div class="meta-chip status-chip" id="metaStatus">
|
||
<span class="status-dot" aria-hidden="true"></span>
|
||
<span>状态</span>
|
||
<strong>在线</strong>
|
||
</div>
|
||
</div>
|
||
<div class="chip-group chip-group-actions">
|
||
<div class="meta-chip action-chip" id="metaThemeSwitcher">
|
||
<div class="theme-switcher" id="themeSwitcher">
|
||
<button class="theme-btn active" type="button" data-theme="gov_blue_gold">政务蓝金</button>
|
||
<button class="theme-btn" type="button" data-theme="tech_cyan_blue">科技青蓝</button>
|
||
<button class="theme-btn" type="button" data-theme="industry_ink_green">工业墨绿</button>
|
||
<button class="theme-btn" type="button" data-theme="meeting_red_gold">会议红金</button>
|
||
</div>
|
||
</div>
|
||
<div class="meta-chip action-chip" id="metaDrawerAction">
|
||
<button class="drawer-toggle primary" type="button" id="drawerToggle">数据输入</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<section class="kpi-grid" id="summaryGrid"></section>
|
||
|
||
<section class="main-grid">
|
||
<article class="panel chart-panel">
|
||
<div class="panel-head">
|
||
<div class="panel-title-wrap">
|
||
<div class="panel-kicker">Chart 01</div>
|
||
<h2 class="panel-title">分类条目分布</h2>
|
||
<p class="panel-desc">按主题统计上榜条目数量,呈现当前热榜内容覆盖结构。</p>
|
||
</div>
|
||
</div>
|
||
<div class="chart-box" id="categoryCountChart"></div>
|
||
</article>
|
||
|
||
<article class="panel chart-panel">
|
||
<div class="panel-head">
|
||
<div class="panel-title-wrap">
|
||
<div class="panel-kicker">Chart 02</div>
|
||
<h2 class="panel-title">分类热度分布</h2>
|
||
<p class="panel-desc">按主题汇总热度规模,突出当前热榜关注度集中方向。</p>
|
||
</div>
|
||
</div>
|
||
<div class="chart-box" id="categoryHeatChart"></div>
|
||
</article>
|
||
|
||
<aside class="panel insights-panel">
|
||
<div class="panel-head">
|
||
<div class="panel-title-wrap">
|
||
<h2 class="panel-title">重点摘要卡</h2>
|
||
<p class="panel-desc">按热度筛出 Top3 热点,并输出当前快照的模板化结论。</p>
|
||
</div>
|
||
</div>
|
||
<div class="insights-body">
|
||
<section class="insight-section">
|
||
<h3>Top3 话题</h3>
|
||
<div class="insight-list" id="insightTopList"></div>
|
||
</section>
|
||
<section class="insight-section">
|
||
<h3>研判结论</h3>
|
||
<div class="conclusion-list" id="insightConclusionList"></div>
|
||
</section>
|
||
<section class="insight-section">
|
||
<h3>提示说明</h3>
|
||
<div class="conclusion-list" id="insightTipsList"></div>
|
||
</section>
|
||
</div>
|
||
</aside>
|
||
</section>
|
||
|
||
<section class="panel table-panel">
|
||
<div class="panel-head">
|
||
<div class="panel-title-wrap">
|
||
<h2 class="panel-title">热榜明细表</h2>
|
||
<p class="panel-desc">逐条查看排名、标题、类别、热度以及互动指标,支撑汇报落到具体热点。</p>
|
||
</div>
|
||
</div>
|
||
<div class="category-pills" id="categoryPills"></div>
|
||
<div class="table-wrap" id="tableWrap"></div>
|
||
</section>
|
||
|
||
<footer class="screen-footer">
|
||
<div id="footerNote">数据说明:热度来自热榜展示值;互动指标为空则显示 0。</div>
|
||
<div class="footer-meta">
|
||
<span>生成时间 <strong id="footerGeneratedAt">-</strong></span>
|
||
<span>当前皮肤 <strong id="footerSkin">政务蓝金</strong></span>
|
||
</div>
|
||
</footer>
|
||
</div>
|
||
|
||
<div class="drawer-backdrop" id="drawerBackdrop"></div>
|
||
<section class="data-drawer" id="dataDrawer" aria-hidden="true">
|
||
<div class="drawer-head">
|
||
<div>
|
||
<h2 class="drawer-title">数据输入</h2>
|
||
<p class="drawer-note">支持粘贴 skill 输出的 payload,若为外层对象会自动提取 `echarts` 字段。按 Esc 关闭,Ctrl+Enter 可直接渲染。</p>
|
||
</div>
|
||
<button class="drawer-close" type="button" id="drawerClose" aria-label="关闭输入抽屉">×</button>
|
||
</div>
|
||
<textarea id="payloadInput" spellcheck="false"></textarea>
|
||
<div class="drawer-actions">
|
||
<button class="action-btn primary" type="button" id="renderBtn">渲染看板</button>
|
||
<button class="action-btn secondary" type="button" id="sampleBtn">载入示例</button>
|
||
<button class="action-btn ghost" type="button" id="formatBtn">格式化 JSON</button>
|
||
</div>
|
||
<div class="feedback" id="feedback">已加载示例数据。</div>
|
||
</section>
|
||
|
||
<script>
|
||
const defaultPayload = {
|
||
snapshot_id: "demo-snapshot-20260328",
|
||
generated_at_ms: 1774628400000,
|
||
categories: [
|
||
{ category_code: "society", category_label: "社会", item_count: 3, total_heat: 138000, avg_heat: 46000 },
|
||
{ category_code: "entertainment", category_label: "娱乐", item_count: 2, total_heat: 116000, avg_heat: 58000 },
|
||
{ category_code: "technology", category_label: "科技", item_count: 2, total_heat: 103000, avg_heat: 51500 },
|
||
{ category_code: "sports", category_label: "体育", item_count: 2, total_heat: 93000, avg_heat: 46500 },
|
||
{ category_code: "military", category_label: "军事", item_count: 1, total_heat: 42000, avg_heat: 42000 },
|
||
{ category_code: "finance", category_label: "财经", item_count: 1, total_heat: 39000, avg_heat: 39000 }
|
||
],
|
||
table: [
|
||
{
|
||
rank: 1,
|
||
title: "明星新电影票房破纪录,娱乐板块热度再度走高",
|
||
url: "https://www.zhihu.com/question/100001",
|
||
category_code: "entertainment",
|
||
category_label: "娱乐",
|
||
heat_text: "6.8 万热度",
|
||
heat_value: 68000,
|
||
reply_count: 132,
|
||
upvote_count: 865,
|
||
favorite_count: 215,
|
||
heart_count: 96
|
||
},
|
||
{
|
||
rank: 2,
|
||
title: "AI 智能体落地企业办公场景,科技话题持续升温",
|
||
url: "https://www.zhihu.com/question/100002",
|
||
category_code: "technology",
|
||
category_label: "科技",
|
||
heat_text: "6.2 万热度",
|
||
heat_value: 62000,
|
||
reply_count: 110,
|
||
upvote_count: 790,
|
||
favorite_count: 188,
|
||
heart_count: 75
|
||
},
|
||
{
|
||
rank: 3,
|
||
title: "城市更新与民生配套建设引发社会讨论",
|
||
url: "https://www.zhihu.com/question/100003",
|
||
category_code: "society",
|
||
category_label: "社会",
|
||
heat_text: "5.5 万热度",
|
||
heat_value: 55000,
|
||
reply_count: 96,
|
||
upvote_count: 602,
|
||
favorite_count: 144,
|
||
heart_count: 52
|
||
},
|
||
{
|
||
rank: 4,
|
||
title: "国足新周期备战方案公布,体育板块关注度回升",
|
||
url: "https://www.zhihu.com/question/100004",
|
||
category_code: "sports",
|
||
category_label: "体育",
|
||
heat_text: "5.2 万热度",
|
||
heat_value: 52000,
|
||
reply_count: 84,
|
||
upvote_count: 573,
|
||
favorite_count: 116,
|
||
heart_count: 49
|
||
},
|
||
{
|
||
rank: 5,
|
||
title: "航母编队演训画面公布,军事类目成为高热讨论点",
|
||
url: "https://www.zhihu.com/question/100005",
|
||
category_code: "military",
|
||
category_label: "军事",
|
||
heat_text: "4.2 万热度",
|
||
heat_value: 42000,
|
||
reply_count: 75,
|
||
upvote_count: 418,
|
||
favorite_count: 103,
|
||
heart_count: 44
|
||
},
|
||
{
|
||
rank: 6,
|
||
title: "居民消费新政发布后,财经与社会议题出现联动",
|
||
url: "https://www.zhihu.com/question/100006",
|
||
category_code: "finance",
|
||
category_label: "财经",
|
||
heat_text: "3.9 万热度",
|
||
heat_value: 39000,
|
||
reply_count: 68,
|
||
upvote_count: 366,
|
||
favorite_count: 91,
|
||
heart_count: 28
|
||
},
|
||
{
|
||
rank: 7,
|
||
title: "短剧与综艺互动模式创新,娱乐内容二次发酵",
|
||
url: "https://www.zhihu.com/question/100007",
|
||
category_code: "entertainment",
|
||
category_label: "娱乐",
|
||
heat_text: "4.8 万热度",
|
||
heat_value: 48000,
|
||
reply_count: 62,
|
||
upvote_count: 312,
|
||
favorite_count: 74,
|
||
heart_count: 26
|
||
},
|
||
{
|
||
rank: 8,
|
||
title: "具身智能新进展吸引开发者讨论,科技类目热度稳定",
|
||
url: "https://www.zhihu.com/question/100008",
|
||
category_code: "technology",
|
||
category_label: "科技",
|
||
heat_text: "4.1 万热度",
|
||
heat_value: 41000,
|
||
reply_count: 55,
|
||
upvote_count: 295,
|
||
favorite_count: 67,
|
||
heart_count: 25
|
||
},
|
||
{
|
||
rank: 9,
|
||
title: "大型赛事带动城市消费,体育与社会话题并行上涨",
|
||
url: "https://www.zhihu.com/question/100009",
|
||
category_code: "sports",
|
||
category_label: "体育",
|
||
heat_text: "4.1 万热度",
|
||
heat_value: 41000,
|
||
reply_count: 49,
|
||
upvote_count: 248,
|
||
favorite_count: 61,
|
||
heart_count: 19
|
||
},
|
||
{
|
||
rank: 10,
|
||
title: "基层治理数字化试点扩围,社会治理议题升温",
|
||
url: "https://www.zhihu.com/question/100010",
|
||
category_code: "society",
|
||
category_label: "社会",
|
||
heat_text: "4.4 万热度",
|
||
heat_value: 44000,
|
||
reply_count: 58,
|
||
upvote_count: 301,
|
||
favorite_count: 77,
|
||
heart_count: 22
|
||
},
|
||
{
|
||
rank: 11,
|
||
title: "就业与职业转型讨论走热,社会类目保持高频曝光",
|
||
url: "https://www.zhihu.com/question/100011",
|
||
category_code: "society",
|
||
category_label: "社会",
|
||
heat_text: "3.9 万热度",
|
||
heat_value: 39000,
|
||
reply_count: 46,
|
||
upvote_count: 229,
|
||
favorite_count: 54,
|
||
heart_count: 17
|
||
}
|
||
]
|
||
}
|
||
|
||
const themeMeta = {
|
||
gov_blue_gold: {
|
||
label: "政务蓝金",
|
||
palette: ["#3EC7FF", "#D8B36A", "#5B8CFF", "#74C69D", "#E8C16E", "#6EE7F9", "#9CC3FF", "#FDBA74", "#FCA5A5", "#A7F3D0"]
|
||
},
|
||
tech_cyan_blue: {
|
||
label: "科技青蓝",
|
||
palette: ["#22D3EE", "#5B8CFF", "#99F6FF", "#60A5FA", "#2DD4BF", "#A5F3FC", "#93C5FD", "#C4B5FD", "#F0ABFC", "#67E8F9"]
|
||
},
|
||
industry_ink_green: {
|
||
label: "工业墨绿",
|
||
palette: ["#74C69D", "#D6B56E", "#34D399", "#6EE7B7", "#FCD34D", "#A7F3D0", "#86EFAC", "#FBBF24", "#FDBA74", "#EAB308"]
|
||
},
|
||
meeting_red_gold: {
|
||
label: "会议红金",
|
||
palette: ["#E05A47", "#D79A43", "#FB7185", "#F59E0B", "#FCA5A5", "#FDBA74", "#FCD34D", "#F87171", "#FBBF24", "#F9A8D4"]
|
||
}
|
||
}
|
||
|
||
const state = {
|
||
payload: null,
|
||
lastSuccessfulPayload: null,
|
||
theme: "gov_blue_gold",
|
||
drawerOpen: false,
|
||
categoryColorMap: {},
|
||
lastRenderedAtMs: 0,
|
||
status: "正常"
|
||
}
|
||
|
||
const payloadInput = document.getElementById("payloadInput")
|
||
const feedback = document.getElementById("feedback")
|
||
const summaryGrid = document.getElementById("summaryGrid")
|
||
const snapshotIdEl = document.getElementById("snapshotId")
|
||
const generatedAtEl = document.getElementById("generatedAt")
|
||
const currentTimeEl = document.getElementById("currentTime")
|
||
const footerGeneratedAtEl = document.getElementById("footerGeneratedAt")
|
||
const footerSkinEl = document.getElementById("footerSkin")
|
||
const categoryPills = document.getElementById("categoryPills")
|
||
const insightTopList = document.getElementById("insightTopList")
|
||
const insightConclusionList = document.getElementById("insightConclusionList")
|
||
const tableWrap = document.getElementById("tableWrap")
|
||
const drawerToggle = document.getElementById("drawerToggle")
|
||
const drawerClose = document.getElementById("drawerClose")
|
||
const drawerBackdrop = document.getElementById("drawerBackdrop")
|
||
const dataDrawer = document.getElementById("dataDrawer")
|
||
const themeSwitcher = document.getElementById("themeSwitcher")
|
||
const metaStatus = document.getElementById("metaStatus")
|
||
const metaStatusDot = metaStatus ? metaStatus.querySelector(".status-dot") : null
|
||
const metaStatusText = metaStatus ? metaStatus.querySelector("strong") : null
|
||
|
||
const hexagonSymbol = "path://M12 2 L36 2 L46 18 L36 34 L12 34 L2 18 Z"
|
||
|
||
let countChart = null
|
||
let heatChart = null
|
||
|
||
function escapeHtml(value) {
|
||
return String(value)
|
||
.replaceAll("&", "&")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">")
|
||
.replaceAll('"', """)
|
||
.replaceAll("'", "'")
|
||
}
|
||
|
||
function formatNumber(value) {
|
||
return Number(value || 0).toLocaleString("zh-CN")
|
||
}
|
||
|
||
function formatTime(timestamp) {
|
||
if (!timestamp) return "-"
|
||
const date = new Date(timestamp)
|
||
if (Number.isNaN(date.getTime())) return "-"
|
||
return new Intl.DateTimeFormat("zh-CN", {
|
||
year: "numeric",
|
||
month: "2-digit",
|
||
day: "2-digit",
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
second: "2-digit",
|
||
hour12: false
|
||
}).format(date)
|
||
}
|
||
|
||
function formatClock(timestamp) {
|
||
if (!timestamp) return "-"
|
||
const date = new Date(timestamp)
|
||
if (Number.isNaN(date.getTime())) return "-"
|
||
return new Intl.DateTimeFormat("zh-CN", {
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
second: "2-digit",
|
||
hour12: false
|
||
}).format(date)
|
||
}
|
||
|
||
function displayStructural(value) {
|
||
const text = String(value ?? "").trim()
|
||
if (!text) return "-"
|
||
if (text === "#" || text === "null" || text === "undefined") return "-"
|
||
return text
|
||
}
|
||
|
||
function displaySnapshotId(value) {
|
||
const text = String(value ?? "").trim()
|
||
if (!text || text === "null" || text === "undefined") return "未命名快照"
|
||
return text
|
||
}
|
||
|
||
function formatHeatDisplay(heatText, heatValue) {
|
||
const text = String(heatText ?? "").trim()
|
||
if (text) return text
|
||
return formatNumber(heatValue)
|
||
}
|
||
|
||
function getGeneratedAtDisplay(payload) {
|
||
if (!payload) return "-"
|
||
const generatedAtLocal = String(payload.generatedAtLocal ?? "").trim()
|
||
if (generatedAtLocal && generatedAtLocal !== "-") return generatedAtLocal
|
||
const formatted = formatTime(payload.generated_at_ms)
|
||
if (!formatted || formatted === "-") return "-"
|
||
return formatted
|
||
}
|
||
|
||
function formatCompactHeat(value) {
|
||
const num = Number(value || 0)
|
||
if (num >= 10000) {
|
||
return `${(Math.round(num / 100) / 100).toFixed(2).replace(/\.00$/, "").replace(/(\.\d)0$/, "$1")} 万`
|
||
}
|
||
return formatNumber(num)
|
||
}
|
||
|
||
function truncateLabel(value, maxLength) {
|
||
const text = String(value || "")
|
||
if (text.length <= maxLength) return text
|
||
return `${text.slice(0, Math.max(0, maxLength - 1))}…`
|
||
}
|
||
|
||
function normalizeNumber(value) {
|
||
const num = Number(value)
|
||
return Number.isFinite(num) ? num : 0
|
||
}
|
||
|
||
function extractJsonCandidate(raw) {
|
||
const fencedMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/i)
|
||
if (fencedMatch && fencedMatch[1]) {
|
||
return fencedMatch[1].trim()
|
||
}
|
||
const firstBrace = raw.indexOf("{")
|
||
const lastBrace = raw.lastIndexOf("}")
|
||
if (firstBrace >= 0 && lastBrace > firstBrace) {
|
||
return raw.slice(firstBrace, lastBrace + 1)
|
||
}
|
||
return raw
|
||
}
|
||
|
||
function unwrapPayload(parsed) {
|
||
if (parsed && parsed.echarts && typeof parsed.echarts === "object") {
|
||
return parsed.echarts
|
||
}
|
||
return parsed
|
||
}
|
||
|
||
function normalizeRow(row, index) {
|
||
const rank = normalizeNumber(row?.rank) || index + 1
|
||
const categoryCode = String(row?.category_code || row?.category_label || "other").trim() || "other"
|
||
const categoryLabel = String(row?.category_label || row?.category_code || "其他").trim() || "其他"
|
||
return {
|
||
rank,
|
||
title: String(row?.title || "未命名话题"),
|
||
url: String(row?.url || "#"),
|
||
category_code: categoryCode,
|
||
category_label: categoryLabel,
|
||
heat_text: String(row?.heat_text || ""),
|
||
heat_value: normalizeNumber(row?.heat_value),
|
||
reply_count: normalizeNumber(row?.reply_count),
|
||
upvote_count: normalizeNumber(row?.upvote_count),
|
||
favorite_count: normalizeNumber(row?.favorite_count),
|
||
heart_count: normalizeNumber(row?.heart_count)
|
||
}
|
||
}
|
||
|
||
function deriveCategoriesFromTable(table) {
|
||
const map = new Map()
|
||
table.forEach((row) => {
|
||
const key = row.category_code || row.category_label || "other"
|
||
const current = map.get(key) || {
|
||
category_code: key,
|
||
category_label: row.category_label || "其他",
|
||
item_count: 0,
|
||
total_heat: 0,
|
||
avg_heat: 0
|
||
}
|
||
current.item_count += 1
|
||
current.total_heat += normalizeNumber(row.heat_value)
|
||
map.set(key, current)
|
||
})
|
||
|
||
return Array.from(map.values()).map((item) => ({
|
||
...item,
|
||
avg_heat: item.item_count ? Math.round(item.total_heat / item.item_count) : 0
|
||
}))
|
||
}
|
||
|
||
function normalizeCategories(categories, table) {
|
||
const source = Array.isArray(categories) && categories.length ? categories : deriveCategoriesFromTable(table)
|
||
return source.map((item) => ({
|
||
category_code: String(item?.category_code || item?.category_label || "other").trim() || "other",
|
||
category_label: String(item?.category_label || item?.category_code || "其他").trim() || "其他",
|
||
item_count: normalizeNumber(item?.item_count),
|
||
total_heat: normalizeNumber(item?.total_heat),
|
||
avg_heat: normalizeNumber(item?.avg_heat)
|
||
}))
|
||
}
|
||
|
||
function normalizePayload(parsed) {
|
||
const payload = unwrapPayload(parsed)
|
||
if (!payload || typeof payload !== "object") {
|
||
throw new Error("输入内容不是有效的对象 JSON。")
|
||
}
|
||
|
||
const table = Array.isArray(payload.table) ? payload.table.map(normalizeRow) : []
|
||
const categories = normalizeCategories(payload.categories, table)
|
||
|
||
return {
|
||
snapshot_id: String(payload.snapshot_id || "未命名快照"),
|
||
generated_at_ms: normalizeNumber(payload.generated_at_ms),
|
||
generatedAtLocal: formatTime(payload.generated_at_ms),
|
||
categories,
|
||
table
|
||
}
|
||
}
|
||
|
||
function parsePayloadFromInput() {
|
||
const raw = payloadInput.value.trim()
|
||
if (!raw) {
|
||
throw new Error("请先粘贴 skill 返回的 JSON。")
|
||
}
|
||
const candidate = extractJsonCandidate(raw)
|
||
return normalizePayload(JSON.parse(candidate))
|
||
}
|
||
|
||
function setFeedback(message, isError) {
|
||
feedback.textContent = message
|
||
feedback.classList.toggle("error", Boolean(isError))
|
||
}
|
||
|
||
function getStatusMeta(statusText) {
|
||
const canonicalStatus = String(statusText || "").trim() || "正常"
|
||
if (canonicalStatus === "解析失败" || canonicalStatus === "ECharts 未加载") {
|
||
return { text: canonicalStatus, className: "kpi-status-danger", cssVar: "--danger" }
|
||
}
|
||
if (canonicalStatus === "无数据") {
|
||
return { text: canonicalStatus, className: "kpi-status-warning", cssVar: "--warning" }
|
||
}
|
||
return { text: canonicalStatus, className: "kpi-status-normal", cssVar: "--success" }
|
||
}
|
||
|
||
function resolveDashboardStatus(payload, overrideStatus) {
|
||
if (overrideStatus) return getStatusMeta(overrideStatus).text
|
||
if (!window.echarts) return getStatusMeta("ECharts 未加载").text
|
||
if (!payload || !Array.isArray(payload.table) || payload.table.length === 0) return getStatusMeta("无数据").text
|
||
return getStatusMeta("正常").text
|
||
}
|
||
|
||
function compareTextAsc(left, right) {
|
||
return String(left || "").localeCompare(String(right || ""), "zh-CN")
|
||
}
|
||
|
||
function compareTopCategory(left, right) {
|
||
return Number(right?.item_count || 0) - Number(left?.item_count || 0)
|
||
|| Number(right?.total_heat || 0) - Number(left?.total_heat || 0)
|
||
|| compareTextAsc(left?.category_label, right?.category_label)
|
||
}
|
||
|
||
function compareHottestItem(left, right) {
|
||
return Number(right?.heat_value || 0) - Number(left?.heat_value || 0)
|
||
|| Number(left?.rank || 0) - Number(right?.rank || 0)
|
||
|| compareTextAsc(left?.title, right?.title)
|
||
}
|
||
|
||
function compareCapsuleCategory(left, right) {
|
||
return Number(right?.total_heat || 0) - Number(left?.total_heat || 0)
|
||
|| Number(right?.item_count || 0) - Number(left?.item_count || 0)
|
||
|| compareTextAsc(left?.category_label, right?.category_label)
|
||
}
|
||
|
||
function getTopCategory(categories) {
|
||
return categories.slice().sort(compareTopCategory)[0]
|
||
}
|
||
|
||
function getHottestItem(table) {
|
||
return table.slice().sort(compareHottestItem)[0]
|
||
}
|
||
|
||
function buildSummaryCards(payload, statusText) {
|
||
const totalItems = payload.table.length
|
||
const totalHeat = payload.categories.reduce((sum, item) => sum + normalizeNumber(item.total_heat), 0)
|
||
const topCategory = getTopCategory(payload.categories)
|
||
const hottestItem = getHottestItem(payload.table)
|
||
const statusMeta = getStatusMeta(statusText)
|
||
const cards = [
|
||
{
|
||
label: "总热度",
|
||
value: formatNumber(totalHeat),
|
||
sub: `汇总 ${formatNumber(payload.categories.length)} 个分类`,
|
||
large: true,
|
||
badge: {
|
||
text: `${formatNumber(totalItems)} 条`,
|
||
title: `热榜条数 ${formatNumber(totalItems)}`
|
||
}
|
||
},
|
||
{
|
||
label: "主导类别",
|
||
value: topCategory ? displayStructural(topCategory.category_label) : "-",
|
||
sub: topCategory ? `共 ${formatNumber(topCategory.item_count)} 条,热度 ${formatNumber(topCategory.total_heat)}` : "暂无数据",
|
||
large: true,
|
||
badge: topCategory ? {
|
||
text: formatCompactHeat(topCategory.total_heat),
|
||
title: `主导类别总热度 ${formatNumber(topCategory.total_heat)}`
|
||
} : {
|
||
text: "暂无数据",
|
||
title: "暂无数据",
|
||
muted: true
|
||
}
|
||
},
|
||
{
|
||
label: "热榜条数",
|
||
value: formatNumber(totalItems),
|
||
sub: `覆盖 ${formatNumber(payload.categories.length)} 个类别`,
|
||
badge: {
|
||
text: "条目",
|
||
title: "热榜条目数",
|
||
muted: true
|
||
}
|
||
},
|
||
{
|
||
label: "最高热话题",
|
||
value: hottestItem ? displayStructural(hottestItem.title) : "-",
|
||
sub: hottestItem ? `热度 ${formatHeatDisplay(hottestItem.heat_text, hottestItem.heat_value)}` : "暂无数据",
|
||
long: true,
|
||
badge: hottestItem ? {
|
||
text: displayStructural(hottestItem.category_label),
|
||
title: `所属类别 ${displayStructural(hottestItem.category_label)}`
|
||
} : {
|
||
text: "暂无数据",
|
||
title: "暂无数据",
|
||
muted: true
|
||
}
|
||
},
|
||
{
|
||
label: "数据状态",
|
||
value: statusMeta.text,
|
||
sub: `上次渲染:${formatClock(state.lastRenderedAtMs)}`,
|
||
className: statusMeta.className,
|
||
badge: {
|
||
text: statusMeta.text,
|
||
title: `当前状态 ${statusMeta.text}`,
|
||
dotColorVar: statusMeta.cssVar
|
||
}
|
||
}
|
||
]
|
||
|
||
summaryGrid.innerHTML = cards.map((card) => {
|
||
const badgeHtml = card.badge ? `
|
||
<div class="kpi-badge-slot">
|
||
<span class="kpi-badge ${card.badge.muted ? "is-muted" : ""} ${card.badge.dotColorVar ? "has-tone" : ""}" title="${escapeHtml(card.badge.title || card.badge.text)}"${card.badge.dotColorVar ? ` style="color: var(${card.badge.dotColorVar}); border-color: currentColor; background: transparent;"` : ""}>
|
||
${card.badge.dotColorVar ? '<span class="kpi-badge-dot" aria-hidden="true" style="background: currentColor;"></span>' : ""}
|
||
<span>${escapeHtml(card.badge.text)}</span>
|
||
</span>
|
||
</div>
|
||
` : '<div class="kpi-badge-slot" aria-hidden="true"></div>'
|
||
return `
|
||
<article class="kpi-card ${card.large ? "kpi-card-large" : "kpi-card-small"}">
|
||
<div class="kpi-label">${escapeHtml(card.label)}</div>
|
||
${badgeHtml}
|
||
<div class="kpi-value ${card.long ? "long" : ""} ${card.className || ""}" title="${escapeHtml(card.value)}">${escapeHtml(card.value)}</div>
|
||
<div class="kpi-sub">${escapeHtml(card.sub)}</div>
|
||
</article>
|
||
`
|
||
}).join("")
|
||
}
|
||
|
||
function syncCategoryColorMap(categories) {
|
||
const known = { ...state.categoryColorMap }
|
||
const usedSlots = new Set(Object.values(known))
|
||
const codes = categories.map((item) => item.category_code).filter(Boolean)
|
||
const newCodes = Array.from(new Set(codes)).filter((code) => known[code] === undefined).sort()
|
||
newCodes.forEach((code) => {
|
||
let slot = 0
|
||
while (usedSlots.has(slot)) {
|
||
slot += 1
|
||
}
|
||
known[code] = slot
|
||
usedSlots.add(slot)
|
||
})
|
||
state.categoryColorMap = known
|
||
}
|
||
|
||
function getThemePalette() {
|
||
const theme = themeMeta[state.theme] || themeMeta.gov_blue_gold
|
||
return theme.palette
|
||
}
|
||
|
||
function getCategoryColor(code) {
|
||
const palette = getThemePalette()
|
||
const slot = state.categoryColorMap[code] ?? 0
|
||
return palette[slot % palette.length]
|
||
}
|
||
|
||
function makeGradient(baseColor) {
|
||
if (!window.echarts) return baseColor
|
||
return new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: baseColor },
|
||
{ offset: 1, color: `${baseColor}22` }
|
||
])
|
||
}
|
||
|
||
function renderCategoryPills(categories) {
|
||
if (!categories.length) {
|
||
categoryPills.innerHTML = '<div class="table-empty">暂无分类汇总</div>'
|
||
return
|
||
}
|
||
const sorted = categories.slice().sort(compareCapsuleCategory)
|
||
categoryPills.innerHTML = sorted.map((category) => {
|
||
const color = getCategoryColor(category.category_code)
|
||
return `
|
||
<span class="category-pill" style="border-color:${color}55;color:${color};background:${color}14;">
|
||
<span class="swatch" style="background:${color};color:${color};"></span>
|
||
${escapeHtml(displayStructural(category.category_label))}
|
||
<span>· ${formatNumber(category.item_count)} 条</span>
|
||
<span>· 热度 ${formatNumber(category.total_heat)}</span>
|
||
</span>
|
||
`
|
||
}).join("")
|
||
}
|
||
|
||
function medalClassByRank(rank) {
|
||
if (rank === 1) return "medal-gold"
|
||
if (rank === 2) return "medal-silver"
|
||
if (rank === 3) return "medal-bronze"
|
||
return ""
|
||
}
|
||
|
||
function renderTopTopics(table) {
|
||
if (!table.length) {
|
||
insightTopList.innerHTML = '<div class="insight-empty">当前没有可展示的热点话题。</div>'
|
||
return
|
||
}
|
||
const topTopics = table.slice().sort(compareHottestItem).slice(0, 3)
|
||
insightTopList.innerHTML = topTopics.map((row, index) => `
|
||
<div class="insight-item">
|
||
<div class="insight-rank ${medalClassByRank(index + 1)}">${index + 1}</div>
|
||
<div>
|
||
<div class="insight-title" title="${escapeHtml(displayStructural(row.title))}">${escapeHtml(displayStructural(row.title))}</div>
|
||
<div class="insight-meta">${escapeHtml(displayStructural(row.category_label))} · 热度 ${escapeHtml(formatHeatDisplay(row.heat_text, row.heat_value))}</div>
|
||
</div>
|
||
</div>
|
||
`).join("")
|
||
}
|
||
|
||
function renderTips() {
|
||
const tips = [
|
||
"数据输入:打开右上角“数据输入”,粘贴 echarts JSON(或包含 echarts 字段的 payload)后点击渲染。",
|
||
"颜色口径:同一类别在图表、胶囊带、表格标签中颜色一致。",
|
||
"数据口径:热度来自热榜展示值;互动指标为空则显示 0。"
|
||
]
|
||
const el = document.getElementById("insightTipsList")
|
||
if (!el) return
|
||
el.innerHTML = tips.map((item) => `
|
||
<div class="conclusion-item" title="${escapeHtml(item)}">${escapeHtml(item)}</div>
|
||
`).join("")
|
||
}
|
||
|
||
function renderConclusions(payload) {
|
||
const topCategory = getTopCategory(payload.categories)
|
||
const hottestItem = getHottestItem(payload.table)
|
||
const topCategoryLabel = topCategory ? displayStructural(topCategory.category_label) : "-"
|
||
const topCategoryCount = topCategory ? formatNumber(topCategory.item_count) : "0"
|
||
const hottestTitle = hottestItem ? displayStructural(hottestItem.title) : "-"
|
||
const hottestHeat = hottestItem ? formatHeatDisplay(hottestItem.heat_text, hottestItem.heat_value) : "0"
|
||
const generatedAtLocal = getGeneratedAtDisplay(payload)
|
||
const conclusions = [
|
||
`主导类别:${topCategoryLabel}(${topCategoryCount} 条)`,
|
||
`关注度最高:${hottestTitle}(热度 ${hottestHeat})`,
|
||
`数据更新时间:${generatedAtLocal}`
|
||
]
|
||
insightConclusionList.innerHTML = conclusions.map((item) => `
|
||
<div class="conclusion-item" title="${escapeHtml(item)}">${escapeHtml(item)}</div>
|
||
`).join("")
|
||
}
|
||
|
||
function renderTable(table) {
|
||
if (!table.length) {
|
||
tableWrap.innerHTML = '<div class="table-empty">当前没有可展示的明细数据。</div>'
|
||
return
|
||
}
|
||
|
||
const rows = table.slice().sort((a, b) => a.rank - b.rank)
|
||
tableWrap.innerHTML = `
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>排名</th>
|
||
<th>标题</th>
|
||
<th>类别</th>
|
||
<th>热度</th>
|
||
<th>回复</th>
|
||
<th>赞同</th>
|
||
<th>收藏</th>
|
||
<th>红心</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${rows.map((row, index) => {
|
||
const rank = row.rank || index + 1
|
||
const themeColor = getCategoryColor(row.category_code)
|
||
const topClass = rank === 1 ? "top-1" : rank === 2 ? "top-2" : rank === 3 ? "top-3" : ""
|
||
return `
|
||
<tr class="${topClass}">
|
||
<td><span class="rank-badge ${medalClassByRank(rank)}">#${escapeHtml(rank)}</span></td>
|
||
<td>
|
||
<div class="topic-cell">
|
||
<div class="topic-title" title="${escapeHtml(displayStructural(row.title))}">${escapeHtml(displayStructural(row.title))}</div>
|
||
<a class="topic-url" href="${escapeHtml(displayStructural(row.url)) === "-" ? "#" : escapeHtml(displayStructural(row.url))}" target="_blank" rel="noreferrer" title="${escapeHtml(displayStructural(row.url))}">${escapeHtml(displayStructural(row.url))}</a>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<span class="category-tag" style="border-color:${themeColor}55;color:${themeColor};background:${themeColor}14;">
|
||
<span class="swatch" style="background:${themeColor};color:${themeColor};"></span>
|
||
${escapeHtml(displayStructural(row.category_label))}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<div class="heat-value">${escapeHtml(formatHeatDisplay(row.heat_text, row.heat_value))}</div>
|
||
<div class="muted">数值 ${formatNumber(row.heat_value)}</div>
|
||
</td>
|
||
<td>${formatNumber(row.reply_count)}</td>
|
||
<td>${formatNumber(row.upvote_count)}</td>
|
||
<td>${formatNumber(row.favorite_count)}</td>
|
||
<td>${formatNumber(row.heart_count)}</td>
|
||
</tr>
|
||
`
|
||
}).join("")}
|
||
</tbody>
|
||
</table>
|
||
`
|
||
}
|
||
|
||
function buildEmptyChartOption(text) {
|
||
return {
|
||
animation: false,
|
||
title: {
|
||
text,
|
||
left: "center",
|
||
top: "center",
|
||
textStyle: {
|
||
color: getComputedStyle(document.documentElement).getPropertyValue("--muted").trim() || "#8faecc",
|
||
fontSize: 14,
|
||
fontWeight: 500
|
||
}
|
||
},
|
||
xAxis: { show: false },
|
||
yAxis: { show: false },
|
||
series: []
|
||
}
|
||
}
|
||
|
||
function chartBaseOption(labels) {
|
||
const styles = getComputedStyle(document.documentElement)
|
||
const text = styles.getPropertyValue("--text").trim() || "#eaf4ff"
|
||
const muted = styles.getPropertyValue("--muted").trim() || "#8faecc"
|
||
const line = styles.getPropertyValue("--line").trim() || "rgba(62, 199, 255, 0.2)"
|
||
const panelStrong = styles.getPropertyValue("--panel-strong").trim() || "rgba(14, 42, 77, 0.97)"
|
||
return {
|
||
backgroundColor: "transparent",
|
||
grid: { left: 56, right: 24, top: 52, bottom: 56 },
|
||
tooltip: {
|
||
trigger: "axis",
|
||
backgroundColor: panelStrong,
|
||
borderColor: line,
|
||
borderWidth: 1,
|
||
textStyle: { color: text },
|
||
extraCssText: "box-shadow:0 12px 28px rgba(0,0,0,0.28);border-radius:12px;"
|
||
},
|
||
xAxis: {
|
||
type: "category",
|
||
data: labels,
|
||
axisTick: { show: false },
|
||
axisLine: { lineStyle: { color: line } },
|
||
axisLabel: {
|
||
color: muted,
|
||
fontSize: 12,
|
||
interval: 0,
|
||
margin: 16,
|
||
formatter(value) {
|
||
return truncateLabel(value, 6)
|
||
}
|
||
}
|
||
},
|
||
yAxis: {
|
||
type: "value",
|
||
nameTextStyle: {
|
||
color: muted,
|
||
fontSize: 11,
|
||
padding: [0, 0, 8, 0]
|
||
},
|
||
splitLine: { lineStyle: { color: line, type: "dashed" } },
|
||
axisLine: { show: false },
|
||
axisLabel: { color: muted, fontSize: 12 }
|
||
}
|
||
}
|
||
}
|
||
|
||
function renderCountChart(categories) {
|
||
if (!countChart) return
|
||
if (!categories.length) {
|
||
countChart.clear()
|
||
countChart.setOption(buildEmptyChartOption("暂无分类汇总"))
|
||
return
|
||
}
|
||
const sorted = categories.slice().sort(compareTopCategory)
|
||
const labels = sorted.map((item) => item.category_label)
|
||
countChart.setOption({
|
||
...chartBaseOption(labels),
|
||
yAxis: {
|
||
...chartBaseOption(labels).yAxis,
|
||
name: "条目数"
|
||
},
|
||
series: [
|
||
{
|
||
name: "热榜条数",
|
||
type: "bar",
|
||
barWidth: 30,
|
||
data: sorted.map((item) => ({
|
||
value: item.item_count,
|
||
itemStyle: {
|
||
color: makeGradient(getCategoryColor(item.category_code)),
|
||
borderRadius: [10, 10, 0, 0],
|
||
shadowBlur: 14,
|
||
shadowColor: `${getCategoryColor(item.category_code)}66`
|
||
}
|
||
})),
|
||
label: {
|
||
show: true,
|
||
position: "top",
|
||
distance: 10,
|
||
color: getComputedStyle(document.documentElement).getPropertyValue("--text").trim(),
|
||
fontSize: 12,
|
||
fontWeight: 700,
|
||
formatter: ({ value }) => `${formatNumber(value)} 条`
|
||
}
|
||
},
|
||
{
|
||
name: "顶部强调",
|
||
type: "pictorialBar",
|
||
symbol: hexagonSymbol,
|
||
symbolSize: [20, 14],
|
||
symbolOffset: [0, -7],
|
||
symbolPosition: "end",
|
||
z: 10,
|
||
data: sorted.map((item) => ({
|
||
value: item.item_count,
|
||
itemStyle: {
|
||
color: getCategoryColor(item.category_code),
|
||
shadowBlur: 10,
|
||
shadowColor: `${getCategoryColor(item.category_code)}88`
|
||
}
|
||
}))
|
||
}
|
||
]
|
||
}, true)
|
||
}
|
||
|
||
function renderHeatChart(categories) {
|
||
if (!heatChart) return
|
||
if (!categories.length) {
|
||
heatChart.clear()
|
||
heatChart.setOption(buildEmptyChartOption("暂无热度数据"))
|
||
return
|
||
}
|
||
const sorted = categories.slice().sort(compareCapsuleCategory)
|
||
const labels = sorted.map((item) => item.category_label)
|
||
heatChart.setOption({
|
||
...chartBaseOption(labels),
|
||
yAxis: {
|
||
...chartBaseOption(labels).yAxis,
|
||
name: "热度值",
|
||
axisLabel: {
|
||
color: getComputedStyle(document.documentElement).getPropertyValue("--muted").trim(),
|
||
fontSize: 12,
|
||
formatter(value) {
|
||
const num = Number(value || 0)
|
||
if (num >= 10000) {
|
||
return `${Math.round(num / 1000) / 10}万`
|
||
}
|
||
return formatNumber(num)
|
||
}
|
||
}
|
||
},
|
||
series: [
|
||
{
|
||
name: "总热度",
|
||
type: "bar",
|
||
barWidth: 30,
|
||
data: sorted.map((item) => ({
|
||
value: item.total_heat,
|
||
itemStyle: {
|
||
color: makeGradient(getCategoryColor(item.category_code)),
|
||
borderRadius: [10, 10, 0, 0],
|
||
shadowBlur: 14,
|
||
shadowColor: `${getCategoryColor(item.category_code)}66`
|
||
}
|
||
})),
|
||
label: {
|
||
show: true,
|
||
position: "top",
|
||
distance: 10,
|
||
color: getComputedStyle(document.documentElement).getPropertyValue("--text").trim(),
|
||
fontSize: 12,
|
||
fontWeight: 700,
|
||
formatter: ({ value }) => `${formatCompactHeat(value)} 热度`
|
||
},
|
||
markPoint: {
|
||
symbol: "pin",
|
||
symbolSize: 42,
|
||
label: {
|
||
color: "#05121d",
|
||
formatter: `最高:${truncateLabel(sorted[0].category_label, 6)}`,
|
||
fontSize: 11,
|
||
fontWeight: 700
|
||
},
|
||
itemStyle: {
|
||
color: getCategoryColor(sorted[0].category_code)
|
||
},
|
||
data: [{ type: "max", name: "最高热度" }]
|
||
}
|
||
}
|
||
]
|
||
}, true)
|
||
}
|
||
|
||
function updateHeaderMeta(payload) {
|
||
snapshotIdEl.textContent = displaySnapshotId(payload?.snapshot_id)
|
||
generatedAtEl.textContent = getGeneratedAtDisplay(payload)
|
||
}
|
||
|
||
function updateHeaderStatus(statusText) {
|
||
if (!metaStatus || !metaStatusText || !metaStatusDot) return
|
||
const statusMeta = getStatusMeta(statusText)
|
||
const computed = getComputedStyle(document.documentElement)
|
||
const statusColor = computed.getPropertyValue(statusMeta.cssVar).trim() || computed.getPropertyValue("--success").trim()
|
||
metaStatusText.textContent = statusMeta.text
|
||
metaStatus.style.color = "rgba(234, 244, 255, 0.92)"
|
||
metaStatus.style.borderColor = `${statusColor}55`
|
||
metaStatus.style.background = `${statusColor}14`
|
||
metaStatusDot.style.background = statusColor
|
||
metaStatusDot.style.boxShadow = `0 0 10px ${statusColor}66`
|
||
metaStatusText.style.color = statusColor
|
||
}
|
||
|
||
function updateFooterMeta(payload) {
|
||
const generatedAtLocal = getGeneratedAtDisplay(payload)
|
||
footerGeneratedAtEl.textContent = generatedAtLocal
|
||
footerSkinEl.textContent = (themeMeta[state.theme] || themeMeta.gov_blue_gold).label
|
||
}
|
||
|
||
function renderDashboard(payload, statusText, options = {}) {
|
||
const resolvedStatus = resolveDashboardStatus(payload, statusText)
|
||
const statusMeta = getStatusMeta(resolvedStatus)
|
||
const shouldUpdateLastRenderedAtMs = options.updateLastRenderedAtMs !== false
|
||
state.payload = payload
|
||
state.status = statusMeta.text
|
||
if (shouldUpdateLastRenderedAtMs) {
|
||
state.lastRenderedAtMs = Date.now()
|
||
}
|
||
syncCategoryColorMap(payload.categories)
|
||
updateHeaderMeta(payload)
|
||
updateHeaderStatus(statusMeta.text)
|
||
updateFooterMeta(payload)
|
||
buildSummaryCards(payload, statusMeta.text)
|
||
renderCategoryPills(payload.categories)
|
||
renderTopTopics(payload.table)
|
||
renderConclusions(payload)
|
||
renderTips()
|
||
renderCountChart(payload.categories)
|
||
renderHeatChart(payload.categories)
|
||
renderTable(payload.table)
|
||
}
|
||
|
||
function safeLocalStorageGet(key) {
|
||
try {
|
||
return window.localStorage.getItem(key)
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
function safeLocalStorageSet(key, value) {
|
||
try {
|
||
window.localStorage.setItem(key, value)
|
||
} catch {
|
||
return null
|
||
}
|
||
return null
|
||
}
|
||
|
||
function updateThemeButtons() {
|
||
themeSwitcher.querySelectorAll(".theme-btn").forEach((button) => {
|
||
button.classList.toggle("active", button.dataset.theme === state.theme)
|
||
})
|
||
}
|
||
|
||
function resizeCharts() {
|
||
if (countChart) countChart.resize()
|
||
if (heatChart) heatChart.resize()
|
||
}
|
||
|
||
function setTheme(themeName) {
|
||
if (!themeMeta[themeName]) return
|
||
state.theme = themeName
|
||
document.documentElement.dataset.theme = themeName
|
||
safeLocalStorageSet("zhihu-hotlist-theme", themeName)
|
||
updateThemeButtons()
|
||
if (state.lastSuccessfulPayload) {
|
||
renderDashboard(state.lastSuccessfulPayload, state.status)
|
||
}
|
||
requestAnimationFrame(() => resizeCharts())
|
||
}
|
||
|
||
function openDrawer() {
|
||
state.drawerOpen = true
|
||
dataDrawer.classList.add("open")
|
||
drawerBackdrop.classList.add("open")
|
||
dataDrawer.setAttribute("aria-hidden", "false")
|
||
requestAnimationFrame(() => payloadInput.focus())
|
||
}
|
||
|
||
function closeDrawer() {
|
||
state.drawerOpen = false
|
||
dataDrawer.classList.remove("open")
|
||
drawerBackdrop.classList.remove("open")
|
||
dataDrawer.setAttribute("aria-hidden", "true")
|
||
drawerToggle.focus()
|
||
requestAnimationFrame(() => resizeCharts())
|
||
}
|
||
|
||
function toggleDrawer() {
|
||
if (state.drawerOpen) {
|
||
closeDrawer()
|
||
} else {
|
||
openDrawer()
|
||
}
|
||
}
|
||
|
||
function tryRenderFromInput() {
|
||
try {
|
||
const payload = parsePayloadFromInput()
|
||
state.lastSuccessfulPayload = payload
|
||
const statusText = resolveDashboardStatus(payload)
|
||
state.status = statusText
|
||
renderDashboard(payload, statusText)
|
||
setFeedback(`看板已更新:${payload.categories.length} 个类别,${payload.table.length} 条热榜。`, false)
|
||
} catch (error) {
|
||
state.status = resolveDashboardStatus(state.lastSuccessfulPayload, !window.echarts ? "ECharts 未加载" : "解析失败")
|
||
if (state.lastSuccessfulPayload) {
|
||
renderDashboard(state.lastSuccessfulPayload, state.status, { updateLastRenderedAtMs: false })
|
||
}
|
||
setFeedback(error?.message || "渲染失败,请检查 JSON 格式。", true)
|
||
}
|
||
}
|
||
|
||
function formatPayloadIntoInput(payload) {
|
||
payloadInput.value = JSON.stringify(payload, null, 2)
|
||
}
|
||
|
||
function startClock() {
|
||
const tick = () => {
|
||
currentTimeEl.textContent = formatTime(Date.now())
|
||
}
|
||
tick()
|
||
window.setInterval(tick, 1000)
|
||
}
|
||
|
||
function initCharts() {
|
||
if (!window.echarts) return
|
||
countChart = echarts.init(document.getElementById("categoryCountChart"))
|
||
heatChart = echarts.init(document.getElementById("categoryHeatChart"))
|
||
}
|
||
|
||
function bindEvents() {
|
||
document.getElementById("renderBtn").addEventListener("click", tryRenderFromInput)
|
||
document.getElementById("sampleBtn").addEventListener("click", () => {
|
||
formatPayloadIntoInput(defaultPayload)
|
||
tryRenderFromInput()
|
||
})
|
||
document.getElementById("formatBtn").addEventListener("click", () => {
|
||
try {
|
||
const payload = parsePayloadFromInput()
|
||
formatPayloadIntoInput(payload)
|
||
setFeedback("JSON 已格式化。", false)
|
||
} catch (error) {
|
||
setFeedback(error?.message || "当前内容不是合法 JSON。", true)
|
||
}
|
||
})
|
||
drawerToggle.addEventListener("click", toggleDrawer)
|
||
drawerClose.addEventListener("click", closeDrawer)
|
||
drawerBackdrop.addEventListener("click", closeDrawer)
|
||
dataDrawer.addEventListener("transitionend", resizeCharts)
|
||
themeSwitcher.querySelectorAll(".theme-btn").forEach((button) => {
|
||
button.addEventListener("click", () => setTheme(button.dataset.theme))
|
||
})
|
||
payloadInput.addEventListener("keydown", (event) => {
|
||
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
||
event.preventDefault()
|
||
tryRenderFromInput()
|
||
}
|
||
})
|
||
document.addEventListener("keydown", (event) => {
|
||
if (event.key === "Escape" && state.drawerOpen) {
|
||
event.preventDefault()
|
||
closeDrawer()
|
||
}
|
||
})
|
||
window.addEventListener("resize", resizeCharts)
|
||
}
|
||
|
||
function init() {
|
||
const storedTheme = safeLocalStorageGet("zhihu-hotlist-theme")
|
||
if (storedTheme && themeMeta[storedTheme]) {
|
||
state.theme = storedTheme
|
||
document.documentElement.dataset.theme = storedTheme
|
||
}
|
||
updateThemeButtons()
|
||
initCharts()
|
||
bindEvents()
|
||
formatPayloadIntoInput(defaultPayload)
|
||
startClock()
|
||
|
||
const normalizedDefault = normalizePayload(defaultPayload)
|
||
state.lastSuccessfulPayload = normalizedDefault
|
||
renderDashboard(normalizedDefault, resolveDashboardStatus(normalizedDefault, !window.echarts ? "ECharts 未加载" : "正常"))
|
||
if (!window.echarts) {
|
||
setFeedback("ECharts 脚本加载失败,请确认当前环境可以访问 CDN。", true)
|
||
} else {
|
||
setFeedback("已加载示例数据。", false)
|
||
}
|
||
}
|
||
|
||
init()
|
||
</script>
|
||
</body>
|
||
</html>
|