From: wang.yu Date: Wed, 19 Aug 2026 07:14:32 +0000 (+0800) Subject: 提示框改为页面居中:用自定义弹窗替换原生 confirm/prompt X-Git-Url: http://acesimba.cloud/gitweb/?a=commitdiff_plain;h=ffd924a98bb87a9d4a0ce1be4b7e982c10750a1e;p=StationBuilder.git 提示框改为页面居中:用自定义弹窗替换原生 confirm/prompt - 新增 useConfirm/usePrompt hook,返回 promise 化 confirm/prompt 及居中弹窗节点 - 替换全部原生 confirm(9处) 与 prompt(2处) 调用,组件根部渲染对应弹窗 - globals.css 新增 .dlg-backdrop 全屏遮罩 + .dlg-box 居中对话框(含输入框/Enter确认/Esc取消) - 文件上传仍用原生文件控件(非提示框,无法CSS居中) --- diff --git a/app/globals.css b/app/globals.css index 046b344..a6aa1a6 100644 --- a/app/globals.css +++ b/app/globals.css @@ -250,3 +250,12 @@ .tpl-thumb{width:48px;height:34px;object-fit:cover;border:1px solid var(--line);border-radius:3px;display:block;background:#f2f5f7} .tpl-thumb-empty{color:#c2c8cc} +/* ===== 自定义确认 / 输入弹窗(替代原生 confirm/prompt,居中显示) ===== */ +.dlg-backdrop{position:fixed;inset:0;z-index:90;background:#00000059;display:flex;align-items:center;justify-content:center;padding:20px} +.dlg-box{background:#fff;border:1px solid #d2d6da;border-radius:8px;box-shadow:0 12px 36px #0003;width:min(420px,100%);max-width:100%;padding:20px 22px} +.dlg-box p{margin:0 0 18px;font-size:13px;line-height:1.6;color:#333;white-space:pre-wrap;word-break:break-word} +.dlg-box input{width:100%;border:1px solid var(--line);border-radius:4px;height:32px;padding:0 9px;font-size:13px} +.dlg-box input:focus{outline:1px solid var(--blue)} +.dlg-actions{display:flex;justify-content:flex-end;gap:10px} +.dlg-actions .button{margin-left:0} + diff --git a/app/page.tsx b/app/page.tsx index fdf6637..81eea3a 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -322,7 +322,7 @@ export default function Home(){ }; 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))); @@ -505,12 +505,13 @@ function Projects({view,setView,sites,loading,go,loadSite,notify,onDelete}:{view return {all,draft,running,done,partial}; },[sites]); const [selected,setSelected] = useState>(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
加载中…
; - return <>
{[[String(counts.all),"全部项目"],[String(counts.draft),"草稿"],[String(counts.running),"生成中"],[String(counts.done),"生成完成"],[String(counts.partial),"部分失败"]].map(x=>
{x[0]}{x[1]}
)}
{selected.size>0 && }
{view==="table"?{sites.map(row=>)}
站点名称 ↕所属领域设备数量MU 页面Cube 页面状态最近修改操作
toggle(row.id)}/>{row.domain}{row.device_count}{row.mu_count}{row.cube_count}{row.status==="generated"?"生成完成":row.status==="partial"?"部分失败":row.status==="running"?"生成中":"草稿"}{row.updated_at}
:
{sites.map(row=>
go(row.status==="generated"?"result":"basic", row.id)}>
⬡{row.status==="generated"?"生成完成":row.status==="partial"?"部分失败":row.status==="running"?"生成中":"草稿"}

{row.name || "未命名站点"}

{row.domain}

{row.device_count} 设备{row.mu_count} MU{row.cube_count} Cube
更新于 {row.updated_at}
)}
}
共 {sites.length} 条
+ return <>{confirmEl}
{[[String(counts.all),"全部项目"],[String(counts.draft),"草稿"],[String(counts.running),"生成中"],[String(counts.done),"生成完成"],[String(counts.partial),"部分失败"]].map(x=>
{x[0]}{x[1]}
)}
{selected.size>0 && }
{view==="table"?{sites.map(row=>)}
站点名称 ↕所属领域设备数量MU 页面Cube 页面状态最近修改操作
toggle(row.id)}/>{row.domain}{row.device_count}{row.mu_count}{row.cube_count}{row.status==="generated"?"生成完成":row.status==="partial"?"部分失败":row.status==="running"?"生成中":"草稿"}{row.updated_at}
:
{sites.map(row=>
go(row.status==="generated"?"result":"basic", row.id)}>
⬡{row.status==="generated"?"生成完成":row.status==="partial"?"部分失败":row.status==="running"?"生成中":"草稿"}

{row.name || "未命名站点"}

{row.domain}

{row.device_count} 设备{row.mu_count} MU{row.cube_count} Cube
更新于 {row.updated_at}
)}
}
共 {sites.length} 条
} function Basic({site,domains,onChange,onAddDomain,onUpload,saving,notify,onViewCompletePrompt}:{site:SiteDetail|null;domains:string[];onChange:(p:Partial)=>void;onAddDomain:(n:string)=>void;onUpload:(f:File)=>void;saving:boolean;notify:(m:string)=>void;onViewCompletePrompt:()=>void}){ @@ -985,6 +986,37 @@ function Preview({site,tab,setTab,setModal,notify}:{site:SiteDetail|null;tab:"in 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(resolve=>setState({open:true,message,resolve})); + const close=(v:boolean)=>{ state.resolve?.(v); setState(s=>({...s,open:false})); }; + const el = state.open ? ( +
close(false)}> +
e.stopPropagation()}> +

{state.message}

+
+
+
+ ) : 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(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 ? ( +
close(null)}> +
e.stopPropagation()}> +

{state.message}

+ setState(s=>({...s,value:e.target.value}))} onKeyDown={e=>{ if(e.key==="Enter") close(state.value); if(e.key==="Escape") close(null); }}/> +
+
+
+ ) : 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([]); const [loading,setLoading]=useState(true); @@ -995,7 +1027,8 @@ function DomainLibrary({go,notify,onOpen,domainFilter,domainOptions}:{go:(p:Page 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?({sort.dir===1?"▲":"▼"}):null; @@ -1014,6 +1047,7 @@ function DomainLibrary({go,notify,onOpen,domainFilter,domainOptions}:{go:(p:Page if(avbv) return 1*sort.dir; return 0; }); return <> + {confirmEl}
DOMAIN SOLUTION LIBRARY

领域方案库

沉淀各领域标准方案,供客户站点直接使用或快速微调。

{domainOptions.map(d=>
{list.filter(t=>t.domain_id===d.id).length}{d.name}
)}
@@ -1104,15 +1138,16 @@ function DeviceTemplateCard({dev,notify,onChange,onBack}:{dev:any;notify:(m:stri const [imgPreview,setImgPreview]=useState(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((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
{name||"(未命名)"}
@@ -1145,6 +1180,7 @@ function DeviceTemplateCard({dev,notify,onChange,onBack}:{dev:any;notify:(m:stri
+ {confirmEl} ; } @@ -1170,8 +1206,10 @@ function DeviceTemplates({notify}:{notify:(m:string)=>void}){ 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}
DEVICE TEMPLATE LIBRARY

设备模版

独立维护设备模版及其采集点,编辑后可在领域方案中按需选用。

setOpenId("create")}>+ 新增设备模版}>
setQ(e.target.value)}/>
@@ -1230,9 +1268,10 @@ function DeviceTemplatePicker({tmplId,go,selectedIds,notify,onChange}:{tmplId:nu 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
{name}{editing? <> : <> }
+ const del=async()=>{ if(!(await confirm("删除该页面?"))) return; await fetchDb(`/domain-pages/${p.id}`,{method:"DELETE"}); onChange(); }; + return
{confirmEl}
{name}{editing? <> : <> }
{editing &&
@@ -1248,6 +1287,8 @@ function DomainDetail({id,go,notify,onBack,onCreated}:{id:number|null;go:(p:Page const [t,setT]=useState(null); const [loading,setLoading]=useState(false); const [m,setM]=useState(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]); @@ -1258,10 +1299,11 @@ function DomainDetail({id,go,notify,onBack,onCreated}:{id:number|null;go:(p:Page 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}
@@ -1294,14 +1336,15 @@ function DomainDetail({id,go,notify,onBack,onCreated}:{id:number|null;go:(p:Page function PageStyleLibrary({pageStyles, loadPageStyles, notify}:{pageStyles:PageStyle[]; loadPageStyles:()=>Promise; notify:(m:string)=>void}){ const [editor,setEditor]=useState<{open:boolean; id?:number}>({open:false}); const [preview,setPreview]=useState(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&&setPreview(null)}/>} + return <>{confirmEl}{preview&&setPreview(null)}/>} setEditor({open:true})}>+ 新增样式}> {pageStyles.length===0 ?

暂无页面样式,点击右上角「+ 新增样式」上传 JSON 与预览图即可创建。

@@ -1329,6 +1372,7 @@ function StylePreview({style,onClose}:{style:PageStyle;onClose:()=>void}){ } function StyleEditorModal({id,notify,afterChange,onClose}:{id?:number;notify:(m:string)=>void;afterChange:(msg:string)=>Promise;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); @@ -1358,11 +1402,11 @@ function StyleEditorModal({id,notify,afterChange,onClose}:{id?:number;notify:(m: }; 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
e.stopPropagation()}> + return
{confirmEl}
e.stopPropagation()}>

{id?"编辑页面样式":"新增页面样式"}

页面样式包含一份 Cube 仪表盘 JSON(配色与视觉主题)和一张预览图。名称用于在样式库中展示。