From: wang.yu Date: Sat, 15 Aug 2026 09:20:43 +0000 (+0800) Subject: feat: 新建站点向导 + 生成提示词可查 + 站点列表多选删除 X-Git-Url: http://acesimba.cloud/gitweb/?a=commitdiff_plain;h=f65579148e79eb7d6fbf7dcd1a20e4592b1c6e0e;p=StationBuilder.git feat: 新建站点向导 + 生成提示词可查 + 站点列表多选删除 新建站点全流程(基本属性 / 资产配置 / 功能与菜单 / 生成确认 / 结果 / 预览), 数据持久化到 SQLite(新增 db-sidecar 后端 + /api/db 代理路由): - 新建站点向导:Basic 基本信息(名称≤20字、领域可扩充下拉)、Assets 设备资产配置 (默认 4 行占位、资产层级预览)、Functions(MU 标准页树选 + Cube 扩展页编辑 + 菜单结构)、Generate 确认(模型准备 + 各 Cube + 打包校验)、Result/Preview。 - 生成前可查看组合提示词:每个 Cube 页面 + 站点级通用上下文(站点背景 + 完整 Cube 规则 spec),便于在点击“开始生成”前核对、定位生成质量问题。 - 站点列表:Asset 列改为“设备数量”(资产 count 求和);删除“创建人”列及对应筛选; “操作”列改为删除按钮;新增多选勾选 + 批量删除(DELETE /api/sites,级联清理 资产/页面/生成文件并删除生成目录)。 已知状态 / AI 创建功能说明: Cube JSON 生成已通过 codebuddy 调用 dashboard-json-generator Skill 实现,并带 JSON 结构自校验(最多 3 次自动修复重试)。但目前生成质量尚未达到预期(即“还没有成功”), 本次新增的“提示词可查”能力正是为此而加,用于排查与迭代;生成效果仍需进一步调优与 验证,不建议直接当作最终交付物使用。 --- diff --git a/app/api/db/[[...slug]]/route.ts b/app/api/db/[[...slug]]/route.ts new file mode 100644 index 0000000..21264c8 --- /dev/null +++ b/app/api/db/[[...slug]]/route.ts @@ -0,0 +1,39 @@ +import { type NextRequest } from "next/server"; + +const SIDECAR = "http://127.0.0.1:37823"; + +async function proxy(req: NextRequest, { params }: { params: Promise<{ slug?: string[] }> }) { + const { slug } = await params; + const suffix = slug ? "/" + slug.join("/") : ""; + const target = `${SIDECAR}/api${suffix}${req.nextUrl.search}`; + + const headers = new Headers(); + const ct = req.headers.get("content-type"); + if (ct) headers.set("Content-Type", ct); + + const upstream = await fetch(target, { + method: req.method, + headers, + body: req.method !== "GET" && req.method !== "HEAD" ? req.body : undefined, + // @ts-expect-error duplex is required when forwarding ReadableStream bodies + duplex: "half", + }); + + const responseHeaders = new Headers(); + const contentType = upstream.headers.get("content-type"); + if (contentType) responseHeaders.set("Content-Type", contentType); + const cd = upstream.headers.get("content-disposition"); + if (cd) responseHeaders.set("Content-Disposition", cd); + + return new Response(upstream.body, { + status: upstream.status, + statusText: upstream.statusText, + headers: responseHeaders, + }); +} + +export const GET = proxy; +export const POST = proxy; +export const PUT = proxy; +export const DELETE = proxy; +export const OPTIONS = proxy; diff --git a/app/globals.css b/app/globals.css index 8fbf53e..8cb2947 100644 --- a/app/globals.css +++ b/app/globals.css @@ -51,3 +51,80 @@ .ai-dialog .ai-send:disabled{opacity:.5;cursor:not-allowed} .ai-dialog .ai-send:not(:disabled):hover{background:#008ac4} +/* ===== 新建站点向导:基本属性 ===== */ +.domain-select{display:flex;align-items:center;gap:8px} +.domain-select input{flex:1;border:1px solid var(--line);border-radius:3px;height:30px;padding:0 9px;background:#fff} +.domain-select input:focus{border-color:var(--blue);outline:none} +.domain-add{display:flex;align-items:center;gap:8px;margin-top:9px} +.domain-add input{flex:1;border:1px solid var(--line);border-radius:3px;height:30px;padding:0 9px;background:#fff} +.domain-add input:focus{border-color:var(--blue);outline:none} +.file-list{list-style:none;margin:10px 0 0;padding:0;display:flex;flex-direction:column;gap:6px} +.file-list li{display:flex;align-items:center;gap:6px;background:var(--soft);border:1px solid var(--line);border-radius:4px;padding:6px 9px;font-size:12px} +.file-list a{color:var(--blue);text-decoration:none} +.file-list a:hover{text-decoration:underline} +.file-list small{color:var(--muted);margin-left:auto} +.invalid{border-color:var(--red)!important;background:#fff4f2!important} +.hint{color:var(--muted);font-size:11px;margin:0 0 10px;line-height:1.6} + +/* ===== 可编辑表格内的输入控件 ===== */ +.data-table input,.data-table select,.data-table textarea{border:1px solid #d9dee2;border-radius:3px;height:28px;padding:0 8px;width:100%;background:#fff;font-size:12px} +.data-table input:focus,.data-table select:focus,.data-table textarea:focus{border-color:var(--blue);outline:none} +.data-table .number{width:90px;text-align:right} +.data-table input::placeholder{color:#b9bfc3} + +/* ===== Assets 资产配置 ===== */ +.empty-row td{background:#fafbfc} +.empty-row input{color:#b7bcc0;background:#fafbfc} +.empty-row input::placeholder{color:#c3c8cc} +.asset-tree-preview{background:#f5f8f9;border:1px solid #e7ebed;border-radius:6px;padding:12px 14px;font-size:12px} +.tree-root{font-weight:bold;color:#33414a;margin-bottom:8px;display:flex;align-items:center;gap:5px} +.tree-branch{margin:4px 0 4px 14px;padding-left:12px;border-left:1px dashed #cdd5d9} +.tree-node{display:flex;align-items:center;gap:6px;font-weight:bold;color:#3a4750} +.tree-count{background:var(--pale);color:var(--blue2);border-radius:9px;padding:0 7px;font-size:10px;font-weight:normal} +.tree-leaves{color:#788187;font-size:11px;line-height:1.7;margin:3px 0 0 20px;word-break:break-all} + +/* ===== Functions 功能与菜单 ===== */ +.mu-tree{display:flex;flex-direction:column;gap:14px} +.mu-group>b{display:block;color:var(--blue2);font-size:13px;margin-bottom:6px} +.mu-items{display:grid;grid-template-columns:repeat(3,1fr);gap:7px;padding-left:4px} +.mu-items label{display:flex;align-items:flex-start;gap:7px;border:1px solid var(--line);border-radius:5px;padding:7px 9px;background:#fff;cursor:pointer;font-size:12px} +.mu-items label:hover{border-color:#9ed3ec;background:#f4fafe} +.mu-items label.disabled{opacity:.5;cursor:not-allowed;background:#f6f7f8} +.mu-items label input{margin-top:2px;accent-color:var(--blue)} +.mu-items label span{display:flex;flex-direction:column;line-height:1.3} +.mu-items label small{color:var(--muted);font-size:10px} +.cube-editor table{width:100%} +.cube-editor input{width:100%} +.cube-actions{display:flex;gap:5px;white-space:nowrap} +.cube-actions button{border:1px solid var(--line);background:#fff;border-radius:4px;height:26px;padding:0 9px;font-size:11px;color:#444} +.cube-actions button:hover{border-color:var(--blue);color:var(--blue)} +.cube-actions .trash{border-color:#f0c4bd;color:var(--red)} +.cube-actions .trash:hover{background:#fdeae6;border-color:var(--red)} +.menu-editor{display:flex;flex-direction:column;gap:10px} +.menu-row{display:grid;grid-template-columns:1.4fr 1fr 1.4fr auto;gap:8px;align-items:center} +.menu-row input,.menu-row select{border:1px solid var(--line);border-radius:3px;height:30px;padding:0 8px;background:#fff;width:100%} +.menu-row .trash{border:1px solid #f0c4bd;background:#fff;color:var(--red);border-radius:4px;height:30px;width:34px;font-size:15px} +.menu-row .trash:hover{background:#fdeae6;border-color:var(--red)} + +/* ===== Generate 预览 line 面板 ===== */ +.line-panel{background:#fff;border:1px solid var(--line);border-radius:6px;padding:11px 13px;display:flex;flex-direction:column;gap:8px} +.line-panel h3{margin:0;font-size:13px;display:flex;align-items:center;justify-content:space-between} +.line-panel h3 span{color:var(--muted);font-size:10px;font-weight:normal} + +/* ===== 生成提示词查看 ===== */ +.prompt-list{display:flex;flex-direction:column;gap:8px} +.prompt-row{display:flex;align-items:center;justify-content:space-between;gap:10px;border:1px solid var(--line);border-radius:5px;padding:8px 11px;background:#fff} +.prompt-row span{display:flex;flex-direction:column;line-height:1.35;min-width:0} +.prompt-row span small{color:var(--muted);font-size:10px;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:420px} +.prompt-row b{font-size:13px} +.prompt-view{margin:0;background:#0f1115;color:#e6edf3;border:1px solid #2a2f36;border-radius:6px;padding:14px;font-family:"SFMono-Regular",Consolas,"Liberation Mono",Menlo,monospace;font-size:11.5px;line-height:1.6;white-space:pre-wrap;word-break:break-word;max-height:60vh;overflow:auto} + +/* ===== 站点列表:多选与删除 ===== */ +.data-table input[type=checkbox]{width:16px;height:16px;accent-color:var(--blue);cursor:pointer;vertical-align:middle} +.data-table thead th:first-child{width:34px;text-align:center} +.link.danger{color:var(--red)} +.link.danger:hover{text-decoration:underline} +.project-grid article{position:relative} +.card-del{position:absolute;top:8px;right:8px;border:1px solid #f0c4bd;background:#fff;color:var(--red);border-radius:4px;height:26px;padding:0 9px;font-size:11px;cursor:pointer} +.card-del:hover{background:#fdeae6;border-color:var(--red)} + diff --git a/app/page.tsx b/app/page.tsx index 08fdab1..b864888 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,37 +2,24 @@ import { useEffect, useMemo, useRef, useState } from "react"; -type PageKey = "login" | "projects" | "basic" | "assets" | "functions" | "ai" | "generate" | "result" | "preview" | "domains" | "domainDetail"; -type ModalKey = "" | "ai" | "feedback" | "download" | "history"; +type PageKey = "login" | "projects" | "basic" | "assets" | "functions" | "generate" | "result" | "preview" | "domains" | "domainDetail"; +type ModalKey = "" | "feedback" | "download" | "history" | "prompt"; + const meta: Record = { login:{label:"内部登录",icon:"◇",subtitle:"使用公司账号登录"}, projects:{label:"站点项目",icon:"▦",subtitle:"创建、查找和管理站点生成项目"}, basic:{label:"站点基本属性",icon:"▤",subtitle:"描述站点业务背景,信息越完整,生成结果越符合预期"}, assets:{label:"Asset 资产设置",icon:"▧",subtitle:"配置设备类型、数量和资产层级,为页面和模拟数据提供基础"}, functions:{label:"功能与菜单",icon:"☷",subtitle:"选择 MU 标准页面,定义 Cube 扩展页面及菜单结构"}, - ai:{label:"AI 配置",icon:"✦",subtitle:"选择 Wordbuddy 模型并配置站点生成策略"}, generate:{label:"生成确认",icon:"↻",subtitle:"确认生成范围,调用 Cube Skill 生成页面和模拟数据"}, result:{label:"站点结果",icon:"◫",subtitle:"集中查看、预览、校验并下载站点交付物"}, preview:{label:"Cube 页面预览",icon:"▣",subtitle:"检查单个 Cube 页面效果,并通过反馈快速重新生成"}, domains:{label:"领域方案库",icon:"◆",subtitle:"沉淀各领域标准方案,供客户站点直接使用或快速微调"}, domainDetail:{label:"水务 · 自来水厂方案",icon:"◇",subtitle:"查看领域资产、运维流程、Cube 页面、菜单及知识内容"}, }; -const nav: PageKey[] = ["projects","domains","basic","assets","functions","ai","generate","result","preview"]; -const projects = [ - ["苏州工业园能源站","工业园区","25","6","3","生成完成","2026/08/14 11:20","王宇"], - ["上海数据中心 A 区","数据中心","48","8","4","草稿","2026/08/14 09:42","陈晨"], - ["滨江智慧楼宇","智慧楼宇","32","5","2","生成中","2026/08/13 17:08","李静"], - ["无锡装备工厂","工业制造","16","6","3","部分失败","2026/08/13 15:31","周扬"], - ["青岛水务示范站","智慧水务","61","7","5","生成完成","2026/08/12 16:03","王宇"], -]; -const startAssets = [ - {type:"冷水机组",count:3,rule:"CH-##",parent:"制冷系统",area:"动力中心"}, - {type:"循环水泵",count:6,rule:"PUMP-##",parent:"水循环系统",area:"泵房"}, - {type:"配电柜",count:4,rule:"PDC-##",parent:"配电系统",area:"一层配电室"}, - {type:"智能电表",count:12,rule:"METER-##",parent:"配电系统",area:"各用电区域"}, -]; -// Wordbuddy 支持的模型。只有 rate 为 "0.00x"(免费) 的模型可选,其余灰显。 +const nav: PageKey[] = ["projects","domains","basic","assets","functions","generate","result","preview"]; + const MODELS = [ { name: "Auto", tag: "自动选择", rate: "—" }, { name: "Hy3", tag: "免费", rate: "0.00x" }, @@ -45,20 +32,229 @@ const MODELS = [ { name: "Kimi-K3", tag: "订阅优先", rate: "1.62x" }, ]; -function Status({children}:{children:React.ReactNode}){const v=String(children);const tone=/完成|正常|成功|通过|推荐/.test(v)?"ok":/生成中|校验中|告警/.test(v)?"warn":/失败|异常/.test(v)?"bad":"muted";return {children}} -function Button({children,secondary,onClick,disabled}:{children:React.ReactNode;secondary?:boolean;onClick?:()=>void;disabled?:boolean}){return } +// ---- types ---- +type SiteSummary = { id:number; name:string; domain:string; status:string; device_count:number; mu_count:number; cube_count:number; updated_at:string; created_at:string; }; +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; }; +type ReferenceFile = { id:number; filename:string; mime:string; size:number; created_at:string; }; +type SiteDetail = { + id:number; name:string; domain:string; description:string; supplement:string; status:string; + created_at:string; updated_at:string; + assets: Asset[]; cubes: Cube[]; mu: string[]; menu: MenuItem[]; files: ReferenceFile[]; +}; +type MuTab = string | { label:string; disabled?:boolean; note?:string }; +type MuNode = { key:string; children?:MuNode[]; tabs?:MuTab[] }; + +// ---- helpers ---- +function Status({children}:{children:React.ReactNode}){const v=String(children);const tone=/完成|正常|成功|通过|推荐|generated/.test(v)?"ok":/生成中|校验中|告警|running/.test(v)?"warn":/失败|异常|failed/.test(v)?"bad":"muted";return {children}} +function Button({children,secondary,onClick,disabled,title}:{children:React.ReactNode;secondary?:boolean;onClick?:()=>void;disabled?:boolean;title?:string}){return } function Panel({title,tools,children,className=""}:{title?:string;tools?:React.ReactNode;children:React.ReactNode;className?:string}){return
{title&&
{title}
{tools}
}
{children}
} function Field({label,required,hint,children}:{label:string;required?:boolean;hint?:string;children:React.ReactNode}){return } +async function fetchDb(path:string, opts?:RequestInit){ + const res = await fetch(`/api/db${path}`, opts); + if(!res.ok){ + const text = await res.text().catch(()=>""); + throw new Error(text || `HTTP ${res.status}`); + } + const ct = res.headers.get("content-type") || ""; + if(ct.includes("application/json")) return res.json(); + return res; +} + +function downloadBlob(blob:Blob, filename:string){ + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; a.download = filename; a.click(); + URL.revokeObjectURL(url); +} + +function formatInstances(rule:string, count:number){ + if(!rule || count<=0) return ""; + const pad = Math.max(2, String(count).length); + const parts:string[] = []; + for(let i=1;i<=count;i++){ + const num = String(i).padStart(pad,"0"); + parts.push(rule.replace(/#/g, num)); + } + return parts.join("、"); +} + +function buildMuLeaves(nodes:MuNode[], parent=""): {key:string, label:string, disabled?:boolean, note?:string, parentMenu:string}[] { + const out:{key:string,label:string,disabled?:boolean,note?:string,parentMenu:string}[] = []; + for(const n of nodes){ + const myParent = parent ? `${parent}/${n.key}` : n.key; + if(n.tabs && n.tabs.length){ + for(const t of n.tabs){ + if(typeof t === "string") out.push({key:`${myParent}/${t}`, label:t, parentMenu:parent||n.key}); + else out.push({key:`${myParent}/${t.label}`, label:t.label, disabled:t.disabled, note:t.note, parentMenu:parent||n.key}); + } + } else if(n.children){ + out.push(...buildMuLeaves(n.children, myParent)); + } else { + out.push({key:myParent, label:n.key, parentMenu:parent||""}); + } + } + return out; +} + +function countChars(v:string){ return Array.from(v||"").length; } + +// ---- main ---- export default function Home(){ - const [page,setPage]=useState("projects"); const [toast,setToast]=useState(""); const [modal,setModal]=useState(""); const [model,setModel]=useState("Hy3"); const [modelOpen,setModelOpen]=useState(false); - const [aiOpen,setAiOpen]=useState(false); const [aiMessages,setAiMessages]=useState<{role:"user"|"assistant";content:string}[]>([]); const [aiDraft,setAiDraft]=useState(""); const [aiBusy,setAiBusy]=useState(false); const aiEndRef=useRef(null); - const [view,setView]=useState<"table"|"card">("table"); const [assets,setAssets]=useState(startAssets); const [functionTab,setFunctionTab]=useState<"mu"|"cube"|"menu">("mu"); - const [previewTab,setPreviewTab]=useState<"info"|"json"|"feedback">("info"); const [selectedMu,setSelectedMu]=useState(["设备总览","实时监控","告警中心","能耗分析"]); const [progress,setProgress]=useState(0); - const totalAssets=useMemo(()=>assets.reduce((n,item)=>n+Number(item.count),0),[assets]); - const go=(target:PageKey)=>{setPage(target);window.history.replaceState({},"","?page="+target)}; const notify=(m:string)=>{setToast(m);window.setTimeout(()=>setToast(""),2200)}; - const generate=()=>{setProgress(10);let v=10;const timer=window.setInterval(()=>{v+=15;setProgress(Math.min(v,100));if(v>=100){window.clearInterval(timer);notify("站点方案生成完成")}},360)}; + const [page,setPage]=useState("projects"); + const [toast,setToast]=useState(""); + const [modal,setModal]=useState(""); + const [promptData,setPromptData]=useState<{name:string;text:string}|null>(null); + const [model,setModel]=useState("Hy3"); + const [modelOpen,setModelOpen]=useState(false); + const [aiOpen,setAiOpen]=useState(false); + const [aiMessages,setAiMessages]=useState<{role:"user"|"assistant";content:string}[]>([]); + const [aiDraft,setAiDraft]=useState(""); + const [aiBusy,setAiBusy]=useState(false); + const aiEndRef=useRef(null); + + const [sites,setSites]=useState([]); + const [siteId,setSiteId]=useState(null); + const [site,setSite]=useState(null); + const [loading,setLoading]=useState(true); + const [saving,setSaving]=useState(false); + const [domains,setDomains]=useState([]); + const [muTree,setMuTree]=useState([]); + + const [view,setView]=useState<"table"|"card">("table"); + const [functionTab,setFunctionTab]=useState<"mu"|"cube"|"menu">("mu"); + const [previewTab,setPreviewTab]=useState<"info"|"json"|"feedback">("info"); + + const notify=(m:string)=>{setToast(m);window.setTimeout(()=>setToast(""),2200);}; + + const viewPrompt=async(cubeId:number,name:string)=>{ + const sid = site?.id ?? siteId; + if(sid==null) { notify("请先创建或打开一个站点"); return; } + try{ + const data:{prompt:string} = await fetchDb(`/sites/${sid}/cubes/${cubeId}/prompt`); + setPromptData({name, text:data.prompt}); + setModal("prompt"); + }catch(e:any){ notify("读取提示词失败: "+e.message); } + }; + const viewSitePrompt=async()=>{ + const sid = site?.id ?? siteId; + if(sid==null) { notify("请先创建或打开一个站点"); return; } + try{ + const data:{prompt:string} = await fetchDb(`/sites/${sid}/prompt`); + setPromptData({name:"站点级提示词(通用上下文)", text:data.prompt}); + setModal("prompt"); + }catch(e:any){ notify("读取提示词失败: "+e.message); } + }; + useEffect(()=>{ if(aiEndRef.current) aiEndRef.current.scrollIntoView({block:"end"}); }, [aiMessages, aiOpen]); + + // load sites and domains + useEffect(()=>{ + setLoading(true); + Promise.all([ + fetchDb("/sites") as Promise, + fetchDb("/domains") as Promise<{name:string}[]>, + fetchDb("/mu-tree") as Promise, + ]).then(([s,d,m])=>{ + setSites(s||[]); + setDomains((d||[]).map(x=>x.name)); + setMuTree(m||[]); + }).catch(e=>notify("加载失败: "+e.message)).finally(()=>setLoading(false)); + },[]); + + const loadSite=async(id:number)=>{ + const data:SiteDetail = await fetchDb(`/sites/${id}`); + setSite(data); + setSiteId(id); + }; + + const createSite=async()=>{ + const data:SiteDetail = await fetchDb("/sites", {method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify({name:"",domain:"",description:"",supplement:""})}); + setSites(prev=>[data as any, ...prev]); + setSite(data); + setSiteId(data.id); + return data.id; + }; + const deleteSites=async(ids:number[])=>{ + if(!ids.length) return; + if(!window.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))); + if(siteId!=null && ids.includes(siteId)){ setSite(null); setSiteId(null); } + notify(`已删除 ${ids.length} 个站点`); + }catch(e:any){ notify("删除失败: "+e.message); } + }; + + const go=async(target:PageKey)=>{ + try{ + if(["basic","assets","functions","generate","result"].includes(target) && !siteId){ + await createSite(); + } + if(target==="assets" && siteId && (!site || site.id!==siteId)) await loadSite(siteId); + if(target==="functions" && siteId && (!site || site.id!==siteId)) await loadSite(siteId); + if(target==="generate" && siteId && (!site || site.id!==siteId)) await loadSite(siteId); + if(target==="result" && siteId && (!site || site.id!==siteId)) await loadSite(siteId); + setPage(target); + window.history.replaceState({}, "", "?page="+target+(siteId?"&id="+siteId:"")); + }catch(e:any){ + notify("操作失败: "+(e?.message||e)); + } + }; + + const saveSiteBasic=async(patch:Partial)=>{ + if(!site) return; + const next = {...site, ...patch}; + setSite(next); + setSaving(true); + try{ + await fetchDb(`/sites/${site.id}`, {method:"PUT", headers:{"Content-Type":"application/json"}, body:JSON.stringify(next)}); + setSites(prev=>prev.map(s=>s.id===site.id?{...s, name:next.name, domain:next.domain, status:next.status}:s)); + }catch(e:any){ notify("保存失败: "+e.message); } + finally{ setSaving(false); } + }; + + const saveAssets=async(assets:Asset[])=>{ + if(!site) return; + setSite({...site, assets}); + setSaving(true); + try{ + await fetchDb(`/sites/${site.id}/assets`, {method:"PUT", headers:{"Content-Type":"application/json"}, body:JSON.stringify({assets})}); + }catch(e:any){ notify("资产保存失败: "+e.message); } + finally{ setSaving(false); } + }; + + const saveMu=async(selected:string[])=>{ + if(!site) return; + setSite({...site, mu:selected}); + try{ + await fetchDb(`/sites/${site.id}/mu`, {method:"PUT", headers:{"Content-Type":"application/json"}, body:JSON.stringify({selected})}); + }catch(e:any){ notify("MU 选择保存失败: "+e.message); } + }; + + const addDomain=async(name:string)=>{ + if(!name || name.length>6) return; + try{ + await fetchDb("/domains", {method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify({name})}); + setDomains(prev=>[...new Set([...prev, name])]); + }catch(e:any){ notify("添加领域失败: "+e.message); } + }; + + const uploadFile=async(file:File)=>{ + if(!site) return; + const reader = new FileReader(); + reader.readAsDataURL(file); + const data = await new Promise(res=>reader.onload=()=>res(String(reader.result))); + const base64 = data.split(",")[1]; + try{ + const result = await fetchDb(`/sites/${site.id}/files`, {method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify({filename:file.name, mime:file.type, data:base64})}) as ReferenceFile; + setSite({...site, files:[...site.files, result]}); + notify("已上传 "+file.name); + }catch(e:any){ notify("上传失败: "+e.message); } + }; + const sendAi=async()=>{ const text=aiDraft.trim(); if(!text||aiBusy)return; @@ -77,11 +273,13 @@ export default function Home(){ else c.push({role:"assistant",content:"[连接出错] "+(e?.message||e)}); return c;}); }finally{ setAiBusy(false); } }; + if(page==="login")return go("projects")}/>; + return
⬡
iStationiStation TeamStationBuilder⌄
⌕♧?▦wang.yu@advantech.com
iStation Team
-