@pubinfo/core 2.0.15 → 2.1.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/dist/{AppSetting-CF0Y36Kx.js → AppSetting-CmT5_15W.js} +17 -17
- package/dist/{HCheckList.vue_vue_type_script_setup_true_lang-B7ZyR4n6.js → HCheckList.vue_vue_type_script_setup_true_lang-CHzkJth7.js} +1 -1
- package/dist/{HToggle-Dap0xf8L.js → HToggle-DpDYLh8y.js} +1 -1
- package/dist/HeaderThinMenu-D6jF8yl1.js +4 -0
- package/dist/{PreferencesContent-DWHQ2EOo.js → PreferencesContent-AXqWatOF.js} +6 -6
- package/dist/{SettingBreadcrumb-P-C21e5U.js → SettingBreadcrumb-CBWdS_nO.js} +3 -3
- package/dist/{SettingCopyright-XUQuMUND.js → SettingCopyright-D5Jhdu1J.js} +2 -2
- package/dist/{SettingEnableTransition-hJTXOtuP.js → SettingEnableTransition-DQJozSZH.js} +2 -2
- package/dist/{SettingHome-1C9Ph1Dl.js → SettingHome-DAwn9Ypx.js} +3 -3
- package/dist/{SettingMenu-B2h4e61Z.js → SettingMenu-DEQRAtWl.js} +4 -4
- package/dist/{SettingMode-bStWvnHg.js → SettingMode-bh3I8UBZ.js} +1 -1
- package/dist/{SettingNavSearch-BvTlahqj.js → SettingNavSearch-iYc-eRzY.js} +3 -3
- package/dist/{SettingOther-ihHycIPn.js → SettingOther-C2TS6okv.js} +3 -3
- package/dist/{SettingPage-d8HGOfhZ.js → SettingPage-B2_SNyn5.js} +2 -2
- package/dist/{SettingTabbar-YLshyP8G.js → SettingTabbar-BETdKJxz.js} +6 -6
- package/dist/{SettingThemes-Do-SwCzx.js → SettingThemes-ComNCP3P.js} +16 -15
- package/dist/{SettingToolbar-DHbr1d6B.js → SettingToolbar-D0DTBbbb.js} +3 -3
- package/dist/{SettingTopbar-QwqH9maD.js → SettingTopbar-BcO5Hcxm.js} +4 -4
- package/dist/{SettingWidthMode-CGmv4JQk.js → SettingWidthMode-wzTMq96A.js} +2 -2
- package/dist/built-in/devtools/index.d.ts +13 -0
- package/dist/built-in/index.d.ts +1 -0
- package/dist/built-in/layout-component/Layout.vue.d.ts +7 -1
- package/dist/built-in/layout-component/components/Topbar/index.vue.d.ts +1 -1
- package/dist/built-in/layout-component/composables/useLayoutVisible.d.ts +8 -0
- package/dist/built-in/layout-component/interface.d.ts +22 -0
- package/dist/built-in/pinia-plugin/plugins/persist.d.ts +2 -2
- package/dist/built-in/pinia-plugin/plugins/persistedstate/index.d.ts +3 -0
- package/dist/built-in/pinia-plugin/plugins/persistedstate/persistedstate.d.ts +15 -0
- package/dist/built-in/pinia-plugin/plugins/persistedstate/types.d.ts +150 -0
- package/dist/built-in/pinia-plugin/plugins/persistedstate/utils.d.ts +17 -0
- package/dist/core/ctx.d.ts +1 -0
- package/dist/core/interface.d.ts +26 -1
- package/dist/core/utils/index.d.ts +2 -0
- package/dist/features/composables/index.d.ts +1 -0
- package/dist/features/composables/partyLogin.d.ts +7 -1
- package/dist/features/stores/modules/settings.d.ts +6 -6
- package/dist/features/stores/modules/user.d.ts +14 -2
- package/dist/{index-WLA0JU4Y.js → index-6W8u4oWQ.js} +1 -1
- package/dist/{index-CEodMjIg.js → index-B9i7R1pn.js} +4 -5
- package/dist/{index-Ctu3_aXu.js → index-BXLF9xfN.js} +17666 -17025
- package/dist/{index-egA-td8O.js → index-C5X0cH7a.js} +3 -3
- package/dist/{index-Da79wYQP.js → index-CMSPnrUx.js} +1 -1
- package/dist/index-DSKHePRb.js +4 -0
- package/dist/{index-B8wbywmR.js → index-FATjHAwl.js} +1 -1
- package/dist/{index-B69sIuLj.js → index-_VKoUSGo.js} +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +13 -12
- package/dist/{pick-BS5wr8z9.js → pick-BvMRfqim.js} +1 -1
- package/dist/{question-line-CfkciTFq.js → question-line-DCMVyZ3e.js} +2 -2
- package/dist/{right-Bfe2p1o0.js → right-aIVrXLnr.js} +4 -4
- package/dist/style.css +1 -1
- package/dist/utils/index.d.ts +0 -1
- package/package.json +10 -9
- package/src/built-in/devtools/index.ts +292 -0
- package/src/built-in/index.ts +1 -0
- package/src/built-in/layout-component/Layout.vue +93 -9
- package/src/built-in/layout-component/components/Header/index.vue +4 -11
- package/src/built-in/layout-component/components/Sidebar/MainSidebar.vue +3 -1
- package/src/built-in/layout-component/components/Sidebar/SubSidebar.vue +4 -21
- package/src/built-in/layout-component/components/Topbar/index.vue +4 -105
- package/src/built-in/layout-component/composables/useLayoutVisible.ts +87 -0
- package/src/built-in/layout-component/interface.ts +25 -0
- package/src/built-in/pinia-plugin/index.ts +2 -2
- package/src/built-in/pinia-plugin/plugins/persist.ts +15 -4
- package/src/built-in/pinia-plugin/plugins/persistedstate/README.md +551 -0
- package/src/built-in/pinia-plugin/plugins/persistedstate/index.ts +3 -0
- package/src/built-in/pinia-plugin/plugins/persistedstate/persistedstate.ts +575 -0
- package/src/built-in/pinia-plugin/plugins/persistedstate/types.ts +162 -0
- package/src/built-in/pinia-plugin/plugins/persistedstate/utils.ts +46 -0
- package/src/core/create.ts +1 -3
- package/src/core/ctx.ts +24 -1
- package/src/core/interface.ts +31 -1
- package/src/core/request.ts +0 -1
- package/src/core/resolver/icon.ts +1 -1
- package/src/core/utils/index.ts +2 -0
- package/src/features/assets/styles/globals.css +2 -1
- package/src/features/composables/index.ts +1 -0
- package/src/features/composables/log.ts +3 -5
- package/src/features/composables/partyLogin.ts +180 -38
- package/src/features/context/index.ts +1 -1
- package/src/features/stores/modules/keepAlive.ts +10 -14
- package/src/features/stores/modules/settings.ts +2 -7
- package/src/features/stores/modules/user.ts +57 -2
- package/src/index.ts +9 -5
- package/src/utils/index.ts +0 -1
- package/src/utils/proxy.ts +1 -1
- package/types/index.d.ts +1 -0
- package/types/pinia.d.ts +94 -0
- package/types/vue-router.d.ts +7 -0
- package/dist/HeaderThinMenu-Bu4X2-vs.js +0 -4
- package/dist/index-ondkWqUz.js +0 -4
- /package/dist/{utils → core/utils}/global.d.ts +0 -0
- /package/dist/core/{utils.d.ts → utils/utils.d.ts} +0 -0
- /package/src/{utils → core/utils}/global.ts +0 -0
- /package/src/core/{utils.ts → utils/utils.ts} +0 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
import type { PiniaPluginContext, StateTree } from 'pinia';
|
|
2
|
+
import type {
|
|
3
|
+
CustomStorageEventDetail,
|
|
4
|
+
ExtendedPersistenceOptions,
|
|
5
|
+
NormalizedGlobalOptions,
|
|
6
|
+
PersistenceEntry,
|
|
7
|
+
Serializer,
|
|
8
|
+
StorageArea,
|
|
9
|
+
StorageBinding,
|
|
10
|
+
SyncedPersistedStateOptions,
|
|
11
|
+
} from './types';
|
|
12
|
+
import type { PiniaPersistReadHookPayload, PiniaPersistWriteHookPayload } from '@/core/interface';
|
|
13
|
+
import { deepOmitUnsafe, deepPickUnsafe } from 'deep-pick-omit';
|
|
14
|
+
import { destr } from 'destr';
|
|
15
|
+
import { cloneDeep } from 'lodash-es';
|
|
16
|
+
import { callHookSync } from '@/core/ctx';
|
|
17
|
+
import { CUSTOM_EVENT, dispatchLocalMutation, resolveStorageArea, scheduleMicrotask } from './utils';
|
|
18
|
+
|
|
19
|
+
export type {
|
|
20
|
+
PersistenceOptions,
|
|
21
|
+
Serializer,
|
|
22
|
+
StorageLike,
|
|
23
|
+
SyncedPersistedStateOptions,
|
|
24
|
+
} from './types';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 默认的序列化器,使用 JSON.stringify 和 destr
|
|
28
|
+
*/
|
|
29
|
+
const DEFAULT_SERIALIZER: Serializer = {
|
|
30
|
+
serialize: (data) => {
|
|
31
|
+
return JSON.stringify(data);
|
|
32
|
+
},
|
|
33
|
+
deserialize: (value) => {
|
|
34
|
+
return destr<StateTree>(value);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
interface BindingHandlers {
|
|
39
|
+
hydrate: (binding: StorageBinding, options?: { runHooks?: boolean, skipPersist?: boolean }) => void
|
|
40
|
+
reset: (binding: StorageBinding) => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface BindingRegistry {
|
|
44
|
+
register: (binding: StorageBinding) => void
|
|
45
|
+
unregister: (binding: StorageBinding) => void
|
|
46
|
+
suppressNextLocalEvent: (fullKey?: string) => void
|
|
47
|
+
ensurePatchedFor: (entry: PersistenceEntry) => void
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createBindingRegistry(
|
|
51
|
+
_globalOptions: NormalizedGlobalOptions,
|
|
52
|
+
handlers: BindingHandlers,
|
|
53
|
+
): BindingRegistry {
|
|
54
|
+
const keyBindingRegistry = new Map<string, StorageBinding[]>();
|
|
55
|
+
const suppressedLocalEvents = new Set<string>();
|
|
56
|
+
let listenersReady = false;
|
|
57
|
+
let localPatchApplied = false;
|
|
58
|
+
|
|
59
|
+
ensureListeners();
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
register(binding) {
|
|
63
|
+
const { entry } = binding;
|
|
64
|
+
if (!entry.fullKey) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const list = keyBindingRegistry.get(entry.fullKey) ?? [];
|
|
68
|
+
list.push(binding);
|
|
69
|
+
keyBindingRegistry.set(entry.fullKey, list);
|
|
70
|
+
},
|
|
71
|
+
unregister(binding) {
|
|
72
|
+
const { entry } = binding;
|
|
73
|
+
if (!entry.fullKey) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const bindings = keyBindingRegistry.get(entry.fullKey);
|
|
77
|
+
if (!bindings) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const next = bindings.filter(item => item !== binding);
|
|
81
|
+
if (next.length) {
|
|
82
|
+
keyBindingRegistry.set(entry.fullKey, next);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
keyBindingRegistry.delete(entry.fullKey);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
suppressNextLocalEvent(fullKey) {
|
|
89
|
+
if (!fullKey) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
suppressedLocalEvents.add(fullKey);
|
|
93
|
+
scheduleMicrotask(() => {
|
|
94
|
+
suppressedLocalEvents.delete(fullKey);
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
ensurePatchedFor(entry) {
|
|
98
|
+
if (entry.storageArea === 'local') {
|
|
99
|
+
ensureStoragePatch();
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function ensureListeners() {
|
|
105
|
+
if (listenersReady || typeof window === 'undefined') {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
window.addEventListener('storage', (event) => {
|
|
110
|
+
if (!event.key) {
|
|
111
|
+
if (event.storageArea === window.localStorage) {
|
|
112
|
+
handleStorageClear('local');
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const storageArea: StorageArea | undefined = event.storageArea === window.localStorage
|
|
118
|
+
? 'local'
|
|
119
|
+
: event.storageArea === window.sessionStorage
|
|
120
|
+
? 'session'
|
|
121
|
+
: undefined;
|
|
122
|
+
|
|
123
|
+
if (!storageArea) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
handleStorageSignal({
|
|
128
|
+
key: event.key,
|
|
129
|
+
newValue: event.newValue,
|
|
130
|
+
oldValue: event.oldValue,
|
|
131
|
+
storageArea,
|
|
132
|
+
}, 'native');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
window.addEventListener(CUSTOM_EVENT, (event) => {
|
|
136
|
+
const detail = (event as CustomEvent<CustomStorageEventDetail>).detail;
|
|
137
|
+
if (!detail) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
handleStorageSignal(detail, 'local');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
listenersReady = true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function ensureStoragePatch() {
|
|
147
|
+
if (localPatchApplied || typeof window === 'undefined') {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const StorageCtor = window.Storage;
|
|
152
|
+
if (!StorageCtor || !StorageCtor.prototype) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const prototype = StorageCtor.prototype;
|
|
157
|
+
const originalSetItem = prototype.setItem;
|
|
158
|
+
const originalRemoveItem = prototype.removeItem;
|
|
159
|
+
const originalClear = prototype.clear;
|
|
160
|
+
|
|
161
|
+
if (typeof originalSetItem !== 'function' || typeof originalRemoveItem !== 'function' || typeof originalClear !== 'function') {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const dispatchFromInstance = (storage: Storage, detail: Omit<CustomStorageEventDetail, 'storageArea'> & { storageArea?: StorageArea }) => {
|
|
166
|
+
const storageArea = detail.storageArea ?? resolveStorageArea(storage);
|
|
167
|
+
if (!storageArea) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (storageArea === 'local') {
|
|
171
|
+
dispatchLocalMutation({
|
|
172
|
+
key: detail.key,
|
|
173
|
+
newValue: detail.newValue,
|
|
174
|
+
oldValue: detail.oldValue,
|
|
175
|
+
storageArea,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
prototype.setItem = function patchedSetItem(key: string, value: string) {
|
|
181
|
+
const storage = this as Storage;
|
|
182
|
+
const oldValue = storage.getItem(key);
|
|
183
|
+
originalSetItem.call(storage, key, value);
|
|
184
|
+
dispatchFromInstance(storage, { key, newValue: value, oldValue });
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
prototype.removeItem = function patchedRemoveItem(key: string) {
|
|
188
|
+
const storage = this as Storage;
|
|
189
|
+
const oldValue = storage.getItem(key);
|
|
190
|
+
originalRemoveItem.call(storage, key);
|
|
191
|
+
dispatchFromInstance(storage, { key, newValue: null, oldValue });
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
prototype.clear = function patchedClear() {
|
|
195
|
+
const storage = this as Storage;
|
|
196
|
+
const storageArea = resolveStorageArea(storage);
|
|
197
|
+
originalClear.call(storage);
|
|
198
|
+
if (storageArea === 'local') {
|
|
199
|
+
dispatchLocalMutation({
|
|
200
|
+
key: null,
|
|
201
|
+
newValue: null,
|
|
202
|
+
oldValue: null,
|
|
203
|
+
storageArea,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
localPatchApplied = true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function handleStorageClear(area: StorageArea) {
|
|
212
|
+
keyBindingRegistry.forEach((bindings) => {
|
|
213
|
+
bindings.forEach((binding) => {
|
|
214
|
+
if (binding.entry.storageArea === area) {
|
|
215
|
+
handlers.reset(binding);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function handleStorageSignal(detail: CustomStorageEventDetail, source: 'native' | 'local') {
|
|
222
|
+
if (!detail.key) {
|
|
223
|
+
if (detail.storageArea === 'local') {
|
|
224
|
+
handleStorageClear('local');
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (detail.storageArea === 'local' && source === 'local' && shouldIgnoreLocalEvent(detail.key)) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const bindings = keyBindingRegistry.get(detail.key);
|
|
234
|
+
if (!bindings?.length) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
bindings.forEach((binding) => {
|
|
239
|
+
if (detail.newValue === null) {
|
|
240
|
+
handlers.reset(binding);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
handlers.hydrate(binding, { runHooks: false, skipPersist: true });
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function shouldIgnoreLocalEvent(fullKey: string) {
|
|
249
|
+
if (!suppressedLocalEvents.has(fullKey)) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
suppressedLocalEvents.delete(fullKey);
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 解析存储区域类型
|
|
259
|
+
* @param storage - Storage 实例
|
|
260
|
+
* @returns 存储区域类型或 undefined
|
|
261
|
+
*/
|
|
262
|
+
/**
|
|
263
|
+
* 创建同步持久化状态插件
|
|
264
|
+
* 该插件支持跨标签页状态同步,并提供完整的持久化控制
|
|
265
|
+
* @param options - 插件配置选项
|
|
266
|
+
* @returns Pinia 插件函数
|
|
267
|
+
*/
|
|
268
|
+
export function createSyncedPersistedState(options: SyncedPersistedStateOptions = {}) {
|
|
269
|
+
// 在非浏览器环境中提前返回空操作插件(用于 SSR)
|
|
270
|
+
if (typeof window === 'undefined') {
|
|
271
|
+
return function noopPlugin() {
|
|
272
|
+
// SSR 环境下的空操作插件
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const globalOptions = normalizeGlobalOptions(options);
|
|
277
|
+
const registry = createBindingRegistry(globalOptions, {
|
|
278
|
+
hydrate: (binding, hydrateOptions) => hydrateBinding(binding, hydrateOptions),
|
|
279
|
+
reset: resetBinding,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* 同步持久化状态插件的实际实现
|
|
284
|
+
* @param context - Pinia 插件上下文
|
|
285
|
+
*/
|
|
286
|
+
return function syncedPersistedStatePlugin(context: PiniaPluginContext) {
|
|
287
|
+
// 标准化持久化选项
|
|
288
|
+
const persistOptions = normalizeStorePersistOptions(context.options.persist, globalOptions.auto);
|
|
289
|
+
if (!persistOptions?.length) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 解析所有持久化条目
|
|
294
|
+
const entries = persistOptions
|
|
295
|
+
.map((option) => {
|
|
296
|
+
return resolvePersistenceEntry(context, option, globalOptions);
|
|
297
|
+
})
|
|
298
|
+
.filter((entry): entry is PersistenceEntry => Boolean(entry));
|
|
299
|
+
|
|
300
|
+
if (!entries.length) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const storeBindings: StorageBinding[] = [];
|
|
305
|
+
// 保存初始状态快照,用于重置
|
|
306
|
+
const initialState = cloneDeep(context.store.$state);
|
|
307
|
+
|
|
308
|
+
// 为每个持久化条目创建绑定
|
|
309
|
+
entries.forEach((entry) => {
|
|
310
|
+
registry.ensurePatchedFor(entry);
|
|
311
|
+
|
|
312
|
+
const binding: StorageBinding = {
|
|
313
|
+
context,
|
|
314
|
+
entry,
|
|
315
|
+
skipPersist: 0,
|
|
316
|
+
stopSubscription: () => { },
|
|
317
|
+
initialState,
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// 订阅 store 状态变化
|
|
321
|
+
binding.stopSubscription = context.store.$subscribe((_mutation, state) => {
|
|
322
|
+
if (binding.skipPersist > 0) {
|
|
323
|
+
// 跳过本次持久化(避免循环)
|
|
324
|
+
binding.skipPersist -= 1;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
persistState(
|
|
328
|
+
state,
|
|
329
|
+
entry,
|
|
330
|
+
context.store.$id,
|
|
331
|
+
binding.entry.fullKey ? () => registry.suppressNextLocalEvent(binding.entry.fullKey) : undefined,
|
|
332
|
+
);
|
|
333
|
+
}, { detached: true });
|
|
334
|
+
|
|
335
|
+
// 初始恢复状态
|
|
336
|
+
hydrateBinding(binding, { skipPersist: true });
|
|
337
|
+
registry.register(binding);
|
|
338
|
+
storeBindings.push(binding);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// 添加 $hydrate 方法,用于手动恢复状态
|
|
342
|
+
context.store.$hydrate = ({ runHooks = true } = {}) => {
|
|
343
|
+
storeBindings.forEach(binding => hydrateBinding(binding, { runHooks }));
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// 添加 $persist 方法,用于手动持久化状态
|
|
347
|
+
context.store.$persist = () => {
|
|
348
|
+
storeBindings.forEach((binding) => {
|
|
349
|
+
persistState(
|
|
350
|
+
context.store.$state,
|
|
351
|
+
binding.entry,
|
|
352
|
+
context.store.$id,
|
|
353
|
+
binding.entry.fullKey ? () => registry.suppressNextLocalEvent(binding.entry.fullKey) : undefined,
|
|
354
|
+
);
|
|
355
|
+
});
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
// 增强 $dispose 方法,清理资源
|
|
359
|
+
const originalDispose = context.store.$dispose.bind(context.store);
|
|
360
|
+
context.store.$dispose = () => {
|
|
361
|
+
storeBindings.forEach((binding) => {
|
|
362
|
+
binding.stopSubscription();
|
|
363
|
+
registry.unregister(binding);
|
|
364
|
+
});
|
|
365
|
+
originalDispose();
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* 标准化全局配置选项
|
|
372
|
+
* @param options - 原始配置选项
|
|
373
|
+
* @returns 标准化后的配置
|
|
374
|
+
*/
|
|
375
|
+
function normalizeGlobalOptions(options: SyncedPersistedStateOptions): NormalizedGlobalOptions {
|
|
376
|
+
return {
|
|
377
|
+
key: options.key ?? (value => value),
|
|
378
|
+
serializer: options.serializer ?? DEFAULT_SERIALIZER,
|
|
379
|
+
storage: options.storage,
|
|
380
|
+
storageArea: options.storageArea ?? 'local',
|
|
381
|
+
debug: options.debug ?? false,
|
|
382
|
+
auto: options.auto ?? false,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* 标准化 store 的持久化选项
|
|
388
|
+
* @param persist - 原始持久化配置
|
|
389
|
+
* @param auto - 是否自动持久化
|
|
390
|
+
* @returns 标准化后的选项数组
|
|
391
|
+
*/
|
|
392
|
+
function normalizeStorePersistOptions<State extends StateTree>(
|
|
393
|
+
persist: boolean | ExtendedPersistenceOptions<State> | ExtendedPersistenceOptions<State>[] | undefined,
|
|
394
|
+
auto: boolean,
|
|
395
|
+
) {
|
|
396
|
+
if (Array.isArray(persist)) {
|
|
397
|
+
return persist;
|
|
398
|
+
}
|
|
399
|
+
if (persist === true) {
|
|
400
|
+
return [{}];
|
|
401
|
+
}
|
|
402
|
+
if (persist && typeof persist === 'object') {
|
|
403
|
+
return [persist];
|
|
404
|
+
}
|
|
405
|
+
if (auto) {
|
|
406
|
+
return [{}];
|
|
407
|
+
}
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* 解析持久化条目
|
|
413
|
+
* @param context - Pinia 插件上下文
|
|
414
|
+
* @param option - 持久化选项
|
|
415
|
+
* @param globalOptions - 全局配置
|
|
416
|
+
* @returns 持久化条目或 null
|
|
417
|
+
*/
|
|
418
|
+
function resolvePersistenceEntry(
|
|
419
|
+
context: PiniaPluginContext,
|
|
420
|
+
option: ExtendedPersistenceOptions,
|
|
421
|
+
globalOptions: NormalizedGlobalOptions,
|
|
422
|
+
): PersistenceEntry | null {
|
|
423
|
+
const keyBuilder = globalOptions.key ?? ((value: string) => value);
|
|
424
|
+
const resolvedKey = keyBuilder(option.key ?? context.store.$id);
|
|
425
|
+
const storage = option.storage ?? globalOptions.storage;
|
|
426
|
+
|
|
427
|
+
if (!storage) {
|
|
428
|
+
if (globalOptions.debug) {
|
|
429
|
+
console.warn('[persistedstate] storage is not available for store', context.store.$id);
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const serializer = option.serializer ?? globalOptions.serializer;
|
|
435
|
+
const storageArea = option.storageArea ?? globalOptions.storageArea;
|
|
436
|
+
const storageKey = resolvedKey;
|
|
437
|
+
const syncKey = storageArea === 'local' ? storageKey : undefined;
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
key: storageKey,
|
|
441
|
+
storage,
|
|
442
|
+
serializer,
|
|
443
|
+
debug: option.debug ?? globalOptions.debug,
|
|
444
|
+
beforeHydrate: option.beforeHydrate,
|
|
445
|
+
afterHydrate: option.afterHydrate,
|
|
446
|
+
pick: option.pick,
|
|
447
|
+
omit: option.omit,
|
|
448
|
+
storageArea,
|
|
449
|
+
fullKey: syncKey,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* 持久化状态到存储
|
|
455
|
+
* @param state - 状态对象
|
|
456
|
+
* @param entry - 持久化条目
|
|
457
|
+
*/
|
|
458
|
+
function persistState(
|
|
459
|
+
state: StateTree,
|
|
460
|
+
entry: PersistenceEntry,
|
|
461
|
+
storeId: string,
|
|
462
|
+
suppressEvent?: () => void,
|
|
463
|
+
) {
|
|
464
|
+
try {
|
|
465
|
+
// 应用白名单过滤
|
|
466
|
+
const picked = entry.pick ? deepPickUnsafe(state, entry.pick as string[]) : state;
|
|
467
|
+
// 应用黑名单过滤
|
|
468
|
+
const omitted = entry.omit ? deepOmitUnsafe(picked, entry.omit as string[]) : picked;
|
|
469
|
+
// 序列化状态
|
|
470
|
+
const value = entry.serializer.serialize(omitted as StateTree);
|
|
471
|
+
const payload = callHookSync<PiniaPersistWriteHookPayload>('pinia:persist:write', {
|
|
472
|
+
storeId,
|
|
473
|
+
key: entry.key,
|
|
474
|
+
fullKey: entry.fullKey,
|
|
475
|
+
storageArea: entry.storageArea,
|
|
476
|
+
state: omitted as StateTree,
|
|
477
|
+
value,
|
|
478
|
+
});
|
|
479
|
+
const nextValue = payload?.value ?? value;
|
|
480
|
+
|
|
481
|
+
// 抑制由 Pinia store 状态变化引起的本地事件,避免循环更新
|
|
482
|
+
// 但允许其他方式(如手动修改 localStorage)触发的事件通过
|
|
483
|
+
suppressEvent?.();
|
|
484
|
+
entry.storage.setItem(entry.key, nextValue);
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
if (entry.debug) {
|
|
488
|
+
console.error('[persistedstate] failed to persist store', error);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* 从存储恢复状态到 store
|
|
495
|
+
*/
|
|
496
|
+
function hydrateBinding(
|
|
497
|
+
binding: StorageBinding,
|
|
498
|
+
options: { runHooks?: boolean, skipPersist?: boolean } = {},
|
|
499
|
+
) {
|
|
500
|
+
const { entry, context } = binding;
|
|
501
|
+
const { runHooks = true, skipPersist = false } = options;
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
// 执行恢复前钩子
|
|
505
|
+
if (runHooks) {
|
|
506
|
+
entry.beforeHydrate?.(context);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// 从存储中读取数据
|
|
510
|
+
const sourceKey = entry.key;
|
|
511
|
+
const value = entry.storage.getItem(entry.key);
|
|
512
|
+
if (value === null) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const readPayload = callHookSync<PiniaPersistReadHookPayload>('pinia:persist:read', {
|
|
517
|
+
storeId: context.store.$id,
|
|
518
|
+
key: entry.key,
|
|
519
|
+
fullKey: entry.fullKey,
|
|
520
|
+
storageArea: entry.storageArea,
|
|
521
|
+
sourceKey,
|
|
522
|
+
value,
|
|
523
|
+
});
|
|
524
|
+
const normalizedValue = readPayload?.value ?? value;
|
|
525
|
+
|
|
526
|
+
// 反序列化数据
|
|
527
|
+
const parsed = entry.serializer.deserialize(normalizedValue);
|
|
528
|
+
// 应用白名单过滤
|
|
529
|
+
const picked = entry.pick ? deepPickUnsafe(parsed, entry.pick as string[]) : parsed;
|
|
530
|
+
// 应用黑名单过滤
|
|
531
|
+
const omitted = entry.omit ? deepOmitUnsafe(picked, entry.omit as string[]) : picked;
|
|
532
|
+
|
|
533
|
+
// 标记跳过下一次持久化(避免循环)
|
|
534
|
+
if (skipPersist) {
|
|
535
|
+
binding.skipPersist += 1;
|
|
536
|
+
}
|
|
537
|
+
// 更新 store 状态
|
|
538
|
+
context.store.$patch(omitted);
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
if (entry.debug) {
|
|
542
|
+
console.error('[persistedstate] failed to hydrate store', error);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
finally {
|
|
546
|
+
// 执行恢复后钩子
|
|
547
|
+
if (runHooks) {
|
|
548
|
+
entry.afterHydrate?.(context);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* 重置绑定到初始状态
|
|
555
|
+
* @param binding - 存储绑定
|
|
556
|
+
*/
|
|
557
|
+
function resetBinding(binding: StorageBinding) {
|
|
558
|
+
const snapshot = binding.initialState;
|
|
559
|
+
const { entry, context } = binding;
|
|
560
|
+
try {
|
|
561
|
+
// 应用白名单过滤
|
|
562
|
+
const picked = entry.pick ? deepPickUnsafe(snapshot, entry.pick as string[]) : snapshot;
|
|
563
|
+
// 应用黑名单过滤
|
|
564
|
+
const omitted = entry.omit ? deepOmitUnsafe(picked, entry.omit as string[]) : picked;
|
|
565
|
+
// 标记跳过下一次持久化
|
|
566
|
+
binding.skipPersist += 1;
|
|
567
|
+
// 使用深拷贝避免引用问题
|
|
568
|
+
context.store.$patch(cloneDeep(omitted));
|
|
569
|
+
}
|
|
570
|
+
catch (error) {
|
|
571
|
+
if (entry.debug) {
|
|
572
|
+
console.error('[persistedstate] failed to reset store', error);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|