From 9f2cb6ef038d7ab5812bf2d90c7c7b295a919f69 Mon Sep 17 00:00:00 2001 From: CochraneK <48022373+CochraneK@users.noreply.github.com> Date: Mon, 14 Sep 2026 12:11:41 +0800 Subject: [PATCH 01/14] ui: restore classic visual theme --- web/css/theme.css | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/web/css/theme.css b/web/css/theme.css index d6abec9..4983f52 100644 --- a/web/css/theme.css +++ b/web/css/theme.css @@ -1,25 +1,7 @@ -/* ===== Visual polish 2026-09 · Ming digital-humanities skin ===== */ -:root{--paper-deep:#fbf6ea;--ink-soft:#544b43;--radius-lg:14px;--radius-md:10px;--shadow:0 10px 28px rgba(54,42,30,.075);--shadow-hover:0 16px 38px rgba(54,42,30,.12);--focus-ring:0 0 0 3px rgba(184,139,53,.28)} -html{background:#ece5da}body{background:radial-gradient(circle at 12% -8%,rgba(184,139,53,.13),transparent 34rem),radial-gradient(circle at 92% 8%,rgba(141,48,37,.07),transparent 28rem),linear-gradient(180deg,#f5f0e7 0,#eee8de 100%);background-attachment:fixed}::selection{background:#d9bf83;color:#2b251f} -.skip-link{position:fixed;left:16px;top:10px;z-index:100;transform:translateY(-180%);padding:8px 12px;border-radius:8px;background:#fffdf8;color:var(--accent);border:1px solid #cdbd9e;box-shadow:var(--shadow);font-weight:700;text-decoration:none}.skip-link:focus{transform:translateY(0)} -button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid #b88b35;outline-offset:2px;box-shadow:var(--focus-ring)} -.app-header{background:linear-gradient(135deg,rgba(37,31,26,.985),rgba(55,45,37,.985));border-bottom-color:#59493b;box-shadow:0 5px 20px rgba(31,24,18,.16)}@supports (backdrop-filter:blur(12px)){.app-header{background:linear-gradient(135deg,rgba(37,31,26,.93),rgba(55,45,37,.93));backdrop-filter:blur(12px) saturate(115%)}} -.header-inner{min-height:64px}.brand{font-family:'Songti SC','STSong','SimSun','Noto Serif CJK SC',serif;letter-spacing:.075em;text-shadow:0 1px 0 rgba(0,0,0,.2)}.header-stat{color:#cfc1ae}.tabs{scrollbar-width:thin;scrollbar-color:#655548 transparent}.tabs button,.subnav button{transition:background .16s ease,border-color .16s ease,color .16s ease,transform .16s ease,box-shadow .16s ease}.tabs button{border-radius:999px;background:rgba(255,255,255,.055);color:#eadfd2}.tabs button:hover{background:rgba(255,255,255,.1);transform:translateY(-1px)}.tabs button.active{background:linear-gradient(180deg,#9b3a2d,#81281f);border-color:#b05244;box-shadow:inset 0 1px 0 rgba(255,255,255,.16),0 5px 14px rgba(80,26,20,.2)} -.shell{max-width:1380px;padding-top:28px}.section-head{margin-bottom:18px}.section-head h2{position:relative;font-family:'Songti SC','STSong','SimSun','Noto Serif CJK SC',serif;font-weight:700;letter-spacing:.055em;color:#302820}.section-head h2::after{content:'';display:block;width:34px;height:2px;margin-top:7px;background:linear-gradient(90deg,var(--accent),var(--gold));border-radius:999px}.section-head p{max-width:78ch;color:#746a61} -.panel{border-color:#ddd3c4;border-radius:var(--radius-lg);box-shadow:var(--shadow)}.hero-copy{position:relative;overflow:hidden;background:radial-gradient(circle at 88% 15%,rgba(184,139,53,.22),transparent 22rem),linear-gradient(135deg,#302821,#45382d);border:1px solid #5a493b;border-radius:var(--radius-lg);box-shadow:0 14px 36px rgba(49,36,25,.14)}.hero-copy::after{content:'';position:absolute;right:-54px;bottom:-76px;width:220px;height:220px;border:1px solid rgba(222,190,121,.14);border-radius:50%;box-shadow:0 0 0 22px rgba(222,190,121,.025),0 0 0 48px rgba(222,190,121,.018);pointer-events:none}.hero-copy h2{font-family:'Songti SC','STSong','SimSun','Noto Serif CJK SC',serif;letter-spacing:.065em} -.metric,.distribution-stat,.location-card,.dynasty-card,.clean-card,.insight-toc-card{transition:transform .16s ease,box-shadow .16s ease,border-color .16s ease}.metric{border-color:#e2d8c8;border-radius:var(--radius-md);box-shadow:0 3px 12px rgba(54,42,30,.035)}.metric:hover,.distribution-stat:hover{transform:translateY(-2px);border-color:#cfbea3;box-shadow:0 10px 22px rgba(54,42,30,.08)}.metric .value,.distribution-stat strong{font-variant-numeric:tabular-nums}.quality-grid .quality{border-radius:0 9px 9px 0;background:linear-gradient(90deg,#fbf6ea,#fffdf8)} -.toolbar input,.toolbar select,.visual-toolbar select,.network-controls select{border-color:#d1c4b2;box-shadow:inset 0 1px 2px rgba(55,43,32,.025);transition:border-color .15s ease,box-shadow .15s ease,background .15s ease}.toolbar input:focus,.toolbar select:focus,.visual-toolbar select:focus,.network-controls select:focus{border-color:#b88b35;background:#fffdf9} -.location-card{border-color:#dfd5c6;border-radius:12px;box-shadow:0 4px 14px rgba(50,40,30,.045)}.location-card:hover{transform:translateY(-2px);border-color:#cdbb9f;box-shadow:var(--shadow-hover)}.card-title{font-family:'Songti SC','STSong','SimSun','Noto Serif CJK SC',serif;letter-spacing:.025em}.tag,.source-chip,.ep-tag{border-radius:999px} -.table-wrap{border-color:#d9cdbc;border-radius:12px;box-shadow:0 6px 18px rgba(54,42,30,.04)}.data-table th{background:linear-gradient(180deg,#faf6ed,#f3ede3);color:#5f5348}.data-table tr:nth-child(even) td{background:rgba(248,244,236,.42)}.data-table tr:hover td{background:#fff8ec} -.map-box,.network-svg,.full-graph-canvas{border-color:#d3c7b7;border-radius:12px;box-shadow:inset 0 0 0 1px rgba(255,255,255,.35)}.loc-dock{border-radius:12px;border-color:#d9cdbc}.character-card{border-radius:12px;box-shadow:0 5px 16px rgba(50,40,30,.07)}.character-card:hover{box-shadow:0 14px 30px rgba(50,40,30,.14)}.dynasty-card{border-radius:12px}.insight-toc,.insight-sec,.insight-refs{box-shadow:0 6px 18px rgba(60,45,25,.04)} -dialog{border:1px solid #cfc2b1;border-radius:14px;box-shadow:0 24px 78px rgba(20,15,10,.34)}dialog::backdrop{background:rgba(27,22,18,.56);backdrop-filter:blur(2px)}.dialog-head{position:sticky;top:-22px;z-index:2;background:var(--paper);padding-top:22px}.dialog-close,.loc-dock .dock-close{transition:background .15s ease,color .15s ease,transform .15s ease}.dialog-close:hover,.loc-dock .dock-close:hover{background:#e7ddcf;color:var(--accent)}.app-footer{text-align:center;padding:4px 24px 30px;font-size:12px;letter-spacing:.02em;color:#81776d} - -/* ===== V2 信息架构:分组导航 + 探索路径 + 可视化统一 ===== */ -.header-inner{display:grid;grid-template-columns:max-content minmax(0,1fr);grid-template-areas:'brand stat' 'tabs tabs';column-gap:18px;row-gap:9px;align-items:center}.brand{grid-area:brand}.header-stat{grid-area:stat;justify-self:end}.tabs{grid-area:tabs;width:100%;margin-left:0;padding:9px 0 1px;border-top:1px solid rgba(238,225,208,.12);gap:10px;align-items:center} -.nav-cluster{display:flex;align-items:center;gap:4px;padding-left:10px;border-left:1px solid rgba(238,225,208,.15);flex:none}.nav-cluster:first-child{padding-left:0;border-left:0}.nav-cluster-label{margin-right:2px;color:#bda990;font-size:9px;line-height:1;letter-spacing:.16em;white-space:nowrap}.nav-cluster button{padding:6px 10px;font-size:12px} -.v2-journeys{margin:0 0 18px}.v2-journey-head{display:flex;justify-content:space-between;align-items:end;gap:14px;margin-bottom:10px}.v2-journey-head h3{margin:0;font-family:'Songti SC','STSong','SimSun','Noto Serif CJK SC',serif;font-size:18px;letter-spacing:.05em;color:#372f28}.v2-journey-head p{margin:0;color:var(--muted);font-size:12px}.v2-journey-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}.v2-journey-card{position:relative;overflow:hidden;background:linear-gradient(145deg,rgba(255,253,248,.96),rgba(247,241,230,.92));border:1px solid #ddd1bf;border-radius:12px;padding:15px 16px;box-shadow:0 6px 18px rgba(54,42,30,.045)}.v2-journey-card::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--journey,var(--gold))}.v2-journey-card:nth-child(1){--journey:var(--accent)}.v2-journey-card:nth-child(2){--journey:var(--blue)}.v2-journey-card:nth-child(3){--journey:var(--green)}.v2-journey-kicker{display:block;color:var(--journey);font-size:10px;font-weight:700;letter-spacing:.14em}.v2-journey-card h4{margin:4px 0 5px;font-size:16px;color:#3a3129}.v2-journey-card p{margin:0;color:#70665d;font-size:12px;line-height:1.65}.v2-journey-links{display:flex;flex-wrap:wrap;gap:6px;margin-top:11px}.v2-journey-links button{border:1px solid #d5c7b3;background:#fffdf8;color:#645649;border-radius:999px;padding:4px 10px;font-size:11px}.v2-journey-links button:hover{border-color:var(--journey);color:var(--journey);background:#fffaf0} -.visuals-panel,.map-wrap{position:relative;overflow:hidden}.visuals-panel::before,.map-wrap::before{content:'';position:absolute;top:0;left:0;width:92px;height:3px;background:linear-gradient(90deg,var(--accent),var(--gold),transparent);z-index:2}.map-wrap::before{background:linear-gradient(90deg,var(--green),var(--blue),transparent)}.heatmap-wrap,.network-svg,.full-graph-canvas,.map-box{background:linear-gradient(180deg,#fffdf9,#faf6ee)}.timeline{background:linear-gradient(90deg,rgba(255,253,248,.94),rgba(249,244,234,.92));border:1px solid #ded2c1;border-left:3px solid var(--gold);border-radius:0 12px 12px 0;padding:16px 18px 16px 24px;box-shadow:0 6px 18px rgba(54,42,30,.04)}.chronicle-wrap{background:linear-gradient(180deg,rgba(255,253,248,.94),rgba(248,243,233,.9));border:1px solid #ded2c1;border-radius:12px;padding:14px;box-shadow:0 6px 18px rgba(54,42,30,.04)}.dynasty-band{border-radius:9px;overflow:hidden;box-shadow:inset 0 0 0 1px rgba(255,255,255,.3)}.insight-sec{scroll-margin-top:122px} -@media(max-width:900px){.v2-journey-grid{grid-template-columns:1fr}.header-inner{grid-template-columns:1fr;grid-template-areas:'brand' 'stat' 'tabs';row-gap:6px}.header-stat{justify-self:start}.tabs{padding-top:8px}} -@media(max-width:640px){body{background-attachment:scroll}.header-inner{align-items:flex-start}.tabs{scroll-snap-type:x proximity;padding-bottom:4px;gap:8px}.nav-cluster{scroll-snap-align:start}.nav-cluster-label{font-size:8px}.tabs button{flex:0 0 auto;scroll-snap-align:start}.shell{padding-top:20px}.section-head h2::after{margin-top:5px}.panel{border-radius:11px}.metric:hover,.distribution-stat:hover,.location-card:hover{transform:none}.v2-journey-head{align-items:flex-start;flex-direction:column;gap:2px}.v2-journey-card{padding:13px 14px}.timeline{padding:13px 12px 13px 18px}.chronicle-wrap{padding:10px}.insight-sec{scroll-margin-top:142px}} +/* ===== Classic UI compatibility layer ===== + Keep the original pre-V1 visual language. This file now carries only + non-invasive accessibility behavior; app.css remains the visible skin. */ +.skip-link{position:fixed;left:16px;top:10px;z-index:100;transform:translateY(-180%);padding:8px 12px;border-radius:6px;background:#fffdf8;color:var(--accent);border:1px solid #cdbd9e;font-weight:700;text-decoration:none}.skip-link:focus{transform:translateY(0)} +button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid #b88b35;outline-offset:2px} @media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}} @media print{.skip-link{display:none!important}} From 23e3ff0a266dd1fee9d12feb762e5d847e0ff3af Mon Sep 17 00:00:00 2001 From: CochraneK <48022373+CochraneK@users.noreply.github.com> Date: Mon, 14 Sep 2026 12:11:51 +0800 Subject: [PATCH 02/14] ui: disable visible experience overlays --- web/css/experience.css | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/web/css/experience.css b/web/css/experience.css index a2fd3d1..6d57526 100644 --- a/web/css/experience.css +++ b/web/css/experience.css @@ -1,8 +1,2 @@ -/* ===== V3 product experience components ===== */ -.header-stat{display:flex;align-items:center;justify-content:flex-end;gap:10px;flex-wrap:wrap}.command-trigger,.command-inline{display:inline-flex;align-items:center;gap:7px;border:1px solid rgba(232,216,190,.28);border-radius:999px;background:rgba(255,255,255,.07);color:#f1e6d8;padding:5px 9px;font-size:11px;line-height:1;white-space:nowrap}.command-trigger:hover{background:rgba(255,255,255,.12);border-color:#c5a05f}.command-trigger kbd,.command-inline kbd,.command-foot kbd,.command-input-wrap kbd{font:inherit;font-size:9px;border:1px solid currentColor;border-radius:4px;padding:2px 5px;opacity:.72}.command-inline{background:#3a3129;color:#f7efe2;border-color:#5b4b3d;padding:7px 10px}.command-inline:hover{border-color:var(--gold);color:#fff} -.command-shell[hidden]{display:none}.command-shell{position:fixed;inset:0;z-index:120;display:flex;align-items:flex-start;justify-content:center;padding:10vh 18px 24px;background:rgba(25,20,16,.56);backdrop-filter:blur(6px)}body.command-open{overflow:hidden}.command-panel{width:min(760px,100%);max-height:min(74vh,720px);display:flex;flex-direction:column;overflow:hidden;background:#fffdf8;border:1px solid #cdbfae;border-radius:16px;box-shadow:0 28px 90px rgba(20,15,10,.38)}.command-head{display:flex;justify-content:space-between;gap:16px;align-items:start;padding:16px 18px 10px}.command-head>div{display:flex;flex-direction:column;gap:2px}.command-head strong{font-family:'Songti SC','STSong','SimSun','Noto Serif CJK SC',serif;font-size:19px;color:#302820}.command-head span{font-size:11px;color:var(--muted)}.command-close{border:0;background:#f1ebe2;color:#64594e;border-radius:7px;width:30px;height:30px;font-size:18px;line-height:1}.command-close:hover{background:#e7ddcf;color:var(--accent)}.command-input-wrap{display:flex;align-items:center;gap:9px;margin:0 14px 10px;padding:10px 12px;background:#f8f3ea;border:1px solid #d9cdbc;border-radius:11px}.command-search-icon{font-size:19px;color:var(--accent)}.command-input-wrap input{flex:1;min-width:0;border:0;outline:0;background:transparent;color:var(--ink);font-size:15px}.command-input-wrap kbd{color:#81776d}.command-results{overflow:auto;padding:0 8px 8px}.command-item{width:100%;display:grid;grid-template-columns:54px minmax(0,1fr) 24px;gap:9px;align-items:center;border:0;border-radius:9px;background:transparent;padding:9px 10px;text-align:left;color:var(--ink)}.command-item:hover,.command-item.active{background:#f2ead9}.command-kind{justify-self:start;border-radius:999px;background:#ece5d7;color:#7b6542;padding:2px 7px;font-size:10px}.command-copy{display:flex;flex-direction:column;min-width:0}.command-copy strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px}.command-copy small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--muted);font-size:10px}.command-enter{color:#9a8d80}.command-empty{padding:28px;text-align:center;color:var(--muted);font-size:12px}.command-foot{display:flex;justify-content:space-between;gap:12px;border-top:1px solid #e6ddd0;padding:8px 16px;color:#8a8076;font-size:10px} -.v3-story{margin:0 0 18px}.v3-story-head{display:flex;justify-content:space-between;gap:18px;align-items:start;margin-bottom:14px}.v3-eyebrow{display:block;color:var(--gold);font-size:9px;font-weight:800;letter-spacing:.18em}.v3-story-head h3{margin:3px 0 3px;font-family:'Songti SC','STSong','SimSun','Noto Serif CJK SC',serif;font-size:20px;color:#342c25}.v3-story-head p{margin:0;color:var(--muted);font-size:11px}.v3-story-grid{display:grid;grid-template-columns:1fr 1.15fr;gap:12px}.v3-story-card{background:linear-gradient(145deg,#fffdf9,#f8f2e7);border:1px solid #ded2c0;border-radius:11px;padding:14px 15px}.v3-story-card>strong{display:block;color:#44382e;margin-bottom:9px}.v3-story-card p{margin:8px 0 0;color:var(--muted);font-size:10.5px}.v3-progress-row{display:flex;justify-content:space-between;gap:10px;margin-top:7px;color:#655a50;font-size:11px}.v3-progress-row b{color:var(--accent);font-variant-numeric:tabular-nums}.v3-progress{height:6px;background:#eae1d4;border-radius:999px;overflow:hidden}.v3-progress i{display:block;height:100%;background:linear-gradient(90deg,var(--accent),var(--gold));border-radius:inherit}.v3-signal-list{margin:0;display:grid;grid-template-columns:1fr 1fr;gap:7px 12px}.v3-signal-list div{min-width:0;border-bottom:1px dashed #e5dac9;padding-bottom:6px}.v3-signal-list dt{color:var(--muted);font-size:9px}.v3-signal-list dd{margin:1px 0 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#44382e;font-size:11.5px;font-weight:700}.v3-story-actions{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:12px;color:var(--muted);font-size:10px}.v3-story-actions button{border:1px solid #d7c9b5;border-radius:999px;background:#fffdf8;color:#67584a;padding:4px 9px;font-size:10px}.v3-story-actions button:hover{border-color:var(--gold);color:var(--accent)} -.v3-graph-reader{margin:12px 0;padding:13px 14px;border:1px solid #ded2c1;border-radius:11px;background:linear-gradient(145deg,#fbf7ef,#fffdf9)}.v3-graph-reader-head{display:flex;align-items:start;justify-content:space-between;gap:14px}.v3-graph-reader-head strong{display:block;margin:2px 0;color:#3d332b;font-size:14px}.v3-graph-reader-head p{margin:0;max-width:78ch;color:var(--muted);font-size:10.5px;line-height:1.6}.v3-graph-tools{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:10px}.v3-graph-tools label{display:flex;align-items:center;gap:7px;color:var(--muted);font-size:10px;flex:1;min-width:240px}.v3-graph-tools input{flex:1;min-width:150px;border:1px solid #d2c5b3;border-radius:7px;background:#fff;padding:6px 8px;color:var(--ink)}.v3-graph-status{color:#806f5d;font-size:10px}.v3-hubs{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:10px}.v3-hubs>span{color:var(--muted);font-size:10px;margin-right:2px}.v3-hubs button{display:inline-flex;align-items:baseline;gap:5px;border:1px solid #d9cdbc;border-radius:999px;background:#fffdf8;color:#584a3d;padding:4px 9px}.v3-hubs button:hover{border-color:var(--accent);color:var(--accent)}.v3-hubs small{color:#9a8c7d;font-size:8px}.v3-graph-explain{display:flex;gap:12px;flex-wrap:wrap;margin-top:10px;padding-top:9px;border-top:1px dashed #ded3c4;color:#766a5f;font-size:9.5px}.v3-graph-explain span{display:inline-flex;align-items:center;gap:4px}.v3-dot{width:8px;height:8px;border-radius:50%;background:var(--accent);display:inline-block}.v3-dot.focus{box-shadow:0 0 0 3px rgba(141,48,37,.16)}.v3-line{width:16px;height:2px;background:var(--blue);display:inline-block} -@media(max-width:900px){.v3-story-grid{grid-template-columns:1fr}.v3-signal-list{grid-template-columns:1fr 1fr}.header-stat{justify-content:flex-start}} -@media(max-width:640px){.command-shell{padding:5vh 8px 10px}.command-panel{max-height:88vh;border-radius:12px}.command-foot{display:none}.command-item{grid-template-columns:48px minmax(0,1fr) 18px}.command-trigger span{display:none}.v3-story-head{flex-direction:column;gap:8px}.v3-signal-list{grid-template-columns:1fr}.v3-graph-reader-head{flex-direction:column}.v3-graph-tools label{min-width:100%;align-items:flex-start;flex-direction:column}.v3-graph-tools input{width:100%}} -@media print{.command-trigger,.command-shell,.command-inline,.v3-graph-reader{display:none!important}} +/* Classic UI mode: no visible V2/V3 product-experience overlays. + The online delivery/search infrastructure remains available underneath. */ From b2be9f3d17b2e539d1e903ea643d0cc2830ec2d5 Mon Sep 17 00:00:00 2001 From: CochraneK <48022373+CochraneK@users.noreply.github.com> Date: Mon, 14 Sep 2026 12:12:04 +0800 Subject: [PATCH 03/14] ui: disable progressive visible UI enhancements --- web/js/experience.js | 200 +------------------------------------------ 1 file changed, 4 insertions(+), 196 deletions(-) diff --git a/web/js/experience.js b/web/js/experience.js index 4ed9fc8..e1ee821 100644 --- a/web/js/experience.js +++ b/web/js/experience.js @@ -1,197 +1,5 @@ -/* ===== V3 产品体验层:全局搜索 / 首页叙事 / 图谱阅读器 ===== - 这是 app.js 之后加载的渐进增强层:只调用现有 DATA / setView / show* / graph helpers, - 不复制业务渲染器。删除本文件后,12 个原视图仍应保持完整可用。 */ -(function(){ -'use strict'; -if(typeof DATA==='undefined'||typeof setView!=='function')return; - -const VIEWS=[ - ['overview','总览','知识库 首页 全局'],['distribution','分布','章节 密度 抽取 诊断'], - ['timeline','时间轴','年份 事件 时间'],['dynasty','帝王','皇帝 年号 在位'],['chronicle','年谱','人物 生平 年代'], - ['characters','人物','角色 人物卡'],['locations','地点','地名 地图 空间'],['events','事件','事件 索引'],['relations','关系','人物关系 网络'], - ['visuals','图谱','网络 热力图 可视化'],['map','地图','圣地巡礼 郑和 航线'],['insight','洞察','跨学科 报告'] -]; -const TYPE_LABEL={view:'视图',person:'人物',place:'地点',event:'事件'}; -const safe=v=>String(v==null?'':v).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); -const norm=v=>String(v==null?'':v).trim().toLowerCase(); -const pct=(a,b)=>b?Math.round(a*100/b):0; - -/* ------------------------------ 全局快捷搜索 ------------------------------ */ -/* V9_SHARDED_CHARACTER_DETAIL_COMMAND_DATA */ -let commandRows=null,commandRowsMode='',commandResults=[],commandActive=0,commandLastFocus=null; -function commandHasFullData(){return typeof window.__MING_ENSURE_FULL_DATA!=='function'||window.__MING_FULL_DATA_READY===true;} -function commandHasSearchIndex(){return window.__MING_SEARCH_INDEX_READY===true&&window.__MING_SEARCH_INDEX__&&Array.isArray(window.__MING_SEARCH_INDEX__.rows);} -function commandMode(){return commandHasFullData()?'full':(commandHasSearchIndex()?'search':'boot');} -function buildCommandRows(){ - const mode=commandMode(); - if(commandRows&&commandRowsMode===mode)return commandRows; - const rows=[]; - VIEWS.forEach(([id,title,keys])=>rows.push({kind:'view',id,title,meta:'切换到 '+title,search:norm(title+' '+keys)})); - if(mode==='full'){ - (DATA.characters||[]).forEach(x=>{ - const aliases=(x.aliases||[]).join(' '),p=x.profile||{}; - rows.push({kind:'person',id:x.name,title:x.name,meta:[x.role,(p.factions||[]).join('、'),x.life].filter(Boolean).join(' · '),search:norm([x.name,aliases,x.role,x.faction,p.raw,(p.factions||[]).join(' ')].join(' ')),raw:x}); - }); - (DATA.locations||[]).forEach(x=>rows.push({kind:'place',id:x.ancient,title:x.ancient,meta:[x.modern,x.region].filter(Boolean).join(' · '),search:norm([x.ancient,x.modern,x.region,(x.altNames||[]).join(' '),(x.mentionedAs||[]).join(' ')].join(' ')),raw:x})); - (DATA.events||[]).forEach(x=>rows.push({kind:'event',id:x.id,title:x.name,meta:[x.year||'年份待考',x.category,x.location].filter(Boolean).join(' · '),search:norm([x.name,x.year,x.category,x.type,x.location,(x.participants||[]).join(' ')].join(' ')),raw:x})); - }else if(mode==='search'){ - (window.__MING_SEARCH_INDEX__.rows||[]).forEach(x=>rows.push({kind:x.kind,id:x.id,title:x.title,meta:x.meta||'',search:norm(x.search||x.title)})); - } - commandRows=rows;commandRowsMode=mode; - return rows; -} -function commandScore(row,q){ - const t=norm(row.title);if(!q)return row.kind==='view'?90:(row.kind==='person'?30:10); - if(t===q)return 140; - if(t.startsWith(q))return 120-Math.min(t.length-q.length,20); - const i=row.search.indexOf(q);return i<0?-1:90-Math.min(i,45); -} -function commandShell(){ - let shell=document.getElementById('commandPalette'); - if(shell)return shell; - shell=document.createElement('div');shell.id='commandPalette';shell.className='command-shell';shell.hidden=true; - shell.innerHTML=''; - document.body.appendChild(shell); - shell.addEventListener('click',e=>{if(e.target===shell||e.target.closest('.command-close'))closeCommand();const b=e.target.closest('[data-command-index]');if(b){const i=Number(b.dataset.commandIndex);if(commandResults[i])runCommand(commandResults[i]);}}); - const input=shell.querySelector('#commandInput'); - input.addEventListener('input',()=>renderCommand(input.value)); - input.addEventListener('keydown',e=>{ - if(e.key==='ArrowDown'||e.key==='ArrowUp'){e.preventDefault();moveCommand(e.key==='ArrowDown'?1:-1);return;} - if(e.key==='Enter'){e.preventDefault();const r=commandResults[commandActive];if(r)runCommand(r);} - }); - return shell; -} -function renderCommand(raw){ - const q=norm(raw),rows=buildCommandRows(); - commandResults=rows.map(r=>[commandScore(r,q),r]).filter(x=>x[0]>=0).sort((a,b)=>b[0]-a[0]||a[1].title.localeCompare(b[1].title,'zh-CN')).slice(0,12).map(x=>x[1]); - commandActive=Math.min(commandActive,Math.max(0,commandResults.length-1)); - const host=document.getElementById('commandResults');if(!host)return; - host.innerHTML=commandResults.length?commandResults.map((r,i)=>'').join(''):'
没有匹配结果。可尝试人物别名、古地名、事件名或视图名称。
'; - const input=document.getElementById('commandInput');if(input)input.setAttribute('aria-activedescendant',commandResults.length?'commandOption'+commandActive:''); -} -function moveCommand(delta){ - if(!commandResults.length)return;commandActive=(commandActive+delta+commandResults.length)%commandResults.length;renderCommand(document.getElementById('commandInput').value);const el=document.getElementById('commandOption'+commandActive);if(el)el.scrollIntoView({block:'nearest'}); -} -function openCommand(seed){ - const shell=commandShell();commandLastFocus=document.activeElement;shell.hidden=false;document.body.classList.add('command-open');document.querySelectorAll('.command-trigger').forEach(b=>b.setAttribute('aria-expanded','true')); - const input=shell.querySelector('#commandInput');input.value=seed||'';commandActive=0;renderCommand(input.value); - if(!commandHasFullData()&&!commandHasSearchIndex()&&typeof window.__MING_ENSURE_SEARCH_INDEX==='function'){ - window.__MING_ENSURE_SEARCH_INDEX('command').then(()=>{commandRows=null;commandRowsMode='';renderCommand(input.value);}).catch(err=>{ - const host=document.getElementById('commandResults');if(host)host.innerHTML='
搜索索引加载失败,可稍后重试。
'; - if(typeof failBar==='function')failBar('search-index','搜索索引加载失败:'+String((err&&err.message)||err),{level:'warn'}); - }); - } - requestAnimationFrame(()=>input.focus()); -} -function closeCommand(){ - const shell=document.getElementById('commandPalette');if(!shell||shell.hidden)return;shell.hidden=true;document.body.classList.remove('command-open');document.querySelectorAll('.command-trigger').forEach(b=>b.setAttribute('aria-expanded','false'));if(commandLastFocus&&document.contains(commandLastFocus))try{commandLastFocus.focus()}catch(_){} -} -function executeEntityCommand(r){ - if(r.kind==='person'){ - setView('characters');state.charQuery=r.title;state.charPage=1;if(typeof rerender==='function')rerender('characters');if(typeof locatePersonCard==='function')locatePersonCard(r.title);if(typeof showPerson==='function')showPerson(r.title);return; - } - if(r.kind==='place'){ - const x=(DATA.locations||[]).find(v=>v.ancient===r.id)||(DATA.locations||[]).find(v=>v.ancient===r.title);setView('locations');if(x&&typeof showLocation==='function')showLocation(x);return; - } - if(r.kind==='event'){ - const x=(DATA.events||[]).find(v=>v.id===r.id)||(DATA.events||[]).find(v=>v.name===r.title);setView('events');if(x&&typeof showEvent==='function')showEvent(x); - } -} -function runCommand(r){ - closeCommand(); - if(r.kind==='view'){ - setView(r.id);if(typeof writeHash==='function')writeHash({view:r.id,person:null,event:null,place:null,detail:null});return; - } - const execute=()=>{commandRows=null;commandRowsMode='';executeEntityCommand(r);}; - if(typeof window.__MING_ENSURE_ENTITY_DATA==='function'){ - window.__MING_ENSURE_ENTITY_DATA(r.kind,'command-result:'+r.kind,r.id).then(execute).catch(err=>{ - if(typeof failBar==='function')failBar('entity-data','实体数据加载失败:'+String((err&&err.message)||err),{level:'error',retry:()=>location.reload(),retryLabel:'重新加载'}); - }); - return; - } - if(commandHasFullData()){execute();return;} - if(typeof window.__MING_ENSURE_FULL_DATA==='function'){ - window.__MING_ENSURE_FULL_DATA('command-result:'+r.kind).then(execute).catch(err=>{ - if(typeof failBar==='function')failBar('full-data','完整知识库加载失败:'+String((err&&err.message)||err),{level:'error',retry:()=>location.reload(),retryLabel:'重新加载'}); - }); - return; - } - execute(); -} -function ensureCommandTrigger(){ - const stat=document.getElementById('headerStat');if(!stat||stat.querySelector('.command-trigger'))return; - const b=document.createElement('button');b.type='button';b.className='command-trigger';b.setAttribute('aria-haspopup','dialog');b.setAttribute('aria-controls','commandPalette');b.setAttribute('aria-expanded','false');b.innerHTML='全局搜索⌘/Ctrl K';b.addEventListener('click',()=>openCommand(''));stat.appendChild(b); -} -document.addEventListener('keydown',e=>{ - const tag=(e.target&&e.target.tagName)||'',editing=/^(INPUT|TEXTAREA|SELECT)$/.test(tag)||(e.target&&e.target.isContentEditable); - if((e.metaKey||e.ctrlKey)&&e.key.toLowerCase()==='k'){e.preventDefault();openCommand('');return;} - if(e.key==='/'&&!editing&&document.getElementById('commandPalette')?.hidden!==false){e.preventDefault();openCommand('');return;} - if(e.key==='Escape'&&document.getElementById('commandPalette')?.hidden===false){e.preventDefault();closeCommand();} -}); -const headerStat=document.getElementById('headerStat');if(headerStat&&window.MutationObserver)new MutationObserver(ensureCommandTrigger).observe(headerStat,{childList:true});ensureCommandTrigger();commandShell(); - -/* ------------------------------ 首页数据叙事 ------------------------------ */ -function maxEntry(obj){return Object.entries(obj||{}).sort((a,b)=>b[1]-a[1])[0]||['暂无',0];} -function storyHTML(){ - const lp=pct(DATA.metrics.locatedLocations,DATA.metrics.locations),tp=pct(DATA.metrics.timedEvents,DATA.metrics.events); - const graph=DATA.relationGraphFull||{nodes:[]};const hub=[...(graph.nodes||[])].sort((a,b)=>(b.degree||0)-(a.degree||0))[0]; - const chapters=(DATA.distribution&&DATA.distribution.chapters)||[];const dense=[...chapters].sort((a,b)=>(b.density||0)-(a.density||0))[0]; - const ev=maxEntry(DATA.cleaning&&DATA.cleaning.event_categories),rel=maxEntry(DATA.cleaning&&DATA.cleaning.relation_categories); - return '
DATA STORY

先读懂这份知识图谱,再进入细节

下面是数据覆盖与结构信号,不是人物重要性或历史价值排名。

'+ - '
覆盖度
地点已定位'+lp+'%
事件可纪年'+tp+'%

'+DATA.metrics.locatedLocations+'/'+DATA.metrics.locations+' 个地点有坐标;'+DATA.metrics.timedEvents+'/'+DATA.metrics.events+' 件事件进入数值时间轴。

'+ - '
结构信号
关系网络入口
'+(hub?safe(hub.name)+' · '+(hub.degree||0)+' 条关系':'暂无')+'
抽取密度峰值
'+(dense?safe(dense.title)+' · '+dense.density+'/万字':'暂无')+'
最多事件类别
'+safe(ev[0])+' · '+ev[1]+' 件
最多关系类别
'+safe(rel[0])+' · '+rel[1]+' 条
'+ - '
建议阅读:
'; -} -function ensureStory(){ - const host=document.getElementById('overview');if(!host||host.querySelector('.v3-story')||!host.querySelector('.summary-hero'))return; - const box=document.createElement('div');box.innerHTML=storyHTML();const story=box.firstElementChild;const anchor=host.querySelector('.v2-journeys')||host.querySelector('.summary-hero');anchor.insertAdjacentElement('afterend',story); - story.querySelector('[data-v3-command]').addEventListener('click',()=>openCommand('')); -} -const overview=document.getElementById('overview');if(overview&&window.MutationObserver)new MutationObserver(()=>requestAnimationFrame(ensureStory)).observe(overview,{childList:true});ensureStory(); - -/* ------------------------------ 图谱阅读器 / 聚焦降噪 ------------------------------ */ -function readerMode(){ - const select=document.getElementById('netMode'),mode=(select&&select.value)||state.netMode; - return mode==='full'||mode==='entity'?mode:'ego'; -} -function graphForReader(mode){const m=mode||readerMode();return m==='entity'&&DATA.relationGraphEntities?DATA.relationGraphEntities:DATA.relationGraphFull;} -function graphReaderHTML(mode){ - const m=mode||readerMode(),g=graphForReader(m)||{nodes:[],links:[]},entity=m==='entity';const hubs=[...(g.nodes||[])].sort((a,b)=>(b.degree||0)-(a.degree||0)||String(a.name).localeCompare(String(b.name),'zh-CN')).slice(0,8); - return '
GRAPH READER'+(entity?'实体总图':'人物总图')+'怎么读

节点越大,关系度数越高;线条颜色表示关系类别。点击节点后,无关节点自动降到 12% 不透明度,用邻域聚焦降低视觉噪声。

'+ - '
'+(g.nodes||[]).length+' 节点 · '+(g.links||[]).length+' 关系
'+ - '
高连接入口'+hubs.map(n=>'').join('')+'
'+ - '
节点大小 = 关系度数线颜色 = 关系类别点击 = 只突出一跳邻域'+(entity?'实体图包含人物、地点、机构、政权;节点总数不能读作人物数。':'人物图仅保留人物↔人物关系,适合观察人物关系结构。')+'
'; -} -function focusGraphName(raw,root){ - const q=norm(raw),g=graphForReader();if(!q||!g)return false;const nodes=g.nodes||[];const n=nodes.find(x=>norm(x.name)===q)||nodes.find(x=>norm(x.name).startsWith(q))||nodes.find(x=>norm(x.name).includes(q)); - const status=root&&root.querySelector('.v3-graph-status');if(!n){if(status)status.textContent='没有找到“'+raw+'”';return false;}if(typeof showFullNode==='function')showFullNode(n.name);if(status)status.textContent='已聚焦 '+n.name+' · '+(n.degree||0)+' 条关系';return true; -} -function bindGraphReader(root){ - if(root.dataset.bound==='1')return;root.dataset.bound='1'; - root.addEventListener('click',e=>{const f=e.target.closest('[data-v3-focus]');if(f){focusGraphName(f.dataset.v3Focus,root);return;}if(e.target.closest('[data-v3-reset]')){if(typeof resetFullHighlight==='function')resetFullHighlight();const s=root.querySelector('.v3-graph-status'),g=graphForReader();if(s&&g)s.textContent=(g.nodes||[]).length+' 节点 · '+(g.links||[]).length+' 关系';}}); - const input=root.querySelector('[data-v3-node-search]');if(input)input.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();focusGraphName(input.value,root);}}); -} -function ensureGraphReader(){ - const host=document.getElementById('visuals');if(!host)return false;let reader=host.querySelector('.v3-graph-reader');const mode=readerMode(); - if(mode==='ego'){if(reader)reader.remove();return false;} - const full=host.querySelector('#fullNet');if(!full)return false; - if(reader&&reader.dataset.mode!==mode){reader.remove();reader=null;} - if(!reader){const box=document.createElement('div');box.innerHTML=graphReaderHTML(mode);reader=box.firstElementChild;full.parentNode.insertBefore(reader,full);} - bindGraphReader(reader);return true; -} -let graphRetryTimer=null; -function scheduleGraphReader(){ - requestAnimationFrame(ensureGraphReader); - clearTimeout(graphRetryTimer);graphRetryTimer=setTimeout(ensureGraphReader,180); -} -const visuals=document.getElementById('visuals');if(visuals&&window.MutationObserver)new MutationObserver(scheduleGraphReader).observe(visuals,{childList:true,subtree:true}); -document.addEventListener('change',e=>{if(e.target&&e.target.id==='netMode')setTimeout(scheduleGraphReader,0)}); -window.addEventListener('hashchange',()=>setTimeout(scheduleGraphReader,40)); -scheduleGraphReader();setTimeout(ensureGraphReader,500); - +/* Classic UI mode. + V2/V3's grouped navigation, homepage story, command-palette trigger and graph-reader + overlays are intentionally disabled so the original pre-V1 frontend remains visible. + Modern delivery/data-loader/lazy-data infrastructure is kept in separate files. */ window.__MING_EXPERIENCE_READY=true; -})(); From 477a947193c0c30a9931491949de12c5a8843ea0 Mon Sep 17 00:00:00 2001 From: CochraneK <48022373+CochraneK@users.noreply.github.com> Date: Mon, 14 Sep 2026 12:12:28 +0800 Subject: [PATCH 04/14] ui: restore original navigation and page skeleton --- web/template/index.html | 268 +++------------------------------------- 1 file changed, 14 insertions(+), 254 deletions(-) diff --git a/web/template/index.html b/web/template/index.html index b498ee2..ecd12e4 100644 --- a/web/template/index.html +++ b/web/template/index.html @@ -11,29 +11,11 @@ /*{{INLINE_CSS}}*/ - + \ No newline at end of file From 10946e98a3367009ac9e3c67593e6cc69b081031 Mon Sep 17 00:00:00 2001 From: CochraneK <48022373+CochraneK@users.noreply.github.com> Date: Mon, 14 Sep 2026 12:12:44 +0800 Subject: [PATCH 05/14] test: lock classic UI without rolling back delivery --- tests/test_experience.py | 52 ++++++++++++++-------------------------- 1 file changed, 18 insertions(+), 34 deletions(-) diff --git a/tests/test_experience.py b/tests/test_experience.py index 594bba4..dace860 100644 --- a/tests/test_experience.py +++ b/tests/test_experience.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""V3 产品体验增强层:单一源、无侵入与关键交互钩子。""" +"""Classic UI:体验镜像仍可同步,但不再注入可见 V2/V3 覆盖层。""" from __future__ import annotations import build as B @@ -19,47 +19,31 @@ def test_experience_sources_sync_idempotently_into_template(): SE.SCRIPT_START, SE.SCRIPT_END, ): assert converted.count(token) == 1, token - # 产品增强层必须在主 app.js 之后执行,才能复用 setView/show*/graph helpers。 assert converted.index(SE.SCRIPT_ATTR) > converted.index("/*{{INLINE_JS}}*/") assert SE.render_template(converted, css, js) == converted -def test_command_palette_covers_views_and_three_entity_types(): +def test_classic_mode_disables_visible_v2_v3_overlays(): + css = SE.CSS_SOURCE.read_text(encoding="utf-8") js = SE.JS_SOURCE.read_text(encoding="utf-8") - for token in ( - "全局搜索", "⌘/Ctrl K", "DATA.characters", "DATA.locations", "DATA.events", - "aliases", "showPerson", "showLocation", "showEvent", "window.__MING_EXPERIENCE_READY=true", + assert "Classic UI mode" in css + assert "Classic UI mode" in js + assert "window.__MING_EXPERIENCE_READY=true" in js + for forbidden in ( + "commandPalette", "v3-story", "v3-graph-reader", "command-trigger", + "function renderCharacters", "function renderLocations", "function renderEvents", ): - assert token in js, token - # 渐进增强层不得复制核心业务渲染器。 - for forbidden in ("function renderCharacters", "function renderLocations", "function renderEvents", "function setView"): assert forbidden not in js, forbidden + for forbidden in (".command-shell", ".v3-story-grid", ".v3-graph-reader", ".command-trigger"): + assert forbidden not in css, forbidden -def test_home_story_is_metric_driven_not_hardcoded(): - js = SE.JS_SOURCE.read_text(encoding="utf-8") - assert "DATA.metrics.locatedLocations" in js - assert "DATA.metrics.timedEvents" in js - assert "DATA.relationGraphFull" in js - assert "DATA.distribution" in js - assert "数据覆盖与结构信号" in js - # 旧 README 曾出现过 581 地点;体验层不能重新引入这种会漂移的硬编码口径。 - assert "581" not in js - - -def test_graph_reader_reuses_existing_focus_mechanism(): - js = SE.JS_SOURCE.read_text(encoding="utf-8") - for token in ( - "v3-graph-reader", "showFullNode", "resetFullHighlight", "12% 不透明度", - "高连接入口", "实体图包含人物、地点、机构、政权", - ): - assert token in js, token - - -def test_experience_css_has_responsive_and_print_guards(): - css = SE.CSS_SOURCE.read_text(encoding="utf-8") +def test_modern_delivery_remains_outside_experience_layer(): + loader = B.DATA_LOADER_JS_PATH.read_text(encoding="utf-8") + lazy = B.LAZY_DATA_JS_PATH.read_text(encoding="utf-8") for token in ( - ".command-shell", ".v3-story-grid", ".v3-graph-reader", - "@media(max-width:640px)", "@media print", + "__MING_ENSURE_DATA_CHUNKS", "character-detail-", "location-detail-", + "event-detail-", "relation-meta", "lifespans", ): - assert token in css, token + assert token in loader, token + assert "__MING_ENSURE_VIEW_DATA" in lazy From 6ffbd2377afde6e2eb9435c008f81971dc01db67 Mon Sep 17 00:00:00 2001 From: CochraneK <48022373+CochraneK@users.noreply.github.com> Date: Mon, 14 Sep 2026 12:12:59 +0800 Subject: [PATCH 06/14] test: restore classic navigation and theme contract --- tests/test_visual_polish.py | 46 ++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/tests/test_visual_polish.py b/tests/test_visual_polish.py index 05a1d8b..9e9eb28 100644 --- a/tests/test_visual_polish.py +++ b/tests/test_visual_polish.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""视觉 / 可访问性与 web target 资源完整性的回归检查。""" +"""Classic UI / 可访问性与 web target 资源完整性的回归检查。""" from __future__ import annotations import tempfile @@ -10,14 +10,14 @@ import sync_theme as ST -def test_visual_polish_and_a11y_hooks_exist(): +def test_classic_ui_and_a11y_hooks_exist(): skeleton = B.G.TEMPLATE_PATH.read_text(encoding="utf-8") assert 'name="theme-color"' in skeleton assert 'name="color-scheme"' in skeleton assert 'class="skip-link" href="#mainContent"' in skeleton assert '
' in skeleton assert 'class="muted app-footer"' in skeleton - assert "Visual polish 2026-09" in skeleton + assert "Classic UI compatibility layer" in skeleton assert ":focus-visible" in skeleton assert "prefers-reduced-motion:reduce" in skeleton @@ -33,22 +33,32 @@ def test_theme_css_is_single_source_for_template_mirror(): assert ST.render_template(converted, theme) == converted -def test_v2_information_architecture_hooks_exist(): +def test_original_twelve_button_navigation_is_restored(): skeleton = B.G.TEMPLATE_PATH.read_text(encoding="utf-8") - expected = ("overview","distribution","timeline","dynasty","chronicle","characters","locations","events","relations","visuals","map","insight") - for view in expected: assert skeleton.count('data-view="%s"' % view) == 1, view - assert skeleton.count('class="nav-cluster"') == 3 - for label in ("全局叙事", "实体索引", "探索分析"): assert 'aria-label="%s"' % label in skeleton - assert "V2 首页探索路径" in skeleton - assert "MutationObserver" in skeleton - assert "function setView" not in skeleton + expected = ( + "overview", "distribution", "visuals", "locations", "map", "characters", + "events", "relations", "timeline", "dynasty", "chronicle", "insight", + ) + positions = [] + for view in expected: + token = 'data-view="%s"' % view + assert skeleton.count(token) == 1, view + positions.append(skeleton.index(token)) + assert positions == sorted(positions) + assert 'class="nav-cluster"' not in skeleton + for label in ("全局叙事", "实体索引", "探索分析", "V2 首页探索路径"): + assert label not in skeleton -def test_v2_visual_language_is_shared_across_views(): +def test_classic_theme_does_not_reskin_base_app_css(): theme = ST.THEME.read_text(encoding="utf-8") - for token in (".v2-journey-grid", ".visuals-panel::before", ".map-wrap::before", ".timeline{", ".chronicle-wrap{", ".dynasty-band{"): - assert token in theme, token - assert "scroll-margin-top:122px" in theme + for forbidden in ( + ".nav-cluster", ".v2-journey-grid", ".visuals-panel::before", + ".map-wrap::before", "radial-gradient", "backdrop-filter", + ): + assert forbidden not in theme, forbidden + assert ".skip-link" in theme + assert ":focus-visible" in theme def test_web_target_copies_service_worker_and_reports_all_assets(): @@ -69,10 +79,10 @@ def test_web_target_copies_service_worker_and_reports_all_assets(): assert written["sw.js"] == len(B.SW_PATH.read_bytes()) -def test_service_worker_cache_bumped_for_frontend_change(): +def test_service_worker_cache_bumped_for_classic_ui_restore(): sw = B.SW_PATH.read_text(encoding="utf-8") - assert "CACHE_PREFIX + 'v22'" in sw - assert "V13" in sw + assert "CACHE_PREFIX + 'v23'" in sw + assert "classic UI" in sw assert "relation-meta" in sw assert "event-detail" in sw assert "lifespans" in sw From b69cf7fa8fa55720ce0f53de149c7dc8eaf6286c Mon Sep 17 00:00:00 2001 From: CochraneK <48022373+CochraneK@users.noreply.github.com> Date: Mon, 14 Sep 2026 12:13:14 +0800 Subject: [PATCH 07/14] ui: bump cache for classic UI restore --- sw.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sw.js b/sw.js index 2f69f2e..4d16df8 100644 --- a/sw.js +++ b/sw.js @@ -8,8 +8,9 @@ // v20:V11 时间分层:lifespans 从 timeline 独立;年谱只缓存 characters + lifespans。 // v21:V12 事件分层:events 仅摘要,单事件详情进入 8 个 event-detail-*;时间轴/帝王只缓存 time。 // v22:V13 关系分层:relations 仅可见 core;relation-meta 只在完整 DATA 路径按需缓存。 +// v23:恢复 pre-V1 classic UI;只换视觉/导航层,现代分片交付与懒加载保持不变。 const CACHE_PREFIX = 'ming-report-'; -const CACHE = CACHE_PREFIX + 'v22'; +const CACHE = CACHE_PREFIX + 'v23'; self.addEventListener('install', () => { self.skipWaiting(); }); self.addEventListener('activate', (event) => { @@ -26,8 +27,8 @@ self.addEventListener('fetch', (event) => { const url = new URL(req.url); if (url.origin !== self.location.origin) return; - // V13 仍只缓存用户真实走过的路径:关系索引只取 relations core;full DATA 才取 relation-meta; - // 事件、人物、地点详情继续只缓存被真正访问的对应 shard。 + // classic UI 只改变可见外观;V13 交付策略保持:关系索引只取 relations core; + // full DATA 才取 relation-meta;事件、人物、地点详情继续只缓存真正访问的对应 shard。 event.respondWith((async () => { const cache = await caches.open(CACHE); const cached = await cache.match(req); From b0f2137be440f5e3284e7aea3709bc61b12a5b05 Mon Sep 17 00:00:00 2001 From: CochraneK <48022373+CochraneK@users.noreply.github.com> Date: Mon, 14 Sep 2026 12:13:45 +0800 Subject: [PATCH 08/14] test: replace V3 browser checks with classic UI checks --- .dump/_experience_browser_check.py | 57 +++++++++++------------------- 1 file changed, 21 insertions(+), 36 deletions(-) diff --git a/.dump/_experience_browser_check.py b/.dump/_experience_browser_check.py index f2edbce..e87be85 100644 --- a/.dump/_experience_browser_check.py +++ b/.dump/_experience_browser_check.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""V3 产品体验层的真实浏览器冒烟:首页叙事、快捷搜索、图谱阅读器。""" +"""Classic UI 真实浏览器冒烟:旧导航/旧首页/图谱仍可用,V2/V3 覆盖层不再出现。""" from __future__ import annotations import os @@ -47,7 +47,6 @@ def chrome_dump(path: Path, hash_: str = "", budget: int = 7000) -> str: def rendered_only(dom: str) -> str: - # 产物把组件 HTML 模板字符串内联在 ", "", dom, flags=re.I | re.S) dom = re.sub(r"]*>.*?", "", dom, flags=re.I | re.S) return dom @@ -59,60 +58,46 @@ def assert_has(dom: str, *patterns: str): raise AssertionError("缺少渲染结果:%s" % p) +def assert_not_has(dom: str, *patterns: str): + for p in patterns: + if re.search(p, dom, re.S): + raise AssertionError("经典 UI 不应出现:%s" % p) + + def check_home(): dom = rendered_only(chrome_dump(TARGET)) - assert_has( - dom, - r'') + assert_has(dom, r'class="summary-hero"', r'class="metric-grid"') + assert_not_has(dom, r'class="nav-cluster"', r'class="v3-story', r'class="command-trigger"') + print("ok Classic UI 首页 + 原 12 视图导航") -def check_command_palette(): +def check_no_command_overlay(): doc = TARGET.read_text(encoding="utf-8") probe = """\n""" - if "" not in doc: - raise AssertionError("产物缺少 ") with tempfile.TemporaryDirectory() as td: p = Path(td) / "probe.html" p.write_text(doc.replace("", probe + "", 1), encoding="utf-8") dom = rendered_only(chrome_dump(p, budget=7500)) - assert_has( - dom, - r'
', - r'', + ) + for forbidden in (r'class="command-trigger"', r'class="v3-story panel"', r'class="nav-cluster"'): + if re.search(forbidden, dom, re.S): + raise AssertionError("Classic UI 首页不应出现:%s" % forbidden) + if 'data-ming-data-chunk=' in dom or 'data-ming-search-chunk="index"' in dom: + raise AssertionError("首页意外加载按需块") + print("ok Classic UI 在线首页仅 boot") + + +def _search_loader_probe(): + doc = TARGET.read_text(encoding="utf-8") + probe = """\n""" + p = TARGET.parent / ".classic-search-probe.html" + p.write_text(doc.replace("", probe + "", 1), encoding="utf-8") + return p + + +def check_search_backend_only(): + p = _search_loader_probe() + try: + dom = base.chrome_dump(p, budget=9000) + finally: + p.unlink(missing_ok=True) + base.assert_has( + dom, + r'data-classic-search-probe="ready"', + r'data-ming-data="boot"', + r'data-ming-search="ready"', + r'data-ming-search-chunk="index"', + ) + if 'data-ming-data-chunk=' in dom: + raise AssertionError("只测试 search-index 时不应加载领域块") + if re.search(r'id="commandPalette"|class="command-shell"', dom, re.S): + raise AssertionError("Classic UI 不应恢复可见命令面板") + print("ok Classic UI 保留 lazy search-index 后端,不暴露命令面板") + + +def check_person_detail_deep_link(): + dom = base.chrome_dump( + TARGET, + "#view=characters&person=%E4%BA%8E%E8%B0%A6&detail=1", + 11000, + ) + base.assert_has( + dom, + r'data-ming-chunks="characters,character-detail-13,insight"', + r'data-ming-data-chunk="character-detail-13"', + r'data-ming-data-chunk="insight"', + r'id="characters" class="view active"', + r']*role="img"', + r'aria-label="全书人物关系图:\d+ 人、\d+ 条人物关系', + ) + if re.search(r'data-v3-node-search|class="v3-graph-reader"', dom, re.S): + raise AssertionError("Classic UI 图谱不应出现 V3 reader") + base.assert_no_chunk( + dom, + 'characters', *base.CHAR_SHARDS, 'locations', *base.LOC_SHARDS, + 'events', *base.EVENT_SHARDS, 'time', 'lifespans', 'insight', 'meta', + ) + print("ok Classic UI 原图谱仅 graphs,无 V3 reader") + + +if __name__ == "__main__": + if not TARGET.exists(): + raise SystemExit("目标文件不存在:%s" % TARGET) + + check_home() + check_search_backend_only() + base.check_character_index() + check_person_detail_deep_link() + base.check_location_index() + base.check_place_detail() + base.check_map_core_only() + base.check_location_chapter_mode() + base.check_voyage_mode() + base.check_chronicle() + check_graph_classic() + base.check_timeline() + base.check_dynasty() + base.check_event_index_core_only() + base.check_event_deep_link_one_shard() From 9e37d5f1cdf13a5d7a29dc73d39a32c6c9216ebe Mon Sep 17 00:00:00 2001 From: CochraneK <48022373+CochraneK@users.noreply.github.com> Date: Mon, 14 Sep 2026 12:21:24 +0800 Subject: [PATCH 13/14] ci: adapt online browser regression to classic UI --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3c1a4f..ed4a5bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ permissions: jobs: check: - name: 语法 · 不变量 · 构建 · V13 浏览器 + name: 语法 · 不变量 · 构建 · Classic UI / V13 浏览器 runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -35,7 +35,7 @@ jobs: echo "未发现原书全文文件 ✓" - name: Python 语法检查 - run: python -m compileall -q src tests .dump/_web_browser_check.py .dump/_relation_browser_check_v13.py + run: python -m compileall -q src tests .dump/_web_browser_check.py .dump/_web_browser_check_classic.py .dump/_experience_browser_check.py .dump/_relation_browser_check_v13.py - name: 前端动态统计口径规范化 run: | @@ -69,7 +69,7 @@ jobs: python src/sync_theme.py git diff --check -- web/template/index.html - - name: V3 / V13 产品体验镜像同步冒烟 + - name: Classic UI / V13 体验镜像同步冒烟 run: | python src/sync_experience.py python src/sync_experience.py --check @@ -127,11 +127,11 @@ jobs: - name: 单文件无头浏览器自检 run: python .dump/_browser_check.py .ci/standalone.html - - name: V3 单文件产品体验浏览器自检 + - name: Classic UI 单文件浏览器自检 run: python .dump/_experience_browser_check.py .ci/standalone.html - - name: V12 在线分层浏览器自检 - run: python .dump/_web_browser_check.py .ci/site/index.html + - name: Classic UI 在线分层浏览器自检 + run: python .dump/_web_browser_check_classic.py .ci/site/index.html - name: V13 关系索引在线浏览器自检 run: python .dump/_relation_browser_check_v13.py .ci/site/index.html From 88ba5f6c6af264678bed503e7305027956079804 Mon Sep 17 00:00:00 2001 From: CochraneK <48022373+CochraneK@users.noreply.github.com> Date: Mon, 14 Sep 2026 12:23:55 +0800 Subject: [PATCH 14/14] test: loosen classic online nav assertion to stable DOM --- .dump/_web_browser_check_classic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.dump/_web_browser_check_classic.py b/.dump/_web_browser_check_classic.py index e19f70d..2e080e4 100644 --- a/.dump/_web_browser_check_classic.py +++ b/.dump/_web_browser_check_classic.py @@ -28,7 +28,7 @@ def check_home(): r'data-ming-chunks=""', r'data-ming-search="idle"', r'