<select value={domainSel} onChange={e=>setDomainSel(e.target.value)}><option value="">全部领域</option>{domainOptions.map(d=><option key={d.id} value={String(d.id)}>{d.name}</option>)}</select>
<div className="filter-actions"><Button onClick={()=>notify(`已显示 ${filtered.length} 个领域方案`)}>查询</Button><Button secondary onClick={()=>{setQuery("");setDomainSel("");setStatusSel("");}}>重置</Button></div>
</div>
- {loading?<div className="empty-state">加载中…</div>:<><table className="data-table company-table resource-table domain-resource-table"><thead><tr><th>方案名称</th><th>所属领域</th><th>版本</th><th>设备</th><th>页面</th><th>文档</th><th>状态</th><th>创建人</th><th>操作</th></tr></thead><tbody>{filtered.slice((pageIndex-1)*pageSize,pageIndex*pageSize).map(row=><tr key={row.id} onClick={()=>setPreview(row)}><td><button className="link company-record-link" title={row.name} onClick={e=>{e.stopPropagation();setPreview(row);}}><b>{row.name||"未命名方案"}</b></button></td><td>{row.domain_name||"—"}</td><td>{row.version||"—"}</td><td>{row.device_count||0}</td><td>{row.page_count||0}</td><td>{row.doc_count||0}</td><td><Status>{row.status==="available"?"可用":"不可用"}</Status></td><td>{row.creator||"—"}</td><td onClick={e=>e.stopPropagation()}><Dropdown trigger={["click"]} menu={{items:[{key:"edit",label:"编辑方案",icon:<EditOutlined/>},{key:"delete",label:"删除",danger:true,icon:<DeleteOutlined/>}],onClick:({key})=>key==="edit"?onOpen(row.id):remove(row)}}><AntButton type="text" aria-label="更多操作" icon={<EllipsisOutlined/>}/></Dropdown></td></tr>)}</tbody></table>{filtered.length===0&&<div className="empty-state">暂无符合条件的领域方案</div>}<div className="pagination"><span>{filtered.length} / 总数</span><button disabled={pageIndex<=1} onClick={()=>setPageIndex(p=>Math.max(1,p-1))}>‹</button><button className="active">{pageIndex}</button><button disabled={pageIndex>=Math.max(1,Math.ceil(filtered.length/pageSize))} onClick={()=>setPageIndex(p=>Math.min(Math.max(1,Math.ceil(filtered.length/pageSize)),p+1))}>›</button><select value={pageSize} onChange={e=>{setPageSize(Number(e.target.value));setPageIndex(1);}}><option value={10}>10 / 页</option><option value={20}>20 / 页</option><option value={50}>50 / 页</option></select></div></>}
+ {loading?<div className="empty-state">加载中…</div>:<><table className="data-table company-table resource-table domain-resource-table"><thead><tr><th>方案名称</th><th>所属领域</th><th>版本</th><th>设备</th><th>页面</th><th>文档</th><th>状态</th><th>创建人</th><th>从模板创建</th><th>操作</th></tr></thead><tbody>{filtered.slice((pageIndex-1)*pageSize,pageIndex*pageSize).map(row=><tr key={row.id} onClick={()=>setPreview(row)}><td><button className="link company-record-link" title={row.name} onClick={e=>{e.stopPropagation();setPreview(row);}}><b>{row.name||"未命名方案"}</b></button></td><td>{row.domain_name||"—"}</td><td>{row.version||"—"}</td><td>{row.device_count||0}</td><td>{row.page_count||0}</td><td>{row.doc_count||0}</td><td><Status>{row.status==="available"?"可用":"不可用"}</Status></td><td>{row.creator||"—"}</td><td onClick={e=>e.stopPropagation()}><button className="link" onClick={async(e:React.MouseEvent)=>{e.stopPropagation();try{const s=await fetchDb(`/domain-templates/${row.id}/create-site`,{method:"POST",headers:{"Content-Type":"application/json"}}) as any;go("siteEdit",s.id);}catch(err:any){notify("创建站点失败:"+(err?.message||err));}}}>创建</button></td><td onClick={e=>e.stopPropagation()}><Dropdown trigger={["click"]} menu={{items:[{key:"edit",label:"编辑方案",icon:<EditOutlined/>},{key:"delete",label:"删除",danger:true,icon:<DeleteOutlined/>}],onClick:({key})=>key==="edit"?onOpen(row.id):remove(row)}}><AntButton type="text" aria-label="更多操作" icon={<EllipsisOutlined/>}/></Dropdown></td></tr>)}</tbody></table>{filtered.length===0&&<div className="empty-state">暂无符合条件的领域方案</div>}<div className="pagination"><span>{filtered.length} / 总数</span><button disabled={pageIndex<=1} onClick={()=>setPageIndex(p=>Math.max(1,p-1))}>‹</button><button className="active">{pageIndex}</button><button disabled={pageIndex>=Math.max(1,Math.ceil(filtered.length/pageSize))} onClick={()=>setPageIndex(p=>Math.min(Math.max(1,Math.ceil(filtered.length/pageSize)),p+1))}>›</button><select value={pageSize} onChange={e=>{setPageSize(Number(e.target.value));setPageIndex(1);}}><option value={10}>10 / 页</option><option value={20}>20 / 页</option><option value={50}>50 / 页</option></select></div></>}
</Panel>
<Drawer title={preview?.name||"方案详情"} width={440} open={!!preview} onClose={()=>setPreview(null)} extra={preview&&<Button onClick={()=>onOpen(preview.id)}>编辑方案</Button>}>
{preview&&<div className="company-detail resource-detail"><div className="detail-status"><Status>{preview.status==="available"?"可用":"不可用"}</Status></div><h3>基础信息</h3><dl><dt>所属领域</dt><dd>{preview.domain_name||"—"}</dd><dt>版本</dt><dd>{preview.version||"—"}</dd><dt>创建人</dt><dd>{preview.creator||"—"}</dd></dl><h3>方案内容</h3><dl><dt>设备模板</dt><dd>{preview.device_count||0}</dd><dt>页面</dt><dd>{preview.page_count||0}</dd><dt>知识文档</dt><dd>{preview.doc_count||0}</dd></dl>{preview.description&&<><h3>方案说明</h3><p>{preview.description}</p></>}</div>}
respondJson(res, 200, { ok: true });
}
+async function handleCreateSiteFromTemplate(req, res, tmplId) {
+ const dt = db.prepare("SELECT dt.*, d.name AS domain_name FROM domain_templates dt JOIN domains d ON d.id=dt.domain_id WHERE dt.id=?").get(tmplId);
+ if (!dt) return respondError(res, 404, "领域方案不存在");
+ // 创建站点
+ const info = db.prepare("INSERT INTO sites (name, domain, description, supplement, status) VALUES (?,?,?,?,?)").run(
+ (dt.name || "") + " - 站点", dt.domain_name || "", dt.description || "", dt.supplement || "", "draft"
+ );
+ const siteId = info.lastInsertRowid;
+ // 复制模板资产到站点
+ const tmplAssets = db.prepare("SELECT device_template_id, count, rule, sort_order FROM domain_template_assets WHERE domain_template_id=? ORDER BY sort_order,id").all(tmplId);
+ const insertAsset = db.prepare("INSERT INTO assets (site_id, type, count, rule, sort_order) VALUES (?,?,?,?,?)");
+ for (const a of tmplAssets) {
+ insertAsset.run(siteId, String(a.device_template_id), a.count || 0, a.rule || "", a.sort_order ?? 0);
+ }
+ if (tmplAssets.length === 0) {
+ for (let i = 0; i < 4; i++) insertAsset.run(siteId, "", 0, "", i);
+ }
+ // 复制模板 MU 页面选择
+ const muKeys = db.prepare("SELECT page_key FROM domain_template_mu WHERE domain_template_id=?").all(tmplId).map(r => r.page_key);
+ if (muKeys.length > 0) {
+ db.prepare("UPDATE sites SET mu=? WHERE id=?").run(JSON.stringify(muKeys), siteId);
+ }
+ // 复制 style_id
+ if (dt.style_id != null) {
+ db.prepare("UPDATE sites SET style_id=? WHERE id=?").run(dt.style_id, siteId);
+ }
+ const site = hydrateSite(siteId);
+ respondJson(res, 200, site);
+}
+
// ===== 领域方案 AI 补全 / Cube 生成(复用站点侧提示词构造与生成逻辑)=====
async function hydrateDomainTemplateCtx(tmplId) {
const dt = db.prepare("SELECT dt.*, d.name AS domain_name FROM domain_templates dt JOIN domains d ON d.id=dt.domain_id WHERE dt.id=?").get(tmplId);
const mDtCompleteCubesPrompt = path.match(/^\/api\/domain-templates\/(\d+)\/complete-cubes-prompt$/);
const mDtPageGen = path.match(/^\/api\/domain-templates\/(\d+)\/pages\/(\d+)\/generate$/);
const mDtPageRefine = path.match(/^\/api\/domain-templates\/(\d+)\/pages\/(\d+)\/refine$/);
+ const mDtCreateSite = path.match(/^\/api\/domain-templates\/(\d+)\/create-site$/);
const mDevList = path === "/api/device-templates";
const mDevCp = path.match(/^\/api\/device-templates\/(\d+)\/collection-points$/);
const mDevAttr = path.match(/^\/api\/device-templates\/(\d+)\/attributes$/);
if (req.method === "PUT") return handleDomainTemplateUpdate(req, res, Number(mDt[1]));
if (req.method === "DELETE") return handleDomainTemplateDelete(res, Number(mDt[1]));
}
+ if (mDtCreateSite && req.method === "POST") return handleCreateSiteFromTemplate(req, res, Number(mDtCreateSite[1]));
if (mDevList) {
if (req.method === "GET") return handleDeviceTemplatesList(res);
if (req.method === "POST") return handleDeviceTemplateCreate(req, res);