---
name: create-theme-template
description: 快速创建 ThinkCMF 主题模板文件的脚手架工具。支持生成后台 CRUD 页面模板（列表、添加、编辑）、前台页面模板、Widget 组件和可视化配置 JSON。当用户需要创建模板、页面、视图、主题文件时使用此技能。
---

# 快速创建 ThinkCMF 主题模板

## 工作流程

1. 确认模板类型（后台CRUD / 前台页面 / Widget组件）
2. 确认所属应用和模块名称
3. 生成对应模板文件
4. 如为前台页面，同步生成 .json 可视化配置

## 需求收集

| 参数 | 说明 | 示例 |
|------|------|------|
| 模板类型 | 后台列表/添加/编辑/前台页面/Widget | `admin_list` |
| 应用名 | 所属应用目录名 | `admin`、`portal`、`blog` |
| 模块名 | 控制器对应的目录名（小写下划线） | `link`、`article` |
| 数据变量名 | 模板中使用的数据变量 | `links`（列表）、`link`（单条） |
| 字段列表 | 需要展示/编辑的字段 | `name,url,status` |

## 模板存放规则

```
后台模板: public/themes/admin_default/{应用名}/{模块名}/{动作}.html
前台模板: public/themes/default/{应用名}/{动作}.html
前台JSON: public/themes/default/{应用名}/{动作}.json
Widget:   public/themes/default/public/widgets/{widget名}/widget.html
```

---

## 后台模板

### 通用继承方式

所有后台页面继承 `public@base`（Bootstrap 5），通过 `<block>` 填充内容：

```html
<extend name="public@base"/>
<block name="head"><!-- CSS/函数 --></block>
<block name="main"><!-- 主体内容 --></block>
<block name="scripts"><!-- JS脚本 --></block>
```

### 列表页模板

```html
<extend name="public@base"/>
<block name="main">
    <div class="wrap js-check-wrap">
        <!-- Tab 导航 -->
        <ul class="nav nav-underline">
            <li class="nav-item">
                <a class="nav-link active" href="{:url('{Controller}/index')}">{:lang('{列表标题}')}</a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="{:url('{Controller}/add')}">{:lang('{添加标题}')}</a>
            </li>
        </ul>

        <!-- 批量操作表单 -->
        <form method="post" class="js-ajax-form margin-top-20" action="{api_list_order_url}">
            <div class="table-actions">
                <button class="btn btn-sm btn-primary js-ajax-submit" type="submit">
                    <i class="fa fa-sort-numeric-asc fa-fw"></i> {:lang('SORT')}
                </button>
                <button class="btn btn-sm btn-danger js-ajax-submit" type="submit"
                        data-action="{delete_url}" data-subcheck="true">
                    <i class="fa fa-trash fa-fw"></i> {:lang('DELETE')}
                </button>
            </div>

            <!-- 数据表格 -->
            <table class="table table-hover table-bordered table-list">
                <thead>
                <tr>
                    <th width="16"><input type="checkbox" class="js-check-all" data-direction="x"></th>
                    <th width="50">{:lang('SORT')}</th>
                    <th width="50">ID</th>
                    <!-- {字段表头} -->
                    <th width="50">{:lang('STATUS')}</th>
                    <th width="120">{:lang('ACTION')}</th>
                </tr>
                </thead>
                <tbody>
                <foreach name="{items}" item="vo">
                    <tr>
                        <td><input type="checkbox" class="js-check" name="ids[]" value="{$vo.id}"></td>
                        <td><input name="list_orders[{$vo.id}]" class="input input-order" type="text" size="3" value="{$vo.list_order}"></td>
                        <td>{$vo.id}</td>
                        <!-- {字段值} -->
                        <td>
                            <if condition="$vo['status']==1">
                                <span class="badge text-bg-success">{:lang('DISPLAY')}</span>
                                <else/>
                                <span class="badge text-bg-secondary">{:lang('HIDDEN')}</span>
                            </if>
                        </td>
                        <td>
                            <a class="btn btn-xs btn-primary" href="{:url('{Controller}/edit',['id'=>$vo['id']])}" title="{:lang('EDIT')}">
                                <i class="fa fa-edit fa-fw"></i>
                            </a>
                            <a class="btn btn-xs btn-danger js-ajax-delete" href="javascript:;" data-href="{delete_item_url}/{$vo.id}" title="{:lang('DELETE')}">
                                <i class="fa fa-trash fa-fw"></i>
                            </a>
                        </td>
                    </tr>
                </foreach>
                </tbody>
            </table>
            <!-- 分页 -->
            <div>{$page|default=''}</div>
        </form>
    </div>
</block>
```

### 添加页模板

```html
<extend name="public@base"/>
<block name="main">
    <div class="wrap">
        <!-- Tab 导航 -->
        <ul class="nav nav-underline">
            <li class="nav-item">
                <a class="nav-link" href="{:url('{Controller}/index')}">{:lang('{列表标题}')}</a>
            </li>
            <li class="nav-item">
                <a class="nav-link active" href="{:url('{Controller}/add')}">{:lang('{添加标题}')}</a>
            </li>
        </ul>

        <!-- 表单 -->
        <form method="post" class="js-ajax-form margin-top-20" action="{api_create_url}">
            <!-- 表单字段（参考表单组件） -->

            <!-- 提交按钮 -->
            <div class="row mb-3">
                <div class="offset-sm-2 col-sm-10">
                    <button type="submit" class="btn btn-primary js-ajax-submit">
                        <i class="fa fa-save fa-fw"></i> {:lang('ADD')}
                    </button>
                    <a class="btn btn-secondary" href="javascript:history.back(-1);">{:lang('BACK')}</a>
                </div>
            </div>
        </form>
    </div>
</block>
```

### 编辑页模板

```html
<extend name="public@base"/>
<block name="main">
    <div class="wrap">
        <!-- Tab 导航 -->
        <ul class="nav nav-underline">
            <li class="nav-item">
                <a class="nav-link" href="{:url('{Controller}/index')}">{:lang('{列表标题}')}</a>
            </li>
            <li class="nav-item d-none d-sm-inline-block">
                <a class="nav-link" href="{:url('{Controller}/add')}">{:lang('{添加标题}')}</a>
            </li>
            <li class="nav-item">
                <a class="nav-link active">{:lang('{编辑标题}')}</a>
            </li>
        </ul>

        <!-- 表单（PUT 方法） -->
        <form method="put" class="js-ajax-form margin-top-20" action="{api_update_url}/{${data}.id}">
            <!-- 表单字段，value 用 {${data}.field} 预填 -->

            <!-- 提交按钮 -->
            <div class="row mb-3">
                <div class="offset-sm-2 col-sm-10">
                    <input type="hidden" name="id" value="{${data}.id}">
                    <button type="submit" class="btn btn-primary js-ajax-submit">
                        <i class="fa fa-save fa-fw"></i> {:lang('SAVE')}
                    </button>
                    <a class="btn btn-secondary" href="{:url('{Controller}/index')}">{:lang('BACK')}</a>
                </div>
            </div>
        </form>
    </div>
</block>
```

---

## 表单组件速查

### 文本输入

```html
<div class="row mb-3">
    <label for="input-{field}" class="col-sm-2 col-form-label text-sm-end">
        <span class="form-required">*</span>{标签}
    </label>
    <div class="col-md-8 col-sm-10">
        <input type="text" class="form-control" id="input-{field}" name="{field}" value="{${data}.{field}|default=''}">
    </div>
</div>
```

### 下拉选择

```html
<div class="row mb-3">
    <label for="input-{field}" class="col-sm-2 col-form-label text-sm-end">{标签}</label>
    <div class="col-md-8 col-sm-10">
        <select class="form-control" name="{field}" id="input-{field}">
            <foreach name="{options}" item="vo">
                <php>$selected=${data}['{field}']==$key?"selected='selected'":'';</php>
                <option value="{$key}" {$selected}>{$vo}</option>
            </foreach>
        </select>
    </div>
</div>
```

### 文本域

```html
<div class="row mb-3">
    <label for="input-{field}" class="col-sm-2 col-form-label text-sm-end">{标签}</label>
    <div class="col-md-8 col-sm-10">
        <textarea class="form-control" id="input-{field}" name="{field}" rows="5">{${data}.{field}|default=''}</textarea>
    </div>
</div>
```

### 图片上传

```html
<div class="row mb-3">
    <label for="input-{field}" class="col-sm-2 col-form-label text-sm-end">{标签}</label>
    <div class="col-md-8 col-sm-10">
        <div class="input-group">
            <input type="text" id="input-{field}" name="{field}" class="form-control" value="{${data}.{field}|default=''}">
            <span class="input-group-btn">
                <a class="btn btn-primary" href="javascript:uploadOneImage('{:lang('Upload Image')}','#input-{field}');">上传图片</a>
            </span>
        </div>
    </div>
</div>
```

### 状态切换（badge 显示）

```html
<if condition="$vo['status']==1">
    <span class="badge text-bg-success">{:lang('DISPLAY')}</span>
    <else/>
    <span class="badge text-bg-secondary">{:lang('HIDDEN')}</span>
</if>
```

---

## 前台模板

### 页面模板（继承 base.html）

```html
<extend name="public@base"/>
<block name="seo">
    <title>{$seo_title|default=''} - {$site_info.site_name|default=''}</title>
    <meta name="keywords" content="{$seo_keywords|default=''}"/>
    <meta name="description" content="{$seo_description|default=''}">
</block>
```

前台页面内容通过 Widget 动态填充，HTML 模板通常只需覆盖 SEO block。

### 页面 JSON 配置（可视化设计）

每个前台页面 .html 对应一个 .json 配置文件：

```json
{
    "name": "{页面名称}",
    "action": "{应用}/{控制器}/{方法}",
    "description": "{页面描述}",
    "order": 10.0,
    "more": {
        "vars": {},
        "widgets_blocks": {
            "top": {
                "title": "顶部",
                "widgets": {
                    "{page}_{widget}_{timestamp}": {
                        "title": "{Widget标题}",
                        "name": "{widget名}",
                        "display": "1",
                        "public_widget_id": "{全局ID}"
                    }
                }
            },
            "body": { "title": "主体", "widgets": {} },
            "body_left": { "title": "主体左", "widgets": {} },
            "body_right": { "title": "主体右", "widgets": {} },
            "footer": { "title": "底部", "widgets": {} }
        }
    }
}
```

**Widget 区块名称**对应 base.html 中的 `<widgetsBlock name="xxx"/>`。

---

## Widget 组件

### Widget 目录结构

```
public/themes/default/public/widgets/{widget_name}/
├── widget.html    # Widget HTML 模板
├── widget.json    # Widget 配置（可选）
├── preview.jpg    # 预览图（可选）
└── readme.md      # 说明（可选）
```

### Widget HTML 模板

```html
<widget tag="div" class="{自定义类名}">
    <div class="container">
        <!-- Widget 内容 -->
        <!-- 使用 {$widget.vars.xxx} 访问 Widget 变量 -->
    </div>
</widget>
```

### 常用 Widget 内容标签

```html
<!-- 导航 -->
<navigation nav-id="$_nav_id" root="" id="main-navigation" class="nav">
    <navigationMenu>单层菜单项</navigationMenu>
    <navigationFolder>下拉菜单项</navigationFolder>
</navigation>

<!-- 幻灯片 -->
<slides id="$top_slide_id">
    <li><img src="{:cmf_get_image_url($vo.image)}"></li>
</slides>
<noslides id="$top_slide_id">无数据时展示</noslides>

<!-- 友情链接 -->
<links>
    <a href="{$vo.url}" target="{$vo.target}">{$vo.name}</a>
</links>

<!-- 钩子 -->
<hook name="footer_start"/>
<hook name="before_content" param="hook_param"/>
```

---

## 模板语法速查

| 语法 | 说明 |
|------|------|
| `<extend name="public@base5"/>` | 继承父模板 |
| `<block name="main">...</block>` | 定义/覆盖区块 |
| `<include file="public@header"/>` | 包含模板片段 |
| `{$var}` | 输出变量 |
| `{$var\|default=''}` | 带默认值输出 |
| `{:url('Ctrl/act')}` | 生成 URL |
| `{:lang('KEY')}` | 国际化文本 |
| `{:cmf_get_image_url($path)}` | 图片 URL 转换 |
| `<foreach name="list" item="vo">` | 循环 |
| `<volist name="list" id="vo">` | 循环（简洁版） |
| `<if condition="$a==1">...<else/>...</if>` | 条件判断 |
| `<empty name="var">...</empty>` | 空值判断 |
| `<notempty name="var">...</notempty>` | 非空判断 |
| `<php>...</php>` | 内嵌 PHP 代码 |
| `<hook name="hook_name"/>` | 钩子占位 |
| `<widgetsBlock tag="div" name="xxx"/>` | Widget 区块 |
| `<widgetsHead/>` | Widget CSS 注入点 |
| `<widgetsScript/>` | Widget JS 注入点 |
| `<widget tag="div">...</widget>` | Widget 容器 |

## JS 交互约定

| CSS 类名 | 功能 |
|---------|------|
| `js-ajax-form` | 表单 AJAX 提交 |
| `js-ajax-submit` | 提交按钮绑定 |
| `js-ajax-delete` | 删除确认弹窗 |
| `js-check-wrap` | 全选容器 |
| `js-check-all` | 全选复选框 |
| `js-check` | 单行复选框 |

## 检查清单

- [ ] 后台模板继承 `public@base5`
- [ ] 前台模板继承 `public@base`
- [ ] 表单使用 `js-ajax-form` 类
- [ ] 列表页包含全选、排序、批量操作
- [ ] 编辑页表单 method 为 `put`，包含隐藏 id 字段
- [ ] 前台页面同步创建 .json 配置文件
- [ ] Widget 用 `<widget>` 标签包裹
- [ ] 必填字段标注红色星号 `form-required`
- [ ] assign 变量使用小写+下划线命名
