模板管理

发布时间:

模版管理

如果要制作自己的模版请点 【新增模版】会从默认模版复制一份,这样当系统升级的时候不会覆盖掉你制作的模版

  • 首页模版,命名规则 index_模版名称.html
  • 列表模版,命名规则 list_模版名称.html
  • 详情模版,命名规则 detail_模版名称.html
  • 公共模版,命名规则 inc_模版名称.html
  • 评论模版,命名规则 comment_模版名称.html

模版存放目录

模版目录的命名规则 模版名称-模版的UUID,比如上面的cmswing模版,模版目录就是 cmswing-37934d72-fe2c-4a0d-afbb-414993e63986,这个目录分别在 app/public/cms 目录和 app/view/cms目录各有一个,public下的是这个模版的静态文件目录,制作模版的时候需要的图片,js,css,放到这个目录下的,view 下的是这个模版的模版文件,模版的目录结构在后台添加模版时会自动生生成,无需手动创建。

目录结构

//以默认模版为例
├── public
│   ├── cms
│   │   └── default-1f8dda2a-6e9e-4762-a273-1bc138ec9ffa //默认模版静态资源目录
│   │       ├── css
│   │       │   └── app.css
│   │       ├── favicon.ico //网站 ico
│   │       ├── images
│   │       │   ├── detail.png //模版详情预览图 必须存在
│   │       │   ├── index.png //模版首页预览图 必须存在
│   │       │   ├── list.png //模版列表预览图 必须存在
│   │       │   └── logo 
│   │       │       ├── icon_512x512.png
│   │       │       ├── logo_dark.svg
│   │       │       ├── logo_light.svg
│   │       │       └── logo_sm.svg
│   │       └── js
│   │           └── list.js //默认的列表子分类筛选js,如果你没有自己重写,制作列表模版的时候要引入它
└── view
    ├── cms
    │   ├── default-1f8dda2a-6e9e-4762-a273-1bc138ec9ffa //默认模版的模版文件目录
    │   │   ├── comment_ajax_edit.html //评论模版编辑
    │   │   ├── comment_ajax_list.html //评论模版 列表
    │   │   ├── comment_ajax_reply.html //评论模版 回复
    │   │   ├── comment_ajax_reply_edit.html //评论模版 回复编辑
    │   │   ├── comment_index.html //评论模版 评论入口
    │   │   ├── detail_default.html //默认详情模版
    │   │   ├── inc_404.html //默认404
    │   │   ├── inc_base.html //默认公共页面
    │   │   ├── inc_footer.html //默认公共底部
    │   │   ├── inc_header.html //默认公共头部
    │   │   ├── index_default.html //默认首页模版
    │   │   └── list_default.html //默认列表模版

 

模版标签

模版标签是在html模版里面调用后台数据的集合 格式 {{参数1|@标签名(...参数n)}},模版引擎是使用的 Nunjuck 如果对Nunjuck不熟悉模版制作前请先阅读 Nunjuck文档
前端框架用的是 Bootstrap v5版本

为什么不使用目前流行的 vue或react 来开发cms前台模版?首先前后端分离的方案并不适合cms前台开发,不利于seo,当然你也可以说你可以采用服务器渲染,服务器渲染是可以解决但是大大增加了技术成本,我就想搞个企业站或者一个中小型的网站,搞那么多花里胡哨干嘛!技术是为人服务的不是难为人的。一个网站总共就那么几个模版页面,没必要搞的那么复杂。

全局标签

全局标签是在所有的cms前台模版都可以使用的标签

'cms'|@template('参数')

  • 参数 'path',获取当前模板路径
  • 参数 'version',获取当前模板版本号

cms模版路径标签,因为静态资源的缓存,所以我们在静态资源携带版本号 {{'cms'|@template('version')}} 以便模版更新修改后能够及时更新。

    <link rel="stylesheet" href="/public/cms/{{'cms'|@template('path')}}/css/app.css?v={{'cms'|@template('version')}}">
    <link rel="shortcut icon" href="/public/cms/{{'cms'|@template('path')}}/favicon.ico?v={{'cms'|@template('version')}}">
    <link rel="apple-touch-icon" href="/public/cms/{{'cms'|@template('path')}}/logo/icon_512x512.png?v={{'cms'|@template('version')}}">
   <!--{{'cms'|@template('path')}} 会返回 当前模版的目录名称 比如:default-1f8dda2a-6e9e-4762-a273-1bc138ec9ffa -->
 

参数|@navigation

系统导航标签

参数: 'header'|'footer'

头部导航 {{'header'|@navigation}},具体返回参数 可以使用 {{'header'|@navigation|dump}} 在模版内打印查看。

{%for item in 'header'|@navigation%}
<li class="nav-item dropdown">
<a href="{{item.url}}" {%if item.children%}id="nav_{{item.id}}" data-bs-toggle="dropdown"
aria-haspopup="true" aria-expanded="false" {%endif%} {%if item.target%}target="_blank"
{%endif%} class="nav-link {%if item.children%}dropdown-toggle{%endif%}">
{{item.title}}
</a>
{%if item.children%}
<div aria-labelledby="nav_{{item.id}}"
class="dropdown-menu dropdown-menu-clean dropdown-menu-hover dropdown-fadeinup">
<ul class="list-unstyled m-0 p-0">
{%for _item in item.children%}
<li class="dropdown-item dropdown">
<a class="dropdown-link" href="{{_item.url}}" {%if
_item.children%}data-bs-toggle="dropdown" {%endif%} {%if
_item.target%}target="_blank" {%endif%}>{{_item.title}}</a>
{%if _item.children%}
<ul
class="dropdown-menu dropdown-menu-hover dropdown-menu-block-md shadow-lg rounded-xl border-0 m-0">
{%for __item in _item.children%}
<li class="dropdown-item"><a href="{{__item.url}}" {%if
__item.target%}target="_blank" {%endif%}
class="dropdown-link">{{__item.title}}</a></li>
{%endfor%}
</ul>
{%endif%}
</li>
{%endfor%}
</ul>
</div>
{%endif%}
</li>
{%endfor%}

底部导航 {{'footer'|@navigation}},具体返回参数 可以使用 {{'footer'|@navigation|dump}} 在模版内打印查看。

'表名'|@findAll('条件')

  • 表名:要调用的数据表
  • 条件:{}

@findAll 同等于 sequelize 的 findAll,具体可以参考sequelize文档 它生成一个标准的 SELECT 查询,该查询将从表中检索所有条目(除非受到 where 子句的限制).

比如要调取 cms_doc表内 classify_id等于 2的并且 position 字段是 '1,2,3' 这样的存储格式,需要postition 等于1,调取10条,并且要关联cms_classify表。

{% set position = 'cms_doc'|@findAll({
        include : 'cms_classify',
        where:{
        classify_id:2,
        FIND_IN_SET:['position',1]
        },
        offset: 0,
        limit: 10
        })%}

比如要调取所id大于3的用户

{% set memeber = 'mc_member'|@findAll({
where:{
id:{op_gt:3} 
} 
})%}

{%for item in memeber%}
....
{%endfor%}

上面代码 大于3 在Sequelize中的默认操作符是 {[Op.gt]: 3} 在 cms模版标签中是{op_gt:3},操作法对映如下,具体可以参考 Sequelize操作符

 operatorsAliases: {
    op_eq: Op.eq, //等于
    op_ne: Op.ne, //不等于
    op_gte: Op.gte,//大于等于
    op_gt: Op.gt,//大于
    op_lte: Op.lte,//小于等于
    op_lt: Op.lt,//小于
    op_not: Op.not, //NOT
    op_in: Op.in, //IN
    op_notIn: Op.notIn
    op_is: Op.is,
    op_like: Op.like,
    op_notLike: Op.notLike,
    op_iLike: Op.iLike,
    op_notILike: Op.notILike,
    op_startsWith: Op.startsWith,
    op_endsWith: Op.endsWith,
    op_substring: Op.substring,
    op_regexp: Op.regexp,
    op_notRegexp: Op.notRegexp,
    op_iRegexp: Op.iRegexp,
    op_notIRegexp: Op.notIRegexp,
    op_between: Op.between,
    op_notBetween: Op.notBetween,
    op_overlap: Op.overlap,
    op_contains: Op.contains,
    op_contained: Op.contained,
    op_adjacent: Op.adjacent,
    op_strictLeft: Op.strictLeft,
    op_strictRight: Op.strictRight,
    op_noExtendRight: Op.noExtendRight,
    op_noExtendLeft: Op.noExtendLeft,
    op_and: Op.and,
    op_or: Op.or,
    op_any: Op.any,
    op_all: Op.all,
    op_values: Op.values,
    op_col: Op.col,
  },

 

'表名'|@findOne('条件')

  • 表名:要调用的数据表
  • 条件:{}

@findOne 方法获得它找到的第一个条目(它可以满足提供的可选查询参数).

查询 id =3的用户

{%set memeber = 'mc_member'|@findOne({
id:3
})%}
{{memeber|dump}}
 

ctx.userInfo.uuid|@mc_menu

用户中心(MC)菜单标签

{% set menu=ctx.userInfo.uuid|@mc_menu %}
{%for item in menu%}
<li
class="nav-item {%if (item.path ==ctx.url) or (ctx.url in (item.arrPath or [])) %}active{%endif%}">
{%if item.children%}
<a class="nav-link px-0" href="#">
<span class="group-icon">
<i class="fi fi-arrow-end"></i>
<i class="fi fi-arrow-down"></i>
</span>
<span class="px-2 d-inline-block"> {{item.name}} </span>
</a>
{%else%}
<a class="nav-link px-0" href="{{item.path}}">
<i class="fi fi-arrow-end m-0 smaller"></i>
<span class="px-2 d-inline-block"> {{item.name}} </span>
</a>
{%endif%} {%if item.children%}
<ul class="nav flex-column ps-2">
{%for _item in item.children%}
<li class="nav-item {%if _item.path ==ctx.url%}active{%endif%}">
<a class="nav-link" href="{{_item.path}}"> {{_item.name}} </a>
</li>
{%endfor%}
</ul>
{%endif%}
</li>
{%endfor%}

 

'栏目id'|@classify('类型')

根据id调取栏目

  • 栏目id  要调取的栏目id
  • 类型 'top' 获取当前栏目最顶级父栏目下所有的子栏目,'sub' 获取当前栏目的子分类

获取当前栏目和下面的子栏目

{%set classifyList = classify.id|@classify('top')%} 
{%if classifyList%}
<!-- CATEGORIES -->
<nav
class="nav-deep nav-deep-light mb-3 shadow-xs shadow-none-md shadow-none-xs px-3 pb-3 p-0-md p-0-xs rounded">
<!-- mobile trigger : categories -->
<button
class="clearfix btn btn-toggle btn-sm w-100 text-align-left shadow-md border rounded mb-1 d-block d-lg-none"
data-bs-target="#nav_responsive"
data-toggle-container-class="d-none d-sm-block bg-white shadow-md border animate-fadein rounded p-3"
>
<span class="group-icon px-2 py-2 float-start">
<i class="fi fi-bars-2"></i>
<i class="fi fi-close"></i>
</span>
<span class="h5 py-2 m-0 float-start"> 分类 </span>
</button>

<!-- desktop only -->
<h5 class="py-3 m-0 d-none d-lg-block">分类</h5>

<!-- navbar : navigation -->
<ul id="nav_responsive" class="nav flex-column d-none d-lg-block">
{%for item in classifyList%}
<li
class="nav-item {%if (item.id ==classify.id) or (classify.id in (item.arrId or [])) %}active{%endif%}">
{%if item.children%}
<a class="nav-link px-0" href="#">
<span class="group-icon">
<i class="fi fi-arrow-end"></i>
<i class="fi fi-arrow-down"></i>
</span>
<span class="px-2 d-inline-block"> {{item.title}} </span>
</a>
{%else%}
<a class="nav-link px-0" href="{{item.url}}">
<i class="fi fi-arrow-end m-0 smaller"></i>
<span class="px-2 d-inline-block"> {{item.title}} </span>
</a>
{%endif%} {%if item.children%}
<ul class="nav flex-column ps-2">
{%for _item in item.children%}
<li class="nav-item {%if _item.id ==classify.id%}active{%endif%}">
<a class="nav-link" href="{{_item.url}}"> {{_item.title}} </a>
</li>
{%endfor%}
</ul>
{%endif%}
</li>
{%endfor%}
</ul>
</nav>
<!-- /CATEGORIES -->
{%endif%}

 

获取当前栏目的子分类

{% set subclassify = classify.id|@classify('sub') %} {% for
item in subclassify%}
<!-- Suggestions -->
<div
class="card border-0 shadow-xs d-block mb-3 px-3 pb-3 shadow-none-md shadow-none-xs p-0-md p-0-xs rounded">
<!-- mobile trigger : categories -->
<button
class="clearfix btn btn-toggle btn-sm w-100 text-align-left shadow-md border rounded mb-1 d-block d-lg-none"
data-bs-target="#nav_{{ item.name }}"
data-toggle-container-class="d-none d-sm-block bg-white shadow-md border animate-fadein rounded p-3"
>
<span class="group-icon px-2 py-2 float-start">
<i class="fi fi-bars-2"></i>
<i class="fi fi-close"></i>
</span>
<span class="h5 py-2 m-0 float-start"> {{ item.label }} </span>
</button>
<!-- desktop only -->
<h6 class="py-3 m-0 d-none d-lg-block">{{ item.label }}</h6>
{% if item.type=='radios' %}
<div id="nav_{{ item.name }}" class="checkgroup flex-column d-none d-lg-block classify-sub"
data-type="{{ item.type }}" data-name="{{ item.name }}" data-url="{{ item.url }}"
data-checkgroup-checkbox-single="true">
{% for v in item.options %}
<div class="form-check mb-1">
<!-- item -->
<input
class="form-check-input form-check-input-primary class-sub-input"
name="{{ item.name }}"
type="checkbox"
value="{{ v.value }}"
id="{{ item.name }}-{{ v.value }}"
{%if v.check %}checked{% endif %}
/>
<label
class="form-check-label"
for="{{ item.name }}-{{ v.value }}"
>
{{ v.label }}
</label>
</div>
{% endfor %}
</div>
{% elif item.type=='checkboxes' %}
<div id="nav_{{ item.name }}" class="checkgroup flex-column d-none d-lg-block">
<div class="form-check mb-2">
<!-- check all -->
<input
data-checkall-container="#checkall-{{ item.name }}"
class="form-check-input form-check-input-default class-sub-input"
type="checkbox"
value=""
id="checkall-{{ item.name }}-top"
/>
<label
class="form-check-label"
for="checkall-{{ item.name }}-top"
>
全选/不选
</label>
</div>

<div id="checkall-{{ item.name }}" class="ps-0 classify-sub" data-type="{{ item.type }}"
data-name="{{ item.name }}" data-url="{{ item.url }}">
{% for v in item.options %}
<div class="form-check mb-1">
<!-- item -->
<input
class="form-check-input form-check-input-primary class-sub-input"
name="{{ item.name }}"
type="checkbox"
value="{{ v.value }}"
id="{{ item.name }}-{{ v.value }}"
{% if v.check %}checked{%endif%}
/>
<label
class="form-check-label"
for="{{ item.name }}-{{ v.value }}"
>
{{ v.label }}
</label>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
<!-- /Suggestions -->
{% endfor %}

上面的代码要引入

{%block script%}
<script src="/public/cms/{{'cms'|@templatePath}}/js/list.js"></script>
{%endblock%}

 

列表标签

列表标签只能在列表模版内使用

classify

当前栏目信息,在模版中直接可以 使用classify,比如分类标题 classify.title等,具体参数可以在模版中通过{{classify|dump}}打印查看

比如我们要查询当前栏目浏览最多的前5条内容

{%endif%}
{% set hotlist = 'cms_doc'|@findAll({
where:{
classify_id:classify.id
},
offset: 0,
limit: 5,
order:[['view','desc']]
})%}
{%if hotlist%}
<div class="d-none d-lg-block">
<h3 class="h5 mt-5 mt-0-xs">热点</h3>
<ul class="list-unstyled mb-0">
{%for item in hotlist%}
<li class="list-item d-flex align-items-center position-relative mt-4">
<div class="ratio ratio-16x9 bg-light me-3" style="max-width: 50px;">
<div class="bg-cover rounded" style="background-image:url({{item.cover_url}});"></div>
</div>
<a href="/cms/detail/{{item.id}}"
class="link-normal small stretched-link">{{item.title|truncate(20, true, "...")}}</a>
</li>
{%endfor%}
</ul>
</div>
{%endif%}

 

list

  • 返回{count:number,rows:[]}
  • count 列表总数量
  • rows 当页集合

list 是返回当前栏目的集合 包括该栏目下的子栏目,list 要配合 {{pagination|safe}} 分页标签一起使用

{%for item in list.rows%}
<!-- item -->
<div class="shadow-xs rounded mb-3 p-3">
<div class="row g-0">
<div class="col-12 col-md-3 p--0">
<figure class="overflow-hidden text-center clearfix d-block m-0 position-relative">
<a href="/cms/detail/{{item.id}}" class="text-decoration-none" target="_blank">
{%if not item.cover_url%}
<svg class="d-block w-100 fs-1 rounded" xmlns="http://www.w3.org/2000/svg"
role="img" style="text-anchor: middle; user-select: none">
<rect width="100%" height="100%" fill="#c6d3e6"></rect>
<text x="50%" y="50%" fill="#869ab8" dy=".3em">
暂无图片
</text>
</svg>
{%else%}
<img
class="img-fluid rounded lazy"
data-src="{{item.cover_url}}"
src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
alt="..."
/>
{%endif%}
</a>
</figure>
</div>
<div class="col-12 mt-3 d-block d-sm-none">
<!-- mobile spacer -->
</div>
<div class="col-12 col-md-9 order-xs-3 order-md-2 order-lg-2 p-0 position-relative">
{%if item.level>0%}
<span class="position-absolute top-0 end-0 mt-1" data-bs-toggle="tooltip" data-bs-placement="top" title="置顶[{{item.level}}]">
<svg width="15" viewBox="0 0 512 512" class="fill-danger"><path d="M141.938 324.965l-130.119 159.058c-3.387 4.146-3.517 10.24 0 14.535 4.002 4.918 11.247 5.646 16.147 1.625l159.107-130.131c-8.13-7.24-15.955-14.598-23.244-21.893-7.63-7.626-14.92-15.401-21.891-23.194zM447.89 64.1c-44.49-44.502-94.756-66.346-112.309-48.846-.903.908-92.757 126.197-138.796 188.98-30.166-21.5-57.219-34.082-74.656-34.068-6.374 0-11.485 1.662-14.971 5.18-19.197 19.164 16.646 86.076 80.028 149.459 51.797 51.797 105.971 85.223 134.488 85.223 6.387 0 11.472-1.68 14.971-5.178 13.084-13.084.548-48.41-28.858-89.658 62.802-46.043 188.047-137.893 188.932-138.781 17.535-17.518-4.339-67.804-48.829-112.311z"></path></svg>
</span>
{%endif%}
<div class="p-0 p-lg-3">
<h3 class="h5 mb-2">
<a href="/cms/detail/{{item.id}}" target="_blank"> {{item.title}} </a>
</h3>

<p class="mb-2">
{{item.description|truncate(80, true, "...")}}
</p>

<ul class="list-inline text-gray-700 small mb-2">
<li class="list-inline-item">{{item.pathTitle}}</li>
<li class="list-inline-item">/</li>
<li class="list-inline-item">
{{helper.moment(item.createdAt).fromNow()}}
</li>
<li class="list-inline-item">/</li>
<li class="list-inline-item">{{item.view}}次</li>
</ul>
</div>
</div>
</div>
</div>
<!-- /item -->
{%endfor%}
<!-- pagination -->
<nav aria-label="pagination" class="mt-4">{{pagination|safe}}</nav>
<!-- pagination -->

 

详情标签

详情标签只能在详情页模版使用

breadcrumb

面包屑

{%for item in breadcrumb%}
<li class="breadcrumb-item"><a href="{{item.url}}">{{item.name}}</a></li>
{%endfor%}

detail

详情内容,具体参数可以通过{{detail|dump}}打印查看

如果不同的文档类型公用一个详情模版,具体文档内容可以通过 detail.models_uuid 来判断,具体可以参考默认详情模版

  • 文档 'e86401ba-85cb-47f7-9f53-853e26b939bd'
  • 图片 '4e0da60c-13af-4965-8f35-e2b13742398e'
  • 下载 'aac4b5a3-89f2-4c41-9213-39f43dcc0860'
<h1 class="h3 mb-0">{{detail.title}}</h1>
<p class="d-block text-muted small">
发布时间: <time>{{helper.moment(detail.createdAt).format('YYYY-MM-DD HH:mm:ss')}}</time>
</p>
{%if detail.models_uuid==='e86401ba-85cb-47f7-9f53-853e26b939bd'%}
<!-- 文档内容 -->
{%elif detail.models_uuid==='4e0da60c-13af-4965-8f35-e2b13742398e'%}
<!-- 图片内容 -->
{%elif detail.models_uuid==='aac4b5a3-89f2-4c41-9213-39f43dcc0860'%}
<!-- 下载内容 -->
{%endif%}
<p class="text-muted border-bottom pb-2 small mt-5">
最后更新时间: {{helper.moment(detail.updatedAt).format('YYYY-MM-DD HH:mm:ss')}}
</p>
{%set tags = detail.tags|split(',')%}
{%if tags%}
...
{%endif%}

 引入评论功能

<!-- COMMENTS -->
{%if detail.cms_classify.reply%}
{% include "./comment_index.html" %}
{%endif%}
<!-- /COMMENTS -->

引入评论功能后要加入下面的js代码

{%block script%}
<script>
//评论
var cms_comments_callback = function(from,data){
if(data.status===1000){
$.SOW.core.toast.show('danger', 'Error', data.msg, 'top-end');
}else{
$.SOW.core.toast.show('success', 'Success', data.msg, 'top-end');
$(from).find('[name="content"]').empty()
$.SOW.core.ajax_content.__ajaxDivProcess('cms_comments_container', '/cms/comments/ajaxList?doc_id={{detail.id}}', '#cms_comments_container',$.SOW.core.ajax_content.config.method,null,$.SOW.core.ajax_content.config.callback_function,$.SOW.core.ajax_content.config.callback_before_push);
}
}
var cms_comments_reply_callback = function(from,data){
if(data.status===1000){
$.SOW.core.toast.show('danger', 'Error', data.msg, 'top-end');
}else{
$.SOW.core.toast.show('success', 'Success', data.msg, 'top-end');
$(from).find('[name="content"]').empty()
$('#sow_ajax_modal').modal('hide');
$.SOW.core.ajax_content.__ajaxDivProcess('cms_comments_container', '/cms/comments/ajaxList?doc_id={{detail.id}}&page='+data.data, '#cms_comments_container',$.SOW.core.ajax_content.config.method,null,$.SOW.core.ajax_content.config.callback_function,$.SOW.core.ajax_content.config.callback_before_push);
}
}
</script>
{%endblock%}

最后更新时间: 2022-12-06 15:00:00

评论