VVCMS5 Logo
首页 / 开发文档 / 主题开发
主题开发

主题开发:渲染变量与数据约束(ViewModel 约定)

解释模板渲染变量、字段约束与兼容策略:缺字段/空列表/分页等。

2026-03-04 106

ViewModel(渲染变量)与数据约束(主题开发约定)

本文从 aithemes.md 提炼:主题模板(Go template)可用的渲染变量(usecase.ViewModel)以及与之相关的数据形态/约束,供主题开发时直接对照使用。


1. 顶层渲染对象:.(usecase.ViewModel)

所有页面渲染时注入的顶层数据结构是 usecase.ViewModel,模板中用 . 访问。

1.1 字段清单(模板可用)

  • Uri当前请求 uri
  • Site(*conf.SiteOption)站点全局配置
  • ThemesExt(map[string]any)主题扩展数据(来自主题的 vvdata.json)
  • Type(string)页面类型常见:home、category、page、article、search、tag、user、product、not_found 等
  • Title / Description / Keywords页面 TDK(有兜底)
  • List([]*ContentCardVM)列表数据(首页/分类/搜索/标签/用户等)
  • Pager(PagerVM)分页数据
  • Columns([]*models.Column)栏目列表(常用于导航 nav)
  • Nav([]*models.Column)面包屑栏目链
  • Single(*ColumnVM)单页数据(也可在分类/详情页作为“当前栏目完整信息”使用,见下)
  • Content(*ContentCardVM)文章/内容详情
  • CurrentColumn(*models.Column)当前栏目信息(导航高亮、上下文)
  • Request(*RequestData)请求数据(例如模板可读 Request.Query.key)
  • Prev / Next(*ContentCardVM)上一篇/下一篇(仅详情页可能存在)

1.2 通用约束(必须遵守)

  • 模板里不要假设字段必定非空:Site / Single / Content / Prev / Next / Request 都可能为空使用前先 {{if ...}} 做空判断,避免 nil 访问
  • 字段名大小写严格按 struct:ContentCardVM 的链接字段是 URL(全大写),不是 .Url
  • 访问 map 推荐写法:{{ index .ThemesExt "key" }}{{ index .Content.Ext "key" }}不存在 key 会得到 <no value>,建议配合 if 判断


2. 页面类型(.Type)与数据形态

.Type 用来区分页面数据形态,主题模板务必按它做分支,避免在错误页面读取错误字段。

2.1 首页(Home)

  • 触发:Uri == "/"
  • 通常:.Type == "home".List + .Pager 有意义轮播/推荐等组件数据通常通过模板函数读取(与 .List 无关)

2.2 分类页(Category:承载列表数据)

  • 触发:/{colSlug} 且栏目不是跳转/外链/单页模式
  • 通常:.Type == "category".CurrentColumn 存当前栏目(用于导航高亮).List + .Pager 为主要列表数据.Single 可能存在,用作栏目完整信息(栏目 banner/SEO/扩展等)

2.3 详情页(Detail:文章/产品等内容详情)

  • 触发:/{colSlug}/{postSlug}
  • 通常:.Type == "article".Content 为详情内容.Prev / .Next 可能存在

2.4 单页(Page:栏目自身内容页)

  • 触发:/{colSlug} 且栏目渲染模式为单页(如 RenderMode=2)
  • 通常:.Type == "page".Single 为单页栏目数据正文通常读:{{.Single.Content|Str2Html}}

2.5 搜索/标签/用户列表等(复用列表形态)

  • 通常复用 category.html 的列表结构:.Type 可能为 search / tag / user可能没有 .Single(因为不是栏目页),模板需要 {{if .Single}}...{{end}} 兼容


3. 主题扩展数据:ThemesExt(vvdata.json)

  • 来源:app1/themes/<theme_name>/vvdata.json
  • 注入:模板 ViewModel 的 ThemesExt(map[string]any)

3.1 数据约束

  • vvdata.json 必须是合法 JSON 对象({}),否则会被当作空对象处理

3.2 模板访问方式

  • 整体:{{ .ThemesExt }}
  • 取 key:{{ index .ThemesExt "key" }}


4. 内容与栏目扩展数据(Ext):约束与用法

4.1 ContentCardVM.Ext(内容扩展值)

  • ContentCardVM.Ext:内容的扩展数据(map)

模板读取示例:

  • 详情页:

{{ $v := index .Content.Ext "age" }} {{ if $v }}<p class="sub">{{$v}}</p>{{ end }}

  • 列表页:

{{ range .List }} {{ $v := index .Ext "age" }} {{ if $v }}<span>{{$v}}</span>{{ end }} {{ end }}

4.2 ColumnVM.Ext(栏目扩展)

  • ColumnVM.Ext:栏目 ext 的 map(模板侧扩展点)
  • 在模板侧常见访问:{{ index .Single.Ext "xxx" }}

语义约束:

  • 单页(.Type == "page")时:.Single.Ext 的语义是 kv(键值对),用于主题展示定制(开关/颜色/布局/SEO 辅助字段等)
  • 文章/商品等栏目:栏目 ext 也可以承载“表单 schema”(见 4.3)

4.3 栏目 ext 作为“表单元数据(schema)”的约定

适用场景:栏目类型是“文章/商品”等需要自定义字段的内容模型时,把字段定义放在栏目 ext 里。

  • 栏目 ext:存字段定义(schema)
  • 内容 ext:存字段值(value)

推荐 Column.ext JSON 结构:

{ "fields": [ { "defaultValue": "", "key": "age", "label": "abc", "options": "", "placeholder": "", "required": false, "type": "input" }, { "defaultValue": "", "key": "name", "label": "asdg", "options": "", "placeholder": "", "required": false, "type": "input" } ] }

强约束(映射关系):

  • 栏目 ext.fields[i].key 约定为内容 Content.ext 的 key
  • 模板读取 ContentCardVM.Ext 时,key 必须与栏目 schema 的 key 一致


5. 常用子结构(供模板对照)

本节是便于主题模板对照的“最常用字段”,以 aithemes.md 中贴出的结构为准。

5.1 RequestData(模板常用:Request.Query)

  • Request.Query:map[string]string

模板示例(搜索框回填):

value="{{.Request.Query.key}}"

5.2 PagerVM(分页)

模板侧建议:

  • 优先遍历 .Pager.Items 渲染
  • Kind == "gap" 时不要输出链接
  • Disabled 时输出不可点样式

5.3 ContentCardVM(列表卡片 + 详情内容)

模板侧注意:

  • 正文一般需要:{{.Content.Content|Str2Html}}
  • 作者展示推荐:{{default .Content.Author .Content.User.Nickname}}
  • 链接字段是 URL(不是 Url)

5.4 ColumnVM(.Single:栏目完整数据别名)

  • .Single 是 *ColumnVM,内嵌了 *models.Column
  • 模板可直接访问:{{.Single.Name}}、{{.Single.Slug}}、{{.Single.Description}}扩展:{{index .Single.Ext "xxx"}}


6. 主题开发渲染侧“严格遵守”清单

  • 不要杜撰不存在的变量/方法;以 ViewModel 字段与 FuncMap 为准
  • ContentCardVM 的链接字段使用 .URL(全大写),不是 .Url
  • 建议对 Site 等对象做空判断再访问,避免渲染 panic
  • map 取值使用 index,并配合 if 判断避免 <no value> 直接输出