From 987583201996b2e6d4f0fed214a1688d0152f968 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=89=AB=E5=9C=B0=E5=83=A7?= <357099073@qq.com>
Date: Tue, 15 Sep 2026 00:08:36 +0800
Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0website=E5=90=8E=E7=AB=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 4 +
go/controllers/platform_website_setting.go | 107 +++++++
go/controllers/tenant_site.go | 15 +
go/docs/sql/create_platform_site_setting.sql | 28 ++
go/docs/sql/init_platform_site_setting.sql | 67 ++++
go/models/init.go | 2 +
go/models/platform_site_setting.go | 289 ++++++++++++++++++
go/pkg/tagengine/engine.go | 13 +
go/pkg/tagengine/meta.go | 20 +-
go/pkg/tagengine/provider.go | 16 +
go/routers/index/index.go | 3 +
go/routers/platform/platform.go | 4 +
platform/src/api/websiteSetting.js | 22 ++
platform/src/router/index.js | 6 +
.../website/setting/components/TagTip.vue | 56 ++++
.../website/setting/components/tab_basic.vue | 154 ++++++++++
.../setting/components/tab_company.vue | 66 ++++
.../website/setting/components/tab_policy.vue | 52 ++++
.../website/setting/components/tab_record.vue | 51 ++++
platform/src/views/website/setting/index.vue | 184 +++++++++++
website/src/api/site.ts | 60 ++++
website/src/config/site.ts | 92 +++++-
website/src/directives/yz.ts | 48 +++
website/src/layout/SiteFooter.vue | 48 ++-
website/src/layout/SiteHeader.vue | 23 +-
website/src/main.ts | 8 +
website/src/router/index.ts | 6 +-
website/src/views/detail.vue | 8 +-
.../src/views/home/components/HeroBanner.vue | 6 +-
website/src/views/single.vue | 8 +-
30 files changed, 1428 insertions(+), 38 deletions(-)
create mode 100644 go/controllers/platform_website_setting.go
create mode 100644 go/docs/sql/create_platform_site_setting.sql
create mode 100644 go/docs/sql/init_platform_site_setting.sql
create mode 100644 go/models/platform_site_setting.go
create mode 100644 platform/src/api/websiteSetting.js
create mode 100644 platform/src/views/website/setting/components/TagTip.vue
create mode 100644 platform/src/views/website/setting/components/tab_basic.vue
create mode 100644 platform/src/views/website/setting/components/tab_company.vue
create mode 100644 platform/src/views/website/setting/components/tab_policy.vue
create mode 100644 platform/src/views/website/setting/components/tab_record.vue
create mode 100644 platform/src/views/website/setting/index.vue
create mode 100644 website/src/api/site.ts
create mode 100644 website/src/directives/yz.ts
diff --git a/.gitignore b/.gitignore
index aa7825e..66e6666 100644
--- a/.gitignore
+++ b/.gitignore
@@ -52,6 +52,10 @@ backend/dist
backend/dist.zip
backend/output
+# website
+website/dist
+website/dist.zip
+
# go
go/go-platform.zip
go/server.exe
diff --git a/go/controllers/platform_website_setting.go b/go/controllers/platform_website_setting.go
new file mode 100644
index 0000000..e3ef7dd
--- /dev/null
+++ b/go/controllers/platform_website_setting.go
@@ -0,0 +1,107 @@
+package controllers
+
+import (
+ "encoding/json"
+ "io"
+ "strings"
+
+ "server/models"
+
+ beego "github.com/beego/beego/v2/server/web"
+)
+
+// PlatformWebsiteSettingController 平台官网站点设置(平台端专用)
+// 对应前端 views/website/setting/index.vue:
+// - GET /platform/website/setting/info
+// - POST /platform/website/setting/save
+//
+// 数据落在 yz_platform_site_setting(通用键值结构:title/value/tag/remark)。
+// 该表完全数据驱动:表里插一行就是一个新的设置项,模板即可用 {yz:tag} 输出,
+// platform 端界面也会自动出现该项,无需改动后端代码。
+type PlatformWebsiteSettingController struct {
+ beego.Controller
+}
+
+func (c *PlatformWebsiteSettingController) jsonErr(httpStatus, bizCode int, msg string) {
+ c.Ctx.Output.SetStatus(httpStatus)
+ c.Data["json"] = map[string]any{"code": bizCode, "msg": msg}
+ _ = c.ServeJSON()
+}
+
+// Info GET /platform/website/setting/info
+// 直接把表里所有设置项返回,前端按行渲染(新增设置项无需改代码):
+// data.list = [{ id, title, value, tag, remark }]
+// data.values = { "sitename": "云泽科技", ... }(key 即标签名,方便直接使用)
+func (c *PlatformWebsiteSettingController) Info() {
+ if _, err := requirePlatform(&c.Controller); err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+
+ rows := models.GetPlatformSiteSettingRows()
+ list := make([]map[string]any, 0, len(rows))
+ for _, r := range rows {
+ list = append(list, map[string]any{
+ "id": r.ID,
+ "title": r.Title,
+ "value": r.Value,
+ "tag": r.Tag,
+ "remark": r.Remark,
+ })
+ }
+
+ c.Data["json"] = map[string]any{
+ "code": 200,
+ "msg": "success",
+ "data": map[string]any{
+ "list": list,
+ "values": models.GetPlatformSiteTagMap(),
+ },
+ }
+ _ = c.ServeJSON()
+}
+
+// Save POST /platform/website/setting/save
+// 入参 { "sitename": "云泽科技", "subtitle": "...", ... },key 为标签名,按标签逐项 upsert。
+// 通用:表里已存在或新增的任意合法标签都可写入,不需要登记。
+func (c *PlatformWebsiteSettingController) Save() {
+ if _, err := requirePlatform(&c.Controller); err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+
+ raw, err := io.ReadAll(c.Ctx.Request.Body)
+ if err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+
+ var payload map[string]string
+ if uerr := json.Unmarshal(raw, &payload); uerr != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ if len(payload) == 0 {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+
+ if strings.TrimSpace(payload["sitename"]) == "" {
+ c.jsonErr(400, 400, "站点名称不能为空")
+ return
+ }
+
+ // 统一去掉首尾空格,避免模板里输出多余空白
+ values := make(map[string]string, len(payload))
+ for k, v := range payload {
+ values[k] = strings.TrimSpace(v)
+ }
+
+ if err = models.SavePlatformSiteSettings(values); err != nil {
+ c.jsonErr(500, 500, "保存失败: "+err.Error())
+ return
+ }
+
+ c.Data["json"] = map[string]any{"code": 200, "msg": "保存成功"}
+ _ = c.ServeJSON()
+}
diff --git a/go/controllers/tenant_site.go b/go/controllers/tenant_site.go
index 0100590..904cd06 100644
--- a/go/controllers/tenant_site.go
+++ b/go/controllers/tenant_site.go
@@ -291,6 +291,21 @@ func (c *TenantSiteController) NewsDetail() {
})
}
+// SiteSetting GET /site/setting 官网前台站点设置(公开接口,无需登录)
+// 按访问域名识别租户,返回 标签名 -> 值,数据来自 yz_platform_site_setting,与模板标签同源。
+// 完全数据驱动:表里有什么 tag,这里就返回什么标签,新增设置项无需改代码。
+// 供 SPA 官网(website/)读取,使 SPA 与 themes 直出模板的站点信息保持一致。
+func (c *TenantSiteController) SiteSetting() {
+ out := map[string]string{}
+ if tid, ok := c.resolveTid(); ok {
+ for k, v := range tagengine.SiteGlobals(tid) {
+ out[k] = v
+ }
+ }
+ c.Data["json"] = map[string]any{"code": 200, "msg": "success", "data": out}
+ _ = c.ServeJSON()
+}
+
// Page GET /page/:path 单页
func (c *TenantSiteController) Page() {
path := strings.TrimSpace(c.GetString(":path"))
diff --git a/go/docs/sql/create_platform_site_setting.sql b/go/docs/sql/create_platform_site_setting.sql
new file mode 100644
index 0000000..fa0e9fc
--- /dev/null
+++ b/go/docs/sql/create_platform_site_setting.sql
@@ -0,0 +1,28 @@
+-- 平台官网站点设置表 yz_platform_site_setting(通用键值结构)
+-- 一行一个设置项:title 名称 / value 值 / tag 标签名 / remark 说明。
+-- 模板里用 {yz:tag} 输出对应 value;新增设置项只需插一行,无需加字段。
+-- 服务端启动时会自动建表并补齐默认项(models.EnsurePlatformSiteSettingTable)。
+-- 已登记的默认设置项:
+-- sitename 站点名称
+-- subtitle 副标题
+-- logo 深色 Logo(浅色背景)
+-- logow 浅色 Logo(深色背景)
+-- icp ICP 备案号
+-- policeicp 公安备案号
+-- copyright 版权信息
+-- privacy 隐私政策链接
+-- terms 使用条款链接
+
+CREATE TABLE IF NOT EXISTS `yz_platform_site_setting` (
+ `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
+ `title` varchar(128) NOT NULL DEFAULT '' COMMENT '设置项名称',
+ `value` text COMMENT '设置项的值',
+ `tag` varchar(64) NOT NULL DEFAULT '' COMMENT '标签名,模板中写作 {yz:tag}',
+ `remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注/填写说明',
+ `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+ `delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_tag` (`tag`, `delete_time`),
+ KEY `idx_delete_time` (`delete_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台官网站点设置表';
diff --git a/go/docs/sql/init_platform_site_setting.sql b/go/docs/sql/init_platform_site_setting.sql
new file mode 100644
index 0000000..1a083bb
--- /dev/null
+++ b/go/docs/sql/init_platform_site_setting.sql
@@ -0,0 +1,67 @@
+-- 平台官网站点设置 - 初始化/调整数据(yz_platform_site_setting,通用键值结构)
+--
+-- 前提:表已按 create_platform_site_setting.sql 建成通用结构(title/value/tag/remark)。
+--
+-- remark 可选标记(用 | 分隔,未识别片段作为说明文字展示):
+-- group:分组名 决定 platform 端界面落在哪个 tab(缺省「基本设置」)
+-- type:控件类型 text(默认) / textarea / image(图片上传)
+--
+-- 当前分组:基本设置 / 公司信息 / 备案信息 / 政策条款
+-- value 为建议初始值,请按实际情况修改(备案号请替换为真实备案号)。
+--
+-- ============================================================
+-- PART 1:全新环境(表中无数据)—— 直接执行
+-- ============================================================
+INSERT INTO `yz_platform_site_setting`
+ (`title`, `value`, `tag`, `remark`, `create_time`, `update_time`)
+VALUES
+ -- 基本设置
+ ('站点名称', '云泽科技', 'sitename', '常用于
和页头 | group:基本设置', NOW(), NOW()),
+ ('副标题', 'YUNZER TECH', 'subtitle', '站点名称下方的补充说明,常用于页头/首页 | group:基本设置', NOW(), NOW()),
+ ('Logo(深色)', '', 'logo', '深色 Logo,用于浅色背景(如页头) | group:基本设置 | type:image', NOW(), NOW()),
+ ('Logo(浅色)', '', 'logow', '浅色 Logo,用于深色背景(如页脚) | group:基本设置 | type:image', NOW(), NOW()),
+ -- 公司信息
+ ('公司名称', '云泽科技有限公司', 'companyname', '公司全称,常用于页脚与「关于我们」 | group:公司信息', NOW(), NOW()),
+ ('联系电话', '400-000-0000', 'phone', '服务热线,常用于页头与页脚 | group:公司信息', NOW(), NOW()),
+ ('电子邮箱', 'contact@yunzer.com', 'email', '商务合作邮箱,可配合 mailto: 使用 | group:公司信息', NOW(), NOW()),
+ ('公司地址', '北京市海淀区中关村软件园 25 号楼', 'address', '公司所在地址,展示于页脚/联系页面 | group:公司信息', NOW(), NOW()),
+ ('工作时间', '工作日 9:00 - 18:00', 'worktime', '如:工作日 9:00 - 18:00 | group:公司信息', NOW(), NOW()),
+ -- 备案信息
+ ('ICP 备案号', '京ICP备00000000号-1', 'icp', '请替换为真实备案号,常用于页脚 | group:备案信息', NOW(), NOW()),
+ ('公安备案', '京公网安备11010800000000号', 'policeicp', '请替换为真实公安备案号,常用于页脚 | group:备案信息', NOW(), NOW()),
+ ('版权信息', 'COPYRIGHT (C) 2026 YUNZER TECHNOLOGY CO., LTD. ALL RIGHTS RESERVED.', 'copyright', '常用于页脚 | group:备案信息', NOW(), NOW()),
+ -- 政策条款
+ ('隐私政策', '/policy/privacy', 'privacy', '隐私政策页面链接 | group:政策条款', NOW(), NOW()),
+ ('使用条款', '/policy/terms', 'terms', '使用条款页面链接 | group:政策条款', NOW(), NOW());
+
+-- ============================================================
+-- PART 2:已有数据(表里有旧的 9 项)—— 只做增量调整,可重复执行
+-- ============================================================
+
+-- 2.1 隐私政策 / 使用条款 归入「政策条款」tab
+UPDATE `yz_platform_site_setting`
+ SET `remark` = '隐私政策页面链接 | group:政策条款', `update_time` = NOW()
+ WHERE `tag` = 'privacy' AND `delete_time` IS NULL;
+
+UPDATE `yz_platform_site_setting`
+ SET `remark` = '使用条款页面链接 | group:政策条款', `update_time` = NOW()
+ WHERE `tag` = 'terms' AND `delete_time` IS NULL;
+
+-- 2.2 新增「公司信息」分组的 5 个设置项(已存在同 tag 的记录不会重复插入)
+INSERT INTO `yz_platform_site_setting`
+ (`title`, `value`, `tag`, `remark`, `create_time`, `update_time`)
+SELECT v.`title`, v.`value`, v.`tag`, v.`remark`, NOW(), NOW()
+FROM (
+ SELECT '公司名称' AS `title`, '云泽科技有限公司' AS `value`, 'companyname' AS `tag`,
+ '公司全称,常用于页脚与「关于我们」 | group:公司信息' AS `remark`
+ UNION ALL SELECT '联系电话', '400-000-0000', 'phone', '服务热线,常用于页头与页脚 | group:公司信息'
+ UNION ALL SELECT '电子邮箱', 'contact@yunzer.com', 'email', '商务合作邮箱,可配合 mailto: 使用 | group:公司信息'
+ UNION ALL SELECT '公司地址', '北京市海淀区中关村软件园 25 号楼', 'address', '公司所在地址,展示于页脚/联系页面 | group:公司信息'
+ UNION ALL SELECT '工作时间', '工作日 9:00 - 18:00', 'worktime', '如:工作日 9:00 - 18:00 | group:公司信息'
+) v
+WHERE NOT EXISTS (
+ SELECT 1 FROM `yz_platform_site_setting` s
+ WHERE s.`tag` = v.`tag` AND s.`delete_time` IS NULL
+);
+
+-- 校验:SELECT tag, title, value, remark FROM yz_platform_site_setting WHERE delete_time IS NULL ORDER BY id;
diff --git a/go/models/init.go b/go/models/init.go
index 72fd3e7..07b87d4 100644
--- a/go/models/init.go
+++ b/go/models/init.go
@@ -57,6 +57,7 @@ func Init(_ string) {
new(SystemModules),
new(StorageConfig),
new(TenantSiteSetting),
+ new(PlatformSiteSetting),
new(ComplaintCategory),
new(PlatformComplaint),
new(SystemSoftwareUpgrade),
@@ -157,6 +158,7 @@ func Init(_ string) {
EnsureCrmProductDevColumns()
EnsureCrmUnitTable()
EnsureErpCompanyContactMobilesColumn()
+ EnsurePlatformSiteSettingTable()
}
// EnsureCrmPaybackTable 回款计划表建表(CREATE TABLE IF NOT EXISTS,可重复执行;
diff --git a/go/models/platform_site_setting.go b/go/models/platform_site_setting.go
new file mode 100644
index 0000000..6367915
--- /dev/null
+++ b/go/models/platform_site_setting.go
@@ -0,0 +1,289 @@
+package models
+
+import (
+ "regexp"
+ "strings"
+ "time"
+)
+
+// PlatformSiteSetting 平台官网站点设置表 yz_platform_site_setting(通用键值结构)
+//
+// 一行一个设置项:title 名称 / value 值 / tag 标签名 / remark 说明。
+// 模板里用 {yz:tag} 即可输出对应 value,例如 tag=sitename → {yz:sitename}。
+// 新增设置项只需在 PlatformSiteSettingDefs 里登记一条(或直接在表里插一行),
+// 无需再加表字段,因此该表对所有站点级设置通用。
+type PlatformSiteSetting struct {
+ ID uint64 `orm:"column(id);pk;auto" json:"id"`
+ Title string `orm:"column(title);size(128);default('')" json:"title"` // 设置项名称
+ Value string `orm:"column(value);type(text);null" json:"value"` // 设置项的值
+ Tag string `orm:"column(tag);size(64);default('')" json:"tag"` // 标签名,模板中写作 {yz:tag}
+ Remark string `orm:"column(remark);size(255);default('')" json:"remark"` // 备注/填写说明
+
+ CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add;null" json:"create_time"`
+ UpdateTime *time.Time `orm:"column(update_time);type(datetime);auto_now;null" json:"update_time"`
+ DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
+}
+
+func (m *PlatformSiteSetting) TableName() string {
+ return "yz_platform_site_setting"
+}
+
+// PlatformSiteSettingDef 站点设置项定义(顺序即 platform 端界面展示顺序)
+type PlatformSiteSettingDef struct {
+ Tag string // 标签名,模板中 {yz:tag}
+ Title string // 设置项名称
+ Remark string // 填写说明
+}
+
+// PlatformSiteSettingDefs 站点设置项定义清单(仅作初始化种子,不限制可新增的设置项)
+//
+// 该表现在完全由数据驱动:直接在表里插一行即成为一个新设置项,无需改动任何代码:
+// - tag 即模板标签名,表里写了什么标签,模板就能用 {yz:tag}
+// - remark 支持可选标记,用 | 分隔,未识别的片段当作说明文字展示:
+// group:分组名 决定 platform 端界面落在哪个 tab(缺省归入「基本设置」)
+// type:控件类型 text(默认,单行输入) / textarea(多行) / image(图片上传)
+// 例:深色 Logo,用于浅色背景 | group:基本设置 | type:image
+var PlatformSiteSettingDefs = []PlatformSiteSettingDef{
+ // 基本设置
+ {Tag: "sitename", Title: "站点名称", Remark: "常用于 和页头 | group:基本设置"},
+ {Tag: "subtitle", Title: "副标题", Remark: "站点名称下方的补充说明,常用于页头/首页 | group:基本设置"},
+ {Tag: "logo", Title: "Logo(深色)", Remark: "深色 Logo,用于浅色背景(如页头) | group:基本设置 | type:image"},
+ {Tag: "logow", Title: "Logo(浅色)", Remark: "浅色 Logo,用于深色背景(如页脚) | group:基本设置 | type:image"},
+
+ // 公司信息
+ {Tag: "companyname", Title: "公司名称", Remark: "公司全称,常用于页脚与「关于我们」 | group:公司信息"},
+ {Tag: "phone", Title: "联系电话", Remark: "服务热线,常用于页头与页脚 | group:公司信息"},
+ {Tag: "email", Title: "电子邮箱", Remark: "商务合作邮箱,可配合 mailto: 使用 | group:公司信息"},
+ {Tag: "address", Title: "公司地址", Remark: "公司所在地址,展示于页脚/联系页面 | group:公司信息"},
+ {Tag: "worktime", Title: "工作时间", Remark: "如:工作日 9:00 - 18:00 | group:公司信息"},
+
+ // 备案信息
+ {Tag: "icp", Title: "ICP 备案号", Remark: "如:苏ICP备2026000000号-1,常用于页脚 | group:备案信息"},
+ {Tag: "policeicp", Title: "公安备案", Remark: "如:苏公网安备32010000000000号,常用于页脚 | group:备案信息"},
+ {Tag: "copyright", Title: "版权信息", Remark: "如:© 2026 公司名称 版权所有 | group:备案信息"},
+
+ // 政策条款
+ {Tag: "privacy", Title: "隐私政策", Remark: "隐私政策链接,如 /page/privacy | group:政策条款"},
+ {Tag: "terms", Title: "使用条款", Remark: "使用条款链接,如 /page/terms | group:政策条款"},
+}
+
+// rePlatformSiteTag 标签名校验:与 tagengine 全局标签正则保持一致
+var rePlatformSiteTag = regexp.MustCompile(`^[a-zA-Z_]\w*$`)
+
+// platformSiteSettingDef 按 tag 取定义;未登记时返回空定义
+func platformSiteSettingDef(tag string) PlatformSiteSettingDef {
+ for _, d := range PlatformSiteSettingDefs {
+ if d.Tag == tag {
+ return d
+ }
+ }
+ return PlatformSiteSettingDef{Tag: tag, Title: tag, Remark: ""}
+}
+
+// GetPlatformSiteTagMap 返回 标签名 -> 值。
+// 已登记的定义项必定出现在返回结果中(未配置时为 ""),保证调用方不会缺 key。
+func GetPlatformSiteTagMap() map[string]string {
+ out := make(map[string]string, len(PlatformSiteSettingDefs))
+ for _, d := range PlatformSiteSettingDefs {
+ out[d.Tag] = ""
+ }
+ var rows []PlatformSiteSetting
+ if _, err := Orm.QueryTable(new(PlatformSiteSetting)).
+ Filter("delete_time__isnull", true).
+ All(&rows); err != nil {
+ return out
+ }
+ for _, r := range rows {
+ if r.Tag != "" {
+ out[r.Tag] = r.Value
+ }
+ }
+ return out
+}
+
+// SavePlatformSiteSettings 按标签名批量写入设置项(tag -> value)。
+// 已存在则更新 value,不存在则按定义插入;标签名非法的项直接忽略。
+func SavePlatformSiteSettings(values map[string]string) error {
+ now := time.Now()
+ for tag, val := range values {
+ tag = strings.TrimSpace(tag)
+ if !rePlatformSiteTag.MatchString(tag) {
+ continue
+ }
+
+ cnt, err := Orm.QueryTable(new(PlatformSiteSetting)).
+ Filter("tag", tag).
+ Filter("delete_time__isnull", true).
+ Count()
+ if err != nil {
+ return err
+ }
+
+ if cnt == 0 {
+ def := platformSiteSettingDef(tag)
+ if _, err = Orm.Insert(&PlatformSiteSetting{
+ Title: def.Title,
+ Tag: tag,
+ Value: val,
+ Remark: def.Remark,
+ UpdateTime: &now,
+ }); err != nil {
+ return err
+ }
+ continue
+ }
+
+ if _, err = Orm.QueryTable(new(PlatformSiteSetting)).
+ Filter("tag", tag).
+ Filter("delete_time__isnull", true).
+ Update(map[string]any{"value": val, "update_time": now}); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// GetPlatformSiteSettingRows 返回库中实际存在的设置行(不做默认值补齐)
+func GetPlatformSiteSettingRows() []PlatformSiteSetting {
+ var rows []PlatformSiteSetting
+ if _, err := Orm.QueryTable(new(PlatformSiteSetting)).
+ Filter("delete_time__isnull", true).
+ OrderBy("id").
+ All(&rows); err != nil {
+ return nil
+ }
+ return rows
+}
+
+// EnsurePlatformSiteSettingDefaults 补齐已登记但未入库的设置项(缺失时插入空值行)
+// 注意:须用库中实际行判断,不能用 GetPlatformSiteTagMap —— 它预置了所有默认 tag 的 key,
+// 会导致「已存在」判断永远成立、一行都插不进去。
+func EnsurePlatformSiteSettingDefaults() {
+ exist := map[string]bool{}
+ for _, r := range GetPlatformSiteSettingRows() {
+ if r.Tag != "" {
+ exist[r.Tag] = true
+ }
+ }
+
+ now := time.Now()
+ for _, d := range PlatformSiteSettingDefs {
+ if exist[d.Tag] {
+ continue
+ }
+ if _, err := Orm.Insert(&PlatformSiteSetting{
+ Title: d.Title,
+ Tag: d.Tag,
+ Value: "",
+ Remark: d.Remark,
+ UpdateTime: &now,
+ }); err != nil {
+ continue
+ }
+ exist[d.Tag] = true
+ }
+}
+
+// EnsurePlatformSiteSettingTable 建表(通用键值结构,CREATE TABLE IF NOT EXISTS,可重复执行)。
+// 若检测到旧宽表结构(含 logo_dark 列),先把历史数据迁移为键值行,再补齐默认项。
+func EnsurePlatformSiteSettingTable() {
+ table := new(PlatformSiteSetting).TableName()
+ sql := `CREATE TABLE IF NOT EXISTS ` + table + ` (
+ id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
+ title varchar(128) NOT NULL DEFAULT '' COMMENT '设置项名称',
+ value text COMMENT '设置项的值',
+ tag varchar(64) NOT NULL DEFAULT '' COMMENT '标签名,模板中写作 {yz:tag}',
+ remark varchar(255) NOT NULL DEFAULT '' COMMENT '备注/填写说明',
+ create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ update_time datetime DEFAULT NULL COMMENT '更新时间',
+ delete_time datetime DEFAULT NULL COMMENT '删除时间(软删除)',
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_tag (tag, delete_time),
+ KEY idx_delete_time (delete_time)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台官网站点设置表(通用键值结构)'`
+ if _, err := Orm.Raw(sql).Exec(); err != nil {
+ return
+ }
+
+ // 旧宽表升级场景:CREATE TABLE IF NOT EXISTS 不会改已有表,需补齐通用结构列
+ ensurePlatformSiteSettingColumns(table)
+
+ if hasTableColumn(table, "logo_dark") {
+ migrateLegacyPlatformSiteSetting(table)
+ }
+ EnsurePlatformSiteSettingDefaults()
+}
+
+// ensurePlatformSiteSettingColumns 补齐通用结构列(列缺失时新增,已存在则忽略)
+func ensurePlatformSiteSettingColumns(table string) {
+ cols := []struct{ name, def string }{
+ {"title", "varchar(128) NOT NULL DEFAULT '' COMMENT '设置项名称'"},
+ {"value", "text COMMENT '设置项的值'"},
+ {"tag", "varchar(64) NOT NULL DEFAULT '' COMMENT '标签名,模板中写作 {yz:tag}'"},
+ {"remark", "varchar(255) NOT NULL DEFAULT '' COMMENT '备注/填写说明'"},
+ }
+ for _, c := range cols {
+ if hasTableColumn(table, c.name) {
+ continue
+ }
+ _, _ = Orm.Raw("ALTER TABLE " + table + " ADD COLUMN " + c.def).Exec()
+ }
+}
+
+// hasTableColumn 判断表是否存在指定列(表不存在或查询异常时返回 false)
+func hasTableColumn(table, column string) bool {
+ var cnt int64
+ err := Orm.Raw(
+ "SELECT COUNT(1) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?",
+ table, column,
+ ).QueryRow(&cnt)
+ return err == nil && cnt > 0
+}
+
+// migrateLegacyPlatformSiteSetting 旧宽表结构迁移:
+// 旧列为 sitename/logo_dark/logo_light/icp/police_icp/copyright/privacy_policy/terms_of_service,
+// 迁移时仅在目标标签行为空的情况下写入,避免覆盖新配置。
+func migrateLegacyPlatformSiteSetting(table string) {
+ type legacyRow struct {
+ Sitename string
+ LogoDark string
+ LogoLight string
+ Icp string
+ PoliceIcp string
+ Copyright string
+ Privacy string
+ Terms string
+ Companyname string
+ }
+ var rows []legacyRow
+ if _, err := Orm.Raw(
+ "SELECT IFNULL(sitename,'') sitename, IFNULL(logo_dark,'') logo_dark, IFNULL(logo_light,'') logo_light," +
+ " IFNULL(icp,'') icp, IFNULL(police_icp,'') police_icp, IFNULL(copyright,'') copyright," +
+ " IFNULL(privacy_policy,'') privacy_policy, IFNULL(terms_of_service,'') terms_of_service" +
+ " FROM " + table + " WHERE delete_time IS NULL ORDER BY id LIMIT 1").QueryRows(&rows); err != nil {
+ return
+ }
+ if len(rows) == 0 {
+ return
+ }
+
+ old := rows[0]
+ current := GetPlatformSiteTagMap()
+ pending := map[string]string{
+ "sitename": old.Sitename,
+ "logo": old.LogoDark,
+ "logow": old.LogoLight,
+ "icp": old.Icp,
+ "policeicp": old.PoliceIcp,
+ "copyright": old.Copyright,
+ "privacy": old.Privacy,
+ "terms": old.Terms,
+ }
+ for tag, val := range pending {
+ if val == "" || strings.TrimSpace(current[tag]) != "" {
+ continue
+ }
+ current[tag] = val
+ }
+ _ = SavePlatformSiteSettings(pending)
+}
diff --git a/go/pkg/tagengine/engine.go b/go/pkg/tagengine/engine.go
index 851b035..5521ff9 100644
--- a/go/pkg/tagengine/engine.go
+++ b/go/pkg/tagengine/engine.go
@@ -213,6 +213,19 @@ func renderSelfClose(content string, ctx *RenderCtx) (string, error) {
return out, nil
}
+// SiteGlobals 返回站点级全局标签值(key 即标签名),与模板内 {yz:xxx} 完全同源。
+// 供前台公开接口(GET /site/setting)输出给 SPA 官网使用,保证 SPA 与 themes 直出模板展示一致。
+func SiteGlobals(tid uint64) map[string]string {
+ if globalProvider == nil {
+ return map[string]string{}
+ }
+ vals, err := globalProvider(tid, &RenderCtx{Tid: tid})
+ if err != nil || vals == nil {
+ return map[string]string{}
+ }
+ return vals
+}
+
// renderGlobals 替换全局标签;未提供的标签置空
func renderGlobals(content string, ctx *RenderCtx) string {
var vals map[string]string
diff --git a/go/pkg/tagengine/meta.go b/go/pkg/tagengine/meta.go
index 340a684..7d448d5 100644
--- a/go/pkg/tagengine/meta.go
+++ b/go/pkg/tagengine/meta.go
@@ -37,26 +37,30 @@ func TagDocs() []TagMeta {
Name: "站点基本信息",
Category: "global",
Syntax: "{yz:sitename}、{yz:logo}、{yz:ico} …直接写在模板任意位置",
- Desc: "取自当前租户的站点设置(backend 端 - 站点设置 - 基本信息)。直接替换为文本或地址,无需闭合;未配置的项输出空字符串。点击行末复制按钮可复制标签。",
+ Desc: "取自当前站点的站点设置。租户站点来自「站点设置 - 基本信息」(backend 端);平台官网(平台主域名)优先取「官网管理 - 站点设置」(platform 端),未填写时回退基本信息。直接替换为文本或地址,无需闭合;未配置的项输出空字符串。点击行末复制按钮可复制标签。",
Params: nil,
Fields: []string{
- "sitename 站点名称,常用于 和页头 来源:基本信息-站点名称",
- "logo 彩色 Logo 图片地址,适合浅色背景 来源:基本信息-站点LOGO",
- "logow 白色 Logo 图片地址,适合深色背景/页脚 来源:基本信息-白色LOGO",
+ "sitename 站点名称,常用于 和页头 来源:站点设置-站点名称",
+ "subtitle 副标题,站点名称下方的补充说明 来源:官网管理-站点设置-副标题",
+ "logo 深色 Logo 图片地址,适合浅色背景 来源:站点设置-深色LOGO",
+ "logow 浅色 Logo 图片地址,适合深色背景/页脚 来源:站点设置-浅色LOGO",
"ico 站点图标地址,用于 来源:基本信息-站点图标",
"companyname 公司全称 来源:基本信息-公司名称",
"companyintroduction 企业介绍(富文本HTML,直接用 div 承接) 来源:基本信息-企业介绍",
"description 站点简介,可用于 meta description 来源:基本信息-站点描述",
- "copyright 版权信息,常用于页脚 来源:基本信息-版权信息",
- "icp ICP 备案号,常用于页脚 来源:基本信息-ICP备案",
+ "copyright 版权信息,常用于页脚 来源:站点设置-版权信息",
+ "icp ICP 备案号,常用于页脚 来源:站点设置-ICP备案",
+ "policeicp 公安备案号/公安网备标识,常用于页脚 来源:官网管理-站点设置-公安备案",
+ "privacy 隐私政策链接地址,如 /page/privacy 来源:官网管理-站点设置-隐私政策",
+ "terms 使用条款链接地址,如 /page/terms 来源:官网管理-站点设置-使用条款",
},
- Example: "\n {yz:sitename}\n \n \n\n
\n{yz:companyintroduction}
\n",
+ Example: "\n {yz:sitename}\n \n \n\n
\n{yz:companyintroduction}
\n",
},
{
Name: "联系方式",
Category: "global",
Syntax: "{yz:phone}、{yz:email}、{yz:address}、{yz:worktime}",
- Desc: "取自租户的公司信息(backend 端 - 公司信息),常用于页头联系栏和页脚。直接替换为文本,无需闭合。",
+ Desc: "取自公司信息。平台官网优先取「官网管理 - 站点设置 - 公司信息」,未填写时回退租户的公司信息(backend 端 - 公司信息)。直接替换为文本,无需闭合。",
Params: nil,
Fields: []string{
"phone 联系电话,别名 {yz:tel}、{yz:mobile} 三者等价 来源:公司信息-联系电话",
diff --git a/go/pkg/tagengine/provider.go b/go/pkg/tagengine/provider.go
index 9c6f465..4166d7e 100644
--- a/go/pkg/tagengine/provider.go
+++ b/go/pkg/tagengine/provider.go
@@ -68,6 +68,7 @@ func globalSiteInfo(tid uint64, ctx *RenderCtx) (map[string]string, error) {
"companyintroduction": "",
"phone": "", "tel": "", "mobile": "", "email": "", "address": "", "worktime": "",
"keywords": "", "seotitle": "", "seodescription": "",
+ "policeicp": "", "privacy": "", "terms": "",
"aboutus": "/page/about", "contact": "/page/contact", "news": "/news", "team": "/page/team",
}
var row models.TenantSiteSetting
@@ -115,6 +116,21 @@ func globalSiteInfo(tid uint64, ctx *RenderCtx) (map[string]string, error) {
out["seotitle"] = vals["seoTitle"]
out["seodescription"] = vals["seoDescription"]
}
+
+ // 平台官网(tid 1)额外读取 platform 端「官网管理 - 站点设置」
+ // (yz_platform_site_setting,通用键值结构:tag 即标签名)。
+ // 值非空时覆盖同名全局标签,为空时保证标签存在(输出空串);租户站点不受影响。
+ if tid == 1 {
+ for k, v := range models.GetPlatformSiteTagMap() {
+ if v == "" {
+ if _, ok := out[k]; !ok {
+ out[k] = ""
+ }
+ continue
+ }
+ out[k] = v
+ }
+ }
return out, nil
}
diff --git a/go/routers/index/index.go b/go/routers/index/index.go
index d472c40..a69518e 100644
--- a/go/routers/index/index.go
+++ b/go/routers/index/index.go
@@ -14,6 +14,9 @@ func Register() {
beego.Router("/news/:id", &controllers.TenantSiteController{}, "get:NewsDetail")
beego.Router("/page/:path", &controllers.TenantSiteController{}, "get:Page")
+ // 官网前台站点设置(公开接口,供 SPA 官网读取,字段与模板标签 {yz:xxx} 同源)
+ beego.Router("/site/setting", &controllers.TenantSiteController{}, "get:SiteSetting")
+
beego.Router("/index/headmenu", &controllers.IndexPortalController{}, "get:GetHeadMenu")
beego.Router("/index/footerdata", &controllers.IndexPortalController{}, "get:GetFooterData")
beego.Router("/index/newscentertop4", &controllers.IndexPortalController{}, "get:GetNewsCenterTop4")
diff --git a/go/routers/platform/platform.go b/go/routers/platform/platform.go
index 6596ad0..7f7e3a8 100644
--- a/go/routers/platform/platform.go
+++ b/go/routers/platform/platform.go
@@ -305,4 +305,8 @@ func Register() {
beego.Router("/platform/template/status", &controllers.PlatformTemplateController{}, "post:Status")
beego.Router("/platform/template/delete/:id", &controllers.PlatformTemplateController{}, "post:Delete")
beego.Router("/platform/template/tags", &controllers.PlatformTemplateController{}, "get:Tags")
+
+ // 平台官网站点设置(yz_platform_site_setting)
+ beego.Router("/platform/website/setting/info", &controllers.PlatformWebsiteSettingController{}, "get:Info")
+ beego.Router("/platform/website/setting/save", &controllers.PlatformWebsiteSettingController{}, "post:Save")
}
diff --git a/platform/src/api/websiteSetting.js b/platform/src/api/websiteSetting.js
new file mode 100644
index 0000000..f9a66af
--- /dev/null
+++ b/platform/src/api/websiteSetting.js
@@ -0,0 +1,22 @@
+import request from '@/utils/request'
+
+// ==================== 官网管理 - 站点设置(平台端) ====================
+// 数据落在 yz_platform_site_setting(单行配置),字段可被官网模板标签调用:
+// {yz:sitename} {yz:logo} {yz:logow} {yz:icp} {yz:policeicp} {yz:copyright} {yz:privacy} {yz:terms}
+
+// 读取站点设置
+export function getWebsiteSetting() {
+ return request({
+ url: '/platform/website/setting/info',
+ method: 'get'
+ })
+}
+
+// 保存站点设置
+export function saveWebsiteSetting(data) {
+ return request({
+ url: '/platform/website/setting/save',
+ method: 'post',
+ data
+ })
+}
diff --git a/platform/src/router/index.js b/platform/src/router/index.js
index a0bfdf2..f3e4933 100644
--- a/platform/src/router/index.js
+++ b/platform/src/router/index.js
@@ -27,6 +27,12 @@ const staticMainChildren = [
component: () => import("@/views/tools/passwordStore/index.vue"),
meta: { requiresAuth: true, title: "密码存储" }
},
+ {
+ path: "/website/setting",
+ name: "PlatformWebsiteSetting",
+ component: () => import("@/views/website/setting/index.vue"),
+ meta: { requiresAuth: true, title: "站点设置" }
+ },
{
path: "/template/index",
name: "PlatformTemplateManage",
diff --git a/platform/src/views/website/setting/components/TagTip.vue b/platform/src/views/website/setting/components/TagTip.vue
new file mode 100644
index 0000000..7c74dc4
--- /dev/null
+++ b/platform/src/views/website/setting/components/TagTip.vue
@@ -0,0 +1,56 @@
+
+
+ 调用标签
+ {{ tag }}
+ 复制
+ {{ desc }}
+
+
+
+
+
+
diff --git a/platform/src/views/website/setting/components/tab_basic.vue b/platform/src/views/website/setting/components/tab_basic.vue
new file mode 100644
index 0000000..619182e
--- /dev/null
+++ b/platform/src/views/website/setting/components/tab_basic.vue
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存设置
+ 重置
+
+
+
+
+
+
+
diff --git a/platform/src/views/website/setting/components/tab_company.vue b/platform/src/views/website/setting/components/tab_company.vue
new file mode 100644
index 0000000..706a2b1
--- /dev/null
+++ b/platform/src/views/website/setting/components/tab_company.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存设置
+ 重置
+
+
+
+
+
+
+
diff --git a/platform/src/views/website/setting/components/tab_policy.vue b/platform/src/views/website/setting/components/tab_policy.vue
new file mode 100644
index 0000000..adc5efc
--- /dev/null
+++ b/platform/src/views/website/setting/components/tab_policy.vue
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存设置
+ 重置
+
+
+
+
+
+
+
diff --git a/platform/src/views/website/setting/components/tab_record.vue b/platform/src/views/website/setting/components/tab_record.vue
new file mode 100644
index 0000000..db0a844
--- /dev/null
+++ b/platform/src/views/website/setting/components/tab_record.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存设置
+ 重置
+
+
+
+
+
+
+
diff --git a/platform/src/views/website/setting/index.vue b/platform/src/views/website/setting/index.vue
new file mode 100644
index 0000000..f025891
--- /dev/null
+++ b/platform/src/views/website/setting/index.vue
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/website/src/api/site.ts b/website/src/api/site.ts
new file mode 100644
index 0000000..d56f582
--- /dev/null
+++ b/website/src/api/site.ts
@@ -0,0 +1,60 @@
+/**
+ * 站点设置接口
+ *
+ * 数据来源:后端 yz_platform_site_setting(platform 端「官网管理 - 站点设置」)。
+ * 返回字段名与官网模板标签一一对应,因此 SPA 官网与 themes 直出模板展示的
+ * 站点信息始终一致:
+ * sitename {yz:sitename} 站点名称
+ * logo {yz:logo} 深色 Logo(浅色背景)
+ * logow {yz:logow} 浅色 Logo(深色背景)
+ * icp {yz:icp} ICP 备案号
+ * policeicp {yz:policeicp} 公安备案号
+ * copyright {yz:copyright} 版权信息
+ * privacy {yz:privacy} 隐私政策链接
+ * terms {yz:terms} 使用条款链接
+ */
+
+/**
+ * 后端返回的站点设置:key 就是模板标签名
+ * 完全数据驱动 —— 表里有什么 tag 就返回什么,新增设置项前端无需改动。
+ * 下列为常用项的类型提示,其余标签通过索引签名访问即可。
+ */
+export type SiteSetting = Record & {
+ /** {yz:sitename} 站点名称 */
+ sitename?: string;
+ /** {yz:subtitle} 副标题 */
+ subtitle?: string;
+ /** {yz:logo} 深色 Logo,用于浅色背景(如页头) */
+ logo?: string;
+ /** {yz:logow} 浅色 Logo,用于深色背景(如页脚) */
+ logow?: string;
+ /** {yz:icp} ICP 备案号 */
+ icp?: string;
+ /** {yz:policeicp} 公安备案号 */
+ policeicp?: string;
+ /** {yz:copyright} 版权信息 */
+ copyright?: string;
+ /** {yz:privacy} 隐私政策链接 */
+ privacy?: string;
+ /** {yz:terms} 使用条款链接 */
+ terms?: string;
+};
+
+/** 接口前缀:开发期由 vite 代理 /api 到后端,生产可用 VITE_API_BASE_URL 覆盖 */
+const API_BASE = (import.meta as any).env?.VITE_API_BASE_URL || "/api";
+
+/** 拉取当前域名的站点设置;接口不可用时返回 null,由调用方保留默认配置 */
+export async function fetchSiteSetting(): Promise {
+ try {
+ const res = await fetch(`${API_BASE}/site/setting`, {
+ headers: { Accept: "application/json" },
+ });
+ if (!res.ok) return null;
+ const body = await res.json();
+ if (body?.code !== 200 || !body.data) return null;
+ return body.data as SiteSetting;
+ } catch (error) {
+ console.warn("[site] 站点设置获取失败,使用默认配置", error);
+ return null;
+ }
+}
diff --git a/website/src/config/site.ts b/website/src/config/site.ts
index bd34df3..9c84403 100644
--- a/website/src/config/site.ts
+++ b/website/src/config/site.ts
@@ -1,7 +1,20 @@
/**
* 站点级公共配置:品牌、导航、页脚
- * 后续接入后端 CMS 时,只需替换此处的数据来源即可。
+ *
+ * 站点信息的数据来源已切换为后端「官网管理 - 站点设置」(应用启动时由 loadSiteInfo 拉取),
+ * 与官网模板 themes/ 中使用的标签同源,括号内为对应标签:
+ * sitename 站点名称 {yz:sitename}
+ * logoDark 深色 Logo {yz:logo}
+ * logoLight 浅色 Logo {yz:logow}
+ * icp ICP 备案号 {yz:icp}
+ * policeIcp 公安备案号 {yz:policeicp}
+ * copyright 版权信息 {yz:copyright}
+ * privacy 隐私政策链接 {yz:privacy}
+ * terms 使用条款链接 {yz:terms}
+ * 后端未返回的字段保留文件内默认值。
*/
+import { computed, reactive } from "vue";
+import { fetchSiteSetting } from "@/api/site";
export interface NavItem {
label: string;
@@ -13,7 +26,7 @@ export interface FooterColumn {
links: NavItem[];
}
-export const siteInfo = {
+export const siteInfo = reactive({
brandMark: "YZ",
brandName: "云泽科技",
brandSub: "YUNZER TECH",
@@ -25,7 +38,63 @@ export const siteInfo = {
postalCode: "100193",
copyright:
"COPYRIGHT (C) 2026 YUNZER TECHNOLOGY CO., LTD. ALL RIGHTS RESERVED.",
-};
+
+ // ↓↓↓ 以下字段由「官网管理 - 站点设置」下发(对应官网模板标签) ↓↓↓
+ /** {yz:sitename} 站点名称 */
+ sitename: "",
+ /** {yz:subtitle} 副标题 */
+ subtitle: "",
+ /** {yz:logo} 深色 Logo,用于浅色背景(页头) */
+ logoDark: "",
+ /** {yz:logow} 浅色 Logo,用于深色背景(页脚) */
+ logoLight: "",
+ /** {yz:icp} ICP 备案号 */
+ icp: "",
+ /** {yz:policeicp} 公安备案号 */
+ policeIcp: "",
+ /** {yz:privacy} 隐私政策链接 */
+ privacy: "",
+ /** {yz:terms} 使用条款链接 */
+ terms: "",
+});
+
+/**
+ * 站点设置标签值:key 就是模板标签名,与后端 {yz:xxx} 完全一致
+ * 供 v-yz 指令解析({yz:icp}
),未配置或后端未返回时输出空字符串
+ */
+export const siteTags = reactive>({});
+
+/**
+ * 拉取站点设置并回填 siteInfo(应用启动时调用一次)。
+ * 接口异常时静默处理,页面继续使用默认配置。
+ */
+export async function loadSiteInfo(): Promise {
+ const data = await fetchSiteSetting();
+ if (!data) return;
+
+ // {yz:sitename} → 页头品牌名
+ if (data.sitename) {
+ siteInfo.sitename = data.sitename;
+ siteInfo.brandName = data.sitename;
+ }
+ if (data.subtitle) siteInfo.subtitle = data.subtitle;
+ if (data.logo) siteInfo.logoDark = data.logo;
+ if (data.logow) siteInfo.logoLight = data.logow;
+ // 公司信息:站点设置里有值就覆盖本地默认(页脚联系栏、页脚品牌全称),没填则保留默认
+ if (data.companyname) siteInfo.fullName = data.companyname;
+ if (data.phone) siteInfo.phone = data.phone;
+ if (data.email) siteInfo.email = data.email;
+ if (data.address) siteInfo.address = data.address;
+ if (data.icp) siteInfo.icp = data.icp;
+ if (data.policeicp) siteInfo.policeIcp = data.policeicp;
+ if (data.copyright) siteInfo.copyright = data.copyright;
+ if (data.privacy) siteInfo.privacy = data.privacy;
+ if (data.terms) siteInfo.terms = data.terms;
+
+ // 标签值:直接以接口返回的「标签名 -> 值」为准(v-yz 指令解析用)。
+ // 数据驱动:表里新增的设置项会自动进入 siteTags,模板里写 {yz:新tag} 即可,无需改代码。
+ Object.assign(siteTags, data);
+}
/** 顶部工具条链接 */
export const utilityLinks: NavItem[] = [
@@ -65,9 +134,16 @@ export const footerColumns: FooterColumn[] = [
},
];
-/** 页脚底部合规链接 */
-export const policyLinks: NavItem[] = [
- { label: "隐私政策", path: "/policy/privacy" },
- { label: "使用条款", path: "/policy/terms" },
+/**
+ * 页脚底部合规链接
+ * 隐私政策 {yz:privacy} / 使用条款 {yz:terms} 取自站点设置,未配置时回退站内默认页面
+ */
+export const policyLinks = computed(() => [
+ { label: "隐私政策", path: siteInfo.privacy || "/policy/privacy" },
+ { label: "使用条款", path: siteInfo.terms || "/policy/terms" },
{ label: "网站地图", path: "/policy/sitemap" },
-];
+]);
+
+/** 是否为站外链接(站点设置里可能填完整 URL) */
+export const isExternalLink = (path: string): boolean =>
+ /^https?:\/\//i.test(path);
diff --git a/website/src/directives/yz.ts b/website/src/directives/yz.ts
new file mode 100644
index 0000000..6104ce4
--- /dev/null
+++ b/website/src/directives/yz.ts
@@ -0,0 +1,48 @@
+/**
+ * {yz:xxx} 站点标签指令(dedecms 风格)
+ *
+ * 用法:在模板里直接写标签,元素加 v-yz 即可
+ * {yz:icp}
+ * 备案号:{yz:icp} | {yz:policeicp}
+ *
+ * 元素内所有 {yz:标签名} 会被替换为「官网管理 - 站点设置」中对应的值,
+ * 未配置的标签输出空字符串(与后端 themes 模板行为一致)。
+ *
+ * 注意:指令通过 innerHTML 替换内容,因此元素内只能放纯文本/HTML 片段,
+ * 不要放子组件或需要响应式绑定的内容;需要条件渲染的地方请用 siteInfo.xxx。
+ */
+import { watchEffect, type Directive } from "vue";
+import { siteTags } from "@/config/site";
+
+/** 与后端 tagengine 的全局标签正则保持一致 */
+const TAG_PATTERN = /\{yz:([a-zA-Z_]\w*)\}/g;
+
+/** 每个元素对应的 watchEffect 停止函数 */
+const stops = new WeakMap void>();
+
+/** 依据当前标签值渲染元素内容;始终基于首次保存的原文,避免重复替换 */
+function render(el: HTMLElement): void {
+ if (!el.dataset.yzRaw) {
+ el.dataset.yzRaw = el.innerHTML;
+ }
+ const raw = el.dataset.yzRaw;
+
+ el.innerHTML = raw.replace(
+ TAG_PATTERN,
+ (_match: string, key: string) => siteTags[key] ?? ""
+ );
+}
+
+export const yz: Directive = {
+ mounted(el) {
+ // watchEffect 会追踪 render 中读取到的标签值;
+ // 站点设置是异步拉取的,数据到位后会自动重新渲染
+ stops.set(el, watchEffect(() => render(el)));
+ },
+ unmounted(el) {
+ stops.get(el)?.();
+ stops.delete(el);
+ },
+};
+
+export default yz;
diff --git a/website/src/layout/SiteFooter.vue b/website/src/layout/SiteFooter.vue
index fe4b02f..c744f95 100644
--- a/website/src/layout/SiteFooter.vue
+++ b/website/src/layout/SiteFooter.vue
@@ -1,5 +1,5 @@
@@ -8,8 +8,19 @@ import { footerColumns, policyLinks, siteInfo } from "@/config/site";