This commit is contained in:
oneao committed 2026-10-15 22:47:42 +08:00
1 parent bf9dca19de
commit e7c84a82a2
31 files changed
+4063 -2362

No files matched your search

@@ -0,0 +1,18 @@
---
'@g3soft/ui': minor
---
G3Table:校验态补「左侧语义色竖条」、禁用灰底加深一档 —— 让「格级」与「行级」状态重新分得开。
- **校验态(`errorStyle: 'cell'`)新增左侧 3px 语义色竖条**(error 红 / warning 琥珀),底色浓度不变(8% / 5%)。
- 背景:表体的纵向列线已经去掉(见上一条 changeset)。原来「格级色块被列线围成矩形、行级是一条长带」的**形状差异**没了,校验底色退化成无边界的横带,只剩色相一个通道 —— 「这一格有问题」很容易被读成「这一段行有问题」。左侧竖条把形状还回来:竖条 = 格级,横带 = 行级。
- 画在 `background-image` 上,不是 `border`(进盒模型会顶高行高、破坏虚拟滚动 `translateY` 的基准),也不是 `box-shadow`(单值属性,会与库的单元格选区描边、焦点环、固定列叠色互相整条覆盖)。因此 `background-origin` 按层指定:第 1 层用 `border-box` —— 表体纯文本格有 `--cell-padding-x`(12px)内边距,用默认的 `padding-box` 竖条会飘在格子内部 12px 处。
- 「正在编辑的那一格让位」的既有行为不变:编辑时既无底色也无竖条,只剩语义色焦点环 + 红字。
- **禁用(改不了)灰底从 3% 提到 6%**(暗色 4% → 8%)。灰底与斑马纹同色相、都是一条横带,原来只差约 1% 亮度,肉眼分不出「这行是条纹」还是「这格改不了」;拉开一档后可辨。整列只读 / 整列禁用 / 按行禁用三种情形共用这一套。
- **「既禁用又校验不过」的格子仍然只显示灰**(既有决策:改不了就别再提示这里错了)。灰底规则里**显式写回**库那两层格线(`background-image: var(--bg-border-right), var(--bg-border-bottom)`)—— 竖条加在 `background-image` 第 1 层上,而 background-image 不能按层覆盖,只改 `background-color` 的话灰底上会留一条红 / 黄竖条。亮 / 暗两份灰底规则都要写(只写亮色就会出现「亮色纯灰、暗色留红条」)。
- **注释修正**:`G3Table.vue` 里「灰底权重 (0,6,1) 高于校验态 (0,4,1)」这句是旧数字 —— 校验态选择器带 `:has()`,特异性取参数中最高者,按行禁用那条与校验态实测同为 `(0,7,1)`,胜负实际由「谁写在后面」决定。已改成准确表述并加了警告。
- **失败原因现在有出口了**:`validate` / `validateWarning` 返回的字符串(「金额必须大于 0」)此前在 `resolveEditorStatus` 里**只被取 `.length` 判等级、内容直接丢掉** —— 业务精心写的失败原因没有任何出口,用户只知道「这格红了」。现在该结果归一为 `{ level, message }` 一次带出,挂到内容节点的 `data-validate-message` 上,表格侧 **hover 那一格**时弹一个自绘浮层显示原因(悬停格内任意位置都行,移开 / 滚动即收)。
- 浮层是**全表共用一个 DOM**(`v-if` + 根节点上的 `mouseover` 事件委托),常态下定时器 / 监听器 / DOM 全都不存在;落到出错格上只做一次 `getBoundingClientRect()`,且滚动即关闭、不做跟随,所以滚动路径上不会出现逐帧测量。刻意没有给每个出错格挂 Tooltip:那是每个实例两组监听器,虚拟滚动下还要随行反复挂载销毁,再叠上固定列 `sticky` 的定位与层叠,容易写出「只在某个滚动位置下错位」的问题。
- 委托的判定单位是**格子**(`td`)而不是事件目标的祖先链:空值格的展示 span **宽度为 0**、右对齐数字格也常只有文字那么宽,鼠标绝大多数位置落在 `td` 的内边距上,按祖先链找会静默弹不出来(实测踩到)。
- 有原因时该格的 `title` 会让位(原本它挂的是「值」,用于内容被省略时兜底),避免原生 tooltip 与浮层叠成两个框;`aria-label` 则拼成「金额,金额必须大于 0」供屏幕阅读器。
- 文档(`docs/content/ui/data/table.md` 的「展示形态 `errorStyle`」与「不可改的格子」两节)同步更新。
@@ -0,0 +1,18 @@
---
'@g3soft/ui': minor
---
G3Table 新增日期编辑器(`editor.type: 'date'`),并为 G3DatePicker 补上无缝模式与两个对外方法。
- **`editor.type: 'date'`**:点格即弹日历(不必先点输入框),也支持手输;值始终是**字符串**,不做 `Date` ↔ string 的隐式转换。
- `format` 同时决定面板精度与绑定值格式(默认 `YYYY-MM-DD`;写 `'YYYY-MM-DD HH:mm'` 即变成日期时间,选完要点「确定」才提交)。界面格式与存储格式不同时用 `valueFormat`。
- 校验 / 失败原因浮层 / 禁用灰底 / `errorStyle` 与其它编辑器完全一致,没有另开一套。
- 它是**唯一「只给当前格挂编辑器」**的类型:切换格时新格是重新挂载,日历才会跟着弹开。若跟着整行挂载,同行内从别的列切到日期格就不会重新挂载、面板不弹,表现为「有时弹、有时不弹」这种最难解释的不一致。顺带也更省 —— 不激活时是纯展示 span,而 DatePicker 是浮层组件。
- **`G3DatePicker` 新增 `seamless`**:去掉触发器的 32px 下限 / 内边距 / 描边 / 圆角。与 `G3Select` 的 `seamless` 同因同解 —— 那些盒子在 Popover 内部,从表格侧写后代选择器命中不到(权重相同、顺序不可控),必须在组件自己的样式块里覆盖。不开的话 32 > 30 会把表格行顶高,看着像「格子里浮着一个小卡片」。
- **`G3DatePicker` 新增 `defineExpose({ openPanel, commitPending })`**:前者供表格挂载后主动展开面板,后者供表格在编辑器**卸载前**把没提交的手输文本交掉。
- **修一个「手输的日期会静默丢失」的真问题**:关闭面板时原先直接清空了内部草稿,而「点到别处」这条路径**不会**经过 `input` 的 `blur` —— 表格在 mousedown 阶段 `preventDefault`(避免点击干扰单元格选区),焦点压根没离开输入框。于是用户敲完日期点别的格,值就地消失。现在**关闭面板时先提交再收尾**;Esc 分支已在 `onKeydown` 里先把草稿清成 null,所以「按 Esc 取消」依然是取消而非提交(那个顺序不能反)。
- 配套:playground 新增一张独立的小日期表(`table-date-wrap`)作为 e2e 宿主(没往 `table-wrap` 里加列 —— 那张表的列序被 Tab 跳格用例逐格锁着);erp 的 `table-test` 页面把「开航日 / 截单日」两列从 `text` 换成 `date`(后者带时间精度)。
- **手输解析不出来时不再静默丢弃**:`G3DatePicker` 新增 `invalidInput` 事件(默认无监听者,表单行为不变),表格监听它把原文**写进行数据**,再由一条内置的日期校验标红并给出原因「日期格式不正确」—— 与「金额输 0」「数量超精度」同一套立场:值不合法不等于要抹掉它。配套导出 `checkDate(value, format)`,与组件内置判定**是同一个函数**,供业务在保存前拦提交。
- 判定口径刻意**跟随 `G3DatePicker` 手输解析的那个 `parseDateInput`**(不是自己写一套):否则会出现「手输时组件接受了、退回展示态又被标红」这种自相矛盾。注意**不要**换成 `parseDate` —— 它是宽松解析、会退到原生 `new Date()` 兜底,把 `2026-13-45` 这类越界文本「救活」(`parseDateInput` 里那道 `DATE_LIKE_TEXT` 保护正是拦这个的)。
- 顺带记录的既有口径:分隔符会被规范化(`2026/03/15` 算合法),而配了时间精度的列只给到「日」算不合法。
- e2e 新增三条:点格弹面板并选日期写回字符串;必填校验 + 手输失焦后写回;手输非法日期时值原样保留、标红、悬停能看到原因。
@@ -0,0 +1,25 @@
---
'@g3soft/ui': minor
---
G3Table 键盘:方向键显式分工(左右归输入框、上下给表格导航)、Enter 确认并下移、多行编辑器回车换行。
**单击格仍直接进入编辑** —— 本组件的既有交互不变,这次只调整键盘分工与几处不符习惯的行为。
- **方向键显式分工**。此前方向键**全部**被输入框吃掉(编辑器一挂上就聚焦),想改中间一位时整格跳走、想换行时又只能靠 Tab —— 两个方向都别扭。现在:
- **左右** → 交给输入框移动光标 / `Shift+左右` 选中文本(编辑文本的刚需,不能让出去);
- **上下**(单行)→ **让给表格**导航,切到上 / 下一行的同一列**并进入编辑**(单行输入框里上下键没有任何原生用途,让出去是零损失);
- `Shift+上下` → 仍归输入框(原生语义是「选中到首尾」);
- `textarea` 里的上下 → 仍归输入框(要跨行移动光标)。
- ⚠️ 左右不能让给表格:同一个键不可能既移光标又切格。横向切换用 `Tab`(它还会跳过只读列,比按列 ±1 更贴合录入语境)。
- **Enter(单行)= 确认 + 下移到下一行同列**(Excel / Sheets / AG Grid / Handsontable 的通行约定)。
- 旧行为是 `preventDefault` + `select()` —— **全选文本,且不提交**。它既不是浏览器行为(单行 `input` 的 Enter 原生是「提交表单 / 无动作」,**不换行**),也不是表格软件的行为,而且「确认」这个动作整个缺失:改完按回车值还没落库。
- 下一行同一列若是只读列、或那一行被按行禁用 → **跳过**,落到下面第一个可编辑格;到表尾 → **停在原地**(不循环回表头:回车一路按到底突然跳回第一行会很意外)。
- ⚠️ 实现上必须**显式提交**当前值:移动会让编辑器 vnode 卸载,而 DOM 元素被移除时浏览器**不派发 blur**(做日期编辑器时踩过这个坑),只靠 `onBlur` 的话敲完按回车值就丢了。
- **Enter(`type: 'textarea'`)= 换行**(浏览器原生语义)。此前多行与单行共用同一段 `onKeydown` 且没按类型分流,导致备注这类多行格**按回车会把整段文本全选掉、根本换不了行**。
- **`Esc` 会退出编辑态并把焦点还给该格**。此前它只让输入框失焦,而编辑态是**行级**的、那一行仍停在编辑态(表现是「Esc 一下没反应」);更要紧的是焦点掉到 `body` —— 之后按方向键完全不经过表格,等于键盘失灵。
- **checkbox 补上 `Space` 切换**(原生 checkbox 的切换键);`Enter` 保留(表格用户的既有期待,连续录入时手不离 Enter)。此前只认 Enter。
- **`date` 编辑器**按同一套分工:左右归输入框(日期是文本,用户会想改其中一段),上下给导航。
- 配套:playground 新增 `table-keys-wrap` 小表(text / number / textarea 三列,行高给足以容纳多行编辑器)作为宿主 —— `table-wrap` 的列序被 Tab 跳格用例逐格锁着,且它里面没有多行编辑器,锁不住「单行 / 多行 Enter 语义不同」这条区分。
- e2e 新增 7 条:方向键分工 5 条(单击即编辑 / 上下切行 / 左右移光标 / `Shift+左右` 选文本 / `textarea` 跨行)、Enter 单行确认下移、Enter 多行换行。方向键那几条断言的是**光标真的动了 / 选区真的变了**,而不只是「没跳格」。
- 文档新增「键盘:单击即编辑,方向键分工」一节。
@@ -0,0 +1,14 @@
---
'@g3soft/ui': minor
---
G3Table:表体的纵向列线关闭,表头保留一条更轻的「短竖线」列界。
- **表体去掉纵向格线**:覆盖层把底座的 `--bg-border-right` / `--bg-border-left` 涂成透明渐变 —— 库给每个 `th`/`td` 铺的就是 `background-image: var(--bg-border-right), var(--bg-border-bottom)`,所以关掉这一个变量就够了。横线(`--bg-border-bottom`)与自绘圆角外框**完全不受影响**,它们走的是另外两条路径。
- 「按列摘线」那套已整体删除:早先为「最右列外侧线归外框管」写过 `th/td:last-child` 与右固定列的摘线规则,那个假设只在「列宽铺满容器」时成立(列宽不足时表现为「线没了 + 右侧留白」,有右固定列且横滚时更是摘错了对象)。
- **表头保留列界,但换成「短竖线」**:`th` 自己接管 `background-image`,画一条高 `calc(100% - 8px)`(上下各留 4px)、宽 1px 的短线,颜色用新增的 `--g3-table-header-divider`(亮 8% 的黑 ≈ #e1e1e1 / 暗 12% 的白,比 `--border-color` 那档格线色轻一档)。列界仍是「认列 / 拖列宽」的落点,但在比数据行更高的表头上不会顶满上下,观感更柔和。
- 为什么接管**整个** `background-image` 而不是只改其中一层:`background-image` / `-size` / `-position` 都是**按层一一对应**的列表属性,而库的层数并不固定(表头首行是 top/right/bottom 三层、多级表头的其它行只有两层)——只补 size / position 会把尺寸套到错误的层上(实测把竖线铺成整格色块)。
- 无框模式(`bordered: false`)不受影响:规则限定在 `.stk-table.bordered` 下。
- 贴住外框内右缘的右固定列用 `:not(.fixed-cell--right)` 排除,避免那条线与外框 `border` 并排成 2px;「DOM 末列」不排除(它是拖拽该列宽度的落点,列宽没铺满容器时也靠它指出末列到哪儿结束)。
- **固定列在数据区的边界改由 `fixedColShadow` 承载**:表体既无竖线,冻结列与滚过去的列又同为不透明基色、紧贴在一起,边界只剩那 10px 渐变能表达。用到左右冻结列的页面**务必**打开 `fixedColShadow`(默认仍为 `false`,不改默认值是刻意的:没有固定列的表格开它什么都画不出来;`g3soft-erp` 的订舱页早已显式开着)。
- 同步更新:`docs/content/ui/data/table.md` 的「固定列」与「实现说明」两处;`e2e/table.spec.ts` 的两条用例(表体只剩横线一条 / 表头短竖线的尺寸、位置与色值),并顺手抽出一个 `splitCssLayers` —— 按**顶层逗号**切 CSS 列表值,替掉原先 `split(/,(?![^(]*\))/)` 那种在 `rgba(` 前会失灵的写法(它把 `linear-gradient(rgba(...), rgba(...))` 切成两层,把「两条线」数成三条)。
@@ -0,0 +1,23 @@
---
'@g3soft/ui': minor
---
G3Table:新增 `editor.precision`(数值精度)与列级 `format`(展示格式化),并导出 `checkPrecision` / `formatNumber`。
- **`editor.precision`:数值精度(小数位数)**,可传函数按行给。它同时驱动两件事,是**一处真源**:
- **展示**:非编辑态按精度格式化(`1200.5` → `1200.50`);
- **校验**:超出精度即 error(红底 + 左侧竖条),悬停浮层显示「最多 N 位小数」—— 这是**组件内置**的规则,业务不需要写任何与精度有关的 `validate`。
- ⚠️ **不静默四舍五入**:用户输了 `123.456` 而精度是 2,值**仍然是** `123.456`,只是这一格被标红,由用户自己改。悄悄改成 `123.46` 会让「屏幕上的」与「落库的」不是一回事 —— 财务对账时就是「我输的不是这个」。
- ⚠️ **超精度时不做格式化展示**(原样裸露真实值):按精度格式化成 `123.46` 会让屏幕完全合规、数据却不合规,用户永远发现不了自己错了。
- 录入时**不做实时拦截**:实时的精度过滤要与光标位置、输入法组合态、半输入态(`12.` / `-`)搏斗,是公认的坑;「输完再判」既确定又可预测。
- 判定用**字符串**数小数位而不是数值运算:`Math.round(v * 100) / 100` 在 `1.005` 上给出的是 `1`(`1.005 * 100 = 100.49999999999999`),而金额最容易撞上这种「看着是两位小数、二进制里不是」的值。
- **`checkPrecision(value, precision)` 对外导出**:业务在保存前做提交拦截时必须复用**同一个函数**,否则会出现「格子红了但存档能过」这类两边不一致、又极难排查的问题。
- **列级 `format`:展示态格式化**(只影响显示,落库值不变)。传选项对象走底座的 `formatNumber`(千分位 / `¥` 前后缀 / 万-亿缩写 / 百分比);传函数则完全自定义(`Intl.NumberFormat` 的正解 —— 多语言的小数点与千分位符号、会计的括号负数这些 locale 规则,重写一遍必然缺选项)。小数位取 `format.decimals ?? editor.precision`。
- **顺带解决一个缺口**:底座的 `createNumberCell` 是**纯展示**单元格,用法是 `customCell: NumberCell()`,而我们的 `resolveCustomCell` 对显式 `customCell` 是直接放行的 —— 业务一旦用它就绕过了整条编辑链路(编辑、校验、灰底、Tab 跳格、Enter/Esc 语义、草稿与失焦提交全没了)。也就是说「**可编辑的金额列**」此前没有任何合法方式做格式化展示。现在 `format` 与 `editor` 正交,两者可同时存在。
- **门控(性能)**:`resolveCustomCell` 新增两条常驻判据 —— 配了 `precision` 的 number 列、以及配了 `format` 的**纯展示**列(后者走一个只做格式化的轻量 cell)。代价只落在**显式配了这两样**的列上,普通列照旧走库的原生 td 渲染、零组件开销。
- 单测覆盖边界(`utils/precision.test.ts`,13 条):空值/非数字/展示文本/科学计数法一律放行、负数符号不参与位数、`1.005` 这类浮点陷阱、非法精度配置当作没配、格式化与自定义函数的优先级。
- **修复:右对齐列一点进编辑,文字就跳到左边。** `text-align` 本是继承属性,但**表单控件**的 UA 样式表把它覆盖成 `start` —— 实测点进金额格后 input 拿到的是 `start`(LTR 下即左对齐),于是文字与光标从右跳到左,进出编辑有明显位移(与「编辑器无缝嵌入、进出零位移」的目标正好相反)。现在表格侧按库的列对齐类(`.text-r` / `.text-c`)把 `input` / `textarea` 接回来。
- **没有给 `G3Input` 加 prop**:右对齐是**列**的属性(`align`),只有表格知道该不该右对齐;加在公共组件上只会多一个当前没人用的 API。将来表单里出现真实诉求再加不迟。
- 判据依赖库的 `.text-r` / `.text-c` 类名(实测打在 `td` 上);库若改名这层会**静默失效**(表现只是「编辑时又变回左对齐」),代码注释里写明了排查方式。
- 只管 `input` / `textarea`:右对齐的 `select` 极少见,且它的对齐由触发器内部的 flex 布局决定,不是一条 `text-align` 能解决的。
- 文档新增「数值精度与金额」一节(含保存前拦截的完整示例)。