项目 AG Grid 表格统一使用 steelTradeAggrid 组件,数值列的编辑与粘贴校验通过
numberValidator + extractNumericRule 体系完成(详见
docs/AG-Grid编辑功能改造说明.md)。
在期现业务弹窗(分配方式 / 分配本次使用金额)的落地过程中,暴露了 4 个问题:
| 序号 | 问题 | 类型 |
|---|---|---|
| 1 | 数值列需要「上限取自本行另一字段(如可用金额 sPickAmt)」的能力,旧组件只能通过外部 TableInput 的 v-limit 实现 |
能力缺口 |
| 2 | 列配置里既要写编辑器 props、又要重复写 numericRule,规则来源不统一 |
可优化 |
| 3 | 粘贴报错只说「格式不正确」,不告诉用户到底是超上限还是小数位超限 | 缺陷 |
| 4 | 批量粘贴时校验漏检:选中多行粘贴,超限值能绕过校验写入 | 严重缺陷 |
| # | 增强点 | 文件 | 属性 |
|---|---|---|---|
| 1 | CndInputNumber 新增 maxField,支持「取本行字段值为上限」+ 输入时实时截断 |
src/components/agComponent/AgInputNumber.vue | 新增 |
| 2 | extractNumericRule 反推规则时纳入 maxField,粘贴/填充自动带上限,无需 numericRule |
src/utils/numberValidator.js | 增强 |
| 3 | 粘贴报错带上具体原因(复用 validateNumber.message) |
src/components/steelTradeAggrid/index.vue | 修复 |
| 4 | 重写粘贴校验行列映射,逐条对齐 AG Grid v25 真实写入规则;行数据获取加虚拟滚动兜底 | src/components/steelTradeAggrid/index.vue | 修复 |
maxField 上限能力 新增
新增 prop maxField(String,默认 '',不传时行为完全不变)。
语义是「上限取自当前行的某个字段」——例如本次使用金额不能超过本行的可用金额 sPickAmt。
maxField 而不是 max?range: [min, max] 表达静态取值范围,若再叫 max 会产生两套上限语义冲突;
且「取本行某字段」这一跨字段引用含义,maxField 表达更准确。
cellEditorFramework: Vue.extend(customMiddleware.createComponent(
'CndInputNumber',
{
mark: 'vCurAmt',
type: 'number',
decimalDigit: 2,
maxField: 'sPickAmt' // ← 上限 = 本行 sPickAmt 字段值
}
))
在 setVal() 中实时判断(每次输入都执行,等效旧 TableInput 的 v-limit,但改为组件内实现):
| 收款日期 | 可用金额 | 本次使用金额 | 备注 |
|---|---|---|---|
| 2026-09-01 | 29.70 | 不能超过29.7。 29.7 | 输入 100 时被实时压回 29.7 |
| 2026-09-02 | 49.70 | 0.00 |
// 取 maxField 指定字段的行值作为上限
getMaxFieldValue() {
if (!this.maxField) return null
const data = this.params && this.params.data
if (!data) return null
const max = Number(data[this.maxField])
return Number.isFinite(max) ? max : null
}
// setVal() 中(整数位截断之后、emit 之前)
const maxFieldVal = this.getMaxFieldValue()
if (maxFieldVal !== null && this.childValue !== '' && ... && Number(this.childValue) > maxFieldVal) {
this.childValue = String(maxFieldVal)
this.showLimitTip(`不能超过${maxFieldVal}。`)
}
visible1(气泡),不置 visible2(红框)——
因为 setVal 的 getVal emit 在 visible2 为真时会回写 oldChildValue(旧值),
置红框会导致截断值无法写回表格。
extractNumericRule 的 CndInputNumber 分支新增 maxField 映射:
// 改前
max: props.range && props.range[1]
// 改后:maxField(字段引用)优先于 range[1](静态上限)
max: props.maxField ? props.maxField : (props.range && props.range[1])
createComponent('CndInputNumber', {
decimalDigit: 2,
...
}),
numericRule: { // ← 需重复声明
scale: 2,
max: 'sPickAmt',
allowNegative: false
}
createComponent('CndInputNumber', {
decimalDigit: 2,
maxField: 'sPickAmt' // ← 校验规则由此反推
})
CndInputNumber 配了 maxField,粘贴 / 拖拽填充的校验就自动带上限,
无需再写 numericRule。纯增强,不影响任何既有列行为。
validateNumber 本已算出具体原因(result.message),但旧实现把它丢弃了,
报错只显示行列和原值,用户不知道「为什么不合法」。
各校验维度均可透出(由 validateNumber 提供):
| 场景 | 提示文案 |
|---|---|
| 超上限 | 【本次使用金额】不能大于 29.7 |
| 小数位超限 | 【本次使用金额】最多保留 2 位小数 |
| 负数 | 【本次使用金额】不允许输入负数 |
| 非数值 | 【本次使用金额】必须是有效数值 |
弹窗标题同步由「以下单元格格式不正确」改为「以下单元格数据不合法」(原因可能是超金额而非格式)。
旧实现假设「剪贴板第 r 行 → 选区起点 + r 行」的简单线性映射;而 AG Grid v25 真实写入规则是
逐行遍历选区、剪贴板循环填充,且行数不整除时超出部分不写入。两者错位,导致校验行号与
实际写入行号不一致,取到的上限字段值(sPickAmt)张冠李戴。
| 行 | 可用金额 sPickAmt | 粘贴值 | 结果 |
|---|---|---|---|
| 2 | 29.70 | 9958011.9 | 超上限 |
| 3 | 49.70 | 390 | 超上限 |
| 4 | 9958011.90 | 390 | 合法 |
| 5 | 390.00 | 90 | 合法 |
| 6 | 390.00 | (第5个值 300 被 AG Grid 丢弃:4 % 5 ≠ 0) | — |
重写 processClipboardData,按三种规则收集「待校验单元格」,再统一校验:
| 规则 | 场景 | 映射方式 |
|---|---|---|
| ① | 单值 + 有选区 | 值铺满选区所有行 × 列 |
| ② | 多值 + 选区多格 | 逐行遍历选区,clipboardData[index] 循环填充;行数不被剪贴板行数整除时,超出部分跳过(对齐 AG Grid 不写入) |
| ③ | 从焦点格往下 | 按剪贴板行数逐行往下,列自焦点列依次对应 |
// 规则② 核心(对齐 ag-grid pasteIntoActiveRange)
const abortRepeating = rangeRowCount % data.length !== 0
let indexOffset = 0, dataRowIndex = 0
for (let rowIndex = startRow; rowIndex <= endRow; rowIndex++) {
const index = rowIndex - startRow
let currentIndex = index - indexOffset
if (currentIndex >= data.length) {
if (abortRepeating) continue // 不整除,该行不写入、不校验
indexOffset += dataRowIndex; dataRowIndex = 0
currentIndex = index - indexOffset
}
const clipboardRow = data[currentIndex]
range.columns.forEach((column, colIdx) => {
if (colIdx >= clipboardRow.length) return
cells.push({ value: clipboardRow[colIdx], column, rowIndex })
})
dataRowIndex++
}
getRowDataByIndex 原先只用 getDisplayedRowAtIndex,虚拟滚动下未渲染行返回空,
导致上限字段取不到 → 上限校验被静默跳过(另一处漏检原因)。现增加行模型兜底:
getRowDataByIndex(rowIndex) {
const api = this._lastFocusedGrid.api
let node = api.getDisplayedRowAtIndex(rowIndex)
if (!node && api.getModel) node = api.getModel().getRow(rowIndex) // 兜底
return node ? node.data : null
}
「本次使用金额」(vCurAmt)列由旧 TableInput 迁移至 CndInputNumber,并配置 maxField:
// 改前
{
field: 'vCurAmt',
editable: true,
max: 'sPickAmt',
cellEditor: 'TableInput'
}
// 改后
{
field: 'vCurAmt',
editable: true,
cellEditorFramework: Vue.extend(customMiddleware.createComponent(
'CndInputNumber',
{ mark: 'vCurAmt', type: 'number', decimalDigit: 2, maxField: 'sPickAmt' }
))
}
| 路径 | 规则来源 | 超限行为 |
|---|---|---|
| 输入(手动编辑) | 组件 prop maxField | 实时截断 + 气泡提示 |
| 粘贴 | extractNumericRule 反推 maxField | 报错并取消整次粘贴(原因可见) |
| 拖拽填充 | 同上 | 不合规单元格保持原值 |
| 对比项 | TableInput(旧) | CndInputNumber + maxField(新) |
|---|---|---|
| 上限实现 | 外部 v-limit 指令 | 组件内 setVal 实时判断 |
| 小数位默认 | 4 位 | 由列配 decimalDigit 决定(当前 2 位) |
| 超限提示 | 静默截断 | 截断 + 气泡提示 |
| 粘贴校验 | 靠 numericRule | 自动反推,无需 numericRule |
| # | 文件 | 改动 | 影响面 |
|---|---|---|---|
| 1 | src/components/agComponent/AgInputNumber.vue | 新增 maxField prop + getMaxFieldValue + setVal 实时钳制 |
公共组件,默认不传 = 行为不变 |
| 2 | src/utils/numberValidator.js | extractNumericRule 反推纳入 maxField |
公共工具,纯增强 |
| 3 | src/components/steelTradeAggrid/index.vue | 报错信息完善;processClipboardData 重写;getRowDataByIndex 兜底 |
公共组件,所有表格粘贴校验生效 |
| 4 | src/views/contract/domesticSales/waitinvalidDialog.vue src/views/contract/domesticSales/waitinvalidDialoghz.vue |
vCurAmt 列迁移至 CndInputNumber + maxField |
业务页面 |
steelTradeAggrid 公共组件改动,
所有使用该组件的表格粘贴校验都会跟着变化——属修复缺陷 / 信息增强,不改变正常粘贴行为。
上线后建议对高频粘贴场景(批量导入、跨列出货单等)做一轮回归。
getModel().getRow 兜底依赖 AG Grid v25 接口,未来升级 AG Grid 需回归验证。maxField 能力沉淀为通用数值列规范,逐步替代 TableInput。
本文档为 docs/AG-Grid编辑功能改造说明.md 的演进补充,描述 2026-09-11 的校验增强。