@wwog/react 1.3.6 → 1.3.9

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.
@@ -0,0 +1,340 @@
1
+ // ==== 基础规则类型 ====
2
+ export type BaseRule<TAll, TValue> = {
3
+ required?: boolean;
4
+ message?: string;
5
+ validator?: (value: TValue, data: Partial<TAll>) => boolean | string;
6
+ dependsOn?: (data: Partial<TAll>) => boolean | string; // 支持依赖校验
7
+ };
8
+
9
+ // ==== 长度相关规则(字符串和数组共用) ====
10
+ export type LengthRuleProps = {
11
+ min?: number;
12
+ max?: number;
13
+ len?: number;
14
+ };
15
+
16
+ // ==== 数字范围规则 ====
17
+ export type NumberRangeProps = {
18
+ min?: number;
19
+ max?: number;
20
+ };
21
+
22
+ // ==== 字符串特有规则 ====
23
+ export type StringSpecificProps = {
24
+ regex?: RegExp;
25
+ email?: boolean;
26
+ url?: boolean;
27
+ phone?: boolean;
28
+ };
29
+
30
+ // ==== 数组特有规则 ====
31
+ export type ArraySpecificProps = {
32
+ unique?: boolean;
33
+ };
34
+
35
+ // ==== 各类型规则 ====
36
+ export type NumberRule<TAll> = BaseRule<TAll, number> & NumberRangeProps;
37
+
38
+ export type StringRule<TAll> = BaseRule<TAll, string> &
39
+ LengthRuleProps &
40
+ StringSpecificProps;
41
+
42
+ export type BooleanRule<TAll> = BaseRule<TAll, boolean>;
43
+
44
+ export type ArrayRule<TAll, U> = BaseRule<TAll, U[]> &
45
+ LengthRuleProps &
46
+ ArraySpecificProps & {
47
+ elementRule?: FieldRule<U, TAll>; // 元素级规则
48
+ };
49
+
50
+ // ==== 泛型推导:字段类型 → 规则类型 ====
51
+ export type FieldRule<TValue, TAll> = TValue extends string
52
+ ? StringRule<TAll>
53
+ : TValue extends number
54
+ ? NumberRule<TAll>
55
+ : TValue extends boolean
56
+ ? BooleanRule<TAll>
57
+ : TValue extends (infer U)[]
58
+ ? ArrayRule<TAll, U>
59
+ : BaseRule<TAll, TValue>;
60
+
61
+ // ==== 描述对象 ====
62
+ export type RuleDescription<T extends Record<string, unknown>> = {
63
+ [K in keyof T]?: FieldRule<T[K], T> | FieldRule<T[K], T>[];
64
+ };
65
+
66
+ // ==== Required 判断 ====
67
+ type IsRequired<R> = R extends { required: true } ? true : false;
68
+
69
+ export type ApplyRules<
70
+ T extends Record<string, unknown>,
71
+ R extends RuleDescription<T>,
72
+ > = {
73
+ [K in keyof T]: IsRequired<R[K]> extends true ? T[K] : T[K] | undefined;
74
+ };
75
+
76
+ // ==== 错误收集 ====
77
+ type FieldErrors<T> = Partial<Record<keyof T, string[]>>;
78
+
79
+ function pushError<T extends Record<string, unknown>>(
80
+ fieldErrors: FieldErrors<T>,
81
+ field: keyof T,
82
+ msg: string
83
+ ) {
84
+ if (!msg) return;
85
+ if (!fieldErrors[field]) fieldErrors[field] = [];
86
+ (fieldErrors[field] as string[]).push(msg);
87
+ }
88
+
89
+ function isPresent(value: unknown): boolean {
90
+ if (value === null || value === undefined) return false;
91
+ if (typeof value === 'string') return value.trim().length > 0;
92
+ if (Array.isArray(value)) return value.length > 0;
93
+ return true;
94
+ }
95
+
96
+ function isValueProvided(value: unknown): boolean {
97
+ return value !== null && value !== undefined;
98
+ }
99
+
100
+ function defaultMsg(field: string | number | symbol, reason: string) {
101
+ return `${String(field)} ${reason}`;
102
+ }
103
+
104
+ // ==== 内置校验器 ====
105
+ const builtins = {
106
+ email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
107
+ url: /^(https?:\/\/)?([\w.-]+)\.([a-z]{2,6})([/\w .-]*)*\/?$/,
108
+ phone: /^1[3-9]\d{9}$/, // 中国手机号
109
+ };
110
+
111
+ // ==== 单字段校验 ====
112
+ function validateRule<T extends Record<string, unknown>, V>(
113
+ key: keyof T,
114
+ value: V,
115
+ rule: FieldRule<V, T>,
116
+ data: Partial<T>,
117
+ fieldErrors: FieldErrors<T>
118
+ ) {
119
+ const present = isPresent(value);
120
+ const valueProvided = isValueProvided(value);
121
+
122
+ // required
123
+ if (rule.required && !present) {
124
+ pushError(fieldErrors, key, rule.message ?? defaultMsg(key, '为必填项'));
125
+ return;
126
+ }
127
+ // 如果值没有提供且不是必填,跳过验证
128
+ if (!valueProvided && !rule.required) return;
129
+
130
+ // dependsOn
131
+ if (rule.dependsOn) {
132
+ const res = rule.dependsOn(data);
133
+ if (res === false)
134
+ pushError(
135
+ fieldErrors,
136
+ key,
137
+ rule.message ?? defaultMsg(key, '依赖条件未满足')
138
+ );
139
+ else if (typeof res === 'string') pushError(fieldErrors, key, res);
140
+ }
141
+
142
+ // string 相关校验
143
+ if (typeof value === 'string') {
144
+ const stringRule = rule as StringRule<T>;
145
+ const { len, min, max, regex, email, url, phone } = stringRule;
146
+
147
+ if (typeof len === 'number' && value.length !== len) {
148
+ pushError(
149
+ fieldErrors,
150
+ key,
151
+ rule.message ?? defaultMsg(key, `长度必须为 ${len}`)
152
+ );
153
+ }
154
+ if (typeof min === 'number' && value.length < min) {
155
+ pushError(
156
+ fieldErrors,
157
+ key,
158
+ rule.message ?? defaultMsg(key, `长度不能少于 ${min}`)
159
+ );
160
+ }
161
+ if (typeof max === 'number' && value.length > max) {
162
+ pushError(
163
+ fieldErrors,
164
+ key,
165
+ rule.message ?? defaultMsg(key, `长度不能超过 ${max}`)
166
+ );
167
+ }
168
+ if (regex && !regex.test(value)) {
169
+ pushError(
170
+ fieldErrors,
171
+ key,
172
+ rule.message ?? defaultMsg(key, '格式不正确')
173
+ );
174
+ }
175
+ if (email && !builtins.email.test(value)) {
176
+ pushError(
177
+ fieldErrors,
178
+ key,
179
+ rule.message ?? defaultMsg(key, '不是有效的邮箱')
180
+ );
181
+ }
182
+ if (url && !builtins.url.test(value)) {
183
+ pushError(
184
+ fieldErrors,
185
+ key,
186
+ rule.message ?? defaultMsg(key, '不是有效的URL')
187
+ );
188
+ }
189
+ if (phone && !builtins.phone.test(value)) {
190
+ pushError(
191
+ fieldErrors,
192
+ key,
193
+ rule.message ?? defaultMsg(key, '不是有效的手机号')
194
+ );
195
+ }
196
+ }
197
+
198
+ // number 相关校验
199
+ if (typeof value === 'number') {
200
+ const numberRule = rule as NumberRule<T>;
201
+ const { min, max } = numberRule;
202
+ if (typeof min === 'number' && value < min) {
203
+ pushError(
204
+ fieldErrors,
205
+ key,
206
+ rule.message ?? defaultMsg(key, `不能小于 ${min}`)
207
+ );
208
+ }
209
+ if (typeof max === 'number' && value > max) {
210
+ pushError(
211
+ fieldErrors,
212
+ key,
213
+ rule.message ?? defaultMsg(key, `不能大于 ${max}`)
214
+ );
215
+ }
216
+ }
217
+
218
+ // array 相关校验
219
+ if (Array.isArray(value)) {
220
+ const arrayRule = rule as ArrayRule<T, unknown>;
221
+ const { len, min, max, unique, elementRule } = arrayRule;
222
+
223
+ if (typeof len === 'number' && value.length !== len) {
224
+ pushError(
225
+ fieldErrors,
226
+ key,
227
+ rule.message ?? defaultMsg(key, `长度必须为 ${len}`)
228
+ );
229
+ }
230
+ if (typeof min === 'number' && value.length < min) {
231
+ pushError(
232
+ fieldErrors,
233
+ key,
234
+ rule.message ?? defaultMsg(key, `长度不能小于 ${min}`)
235
+ );
236
+ }
237
+ if (typeof max === 'number' && value.length > max) {
238
+ pushError(
239
+ fieldErrors,
240
+ key,
241
+ rule.message ?? defaultMsg(key, `长度不能大于 ${max}`)
242
+ );
243
+ }
244
+ if (unique && new Set(value).size !== value.length) {
245
+ pushError(
246
+ fieldErrors,
247
+ key,
248
+ rule.message ?? defaultMsg(key, '元素必须唯一')
249
+ );
250
+ }
251
+ if (elementRule) {
252
+ (value as unknown[]).forEach((v, i) => {
253
+ validateRule(
254
+ `${String(key)}[${i}]` as keyof T,
255
+ v as unknown,
256
+ elementRule as FieldRule<unknown, T>,
257
+ data,
258
+ fieldErrors
259
+ );
260
+ });
261
+ }
262
+ }
263
+
264
+ // 自定义 validator
265
+ if (rule.validator) {
266
+ const res = (
267
+ rule.validator as
268
+ | ((value: unknown, data: Partial<T>) => boolean | string)
269
+ | undefined
270
+ )?.(value as unknown, data);
271
+ if (res === false)
272
+ pushError(
273
+ fieldErrors,
274
+ key,
275
+ rule.message ?? defaultMsg(key, '校验未通过')
276
+ );
277
+ else if (typeof res === 'string') pushError(fieldErrors, key, res);
278
+ }
279
+ }
280
+
281
+ // ==== 主函数 ====
282
+ export function ruleChecker<
283
+ T extends Record<string, unknown>,
284
+ R extends RuleDescription<T>,
285
+ >(
286
+ data: Partial<T>,
287
+ rules: R
288
+ ):
289
+ | { valid: true; data: ApplyRules<T, R> }
290
+ | { valid: false; errors: string[]; fieldErrors: FieldErrors<T> } {
291
+ const fieldErrors: FieldErrors<T> = {};
292
+
293
+ for (const k in rules) {
294
+ const key = k as keyof T;
295
+ const ruleOrRules = rules[key] as
296
+ | FieldRule<T[typeof key] | undefined, T>
297
+ | FieldRule<T[typeof key] | undefined, T>[]
298
+ | undefined;
299
+ if (!ruleOrRules) continue;
300
+
301
+ const value = data[key] as T[typeof key] | undefined;
302
+
303
+ // 支持单个规则或规则数组
304
+ if (Array.isArray(ruleOrRules)) {
305
+ // 处理规则数组
306
+ for (const rule of ruleOrRules) {
307
+ validateRule<T, T[typeof key] | undefined>(
308
+ key,
309
+ value,
310
+ rule,
311
+ data,
312
+ fieldErrors
313
+ );
314
+ }
315
+ } else {
316
+ // 处理单个规则
317
+ validateRule<T, T[typeof key] | undefined>(
318
+ key,
319
+ value,
320
+ ruleOrRules,
321
+ data,
322
+ fieldErrors
323
+ );
324
+ }
325
+ }
326
+
327
+ // 聚合错误时,避免 [] 被推断为 never[] 导致的类型问题
328
+ const errors: string[] = (
329
+ Object.values(fieldErrors) as Array<string[] | undefined>
330
+ ).reduce<string[]>((acc, v) => {
331
+ if (v) acc.push(...v);
332
+ return acc;
333
+ }, []);
334
+
335
+ if (errors.length > 0) {
336
+ return { valid: false, errors, fieldErrors };
337
+ }
338
+
339
+ return { valid: true, data: data as ApplyRules<T, R> };
340
+ }