Appearance
可视化装修系统
本文档详细说明 DJAOD 主题的可视化装修系统——后台管理面板如何通过拖拽式操作配置主题的页面布局、侧边栏菜单、主题样式等,以及这些数据如何在前台模板中被消费。
装修系统的数据流
后台管理面板 (拖拽操作)
│
▼
design.json (保存到 data/Design.json)
│
▼
Redis 缓存 (key: template_design_<theme>)
│
▼
Base.view() 读取 → 注入模板变量 design
│
▼
模板中使用 (design.index.subject / design.sidebar / design.config)配置架构关系
| 文件 | 角色 | 谁写 |
|---|---|---|
config/design.json | 装修组件定义:声明该主题提供了哪些可拖拽的装修组件 | 主题开发者写 |
data/config.js | 主题能力声明:侧边栏支持、可用组件、可用链接、后台配置表单 | 主题开发者写 |
data/Design.json | 装修数据:实际保存的装修布局(组件的排列和参数) | 后台管理员操作生成 |
config/index.json | 简单配置项:键值对形式的配置 | 主题开发者写 |
数据文件详解
config/design.json —— 装修组件定义
声明主题提供的所有可拖拽组件,后台管理面板据此生成组件选择列表:
json
{
"components": [
{
"name": "header",
"title": "头部",
"icon": "el-icon-menu",
"options": {}
},
{
"name": "banner",
"title": "轮播图",
"icon": "el-icon-menu",
"options": {}
},
{
"name": "product",
"title": "产品",
"icon": "el-icon-menu",
"options": {}
},
{
"name": "footer",
"title": "底部",
"icon": "el-icon-menu",
"options": {}
}
],
"page": [
{
"title": "首页",
"name": "index",
"options": {}
}
]
}字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
components | Array | 可用组件清单 |
components[].name | String | 组件唯一标识(对应模板文件) |
components[].title | String | 后台显示的中文名称 |
components[].icon | String | Element UI 图标类名 |
components[].options | Object | 组件扩展选项(保留) |
page | Array | 可装修的页面列表 |
page[].title | String | 页面中文名称 |
page[].name | String | 页面标识(对应 Design.json 的 key) |
data/config.js —— 主题能力声明
这是主题最核心的声明文件,定义了主题的功能边界和后台表单配置。
js
export default {
// 是否支持侧边栏
is_sidebar: true,
// 是否支持右侧栏
is_right: true,
// 可用组件清单(首页装修时可选)
components: [
{
component: "../index/components/swiper-index-full",
title: "首页轮播",
type: "subject",
models: [],
param: {
condition: {
query: { config_name: "index_swiper" },
cachetime: 60 * 60
}
},
options: {}
},
{
component: "../index/components/block-home-top",
title: "推荐专辑",
type: "subject",
models: ["block", "music_album", "music_song", "video_movie"],
param: {
model: "block",
condition: {
orderby: "month_downs",
limit: "20",
cachetime: 600
}
},
options: {}
},
{
component: "../index/components/block-special",
title: "推荐歌单",
type: "subject",
models: [],
param: {
model: "music_special",
condition: {
orderby: "month_downs",
limit: "20",
cachetime: 600
}
},
options: {}
},
{
component: "../index/components/block-slide-author",
title: "制作人",
models: [],
param: {
condition: {
orderway: "desc",
orderby: "songs",
limit: "20",
cachetime: 600
}
},
type: "subject",
options: {}
},
{
component: "../index/components/block-slide-top",
title: "特别推荐",
type: "subject",
models: ["block", "music_album", "music_song", "video_movie"],
param: {
model: "music_song",
condition: {
orderby: "plays",
limit: "20",
cachetime: 600
}
},
options: {}
},
{
component: "../index/components/block-update",
title: "更新时间表",
type: "subject",
models: [],
param: {
model: "music_song",
condition: {
orderway: "desc",
orderby: "createtime",
limit: "20",
cachetime: 600
}
},
options: {}
},
{
component: "../index/components/block-product",
title: "首页商品",
type: "subject",
models: [],
param: {
model: "store_product",
condition: {
orderway: "desc",
orderby: "createtime",
limit: "20",
cachetime: 600
}
},
options: {}
},
{
component: "../index/components/block-video",
title: "首页视频",
type: "subject",
models: [],
param: {
model: "store_product",
condition: {
orderway: "desc",
orderby: "createtime",
limit: "20",
cachetime: 600
}
},
options: {}
},
{
component: "../index/components/block-combination",
title: "首页团购组件",
type: "subject",
models: [],
param: {},
options: {}
},
{
component: "../index/components/block-seckill",
title: "首页秒杀组件",
type: "subject",
models: [],
param: {},
options: {}
},
{
component: "../index/components/block-presell",
title: "首页预售组件",
type: "subject",
models: [],
param: {},
options: {}
},
// 右侧栏组件 (type: "right")
{
component: "../index/components/right-rank-list",
title: "排行榜",
type: "right",
models: [],
param: {},
options: {}
},
{
component: "../index/components/right-new-track",
title: "新曲目",
type: "right",
models: [],
param: {},
options: {}
},
{
component: "../components/grid-hot/index",
title: "排行榜(带Tab)",
type: "right",
models: ["music_album", "music_song", "video_movie"],
param: {
modelName: "music_song",
condition: {
is_tabs: true,
title: "排行榜",
orderway: "desc",
orderby: "week_downs",
limit: "20",
query: {},
cachetime: 3600
}
},
options: {}
}
],
// 可用链接列表(后台菜单配置)
links: [
{ "title": "首页", "path": "/" },
{ "title": "登录", "path": "User/login" },
{ "title": "注册", "path": "User/register" },
{ "title": "充值", "path": "Recharge/index" },
{ "title": "用户中心", "path": "User/index" },
{ "title": "用户信息", "path": "User/profile" },
{ "title": "创作者中心", "path": "Author/creator" },
{ "title": "我的作品", "path": "Music/index" },
{ "title": "我的专辑", "path": "Author/album" },
{ "title": "创作者收益", "path": "Author/income" },
{ "title": "通知中心", "path": "Notify/index" },
{ "title": "会员中心", "path": "Vip/index" },
{ "title": "我的收藏", "path": "User/favs" },
{ "title": "购物车", "path": "Cart/index" },
{ "title": "订单中心", "path": "Order/index" },
{ "title": "播放记录", "path": "User/record" },
{ "title": "下载记录", "path": "User/download" },
{ "title": "我的歌单", "path": "Special/my" },
{ "title": "音乐库", "path": "Dynamic/music" },
{ "title": "歌单中心", "path": "Dynamic/specials" },
{ "title": "圈子", "path": "Dynamic/circles" }
],
// 后台可视化配置表单
formBuilder: [
{
"type": "radio",
"field": "theme_mode",
"title": "默认模式",
"value": "light",
"props": {
"placeholder": "请选择主题模式",
"type": "div",
"class": "tips-info"
},
"col": { "span": 13 },
"options": [
{ "label": "白色模式", "value": "light" },
{ "label": "暗黑模式", "value": "dark" },
{ "label": "随系统", "value": "system" }
]
},
{
"type": "radio",
"field": "theme_language",
"title": "默认语言",
"value": "chinese_simplified",
"options": [
{ "label": "中文", "value": "chinese_simplified" },
{ "label": "英文", "value": "english" },
{ "label": "韩文", "value": "korean" }
]
},
{
type: "ColorPicker",
field: "themeColor",
title: "主题颜色",
value: "#4258ff",
props: {
predefine: [
"#4258ff", "#ff8c00", "#ffd700",
"#90ee90", "#00ced1", "#1e90ff", "#c71585"
]
}
},
{
"type": "ColorPicker",
"field": "progressColor",
"title": "波形颜色",
"value": "#4258ff",
"props": {
"predefine": [
"#4258ff", "#ff8c00", "#ffd700",
"#90ee90", "#00ced1", "#1e90ff", "#c71585"
]
}
}
]
}components 数组元素详细说明:
| 字段 | 类型 | 说明 |
|---|---|---|
component | String | 组件模板路径(相对于 view/,不含 .ejs) |
title | String | 组件中文名(后台显示) |
type | String | 组件位置类型:subject(主内容区)、right(右侧栏) |
models | Array | 该组件可关联的数据模型列表 |
param | Object | 组件默认参数 |
param.model | String | 默认数据模型 |
param.condition | Object | 查询条件配置 |
param.condition.query | Object | MongoDB 查询条件 |
param.condition.orderby | String | 默认排序字段 |
param.condition.orderway | String | 排序方向 |
param.condition.limit | String | 默认每页数量 |
param.condition.cachetime | Number | 默认缓存时间(秒) |
param.condition.is_tabs | Boolean | 是否显示分类 Tab |
param.condition.title | String | 区块标题 |
options | Object | 额外选项(保留) |
links 数组元素说明:
| 字段 | 类型 | 说明 |
|---|---|---|
title | String | 链接显示名称 |
path | String | 路由路径(传递给 url() 函数的参数) |
formBuilder 数组元素说明:
| 字段 | 类型 | 说明 |
|---|---|---|
type | String | 表单控件类型:radio、ColorPicker |
field | String | 保存到 design.config 的键名 |
title | String | 表单标签 |
value | Any | 默认值 |
props | Object | 控件额外属性 |
col | Object | Element UI 栅格布局 { span: 13 } |
options | Array | 选项列表 [{ label, value }] |
props.predefine | Array | 颜色选择器的预设颜色列表 |
data/Design.json —— 装修数据(实际保存的布局)
实际保存的布局数据,由后台管理员通过拖拽操作生成:
json
{
"config": {
"theme_mode": "system",
"theme_language": "chinese_simplified",
"themeColor": "#E0842C",
"progressColor": "#90EE90"
},
"sidebar": [
{
"title": "发现",
"children": [
{
"title": "首页",
"icon": "fa fa-home",
"path": "/",
"id": "item_xxx"
},
{
"title": "VIP",
"icon": "fab fa-square-vimeo",
"path": "Vip/index",
"id": "item_yyy"
}
],
"id": "group_xxx"
},
{
"title": "我的",
"children": [
{
"title": "我的收藏",
"icon": "fas fa-heart",
"path": "User/favs",
"id": "item_zzz"
},
{
"title": "购物车",
"icon": "fas fa-shopping-cart",
"path": "Cart/index",
"id": "item_aaa"
}
],
"id": "group_yyy"
}
],
"index": {
"subject": [
{
"component": "../index/components/swiper-index-full",
"title": "首页轮播",
"type": "subject",
"models": [],
"param": {
"condition": {
"query": { "config_name": "index_swiper" },
"cachetime": 3600
}
},
"options": {},
"id": "comp_xxx"
},
{
"component": "../index/components/block-home-top",
"title": "推荐专辑",
"type": "subject",
"models": ["block", "music_album", "music_song", "video_movie"],
"param": {
"modelName": "music_album",
"condition": {
"orderby": "month_downs",
"limit": "20",
"cachetime": 600
}
},
"options": {},
"id": "comp_yyy"
}
],
"right": [
{
"component": "../index/components/right-rank-list",
"title": "排行榜",
"type": "right",
"models": [],
"param": {},
"options": {},
"id": "comp_right_xxx"
},
{
"component": "../components/grid-hot/index",
"title": "排行榜(Tab)",
"type": "right",
"models": ["music_album", "music_song", "video_movie"],
"param": {
"modelName": "music_song",
"condition": {
"is_tabs": true,
"title": "排行榜",
"orderway": "desc",
"orderby": "week_downs",
"limit": "20",
"query": {},
"cachetime": 3600
}
},
"options": {},
"id": "comp_right_yyy"
}
]
}
}数据存储与缓存机制:
js
// 从 Redis 缓存读取
const design = await fastify?.redis.get(`template_design_${theme}`);
if (design) return design;
// 缓存未命中,从文件读取
const designPath = path.resolve(global.PATH_TEMPLATE, theme, "data", "design.json");
if (fs.existsSync(designPath)) {
const design = fs.readFileSync(designPath, 'utf-8');
fastify?.redis.set(`template_design_${theme}`, design, { EX: 60 * 60 * 24 }); // 24小时
return JSON.parse(design);
}模板中的装修数据消费
首页组件渲染 (view/index/index.ejs)
ejs
<%
let { subject, right } = design.index;
%>
<div class="columns is-multiline">
<div class="column is-12-mobile is-12-tablet is-12-desktop is-9-fullhd">
<% if (subject) { %>
<% for (let item of subject) { %>
<%- await include(item.component, {
...item.param,
title: item.title,
model: model
}); %>
<% } %>
<% } %>
</div>
<div class="column is-12-mobile is-12-tablet is-12-desktop is-3-fullhd">
<% if (right) { %>
<% for (let item of right) { %>
<%- await include(item.component, item.param); %>
<% } %>
<% } %>
</div>
</div>侧边栏菜单渲染 (view/common/aside.ejs)
ejs
<%
let sidebar = design?.sidebar || [];
%>
<aside class="menu sidebar">
<% for (let item of sidebar) { %>
<p class="menu-label"><%= item.title %></p>
<ul class="menu-list">
<% for (let child of item.children) { %>
<li>
<a href="<%= url(child.path)%>" data-link="ajax">
<span class="icon mr-2">
<i class="<%= child.icon %>"></i>
</span>
<%- child.title %>
</a>
</li>
<% } %>
</ul>
<% } %>
</aside>主题配置消费 (view/common/public.ejs)
ejs
<%
var { themeColor, progressColor } = design.config;
%>
<style>
:root {
--theme-color: <%= themeColor %>;
}
</style>
<script>
var AOD = {
__TPL_STATIC__: "<%= __TPL_STATIC__ %>",
plugins: <%- JSON.stringify(plugins) %>,
progressColor: '<%- progressColor %>',
}
</script>主题模式消费 (view/layout/default.ejs)
ejs
<%
let theme = cookie.theme || design?.config?.theme_mode || 'light';
%>
<!DOCTYPE html>
<html lang="zh" data-theme="<%= theme %>">DynamicPage 动态页面机制
数据定义 (data/DynamicPage.js)
js
export default [
{
type_ids: null,
diyname: "header", // 页面分组
title: "专辑",
seotitle: "最新音乐专辑",
keywords: ["专辑", "合辑", "音乐"],
description: "提供热门专辑展示",
data: {},
image: "",
route: "/albums.html",
model_name: "MusicSong", // 数据模型
tpl: "dynamic/albums", // 渲染模板
theme: "default_pc",
sort: 0,
isguest: 1,
status: 1
},
{
diyname: "event", // 活动类页面
title: "团购",
route: "/group",
model_name: "MusicGroup",
tpl: "dynamic/combination",
theme: "default_pc",
sort: 0,
isguest: 1,
status: 1
},
{
diyname: "agreement", // 协议类页面
title: "用户协议",
route: "/agreement/user.html",
model_name: "DynamicPage",
tpl: "dynamic/agreement",
theme: "default_pc",
sort: 0,
isguest: 1,
status: 1
}
]在模板中的使用
导航栏中加载(diyname: "header"):
ejs
<%
const navbar = await model("dynamic_page", {
query: { diyname: "header", status: 1 },
orderway: -1,
orderby: "sort",
limit: "20",
cachetime: 600
});
%>
<% navbar.forEach((item) => { %>
<a class="navbar-item" href="<%= item.url %>"><%= item.title %></a>
<% }); %>活动下拉菜单(diyname: "event"):
ejs
<%
const navbar_dropdown = await model("dynamic_page", {
query: { diyname: "event", status: 1 },
orderway: "asc",
orderby: "createtime",
limit: "20",
cachetime: 600
});
%>
<% navbar_dropdown.forEach((item) => { %>
<a class="navbar-item" href="<%= item.url %>"><%= item.title %></a>
<% }); %>页脚协议链接(diyname: "agreement"):
ejs
<%
const agreement = await model("dynamic_page", {
query: { diyname: "agreement", status: 1 },
orderway: -1,
orderby: "sort",
limit: "20",
cachetime: 600
});
%>
<% for (let item of agreement) { %>
<a href="<%= item.url %>"><%= item.title %></a>
<% } %>动态页面控制器 (controller/Dynamic.js)
js
async index(request, reply) {
const site_name = await this.fastify.getConfig("site_name");
let template = 'dynamic/index';
const dynamic = reply.routeOptions.config;
if (!dynamic) {
return this.error('页面不存在');
}
template = dynamic.tpl;
this.assign('obj', dynamic);
this.assign('title', dynamic.title + " - " + site_name);
this.assign('keywords', dynamic.keywords ? dynamic.keywords.join(",") : "");
this.assign('description', dynamic.description);
this.assign('query', request.query);
this.assign('page_model', dynamic.model_name);
return this.fetch(template);
}设计模式与最佳实践
新增一个装修组件
- 在
data/config.js的components数组中添加声明:
js
{
component: "../index/components/block-news",
title: "最新资讯",
type: "subject",
models: ["article"],
param: {
model: "article",
condition: { orderby: "createtime", limit: "10", cachetime: 600 }
},
options: {}
}- 创建对应的模板文件
view/index/components/block-news.ejs:
ejs
<div class="block">
<h2 class="title is-4"><%= title %></h2>
<%
const articles = await model("article", {
orderby: "createtime",
limit: condition?.limit || "10",
cachetime: condition?.cachetime || 600
}) || [];
%>
<% articles.forEach(item => { %>
<div class="box">
<a href="<%= item.url %>"><%= item.title %></a>
</div>
<% }); %>
</div>新增一个可视化配置项
在 data/config.js 的 formBuilder 中添加:
js
{
"type": "radio",
"field": "footer_style",
"title": "页脚样式",
"value": "simple",
"options": [
{ "label": "简约", "value": "simple" },
{ "label": "完整", "value": "full" }
]
}在模板中使用 design.config.footer_style:
ejs
<% if (design.config.footer_style === 'full') { %>
<%- await include('../common/footer_full') %>
<% } else { %>
<%- await include('../common/footer') %>
<% } %>新增侧边栏支持
data/config.js中设置is_sidebar: trueview/common/aside.ejs中读取design.sidebar渲染菜单- 管理员在后台配置侧边栏菜单,数据保存到
Data.json的sidebar字段