@pubinfo/core 2.1.0 → 2.1.2

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.
Files changed (67) hide show
  1. package/dist/{AppSetting-Dzj6YBgW.js → AppSetting-CmT5_15W.js} +15 -15
  2. package/dist/{HCheckList.vue_vue_type_script_setup_true_lang-OWv01eXk.js → HCheckList.vue_vue_type_script_setup_true_lang-CHzkJth7.js} +1 -1
  3. package/dist/{HToggle-BnESuuPG.js → HToggle-DpDYLh8y.js} +1 -1
  4. package/dist/HeaderThinMenu-D6jF8yl1.js +4 -0
  5. package/dist/{PreferencesContent-DBKQJ8Ob.js → PreferencesContent-AXqWatOF.js} +4 -4
  6. package/dist/{SettingBreadcrumb-DmP1nJLZ.js → SettingBreadcrumb-CBWdS_nO.js} +3 -3
  7. package/dist/{SettingCopyright-C8_F7BB2.js → SettingCopyright-D5Jhdu1J.js} +2 -2
  8. package/dist/{SettingEnableTransition-BeJMq6ny.js → SettingEnableTransition-DQJozSZH.js} +2 -2
  9. package/dist/{SettingHome-yGmbxzJm.js → SettingHome-DAwn9Ypx.js} +2 -2
  10. package/dist/{SettingMenu-yWnqZqmz.js → SettingMenu-DEQRAtWl.js} +3 -3
  11. package/dist/{SettingMode-B8fgUhvi.js → SettingMode-bh3I8UBZ.js} +1 -1
  12. package/dist/{SettingNavSearch--yQEXqEL.js → SettingNavSearch-iYc-eRzY.js} +2 -2
  13. package/dist/{SettingOther-CSs1_D9L.js → SettingOther-C2TS6okv.js} +2 -2
  14. package/dist/{SettingPage-Pm7KblGG.js → SettingPage-B2_SNyn5.js} +2 -2
  15. package/dist/{SettingTabbar-DM5hdlQf.js → SettingTabbar-BETdKJxz.js} +3 -3
  16. package/dist/{SettingThemes-DqygGgDK.js → SettingThemes-ComNCP3P.js} +12 -11
  17. package/dist/{SettingToolbar-BP-yWiKo.js → SettingToolbar-D0DTBbbb.js} +2 -2
  18. package/dist/{SettingTopbar-Dd_SXy2e.js → SettingTopbar-BcO5Hcxm.js} +3 -3
  19. package/dist/{SettingWidthMode-DOtJg5uB.js → SettingWidthMode-wzTMq96A.js} +1 -1
  20. package/dist/built-in/pinia-plugin/plugins/persist.d.ts +2 -2
  21. package/dist/built-in/pinia-plugin/plugins/persistedstate/index.d.ts +3 -0
  22. package/dist/built-in/pinia-plugin/plugins/persistedstate/persistedstate.d.ts +15 -0
  23. package/dist/built-in/pinia-plugin/plugins/persistedstate/types.d.ts +150 -0
  24. package/dist/built-in/pinia-plugin/plugins/persistedstate/utils.d.ts +17 -0
  25. package/dist/core/ctx.d.ts +1 -0
  26. package/dist/core/interface.d.ts +26 -1
  27. package/dist/core/utils/index.d.ts +2 -0
  28. package/dist/features/composables/index.d.ts +1 -0
  29. package/dist/features/composables/partyLogin.d.ts +7 -1
  30. package/dist/{index-npW0xuOi.js → index-6W8u4oWQ.js} +1 -1
  31. package/dist/{index-igxVB1m3.js → index-B9i7R1pn.js} +2 -2
  32. package/dist/{index-C88VclMA.js → index-BXLF9xfN.js} +16300 -15834
  33. package/dist/{index-CnbD4YWA.js → index-C5X0cH7a.js} +3 -3
  34. package/dist/{index--xVOZLmn.js → index-CMSPnrUx.js} +1 -1
  35. package/dist/index-DSKHePRb.js +4 -0
  36. package/dist/{index-D8kKHmiD.js → index-FATjHAwl.js} +1 -1
  37. package/dist/{index-BSTB6EhQ.js → index-_VKoUSGo.js} +1 -1
  38. package/dist/index.d.ts +2 -1
  39. package/dist/index.js +13 -12
  40. package/dist/{pick-l2RrF3SE.js → pick-BvMRfqim.js} +1 -1
  41. package/dist/utils/index.d.ts +0 -1
  42. package/package.json +9 -8
  43. package/src/built-in/pinia-plugin/index.ts +2 -2
  44. package/src/built-in/pinia-plugin/plugins/persist.ts +15 -4
  45. package/src/built-in/pinia-plugin/plugins/persistedstate/README.md +551 -0
  46. package/src/built-in/pinia-plugin/plugins/persistedstate/index.ts +3 -0
  47. package/src/built-in/pinia-plugin/plugins/persistedstate/persistedstate.ts +575 -0
  48. package/src/built-in/pinia-plugin/plugins/persistedstate/types.ts +162 -0
  49. package/src/built-in/pinia-plugin/plugins/persistedstate/utils.ts +46 -0
  50. package/src/core/ctx.ts +24 -1
  51. package/src/core/interface.ts +31 -1
  52. package/src/core/resolver/icon.ts +1 -1
  53. package/src/core/utils/index.ts +2 -0
  54. package/src/features/composables/index.ts +1 -0
  55. package/src/features/composables/partyLogin.ts +180 -38
  56. package/src/features/context/index.ts +1 -1
  57. package/src/index.ts +7 -5
  58. package/src/utils/index.ts +0 -1
  59. package/src/utils/proxy.ts +1 -1
  60. package/types/index.d.ts +1 -0
  61. package/types/pinia.d.ts +94 -0
  62. package/dist/HeaderThinMenu-Co2S6vIB.js +0 -4
  63. package/dist/index-GSKGoyv_.js +0 -4
  64. /package/dist/{utils → core/utils}/global.d.ts +0 -0
  65. /package/dist/core/{utils.d.ts → utils/utils.d.ts} +0 -0
  66. /package/src/{utils → core/utils}/global.ts +0 -0
  67. /package/src/core/{utils.ts → utils/utils.ts} +0 -0
@@ -0,0 +1,162 @@
1
+ import type { PiniaPluginContext, StateTree } from 'pinia';
2
+
3
+ /**
4
+ * 存储区域类型:本地存储或会话存储
5
+ */
6
+ export type StorageArea = 'local' | 'session';
7
+
8
+ /**
9
+ * 类似 Storage 的接口,用于抽象存储操作
10
+ */
11
+ export interface StorageLike {
12
+ /** 获取指定键的值 */
13
+ getItem: (key: string) => string | null
14
+ /** 设置指定键的值 */
15
+ setItem: (key: string, value: string) => void
16
+ }
17
+
18
+ /**
19
+ * 序列化器接口,用于状态的序列化和反序列化
20
+ */
21
+ export interface Serializer {
22
+ /** 将状态对象序列化为字符串 */
23
+ serialize: (data: StateTree) => string
24
+ /** 将字符串反序列化为状态对象 */
25
+ deserialize: (data: string) => StateTree
26
+ }
27
+
28
+ /**
29
+ * 持久化配置接口
30
+ */
31
+ export interface Persistence<State extends StateTree = StateTree> {
32
+ /** 存储键名 */
33
+ key: string
34
+ /** 是否开启调试模式 */
35
+ debug: boolean
36
+ /** 存储实例 */
37
+ storage: StorageLike
38
+ /** 序列化器 */
39
+ serializer: Serializer
40
+ /** 恢复数据前的钩子 */
41
+ beforeHydrate?: (context: PiniaPluginContext) => void
42
+ /** 恢复数据后的钩子 */
43
+ afterHydrate?: (context: PiniaPluginContext) => void
44
+ /** 要持久化的状态字段(白名单) */
45
+ pick?: string[]
46
+ /** 不需要持久化的状态字段(黑名单) */
47
+ omit?: string[]
48
+ }
49
+
50
+ /**
51
+ * 插件基础配置选项
52
+ */
53
+ export interface BasePluginOptions {
54
+ /** 存储实例 */
55
+ storage?: StorageLike
56
+ /** 序列化器 */
57
+ serializer?: Serializer
58
+ /** 是否开启调试模式 */
59
+ debug?: boolean
60
+ /** 存储键名生成函数 */
61
+ key?: (storeKey: string) => string
62
+ /** 是否自动为所有 store 启用持久化 */
63
+ auto?: boolean
64
+ }
65
+
66
+ /**
67
+ * 持久化选项类型(提供给 store 使用)
68
+ */
69
+ export type PersistenceOptions<State extends StateTree = StateTree> = Partial<Omit<Persistence<State>, 'key'>> & {
70
+ /** 自定义存储键名 */
71
+ key?: string
72
+ };
73
+
74
+ /**
75
+ * 扩展的持久化选项,包含存储区域配置
76
+ */
77
+ export type ExtendedPersistenceOptions<State extends StateTree = StateTree> = PersistenceOptions<State> & {
78
+ /** 存储区域类型 */
79
+ storageArea?: StorageArea
80
+ };
81
+
82
+ /**
83
+ * 自定义存储事件详情
84
+ */
85
+ export interface CustomStorageEventDetail {
86
+ /** 变化的键名,null 表示清空操作 */
87
+ key: string | null
88
+ /** 新值 */
89
+ newValue: string | null
90
+ /** 旧值 */
91
+ oldValue: string | null
92
+ /** 存储区域 */
93
+ storageArea: StorageArea
94
+ }
95
+
96
+ /**
97
+ * 标准化后的全局配置选项
98
+ */
99
+ export interface NormalizedGlobalOptions {
100
+ /** 键名生成函数 */
101
+ key: (storeKey: string) => string
102
+ /** 序列化器 */
103
+ serializer: Serializer
104
+ /** 存储实例 */
105
+ storage?: StorageLike
106
+ /** 存储区域 */
107
+ storageArea?: StorageArea
108
+ /** 是否开启调试 */
109
+ debug: boolean
110
+ /** 是否自动持久化 */
111
+ auto: boolean
112
+ }
113
+
114
+ /**
115
+ * 持久化条目,包含完整的配置信息
116
+ */
117
+ export interface PersistenceEntry<State extends StateTree = StateTree> {
118
+ /** 存储键名 */
119
+ key: string
120
+ /** 存储实例 */
121
+ storage: StorageLike
122
+ /** 序列化器 */
123
+ serializer: Serializer
124
+ /** 存储区域 */
125
+ storageArea?: StorageArea
126
+ /** 完整的存储键名(用于同步监听) */
127
+ fullKey?: string
128
+ /** 是否开启调试 */
129
+ debug: boolean
130
+ /** 恢复前钩子 */
131
+ beforeHydrate?: (context: PiniaPluginContext) => void
132
+ /** 恢复后钩子 */
133
+ afterHydrate?: (context: PiniaPluginContext) => void
134
+ /** 白名单字段 */
135
+ pick?: PersistenceOptions<State>['pick']
136
+ /** 黑名单字段 */
137
+ omit?: PersistenceOptions<State>['omit']
138
+ }
139
+
140
+ /**
141
+ * 存储绑定,关联 store 和持久化配置
142
+ */
143
+ export interface StorageBinding<State extends StateTree = StateTree> {
144
+ /** Pinia 插件上下文 */
145
+ context: PiniaPluginContext
146
+ /** 持久化条目 */
147
+ entry: PersistenceEntry<State>
148
+ /** 跳过持久化次数(用于避免循环) */
149
+ skipPersist: number
150
+ /** 停止订阅的函数 */
151
+ stopSubscription: () => void
152
+ /** 初始状态快照 */
153
+ initialState: StateTree
154
+ }
155
+
156
+ /**
157
+ * 同步持久化状态插件的配置选项
158
+ */
159
+ export interface SyncedPersistedStateOptions extends BasePluginOptions {
160
+ /** 存储区域 */
161
+ storageArea?: StorageArea
162
+ }
@@ -0,0 +1,46 @@
1
+ import type { CustomStorageEventDetail, StorageArea } from './types';
2
+
3
+ /** 自定义事件名称,用于跨标签页通信 */
4
+ export const CUSTOM_EVENT = 'pubinfo:persistedstate:local';
5
+
6
+ /**
7
+ * 调度微任务,优先使用原生 queueMicrotask
8
+ */
9
+ export const scheduleMicrotask = typeof queueMicrotask === 'function'
10
+ ? queueMicrotask
11
+ : (fn: () => void) => {
12
+ Promise.resolve().then(fn);
13
+ };
14
+
15
+ /**
16
+ * 解析存储区域类型
17
+ * @param storage - Storage 实例
18
+ * @returns 存储区域类型或 undefined
19
+ */
20
+ export function resolveStorageArea(storage: Storage | null | undefined): StorageArea | undefined {
21
+ if (!storage || typeof window === 'undefined') {
22
+ return undefined;
23
+ }
24
+ try {
25
+ if (storage === window.localStorage) {
26
+ return 'local';
27
+ }
28
+ if (storage === window.sessionStorage) {
29
+ return 'session';
30
+ }
31
+ }
32
+ catch {
33
+ return undefined;
34
+ }
35
+ return undefined;
36
+ }
37
+
38
+ /**
39
+ * 分发本地 storage 变更事件
40
+ */
41
+ export function dispatchLocalMutation(detail: CustomStorageEventDetail) {
42
+ if (typeof window === 'undefined' || typeof window.CustomEvent !== 'function' || typeof window.dispatchEvent !== 'function') {
43
+ return;
44
+ }
45
+ window.dispatchEvent(new CustomEvent<CustomStorageEventDetail>(CUSTOM_EVENT, { detail }));
46
+ }
package/src/core/ctx.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createHooks } from 'hookable';
2
- import { getPubinfoNamespace } from '../utils/global';
2
+ import { getPubinfoNamespace } from './utils';
3
3
 
4
4
  const HOOKS_KEY = '__pubinfo_core_hooks__';
5
5
  type HookStore = ReturnType<typeof createHooks>;
@@ -21,3 +21,26 @@ export async function callHookAsync<T>(hookname: string, data: T) {
21
21
  data,
22
22
  );
23
23
  }
24
+
25
+ export function callHookSync<T>(hookname: string, data: T) {
26
+ return hooks.callHookWith(
27
+ (hookFns, args) => {
28
+ let res = args[0] as T;
29
+ if (!hookFns.length) {
30
+ return res;
31
+ }
32
+ for (const fn of hookFns) {
33
+ const output = fn(res);
34
+ if (output && typeof (output as PromiseLike<unknown>).then === 'function') {
35
+ throw new Error(`[hooks] "${hookname}" 当前仅支持同步回调`);
36
+ }
37
+ if (typeof output !== 'undefined') {
38
+ res = output as T;
39
+ }
40
+ }
41
+ return res;
42
+ },
43
+ hookname,
44
+ data,
45
+ );
46
+ }
@@ -1,7 +1,7 @@
1
1
  import type { AdapterCreateOptions, AlovaAxiosRequestConfig } from '@alova/adapter-axios';
2
2
  import type { Alova, AlovaGenerics, AlovaOptions, Method, ResponseCompleteHandler, ResponseErrorHandler } from 'alova';
3
3
  import type { AxiosResponse, AxiosResponseHeaders } from 'axios';
4
- import type { Pinia } from 'pinia';
4
+ import type { Pinia, StateTree } from 'pinia';
5
5
  import type { App, Component } from 'vue';
6
6
  import type { Router, RouterOptions as VueRouterOptions } from 'vue-router';
7
7
  import type { MenuData, RouteData } from '@/features';
@@ -100,6 +100,12 @@ export interface Hooks {
100
100
 
101
101
  /** 路由数据转换 */
102
102
  'route:transform': (routeData: RouteData[]) => RouteData[] | Promise<RouteData[]>
103
+
104
+ /** Pinia 持久化写入前处理(需同步返回) */
105
+ 'pinia:persist:write': (payload: PiniaPersistWriteHookPayload) => PiniaPersistWriteHookPayload
106
+
107
+ /** Pinia 持久化读取前处理(需同步返回) */
108
+ 'pinia:persist:read': (payload: PiniaPersistReadHookPayload) => PiniaPersistReadHookPayload
103
109
  }
104
110
 
105
111
  // overwrite alova
@@ -110,3 +116,27 @@ export interface RespondedHandlerRecord<AG extends AlovaGenerics> {
110
116
  onError?: ResponseErrorHandler<AG>
111
117
  onComplete?: ResponseCompleteHandler<AG>
112
118
  }
119
+
120
+ export interface PiniaPersistHookBasePayload {
121
+ storeId: string
122
+ /** 实际存储键 */
123
+ key: string
124
+ /** local 或 session */
125
+ storageArea?: 'local' | 'session'
126
+ /** 仅在 localStorage 同步时可用 */
127
+ fullKey?: string
128
+ }
129
+
130
+ export interface PiniaPersistWriteHookPayload extends PiniaPersistHookBasePayload {
131
+ /** 序列化后的字符串 */
132
+ value: string
133
+ /** 过滤后的状态对象 */
134
+ state: StateTree
135
+ }
136
+
137
+ export interface PiniaPersistReadHookPayload extends PiniaPersistHookBasePayload {
138
+ /** 从存储读取的原始字符串 */
139
+ value: string
140
+ /** 实际读取数据的键 */
141
+ sourceKey?: string
142
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ModuleRecord } from './resolver';
2
- import { getPubinfoNamespace } from '../../utils/global';
2
+ import { getPubinfoNamespace } from '../utils';
3
3
  import { loadProjectModules, readProjectModules } from './resolver';
4
4
 
5
5
  // 持久化图标模块,支持 HMR 后恢复
@@ -0,0 +1,2 @@
1
+ export * from './global';
2
+ export * from './utils';
@@ -1,4 +1,5 @@
1
1
  export * from './auth';
2
2
  export * from './log';
3
+ export * from './partyLogin';
3
4
  export * from './theme';
4
5
  export * from './watchDiff';
@@ -1,28 +1,36 @@
1
+ import type { Pinia } from 'pinia';
1
2
  import type { PartyLoginConfig, PartyLoginParamItem } from '../components/PartyLogin/type';
3
+ import { getActivePinia } from 'pinia';
2
4
  import { ref } from 'vue';
3
- import {
4
- useFavoritesStore,
5
- useIframeStore,
6
- useKeepAliveStore,
7
- useMenuStore,
8
- useRouteStore,
9
- useSettingsStore,
10
- useTabbarStore,
11
- useUserStore,
12
- } from '@/features/stores';
13
-
14
- // Store 名称映射表
15
- const STORE_MAP: Record<string, () => any> = {
16
- user: useUserStore,
17
- settings: useSettingsStore,
18
- menu: useMenuStore,
19
- route: useRouteStore,
20
- tabbar: useTabbarStore,
21
- iframe: useIframeStore,
22
- keepAlive: useKeepAliveStore,
23
- favorites: useFavoritesStore,
5
+
6
+ export {
7
+ PartyLoginConfig,
24
8
  };
25
9
 
10
+ const SUPPORTED_STORE_IDS = new Set([
11
+ 'user',
12
+ 'settings',
13
+ 'menu',
14
+ 'route',
15
+ 'tabbar',
16
+ 'iframe',
17
+ 'keepAlive',
18
+ 'favorites',
19
+ ]);
20
+
21
+ const missingPiniaWarnings = new Set<string>();
22
+
23
+ function warnMissingPinia(storeName: string) {
24
+ if (missingPiniaWarnings.has(storeName)) {
25
+ return;
26
+ }
27
+ missingPiniaWarnings.add(storeName);
28
+ console.warn(
29
+ `[usePartyLogin] Pinia instance is not ready when trying to access the "${storeName}" store. `
30
+ + 'Provide a Pinia instance via usePartyLogin(..., { pinia }) or ensure Pinia is registered before calling this hook.',
31
+ );
32
+ }
33
+
26
34
  /**
27
35
  * 根据路径获取对象的嵌套值
28
36
  * @param obj 目标对象
@@ -48,6 +56,11 @@ function getNestedValue(obj: any, path: string): any {
48
56
  return result ?? '';
49
57
  }
50
58
 
59
+ function getStoreFromPinia(pinia: Pinia | undefined, storeName: string) {
60
+ const storeMap = (pinia as any)?._s as Map<string, any> | undefined;
61
+ return storeMap?.get(storeName);
62
+ }
63
+
51
64
  /**
52
65
  * 解析参数值
53
66
  *
@@ -95,6 +108,7 @@ function getNestedValue(obj: any, path: string): any {
95
108
  function resolveParamValue(
96
109
  item: PartyLoginParamItem,
97
110
  previousResponse?: Record<string, any>,
111
+ pinia?: Pinia,
98
112
  ): string | number | boolean {
99
113
  switch (item.type) {
100
114
  case 'string':
@@ -116,13 +130,23 @@ function resolveParamValue(
116
130
  }
117
131
 
118
132
  try {
119
- const useStore = STORE_MAP[storeName];
120
- if (!useStore) {
121
- console.warn(`Store "${storeName}" not found in STORE_MAP`);
133
+ if (!SUPPORTED_STORE_IDS.has(storeName)) {
134
+ console.warn(`Store "${storeName}" is not supported in usePartyLogin.`);
135
+ return '';
136
+ }
137
+
138
+ const activePinia = pinia ?? getActivePinia();
139
+ if (!activePinia) {
140
+ warnMissingPinia(storeName);
141
+ return '';
142
+ }
143
+
144
+ const store = getStoreFromPinia(activePinia, storeName);
145
+ if (!store) {
146
+ warnMissingStore(storeName);
122
147
  return '';
123
148
  }
124
149
 
125
- const store = useStore();
126
150
  return getNestedValue(store, fieldPath);
127
151
  }
128
152
  catch (error) {
@@ -151,12 +175,13 @@ function resolveParamValue(
151
175
  function buildParams(
152
176
  params: PartyLoginParamItem[],
153
177
  previousResponse?: Record<string, any>,
178
+ pinia?: Pinia,
154
179
  ): Record<string, any> {
155
180
  const result: Record<string, any> = {};
156
181
 
157
182
  params.forEach((item) => {
158
183
  if (item.key) {
159
- result[item.key] = resolveParamValue(item, previousResponse);
184
+ result[item.key] = resolveParamValue(item, previousResponse, pinia);
160
185
  }
161
186
  });
162
187
 
@@ -173,16 +198,112 @@ function buildOAuthUrl(
173
198
  baseUrl: string,
174
199
  params: PartyLoginParamItem[],
175
200
  previousResponse?: Record<string, any>,
201
+ pinia?: Pinia,
176
202
  ): string {
177
- const queryParams = buildParams(params, previousResponse);
203
+ const queryParams = buildParams(params, previousResponse, pinia);
204
+ const entries = Object.entries(queryParams);
205
+
206
+ if (entries.length === 0) {
207
+ return safeDecodeUrl(baseUrl);
208
+ }
178
209
 
179
- const url = new URL(baseUrl);
180
- Object.entries(queryParams).forEach(([key, value]) => {
181
- url.searchParams.append(key, String(value));
210
+ const searchParams = new URLSearchParams();
211
+ entries.forEach(([key, value]) => {
212
+ searchParams.append(key, String(value));
182
213
  });
214
+ const serializedParams = searchParams.toString();
215
+
216
+ const hashIndex = baseUrl.indexOf('#');
217
+ const hasHash = hashIndex >= 0;
218
+ const basePart = hasHash ? baseUrl.slice(0, hashIndex) : baseUrl;
219
+ const hashContent = hasHash ? baseUrl.slice(hashIndex + 1) : '';
220
+
221
+ if (hasHash && shouldAppendParamsToHash(hashContent)) {
222
+ const hashWithQuery = appendParamsToHash(hashContent, serializedParams);
223
+ const finalUrl = `${basePart}#${hashWithQuery}`;
224
+ return safeDecodeUrl(finalUrl);
225
+ }
226
+
227
+ const baseWithQuery = appendParamsToBaseUrl(basePart, searchParams, serializedParams);
228
+ const finalUrl = hasHash ? `${baseWithQuery}#${hashContent}` : baseWithQuery;
183
229
 
184
230
  // 解码 URL,将编码的中文转换为可读形式
185
- return decodeURIComponent(url.toString());
231
+ return safeDecodeUrl(finalUrl);
232
+ }
233
+
234
+ function shouldAppendParamsToHash(hashContent: string): boolean {
235
+ if (!hashContent) {
236
+ return false;
237
+ }
238
+
239
+ const trimmed = hashContent.trim();
240
+ if (!trimmed) {
241
+ return false;
242
+ }
243
+
244
+ return trimmed.startsWith('/')
245
+ || trimmed.startsWith('!/')
246
+ || trimmed.startsWith('?');
247
+ }
248
+
249
+ function appendParamsToHash(hashContent: string, serializedParams: string): string {
250
+ if (!serializedParams) {
251
+ return hashContent;
252
+ }
253
+
254
+ if (!hashContent) {
255
+ return `?${serializedParams}`;
256
+ }
257
+
258
+ const separator = resolveQuerySeparator(hashContent);
259
+ return `${hashContent}${separator}${serializedParams}`;
260
+ }
261
+
262
+ function appendParamsToBaseUrl(
263
+ baseUrl: string,
264
+ params: URLSearchParams,
265
+ serializedParams: string,
266
+ ): string {
267
+ if (!serializedParams) {
268
+ return baseUrl;
269
+ }
270
+
271
+ if (!baseUrl) {
272
+ return `?${serializedParams}`;
273
+ }
274
+
275
+ try {
276
+ const url = new URL(baseUrl);
277
+ params.forEach((value, key) => {
278
+ url.searchParams.append(key, value);
279
+ });
280
+ return url.toString();
281
+ }
282
+ catch {
283
+ const separator = resolveQuerySeparator(baseUrl);
284
+ return `${baseUrl}${separator}${serializedParams}`;
285
+ }
286
+ }
287
+
288
+ function resolveQuerySeparator(baseUrl: string): string {
289
+ if (!baseUrl) {
290
+ return '?';
291
+ }
292
+
293
+ if (!baseUrl.includes('?')) {
294
+ return '?';
295
+ }
296
+
297
+ return /[?&]$/.test(baseUrl) ? '' : '&';
298
+ }
299
+
300
+ function safeDecodeUrl(value: string): string {
301
+ try {
302
+ return decodeURIComponent(value);
303
+ }
304
+ catch {
305
+ return value;
306
+ }
186
307
  }
187
308
 
188
309
  /**
@@ -278,13 +399,27 @@ function deepParseJSON(obj: any): any {
278
399
  return obj;
279
400
  }
280
401
 
402
+ export interface UsePartyLoginOptions {
403
+ execute?: boolean
404
+ pinia?: Pinia
405
+ }
406
+
281
407
  /**
282
408
  * Party Login Hook
283
409
  * 处理第三方登录的多步骤流程
284
410
  * @param config Party Login 配置
285
411
  * @param targetUrl 目标跳转 URL (来自 meta.link 或 meta.iframe)
286
412
  */
287
- export async function usePartyLogin(config: PartyLoginConfig, targetUrl: string) {
413
+ export async function usePartyLogin(
414
+ config: PartyLoginConfig,
415
+ targetUrl: string,
416
+ options: UsePartyLoginOptions = {},
417
+ ) {
418
+ const {
419
+ execute: shouldAutoExecute = true,
420
+ pinia: injectedPinia,
421
+ } = options;
422
+
288
423
  const url = ref<string>('');
289
424
  const loading = ref(false);
290
425
  const error = ref<Error | null>(null);
@@ -319,12 +454,13 @@ export async function usePartyLogin(config: PartyLoginConfig, targetUrl: string)
319
454
  }
320
455
 
321
456
  const { url: requestUrl, method, params } = step1.request;
457
+ const activePinia = injectedPinia ?? getActivePinia();
322
458
 
323
459
  // 构建第一步请求的各类参数(此时还没有 previousResponse,所以传 undefined)
324
- const queryParams = params.query ? buildParams(params.query) : {};
325
- const bodyParams = params.body ? buildParams(params.body) : {};
326
- const headerParams = params.header ? buildParams(params.header) : {};
327
- const cookieParams = params.cookie ? buildParams(params.cookie) : {};
460
+ const queryParams = params.query ? buildParams(params.query, undefined, activePinia) : {};
461
+ const bodyParams = params.body ? buildParams(params.body, undefined, activePinia) : {};
462
+ const headerParams = params.header ? buildParams(params.header, undefined, activePinia) : {};
463
+ const cookieParams = params.cookie ? buildParams(params.cookie, undefined, activePinia) : {};
328
464
 
329
465
  // 构建请求头
330
466
  const headers: Record<string, string> = {};
@@ -397,6 +533,7 @@ export async function usePartyLogin(config: PartyLoginConfig, targetUrl: string)
397
533
  targetUrl,
398
534
  step2.oauth.params,
399
535
  responseData,
536
+ activePinia,
400
537
  );
401
538
 
402
539
  url.value = oauthUrl;
@@ -411,8 +548,10 @@ export async function usePartyLogin(config: PartyLoginConfig, targetUrl: string)
411
548
  }
412
549
  }
413
550
 
414
- // 自动执行
415
- await execute();
551
+ if (shouldAutoExecute) {
552
+ // 自动执行
553
+ await execute();
554
+ }
416
555
 
417
556
  return {
418
557
  url,
@@ -421,3 +560,6 @@ export async function usePartyLogin(config: PartyLoginConfig, targetUrl: string)
421
560
  execute,
422
561
  };
423
562
  }
563
+ function warnMissingStore(storeName: string) {
564
+ console.warn(`[usePartyLogin] Store "${storeName}" is not registered in Pinia.`);
565
+ }
@@ -1,7 +1,7 @@
1
1
  import type { Router } from 'vue-router';
2
2
  import type { RequestInstance } from '@/core';
3
3
 
4
- import { createContext } from '@/utils';
4
+ import { createContext } from '../../core/utils';
5
5
 
6
6
  interface InternalContext {
7
7
  router: Router
package/src/index.ts CHANGED
@@ -107,19 +107,21 @@ export {
107
107
  readProjectModules,
108
108
  } from './core';
109
109
  export * from './core/interface';
110
+ export {
111
+ clearPersistedState,
112
+ createContext,
113
+ getPersistedState,
114
+ getPubinfoNamespace,
115
+ setPersistedState,
116
+ } from './core/utils';
110
117
 
111
118
  export * from './features';
112
119
 
113
120
  export {
114
121
  cleanup,
115
122
  cleanupWithoutUser,
116
- clearPersistedState,
117
- createContext,
118
123
  createRawContext,
119
- getPersistedState,
120
- getPubinfoNamespace,
121
124
  publicKeyEncryption,
122
- setPersistedState,
123
125
  storage,
124
126
  wrapProxy,
125
127
  } from './utils';
@@ -1,5 +1,4 @@
1
1
  export * from './cleanup';
2
- export * from './global';
3
2
  export * from './path';
4
3
  export * from './proxy';
5
4
  export * from './publicKey';
@@ -1,4 +1,4 @@
1
- import { getPersistedState } from './global';
1
+ import { getPersistedState } from '../core/utils';
2
2
 
3
3
  export function wrapProxy<T extends Record<K, any>, K extends keyof T>(
4
4
  fn: () => T,
package/types/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import './favorites.d.ts';
2
2
  import './iframe.d.ts';
3
3
  import './menu.d.ts';
4
4
  import './module.d.ts';
5
+ import './pinia.d.ts';
5
6
  import './settings.d.ts';
6
7
  import './tabbar.d.ts';
7
8
  import './user.d.ts';