mixin.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. import XEUtils from 'xe-utils'
  2. import { UtilTools, DomTools } from '../../tools'
  3. /**
  4. * 校验规则
  5. */
  6. class Rule {
  7. constructor (rule) {
  8. Object.assign(this, {
  9. $options: rule,
  10. required: rule.required,
  11. min: rule.min,
  12. max: rule.max,
  13. type: rule.type,
  14. pattern: rule.pattern,
  15. validator: rule.validator,
  16. trigger: rule.trigger,
  17. maxWidth: rule.maxWidth
  18. })
  19. }
  20. /**
  21. * 获取校验不通过的消息
  22. * 支持国际化翻译
  23. */
  24. get message () {
  25. return UtilTools.getFuncText(this.$options.message)
  26. }
  27. }
  28. export default {
  29. methods: {
  30. /**
  31. * 完整校验,和 validate 的区别就是会给有效数据中的每一行进行校验
  32. */
  33. _fullValidate (rows, cb) {
  34. return this.beginValidate(rows, cb, true)
  35. },
  36. /**
  37. * 快速校验,如果存在记录不通过的记录,则返回不再继续校验(异步校验除外)
  38. */
  39. _validate (rows, cb) {
  40. return this.beginValidate(rows, cb)
  41. },
  42. /**
  43. * 聚焦到校验通过的单元格并弹出校验错误提示
  44. */
  45. handleValidError (params) {
  46. return new Promise(resolve => {
  47. if (this.validOpts.autoPos === false) {
  48. this.emitEvent('valid-error', params)
  49. resolve()
  50. } else {
  51. this.handleActived(params, { type: 'valid-error', trigger: 'call' }).then(() => {
  52. setTimeout(() => {
  53. resolve(this.showValidTooltip(params))
  54. }, 10)
  55. })
  56. }
  57. })
  58. },
  59. /**
  60. * 对表格数据进行校验
  61. * 如果不指定数据,则默认只校验临时变动的数据,例如新增或修改
  62. * 如果传 true 则校验当前表格数据
  63. * 如果传 row 指定行记录,则只验证传入的行
  64. * 如果传 rows 为多行记录,则只验证传入的行
  65. * 如果只传 callback 否则默认验证整个表格数据
  66. * 返回 Promise 对象,或者使用回调方式
  67. */
  68. beginValidate (rows, cb, isFull) {
  69. const validRest = {}
  70. const { editRules, afterFullData, treeConfig, treeOpts } = this
  71. let vaildDatas
  72. if (rows === true) {
  73. vaildDatas = afterFullData
  74. } else if (rows) {
  75. if (XEUtils.isFunction(rows)) {
  76. cb = rows
  77. } else {
  78. vaildDatas = XEUtils.isArray(rows) ? rows : [rows]
  79. }
  80. }
  81. if (!vaildDatas) {
  82. vaildDatas = this.getInsertRecords().concat(this.getUpdateRecords())
  83. }
  84. const rowValids = []
  85. this.lastCallTime = Date.now()
  86. this.validRuleErr = false // 如果为快速校验,当存在某列校验不通过时将终止执行
  87. this.clearValidate()
  88. if (editRules) {
  89. const columns = this.getColumns()
  90. const handleVaild = row => {
  91. if (isFull || !this.validRuleErr) {
  92. const colVailds = []
  93. columns.forEach((column) => {
  94. if ((isFull || !this.validRuleErr) && XEUtils.has(editRules, column.property)) {
  95. colVailds.push(
  96. this.validCellRules('all', row, column)
  97. .catch(({ rule, rules }) => {
  98. const rest = { rule, rules, rowIndex: this.getRowIndex(row), row, columnIndex: this.getColumnIndex(column), column, $table: this }
  99. if (!validRest[column.property]) {
  100. validRest[column.property] = []
  101. }
  102. validRest[column.property].push(rest)
  103. if (!isFull) {
  104. this.validRuleErr = true
  105. return Promise.reject(rest)
  106. }
  107. })
  108. )
  109. }
  110. })
  111. rowValids.push(Promise.all(colVailds))
  112. }
  113. }
  114. if (treeConfig) {
  115. XEUtils.eachTree(vaildDatas, handleVaild, treeOpts)
  116. } else {
  117. vaildDatas.forEach(handleVaild)
  118. }
  119. return Promise.all(rowValids).then(() => {
  120. const ruleProps = Object.keys(validRest)
  121. return this.$nextTick().then(() => {
  122. if (ruleProps.length) {
  123. return Promise.reject(validRest[ruleProps[0]][0])
  124. }
  125. if (cb) {
  126. cb()
  127. }
  128. })
  129. }).catch(firstErrParams => {
  130. return new Promise((resolve, reject) => {
  131. const finish = () => {
  132. this.$nextTick(() => {
  133. if (cb) {
  134. cb(validRest)
  135. resolve()
  136. } else {
  137. reject(validRest)
  138. }
  139. })
  140. }
  141. const posAndFinish = () => {
  142. firstErrParams.cell = this.getCell(firstErrParams.row, firstErrParams.column)
  143. DomTools.toView(firstErrParams.cell)
  144. this.handleValidError(firstErrParams).then(finish)
  145. }
  146. /**
  147. * 当校验不通过时
  148. * 将表格滚动到可视区
  149. * 由于提示信息至少需要占一行,定位向上偏移一行
  150. */
  151. const row = firstErrParams.row
  152. const rowIndex = afterFullData.indexOf(row)
  153. const locatRow = rowIndex > 0 ? afterFullData[rowIndex - 1] : row
  154. if (this.validOpts.autoPos === false) {
  155. finish()
  156. } else {
  157. if (treeConfig) {
  158. this.scrollToTreeRow(locatRow).then(posAndFinish)
  159. } else {
  160. this.scrollToRow(locatRow).then(posAndFinish)
  161. }
  162. }
  163. })
  164. })
  165. }
  166. return this.$nextTick().then(() => {
  167. if (cb) {
  168. cb()
  169. }
  170. })
  171. },
  172. hasCellRules (type, row, column) {
  173. const { editRules } = this
  174. const { property } = column
  175. if (property && editRules) {
  176. const rules = XEUtils.get(editRules, property)
  177. return rules && XEUtils.find(rules, rule => type === 'all' || !rule.trigger || type === rule.trigger)
  178. }
  179. return false
  180. },
  181. /**
  182. * 校验数据
  183. * 按表格行、列顺序依次校验(同步或异步)
  184. * 校验规则根据索引顺序依次校验,如果是异步则会等待校验完成才会继续校验下一列
  185. * 如果校验失败则,触发回调或者Promise<不通过列的错误消息>
  186. * 如果是传回调方式这返回一个校验不通过列的错误消息
  187. *
  188. * rule 配置:
  189. * required=Boolean 是否必填
  190. * min=Number 最小长度
  191. * max=Number 最大长度
  192. * validator=Function({ cellValue, rule, rules, row, column, rowIndex, columnIndex }) 自定义校验,接收一个 Promise
  193. * trigger=blur|change 触发方式(除非特殊场景,否则默认为空就行)
  194. */
  195. validCellRules (type, row, column, val) {
  196. const { editRules } = this
  197. const { property } = column
  198. const errorRules = []
  199. const syncVailds = []
  200. if (property && editRules) {
  201. const rules = XEUtils.get(editRules, property)
  202. if (rules) {
  203. const cellValue = XEUtils.isUndefined(val) ? XEUtils.get(row, property) : val
  204. rules.forEach(rule => {
  205. if (type === 'all' || !rule.trigger || type === rule.trigger) {
  206. if (XEUtils.isFunction(rule.validator)) {
  207. const customValid = rule.validator({
  208. cellValue,
  209. rule,
  210. rules,
  211. row,
  212. rowIndex: this.getRowIndex(row),
  213. column,
  214. columnIndex: this.getColumnIndex(column),
  215. $table: this
  216. })
  217. if (customValid) {
  218. if (XEUtils.isError(customValid)) {
  219. this.validRuleErr = true
  220. errorRules.push(new Rule({ type: 'custom', trigger: rule.trigger, message: customValid.message, rule: new Rule(rule) }))
  221. } else if (customValid.catch) {
  222. // 如果为异步校验(注:异步校验是并发无序的)
  223. syncVailds.push(
  224. customValid.catch(e => {
  225. this.validRuleErr = true
  226. errorRules.push(new Rule({ type: 'custom', trigger: rule.trigger, message: e ? e.message : rule.message, rule: new Rule(rule) }))
  227. })
  228. )
  229. }
  230. }
  231. } else {
  232. const isNumType = rule.type === 'number'
  233. const isArrType = rule.type === 'array'
  234. const numVal = isNumType ? XEUtils.toNumber(cellValue) : XEUtils.getSize(cellValue)
  235. if (rule.required && (isArrType ? (!XEUtils.isArray(cellValue) || !cellValue.length) : (cellValue === null || cellValue === undefined || cellValue === ''))) {
  236. this.validRuleErr = true
  237. errorRules.push(new Rule(rule))
  238. } else if (
  239. (isNumType && isNaN(cellValue)) ||
  240. (!isNaN(rule.min) && numVal < parseFloat(rule.min)) ||
  241. (!isNaN(rule.max) && numVal > parseFloat(rule.max)) ||
  242. (rule.pattern && !(rule.pattern.test ? rule.pattern : new RegExp(rule.pattern)).test(cellValue))
  243. ) {
  244. this.validRuleErr = true
  245. errorRules.push(new Rule(rule))
  246. }
  247. }
  248. }
  249. })
  250. }
  251. }
  252. return Promise.all(syncVailds).then(() => {
  253. if (errorRules.length) {
  254. const rest = { rules: errorRules, rule: errorRules[0] }
  255. return Promise.reject(rest)
  256. }
  257. })
  258. },
  259. _clearValidate () {
  260. const validTip = this.$refs.validTip
  261. Object.assign(this.validStore, {
  262. visible: false,
  263. row: null,
  264. column: null,
  265. content: '',
  266. rule: null
  267. })
  268. if (validTip && validTip.visible) {
  269. validTip.close()
  270. }
  271. return this.$nextTick()
  272. },
  273. /**
  274. * 触发校验
  275. */
  276. triggerValidate (type) {
  277. const { editConfig, editStore, editRules, validStore } = this
  278. const { actived } = editStore
  279. if (actived.row && editRules) {
  280. const { row, column, cell } = actived.args
  281. if (this.hasCellRules(type, row, column)) {
  282. return this.validCellRules(type, row, column).then(() => {
  283. if (editConfig.mode === 'row') {
  284. if (validStore.visible && validStore.row === row && validStore.column === column) {
  285. this.clearValidate()
  286. }
  287. }
  288. }).catch(({ rule }) => {
  289. // 如果校验不通过与触发方式一致,则聚焦提示错误,否则跳过并不作任何处理
  290. if (!rule.trigger || type === rule.trigger) {
  291. const rest = { rule, row, column, cell }
  292. this.showValidTooltip(rest)
  293. return Promise.reject(rest)
  294. }
  295. return Promise.resolve()
  296. })
  297. }
  298. }
  299. return Promise.resolve()
  300. },
  301. /**
  302. * 弹出校验错误提示
  303. */
  304. showValidTooltip (params) {
  305. const { $refs, height, tableData, validOpts } = this
  306. const { rule, row, column, cell } = params
  307. const validTip = $refs.validTip
  308. const content = rule.message
  309. return this.$nextTick(() => {
  310. Object.assign(this.validStore, {
  311. row,
  312. column,
  313. rule,
  314. content,
  315. visible: true
  316. })
  317. this.emitEvent('valid-error', params)
  318. if (validTip && (validOpts.message === 'tooltip' || (validOpts.message === 'default' && !height && tableData.length < 2))) {
  319. return validTip.open(cell, content)
  320. }
  321. })
  322. }
  323. }
  324. }