Skip to content

可视化装修系统

本文档详细说明 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": {}
        }
    ]
}

字段说明

字段类型说明
componentsArray可用组件清单
components[].nameString组件唯一标识(对应模板文件)
components[].titleString后台显示的中文名称
components[].iconStringElement UI 图标类名
components[].optionsObject组件扩展选项(保留)
pageArray可装修的页面列表
page[].titleString页面中文名称
page[].nameString页面标识(对应 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 数组元素详细说明

字段类型说明
componentString组件模板路径(相对于 view/,不含 .ejs
titleString组件中文名(后台显示)
typeString组件位置类型:subject(主内容区)、right(右侧栏)
modelsArray该组件可关联的数据模型列表
paramObject组件默认参数
param.modelString默认数据模型
param.conditionObject查询条件配置
param.condition.queryObjectMongoDB 查询条件
param.condition.orderbyString默认排序字段
param.condition.orderwayString排序方向
param.condition.limitString默认每页数量
param.condition.cachetimeNumber默认缓存时间(秒)
param.condition.is_tabsBoolean是否显示分类 Tab
param.condition.titleString区块标题
optionsObject额外选项(保留)

links 数组元素说明

字段类型说明
titleString链接显示名称
pathString路由路径(传递给 url() 函数的参数)

formBuilder 数组元素说明

字段类型说明
typeString表单控件类型:radioColorPicker
fieldString保存到 design.config 的键名
titleString表单标签
valueAny默认值
propsObject控件额外属性
colObjectElement UI 栅格布局 { span: 13 }
optionsArray选项列表 [{ label, value }]
props.predefineArray颜色选择器的预设颜色列表

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);
}

设计模式与最佳实践

新增一个装修组件

  1. data/config.jscomponents 数组中添加声明
js
{
    component: "../index/components/block-news",
    title: "最新资讯",
    type: "subject",
    models: ["article"],
    param: {
        model: "article",
        condition: { orderby: "createtime", limit: "10", cachetime: 600 }
    },
    options: {}
}
  1. 创建对应的模板文件 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.jsformBuilder 中添加:

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') %>
<% } %>

新增侧边栏支持

  1. data/config.js 中设置 is_sidebar: true
  2. view/common/aside.ejs 中读取 design.sidebar 渲染菜单
  3. 管理员在后台配置侧边栏菜单,数据保存到 Data.jsonsidebar 字段

Released under the MIT License.