dashboard-prototype.html 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width,initial-scale=1">
  6. <title>PIXEL WRITER HUB - Dashboard Prototype</title>
  7. <link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
  8. <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
  9. <style>
  10. /* ===== PIXEL WRITER HUB DESIGN SYSTEM ===== */
  11. :root {
  12. --bg-main: #fff7e8;
  13. --bg-panel: #fffdf6;
  14. --bg-card: #fffaf0;
  15. --bg-card-2: #fff3d5;
  16. --text-main: #2a220f;
  17. --text-sub: #5d5035;
  18. --text-mute: #8f7f5c;
  19. --accent-blue: #26a8ff;
  20. --accent-purple: #7f5af0;
  21. --accent-green: #2ec27e;
  22. --accent-amber: #f5a524;
  23. --accent-red: #d7263d;
  24. --accent-cyan: #00b8d4;
  25. --border-main: #2a220f;
  26. --border-soft: #8f7f5c;
  27. --shadow-main: 6px 6px 0 #2a220f;
  28. --shadow-soft: 3px 3px 0 #8f7f5c;
  29. --font-display: 'Press Start 2P', monospace;
  30. --font-body: 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
  31. }
  32. * { margin:0; padding:0; box-sizing:border-box; }
  33. html, body { height:100%; }
  34. body {
  35. font-family: var(--font-body);
  36. color: var(--text-main);
  37. background: var(--bg-main);
  38. background-image:
  39. linear-gradient(90deg, rgba(42,34,15,.05) 1px, transparent 1px),
  40. linear-gradient(rgba(42,34,15,.05) 1px, transparent 1px);
  41. background-size: 14px 14px;
  42. }
  43. .app-layout {
  44. display: grid;
  45. grid-template-columns: 240px minmax(0,1fr);
  46. height: 100vh;
  47. }
  48. /* ===== SIDEBAR ===== */
  49. .sidebar {
  50. border-right: 3px solid var(--border-main);
  51. background: linear-gradient(180deg, #ffe8b8, #ffe19f);
  52. display: flex;
  53. flex-direction: column;
  54. }
  55. .sidebar-header {
  56. padding: 16px;
  57. border-bottom: 3px solid var(--border-main);
  58. }
  59. .sidebar-header h1 {
  60. font-family: var(--font-display);
  61. font-size: 11px;
  62. letter-spacing: .08em;
  63. line-height: 1.45;
  64. }
  65. .sidebar-header .subtitle {
  66. margin-top: 10px;
  67. font-size: 14px;
  68. font-weight: 500;
  69. color: var(--text-sub);
  70. }
  71. .sidebar-nav {
  72. flex: 1;
  73. overflow-y: auto;
  74. padding: 10px;
  75. display: flex;
  76. flex-direction: column;
  77. gap: 8px;
  78. }
  79. .nav-item {
  80. width: 100%;
  81. border: 2px solid var(--border-main);
  82. background: #fff9e8;
  83. color: var(--text-main);
  84. text-align: left;
  85. display: flex;
  86. align-items: center;
  87. gap: 8px;
  88. padding: 10px 12px;
  89. font-size: 14px;
  90. font-weight: 600;
  91. cursor: pointer;
  92. box-shadow: var(--shadow-soft);
  93. transition: transform .08s;
  94. font-family: var(--font-body);
  95. }
  96. .nav-item:hover { transform: translate(-1px,-1px); }
  97. .nav-item.active { background: #dff3ff; border-color: var(--accent-blue); }
  98. .nav-item .icon { width: 22px; text-align: center; }
  99. .live-indicator {
  100. border-top: 3px solid var(--border-main);
  101. padding: 10px 12px;
  102. font-size: 13px;
  103. font-weight: 500;
  104. display: flex;
  105. align-items: center;
  106. gap: 8px;
  107. }
  108. .live-dot {
  109. width: 10px; height: 10px;
  110. background: var(--accent-green);
  111. border: 2px solid var(--border-main);
  112. }
  113. /* ===== MAIN ===== */
  114. .main-content {
  115. overflow-y: auto;
  116. padding: 22px;
  117. }
  118. .page { display: none; }
  119. .page.active { display: block; }
  120. .page-header {
  121. display: flex;
  122. align-items: center;
  123. gap: 12px;
  124. margin-bottom: 14px;
  125. }
  126. .page-header h2 { font-size: 22px; font-weight: 700; }
  127. /* ===== CARD ===== */
  128. .card {
  129. background: var(--bg-card);
  130. border: 3px solid var(--border-main);
  131. box-shadow: var(--shadow-main);
  132. padding: 16px;
  133. margin-bottom: 16px;
  134. }
  135. .card-header {
  136. display: flex;
  137. align-items: center;
  138. justify-content: space-between;
  139. gap: 12px;
  140. margin-bottom: 10px;
  141. }
  142. .card-title { font-size: 17px; font-weight: 700; }
  143. .badge {
  144. border: 2px solid var(--border-main);
  145. font-size: 12px;
  146. font-weight: 700;
  147. padding: 3px 8px;
  148. background: #fff;
  149. display: inline-block;
  150. }
  151. .badge-blue { background: #dff3ff; color: #055d8b; }
  152. .badge-green { background: #dcfce7; color: #0f5132; }
  153. .badge-amber { background: #fff1cd; color: #8a5b00; }
  154. .badge-red { background: #ffe0e5; color: #8f1d30; }
  155. .badge-purple { background: #ece3ff; color: #4a2ea8; }
  156. .badge-cyan { background: #dcfafe; color: #155e75; }
  157. /* ===== STAT GRID ===== */
  158. .stat-grid {
  159. display: grid;
  160. grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  161. gap: 12px;
  162. margin-bottom: 14px;
  163. }
  164. .stat-card .stat-label {
  165. font-size: 13px;
  166. font-weight: 600;
  167. color: var(--text-mute);
  168. }
  169. .stat-card .stat-value {
  170. font-size: 28px;
  171. line-height: 1.15;
  172. margin: 6px 0 2px;
  173. color: var(--accent-blue);
  174. font-variant-numeric: tabular-nums;
  175. }
  176. .stat-card .stat-value.plain { color: var(--text-main); }
  177. .stat-sub { font-size: 13px; font-weight: 500; color: var(--text-sub); }
  178. .progress-track {
  179. margin-top: 8px;
  180. height: 12px;
  181. border: 2px solid var(--border-main);
  182. background: #f8e3b8;
  183. }
  184. .progress-fill {
  185. height: 100%;
  186. background: linear-gradient(90deg, #26a8ff, #7f5af0);
  187. }
  188. /* ===== CHART CONTAINER ===== */
  189. .chart-box {
  190. width: 100%;
  191. height: 320px;
  192. }
  193. .chart-box.tall { height: 420px; }
  194. /* ===== TABLE ===== */
  195. .table-wrap {
  196. overflow-x: auto;
  197. border: 2px solid var(--border-soft);
  198. background: var(--bg-panel);
  199. }
  200. .data-table {
  201. width: 100%;
  202. min-width: 580px;
  203. border-collapse: collapse;
  204. font-size: 14px;
  205. font-variant-numeric: tabular-nums;
  206. }
  207. .data-table th {
  208. text-align: left;
  209. padding: 8px 10px;
  210. border-bottom: 2px solid var(--border-soft);
  211. background: var(--bg-card-2);
  212. white-space: nowrap;
  213. font-weight: 700;
  214. }
  215. .data-table td {
  216. padding: 8px 10px;
  217. border-bottom: 1px solid #d8ccb2;
  218. font-weight: 500;
  219. }
  220. .data-table tbody tr:hover td { background: #fff4d8; }
  221. /* ===== FILTER BUTTONS ===== */
  222. .filter-group {
  223. display: flex;
  224. flex-wrap: wrap;
  225. gap: 8px;
  226. margin-bottom: 12px;
  227. }
  228. .filter-btn {
  229. border: 2px solid var(--border-main);
  230. background: #fff8e6;
  231. color: var(--text-main);
  232. font-family: var(--font-body);
  233. font-size: 13px;
  234. font-weight: 600;
  235. padding: 5px 10px;
  236. cursor: pointer;
  237. }
  238. .filter-btn.active { background: #e6f7ff; border-color: var(--accent-blue); }
  239. /* ===== PAGER ===== */
  240. .pager {
  241. display: flex;
  242. align-items: center;
  243. justify-content: space-between;
  244. gap: 10px;
  245. margin-top: 8px;
  246. }
  247. .page-btn {
  248. border: 2px solid var(--border-main);
  249. background: #fff8e6;
  250. font-family: var(--font-body);
  251. font-size: 13px;
  252. font-weight: 600;
  253. padding: 4px 10px;
  254. cursor: pointer;
  255. }
  256. .page-btn:hover { background: #e6f7ff; border-color: var(--accent-blue); }
  257. .page-info { font-size: 13px; font-weight: 600; color: var(--text-sub); }
  258. /* ===== SPLIT LAYOUT ===== */
  259. .split-layout {
  260. display: grid;
  261. grid-template-columns: minmax(0,1fr) 340px;
  262. gap: 14px;
  263. }
  264. /* ===== GANTT ===== */
  265. .chart-box.gantt { height: 380px; }
  266. /* ===== SECTION LABEL ===== */
  267. .section-label {
  268. font-family: var(--font-display);
  269. font-size: 9px;
  270. letter-spacing: .1em;
  271. color: var(--text-mute);
  272. margin-bottom: 8px;
  273. text-transform: uppercase;
  274. }
  275. /* ===== PAGE NOTE ===== */
  276. .proto-note {
  277. background: #fff3d5;
  278. border: 2px dashed var(--border-soft);
  279. padding: 10px 14px;
  280. font-size: 13px;
  281. color: var(--text-sub);
  282. margin-bottom: 14px;
  283. }
  284. </style>
  285. </head>
  286. <body>
  287. <div class="app-layout">
  288. <!-- SIDEBAR -->
  289. <aside class="sidebar">
  290. <div class="sidebar-header">
  291. <h1>PIXEL WRITER<br>HUB</h1>
  292. <div class="subtitle">《仙道长青》</div>
  293. </div>
  294. <nav class="sidebar-nav" id="nav">
  295. <button class="nav-item active" data-page="overview"><span class="icon">📊</span><span>总览</span></button>
  296. <button class="nav-item" data-page="characters"><span class="icon">👤</span><span>角色图鉴</span></button>
  297. <button class="nav-item" data-page="pacing"><span class="icon">📈</span><span>节奏雷达</span></button>
  298. <button class="nav-item" data-page="foreshadowing"><span class="icon">🔖</span><span>伏笔追踪</span></button>
  299. <button class="nav-item" data-page="files"><span class="icon">📁</span><span>文档浏览</span></button>
  300. <button class="nav-item" data-page="system"><span class="icon">⚙️</span><span>系统状态</span></button>
  301. </nav>
  302. <div class="live-indicator">
  303. <span class="live-dot"></span>
  304. 实时同步中
  305. </div>
  306. </aside>
  307. <!-- MAIN -->
  308. <main class="main-content">
  309. <!-- ==================== PAGE 1: 总览 ==================== -->
  310. <div class="page active" id="page-overview">
  311. <div class="page-header">
  312. <h2>📊 总览</h2>
  313. <span class="badge badge-blue">仙侠</span>
  314. </div>
  315. <div class="stat-grid">
  316. <div class="card stat-card">
  317. <span class="stat-label">总字数</span>
  318. <span class="stat-value">128.6 万</span>
  319. <span class="stat-sub">目标 200 万字 · 64.3%</span>
  320. <div class="progress-track"><div class="progress-fill" style="width:64.3%"></div></div>
  321. </div>
  322. <div class="card stat-card">
  323. <span class="stat-label">当前章节</span>
  324. <span class="stat-value">第 412 章</span>
  325. <span class="stat-sub">目标 800 章 · 卷 5</span>
  326. </div>
  327. <div class="card stat-card">
  328. <span class="stat-label">Story Runtime</span>
  329. <span class="stat-value plain">Mainline</span>
  330. <span class="stat-sub">accepted · projection OK</span>
  331. </div>
  332. <div class="card stat-card">
  333. <span class="stat-label">审查均分</span>
  334. <span class="stat-value">7.8</span>
  335. <span class="stat-sub">最近 50 章平均</span>
  336. </div>
  337. <div class="card stat-card">
  338. <span class="stat-label">紧急伏笔</span>
  339. <span class="stat-value" style="color:var(--accent-amber)">4</span>
  340. <span class="stat-sub">总计 37 条伏笔</span>
  341. </div>
  342. </div>
  343. <!-- 审查得分折线图 -->
  344. <div class="card">
  345. <div class="card-header">
  346. <span class="card-title">审查得分趋势</span>
  347. <div>
  348. <span class="badge badge-green">最近 50 章</span>
  349. <button class="page-btn" style="margin-left:6px">← 前 50</button>
  350. <button class="page-btn">跳到最新 →</button>
  351. </div>
  352. </div>
  353. <div class="chart-box" id="chart-review-score"></div>
  354. </div>
  355. <!-- 字数分布柱状图 -->
  356. <div class="card">
  357. <div class="card-header">
  358. <span class="card-title">字数分布(按卷)</span>
  359. <span class="badge badge-purple">5 卷</span>
  360. </div>
  361. <div class="chart-box" id="chart-word-dist"></div>
  362. </div>
  363. <!-- Strand Weave -->
  364. <div class="card">
  365. <div class="card-header">
  366. <span class="card-title">Strand Weave 整体分布</span>
  367. <span class="badge badge-purple">constellation</span>
  368. </div>
  369. <div class="chart-box" id="chart-strand-overview" style="height:260px"></div>
  370. </div>
  371. <!-- 紧急伏笔 Top 5 -->
  372. <div class="card">
  373. <div class="card-header">
  374. <span class="card-title">紧急伏笔 Top 5</span>
  375. </div>
  376. <div class="table-wrap">
  377. <table class="data-table">
  378. <thead><tr><th>内容</th><th>状态</th><th>埋设章</th><th>目标章</th><th>紧急度</th></tr></thead>
  379. <tbody>
  380. <tr><td>青元秘境的钥匙碎片下落</td><td><span class="badge badge-red">超期</span></td><td>285</td><td>350</td><td><span class="badge badge-red">critical</span></td></tr>
  381. <tr><td>凤灵儿真实身份暗示</td><td><span class="badge badge-amber">紧急</span></td><td>312</td><td>420</td><td><span class="badge badge-amber">high</span></td></tr>
  382. <tr><td>老道士临终遗言中的数字</td><td><span class="badge badge-amber">紧急</span></td><td>356</td><td>430</td><td><span class="badge badge-amber">high</span></td></tr>
  383. <tr><td>黑市拍卖会幕后势力</td><td><span class="badge badge-amber">紧急</span></td><td>389</td><td>440</td><td><span class="badge badge-amber">medium</span></td></tr>
  384. <tr><td>主角功法异变的真实原因</td><td><span class="badge badge-blue">活跃</span></td><td>401</td><td>500</td><td><span class="badge badge-blue">normal</span></td></tr>
  385. </tbody>
  386. </table>
  387. </div>
  388. </div>
  389. </div>
  390. <!-- ==================== PAGE 2: 角色图鉴 ==================== -->
  391. <div class="page" id="page-characters">
  392. <div class="page-header">
  393. <h2>👤 角色图鉴</h2>
  394. <span class="badge badge-green">48 / 127 个实体</span>
  395. </div>
  396. <div class="filter-group">
  397. <button class="filter-btn active">全部</button>
  398. <button class="filter-btn">角色</button>
  399. <button class="filter-btn">势力</button>
  400. <button class="filter-btn">地点</button>
  401. <button class="filter-btn">法宝</button>
  402. </div>
  403. <div class="proto-note">Tab 1: 实体列表 + 详情面板(保留现有逻辑) | Tab 2: 关系图谱(下方预览)</div>
  404. <!-- 关系图谱 -->
  405. <div class="card">
  406. <div class="card-header">
  407. <span class="card-title">关系图谱</span>
  408. <span class="badge badge-blue">ECharts graph · 力导向 · 时间轴</span>
  409. </div>
  410. <!-- 时间轴控制器 -->
  411. <div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;flex-wrap:wrap;">
  412. <button class="page-btn" id="graph-play-btn" style="min-width:60px">▶ 播放</button>
  413. <span style="font-size:13px;font-weight:600;color:var(--text-mute);white-space:nowrap">第 1 章</span>
  414. <input type="range" id="graph-timeline" min="1" max="412" value="412"
  415. style="flex:1;min-width:200px;height:12px;accent-color:#26a8ff;cursor:pointer">
  416. <span style="font-size:13px;font-weight:600;color:var(--text-mute);white-space:nowrap">第 412 章</span>
  417. <span id="graph-chapter-label" class="badge badge-blue" style="min-width:90px;text-align:center">第 412 章</span>
  418. <span id="graph-node-count" class="badge badge-green" style="min-width:60px;text-align:center">8 人</span>
  419. </div>
  420. <div class="chart-box tall" id="chart-relation-graph"></div>
  421. </div>
  422. </div>
  423. <!-- ==================== PAGE 3: 节奏雷达 ==================== -->
  424. <div class="page" id="page-pacing">
  425. <div class="page-header">
  426. <h2>📈 节奏雷达</h2>
  427. <span class="badge badge-amber">412 章数据</span>
  428. </div>
  429. <!-- 钩子强度面积图 -->
  430. <div class="card">
  431. <div class="card-header">
  432. <span class="card-title">钩子强度走势</span>
  433. <div>
  434. <span class="badge badge-green">第 363-412 章</span>
  435. <button class="page-btn" style="margin-left:6px">← 前 50</button>
  436. <button class="page-btn">跳到最新 →</button>
  437. </div>
  438. </div>
  439. <div class="chart-box" id="chart-hook-strength"></div>
  440. </div>
  441. <!-- Strand 堆叠柱状图 -->
  442. <div class="card">
  443. <div class="card-header">
  444. <span class="card-title">Strand 分布(逐章)</span>
  445. <span class="badge badge-purple">堆叠柱状图</span>
  446. </div>
  447. <div class="chart-box" id="chart-strand-stack"></div>
  448. </div>
  449. <!-- 字数分布 -->
  450. <div class="card">
  451. <div class="card-header">
  452. <span class="card-title">章节字数分布</span>
  453. <span class="badge badge-blue">按卷分组</span>
  454. </div>
  455. <div class="chart-box" id="chart-pacing-words"></div>
  456. </div>
  457. </div>
  458. <!-- ==================== PAGE 4: 伏笔追踪 ==================== -->
  459. <div class="page" id="page-foreshadowing">
  460. <div class="page-header">
  461. <h2>🔖 伏笔追踪</h2>
  462. </div>
  463. <div class="stat-grid">
  464. <div class="card stat-card">
  465. <span class="stat-label">总伏笔</span>
  466. <span class="stat-value plain">37</span>
  467. </div>
  468. <div class="card stat-card">
  469. <span class="stat-label">活跃</span>
  470. <span class="stat-value" style="color:var(--accent-blue)">18</span>
  471. </div>
  472. <div class="card stat-card">
  473. <span class="stat-label">已回收</span>
  474. <span class="stat-value" style="color:var(--accent-green)">15</span>
  475. </div>
  476. <div class="card stat-card">
  477. <span class="stat-label">紧急/超期</span>
  478. <span class="stat-value" style="color:var(--accent-red)">4</span>
  479. </div>
  480. </div>
  481. <div class="filter-group">
  482. <button class="filter-btn active">全部</button>
  483. <button class="filter-btn">紧急</button>
  484. <button class="filter-btn">活跃</button>
  485. <button class="filter-btn">已回收</button>
  486. </div>
  487. <!-- 甘特时间线 -->
  488. <div class="card">
  489. <div class="card-header">
  490. <span class="card-title">伏笔时间线</span>
  491. <span class="badge badge-cyan">ECharts 自定义 bar · 甘特</span>
  492. </div>
  493. <div class="chart-box gantt" id="chart-foreshadow-gantt"></div>
  494. </div>
  495. <!-- 伏笔完整表格 -->
  496. <div class="card">
  497. <div class="card-header">
  498. <span class="card-title">完整伏笔列表</span>
  499. </div>
  500. <div class="table-wrap">
  501. <table class="data-table">
  502. <thead><tr><th>内容</th><th>状态</th><th>埋设章</th><th>目标章</th><th>紧急度</th></tr></thead>
  503. <tbody>
  504. <tr><td>青元秘境的钥匙碎片下落</td><td><span class="badge badge-red">超期</span></td><td>285</td><td>350</td><td><span class="badge badge-red">critical</span></td></tr>
  505. <tr><td>凤灵儿真实身份暗示</td><td><span class="badge badge-amber">紧急</span></td><td>312</td><td>420</td><td><span class="badge badge-amber">high</span></td></tr>
  506. <tr><td>天魔血脉觉醒征兆</td><td><span class="badge badge-blue">活跃</span></td><td>345</td><td>500</td><td><span class="badge badge-blue">normal</span></td></tr>
  507. <tr><td>第一卷师门灭门线索</td><td><span class="badge badge-green">已回收</span></td><td>12</td><td>180</td><td>—</td></tr>
  508. </tbody>
  509. </table>
  510. </div>
  511. <div class="pager">
  512. <button class="page-btn">上一页</button>
  513. <span class="page-info">第 1 / 4 页 · 共 37 条</span>
  514. <button class="page-btn">下一页</button>
  515. </div>
  516. </div>
  517. </div>
  518. <!-- ==================== PAGE 5: 文档浏览 ==================== -->
  519. <div class="page" id="page-files">
  520. <div class="page-header">
  521. <h2>📁 文档浏览</h2>
  522. </div>
  523. <div class="proto-note">逻辑不变,从现有 App.jsx 迁移。左侧文件树 + 右侧内容预览。</div>
  524. <div class="card" style="height:500px;display:flex;align-items:center;justify-content:center;">
  525. <span style="font-size:40px;margin-right:12px">📂</span>
  526. <span style="color:var(--text-mute);font-size:16px;font-weight:600">文件树 + 内容预览(直接迁移,无变化)</span>
  527. </div>
  528. </div>
  529. <!-- ==================== PAGE 6: 系统状态 ==================== -->
  530. <div class="page" id="page-system">
  531. <div class="page-header">
  532. <h2>⚙️ 系统状态</h2>
  533. </div>
  534. <div class="stat-grid">
  535. <div class="card stat-card">
  536. <span class="stat-label">Story Runtime</span>
  537. <span class="stat-value plain">Mainline</span>
  538. <span class="stat-sub">fallback: state.json, index.db</span>
  539. </div>
  540. <div class="card stat-card">
  541. <span class="stat-label">Latest Commit</span>
  542. <span class="stat-value plain">accepted</span>
  543. <span class="stat-sub">第 412 章 · 5 路 projection OK</span>
  544. </div>
  545. <div class="card stat-card">
  546. <span class="stat-label">RAG Mode</span>
  547. <span class="stat-value" style="color:var(--accent-green)">full</span>
  548. <span class="stat-sub">embed + rerank 就绪</span>
  549. </div>
  550. <div class="card stat-card">
  551. <span class="stat-label">Vector DB</span>
  552. <span class="stat-value plain">2,847</span>
  553. <span class="stat-sub">条向量记录</span>
  554. </div>
  555. </div>
  556. <!-- 合同树概览 -->
  557. <div class="card">
  558. <div class="card-header">
  559. <span class="card-title">合同树概览</span>
  560. </div>
  561. <div class="table-wrap">
  562. <table class="data-table">
  563. <thead><tr><th>类型</th><th>数量</th><th>说明</th></tr></thead>
  564. <tbody>
  565. <tr><td>MASTER_SETTING</td><td><span class="badge badge-green">1</span></td><td>仙侠 · 沉稳厚重</td></tr>
  566. <tr><td>VOLUME_BRIEF</td><td><span class="badge badge-blue">5</span></td><td>卷 1-5</td></tr>
  567. <tr><td>CHAPTER_BRIEF</td><td><span class="badge badge-blue">412</span></td><td>全章</td></tr>
  568. <tr><td>REVIEW_CONTRACT</td><td><span class="badge badge-purple">412</span></td><td>全章审查合同</td></tr>
  569. </tbody>
  570. </table>
  571. </div>
  572. </div>
  573. <!-- 最近 Commit -->
  574. <div class="card">
  575. <div class="card-header">
  576. <span class="card-title">最近 Commit 历史</span>
  577. </div>
  578. <div class="table-wrap">
  579. <table class="data-table">
  580. <thead><tr><th>章节</th><th>状态</th><th>state</th><th>index</th><th>summary</th><th>memory</th><th>dashboard</th></tr></thead>
  581. <tbody>
  582. <tr><td>第 412 章</td><td><span class="badge badge-green">accepted</span></td><td><span class="badge badge-green">OK</span></td><td><span class="badge badge-green">OK</span></td><td><span class="badge badge-green">OK</span></td><td><span class="badge badge-green">OK</span></td><td><span class="badge badge-green">OK</span></td></tr>
  583. <tr><td>第 411 章</td><td><span class="badge badge-green">accepted</span></td><td><span class="badge badge-green">OK</span></td><td><span class="badge badge-green">OK</span></td><td><span class="badge badge-green">OK</span></td><td><span class="badge badge-green">OK</span></td><td><span class="badge badge-green">OK</span></td></tr>
  584. <tr><td>第 410 章</td><td><span class="badge badge-red">rejected</span></td><td><span class="badge badge-amber">skip</span></td><td><span class="badge badge-amber">skip</span></td><td><span class="badge badge-amber">skip</span></td><td><span class="badge badge-amber">skip</span></td><td><span class="badge badge-amber">skip</span></td></tr>
  585. </tbody>
  586. </table>
  587. </div>
  588. </div>
  589. <!-- RAG 诊断 -->
  590. <div class="card">
  591. <div class="card-header">
  592. <span class="card-title">RAG 环境</span>
  593. <button class="page-btn">运行诊断</button>
  594. </div>
  595. <div class="table-wrap">
  596. <table class="data-table">
  597. <thead><tr><th>组件</th><th>状态</th><th>详情</th></tr></thead>
  598. <tbody>
  599. <tr><td>Embedding Key</td><td><span class="badge badge-green">OK</span></td><td>VOYAGE_API_KEY 已配置</td></tr>
  600. <tr><td>Rerank Key</td><td><span class="badge badge-green">OK</span></td><td>COHERE_API_KEY 已配置</td></tr>
  601. <tr><td>Vector DB</td><td><span class="badge badge-green">OK</span></td><td>2,847 records · 128 MB</td></tr>
  602. <tr><td>RAG Mode</td><td><span class="badge badge-green">full</span></td><td>embed + rerank</td></tr>
  603. </tbody>
  604. </table>
  605. </div>
  606. </div>
  607. </div>
  608. </main>
  609. </div>
  610. <script>
  611. // ===== NAV SWITCHING =====
  612. document.getElementById('nav').addEventListener('click', e => {
  613. const btn = e.target.closest('.nav-item');
  614. if (!btn) return;
  615. document.querySelectorAll('.nav-item').forEach(b => b.classList.remove('active'));
  616. btn.classList.add('active');
  617. document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
  618. document.getElementById('page-' + btn.dataset.page).classList.add('active');
  619. // re-render charts on page switch
  620. setTimeout(() => window.dispatchEvent(new Event('resize')), 50);
  621. });
  622. // ===== ECHARTS PIXEL THEME =====
  623. const PIXEL_THEME = {
  624. color: ['#26a8ff','#f5a524','#7f5af0','#2ec27e','#d7263d','#00b8d4','#ff5c8a'],
  625. backgroundColor: 'transparent',
  626. textStyle: { fontFamily: "'Noto Sans SC', sans-serif", color: '#2a220f' },
  627. title: { textStyle: { fontFamily: "'Press Start 2P', monospace", fontSize: 11, color: '#2a220f' } },
  628. legend: { textStyle: { fontSize: 13, fontWeight: 600, color: '#5d5035' } },
  629. tooltip: {
  630. backgroundColor: '#fffaf0',
  631. borderColor: '#2a220f',
  632. borderWidth: 2,
  633. textStyle: { color: '#2a220f', fontSize: 13 },
  634. extraCssText: 'border-radius:0;box-shadow:3px 3px 0 #2a220f;'
  635. },
  636. categoryAxis: {
  637. axisLine: { lineStyle: { color: '#8f7f5c', width: 2 } },
  638. axisTick: { lineStyle: { color: '#8f7f5c' } },
  639. axisLabel: { color: '#8f7f5c', fontSize: 12 },
  640. splitLine: { lineStyle: { color: '#e8dcc4', type: 'dashed' } }
  641. },
  642. valueAxis: {
  643. axisLine: { lineStyle: { color: '#8f7f5c', width: 2 } },
  644. axisTick: { lineStyle: { color: '#8f7f5c' } },
  645. axisLabel: { color: '#8f7f5c', fontSize: 12 },
  646. splitLine: { lineStyle: { color: '#e8dcc4', type: 'dashed' } }
  647. },
  648. grid: { left: 50, right: 20, top: 30, bottom: 40 }
  649. };
  650. echarts.registerTheme('pixel', PIXEL_THEME);
  651. function px(id, opt) {
  652. const el = document.getElementById(id);
  653. if (!el) return null;
  654. const chart = echarts.init(el, 'pixel');
  655. chart.setOption(opt);
  656. window.addEventListener('resize', () => chart.resize());
  657. return chart;
  658. }
  659. // ===== SAMPLE DATA =====
  660. const chapters50 = Array.from({length:50}, (_,i) => i+363);
  661. const reviewScores = [7.2,7.5,6.8,7.9,8.1,7.4,7.0,7.8,8.3,7.6,6.5,7.2,7.8,8.0,7.1,7.5,7.9,8.2,7.3,6.9,7.7,8.0,7.4,7.6,8.1,7.8,7.2,7.5,8.4,7.0,7.3,7.9,8.0,7.6,7.1,7.8,8.2,7.5,7.3,8.0,7.7,7.4,8.1,7.9,7.2,7.6,8.3,7.8,7.5,7.8];
  662. // 1. Review Score Line
  663. px('chart-review-score', {
  664. xAxis: { type: 'category', data: chapters50.map(c => '第'+c+'章'), axisLabel: { interval: 9 } },
  665. yAxis: { type: 'value', min: 5, max: 10 },
  666. series: [{
  667. type: 'line', data: reviewScores, smooth: false, step: false,
  668. lineStyle: { width: 3, color: '#26a8ff' },
  669. itemStyle: { color: '#26a8ff', borderColor: '#2a220f', borderWidth: 2 },
  670. symbol: 'rect', symbolSize: 8,
  671. markLine: { data: [{ type: 'average', label: { formatter: '均值 {c}' } }], lineStyle: { color: '#f5a524', width: 2, type: 'dashed' } },
  672. areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: 'rgba(38,168,255,.2)' }, { offset: 1, color: 'rgba(38,168,255,0)' }] } }
  673. }]
  674. });
  675. // 2. Word Distribution by Volume
  676. px('chart-word-dist', {
  677. xAxis: { type: 'category', data: ['卷一\n求道篇','卷二\n筑基篇','卷三\n历练篇','卷四\n金丹篇','卷五\n元婴篇'] },
  678. yAxis: { type: 'value', axisLabel: { formatter: v => (v/10000).toFixed(0)+'万' } },
  679. series: [{
  680. type: 'bar', data: [
  681. { value: 280000, itemStyle: { color: '#26a8ff' } },
  682. { value: 310000, itemStyle: { color: '#7f5af0' } },
  683. { value: 265000, itemStyle: { color: '#2ec27e' } },
  684. { value: 290000, itemStyle: { color: '#f5a524' } },
  685. { value: 141000, itemStyle: { color: '#00b8d4' } }
  686. ],
  687. barWidth: '50%',
  688. label: { show: true, position: 'top', formatter: p => (p.value/10000).toFixed(1)+'万', fontSize: 12, fontWeight: 700 },
  689. itemStyle: { borderColor: '#2a220f', borderWidth: 2 }
  690. }]
  691. });
  692. // 3. Strand Overview Pie
  693. px('chart-strand-overview', {
  694. legend: { bottom: 0, data: ['Quest','Fire','Constellation'] },
  695. series: [{
  696. type: 'pie', radius: ['40%','70%'], center: ['50%','45%'],
  697. data: [
  698. { value: 145, name: 'Quest', itemStyle: { color: '#26a8ff', borderColor: '#2a220f', borderWidth: 2 } },
  699. { value: 128, name: 'Fire', itemStyle: { color: '#ff5c8a', borderColor: '#2a220f', borderWidth: 2 } },
  700. { value: 139, name: 'Constellation', itemStyle: { color: '#7f5af0', borderColor: '#2a220f', borderWidth: 2 } }
  701. ],
  702. label: { formatter: '{b}\n{d}%', fontSize: 13, fontWeight: 600 },
  703. itemStyle: { borderColor: '#2a220f', borderWidth: 2 }
  704. }]
  705. });
  706. // 4. Relation Graph with Timeline
  707. const graphNodes = [
  708. { name: '林长青', category: 0, appear: 1, symbolSize: [70,35], itemStyle: { color: '#f5a524' } },
  709. { name: '老道士', category: 0, appear: 1 },
  710. { name: '太虚宗', category: 1, appear: 5 },
  711. { name: '凤灵儿', category: 0, appear: 28 },
  712. { name: '初代掌门遗物', category: 2, appear: 45 },
  713. { name: '青元秘境', category: 2, appear: 80 },
  714. { name: '白玉京', category: 0, appear: 120 },
  715. { name: '天魔教', category: 1, appear: 150 },
  716. { name: '东海仙城', category: 2, appear: 220 },
  717. { name: '龙脉封印', category: 2, appear: 260 },
  718. { name: '黑市掮客', category: 0, appear: 310 },
  719. { name: '剑灵', category: 0, appear: 380 }
  720. ];
  721. const graphLinks = [
  722. { source: '林长青', target: '老道士', name: '师徒', appear: 1 },
  723. { source: '老道士', target: '太虚宗', name: '前长老', appear: 5 },
  724. { source: '林长青', target: '太虚宗', name: '入门', appear: 12 },
  725. { source: '林长青', target: '凤灵儿', name: '初识', appear: 28, changeTo: '师兄妹', changeAt: 60 },
  726. { source: '林长青', target: '初代掌门遗物', name: '获得', appear: 50 },
  727. { source: '太虚宗', target: '青元秘境', name: '管辖', appear: 80 },
  728. { source: '林长青', target: '青元秘境', name: '历练', appear: 85 },
  729. { source: '凤灵儿', target: '太虚宗', name: '弟子', appear: 35 },
  730. { source: '林长青', target: '白玉京', name: '初遇', appear: 120, changeTo: '宿敌', changeAt: 200 },
  731. { source: '白玉京', target: '天魔教', name: '加入', appear: 180, changeTo: '长老', changeAt: 300 },
  732. { source: '太虚宗', target: '天魔教', name: '敌对', appear: 200 },
  733. { source: '老道士', target: '东海仙城', name: '隐居', appear: 220 },
  734. { source: '林长青', target: '东海仙城', name: '造访', appear: 235 },
  735. { source: '林长青', target: '龙脉封印', name: '发现', appear: 260 },
  736. { source: '黑市掮客', target: '天魔教', name: '线人', appear: 320 },
  737. { source: '林长青', target: '黑市掮客', name: '交易', appear: 330 },
  738. { source: '林长青', target: '剑灵', name: '契约', appear: 385 },
  739. { source: '剑灵', target: '初代掌门遗物', name: '寄宿', appear: 390 }
  740. ];
  741. const graphCategories = [
  742. { name: '角色', itemStyle: { color: '#26a8ff' } },
  743. { name: '势力', itemStyle: { color: '#7f5af0' } },
  744. { name: '地点', itemStyle: { color: '#2ec27e' } }
  745. ];
  746. function getGraphDataAtChapter(ch) {
  747. const nodes = graphNodes
  748. .filter(n => n.appear <= ch)
  749. .map(n => ({
  750. ...n,
  751. symbol: 'rect',
  752. symbolSize: n.symbolSize || [60, 30],
  753. label: { show: true, fontSize: 12, fontWeight: 700, color: '#fff' },
  754. itemStyle: { ...(n.itemStyle || {}), borderColor: '#2a220f', borderWidth: 2 }
  755. }));
  756. const nodeNames = new Set(nodes.map(n => n.name));
  757. const links = graphLinks
  758. .filter(l => l.appear <= ch && nodeNames.has(l.source) && nodeNames.has(l.target))
  759. .map(l => ({
  760. source: l.source,
  761. target: l.target,
  762. name: (l.changeTo && ch >= l.changeAt) ? l.changeTo : l.name
  763. }));
  764. return { nodes, links };
  765. }
  766. const graphEl = document.getElementById('chart-relation-graph');
  767. const graphChart = echarts.init(graphEl, 'pixel');
  768. const slider = document.getElementById('graph-timeline');
  769. const chLabel = document.getElementById('graph-chapter-label');
  770. const nodeCount = document.getElementById('graph-node-count');
  771. const playBtn = document.getElementById('graph-play-btn');
  772. let playing = false, playTimer = null;
  773. function renderGraph(ch) {
  774. const { nodes, links } = getGraphDataAtChapter(ch);
  775. chLabel.textContent = '第 ' + ch + ' 章';
  776. nodeCount.textContent = nodes.length + ' 人';
  777. graphChart.setOption({
  778. animationDuration: 300,
  779. animationEasingUpdate: 'cubicOut',
  780. series: [{
  781. type: 'graph', layout: 'force', roam: true,
  782. symbol: 'rect',
  783. edgeLabel: { show: true, fontSize: 11, formatter: p => p.data.name, color: '#5d5035' },
  784. force: { repulsion: 350, edgeLength: [120, 200], gravity: 0.1 },
  785. lineStyle: { color: '#8f7f5c', width: 2, curveness: 0.1 },
  786. categories: graphCategories,
  787. nodes: nodes,
  788. links: links
  789. }]
  790. });
  791. }
  792. slider.addEventListener('input', () => {
  793. renderGraph(parseInt(slider.value));
  794. });
  795. playBtn.addEventListener('click', () => {
  796. if (playing) {
  797. playing = false;
  798. clearInterval(playTimer);
  799. playBtn.textContent = '▶ 播放';
  800. } else {
  801. playing = true;
  802. playBtn.textContent = '⏸ 暂停';
  803. if (parseInt(slider.value) >= 412) slider.value = 1;
  804. playTimer = setInterval(() => {
  805. let v = parseInt(slider.value) + 5;
  806. if (v > 412) { v = 412; playing = false; clearInterval(playTimer); playBtn.textContent = '▶ 播放'; }
  807. slider.value = v;
  808. renderGraph(v);
  809. }, 120);
  810. }
  811. });
  812. renderGraph(412);
  813. window.addEventListener('resize', () => graphChart.resize());
  814. // 5. Hook Strength Area
  815. const hookValues = [3,4,3,5,4,3,2,5,4,3,4,5,3,4,5,4,3,5,4,3,2,4,5,3,4,5,4,3,5,4,3,4,5,3,4,2,5,4,3,5,4,3,5,4,3,4,5,4,3,4];
  816. px('chart-hook-strength', {
  817. xAxis: { type: 'category', data: chapters50.map(c => c+''), axisLabel: { interval: 9 } },
  818. yAxis: { type: 'value', min: 0, max: 5, axisLabel: { formatter: v => ['','weak','','medium','','strong'][v] || '' } },
  819. series: [{
  820. type: 'line', data: hookValues, smooth: false,
  821. lineStyle: { width: 3, color: '#f5a524' },
  822. itemStyle: { color: '#f5a524', borderColor: '#2a220f', borderWidth: 2 },
  823. symbol: 'rect', symbolSize: 6,
  824. areaStyle: { color: { type: 'linear', x:0,y:0,x2:0,y2:1, colorStops: [{offset:0,color:'rgba(245,165,36,.3)'},{offset:1,color:'rgba(245,165,36,0)'}] } }
  825. }]
  826. });
  827. // 6. Strand Stack Bar
  828. const strandChapters = chapters50.map(c => c+'');
  829. const questData = chapters50.map(() => Math.floor(Math.random()*3)+1);
  830. const fireData = chapters50.map(() => Math.floor(Math.random()*3)+1);
  831. const constData = chapters50.map(() => Math.floor(Math.random()*3)+1);
  832. px('chart-strand-stack', {
  833. legend: { data: ['Quest','Fire','Constellation'], bottom: 0 },
  834. xAxis: { type: 'category', data: strandChapters, axisLabel: { interval: 9 } },
  835. yAxis: { type: 'value' },
  836. series: [
  837. { name: 'Quest', type: 'bar', stack: 'strand', data: questData, itemStyle: { color: '#26a8ff', borderColor: '#2a220f', borderWidth: 1 }, barWidth: '60%' },
  838. { name: 'Fire', type: 'bar', stack: 'strand', data: fireData, itemStyle: { color: '#ff5c8a', borderColor: '#2a220f', borderWidth: 1 } },
  839. { name: 'Constellation', type: 'bar', stack: 'strand', data: constData, itemStyle: { color: '#7f5af0', borderColor: '#2a220f', borderWidth: 1 } }
  840. ]
  841. });
  842. // 7. Pacing Words by Volume
  843. const vol1 = Array.from({length:80}, () => 2800+Math.random()*1200);
  844. const vol2 = Array.from({length:90}, () => 3000+Math.random()*1500);
  845. const vol3 = Array.from({length:75}, () => 2500+Math.random()*1800);
  846. const vol4 = Array.from({length:85}, () => 3200+Math.random()*1200);
  847. const vol5 = Array.from({length:82}, () => 2600+Math.random()*1600);
  848. function volBoxData(arr) {
  849. const s = [...arr].sort((a,b)=>a-b);
  850. return [s[0], s[Math.floor(s.length*.25)], s[Math.floor(s.length*.5)], s[Math.floor(s.length*.75)], s[s.length-1]].map(Math.round);
  851. }
  852. px('chart-pacing-words', {
  853. xAxis: { type: 'category', data: ['卷一','卷二','卷三','卷四','卷五'] },
  854. yAxis: { type: 'value', axisLabel: { formatter: v => (v/1000).toFixed(0)+'k' } },
  855. series: [{
  856. type: 'boxplot',
  857. data: [volBoxData(vol1), volBoxData(vol2), volBoxData(vol3), volBoxData(vol4), volBoxData(vol5)],
  858. itemStyle: { color: '#fffaf0', borderColor: '#26a8ff', borderWidth: 2 }
  859. }]
  860. });
  861. // 8. Foreshadowing Gantt
  862. const foreshadowData = [
  863. { name: '青元秘境钥匙碎片', start: 285, end: 350, status: 'overdue' },
  864. { name: '凤灵儿真实身份', start: 312, end: 420, status: 'urgent' },
  865. { name: '老道士遗言数字', start: 356, end: 430, status: 'urgent' },
  866. { name: '黑市幕后势力', start: 389, end: 440, status: 'urgent' },
  867. { name: '功法异变原因', start: 401, end: 500, status: 'active' },
  868. { name: '天魔血脉觉醒', start: 345, end: 500, status: 'active' },
  869. { name: '仙城禁地秘密', start: 220, end: 480, status: 'active' },
  870. { name: '师门灭门线索', start: 12, end: 180, status: 'resolved' },
  871. { name: '初代掌门遗物', start: 45, end: 150, status: 'resolved' },
  872. { name: '龙脉封印', start: 100, end: 260, status: 'resolved' }
  873. ];
  874. const statusColor = { overdue: '#d7263d', urgent: '#f5a524', active: '#26a8ff', resolved: '#2ec27e' };
  875. const yLabels = foreshadowData.map(d => d.name);
  876. px('chart-foreshadow-gantt', {
  877. grid: { left: 140, right: 30, top: 10, bottom: 40 },
  878. xAxis: { type: 'value', min: 0, max: 520, axisLabel: { formatter: v => '第'+v+'章' }, splitLine: { lineStyle: { color: '#e8dcc4', type: 'dashed' } } },
  879. yAxis: { type: 'category', data: yLabels, inverse: true, axisLabel: { fontSize: 12, fontWeight: 600 } },
  880. series: [
  881. {
  882. type: 'custom',
  883. renderItem: function(params, api) {
  884. const catIdx = api.value(0);
  885. const start = api.coord([api.value(1), catIdx]);
  886. const end = api.coord([api.value(2), catIdx]);
  887. const height = api.size([0, 1])[1] * 0.5;
  888. return {
  889. type: 'rect',
  890. shape: { x: start[0], y: start[1] - height/2, width: end[0] - start[0], height: height },
  891. style: { fill: api.value(3), stroke: '#2a220f', lineWidth: 2 }
  892. };
  893. },
  894. encode: { x: [1, 2], y: 0 },
  895. data: foreshadowData.map((d, i) => [i, d.start, d.end, statusColor[d.status]])
  896. },
  897. {
  898. type: 'line', z: 10,
  899. markLine: {
  900. silent: true, symbol: 'none',
  901. lineStyle: { color: '#26a8ff', width: 3, type: 'solid' },
  902. data: [{ xAxis: 412 }],
  903. label: { formatter: '当前 412章', position: 'end', fontSize: 11, fontWeight: 700, color: '#26a8ff' }
  904. },
  905. data: []
  906. }
  907. ]
  908. });
  909. </script>
  910. </body>
  911. </html>