mixin.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. import XEUtils from 'xe-utils'
  2. import { VxeUI } from '../../../ui'
  3. import { eqEmptyValue, getFuncText } from '../../../ui/src/utils'
  4. import { scrollToView } from '../../../ui/src/dom'
  5. import { handleFieldOrColumn, getRowid } from '../../src/util'
  6. import { warnLog, errLog } from '../../../ui/src/log'
  7. import type { VxeTableDefines, TableInternalData, TableReactData, VxeTableConstructor, VxeTablePrivateMethods } from '../../../../types'
  8. const { getConfig, validators } = VxeUI
  9. /**
  10. * 校验规则
  11. */
  12. class Rule {
  13. $options: any
  14. constructor (rule: any) {
  15. Object.assign(this, {
  16. $options: rule,
  17. required: rule.required,
  18. min: rule.min,
  19. max: rule.max,
  20. type: rule.type,
  21. pattern: rule.pattern,
  22. validator: rule.validator,
  23. trigger: rule.trigger,
  24. maxWidth: rule.maxWidth
  25. })
  26. }
  27. /**
  28. * 获取校验不通过的消息
  29. * 支持国际化翻译
  30. */
  31. get content () {
  32. return getFuncText(this.$options.content || this.$options.message)
  33. }
  34. get message () {
  35. return this.content
  36. }
  37. }
  38. // 如果存在 pattern,判断正则
  39. function validREValue (pattern: string | RegExp | undefined, val: string) {
  40. if (pattern && !(XEUtils.isRegExp(pattern) ? pattern : new RegExp(pattern)).test(val)) {
  41. return false
  42. }
  43. return true
  44. }
  45. // 如果存在 max,判断最大值
  46. function validMaxValue (max: string | number | undefined, num: number) {
  47. if (!XEUtils.eqNull(max) && num > XEUtils.toNumber(max)) {
  48. return false
  49. }
  50. return true
  51. }
  52. // 如果存在 min,判断最小值
  53. function validMinValue (min: string | number | undefined, num: number) {
  54. if (!XEUtils.eqNull(min) && num < XEUtils.toNumber(min)) {
  55. return false
  56. }
  57. return true
  58. }
  59. function validRuleValue (rule: VxeTableDefines.ValidatorRule, val: any, required: boolean | undefined) {
  60. const { type, min, max, pattern } = rule
  61. const isArrType = type === 'array'
  62. const isNumType = type === 'number'
  63. const isStrType = type === 'string'
  64. const strVal = `${val}`
  65. if (!validREValue(pattern, strVal)) {
  66. return false
  67. }
  68. if (isArrType) {
  69. if (!XEUtils.isArray(val)) {
  70. return false
  71. }
  72. if (required) {
  73. if (!val.length) {
  74. return false
  75. }
  76. }
  77. if (!validMinValue(min, val.length)) {
  78. return false
  79. }
  80. if (!validMaxValue(max, val.length)) {
  81. return false
  82. }
  83. } else if (isNumType) {
  84. const numVal = Number(val)
  85. if (isNaN(numVal)) {
  86. return false
  87. }
  88. if (!validMinValue(min, numVal)) {
  89. return false
  90. }
  91. if (!validMaxValue(max, numVal)) {
  92. return false
  93. }
  94. } else {
  95. if (isStrType) {
  96. if (!XEUtils.isString(val)) {
  97. return false
  98. }
  99. }
  100. if (required) {
  101. if (!strVal) {
  102. return false
  103. }
  104. }
  105. if (!validMinValue(min, strVal.length)) {
  106. return false
  107. }
  108. if (!validMaxValue(max, strVal.length)) {
  109. return false
  110. }
  111. }
  112. return true
  113. }
  114. function checkRuleStatus (rule: VxeTableDefines.ValidatorRule, val: any) {
  115. const { required } = rule
  116. const isEmptyVal = XEUtils.isArray(val) ? !val.length : eqEmptyValue(val)
  117. if (required) {
  118. if (isEmptyVal) {
  119. return false
  120. }
  121. if (!validRuleValue(rule, val, required)) {
  122. return false
  123. }
  124. } else {
  125. if (!isEmptyVal) {
  126. if (!validRuleValue(rule, val, required)) {
  127. return false
  128. }
  129. }
  130. }
  131. return true
  132. }
  133. export default {
  134. methods: {
  135. /**
  136. * 完整校验,和 validate 的区别就是会给有效数据中的每一行进行校验
  137. */
  138. _fullValidate (rows: any, cb: any) {
  139. if (XEUtils.isFunction(cb)) {
  140. warnLog('vxe.error.notValidators', ['fullValidate(rows, callback)', 'fullValidate(rows)'])
  141. }
  142. return this.beginValidate(rows, null, cb, true)
  143. },
  144. /**
  145. * 快速校验,如果存在记录不通过的记录,则返回不再继续校验(异步校验除外)
  146. */
  147. _validate (rows: any, cb: any) {
  148. if (XEUtils.isFunction(cb)) {
  149. warnLog('vxe.error.notValidators', ['validate(rows, callback)', 'validate(rows)'])
  150. }
  151. return this.beginValidate(rows, null, cb)
  152. },
  153. /**
  154. * 完整校验单元格,和 validateField 的区别就是会给有效数据中的每一行进行校验
  155. */
  156. _fullValidateField (rows: any, fieldOrColumn: any) {
  157. const colList = (XEUtils.isArray(fieldOrColumn) ? fieldOrColumn : (fieldOrColumn ? [fieldOrColumn] : [])).map(column => handleFieldOrColumn(this, column))
  158. if (colList.length) {
  159. return this.beginValidate(rows, colList, null, true)
  160. }
  161. return this.$nextTick()
  162. },
  163. /**
  164. * 快速校验单元格,如果存在记录不通过的记录,则返回不再继续校验(异步校验除外)
  165. */
  166. _validateField (rows: any, fieldOrColumn: any) {
  167. const colList = (XEUtils.isArray(fieldOrColumn) ? fieldOrColumn : (fieldOrColumn ? [fieldOrColumn] : [])).map(column => handleFieldOrColumn(this, column))
  168. if (colList.length) {
  169. return this.beginValidate(rows, colList, null)
  170. }
  171. return this.$nextTick()
  172. },
  173. /**
  174. * 聚焦到校验通过的单元格并弹出校验错误提示
  175. */
  176. handleValidError (params: any) {
  177. const $xeTable = this as VxeTableConstructor & VxeTablePrivateMethods
  178. const { validOpts } = this
  179. return new Promise<void>(resolve => {
  180. if (validOpts.autoPos === false) {
  181. $xeTable.dispatchEvent('valid-error', params, null)
  182. resolve()
  183. } else {
  184. this.handleEdit(params, { type: 'valid-error', trigger: 'call' }).then(() => {
  185. setTimeout(() => {
  186. resolve(this.showValidTooltip(params))
  187. }, 10)
  188. })
  189. }
  190. })
  191. },
  192. handleErrMsgMode (validErrMaps: any) {
  193. const { validOpts } = this
  194. if (validOpts.msgMode === 'single') {
  195. const keys = Object.keys(validErrMaps)
  196. const resMaps: Record<string, {
  197. row: any;
  198. column: any;
  199. rule: any;
  200. content: any;
  201. }> = {}
  202. if (keys.length) {
  203. const firstKey = keys[0]
  204. resMaps[firstKey] = validErrMaps[firstKey]
  205. }
  206. return resMaps
  207. }
  208. return validErrMaps
  209. },
  210. /**
  211. * 对表格数据进行校验
  212. * 如果不指定数据,则默认只校验临时变动的数据,例如新增或修改
  213. * 如果传 true 则校验当前表格数据
  214. * 如果传 row 指定行记录,则只验证传入的行
  215. * 如果传 rows 为多行记录,则只验证传入的行
  216. * 如果只传 callback 否则默认验证整个表格数据
  217. * 返回 Promise 对象,或者使用回调方式
  218. */
  219. beginValidate (rows: any, cols: VxeTableDefines.ColumnInfo[] | null, cb: any, isFull: any) {
  220. const $xeTable = this as VxeTableConstructor & VxeTablePrivateMethods
  221. const props = $xeTable
  222. const reactData = $xeTable as unknown as TableReactData
  223. const internalData = $xeTable as unknown as TableInternalData
  224. const validRest: any = {}
  225. const { editRules, treeConfig } = props
  226. const { isRowGroupStatus } = reactData
  227. const { afterFullData, pendingRowMaps, removeRowMaps } = internalData
  228. const treeOpts = $xeTable.computeTreeOpts
  229. const aggregateOpts = $xeTable.computeAggregateOpts
  230. let validList
  231. if (rows === true) {
  232. validList = afterFullData
  233. } else if (rows) {
  234. if (XEUtils.isFunction(rows)) {
  235. cb = rows
  236. } else {
  237. validList = XEUtils.isArray(rows) ? rows : [rows]
  238. }
  239. }
  240. if (!validList) {
  241. validList = this.getInsertRecords().concat(this.getUpdateRecords())
  242. }
  243. const rowValidErrs: any[] = []
  244. this.lastCallTime = Date.now()
  245. this.validRuleErr = false // 如果为快速校验,当存在某列校验不通过时将终止执行
  246. this.clearValidate()
  247. const validErrMaps: any = {}
  248. if (editRules) {
  249. const columns = cols && cols.length ? cols : this.getColumns()
  250. const handleVaild = (row: any) => {
  251. const rowid = getRowid($xeTable, row)
  252. // 是否删除
  253. if (removeRowMaps[rowid]) {
  254. return
  255. }
  256. // 是否标记删除
  257. if (pendingRowMaps[rowid]) {
  258. return
  259. }
  260. if ($xeTable.isAggregateRecord(row)) {
  261. return
  262. }
  263. if (isFull || !this.validRuleErr) {
  264. const colVailds: any[] = []
  265. columns.forEach((column: any) => {
  266. const field = XEUtils.isString(column) ? column : column.field
  267. if ((isFull || !this.validRuleErr) && XEUtils.has(editRules, field)) {
  268. colVailds.push(
  269. this.validCellRules('all', row, column)
  270. .catch(({ rule, rules }: any) => {
  271. const rest = {
  272. rule,
  273. rules,
  274. rowIndex: this.getRowIndex(row),
  275. row,
  276. columnIndex: this.getColumnIndex(column),
  277. column,
  278. field,
  279. $table: this
  280. }
  281. if (!validRest[field]) {
  282. validRest[field] = []
  283. }
  284. validErrMaps[`${getRowid(this, row)}:${column.id}`] = {
  285. column,
  286. row,
  287. rule,
  288. content: rule.content
  289. }
  290. validRest[field].push(rest)
  291. if (!isFull) {
  292. this.validRuleErr = true
  293. return Promise.reject(rest)
  294. }
  295. })
  296. )
  297. }
  298. })
  299. rowValidErrs.push(Promise.all(colVailds))
  300. }
  301. }
  302. if (isRowGroupStatus) {
  303. XEUtils.eachTree(validList, handleVaild, { children: aggregateOpts.mapChildrenField })
  304. } else if (treeConfig) {
  305. const childrenField = treeOpts.children || treeOpts.childrenField
  306. XEUtils.eachTree(validList, handleVaild, { children: childrenField })
  307. } else {
  308. validList.forEach(handleVaild)
  309. }
  310. return Promise.all(rowValidErrs).then(() => {
  311. const ruleProps = Object.keys(validRest)
  312. this.validErrorMaps = this.handleErrMsgMode(validErrMaps)
  313. return this.$nextTick().then(() => {
  314. if (ruleProps.length) {
  315. return Promise.reject(validRest[ruleProps[0]][0])
  316. }
  317. if (cb) {
  318. cb()
  319. }
  320. })
  321. }).catch(firstErrParams => {
  322. return new Promise<void>((resolve, reject) => {
  323. const finish = () => {
  324. this.$nextTick(() => {
  325. if (cb) {
  326. cb(validRest)
  327. resolve()
  328. } else {
  329. if (getConfig().validToReject === 'obsolete') {
  330. // 已废弃,校验失败将不会执行catch
  331. reject(validRest)
  332. } else {
  333. resolve(validRest)
  334. }
  335. }
  336. })
  337. }
  338. const posAndFinish = () => {
  339. firstErrParams.cell = this.getCellElement(firstErrParams.row, firstErrParams.column)
  340. scrollToView(firstErrParams.cell)
  341. this.handleValidError(firstErrParams).then(finish)
  342. }
  343. /**
  344. * 当校验不通过时
  345. * 将表格滚动到可视区
  346. * 由于提示信息至少需要占一行,定位向上偏移一行
  347. */
  348. if (this.validOpts.autoPos === false) {
  349. finish()
  350. } else {
  351. const row = firstErrParams.row
  352. const column = firstErrParams.column
  353. this.scrollToRow(row, column).then(posAndFinish)
  354. }
  355. })
  356. })
  357. } else {
  358. this.validErrorMaps = {}
  359. }
  360. return this.$nextTick().then(() => {
  361. if (cb) {
  362. cb()
  363. }
  364. })
  365. },
  366. hasCellRules (type: any, row: any, column: any) {
  367. const { editRules } = this
  368. const { property } = column
  369. if (property && editRules) {
  370. const rules = XEUtils.get(editRules, property)
  371. return rules && XEUtils.find(rules, rule => type === 'all' || !rule.trigger || type === rule.trigger)
  372. }
  373. return false
  374. },
  375. /**
  376. * 校验数据
  377. * 按表格行、列顺序依次校验(同步或异步)
  378. * 校验规则根据索引顺序依次校验,如果是异步则会等待校验完成才会继续校验下一列
  379. * 如果校验失败则,触发回调或者Promise<不通过列的错误消息>
  380. * 如果是传回调方式这返回一个校验不通过列的错误消息
  381. *
  382. * rule 配置:
  383. * required=Boolean 是否必填
  384. * min=Number 最小长度
  385. * max=Number 最大长度
  386. * validator=Function({ cellValue, rule, rules, row, column, rowIndex, columnIndex }) 自定义校验,接收一个 Promise
  387. * trigger=blur|change 触发方式(除非特殊场景,否则默认为空就行)
  388. */
  389. validCellRules (validType: any, row: any, column: any, val: any) {
  390. const { editRules } = this
  391. const { property } = column
  392. const errorRules: any[] = []
  393. const syncValidList: any[] = []
  394. if (property && editRules) {
  395. const rules = XEUtils.get(editRules, property)
  396. if (rules) {
  397. const cellValue = XEUtils.isUndefined(val) ? XEUtils.get(row, property) : val
  398. rules.forEach((rule: any) => {
  399. const { trigger, validator } = rule
  400. if (validType === 'all' || !trigger || validType === trigger) {
  401. if (validator) {
  402. const validParams: any = {
  403. cellValue,
  404. rule,
  405. rules,
  406. row,
  407. rowIndex: this.getRowIndex(row),
  408. column,
  409. columnIndex: this.getColumnIndex(column),
  410. field: column.property,
  411. $table: this
  412. }
  413. let customValid
  414. if (XEUtils.isString(validator)) {
  415. const gvItem = validators.get(validator)
  416. if (gvItem) {
  417. const tcvMethod = gvItem.tableCellValidatorMethod || gvItem.cellValidatorMethod
  418. if (tcvMethod) {
  419. customValid = tcvMethod(validParams)
  420. } else {
  421. errLog('vxe.error.notValidators', [validator])
  422. }
  423. } else {
  424. errLog('vxe.error.notValidators', [validator])
  425. }
  426. } else {
  427. customValid = validator(validParams)
  428. }
  429. if (customValid) {
  430. if (XEUtils.isError(customValid)) {
  431. this.validRuleErr = true
  432. errorRules.push(new Rule({ type: 'custom', trigger, content: customValid.message, rule: new Rule(rule) }))
  433. } else if (customValid.catch) {
  434. // 如果为异步校验(注:异步校验是并发无序的)
  435. syncValidList.push(
  436. customValid.catch((e: any) => {
  437. this.validRuleErr = true
  438. errorRules.push(new Rule({ type: 'custom', trigger, content: e && e.message ? e.message : (rule.content || rule.message), rule: new Rule(rule) }))
  439. })
  440. )
  441. }
  442. }
  443. } else {
  444. if (!checkRuleStatus(rule, cellValue)) {
  445. this.validRuleErr = true
  446. errorRules.push(new Rule(rule))
  447. }
  448. }
  449. }
  450. })
  451. }
  452. }
  453. return Promise.all(syncValidList).then(() => {
  454. if (errorRules.length) {
  455. const rest = { rules: errorRules, rule: errorRules[0] }
  456. return Promise.reject(rest)
  457. }
  458. })
  459. },
  460. _clearValidate (rows: any, fieldOrColumn: any) {
  461. const { validOpts, validErrorMaps } = this
  462. const validTip = this.$refs.refValidTooltip
  463. const rowList = XEUtils.isArray(rows) ? rows : (rows ? [rows] : [])
  464. const colList = (XEUtils.isArray(fieldOrColumn) ? fieldOrColumn : (fieldOrColumn ? [fieldOrColumn] : [])).map(column => handleFieldOrColumn(this, column))
  465. let validErrMaps: any = {}
  466. if (validTip && validTip.visible) {
  467. validTip.close()
  468. }
  469. // 如果是单个提示模式
  470. if (validOpts.msgMode === 'single') {
  471. this.validErrorMaps = {}
  472. return this.$nextTick()
  473. }
  474. if (rowList.length && colList.length) {
  475. validErrMaps = Object.assign({}, validErrorMaps)
  476. rowList.forEach(row => {
  477. colList.forEach((column) => {
  478. const validKey = `${getRowid(this, row)}:${column.id}`
  479. if (validErrMaps[validKey]) {
  480. delete validErrMaps[validKey]
  481. }
  482. })
  483. })
  484. } else if (rowList.length) {
  485. const rowIdList = rowList.map(row => `${getRowid(this, row)}`)
  486. XEUtils.each(validErrorMaps, (item, key) => {
  487. if (rowIdList.indexOf(key.split(':')[0]) > -1) {
  488. validErrMaps[key] = item
  489. }
  490. })
  491. } else if (colList.length) {
  492. const colidList = colList.map(column => `${column.id}`)
  493. XEUtils.each(validErrorMaps, (item, key) => {
  494. if (colidList.indexOf(key.split(':')[1]) > -1) {
  495. validErrMaps[key] = item
  496. }
  497. })
  498. }
  499. this.validErrorMaps = validErrMaps
  500. return this.$nextTick()
  501. },
  502. /**
  503. * 触发校验
  504. */
  505. triggerValidate (type: any) {
  506. const { editConfig, editStore, editRules, editOpts, validOpts } = this
  507. const { actived } = editStore
  508. // 检查清除校验消息
  509. if (editRules && validOpts.msgMode === 'single') {
  510. this.validErrorMaps = {}
  511. }
  512. // 校验单元格
  513. if (editConfig && editRules && actived.row) {
  514. const { row, column, cell } = actived.args
  515. if (this.hasCellRules(type, row, column)) {
  516. return this.validCellRules(type, row, column).then(() => {
  517. if (editOpts.mode === 'row') {
  518. this.clearValidate(row, column)
  519. }
  520. }).catch(({ rule }: any) => {
  521. // 如果校验不通过与触发方式一致,则聚焦提示错误,否则跳过并不作任何处理
  522. if (!rule.trigger || type === rule.trigger) {
  523. const rest = { rule, row, column, cell }
  524. this.showValidTooltip(rest)
  525. return Promise.reject(rest)
  526. }
  527. return Promise.resolve()
  528. })
  529. }
  530. }
  531. return Promise.resolve()
  532. },
  533. /**
  534. * 弹出校验错误提示
  535. */
  536. showValidTooltip (params: any) {
  537. const $xeTable = this as VxeTableConstructor & VxeTablePrivateMethods
  538. const { $refs, height, validStore, validErrorMaps, tableData, validOpts } = this
  539. const { rule, row, column, cell } = params
  540. const validTip = $refs.refValidTooltip
  541. const content = rule.content
  542. validStore.visible = true
  543. if (validOpts.msgMode === 'single') {
  544. this.validErrorMaps = {
  545. [`${getRowid(this, row)}:${column.id}`]: {
  546. column,
  547. row,
  548. rule,
  549. content
  550. }
  551. }
  552. } else {
  553. this.validErrorMaps = Object.assign({}, validErrorMaps, {
  554. [`${getRowid(this, row)}:${column.id}`]: {
  555. column,
  556. row,
  557. rule,
  558. content
  559. }
  560. })
  561. }
  562. $xeTable.dispatchEvent('valid-error', params, null)
  563. if (validTip) {
  564. if (validTip && (validOpts.message === 'tooltip' || (validOpts.message === 'default' && !height && tableData.length < 2))) {
  565. return validTip.open(cell, content)
  566. }
  567. }
  568. return this.$nextTick()
  569. }
  570. } as any
  571. }