From 09d2a5dad6cd0aa0a6e5af7c7cdf0f277343291a Mon Sep 17 00:00:00 2001 From: "wang.yu" Date: Mon, 17 Aug 2026 12:11:15 +0800 Subject: [PATCH] =?utf8?q?feat:=20=E7=AB=99=E7=82=B9=E7=BA=A7=E9=A1=B5?= =?utf8?q?=E9=9D=A2=E6=A0=B7=E5=BC=8F=E9=9B=86=E6=88=90=EF=BC=88=E5=8A=9F?= =?utf8?q?=E8=83=BD=E4=B8=8E=E8=8F=9C=E5=8D=95=E4=B8=AD=E5=8D=95=E9=80=89?= =?utf8?q?=E6=A0=B7=E5=BC=8F=E5=B9=B6=E7=BB=84=E5=90=88=E5=88=B0=E7=94=9F?= =?utf8?q?=E6=88=90=E6=8F=90=E7=A4=BA=E8=AF=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- app/globals.css | 11 ++++++++++ app/page.tsx | 27 ++++++++++++++++++++++-- db-sidecar.mjs | 56 +++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/app/globals.css b/app/globals.css index b138d0a..e90c858 100644 --- a/app/globals.css +++ b/app/globals.css @@ -154,4 +154,15 @@ .style-json-row textarea{flex:1;min-height:150px;resize:vertical;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:11px;line-height:1.5} .style-json-row .upload{width:120px;height:64px;flex:0 0 auto;margin-top:2px} .modal>footer .spacer{flex:1} +.style-picker{margin-top:18px;border-top:1px dashed var(--line);padding-top:14px} +.style-picker h4{margin:0 0 4px;font-size:14px} +.style-picker h4 small{color:#999;font-weight:normal;font-size:10px;margin-left:6px} +.style-pick-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px;margin-top:8px} +.style-pick{border:1px solid var(--line);background:white;padding:7px;display:flex;flex-direction:column;gap:6px;cursor:pointer;text-align:center;color:#555} +.style-pick:hover{border-color:var(--blue)} +.style-pick.active{border:2px solid var(--blue);background:#f4fbff;color:#007cc3} +.style-pick-thumb{height:78px;background:#f2f5f7;display:grid;place-items:center;overflow:hidden} +.style-pick-thumb img{width:100%;height:100%;object-fit:contain} +.style-pick-thumb.none{color:#aaa;font-size:13px} +.style-pick span{font-size:11px;line-height:1.3} diff --git a/app/page.tsx b/app/page.tsx index 6f98155..5775052 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -41,6 +41,7 @@ type MenuItem = { id?:number; parent_id:number|null; label:string; page_type:"mu 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; + style_id?: number | null; created_at:string; updated_at:string; assets: Asset[]; cubes: Cube[]; mu: string[]; menu: MenuItem[]; files: ReferenceFile[]; }; @@ -395,7 +396,7 @@ export default function Home(){ {page==="styleLibrary"&&} {page==="basic"&&} {page==="assets"&&} - {page==="functions"&&} + {page==="functions"&&} {page==="generate"&&} {page==="result"&&} {page==="preview"&&} @@ -515,7 +516,7 @@ function Assets({site,onChange,saving}:{site:SiteDetail|null;onChange:(assets:As ; } -function Functions({site,muTree,onMuChange,onSiteChange,notify}:{site:SiteDetail|null;muTree:MuNode[];onMuChange:(s:string[])=>void;onSiteChange:(s:SiteDetail)=>void;notify:(m:string)=>void}){ +function Functions({site,muTree,onMuChange,onSiteChange,pageStyles,notify}:{site:SiteDetail|null;muTree:MuNode[];onMuChange:(s:string[])=>void;onSiteChange:(s:SiteDetail)=>void;pageStyles:PageStyle[];notify:(m:string)=>void}){ const [tab,setTab]=useState<"mu"|"cube"|"menu">("mu"); const selectedMu = site?.mu || []; const leaves = useMemo(()=>buildMuLeaves(muTree),[muTree]); @@ -553,6 +554,15 @@ function Functions({site,muTree,onMuChange,onSiteChange,notify}:{site:SiteDetail if(cube.id) try{ await fetchDb(`/sites/${site.id}/cubes/${cube.id}`, {method:"DELETE"}); }catch(e:any){ notify("删除失败: "+e.message); return; } onSiteChange({...site, cubes:site.cubes.filter((_,i)=>i!==idx)}); }; + const selectStyle=async(id:number|null)=>{ + try{ + const refreshed:SiteDetail = await fetchDb(`/sites/${site.id}`, {method:"PUT", headers:{"Content-Type":"application/json"}, body:JSON.stringify({ + name:site.name, domain:site.domain, description:site.description, supplement:site.supplement, style_id: id + })}) as SiteDetail; + onSiteChange(refreshed); + notify(id ? "已应用页面样式" : "已清除页面样式"); + }catch(e:any){ notify("应用样式失败: "+e.message); } + }; const generateCube=async(idx:number)=>{ const cube = site.cubes[idx]; if(!cube.id) await saveCube(idx); @@ -591,6 +601,19 @@ function Functions({site,muTree,onMuChange,onSiteChange,notify}:{site:SiteDetail {cube.status==="generated"? : 未生成} )}} + {tab==="cube"&&
+

站点页面样式 站点级 · 全部 Cube 页面共用

+

选择一个样式库方案作为本站点所有 Cube 页面的视觉风格(生成时参考其 JSON 主题参数)。

+
+ + {pageStyles.map(s=>)} +
+
} {tab==="menu"&&

配置站点菜单结构,可选择引用 MU 标准页或 Cube 扩展页。

{menu.map((m,i)=>
{const next=[...menu]; next[i]={...next[i],label:e.target.value}; setMenu(next);}}/> {m.page_type==="mu"&&} diff --git a/db-sidecar.mjs b/db-sidecar.mjs index fd7f7c9..ac8fbc5 100644 --- a/db-sidecar.mjs +++ b/db-sidecar.mjs @@ -237,6 +237,7 @@ function ensureColumn(table, column, def) { if (!cols.includes(column)) db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${def}`); } ensureColumn("cubes", "prompt", "TEXT"); +ensureColumn("sites", "style_id", "INTEGER"); function respondJson(res, status, obj) { @@ -420,7 +421,7 @@ Site context (所有页面通用): 请生成对应的 JSON 文件。`; } function buildSiteContextPrompt(site) { - return siteContextText(site); + return `${siteContextText(site)}${buildStylePrompt(site)}`; } function buildCubePrompt(site, cube) { return `${siteContextText(site)} @@ -430,7 +431,53 @@ function buildCubePrompt(site, cube) { - 页面内容/要求: ${cube.content || "(未填写,请按总览型页面设计)"} 请针对以上页面,生成对应的 Cube 仪表盘 JSON 文件(只输出该页面)。模拟数据需与上方站点资产配置保持一致;若属于同一站点的多个 Cube 页面,请保持各页面之间的数据关系合理。该 JSON 必须符合 dashboard-json-generator Skill 中 Cube 工具的规则,结构如下: -${DASHBOARD_SPEC}`; +${DASHBOARD_SPEC}${buildStylePrompt(site)}`; +} + +// 站点级视觉风格范本:从所选样式库方案中提取可写入 grid 的主题参数(背景大图以占位描述代替,避免上下文膨胀), +// 并在 buildCubePrompt 中复用,保证生成 / 重新生成 / 查看提示词 等路径都带同一段范本。 +function buildStylePrompt(site) { + if (!site || !site.style_id) return ""; + const row = db.prepare("SELECT id,name,json_text FROM page_styles WHERE id=?").get(site.style_id); + if (!row) return ""; + let style; + try { style = JSON.parse(row.json_text); } catch { return ""; } + const g = style.grid || {}; + const bgImgLen = typeof g.bgImg === "string" ? g.bgImg.length : 0; + const theme = { + theme: g.theme, + bgType: g.bgType, + bgColor: g.bgColor, + bgImg: bgImgLen ? `<原样式背景底图,base64 长度约 ${bgImgLen} 字符,此处已省略;请沿用该图的明暗、色调与渐变质感>` : undefined, + bgImgType: g.bgImgType, + radialGradientParam1: g.radialGradientParam1, + radialGradientParam2: g.radialGradientParam2, + radialGradientParam3: g.radialGradientParam3, + linearGradientParamDeg: g.linearGradientParamDeg, + linearGradientParam1: g.linearGradientParam1, + linearGradientParam2: g.linearGradientParam2, + linearGradientParam3: g.linearGradientParam3, + borderColor: g.borderColor, + borderWidth: g.borderWidth, + showBorder: g.showBorder, + borderStyle: g.borderStyle, + borderRadius: g.borderRadius, + pageHeight: g.pageHeight, + pageWidth: g.pageWidth, + panelBgType: g.panelBgType, + panelBgColor: g.panelBgColor, + panel: g.panel, + gap: g.gap, + pageType: g.pageType, + }; + Object.keys(theme).forEach(k => theme[k] === undefined && delete theme[k]); + return ` + +视觉风格要求(站点级,适用本站点全部 Cube 页面): +所选页面样式库方案:「${row.name}」。请严格参考以下视觉主题参数生成 dashboard JSON(仅输出符合该风格的外观,结构仍遵循上方 Cube 规范): +${JSON.stringify(theme, null, 2)} + +说明:bgImg 为原样式的背景底图(已省略其 base64 内容),请保持与该图一致的明暗、色调与渐变质感;其余参数均为可直接写入 grid 的样式值。`; } async function generateCubeJson(site, cube, attempt = 1, previousErrors = "") { const promptBase = buildCubePrompt(site, cube); @@ -883,8 +930,9 @@ async function route(req, res) { } if (req.method === "PUT") { const body = await parseBody(req); - db.prepare("UPDATE sites SET name=?, domain=?, description=?, supplement=?, updated_at=CURRENT_TIMESTAMP WHERE id=?").run( - body.name ?? "", body.domain ?? "", body.description ?? "", body.supplement ?? "", siteId + db.prepare("UPDATE sites SET name=?, domain=?, description=?, supplement=?, style_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 ); return respondJson(res, 200, hydrateSite(siteId)); } -- 2.43.0