@veltra/utils 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dom/class-name.d.ts +10 -0
- package/dist/dom/class-name.js +16 -0
- package/dist/dom/class-name.js.map +1 -0
- package/dist/dom/highlight.d.ts +14 -0
- package/dist/dom/highlight.js +19 -0
- package/dist/dom/highlight.js.map +1 -0
- package/dist/dom/position.d.ts +28 -0
- package/dist/dom/position.js +59 -0
- package/dist/dom/position.js.map +1 -0
- package/dist/dom/style.d.ts +25 -0
- package/dist/dom/style.js +38 -0
- package/dist/dom/style.js.map +1 -0
- package/dist/dom/z-index.d.ts +9 -0
- package/dist/dom/z-index.js +11 -0
- package/dist/dom/z-index.js.map +1 -0
- package/dist/form/validate.d.ts +33 -0
- package/dist/form/validate.js +197 -0
- package/dist/form/validate.js.map +1 -0
- package/dist/helper/create-increase.d.ts +10 -0
- package/dist/helper/create-increase.js +17 -0
- package/dist/helper/create-increase.js.map +1 -0
- package/dist/helper/create-toggle.d.ts +15 -0
- package/dist/helper/create-toggle.js +24 -0
- package/dist/helper/create-toggle.js.map +1 -0
- package/dist/helper/frame.d.ts +9 -0
- package/dist/helper/frame.js +14 -0
- package/dist/helper/frame.js.map +1 -0
- package/dist/helper/make-bem.d.ts +52 -0
- package/dist/helper/make-bem.js +39 -0
- package/dist/helper/make-bem.js.map +1 -0
- package/dist/helper/tween.d.ts +43 -0
- package/dist/helper/tween.js +89 -0
- package/dist/helper/tween.js.map +1 -0
- package/dist/helper/vue.d.ts +44 -0
- package/dist/helper/vue.js +62 -0
- package/dist/helper/vue.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +15 -0
- package/dist/reactive/proxy.d.ts +19 -0
- package/dist/reactive/proxy.js +57 -0
- package/dist/reactive/proxy.js.map +1 -0
- package/dist/shared/constants.d.ts +10 -0
- package/dist/shared/constants.js +11 -0
- package/dist/shared/constants.js.map +1 -0
- package/dist/types/component-common.d.ts +37 -0
- package/dist/types/form-context.d.ts +6 -0
- package/dist/types/helper.d.ts +22 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.js +0 -0
- package/dist/types/utils/form/validate.d.ts +34 -0
- package/package.json +47 -0
- package/src/dom/class-name.ts +20 -0
- package/src/dom/highlight.ts +23 -0
- package/src/dom/position.ts +96 -0
- package/src/dom/style.ts +60 -0
- package/src/dom/z-index.ts +7 -0
- package/src/env.d.ts +1 -0
- package/src/form/validate.ts +278 -0
- package/src/helper/create-increase.ts +14 -0
- package/src/helper/create-toggle.ts +29 -0
- package/src/helper/frame.ts +9 -0
- package/src/helper/make-bem.ts +109 -0
- package/src/helper/tween.ts +123 -0
- package/src/helper/vue.ts +88 -0
- package/src/index.ts +28 -0
- package/src/reactive/proxy.ts +90 -0
- package/src/shared/constants.ts +8 -0
- package/src/shared/index.ts +1 -0
- package/src/types/component-common.ts +41 -0
- package/src/types/form-context.ts +2 -0
- package/src/types/helper.ts +31 -0
- package/src/types/index.ts +4 -0
- package/src/types/utils/form/validate.ts +33 -0
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@veltra/utils",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"files": [
|
|
5
|
+
"dist",
|
|
6
|
+
"src"
|
|
7
|
+
],
|
|
8
|
+
"type": "module",
|
|
9
|
+
"sideEffects": [
|
|
10
|
+
"*.css"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"development": "./src/index.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./types": {
|
|
19
|
+
"types": "./dist/types/index.d.ts",
|
|
20
|
+
"development": "./src/types/index.ts",
|
|
21
|
+
"import": "./dist/types/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./types/*": {
|
|
24
|
+
"types": "./dist/types/*.d.ts",
|
|
25
|
+
"development": "./src/types/*",
|
|
26
|
+
"import": "./dist/types/*.js"
|
|
27
|
+
},
|
|
28
|
+
"./*": {
|
|
29
|
+
"types": "./dist/*.d.ts",
|
|
30
|
+
"development": "./src/*",
|
|
31
|
+
"import": "./dist/*.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsdown",
|
|
36
|
+
"check-types": "tsc -p tsconfig.json --noEmit"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@cat-kit/core": "^1.0.1"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"tsdown": "^0.21.7"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"vue": "^3.5.0"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { makeBEM, type BEMFactory } from '../helper/make-bem'
|
|
2
|
+
import { CLS_PREFIX } from '../shared/constants'
|
|
3
|
+
|
|
4
|
+
export const bem: BEMFactory<typeof CLS_PREFIX> = makeBEM(CLS_PREFIX)
|
|
5
|
+
|
|
6
|
+
export function addClass(el: HTMLElement, className: string | string[]): void {
|
|
7
|
+
if (Array.isArray(className)) {
|
|
8
|
+
className.forEach((c) => el.classList.add(c))
|
|
9
|
+
} else {
|
|
10
|
+
el.classList.add(className)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function removeClass(el: HTMLElement, className: string | string[]): void {
|
|
15
|
+
if (Array.isArray(className)) {
|
|
16
|
+
className.forEach((c) => el.classList.remove(c))
|
|
17
|
+
} else {
|
|
18
|
+
el.classList.remove(className)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// TODO: 在多种场景下, 比较KMP算法, BM算法, two-way算法是否比正则更快
|
|
2
|
+
|
|
3
|
+
interface HighlightChunk {
|
|
4
|
+
text: string
|
|
5
|
+
highlight: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const escapeRegexp = (term: string): string =>
|
|
9
|
+
term.replace(/[|\\{}()[\]^$+*?.-]/g, (char: string) => `\\${char}`)
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 获取文本高亮片段
|
|
13
|
+
* @param string 字符串
|
|
14
|
+
* @param substrings 需要匹配的字串列表
|
|
15
|
+
*/
|
|
16
|
+
export function getHighlightChunks(str: string, substrings: string[]): HighlightChunk[] {
|
|
17
|
+
const _substrings = substrings.filter((s) => !!s).map((s) => escapeRegexp(s.trim()))
|
|
18
|
+
const re = new RegExp(`(${_substrings.join('|')})`, 'gi')
|
|
19
|
+
return str
|
|
20
|
+
.split(re)
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.map((text) => ({ text, highlight: re.test(text) }))
|
|
23
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 获取可滚动的父级
|
|
3
|
+
* @param el 元素
|
|
4
|
+
* @returns
|
|
5
|
+
*/
|
|
6
|
+
export function getScrollParents(el: HTMLElement): HTMLElement[] {
|
|
7
|
+
const parents: HTMLElement[] = []
|
|
8
|
+
let parent = el.parentElement
|
|
9
|
+
while (parent) {
|
|
10
|
+
if (parent.scrollHeight > parent.clientHeight) {
|
|
11
|
+
parents.push(parent)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
parent = parent.parentElement
|
|
15
|
+
}
|
|
16
|
+
return parents
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 获取最近的可滚动父级
|
|
21
|
+
* @param el 元素
|
|
22
|
+
* @returns 最近的可滚动父级
|
|
23
|
+
*/
|
|
24
|
+
export function getNearestScrollParent(el: HTMLElement): HTMLElement | null {
|
|
25
|
+
let parent = el.parentElement
|
|
26
|
+
while (parent) {
|
|
27
|
+
if (parent.scrollHeight > parent.clientHeight) {
|
|
28
|
+
return parent
|
|
29
|
+
}
|
|
30
|
+
parent = parent.parentElement
|
|
31
|
+
}
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type ScrollViewPosition = 'center' | 'start' | 'end'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 滚动元素到容器视图中
|
|
39
|
+
* @description 用于替代 `el.scrollIntoView` 方法,因为 `el.scrollIntoView` 在某些情况下会导致外部元素滚动
|
|
40
|
+
* @param el 元素
|
|
41
|
+
* @param container 可滚动容器
|
|
42
|
+
* @param options 滚动选项
|
|
43
|
+
*/
|
|
44
|
+
export function scrollIntoContainerView(
|
|
45
|
+
el: HTMLElement,
|
|
46
|
+
container: HTMLElement | null,
|
|
47
|
+
options?: { block?: ScrollViewPosition; inline?: ScrollViewPosition }
|
|
48
|
+
): void {
|
|
49
|
+
container = container || getNearestScrollParent(el)
|
|
50
|
+
if (!container) return
|
|
51
|
+
|
|
52
|
+
const { block = 'center', inline = 'center' } = options || {}
|
|
53
|
+
|
|
54
|
+
const {
|
|
55
|
+
offsetTop: eOffsetTop,
|
|
56
|
+
offsetLeft: eOffsetLeft,
|
|
57
|
+
offsetHeight: eOffsetHeight,
|
|
58
|
+
offsetWidth: eOffsetWidth
|
|
59
|
+
} = el
|
|
60
|
+
|
|
61
|
+
const {
|
|
62
|
+
clientHeight: cClientHeight,
|
|
63
|
+
clientWidth: cClientWidth,
|
|
64
|
+
scrollTop: cScrollTop,
|
|
65
|
+
scrollLeft: cScrollLeft
|
|
66
|
+
} = container
|
|
67
|
+
|
|
68
|
+
const isVerticalInView =
|
|
69
|
+
cScrollTop + cClientHeight > eOffsetTop + eOffsetHeight && cScrollTop < eOffsetTop
|
|
70
|
+
|
|
71
|
+
const isHorizontalInView =
|
|
72
|
+
cScrollLeft + cClientWidth > eOffsetLeft + eOffsetWidth && cScrollLeft < eOffsetLeft
|
|
73
|
+
|
|
74
|
+
// 垂直方向和水平方向都已经在视图中,则不进行滚动
|
|
75
|
+
if (isVerticalInView && isHorizontalInView) return
|
|
76
|
+
|
|
77
|
+
if (!isVerticalInView) {
|
|
78
|
+
if (block === 'center') {
|
|
79
|
+
container.scrollTop = eOffsetTop - cClientHeight / 2 + eOffsetHeight / 2
|
|
80
|
+
} else if (block === 'start') {
|
|
81
|
+
container.scrollTop = eOffsetTop
|
|
82
|
+
} else if (block === 'end') {
|
|
83
|
+
container.scrollTop = eOffsetTop - cClientHeight + eOffsetHeight
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!isHorizontalInView) {
|
|
88
|
+
if (inline === 'center') {
|
|
89
|
+
container.scrollLeft = eOffsetLeft - cClientWidth / 2 + eOffsetWidth / 2
|
|
90
|
+
} else if (inline === 'start') {
|
|
91
|
+
container.scrollLeft = eOffsetLeft
|
|
92
|
+
} else if (inline === 'end') {
|
|
93
|
+
container.scrollLeft = eOffsetLeft - cClientWidth + eOffsetWidth
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
package/src/dom/style.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { str } from '@cat-kit/core'
|
|
2
|
+
import type { CSSProperties } from 'vue'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 给数值加上单位
|
|
6
|
+
* @param value 数值
|
|
7
|
+
* @param unit 单位
|
|
8
|
+
* @returns
|
|
9
|
+
*/
|
|
10
|
+
export function withUnit(value: number | string | undefined, unit: string): string | undefined {
|
|
11
|
+
return value === undefined
|
|
12
|
+
? undefined
|
|
13
|
+
: typeof value === 'number' || !isNaN(+value)
|
|
14
|
+
? String(value) + unit
|
|
15
|
+
: value
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 设置元素样式,优先使用高性能的方式
|
|
20
|
+
* @param el 元素
|
|
21
|
+
* @param styles 样式
|
|
22
|
+
*/
|
|
23
|
+
export function setStyles(el: HTMLElement, styles: CSSProperties): void {
|
|
24
|
+
Object.keys(styles).forEach((key) => {
|
|
25
|
+
el.style[key] = styles[key]
|
|
26
|
+
})
|
|
27
|
+
// TODO: 此处有问题,在某些情况下会导致样式设置失效
|
|
28
|
+
// 例如在 Tabs 组件中无法设置overflow属性
|
|
29
|
+
// if (el.attributeStyleMap) {
|
|
30
|
+
// Object.keys(styles).forEach(key => {
|
|
31
|
+
// const value = styles[key]
|
|
32
|
+
// if (!value && value !== 0) {
|
|
33
|
+
// el.attributeStyleMap.delete(kebabCase(key))
|
|
34
|
+
// } else {
|
|
35
|
+
// el.attributeStyleMap.set(kebabCase(key), value)
|
|
36
|
+
// }
|
|
37
|
+
// })
|
|
38
|
+
// } else {
|
|
39
|
+
// Object.keys(styles).forEach(key => {
|
|
40
|
+
// el.style[key] = styles[key]
|
|
41
|
+
// })
|
|
42
|
+
// }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 移除样式
|
|
47
|
+
* @param el dom元素
|
|
48
|
+
* @param props 要移除的样式属性
|
|
49
|
+
*/
|
|
50
|
+
export function removeStyles(el: HTMLElement, props: string[]): void {
|
|
51
|
+
if (el.attributeStyleMap) {
|
|
52
|
+
props.forEach((key) => {
|
|
53
|
+
el.attributeStyleMap.delete(str(key).kebabCase())
|
|
54
|
+
})
|
|
55
|
+
} else {
|
|
56
|
+
props.forEach((key) => {
|
|
57
|
+
el.style.removeProperty(key)
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module '*.scss' {}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { o } from '@cat-kit/core'
|
|
2
|
+
|
|
3
|
+
import type { Undef } from '../types/helper'
|
|
4
|
+
import type { ValidateRule, Data, ValidatorConfig, PresetRule } from '../types/utils/form/validate'
|
|
5
|
+
|
|
6
|
+
const isEmpty = (value: any): value is null | undefined => {
|
|
7
|
+
return value === null || value === undefined
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const presetRules: Record<PresetRule, (value: string) => string | undefined> = {
|
|
11
|
+
email(v) {
|
|
12
|
+
const re = /^([\w_-]+)@([\w-]+[.]?)*[\w]+\.[a-zA-Z]{2,10}$/
|
|
13
|
+
if (!re.test(v)) {
|
|
14
|
+
return '邮箱格式不正确'
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
phone(v) {
|
|
18
|
+
const re = /^\d{11}$/
|
|
19
|
+
if (!re.test(v)) {
|
|
20
|
+
return '手机号格式不正确'
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
num(v) {
|
|
24
|
+
const re = /^\d+$/
|
|
25
|
+
if (!re.test(v)) {
|
|
26
|
+
return '数字格式不正确'
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
url(v) {
|
|
30
|
+
const re = /^(ftp|https?):\/\/([\w_-]+)\.([\w-]+[.]?)*[\w]+\.[a-zA-Z]{2,10}(.*)/
|
|
31
|
+
if (!re.test(v)) {
|
|
32
|
+
return '链接格式不正确'
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
idCard(v) {
|
|
37
|
+
const re = /^(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([0-9]|X)$/
|
|
38
|
+
if (!re.test(v)) {
|
|
39
|
+
return '身份证格式不正确'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 预设规则 */
|
|
45
|
+
const ruleTypes = {
|
|
46
|
+
required(value: any, required: ValidateRule['required']): Undef<string> {
|
|
47
|
+
if (required === false) return
|
|
48
|
+
|
|
49
|
+
const errMsg = typeof required === 'string' ? required : '该项不能为空'
|
|
50
|
+
if (isEmpty(value)) return errMsg
|
|
51
|
+
|
|
52
|
+
if (Array.isArray(value) && !value.length) return errMsg
|
|
53
|
+
if (typeof value === 'string' && !value) return errMsg
|
|
54
|
+
},
|
|
55
|
+
min(value: any, rule: ValidateRule['min']): Undef<string> {
|
|
56
|
+
if (isEmpty(value)) return
|
|
57
|
+
let _rule = Array.isArray(rule) ? rule[0] : rule!
|
|
58
|
+
let errMsg = Array.isArray(rule) ? rule[1] : `该项必须大于等于${_rule}`
|
|
59
|
+
if (typeof value !== 'number') return `${value}不是一个数字`
|
|
60
|
+
if (value < _rule) return errMsg
|
|
61
|
+
},
|
|
62
|
+
max(value: any, rule: ValidateRule['max']): Undef<string> {
|
|
63
|
+
if (isEmpty(value)) return
|
|
64
|
+
let _rule = Array.isArray(rule) ? rule[0] : rule!
|
|
65
|
+
let errMsg = Array.isArray(rule) ? rule[1] : `该项必须小于等于${_rule}`
|
|
66
|
+
if (typeof value !== 'number') return `${value}不是一个数字`
|
|
67
|
+
if (value > _rule) return errMsg
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
minLen(value: any, rule: ValidateRule['minLen']): Undef<string> {
|
|
71
|
+
if (isEmpty(value)) return
|
|
72
|
+
let _rule = Array.isArray(rule) ? rule[0] : rule!
|
|
73
|
+
let errMsg = Array.isArray(rule) ? rule[1] : `该项长度必须大于等于${_rule}`
|
|
74
|
+
if (!Array.isArray(value) && typeof value !== 'string') return `${value}不是一个字符串或数组`
|
|
75
|
+
if (value.length < _rule) return errMsg
|
|
76
|
+
},
|
|
77
|
+
maxLen(value: any, rule: ValidateRule['maxLen']): Undef<string> {
|
|
78
|
+
if (isEmpty(value)) return
|
|
79
|
+
let _rule = Array.isArray(rule) ? rule[0] : rule!
|
|
80
|
+
let errMsg = Array.isArray(rule) ? rule[1] : `该项长度必须小于等于:${_rule}`
|
|
81
|
+
if (!Array.isArray(value) && typeof value !== 'string') return `${value}不是一个字符串或数组`
|
|
82
|
+
if (value.length > _rule) return errMsg
|
|
83
|
+
},
|
|
84
|
+
match(value: any, rule: ValidateRule['match']): Undef<string> {
|
|
85
|
+
if (isEmpty(value) || value === '') return
|
|
86
|
+
if (typeof rule === 'string') {
|
|
87
|
+
if (!rule) return
|
|
88
|
+
rule = new RegExp(rule)
|
|
89
|
+
}
|
|
90
|
+
let _rule = Array.isArray(rule) ? rule[0] : rule!
|
|
91
|
+
let errMsg = Array.isArray(rule) ? rule[1] : `该项不匹配正则:${_rule}`
|
|
92
|
+
if (typeof value !== 'string') return `${value}不是一个字符串`
|
|
93
|
+
if (!_rule.test(value)) return errMsg
|
|
94
|
+
},
|
|
95
|
+
preset(value: any, rule: ValidateRule['preset']): Undef<string> {
|
|
96
|
+
if (isEmpty(value) || value === '' || !rule) return
|
|
97
|
+
if (typeof value !== 'string') return `${value}不是一个字符串`
|
|
98
|
+
const ruleValidator = presetRules[rule]
|
|
99
|
+
return ruleValidator(value)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** */
|
|
103
|
+
export class Validator<
|
|
104
|
+
Rules extends Record<string, ValidateRule> = Record<string, ValidateRule>,
|
|
105
|
+
Field extends keyof Rules = keyof Rules
|
|
106
|
+
> {
|
|
107
|
+
protected rules: Rules
|
|
108
|
+
|
|
109
|
+
private config?: ValidatorConfig
|
|
110
|
+
|
|
111
|
+
constructor(rules: Rules, config?: ValidatorConfig) {
|
|
112
|
+
this.rules = rules
|
|
113
|
+
if (config) {
|
|
114
|
+
this.config = config
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 是否存在规则 */
|
|
119
|
+
private get existRules() {
|
|
120
|
+
for (const _ in this.rules) {
|
|
121
|
+
return true
|
|
122
|
+
}
|
|
123
|
+
return false
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 校验单条数据
|
|
128
|
+
* @param data 数据
|
|
129
|
+
*/
|
|
130
|
+
private async validateSingleData(
|
|
131
|
+
data: Record<any, any>,
|
|
132
|
+
fields?: Field | Field[]
|
|
133
|
+
): Promise<{ [key in Field]?: string[] }> {
|
|
134
|
+
const fieldErrors: { [key in Field]?: string[] } = {}
|
|
135
|
+
|
|
136
|
+
if (!this.existRules) return fieldErrors
|
|
137
|
+
|
|
138
|
+
const lazy = this.config?.lazy ?? true
|
|
139
|
+
|
|
140
|
+
fields = fields
|
|
141
|
+
? Array.isArray(fields)
|
|
142
|
+
? fields
|
|
143
|
+
: [fields]
|
|
144
|
+
: (Object.keys(this.rules) as Field[])
|
|
145
|
+
|
|
146
|
+
// 懒校验,当有一个规则不通过,剩下的规则不再校验
|
|
147
|
+
if (lazy) {
|
|
148
|
+
// 校验字段
|
|
149
|
+
for (let i = 0; i < fields.length; i++) {
|
|
150
|
+
const field = fields[i]!
|
|
151
|
+
// oxlint-disable-next-line eslint(no-await-in-loop)
|
|
152
|
+
const errors = await this.validateValueLazy(data, field)
|
|
153
|
+
if (errors.length === 0) continue
|
|
154
|
+
fieldErrors[field] = errors
|
|
155
|
+
}
|
|
156
|
+
return fieldErrors
|
|
157
|
+
} else {
|
|
158
|
+
// 校验字段
|
|
159
|
+
for (let i = 0; i < fields.length; i++) {
|
|
160
|
+
const field = fields[i]!
|
|
161
|
+
// oxlint-disable-next-line eslint(no-await-in-loop)
|
|
162
|
+
const errors = await this.validateValue(data, field)
|
|
163
|
+
if (errors.length === 0) continue
|
|
164
|
+
fieldErrors[field] = errors
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return fieldErrors
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private async validateValue(data: Record<any, any>, field: Field) {
|
|
172
|
+
const rules = this.rules[field]!
|
|
173
|
+
const value = o(data).get(field as string)
|
|
174
|
+
|
|
175
|
+
const { validator, required, ...normalRules } = rules
|
|
176
|
+
|
|
177
|
+
let errors: string[] = []
|
|
178
|
+
|
|
179
|
+
// 必填要先去校验
|
|
180
|
+
if (required) {
|
|
181
|
+
const err = ruleTypes.required(value, required)
|
|
182
|
+
err && errors.push(err)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 校验规则
|
|
186
|
+
for (const ruleKey in normalRules) {
|
|
187
|
+
const validate = ruleTypes[ruleKey]
|
|
188
|
+
if (!validate) continue
|
|
189
|
+
const err = validate(value, normalRules[ruleKey])
|
|
190
|
+
err && errors.push(err)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 自定义校验
|
|
194
|
+
if (validator) {
|
|
195
|
+
const err = await validator(value, data)
|
|
196
|
+
err && errors.push(err)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return errors
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private async validateValueLazy(data: Record<any, any>, field: Field): Promise<string[]> {
|
|
203
|
+
const rules = this.rules[field]
|
|
204
|
+
const value = o(data).get(field as string)
|
|
205
|
+
let errors: string[] = []
|
|
206
|
+
|
|
207
|
+
if (!rules) return errors
|
|
208
|
+
|
|
209
|
+
const { validator, required, ...normalRules } = rules
|
|
210
|
+
|
|
211
|
+
// 必填要先去校验
|
|
212
|
+
if (required) {
|
|
213
|
+
const err = ruleTypes.required(value, required)
|
|
214
|
+
if (err) {
|
|
215
|
+
errors.push(err)
|
|
216
|
+
return errors
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 校验规则
|
|
221
|
+
for (const ruleKey in normalRules) {
|
|
222
|
+
const validate = ruleTypes[ruleKey]
|
|
223
|
+
if (!validate) continue
|
|
224
|
+
|
|
225
|
+
const err = validate(value, normalRules[ruleKey])
|
|
226
|
+
|
|
227
|
+
if (err) {
|
|
228
|
+
errors.push(err)
|
|
229
|
+
return errors
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 自定义校验最火校验
|
|
234
|
+
if (validator) {
|
|
235
|
+
const err = await validator(value, data)
|
|
236
|
+
if (err) {
|
|
237
|
+
errors.push(err)
|
|
238
|
+
return errors
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return errors
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 校验多条数据
|
|
247
|
+
* @param field 需要校验的字段
|
|
248
|
+
*/
|
|
249
|
+
private async validateManyData(
|
|
250
|
+
data: Data,
|
|
251
|
+
field?: Field | Field[]
|
|
252
|
+
): Promise<{ [key in Field]?: string[] }> {
|
|
253
|
+
let i = 0
|
|
254
|
+
while (i < data.length) {
|
|
255
|
+
const item = data[i]!
|
|
256
|
+
// oxlint-disable-next-line eslint(no-await-in-loop)
|
|
257
|
+
const fieldErrors = await this.validateSingleData(item, field)
|
|
258
|
+
if (Object.keys(fieldErrors).length > 0) {
|
|
259
|
+
return fieldErrors
|
|
260
|
+
}
|
|
261
|
+
i++
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 校验
|
|
269
|
+
* @param data 数据
|
|
270
|
+
* @param fields 字段
|
|
271
|
+
* @returns
|
|
272
|
+
*/
|
|
273
|
+
async validate(data: Data, fields?: Field | Field[]): Promise<{ [key in Field]?: string[] }> {
|
|
274
|
+
return Array.isArray(data)
|
|
275
|
+
? this.validateManyData(data, fields)
|
|
276
|
+
: this.validateSingleData(data, fields)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
type Active = boolean | ((active: boolean) => boolean) | ((active: boolean) => Promise<boolean>)
|
|
2
|
+
|
|
3
|
+
type ToggleReturn = [{ value: boolean }, (active: Active) => void]
|
|
4
|
+
/**
|
|
5
|
+
* 创建一个toggle函数
|
|
6
|
+
* @param initial 初始值
|
|
7
|
+
* @param onChange 值变化时的回调
|
|
8
|
+
* @returns
|
|
9
|
+
*/
|
|
10
|
+
export function createToggle(initial = false, onChange?: (active: boolean) => void): ToggleReturn {
|
|
11
|
+
const state = { value: initial }
|
|
12
|
+
|
|
13
|
+
function toggle(active: Active) {
|
|
14
|
+
if (typeof active === 'boolean') {
|
|
15
|
+
state.value = active
|
|
16
|
+
return onChange?.(state.value)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const result = active(state.value)
|
|
20
|
+
|
|
21
|
+
if (result instanceof Promise) {
|
|
22
|
+
result.then(toggle)
|
|
23
|
+
} else {
|
|
24
|
+
toggle(result)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return [state, toggle]
|
|
29
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** BEM实例 */
|
|
2
|
+
export type BEM<N extends string, P extends string = 'u-', B extends string = `${P}${N}`> = {
|
|
3
|
+
/** BEM中的块 */
|
|
4
|
+
b: B
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* BEM中的元素(E)
|
|
8
|
+
* @param name 元素名称
|
|
9
|
+
* @returns
|
|
10
|
+
*/
|
|
11
|
+
e<const E extends string>(name: E): `${B}__${E}`
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 基于当前bem创建一个新的bem
|
|
15
|
+
* @param block 块名称
|
|
16
|
+
*/
|
|
17
|
+
create<const Block extends string>(block: Block): BEM<`${N}-${Block}`, P>
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* BEM中的修饰符(M)
|
|
21
|
+
* @param m 修饰符名
|
|
22
|
+
* @returns 修饰符
|
|
23
|
+
*/
|
|
24
|
+
m<const M extends string>(m: M): `${B}--${M}`
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* BEM中的元素与修饰符(E--M)
|
|
28
|
+
* @param e 元素名
|
|
29
|
+
* @param m 修饰符名
|
|
30
|
+
* @returns
|
|
31
|
+
*/
|
|
32
|
+
em<const E extends string, const M extends string>(e: E, m: M): `${B}__${E}--${M}`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** BEM工厂 */
|
|
36
|
+
export interface BEMFactory<Prefix extends string> {
|
|
37
|
+
<N extends string>(name: N): BEM<N, Prefix>
|
|
38
|
+
/**
|
|
39
|
+
* 生成is辅助类
|
|
40
|
+
* @param name 辅助类名称
|
|
41
|
+
*/
|
|
42
|
+
is<const N extends string>(name: N): `is-${N}`
|
|
43
|
+
/**
|
|
44
|
+
* 生成is辅助类
|
|
45
|
+
* @param name 辅助类名称
|
|
46
|
+
* @param condition 辅助类显示条件
|
|
47
|
+
*/
|
|
48
|
+
is<const N extends string, C extends boolean | undefined>(
|
|
49
|
+
name: N,
|
|
50
|
+
condition: C
|
|
51
|
+
): C extends true ? `is-${N}` : ''
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 创建一个bem函数
|
|
56
|
+
* @param prefix 前缀
|
|
57
|
+
*/
|
|
58
|
+
export function makeBEM<Prefix extends '' | `${string}-`>(prefix: Prefix): BEMFactory<Prefix> {
|
|
59
|
+
/**
|
|
60
|
+
* css类命名辅助
|
|
61
|
+
* @param name 类block名称
|
|
62
|
+
*/
|
|
63
|
+
function bem<N extends string>(name: N): BEM<N, Prefix> {
|
|
64
|
+
const b = `${prefix}${name}` as BEM<N, Prefix>['b']
|
|
65
|
+
return {
|
|
66
|
+
b,
|
|
67
|
+
|
|
68
|
+
e(name) {
|
|
69
|
+
return `${b}__${name}`
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
create(block) {
|
|
73
|
+
return bem(`${name}-${block}`)
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
m(m) {
|
|
77
|
+
return `${b}--${m}`
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
em(e, m) {
|
|
81
|
+
return `${b}__${e}--${m}`
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 生成is辅助类
|
|
88
|
+
* @param name 辅助类名称
|
|
89
|
+
*/
|
|
90
|
+
function is<const N extends string>(name: N): `is-${N}`
|
|
91
|
+
/**
|
|
92
|
+
* 生成is辅助类
|
|
93
|
+
* @param name 辅助类名称
|
|
94
|
+
* @param condition 辅助类显示条件
|
|
95
|
+
*/
|
|
96
|
+
function is<const N extends string, C extends boolean | undefined>(
|
|
97
|
+
name: N,
|
|
98
|
+
condition: C
|
|
99
|
+
): C extends true ? `is-${N}` : ''
|
|
100
|
+
|
|
101
|
+
function is<N extends string>(name: N, condition?: boolean) {
|
|
102
|
+
if (arguments.length < 2) return `is-${name}`
|
|
103
|
+
return condition !== true ? '' : (`is-${name}` as const)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
bem.is = is
|
|
107
|
+
|
|
108
|
+
return bem
|
|
109
|
+
}
|