Files
go-admin/app/other/apis/tools/column_width.go
T
zhangwenjian 545453c93e 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.
2026-09-19 14:16:37 +08:00

127 lines
4.8 KiB
Go

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)
}
}
}