Commit f7f3b942 authored by xuchentao's avatar xuchentao

feat: 官网外观模版画廊与可视化创建流程

- 新增 3 套匿名化外观模版(出行服务/大健康连锁/匠心手作)及 3 套配色、1 个布局预设
- 配色与模版解耦自由组合;新增等粗黑体/不等粗宋体免费商用字体方案
- 创建页拆分为独立卡片,01 区改为左列表右实时预览的画廊(真实文案+配色字体布局联动)
- 目录 API 暴露模版文案与完整色板;“页面成品”统一更名为“官网外观模版”
parent 61c4a3b5
......@@ -8,3 +8,4 @@
4. 样式自由;公开页须用 `BaseLayout` 保留 SEO/GEO。
5. 页面图片放 `public/images/`,路径兼容 `SITE_BASE_PATH`
6. 禁止访问站点外目录、部署或提交;完成后检查 diff。
7. `src/data/design.json``gradientEmphasis` 默认关闭;除非用户明确要求强调渐变,否则不要新增大面积渐变背景、渐变文字或多重光晕。
......@@ -50,7 +50,8 @@ const titleLines = home.heroTitle.split("\n");
.visual-title { margin-top:48px; display:flex; flex-direction:column; gap:10px; }
.visual-title span { font-size:14px; opacity:.65; }.visual-title strong { font-size:52px; letter-spacing:-.05em; }
.chart { height:170px; display:flex; align-items:end; gap:12px; margin:30px 0 26px; border-bottom:1px solid rgba(255,255,255,.15); }
.chart i { flex:1; min-height:20px; border-radius:7px 7px 0 0; background:linear-gradient(to top,rgba(255,255,255,.18),rgba(255,255,255,.82)); }
.chart i { flex:1; min-height:20px; border-radius:7px 7px 0 0; background:rgba(255,255,255,.62); }
:global(body[data-gradient="emphasized"]) .chart i { background:linear-gradient(to top,rgba(255,255,255,.18),rgba(255,255,255,.82)); }
.visual-footer { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
.visual-footer div { padding:17px; border:1px solid rgba(255,255,255,.12); border-radius:14px; background:rgba(255,255,255,.07); }
.visual-footer span,.visual-footer strong { display:block; }.visual-footer span { opacity:.58;font-size:11px;margin-bottom:7px;}.visual-footer strong{font-size:14px;}
......
{
"pageTemplate": { "id": "technology-corporate", "version": "1.0.0" },
"themeId": "violet-tech",
"layout": { "hero": "split", "density": "comfortable", "radius": "soft" }
"themeVersion": "1.0.0",
"fontId": "modern-sans",
"fontVersion": "1.0.0",
"layoutPresetId": "modern-split",
"layoutPresetVersion": "1.0.0",
"layout": { "hero": "split", "density": "comfortable", "radius": "soft", "gradientEmphasis": false }
}
......@@ -26,5 +26,5 @@ const props = Astro.props;
<SeoHead {...props} />
<slot name="head" />
</head>
<body data-page-template={design.pageTemplate.id} data-hero={design.layout.hero} data-density={design.layout.density} data-radius={design.layout.radius}><slot /></body>
<body data-page-template={design.pageTemplate.id} data-hero={design.layout.hero} data-density={design.layout.density} data-radius={design.layout.radius} data-gradient={design.layout.gradientEmphasis ? "emphasized" : "minimal"}><slot /></body>
</html>
:root {
--font-heading: Inter, "MiSans", "HarmonyOS Sans SC", "Source Han Sans SC", "Noto Sans CJK SC", "PingFang SC", "Microsoft YaHei", sans-serif;
--font-body: Inter, "MiSans", "HarmonyOS Sans SC", "Source Han Sans SC", "Noto Sans CJK SC", "PingFang SC", "Microsoft YaHei", sans-serif;
}
@import "./theme.css";
@import "./fonts.css";
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
......@@ -6,9 +7,10 @@ body {
margin: 0;
color: var(--color-text-primary);
background: var(--color-surface);
font-family: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
}
h1, h2, h3 { font-family: var(--font-heading); }
a { color: inherit; text-decoration: none; }
button, input { font: inherit; }
.container { width: min(1180px, calc(100% - 48px)); margin: 0 auto; }
......@@ -25,10 +27,10 @@ body[data-hero="centered"] .hero-copy { max-width: 920px; margin: 0 auto; }
body[data-hero="centered"] .hero-copy > p { margin-left: auto; margin-right: auto; }
body[data-hero="centered"] .actions, body[data-hero="centered"] .metrics { justify-content: center; }
body[data-hero="centered"] .visual { display: none; }
body[data-gradient="minimal"] .orb { display: none; }
body[data-page-template="professional-services"] .section-heading { margin-left: auto; margin-right: auto; text-align: center; }
body[data-page-template="professional-services"] .section-heading .eyebrow { justify-content: center; }
body[data-page-template="professional-services"] .service-grid article { border-top: 3px solid var(--color-primary); box-shadow: none; }
body[data-page-template="brand-story"] h1, body[data-page-template="brand-story"] h2 { font-family: Georgia, "Songti SC", serif; font-weight: 500; }
body[data-page-template="brand-story"] .service-grid { gap: 30px; }
body[data-page-template="brand-story"] .service-grid article { border: 0; box-shadow: 0 22px 60px color-mix(in srgb, var(--color-primary) 10%, transparent); }
.section-heading { max-width: 680px; margin-bottom: 56px; }
......
......@@ -12,7 +12,7 @@
--color-text-muted: #8b8e99;
--color-border: #e7e3f3;
--color-divider: #eeeff4;
--gradient-primary: linear-gradient(135deg, #7028ff 0%, #536cff 100%);
--gradient-dark: linear-gradient(135deg, #24105f 0%, #4a1ca8 55%, #7028ff 100%);
--gradient-soft: linear-gradient(180deg, #f3f0ff 0%, #ffffff 100%);
--gradient-primary: #7028ff;
--gradient-dark: #24105f;
--gradient-soft: #f3f0ff;
}
......@@ -20,8 +20,12 @@
"@qwen-code/sdk": "0.1.8",
"@webagent/shared": "workspace:*",
"dotenv": "^16.5.0",
"exceljs": "^4.4.0",
"execa": "^9.5.2",
"fastify": "^5.3.2",
"mammoth": "^1.12.0",
"pdf-parse": "^2.4.5",
"word-extractor": "^1.0.4",
"yaml": "^2.9.0",
"zod": "^3.24.2"
},
......
......@@ -7,25 +7,38 @@ export const createSiteSchema = z.object({
services: z.array(z.string().trim().min(2).max(80)).min(1).max(8),
email: z.string().email(),
phone: z.string().trim().max(30).optional(),
brandColor: z.string().regex(/^#[0-9a-fA-F]{6}$/, "品牌色必须是 6 位十六进制颜色"),
pageTemplateId: z.string().regex(/^[a-z0-9-]+$/),
pageTemplateVersion: z.string().regex(/^\d+\.\d+\.\d+$/),
themeId: z.string().regex(/^[a-z0-9-]+$/),
layout: z.object({
hero: z.enum(["split", "centered"]),
density: z.enum(["comfortable", "compact"]),
radius: z.enum(["soft", "square"]),
design: z.object({
themeId: z.string().regex(/^[a-z0-9-]+$/),
themeVersion: z.string().regex(/^\d+\.\d+\.\d+$/),
brandColor: z.string().regex(/^#[0-9a-fA-F]{6}$/, "品牌色必须是 6 位十六进制颜色"),
fontId: z.string().regex(/^[a-z0-9-]+$/),
fontVersion: z.string().regex(/^\d+\.\d+\.\d+$/),
layoutPresetId: z.string().regex(/^[a-z0-9-]+$/),
layoutPresetVersion: z.string().regex(/^\d+\.\d+\.\d+$/),
layout: z.object({
hero: z.enum(["split", "centered"]),
density: z.enum(["comfortable", "compact"]),
radius: z.enum(["soft", "square"]),
gradientEmphasis: z.boolean().default(false),
}),
}),
initialPrompt: z.string().trim().max(2000).optional().or(z.literal("").transform(() => undefined)),
});
export const companyDocumentExtractSchema = z.object({
filename: z.string().trim().min(1).max(180).regex(/\.(?:md|markdown|doc|docx|pdf|xlsx)$/i, "仅支持 Markdown、Word、PDF 和 Excel 文件"),
dataUrl: z.string().max(11 * 1024 * 1024).regex(/^data:[^;,]+;base64,/i, "文件内容格式无效"),
});
export const loginSchema = z.object({
username: z.string().trim().min(1),
password: z.string().min(1),
});
export const chatSchema = z.object({
message: z.string().trim().min(2).max(2000),
message: z.string().trim().min(2).max(30_000),
model: z.string().trim().min(1).max(120).optional(),
sessionId: z.string().trim().min(1).max(200).optional(),
});
......
......@@ -12,7 +12,7 @@ import { config, runtimePaths } from "./config.js";
import { GitManager } from "./git/git-manager.js";
import { PreviewProcessManager } from "./preview/preview-process-manager.js";
import {
addDomainSchema, agentSettingsSchema, articleImageUploadSchema, articleImportSchema, articleInputSchema, chatSchema, createSiteSchema, createTenantSchema, loginSchema, resetPasswordSchema, saveDraftSchema,
addDomainSchema, agentSettingsSchema, articleImageUploadSchema, articleImportSchema, articleInputSchema, chatSchema, companyDocumentExtractSchema, createSiteSchema, createTenantSchema, loginSchema, resetPasswordSchema, saveDraftSchema,
tenantStatusSchema, updateDomainSchema, versionCommitSchema,
} from "./schemas.js";
import { CreateSiteService } from "./sites/create-site.js";
......@@ -26,12 +26,13 @@ import { DomainRoutingConfig } from "./domains/domain-routing-config.js";
import { DomainService } from "./domains/domain-service.js";
import { createDomainProvider } from "./domains/domain-provider.js";
import { ArticleService } from "./articles/article-service.js";
import { CompanyDocumentService } from "./sites/company-document-service.js";
declare module "fastify" {
interface FastifyRequest { auth?: SessionInfo }
}
const app = Fastify({ logger: { level: process.env.LOG_LEVEL || "info" }, bodyLimit: 8 * 1024 * 1024 });
const app = Fastify({ logger: { level: process.env.LOG_LEVEL || "info" }, bodyLimit: 12 * 1024 * 1024 });
await app.register(cors, { origin: [config.frontendOrigin, "http://127.0.0.1:5173"] });
const auth = new AuthRepository();
......@@ -52,6 +53,7 @@ const domainDeployments = new DomainDeploymentService(sites, domainRepository, g
const domainService = new DomainService(sites, domainRepository, domainRouting, domainProvider);
const siteLifecycle = new SiteLifecycleService(sites, previews, domainRepository, domainRouting, domainProvider);
const articles = new ArticleService(sites, builds, previews);
const companyDocuments = new CompanyDocumentService();
app.addHook("onRequest", async (request, reply) => {
if (!request.url.startsWith("/api/") || request.url === "/api/health" || request.url === "/api/login") return;
......@@ -91,6 +93,7 @@ app.put("/api/agent-settings", async (request) => {
app.get("/api/sites", async (request) => sites.list(requireTenant(request.auth)));
app.get("/api/site-templates", async () => templateCatalog.list());
app.post("/api/company-documents/extract", async (request) => companyDocuments.extract(companyDocumentExtractSchema.parse(request.body)));
app.get("/api/archived-sites", async (request) => sites.listArchived(requireTenant(request.auth)));
app.get<{ Params: { siteId: string } }>("/api/sites/:siteId", async (request) => sites.get(requireTenant(request.auth), request.params.siteId));
app.post("/api/sites", async (request, reply) => reply.code(201).send(await createSite.execute(requireTenant(request.auth), createSiteSchema.parse(request.body))));
......
import assert from "node:assert/strict";
import test from "node:test";
import ExcelJS from "exceljs";
import { CompanyDocumentService } from "./company-document-service.js";
test("extracts normalized Markdown company material", async () => {
const service = new CompanyDocumentService();
const result = await service.extract({
filename: "company.md",
dataUrl: dataUrl("text/markdown", Buffer.from("# 星河科技\r\n\r\n企业级 AI 服务\r\n")),
});
assert.equal(result.format, "markdown");
assert.equal(result.text, "# 星河科技\n\n企业级 AI 服务");
assert.equal(result.truncated, false);
});
test("extracts worksheet names and cell values from Excel company material", async () => {
const workbook = new ExcelJS.Workbook();
const sheet = workbook.addWorksheet("公司信息");
sheet.addRow(["企业名称", "星河科技"]);
sheet.addRow(["核心服务", "智能客服"]);
const buffer = Buffer.from(await workbook.xlsx.writeBuffer());
const result = await new CompanyDocumentService().extract({
filename: "company.xlsx",
dataUrl: dataUrl("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buffer),
});
assert.equal(result.format, "excel");
assert.match(result.text, /工作表:公司信息/);
assert.match(result.text, /企业名称 \| 星河科技/);
assert.match(result.text, /核心服务 \| 智能客服/);
});
test("rejects unsupported or empty company material", async () => {
const service = new CompanyDocumentService();
await assert.rejects(() => service.extract({ filename: "company.txt", dataUrl: dataUrl("text/plain", Buffer.from("hello")) }), /仅支持/);
await assert.rejects(() => service.extract({ filename: "empty.md", dataUrl: dataUrl("text/markdown", Buffer.from(" \n")) }), /没有提取到/);
});
function dataUrl(mime: string, buffer: Buffer): string {
return `data:${mime};base64,${buffer.toString("base64")}`;
}
import path from "node:path";
import ExcelJS from "exceljs";
import mammoth from "mammoth";
import { PDFParse } from "pdf-parse";
import WordExtractor from "word-extractor";
import type { CompanyDocumentExtractInput, CompanyDocumentExtractResult } from "@webagent/shared";
const MAX_FILE_BYTES = 8 * 1024 * 1024;
const MAX_TEXT_CHARACTERS = 40_000;
export class CompanyDocumentService {
async extract(input: CompanyDocumentExtractInput): Promise<CompanyDocumentExtractResult> {
const extension = path.extname(input.filename).toLowerCase();
const buffer = decodeDataUrl(input.dataUrl);
if (buffer.byteLength > MAX_FILE_BYTES) throw Object.assign(new Error("单个公司资料不能超过 8 MB"), { statusCode: 413 });
let format: CompanyDocumentExtractResult["format"];
let text: string;
try {
if (extension === ".md" || extension === ".markdown") {
format = "markdown";
text = buffer.toString("utf8").replace(/^\uFEFF/, "");
} else if (extension === ".docx") {
format = "word";
text = (await mammoth.extractRawText({ buffer })).value;
} else if (extension === ".doc") {
format = "word";
text = (await new WordExtractor().extract(buffer)).getBody();
} else if (extension === ".pdf") {
format = "pdf";
text = await this.extractPdf(buffer);
} else if (extension === ".xlsx") {
format = "excel";
text = await this.extractExcel(buffer);
} else {
throw Object.assign(new Error("仅支持 Markdown、Word、PDF 和 Excel 文件"), { statusCode: 400 });
}
} catch (error) {
if (typeof error === "object" && error && "statusCode" in error) throw error;
throw Object.assign(new Error(`无法读取公司资料“${input.filename}”,请确认文件未损坏且没有密码保护`), { statusCode: 400, cause: error });
}
const normalized = normalizeText(text);
if (!normalized) throw Object.assign(new Error("文件中没有提取到可用文字"), { statusCode: 400 });
const truncated = normalized.length > MAX_TEXT_CHARACTERS;
const result = truncated ? normalized.slice(0, MAX_TEXT_CHARACTERS) : normalized;
return { filename: input.filename, format, text: result, characters: result.length, truncated };
}
private async extractPdf(buffer: Buffer): Promise<string> {
const parser = new PDFParse({ data: new Uint8Array(buffer) });
try {
return (await parser.getText()).text;
} finally {
await parser.destroy();
}
}
private async extractExcel(buffer: Buffer): Promise<string> {
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(buffer as unknown as ExcelJS.Buffer);
const sheets: string[] = [];
workbook.eachSheet((sheet) => {
const rows: string[] = [];
sheet.eachRow((row) => {
const cells = row.values instanceof Array
? row.values.slice(1).map((value) => formatCell(value)).filter(Boolean)
: [];
if (cells.length) rows.push(cells.join(" | "));
});
if (rows.length) sheets.push(`## 工作表:${sheet.name}\n${rows.join("\n")}`);
});
return sheets.join("\n\n");
}
}
function decodeDataUrl(dataUrl: string): Buffer {
const separator = dataUrl.indexOf(",");
if (separator < 0) throw Object.assign(new Error("文件内容格式无效"), { statusCode: 400 });
return Buffer.from(dataUrl.slice(separator + 1), "base64");
}
function normalizeText(value: string): string {
return value.replaceAll("\0", "").replace(/\r\n?/g, "\n").replace(/[ \t]+\n/g, "\n").replace(/\n{4,}/g, "\n\n\n").trim();
}
function formatCell(value: ExcelJS.CellValue | undefined): string {
if (value == null) return "";
if (value instanceof Date) return value.toISOString().slice(0, 10);
if (typeof value === "object") {
if ("result" in value && value.result != null) return String(value.result);
if ("text" in value) return String(value.text);
if ("richText" in value) return value.richText.map((item) => item.text).join("");
if ("hyperlink" in value) return "text" in value && value.text ? String(value.text) : String(value.hyperlink);
}
return String(value);
}
......@@ -20,7 +20,7 @@ export class CreateSiteService {
async execute(tenantId: string, input: CreateSiteInput): Promise<SiteInfo> {
await this.sites.ensureRuntime();
const selection = await this.templates.resolve(input.pageTemplateId, input.pageTemplateVersion, input.themeId, input.layout);
const selection = await this.templates.resolve(input.pageTemplateId, input.pageTemplateVersion, input.design);
const siteId = "site_" + crypto.randomBytes(16).toString("hex");
const projectPath = this.sites.getProjectPath(tenantId, siteId);
const previewPort = await this.sites.allocatePreviewPort();
......@@ -31,7 +31,7 @@ export class CreateSiteService {
kernel: selection.kernel,
createdFrom: {
pageTemplateId: selection.template.id, pageTemplateVersion: selection.template.version,
themeId: selection.theme.id, layout: input.layout,
design: { themeId: selection.theme.id, themeVersion: selection.theme.version, brandColor: input.design.brandColor, fontId: selection.font.id, fontVersion: selection.font.version, layoutPresetId: selection.layoutPreset.id, layoutPresetVersion: selection.layoutPreset.version, layout: input.design.layout },
},
...(input.initialPrompt ? { initialPrompt: input.initialPrompt } : {}),
},
......@@ -72,10 +72,11 @@ export class CreateSiteService {
home.services = input.services.map((service, index) => ({ number: String(index + 1).padStart(2, "0"), title: service, description: "以专业方法和可靠交付,为客户提供高质量的" + service + "服务。" }));
await writeFile(homePath, JSON.stringify(home, null, 2) + "\n", "utf8");
const themePath = path.join(projectPath, "src/styles/theme.css");
await writeFile(themePath, this.templates.renderTheme(selection.theme, input.brandColor), "utf8");
await writeFile(themePath, this.templates.renderTheme(selection.theme, input.design.brandColor, input.design.layout.gradientEmphasis), "utf8");
await writeFile(path.join(projectPath, "src/styles/fonts.css"), this.templates.renderFont(selection.font), "utf8");
await writeFile(path.join(projectPath, "src/data/design.json"), JSON.stringify({
pageTemplate: { id: selection.template.id, version: selection.template.version },
themeId: selection.theme.id, layout: input.layout,
themeId: selection.theme.id, themeVersion: selection.theme.version, fontId: selection.font.id, fontVersion: selection.font.version, layoutPresetId: selection.layoutPreset.id, layoutPresetVersion: selection.layoutPreset.version, layout: input.design.layout,
}, null, 2) + "\n", "utf8");
await writeFile(path.join(projectPath, "webagent.site.json"), JSON.stringify(site.template, null, 2) + "\n", "utf8");
const dist = await this.builds.build(projectPath, "create_" + siteId, {
......
......@@ -7,33 +7,61 @@ test("catalog exposes the stable kernel and published page templates", async ()
const catalog = await service.list();
assert.deepEqual(catalog.kernel, { id: "official-site", version: "1.0.0" });
assert.deepEqual(catalog.pageTemplates.map((template) => template.id).sort(), [
"brand-story", "professional-services", "technology-corporate",
"artisan-jewelry", "brand-story", "mobility-services",
"professional-services", "technology-corporate", "wellness-franchise",
]);
assert.equal(catalog.themes.length, 5);
assert.equal(catalog.themes.length, 8);
assert.equal(catalog.fonts.length, 2);
assert.equal(catalog.layoutPresets.length, 4);
assert.ok(catalog.themes[0].primary && catalog.themes[0].soft, "目录应暴露完整色板供实时预览");
assert.ok(catalog.pageTemplates[0].content.heroTitle, "目录应暴露模版默认文案供实时预览");
});
test("catalog resolves only supported template, theme and layout combinations", async () => {
const service = new SiteTemplateCatalogService();
const selection = await service.resolve("technology-corporate", "1.0.0", "ocean-blue", {
hero: "split", density: "compact", radius: "soft",
});
const selection = await service.resolve("technology-corporate", "1.0.0", { themeId: "ocean-blue", themeVersion: "1.0.0", brandColor: "#175CD3", fontId: "modern-sans", fontVersion: "1.0.0", layoutPresetId: "modern-split", layoutPresetVersion: "1.0.0", layout: {
hero: "split", density: "comfortable", radius: "soft", gradientEmphasis: false,
} });
assert.equal(selection.kernel.version, "1.0.0");
assert.equal(selection.template.id, "technology-corporate");
assert.equal(selection.theme.id, "ocean-blue");
await assert.rejects(() => service.resolve("technology-corporate", "1.0.0", "warm-amber", {
hero: "split", density: "compact", radius: "soft",
}), /不支持所选配色/);
assert.equal(selection.font.id, "modern-sans");
const freeCombination = await service.resolve("technology-corporate", "1.0.0", { themeId: "warm-amber", themeVersion: "1.0.0", brandColor: "#C2410C", fontId: "elegant-serif", fontVersion: "1.0.0", layoutPresetId: "modern-split", layoutPresetVersion: "1.0.0", layout: {
hero: "split", density: "comfortable", radius: "soft", gradientEmphasis: false,
} });
assert.equal(freeCombination.theme.id, "warm-amber");
await assert.rejects(() => service.resolve("technology-corporate", "1.0.0", { themeId: "no-such-theme", themeVersion: "1.0.0", brandColor: "#C2410C", fontId: "modern-sans", fontVersion: "1.0.0", layoutPresetId: "modern-split", layoutPresetVersion: "1.0.0", layout: {
hero: "split", density: "comfortable", radius: "soft", gradientEmphasis: false,
} }), /配色模板不存在/);
});
test("font renderer emits commercial-free heading and body stacks", async () => {
const service = new SiteTemplateCatalogService();
const { font } = await service.resolve("artisan-jewelry", "1.0.0", { themeId: "heritage-crimson", themeVersion: "1.0.0", brandColor: "#D1251A", fontId: "elegant-serif", fontVersion: "1.0.0", layoutPresetId: "craft-split", layoutPresetVersion: "1.0.0", layout: {
hero: "split", density: "comfortable", radius: "square", gradientEmphasis: false,
} });
const css = service.renderFont(font);
assert.match(css, /--font-heading: .*Noto Serif CJK SC/);
assert.match(css, /--font-body: .*Noto Sans CJK SC/);
});
test("theme renderer emits a complete semantic palette and custom primary color", async () => {
const service = new SiteTemplateCatalogService();
const { theme } = await service.resolve("brand-story", "1.0.0", "warm-amber", {
hero: "centered", density: "comfortable", radius: "soft",
});
const css = service.renderTheme(theme, "#123456");
const { theme } = await service.resolve("brand-story", "1.0.0", { themeId: "warm-amber", themeVersion: "1.0.0", brandColor: "#C2410C", fontId: "elegant-serif", fontVersion: "1.0.0", layoutPresetId: "editorial-centered", layoutPresetVersion: "1.0.0", layout: {
hero: "centered", density: "comfortable", radius: "soft", gradientEmphasis: false,
} });
const css = service.renderTheme(theme, "#123456", false);
assert.match(css, /--color-primary: #123456;/);
assert.match(css, /--color-text-primary:/);
assert.match(css, /--color-border:/);
assert.match(css, /--gradient-primary:/);
assert.match(css, /--gradient-primary: #123456;/);
assert.doesNotMatch(css, /function shade/);
});
test("theme renderer only emits gradients when explicitly emphasized", async () => {
const service = new SiteTemplateCatalogService();
const { theme } = await service.resolve("technology-corporate", "1.0.0", { themeId: "violet-tech", themeVersion: "1.0.0", brandColor: "#7028FF", fontId: "modern-sans", fontVersion: "1.0.0", layoutPresetId: "modern-split", layoutPresetVersion: "1.0.0", layout: {
hero: "split", density: "comfortable", radius: "soft", gradientEmphasis: true,
} });
assert.match(service.renderTheme(theme, "#7028FF", true), /--gradient-primary: linear-gradient/);
});
import path from "node:path";
import { readFile, readdir } from "node:fs/promises";
import type { PageTemplateInfo, SiteLayoutParameters, SiteTemplateCatalog, ThemePresetInfo } from "@webagent/shared";
import type { FontPresetInfo, LayoutPresetInfo, PageTemplateInfo, SiteDesignParameters, SiteTemplateCatalog, ThemePresetInfo } from "@webagent/shared";
import { config } from "../config.js";
type ThemeDefinition = ThemePresetInfo & {
primary: string; hover: string; light: string; secondary: string; dark: string;
background: string; soft: string; text: string; textSecondary: string; muted: string; border: string;
};
type ThemeDefinition = ThemePresetInfo;
type FontDefinition = FontPresetInfo;
type PageTemplateDefinition = PageTemplateInfo & {
status: "draft" | "testing" | "published" | "retired";
compatibleKernel: string;
content: {
eyebrow: string; heroTitle: string; heroDescription: string; primaryAction: string; secondaryAction: string;
metrics: Array<{ value: string; label: string }>;
advantages: Array<{ title: string; description: string }>;
};
};
export class SiteTemplateCatalogService {
async list(): Promise<SiteTemplateCatalog> {
const kernel = await this.kernel();
const [themes, pageTemplates] = await Promise.all([this.themes(), this.pageTemplates()]);
const [themes, fonts, pageTemplates, layoutPresets] = await Promise.all([this.themes(), this.fonts(), this.pageTemplates(), this.layoutPresets()]);
return {
kernel, themes: themes.map(({ id, name, colors }) => ({ id, name, colors })),
pageTemplates: pageTemplates.map(({ status: _status, compatibleKernel: _compatible, content: _content, ...template }) => template),
kernel, themes,
fonts, layoutPresets, pageTemplates: pageTemplates.map(({ status: _status, compatibleKernel: _compatible, ...template }) => template),
};
}
async resolve(pageTemplateId: string, version: string, themeId: string, layout: SiteLayoutParameters) {
const [kernel, templates, themes] = await Promise.all([this.kernel(), this.pageTemplates(), this.themes()]);
async resolve(pageTemplateId: string, version: string, design: SiteDesignParameters) {
const [kernel, templates, themes, fonts, layouts] = await Promise.all([this.kernel(), this.pageTemplates(), this.themes(), this.fonts(), this.layoutPresets()]);
const template = templates.find((item) => item.id === pageTemplateId && item.version === version);
if (!template) throw Object.assign(new Error("页面模板不存在或已下架"), { statusCode: 400 });
if (!template.compatibleKernel.startsWith(kernel.version.split(".")[0] + ".")) throw new Error("页面模板与当前官网内核不兼容");
if (!template.themeIds.includes(themeId)) throw Object.assign(new Error("当前页面模板不支持所选配色"), { statusCode: 400 });
const theme = themes.find((item) => item.id === themeId);
if (!template) throw Object.assign(new Error("官网外观模版不存在或已下架"), { statusCode: 400 });
if (!template.compatibleKernel.startsWith(kernel.version.split(".")[0] + ".")) throw new Error("官网外观模版与当前官网内核不兼容");
const theme = themes.find((item) => item.id === design.themeId && item.version === design.themeVersion);
if (!theme) throw Object.assign(new Error("配色模板不存在"), { statusCode: 400 });
const font = fonts.find((item) => item.id === design.fontId && item.version === design.fontVersion);
if (!font) throw Object.assign(new Error("字体方案不存在"), { statusCode: 400 });
if (!template.layoutPresetIds.includes(design.layoutPresetId)) throw Object.assign(new Error("当前官网外观模版不支持所选布局方案"), { statusCode: 400 });
const layoutPreset = layouts.find((item) => item.id === design.layoutPresetId && item.version === design.layoutPresetVersion);
if (!layoutPreset) throw Object.assign(new Error("布局方案不存在"), { statusCode: 400 });
for (const key of ["hero", "density", "radius"] as const) {
if (!(template.layouts[key] as string[]).includes(layout[key])) throw Object.assign(new Error(`页面模板不支持所选${key}参数`), { statusCode: 400 });
if (design.layout[key] !== layoutPreset.parameters[key]) throw Object.assign(new Error("布局参数与所选布局方案不一致"), { statusCode: 400 });
}
return { kernel, template, theme };
return { kernel, template, theme, font, layoutPreset };
}
renderTheme(theme: ThemeDefinition, brandColor: string): string {
renderFont(font: FontDefinition): string {
return `:root {
--font-heading: ${font.heading};
--font-body: ${font.body};
}
`;
}
renderTheme(theme: ThemeDefinition, brandColor: string, gradientEmphasis: boolean): string {
const primary = brandColor.toLowerCase();
const hover = shade(primary, -16);
const light = shade(primary, 24);
......@@ -60,9 +66,9 @@ export class SiteTemplateCatalogService {
--color-text-muted: ${theme.muted};
--color-border: ${theme.border};
--color-divider: ${theme.border};
--gradient-primary: linear-gradient(135deg, ${primary} 0%, ${theme.secondary} 100%);
--gradient-dark: linear-gradient(135deg, ${theme.dark} 0%, ${theme.primary} 60%, ${theme.secondary} 100%);
--gradient-soft: linear-gradient(180deg, ${theme.soft} 0%, #ffffff 100%);
--gradient-primary: ${gradientEmphasis ? `linear-gradient(135deg, ${primary} 0%, ${theme.secondary} 100%)` : primary};
--gradient-dark: ${gradientEmphasis ? `linear-gradient(135deg, ${theme.dark} 0%, ${theme.primary} 60%, ${theme.secondary} 100%)` : theme.dark};
--gradient-soft: ${gradientEmphasis ? `linear-gradient(180deg, ${theme.soft} 0%, #ffffff 100%)` : theme.soft};
}
`;
}
......@@ -77,6 +83,14 @@ export class SiteTemplateCatalogService {
return JSON.parse(await readFile(path.join(config.siteKitDir, "themes.json"), "utf8")) as ThemeDefinition[];
}
private async fonts(): Promise<FontDefinition[]> {
return JSON.parse(await readFile(path.join(config.siteKitDir, "fonts.json"), "utf8")) as FontDefinition[];
}
private async layoutPresets(): Promise<LayoutPresetInfo[]> {
return JSON.parse(await readFile(path.join(config.siteKitDir, "layouts.json"), "utf8")) as LayoutPresetInfo[];
}
private async pageTemplates(): Promise<PageTemplateDefinition[]> {
const root = path.join(config.siteKitDir, "page-templates");
const templateDirectories = await readdir(root, { withFileTypes: true });
......
declare module "word-extractor" {
interface WordDocument {
getBody(options?: { filterUnicode?: boolean }): string;
}
export default class WordExtractor {
extract(source: string | Buffer): Promise<WordDocument>;
}
}
......@@ -3,22 +3,26 @@
官网创建由三个独立版本域组成:
1. **内核版本**:平台维护的构建、SEO/GEO、文章协议与发布能力。普通用户不选择,站点保存精确版本。
2. **页面成品版本**:用户在创建时选择的页面形状、默认内容和视觉方向。创建后成为站点自己的页面,不自动跟随模板升级。
2. **官网外观模版版本**:用户在创建时选择的页面形状、默认内容和视觉方向。创建后成为站点自己的页面,不自动跟随模版升级。
3. **站点 Git 版本**:用户和 Agent 对生成结果的持续修改历史。
## 目录职责
- `site-kit/kernel/manifest.json`:当前稳定内核的身份和版本。
- `site-kit/page-templates/<id>/<version>/manifest.json`:已发布页面成品及其兼容范围、默认内容、支持配色和布局参数。
- `site-kit/page-templates/<id>/<version>/manifest.json`:已发布官网外观模版及其兼容范围、默认内容、默认配色与字体、兼容布局参数。配色方案与外观模版自由组合,没有从属关系。
- `site-kit/layouts.json`:可复用、带版本的布局方案;官网外观模版仅声明兼容的布局 ID。
- `site-kit/fonts.json`:可复用、带版本的字体方案,均为免费商用字体栈;官网外观模版声明默认字体,创建时可改选。
- `site-kit/themes.json`:完整语义配色方案。
- `astro-template/`:当前内核运行载体和页面渲染契约。
页面成品发布后不可原地修改;任何调整必须创建新版本。创建服务只接受目录中已发布且与当前内核兼容的精确版本组合。
官网外观模版发布后不可原地修改;任何调整必须创建新版本。创建服务只接受目录中已发布且与当前内核兼容的精确版本组合。
## 创建不变量
- 先生成并构建不依赖模型的基础成品。
- 用户未填写 Prompt 时,基础成品直接成为第一版官网。
- 用户填写 Prompt 时,Agent 只在站点草稿中修改并接受路径与构建校验。
- 页面视觉配置集中管理布局、配色、品牌色、密度、圆角与渐变偏好;`gradientEmphasis` 默认关闭,未明确开启时禁止 Agent 主动增加大面积渐变。
- 公司资料先在平台侧提取为受长度限制的文本,再作为首次定制上下文使用;二进制文件不进入站点源码和 Git 历史。
- 智能定制成功后保存为第二个 Git 版本;失败则丢弃草稿,保留已经可用的基础成品。
- `metadata/site.json` 和站点根目录的 `webagent.site.json` 记录精确创建来源;页面模板后续升级不会覆盖已有网站。
- `metadata/site.json` 和站点根目录的 `webagent.site.json` 记录精确创建来源;官网外观模版后续升级不会覆盖已有网站。
This diff is collapsed.
import type { CSSProperties } from "react";
import type { FontPresetInfo, PageTemplateInfo, SiteLayoutParameters, ThemePresetInfo } from "@webagent/shared";
const NAV_LINKS = ["关于我们", "服务能力", "企业优势", "文章"];
const CHART_BARS = [32, 44, 39, 62, 58, 76, 92];
interface TemplateLivePreviewProps {
template: PageTemplateInfo;
theme: ThemePresetInfo;
font: FontPresetInfo;
brandColor: string;
layout: SiteLayoutParameters;
}
export function TemplateLivePreview({ template, theme, font, brandColor, layout }: TemplateLivePreviewProps) {
const { content, preview } = template;
const style = {
"--tlp-primary": brandColor,
"--tlp-light": theme.light,
"--tlp-secondary": theme.secondary,
"--tlp-dark": theme.dark,
"--tlp-bg": theme.background,
"--tlp-soft": theme.soft,
"--tlp-text": theme.text,
"--tlp-text2": theme.textSecondary,
"--tlp-muted": theme.muted,
"--tlp-border": theme.border,
"--tlp-heading-font": font.heading,
"--tlp-body-font": font.body,
"--tlp-radius": layout.radius === "square" ? "0.5cqw" : "1.8cqw",
"--tlp-radius-sm": layout.radius === "square" ? "0.3cqw" : "1.1cqw",
} as CSSProperties;
const heroStyle: CSSProperties = { background: layout.gradientEmphasis ? `linear-gradient(180deg, ${theme.soft} 0%, #ffffff 100%)` : theme.soft };
const visualStyle: CSSProperties = { background: layout.gradientEmphasis ? `linear-gradient(135deg, ${theme.dark} 0%, ${brandColor} 60%, ${theme.secondary} 100%)` : theme.dark };
return <div className={`template-live-preview${layout.density === "compact" ? " tlp-compact" : ""}`} style={style}>
<nav className="tlp-nav">
<span className="tlp-brand"><i>W</i><b /></span>
<span className="tlp-links">{NAV_LINKS.map((link) => <u key={link}>{link}</u>)}</span>
<span className="tlp-nav-action">联系合作 <i></i></span>
</nav>
<header className={`tlp-hero tlp-hero-${layout.hero}`} style={heroStyle}>
<div className="tlp-hero-copy">
<span className="tlp-eyebrow">{content.eyebrow}</span>
<h1>{content.heroTitle.split("\n").map((line) => <span key={line}>{line}</span>)}</h1>
<p>{content.heroDescription}</p>
<div className="tlp-actions"><a>{content.primaryAction}<i></i></a><a className="tlp-secondary">{content.secondaryAction}</a></div>
<div className="tlp-metrics">{content.metrics.map((metric) => <div key={metric.label}><strong>{metric.value}</strong><span>{metric.label}</span></div>)}</div>
</div>
{layout.hero === "split" && (preview.heroImage
? <div className="tlp-hero-image"><s>{preview.heroImage.width}×{preview.heroImage.height}</s></div>
: <div className="tlp-visual" style={visualStyle}>
<div className="tlp-visual-top"><span>BUSINESS INTELLIGENCE</span><em>● LIVE</em></div>
<div className="tlp-visual-title"><span>增长动能</span><strong>+38.6%</strong></div>
<div className="tlp-visual-chart">{CHART_BARS.map((height) => <i key={height} style={{ height: `${height}%` }} />)}</div>
<div className="tlp-visual-footer"><div><span>智能决策</span><strong>实时洞察</strong></div><div><span>业务协同</span><strong>全链连接</strong></div></div>
</div>)}
</header>
<section className="tlp-section tlp-services">
<div className="tlp-heading"><span className="tlp-eyebrow">WHAT WE DO</span><h2>聚焦关键问题,<br />提供完整解法。</h2></div>
<div className="tlp-service-grid">{template.industries.slice(0, 3).map((industry, index) => <article key={industry}>
<span className="tlp-number">{String(index + 1).padStart(2, "0")}</span>
<div className="tlp-icon"></div>
<h3>{industry}</h3>
<p>以专业方法和可靠交付,为客户提供高质量的{industry}服务。</p>
<a>探索服务 <i></i></a>
</article>)}</div>
</section>
<section className="tlp-section tlp-advantages" style={{ background: theme.dark }}>
<div className="tlp-advantage-intro">
<span className="tlp-eyebrow">WHY CHOOSE US</span>
<h2>不止完成项目,<br />更在意结果发生。</h2>
<p>清晰、克制、可靠,是我们对每一次合作的共同承诺。</p>
</div>
<div className="tlp-advantage-list">{content.advantages.map((advantage, index) => <article key={advantage.title}><span>0{index + 1}</span><div><h3>{advantage.title}</h3><p>{advantage.description}</p></div></article>)}</div>
</section>
<section className="tlp-cta"><h2>准备好开启数字化增长了吗?</h2><a>{content.primaryAction}<i></i></a></section>
<footer className="tlp-footer" style={{ background: theme.dark }}><i /><span /><span /><span /></footer>
</div>;
}
import type { AgentRunEvent, AgentRunInfo, AgentSettings, ArticleCompileResult, ArticleDocument, ArticleImageUploadResult, ArticleInput, ArticleListResult, ArticleMutationResult, CreateSiteInput, CreateTenantInput, DomainBinding, DomainListResult, DraftPreviewResult, GitHistoryItem, PreviewVersionResult, PublishResult, SessionInfo, SiteInfo, SiteTemplateCatalog, TenantAdminInfo, TenantAdminSiteInfo } from "@webagent/shared";
import type { AgentRunEvent, AgentRunInfo, AgentSettings, ArticleCompileResult, ArticleDocument, ArticleImageUploadResult, ArticleInput, ArticleListResult, ArticleMutationResult, CompanyDocumentExtractResult, CreateSiteInput, CreateTenantInput, DomainBinding, DomainListResult, DraftPreviewResult, GitHistoryItem, PreviewVersionResult, PublishResult, SessionInfo, SiteInfo, SiteTemplateCatalog, TenantAdminInfo, TenantAdminSiteInfo } from "@webagent/shared";
async function request<T>(url: string, options?: RequestInit): Promise<T> {
const headers = new Headers(options?.headers);
......@@ -27,6 +27,7 @@ export const api = {
updateAgentSettings: (provider: AgentSettings["provider"]) => request<AgentSettings>("/api/agent-settings", { method: "PUT", body: JSON.stringify({ provider }) }),
sites: () => request<SiteInfo[]>("/api/sites"),
siteTemplates: () => request<SiteTemplateCatalog>("/api/site-templates"),
extractCompanyDocument: (filename: string, dataUrl: string) => request<CompanyDocumentExtractResult>("/api/company-documents/extract", { method: "POST", body: JSON.stringify({ filename, dataUrl }) }),
archivedSites: () => request<SiteInfo[]>("/api/archived-sites"),
site: (siteId: string) => request<SiteInfo>("/api/sites/" + siteId),
createSite: (input: CreateSiteInput) => request<SiteInfo>("/api/sites", { method: "POST", body: JSON.stringify(input) }),
......
This diff is collapsed.
This diff is collapsed.
......@@ -9,24 +9,103 @@ export interface CreateSiteInput {
services: string[];
email: string;
phone?: string;
brandColor: string;
pageTemplateId: string;
pageTemplateVersion: string;
design: SiteDesignParameters;
initialPrompt?: string;
}
export interface SiteDesignParameters {
themeId: string;
themeVersion: string;
brandColor: string;
fontId: string;
fontVersion: string;
layoutPresetId: string;
layoutPresetVersion: string;
layout: SiteLayoutParameters;
initialPrompt?: string;
}
export interface SiteLayoutParameters {
hero: "split" | "centered";
density: "comfortable" | "compact";
radius: "soft" | "square";
gradientEmphasis: boolean;
}
export interface CompanyDocumentExtractInput {
filename: string;
dataUrl: string;
}
export interface CompanyDocumentExtractResult {
filename: string;
format: "markdown" | "word" | "pdf" | "excel";
text: string;
characters: number;
truncated: boolean;
}
export interface ThemePresetInfo {
id: string;
version: string;
name: string;
colors: string[];
primary: string;
hover: string;
light: string;
secondary: string;
dark: string;
background: string;
soft: string;
text: string;
textSecondary: string;
muted: string;
border: string;
}
export interface LayoutPresetInfo {
id: string;
version: string;
name: string;
description: string;
parameters: Omit<SiteLayoutParameters, "gradientEmphasis">;
}
export interface FontPresetInfo {
id: string;
version: string;
name: string;
style: "sans" | "serif";
description: string;
heading: string;
body: string;
}
export interface TemplatePreviewImage {
width: number;
height: number;
}
export interface PageTemplatePreview {
accent: string;
surface: string;
mode: "light" | "dark";
hero: "centered" | "split";
heroImage?: TemplatePreviewImage;
stats?: "light" | "dark";
gallery?: { count: number; image: TemplatePreviewImage };
cards?: number;
}
export interface PageTemplateContent {
eyebrow: string;
heroTitle: string;
heroDescription: string;
primaryAction: string;
secondaryAction: string;
metrics: Array<{ value: string; label: string }>;
advantages: Array<{ title: string; description: string }>;
}
export interface PageTemplateInfo {
......@@ -35,21 +114,20 @@ export interface PageTemplateInfo {
name: string;
description: string;
industries: string[];
preview: { accent: string; surface: string; mode: "light" | "dark" };
preview: PageTemplatePreview;
defaultThemeId: string;
themeIds: string[];
layouts: {
hero: SiteLayoutParameters["hero"][];
density: SiteLayoutParameters["density"][];
radius: SiteLayoutParameters["radius"][];
};
defaults: SiteLayoutParameters;
defaultFontId: string;
layoutPresetIds: string[];
defaultLayoutPresetId: string;
content: PageTemplateContent;
}
export interface SiteTemplateCatalog {
kernel: { id: string; version: string };
pageTemplates: PageTemplateInfo[];
themes: ThemePresetInfo[];
fonts: FontPresetInfo[];
layoutPresets: LayoutPresetInfo[];
}
export interface SiteTemplateProvenance {
......@@ -57,8 +135,7 @@ export interface SiteTemplateProvenance {
createdFrom: {
pageTemplateId: string;
pageTemplateVersion: string;
themeId: string;
layout: SiteLayoutParameters;
design: SiteDesignParameters;
};
initialPrompt?: string;
}
......
[
{
"id": "modern-sans",
"version": "1.0.0",
"name": "等粗 · 现代黑体",
"style": "sans",
"description": "笔画均匀、干净利落,思源黑体 / MiSans 等免费商用字体",
"heading": "Inter, \"MiSans\", \"HarmonyOS Sans SC\", \"Source Han Sans SC\", \"Noto Sans CJK SC\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif",
"body": "Inter, \"MiSans\", \"HarmonyOS Sans SC\", \"Source Han Sans SC\", \"Noto Sans CJK SC\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif"
},
{
"id": "elegant-serif",
"version": "1.0.0",
"name": "不等粗 · 典雅宋体",
"style": "serif",
"description": "横细竖粗、典雅书卷气,思源宋体免费商用,正文保持黑体易读",
"heading": "\"Source Han Serif SC\", \"Noto Serif CJK SC\", \"Songti SC\", \"STSong\", \"SimSun\", serif",
"body": "Inter, \"MiSans\", \"HarmonyOS Sans SC\", \"Source Han Sans SC\", \"Noto Sans CJK SC\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif"
}
]
[
{ "id": "modern-split", "version": "1.0.0", "name": "现代分栏", "description": "左右首屏、舒展留白和柔和圆角", "parameters": { "hero": "split", "density": "comfortable", "radius": "soft" } },
{ "id": "editorial-centered", "version": "1.0.0", "name": "叙事居中", "description": "居中首屏、舒展节奏和柔和圆角", "parameters": { "hero": "centered", "density": "comfortable", "radius": "soft" } },
{ "id": "professional-compact", "version": "1.0.0", "name": "专业紧凑", "description": "居中首屏、紧凑信息和利落直角", "parameters": { "hero": "centered", "density": "compact", "radius": "square" } },
{ "id": "craft-split", "version": "1.0.0", "name": "雅致分栏", "description": "左右首屏、舒展留白和利落直角", "parameters": { "hero": "split", "density": "comfortable", "radius": "square" } }
]
{
"id": "artisan-jewelry",
"version": "1.0.0",
"name": "匠心手作品牌",
"description": "朱红与金韵相映的东方质感,左右首屏带产品主图,适合珠宝银饰、非遗手作与私人定制。",
"industries": ["珠宝银饰", "非遗手作", "私人定制"],
"status": "published",
"compatibleKernel": "1.x",
"preview": { "accent": "#D1251A", "surface": "#F7F3EA", "mode": "light", "hero": "split", "heroImage": { "width": 800, "height": 800 }, "stats": "dark", "gallery": { "count": 3, "image": { "width": 800, "height": 800 } } },
"defaultThemeId": "heritage-crimson",
"defaultFontId": "elegant-serif",
"layoutPresetIds": ["craft-split", "modern-split"],
"defaultLayoutPresetId": "craft-split",
"content": {
"eyebrow": "HANDCRAFTED WITH CARE",
"heroTitle": "把重要的心意,\n做成一件长久信物",
"heroDescription": "从首饰零售、来料加工到专属雕刻与手作体验,以透明用料、匠人手作和长效售后,让每一件作品经得起时间。",
"primaryAction": "沟通定制构想",
"secondaryAction": "探索手作系列",
"metrics": [{"value":"10年+","label":"手作行业积淀"},{"value":"100+","label":"线下服务门店"},{"value":"终身","label":"免费养护承诺"}],
"advantages": [{"title":"真材实料","description":"用料可复检,材质、克重与工费清晰说明。"},{"title":"专属定制","description":"从刻字、錾刻到旧料改造,实现每一份独特构想。"},{"title":"长久陪伴","description":"终身免费清洗、抛光与基础养护,历久弥新。"}]
}
}
......@@ -6,11 +6,11 @@
"industries": ["消费品牌", "文化创意", "设计机构"],
"status": "published",
"compatibleKernel": "1.x",
"preview": { "accent": "#C2410C", "surface": "#FFF7ED", "mode": "light" },
"preview": { "accent": "#C2410C", "surface": "#FFF7ED", "mode": "light", "hero": "centered", "cards": 3 },
"defaultThemeId": "warm-amber",
"themeIds": ["warm-amber", "graphite", "forest-green", "violet-tech"],
"layouts": { "hero": ["centered", "split"], "density": ["comfortable", "compact"], "radius": ["soft", "square"] },
"defaults": { "hero": "centered", "density": "comfortable", "radius": "soft" },
"defaultFontId": "elegant-serif",
"layoutPresetIds": ["editorial-centered", "modern-split"],
"defaultLayoutPresetId": "editorial-centered",
"content": {
"eyebrow": "MADE WITH PURPOSE",
"heroTitle": "把日常的灵感,\n做成值得记住的品牌",
......
{
"id": "mobility-services",
"version": "1.0.0",
"name": "出行服务企业",
"description": "直接有力的暖橙行动派版式,适合网约车、汽车租售与同城生活服务。",
"industries": ["网约车服务", "汽车租售", "同城服务"],
"status": "published",
"compatibleKernel": "1.x",
"preview": { "accent": "#FF7D41", "surface": "#F7F6F5", "mode": "light", "hero": "centered", "stats": "dark", "cards": 3 },
"defaultThemeId": "vibrant-orange",
"defaultFontId": "modern-sans",
"layoutPresetIds": ["editorial-centered", "professional-compact"],
"defaultLayoutPresetId": "editorial-centered",
"content": {
"eyebrow": "ONE-STOP LOCAL SERVICE",
"heroTitle": "入行大小事,\n一站一次办清",
"heroDescription": "车辆租售、证照代办、平台入驻与上岗培训,本地实体门店全程对接,合同与费用当面确认,让每一步都清楚透明。",
"primaryAction": "咨询入行方案",
"secondaryAction": "查看服务流程",
"metrics": [{"value":"1000+","label":"服务司机"},{"value":"7×16","label":"客服在线"},{"value":"一站式","label":"租售办证闭环"}],
"advantages": [{"title":"正规资质","description":"平台合作与经营资质齐全,合作信息透明可查。"},{"title":"因人定案","description":"按预算、时间与经验匹配车型、平台和用车方式。"},{"title":"实体门店","description":"咨询、签约、培训与售后均可面对面沟通。"}]
}
}
......@@ -6,11 +6,11 @@
"industries": ["管理咨询", "法律财税", "专业服务"],
"status": "published",
"compatibleKernel": "1.x",
"preview": { "accent": "#175CD3", "surface": "#EFF8FF", "mode": "light" },
"preview": { "accent": "#175CD3", "surface": "#EFF8FF", "mode": "light", "hero": "centered", "cards": 3 },
"defaultThemeId": "ocean-blue",
"themeIds": ["ocean-blue", "forest-green", "graphite", "violet-tech"],
"layouts": { "hero": ["centered", "split"], "density": ["comfortable", "compact"], "radius": ["square", "soft"] },
"defaults": { "hero": "centered", "density": "comfortable", "radius": "square" },
"defaultFontId": "modern-sans",
"layoutPresetIds": ["professional-compact", "editorial-centered", "modern-split"],
"defaultLayoutPresetId": "professional-compact",
"content": {
"eyebrow": "TRUSTED PROFESSIONALS",
"heroTitle": "以专业判断,\n回应复杂商业问题",
......
......@@ -6,11 +6,11 @@
"industries": ["人工智能", "企业软件", "数字化服务"],
"status": "published",
"compatibleKernel": "1.x",
"preview": { "accent": "#7028FF", "surface": "#F3F0FF", "mode": "light" },
"preview": { "accent": "#7028FF", "surface": "#F3F0FF", "mode": "light", "hero": "split", "cards": 3 },
"defaultThemeId": "violet-tech",
"themeIds": ["violet-tech", "ocean-blue", "forest-green", "graphite"],
"layouts": { "hero": ["split", "centered"], "density": ["comfortable", "compact"], "radius": ["soft", "square"] },
"defaults": { "hero": "split", "density": "comfortable", "radius": "soft" },
"defaultFontId": "modern-sans",
"layoutPresetIds": ["modern-split", "editorial-centered", "professional-compact"],
"defaultLayoutPresetId": "modern-split",
"content": {
"eyebrow": "AI-POWERED BUSINESS",
"heroTitle": "让每一次数字化升级,\n都成为增长的起点",
......
{
"id": "wellness-franchise",
"version": "1.0.0",
"name": "大健康连锁企业",
"description": "温和可信的青碧色调与居中叙事,适合美业、健康调理与连锁招商品牌。",
"industries": ["美业美体", "健康调理", "连锁招商"],
"status": "published",
"compatibleKernel": "1.x",
"preview": { "accent": "#16AE9C", "surface": "#FAF8F4", "mode": "light", "hero": "centered", "stats": "light", "cards": 4 },
"defaultThemeId": "teal-wellness",
"defaultFontId": "modern-sans",
"layoutPresetIds": ["editorial-centered", "modern-split"],
"defaultLayoutPresetId": "editorial-centered",
"content": {
"eyebrow": "HOLISTIC WELLNESS CARE",
"heroTitle": "以东方调理智慧,\n守护身体自然状态",
"heroDescription": "以非侵入式调理与周期管理,为消费者提供体重、塑形与日常健康管理的一体化方案,并与每一家门店长期共建。",
"primaryAction": "预约到店体验",
"secondaryAction": "了解调理方案",
"metrics": [{"value":"300+","label":"连锁服务门店"},{"value":"10年","label":"行业深耕"},{"value":"4维","label":"健康管理目标"}],
"advantages": [{"title":"温和调理","description":"以非侵入式方法顺应身体节律,体验更安心。"},{"title":"周期方案","description":"结合个人状态制定阶段计划,变化更可持续。"},{"title":"门店共建","description":"培训、运营与售后团队持续支持每一家门店。"}]
}
}
[
{ "id": "violet-tech", "name": "科技紫", "colors": ["#7028FF", "#536CFF", "#F3F0FF"], "primary": "#7028ff", "hover": "#5c1fe0", "light": "#8b5cff", "secondary": "#536cff", "dark": "#24105f", "background": "#f8f8fc", "soft": "#f3f0ff", "text": "#191927", "textSecondary": "#4f5260", "muted": "#8b8e99", "border": "#e7e3f3" },
{ "id": "ocean-blue", "name": "商务蓝", "colors": ["#175CD3", "#2E90FA", "#EFF8FF"], "primary": "#175cd3", "hover": "#144da8", "light": "#53b1fd", "secondary": "#2e90fa", "dark": "#102a56", "background": "#f7faff", "soft": "#eff8ff", "text": "#101828", "textSecondary": "#475467", "muted": "#98a2b3", "border": "#d1e9ff" },
{ "id": "forest-green", "name": "自然绿", "colors": ["#087A5B", "#12B76A", "#ECFDF3"], "primary": "#087a5b", "hover": "#066149", "light": "#32d583", "secondary": "#12b76a", "dark": "#12372d", "background": "#f8fbf9", "soft": "#ecfdf3", "text": "#16251f", "textSecondary": "#486158", "muted": "#8ca198", "border": "#ccebdc" },
{ "id": "warm-amber", "name": "品牌暖橙", "colors": ["#C2410C", "#F59E0B", "#FFF7ED"], "primary": "#c2410c", "hover": "#9a3412", "light": "#fb923c", "secondary": "#f59e0b", "dark": "#431407", "background": "#fffbf7", "soft": "#fff7ed", "text": "#29201b", "textSecondary": "#65544a", "muted": "#9f8d82", "border": "#fed7aa" },
{ "id": "graphite", "name": "黑金质感", "colors": ["#B78932", "#E4C477", "#F6F2E8"], "primary": "#9a7025", "hover": "#795719", "light": "#d6b45f", "secondary": "#b78932", "dark": "#1e1c18", "background": "#f8f6f1", "soft": "#f6f2e8", "text": "#1d1c19", "textSecondary": "#5e594f", "muted": "#969083", "border": "#ded7c7" }
{ "id": "violet-tech", "version": "1.0.0", "name": "科技紫", "colors": ["#7028FF", "#536CFF", "#F3F0FF"], "primary": "#7028ff", "hover": "#5c1fe0", "light": "#8b5cff", "secondary": "#536cff", "dark": "#24105f", "background": "#f8f8fc", "soft": "#f3f0ff", "text": "#191927", "textSecondary": "#4f5260", "muted": "#8b8e99", "border": "#e7e3f3" },
{ "id": "ocean-blue", "version": "1.0.0", "name": "商务蓝", "colors": ["#175CD3", "#2E90FA", "#EFF8FF"], "primary": "#175cd3", "hover": "#144da8", "light": "#53b1fd", "secondary": "#2e90fa", "dark": "#102a56", "background": "#f7faff", "soft": "#eff8ff", "text": "#101828", "textSecondary": "#475467", "muted": "#98a2b3", "border": "#d1e9ff" },
{ "id": "forest-green", "version": "1.0.0", "name": "自然绿", "colors": ["#087A5B", "#12B76A", "#ECFDF3"], "primary": "#087a5b", "hover": "#066149", "light": "#32d583", "secondary": "#12b76a", "dark": "#12372d", "background": "#f8fbf9", "soft": "#ecfdf3", "text": "#16251f", "textSecondary": "#486158", "muted": "#8ca198", "border": "#ccebdc" },
{ "id": "warm-amber", "version": "1.0.0", "name": "品牌暖橙", "colors": ["#C2410C", "#F59E0B", "#FFF7ED"], "primary": "#c2410c", "hover": "#9a3412", "light": "#fb923c", "secondary": "#f59e0b", "dark": "#431407", "background": "#fffbf7", "soft": "#fff7ed", "text": "#29201b", "textSecondary": "#65544a", "muted": "#9f8d82", "border": "#fed7aa" },
{ "id": "graphite", "version": "1.0.0", "name": "黑金质感", "colors": ["#B78932", "#E4C477", "#F6F2E8"], "primary": "#9a7025", "hover": "#795719", "light": "#d6b45f", "secondary": "#b78932", "dark": "#1e1c18", "background": "#f8f6f1", "soft": "#f6f2e8", "text": "#1d1c19", "textSecondary": "#5e594f", "muted": "#969083", "border": "#ded7c7" },
{ "id": "teal-wellness", "version": "1.0.0", "name": "青碧康养", "colors": ["#16AE9C", "#0E727C", "#E4EFEA"], "primary": "#16ae9c", "hover": "#11897b", "light": "#43c4b3", "secondary": "#0e727c", "dark": "#173f38", "background": "#faf8f4", "soft": "#e4efea", "text": "#24322f", "textSecondary": "#5d6d68", "muted": "#93a59e", "border": "#d6e4de" },
{ "id": "vibrant-orange", "version": "1.0.0", "name": "活力暖橙", "colors": ["#FF7D41", "#FFA76B", "#FFF1E9"], "primary": "#ff7d41", "hover": "#e96a32", "light": "#ffa375", "secondary": "#ffa76b", "dark": "#231916", "background": "#f7f6f5", "soft": "#fff1e9", "text": "#231916", "textSecondary": "#6b625f", "muted": "#a89f9b", "border": "#e8e5e3" },
{ "id": "heritage-crimson", "version": "1.0.0", "name": "朱红金韵", "colors": ["#D1251A", "#C6A84B", "#F7F3EA"], "primary": "#d1251a", "hover": "#a81d15", "light": "#e5584e", "secondary": "#c6a84b", "dark": "#2b2420", "background": "#faf7f1", "soft": "#f7f3ea", "text": "#1e1916", "textSecondary": "#4b4541", "muted": "#9a9188", "border": "#e8e0d6" }
]
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment