feat: remove Deleted-projects section (delete = permanent removal, files kept) + refresh button on iframe tab windows

This commit is contained in:
Aisland
2026-08-20 22:29:39 +08:00
parent 3d7efadfe2
commit 47c5ccc034
7 changed files with 140 additions and 34 deletions
+14 -25
View File
@@ -1352,13 +1352,7 @@ function buildCustomLayoutPrompt(req: string): string {
})
}
/** 重新添加已删除的入驻项目(删除 ↔ 重新添加 完全可逆) */
const readdProject = (id: string) => {
persistProjects((prev) => ({
...prev,
removed: prev.removed.filter((x) => x !== id),
}))
}
// ── 快捷方式(表单已移除,仅保留存量条目的删除能力) ──
const removeShortcut = (id: string) => {
@@ -1441,13 +1435,20 @@ function buildCustomLayoutPrompt(req: string): string {
return def ? def.id : '2h'
}
// ── 删除(全部走二次确认;常驻项目 = 移出工作台,插件不卸载) ──
// ── 删除(全部走二次确认;项目彻底移出工作台:对话与项目文件均保留,仅清理本地关联状态) ──
const removeProject = (id: string) => {
persistProjects((prev) => ({
...prev,
removed: prev.removed.includes(id) ? prev.removed : [...prev.removed, id],
hidden: prev.hidden.filter((x) => x !== id),
}))
persistProjects((prev) => {
const next = {
...prev,
removed: prev.removed.includes(id) ? prev.removed : [...prev.removed, id],
hidden: prev.hidden.filter((x) => x !== id),
order: prev.order.filter((x) => x !== id),
}
if (next.bindings[id]) { next.bindings = { ...next.bindings }; delete next.bindings[id] }
if (next.folders[id]) { next.folders = { ...next.folders }; delete next.folders[id] }
if (next.views[id]) { next.views = { ...next.views }; delete next.views[id] }
return next
})
}
const askDelete = (kind: 'layout' | 'shortcut' | 'project', id: string, name: string) => {
setRequestDelete({ kind, id, name })
@@ -1925,18 +1926,6 @@ function buildCustomLayoutPrompt(req: string): string {
<button type="button" className="dsh-wt_manageBtn" title={t('manage.deleteShortcut')} onClick={() => askDelete('shortcut', s.id, s.name)}>✕</button>
</div>
))}
{projects.removed.length > 0 && (
<>
<div className="dsh-wt_menuSep" />
<span className="dsh-wt_menuLabel">{t('manage.removed')}</span>
{projects.removed.map((rid) => (
<div key={rid} className="dsh-wt_manageRow dsh-wt_manageRowOff dsh-wt_manageRowRemoved">
<span className="dsh-wt_manageScName">{projects.nameOverrides[rid] ?? metas[rid]?.name ?? rid}</span>
<button type="button" className="dsh-wt_manageBtn" title={t('manage.readd')} onClick={() => readdProject(rid)}>↺</button>
</div>
))}
</>
)}
</div>
)}
+2 -2
View File
@@ -55,7 +55,7 @@ export const zh = {
'confirm.delete': '删除',
'confirm.layoutBody': '确定删除「{name}」?该工作区及其布局将被移除,此操作不可恢复。',
'confirm.shortcutBody': '确定删除快捷方式「{name}」?此操作不可恢复。',
'confirm.projectBody': '确定从工作台删除「{name}」?插件本身不会被卸载,「恢复默认」也不会复活它;之后可在设置里「已删除的项目」中重新添加。',
'confirm.projectBody': '「{name}」将从工作台彻底移除(对话与项目文件都保留,只是不再显示项目卡片)。',
'pane.browser': '浏览器',
'pane.explorer': '资源管理器',
'pane.scm': '源代码管理',
@@ -189,7 +189,7 @@ export const en = {
'confirm.delete': 'Delete',
'confirm.layoutBody': 'Delete "{name}"? Its workspace and layout will be removed. This cannot be undone.',
'confirm.shortcutBody': 'Delete shortcut "{name}"? This cannot be undone.',
'confirm.projectBody': 'Remove "{name}" from the worktable? The plugin stays installed; Reset defaults will NOT bring it back — re-add it later under Removed projects in Settings.',
'confirm.projectBody': '"{name}" will be removed from the worktable permanently (the conversation and project files are kept).',
'pane.browser': 'Browser',
'pane.explorer': 'Explorer',
'pane.scm': 'Source control',
+18 -1
View File
@@ -891,6 +891,23 @@ function BrowserPane() {
)
}
/** iframe 内容标签(网页/站点产物):右上角常驻刷新按钮(重挂载整页刷新,跨域可靠) */
function IframePane(props: { url: string; title?: string }) {
const [reloadKey, setReloadKey] = useState(0)
return (
<div className="dsh-wt_frameWrap">
<iframe key={reloadKey} className="dsh-wt_paneFrame" src={props.url} title={props.title ?? ''} />
<button
type="button"
className="dsh-wt_frameRefresh"
title={T('pane.refresh')}
aria-label={T('pane.refresh')}
onClick={() => setReloadKey((k) => k + 1)}
>↻</button>
</div>
)
}
/** 动画播放窗:iframe 壳 + 地址栏(站内自带项目/场景列表、播放、画幅切换、导出等全部控件) */
function AnimPane() {
const [url, setUrl] = useState('')
@@ -1534,7 +1551,7 @@ function CustomPane(props: { paneTitle?: string }) {
function PaneTabBody(props: { tab: PaneTab; row: PaneRow; index: number; paneTitle?: string }) {
const content = props.tab.content
if (content.kind === 'iframe') {
return <iframe className="dsh-wt_paneFrame" src={content.url} title={props.tab.title} />
return <IframePane url={content.url} title={content.title ?? props.tab.title} />
}
if (content.kind === 'file') {
return <FileViewer path={content.path} />
+4
View File
@@ -138,6 +138,10 @@ export const css = xtermCss + '\n' + [
'.dsh-wt_paneBar{flex:none;display:flex;align-items:center;height:22px;padding:0 8px;border-bottom:1px solid var(--dsw-alias-border-l1,#262b36);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.02))}',
'.dsh-wt_paneTitle{flex:1;min-width:0;font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary,#9aa4b2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
'.dsh-wt_paneFrame{flex:1;width:100%;border:0;background:#010409;min-height:0}',
// iframe 内容标签:容器 + 右上角常驻刷新按钮
'.dsh-wt_frameWrap{position:relative;flex:1;min-height:0;display:flex;flex-direction:column}',
'.dsh-wt_frameRefresh{position:absolute;top:6px;right:6px;width:24px;height:24px;display:flex;align-items:center;justify-content:center;border:1px solid var(--dsw-alias-border-l2,#3a4150);border-radius:6px;background:var(--dsw-alias-fill-l2,#171b22);color:var(--dsw-alias-label-primary,#e6e8eb);font-size:13px;line-height:1;cursor:pointer;opacity:.72;z-index:5}',
'.dsh-wt_frameRefresh:hover{opacity:1;border-color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
'.dsh-wt_splitDivider{background:var(--dsw-alias-border-l2,#3a4150);cursor:col-resize;touch-action:none;box-sizing:border-box}',
'.dsh-wt_splitDividerH{cursor:row-resize}',
'.dsh-wt_splitDivider:hover,.dsh-wt_splitDividerH:hover{background:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
@@ -1076,6 +1076,20 @@
(remounted=true)✓;functional-diag 全 20 STEP ERROR_COUNT: 0。
- 备注:纯客户端改动,F5 即生效。
## 补记(移除「已删除项目」区块 + iframe 标签右上角刷新)
- 用户反馈:① 设置里的「已删除的项目」区块不要了——删除项目就是彻底移出工作台(对话与
项目文件本就保留);② 做好的网页窗口(iframe 标签)右上角没有刷新按钮。
- 处理:① 移除设置面板「已删除的项目」区块与 readdProject;removeProject 删除时同步清理
bindings/folders/views/order 本地关联状态;确认文案改为「彻底移除(对话与项目文件保留)」;
② 新增 IframePane 组件(iframe 标签统一外壳):右上角常驻 ↻(重挂载整页刷新),
PaneTabBody 的 iframe 分支改走它。
- 验证(probe-batch13.cjs):iframe 标签 frameWrap/刷新按钮(title=刷新)✓、点击后 iframe
重挂载 ✓;删除常驻项目后 removedSectionGone=true、行数 2→1 ✓;functional-diag 全 20 STEP
(STEP10 断言按新行为重写)ERROR_COUNT: 0。
- 备注:纯客户端改动,F5 即生效;重启不会自动出现该按钮,必须本次改动生效。
+2 -6
View File
@@ -360,14 +360,10 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
await new Promise(function(r){setTimeout(r,300)});
out.layoutGoneAfterDelete=!document.querySelector('.dsh-wt_layout');
try{ out.layoutsLeft=JSON.parse(localStorage.getItem('dsh.worktable.projects.v1')).layouts.length }catch(e){ out.layoutsLeft='ERR' }
// 真删除:「已删除的项目」区块出现,重新添加可逆(无恢复默认按钮)
// 真删除:「已删除的项目」区块已移除——删除即彻底移出工作台(无重新添加入口)
out.removedSection=!!document.querySelector('.dsh-wt_settings .dsh-wt_manageRowRemoved');
out.resetBtnGone=!document.querySelector('.dsh-wt_manageReset');
var readdBtn=document.querySelector('.dsh-wt_settings .dsh-wt_manageRowRemoved .dsh-wt_manageBtn');
if(readdBtn){ readdBtn.click(); }
await new Promise(function(r){setTimeout(r,300)});
try{ out.removedAfterReadd=JSON.parse(localStorage.getItem('dsh.worktable.projects.v1')).removed }catch(e){ out.removedAfterReadd='ERR' }
out.prBackAfterReadd=document.querySelectorAll('.dsh-wt_projects .pr_card').length;
out.prGoneAfterDelete=document.querySelectorAll('.dsh-wt_projects .pr_card').length===0;
var done=document.querySelector('.dsh-wt_manageDone');
if(done){ done.click(); }
await new Promise(function(r){setTimeout(r,200)});
+86
View File
@@ -0,0 +1,86 @@
// probe-batch13.cjs — 已删除项目区块移除 + iframe 标签右上角刷新
const http = require('http');
const { spawn } = require('child_process');
const WebSocket = require('ws');
const PORT = 9370;
const proc = spawn('C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', [
'--headless=new', '--disable-gpu', '--no-first-run', '--no-default-browser-check',
'--window-size=1440,900', '--force-device-scale-factor=1',
'--remote-debugging-port=' + PORT,
'--user-data-dir=C:\\Users\\SJL\\AppData\\Local\\Temp\\wt-chrome-batch13',
'about:blank',
], { stdio: 'ignore' });
const getJSON = (p) => new Promise((res, rej) => {
http.get({ host: '127.0.0.1', port: PORT, path: p }, (r) => { let d=''; r.on('data',c=>d+=c); r.on('end',()=>{try{res(JSON.parse(d))}catch(e){rej(e)}}) }).on('error', rej);
});
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
(async () => {
let list = null;
for (let i = 0; i < 40; i++) { try { list = await getJSON('/json/list'); if (list && list.length) break; } catch {} await sleep(500); }
const target = list.find((t) => t.type === 'page') || list[0];
const ws = new WebSocket(target.webSocketDebuggerUrl);
let id = 0; const pending = new Map();
const send = (method, params) => new Promise((res) => { const mid = ++id; pending.set(mid, res); ws.send(JSON.stringify({ id: mid, method, params })); });
ws.on('message', (raw) => { const m = JSON.parse(String(raw)); if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id); } });
await new Promise((r) => ws.on('open', r));
await send('Runtime.enable');
await send('Page.enable');
await send('Page.navigate', { url: 'http://127.0.0.1:3080/' });
const evaluate = async (expr) => {
const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true });
if (r.result && r.result.exceptionDetails) return { __error: (r.result.exceptionDetails.exception || {}).description || r.result.exceptionDetails.text };
return r.result && r.result.result ? r.result.result.value : undefined;
};
let ready = null;
for (let i = 0; i < 30; i++) { await sleep(1000); ready = await evaluate("!!window.__dshWorktable"); if (ready === true) break; }
await sleep(1500);
const step = await evaluate(`(async function(){
var out = {};
var sleep = function(ms){ return new Promise(function(r){ setTimeout(r, ms); }); };
var vis = function(sel){ return [].slice.call(document.querySelectorAll(sel)).find(function(el){ return (getComputedStyle(el).visibility!=='hidden'&&el.getBoundingClientRect().height>0); }); };
var st = window.__dshWorktable.splitStore;
try {
// A. iframe 标签右上角刷新
st.open({id:'t-frame',title:'f',top:null,main:[{id:'p1',title:'W',min:200,content:null}],chatWidth:{default:320,min:240,max:600}});
await sleep(600);
st.openTab('main', 0, { kind: 'iframe', url: 'https://example.com', title: 'web' });
await sleep(900);
out.frameWrap = !!vis('.dsh-wt_frameWrap');
var rb = vis('.dsh-wt_frameRefresh');
out.refreshBtn = !!rb;
out.refreshTitle = rb ? rb.getAttribute('title') : null;
var f1 = vis('.dsh-wt_frameWrap iframe');
if (rb) { rb.click(); }
await sleep(700);
var f2 = vis('.dsh-wt_frameWrap iframe');
out.remounted = !!f1 && !!f2 && f1 !== f2;
st.close();
await sleep(300);
// B. 已删除项目区块:删除常驻项目后设置面板里不得再有该区块
var btnS = document.querySelector('.dsh-wt_actions .dsh-wt_iconBtn:nth-child(2)');
if (btnS) { btnS.click(); }
await sleep(300);
var rows = document.querySelectorAll('.dsh-wt_settings .dsh-wt_manageRow');
out.rowsBefore = rows.length;
// 删除第一个常驻项目行(其最后一个按钮 = ✕)
var xBtn = rows[0] && rows[0].querySelectorAll('.dsh-wt_manageBtn')[rows[0].querySelectorAll('.dsh-wt_manageBtn').length-1];
if (xBtn) { xBtn.click(); }
await sleep(250);
var del = document.querySelector('.dsh-wt_confirmDelete');
if (del) { del.click(); }
await sleep(350);
out.removedSectionGone = !document.querySelector('.dsh-wt_settings .dsh-wt_manageRowRemoved');
out.rowsAfter = document.querySelectorAll('.dsh-wt_settings .dsh-wt_manageRow').length;
// 清理:把删除的项目恢复(直接改 localStorage + 刷新页面状态不便——用 removed 还原)
var j = JSON.parse(localStorage.getItem('dsh.worktable.projects.v1'));
j.removed = [];
localStorage.setItem('dsh.worktable.projects.v1', JSON.stringify(j));
var done = document.querySelector('.dsh-wt_manageDone');
if (done) { done.click(); }
await sleep(200);
} catch(e) { out.err = String(e); }
return JSON.stringify(out);
})()`);
console.log('BATCH13:', JSON.stringify(step));
ws.close(); proc.kill(); process.exit(0);
})().catch((e) => { console.error('FATAL', e && e.stack || e); try { proc.kill(); } catch {} process.exit(1); });