From 3625ce851b11f7bd70f24158521917593b3be198 Mon Sep 17 00:00:00 2001 From: zhangwenjian Date: Sat, 19 Sep 2026 20:36:24 +0800 Subject: [PATCH 1/6] =?UTF-8?q?fix=F0=9F=90=9B:=20empty=20Query=20interfac?= =?UTF-8?q?e=20for=20zero-IsQuery=20tables=20trips=20no-empty-object-type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A plain display table with no search form (zero columns marked IsQuery=1) is a normal shape, not an edge case - but ts.go.template's Query interface only had a body when the range over .Columns found a match, so it rendered `export interface {ClassName}Query {}`, which @typescript-eslint/no-empty-object-type flags and pnpm lint fails on. Falls back to `export type {ClassName}Query = Record` when no column qualifies - the same answer go-admin-ui's own useTable.ts already gives this shape (`TQuery extends object = Record`), so it intersects with PageQuery the same way an empty interface would have and callers do not need to special-case a query-less table. Audited the rest of this template for the same "zero of some optional feature" shape while in here, since this is the third time a query-less/ select-less/whatever-less table has slipped through (dictLabel, DateCell, this one): - a zero-column table: cannot occur - a table without at least a primary key column cannot exist to be imported from information_schema in the first place, so .Columns is never empty here. - a table with only its primary key column: the row interface still has one property (the pk); not empty, no lint issue. - a table where every column has IsInsert=0: does not affect this template - add{Class}/update{Class} both take the full row interface unconditionally (every column, not just IsInsert ones), so there is no column-count-dependent shape to go empty here. Verified with node 24.11.0 (not the machine default): generated a zero-IsQuery table and a with-IsQuery table, copied both into go-admin-ui and ran eslint + vue-tsc --noEmit. Confirmed red first - `export interface VerifyNoQueryQuery {}` failed eslint with exactly the no-empty-object-type error. Restored the fix - clean on both, plus a throwaway call site instantiating useTable to prove the type satisfies useTable's `TQuery extends object` constraint, not just that it parses in isolation. --- template/v4/ts.go.template | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/template/v4/ts.go.template b/template/v4/ts.go.template index 7b1ed598..4387eff1 100644 --- a/template/v4/ts.go.template +++ b/template/v4/ts.go.template @@ -8,8 +8,10 @@ TS compile error at the call site, not a runtime bug. */ -}} {{- $pkType := "number" -}} +{{- $hasQuery := false -}} {{- range .Columns -}} {{- if and .Pk (eq .GoType "string") }}{{$pkType = "string"}}{{end -}} + {{- if eq .IsQuery "1" }}{{$hasQuery = true}}{{end -}} {{- end -}} import request from '@/utils/request' import type { ApiResponse, PageQuery, PageResult, Id } from '@/types/api' @@ -30,6 +32,7 @@ export interface {{.ClassName}} { {{- end}} } +{{if $hasQuery -}} export interface {{.ClassName}}Query { {{- range .Columns}} {{- if eq .IsQuery "1"}} @@ -47,6 +50,21 @@ export interface {{.ClassName}}Query { {{- end}} {{- end}} } +{{- else -}} +{{- /* + No column is marked IsQuery - a plain display table with no search form + is a normal shape, not an edge case, so this still has to produce a type + useTable/list{ClassName}(query: Query & PageQuery) can use. + `export interface {ClassName}Query {}` is what naturally falls out of the + range above finding nothing to iterate, but an empty interface trips + @typescript-eslint/no-empty-object-type and fails pnpm lint. + Record is go-admin-ui's own answer to the same shape - + see composables/useTable.ts's `TQuery extends object = Record` + - and it intersects with PageQuery the same way an empty interface would + have, so callers do not have to special-case a query-less table. +*/ -}} +export type {{.ClassName}}Query = Record +{{- end}} export function list{{.ClassName}}(query: {{.ClassName}}Query & PageQuery) { return request>>({ From 05661e2f3e055db7211bd92912812a19388366e7 Mon Sep 17 00:00:00 2001 From: zhangwenjian Date: Sat, 19 Sep 2026 20:37:01 +0800 Subject: [PATCH 2/6] =?UTF-8?q?fix=F0=9F=90=9B:=20jsonField=20format=20che?= =?UTF-8?q?ck=20relaxed=20to=20any=20legal=20identifier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jsonFieldPattern copied businessName's rule (^[a-z][A-Za-z]+$: at least two letters, no digits) on the theory that jsonField should tighten to the same identifier shape. That does not hold: businessName is typed by a person on genInfoForm.vue, so a strict pattern is a reasonable guardrail on human input. jsonField is computed by the importer from the column name (sys_tables.go's namelist/JsonField loop) - nobody types it, so the same pattern only rejected names the importer legitimately produces: a single-letter column ("x") or one whose last segment ends in a digit ("address2", "a1") both collapse to a single camelCase word with nothing left to re-capitalize, and both failed the old check. The blast radius is wider than "this one column can't be edited": validateAndSanitizeColumns runs over every column on every Update, so a table that merely contains one such column could not save any config change at all, including edits with nothing to do with that column. Relaxed to ^[a-z][A-Za-z0-9]*$ - any legal JS/TS identifier starting with a lowercase letter. Still rejects what has to be rejected: empty, whitespace/punctuation, and leading-digit names, since those cannot be unquoted object keys in the generated interface/lang file at all. Uniqueness and the expression-content check on defaultValue are unchanged - defaultValue is genuinely user-typed (F6's config page), so tightening it was the right call to begin with; this was the only place a human-input rule had been copied onto machine-generated data. Verified against the real import path, not hand-typed jsonField values: built a table with columns id/x/address2/a1 in a fake information_schema, ran it through the real SysTable.Insert, confirmed the importer computes exactly jsonField x/address2/a1, then submitted an update through the real SysTable.Update changing only tableComment (nothing about those columns). Confirmed red first - 500, "jsonField 格式不合法:\"x\"" - a change unrelated to any of the three columns was rejected solely because they existed on the table. Restored the fix - 200, "修改成功". --- app/other/apis/tools/sys_tables_validate.go | 30 +++++++++++++++---- .../apis/tools/sys_tables_validate_test.go | 12 ++++++-- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/app/other/apis/tools/sys_tables_validate.go b/app/other/apis/tools/sys_tables_validate.go index c9353b32..067f6aaa 100644 --- a/app/other/apis/tools/sys_tables_validate.go +++ b/app/other/apis/tools/sys_tables_validate.go @@ -10,11 +10,31 @@ import ( "go-admin/app/other/models/tools" ) -// jsonFieldPattern mirrors genInfoForm.vue's businessName rule -// (`/^[a-z][A-Za-z]+$/`) - jsonField has never had a format rule of its own, -// unlike businessName/tableName/className, and API契约.md §1.2 recommends -// tightening it to the same identifier shape the other three already use. -var jsonFieldPattern = regexp.MustCompile(`^[a-z][A-Za-z]+$`) +// jsonFieldPattern accepts any legal JS/TS identifier that starts with a +// lowercase letter - not businessName's rule. +// +// This used to be businessName's own pattern (^[a-z][A-Za-z]+$, requiring at +// least two letters and no digits), copied over on the theory that jsonField +// "should tighten to the same identifier shape". That theory does not hold: +// businessName is typed by a person on genInfoForm.vue, so a strict pattern +// is a reasonable guardrail on human input. jsonField is computed by the +// importer from the column name (sys_tables.go's namelist/JsonField loop) - +// nobody types it, so the same pattern only rejects names the importer +// legitimately produces. A one-letter column ("x") or a column ending in a +// digit ("address2", "a1") both import to a single camelCase word with no +// separators to re-capitalize, and both used to fail this check - meaning a +// table that merely contained such a column could never save any config +// again, unrelated columns included, since this check runs over every +// column on every Update. +// +// What still has to be rejected is a jsonField that cannot be a raw object +// key at all: empty, containing whitespace/punctuation, or leading with a +// digit (`2faEnabled: 1` is not valid JS - identifiers cannot start with a +// digit, and this is what lands as the property name in gen.go's generated +// interface / lang file, both unquoted). Hence still anchoring on a +// lowercase letter first, but no longer requiring a second character or +// forbidding digits after it. +var jsonFieldPattern = regexp.MustCompile(`^[a-z][A-Za-z0-9]*$`) // colWidthMin/colWidthMax are API契约.md §2.1's suggested range for colWidth. const ( diff --git a/app/other/apis/tools/sys_tables_validate_test.go b/app/other/apis/tools/sys_tables_validate_test.go index f2a31c77..0d80643e 100644 --- a/app/other/apis/tools/sys_tables_validate_test.go +++ b/app/other/apis/tools/sys_tables_validate_test.go @@ -17,9 +17,17 @@ func TestValidateAndSanitizeColumns_JsonFieldFormat(t *testing.T) { }{ {"lower camelCase", "userName", false}, {"two-letter lowercase", "id", false}, + // The importer's own output (sys_tables.go's namelist/JsonField + // loop), not made up: a single-letter column ("x"), and a column + // whose last name segment ends in a digit ("address2", "a1") both + // produce a jsonField with no separator left to re-capitalize. + // These three used to be rejected - the whole point of this fix. + {"single letter, real importer output for a column named x", "x", false}, + {"letters then a trailing digit, real importer output for address2", "address2", false}, + {"two letters then a digit, real importer output for a1", "a1", false}, {"leading underscore rejected", "_id", true}, - {"leading digit rejected", "1name", true}, - {"snake_case rejected", "user_name", true}, + {"leading digit rejected (not a legal identifier start)", "1name", true}, + {"snake_case rejected (importer never emits an underscore)", "user_name", true}, {"dot rejected, would break the gen/{pkg}/{biz}.ts key path", "user.name", true}, {"empty rejected", "", true}, } From 630e13686c65258e86cde9d9835638d5b4d877c6 Mon Sep 17 00:00:00 2001 From: zhangwenjian Date: Sat, 19 Sep 2026 20:40:29 +0800 Subject: [PATCH 3/6] =?UTF-8?q?fix=F0=9F=90=9B:=20gate=20optional=20import?= =?UTF-8?q?s=20on=20where=20they're=20actually=20used,=20not=20on=20raw=20?= =?UTF-8?q?metadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every $hasX flag controlling an optional import matched "this column carries the metadata" rather than "some rendered branch actually reads it" -- necessary but not sufficient, since FkTableName/DictType lose to each other by priority (FK wins search, list and the form's select branch; the form's radio branch never checks FK at all) and a column can carry either while being neither queryable, listed, nor an insertable select/radio. $hasDatetime was the reachable case integration testing found: sys_tables.go assigns HtmlType "datetime" to any timestamp/datetime column on import regardless of IsList, because GetList's audit-column exclusion is a separate, later step editTable.vue never surfaces created_at/updated_at through anyway. Nearly every real table has both, so nearly every table imported DateCell without using it. $hasFk and $hasDict had the identical shape one level down: the per-column ref/onMounted/useDict declarations were gated on "this column has FkTableName/DictType", not on whether the column reaches a branch that reads the resulting Options ref -- an FK column used only via search (no IsList, no insert-select) still declared a Label function nothing calls, and a dict column used only in an insert radio (no IsQuery, no IsList) still would have, had the two flags controlling its import stayed as wide as the per-column check they were meant to gate. Rewrote both to a shared $dictUsed/$fkUsed condition, matching each consuming branch's own guard term for term, and split the FK block's Label function under its own IsList check -- Options can be needed for search or the form's select without List ever being true. $hasDictList already had this shape from the previous fix and needed no change. Verified with two new fixtures, rendered through the real template.Execute and checked against a throwaway go-admin-ui worktree (deleted afterwards) with hand-written API-module stubs: a bare table carrying only the standard created_at/updated_at pair -- confirmed red on DateCell before this change, green after -- and a table exercising every optional import through a path distinct from the ones the two earlier verification rounds covered (a dict column read only from an insert radio, an FK column read only from search, and a business datetime column that IS listed, so DateCell still has to import when the real thing needs it). Re-ran the three fixtures from the previous two rounds alongside these two; all five stayed green. pnpm type-check and pnpm lint both zero error, on Node 24.11.0. --- template/v4/vue.go.template | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/template/v4/vue.go.template b/template/v4/vue.go.template index 72da9422..503fdc2a 100644 --- a/template/v4/vue.go.template +++ b/template/v4/vue.go.template @@ -34,6 +34,21 @@ 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 -}} @@ -43,10 +58,12 @@ {{- $hasQuery := false -}} {{- $pkType := "number" -}} {{- range .Columns -}} - {{- if ne .DictType "" }}{{$hasDict = true}}{{end -}} + {{- $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 ne .FkTableName "" }}{{$hasFk = true}}{{end -}} - {{- if eq .HtmlType "datetime" }}{{$hasDatetime = 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"}}{{end -}} @@ -276,21 +293,25 @@ import type { {{.FkTableNameClass}} } from '@/api/{{$package}}/{{.FkTableNamePac defineOptions({ name: '{{.ClassName}}Manage' }) {{- range .Columns}} -{{- if ne .DictType ""}} +{{- $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}}Options } = useDict('{{.DictType}}') {{- end}} -{{- if ne .FkTableName ""}} +{{- if $fkUsed}} const {{.JsonField}}Options = ref<{{.FkTableNameClass}}[]>([]) onMounted(async() => { const res = await list{{.FkTableNameClass}}({ pageIndex: 1, pageSize: 100 }) {{.JsonField}}Options.value = res.data?.list ?? [] }) +{{- if eq .IsList "1"}} const {{.JsonField}}Label = (value: unknown) => {{.JsonField}}Options.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: From 7a52a5096408d23c699215d07dd3f6c94c8ec7c9 Mon Sep 17 00:00:00 2001 From: zhangwenjian Date: Sat, 19 Sep 2026 20:48:58 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix=F0=9F=90=9B:=20dedupe=20FK=20imports=20?= =?UTF-8?q?by=20target=20table=20and=20filter=20by=20actual=20use?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two columns pointing at the same foreign table -- owner and approver both selecting from the same users table, say -- each triggered their own `import { listX } from ...` / `import type { X } from ...` line, which is a duplicate ES module import once both fire: TS2300. Nothing here ever asked whether a target table had already been imported by an earlier column, because nothing tracked target tables at all -- only source columns, and "one FK-configured column" was never the same thing as "one distinct target table". The same import was also gated on the column having FkTableName set, not on $fkUsed -- the condition the previous fix already applies to the const declarations that read the import. A column carrying FK metadata but reaching no query, list or insert-select branch imported a module nothing in the file references. 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 recomputed for both the outer and the inner column rather than factored out, since text/template has no way to carry a per-column value computed in one range into a second, later range over the same data. Verified with a fixture carrying three columns pointing at the same target table -- one read only from search, one only from the list, one from neither -- rendered through the real template.Execute and checked against a throwaway go-admin-ui worktree (deleted afterwards): before this change, TS2300 fired six times (the function and the type, three times over); after, exactly one import of each, and the unused third column contributes neither. Re-ran the previous rounds' fixtures alongside it; all stayed green. pnpm type-check and pnpm lint both zero error, on Node 24.11.0. --- template/v4/vue.go.template | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/template/v4/vue.go.template b/template/v4/vue.go.template index 503fdc2a..789ab16b 100644 --- a/template/v4/vue.go.template +++ b/template/v4/vue.go.template @@ -273,10 +273,31 @@ import { add{{.ClassName}}, del{{.ClassName}}, get{{.ClassName}}, list{{.ClassName}}, update{{.ClassName}} } from '@/api/{{.PackageName}}/{{.MLTBName}}' import type { {{.ClassName}}, {{.ClassName}}Query } from '@/api/{{.PackageName}}/{{.MLTBName}}' -{{- range .Columns}} -{{- if ne .FkTableName ""}} -import { list{{.FkTableNameClass}} } from '@/api/{{$package}}/{{.FkTableNamePackage}}' -import type { {{.FkTableNameClass}} } from '@/api/{{$package}}/{{.FkTableNamePackage}}' +{{- /* + 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}} {{- /* From 3beb00143aa2aa0b12ea1116dddabe03c775b5d6 Mon Sep 17 00:00:00 2001 From: zhangwenjian Date: Sat, 19 Sep 2026 20:49:41 +0800 Subject: [PATCH 5/6] =?UTF-8?q?fix=F0=9F=90=9B:=20give=20FK=20and=20dict?= =?UTF-8?q?=20options=20separate=20variable=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A column configured with both FkTableName and DictType -- and reaching a branch of each, e.g. required + IsQuery=1 + IsInsert=1 with HtmlType=radio -- had both blocks declare `const {JsonField}Options`: the dict branch from useDict, the FK branch from ref(). TS2451, Cannot redeclare block-scoped variable, and the page does not compile. This is reachable precisely because FkTableName and DictType do not exclude each other consistently: search, list and the form's select branch check FK first and fall back to dict, but the form's radio branch never looks at FK at all -- it was already established (the $dictUsed/$fkUsed audit two commits back) that a radio column's dict options are used regardless of whatever FkTableName says. A column that is both radio and query-or-insert-select can legitimately need both sources at once, under one shared name. Renamed to {JsonField}DictOptions and {JsonField}FkOptions and updated every consuming branch to the name that matches what it was already branching on: FK branches (search select, form select, the list column's Label function) read FkOptions; dict branches (search select, form select, form radio, the list column's dictLabel call) read DictOptions. Mechanical rename, no new conditions -- each site already knew which source it wanted from its own if/else-if. Verified with team-lead's exact repro (FkTableName + DictType + IsQuery=1 + IsInsert=1 + HtmlType=radio) rendered through the real template.Execute and checked against a throwaway go-admin-ui worktree (deleted afterwards): TS2451 fired twice before this change, zero after -- and the rendered file confirms the search select actually reads kindFkOptions (FK wins search's priority) while the insert radio reads kindDictOptions (radio never checks FK), so both sources are live, not just declared. Re-ran every fixture from every previous round alongside it; all stayed green. pnpm type-check and pnpm lint both zero error, on Node 24.11.0. --- template/v4/vue.go.template | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/template/v4/vue.go.template b/template/v4/vue.go.template index 789ab16b..de42dad0 100644 --- a/template/v4/vue.go.template +++ b/template/v4/vue.go.template @@ -80,7 +80,7 @@ {{- if ne .FkTableName ""}} - + {{- else if eq .HtmlType "datetime"}} @@ -189,7 +189,7 @@ {{- if ne .FkTableName ""}} - + {{ "{{" }} dict.label {{ "}}" }} @@ -318,18 +318,18 @@ defineOptions({ name: '{{.ClassName}}Manage' }) {{- $fkUsed := and (ne .FkTableName "") (or (eq .IsQuery "1") (eq .IsList "1") (and (eq .IsInsert "1") (eq .HtmlType "select"))) -}} {{- if $dictUsed}} -const { {{.DictType}}: {{.JsonField}}Options } = useDict('{{.DictType}}') +const { {{.DictType}}: {{.JsonField}}DictOptions } = useDict('{{.DictType}}') {{- end}} {{- if $fkUsed}} -const {{.JsonField}}Options = ref<{{.FkTableNameClass}}[]>([]) +const {{.JsonField}}FkOptions = ref<{{.FkTableNameClass}}[]>([]) onMounted(async() => { const res = await list{{.FkTableNameClass}}({ pageIndex: 1, pageSize: 100 }) - {{.JsonField}}Options.value = res.data?.list ?? [] + {{.JsonField}}FkOptions.value = res.data?.list ?? [] }) {{- if eq .IsList "1"}} const {{.JsonField}}Label = (value: unknown) => - {{.JsonField}}Options.value.find(item => item.{{.FkLabelId}} === value)?.{{.FkLabelName}} ?? value + {{.JsonField}}FkOptions.value.find(item => item.{{.FkLabelId}} === value)?.{{.FkLabelName}} ?? value {{- end}} {{- end}} {{- end}} From f6bd306d6d6976349abdd8095c109b7011e0e803 Mon Sep 17 00:00:00 2001 From: zhangwenjian Date: Sat, 19 Sep 2026 20:50:13 +0800 Subject: [PATCH 6/6] =?UTF-8?q?fix=F0=9F=90=9B:=20send=20datetime=20fields?= =?UTF-8?q?=20as=20RFC3339,=20not=20space-separated=20local=20time?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both date-pickers -- the search filter and the insert/edit form -- used value-format="YYYY-MM-DD HH:mm:ss", which formats a picked instant as e.g. "2026-09-19 12:30:00": no T separator, no offset. dto.go.template declares every datetime column's InsertReq/UpdateReq field as time.Time with a plain `json:"..."` tag (R6 leaves that file alone, so there is no time_format tag to reach for instead), and encoding/json's default (Un)MarshalJSON for time.Time only accepts RFC3339. The generated form would submit new and edited datetime values in a shape Go's JSON decoder cannot parse -- a runtime failure on every create and update, with nothing in `pnpm type-check` or `pnpm lint` positioned to see it: both check the request is well-typed TypeScript, not that the string it produces is a string Go can read. Changed both to value-format="YYYY-MM-DDTHH:mm:ssZ" -- dayjs's Z token renders the picker's own local offset, which is what a zero-nanosecond time.Time (anything without a database column storing sub-second precision) round-trips to on either side of the wire; confirmed separately against Go's actual json.Marshal/Unmarshal, not assumed from the RFC. The search filter needed the same fix, not just the form: GetPageReq binds a `time.Time` query field via `form:"..."` (dto.go.template), and gin's own default for an untagged time.Time binding is also RFC3339 -- the same failure mode on the query side, one call the report didn't name but the same root cause reaches. This is a runtime behaviour change no compiler catches, so it was verified as one: a Go program exercising encoding/json directly (not assumed from reading the RFC) confirmed a zero-nanosecond time.Time marshals to plain RFC3339 with no fractional seconds, and unmarshals correctly from both a numeric offset and a literal Z. Separately, a Node script loaded go-admin-ui's own installed dayjs 1.11.21 with the customParseFormat plugin -- the same plugin element-plus's date-picker extends dayjs with -- and called the same parseDate path date-picker panel.mjs uses (time-picker/src/utils.ts, no strict flag passed, so lenient parsing): formatting with this value-format produced a valid submission string, and parsing either an offset or a literal-Z string back with the same format produced a valid, correctly-valued date -- covering create, edit prefill, and update in the two directions that matter (browser to Go, Go to browser) without needing a live backend. --- template/v4/vue.go.template | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/template/v4/vue.go.template b/template/v4/vue.go.template index de42dad0..f40e3b10 100644 --- a/template/v4/vue.go.template +++ b/template/v4/vue.go.template @@ -99,7 +99,7 @@ {{- else}} @@ -223,7 +223,7 @@ {{- else if eq .HtmlType "textarea"}}