mirror of
https://github.com/go-admin-team/go-admin.git
synced 2026-09-30 12:51:25 +00:00
feat✨: infer column width from column_type before rendering (PRD 010 R2)
vue.go.template (F4) only had a flat fallback for an unconfigured colWidth: 110 for datetime, 120 for everything else. R2 asks for a precise per-type inference instead - varchar(n) tiered by n, tinyint(1) narrower than a general integer, text/blob wide - sized so a typical table's list columns land inside go-admin-ui's ~580px text-column budget. text/template cannot parse "varchar(255)" itself, so this has to run in gen.go before the template executes, not in the template. Judgment is columnType, not goType, same as F1/F2's earlier reversal (API契约.md §1.1): sys_tables.go:323-338 gives every non-primary-key int/tinyint/bigint/decimal column goType "string", so goType alone cannot tell a boolean flag from a bigint from a name column. InferColumnWidth(columnType string) int is exported and pure per 测试用例.md §2.5's own request, so it can be pinned with an exact input/output table rather than only asserting "the rendered page happens not to overflow" - see column_width_test.go, including the tinyint(1)-vs-tinyint(4) and the varchar tier-boundary cases. applyInferredColumnWidths only touches a column already at the 0 sentinel, wired into both Preview and NOActionsGen ahead of the template execute calls; a column already configured (by the user or F6) is left untouched, also pinned by a test. Verified against the real NOActionsGen call path (not just the unit test): generated a table mixing every branch (tinyint(1), datetime, decimal, varchar at three tiers, text, and one pre-configured column) and printed the resulting tab.Columns[i].ColWidth after generation - every value matched InferColumnWidth's own table, and the pre-configured column's 333 was left untouched.
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go-admin/app/other/models/tools"
|
||||
)
|
||||
|
||||
// columnLengthPattern pulls the first parenthesized integer out of a MySQL
|
||||
// COLUMN_TYPE string - the "(255)" in "varchar(255)", the "(10" in
|
||||
// "decimal(10,2)". Works regardless of trailing modifiers such as
|
||||
// "unsigned" or a charset clause, since it only looks for the first digits
|
||||
// after the first '('.
|
||||
var columnLengthPattern = regexp.MustCompile(`\((\d+)`)
|
||||
|
||||
// InferColumnWidth backs R2's fallback path: when a column's colWidth is
|
||||
// left at its 0 sentinel (unconfigured), this reads sys_columns.column_type
|
||||
// - MySQL's information_schema.COLUMNS.COLUMN_TYPE, which carries length,
|
||||
// e.g. "varchar(255)", "int(11)", "decimal(10,2)", "tinyint(1)" - and
|
||||
// returns a px width sized to fit inside go-admin-ui's ~580px text-column
|
||||
// budget for a 1280px viewport (its AGENTS.md "列宽" section).
|
||||
//
|
||||
// The judgment has to be columnType, not goType: sys_tables.go:323-338
|
||||
// gives every non-primary-key int/tinyint/bigint/decimal column goType
|
||||
// "string" (a bare substring match on "int" that also catches "tinyint"/
|
||||
// "bigint", intentional at import time but useless for telling a boolean
|
||||
// flag from a bigint), so goType alone cannot distinguish a switch column
|
||||
// from a price column from a name column. This is the same judgment call
|
||||
// API契约.md §1.1 made, reversing the PRD's original "GoType" reading of R2.
|
||||
// GoType is not consulted anywhere in this function, including for
|
||||
// datetime/timestamp columns - those are matched on columnType too.
|
||||
//
|
||||
// Exported and pure (string in, int out) so QA can pin an exact input/output
|
||||
// table against it directly (测试用例.md §2.5's own recommendation), rather
|
||||
// than only being able to assert "the rendered page happens not to overflow".
|
||||
func InferColumnWidth(columnType string) int {
|
||||
ct := strings.ToLower(strings.TrimSpace(columnType))
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(ct, "tinyint(1)"):
|
||||
// MySQL's own shape for a boolean/status flag - a tag or a switch,
|
||||
// not text, so it wants less room than a general numeric column.
|
||||
return 70
|
||||
|
||||
case strings.Contains(ct, "datetime"), strings.Contains(ct, "timestamp"),
|
||||
strings.Contains(ct, "date"), strings.Contains(ct, "time"):
|
||||
return 110
|
||||
|
||||
case strings.HasPrefix(ct, "tinyint"), strings.HasPrefix(ct, "smallint"),
|
||||
strings.HasPrefix(ct, "mediumint"), strings.HasPrefix(ct, "int"),
|
||||
strings.HasPrefix(ct, "bigint"), strings.HasPrefix(ct, "decimal"),
|
||||
strings.HasPrefix(ct, "float"), strings.HasPrefix(ct, "double"):
|
||||
// API契约.md §1.1: "decimal/bigint/int 类给数字型窄宽度" groups these
|
||||
// together rather than sizing each individually - none of them need
|
||||
// more than a handful of digits' worth of width.
|
||||
return 90
|
||||
|
||||
case strings.HasPrefix(ct, "varchar"), strings.HasPrefix(ct, "char"):
|
||||
return varcharWidth(columnLength(ct))
|
||||
|
||||
case strings.Contains(ct, "text"), strings.Contains(ct, "blob"):
|
||||
// longtext/mediumtext/text/blob: no declared length to size against,
|
||||
// and content here is free-form, so this errs wide rather than
|
||||
// guessing a number the actual content will not respect.
|
||||
return 260
|
||||
|
||||
default:
|
||||
// Unrecognized column_type (an enum, a json column, a driver this
|
||||
// codebase does not special-case, ...). Matches the flat fallback
|
||||
// vue.go.template already used for every non-datetime column before
|
||||
// this function existed, so a type this does not recognize is no
|
||||
// worse off than the old blanket default.
|
||||
return 120
|
||||
}
|
||||
}
|
||||
|
||||
// varcharWidth tiers a char/varchar column by its declared length. The
|
||||
// tiers are deliberately coarse - R2 only asks for "common tables land in
|
||||
// the 580px budget", not pixel-perfect sizing per character.
|
||||
func varcharWidth(n int) int {
|
||||
switch {
|
||||
case n <= 0:
|
||||
// Length did not parse (unexpected shape) - mid tier, not the
|
||||
// narrowest, since an un-lengthed varchar is unlikely to be a
|
||||
// short code column.
|
||||
return 150
|
||||
case n <= 10:
|
||||
return 90
|
||||
case n <= 20:
|
||||
return 110
|
||||
case n <= 50:
|
||||
return 150
|
||||
case n <= 100:
|
||||
return 200
|
||||
default:
|
||||
return 240
|
||||
}
|
||||
}
|
||||
|
||||
// columnLength extracts the first parenthesized integer, or 0 if the type
|
||||
// string does not have one (already-lowercased input expected).
|
||||
func columnLength(columnType string) int {
|
||||
m := columnLengthPattern.FindStringSubmatch(columnType)
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
n, err := strconv.Atoi(m[1])
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// applyInferredColumnWidths fills in InferColumnWidth's result for every
|
||||
// column still at the 0 "unconfigured" sentinel, in place, before the
|
||||
// template that reads .ColWidth runs. A column the user (or F6's config
|
||||
// page) already gave an explicit width is left untouched.
|
||||
func applyInferredColumnWidths(columns []tools.SysColumns) {
|
||||
for i := range columns {
|
||||
if columns[i].ColWidth == 0 {
|
||||
columns[i].ColWidth = InferColumnWidth(columns[i].ColumnType)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go-admin/app/other/models/tools"
|
||||
)
|
||||
|
||||
// Input/output pins for InferColumnWidth, per 测试用例.md §2.5's own
|
||||
// recommendation ("QA 才能在阶段 4 补一张精确的输入→输出对照表断言, 而不是只测
|
||||
// 结果凑巧没溢出这种弱结论") - this is that table, kept next to the function
|
||||
// it pins rather than only living in a later QA-owned suite.
|
||||
func TestInferColumnWidth(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
columnType string
|
||||
want int
|
||||
}{
|
||||
{"boolean/status flag", "tinyint(1)", 70},
|
||||
{"boolean flag, case-insensitive", "TINYINT(1)", 70},
|
||||
{"datetime", "datetime", 110},
|
||||
{"timestamp", "timestamp", 110},
|
||||
{"date only", "date", 110},
|
||||
{"time only", "time", 110},
|
||||
|
||||
{"plain tinyint (not the (1) boolean shape)", "tinyint(4)", 90},
|
||||
{"smallint", "smallint(6)", 90},
|
||||
{"mediumint", "mediumint(9)", 90},
|
||||
{"int", "int(11)", 90},
|
||||
{"bigint", "bigint(20)", 90},
|
||||
{"decimal", "decimal(10,2)", 90},
|
||||
{"float", "float", 90},
|
||||
{"double", "double", 90},
|
||||
|
||||
{"varchar short code", "varchar(8)", 90},
|
||||
{"varchar at the 10 boundary", "varchar(10)", 90},
|
||||
{"varchar just past the 10 boundary", "varchar(11)", 110},
|
||||
{"varchar at the 20 boundary", "varchar(20)", 110},
|
||||
{"varchar mid length", "varchar(32)", 150},
|
||||
{"varchar at the 50 boundary", "varchar(50)", 150},
|
||||
{"varchar just past the 50 boundary", "varchar(51)", 200},
|
||||
{"varchar(255), the common default", "varchar(255)", 240},
|
||||
{"char, fixed-width", "char(2)", 90},
|
||||
{"varchar with no parsed length", "varchar", 150},
|
||||
|
||||
{"text, no length to size against", "text", 260},
|
||||
{"longtext", "longtext", 260},
|
||||
{"mediumtext", "mediumtext", 260},
|
||||
{"blob", "blob", 260},
|
||||
|
||||
{"unrecognized type falls back to the old flat default", "json", 120},
|
||||
{"empty column_type falls back to the old flat default", "", 120},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := InferColumnWidth(tc.columnType); got != tc.want {
|
||||
t.Errorf("InferColumnWidth(%q) = %d, want %d", tc.columnType, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyInferredColumnWidths(t *testing.T) {
|
||||
columns := []tools.SysColumns{
|
||||
{JsonField: "name", ColumnType: "varchar(64)", ColWidth: 0},
|
||||
{JsonField: "price", ColumnType: "decimal(10,2)", ColWidth: 300}, // already configured
|
||||
}
|
||||
|
||||
applyInferredColumnWidths(columns)
|
||||
|
||||
if columns[0].ColWidth == 0 {
|
||||
t.Error("unconfigured column: want an inferred non-zero width, still 0")
|
||||
}
|
||||
if want := InferColumnWidth("varchar(64)"); columns[0].ColWidth != want {
|
||||
t.Errorf("unconfigured column: want %d (InferColumnWidth's own answer), got %d", want, columns[0].ColWidth)
|
||||
}
|
||||
if columns[1].ColWidth != 300 {
|
||||
t.Errorf("already-configured column: want the user's 300 left untouched, got %d", columns[1].ColWidth)
|
||||
}
|
||||
}
|
||||
@@ -123,6 +123,9 @@ func (e Gen) Preview(c *gin.Context) {
|
||||
}
|
||||
|
||||
tab, _ := table.Get(db, false)
|
||||
// R2: infer a width for any column the config page left at colWidth's 0
|
||||
// sentinel, before vue.go.template reads .ColWidth - see column_width.go.
|
||||
applyInferredColumnWidths(tab.Columns)
|
||||
var b1 bytes.Buffer
|
||||
err = t1.Execute(&b1, tab)
|
||||
var b2 bytes.Buffer
|
||||
@@ -210,6 +213,8 @@ func (e Gen) NOActionsGen(c *gin.Context, tab tools.SysTables) {
|
||||
e.Context = c
|
||||
log := e.GetLogger()
|
||||
tab.MLTBName = strings.Replace(tab.TBName, "_", "-", -1)
|
||||
// R2: see the matching call and comment in Preview above.
|
||||
applyInferredColumnWidths(tab.Columns)
|
||||
|
||||
basePath := "template/v4/"
|
||||
routerFile := basePath + "no_actions/router_check_role.go.template"
|
||||
|
||||
Reference in New Issue
Block a user