Skip to content

模板目录结构与页面路由约定

本文档详细说明主题文件系统的标准目录布局、每个目录和关键文件的职责、页面路由的命名约定。


标准目录树

一个完整的 PC 端主题包含以下目录结构(以 default_pc 为例,实际可按需增删):

<theme_name>/
├── info.json                  # [必需] 主题元数据
├── README.md                  # [可选] 主题说明文档

├── config/                    # [必需] 主题配置
│   ├── design.json            # 页面装修组件定义(声明主题提供了哪些可拖拽组件)
│   └── index.json             # 简单配置项(键值对列表)

├── data/                      # [必需] 运行时数据
│   ├── Design.json            # 实际保存的装修布局数据
│   ├── config.js              # 主题能力声明(侧边栏、组件、链接、表单)
│   ├── DynamicPage.js         # 动态页面定义
│   └── design_bak.json        # [可选] 装修数据的备份

├── utils/                     # [可选] 模板工具函数
│   └── common.js              # 注入模板的自定义 JS 函数(通过 common.xxx() 访问)

└── view/                      # [必需] EJS 模板文件
    ├── layout/                # [必需] 页面布局
    │   ├── default.ejs        # 默认布局(主站 HTML 骨架)
    │   └── <custom>.ejs       # 自定义布局

    ├── common/                # 公共组件(在 layout 中引用)
    │   ├── header.ejs         # 头部导航栏
    │   ├── footer.ejs         # 页脚
    │   ├── aside.ejs          # 侧边栏
    │   ├── public.ejs         # CSS 资源与全局变量注入
    │   ├── script.ejs         # JavaScript 引入入口
    │   └── pagination.ejs     # 分页组件

    ├── components/            # 可复用业务组件(通过 include() 引用)
    │   ├── comment/
    │   │   └── index.ejs      # 评论组件
    │   └── grid-hot/
    │       └── index.ejs      # 排行榜组件

    ├── index/                 # 首页模块
    │   ├── index.ejs          # 首页
    │   └── components/        # 首页专属组件(装修系统可拖拽的组件放这里)
    │       ├── block-home-top.ejs    # 推荐区块示例
    │       └── block-special.ejs     # 歌单区块示例

    ├── music/                 # 音乐模块(示例)
    │   ├── detail.ejs         # 详情页
    │   └── type.ejs           # 列表页
    ├── user/                  # 用户模块
    ├── order/                 # 订单模块
    ├── ...                    # 按需添加更多业务模块

    ├── error/                 # [推荐] 错误页
    │   ├── 404.ejs
    │   └── index.ejs
    └── ajax/
        └── index.ejs          # AJAX 加载更多返回的 HTML 片段

---

## 关键文件详解

### `config/design.json` — 装修组件定义

声明主题向后端管理面板提供了哪些可拖拽的页面组件。

```json
{
    "components": [
        {
            "name": "header",
            "title": "头部",
            "icon": "el-icon-menu",
            "options": {}
        },
        {
            "name": "banner",
            "title": "轮播图",
            "icon": "el-icon-menu",
            "options": {}
        }
    ],
    "page": [
        {
            "title": "首页",
            "name": "index",
            "options": {}
        }
    ]
}
字段说明
components[].name组件唯一标识
components[].title后台显示名称
components[].iconElement UI 图标名
components[].options组件额外选项(保留)
page[].title可装修的页面名称
page[].name页面标识(用于 design.index 的 key)

config/index.json — 简单配置项

提供后台可视化配置表单中的简单字段。

json
[
    {
        "title": "默认语言",
        "name": "default",
        "value": "zh-CN",
        "type": "input"
    },
    {
        "title": "默认颜色",
        "name": "theme",
        "value": "light",
        "type": "input"
    },
    {
        "title": "加载封面",
        "name": "square",
        "value": "0",
        "type": "image"
    }
]
字段类型说明
titleString配置项显示标题
nameString配置键名
valueString默认值
typeString表单控件类型(inputimage

data/Design.json — 装修布局数据

存储实际的装修数据,由后台管理面板的拖拽操作生成。在模板中通过 design 变量访问。

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"
                }
            ],
            "id": "group_xxx"
        }
    ],
    "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"
            }
        ],
        "right": [
            {
                "component": "../index/components/right-rank-list",
                "title": "排行榜",
                "type": "right",
                "models": [],
                "param": {},
                "options": {},
                "id": "comp_xxx"
            }
        ]
    }
}

数据结构说明

路径类型说明
configObject可视化配置项的值(由 data/config.jsformBuilder 定义表单)
config.theme_modeString主题模式:light / dark / system
config.theme_languageString默认语言:chinese_simplified / english / korean
config.themeColorString主题色(CSS 变量 --theme-color
config.progressColorString播放进度条颜色
sidebarArray侧边栏菜单配置(分组结构)
sidebar[].titleString菜单分组标题
sidebar[].childrenArray子菜单项列表
sidebar[].children[].titleString菜单项标题(支持 HTML)
sidebar[].children[].iconStringFont Awesome 图标类名
sidebar[].children[].pathString路由路径(传递给 url() 函数)
index.subjectArray首页主体区域的装修组件列表
index.rightArray首页右侧栏的装修组件列表

组件实例(subject/right 数组元素)

字段类型说明
componentString组件模板路径(相对于 view/,不含 .ejs 后缀)
titleString组件标题
typeString组件类型:subject(主内容区)或 right(右侧栏)
modelsArray组件关联的数据模型名称
paramObject传递给组件的参数
param.conditionObject数据查询条件(orderby, limit, cachetime, query 等)
param.modelNameString指定数据模型名称
optionsObject组件额外选项(保留)
idString组件实例的唯一标识

渲染方式(首页 view/index/index.ejs):

ejs
<%
let { subject, right } = design.index;
%>
<% if (subject) { %>
   <% for (let item of subject) { %>
    <%- await include(item.component, {...item.param, title: item.title, model: model}); %>
  <% } %>
<% } %>

组件通过 include() 加载,参数展开后传入子模板。


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: {}
        },
        // ... 更多组件
    ],
    
    // 可用的页面链接列表(用于后台菜单配置等)
    links: [
        { title: "首页", path: "/" },
        { title: "登录", path: "User/login" },
        { title: "注册", path: "User/register" },
        { title: "充值", path: "Recharge/index" },
        { title: "用户中心", path: "User/index" },
        { title: "购物车", path: "Cart/index" },
        { title: "订单中心", path: "Order/index" },
        // ... 更多链接
    ],
    
    // 后台可视化配置表单定义
    formBuilder: [
        {
            type: "radio",
            field: "theme_mode",
            title: "默认模式",
            value: "light",
            props: { placeholder: "请选择主题模式" },
            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"
                ]
            }
        }
    ]
}

formBuilder 表单项字段说明

字段类型说明
typeString表单控件类型:radioColorPicker
fieldString字段键名(保存到 design.config.<field>
titleString表单标签
valueAny默认值
propsObject额外属性(placeholder、predefine 等)
colObject栅格布局({ span: 13 }
optionsArray选项列表(用于 radio/select 类型)
options[].labelString选项显示文本
options[].valueString选项值

data/DynamicPage.js — 动态页面定义

定义系统中哪些页面是「动态页面」——即通过 URL 规则自动匹配的列表/聚合页。

js
export default [
    {
        type_ids: null,
        diyname: "header",
        title: "专辑",
        seotitle: "最新音乐专辑、合辑、大碟",
        keywords: ["专辑", "合辑", "大碟", "音乐"],
        description: "提供热门专辑展示",
        data: {},
        flag: [],
        content: "",
        image: "",
        route: "/albums.html",
        model_name: "MusicSong",
        tpl: "dynamic/albums",
        theme: "default_pc",
        sort: 0,
        isguest: 1,
        status: 1
    },
    // ... 更多动态页面
]

动态页面字段说明

字段类型说明
diynameString页面分组标识(header 顶部导航 / event 活动类 / agreement 协议类)
titleString页面标题
seotitleStringSEO 标题
keywordsArraySEO 关键词列表
descriptionStringSEO 描述
routeString访问路径
model_nameString关联的数据模型名(如 MusicSong
tplString对应的 EJS 模板路径(相对于 view/,不含 .ejs
themeString所属主题名
sortNumber排序值
isguestNumber是否允许游客访问(1=允许)
statusNumber状态(1=启用)

动态页面在导航栏中的加载方式(view/common/header.ejs):

ejs
<%
const navbar = await model("dynamic_page", {
    query: { diyname: "header", status: 1 },
    orderway: -1,
    orderby: "sort",
    limit: "20",
    cachetime: 600
});
%>
<% navbar.forEach((item, key) => { %>
<a class="navbar-item" href="<%= item.url %>"><%= item.title %></a>
<% }); %>

页面路由命名约定

路由与模板文件的映射关系

路由配置 routes/index.js 定义了「URL 路径 → 控制器方法」的映射,控制器方法中调用 this.fetch('模板路径') 渲染对应 EJS 文件。

路由示例控制器方法模板路径
GET /Indexindexview/index/index.ejs
GET /song/:idMusicdetailview/music/detail.ejs
GET /album/:idAlbumdetailview/album/detail.ejs
GET /video/:idVideodetailview/video/detail.ejs
GET /author/:idAuthordetailview/author/detail.ejs
GET /author/creatorAuthorcreatorview/author/creator.ejs
GET /music/typeMusictypeview/music/type.ejs
GET /dynamicDynamicindex动态匹配 dynamic/<tpl>
GET /user/loginUserloginview/user/login.ejs
GET /user/registerUserregisterview/user/register.ejs
GET /user/indexUserindexview/user/index.ejs
GET /cart/indexCartindexview/cart/index.ejs
GET /order/indexOrderindexview/order/index.ejs
GET /recharge/indexRechargeindexview/recharge/index.ejs
GET /search/indexSearchindexview/search/index.ejs
GET /vip/indexVipindexview/vip/index.ejs
GET /index/sitemapIndexsitemapview/index/sitemap.ejs
GET /rss/*Rssbaidu/google/bing 等view/rss/*.ejs
GET /signin/indexSigninindexview/signin/index.ejs
POST /ajax/indexAjaxindexview/ajax/index.ejs(返回 HTML 片段)

URL 生成函数

在模板中通过 url() 函数生成路由 URL,无需手动拼接路径:

ejs
<%= url('User/login') %>                    <!-- /user/login -->
<%= url('User/login', { redirect: '/home' }) %>  <!-- /user/login?redirect=/home -->
<%= url('Music/detail', { id: song._id }) %>     <!-- /song/abc123 -->
<%= url('Author/detail', { id: author._id }) %>  <!-- /author/abc123 -->
<%= url('Search/index') %>                    <!-- /search/index?wd=xxx -->

AJAX 请求的模板渲染

当请求头部包含 x-requested-with: XMLHttpRequest 时,Base.fetch() 仍然正常渲染模板,但前端通常通过 fetch API 获取 HTML 片段并插入 DOM。例如:

js
// 前端
fetch('/ajax/index?modename=music_song&orderby=createtime&page=1&tplid=music-cell', {
    headers: { 'x-requested-with': 'XMLHttpRequest' }
})
.then(res => res.text())
.then(html => {
    document.getElementById('list-target').innerHTML = html;
});

对应的服务端模板 view/ajax/index.ejs

ejs
<%
let { list: __list__, page } = await model(modeName, {
    orderway, orderby, pageNum, limit: limit,
    paging: true, query, cachetime: 600
});
%>
<% for (let [index, item] of __list__.entries()) { %>
    <% if (tplid == 'item-dynamic') { %>
    <%- await include('../components/item-dynamic', { item }); %>
    <% } else if (tplid == 'hot-cell') { %>
    <%- await include('../components/grid-hot/hot-cell', { item }); %>
    <% } else if (tplid == 'music-cell') { %>
    <%- await include('../components/music-cell', { item }); %>
    <% } %>
<% } %>

布局切换

控制器中通过 this.layout('layoutName') 切换布局。系统默认为 default,也可指定自定义布局:

js
// 使用 default 布局
this.fetch('music/detail');

// 使用自定义布局
this.layout('author');
this.fetch('author/creator');

// 完全禁用布局(用于 AJAX 片段返回)
this.layout(false);
this.fetch('ajax/index');

对应的布局文件必须存在于 view/layout/<name>.ejs


模板中的 <script> 标签约定

每个页面模板的末尾应声明 pageName 变量,用于前端 JS 按页面执行初始化:

ejs
<script>
  var pageName = "music/detail";
</script>

JS 入口据此分发:

js
const pages = {
    "music/detail": initMusicDetail,
    "index/index": initIndex,
};
document.addEventListener("DOMContentLoaded", () => {
    if (window.pageName && pages[window.pageName]) {
        pages[window.pageName]();
    }
});

约定pageName 格式为 "模块/页面",与控制器 fetch 的模板路径一致。这是 JS 与模板之间的唯一命名约定,其余选择器、class 名等由开发者自定。


公共组件(view/common/)说明

文件用途引用方式
public.ejs引入 CSS 资源(Bulma、FontAwesome、主题样式),注入全局 JS 变量 AOD在 layout 的 <head><%- await include('../common/public') %>
script.ejs引入 JS 资源,区分开发/生产模式在 layout 的 </body><%- await include('../common/script') %>
header.ejs全站导航栏(含搜索框、用户菜单、动态页面导航)在 layout 中 <%- await include('../common/header') %>
footer.ejs全站页脚(友链、证书、版权信息)在 layout 中 <%- await include('../common/footer') %>
aside.ejs左侧边栏(菜单、主题切换、语言设置)在 layout 中 <%- await include('../common/aside') %>
pagination.ejs分页导航组件(上一页/下一页/页码)在列表页中 <%- await include('../common/pagination') %>
block.ejs通用区块占位(空模板,仅输出 title)可选
empty.ejs空数据占位提示可选

业务模块模板的通用页面

每个业务模块通常包含以下类型的页面:

页面类型命名约定示例说明
列表页type.ejs / index.ejsmusic/type.ejs分类/聚合列表
详情页detail.ejsmusic/detail.ejs单条数据详情
编辑页update.ejsauthor/update.ejs数据编辑表单
创建页create.ejsspecial/create.ejs数据创建表单
AJAX 片段ajax_*.ejsuser/ajax_login.ejs弹窗中加载的 HTML 片段

utils/common.js — 模板工具函数

主题下的 utils/common.js 是一个 ES Module 文件,导出可在 EJS 模板中调用的工具函数。这些函数通过 templateUtils[theme] 机制注入到模板上下文中,在模板中通过 common.函数名() 访问:

ejs
<%= common.formatTime(obj.duration) %>
<%= common.bytesToKbps(obj.bitrate) %> kbps
<%= common.formatBytes(obj.size) %>
<%= common.formatUtcStrin(obj.createtime) %>
<%= common.paytypeText(order.paytype) %>
<%= common.productType(item.type) %>

完整导出函数列表(基于真实代码 utils/common.js):

函数参数返回值说明
formatTime(seconds)number"MM:SS""HH:MM:SS"秒数转时间格式
randomString(len)number(默认 32)string生成随机字符串
hideMobileOrEmail(str)stringstring脱敏手机号/邮箱
filterUrl(url, param, keys, query)string, object, array, objectstring动态构建 URL(合并参数过滤)
formatUtcStrin(time, format)string, string(默认 YYYY-MM-DD HH:mm:ssstringUTC 时间转本地格式化
paytypeText(paytype)stringstring支付方式中文映射
productType(type)stringstring模型名中文映射
htmlFilter(html)stringstring过滤 HTML 危险标签(script/iframe/video/audio)
fileIcon(url, fileName)string, stringstring(SVG HTML)生成文件类型图标(含云盘识别)
formatBytes(bytes, decimals)number, number(默认 2)string字节转 KB/MB/GB
analyzeRichTextLines(richText)stringnumber分析富文本行数
getSecurityParameter(request, method, model)object, string, stringobject安全过滤请求参数
formNumber(num)numberstring数字转中文单位(千/万/亿)
bytesToKbps(bytes, decimalPlaces)number, number(默认 2)string字节转 kbps
omit(request, keys)object, string|arrayobject移除对象中指定属性
couponTypeText(value)stringstring优惠券类型中文映射

跨模块引用约定

子模板引用(include

使用相对路径引用组件,路径相对于 view/ 目录:

ejs
<%- await include('../components/comment/index', { id: obj._id, type: 'music' }) %>
<%- await include('../components/grid-hot/index', { modelName: 'music_song', condition: {...} }) %>
<%- await include('../common/pagination') %>

include 传参

第二个参数是一个对象,子模板中可直接访问对象中的属性:

ejs
<%# 父模板调用 %>
<%- await include('../components/comment/index', {
    id: obj._id,
    type: 'music',
    source_uid: obj.author.user_id
}) %>

<%# 子模板 components/comment/index.ejs 中可直接使用 %>
<%= id %>
<%= type %>
<%= source_uid %>

Released under the MIT License.