Files
go-admin/template/v4/vue.go.template
T
zhangwenjian 2642d0959d fix🐛: leave the form out of a generated page with nothing to enter
A table whose columns are all its key and ones the framework fills in
got an add button and an edit link opening an empty form. The page now
leaves out both, the dialog, useForm and the API calls only the form
used. A page with anything to enter is generated byte for byte as
before.
2026-09-27 16:07:10 +08:00

417 lines
19 KiB
Plaintext

{{- /*
Vue 3 + Element Plus + TypeScript list page (PRD 010, F4).
Shape matches go-admin-ui/src/views/demo/product/index.vue, the reference
page AGENTS.md names: PageContainer + ProTable + useTable/useForm/useRemove,
<script setup lang="ts">. The old template produced slot-scope/.sync/.native
syntax that Vue 3 removed outright (PRD 010 G1) -- this is not a patch on
that file, it is a different template for a different framework version.
Every label goes through $t('gen.{PackageName}.{BusinessName}.{JsonField}'),
never a literal ColumnComment -- see src/lang/{locale}/gen/index.ts (F9) for
how that namespace is loaded. This is also why the file must not contain a
literal CJK character anywhere, comments included: D10's acceptance check is
a bare regex scan of the rendered output with no exception for "but this one
is a comment", so a Chinese aside here would fail the same test a stray
placeholder="{{"{{"}}.ColumnComment{{"}}"}}" would.
HtmlType has seven stored values (PRD 010 G8) and only four render here on
purpose: checkbox and datetime became selectable in the F7 front-end change
(editTable.vue), so they get a branch; file stays disabled there, but a row
imported or edited before that change can still carry "file" or any other
value this template does not know -- the final branch below renders those,
and anything else future work introduces, as a plain input rather than
emitting nothing (PRD 010 phase-3 constraint #1: a silently empty field is
worse than a plain one).
*/ -}}
{{- $package := .PackageName -}}
{{- $business := .BusinessName -}}
{{- /*
Whether any column needs a given import, computed once by walking .Columns
rather than at each usage site -- text/template has no way to ask "did the
loop below already import this", so the alternative is repeating the same
import line once per matching column. "$var = value" (not ":=") reassigns an
outer-scope variable from inside a range -- a text/template feature since
Go 1.11, needed here because a range body cannot otherwise leave a mark on
anything outside itself.
Each condition below must match, term for term, the condition guarding the
markup or script that actually consumes the import -- not just "this column
has a DictType/FkTableName", which is necessary but not sufficient. A column
can carry dictionary or foreign-key metadata that no rendered branch reads:
FkTableName/DictType lose to each other by priority (FK wins search, list
and the form's select branch; the form's radio branch never looks at FK at
all), and a column can carry either one while being neither queryable nor
listed nor an insertable select/radio -- created_at/updated_at are exactly
this: sys_tables.go assigns HtmlType "datetime" to any timestamp/datetime
column on import whether or not it ever reaches IsList, because GetList's
audit-column exclusion is a separate, later step. Get a term here wrong in
either direction and either an import goes unused (no-unused-vars) or a real
usage silently loses its import (a ReferenceError this template cannot see
coming, since Vue components are the last stage that runs).
*/ -}}
{{- $hasDict := false -}}
{{- $hasDictList := false -}}
{{- $hasFk := false -}}
{{- $hasDatetime := false -}}
{{- $hasRules := false -}}
{{- $hasQuery := false -}}
{{- $hasForm := false -}}
{{- $pkType := "number" -}}
{{- range .Columns -}}
{{- $dictUsed := and (ne .DictType "") (or (and (eq .IsQuery "1") (eq .FkTableName "")) (and (eq .IsList "1") (eq .FkTableName "")) (and (eq .IsInsert "1") (eq .HtmlType "select") (eq .FkTableName "")) (and (eq .IsInsert "1") (eq .HtmlType "radio"))) -}}
{{- $fkUsed := and (ne .FkTableName "") (or (eq .IsQuery "1") (eq .IsList "1") (and (eq .IsInsert "1") (eq .HtmlType "select"))) -}}
{{- if $dictUsed }}{{$hasDict = true}}{{end -}}
{{- if and (eq .IsList "1") (eq .FkTableName "") (ne .DictType "") }}{{$hasDictList = true}}{{end -}}
{{- if $fkUsed }}{{$hasFk = true}}{{end -}}
{{- if and (eq .IsList "1") (eq .FkTableName "") (eq .DictType "") (eq .HtmlType "datetime") }}{{$hasDatetime = true}}{{end -}}
{{- if eq .IsQuery "1" }}{{$hasQuery = true}}{{end -}}
{{- if and (eq .IsInsert "1") (eq .IsRequired "1") (not .Pk) (ne .GoField "CreatedAt") (ne .GoField "UpdatedAt") (ne .GoField "DeletedAt") (ne .GoField "UpdateBy") (ne .GoField "CreateBy") }}{{$hasRules = true}}{{end -}}
{{- if and .Pk (eq .GoType "string") }}{{$pkType = "string"}}{{$hasRules = true}}{{$hasForm = true}}{{end -}}
{{- if and (eq .IsInsert "1") (not .Pk) (ne .GoField "CreatedAt") (ne .GoField "UpdatedAt") (ne .GoField "DeletedAt") (ne .GoField "UpdateBy") (ne .GoField "CreateBy") }}{{$hasForm = true}}{{end -}}
{{- end -}}
{{- /*
$hasForm: the create/edit dialog would hold at least one field - a typed-in
string key or an insertable column the framework does not fill in. Without
one, the add button, the edit link and the dialog are left out, not rendered
around an empty form that saves nothing the user chose.
*/ -}}
<template>
<PageContainer>
<ProTable :table="table" selection row-key="{{.PkJsonField}}">
{{- if $hasQuery}}
<template #search>
{{- range .Columns}}
{{- if eq .IsQuery "1"}}
{{- $key := printf "gen.%s.%s.%s" $package $business .JsonField}}
<el-form-item :label="$t('{{$key}}')">
{{- if ne .FkTableName ""}}
<el-select v-model="table.query.{{.JsonField}}" clearable :placeholder="$t('common.selectPlaceholder')">
<el-option
v-for="item in {{.JsonField}}FkOptions"
:key="item.{{.FkLabelId}}"
:label="item.{{.FkLabelName}}"
:value="item.{{.FkLabelId}}"
/>
</el-select>
{{- else if ne .DictType ""}}
<el-select v-model="table.query.{{.JsonField}}" clearable :placeholder="$t('common.selectPlaceholder')">
<el-option
v-for="dict in {{.JsonField}}DictOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
{{- else if eq .HtmlType "datetime"}}
<el-date-picker
v-model="table.query.{{.JsonField}}"
type="datetime"
value-format="YYYY-MM-DD[T]HH:mm:ssZ"
clearable
/>
{{- else}}
<el-input v-model="table.query.{{.JsonField}}" clearable />
{{- end}}
</el-form-item>
{{- end}}
{{- end}}
</template>
{{- end}}
<template #toolbar>
{{- if $hasForm}}
<el-button v-permisaction="['{{.PackageName}}:{{.BusinessName}}:add']" type="primary" @click="form.openCreate()">
{{ "{{" }} $t('common.add') {{ "}}" }}
</el-button>
{{- end}}
<el-button
v-permisaction="['{{.PackageName}}:{{.BusinessName}}:remove']"
type="danger"
plain
:disabled="table.multiple"
@click="remove(table.selectedIds)"
>
{{ "{{" }} $t('common.delete') {{ "}}" }}
</el-button>
</template>
{{- range .Columns}}
{{- if eq .IsList "1"}}
{{- $key := printf "gen.%s.%s.%s" $package $business .JsonField}}
{{- if ne .FkTableName ""}}
<el-table-column :label="$t('{{$key}}')" min-width="{{if .ColWidth}}{{.ColWidth}}{{else}}120{{end}}" show-overflow-tooltip>
<template #default="{ row }">{{ "{{" }} {{.JsonField}}Label(row.{{.JsonField}}) {{ "}}" }}</template>
</el-table-column>
{{- else if ne .DictType ""}}
<el-table-column :label="$t('{{$key}}')" min-width="{{if .ColWidth}}{{.ColWidth}}{{else}}120{{end}}">
<template #default="{ row }">{{ "{{" }} dictLabel({{.JsonField}}DictOptions, row.{{.JsonField}}) {{ "}}" }}</template>
</el-table-column>
{{- else if eq .HtmlType "datetime"}}
<el-table-column :label="$t('{{$key}}')" min-width="{{if .ColWidth}}{{.ColWidth}}{{else}}110{{end}}">
<template #default="{ row }"><DateCell :value="row.{{.JsonField}}" /></template>
</el-table-column>
{{- else}}
<el-table-column
:label="$t('{{$key}}')"
prop="{{.JsonField}}"
min-width="{{if .ColWidth}}{{.ColWidth}}{{else}}120{{end}}"
show-overflow-tooltip
/>
{{- end}}
{{- end}}
{{- end}}
<template #actions="{ row }">
{{- if $hasForm}}
<el-button v-permisaction="['{{.PackageName}}:{{.BusinessName}}:edit']" link type="primary" @click="form.openEdit(row)">
{{ "{{" }} $t('common.edit') {{ "}}" }}
</el-button>
{{- end}}
<el-button v-permisaction="['{{.PackageName}}:{{.BusinessName}}:remove']" link type="danger" @click="remove(row.{{.PkJsonField}})">
{{ "{{" }} $t('common.delete') {{ "}}" }}
</el-button>
</template>
</ProTable>
{{- if $hasForm}}
<el-dialog
v-model="form.visible"
:title="form.title"
width="500px"
:close-on-click-modal="false"
@closed="form.reset"
>
<el-form
:ref="form.bindFormRef"
v-loading="form.loading"
:model="form.model"
{{- if $hasRules}}
:rules="form.rules"
{{- end}}
label-width="100px"
>
{{- /*
A key the database assigns never appears in the form. A string key
does not assign itself, so it is typed in on create and fixed once
the record exists - form.isEdit is true only for a form openEdit
opened, which needs go-admin-ui's useForm to record the mode rather
than infer it from the key.
*/}}
{{- if eq $pkType "string"}}
{{- $key := printf "gen.%s.%s.%s" $package $business .PkJsonField}}
<el-form-item :label="$t('{{$key}}')" prop="{{.PkJsonField}}">
<el-input v-model="form.model.{{.PkJsonField}}" :disabled="form.isEdit" />
</el-form-item>
{{- end}}
{{- range .Columns}}
{{- if and (eq .IsInsert "1") (not .Pk) (ne .GoField "CreatedAt") (ne .GoField "UpdatedAt") (ne .GoField "DeletedAt") (ne .GoField "UpdateBy") (ne .GoField "CreateBy")}}
{{- $key := printf "gen.%s.%s.%s" $package $business .JsonField}}
<el-form-item :label="$t('{{$key}}')" prop="{{.JsonField}}">
{{- if eq .HtmlType "select"}}
{{- if ne .FkTableName ""}}
<el-select v-model="form.model.{{.JsonField}}" :placeholder="$t('common.selectPlaceholder')">
<el-option
v-for="item in {{.JsonField}}FkOptions"
:key="item.{{.FkLabelId}}"
:label="item.{{.FkLabelName}}"
:value="item.{{.FkLabelId}}"
/>
</el-select>
{{- else if ne .DictType ""}}
<el-select v-model="form.model.{{.JsonField}}" :placeholder="$t('common.selectPlaceholder')">
<el-option
v-for="dict in {{.JsonField}}DictOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
{{- else}}
<el-input v-model="form.model.{{.JsonField}}" />
{{- end}}
{{- else if eq .HtmlType "radio"}}
{{- if ne .DictType ""}}
<el-radio-group v-model="form.model.{{.JsonField}}">
<el-radio v-for="dict in {{.JsonField}}DictOptions" :key="dict.value" :value="dict.value">
{{ "{{" }} dict.label {{ "}}" }}
</el-radio>
</el-radio-group>
{{- else}}
<el-input v-model="form.model.{{.JsonField}}" />
{{- end}}
{{- else if eq .HtmlType "checkbox"}}
<el-checkbox v-model="form.model.{{.JsonField}}" true-value="1" false-value="0" />
{{- else if eq .HtmlType "datetime"}}
<el-date-picker
v-model="form.model.{{.JsonField}}"
type="datetime"
value-format="YYYY-MM-DD[T]HH:mm:ssZ"
/>
{{- else if eq .HtmlType "textarea"}}
<el-input v-model="form.model.{{.JsonField}}" type="textarea" :rows="2" />
{{- else}}
<el-input v-model="form.model.{{.JsonField}}" />
{{- end}}
</el-form-item>
{{- end}}
{{- end}}
</el-form>
<template #footer>
<el-button @click="form.close">{{ "{{" }} $t('common.dialogCancel') {{ "}}" }}</el-button>
<el-button type="primary" :loading="form.submitting" @click="form.submit">
{{ "{{" }} $t('common.dialogConfirm') {{ "}}" }}
</el-button>
</template>
</el-dialog>
{{- end}}
</PageContainer>
</template>
<script setup lang="ts">
{{- if $hasRules}}
import { computed } from 'vue'
{{- end}}
{{- if $hasFk}}
import { ref, onMounted } from 'vue'
{{- end}}
{{- if $hasRules}}
import { useI18n } from 'vue-i18n'
import type { FormRules } from 'element-plus'
{{- end}}
import PageContainer from '@/components/PageContainer/index.vue'
import ProTable from '@/components/ProTable/index.vue'
{{- if $hasDatetime}}
import DateCell from '@/components/DateCell/index.vue'
{{- end}}
import { useTable{{if $hasForm}}, useForm{{end}}, useRemove{{if $hasDict}}, useDict{{if $hasDictList}}, dictLabel{{end}}{{end}} } from '@/composables'
{{- if $hasForm}}
import {
add{{.ClassName}}, del{{.ClassName}}, get{{.ClassName}}, list{{.ClassName}}, update{{.ClassName}}
} from '@/api/{{.PackageName}}/{{.MLTBName}}'
{{- else}}
import { del{{.ClassName}}, list{{.ClassName}} } from '@/api/{{.PackageName}}/{{.MLTBName}}'
{{- end}}
import type { {{.ClassName}}, {{.ClassName}}Query } from '@/api/{{.PackageName}}/{{.MLTBName}}'
{{- /*
Two columns pointing at the same foreign table must not import it twice --
"one FK-configured column" was never the same thing as "one distinct target
table", and gen.go has no concept of a table's FK targets being unique.
text/template has no set to check membership in, so the dedup is a nested
range: a column only imports its target if no earlier, equally-used column
already claimed the same FkTableNameClass. $fkUsed is repeated here (it also
guards the const declarations above) because a column with FkTableName set
but reaching none of them -- unqueried, unlisted, not an insert select --
has nothing that would use the import either.
*/ -}}
{{- range $i, $col := .Columns}}
{{- $fkUsed := and (ne $col.FkTableName "") (or (eq $col.IsQuery "1") (eq $col.IsList "1") (and (eq $col.IsInsert "1") (eq $col.HtmlType "select"))) -}}
{{- if $fkUsed}}
{{- $alreadyImported := false -}}
{{- range $j, $prior := $.Columns}}
{{- if lt $j $i}}
{{- $priorUsed := and (ne $prior.FkTableName "") (or (eq $prior.IsQuery "1") (eq $prior.IsList "1") (and (eq $prior.IsInsert "1") (eq $prior.HtmlType "select"))) -}}
{{- if and $priorUsed (eq $prior.FkTableNameClass $col.FkTableNameClass) }}{{$alreadyImported = true}}{{end -}}
{{- end}}
{{- end}}
{{- if not $alreadyImported}}
import { list{{$col.FkTableNameClass}} } from '@/api/{{$package}}/{{$col.FkTableNamePackage}}'
import type { {{$col.FkTableNameClass}} } from '@/api/{{$package}}/{{$col.FkTableNamePackage}}'
{{- end}}
{{- end}}
{{- end}}
{{- /*
Manage suffix, not just ClassName: this must match gen.go's
Cmenu.MenuName = tab.ClassName + "Manage" byte for byte (PRD 010 R4), or
keep-alive's include list -- built from menu_name -- never matches this
component's name and the page never caches. The old template wrote
name: '{ClassName}' with no suffix; the mismatch went unnoticed because
stores/permission.ts's loadView() rewrites the rendered component's name to
menu_name at runtime regardless of what defineOptions said (PRD 010 G7).
That fallback stays in place after this change -- it is not this template's
to remove -- but the value declared here should be right regardless of it.
*/}}
defineOptions({ name: '{{.ClassName}}Manage' })
{{- range .Columns}}
{{- $dictUsed := and (ne .DictType "") (or (and (eq .IsQuery "1") (eq .FkTableName "")) (and (eq .IsList "1") (eq .FkTableName "")) (and (eq .IsInsert "1") (eq .HtmlType "select") (eq .FkTableName "")) (and (eq .IsInsert "1") (eq .HtmlType "radio"))) -}}
{{- $fkUsed := and (ne .FkTableName "") (or (eq .IsQuery "1") (eq .IsList "1") (and (eq .IsInsert "1") (eq .HtmlType "select"))) -}}
{{- if $dictUsed}}
const { {{.DictType}}: {{.JsonField}}DictOptions } = useDict('{{.DictType}}')
{{- end}}
{{- if $fkUsed}}
const {{.JsonField}}FkOptions = ref<{{.FkTableNameClass}}[]>([])
onMounted(async() => {
const res = await list{{.FkTableNameClass}}({ pageIndex: 1, pageSize: 100 })
{{.JsonField}}FkOptions.value = res.data?.list ?? []
})
{{- if eq .IsList "1"}}
const {{.JsonField}}Label = (value: unknown) =>
{{.JsonField}}FkOptions.value.find(item => item.{{.FkLabelId}} === value)?.{{.FkLabelName}} ?? value
{{- end}}
{{- end}}
{{- end}}
{{- /*
Every object literal below is built on one line, joined with ", " through a
$first flag rather than one field per line with a trailing comma after each:
comma-dangle is "never" (no comma before the closing brace) and comma-style
is "last" (a comma may not open a line), and text/template has no arithmetic
to compute "is this the last matching column" up front -- knowing that would
be what a one-field-per-line, trailing-comma-free rendering needs instead.
*/}}
const table = useTable<{{.ClassName}}, {{.ClassName}}Query>({
api: list{{.ClassName}},
idKey: '{{.PkJsonField}}'
{{- if $hasQuery}},
defaultQuery: () => ({{"{"}} {{$qFirst := true}}{{range .Columns}}{{if eq .IsQuery "1"}}{{if $qFirst}}{{$qFirst = false}}{{else}}, {{end}}{{.JsonField}}: undefined{{end}}{{end}} {{"}"}})
{{- end}}
})
{{- if $hasRules}}
const { t } = useI18n()
{{- /*
Built from the same field-label key rather than a dedicated
gen.{pkg}.{biz}.rules.{field} key: R3 derives one key per field from
PackageName+BusinessName+JsonField, and a second, validation-only key per
required field would double the language pack's surface for a message that
reads fine as the field name alone in the space Element Plus renders it --
directly under the labelled field it failed to validate.
*/}}
const rules = computed<FormRules>(() => ({ {{$rFirst := true}}
{{- if eq $pkType "string"}}{{$rFirst = false}}{{.PkJsonField}}: [{ required: true, message: t('{{printf "gen.%s.%s.%s" $package $business .PkJsonField}}'), trigger: 'blur' }]
{{- end}}
{{- range .Columns}}
{{- if and (eq .IsInsert "1") (eq .IsRequired "1") (not .Pk) (ne .GoField "CreatedAt") (ne .GoField "UpdatedAt") (ne .GoField "DeletedAt") (ne .GoField "UpdateBy") (ne .GoField "CreateBy")}}
{{- $key := printf "gen.%s.%s.%s" $package $business .JsonField}}
{{- if $rFirst}}{{$rFirst = false}}{{else}},
{{end}}{{.JsonField}}: [{ required: true, message: t('{{$key}}'), trigger: '{{if or (eq .HtmlType "select") (eq .HtmlType "radio") (eq .HtmlType "datetime") (eq .HtmlType "checkbox")}}change{{else}}blur{{end}}' }]
{{- end}}
{{- end}}
}))
{{- end}}
{{- if $hasForm}}
const form = useForm<{{.ClassName}}, {{$pkType}}>({
defaultModel: () => ({{"{"}} {{.PkJsonField}}: undefined{{range .Columns}}{{if and (eq .IsInsert "1") (not .Pk) (ne .GoField "CreatedAt") (ne .GoField "UpdatedAt") (ne .GoField "DeletedAt") (ne .GoField "UpdateBy") (ne .GoField "CreateBy")}}, {{.JsonField}}: {{if eq .DefaultValue ""}}undefined{{else if eq .GoType "int"}}{{.DefaultValue}}{{else}}'{{js .DefaultValue}}'{{end}}{{end}}{{end}} {{"}"}}),
idKey: '{{.PkJsonField}}',
{{- if $hasRules}}
rules,
{{- end}}
api: { get: get{{.ClassName}}, add: add{{.ClassName}}, update: update{{.ClassName}} },
onSuccess: () => table.getList()
})
{{- end}}
const { remove } = useRemove({
api: del{{.ClassName}},
onSuccess: () => table.getList()
})
</script>