ZhiCms 模板开发文档
ZhiCms 系统内置 双模板引擎,二者对外接口一致,可在后台「系统设置 → 模板引擎」随时切换:
| 引擎 | 标识 | 说明 |
|---|---|---|
| ZhiCms 自研引擎(默认) | legacy |
ZhiCms\base\Template,ZhiCms 自研原生标签语法 |
| ThinkTemplate | think |
ZhiCms\base\ThinkTemplate,基于真·ThinkPHP 模板引擎(think-template),语法与 ThinkPHP 一致 |
警告:当前主题模板按默认引擎(ZhiCms 自研引擎)语法编写。请勿随意切换引擎,否则前端很可能崩溃(空白页/语法错误/样式错乱)。仅当已用新引擎语法重写全部模板时才可切换。
两种引擎都通过统一的 assign() / display() 接口工作,因此同一套控制器代码无需改动,只需保证模板语法与当前引擎兼容。
切换引擎后请务必到「系统 → 缓存管理」清除模板编译缓存(或删除
data/cache/tpl、data/cache/tpl_compile目录),否则可能命中旧编译结果。
1. 模板目录结构
前台模板位于 app/{模块}/view/{控制器}/ 下,默认模块为 index。
app/index/view/
├── public/ # 公共片段(可被任意页面 include)
│ ├── header.html # 页头:<head> + 顶部导航 + 头部公共变量
│ ├── footer.html # 页脚:友情链接 + 版权
│ ├── sidebar.html # 侧边栏:用户信息 / 登录 / 排行榜等
│ └── pagebar.html # 分页条
├── index/
│ ├── index.html # 首页
│ ├── list.html # 列表页
│ └── view.html # 文章详情
├── brand/ cheaps/ rank/ search/ m/ ucenter/ ... # 各功能页面
- 模板文件默认后缀为
.html(由data/config/global.php中TPL.TPL_SUFFIX控制)。 - 路径约定:
display()为空时自动解析为app/{APP_NAME}/view/{controller}/{action}.html。 - 控制器可通过
$this->display('app/index/view/xxx/yyy')指定任意模板(推荐绝对路径,见「控制器渲染」)。
1.1 一个标准页面的骨架
几乎所有前台页面都以 header / footer 公共片段开始与结束:
{include file="app/index/view/public/header"}
<!-- 页面主体内容 -->
<div class="container">
...
</div>
{include file="app/index/view/public/footer"}
header.html 负责输出 <!DOCTYPE html>、<head>(含 SEO 标题/关键词/描述)、顶部导航与全局 CSS/JS;footer.html 负责输出页脚、友情链接、版权与公共 JS。
2. 控制器与模板的交互
2.1 变量赋值 assign()
public function index(){
$this->assign('newsList', $list); // 数组
$this->assign('title', '示例标题'); // 标量
$this->display(); // 渲染默认模板
}
assign($name, $value) 把数据注入模板,模板中直接以 $name 使用。
2.2 渲染模板 display()
$this->display(); // 默认:app/index/view/{controller}/{action}.html
$this->display('app/index/view/index/index'); // 指定模板(推荐绝对路径,避免 TPL_PATH 为空时报错)
$this->display('', false, false); // $isTpl=false 时 $tpl 为模板字符串
display() 会自动把控制器所有 public 属性($this->xxx)一并注入模板。因此前台控制器最常见的写法是直接给属性赋值,无需逐个 assign():
public function view(){
$this->view = $data; // 详情数据
$this->pageTitle = '标题'; // SEO 标题
$this->canonicalUrl = url(...);
$this->display();
}
2.3 布局(layout)
控制器设置 $this->layout 后,页面内容会套用到该布局中:
public $layout = 'app/index/view/public/layout'; // 所有输出都进入该布局
ThinkTemplate 引擎还额外支持
{extend name="base"}...{block}...{/block}继承语法。
2.4 控制器输出数据到模板(完整流程)
一个「从数据库取数 → 赋值 → 模板渲染」的典型控制器方法:
public function list(){
$where = array("1"); // 查询条件(数组元素会以 AND 拼接)
$where[] = "`status` = '1'"; // 追加条件
// 分页:第 1 参每页条数,返回 ['list'=>..,'count'=>..,'page'=>html]
$page = obj("api/ApiData")->page("10", "yun_article", $where, "`id` DESC", $baseUrl);
$this->page = $page; // 模板里用 $page['list'] 遍历,$page['page'] 输出分页
$this->pageTitle = '列表页 - ' . obj('base/Base')->SiteConfig('sitename');
$this->pageKeywords = '关键词';
$this->display();
}
模板端对应输出:
{foreach $page['list'] as $item}
<a href="{$item['url']}">{$item['title']}</a>
<span>{$item['addtime']|date_format}</span>
{/foreach}
{$page['page']} <!-- 分页条 -->
3. 双引擎通用能力
以下语法在 ZhiCms 自研引擎 与 ThinkTemplate 两种引擎下都可用,是编写「可同时兼容两套引擎」模板的基础。
3.1 变量输出
{$name} <!-- 输出 $name -->
{$user.name} <!-- 输出 $user['name'] -->
{$user['name']} <!-- 数组下标写法,两引擎都支持 -->
{$list[0].title} <!-- 多维访问 -->
3.2 判断(if / elseif / else)
{if $status == 1}
启用
{elseif $status == 2}
暂停
{else}
未知
{/if}
常用判断写法:{if $x == 1}、{if $x != ''}、{if $x > 3}、{if !empty($arr)}、{if isset($a) && $a == 1}。
3.3 循环 foreach
{foreach $list as $item}
<a href="{$item.url}">{$item.title}</a>
{/foreach}
{foreach $map as $key => $val}
{$key}: {$val}
{/foreach}
ZhiCms 自研引擎会在循环内自动维护
$n计数器($n从 1 开始);ThinkTemplate 同样兼容{foreach}。空数组时foreach不会执行,不会报错。
3.4 引入公共片段 include
{include file="app/index/view/public/header"}
{include file="app/index/view/public/footer"}
{include file="app/index/view/index/list_item"} <!-- 可复用列表项 -->
3.5 函数调用输出
{obj('base/Base')->SiteConfig('sitename')} <!-- 站点名 -->
{obj('base/Base')->SEO('index_title')} <!-- SEO 标题 -->
{date('Y-m-d H:i:s', $item['addtime'])} <!-- 时间格式化 -->
{count($list)} <!-- 统计数量 -->
{url('index/index/index')} <!-- 生成路由 URL -->
说明:自研引擎把
{函数(...)}编译为<?php echo 函数(...);?>;ThinkTemplate 中这类「函数直出」建议使用{:func()}写法(见 5.2)。
3.6 isset / empty 防空
为避免变量未定义告警,判断前先用 isset / empty 防护:
{if isset($pageTitle) && $pageTitle != ''}{$pageTitle}{else}默认标题{/if}
{if !empty($banners)} ...轮播... {/if}
4. ZhiCms 自研引擎(默认)
直接通过正则把 {...} 编译成 PHP,输出默认原样、不转义。
4.1 支持的标签
| 语法 | 编译结果 | 说明 |
|---|---|---|
{$var} |
<?php echo $var;?> |
变量输出 |
{$a.b.c} |
<?php echo $a['b']['c'];?> |
点语法访问多维数组 |
{CONSTANT} |
<?php echo CONSTANT;?> |
常量输出(如 {CONTROLLER_NAME}、{ACTION_NAME}、{APP_NAME}、{__PUBLIC__}) |
{if ...}{elseif ...}{else}{/if} |
<?php if... ?>... |
条件判断 |
{foreach $arr as $v} / {foreach $arr as $k => $v} |
<?php foreach... ?> |
循环 |
{for $i=0;$i<10;$i++} / {/for} |
<?php for... ?> |
for 循环 |
{php ...} |
<?php ... ?> |
原生 PHP 块 |
{函数(...)} |
<?php echo 函数(...);?> |
函数输出 |
{include file="..."} |
<?php $__Template->display("...");?> |
引入片段 |
4.2 常见坑
- 不要在页面文案里写裸的
{中文}/{标签}/{示例}:会被引擎当成常量解析,报Undefined constant "xxx"导致整页中断。如需在页面展示模板标签示例,请用 HTML 实体:{={,}=}。 - 输出用户提交的内容(评论、文章等)请自行
htmlspecialchars($x, ENT_QUOTES),因为自研引擎默认不转义。
5. ThinkTemplate(真·ThinkPHP 引擎)
语法与 ThinkPHP 模板引擎一致,额外支持更丰富的写法。
5.1 变量与函数
- 变量输出同样默认原样(本系统把
default_filter设为tpl_raw,不自动转义,与自研引擎保持一致)。 - 函数 / 常量输出用
{:}:{:date('Y-m-d')} <!-- 函数输出 --> {:strtoupper($name)} <!-- 函数带参 --> {__PUBLIC__} <!-- 常量输出 -->
5.2 模板继承
{extend name="app/index/view/public/base"}
{block name="content"}页面内容{/block}
5.3 更多标签
支持 {include file="..."}、{volist}、{switch}、{compare} 等 ThinkPHP 标签。
编写可同时兼容两套引擎的模板:尽量只用第 3 章的通用语法;如需函数直出,用
{:func()}(自研引擎也能正确输出),避免在文案中出现裸大括号。
6. 常用框架对象与数据
模板中常用 obj() 全局助手获取业务数据:
| 表达式 | 说明 |
|---|---|
{obj('base/Base')->SiteConfig('sitename')} |
站点名称 |
{obj('base/Base')->SiteConfig('hosturl')} |
站点域名 |
{obj('base/Base')->SiteConfig('sitekeywords')} |
关键词 |
{obj('base/Base')->SiteConfig('sitedescription')} |
站点描述 |
{obj('base/Base')->SEO('index_title')} |
SEO 标题 |
{obj('base/Base')->SEO('index_dec')} |
SEO 描述 |
{url('index/index/index')} |
生成路由 URL |
{obj('api/ApiData')->...} |
数据查询(一般由控制器封装后赋值) |
控制器 assign() 注入的变量在模板中直接以 $变量名 使用。
7. 常用模板变量(控制器注入)
以下变量由各控制器在渲染前注入,模板可直接使用:
- 站点/SEO:见
obj('base/Base')->SiteConfig(...)系列。 - SEO:
$pageTitle、$pageKeywords、$pageDescription、$canonicalUrl、$ogImage。 - 列表页:
$list/$pageList、$pagebar(分页 HTML)、$page(分页数组,含page字段)。 - 详情页:
$info/$item/$article、$prev、$next。 - 通用:
$loginUser(当前登录用户)、$categories(分类)、$navs(导航)、$banners(幻灯/轮播)。
具体变量名以各控制器
assign()注入为准;开发时可先在同页面控制器源码中查看注入了哪些变量。
7.1 分页输出
分页条已由框架生成 HTML,直接输出即可:
{if isset($page) && isset($page['page'])}
<div class="pagination" style="margin-top:24px;">{$page['page']}</div>
{/if}
7.2 URL 生成
用 {url(...)} 生成带路由规则的地址,避免硬编码:
<a href="{url('index/index/view', ['id' => $item['id']])}">{$item['title']}</a>
8. 数据库操作(控制器取数 / 增改删)
控制器通过 obj("api/ApiData")(即 app\api\model\ApiDataModel)访问数据库。表名可带前缀 {pre}(自动替换为 yun_)或直接用真实表名 yun_xxx。
所有表名前缀统一为
yun_(如文章表yun_article、用户表yun_user、评论表yun_comment、轮播表yun_huan、友链表yun_link)。插件自定义表通常也用yun_前缀建表。
8.1 查询(查)
单条记录(无 $order 时内部走 find(),返回一维数组):
$row = obj("api/ApiData")->dataSelect("yun_article", array("`id` = 123"));
// $row = ['id'=>123, 'title'=>'...', 'content'=>'...']
多条记录(传 $order 走 select(),返回二维数组):
$list = obj("api/ApiData")->dataSelect("yun_article", array("`status` = '1'"), "`id` DESC LIMIT 0, 10");
// $list = [ ['id'=>..,'title'=>..], ... ]
原生 SQL 查询(可带占位参数防注入):
$rows = obj("api/ApiData")->thisQuery(
"SELECT c.*, u.username FROM `{pre}comment` c LEFT JOIN `{pre}user` u ON u.id = c.uid WHERE c.mid = ? ORDER BY c.id DESC LIMIT 6",
array($id)
);
计数:
$count = obj("api/ApiData")->dataCount("yun_comment", array("`mid` = 1 AND `hide` = 'n'"));
流式查询构造器(table()->where()->order()->limit()->select()):
$rows = obj("api/ApiData")->table("yun_article", true)
->where(array("`status` = '1'"))
->order("`id` DESC")
->limit("0, 20")
->select();
8.2 新增(增)
$newId = obj("api/ApiData")->insertData("yun_my_table", array(
'title' => $title,
'content' => $content,
'addtime' => time(),
));
// $newId 为自增主键 id
批量插入(性能更高,一条多值 INSERT):
$dataList = array(
array('title' => 'a', 'content' => '...'),
array('title' => 'b', 'content' => '...'),
);
$affected = obj("api/ApiData")->insertAllData("yun_my_table", $dataList);
8.3 更新(改)
obj("api/ApiData")->dataUpdate("yun_my_table", array(
'title' => $newTitle,
'hits' => $hits + 1,
), array("`id` = $id"));
原生更新(支持参数化):
obj("api/ApiData")->executeQuery(
"UPDATE `{pre}article` SET `hits` = `hits` + 1 WHERE `id` = ?",
array($id)
);
8.4 删除(删)
// 简单按 id 删除(自动参数化)
obj("api/ApiData")->deleteThis("yun_my_table", "`id` = 123");
// 复杂条件删除(推荐参数化)
obj("api/ApiData")->deleteThis("yun_my_table", "`uid` = ? AND `model` = ?", array($uid, 'article'));
// 原生删除
obj("api/ApiData")->executeQuery("DELETE FROM `{pre}my_table` WHERE `id` = ?", array($id));
8.5 控制器中「取数 → 加工 → 赋值给模板」完整示例
public function detail(){
$id = (int)$this->arg('id'); // 接收 GET/POST 参数并转整型
// 1) 查文章
$view = obj("api/ApiData")->dataSelect("yun_article", array("`id` = $id"));
if (empty($view)) { $this->alert('文章不存在'); }
// 2) 加工:补字段 / 触发钩子让插件改写
$view['cateName'] = \app\base\controller\BaseController::getNavName($view['navid'] ?? 0);
\ZhiCms\base\Hook::listen('article_view', array(&$view));
// 3) 赋值(public 属性会被 display() 自动注入)
$this->view = $view;
$this->pageTitle = $view['title'] . ' - ' . obj('base/Base')->SiteConfig('sitename');
$this->canonicalUrl = url('index/index/view/id=<id>', array('id' => $id));
$this->display();
}
8.6 前端提交数据的写入(增删改示例)
配合前端表单,控制器接收 $_POST(用 $this->arg() / $this->Postarg() 安全取值)后写入数据库:
public function add(){
if (!$this->isPost()) { $this->alert('非法请求'); }
$title = trim($this->Postarg('title')); // 安全取 POST,已做 htmlspecialchars 过滤
$content = $_POST['content']; // 富文本内容一般用原始值
obj("api/ApiData")->insertData("yun_article", array(
'title' => $title,
'content' => $content,
'addtime' => time(),
));
echo json_encode(array('info' => '添加成功', 'status' => 'y'));
}
arg():合并$_GET+$_POST,会剥离<script>等并做htmlspecialchars;Postarg()只取 POST。需要原始值(如富文本/JSON)时直接用$_POST['xxx']。
9. 静态资源引入
建议在 public/web/ 下组织 CSS/JS,模板中通过 {__PUBLIC__} 常量引用:
<link rel="stylesheet" href="{__PUBLIC__}web/css/blog.css?v=5.0.2">
<script src="{__PUBLIC__}web/js/common.js?v=5.0.2"></script>
?v=版本号用于缓存刷新,发布模板改动时请更新版本号。- CSS 优先在
<head>引入(避免 FOUC,利于 SEO);JS 建议在</body>前引入(非阻塞渲染)。 - 页面特有样式可内联
<style>,但不要重复定义与公共 CSS 相同的类,以免样式冲突。
10. 明暗主题适配
系统支持 light/dark 双主题(<html data-theme="light|dark">),主题变量定义在 public/web/css/blog.css / common.css:
- 用 CSS 变量(
var(--fontColor)、var(--conBgcolor)、var(--aColor)等)替代硬编码颜色。 - 不要在模板内联
style中写死深色背景上的浅色文字,否则 dark 模式下会看不清。 - 组件卡片背景用
var(--conBgcolor),正文用var(--fontColor),链接用var(--aColor)。 - 移动端用
@media (max-width: 767px)适配(侧栏折叠、卡片纵向排列等),公共样式已内置大部分适配,页面特有部分自行补充。
11. 常见问题排查
| 现象 | 原因 / 处理 |
|---|---|
Template file ... not found |
模板路径不对;display() 建议用绝对路径 app/index/view/xxx,且 data/config/global.php 中 TPL_PATH 为空时不能依赖相对路径 |
Undefined constant "xxx" |
自研引擎把 {中文} / {标签} 当常量解析;将文案中的裸大括号转义为实体 |
| 页面空白 / 无输出 | 模板编译缓存旧版,删除 data/cache/tpl* 后重试 |
{:func()} 不生效 |
当前为自研引擎;自研引擎用 {func()} 写法 |
| 切换引擎后样式错乱 | 引擎不同导致标签渲染差异,核对模板中使用了哪些语法,统一为通用语法 |
| 变量未定义告警 | 用 isset / empty 防空 |
页面显示 HTML 实体 { |
这是转义后的模板标签示例,属正常;如需真的 {,用 { 即可 |
| 背景 / 文字在 dark 下看不清 | 模板内联写死了深色文字;改用 var(--fontColor) 等主题变量 |
本文档基于 ZhiCms 双模板引擎实现编写,覆盖模板目录、页面骨架、变量注入、双引擎语法差异、公共片段、分页/URL、静态资源、明暗主题与常见问题等完整开发要点。
12. 插件模板化(模板与插件协作)
当插件是「模板化插件」(plugin.json 的 type=template + rewrite,见 PLUGIN_DEV.do 第 7 章)时,它的前台页由插件自己的 view/ 目录下的模板渲染,而不是 app/index/view/。模板编写的异同如下。
12.1 模板位置与引擎
| 项 | 前台主站模板 | 模板化插件模板 |
|---|---|---|
| 目录 | app/index/view/ |
plugins/{alias}/view/ |
| 渲染者 | 前台控制器 display() |
插件控制器基类 display()(ZhiCms\base\ThinkTemplate) |
| 默认引擎 | 后台「模板引擎」可切 legacy/think | 默认 think-template(与 guangdiu/kiees 一致,建议保持) |
| 变量来源 | 控制器 assign() / public 属性 |
插件控制器 assign() + display() 里统一注入的站点变量 |
插件模板语法遵循 think-template 引擎(见第 3 章通用语法 + 第 5 章),可用
{foreach}/{$x.y}/{include}/{:func()}。
12.2 插件模板里可用的变量
插件控制器 display() 通常会统一注入以下变量(以 guangdiu 为参考,具体以你的控制器为准):
| 变量 | 说明 |
|---|---|
{$site_name} / {$site_logo} / {$copyright} / {$beian} |
站点名称 / Logo / 版权 / 备案(来自 SiteConfig) |
{$host_url} |
站点域名 |
{$plug_static} |
插件静态资源根目录(/plugins/{alias}/static) |
{$plug_url} |
插件首页链接(伪静态或动态) |
{$plug_base} |
插件链接前缀(如 plug-guangdiu) |
{$is_mobile} |
是否移动端(0/1) |
{$show_sidebar} |
是否显示侧栏(移动端常 0) |
{$nav_index} / {$nav_cheaps} / {$nav_brand} / {$nav_rank} |
各频道链接 |
{$hot} / {$list} / {$article} / {$page} |
业务数据(控制器 assign 注入) |
12.3 在插件模板里引用站点配置
<title>{$site_name}</title>
<footer>© {$copyright} {$beian}</footer>
<link rel="stylesheet" href="{$plug_static}/css/style.css?v=1.0.0">
12.4 插件模板 include 自己的片段
{include file="plugins/guangdiu/view/header"}
{include file="plugins/guangdiu/view/sidebar"}
...页面主体...
{include file="plugins/guangdiu/view/footer"}
路径要写成插件根目录绝对路径(
plugins/guangdiu/view/header),不要用主站app/index/view/public/header,否则会混入主站布局(主站 header 含BaseController注入的变量,插件页没有)。
12.5 插件模板里的链接生成
模板已注入 {$plug_url},拼详情/频道链接:
<a href="{$plug_url}-{$item.id}.html">{$item.title}</a> <!-- 详情 -->
<a href="{$nav_cheaps}">优惠券</a> <!-- 频道 -->
若需动态生成(如翻页),可在模板里用:
<a href="{:plugins\guangdiu\controller\SiteController::pageUrlHref(['page'=>$page['next']])}">下一页</a>
更推荐:控制器把分页 HTML 算好(如
$this->assign('pagebar', $html))直接输出{$pagebar}。
12.6 插件模板的缓存清理
改了插件 view/ 下模板后,若前台有模板编译缓存或静态缓存命中旧版,请清理:
runtime/cache/tpl*与runtime/static_cache/- 插件若有自己的
view/data/cache/tpl_compile/,也一并删除
否则可能看到旧版模板。
12.7 把插件设为整站首页
后台「系统设置 → 站点设置 → 主页展示插件」选择该模板化插件后,访问 / 会渲染插件首页(URL 不变)。插件无需任何改动,框架 App::applyHomePlug() 自动把首页路由转给 PlugController::view() → Plugin::displayPage()。前提:插件已启用、type=template、rewrite 含 plug-<alias>.html。*