import { ref } from "vue"; import Sort from "./sort"; import type { Options, Column, Record, Rule, FieldPath} from "./type" import { awaitTime, validatorDecimal } from "./util"; export default class Validate extends Sort { constructor(options: Options){ super(options) } ValidateErrorTooltips= ref<[Record, Column, any] | []>([]) ValidateErrorTooltipsTimer: any = null isCellError(row: Record, column: Column){ const rowKey = row[this.props.rowKey] const errorKey = Array.isArray(column.dataIndex) ? column.dataIndex.join('-') : column.dataIndex; return this.errorState[rowKey] && this.errorState[rowKey][errorKey] } validate(){ this.clearValidate(); return this.validateFields() } async validateFields(filedList?: FieldPath[], rowindex?: number){ const rules: Rule[] = this.getRules(filedList); if (!rules.length) return; await awaitTime(0); const fns = []; for(const rule of rules){ // 验证所有行 if (rowindex === undefined) fns.push(...await this.validateRows(rule)); // 验证指定行 else if(rowindex !== null) fns.push(this.validateRow(rule, this.dataSource.value[rowindex], rowindex)) } const results = await Promise.allSettled(fns); const errors = results.filter(item => item.status === 'rejected'); if (errors.length) { if (this.props.customEditer?.scrollToFirstError) { this.scrollToField(); } const ethrowInfo: any[] = [] errors.forEach((item: { status: 'rejected', reason: any }) => { ethrowInfo.push(item.reason) }); return Promise.reject(ethrowInfo) ; } } clearValidate(filedList?: FieldPath[], rowindex?: number) { if (filedList ) { const removeError = (rows: Record[], filedList: FieldPath[]) => { rows.forEach((row: Record) => { const rowKey = row[this.props.rowKey] filedList.forEach((field: FieldPath) => { const errorKey = Array.isArray(field) ? field.join('-') : field; if (this.errorState[rowKey] && this.errorState[rowKey][errorKey]) { delete this.errorState[rowKey][errorKey] } }) }) } if (rowindex !== undefined) { removeError([this.dataSource.value[rowindex]], filedList) } else { removeError(this.dataSource.value, filedList) } return; } Object.entries(this.errorState).forEach(([key]) => { delete this.errorState[key] }) this.ValidateErrorTooltips.value = [] } scrollToField() { const errorCell = this.table.value?.querySelector('td.error'); errorCell?.scrollIntoView?.({ behavior: "smooth", block: "center", inline: "nearest" }) } // 验证单个单元格 async tryValidateCell(rowindex: number, colindex: number, trigger: 'blur'| 'change' | 'selected' | 'submit' = 'submit'){ if ([undefined, -1, null, ''].includes(rowindex) || [undefined, -1, null, ''].includes(colindex)) return; if (this.props.keepLastRowEmpty && rowindex === this.dataSource.value.length - 1) return; // console.log(trigger) const row = this.dataSource.value[rowindex] const dataIndex = this.tileColumns.value[colindex].dataIndex; const allRules = this.getRules(); const currentRules = allRules.filter(rule => { const columnErrorKey = Array.isArray(dataIndex) ? dataIndex.join('-') : dataIndex; const errorKey = this.getErrorKey(rule); return columnErrorKey === errorKey; }) // 内容变动时,自动移除错误记录 // console.log(trigger) if (trigger === 'change'){ currentRules.forEach(rule => { this.removeRecordError(rule, row) }); } await awaitTime(0); const rules = currentRules?.filter(rule => { return rule.trigger === trigger; }) rules?.forEach((rule: Rule) => { this.validateRow(rule, row, rowindex)?.then?.(() => { this.removeRecordError(rule, row) })?.catch?.((e) => { if (rule.tooltip) { this.ValidateErrorTooltips.value = [ row, this.tileColumns.value[colindex], e ] clearTimeout(this.ValidateErrorTooltipsTimer) this.ValidateErrorTooltipsTimer = setTimeout(() => { this.ValidateErrorTooltips.value = [] }, 3000) } }) }) } private getRules(filedList?: FieldPath[]) :Rule[] { const columnRules: Rule[] = []; const pushRule = (rules?: Rule[] | Rule, extendField?: Record) => { if (Array.isArray(rules)) columnRules.push( ...rules.map(v => ({ trigger: 'submit', ...extendField, ...v, })) ); else if (rules) columnRules.push({ trigger: 'submit', ...extendField, ...rules }); } let rules: Rule | Rule[] = this.props.customEditer?.rules ?? []; pushRule(rules); this.tileColumns.value.forEach((column: Column) => { pushRule(column.rules, { field: column.dataIndex }) }) const filterRule = (rules: Rule[]): Rule[] => { if(!filedList?.length) return rules; return rules.filter((rule: Rule) => { return filedList.find(field => { // 同数组类型 if(Array.isArray(field) && Array.isArray(rule.field)) return field.join('-') === rule.field.join('-'); return field === rule.field }) }) ?? []; } return filterRule(columnRules); } private async validateRows(rule: Rule): Promise[]>{ const fns = [] // 禁用、只读, 不参与编辑校验 const rows = this.dataSource.value.slice(0, this.props.keepLastRowEmpty ? -1 : undefined).filter((row, index) => { if (this.props.readonly === true) return false; if (this.props.disabled === true) return false; if (Array.isArray(this.props.readonly)){ return !this.props.readonly.includes(row[this.props.rowKey]) } if (Array.isArray(this.props.disabled)){ return !this.props.disabled.includes(row[this.props.rowKey]) } return true; }) let rowindex = 0; for(const row of rows){ const fn = this.validateRow(rule, row, rowindex) fns.push(fn) rowindex += 1; } return fns } private async validateRow(rule: Rule, row: Record, rowindex: number): Promise { const { field, len, min, max, message, pattern, required, validator, precision } = rule; const value = this.getValueByModel(field, row); try{ if (typeof validator === 'function') return await validator({ rule, value, record: row, rowindex }, ( filedList?: FieldPath[], rowindex?: number ) => { return this.validateFields(filedList, rowindex); }) if (required && [undefined, null, ''].includes(value)) throw message; if (pattern instanceof RegExp && !pattern.test(value)) throw message; if (len && String(value).length > len) throw message; if (min !== undefined && Number(value) < min) throw message; if (max !== undefined && Number(value) > max) throw message; if (precision !== undefined) { await validatorDecimal(value, { maxDecimal: precision, message: message }) } } catch (message) { const info = { message, value, rowindex, rule, record: row } this.recordError(rule, row, info) throw info; } } private getErrorKey(rule: Rule): string{ const { field } = rule; const errorKey = Array.isArray(field) ? field.join('-') : field; return errorKey; } private recordError(rule: Rule, row: Record, error: any){ const rowKey = row[this.props.rowKey] const errorKey = this.getErrorKey(rule); if (!this.errorState[rowKey]) { this.errorState[rowKey] = {} } this.errorState[rowKey][errorKey] = error || 'error.'; } private removeRecordError(rule: Rule, row: Record){ const rowKey = row[this.props.rowKey] const errorKey = this.getErrorKey(rule); if (!this.errorState[rowKey]) return; delete this.errorState[rowKey][errorKey]; } checkOpenValidateErrorTooltip(column: Column){ return !!this.getRules([ column.dataIndex ]).find(v => v.tooltip) } checkOpenValidateError(row: Record, column: Column){ return this.ValidateErrorTooltips.value[0] === row && this.ValidateErrorTooltips.value[1] === column } checkOpenValidateErrorMessage(row: Record, column: Column){ if(this.checkOpenValidateError(row, column)) { return this.ValidateErrorTooltips.value[2]?.message } return "" } }