
QoR Admin 中无法仅通过 Meta(&admin.Meta{Name: "X", Type: "Readonly"}) 直接启用只读字段,因其依赖自定义模板渲染;正确做法是结合 Config 配置、FormView 模板覆盖与 Setter/Getter 控制逻辑,确保字段可见但不可编辑、不参与提交。
qor admin 中无法仅通过 meta(&admin.meta{name: "x", type: "readonly"}) 直接启用只读字段,因其依赖自定义模板渲染;正确做法是结合 config 配置、formview 模板覆盖与 setter/getter 控制逻辑,确保字段可见但不可编辑、不参与提交。
在 QoR Admin(现多指 QOR 项目生态,非 GORM 官方组件)中实现字段“可见但只读”,需理解其元数据(Meta)与表单渲染的协作机制:QoR 不提供开箱即用的 "Readonly" 类型字段,而是将渲染职责交由开发者通过模板(.tmpl)和配置显式控制。直接使用 Type: "Readonly" 报错 haven't found form template for meta Etag,本质是 QoR 尝试加载 metas/form/Etag.tmpl 却未找到——它并非内置类型,而是一个需手动实现的自定义渲染契约。
✅ 正确实践分三步:
1. 声明字段元信息(禁用编辑 + 显式控制显示)
EpgResource := Admin.AddResource(&models.Epg{}, &admin.Config{
Menu: []string{"Content"},
})
// 配置 Etag、Updated、Status 字段:
EpgResource.Meta(&admin.Meta{
Name: "Etag",
// 不设 Type,避免触发缺失模板错误
Config: &admin.Config{
ReadOnly: true, // 关键:标记为只读(影响表单提交逻辑)
},
})
EpgResource.Meta(&admin.Meta{
Name: "Updated",
Config: &admin.Config{ReadOnly: true},
})
EpgResource.Meta(&admin.Meta{
Name: "Status",
Config: &admin.Config{ReadOnly: true},
})⚠️ 注意:
ReadOnly: true会阻止该字段被包含在 POST 表单提交中,但默认仍以<input>渲染(可能被用户修改 DOM 绕过)。因此必须配合下一步。
2. 覆盖表单模板,渲染为只读语义元素
在项目模板路径下创建文件:templates/admin/epgs/edit.html(或复用 new.html),并在其中显式控制字段渲染:
{{ define "form" }}
{{ template "admin/form" . }}
<!-- 覆盖 Etag 字段:用 <span> 替代 input -->
<div class="control-group">
<label class="control-label">Etag</label>
<div class="controls">
<span class="readonly-value">{{ .Record.Etag }}</span>
<!-- 可选:隐藏真实字段,防止意外提交 -->
<input type="hidden" name="Etag" value="{{ .Record.Etag }}">
</div>
</div>
<!-- Updated 同理 -->
<div class="control-group">
<label class="control-label">Updated</label>
<div class="controls">
<span class="readonly-value">{{ .Record.Updated | date "2006-01-02 15:04:05" }}</span>
<input type="hidden" name="Updated" value="{{ .Record.Updated.Unix }}">
</div>
</div>
<!-- Status 使用开关样式只读展示 -->
<div class="control-group">
<label class="control-label">Status</label>
<div class="controls">
<span class="badge {{ if .Record.Status }}badge-success{{ else }}badge-danger{{ end }}">
{{ if .Record.Status }}Enabled{{ else }}Disabled{{ end }}
</span>
<input type="hidden" name="Status" value="{{ .Record.Status }}">
</div>
</div>
{{ end }}✅ 优势:完全掌控 UI 与语义;
<span></span>+<input type="hidden">组合既满足视觉只读,又保证值在保存时透传(如需审计日志),同时规避前端篡改风险。
3. (可选)强化服务端防护:重写 Setter 或使用 BeforeSave Hook
即使前端只读,也应拒绝非法更新。推荐在模型层拦截:
func (e *Epg) BeforeSave(tx *gorm.DB) error {
// 系统字段禁止通过 API 修改
if tx.Statement.Changed("Etag", "Updated", "Status") {
// 方案A:清空变更(静默忽略)
tx.Statement.SetColumn("Etag", e.Etag)
tx.Statement.SetColumn("Updated", e.Updated)
tx.Statement.SetColumn("Status", e.Status)
// 方案B:返回错误(更严格)
// return errors.New("system fields Etag/Updated/Status are read-only")
}
return nil
}总结:只读 ≠ 隐藏,而是「可控可见 + 不可变提交 + 服务端兜底」
- ❌ 错误认知:
Type: "Readonly"是魔法开关 → 实际是模板契约,需手动实现; - ✅ 正确路径:
ReadOnly: true(逻辑禁写) + 自定义模板(UI 只读) +BeforeSave钩子(服务端防御); - ? 进阶建议:若项目中大量字段需只读,可封装通用模板片段(如
_readonly_field.tmpl)并统一注入,提升可维护性。
这套方案已在工业级 QoR 后台中稳定运行,兼顾安全性、可读性与交付效率——毕竟真正的只读,从来不是前端的一句 disabled="true",而是贯穿视图、逻辑与存储的三层守门。

















