.badge.src-upload{display:inline-flex;align-items:center;gap:2px;background:#e7f5fc;color:#007cc3;border:1px solid #b9e0f4;border-radius:10px;padding:2px 9px;font-size:10px}
.badge.src-ai{display:inline-flex;align-items:center;gap:2px;background:#f3e8ff;color:#7a3cc0;border:1px solid #dcc4f3;border-radius:10px;padding:2px 9px;font-size:10px}
.badge.src-manual{display:inline-flex;align-items:center;gap:2px;background:#f1f2f3;color:#6b7177;border:1px solid #dfe2e4;border-radius:10px;padding:2px 9px;font-size:10px}
-.domain-preview-canvas{background:#1e2630;min-height:430px;border-radius:6px;overflow:hidden}
-.domain-preview-canvas .dashboard-thumb-live{min-height:430px}
+.domain-preview-canvas{background:#1e2630;height:64vh;border-radius:6px;overflow:auto}
+.domain-preview-canvas .dashboard-thumb-live{min-height:0}
.domain-preview-feedback{margin-top:14px}
.domain-preview-feedback textarea{width:100%;border:1px solid #cfd3d7;border-radius:3px;padding:9px;resize:vertical;line-height:1.6;margin-top:6px}
.domain-review-pages{display:grid;grid-template-columns:1fr;gap:12px}
.domain-nav-input{flex:1;min-width:0;margin:0 4px;height:28px;border:1px solid #cfd4d9;border-radius:3px;padding:0 8px;font-size:12px;background:#fff}
.domain-nav-input:focus{outline:1px solid var(--blue);border-color:var(--blue)}
+/* ===== 方案打包 / 方案概览(v0.32) ===== */
+.gen-overview{display:block}
+.gen-overview .panel{margin-bottom:0}
+.overview-info{display:grid;grid-template-columns:1fr 1fr;gap:10px 18px;padding:4px 2px 14px;border-bottom:1px solid var(--line);margin-bottom:12px}
+.overview-info>div{display:flex;flex-direction:column;gap:3px}
+.overview-info span{font-size:10px;color:#8b9094;font-weight:bold;letter-spacing:.3px}
+.overview-info b{font-size:14px;color:#2b2f33}
+.overview-info .overview-desc{grid-column:1 / -1}
+.overview-info .overview-desc p{margin:0;font-size:12px;color:#555;line-height:1.6;background:#f8fafb;border:1px solid #eef1f3;border-radius:4px;padding:7px 9px}
+.overview-tabs{margin:0 0 12px}
+.overview-tabs button{padding:0 14px;font-size:12px}
+.overview-body{min-height:120px}
+.overview-body .muted{padding:10px 2px}
+.dev-list{display:flex;flex-direction:column;gap:8px}
+.dev-row{border:1px solid var(--line);border-radius:6px;overflow:hidden;background:#fff}
+.dev-row-head{width:100%;border:0;background:transparent;display:flex;align-items:center;gap:10px;padding:11px 13px;text-align:left;cursor:pointer}
+.dev-row-head:hover{background:#f6fafe}
+.dev-row-head b{font-size:13px;flex:1}
+.dev-count{font-size:11px;color:#008ed2;background:#e7f3f8;border-radius:10px;padding:1px 9px}
+.dev-caret{color:#9aa1a5;font-size:11px}
+.dev-detail{border-top:1px solid #eef1f3;padding:11px 13px;background:#fbfcfd}
+.dev-desc{margin:0 0 8px;font-size:12px;color:#555;line-height:1.7}
+.dev-cp b{font-size:11px;color:#333;display:block;margin-bottom:4px}
+.dev-cp ul{margin:0;padding-left:18px;color:#666;font-size:11px;line-height:1.8}
+.doc-list{display:flex;flex-direction:column;gap:7px}
+.doc-item{display:flex;align-items:baseline;gap:10px;padding:9px 11px;border:1px solid var(--line);border-radius:6px;background:#fff}
+.doc-item b{font-size:13px;flex:1}
+.doc-item .muted{font-size:11px;white-space:nowrap}
+.ops-placeholder{display:flex;flex-direction:column;gap:10px;padding:18px;border:1px dashed #cfd6da;border-radius:8px;background:#fafbfc}
+.ops-placeholder .status{align-self:flex-start}
+.ops-placeholder p{margin:0;font-size:12px;color:#777;line-height:1.7}
+/* ===== 方案打包示意图弹窗(v0.32) ===== */
+.pkg-modal{width:560px}
+.pkg-body{padding:18px}
+.pkg-body .hint{margin:0 0 14px}
+.pkg-box{display:flex;justify-content:center;padding:6px 0 16px}
+.pkg-svg{width:240px;height:auto;filter:drop-shadow(0 8px 16px rgba(120,80,20,.18))}
+.pkg-items{display:grid;grid-template-columns:1fr 1fr;gap:10px}
+.pkg-item{display:flex;align-items:flex-start;gap:10px;padding:11px 12px;border:1px solid var(--line);border-radius:8px;background:#fff;box-shadow:0 1px 3px #0000000a}
+.pkg-item:hover{border-color:#9ed3ec;box-shadow:0 2px 8px #008fd21f}
+.pkg-icon{width:34px;height:34px;flex:0 0 auto;border-radius:7px;background:linear-gradient(135deg,#e8f4fb,#d6ecf7);color:#008ed2;display:grid;place-items:center;font-size:17px}
+.pkg-item-text{display:flex;flex-direction:column;gap:3px;min-width:0}
+.pkg-item-text b{font-size:13px;color:#2b2f33}
+.pkg-item-text small{font-size:10px;color:#888;line-height:1.5}
+
{key:"assets",label:"资产设置"},
{key:"functions",label:"功能与菜单"},
{key:"opsflow",label:"运维流程"},
- {key:"generate",label:"生成保存"},
+ {key:"generate",label:"方案打包"},
{key:"expert",label:"专家确认"},
];
for(;;){ const {done,value}=await reader.read(); if(done) break; buf+=dec.decode(value,{stream:true}); const lines=buf.split("\n"); buf=lines.pop()||""; for(const line of lines){ if(!line.trim()) continue; try{ const ev=JSON.parse(line); if(ev.log) setLog(prev=>prev+ev.log); onEv(ev); }catch{} } }
}
-function DomainPageThumb({jsonText,emptyLabel="未生成"}:{jsonText:string; emptyLabel?:string}){
+function DomainPageThumb({jsonText,emptyLabel="未生成",fit=false}:{jsonText:string; emptyLabel?:string; fit?:boolean}){
const outer=useRef<HTMLDivElement>(null);
const inner=useRef<HTMLDivElement>(null);
useEffect(()=>{
(async()=>{
try{
const json=JSON.parse(jsonText);
- if(cancelled||!inner.current) return;
+ if(cancelled||!inner.current||!outer.current) return;
const renderDashboard:any=await loadRenderer();
renderDashboard(json, inner.current, {});
- const w=outer.current?.clientWidth||300;
+ const w=outer.current.clientWidth||300;
const pw=json.grid?.pageWidth||1920;
const rootEl=inner.current.firstElementChild as HTMLElement|null;
const rh=rootEl? rootEl.offsetHeight : (json.grid?.pageHeight||1080);
- const s=Math.max(0.02, w/pw);
+ // fit 模式:同时按宽、高缩放,整页一次可见(修复“只能预览上半部分”)
+ const availH = fit ? (outer.current.parentElement?.clientHeight || rh) : Infinity;
+ const s = fit ? Math.max(0.02, Math.min(w/pw, availH/rh)) : Math.max(0.02, w/pw);
inner.current.style.height=`${rh}px`;
inner.current.style.transform=`scale(${s})`;
inner.current.style.transformOrigin="top left";
- if(outer.current) outer.current.style.height=`${rh*s}px`;
+ outer.current.style.height=`${rh*s}px`;
}catch(e){ if(inner.current) inner.current.innerHTML='<div class="thumb-empty">预览不可用</div>'; }
})();
return ()=>{cancelled=true;};
- },[jsonText, emptyLabel]);
+ },[jsonText, emptyLabel, fit]);
return <div ref={outer} className="dashboard-thumb-live" style={{overflow:"hidden"}}><div ref={inner} style={{width:1920, height:1080, position:"relative"}}/></div>;
}
return <div className="modal-backdrop" onMouseDown={()=>onClose(false)}><div className="modal" style={{width:"min(1100px,94vw)",maxWidth:"94vw"}} onMouseDown={e=>e.stopPropagation()}>
<header><h2>{page.name||"页面预览"}</h2><button onClick={()=>onClose(false)}>×</button></header>
<div className="modal-body">
- <div className="domain-preview-canvas">{hasJson? <DomainPageThumb jsonText={page.json_text}/> : <div className="thumb-empty">尚未生成,请点击「生成」后再预览</div>}</div>
+ <div className="domain-preview-canvas">{hasJson? <DomainPageThumb jsonText={page.json_text} fit/> : <div className="thumb-empty">尚未生成,请点击「生成」后再预览</div>}</div>
<div className="domain-preview-feedback">
<h4>修改意见</h4>
<p className="hint">输入对当前页面的修改意见,AI 将据此重新生成该页面。</p>
const [devCreateOpen,setDevCreateOpen]=useState(false);
const [pendingDevRow,setPendingDevRow]=useState<number|null>(null);
const [previewPage,setPreviewPage]=useState<DomainPage|null>(null);
+ const [pkgOpen,setPkgOpen]=useState(false);
const basicAi=useStreamComplete();
const assetsAi=useStreamComplete();
const cubesAi=useStreamComplete();
pageStyles={pageStyles} styleId={m.style_id??null} onSelectStyle={selectStyle} />}
{step==="opsflow" && <PlaceholderStep title="运维流程" note="运维流程编辑功能待开发,后续版本将支持在方案中配置标准运维 SOP 与告警处理流程。"/>}
- {step==="generate" && <DomainGenerateReview tName={m.name} assets={assets} effSelected={effSelected} grouped={grouped} pages={pages} docs={m.docs||[]} onBack={()=>goStep("functions")} onKnowledge={()=>goStep("knowledge")} onConfirm={()=>notify("方案已生成并保存,可返回上一步继续调整")} onPreviewPage={setPreviewPage}/>}
+ {step==="generate" && <DomainGenerateReview m={m} domainName={domainName} assets={assets} effSelected={effSelected} grouped={grouped} pages={pages} docs={m.docs||[]} onBack={()=>goStep("opsflow")} onPreviewPage={setPreviewPage}/>}
{step==="expert" && <PlaceholderStep title="专家确认" note="专家确认流程待开发,后续版本将支持领域专家在线评审并签署方案。"/>}
</div>
- <div className="sticky-actions"><Button secondary onClick={()=>curIdx>0?goStep(DOMAIN_STEPS[curIdx-1].key):onBack()}>{curIdx>0?"上一步":"返回列表"}</Button><div><Button secondary onClick={()=>notify("草稿已保存")}>保存草稿</Button>{curIdx<DOMAIN_STEPS.length-1 && <Button onClick={()=>goStep(DOMAIN_STEPS[curIdx+1].key)}>下一步</Button>}{curIdx===DOMAIN_STEPS.length-1 && <Button onClick={()=>notify("方案已提交专家确认")}>提交确认</Button>}</div></div>
+ <div className="sticky-actions"><Button secondary onClick={()=>curIdx>0?goStep(DOMAIN_STEPS[curIdx-1].key):onBack()}>{curIdx>0?"上一步":"返回列表"}</Button><div><Button secondary onClick={()=>notify("草稿已保存")}>保存草稿</Button>{step==="generate" && <Button onClick={()=>setPkgOpen(true)}>📦 打包下载</Button>}{step!=="generate" && curIdx<DOMAIN_STEPS.length-1 && <Button onClick={()=>goStep(DOMAIN_STEPS[curIdx+1].key)}>下一步</Button>}{curIdx===DOMAIN_STEPS.length-1 && <Button onClick={()=>notify("方案已提交专家确认")}>提交确认</Button>}</div></div>
+ {pkgOpen && <PackageModal name={m?.name} onClose={()=>setPkgOpen(false)} notify={notify}/>}
</>;
}
</Panel>;
}
-function DomainGenerateReview({tName,assets,effSelected,grouped,pages,docs,onBack,onKnowledge,onConfirm,onPreviewPage}:{tName:string;assets:DomainAsset[];effSelected:string[];grouped:any[];pages:DomainPage[];docs:any[];onBack:()=>void;onKnowledge?:()=>void;onConfirm:()=>void;onPreviewPage?:(p:DomainPage)=>void}){
- const reviewRows:[string,string,string,string?,(()=>void)?][]=[
- ["方案",tName,"","返回修改",onBack],
- ["资产",`${assets.filter(a=>a.device_template_id).length} 类 / ${assets.reduce((n,a)=>n+Number(a.count||0),0)} 个设备`,"3 层资产结构",""],
- ["MU 页面",`${effSelected.length} 个标准页面`,"复用并配置菜单",""],
- ["Cube 页面",`${pages.length} 个扩展页面`,`已生成 ${pages.filter(p=>p.status==="generated").length} 个`,""],
- ["菜单结构","MU + 综合监控",`共 ${effSelected.length+pages.length} 项`,""],
- ["领域知识", docs.length?`${docs.length} 份(${docs.slice(0,3).map((d:any)=>d.name).join("、")}${docs.length>3?" 等":""})`:"暂无知识", docs.length?"已沉淀领域知识,将作为 AI 补全优先参考":"可上传文档 / 配置站点", "返回领域知识", onKnowledge],
- ["运维流程","标准 SOP","(本期占位,待开发)",""],
+function DomainGenerateReview({m,domainName,assets,effSelected,grouped,pages,docs,onBack,onPreviewPage}:{
+ m:any;domainName:string;assets:DomainAsset[];effSelected:string[];grouped:any[];pages:DomainPage[];docs:any[];onBack:()=>void;onPreviewPage?:(p:DomainPage)=>void
+}){
+ const [tab,setTab]=useState<string>("dev");
+ const [devOpen,setDevOpen]=useState<number|null>(null);
+ const [devDetail,setDevDetail]=useState<any>(null);
+ const [devLoading,setDevLoading]=useState(false);
+ const openDev=async(id:number)=>{
+ if(devOpen===id){ setDevOpen(null); return; }
+ setDevOpen(id); setDevLoading(true); setDevDetail(null);
+ try{ const d=await fetchDb(`/device-templates/${id}`) as any; setDevDetail(d); }catch(e:any){} finally{ setDevLoading(false); }
+ };
+ const devList=Array.from(new Map(assets.filter(a=>a.device_template_id).map(a=>[a.device_template_id as number,a])).values());
+ const assetGroups=assets.filter(a=>a.device_template_id);
+ const muItems=grouped.flatMap((g:any)=>g.items).filter((l:any)=>effSelected.includes(l.key));
+ const tabs:{key:string;label:string}[]=[
+ {key:"dev",label:`设备模板 ${devList.length}`},
+ {key:"asset",label:`资产结构 ${assetGroups.length}`},
+ {key:"mu",label:`MU页面 ${effSelected.length}`},
+ {key:"cube",label:`扩展页面 ${pages.length}`},
+ {key:"knowledge",label:`领域知识 ${docs.length}`},
+ {key:"ops",label:"运维流程"},
];
- return <div className="generate-layout"><div>
- <Panel title="生成范围确认"><div className="confirm-grid">
- {reviewRows.map(r=><div key={r[0]}><span>{r[0]}</span><b>{r[1]}</b><small>{r[2]}</small>{r[3]&&<button onClick={r[4]||onBack}>{r[3]}</button>}</div>)}
- </div></Panel>
- <Panel title="资产层级预览"><div className="asset-tree-preview"><div className="tree-root">⌄ <b>{tName||"未命名方案"}</b></div>{assets.filter(a=>a.device_template_id).map((row,i)=><div key={i} className="tree-branch"><div className="tree-node">⌄ {row.device_name}<span className="tree-count">{row.count}</span></div>{row.count>0&&<div className="tree-leaves">{formatInstances(row.rule,row.count)}</div>}</div>)}</div></Panel>
- <Panel title="菜单结构预览"><div className="menu-tree"><div className="menu-root">⌄ {tName||"未命名方案"}</div><div className="menu-section"><div className="menu-section-title">MU 标准页面</div>{effSelected.length===0?<div className="menu-leaf muted">(未选择)</div>:grouped.flatMap((g:any)=>g.items).filter((l:any)=>effSelected.includes(l.key)).map((l:any)=><div key={l.key} className="menu-leaf">· {l.parentMenu}/{l.label}</div>)}</div><div className="menu-section"><div className="menu-section-title">综合监控</div>{pages.filter(p=>p.name).map(p=><div key={p.id} className="menu-leaf cube">· {p.name}</div>)}</div></div></Panel>
- </div>
- <div className="gen-right">
- <Panel title="页面总览" className="task-panel">
- <p className="hint">以下为方案中已配置的 Cube 扩展页面预览(点击可放大查看)。如需调整,请返回「功能与菜单」。</p>
- <div className="domain-review-pages">{pages.length===0? <div className="muted">暂无 Cube 页面</div> : pages.map(p=><article key={p.id} className="domain-review-page" style={{cursor:"pointer"}} onClick={()=>onPreviewPage&&onPreviewPage(p)}><DomainPageThumb jsonText={p.json_text} emptyLabel="未生成"/><div><Status>{p.status==="generated"?"AI已生成":p.status==="uploaded"?"已上传":"未生成"}{p.source?` · ${p.source==="ai"?"AI":p.source==="upload"?"上传":"手动"}`:""}</Status><h4>{p.name}</h4><span className="review-zoom">⛶ 点击放大</span></div></article>)}</div>
- <div style={{display:"flex",gap:8,marginTop:12}}><Button secondary onClick={onBack}>返回上一步</Button><Button onClick={onConfirm}>确认方案</Button></div>
+ return <div className="gen-overview">
+ <Panel title="方案概览">
+ <div className="overview-info">
+ <div><span>所属领域</span><b>{domainName||"—"}</b></div>
+ <div><span>方案名称</span><b>{m.name||"—"}</b></div>
+ <div className="overview-desc"><span>方案描述</span><p>{m.description||"—"}</p></div>
+ <div className="overview-desc"><span>补充描述</span><p>{m.supplement||"—"}</p></div>
+ </div>
+ <div className="tabs overview-tabs">
+ {tabs.map(t=><button key={t.key} className={tab===t.key?"active":""} onClick={()=>setTab(t.key)}>{t.label}</button>)}
+ </div>
+ <div className="overview-body">
+ {tab==="dev" && <div className="dev-list">{devList.length===0? <p className="muted">本方案未选用设备模板。</p> :
+ devList.map((a:any)=>(
+ <div key={a.device_template_id} className="dev-row">
+ <button className="dev-row-head" onClick={()=>openDev(a.device_template_id as number)}>
+ <b>{a.device_name}</b><span className="dev-count">{a.count} 台</span><span className="dev-caret">{devOpen===a.device_template_id?"▾":"▸"}</span>
+ </button>
+ {devOpen===a.device_template_id && <div className="dev-detail">{devLoading? <p className="muted">加载中…</p> :
+ devDetail? <><p className="dev-desc">{devDetail.description||"(无说明)"}</p>
+ <div className="dev-cp"><b>采集点({(devDetail.collection_points||[]).length})</b>{(devDetail.collection_points||[]).length===0? <span className="muted"> 无</span> :
+ <ul>{(devDetail.collection_points||[]).slice(0,40).map((cp:any,i:number)=><li key={i}>{cp.name}{cp.unit?`(${cp.unit})`:""}</li>)}</ul>}</div></> :
+ <p className="muted">无法加载详情。</p>}</div>}
+ </div>
+ ))}</div>}
+ {tab==="asset" && <div className="asset-tree-preview"><div className="tree-root">⌄ <b>{m.name||"未命名方案"}</b></div>{assetGroups.map((row,i)=><div key={i} className="tree-branch"><div className="tree-node">⌄ {row.device_name}<span className="tree-count">{row.count}</span></div>{row.count>0&&<div className="tree-leaves">{formatInstances(row.rule,row.count)}</div>}</div>)}</div>}
+ {tab==="mu" && <div className="mu-tree">{grouped.length===0? <p className="muted">(未加载 MU 树)</p> :
+ grouped.map(g=><div key={g.parentMenu} className="mu-group"><b>{g.parentMenu}</b><div className="mu-items">{g.items.map((item:any)=><label key={item.key} className="disabled"><input type="checkbox" disabled checked={effSelected.includes(item.key)} onChange={()=>{}}/><span>{item.label}</span></label>)}</div></div>)}</div>}
+ {tab==="cube" && <div className="domain-review-pages">{pages.length===0? <div className="muted">暂无扩展页面,返回「功能与菜单 → 领域扩展页面」生成。</div> :
+ pages.map(p=><article key={p.id} className="domain-review-page" style={{cursor:"pointer"}} onClick={()=>onPreviewPage&&onPreviewPage(p)}><DomainPageThumb jsonText={p.json_text} emptyLabel="未生成"/><div><Status>{p.status==="generated"?"AI已生成":p.status==="uploaded"?"已上传":"未生成"}{p.source?` · ${p.source==="ai"?"AI":p.source==="upload"?"上传":"手动"}`:""}</Status><h4>{p.name}</h4><span className="review-zoom">⛶ 点击放大</span></div></article>)}</div>}
+ {tab==="knowledge" && <div className="doc-list">{docs.length===0? <p className="muted">未上传领域知识,返回「领域知识」步骤上传。</p> :
+ docs.map((d:any,i:number)=><div key={i} className="doc-item"><b>{d.name}</b><span className="muted">{d.type==="link"?"参考站点":(d.mime||"文档")}{(d.size||d.file_size)?` · ${(Number(d.size||d.file_size)/1024).toFixed(1)}KB`:""}</span></div>)}</div>}
+ {tab==="ops" && <div className="ops-placeholder"><span className="status muted">待开发</span><p>运维流程编辑功能待开发,后续版本将支持在方案中配置标准运维 SOP 与告警处理流程。</p></div>}
+ </div>
</Panel>
- </div></div>;
+ </div>;
}
+function PackageModal({name,onClose,notify}:{name?:string;onClose:()=>void;notify:(m:string)=>void}){
+ const items=[
+ {icon:"⬡",label:"设备模板",desc:"本方案选用的设备模版及采集点"},
+ {icon:"▦",label:"资产结构",desc:"默认资产组与设备实例层级"},
+ {icon:"❖",label:"扩展页面文件",desc:"已生成的 Cube 扩展页面文件"},
+ {icon:"☰",label:"默认菜单",desc:"MU + 综合监控 菜单结构"},
+ {icon:"📄",label:"领域知识",desc:"上传的领域知识文档"},
+ ];
+ return <div className="modal-backdrop" onMouseDown={onClose}><div className="modal pkg-modal" onMouseDown={e=>e.stopPropagation()}>
+ <header><h2>方案打包</h2><button onClick={onClose}>×</button></header>
+ <div className="modal-body pkg-body">
+ <p className="hint">即将把方案「{name||"未命名方案"}」打包为可交付成果,包含以下组成部分:</p>
+ <div className="pkg-box">
+ <svg viewBox="0 0 260 150" className="pkg-svg" role="img" aria-label="方案打包箱">
+ <polygon points="50,70 130,52 210,70 130,88" fill="#d9a05b" stroke="#b9823f" strokeWidth="1.5"/>
+ <polygon points="50,70 50,118 130,136 130,88" fill="#c98f4c" stroke="#b9823f" strokeWidth="1.5"/>
+ <polygon points="210,70 210,118 130,136 130,88" fill="#e0aa63" stroke="#b9823f" strokeWidth="1.5"/>
+ <polygon points="50,70 130,52 130,30 50,48" fill="#e7b877" stroke="#b9823f" strokeWidth="1.5"/>
+ <polygon points="210,70 130,52 130,30 210,48" fill="#e7b877" stroke="#b9823f" strokeWidth="1.5"/>
+ <rect x="92" y="60" width="76" height="10" rx="2" fill="#fff" opacity=".9"/>
+ <rect x="96" y="50" width="68" height="10" rx="2" fill="#eef7fb" opacity=".95"/>
+ <rect x="100" y="40" width="60" height="10" rx="2" fill="#fff" opacity=".9"/>
+ <rect x="104" y="31" width="52" height="9" rx="2" fill="#e6f3fb" opacity=".95"/>
+ </svg>
+ </div>
+ <div className="pkg-items">
+ {items.map(it=><div key={it.label} className="pkg-item"><span className="pkg-icon">{it.icon}</span><div className="pkg-item-text"><b>{it.label}</b><small>{it.desc}</small></div></div>)}
+ </div>
+ </div>
+ <footer><Button secondary onClick={onClose}>取消</Button><Button onClick={()=>{ notify("方案已打包(示意),可交付给实施团队"); onClose(); }}>确认打包下载</Button></footer>
+ </div></div>;
+}
+
function KnowledgeStep({id,docs,notify,reload,confirm}:{id:number;docs:any[];notify:(m:string)=>void;reload:()=>void;confirm:(msg:string)=>Promise<boolean>}){
const [busy,setBusy]=useState(false);
const [urlName,setUrlName]=useState("");