From: root Date: Fri, 11 Sep 2026 03:56:08 +0000 (+0800) Subject: 站点列表支持新建子站点(递归)与折叠收起,后端 sites 增加 parent_id X-Git-Url: http://acesimba.cloud/gitweb/?a=commitdiff_plain;h=86f0c20667c116cc1478e9479555e7abd4559c5a;p=StationBuilder.git 站点列表支持新建子站点(递归)与折叠收起,后端 sites 增加 parent_id --- diff --git a/app/page.tsx b/app/page.tsx index 9e845e9..67135cf 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -47,7 +47,7 @@ const MODELS = [ ]; // ---- types ---- -type SiteSummary = { id:number; name:string; domain:string; status:string; device_count:number; mu_count:number; cube_count:number; gen_count:number; updated_at:string; created_at:string; }; +type SiteSummary = { id:number; name:string; domain:string; status:string; device_count:number; mu_count:number; cube_count:number; gen_count:number; updated_at:string; created_at:string; parent_id?:number|null; }; type Asset = { id?:number; type:string; count:number; rule:string; sort_order:number; }; type Cube = { id?:number; name:string; content:string; status?:string; gen_file_id?:number|null; gen_filename?:string; generated_at?:string; prompt?:string; sort_order:number; }; type MenuItem = { id?:number; parent_id:number|null; label:string; page_type:"mu"|"cube"|""; ref_id?:number|string; sort_order:number; }; @@ -403,6 +403,14 @@ export default function Home(){ setSiteId(data.id); return data.id; }; + const createSubSite=async(parentId:number)=>{ + const parent = sites.find(s=>s.id===parentId); + const data:SiteDetail = await fetchDb("/sites", {method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify({name:"", domain:parent?.domain||"", description:"", supplement:"", parent_id:parentId})}); + setSites(prev=>[data as any, ...prev]); + setSite(data); + setSiteId(data.id); + setPage("basic"); window.history.replaceState({}, "", "?page=basic&id="+data.id); + }; const deleteSites=async(ids:number[])=>{ if(!ids.length) return; if(!(await confirm(`确认删除选中的 ${ids.length} 个站点?该操作不可恢复,相关资产、页面与生成文件将一并删除。`))) return; @@ -644,7 +652,7 @@ export default function Home(){ )}
iStation StationBuilder / {["domains","domainDetail","styleLibrary","deviceTemplates","toolSettings","domainSettings"].includes(page)?meta[page].label:page==="projects"?"站点列表":page==="siteEdit"?`站点管理 / 站点编辑 / ${siteEditMeta.label}`:`站点详情 / ${meta[page].label}`}

{page==="siteEdit"?siteEditMeta.label:meta[page].label}

{page==="siteEdit"?siteEditMeta.subtitle:meta[page].subtitle}

{page==="projects"&&}{page==="siteEdit"&&}{page==="domains"&&openDomainTemplateId==null&&}{page==="domains"&&openDomainTemplateId!=null&&}{domainFilter!=null&&page==="domains"&&openDomainTemplateId==null&&}{page==="domainSettings"&&}
{page==="siteEdit"&&} {SITE_EDIT_STEPS.includes(page as SiteEditSection)&&}
- {page==="projects"&&} + {page==="projects"&&} {page==="dashboard"&&} {page==="report-dashboard"&&} {page==="report-doc"&&} @@ -709,7 +717,7 @@ function SiteEditTabs({section,onChange}:{section:SiteEditSection;onChange:(sect return ; } -function Projects({view,setView,sites,loading,go,loadSite,notify,onDelete}:{view:"table"|"card";setView:(v:"table"|"card")=>void;sites:SiteSummary[];loading:boolean;go:(p:PageKey, sid?:number|"new")=>void;loadSite:(id:number)=>Promise;notify:(m:string)=>void;onDelete:(ids:number[])=>Promise}){ +function Projects({view,setView,sites,loading,go,loadSite,notify,onDelete,onCreateSub}:{view:"table"|"card";setView:(v:"table"|"card")=>void;sites:SiteSummary[];loading:boolean;go:(p:PageKey, sid?:number|"new")=>void;loadSite:(id:number)=>Promise;notify:(m:string)=>void;onDelete:(ids:number[])=>Promise;onCreateSub:(parentId:number)=>Promise}){ const counts = useMemo(()=>{ const all = sites.length; const draft = sites.filter(s=>s.status==="draft").length; @@ -732,6 +740,58 @@ function Projects({view,setView,sites,loading,go,loadSite,notify,onDelete}:{view const batchDelete = async ()=>{ await onDelete([...selected]); setSelected(new Set()); }; const statusText=(status:string)=>status==="generated"?"生成完成":status==="partial"?"部分失败":status==="running"?"生成中":"草稿"; const openProject=(row:SiteSummary)=>go("siteEdit",row.id); + const [collapsed,setCollapsed]=useState>(new Set()); + const toggleCollapse=(id:number)=>setCollapsed(prev=>{ const n=new Set(prev); if(n.has(id)) n.delete(id); else n.add(id); return n; }); + const childMap=useMemo(()=>{ + const m=new Map(); + for(const s of sites){ const p=Number(s.parent_id)||0; if(!m.has(p)) m.set(p,[]); m.get(p)!.push(s); } + for(const arr of m.values()) arr.sort((a,b)=>(b.updated_at||"").localeCompare(a.updated_at||"")); + return m; + },[sites]); + const matchesFilter=(s:SiteSummary)=>(!query||(s.name||"").toLowerCase().includes(query.toLowerCase()))&&(!domain||s.domain===domain)&&(!statusFilter||s.status===statusFilter); + const visibleIds=useMemo(()=>{ + const set=new Set(); + const dfs=(s:SiteSummary):boolean=>{ + const kids=childMap.get(s.id)||[]; + let visible=matchesFilter(s); + for(const k of kids) if(dfs(k)) visible=true; + if(visible) set.add(s.id); + return visible; + }; + for(const s of (childMap.get(0)||[])) dfs(s); + return set; + },[childMap,query,domain,statusFilter]); + const buildRows=(pid:number, depth:number):React.ReactNode[]=>{ + const kids=(childMap.get(pid)||[]).filter(s=>visibleIds.has(s.id)); + const out:React.ReactNode[]=[]; + for(const row of kids){ + const rowKids=(childMap.get(row.id)||[]).filter(s=>visibleIds.has(s.id)); + const hasKids=rowKids.length>0; + const isCollapsed=collapsed.has(row.id); + out.push( + setPreview(row)}> + e.stopPropagation()}>toggle(row.id)}/> + + {hasKids&&{e.stopPropagation();toggleCollapse(row.id);}} aria-label={isCollapsed?"展开":"收起"} icon={isCollapsed?:}/>} + + + {row.domain||"—"} + {row.device_count} + {row.mu_count} + {row.cube_count} + {statusText(row.status)} + {row.updated_at} + e.stopPropagation()}>}, + {key:"sub",label:"新建子站点",icon:}, + {key:"delete",label:"删除",danger:true,icon:} + ],onClick:({key})=>key==="open"?openProject(row):key==="sub"?onCreateSub(row.id):onDelete([row.id])}}>}/> + + ); + if(hasKids && !isCollapsed) out.push(...buildRows(row.id, depth+1)); + } + return out; + }; if(loading) return
加载中…
; return <>{confirmEl}
@@ -749,9 +809,9 @@ function Projects({view,setView,sites,loading,go,loadSite,notify,onDelete}:{view
{selected.size>0&&}
- {filtered.map(row=>setPreview(row)}>)}
站点名称所属领域设备数量MU 页面Cube 页面状态最近修改操作
e.stopPropagation()}>toggle(row.id)}/>{row.domain||"—"}{row.device_count}{row.mu_count}{row.cube_count}{statusText(row.status)}{row.updated_at}e.stopPropagation()}>},{key:"delete",label:"删除",danger:true,icon:}],onClick:({key})=>key==="open"?openProject(row):onDelete([row.id])}}>}/>
- {filtered.length===0&&
没有符合条件的站点
} -
{filtered.length} / 总数
+ {buildRows(0,0)}
站点名称所属领域设备数量MU 页面Cube 页面状态最近修改操作
+ {visibleIds.size===0&&
没有符合条件的站点
} +
{visibleIds.size} / 总数
setPreview(null)} extra={preview&&}> {preview&&
{statusText(preview.status)}

基础信息

所属领域
{preview.domain||"—"}
设备数量
{preview.device_count}
MU 页面
{preview.mu_count}
Cube 页面
{preview.cube_count}
生成文件
{preview.gen_count}
最近修改
{preview.updated_at}
创建时间
{preview.created_at}
} diff --git a/data/db/StationBuilder.sqlite b/data/db/StationBuilder.sqlite index a8677d5..6ee0bcb 100644 Binary files a/data/db/StationBuilder.sqlite and b/data/db/StationBuilder.sqlite differ diff --git a/db-sidecar.mjs b/db-sidecar.mjs index 6005ac0..1b4cb79 100644 --- a/db-sidecar.mjs +++ b/db-sidecar.mjs @@ -363,6 +363,7 @@ function ensureColumn(table, column, def) { } ensureColumn("cubes", "prompt", "TEXT"); ensureColumn("sites", "style_id", "INTEGER"); +ensureColumn("sites", "parent_id", "INTEGER"); ensureColumn("domain_templates", "supplement", "TEXT"); ensureColumn("domain_templates", "style_id", "INTEGER"); ensureColumn("domain_pages", "status", "TEXT DEFAULT 'pending'"); @@ -2568,7 +2569,7 @@ async function route(req, res) { if (req.method === "GET") return respondJson(res, 200, siteList()); if (req.method === "POST") { const body = await parseBody(req); - const info = db.prepare("INSERT INTO sites (name, domain, description, supplement, status) VALUES (?,?,?,?,'draft')").run(body.name || "", body.domain || "", body.description || "", body.supplement || ""); + const info = db.prepare("INSERT INTO sites (name, domain, description, supplement, status, parent_id) VALUES (?,?,?,?,'draft',?)").run(body.name || "", body.domain || "", body.description || "", body.supplement || "", body.parent_id != null ? Number(body.parent_id) : null); // insert default empty assets const siteId = info.lastInsertRowid; const insertAsset = db.prepare("INSERT INTO assets (site_id, type, count, rule, sort_order) VALUES (?,?,?,?,?)"); @@ -2594,9 +2595,10 @@ async function route(req, res) { } if (req.method === "PUT") { const body = await parseBody(req); - db.prepare("UPDATE sites SET name=?, domain=?, description=?, supplement=?, style_id=?, updated_at=CURRENT_TIMESTAMP WHERE id=?").run( + db.prepare("UPDATE sites SET name=?, domain=?, description=?, supplement=?, style_id=?, parent_id=?, updated_at=CURRENT_TIMESTAMP WHERE id=?").run( body.name ?? "", body.domain ?? "", body.description ?? "", body.supplement ?? "", - body.style_id != null ? Number(body.style_id) : null, siteId + body.style_id != null ? Number(body.style_id) : null, + body.parent_id != null ? Number(body.parent_id) : null, siteId ); return respondJson(res, 200, hydrateSite(siteId)); }