};
const deleteSites=async(ids:number[])=>{
if(!ids.length) return;
- if(!window.confirm(`确认删除选中的 ${ids.length} 个站点?该操作不可恢复,相关资产、页面与生成文件将一并删除。`)) return;
+ if(!(await confirm(`确认删除选中的 ${ids.length} 个站点?该操作不可恢复,相关资产、页面与生成文件将一并删除。`))) return;
try{
await fetchDb("/sites", {method:"DELETE", headers:{"Content-Type":"application/json"}, body:JSON.stringify({ids})});
setSites(prev=>prev.filter(s=>!ids.includes(s.id)));
return {all,draft,running,done,partial};
},[sites]);
const [selected,setSelected] = useState<Set<number>>(new Set());
+ const {confirm, el:confirmEl}=useConfirm();
const toggle = (id:number)=> setSelected(prev=>{ const n=new Set(prev); if(n.has(id)) n.delete(id); else n.add(id); return n; });
const allSelected = sites.length>0 && selected.size===sites.length;
const toggleAll = ()=> setSelected(allSelected ? new Set() : new Set(sites.map(s=>s.id)));
const batchDelete = async ()=>{ await onDelete([...selected]); setSelected(new Set()); };
if(loading) return <Panel><div className="empty-state">加载中…</div></Panel>;
- return <><div className="summary-strip">{[[String(counts.all),"全部项目"],[String(counts.draft),"草稿"],[String(counts.running),"生成中"],[String(counts.done),"生成完成"],[String(counts.partial),"部分失败"]].map(x=><div key={x[1]}><b>{x[0]}</b><span>{x[1]}</span></div>)}</div><Panel><div className="filterbar"><label>⌕ <input placeholder="搜索站点名称"/></label><select><option>全部领域</option></select><select><option>全部状态</option></select><Button>Search</Button><span/>{selected.size>0 && <Button secondary onClick={batchDelete}>批量删除 ({selected.size})</Button>}<div className="view-switch"><button className={view==="table"?"active":""} onClick={()=>setView("table")}>☷</button><button className={view==="card"?"active":""} onClick={()=>setView("card")}>▦</button></div><button className="icon-button" onClick={()=>notify("列表已刷新")}>↻</button></div>{view==="table"?<table className="data-table"><thead><tr><th><input type="checkbox" checked={allSelected} onChange={toggleAll}/></th><th>站点名称 ↕</th><th>所属领域</th><th>设备数量</th><th>MU 页面</th><th>Cube 页面</th><th>状态</th><th>最近修改</th><th>操作</th></tr></thead><tbody>{sites.map(row=><tr key={row.id}><td><input type="checkbox" checked={selected.has(row.id)} onChange={()=>toggle(row.id)}/></td><td><button className="link" onClick={()=>go(row.status==="generated"?"result":"basic", row.id)}><span className="site-icon">⬡</span><b>{row.name || "未命名站点"}</b></button></td><td>{row.domain}</td><td>{row.device_count}</td><td>{row.mu_count}</td><td>{row.cube_count}</td><td><Status>{row.status==="generated"?"生成完成":row.status==="partial"?"部分失败":row.status==="running"?"生成中":"草稿"}</Status></td><td>{row.updated_at}</td><td><button className="link danger" onClick={(e)=>{e.stopPropagation(); onDelete([row.id]);}}>删除</button></td></tr>)}</tbody></table>:<div className="project-grid">{sites.map(row=><article key={row.id} onClick={()=>go(row.status==="generated"?"result":"basic", row.id)}><div><span className="site-icon big">⬡</span><Status>{row.status==="generated"?"生成完成":row.status==="partial"?"部分失败":row.status==="running"?"生成中":"草稿"}</Status></div><h3>{row.name || "未命名站点"}</h3><button className="card-del" onClick={(e)=>{e.stopPropagation(); onDelete([row.id]);}}>删除</button><p>{row.domain}</p><section><span><b>{row.device_count}</b> 设备</span><span><b>{row.mu_count}</b> MU</span><span><b>{row.cube_count}</b> Cube</span></section><small>更新于 {row.updated_at}</small></article>)}</div>}<div className="pagination"><span>共 {sites.length} 条</span><button>‹</button><button className="active">1</button><button>›</button><select><option>20 / page</option></select></div></Panel></>
+ return <>{confirmEl}<div className="summary-strip">{[[String(counts.all),"全部项目"],[String(counts.draft),"草稿"],[String(counts.running),"生成中"],[String(counts.done),"生成完成"],[String(counts.partial),"部分失败"]].map(x=><div key={x[1]}><b>{x[0]}</b><span>{x[1]}</span></div>)}</div><Panel><div className="filterbar"><label>⌕ <input placeholder="搜索站点名称"/></label><select><option>全部领域</option></select><select><option>全部状态</option></select><Button>Search</Button><span/>{selected.size>0 && <Button secondary onClick={batchDelete}>批量删除 ({selected.size})</Button>}<div className="view-switch"><button className={view==="table"?"active":""} onClick={()=>setView("table")}>☷</button><button className={view==="card"?"active":""} onClick={()=>setView("card")}>▦</button></div><button className="icon-button" onClick={()=>notify("列表已刷新")}>↻</button></div>{view==="table"?<table className="data-table"><thead><tr><th><input type="checkbox" checked={allSelected} onChange={toggleAll}/></th><th>站点名称 ↕</th><th>所属领域</th><th>设备数量</th><th>MU 页面</th><th>Cube 页面</th><th>状态</th><th>最近修改</th><th>操作</th></tr></thead><tbody>{sites.map(row=><tr key={row.id}><td><input type="checkbox" checked={selected.has(row.id)} onChange={()=>toggle(row.id)}/></td><td><button className="link" onClick={()=>go(row.status==="generated"?"result":"basic", row.id)}><span className="site-icon">⬡</span><b>{row.name || "未命名站点"}</b></button></td><td>{row.domain}</td><td>{row.device_count}</td><td>{row.mu_count}</td><td>{row.cube_count}</td><td><Status>{row.status==="generated"?"生成完成":row.status==="partial"?"部分失败":row.status==="running"?"生成中":"草稿"}</Status></td><td>{row.updated_at}</td><td><button className="link danger" onClick={(e)=>{e.stopPropagation(); onDelete([row.id]);}}>删除</button></td></tr>)}</tbody></table>:<div className="project-grid">{sites.map(row=><article key={row.id} onClick={()=>go(row.status==="generated"?"result":"basic", row.id)}><div><span className="site-icon big">⬡</span><Status>{row.status==="generated"?"生成完成":row.status==="partial"?"部分失败":row.status==="running"?"生成中":"草稿"}</Status></div><h3>{row.name || "未命名站点"}</h3><button className="card-del" onClick={(e)=>{e.stopPropagation(); onDelete([row.id]);}}>删除</button><p>{row.domain}</p><section><span><b>{row.device_count}</b> 设备</span><span><b>{row.mu_count}</b> MU</span><span><b>{row.cube_count}</b> Cube</span></section><small>更新于 {row.updated_at}</small></article>)}</div>}<div className="pagination"><span>共 {sites.length} 条</span><button>‹</button><button className="active">1</button><button>›</button><select><option>20 / page</option></select></div></Panel></>
}
function Basic({site,domains,onChange,onAddDomain,onUpload,saving,notify,onViewCompletePrompt}:{site:SiteDetail|null;domains:string[];onChange:(p:Partial<SiteDetail>)=>void;onAddDomain:(n:string)=>void;onUpload:(f:File)=>void;saving:boolean;notify:(m:string)=>void;onViewCompletePrompt:()=>void}){
function fileToBase64(file:File){return new Promise<{filename:string;mime:string;data:string}>((resolve,reject)=>{const r=new FileReader();r.onload=()=>{const buf=new Uint8Array(r.result as ArrayBuffer);let bin="";for(const b of buf)bin+=String.fromCharCode(b);resolve({filename:file.name,mime:file.type||"application/octet-stream",data:btoa(bin)});};r.onerror=reject;r.readAsArrayBuffer(file);});}
+function useConfirm(){
+ const [state,setState]=useState<{open:boolean;message:string;resolve?:(v:boolean)=>void}>({open:false,message:""});
+ const confirm=(message:string)=>new Promise<boolean>(resolve=>setState({open:true,message,resolve}));
+ const close=(v:boolean)=>{ state.resolve?.(v); setState(s=>({...s,open:false})); };
+ const el = state.open ? (
+ <div className="dlg-backdrop" onClick={()=>close(false)}>
+ <div className="dlg-box" onClick={e=>e.stopPropagation()}>
+ <p>{state.message}</p>
+ <div className="dlg-actions"><button className="button secondary" onClick={()=>close(false)}>取消</button><button className="button" onClick={()=>close(true)}>确定</button></div>
+ </div>
+ </div>
+ ) : null;
+ return { confirm, el };
+}
+
+function usePrompt(){
+ const [state,setState]=useState<{open:boolean;message:string;value:string;resolve?:(v:string|null)=>void}>({open:false,message:"",value:""});
+ const prompt=(message:string, defaultValue="")=>new Promise<string|null>(resolve=>setState({open:true,message,value:defaultValue,resolve}));
+ const close=(v:string|null)=>{ state.resolve?.(v); setState(s=>({...s,open:false})); };
+ const el = state.open ? (
+ <div className="dlg-backdrop" onClick={()=>close(null)}>
+ <div className="dlg-box" onClick={e=>e.stopPropagation()}>
+ <p>{state.message}</p>
+ <input autoFocus value={state.value} onChange={e=>setState(s=>({...s,value:e.target.value}))} onKeyDown={e=>{ if(e.key==="Enter") close(state.value); if(e.key==="Escape") close(null); }}/>
+ <div className="dlg-actions" style={{marginTop:14}}><button className="button secondary" onClick={()=>close(null)}>取消</button><button className="button" onClick={()=>close(state.value)}>确定</button></div>
+ </div>
+ </div>
+ ) : null;
+ return { prompt, el };
+}
+
function DomainLibrary({go,notify,onOpen,domainFilter,domainOptions}:{go:(p:PageKey)=>void;notify:(m:string)=>void;onOpen:(id:number)=>void;domainFilter:number|null;domainOptions:{id:number;name:string}[]}){
const [list,setList]=useState<any[]>([]);
const [loading,setLoading]=useState(true);
const [sort,setSort]=useState<{key:""|"domain"|"name"|"status"|"creator";dir:1|-1}>({key:"",dir:1});
const load=async()=>{ try{ setList(await fetchDb("/domain-templates") as any); }catch(e:any){ notify("加载失败:"+e.message); } finally{ setLoading(false); } };
useEffect(()=>{ load(); },[]);
- const del=async(id:number)=>{ if(!confirm("确定删除该领域方案?此操作不可恢复")) return; try{ await fetchDb(`/domain-templates/${id}`,{method:"DELETE"}); notify("已删除"); load(); }catch(e:any){ notify("删除失败:"+e.message); } };
+ const {confirm, el:confirmEl}=useConfirm();
+ const del=async(id:number)=>{ if(!(await confirm("确定删除该领域方案?此操作不可恢复"))) return; try{ await fetchDb(`/domain-templates/${id}`,{method:"DELETE"}); notify("已删除"); load(); }catch(e:any){ notify("删除失败:"+e.message); } };
const statusLabel=(s:string)=>s==="available"?"可用":"不可用";
const toggleSort=(key:"domain"|"name"|"status"|"creator")=>setSort(s=>s.key===key?{key,dir:(s.dir*-1) as 1|-1}:{key,dir:1});
const sortInd=(key:"domain"|"name"|"status"|"creator")=>sort.key===key?(<span className="sort-ind">{sort.dir===1?"▲":"▼"}</span>):null;
if(av<bv) return -1*sort.dir; if(av>bv) return 1*sort.dir; return 0;
});
return <>
+ {confirmEl}
<div className="domain-hero"><div><span className="eyebrow">DOMAIN SOLUTION LIBRARY</span><h2>领域方案库</h2><p>沉淀各领域标准方案,供客户站点直接使用或快速微调。</p></div>
<div className="domain-hero-stats">{domainOptions.map(d=><div key={d.id}><b>{list.filter(t=>t.domain_id===d.id).length}</b><span>{d.name}</span></div>)}</div>
</div>
const [imgPreview,setImgPreview]=useState<string|null>(dev.image_path?`/api/db/device-templates/${dev.id}/image`:null);
const [imgB64,setImgB64]=useState<{filename:string;mime:string;data:string}|null>(null);
const [cps,setCps]=useState<any[]>((dev.collection_points||[]).map((c:any)=>({...c,key:c.id,checked:false})));
+ const {confirm, el:confirmEl}=useConfirm();
const onPick=async(f:File)=>{ setImgPreview(URL.createObjectURL(f)); const b=await fileToBase64(f); setImgB64({filename:b.filename,mime:b.mime,data:b.data}); };
const save=async()=>{ const body:any={name,description}; if(imgB64) body.image=imgB64; await fetchDb(`/device-templates/${dev.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)}); notify("设备已保存"); onChange(); };
- const delDev=async()=>{ if(!confirm("删除该设备模板?")) return; await fetchDb(`/device-templates/${dev.id}`,{method:"DELETE"}); onChange(); onBack(); };
+ const delDev=async()=>{ if(!(await confirm("删除该设备模板?"))) return; await fetchDb(`/device-templates/${dev.id}`,{method:"DELETE"}); onChange(); onBack(); };
const addCpRow=()=>setCps([...cps,{key:-(Date.now()),name:"",collect_note:"",unit:"",value_range:"",description:"",checked:false}]);
const updCp=(k:number,field:string,val:any)=>setCps(cps.map(c=>c.key===k?{...c,[field]:val}:c));
const toggleCp=(k:number)=>setCps(cps.map(c=>c.key===k?{...c,checked:!c.checked}:c));
const allChecked=cps.length>0&&cps.every(c=>c.checked);
const toggleAll=()=>setCps(cps.map(c=>({...c,checked:!allChecked})));
- const delCpRows=async()=>{ const sel=cps.filter(c=>c.checked); if(sel.length===0){ notify("请先勾选要删除的采集点"); return; } if(!confirm(`确定删除选中的 ${sel.length} 个采集点?`)) return; for(const c of sel){ if(c.id) await fetchDb(`/collection-points/${c.id}`,{method:"DELETE"}); } setCps(cps.filter(c=>!c.checked)); onChange(); };
+ const delCpRows=async()=>{ const sel=cps.filter(c=>c.checked); if(sel.length===0){ notify("请先勾选要删除的采集点"); return; } if(!(await confirm(`确定删除选中的 ${sel.length} 个采集点?`))) return; for(const c of sel){ if(c.id) await fetchDb(`/collection-points/${c.id}`,{method:"DELETE"}); } setCps(cps.filter(c=>!c.checked)); onChange(); };
const saveCpRows=async()=>{ try{ for(const c of cps){ const payload={name:c.name,collect_note:c.collect_note,unit:c.unit,value_range:c.value_range,description:c.description}; if(c.id){ await fetchDb(`/collection-points/${c.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)}); } else { await fetchDb(`/device-templates/${dev.id}/collection-points`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)}); } } notify("采集点已保存"); onChange(); }catch(e:any){ notify("保存失败:"+e.message); } };
return <div className="device-card">
<div className="device-card-head"><b>{name||"(未命名)"}</b><button className="text-button" onClick={delDev}>删除设备</button></div>
</tbody></table>
</div>
<div style={{marginTop:8}}><Button onClick={save}>保存设备</Button></div>
+ {confirmEl}
</div>;
}
const load=async()=>{ try{ setList(await fetchDb("/device-templates") as any); }catch(e:any){ notify("加载失败:"+e.message); } finally{ setLoading(false); } };
useEffect(()=>{ load(); },[]);
const filtered=list.filter(d=>{ if(!q.trim()) return true; const s=q.toLowerCase(); return (d.name||"").toLowerCase().includes(s) || (d.description||"").toLowerCase().includes(s); });
- const delDev=async(id:number)=>{ if(!confirm("删除该设备模板?")) return; await fetchDb(`/device-templates/${id}`,{method:"DELETE"}); load(); };
+ const {confirm, el:confirmEl}=useConfirm();
+ const delDev=async(id:number)=>{ if(!(await confirm("删除该设备模板?"))) return; await fetchDb(`/device-templates/${id}`,{method:"DELETE"}); load(); };
if(openId===null) return <>
+ {confirmEl}
<div className="domain-hero"><div><span className="eyebrow">DEVICE TEMPLATE LIBRARY</span><h2>设备模版</h2><p>独立维护设备模版及其采集点,编辑后可在领域方案中按需选用。</p></div></div>
<Panel title="设备模版" tools={<Button onClick={()=>setOpenId("create")}>+ 新增设备模版</Button>}>
<div className="list-toolbar"><input className="search" placeholder="搜索设备名称 / 说明" value={q} onChange={e=>setQ(e.target.value)}/></div>
function PageRow({p,notify,onChange}:{p:any;notify:(m:string)=>void;onChange:()=>void}){
const [name,setName]=useState(p.name); const [description,setDesc]=useState(p.description||""); const [json_text,setJson]=useState(p.json_text);
const [editing,setEditing]=useState(false);
+ const {confirm, el:confirmEl}=useConfirm();
const save=async()=>{ try{ JSON.parse(json_text); }catch{ notify("Cube JSON 不是合法 JSON"); return; } await fetchDb(`/domain-pages/${p.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({name,description,json_text})}); notify("页面已保存"); setEditing(false); onChange(); };
- const del=async()=>{ if(!confirm("删除该页面?")) return; await fetchDb(`/domain-pages/${p.id}`,{method:"DELETE"}); onChange(); };
- return <div className="page-row"><div className="page-row-head"><b>{name}</b><span>{editing? <><Button onClick={save}>保存</Button> <button className="text-button" onClick={()=>setEditing(false)}>取消</button></> : <><button className="text-button" onClick={()=>setEditing(true)}>编辑</button> <button className="text-button" onClick={del}>删除</button></>}</span></div>
+ const del=async()=>{ if(!(await confirm("删除该页面?"))) return; await fetchDb(`/domain-pages/${p.id}`,{method:"DELETE"}); onChange(); };
+ return <div className="page-row">{confirmEl}<div className="page-row-head"><b>{name}</b><span>{editing? <><Button onClick={save}>保存</Button> <button className="text-button" onClick={()=>setEditing(false)}>取消</button></> : <><button className="text-button" onClick={()=>setEditing(true)}>编辑</button> <button className="text-button" onClick={del}>删除</button></>}</span></div>
{editing && <div className="form-grid">
<label>页面名称<input value={name} onChange={e=>setName(e.target.value)}/></label>
<label>说明<input value={description} onChange={e=>setDesc(e.target.value)}/></label>
const [t,setT]=useState<any>(null);
const [loading,setLoading]=useState(false);
const [m,setM]=useState<any>(null);
+ const {confirm, el:confirmEl}=useConfirm();
+ const {prompt, el:promptEl}=usePrompt();
const loadDoms=async()=>{ try{ setDoms(await fetchDb("/domains") as any); }catch{} };
const load=async(silent=false)=>{ if(id==null){ setT(null); return; } if(!silent) setLoading(true); try{ setT(await fetchDb(`/domain-templates/${id}`) as any); }catch(e:any){ notify("加载失败:"+e.message); } finally{ if(!silent) setLoading(false); } };
useEffect(()=>{ loadDoms(); load(); },[id]);
const saveMeta=async()=>{ await fetchDb(`/domain-templates/${id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(m)}); notify("已保存模板信息"); load(); };
const addPage=async()=>{ await fetchDb(`/domain-templates/${id}/pages`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:"新页面",description:"",json_text:"{}"})}); load(); };
const addMenu=async()=>{ await fetchDb(`/domain-templates/${id}/menu`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({label:"新菜单项"})}); load(); };
- const addDoc=async()=>{ const nm=prompt("文档名称"); if(!nm) return; const link=confirm("是否为超链?(确定=超链,取消=上传文件)"); if(link){ const u=prompt("文档超链 URL"); if(!u) return; await fetchDb(`/domain-templates/${id}/docs`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:nm,doc_type:"link",url:u})}); } else { const f:any=await new Promise(res=>{ const input=document.createElement("input"); input.type="file"; input.onchange=()=>res(input.files?.[0]); input.click(); }); if(!f) return; const b=await fileToBase64(f); await fetchDb(`/domain-templates/${id}/docs`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:nm,doc_type:"file",file:{filename:b.filename,mime:b.mime,data:b.data}})}); } load(); };
+ const addDoc=async()=>{ const nm=await prompt("文档名称"); if(!nm) return; const link=await confirm("是否为超链?(确定=超链,取消=上传文件)"); if(link){ const u=await prompt("文档超链 URL"); if(!u) return; await fetchDb(`/domain-templates/${id}/docs`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:nm,doc_type:"link",url:u})}); } else { const f:any=await new Promise(res=>{ const input=document.createElement("input"); input.type="file"; input.onchange=()=>res(input.files?.[0]); input.click(); }); if(!f) return; const b=await fileToBase64(f); await fetchDb(`/domain-templates/${id}/docs`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:nm,doc_type:"file",file:{filename:b.filename,mime:b.mime,data:b.data}})}); } load(); };
const delDoc=async(did:number)=>{ await fetchDb(`/knowledge-docs/${did}`,{method:"DELETE"}); load(); };
const dlDoc=async(did:number)=>{ const r=await fetch(`/api/db/knowledge-docs/${did}/file`); const blob=await r.blob(); const d=t.docs.find((x:any)=>x.id===did); downloadBlob(blob, d?.name||"doc"); };
return <>
+ {confirmEl}{promptEl}
<div className="domain-detail-head"><button className="text-button" onClick={onBack}>← 返回列表</button></div>
<Panel title="模板信息"><div className="form-grid">
<label>所属领域<select value={m.domain_id} onChange={e=>setM({...m,domain_id:Number(e.target.value)})}>{doms.map(d=><option key={d.id} value={d.id}>{d.name}</option>)}</select></label>
function PageStyleLibrary({pageStyles, loadPageStyles, notify}:{pageStyles:PageStyle[]; loadPageStyles:()=>Promise<void>; notify:(m:string)=>void}){
const [editor,setEditor]=useState<{open:boolean; id?:number}>({open:false});
const [preview,setPreview]=useState<PageStyle|null>(null);
+ const {confirm, el:confirmEl}=useConfirm();
const afterChange=async(msg:string)=>{ notify(msg); setEditor({open:false, id:undefined}); await loadPageStyles(); };
const openEdit=async(s:PageStyle)=>{ setEditor({open:true, id:s.id}); };
const remove=async(s:PageStyle)=>{
- if(!confirm(`确定删除样式「${s.name}」?此操作不可恢复`)) return;
+ if(!(await confirm(`确定删除样式「${s.name}」?此操作不可恢复`))) return;
try{ await fetchDb(`/page-styles/${s.id}`, {method:"DELETE"}); await afterChange(`已删除「${s.name}」`); }
catch(e:any){ notify("删除失败: "+e.message); }
};
- return <>{preview&&<StylePreview style={preview} onClose={()=>setPreview(null)}/>}
+ return <>{confirmEl}{preview&&<StylePreview style={preview} onClose={()=>setPreview(null)}/>}
<Panel title="全部页面样式" tools={<Button onClick={()=>setEditor({open:true})}>+ 新增样式</Button>}>
{pageStyles.length===0
? <p className="hint">暂无页面样式,点击右上角「+ 新增样式」上传 JSON 与预览图即可创建。</p>
}
function StyleEditorModal({id,notify,afterChange,onClose}:{id?:number;notify:(m:string)=>void;afterChange:(msg:string)=>Promise<void>;onClose:()=>void}){
+ const {confirm, el:confirmEl}=useConfirm();
const [name,setName]=useState("");
const [jsonText,setJsonText]=useState("");
const [image,setImage]=useState<{filename:string;data:string}|null>(null);
};
const remove=async()=>{
if(!id) return;
- if(!confirm("确定删除该样式?此操作不可恢复")) return;
+ if(!(await confirm("确定删除该样式?此操作不可恢复"))) return;
try{ await fetchDb(`/page-styles/${id}`, {method:"DELETE"}); await afterChange("样式已删除"); }
catch(e:any){ notify("删除失败: "+e.message); }
};
- return <div className="modal-backdrop" onMouseDown={onClose}><div className="modal" style={{width:"640px"}} onMouseDown={e=>e.stopPropagation()}>
+ return <div className="modal-backdrop" onMouseDown={onClose}>{confirmEl}<div className="modal" style={{width:"640px"}} onMouseDown={e=>e.stopPropagation()}>
<header><h2>{id?"编辑页面样式":"新增页面样式"}</h2><button onClick={onClose}>×</button></header>
<div className="modal-body">
<div className="modal-note">页面样式包含一份 Cube 仪表盘 JSON(配色与视觉主题)和一张预览图。名称用于在样式库中展示。</div>