Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in / Register
Toggle navigation
W
WebAgent
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
xuchentao
WebAgent
Commits
61c4a3b5
Commit
61c4a3b5
authored
Jul 29, 2026
by
xuchentao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add versioned site creation templates
parent
b542c952
Changes
20
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
486 additions
and
14 deletions
+486
-14
design.json
astro-template/src/data/design.json
+5
-0
BaseLayout.astro
astro-template/src/layouts/BaseLayout.astro
+2
-1
global.css
astro-template/src/styles/global.css
+13
-0
config.ts
backend/src/config.ts
+1
-0
schemas.ts
backend/src/schemas.ts
+9
-0
server.ts
backend/src/server.ts
+4
-1
create-site.ts
backend/src/sites/create-site.ts
+20
-4
site-template-catalog.test.ts
backend/src/sites/site-template-catalog.test.ts
+39
-0
site-template-catalog.ts
backend/src/sites/site-template-catalog.ts
+101
-0
site-template-architecture.md
docs/site-template-architecture.md
+24
-0
App.tsx
frontend/src/App.tsx
+77
-7
api.ts
frontend/src/api.ts
+2
-1
create-template.css
frontend/src/create-template.css
+54
-0
main.tsx
frontend/src/main.tsx
+1
-0
index.ts
shared/src/index.ts
+52
-0
manifest.json
site-kit/kernel/manifest.json
+6
-0
manifest.json
site-kit/page-templates/brand-story/1.0.0/manifest.json
+23
-0
manifest.json
.../page-templates/professional-services/1.0.0/manifest.json
+23
-0
manifest.json
...t/page-templates/technology-corporate/1.0.0/manifest.json
+23
-0
themes.json
site-kit/themes.json
+7
-0
No files found.
astro-template/src/data/design.json
0 → 100644
View file @
61c4a3b5
{
"pageTemplate"
:
{
"id"
:
"technology-corporate"
,
"version"
:
"1.0.0"
},
"themeId"
:
"violet-tech"
,
"layout"
:
{
"hero"
:
"split"
,
"density"
:
"comfortable"
,
"radius"
:
"soft"
}
}
astro-template/src/layouts/BaseLayout.astro
View file @
61c4a3b5
---
import SeoHead from "../_platform/seo/SeoHead.astro";
import site from "../data/site.json";
import design from "../data/design.json";
interface Props {
title: string;
...
...
@@ -25,5 +26,5 @@ const props = Astro.props;
<SeoHead
{...
props
}
/>
<slot
name=
"head"
/>
</head>
<body><slot
/></body>
<body
data-page-template=
{design.pageTemplate.id}
data-hero=
{design.layout.hero}
data-density=
{design.layout.density}
data-radius=
{design.layout.radius}
><slot
/></body>
</html>
astro-template/src/styles/global.css
View file @
61c4a3b5
...
...
@@ -18,6 +18,19 @@ button, input { font: inherit; }
}
.eyebrow
::before
{
content
:
""
;
width
:
22px
;
height
:
2px
;
background
:
var
(
--gradient-primary
);
}
.section
{
padding
:
112px
0
;
}
body
[
data-density
=
"compact"
]
.section
{
padding
:
78px
0
;
}
body
[
data-radius
=
"square"
]
:where
(
.visual
,
.contact-card
,
.service-grid
article
,
.actions
a
,
.nav-action
)
{
border-radius
:
3px
!important
;
}
body
[
data-hero
=
"centered"
]
.hero-grid
{
grid-template-columns
:
1
fr
;
text-align
:
center
;
}
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-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
;
}
.section-heading
h2
{
margin
:
14px
0
18px
;
font-size
:
clamp
(
34px
,
4.6vw
,
58px
);
letter-spacing
:
-.045em
;
line-height
:
1.08
;
}
.section-heading
p
{
margin
:
0
;
color
:
var
(
--color-text-secondary
);
font-size
:
17px
;
line-height
:
1.8
;
}
...
...
backend/src/config.ts
View file @
61c4a3b5
...
...
@@ -46,6 +46,7 @@ export const config = {
rootDir
,
runtimeDir
:
path
.
resolve
(
currentDir
,
"../../.runtime"
),
templateDir
:
path
.
resolve
(
currentDir
,
"../../astro-template"
),
siteKitDir
:
path
.
resolve
(
currentDir
,
"../../site-kit"
),
sitesDir
,
previewsDir
,
productionDir
,
...
...
backend/src/schemas.ts
View file @
61c4a3b5
...
...
@@ -8,6 +8,15 @@ export const createSiteSchema = z.object({
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"
]),
}),
initialPrompt
:
z
.
string
().
trim
().
max
(
2000
).
optional
().
or
(
z
.
literal
(
""
).
transform
(()
=>
undefined
)),
});
export
const
loginSchema
=
z
.
object
({
...
...
backend/src/server.ts
View file @
61c4a3b5
...
...
@@ -16,6 +16,7 @@ import {
tenantStatusSchema
,
updateDomainSchema
,
versionCommitSchema
,
}
from
"./schemas.js"
;
import
{
CreateSiteService
}
from
"./sites/create-site.js"
;
import
{
SiteTemplateCatalogService
}
from
"./sites/site-template-catalog.js"
;
import
{
SiteRepository
}
from
"./sites/site-repository.js"
;
import
{
SiteVersionService
}
from
"./sites/site-version-service.js"
;
import
{
SiteLifecycleService
}
from
"./sites/site-lifecycle-service.js"
;
...
...
@@ -39,7 +40,8 @@ const sites = new SiteRepository();
const
git
=
new
GitManager
();
const
builds
=
new
BuildManager
();
const
previews
=
new
PreviewProcessManager
();
const
createSite
=
new
CreateSiteService
(
sites
,
git
,
builds
,
previews
);
const
templateCatalog
=
new
SiteTemplateCatalogService
();
const
createSite
=
new
CreateSiteService
(
sites
,
git
,
builds
,
previews
,
templateCatalog
);
const
agentRuns
=
new
AgentRunRepository
();
const
agentProviders
=
createAgentProviders
();
const
versions
=
new
SiteVersionService
(
sites
,
git
,
builds
,
previews
);
...
...
@@ -88,6 +90,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
.
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
))));
...
...
backend/src/sites/create-site.ts
View file @
61c4a3b5
...
...
@@ -7,6 +7,7 @@ import { GitManager } from "../git/git-manager.js";
import
{
BuildManager
}
from
"../build/build-manager.js"
;
import
{
PreviewProcessManager
}
from
"../preview/preview-process-manager.js"
;
import
{
SiteRepository
}
from
"./site-repository.js"
;
import
{
SiteTemplateCatalogService
}
from
"./site-template-catalog.js"
;
export
class
CreateSiteService
{
constructor
(
...
...
@@ -14,16 +15,26 @@ export class CreateSiteService {
private
readonly
git
:
GitManager
,
private
readonly
builds
:
BuildManager
,
private
readonly
previews
:
PreviewProcessManager
,
private
readonly
templates
:
SiteTemplateCatalogService
,
)
{}
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
siteId
=
"site_"
+
crypto
.
randomBytes
(
16
).
toString
(
"hex"
);
const
projectPath
=
this
.
sites
.
getProjectPath
(
tenantId
,
siteId
);
const
previewPort
=
await
this
.
sites
.
allocatePreviewPort
();
const
now
=
new
Date
().
toISOString
();
const
site
:
SiteInfo
=
{
tenantId
,
siteId
,
name
:
input
.
name
,
industry
:
input
.
industry
,
status
:
"creating"
,
templateVersion
:
"1.0.0"
,
environmentVersion
:
4
,
tenantId
,
siteId
,
name
:
input
.
name
,
industry
:
input
.
industry
,
status
:
"creating"
,
templateVersion
:
selection
.
kernel
.
version
,
environmentVersion
:
4
,
template
:
{
kernel
:
selection
.
kernel
,
createdFrom
:
{
pageTemplateId
:
selection
.
template
.
id
,
pageTemplateVersion
:
selection
.
template
.
version
,
themeId
:
selection
.
theme
.
id
,
layout
:
input
.
layout
,
},
...(
input
.
initialPrompt
?
{
initialPrompt
:
input
.
initialPrompt
}
:
{}),
},
previewPort
,
previewUrl
:
getPublicPreviewUrl
(
tenantId
,
siteId
),
currentCommit
:
""
,
publishStatus
:
"unpublished"
,
createdAt
:
now
,
updatedAt
:
now
,
};
...
...
@@ -56,12 +67,17 @@ export class CreateSiteService {
siteData
.
organization
.
legalName
=
input
.
name
;
await
writeFile
(
siteDataPath
,
JSON
.
stringify
(
siteData
,
null
,
2
)
+
"
\n
"
,
"utf8"
);
const
homePath
=
path
.
join
(
projectPath
,
"src/data/home.json"
);
const
home
=
JSON
.
parse
(
await
readFile
(
homePath
,
"utf8"
))
as
{
services
:
Array
<
{
number
:
string
;
title
:
string
;
description
:
string
}
>
};
const
home
=
JSON
.
parse
(
await
readFile
(
homePath
,
"utf8"
))
as
Record
<
string
,
unknown
>
&
{
services
:
Array
<
{
number
:
string
;
title
:
string
;
description
:
string
}
>
};
Object
.
assign
(
home
,
selection
.
template
.
content
);
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"
);
const
theme
=
(
await
readFile
(
themePath
,
"utf8"
)).
replaceAll
(
"#7028ff"
,
input
.
brandColor
.
toLowerCase
());
await
writeFile
(
themePath
,
theme
,
"utf8"
);
await
writeFile
(
themePath
,
this
.
templates
.
renderTheme
(
selection
.
theme
,
input
.
brandColor
),
"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
,
},
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
,
{
basePath
:
getPublicPreviewUrl
(
tenantId
,
siteId
),
indexable
:
false
,
articlesDirectory
,
articleImagesDirectory
,
});
...
...
backend/src/sites/site-template-catalog.test.ts
0 → 100644
View file @
61c4a3b5
import
assert
from
"node:assert/strict"
;
import
test
from
"node:test"
;
import
{
SiteTemplateCatalogService
}
from
"./site-template-catalog.js"
;
test
(
"catalog exposes the stable kernel and published page templates"
,
async
()
=>
{
const
service
=
new
SiteTemplateCatalogService
();
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"
,
]);
assert
.
equal
(
catalog
.
themes
.
length
,
5
);
});
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"
,
});
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"
,
}),
/不支持所选配色/
);
});
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"
);
assert
.
match
(
css
,
/--color-primary: #123456;/
);
assert
.
match
(
css
,
/--color-text-primary:/
);
assert
.
match
(
css
,
/--color-border:/
);
assert
.
match
(
css
,
/--gradient-primary:/
);
assert
.
doesNotMatch
(
css
,
/function shade/
);
});
backend/src/sites/site-template-catalog.ts
0 → 100644
View file @
61c4a3b5
import
path
from
"node:path"
;
import
{
readFile
,
readdir
}
from
"node:fs/promises"
;
import
type
{
PageTemplateInfo
,
SiteLayoutParameters
,
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
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
()]);
return
{
kernel
,
themes
:
themes
.
map
(({
id
,
name
,
colors
})
=>
({
id
,
name
,
colors
})),
pageTemplates
:
pageTemplates
.
map
(({
status
:
_status
,
compatibleKernel
:
_compatible
,
content
:
_content
,
...
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
()]);
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
(
!
theme
)
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
});
}
return
{
kernel
,
template
,
theme
};
}
renderTheme
(
theme
:
ThemeDefinition
,
brandColor
:
string
):
string
{
const
primary
=
brandColor
.
toLowerCase
();
const
hover
=
shade
(
primary
,
-
16
);
const
light
=
shade
(
primary
,
24
);
return
`:root {
--color-primary:
${
primary
}
;
--color-primary-hover:
${
hover
}
;
--color-primary-light:
${
light
}
;
--color-secondary:
${
theme
.
secondary
}
;
--color-primary-dark:
${
theme
.
dark
}
;
--color-background:
${
theme
.
background
}
;
--color-background-soft:
${
theme
.
soft
}
;
--color-surface: #ffffff;
--color-text-primary:
${
theme
.
text
}
;
--color-text-secondary:
${
theme
.
textSecondary
}
;
--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%);
}
`
;
}
private
async
kernel
():
Promise
<
{
id
:
string
;
version
:
string
}
>
{
const
value
=
JSON
.
parse
(
await
readFile
(
path
.
join
(
config
.
siteKitDir
,
"kernel"
,
"manifest.json"
),
"utf8"
))
as
{
id
:
string
;
version
:
string
;
status
:
string
};
if
(
value
.
status
!==
"stable"
)
throw
new
Error
(
"当前没有可用的稳定官网内核"
);
return
{
id
:
value
.
id
,
version
:
value
.
version
};
}
private
async
themes
():
Promise
<
ThemeDefinition
[]
>
{
return
JSON
.
parse
(
await
readFile
(
path
.
join
(
config
.
siteKitDir
,
"themes.json"
),
"utf8"
))
as
ThemeDefinition
[];
}
private
async
pageTemplates
():
Promise
<
PageTemplateDefinition
[]
>
{
const
root
=
path
.
join
(
config
.
siteKitDir
,
"page-templates"
);
const
templateDirectories
=
await
readdir
(
root
,
{
withFileTypes
:
true
});
const
manifests
:
PageTemplateDefinition
[]
=
[];
for
(
const
templateDirectory
of
templateDirectories
.
filter
((
entry
)
=>
entry
.
isDirectory
()))
{
const
versionRoot
=
path
.
join
(
root
,
templateDirectory
.
name
);
const
versions
=
await
readdir
(
versionRoot
,
{
withFileTypes
:
true
});
for
(
const
version
of
versions
.
filter
((
entry
)
=>
entry
.
isDirectory
()))
{
const
manifest
=
JSON
.
parse
(
await
readFile
(
path
.
join
(
versionRoot
,
version
.
name
,
"manifest.json"
),
"utf8"
))
as
PageTemplateDefinition
;
if
(
manifest
.
status
===
"published"
)
manifests
.
push
(
manifest
);
}
}
return
manifests
.
sort
((
a
,
b
)
=>
a
.
name
.
localeCompare
(
b
.
name
,
"zh-CN"
));
}
}
function
shade
(
hex
:
string
,
amount
:
number
):
string
{
const
value
=
Number
.
parseInt
(
hex
.
slice
(
1
),
16
);
const
adjust
=
(
channel
:
number
)
=>
Math
.
max
(
0
,
Math
.
min
(
255
,
channel
+
Math
.
round
(
255
*
amount
/
100
)));
const
channels
=
[
value
>>
16
,
(
value
>>
8
)
&
255
,
value
&
255
].
map
(
adjust
);
return
"#"
+
channels
.
map
((
channel
)
=>
channel
.
toString
(
16
).
padStart
(
2
,
"0"
)).
join
(
""
);
}
docs/site-template-architecture.md
0 → 100644
View file @
61c4a3b5
# 官网模板架构
官网创建由三个独立版本域组成:
1.
**内核版本**
:平台维护的构建、SEO/GEO、文章协议与发布能力。普通用户不选择,站点保存精确版本。
2.
**页面成品版本**
:用户在创建时选择的页面形状、默认内容和视觉方向。创建后成为站点自己的页面,不自动跟随模板升级。
3.
**站点 Git 版本**
:用户和 Agent 对生成结果的持续修改历史。
## 目录职责
-
`site-kit/kernel/manifest.json`
:当前稳定内核的身份和版本。
-
`site-kit/page-templates/<id>/<version>/manifest.json`
:已发布页面成品及其兼容范围、默认内容、支持配色和布局参数。
-
`site-kit/themes.json`
:完整语义配色方案。
-
`astro-template/`
:当前内核运行载体和页面渲染契约。
页面成品发布后不可原地修改;任何调整必须创建新版本。创建服务只接受目录中已发布且与当前内核兼容的精确版本组合。
## 创建不变量
-
先生成并构建不依赖模型的基础成品。
-
用户未填写 Prompt 时,基础成品直接成为第一版官网。
-
用户填写 Prompt 时,Agent 只在站点草稿中修改并接受路径与构建校验。
-
智能定制成功后保存为第二个 Git 版本;失败则丢弃草稿,保留已经可用的基础成品。
-
`metadata/site.json`
和站点根目录的
`webagent.site.json`
记录精确创建来源;页面模板后续升级不会覆盖已有网站。
frontend/src/App.tsx
View file @
61c4a3b5
...
...
@@ -6,7 +6,7 @@ import {
Plus
,
RefreshCw
,
Rocket
,
RotateCcw
,
Search
,
Send
,
Settings2
,
ShieldCheck
,
Smartphone
,
Sparkles
,
Trash2
,
FileText
,
ImagePlus
,
Save
,
Upload
,
WandSparkles
,
X
,
}
from
"lucide-react"
;
import
type
{
AgentRunEvent
,
ArticleInput
,
ArticleSummary
,
CreateSiteInput
,
CreateTenantInput
,
DomainBinding
,
GitHistoryItem
,
SessionInfo
,
SiteInfo
,
TenantAdminInfo
}
from
"@webagent/shared"
;
import
type
{
AgentRunEvent
,
ArticleInput
,
ArticleSummary
,
CreateSiteInput
,
CreateTenantInput
,
DomainBinding
,
GitHistoryItem
,
SessionInfo
,
SiteInfo
,
SiteTemplateCatalog
,
TenantAdminInfo
}
from
"@webagent/shared"
;
import
{
api
}
from
"./api"
;
type
ChatMessage
=
{
role
:
"user"
|
"agent"
;
text
:
string
;
meta
?:
string
};
...
...
@@ -202,20 +202,62 @@ function AdminWorkspace({ session, onLogout }: { session: SessionInfo; onLogout:
}
function
CreateSiteForm
({
onCreated
,
onCancel
}:
{
onCreated
:
(
site
:
SiteInfo
)
=>
void
;
onCancel
?:
()
=>
void
})
{
const
templatesQuery
=
useQuery
({
queryKey
:
[
"site-templates"
],
queryFn
:
api
.
siteTemplates
});
const
[
customizationWarning
,
setCustomizationWarning
]
=
useState
(
""
);
const
[
form
,
setForm
]
=
useState
<
CreateSiteInput
>
({
name
:
"云启科技"
,
industry
:
"企业数字化服务"
,
description
:
"专注于为成长型企业提供智能化、可持续的数字解决方案,让技术真正服务于业务增长。"
,
services
:
[
"数字化咨询"
,
"智能产品开发"
,
"品牌体验设计"
],
email
:
"hello@yunqi.example"
,
phone
:
"400-800-2026"
,
brandColor
:
"#7028FF"
,
pageTemplateId
:
"technology-corporate"
,
pageTemplateVersion
:
"1.0.0"
,
themeId
:
"violet-tech"
,
layout
:
{
hero
:
"split"
,
density
:
"comfortable"
,
radius
:
"soft"
},
initialPrompt
:
""
,
});
const
[
serviceText
,
setServiceText
]
=
useState
(
form
.
services
.
join
(
"、"
));
const
mutation
=
useMutation
({
mutationFn
:
api
.
createSite
,
onSuccess
:
onCreated
});
const
mutation
=
useMutation
({
mutationFn
:
async
(
input
:
CreateSiteInput
)
=>
{
setCustomizationWarning
(
""
);
const
site
=
await
api
.
createSite
(
input
);
if
(
!
input
.
initialPrompt
?.
trim
())
return
{
site
,
warning
:
""
};
try
{
await
api
.
runAgent
(
site
.
siteId
,
input
.
initialPrompt
);
await
api
.
saveDraft
(
site
.
siteId
,
"首次智能定制"
);
return
{
site
:
await
api
.
site
(
site
.
siteId
),
warning
:
""
};
}
catch
(
error
)
{
await
api
.
discardDraft
(
site
.
siteId
).
catch
(()
=>
undefined
);
return
{
site
:
await
api
.
site
(
site
.
siteId
).
catch
(()
=>
site
),
warning
:
error
instanceof
Error
?
error
.
message
:
String
(
error
)
};
}
},
onSuccess
:
({
site
,
warning
})
=>
{
setCustomizationWarning
(
warning
);
if
(
warning
)
window
.
setTimeout
(()
=>
onCreated
(
site
),
1800
);
else
onCreated
(
site
);
},
});
const
submit
=
(
event
:
FormEvent
)
=>
{
event
.
preventDefault
();
mutation
.
mutate
({
...
form
,
services
:
serviceText
.
split
(
/
[
、,,
\n]
/
).
map
((
item
)
=>
item
.
trim
()).
filter
(
Boolean
)
});
};
const
catalog
=
templatesQuery
.
data
;
const
selectedTemplate
=
catalog
?.
pageTemplates
.
find
((
item
)
=>
item
.
id
===
form
.
pageTemplateId
&&
item
.
version
===
form
.
pageTemplateVersion
);
const
availableThemes
=
catalog
?.
themes
.
filter
((
theme
)
=>
selectedTemplate
?.
themeIds
.
includes
(
theme
.
id
))
||
[];
const
chooseTemplate
=
(
template
:
SiteTemplateCatalog
[
"pageTemplates"
][
number
])
=>
{
const
theme
=
catalog
?.
themes
.
find
((
item
)
=>
item
.
id
===
template
.
defaultThemeId
);
setForm
({
...
form
,
pageTemplateId
:
template
.
id
,
pageTemplateVersion
:
template
.
version
,
themeId
:
template
.
defaultThemeId
,
brandColor
:
theme
?.
colors
[
0
]
||
form
.
brandColor
,
layout
:
template
.
defaults
});
};
if
(
templatesQuery
.
isLoading
)
return
<
section
className=
"workspace-create-content"
><
div
className=
"template-loading"
><
LoaderCircle
className=
"spin"
size=
{
20
}
/>
正在读取页面成品…
</
div
></
section
>;
if
(
templatesQuery
.
error
)
return
<
section
className=
"workspace-create-content"
><
div
className=
"form-error template-catalog-error"
><
X
size=
{
15
}
/>
{
templatesQuery
.
error
.
message
}
</
div
></
section
>;
return
<
section
className=
"workspace-create-content"
>
<
form
className=
"create-card workspace-create-card"
onSubmit=
{
submit
}
>
<
div
className=
"card-heading"
><
div
><
span
>
创建新网站
</
span
><
h2
>
告诉我你的企业信息
</
h2
><
p
>
填写基础信息,Agent 将生成首个可编辑版本。
</
p
></
div
>
{
onCancel
&&
<
button
className=
"workspace-create-cancel"
type=
"button"
onClick=
{
onCancel
}
><
X
size=
{
15
}
/>
取消
</
button
>
}
</
div
>
<
div
className=
"card-heading"
><
div
><
span
>
创建新网站
</
span
><
h2
>
选择成品,生成第一版官网
</
h2
><
p
>
成品本身即可使用,也可以调整参数或补充智能定制要求。
</
p
></
div
>
{
onCancel
&&
<
button
className=
"workspace-create-cancel"
type=
"button"
onClick=
{
onCancel
}
><
X
size=
{
15
}
/>
取消
</
button
>
}
</
div
>
<
CreateSection
number=
"01"
title=
"选择页面成品"
>
<
div
className=
"template-grid"
>
{
catalog
?.
pageTemplates
.
map
((
template
)
=>
<
button
className=
{
`template-card ${selectedTemplate?.id === template.id ? "active" : ""}`
}
type=
"button"
key=
{
`${template.id}@${template.version}`
}
onClick=
{
()
=>
chooseTemplate
(
template
)
}
>
<
TemplatePreview
template=
{
template
}
/>
<
span
><
strong
>
{
template
.
name
}
</
strong
><
small
>
{
template
.
description
}
</
small
><
em
>
{
template
.
industries
.
join
(
" · "
)
}
</
em
></
span
>
{
selectedTemplate
?.
id
===
template
.
id
&&
<
i
><
Check
size=
{
12
}
/></
i
>
}
</
button
>)
}
</
div
>
</
CreateSection
>
<
CreateSection
number=
"02"
title=
"企业资料"
>
<
div
className=
"form-grid"
>
<
Field
label=
"企业名称"
><
input
required
value=
{
form
.
name
}
onChange=
{
(
e
)
=>
setForm
({
...
form
,
name
:
e
.
target
.
value
})
}
/></
Field
>
<
Field
label=
"所属行业"
><
input
required
value=
{
form
.
industry
}
onChange=
{
(
e
)
=>
setForm
({
...
form
,
industry
:
e
.
target
.
value
})
}
/></
Field
>
...
...
@@ -223,17 +265,45 @@ function CreateSiteForm({ onCreated, onCancel }: { onCreated: (site: SiteInfo) =
<
Field
label=
"核心服务"
hint=
"使用逗号或顿号分隔"
wide
><
input
required
value=
{
serviceText
}
onChange=
{
(
e
)
=>
setServiceText
(
e
.
target
.
value
)
}
/></
Field
>
<
Field
label=
"联系邮箱"
><
input
required
type=
"email"
value=
{
form
.
email
}
onChange=
{
(
e
)
=>
setForm
({
...
form
,
email
:
e
.
target
.
value
})
}
/></
Field
>
<
Field
label=
"联系电话"
><
input
value=
{
form
.
phone
}
onChange=
{
(
e
)
=>
setForm
({
...
form
,
phone
:
e
.
target
.
value
})
}
/></
Field
>
<
Field
label=
"品牌主色"
wide
>
<
div
className=
"color-field"
><
input
type=
"color"
value=
{
form
.
brandColor
}
onChange=
{
(
e
)
=>
setForm
({
...
form
,
brandColor
:
e
.
target
.
value
.
toUpperCase
()
})
}
/><
span
>
{
form
.
brandColor
}
</
span
><
div
className=
"color-swatches"
>
{
[
"#7028FF"
,
"#536CFF"
,
"#0F766E"
,
"#1D4ED8"
,
"#C2410C"
].
map
((
color
)
=>
<
button
type=
"button"
aria
-
label=
{
color
}
key=
{
color
}
style=
{
{
background
:
color
}
}
onClick=
{
()
=>
setForm
({
...
form
,
brandColor
:
color
})
}
className=
{
form
.
brandColor
===
color
?
"active"
:
""
}
/>)
}
</
div
></
div
>
</
Field
>
</
div
>
</
CreateSection
>
<
CreateSection
number=
"03"
title=
"样式与布局"
>
<
div
className=
"preset-label"
>
配色方案
</
div
>
<
div
className=
"theme-options"
>
{
availableThemes
.
map
((
theme
)
=>
<
button
type=
"button"
className=
{
form
.
themeId
===
theme
.
id
?
"active"
:
""
}
key=
{
theme
.
id
}
onClick=
{
()
=>
setForm
({
...
form
,
themeId
:
theme
.
id
,
brandColor
:
theme
.
colors
[
0
]
})
}
><
span
>
{
theme
.
colors
.
map
((
color
)
=>
<
i
key=
{
color
}
style=
{
{
background
:
color
}
}
/>)
}
</
span
><
strong
>
{
theme
.
name
}
</
strong
>
{
form
.
themeId
===
theme
.
id
&&
<
Check
size=
{
12
}
/>
}
</
button
>)
}
</
div
>
<
div
className=
"brand-color-control"
><
span
>
品牌主色
</
span
><
label
><
input
type=
"color"
value=
{
form
.
brandColor
}
onChange=
{
(
event
)
=>
setForm
({
...
form
,
brandColor
:
event
.
target
.
value
.
toUpperCase
()
})
}
/><
code
>
{
form
.
brandColor
}
</
code
><
small
>
可在配色方案基础上覆盖主色
</
small
></
label
></
div
>
<
div
className=
"create-parameter-grid"
>
<
PresetButtons
label=
"首屏结构"
value=
{
form
.
layout
.
hero
}
options=
{
selectedTemplate
?.
layouts
.
hero
||
[]
}
labels=
{
{
split
:
"左右结构"
,
centered
:
"居中结构"
}
}
onChange=
{
(
hero
)
=>
setForm
({
...
form
,
layout
:
{
...
form
.
layout
,
hero
}
})
}
/>
<
PresetButtons
label=
"页面密度"
value=
{
form
.
layout
.
density
}
options=
{
selectedTemplate
?.
layouts
.
density
||
[]
}
labels=
{
{
comfortable
:
"舒展"
,
compact
:
"紧凑"
}
}
onChange=
{
(
density
)
=>
setForm
({
...
form
,
layout
:
{
...
form
.
layout
,
density
}
})
}
/>
<
PresetButtons
label=
"边角风格"
value=
{
form
.
layout
.
radius
}
options=
{
selectedTemplate
?.
layouts
.
radius
||
[]
}
labels=
{
{
soft
:
"柔和圆角"
,
square
:
"利落直角"
}
}
onChange=
{
(
radius
)
=>
setForm
({
...
form
,
layout
:
{
...
form
.
layout
,
radius
}
})
}
/>
</
div
>
</
CreateSection
>
<
CreateSection
number=
"04"
title=
"智能定制(可选)"
>
<
Field
label=
"还有哪些具体要求?"
hint=
"不填写即可直接使用成品"
wide
><
textarea
rows=
{
4
}
placeholder=
"例如:整体更稳重,突出企业级服务,首屏增加预约咨询的表达……"
value=
{
form
.
initialPrompt
||
""
}
onChange=
{
(
event
)
=>
setForm
({
...
form
,
initialPrompt
:
event
.
target
.
value
})
}
/></
Field
>
</
CreateSection
>
{
mutation
.
error
&&
<
div
className=
"form-error"
><
X
size=
{
15
}
/>
{
mutation
.
error
.
message
}
</
div
>
}
<
button
className=
"create-submit"
disabled=
{
mutation
.
isPending
}
>
{
mutation
.
isPending
?
<><
LoaderCircle
className=
"spin"
size=
{
18
}
/>
正在创建、构建并启动预览…
</>
:
<>
生成我的企业官网
<
ArrowRight
size=
{
18
}
/></>
}
</
button
>
{
customizationWarning
&&
<
div
className=
"form-warning"
><
CheckCircle2
size=
{
15
}
/><
span
>
官网成品已创建,智能定制未应用:
{
customizationWarning
}
</
span
></
div
>
}
<
button
className=
"create-submit"
disabled=
{
mutation
.
isPending
||
!
selectedTemplate
}
>
{
mutation
.
isPending
?
<><
LoaderCircle
className=
"spin"
size=
{
18
}
/>
{
form
.
initialPrompt
?
"正在生成成品并进行智能定制…"
:
"正在生成并构建官网…"
}
</>
:
<>
生成第一版官网
<
ArrowRight
size=
{
18
}
/></>
}
</
button
>
<
p
className=
"local-note"
><
ShieldCheck
size=
{
13
}
/>
创建完成后将直接进入工作台编辑
</
p
>
</
form
>
</
section
>;
}
function
CreateSection
({
number
,
title
,
children
}:
{
number
:
string
;
title
:
string
;
children
:
React
.
ReactNode
})
{
return
<
section
className=
"create-section"
><
header
><
span
>
{
number
}
</
span
><
h3
>
{
title
}
</
h3
></
header
>
{
children
}
</
section
>;
}
function
TemplatePreview
({
template
}:
{
template
:
SiteTemplateCatalog
[
"pageTemplates"
][
number
]
})
{
return
<
span
className=
{
`template-preview ${template.preview.mode}`
}
style=
{
{
background
:
template
.
preview
.
surface
}
}
>
<
i
className=
"preview-nav"
><
b
style=
{
{
background
:
template
.
preview
.
accent
}
}
/><
small
/><
small
/><
small
/></
i
>
<
i
className=
"preview-body"
><
span
><
small
style=
{
{
background
:
template
.
preview
.
accent
}
}
/><
strong
/><
strong
/><
em
/><
b
style=
{
{
background
:
template
.
preview
.
accent
}
}
/></
span
><
span
className=
"preview-art"
style=
{
{
background
:
`linear-gradient(145deg, ${template.preview.accent}, #24213b)`
}
}
/></
i
>
<
i
className=
"preview-cards"
><
small
/><
small
/><
small
/></
i
>
</
span
>;
}
function
PresetButtons
<
T
extends
string
>
({
label
,
value
,
options
,
labels
,
onChange
}:
{
label
:
string
;
value
:
T
;
options
:
T
[];
labels
:
Record
<
T
,
string
>
;
onChange
:
(
value
:
T
)
=>
void
})
{
return
<
div
className=
"preset-buttons"
><
span
>
{
label
}
</
span
><
div
>
{
options
.
map
((
option
)
=>
<
button
type=
"button"
className=
{
option
===
value
?
"active"
:
""
}
key=
{
option
}
onClick=
{
()
=>
onChange
(
option
)
}
>
{
labels
[
option
]
}
</
button
>)
}
</
div
></
div
>;
}
function
Field
({
label
,
hint
,
wide
,
children
}:
{
label
:
string
;
hint
?:
string
;
wide
?:
boolean
;
children
:
React
.
ReactNode
})
{
return
<
label
className=
{
wide
?
"field wide"
:
"field"
}
><
span
>
{
label
}{
hint
&&
<
small
>
{
hint
}
</
small
>
}
</
span
>
{
children
}
</
label
>;
}
...
...
frontend/src/api.ts
View file @
61c4a3b5
import
type
{
AgentRunEvent
,
AgentRunInfo
,
AgentSettings
,
ArticleCompileResult
,
ArticleDocument
,
ArticleImageUploadResult
,
ArticleInput
,
ArticleListResult
,
ArticleMutationResult
,
CreateSiteInput
,
CreateTenantInput
,
DomainBinding
,
DomainListResult
,
DraftPreviewResult
,
GitHistoryItem
,
PreviewVersionResult
,
PublishResult
,
SessionInfo
,
SiteInfo
,
TenantAdminInfo
,
TenantAdminSiteInfo
}
from
"@webagent/shared"
;
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"
;
async
function
request
<
T
>
(
url
:
string
,
options
?:
RequestInit
):
Promise
<
T
>
{
const
headers
=
new
Headers
(
options
?.
headers
);
...
...
@@ -26,6 +26,7 @@ export const api = {
agentSettings
:
()
=>
request
<
AgentSettings
>
(
"/api/agent-settings"
),
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"
),
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
)
}),
...
...
frontend/src/create-template.css
0 → 100644
View file @
61c4a3b5
.workspace-create-card
{
width
:
min
(
940px
,
100%
);
}
.template-loading
{
margin
:
auto
;
display
:
flex
;
align-items
:
center
;
gap
:
9px
;
color
:
var
(
--muted
);
font-size
:
12px
;
}
.template-catalog-error
{
margin
:
auto
;
}
.create-section
{
padding
:
25px
0
;
border-top
:
1px
solid
var
(
--divider
);
}
.create-section
>
header
{
display
:
flex
;
align-items
:
center
;
gap
:
9px
;
margin-bottom
:
17px
;
}
.create-section
>
header
span
{
display
:
grid
;
place-items
:
center
;
width
:
24px
;
height
:
24px
;
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border-radius
:
7px
;
font-size
:
9px
;
font-weight
:
800
;
}
.create-section
>
header
h3
{
margin
:
0
;
font-size
:
14px
;
}
.template-grid
{
display
:
grid
;
grid-template-columns
:
repeat
(
3
,
1
fr
);
gap
:
12px
;
}
.template-card
{
position
:
relative
;
min-width
:
0
;
padding
:
8px
;
text-align
:
left
;
background
:
#fff
;
border
:
1px
solid
var
(
--border
);
border-radius
:
13px
;
transition
:
.2s
;
}
.template-card
:hover
{
border-color
:
#bba3f3
;
transform
:
translateY
(
-2px
);
}
.template-card.active
{
border-color
:
var
(
--primary
);
box-shadow
:
0
0
0
2px
rgba
(
112
,
40
,
255
,
.08
);
}
.template-card
>
span
:nth-child
(
2
)
{
display
:
flex
;
flex-direction
:
column
;
padding
:
11px
6px
7px
;
}
.template-card
strong
{
font-size
:
11px
;
}
.template-card
small
{
margin-top
:
5px
;
color
:
var
(
--muted
);
font-size
:
8px
;
line-height
:
1.55
;
}
.template-card
em
{
margin-top
:
8px
;
color
:
var
(
--primary
);
font-size
:
7px
;
font-style
:
normal
;
}
.template-card
>
i
{
position
:
absolute
;
right
:
12px
;
top
:
12px
;
display
:
grid
;
place-items
:
center
;
width
:
20px
;
height
:
20px
;
color
:
#fff
;
background
:
var
(
--primary
);
border
:
2px
solid
#fff
;
border-radius
:
50%
;
}
.template-preview
{
height
:
132px
;
display
:
flex
!important
;
flex-direction
:
column
!important
;
overflow
:
hidden
;
padding
:
0
!important
;
border-radius
:
8px
;
}
.preview-nav
{
height
:
24px
;
padding
:
0
10px
;
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
background
:
rgba
(
255
,
255
,
255
,
.86
);
}
.preview-nav
b
{
width
:
11px
;
height
:
11px
;
border-radius
:
3px
;
}
.preview-nav
small
{
width
:
18px
;
height
:
2px
;
margin
:
0
;
background
:
#c6c2cf
;
border-radius
:
2px
;
}
.preview-nav
small
:first-of-type
{
margin-left
:
auto
;
}
.preview-body
{
flex
:
1
;
display
:
grid
;
grid-template-columns
:
1.15
fr
.85
fr
;
align-items
:
center
;
gap
:
11px
;
padding
:
13px
;
}
.preview-body
>
span
:first-child
{
display
:
flex
;
flex-direction
:
column
;
gap
:
5px
;
}
.preview-body
>
span
small
{
width
:
26px
;
height
:
3px
;
margin
:
0
;
}
.preview-body
strong
{
width
:
84%
;
height
:
6px
;
background
:
#2d2b35
;
border-radius
:
2px
;
}
.preview-body
strong
+
strong
{
width
:
65%
;
}
.preview-body
em
{
width
:
92%
;
height
:
3px
;
background
:
#a6a1ae
;
}
.preview-body
b
{
width
:
34px
;
height
:
10px
;
border-radius
:
3px
;
}
.preview-art
{
height
:
60px
;
border-radius
:
8px
;
}
.preview-cards
{
height
:
26px
;
padding
:
0
13px
9px
;
display
:
grid
;
grid-template-columns
:
repeat
(
3
,
1
fr
);
gap
:
6px
;
}
.preview-cards
small
{
height
:
17px
;
margin
:
0
;
background
:
rgba
(
255
,
255
,
255
,
.92
);
border
:
1px
solid
rgba
(
50
,
45
,
65
,
.08
);
border-radius
:
4px
;
}
.preset-label
{
margin-bottom
:
9px
;
color
:
var
(
--text-2
);
font-size
:
10px
;
font-weight
:
700
;
}
.theme-options
{
display
:
grid
;
grid-template-columns
:
repeat
(
5
,
1
fr
);
gap
:
8px
;
}
.theme-options
button
{
min-width
:
0
;
height
:
52px
;
padding
:
7px
9px
;
display
:
grid
;
grid-template-columns
:
1
fr
auto
;
grid-template-rows
:
auto
auto
;
align-items
:
center
;
text-align
:
left
;
background
:
#fff
;
border
:
1px
solid
var
(
--border
);
border-radius
:
9px
;
}
.theme-options
button
.active
{
border-color
:
var
(
--primary
);
box-shadow
:
0
0
0
2px
rgba
(
112
,
40
,
255
,
.07
);
}
.theme-options
button
>
span
{
display
:
flex
;
grid-column
:
1
/
-1
;
}
.theme-options
button
i
{
width
:
17px
;
height
:
8px
;
}
.theme-options
button
i
:first-child
{
border-radius
:
4px
0
0
4px
;
}
.theme-options
button
i
:last-child
{
border-radius
:
0
4px
4px
0
;
}
.theme-options
button
strong
{
font-size
:
8px
;
}
.theme-options
button
svg
{
color
:
var
(
--primary
);
}
.brand-color-control
{
margin-top
:
13px
;
display
:
grid
;
grid-template-columns
:
78px
1
fr
;
align-items
:
center
;
color
:
var
(
--text-2
);
font-size
:
9px
;
font-weight
:
700
;
}
.brand-color-control
label
{
height
:
38px
;
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
padding
:
4px
9px
;
background
:
#fbfbfd
;
border
:
1px
solid
var
(
--border
);
border-radius
:
8px
;
}
.brand-color-control
input
{
width
:
28px
;
height
:
28px
;
padding
:
1px
;
border
:
0
;
background
:
transparent
;
}
.brand-color-control
code
{
color
:
var
(
--text-2
);
font-size
:
9px
;
}
.brand-color-control
small
{
margin-left
:
auto
;
color
:
var
(
--muted
);
font-size
:
8px
;
font-weight
:
500
;
}
.create-parameter-grid
{
margin-top
:
16px
;
display
:
grid
;
grid-template-columns
:
repeat
(
3
,
1
fr
);
gap
:
12px
;
}
.preset-buttons
>
span
{
display
:
block
;
margin-bottom
:
7px
;
color
:
var
(
--text-2
);
font-size
:
9px
;
font-weight
:
700
;
}
.preset-buttons
>
div
{
display
:
flex
;
padding
:
3px
;
background
:
#f5f4f8
;
border-radius
:
8px
;
}
.preset-buttons
button
{
flex
:
1
;
height
:
29px
;
color
:
var
(
--muted
);
background
:
transparent
;
border
:
0
;
border-radius
:
6px
;
font-size
:
8px
;
font-weight
:
700
;
}
.preset-buttons
button
.active
{
color
:
var
(
--primary
);
background
:
#fff
;
box-shadow
:
0
2px
7px
rgba
(
32
,
24
,
52
,
.08
);
}
.form-warning
{
display
:
flex
;
align-items
:
flex-start
;
gap
:
8px
;
margin-top
:
16px
;
padding
:
11px
13px
;
color
:
#8a5a12
;
background
:
#fff8e7
;
border
:
1px
solid
#f4dfad
;
border-radius
:
9px
;
font-size
:
10px
;
line-height
:
1.6
;
}
.form-warning
svg
{
flex
:
none
;
margin-top
:
1px
;
}
@media
(
max-width
:
800px
)
{
.template-grid
,
.create-parameter-grid
{
grid-template-columns
:
1
fr
;
}
.theme-options
{
grid-template-columns
:
repeat
(
2
,
1
fr
);
}
}
frontend/src/main.tsx
View file @
61c4a3b5
...
...
@@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client";
import
{
QueryClient
,
QueryClientProvider
}
from
"@tanstack/react-query"
;
import
App
from
"./App"
;
import
"./styles.css"
;
import
"./create-template.css"
;
const
queryClient
=
new
QueryClient
({
defaultOptions
:
{
queries
:
{
retry
:
1
,
refetchOnWindowFocus
:
false
}
}
});
...
...
shared/src/index.ts
View file @
61c4a3b5
...
...
@@ -10,6 +10,57 @@ export interface CreateSiteInput {
email
:
string
;
phone
?:
string
;
brandColor
:
string
;
pageTemplateId
:
string
;
pageTemplateVersion
:
string
;
themeId
:
string
;
layout
:
SiteLayoutParameters
;
initialPrompt
?:
string
;
}
export
interface
SiteLayoutParameters
{
hero
:
"split"
|
"centered"
;
density
:
"comfortable"
|
"compact"
;
radius
:
"soft"
|
"square"
;
}
export
interface
ThemePresetInfo
{
id
:
string
;
name
:
string
;
colors
:
string
[];
}
export
interface
PageTemplateInfo
{
id
:
string
;
version
:
string
;
name
:
string
;
description
:
string
;
industries
:
string
[];
preview
:
{
accent
:
string
;
surface
:
string
;
mode
:
"light"
|
"dark"
};
defaultThemeId
:
string
;
themeIds
:
string
[];
layouts
:
{
hero
:
SiteLayoutParameters
[
"hero"
][];
density
:
SiteLayoutParameters
[
"density"
][];
radius
:
SiteLayoutParameters
[
"radius"
][];
};
defaults
:
SiteLayoutParameters
;
}
export
interface
SiteTemplateCatalog
{
kernel
:
{
id
:
string
;
version
:
string
};
pageTemplates
:
PageTemplateInfo
[];
themes
:
ThemePresetInfo
[];
}
export
interface
SiteTemplateProvenance
{
kernel
:
{
id
:
string
;
version
:
string
};
createdFrom
:
{
pageTemplateId
:
string
;
pageTemplateVersion
:
string
;
themeId
:
string
;
layout
:
SiteLayoutParameters
;
};
initialPrompt
?:
string
;
}
export
interface
SiteInfo
{
...
...
@@ -19,6 +70,7 @@ export interface SiteInfo {
industry
:
string
;
status
:
SiteStatus
;
templateVersion
:
string
;
template
?:
SiteTemplateProvenance
;
environmentVersion
?:
number
;
previewPort
:
number
;
previewUrl
:
string
;
...
...
site-kit/kernel/manifest.json
0 → 100644
View file @
61c4a3b5
{
"id"
:
"official-site"
,
"version"
:
"1.0.0"
,
"status"
:
"stable"
,
"source"
:
"../../astro-template"
}
site-kit/page-templates/brand-story/1.0.0/manifest.json
0 → 100644
View file @
61c4a3b5
{
"id"
:
"brand-story"
,
"version"
:
"1.0.0"
,
"name"
:
"品牌故事企业"
,
"description"
:
"更有温度的叙事与舒展版式,适合消费品牌、文化创意和设计机构。"
,
"industries"
:
[
"消费品牌"
,
"文化创意"
,
"设计机构"
],
"status"
:
"published"
,
"compatibleKernel"
:
"1.x"
,
"preview"
:
{
"accent"
:
"#C2410C"
,
"surface"
:
"#FFF7ED"
,
"mode"
:
"light"
},
"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"
},
"content"
:
{
"eyebrow"
:
"MADE WITH PURPOSE"
,
"heroTitle"
:
"把日常的灵感,
\n
做成值得记住的品牌"
,
"heroDescription"
:
"我们相信好品牌来自真实的价值、细腻的表达,以及与每一位用户长久相处的诚意。"
,
"primaryAction"
:
"认识我们的故事"
,
"secondaryAction"
:
"探索品牌作品"
,
"metrics"
:
[{
"value"
:
"36"
,
"label"
:
"城市足迹"
},{
"value"
:
"80万+"
,
"label"
:
"用户选择"
},{
"value"
:
"12项"
,
"label"
:
"设计荣誉"
}],
"advantages"
:
[{
"title"
:
"真实表达"
,
"description"
:
"从品牌本身出发,建立诚恳而鲜明的表达。"
},{
"title"
:
"细节体验"
,
"description"
:
"在每个接触点创造自然、统一的感受。"
},{
"title"
:
"长久陪伴"
,
"description"
:
"让品牌随着用户和时代持续成长。"
}]
}
}
site-kit/page-templates/professional-services/1.0.0/manifest.json
0 → 100644
View file @
61c4a3b5
{
"id"
:
"professional-services"
,
"version"
:
"1.0.0"
,
"name"
:
"专业服务企业"
,
"description"
:
"稳重克制、强调方法与信任,适合咨询、财税、法律和企业服务。"
,
"industries"
:
[
"管理咨询"
,
"法律财税"
,
"专业服务"
],
"status"
:
"published"
,
"compatibleKernel"
:
"1.x"
,
"preview"
:
{
"accent"
:
"#175CD3"
,
"surface"
:
"#EFF8FF"
,
"mode"
:
"light"
},
"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"
},
"content"
:
{
"eyebrow"
:
"TRUSTED PROFESSIONALS"
,
"heroTitle"
:
"以专业判断,
\n
回应复杂商业问题"
,
"heroDescription"
:
"从关键判断到可靠执行,我们与企业并肩工作,让每一项重要决策都有清晰依据。"
,
"primaryAction"
:
"预约顾问"
,
"secondaryAction"
:
"查看服务方法"
,
"metrics"
:
[{
"value"
:
"15年"
,
"label"
:
"行业经验"
},{
"value"
:
"300+"
,
"label"
:
"客户项目"
},{
"value"
:
"92%"
,
"label"
:
"长期合作客户"
}],
"advantages"
:
[{
"title"
:
"资深团队"
,
"description"
:
"由具备一线经验的专业顾问直接参与。"
},{
"title"
:
"事实驱动"
,
"description"
:
"基于充分研究和可靠数据形成判断。"
},{
"title"
:
"结果负责"
,
"description"
:
"从建议延伸到执行,确保方案真正落地。"
}]
}
}
site-kit/page-templates/technology-corporate/1.0.0/manifest.json
0 → 100644
View file @
61c4a3b5
{
"id"
:
"technology-corporate"
,
"version"
:
"1.0.0"
,
"name"
:
"现代科技企业"
,
"description"
:
"清晰有力的左右首屏与数据化视觉,适合 AI、软件和数字化服务企业。"
,
"industries"
:
[
"人工智能"
,
"企业软件"
,
"数字化服务"
],
"status"
:
"published"
,
"compatibleKernel"
:
"1.x"
,
"preview"
:
{
"accent"
:
"#7028FF"
,
"surface"
:
"#F3F0FF"
,
"mode"
:
"light"
},
"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"
},
"content"
:
{
"eyebrow"
:
"AI-POWERED BUSINESS"
,
"heroTitle"
:
"让每一次数字化升级,
\n
都成为增长的起点"
,
"heroDescription"
:
"从策略洞察到产品落地,我们用清晰的方法与先进技术,为企业构建可持续的数字竞争力。"
,
"primaryAction"
:
"开始合作"
,
"secondaryAction"
:
"了解我们的能力"
,
"metrics"
:
[{
"value"
:
"120+"
,
"label"
:
"服务企业"
},{
"value"
:
"98%"
,
"label"
:
"项目交付率"
},{
"value"
:
"7×24"
,
"label"
:
"专业支持"
}],
"advantages"
:
[{
"title"
:
"业务优先"
,
"description"
:
"每一项方案都回应真实业务目标。"
},{
"title"
:
"敏捷共创"
,
"description"
:
"用短周期验证关键假设,让过程透明、结果可控。"
},{
"title"
:
"长期主义"
,
"description"
:
"交付可维护、可演进的系统,而不是一次性成果。"
}]
}
}
site-kit/themes.json
0 → 100644
View file @
61c4a3b5
[
{
"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"
}
]
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment