@composy/store-vue 0.0.1
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/README.md +154 -0
- package/dist/index.js +1536 -0
- package/dist/index.js.map +1 -0
- package/dist/index.min.js +1008 -0
- package/dist/index.min.js.map +1 -0
- package/es/composables/defineSetupStore.d.ts +120 -0
- package/es/composables/defineSetupStore.d.ts.map +1 -0
- package/es/composables/defineSetupStore.js +29 -0
- package/es/composables/defineSetupStore.js.map +1 -0
- package/es/composables/index.d.ts +24 -0
- package/es/composables/index.d.ts.map +1 -0
- package/es/composables/index.js +21 -0
- package/es/composables/useFormStore.d.ts +131 -0
- package/es/composables/useFormStore.d.ts.map +1 -0
- package/es/composables/useFormStore.js +189 -0
- package/es/composables/useFormStore.js.map +1 -0
- package/es/composables/useOptimistic.d.ts +67 -0
- package/es/composables/useOptimistic.d.ts.map +1 -0
- package/es/composables/useOptimistic.js +64 -0
- package/es/composables/useOptimistic.js.map +1 -0
- package/es/composables/usePersist.d.ts +68 -0
- package/es/composables/usePersist.d.ts.map +1 -0
- package/es/composables/usePersist.js +96 -0
- package/es/composables/usePersist.js.map +1 -0
- package/es/composables/usePersistedState.d.ts +59 -0
- package/es/composables/usePersistedState.d.ts.map +1 -0
- package/es/composables/usePersistedState.js +121 -0
- package/es/composables/usePersistedState.js.map +1 -0
- package/es/composables/useSharedState.d.ts +62 -0
- package/es/composables/useSharedState.d.ts.map +1 -0
- package/es/composables/useSharedState.js +94 -0
- package/es/composables/useSharedState.js.map +1 -0
- package/es/composables/useStore.d.ts +99 -0
- package/es/composables/useStore.d.ts.map +1 -0
- package/es/composables/useStore.js +61 -0
- package/es/composables/useStore.js.map +1 -0
- package/es/composables/useStoreDevtools.d.ts +31 -0
- package/es/composables/useStoreDevtools.d.ts.map +1 -0
- package/es/composables/useStoreDevtools.js +112 -0
- package/es/composables/useStoreDevtools.js.map +1 -0
- package/es/composables/useStoreSnapshot.d.ts +122 -0
- package/es/composables/useStoreSnapshot.d.ts.map +1 -0
- package/es/composables/useStoreSnapshot.js +209 -0
- package/es/composables/useStoreSnapshot.js.map +1 -0
- package/es/composables/useSubscribe.d.ts +90 -0
- package/es/composables/useSubscribe.d.ts.map +1 -0
- package/es/composables/useSubscribe.js +53 -0
- package/es/composables/useSubscribe.js.map +1 -0
- package/es/context/injection-keys.d.ts +8 -0
- package/es/context/injection-keys.d.ts.map +1 -0
- package/es/context/injection-keys.js +17 -0
- package/es/context/injection-keys.js.map +1 -0
- package/es/index.d.ts +24 -0
- package/es/index.d.ts.map +1 -0
- package/es/index.js +32 -0
- package/es/index.js.map +1 -0
- package/es/plugins/engine-plugin.d.ts +36 -0
- package/es/plugins/engine-plugin.d.ts.map +1 -0
- package/es/plugins/engine-plugin.js +83 -0
- package/es/plugins/engine-plugin.js.map +1 -0
- package/es/plugins/enhanced-persist-plugin.d.ts +3 -0
- package/es/plugins/enhanced-persist-plugin.d.ts.map +1 -0
- package/es/plugins/enhanced-persist-plugin.js +12 -0
- package/es/plugins/index.d.ts +7 -0
- package/es/plugins/index.d.ts.map +1 -0
- package/es/plugins/index.js +15 -0
- package/es/plugins/pinia-plugin.d.ts +68 -0
- package/es/plugins/pinia-plugin.d.ts.map +1 -0
- package/es/plugins/pinia-plugin.js +172 -0
- package/es/plugins/pinia-plugin.js.map +1 -0
- package/es/plugins/registry-plugin.d.ts +2 -0
- package/es/plugins/registry-plugin.d.ts.map +1 -0
- package/es/plugins/registry-plugin.js +12 -0
- package/es/plugins/vue-plugin.d.ts +10 -0
- package/es/plugins/vue-plugin.d.ts.map +1 -0
- package/es/plugins/vue-plugin.js +24 -0
- package/es/plugins/vue-plugin.js.map +1 -0
- package/es/runtime/install-store-app.d.ts +23 -0
- package/es/runtime/install-store-app.d.ts.map +1 -0
- package/es/runtime/install-store-app.js +54 -0
- package/es/runtime/install-store-app.js.map +1 -0
- package/es/runtime/store-plugin-options.d.ts +40 -0
- package/es/runtime/store-plugin-options.d.ts.map +1 -0
- package/es/runtime/store-plugin-options.js +24 -0
- package/es/runtime/store-plugin-options.js.map +1 -0
- package/es/runtime/store-registry.d.ts +64 -0
- package/es/runtime/store-registry.d.ts.map +1 -0
- package/es/runtime/store-registry.js +102 -0
- package/es/runtime/store-registry.js.map +1 -0
- package/es/runtime/store-runtime.d.ts +46 -0
- package/es/runtime/store-runtime.d.ts.map +1 -0
- package/es/runtime/store-runtime.js +58 -0
- package/es/runtime/store-runtime.js.map +1 -0
- package/es/types.d.ts +167 -0
- package/es/types.d.ts.map +1 -0
- package/es/types.js +10 -0
- package/esm/composables/defineSetupStore.d.ts +120 -0
- package/esm/composables/defineSetupStore.d.ts.map +1 -0
- package/esm/composables/defineSetupStore.js +29 -0
- package/esm/composables/defineSetupStore.js.map +1 -0
- package/esm/composables/index.d.ts +24 -0
- package/esm/composables/index.d.ts.map +1 -0
- package/esm/composables/index.js +21 -0
- package/esm/composables/useFormStore.d.ts +131 -0
- package/esm/composables/useFormStore.d.ts.map +1 -0
- package/esm/composables/useFormStore.js +189 -0
- package/esm/composables/useFormStore.js.map +1 -0
- package/esm/composables/useOptimistic.d.ts +67 -0
- package/esm/composables/useOptimistic.d.ts.map +1 -0
- package/esm/composables/useOptimistic.js +64 -0
- package/esm/composables/useOptimistic.js.map +1 -0
- package/esm/composables/usePersist.d.ts +68 -0
- package/esm/composables/usePersist.d.ts.map +1 -0
- package/esm/composables/usePersist.js +96 -0
- package/esm/composables/usePersist.js.map +1 -0
- package/esm/composables/usePersistedState.d.ts +59 -0
- package/esm/composables/usePersistedState.d.ts.map +1 -0
- package/esm/composables/usePersistedState.js +121 -0
- package/esm/composables/usePersistedState.js.map +1 -0
- package/esm/composables/useSharedState.d.ts +62 -0
- package/esm/composables/useSharedState.d.ts.map +1 -0
- package/esm/composables/useSharedState.js +94 -0
- package/esm/composables/useSharedState.js.map +1 -0
- package/esm/composables/useStore.d.ts +99 -0
- package/esm/composables/useStore.d.ts.map +1 -0
- package/esm/composables/useStore.js +61 -0
- package/esm/composables/useStore.js.map +1 -0
- package/esm/composables/useStoreDevtools.d.ts +31 -0
- package/esm/composables/useStoreDevtools.d.ts.map +1 -0
- package/esm/composables/useStoreDevtools.js +112 -0
- package/esm/composables/useStoreDevtools.js.map +1 -0
- package/esm/composables/useStoreSnapshot.d.ts +122 -0
- package/esm/composables/useStoreSnapshot.d.ts.map +1 -0
- package/esm/composables/useStoreSnapshot.js +209 -0
- package/esm/composables/useStoreSnapshot.js.map +1 -0
- package/esm/composables/useSubscribe.d.ts +90 -0
- package/esm/composables/useSubscribe.d.ts.map +1 -0
- package/esm/composables/useSubscribe.js +53 -0
- package/esm/composables/useSubscribe.js.map +1 -0
- package/esm/context/injection-keys.d.ts +8 -0
- package/esm/context/injection-keys.d.ts.map +1 -0
- package/esm/context/injection-keys.js +17 -0
- package/esm/context/injection-keys.js.map +1 -0
- package/esm/index.d.ts +24 -0
- package/esm/index.d.ts.map +1 -0
- package/esm/index.js +34 -0
- package/esm/index.js.map +1 -0
- package/esm/plugins/engine-plugin.d.ts +36 -0
- package/esm/plugins/engine-plugin.d.ts.map +1 -0
- package/esm/plugins/engine-plugin.js +83 -0
- package/esm/plugins/engine-plugin.js.map +1 -0
- package/esm/plugins/enhanced-persist-plugin.d.ts +3 -0
- package/esm/plugins/enhanced-persist-plugin.d.ts.map +1 -0
- package/esm/plugins/enhanced-persist-plugin.js +12 -0
- package/esm/plugins/index.d.ts +7 -0
- package/esm/plugins/index.d.ts.map +1 -0
- package/esm/plugins/index.js +16 -0
- package/esm/plugins/pinia-plugin.d.ts +68 -0
- package/esm/plugins/pinia-plugin.d.ts.map +1 -0
- package/esm/plugins/pinia-plugin.js +172 -0
- package/esm/plugins/pinia-plugin.js.map +1 -0
- package/esm/plugins/registry-plugin.d.ts +2 -0
- package/esm/plugins/registry-plugin.d.ts.map +1 -0
- package/esm/plugins/registry-plugin.js +12 -0
- package/esm/plugins/vue-plugin.d.ts +10 -0
- package/esm/plugins/vue-plugin.d.ts.map +1 -0
- package/esm/plugins/vue-plugin.js +24 -0
- package/esm/plugins/vue-plugin.js.map +1 -0
- package/esm/runtime/install-store-app.d.ts +23 -0
- package/esm/runtime/install-store-app.d.ts.map +1 -0
- package/esm/runtime/install-store-app.js +54 -0
- package/esm/runtime/install-store-app.js.map +1 -0
- package/esm/runtime/store-plugin-options.d.ts +40 -0
- package/esm/runtime/store-plugin-options.d.ts.map +1 -0
- package/esm/runtime/store-plugin-options.js +24 -0
- package/esm/runtime/store-plugin-options.js.map +1 -0
- package/esm/runtime/store-registry.d.ts +64 -0
- package/esm/runtime/store-registry.d.ts.map +1 -0
- package/esm/runtime/store-registry.js +94 -0
- package/esm/runtime/store-registry.js.map +1 -0
- package/esm/runtime/store-runtime.d.ts +46 -0
- package/esm/runtime/store-runtime.d.ts.map +1 -0
- package/esm/runtime/store-runtime.js +58 -0
- package/esm/runtime/store-runtime.js.map +1 -0
- package/esm/types.d.ts +167 -0
- package/esm/types.d.ts.map +1 -0
- package/esm/types.js +10 -0
- package/lib/composables/defineSetupStore.cjs +33 -0
- package/lib/composables/defineSetupStore.cjs.map +1 -0
- package/lib/composables/defineSetupStore.d.ts +120 -0
- package/lib/composables/index.cjs +46 -0
- package/lib/composables/index.d.ts +24 -0
- package/lib/composables/useFormStore.cjs +197 -0
- package/lib/composables/useFormStore.cjs.map +1 -0
- package/lib/composables/useFormStore.d.ts +131 -0
- package/lib/composables/useOptimistic.cjs +66 -0
- package/lib/composables/useOptimistic.cjs.map +1 -0
- package/lib/composables/useOptimistic.d.ts +67 -0
- package/lib/composables/usePersist.cjs +100 -0
- package/lib/composables/usePersist.cjs.map +1 -0
- package/lib/composables/usePersist.d.ts +68 -0
- package/lib/composables/usePersistedState.cjs +123 -0
- package/lib/composables/usePersistedState.cjs.map +1 -0
- package/lib/composables/usePersistedState.d.ts +59 -0
- package/lib/composables/useSharedState.cjs +96 -0
- package/lib/composables/useSharedState.cjs.map +1 -0
- package/lib/composables/useSharedState.d.ts +62 -0
- package/lib/composables/useStore.cjs +65 -0
- package/lib/composables/useStore.cjs.map +1 -0
- package/lib/composables/useStore.d.ts +99 -0
- package/lib/composables/useStoreDevtools.cjs +114 -0
- package/lib/composables/useStoreDevtools.cjs.map +1 -0
- package/lib/composables/useStoreDevtools.d.ts +31 -0
- package/lib/composables/useStoreSnapshot.cjs +211 -0
- package/lib/composables/useStoreSnapshot.cjs.map +1 -0
- package/lib/composables/useStoreSnapshot.d.ts +122 -0
- package/lib/composables/useSubscribe.cjs +57 -0
- package/lib/composables/useSubscribe.cjs.map +1 -0
- package/lib/composables/useSubscribe.d.ts +90 -0
- package/lib/context/injection-keys.cjs +21 -0
- package/lib/context/injection-keys.cjs.map +1 -0
- package/lib/context/injection-keys.d.ts +8 -0
- package/lib/index.cjs +235 -0
- package/lib/index.cjs.map +1 -0
- package/lib/index.d.ts +24 -0
- package/lib/package.json +3 -0
- package/lib/plugins/engine-plugin.cjs +90 -0
- package/lib/plugins/engine-plugin.cjs.map +1 -0
- package/lib/plugins/engine-plugin.d.ts +36 -0
- package/lib/plugins/enhanced-persist-plugin.cjs +15 -0
- package/lib/plugins/enhanced-persist-plugin.d.ts +3 -0
- package/lib/plugins/index.cjs +28 -0
- package/lib/plugins/index.d.ts +7 -0
- package/lib/plugins/pinia-plugin.cjs +177 -0
- package/lib/plugins/pinia-plugin.cjs.map +1 -0
- package/lib/plugins/pinia-plugin.d.ts +68 -0
- package/lib/plugins/registry-plugin.cjs +18 -0
- package/lib/plugins/registry-plugin.d.ts +2 -0
- package/lib/plugins/vue-plugin.cjs +26 -0
- package/lib/plugins/vue-plugin.cjs.map +1 -0
- package/lib/plugins/vue-plugin.d.ts +10 -0
- package/lib/runtime/install-store-app.cjs +57 -0
- package/lib/runtime/install-store-app.cjs.map +1 -0
- package/lib/runtime/install-store-app.d.ts +23 -0
- package/lib/runtime/store-plugin-options.cjs +26 -0
- package/lib/runtime/store-plugin-options.cjs.map +1 -0
- package/lib/runtime/store-plugin-options.d.ts +40 -0
- package/lib/runtime/store-registry.cjs +108 -0
- package/lib/runtime/store-registry.cjs.map +1 -0
- package/lib/runtime/store-registry.d.ts +64 -0
- package/lib/runtime/store-runtime.cjs +62 -0
- package/lib/runtime/store-runtime.cjs.map +1 -0
- package/lib/runtime/store-runtime.d.ts +46 -0
- package/lib/types.cjs +12 -0
- package/lib/types.d.ts +167 -0
- package/package.json +115 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { ComputedRef, Ref } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* 表单校验器函数类型
|
|
4
|
+
* @param value - 当前字段值
|
|
5
|
+
* @param allValues - 表单所有字段的值(用于跨字段校验)
|
|
6
|
+
* @returns 校验错误信息或 null(无错误),支持异步校验
|
|
7
|
+
*/
|
|
8
|
+
export type FormValidator<T = unknown> = (value: T, allValues: Record<string, unknown>) => string | null | Promise<string | null>;
|
|
9
|
+
/**
|
|
10
|
+
* 字段校验规则接口
|
|
11
|
+
* @property required - 是否必填,可传 string 自定义提示
|
|
12
|
+
* @property rules - 自定义校验器数组,按顺序依次执行
|
|
13
|
+
*/
|
|
14
|
+
export interface FormFieldRules<T = unknown> {
|
|
15
|
+
required?: boolean | string;
|
|
16
|
+
rules?: FormValidator<T>[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* useFormStore 的配置选项接口
|
|
20
|
+
* @property initialValues - 表单初始值(必须提供完整对象)
|
|
21
|
+
* @property validationRules - 各字段的校验规则映射
|
|
22
|
+
* @property onSubmit - 表单提交回调(校验通过后调用)
|
|
23
|
+
* @property validateOn - 触发校验的时机:'blur' | 'change' | 'submit'
|
|
24
|
+
*/
|
|
25
|
+
export interface UseFormStoreOptions<T extends Record<string, unknown>> {
|
|
26
|
+
initialValues: T;
|
|
27
|
+
validationRules?: {
|
|
28
|
+
[K in keyof T]?: FormFieldRules<T[K]>;
|
|
29
|
+
};
|
|
30
|
+
onSubmit?: (values: T) => void | Promise<void>;
|
|
31
|
+
validateOn?: 'blur' | 'change' | 'submit';
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 单个字段的状态接口
|
|
35
|
+
* @property touched - 字段是否被触碰过(获得过焦点或提交过)
|
|
36
|
+
* @property dirty - 字段值是否与初始值不同(被修改过)
|
|
37
|
+
* @property error - 当前校验错误信息,null 表示无错误
|
|
38
|
+
* @property validating - 字段是否正在异步校验中
|
|
39
|
+
*/
|
|
40
|
+
export interface FormFieldState {
|
|
41
|
+
touched: boolean;
|
|
42
|
+
dirty: boolean;
|
|
43
|
+
error: string | null;
|
|
44
|
+
validating: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* useFormStore 返回值接口
|
|
48
|
+
* @property values - 表单当前值(reactive 对象)
|
|
49
|
+
* @property fields - 各字段的状态映射
|
|
50
|
+
* @property isDirty - 是否有任意字段被修改过(计算属性)
|
|
51
|
+
* @property isValid - 是否所有字段都无校验错误(计算属性)
|
|
52
|
+
* @property isSubmitting - 表单是否正在提交中
|
|
53
|
+
* @property submitError - 提交失败的错误信息
|
|
54
|
+
* @property submitCount - 累计提交次数
|
|
55
|
+
* @property setFieldValue - 设置指定字段的值
|
|
56
|
+
* @property setFieldTouched - 标记字段为已触碰
|
|
57
|
+
* @property validateField - 校验单个字段(异步)
|
|
58
|
+
* @property validateAll - 校验所有字段(异步)
|
|
59
|
+
* @property handleSubmit - 提交表单
|
|
60
|
+
* @property reset - 重置整个表单(可传新的初始值)
|
|
61
|
+
* @property resetField - 重置单个字段
|
|
62
|
+
* @property errors - 所有字段错误信息的计算属性
|
|
63
|
+
*/
|
|
64
|
+
export interface UseFormStoreReturn<T extends Record<string, unknown>> {
|
|
65
|
+
values: T;
|
|
66
|
+
fields: Record<string, FormFieldState>;
|
|
67
|
+
isDirty: ComputedRef<boolean>;
|
|
68
|
+
isValid: ComputedRef<boolean>;
|
|
69
|
+
isSubmitting: Ref<boolean>;
|
|
70
|
+
submitError: Ref<string | null>;
|
|
71
|
+
submitCount: Ref<number>;
|
|
72
|
+
setFieldValue: <K extends keyof T>(field: K, value: T[K]) => void;
|
|
73
|
+
setFieldTouched: (field: keyof T) => void;
|
|
74
|
+
validateField: (field: keyof T) => Promise<string | null>;
|
|
75
|
+
validateAll: () => Promise<boolean>;
|
|
76
|
+
handleSubmit: () => Promise<void>;
|
|
77
|
+
reset: (newInitialValues?: Partial<T>) => void;
|
|
78
|
+
resetField: (field: keyof T) => void;
|
|
79
|
+
errors: ComputedRef<Record<string, string | null>>;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 表单状态管理 Composable
|
|
83
|
+
* 提供完整的表单生命周期管理能力,包括值管理、字段状态追踪、校验、提交和重置
|
|
84
|
+
*
|
|
85
|
+
* @param options - 表单配置选项
|
|
86
|
+
* @returns 包含表单状态和操作方法的对象
|
|
87
|
+
*/
|
|
88
|
+
export declare function useFormStore<T extends Record<string, unknown>>(options: UseFormStoreOptions<T>): UseFormStoreReturn<T>;
|
|
89
|
+
/**
|
|
90
|
+
* 创建必填校验器
|
|
91
|
+
* @param message - 自定义错误提示,默认为 '此字段为必填项'
|
|
92
|
+
* @returns 校验器函数
|
|
93
|
+
*/
|
|
94
|
+
export declare function required(message?: string): FormValidator;
|
|
95
|
+
/**
|
|
96
|
+
* 创建最小长度校验器
|
|
97
|
+
* @param min - 最小字符数
|
|
98
|
+
* @param message - 自定义错误提示
|
|
99
|
+
* @returns 字符串校验器函数
|
|
100
|
+
*/
|
|
101
|
+
export declare function minLength(min: number, message?: string): FormValidator<string>;
|
|
102
|
+
/**
|
|
103
|
+
* 创建最大长度校验器
|
|
104
|
+
* @param max - 最大字符数
|
|
105
|
+
* @param message - 自定义错误提示
|
|
106
|
+
* @returns 字符串校验器函数
|
|
107
|
+
*/
|
|
108
|
+
export declare function maxLength(max: number, message?: string): FormValidator<string>;
|
|
109
|
+
/**
|
|
110
|
+
* 创建正则匹配校验器
|
|
111
|
+
* @param regex - 正则表达式
|
|
112
|
+
* @param message - 自定义错误提示,默认为 '格式不正确'
|
|
113
|
+
* @returns 字符串校验器函数
|
|
114
|
+
*/
|
|
115
|
+
export declare function pattern(regex: RegExp, message?: string): FormValidator<string>;
|
|
116
|
+
/**
|
|
117
|
+
* 创建邮箱格式校验器
|
|
118
|
+
* 基于内置的 EMAIL_PATTERN 正则
|
|
119
|
+
* @param message - 自定义错误提示,默认为 '邮箱格式不正确'
|
|
120
|
+
* @returns 字符串校验器函数
|
|
121
|
+
*/
|
|
122
|
+
export declare function email(message?: string): FormValidator<string>;
|
|
123
|
+
/**
|
|
124
|
+
* 创建数值范围校验器
|
|
125
|
+
* @param min - 最小值
|
|
126
|
+
* @param max - 最大值
|
|
127
|
+
* @param message - 自定义错误提示
|
|
128
|
+
* @returns 数字校验器函数
|
|
129
|
+
*/
|
|
130
|
+
export declare function numberRange(min: number, max: number, message?: string): FormValidator<number>;
|
|
131
|
+
//# sourceMappingURL=useFormStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFormStore.d.ts","sourceRoot":"","sources":["../../src/composables/useFormStore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* ***********************************
|
|
3
|
+
* @composy/store-vue v0.0.1 *
|
|
4
|
+
* Built with rollup *
|
|
5
|
+
* Build time: 2024-08-17 11:39:29 *
|
|
6
|
+
* Build mode: production *
|
|
7
|
+
* Minified: No *
|
|
8
|
+
* ***********************************
|
|
9
|
+
*/
|
|
10
|
+
import { computed, reactive, ref, watch } from "vue";
|
|
11
|
+
import { deepClone, deepEqual } from "@composy/store-core";
|
|
12
|
+
//#region src/composables/useFormStore.ts
|
|
13
|
+
const DEFAULT_REQUIRED_MESSAGE = "此字段为必填项";
|
|
14
|
+
const DEFAULT_PATTERN_MESSAGE = "格式不正确";
|
|
15
|
+
const DEFAULT_EMAIL_MESSAGE = "邮箱格式不正确";
|
|
16
|
+
const EMAIL_PATTERN = /^[^\s@]+@[^@][^.@]*\.[^\s@]+$/;
|
|
17
|
+
function useFormStore(options) {
|
|
18
|
+
const { initialValues, validationRules = {}, onSubmit, validateOn = "blur" } = options;
|
|
19
|
+
const storedInitialValues = deepClone(initialValues);
|
|
20
|
+
const values = reactive(deepClone(storedInitialValues));
|
|
21
|
+
const valueRecord = values;
|
|
22
|
+
const initialValueRecord = storedInitialValues;
|
|
23
|
+
const fieldKeys = Object.keys(storedInitialValues);
|
|
24
|
+
const fields = reactive(fieldKeys.reduce((acc, key) => {
|
|
25
|
+
acc[key] = {
|
|
26
|
+
touched: false,
|
|
27
|
+
dirty: false,
|
|
28
|
+
error: null,
|
|
29
|
+
validating: false
|
|
30
|
+
};
|
|
31
|
+
return acc;
|
|
32
|
+
}, {}));
|
|
33
|
+
const isSubmitting = ref(false);
|
|
34
|
+
const submitError = ref(null);
|
|
35
|
+
const submitCount = ref(0);
|
|
36
|
+
const isDirty = computed(() => fieldKeys.some((key) => fields[key]?.dirty));
|
|
37
|
+
const isValid = computed(() => fieldKeys.every((key) => !fields[key]?.error));
|
|
38
|
+
const errors = computed(() => {
|
|
39
|
+
return fieldKeys.reduce((acc, key) => {
|
|
40
|
+
acc[key] = fields[key]?.error ?? null;
|
|
41
|
+
return acc;
|
|
42
|
+
}, {});
|
|
43
|
+
});
|
|
44
|
+
fieldKeys.forEach((key) => {
|
|
45
|
+
watch(() => valueRecord[key], (newValue) => {
|
|
46
|
+
const field = fields[key];
|
|
47
|
+
if (field) field.dirty = !deepEqual(newValue, initialValueRecord[key]);
|
|
48
|
+
if (validateOn === "change" && field?.touched) validateField(key);
|
|
49
|
+
}, { deep: true });
|
|
50
|
+
});
|
|
51
|
+
function setFieldValue(field, value) {
|
|
52
|
+
valueRecord[field] = value;
|
|
53
|
+
}
|
|
54
|
+
function setFieldTouched(field) {
|
|
55
|
+
const fieldState = fields[field];
|
|
56
|
+
if (fieldState) fieldState.touched = true;
|
|
57
|
+
if (validateOn === "blur") validateField(field);
|
|
58
|
+
}
|
|
59
|
+
async function validateField(field) {
|
|
60
|
+
const fieldState = fields[field];
|
|
61
|
+
if (!fieldState) return null;
|
|
62
|
+
const fieldRules = validationRules[field];
|
|
63
|
+
if (!fieldRules) {
|
|
64
|
+
fieldState.error = null;
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
fieldState.validating = true;
|
|
68
|
+
try {
|
|
69
|
+
const value = values[field];
|
|
70
|
+
if (fieldRules.required) {
|
|
71
|
+
if (value === "" || value === null || value === void 0) {
|
|
72
|
+
const message = typeof fieldRules.required === "string" ? fieldRules.required : DEFAULT_REQUIRED_MESSAGE;
|
|
73
|
+
fieldState.error = message;
|
|
74
|
+
return message;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (fieldRules.rules) for (const rule of fieldRules.rules) {
|
|
78
|
+
const error = await rule(value, valueRecord);
|
|
79
|
+
if (error) {
|
|
80
|
+
fieldState.error = error;
|
|
81
|
+
return error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
fieldState.error = null;
|
|
85
|
+
return null;
|
|
86
|
+
} finally {
|
|
87
|
+
fieldState.validating = false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function validateAll() {
|
|
91
|
+
return (await Promise.all(fieldKeys.map((key) => validateField(key)))).every((result) => result === null);
|
|
92
|
+
}
|
|
93
|
+
async function handleSubmit() {
|
|
94
|
+
submitCount.value++;
|
|
95
|
+
submitError.value = null;
|
|
96
|
+
fieldKeys.forEach((key) => {
|
|
97
|
+
const field = fields[key];
|
|
98
|
+
if (field) field.touched = true;
|
|
99
|
+
});
|
|
100
|
+
if (!await validateAll() || !onSubmit) return;
|
|
101
|
+
isSubmitting.value = true;
|
|
102
|
+
try {
|
|
103
|
+
await onSubmit(deepClone(values));
|
|
104
|
+
} catch (error) {
|
|
105
|
+
submitError.value = error instanceof Error ? error.message : String(error);
|
|
106
|
+
} finally {
|
|
107
|
+
isSubmitting.value = false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function reset(newInitialValues) {
|
|
111
|
+
if (newInitialValues) Object.assign(initialValueRecord, deepClone(newInitialValues));
|
|
112
|
+
fieldKeys.forEach((key) => {
|
|
113
|
+
valueRecord[key] = deepClone(initialValueRecord[key]);
|
|
114
|
+
const field = fields[key];
|
|
115
|
+
if (field) {
|
|
116
|
+
field.touched = false;
|
|
117
|
+
field.dirty = false;
|
|
118
|
+
field.error = null;
|
|
119
|
+
field.validating = false;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
submitError.value = null;
|
|
123
|
+
}
|
|
124
|
+
function resetField(field) {
|
|
125
|
+
const fieldKey = field;
|
|
126
|
+
valueRecord[fieldKey] = deepClone(initialValueRecord[fieldKey]);
|
|
127
|
+
const fieldState = fields[fieldKey];
|
|
128
|
+
if (fieldState) {
|
|
129
|
+
fieldState.touched = false;
|
|
130
|
+
fieldState.dirty = false;
|
|
131
|
+
fieldState.error = null;
|
|
132
|
+
fieldState.validating = false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
values,
|
|
137
|
+
fields,
|
|
138
|
+
isDirty,
|
|
139
|
+
isValid,
|
|
140
|
+
isSubmitting,
|
|
141
|
+
submitError,
|
|
142
|
+
submitCount,
|
|
143
|
+
setFieldValue,
|
|
144
|
+
setFieldTouched,
|
|
145
|
+
validateField,
|
|
146
|
+
validateAll,
|
|
147
|
+
handleSubmit,
|
|
148
|
+
reset,
|
|
149
|
+
resetField,
|
|
150
|
+
errors
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function required(message = DEFAULT_REQUIRED_MESSAGE) {
|
|
154
|
+
return (value) => {
|
|
155
|
+
if (value === "" || value === null || value === void 0) return message;
|
|
156
|
+
return null;
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function minLength(min, message) {
|
|
160
|
+
return (value) => {
|
|
161
|
+
if (typeof value === "string" && value.length < min) return message ?? `\u81F3\u5C11\u9700\u8981 ${min} \u4E2A\u5B57\u7B26`;
|
|
162
|
+
return null;
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function maxLength(max, message) {
|
|
166
|
+
return (value) => {
|
|
167
|
+
if (typeof value === "string" && value.length > max) return message ?? `\u4E0D\u80FD\u8D85\u8FC7 ${max} \u4E2A\u5B57\u7B26`;
|
|
168
|
+
return null;
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function pattern(regex, message = DEFAULT_PATTERN_MESSAGE) {
|
|
172
|
+
return (value) => {
|
|
173
|
+
if (typeof value === "string" && !regex.test(value)) return message;
|
|
174
|
+
return null;
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function email(message = DEFAULT_EMAIL_MESSAGE) {
|
|
178
|
+
return pattern(EMAIL_PATTERN, message);
|
|
179
|
+
}
|
|
180
|
+
function numberRange(min, max, message) {
|
|
181
|
+
return (value) => {
|
|
182
|
+
if (typeof value === "number" && (value < min || value > max)) return message ?? `\u5FC5\u987B\u5728 ${min} \u5230 ${max} \u4E4B\u95F4`;
|
|
183
|
+
return null;
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
export { email, maxLength, minLength, numberRange, pattern, required, useFormStore };
|
|
188
|
+
/*! End of @composy/store-vue | Powered by @composy/builder */
|
|
189
|
+
//# sourceMappingURL=useFormStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFormStore.js","names":[],"sources":["../../src/composables/useFormStore.ts"],"sourcesContent":["/* ============================================================\n * useFormStore.ts\n * 表单状态管理 Composable\n * 提供表单值、字段状态、校验、提交、重置等完整表单生命周期管理\n * ============================================================ */\n\n/* 从 Vue 导入类型,用于声明响应式引用 */\nimport type { ComputedRef, Ref } from 'vue'\n/* 从 store-core 工具库导入深拷贝和深比较函数 */\nimport { deepClone, deepEqual } from '@composy/store-core'\n/* 从 Vue 导入响应式 API */\nimport { computed, reactive, ref, watch } from 'vue'\n\n/* ── 默认校验提示信息 ── */\n/* 必填项校验失败时的默认提示 */\nconst DEFAULT_REQUIRED_MESSAGE = '此字段为必填项'\n/* 正则匹配校验失败时的默认提示 */\nconst DEFAULT_PATTERN_MESSAGE = '格式不正确'\n/* 邮箱格式校验失败时的默认提示 */\nconst DEFAULT_EMAIL_MESSAGE = '邮箱格式不正确'\n/* 邮箱格式正则表达式 */\nconst EMAIL_PATTERN = /^[^\\s@]+@[^@][^.@]*\\.[^\\s@]+$/\n\n/* ── 类型定义 ── */\n\n/* 从泛型 T 中提取 string 类型的键名,用作字段标识 */\ntype FieldKey<T> = Extract<keyof T, string>\n\n/**\n * 表单校验器函数类型\n * @param value - 当前字段值\n * @param allValues - 表单所有字段的值(用于跨字段校验)\n * @returns 校验错误信息或 null(无错误),支持异步校验\n */\nexport type FormValidator<T = unknown> = (\n value: T,\n allValues: Record<string, unknown>,\n) => string | null | Promise<string | null>\n\n/**\n * 字段校验规则接口\n * @property required - 是否必填,可传 string 自定义提示\n * @property rules - 自定义校验器数组,按顺序依次执行\n */\nexport interface FormFieldRules<T = unknown> {\n required?: boolean | string\n rules?: FormValidator<T>[]\n}\n\n/**\n * useFormStore 的配置选项接口\n * @property initialValues - 表单初始值(必须提供完整对象)\n * @property validationRules - 各字段的校验规则映射\n * @property onSubmit - 表单提交回调(校验通过后调用)\n * @property validateOn - 触发校验的时机:'blur' | 'change' | 'submit'\n */\nexport interface UseFormStoreOptions<T extends Record<string, unknown>> {\n initialValues: T\n validationRules?: {\n [K in keyof T]?: FormFieldRules<T[K]>\n }\n onSubmit?: (values: T) => void | Promise<void>\n validateOn?: 'blur' | 'change' | 'submit'\n}\n\n/**\n * 单个字段的状态接口\n * @property touched - 字段是否被触碰过(获得过焦点或提交过)\n * @property dirty - 字段值是否与初始值不同(被修改过)\n * @property error - 当前校验错误信息,null 表示无错误\n * @property validating - 字段是否正在异步校验中\n */\nexport interface FormFieldState {\n touched: boolean\n dirty: boolean\n error: string | null\n validating: boolean\n}\n\n/**\n * useFormStore 返回值接口\n * @property values - 表单当前值(reactive 对象)\n * @property fields - 各字段的状态映射\n * @property isDirty - 是否有任意字段被修改过(计算属性)\n * @property isValid - 是否所有字段都无校验错误(计算属性)\n * @property isSubmitting - 表单是否正在提交中\n * @property submitError - 提交失败的错误信息\n * @property submitCount - 累计提交次数\n * @property setFieldValue - 设置指定字段的值\n * @property setFieldTouched - 标记字段为已触碰\n * @property validateField - 校验单个字段(异步)\n * @property validateAll - 校验所有字段(异步)\n * @property handleSubmit - 提交表单\n * @property reset - 重置整个表单(可传新的初始值)\n * @property resetField - 重置单个字段\n * @property errors - 所有字段错误信息的计算属性\n */\nexport interface UseFormStoreReturn<T extends Record<string, unknown>> {\n values: T\n fields: Record<string, FormFieldState>\n isDirty: ComputedRef<boolean>\n isValid: ComputedRef<boolean>\n isSubmitting: Ref<boolean>\n submitError: Ref<string | null>\n submitCount: Ref<number>\n setFieldValue: <K extends keyof T>(field: K, value: T[K]) => void\n setFieldTouched: (field: keyof T) => void\n validateField: (field: keyof T) => Promise<string | null>\n validateAll: () => Promise<boolean>\n handleSubmit: () => Promise<void>\n reset: (newInitialValues?: Partial<T>) => void\n resetField: (field: keyof T) => void\n errors: ComputedRef<Record<string, string | null>>\n}\n\n/**\n * 表单状态管理 Composable\n * 提供完整的表单生命周期管理能力,包括值管理、字段状态追踪、校验、提交和重置\n *\n * @param options - 表单配置选项\n * @returns 包含表单状态和操作方法的对象\n */\nexport function useFormStore<T extends Record<string, unknown>>(\n options: UseFormStoreOptions<T>,\n): UseFormStoreReturn<T> {\n /* 解构配置选项,设置默认值 */\n const {\n initialValues, /* 表单初始值 */\n validationRules = {} as NonNullable<UseFormStoreOptions<T>['validationRules']>, /* 校验规则,默认空对象 */\n onSubmit, /* 提交回调 */\n validateOn = 'blur', /* 校验时机,默认 blur 时触发 */\n } = options\n\n /* 深拷贝初始值并保存,用于后续重置和脏值比较 */\n const storedInitialValues = deepClone(initialValues) as T\n /* 创建响应式表单值对象(从初始值的深拷贝开始) */\n const values = reactive(deepClone(storedInitialValues)) as T\n /* 将 values 转为通用 Record 类型,便于动态属性访问 */\n const valueRecord = values as Record<string, unknown>\n /* 将初始值也转为 Record 类型,用于脏值比较 */\n const initialValueRecord = storedInitialValues as Record<string, unknown>\n /* 获取所有字段名列表 */\n const fieldKeys = Object.keys(storedInitialValues) as Array<FieldKey<T>>\n\n /* 创建每个字段的响应式状态对象(touched/dirty/error/validating) */\n const fields = reactive(\n fieldKeys.reduce<Record<string, FormFieldState>>((acc, key) => {\n acc[key] = {\n touched: false, /* 字段未被触碰 */\n dirty: false, /* 字段值未被修改 */\n error: null, /* 无校验错误 */\n validating: false, /* 未在校验中 */\n }\n return acc\n }, {}),\n )\n\n /* ── 全局状态 ── */\n /* 是否正在提交中 */\n const isSubmitting = ref(false)\n /* 提交错误信息 */\n const submitError = ref<string | null>(null)\n /* 累计提交次数 */\n const submitCount = ref(0)\n\n /* ── 计算属性 ── */\n /* 是否有任意字段被修改过 */\n const isDirty = computed(() => fieldKeys.some(key => fields[key]?.dirty))\n /* 是否所有字段都无错误 */\n const isValid = computed(() => fieldKeys.every(key => !fields[key]?.error))\n\n /* 所有字段错误信息的映射 */\n const errors = computed(() => {\n return fieldKeys.reduce<Record<string, string | null>>((acc, key) => {\n acc[key] = fields[key]?.error ?? null\n return acc\n }, {})\n })\n\n /* ── 值变化监听 ── */\n /* 为每个字段创建 watch,当值变化时更新脏状态并按需触发校验 */\n fieldKeys.forEach((key) => {\n watch(\n () => valueRecord[key], /* 监听字段值 */\n (newValue) => {\n const field = fields[key]\n if (field) {\n /* 比较当前值与初始值,判断是否为脏值 */\n field.dirty = !deepEqual(newValue, initialValueRecord[key])\n }\n\n /* 如果校验时机为 'change' 且字段已被触碰,则触发校验 */\n if (validateOn === 'change' && field?.touched) {\n void validateField(key)\n }\n },\n { deep: true }, /* 深度监听,支持嵌套对象变化 */\n )\n })\n\n /* ── 操作方法 ── */\n\n /**\n * 设置指定字段的值\n * @param field - 字段名\n * @param value - 新值\n */\n function setFieldValue<K extends keyof T>(field: K, value: T[K]): void {\n valueRecord[field as string] = value\n }\n\n /**\n * 标记字段为已触碰状态\n * 如果校验时机为 'blur',同时触发该字段的校验\n * @param field - 字段名\n */\n function setFieldTouched(field: keyof T): void {\n const fieldState = fields[field as string]\n if (fieldState) {\n fieldState.touched = true\n }\n\n /* blur 时自动触发校验 */\n if (validateOn === 'blur') {\n void validateField(field)\n }\n }\n\n /**\n * 校验单个字段\n * 按顺序执行:必填校验 → 自定义规则校验\n * @param field - 字段名\n * @returns 错误信息或 null(无错误)\n */\n async function validateField(field: keyof T): Promise<string | null> {\n const fieldKey = field as string\n const fieldState = fields[fieldKey]\n\n /* 字段状态不存在则跳过 */\n if (!fieldState) {\n return null\n }\n\n /* 获取该字段的校验规则 */\n const fieldRules = validationRules[field]\n /* 无规则则清除错误并返回 */\n if (!fieldRules) {\n fieldState.error = null\n return null\n }\n\n /* 标记开始校验 */\n fieldState.validating = true\n\n try {\n const value = values[field]\n\n /* 第一步:必填校验 */\n if (fieldRules.required) {\n const isEmpty = value === '' || value === null || value === undefined\n if (isEmpty) {\n /* 支持自定义必填提示信息 */\n const message = typeof fieldRules.required === 'string'\n ? fieldRules.required\n : DEFAULT_REQUIRED_MESSAGE\n fieldState.error = message\n return message\n }\n }\n\n /* 第二步:自定义规则校验(按顺序执行,遇到第一个错误即停止) */\n if (fieldRules.rules) {\n for (const rule of fieldRules.rules) {\n const error = await rule(\n value as T[keyof T],\n valueRecord, /* 传入所有表单值,支持跨字段校验 */\n )\n\n if (error) {\n fieldState.error = error\n return error\n }\n }\n }\n\n /* 所有校验通过,清除错误 */\n fieldState.error = null\n return null\n }\n finally {\n /* 无论成功失败,标记校验结束 */\n fieldState.validating = false\n }\n }\n\n /**\n * 校验所有字段\n * @returns 是否所有字段都校验通过\n */\n async function validateAll(): Promise<boolean> {\n const results = await Promise.all(fieldKeys.map(key => validateField(key)))\n return results.every(result => result === null)\n }\n\n /**\n * 提交流程\n * 1. 递增提交计数\n * 2. 标记所有字段为已触碰\n * 3. 执行全量校验\n * 4. 校验通过且有 onSubmit 回调时,调用提交\n */\n async function handleSubmit(): Promise<void> {\n submitCount.value++\n submitError.value = null\n\n /* 标记所有字段为已触碰 */\n fieldKeys.forEach((key) => {\n const field = fields[key]\n if (field) {\n field.touched = true\n }\n })\n\n /* 执行全量校验 */\n const valid = await validateAll()\n /* 校验未通过或没有提交回调则退出 */\n if (!valid || !onSubmit) {\n return\n }\n\n /* 开始提交 */\n isSubmitting.value = true\n try {\n /* 深拷贝 values 后传给 onSubmit,避免外部修改响应式数据 */\n await onSubmit(deepClone(values) as T)\n }\n catch (error) {\n /* 捕获提交异常,保存错误信息 */\n submitError.value = error instanceof Error ? error.message : String(error)\n }\n finally {\n /* 无论成功失败,标记提交结束 */\n isSubmitting.value = false\n }\n }\n\n /**\n * 重置整个表单\n * 可选传入新的初始值(部分更新)\n * @param newInitialValues - 新的初始值(可选,会与现有初始值合并)\n */\n function reset(newInitialValues?: Partial<T>): void {\n /* 如果传入了新初始值,合并到存储的初始值中 */\n if (newInitialValues) {\n Object.assign(initialValueRecord, deepClone(newInitialValues))\n }\n\n /* 恢复每个字段的值和状态 */\n fieldKeys.forEach((key) => {\n /* 恢复为初始值的深拷贝 */\n valueRecord[key] = deepClone(initialValueRecord[key])\n\n /* 重置字段状态 */\n const field = fields[key]\n if (field) {\n field.touched = false /* 清除触碰状态 */\n field.dirty = false /* 清除脏值标记 */\n field.error = null /* 清除校验错误 */\n field.validating = false /* 清除校验中标记 */\n }\n })\n\n /* 清除提交错误 */\n submitError.value = null\n }\n\n /**\n * 重置单个字段\n * @param field - 要重置的字段名\n */\n function resetField(field: keyof T): void {\n const fieldKey = field as string\n /* 恢复字段值为初始值的深拷贝 */\n valueRecord[fieldKey] = deepClone(initialValueRecord[fieldKey])\n\n /* 重置字段状态 */\n const fieldState = fields[fieldKey]\n if (fieldState) {\n fieldState.touched = false\n fieldState.dirty = false\n fieldState.error = null\n fieldState.validating = false\n }\n }\n\n /* ── 返回所有状态和方法 ── */\n return {\n values, /* 表单当前值(响应式) */\n fields, /* 字段状态映射(响应式) */\n isDirty, /* 是否有脏值 */\n isValid, /* 是否全部有效 */\n isSubmitting, /* 是否正在提交 */\n submitError, /* 提交错误信息 */\n submitCount, /* 提交次数 */\n setFieldValue, /* 设置字段值 */\n setFieldTouched, /* 标记字段已触碰 */\n validateField, /* 校验单个字段 */\n validateAll, /* 校验所有字段 */\n handleSubmit, /* 提交表单 */\n reset, /* 重置表单 */\n resetField, /* 重置单个字段 */\n errors, /* 错误信息映射 */\n }\n}\n\n/* ============================================================\n * 预置校验器工厂函数\n * 提供常用的表单校验规则创建函数\n * ============================================================ */\n\n/**\n * 创建必填校验器\n * @param message - 自定义错误提示,默认为 '此字段为必填项'\n * @returns 校验器函数\n */\nexport function required(message = DEFAULT_REQUIRED_MESSAGE): FormValidator {\n return (value) => {\n /* 空字符串、null、undefined 均视为\"空\" */\n if (value === '' || value === null || value === undefined) {\n return message\n }\n\n return null /* 通过校验 */\n }\n}\n\n/**\n * 创建最小长度校验器\n * @param min - 最小字符数\n * @param message - 自定义错误提示\n * @returns 字符串校验器函数\n */\nexport function minLength(min: number, message?: string): FormValidator<string> {\n return (value) => {\n if (typeof value === 'string' && value.length < min) {\n return message ?? `至少需要 ${min} 个字符`\n }\n\n return null\n }\n}\n\n/**\n * 创建最大长度校验器\n * @param max - 最大字符数\n * @param message - 自定义错误提示\n * @returns 字符串校验器函数\n */\nexport function maxLength(max: number, message?: string): FormValidator<string> {\n return (value) => {\n if (typeof value === 'string' && value.length > max) {\n return message ?? `不能超过 ${max} 个字符`\n }\n\n return null\n }\n}\n\n/**\n * 创建正则匹配校验器\n * @param regex - 正则表达式\n * @param message - 自定义错误提示,默认为 '格式不正确'\n * @returns 字符串校验器函数\n */\nexport function pattern(\n regex: RegExp,\n message = DEFAULT_PATTERN_MESSAGE,\n): FormValidator<string> {\n return (value) => {\n if (typeof value === 'string' && !regex.test(value)) {\n return message\n }\n\n return null\n }\n}\n\n/**\n * 创建邮箱格式校验器\n * 基于内置的 EMAIL_PATTERN 正则\n * @param message - 自定义错误提示,默认为 '邮箱格式不正确'\n * @returns 字符串校验器函数\n */\nexport function email(message = DEFAULT_EMAIL_MESSAGE): FormValidator<string> {\n return pattern(EMAIL_PATTERN, message)\n}\n\n/**\n * 创建数值范围校验器\n * @param min - 最小值\n * @param max - 最大值\n * @param message - 自定义错误提示\n * @returns 数字校验器函数\n */\nexport function numberRange(\n min: number,\n max: number,\n message?: string,\n): FormValidator<number> {\n return (value) => {\n if (typeof value === 'number' && (value < min || value > max)) {\n return message ?? `必须在 ${min} 到 ${max} 之间`\n }\n\n return null\n }\n}\n"],"mappings":";;;;;;;;;;;;AAeA,MAAM,2BAA2B;AAEjC,MAAM,0BAA0B;AAEhC,MAAM,wBAAwB;AAE9B,MAAM,gBAAgB;AAqGf,SAAS,aACd,SACuB;CAEvB,MAAM,EACJ,eACA,kBAAkB,CAAC,GACnB,UACA,aAAa,WACX;CAGJ,MAAM,sBAAsB,UAAU,aAAa;CAEnD,MAAM,SAAS,SAAS,UAAU,mBAAmB,CAAC;CAEtD,MAAM,cAAc;CAEpB,MAAM,qBAAqB;CAE3B,MAAM,YAAY,OAAO,KAAK,mBAAmB;CAGjD,MAAM,SAAS,SACb,UAAU,QAAwC,KAAK,QAAQ;EAC7D,IAAI,OAAO;GACT,SAAS;GACT,OAAO;GACP,OAAO;GACP,YAAY;EACd;EACA,OAAO;CACT,GAAG,CAAC,CAAC,CACP;CAIA,MAAM,eAAe,IAAI,KAAK;CAE9B,MAAM,cAAc,IAAmB,IAAI;CAE3C,MAAM,cAAc,IAAI,CAAC;CAIzB,MAAM,UAAU,eAAe,UAAU,MAAK,QAAO,OAAO,IAAG,EAAG,KAAK,CAAC;CAExE,MAAM,UAAU,eAAe,UAAU,OAAM,QAAO,CAAC,OAAO,IAAG,EAAG,KAAK,CAAC;CAG1E,MAAM,SAAS,eAAe;EAC5B,OAAO,UAAU,QAAuC,KAAK,QAAQ;GACnE,IAAI,OAAO,OAAO,IAAG,EAAG,SAAS;GACjC,OAAO;EACT,GAAG,CAAC,CAAC;CACP,CAAC;CAID,UAAU,SAAS,QAAQ;EACzB,YACQ,YAAY,OACjB,aAAa;GACZ,MAAM,QAAQ,OAAO;GACrB,IAAI,OAEF,MAAM,QAAQ,CAAC,UAAU,UAAU,mBAAmB,IAAI;GAI5D,IAAI,eAAe,YAAY,OAAO,SACpC,cAAmB,GAAG;EAE1B,GACA,EAAE,MAAM,KAAK,CACf;CACF,CAAC;CASD,SAAS,cAAiC,OAAU,OAAmB;EACrE,YAAY,SAAmB;CACjC;CAOA,SAAS,gBAAgB,OAAsB;EAC7C,MAAM,aAAa,OAAO;EAC1B,IAAI,YACF,WAAW,UAAU;EAIvB,IAAI,eAAe,QACjB,cAAmB,KAAK;CAE5B;CAQA,eAAe,cAAc,OAAwC;EAEnE,MAAM,aAAa,OAAO;EAG1B,IAAI,CAAC,YACH,OAAO;EAIT,MAAM,aAAa,gBAAgB;EAEnC,IAAI,CAAC,YAAY;GACf,WAAW,QAAQ;GACnB,OAAO;EACT;EAGA,WAAW,aAAa;EAExB,IAAI;GACF,MAAM,QAAQ,OAAO;GAGrB,IAAI,WAAW;QACG,UAAU,MAAM,UAAU,QAAQ,UAAU,KAAA,GAC/C;KAEX,MAAM,UAAU,OAAO,WAAW,aAAa,WAC3C,WAAW,WACX;KACJ,WAAW,QAAQ;KACnB,OAAO;IACT;;GAIF,IAAI,WAAW,OACb,KAAA,MAAW,QAAQ,WAAW,OAAO;IACnC,MAAM,QAAQ,MAAM,KAClB,OACA,WACF;IAEA,IAAI,OAAO;KACT,WAAW,QAAQ;KACnB,OAAO;IACT;GACF;GAIF,WAAW,QAAQ;GACnB,OAAO;EACT,UACA;GAEE,WAAW,aAAa;EAC1B;CACF;CAMA,eAAe,cAAgC;EAE7C,QAAO,MADe,QAAQ,IAAI,UAAU,KAAI,QAAO,cAAc,GAAG,CAAC,CAAC,EAAA,CAC3D,OAAM,WAAU,WAAW,IAAI;CAChD;CASA,eAAe,eAA8B;EAC3C,YAAY;EACZ,YAAY,QAAQ;EAGpB,UAAU,SAAS,QAAQ;GACzB,MAAM,QAAQ,OAAO;GACrB,IAAI,OACF,MAAM,UAAU;EAEpB,CAAC;EAKD,IAAI,CAAC,MAFe,YAAY,KAElB,CAAC,UACb;EAIF,aAAa,QAAQ;EACrB,IAAI;GAEF,MAAM,SAAS,UAAU,MAAM,CAAM;EACvC,SACO,OAAO;GAEZ,YAAY,QAAQ,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EAC3E,UACA;GAEE,aAAa,QAAQ;EACvB;CACF;CAOA,SAAS,MAAM,kBAAqC;EAElD,IAAI,kBACF,OAAO,OAAO,oBAAoB,UAAU,gBAAgB,CAAC;EAI/D,UAAU,SAAS,QAAQ;GAEzB,YAAY,OAAO,UAAU,mBAAmB,IAAI;GAGpD,MAAM,QAAQ,OAAO;GACrB,IAAI,OAAO;IACT,MAAM,UAAU;IAChB,MAAM,QAAQ;IACd,MAAM,QAAQ;IACd,MAAM,aAAa;GACrB;EACF,CAAC;EAGD,YAAY,QAAQ;CACtB;CAMA,SAAS,WAAW,OAAsB;EACxC,MAAM,WAAW;EAEjB,YAAY,YAAY,UAAU,mBAAmB,SAAS;EAG9D,MAAM,aAAa,OAAO;EAC1B,IAAI,YAAY;GACd,WAAW,UAAU;GACrB,WAAW,QAAQ;GACnB,WAAW,QAAQ;GACnB,WAAW,aAAa;EAC1B;CACF;CAGA,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF;AAYO,SAAS,SAAS,UAAU,0BAAyC;CAC1E,QAAQ,UAAU;EAEhB,IAAI,UAAU,MAAM,UAAU,QAAQ,UAAU,KAAA,GAC9C,OAAO;EAGT,OAAO;CACT;AACF;AAQO,SAAS,UAAU,KAAa,SAAyC;CAC9E,QAAQ,UAAU;EAChB,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,KAC9C,OAAO,WAAW,4BAAQ,IAAG;EAG/B,OAAO;CACT;AACF;AAQO,SAAS,UAAU,KAAa,SAAyC;CAC9E,QAAQ,UAAU;EAChB,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,KAC9C,OAAO,WAAW,4BAAQ,IAAG;EAG/B,OAAO;CACT;AACF;AAQO,SAAS,QACd,OACA,UAAU,yBACa;CACvB,QAAQ,UAAU;EAChB,IAAI,OAAO,UAAU,YAAY,CAAC,MAAM,KAAK,KAAK,GAChD,OAAO;EAGT,OAAO;CACT;AACF;AAQO,SAAS,MAAM,UAAU,uBAA8C;CAC5E,OAAO,QAAQ,eAAe,OAAO;AACvC;AASO,SAAS,YACd,KACA,KACA,SACuB;CACvB,QAAQ,UAAU;EAChB,IAAI,OAAO,UAAU,aAAa,QAAQ,OAAO,QAAQ,MACvD,OAAO,WAAW,sBAAO,IAAG,UAAM,IAAG;EAGvC,OAAO;CACT;AACF"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useOptimistic - 乐观更新 Hook
|
|
3
|
+
*
|
|
4
|
+
* 提供乐观更新模式:先立即更新 UI,再异步确认,失败时自动回滚
|
|
5
|
+
* @module composables/useOptimistic
|
|
6
|
+
*/
|
|
7
|
+
import type { Ref } from 'vue';
|
|
8
|
+
/**
|
|
9
|
+
* 乐观更新操作记录
|
|
10
|
+
*/
|
|
11
|
+
export interface OptimisticEntry<T> {
|
|
12
|
+
/** 操作 ID */
|
|
13
|
+
id: string;
|
|
14
|
+
/** 提交前的快照 */
|
|
15
|
+
snapshot: T;
|
|
16
|
+
/** 操作状态 */
|
|
17
|
+
status: 'pending' | 'confirmed' | 'rolled-back';
|
|
18
|
+
/** 时间戳 */
|
|
19
|
+
timestamp: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* useOptimistic 返回值
|
|
23
|
+
*/
|
|
24
|
+
export interface UseOptimisticReturn<T> {
|
|
25
|
+
/** 响应式状态 */
|
|
26
|
+
state: Ref<T>;
|
|
27
|
+
/** 是否有挂起的操作 */
|
|
28
|
+
isPending: Ref<boolean>;
|
|
29
|
+
/** 挂起的操作记录 */
|
|
30
|
+
pending: Ref<OptimisticEntry<T>[]>;
|
|
31
|
+
/** 最近一次操作是否已回滚 */
|
|
32
|
+
isRolledBack: Ref<boolean>;
|
|
33
|
+
/** 执行乐观更新: 先 apply 新值,然后 confirm 异步确认 */
|
|
34
|
+
mutate: (optimisticValue: T | ((prev: T) => T), confirm: () => Promise<void>) => Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 乐观更新 Hook
|
|
38
|
+
*
|
|
39
|
+
* 先立即更新 UI(乐观),再执行异步确认操作。
|
|
40
|
+
* 若确认失败则自动回滚到更新前的状态。
|
|
41
|
+
*
|
|
42
|
+
* @template T - 状态类型
|
|
43
|
+
* @param initialState - 初始状态
|
|
44
|
+
* @returns 乐观更新 API
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* const { state, isPending, isRolledBack, mutate } = useOptimistic({
|
|
49
|
+
* items: ['Apple', 'Banana'],
|
|
50
|
+
* })
|
|
51
|
+
*
|
|
52
|
+
* // 乐观添加
|
|
53
|
+
* await mutate(
|
|
54
|
+
* (prev) => ({ ...prev, items: [...prev.items, 'Cherry'] }),
|
|
55
|
+
* async () => {
|
|
56
|
+
* await fetch('/api/items', {
|
|
57
|
+
* method: 'POST',
|
|
58
|
+
* body: JSON.stringify({ name: 'Cherry' }),
|
|
59
|
+
* })
|
|
60
|
+
* }
|
|
61
|
+
* )
|
|
62
|
+
* // 成功: state 保持新值
|
|
63
|
+
* // 失败: state 自动回滚到添加之前
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare function useOptimistic<T>(initialState: T): UseOptimisticReturn<T>;
|
|
67
|
+
//# sourceMappingURL=useOptimistic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useOptimistic.d.ts","sourceRoot":"","sources":["../../src/composables/useOptimistic.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAC,GAAG,EAAC,MAAM,KAAK,CAAC;AAI7B;;GAEG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,YAAY;IACZ,EAAE,EAAE,MAAM,CAAA;IACV,aAAa;IACb,QAAQ,EAAE,CAAC,CAAA;IACX,WAAW;IACX,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,aAAa,CAAA;IAC/C,UAAU;IACV,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB,CAAC,CAAC;IACpC,YAAY;IACZ,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,CAAA;IACb,eAAe;IACf,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC,CAAA;IACvB,cAAc;IACd,OAAO,EAAE,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAClC,kBAAkB;IAClB,YAAY,EAAE,GAAG,CAAC,OAAO,CAAC,CAAA;IAC1B,yCAAyC;IACzC,MAAM,EAAE,CACN,eAAe,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EACrC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KACzB,OAAO,CAAC,IAAI,CAAC,CAAA;CACnB;AASD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,GAAG,mBAAmB,CAAC,CAAC,CAAC,CAgExE"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* ***********************************
|
|
3
|
+
* @composy/store-vue v0.0.1 *
|
|
4
|
+
* Built with rollup *
|
|
5
|
+
* Build time: 2024-08-17 11:39:29 *
|
|
6
|
+
* Build mode: production *
|
|
7
|
+
* Minified: No *
|
|
8
|
+
* ***********************************
|
|
9
|
+
*/
|
|
10
|
+
import { ref } from "vue";
|
|
11
|
+
import { deepClone } from "@composy/store-core";
|
|
12
|
+
//#region src/composables/useOptimistic.ts
|
|
13
|
+
function uid() {
|
|
14
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
15
|
+
}
|
|
16
|
+
function useOptimistic(initialState) {
|
|
17
|
+
const state = ref(deepClone(initialState));
|
|
18
|
+
const isPending = ref(false);
|
|
19
|
+
const isRolledBack = ref(false);
|
|
20
|
+
const pending = ref([]);
|
|
21
|
+
async function mutate(optimisticValue, confirm) {
|
|
22
|
+
const snapshot = deepClone(state.value);
|
|
23
|
+
const entryId = uid();
|
|
24
|
+
const entry = {
|
|
25
|
+
id: entryId,
|
|
26
|
+
snapshot,
|
|
27
|
+
status: "pending",
|
|
28
|
+
timestamp: Date.now()
|
|
29
|
+
};
|
|
30
|
+
pending.value = [...pending.value, entry];
|
|
31
|
+
isPending.value = true;
|
|
32
|
+
isRolledBack.value = false;
|
|
33
|
+
if (typeof optimisticValue === "function") state.value = optimisticValue(state.value);
|
|
34
|
+
else state.value = optimisticValue;
|
|
35
|
+
try {
|
|
36
|
+
await confirm();
|
|
37
|
+
pending.value = pending.value.map((e) => e.id === entryId ? {
|
|
38
|
+
...e,
|
|
39
|
+
status: "confirmed"
|
|
40
|
+
} : e);
|
|
41
|
+
} catch {
|
|
42
|
+
state.value = snapshot;
|
|
43
|
+
isRolledBack.value = true;
|
|
44
|
+
pending.value = pending.value.map((e) => e.id === entryId ? {
|
|
45
|
+
...e,
|
|
46
|
+
status: "rolled-back"
|
|
47
|
+
} : e);
|
|
48
|
+
} finally {
|
|
49
|
+
pending.value = pending.value.filter((e) => e.status === "pending");
|
|
50
|
+
isPending.value = pending.value.length > 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
state,
|
|
55
|
+
isPending,
|
|
56
|
+
pending,
|
|
57
|
+
isRolledBack,
|
|
58
|
+
mutate
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export { useOptimistic };
|
|
63
|
+
/*! End of @composy/store-vue | Powered by @composy/builder */
|
|
64
|
+
//# sourceMappingURL=useOptimistic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useOptimistic.js","names":[],"sources":["../../src/composables/useOptimistic.ts"],"sourcesContent":["/**\n * useOptimistic - 乐观更新 Hook\n *\n * 提供乐观更新模式:先立即更新 UI,再异步确认,失败时自动回滚\n * @module composables/useOptimistic\n */\n\nimport type {Ref} from 'vue';\nimport { deepClone } from '@composy/store-core'\nimport { ref } from 'vue'\n\n/**\n * 乐观更新操作记录\n */\nexport interface OptimisticEntry<T> {\n /** 操作 ID */\n id: string\n /** 提交前的快照 */\n snapshot: T\n /** 操作状态 */\n status: 'pending' | 'confirmed' | 'rolled-back'\n /** 时间戳 */\n timestamp: number\n}\n\n/**\n * useOptimistic 返回值\n */\nexport interface UseOptimisticReturn<T> {\n /** 响应式状态 */\n state: Ref<T>\n /** 是否有挂起的操作 */\n isPending: Ref<boolean>\n /** 挂起的操作记录 */\n pending: Ref<OptimisticEntry<T>[]>\n /** 最近一次操作是否已回滚 */\n isRolledBack: Ref<boolean>\n /** 执行乐观更新: 先 apply 新值,然后 confirm 异步确认 */\n mutate: (\n optimisticValue: T | ((prev: T) => T),\n confirm: () => Promise<void>,\n ) => Promise<void>\n}\n\n/**\n * 生成唯一 ID\n */\nfunction uid(): string {\n return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`\n}\n\n/**\n * 乐观更新 Hook\n *\n * 先立即更新 UI(乐观),再执行异步确认操作。\n * 若确认失败则自动回滚到更新前的状态。\n *\n * @template T - 状态类型\n * @param initialState - 初始状态\n * @returns 乐观更新 API\n *\n * @example\n * ```typescript\n * const { state, isPending, isRolledBack, mutate } = useOptimistic({\n * items: ['Apple', 'Banana'],\n * })\n *\n * // 乐观添加\n * await mutate(\n * (prev) => ({ ...prev, items: [...prev.items, 'Cherry'] }),\n * async () => {\n * await fetch('/api/items', {\n * method: 'POST',\n * body: JSON.stringify({ name: 'Cherry' }),\n * })\n * }\n * )\n * // 成功: state 保持新值\n * // 失败: state 自动回滚到添加之前\n * ```\n */\nexport function useOptimistic<T>(initialState: T): UseOptimisticReturn<T> {\n const state = ref<T>(deepClone(initialState)) as Ref<T>\n const isPending = ref(false)\n const isRolledBack = ref(false)\n const pending = ref<OptimisticEntry<T>[]>([]) as Ref<OptimisticEntry<T>[]>\n\n async function mutate(\n optimisticValue: T | ((prev: T) => T),\n confirm: () => Promise<void>,\n ): Promise<void> {\n // 保存快照\n const snapshot: T = deepClone(state.value)\n const entryId = uid()\n const entry: OptimisticEntry<T> = {\n id: entryId,\n snapshot,\n status: 'pending',\n timestamp: Date.now(),\n }\n\n pending.value = [...pending.value, entry]\n isPending.value = true\n isRolledBack.value = false\n\n // 立即应用乐观值\n if (typeof optimisticValue === 'function') {\n state.value = (optimisticValue as (prev: T) => T)(state.value)\n }\n else {\n state.value = optimisticValue\n }\n\n try {\n // 异步确认\n await confirm()\n\n // 确认成功\n pending.value = pending.value.map(e =>\n e.id === entryId ? { ...e, status: 'confirmed' as const } : e,\n )\n }\n catch {\n // 确认失败 - 回滚\n state.value = snapshot\n isRolledBack.value = true\n\n pending.value = pending.value.map(e =>\n e.id === entryId ? { ...e, status: 'rolled-back' as const } : e,\n )\n }\n finally {\n // 清除已完成的条目\n pending.value = pending.value.filter(e => e.status === 'pending')\n isPending.value = pending.value.length > 0\n }\n }\n\n return {\n state,\n isPending,\n pending,\n isRolledBack,\n mutate,\n }\n}\n"],"mappings":";;;;;;;;;;;;AA+CA,SAAS,MAAc;CACrB,OAAO,GAAG,KAAK,IAAI,EAAC,GAAI,KAAK,OAAO,CAAA,CAAE,SAAS,EAAE,CAAA,CAAE,MAAM,GAAG,CAAC;AAC/D;AAgCO,SAAS,cAAiB,cAAyC;CACxE,MAAM,QAAQ,IAAO,UAAU,YAAY,CAAC;CAC5C,MAAM,YAAY,IAAI,KAAK;CAC3B,MAAM,eAAe,IAAI,KAAK;CAC9B,MAAM,UAAU,IAA0B,CAAC,CAAC;CAE5C,eAAe,OACb,iBACA,SACe;EAEf,MAAM,WAAc,UAAU,MAAM,KAAK;EACzC,MAAM,UAAU,IAAI;EACpB,MAAM,QAA4B;GAChC,IAAI;GACJ;GACA,QAAQ;GACR,WAAW,KAAK,IAAI;EACtB;EAEA,QAAQ,QAAQ,CAAC,GAAG,QAAQ,OAAO,KAAK;EACxC,UAAU,QAAQ;EAClB,aAAa,QAAQ;EAGrB,IAAI,OAAO,oBAAoB,YAC7B,MAAM,QAAS,gBAAmC,MAAM,KAAK;OAG7D,MAAM,QAAQ;EAGhB,IAAI;GAEF,MAAM,QAAQ;GAGd,QAAQ,QAAQ,QAAQ,MAAM,KAAI,MAChC,EAAE,OAAO,UAAU;IAAE,GAAG;IAAG,QAAQ;GAAqB,IAAI,CAC9D;EACF,QACM;GAEJ,MAAM,QAAQ;GACd,aAAa,QAAQ;GAErB,QAAQ,QAAQ,QAAQ,MAAM,KAAI,MAChC,EAAE,OAAO,UAAU;IAAE,GAAG;IAAG,QAAQ;GAAuB,IAAI,CAChE;EACF,UACA;GAEE,QAAQ,QAAQ,QAAQ,MAAM,QAAO,MAAK,EAAE,WAAW,SAAS;GAChE,UAAU,QAAQ,QAAQ,MAAM,SAAS;EAC3C;CACF;CAEA,OAAO;EACL;EACA;EACA;EACA;EACA;CACF;AACF"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePersist Composable
|
|
3
|
+
*
|
|
4
|
+
* 提供状态持久化功能
|
|
5
|
+
* @module composables/usePersist
|
|
6
|
+
*/
|
|
7
|
+
import type { Ref } from 'vue';
|
|
8
|
+
import type { UsePersistOptions, UsePersistReturn } from '../types';
|
|
9
|
+
/**
|
|
10
|
+
* 状态持久化 Hook
|
|
11
|
+
*
|
|
12
|
+
* 将状态自动保存到 localStorage 或 sessionStorage
|
|
13
|
+
*
|
|
14
|
+
* @template T - 值类型
|
|
15
|
+
* @param options - 持久化选项
|
|
16
|
+
* @returns 持久化操作对象
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const { value, save, restore, clear } = usePersist<User>({
|
|
21
|
+
* key: 'user-info',
|
|
22
|
+
* storage: 'localStorage',
|
|
23
|
+
* defaultValue: { name: '', age: 0 }
|
|
24
|
+
* })
|
|
25
|
+
*
|
|
26
|
+
* // 更新值会自动保存
|
|
27
|
+
* value.value = { name: 'John', age: 25 }
|
|
28
|
+
*
|
|
29
|
+
* // 手动保存
|
|
30
|
+
* save()
|
|
31
|
+
*
|
|
32
|
+
* // 从存储恢复
|
|
33
|
+
* restore()
|
|
34
|
+
*
|
|
35
|
+
* // 清除存储
|
|
36
|
+
* clear()
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function usePersist<T>(options: UsePersistOptions): UsePersistReturn<T>;
|
|
40
|
+
/**
|
|
41
|
+
* 创建持久化的 ref
|
|
42
|
+
*
|
|
43
|
+
* @template T - 值类型
|
|
44
|
+
* @param key - 存储键名
|
|
45
|
+
* @param defaultValue - 默认值
|
|
46
|
+
* @param storage - 存储类型
|
|
47
|
+
* @returns 持久化的 ref
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* const count = usePersistedRef('count', 0)
|
|
52
|
+
*
|
|
53
|
+
* // 更新会自动保存
|
|
54
|
+
* count.value++
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare function usePersistedRef<T>(key: string, defaultValue: T, storage?: 'localStorage' | 'sessionStorage'): Ref<T>;
|
|
58
|
+
/**
|
|
59
|
+
* 创建带过期时间的持久化 ref
|
|
60
|
+
*
|
|
61
|
+
* @template T - 值类型
|
|
62
|
+
* @param key - 存储键名
|
|
63
|
+
* @param defaultValue - 默认值
|
|
64
|
+
* @param ttl - 过期时间(毫秒)
|
|
65
|
+
* @returns 持久化的 ref
|
|
66
|
+
*/
|
|
67
|
+
export declare function usePersistedRefWithTTL<T>(key: string, defaultValue: T, ttl: number): Ref<T>;
|
|
68
|
+
//# sourceMappingURL=usePersist.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usePersist.d.ts","sourceRoot":"","sources":["../../src/composables/usePersist.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAC9B,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAanE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CAAC,CAAC,CAAC,CA8E7E;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAC/B,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,CAAC,EACf,OAAO,GAAE,cAAc,GAAG,gBAAiC,GAC1D,GAAG,CAAC,CAAC,CAAC,CAOR;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,EACtC,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,CAAC,EACf,GAAG,EAAE,MAAM,GACV,GAAG,CAAC,CAAC,CAAC,CA8BR"}
|