Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

当前版本:1.5.0。以下按版本记录主要变更,当前行为以 README 和使用说明为准。

## 外观设置

- 新增外观设置:石墨紫、深海蓝、松林绿、暖白四套配色,支持快捷 / 自选强调色。
- 支持 12–16 px 数据字号、紧凑 / 舒适表格密度和隔行底色;即时生效、本地记忆,并可一键恢复默认。
- 表格、数据树、详情窗口和编辑标记统一使用主题颜色;字体变化时调整当前表格列宽。

## 1.5.0 后续优化

- 鼠标悬停字段整行后按 F2 直接编辑;表格按鼠标所在单元格定位,无需先点击数值。
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Chrome DevTools 中的 JSON 接口调试工具。把响应展开成可搜索的
| 请求复现 | 请求详情、请求体 JSON、Bash / WSL / Git Bash 格式 cURL |
| 数据导出 | 原始响应、当前节点、全部筛选和排序后的 CSV / Markdown / JSON |
| 数值精度 | 普通超安全整数字面量精确保留,贯穿浏览、搜索、排序、复制和 JSON 导出 |
| 个性化外观 | 四套配色、自选强调色、数据字号、表格密度和隔行底色;本地记忆,一键恢复默认 |
| 面板布局 | 独立收起左侧列表、拖动分栏、统一滚动条、布局与分页偏好记忆 |

表头字段保持单行,内容自动列宽通常为 88–380 px;手动列宽通常最高 960 px,超长表头可突破上限。长内容省略展示,隐藏列不影响导出。
Expand Down
67 changes: 67 additions & 0 deletions appearance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Apply saved colors before the first paint; appearance never changes captured data.
const Appearance=(()=>{
const storageKey='api-table-viewer.appearance.v1';
const themes={graphite:{background:'#181a20',accent:'#8b9dff'},ocean:{background:'#171e28',accent:'#4ea1ff'},forest:{background:'#14201d',accent:'#52c9a5'},paper:{background:'#f3f5f9',accent:'#4f5bd5'}};
const defaults={theme:'graphite',accent:themes.graphite.accent,fontSize:13,density:'comfortable',striped:true};
function normalize(value){
const data=value && typeof value==='object'?value:{};
const theme=Object.hasOwn(themes,data.theme)?data.theme:defaults.theme;
return {theme,accent:typeof data.accent==='string' && /^#[\da-f]{6}$/i.test(data.accent)?data.accent:themes[theme].accent,
fontSize:[12,13,14,15,16].includes(data.fontSize)?data.fontSize:defaults.fontSize,
density:data.density==='compact'?'compact':'comfortable',striped:typeof data.striped==='boolean'?data.striped:true};
}
let prefs={...defaults};
try{prefs=normalize(JSON.parse(localStorage.getItem(storageKey)));}catch{}
function luminance(color){
const rgb=color.match(/[\da-f]{2}/gi).map(c=>parseInt(c,16)/255).map(c=>c<=.04045?c/12.92:((c+.055)/1.055)**2.4);
return rgb[0]*.2126+rgb[1]*.7152+rgb[2]*.0722;
}
function readableAccent(color,background){
const bg=luminance(background),contrast=c=>(Math.max(luminance(c),bg)+.05)/(Math.min(luminance(c),bg)+.05);
if(contrast(color)>=4.5)return color;
const rgb=color.match(/[\da-f]{2}/gi).map(c=>parseInt(c,16));
const end=bg>.179?0:255;
for(let step=1;step<=20;step++){
const mixed='#'+rgb.map(c=>Math.round(c+(end-c)*step/20).toString(16).padStart(2,'0')).join('');
if(contrast(mixed)>=4.5)return mixed;
}
return end?'#ffffff':'#000000';
}
function apply(){
const root=document.documentElement;
root.dataset.theme=prefs.theme;root.dataset.density=prefs.density;root.dataset.striped=String(prefs.striped);
root.style.setProperty('--blue',readableAccent(prefs.accent,themes[prefs.theme].background));
root.style.setProperty('--blue2',prefs.accent);
root.style.setProperty('--data-font-size',prefs.fontSize+'px');
}
apply();
function mount(){
const menu=document.getElementById('appearanceMenu'),status=document.getElementById('appearanceStatus');
const color=document.getElementById('appearanceAccent'),font=document.getElementById('appearanceFont'),density=document.getElementById('appearanceDensity'),striped=document.getElementById('appearanceStriped');
const palettes=[...menu.querySelectorAll('[data-theme-choice]')],accents=[...menu.querySelectorAll('[data-accent-choice]')];
function sync(){
color.value=prefs.accent;font.value=String(prefs.fontSize);density.value=prefs.density;striped.checked=prefs.striped;
for(const button of palettes)button.setAttribute('aria-pressed',String(button.dataset.themeChoice===prefs.theme));
for(const button of accents)button.setAttribute('aria-pressed',String(button.dataset.accentChoice.toLowerCase()===prefs.accent.toLowerCase()));
document.getElementById('appearanceAccentValue').textContent=prefs.accent.toUpperCase();
}
function update(changes){
const previousFont=prefs.fontSize;prefs=normalize({...prefs,...changes});apply();sync();
try{localStorage.setItem(storageKey,JSON.stringify(prefs));status.textContent='已保存,下次打开继续使用';}
catch{status.textContent='已应用,本次设置无法保存';}
if(previousFont!==prefs.fontSize)document.dispatchEvent(new CustomEvent('appearancechange'));
}
for(const button of palettes)button.onclick=()=>update({theme:button.dataset.themeChoice,accent:themes[button.dataset.themeChoice].accent});
for(const button of accents)button.onclick=()=>update({accent:button.dataset.accentChoice});
color.oninput=()=>update({accent:color.value});font.onchange=()=>update({fontSize:Number(font.value)});
density.onchange=()=>update({density:density.value});striped.onchange=()=>update({striped:striped.checked});
document.getElementById('appearanceReset').onclick=()=>update(defaults);
document.getElementById('appearanceClose').onclick=()=>close();
function close(){menu.open=false;document.getElementById('appearanceToggle').focus();}
document.addEventListener('pointerdown',e=>{if(menu.open && !menu.contains(e.target))menu.open=false;});
document.addEventListener('keydown',e=>{if(e.key==='Escape' && menu.open){e.preventDefault();e.stopPropagation();close();}},true);
sync();
}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',mount,{once:true});else mount();
return {get fontSize(){return prefs.fontSize;}};
})();
1 change: 1 addition & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ npm test
| `npm run test:fixtures` | 本地 HTTP 接口与 Worker 联动,包括节点阈值、50,000 条记录、精确整数和长文本 |
| `npm run test:preview` | 示例 HAR、预览 HTTP 路由,以及生成预览的脚本语法和 HTML 标识 |
| `npm run test:fields` | 15 项原位字段 Mock 检查:精确整数、类型、数组定位、大响应小补丁、Fetch / XHR、启动请求、会话恢复、站点隔离与 Worker 路径 |
| `npm run test:appearance` | 5 项外观检查:设置持久化与恢复默认、异常配置回退、强调色可读性、面板关闭与焦点恢复、字号变化后调整列宽 |
| `npm run test:mock` | 18 项:Fetch / XHR 拦截、精确匹配、单次并发、取消、类型、规则更新、租约、面板控制和 HTTP 页面联动 |

回归测试在 Node.js 中运行 Worker 代码,通过模拟 DOM 和 DevTools 对象检查界面逻辑。接口测试使用随机端口,结束后自动关闭服务。2026-10-20 的自动检查结果:78 项回归、静态检查、8 个接口子案例通过。
Expand Down
4 changes: 4 additions & 0 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,7 @@ $.data[0]["field.name"]
## 复制详情文本

进入字符串或其他简单值的详情后,内容区顶部显示“复制内容”。字符串按原文复制,保留真实换行和引号,不额外添加 JSON 外层引号或转义;即使正在查看某个万字符分段,也复制当前节点的全部内容。数字(包括超安全整数)、布尔和 null 使用对应的 JSON 数值文本。原有顶部“复制完整数据”继续复制 JSON 表示,便于保留类型。

## 外观设置

右上角“外观设置”可以选择石墨紫、深海蓝、松林绿或暖白,并自选强调色。数据字号为 12–16 px,表格可切换紧凑 / 舒适密度,隔行底色可单独关闭。选择即时生效,下次打开继续使用;“恢复默认”只重置外观,不清除接口记录或 Mock 规则。
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
},
"scripts": {
"preview": "node tests/preview/server.cjs",
"test": "node tests/regression.cjs && node tests/package-check.cjs && npm run test:fixtures && npm run test:mock && npm run test:preview && npm run test:fields",
"test": "node tests/regression.cjs && node tests/package-check.cjs && npm run test:fixtures && npm run test:mock && npm run test:preview && npm run test:fields && npm run test:appearance",
"test:regression": "node tests/regression.cjs",
"test:package": "node tests/package-check.cjs",
"test:manual": "node tests/manual/server.cjs",
"test:fixtures": "node --test tests/manual/check.cjs",
"test:preview": "node --test tests/preview/check.cjs",
"test:mock": "node --test tests/response-mock.cjs",
"test:fields": "node --test tests/field-mock.cjs"
"test:fields": "node --test tests/field-mock.cjs",
"test:appearance": "node --test tests/appearance.cjs"
}
}
24 changes: 24 additions & 0 deletions panel.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,36 @@
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>API Table Viewer</title>
<link rel="stylesheet" href="style.css">
<script src="appearance.js"></script>
</head>
<body>
<div class="app">
<header class="topbar">
<div class="brand"><span class="logo">▦</span><span>API Table Viewer</span></div>
<button id="toggleSidebar" class="btn ghost" aria-controls="requestSidebar" aria-expanded="true" title="收起左侧接口列表">收起列表</button>
<details id="appearanceMenu" class="appearance-menu">
<summary id="appearanceToggle" class="btn ghost appearance-toggle" aria-controls="appearancePanel" title="配色、字体与显示密度"><span aria-hidden="true">◐</span> 外观设置</summary>
<div id="appearancePanel" class="appearance-panel" aria-label="外观设置">
<div class="appearance-heading"><div><strong>让界面更顺眼</strong><p>即时生效,自动记住你的选择</p></div><button id="appearanceClose" class="icon-btn" aria-label="关闭外观设置">×</button></div>
<fieldset><legend>整体配色</legend><div class="appearance-palettes">
<button class="appearance-palette palette-graphite" data-theme-choice="graphite" aria-pressed="true"><span class="palette-preview" aria-hidden="true"><i></i><i></i><i></i></span>石墨紫</button>
<button class="appearance-palette palette-ocean" data-theme-choice="ocean" aria-pressed="false"><span class="palette-preview" aria-hidden="true"><i></i><i></i><i></i></span>深海蓝</button>
<button class="appearance-palette palette-forest" data-theme-choice="forest" aria-pressed="false"><span class="palette-preview" aria-hidden="true"><i></i><i></i><i></i></span>松林绿</button>
<button class="appearance-palette palette-paper" data-theme-choice="paper" aria-pressed="false"><span class="palette-preview" aria-hidden="true"><i></i><i></i><i></i></span>暖白</button>
</div></fieldset>
<fieldset><legend>强调色</legend><div class="appearance-accents">
<button class="accent-choice accent-violet" data-accent-choice="#8b9dff" aria-label="鸢尾紫" aria-pressed="true"></button>
<button class="accent-choice accent-blue" data-accent-choice="#4ea1ff" aria-label="晴空蓝" aria-pressed="false"></button>
<button class="accent-choice accent-teal" data-accent-choice="#52c9a5" aria-label="薄荷绿" aria-pressed="false"></button>
<button class="accent-choice accent-amber" data-accent-choice="#edb35c" aria-label="琥珀金" aria-pressed="false"></button>
<button class="accent-choice accent-rose" data-accent-choice="#ed87a8" aria-label="玫瑰粉" aria-pressed="false"></button>
<label class="appearance-custom" for="appearanceAccent">自选 <input id="appearanceAccent" type="color" value="#8b9dff" aria-label="自定义强调色"></label>
</div><span id="appearanceAccentValue" class="appearance-color-value">#8B9DFF</span></fieldset>
<div class="appearance-options"><label for="appearanceFont">数据字号<select id="appearanceFont"><option value="12">12 px</option><option value="13" selected>13 px</option><option value="14">14 px</option><option value="15">15 px</option><option value="16">16 px</option></select></label><label for="appearanceDensity">表格密度<select id="appearanceDensity"><option value="comfortable" selected>舒适</option><option value="compact">紧凑</option></select></label></div>
<label class="appearance-check"><input id="appearanceStriped" type="checkbox" checked> 隔行底色,方便横向阅读</label>
<div class="appearance-footer"><span id="appearanceStatus" role="status" aria-live="polite">设置仅影响当前扩展的外观</span><button id="appearanceReset" class="btn ghost">恢复默认</button></div>
</div>
</details>
</header>

<div id="responseMockBar" class="response-mock-bar hidden" role="status"><span id="responseMockSummary"></span><button id="responseMockRefresh" class="btn ghost">更新状态</button><button id="responseMockStopAll" class="btn ghost">停用全部替换</button></div>
Expand Down
2 changes: 1 addition & 1 deletion scripts/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
ROOT = Path(__file__).resolve().parent.parent
RUNTIME = (
"manifest.json", "devtools.html", "devtools.js", "panel.html", "panel.js",
"style.css", "json-codec.js", "worker.js", "viewer.js", "viewer-expanded.js",
"style.css", "appearance.js", "json-codec.js", "worker.js", "viewer.js", "viewer-expanded.js",
"developer.js", "table-ui.js", "layout.js", "shortcuts.js", "response-editor.js",
"mock-page.js", "response-mock.js",
"field-patch-core.js", "field-mock.js", "field-mock-page.js", "field-mock-bridge.js", "field-mock-background.js",
Expand Down
Loading