ch3chi-commons-vue 1.8.0 → 1.8.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/package.json +2 -1
- package/src/api/ApiService.ts +869 -0
- package/src/auth/AuthorizationService.ts +138 -0
- package/src/auth/PermissionDescriptor.ts +99 -0
- package/src/auth/keys.ts +5 -0
- package/src/components/CAlert.vue +188 -0
- package/src/components/CAlertDefine.ts +20 -0
- package/src/components/CBSToast.vue +119 -0
- package/src/components/CGlobalSpinner.vue +84 -0
- package/src/components/CImage.vue +67 -0
- package/src/components/CRowCheckBox.vue +75 -0
- package/src/components/CRowTextInput.vue +27 -0
- package/src/components/CTable.vue +524 -0
- package/src/components/CTableDefine.ts +566 -0
- package/src/components/CTableTD.vue +28 -0
- package/src/components/HasPermission.vue +28 -0
- package/src/components/form/CChangePasswordFormField.vue +146 -0
- package/src/components/form/CCheckBoxFormField.vue +91 -0
- package/src/components/form/CCheckBoxPlatFormField.vue +94 -0
- package/src/components/form/CDateFormField.vue +149 -0
- package/src/components/form/CDateQueryField.vue +111 -0
- package/src/components/form/CDateRangeFormField.vue +138 -0
- package/src/components/form/CFilePickerFormField.vue +471 -0
- package/src/components/form/CRadioFormField.vue +62 -0
- package/src/components/form/CRadioPlatFormField.vue +67 -0
- package/src/components/form/CSelectFormField.vue +175 -0
- package/src/components/form/CTextAreaFormField.vue +84 -0
- package/src/components/form/CTextInputFormField.vue +99 -0
- package/src/components/form/CTinyMCEEditorFormField.vue +99 -0
- package/src/components/form/SCTextInputFormField.vue +129 -0
- package/src/composables/useCheckBoxFormField.ts +126 -0
- package/src/composables/useRadioFormField.ts +106 -0
- package/src/directive/CBootstrapDirective.ts +83 -0
- package/src/directive/CDateFormatterDirective.ts +37 -0
- package/src/directive/CFTurnstileDirective.ts +46 -0
- package/src/directive/CFormDirective.ts +57 -0
- package/src/directive/PermissionDirective.ts +102 -0
- package/src/env.d.ts +19 -0
- package/src/index.ts +83 -0
- package/src/model/BSFieldStyleConfig.ts +349 -0
- package/src/model/BaseDictionary.ts +86 -0
- package/src/model/BaseFormDataModel.ts +623 -0
- package/src/model/BaseListViewModel.ts +392 -0
- package/src/model/CBSModalViewModel.ts +91 -0
- package/src/model/CFileDataModel.ts +181 -0
- package/src/model/CImageViewModel.ts +34 -0
- package/src/model/CMenuItem.ts +199 -0
- package/src/model/EmailReceiverDataModel.ts +149 -0
- package/src/model/EmptyDataModel.ts +25 -0
- package/src/model/FormOptions.ts +112 -0
- package/src/model/LoginDataModel.ts +51 -0
- package/src/model/PasswordDataModel.ts +70 -0
- package/src/model/QueryParameter.ts +310 -0
- package/src/model/SessionUser.ts +110 -0
- package/src/model/ShowMessageDataModel.ts +69 -0
- package/src/model/TokenUser.ts +157 -0
- package/src/stores/FormDataStore.ts +73 -0
- package/src/stores/ViewStore.ts +701 -0
- package/src/stores/VueSessionStoreInstaller.ts +22 -0
- package/src/types/turnstile.d.ts +8 -0
- package/src/utils/CToolUtils.ts +133 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
import {ComponentPublicInstance, toRaw} from 'vue'
|
|
2
|
+
import {defineStore, DefineStoreOptions} from 'pinia'
|
|
3
|
+
import CAlert from "../components/CAlert.vue";
|
|
4
|
+
import {CAlertModalData, CAlertModalType} from "../components/CAlertDefine";
|
|
5
|
+
import CBSToast from "../components/CBSToast.vue";
|
|
6
|
+
import CGlobalSpinner from "../components/CGlobalSpinner.vue";
|
|
7
|
+
import {SessionUser} from "../model/SessionUser";
|
|
8
|
+
import {ApiService} from "../api/ApiService";
|
|
9
|
+
import _ from "lodash";
|
|
10
|
+
import {AuthorizationService} from "../auth/AuthorizationService";
|
|
11
|
+
import {CMenuItem} from "../model/CMenuItem";
|
|
12
|
+
import {ShowMessageType} from "../model/ShowMessageDataModel";
|
|
13
|
+
import {v4 as uuidv4} from 'uuid';
|
|
14
|
+
import {AccessToken} from "../model/TokenUser";
|
|
15
|
+
import {BaseDictionary, CBaseDictionary} from "../model/BaseDictionary";
|
|
16
|
+
|
|
17
|
+
// ~ ----------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export type RouterNavigationType = 'push' | 'back' | 'forward' | 'unknown';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 全局視圖相關的狀態
|
|
23
|
+
*/
|
|
24
|
+
export type GlobalViewState = {
|
|
25
|
+
navigationType: RouterNavigationType;
|
|
26
|
+
isSidebarOpen: boolean;
|
|
27
|
+
mainBSModal: ComponentPublicInstance<typeof CAlert> | null;
|
|
28
|
+
toastView: ComponentPublicInstance<typeof CBSToast> | null;
|
|
29
|
+
globalSpinner: ComponentPublicInstance<typeof CGlobalSpinner> | null;
|
|
30
|
+
version: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 全局視圖相關的行為
|
|
35
|
+
*/
|
|
36
|
+
export interface GlobalViewActions {
|
|
37
|
+
routerNavigationType(type: RouterNavigationType): void;
|
|
38
|
+
|
|
39
|
+
toggleSidebar(): void;
|
|
40
|
+
|
|
41
|
+
showModal(data: CAlertModalData): void;
|
|
42
|
+
|
|
43
|
+
showModalError(data: CAlertModalData): void;
|
|
44
|
+
|
|
45
|
+
showModelAlert(data: CAlertModalData): void;
|
|
46
|
+
|
|
47
|
+
showModalConfirm(data: CAlertModalData): void;
|
|
48
|
+
|
|
49
|
+
hideModal(): void;
|
|
50
|
+
|
|
51
|
+
addToast(data: CAlertModalData): void;
|
|
52
|
+
|
|
53
|
+
showSpinner(): void;
|
|
54
|
+
|
|
55
|
+
hideSpinner(): void;
|
|
56
|
+
|
|
57
|
+
setVersion(version: string): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 全局視圖相關的 getters
|
|
62
|
+
*/
|
|
63
|
+
export interface GlobalViewGetters {
|
|
64
|
+
// no getters
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 全局視圖 store 定義類型
|
|
69
|
+
*/
|
|
70
|
+
export type GlobalViewStoreOptionDefine = Omit<DefineStoreOptions<'view', GlobalViewState, GlobalViewGetters, GlobalViewActions>, 'id'>;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 全局視圖 store 定義
|
|
74
|
+
*/
|
|
75
|
+
export const globalViewStoreOptions: GlobalViewStoreOptionDefine = {
|
|
76
|
+
state: (): GlobalViewState => ({
|
|
77
|
+
navigationType: 'unknown' as RouterNavigationType,
|
|
78
|
+
isSidebarOpen: true,
|
|
79
|
+
mainBSModal: null as ComponentPublicInstance<typeof CAlert> | null,
|
|
80
|
+
toastView: null as ComponentPublicInstance<typeof CBSToast> | null,
|
|
81
|
+
globalSpinner: null as ComponentPublicInstance<typeof CGlobalSpinner> | null,
|
|
82
|
+
version: ''
|
|
83
|
+
}),
|
|
84
|
+
actions: {
|
|
85
|
+
routerNavigationType(type: RouterNavigationType) {
|
|
86
|
+
this.navigationType = type;
|
|
87
|
+
},
|
|
88
|
+
toggleSidebar() {
|
|
89
|
+
this.isSidebarOpen = !this.isSidebarOpen
|
|
90
|
+
},
|
|
91
|
+
showModal(data: CAlertModalData) {
|
|
92
|
+
this.mainBSModal?.show({
|
|
93
|
+
...data,
|
|
94
|
+
type: CAlertModalType.Info
|
|
95
|
+
})
|
|
96
|
+
},
|
|
97
|
+
showModalError(data: CAlertModalData) {
|
|
98
|
+
this.mainBSModal?.show({
|
|
99
|
+
...data,
|
|
100
|
+
type: CAlertModalType.Error
|
|
101
|
+
})
|
|
102
|
+
},
|
|
103
|
+
showModelAlert(data: CAlertModalData) {
|
|
104
|
+
this.mainBSModal?.show({
|
|
105
|
+
...data,
|
|
106
|
+
type: CAlertModalType.Alert
|
|
107
|
+
})
|
|
108
|
+
},
|
|
109
|
+
showModalConfirm(data: CAlertModalData) {
|
|
110
|
+
this.mainBSModal?.show({
|
|
111
|
+
...data,
|
|
112
|
+
type: CAlertModalType.Confirm
|
|
113
|
+
})
|
|
114
|
+
},
|
|
115
|
+
hideModal() {
|
|
116
|
+
this.mainBSModal?.hide()
|
|
117
|
+
},
|
|
118
|
+
addToast(data: CAlertModalData) {
|
|
119
|
+
this.toastView?.addToast(data);
|
|
120
|
+
},
|
|
121
|
+
showSpinner() {
|
|
122
|
+
this.globalSpinner?.show();
|
|
123
|
+
},
|
|
124
|
+
hideSpinner() {
|
|
125
|
+
this.globalSpinner?.hide();
|
|
126
|
+
},
|
|
127
|
+
setVersion(version: string) {
|
|
128
|
+
this.version = version;
|
|
129
|
+
// 把 version 資訊,儲存到 localStorage
|
|
130
|
+
localStorage.setItem('app_version', version);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
getters: {}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 保存視圖相關的狀態,例如側邊欄開啟狀態、模態框、Toast、全局加載等
|
|
138
|
+
*/
|
|
139
|
+
export const useViewStore = defineStore('view', globalViewStoreOptions);
|
|
140
|
+
|
|
141
|
+
// 防止 Vite/rollup tree-shake 掉 type export
|
|
142
|
+
export const __GlobalViewStoreDefine__ = null as unknown as
|
|
143
|
+
GlobalViewState &
|
|
144
|
+
GlobalViewActions &
|
|
145
|
+
GlobalViewGetters &
|
|
146
|
+
GlobalViewStoreOptionDefine;
|
|
147
|
+
|
|
148
|
+
// ~ ----------------------------------------------------------
|
|
149
|
+
// 使用者會話相關的 store 定義
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 使用者會話相關的狀態
|
|
153
|
+
*/
|
|
154
|
+
export type UserSessionState<T extends SessionUser = SessionUser> = {
|
|
155
|
+
user: T | null;
|
|
156
|
+
token?: AccessToken | null;
|
|
157
|
+
isAuth: boolean;
|
|
158
|
+
sessionCheckTimer: number | null;
|
|
159
|
+
shouldRedirectToLogin: boolean | null;
|
|
160
|
+
shouldRedirectToMessage?: boolean;
|
|
161
|
+
redirectMessageType?: ShowMessageType | null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 使用者會話相關的行為
|
|
166
|
+
*/
|
|
167
|
+
export interface UserSessionActions<T extends SessionUser = SessionUser> {
|
|
168
|
+
// 保存使用者資訊
|
|
169
|
+
saveUser(user: T | null): void;
|
|
170
|
+
|
|
171
|
+
// 保存 token 資訊
|
|
172
|
+
saveToken(token: AccessToken | null): void;
|
|
173
|
+
|
|
174
|
+
// 設定是否已驗證通過
|
|
175
|
+
setAuthenticated(isAuth: boolean): void;
|
|
176
|
+
|
|
177
|
+
// 檢查目前的 session 是否有效
|
|
178
|
+
checkSessionIsValid(): Promise<boolean>;
|
|
179
|
+
|
|
180
|
+
// 使用自訂的 helper 函式驗證 session
|
|
181
|
+
validateSession(helper: () => Promise<T>): Promise<boolean>;
|
|
182
|
+
|
|
183
|
+
// 使用自訂的 helper 函式驗證 token session
|
|
184
|
+
refreshToken(helper: () => Promise<AccessToken>): Promise<boolean>;
|
|
185
|
+
|
|
186
|
+
// 開始定時檢查 session
|
|
187
|
+
startSessionCheck(): void;
|
|
188
|
+
|
|
189
|
+
// 停止定時檢查 session
|
|
190
|
+
stopSessionCheck(): void;
|
|
191
|
+
|
|
192
|
+
// 檢查是否有指定的權限
|
|
193
|
+
hasPermission(needPermission: string | string[]): boolean;
|
|
194
|
+
|
|
195
|
+
// 登出
|
|
196
|
+
logout(): void;
|
|
197
|
+
|
|
198
|
+
// 手動點擊登出
|
|
199
|
+
triggerManualLogout: () => void;
|
|
200
|
+
|
|
201
|
+
// 重置 ShouldRedirectToLogin 狀態
|
|
202
|
+
resetShouldRedirectToLogin: () => void;
|
|
203
|
+
|
|
204
|
+
// token 驗證失敗時觸發重定向,屬於自動行為
|
|
205
|
+
triggerAuthFailedRedirect: () => void;
|
|
206
|
+
// 重置重定向狀態
|
|
207
|
+
resetAuthFailedRedirect: () => void;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 使用者會話相關的 getters
|
|
212
|
+
*/
|
|
213
|
+
export interface UserSessionGetters<T extends SessionUser = SessionUser> {
|
|
214
|
+
// 取得目前使用者物件
|
|
215
|
+
currentUser(state: UserSessionState<T>): T | null;
|
|
216
|
+
|
|
217
|
+
// 取得目前存取權杖
|
|
218
|
+
currentToken(state: UserSessionState<T>): AccessToken | null;
|
|
219
|
+
|
|
220
|
+
// 是否已驗證通過
|
|
221
|
+
isAuthenticated(state: UserSessionState<T>): boolean;
|
|
222
|
+
|
|
223
|
+
// 取得使用者 UID
|
|
224
|
+
userUid(state: UserSessionState<T>): string | undefined;
|
|
225
|
+
|
|
226
|
+
// 取得使用者帳號
|
|
227
|
+
account(state: UserSessionState<T>): string | undefined;
|
|
228
|
+
|
|
229
|
+
// 取得使用者名稱
|
|
230
|
+
userName(state: UserSessionState<T>): string | undefined;
|
|
231
|
+
|
|
232
|
+
// 取得使用者電子郵件
|
|
233
|
+
email(state: UserSessionState<T>): string | undefined;
|
|
234
|
+
|
|
235
|
+
// 取得使用者角色代碼
|
|
236
|
+
roleCode(state: UserSessionState<T>): string | undefined;
|
|
237
|
+
|
|
238
|
+
// 取得使用者權限列表
|
|
239
|
+
permissions(state: UserSessionState<T>): string[] | undefined;
|
|
240
|
+
|
|
241
|
+
// 取得使用者選單項目
|
|
242
|
+
menuItems(): CMenuItem[];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 使用者會話 store 定義類型
|
|
247
|
+
*/
|
|
248
|
+
export type UserSessionStoreOptionDefine<T extends SessionUser = SessionUser> = Omit<DefineStoreOptions<'session', UserSessionState<T>, UserSessionGetters<T>, UserSessionActions<T>>, 'id'>;
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* 使用者會話 store 定義
|
|
252
|
+
* 此方式僅適用於 SessionUser 類型,只使用 session 當作帳號權限驗證
|
|
253
|
+
*/
|
|
254
|
+
export const userSessionStoreOptions: UserSessionStoreOptionDefine<SessionUser> = {
|
|
255
|
+
state: (): UserSessionState<SessionUser> => ({
|
|
256
|
+
user: null as SessionUser | null, // 使用者資訊
|
|
257
|
+
isAuth: false, // 是否已登入
|
|
258
|
+
sessionCheckTimer: null as number | null, // 用於定時檢查會話的計時器
|
|
259
|
+
shouldRedirectToLogin: false // 是否應該重定向到登入頁面
|
|
260
|
+
}),
|
|
261
|
+
actions: {
|
|
262
|
+
saveUser(user: SessionUser | null) {
|
|
263
|
+
this.user = user;
|
|
264
|
+
this.isAuth = !!user; // 如果 user 不為 null,則 isAuth 為 true
|
|
265
|
+
},
|
|
266
|
+
saveToken(token: AccessToken | null) {
|
|
267
|
+
},
|
|
268
|
+
setAuthenticated(isAuth: boolean) {
|
|
269
|
+
this.isAuth = isAuth;
|
|
270
|
+
},
|
|
271
|
+
// 檢查目前的 session 是否有效
|
|
272
|
+
async checkSessionIsValid(): Promise<boolean> {
|
|
273
|
+
// 呼叫後端 API 以確認 session 是否仍然有效
|
|
274
|
+
const resp = await ApiService.call({endpointKey: 'me'});
|
|
275
|
+
if (!resp.isOk()) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
if (_.isNil(this.user)) {
|
|
279
|
+
this.saveUser(new SessionUser().load(resp.data));
|
|
280
|
+
} else {
|
|
281
|
+
this.user?.merge(resp.data);
|
|
282
|
+
}
|
|
283
|
+
this.isAuth = true;
|
|
284
|
+
return true;
|
|
285
|
+
},
|
|
286
|
+
async validateSession(helper: () => Promise<SessionUser>): Promise<boolean> {
|
|
287
|
+
return helper().then((user) => {
|
|
288
|
+
return !!user;
|
|
289
|
+
});
|
|
290
|
+
},
|
|
291
|
+
async refreshToken(helper: () => Promise<AccessToken>) {
|
|
292
|
+
return Promise.reject('Not implemented');
|
|
293
|
+
},
|
|
294
|
+
// 開始定時檢查 session
|
|
295
|
+
startSessionCheck() {
|
|
296
|
+
if (this.sessionCheckTimer) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
this.sessionCheckTimer = window.setInterval(async () => {
|
|
300
|
+
const isValid = this.checkSessionIsValid();
|
|
301
|
+
if (!isValid) {
|
|
302
|
+
this.logout();
|
|
303
|
+
}
|
|
304
|
+
}, 10 * 1000) // 每 5 分鐘檢查一次
|
|
305
|
+
},
|
|
306
|
+
stopSessionCheck() {
|
|
307
|
+
if (this.sessionCheckTimer) {
|
|
308
|
+
clearInterval(this.sessionCheckTimer)
|
|
309
|
+
this.sessionCheckTimer = null
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
hasPermission(needPermission: string | string[]) {
|
|
313
|
+
return true;
|
|
314
|
+
},
|
|
315
|
+
logout() {
|
|
316
|
+
this.user = null;
|
|
317
|
+
this.isAuth = false;
|
|
318
|
+
this.stopSessionCheck();
|
|
319
|
+
},
|
|
320
|
+
triggerManualLogout () {
|
|
321
|
+
this.logout();
|
|
322
|
+
this.shouldRedirectToLogin = true;
|
|
323
|
+
},
|
|
324
|
+
resetShouldRedirectToLogin() {
|
|
325
|
+
this.shouldRedirectToLogin = false;
|
|
326
|
+
},
|
|
327
|
+
triggerAuthFailedRedirect() {
|
|
328
|
+
},
|
|
329
|
+
resetAuthFailedRedirect() {
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
getters: {
|
|
333
|
+
currentUser: (state): SessionUser | null => {
|
|
334
|
+
return state.user;
|
|
335
|
+
},
|
|
336
|
+
currentToken: (state): AccessToken | null => {
|
|
337
|
+
return state.token || null;
|
|
338
|
+
},
|
|
339
|
+
isAuthenticated: (state) => {
|
|
340
|
+
return state.isAuth;
|
|
341
|
+
},
|
|
342
|
+
userUid: (state) => {
|
|
343
|
+
return _.get(state.user, 'sub', '');
|
|
344
|
+
},
|
|
345
|
+
account: (state) => {
|
|
346
|
+
return _.get(state.user, 'account', '');
|
|
347
|
+
},
|
|
348
|
+
userName: (state) => {
|
|
349
|
+
return _.get(state.user, 'name', '');
|
|
350
|
+
},
|
|
351
|
+
email: (state) => {
|
|
352
|
+
return _.get(state.user, 'email', '');
|
|
353
|
+
},
|
|
354
|
+
roleCode: (state) => {
|
|
355
|
+
return _.get(state.user, 'roleCode', '');
|
|
356
|
+
},
|
|
357
|
+
permissions: (state) => {
|
|
358
|
+
return _.get(state.user, 'permissions', []);
|
|
359
|
+
},
|
|
360
|
+
menuItems(): CMenuItem[] {
|
|
361
|
+
return AuthorizationService.provideMenuByPermission({
|
|
362
|
+
roleCode: this.roleCode || '',
|
|
363
|
+
permissions: toRaw(this.permissions) || []
|
|
364
|
+
});
|
|
365
|
+
},
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* 持久化設定選項
|
|
371
|
+
*/
|
|
372
|
+
export interface PersistOptions<T extends SessionUser = SessionUser> {
|
|
373
|
+
enabled?: boolean;
|
|
374
|
+
key?: string;
|
|
375
|
+
storage?: Storage;
|
|
376
|
+
paths?: string[];
|
|
377
|
+
userConstructor?: new () => T;
|
|
378
|
+
serializer?: {
|
|
379
|
+
serialize: (state: any) => string;
|
|
380
|
+
deserialize: (str: string) => any;
|
|
381
|
+
};
|
|
382
|
+
// 事件回調
|
|
383
|
+
onLogout?: (user: T | null, token: AccessToken | null) => void;
|
|
384
|
+
onLogin?: (user: T) => void;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* 創建使用者會話 store 選項的工廠函數,支援自訂的 SessionUser 類型
|
|
389
|
+
* @param persistOptions
|
|
390
|
+
*/
|
|
391
|
+
export function createUserSessionOptions<T extends SessionUser = SessionUser>(
|
|
392
|
+
persistOptions?: PersistOptions<T>
|
|
393
|
+
): UserSessionStoreOptionDefine<T> {
|
|
394
|
+
// 定義 user class 建構子
|
|
395
|
+
const UserClassConstructor = persistOptions?.userConstructor || (SessionUser as new () => T);
|
|
396
|
+
|
|
397
|
+
// 使用者會話 store 定義
|
|
398
|
+
const options: UserSessionStoreOptionDefine<T> = {
|
|
399
|
+
state: (): UserSessionState<T> => ({
|
|
400
|
+
user: null as T | null,
|
|
401
|
+
token: null as AccessToken | null,
|
|
402
|
+
isAuth: false,
|
|
403
|
+
sessionCheckTimer: null as number | null,
|
|
404
|
+
shouldRedirectToLogin: false,
|
|
405
|
+
shouldRedirectToMessage: false,
|
|
406
|
+
redirectMessageType: null,
|
|
407
|
+
}),
|
|
408
|
+
actions: {
|
|
409
|
+
saveUser(user: T | null) {
|
|
410
|
+
this.user = user as any;
|
|
411
|
+
|
|
412
|
+
// 當 user 不為 null 時,觸發 onLogin 回調
|
|
413
|
+
if (user && persistOptions?.onLogin) {
|
|
414
|
+
persistOptions.onLogin(toRaw(user));
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
saveToken(token: AccessToken | null) {
|
|
418
|
+
this.token = token || null;
|
|
419
|
+
},
|
|
420
|
+
setAuthenticated(isAuth: boolean) {
|
|
421
|
+
this.isAuth = isAuth;
|
|
422
|
+
},
|
|
423
|
+
async checkSessionIsValid(): Promise<boolean> {
|
|
424
|
+
const resp = await ApiService.call({endpointKey: 'me'});
|
|
425
|
+
if (!resp.isOk()) {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
if (_.isNil(this.user)) {
|
|
429
|
+
this.saveUser(new SessionUser().load(resp.data) as T);
|
|
430
|
+
} else {
|
|
431
|
+
this.user?.merge(resp.data);
|
|
432
|
+
}
|
|
433
|
+
this.isAuth = true;
|
|
434
|
+
return true;
|
|
435
|
+
},
|
|
436
|
+
async validateSession(helper: () => Promise<T>): Promise<boolean> {
|
|
437
|
+
if (_.isNil(this.user)) {
|
|
438
|
+
this.isAuth = false;
|
|
439
|
+
return Promise.resolve(false);
|
|
440
|
+
}
|
|
441
|
+
return helper()
|
|
442
|
+
.then((userData) => {
|
|
443
|
+
const r = !!userData;``
|
|
444
|
+
this.isAuth = r;
|
|
445
|
+
this.user!.loadFromValidateSession?.(userData as Record<string, any>);
|
|
446
|
+
return r;
|
|
447
|
+
});
|
|
448
|
+
},
|
|
449
|
+
async refreshToken(helper: () => Promise<AccessToken>) {
|
|
450
|
+
return helper().then((tokenData) => {
|
|
451
|
+
const isValid = !!tokenData && !tokenData.isExpired();
|
|
452
|
+
this.isAuth = isValid;
|
|
453
|
+
if (isValid) {
|
|
454
|
+
this.saveToken(tokenData);
|
|
455
|
+
}
|
|
456
|
+
return isValid;
|
|
457
|
+
});
|
|
458
|
+
},
|
|
459
|
+
startSessionCheck() {
|
|
460
|
+
},
|
|
461
|
+
stopSessionCheck() {
|
|
462
|
+
},
|
|
463
|
+
hasPermission(needPermission: string | string[]) {
|
|
464
|
+
// 取得目前的 permissions
|
|
465
|
+
const permissions = toRaw(this.permissions) || [];
|
|
466
|
+
if(_.isEmpty(permissions)) {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
const neededPermissions = new Set(_.castArray(permissions));
|
|
470
|
+
if (_.isEmpty(neededPermissions)) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
// 檢查是否有任一需要的權限
|
|
474
|
+
return _.some(permissions, perm => neededPermissions.has(perm));
|
|
475
|
+
},
|
|
476
|
+
logout() {
|
|
477
|
+
// 觸發登出前的事件,讓外部可以進行額外處理
|
|
478
|
+
const beforeLogoutEvent = new CustomEvent('user:before-logout', {
|
|
479
|
+
detail: { user: this.user, token: this.token }
|
|
480
|
+
});
|
|
481
|
+
window.dispatchEvent(beforeLogoutEvent);
|
|
482
|
+
|
|
483
|
+
// 執行自定義的 onLogout 回調
|
|
484
|
+
if (persistOptions?.onLogout) {
|
|
485
|
+
persistOptions.onLogout(toRaw(this.user) as T, toRaw(this.token) || null);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 執行登出邏輯
|
|
489
|
+
this.user = null;
|
|
490
|
+
this.token = null;
|
|
491
|
+
this.isAuth = false;
|
|
492
|
+
|
|
493
|
+
// 觸發登出後的事件
|
|
494
|
+
const afterLogoutEvent = new CustomEvent('user:after-logout', {
|
|
495
|
+
detail: { timestamp: Date.now() }
|
|
496
|
+
});
|
|
497
|
+
window.dispatchEvent(afterLogoutEvent);
|
|
498
|
+
},
|
|
499
|
+
triggerManualLogout() {
|
|
500
|
+
this.logout();
|
|
501
|
+
this.shouldRedirectToLogin = true;
|
|
502
|
+
},
|
|
503
|
+
resetShouldRedirectToLogin() {
|
|
504
|
+
this.shouldRedirectToLogin = false;
|
|
505
|
+
},
|
|
506
|
+
triggerAuthFailedRedirect() {
|
|
507
|
+
this.logout();
|
|
508
|
+
this.shouldRedirectToMessage = true;
|
|
509
|
+
this.redirectMessageType = ShowMessageType.AUTH_EXPIRED;
|
|
510
|
+
},
|
|
511
|
+
resetAuthFailedRedirect() {
|
|
512
|
+
this.shouldRedirectToMessage = false;
|
|
513
|
+
this.redirectMessageType = null;
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
getters: {
|
|
517
|
+
currentUser: (state: UserSessionState<T>): T | null => {
|
|
518
|
+
return state.user as T | null;
|
|
519
|
+
},
|
|
520
|
+
currentToken: (state: UserSessionState<T>): AccessToken | null => {
|
|
521
|
+
return state.token || null;
|
|
522
|
+
},
|
|
523
|
+
isAuthenticated: (state: UserSessionState<T>) => {
|
|
524
|
+
return state.isAuth;
|
|
525
|
+
},
|
|
526
|
+
userUid: (state: UserSessionState<T>) => {
|
|
527
|
+
return _.get(state.user, 'userUid', '');
|
|
528
|
+
},
|
|
529
|
+
account: (state: UserSessionState<T>) => {
|
|
530
|
+
return _.get(state.user, 'account', '');
|
|
531
|
+
},
|
|
532
|
+
userName: (state: UserSessionState<T>) => {
|
|
533
|
+
return _.get(state.user, 'name', '');
|
|
534
|
+
},
|
|
535
|
+
email: (state: UserSessionState<T>) => {
|
|
536
|
+
return _.get(state.user, 'email', '');
|
|
537
|
+
},
|
|
538
|
+
roleCode: (state: UserSessionState<T>) => {
|
|
539
|
+
return _.get(state.user, 'roleCode', '');
|
|
540
|
+
},
|
|
541
|
+
permissions: (state: UserSessionState<T>) => {
|
|
542
|
+
return _.get(state.user, 'permissions', []);
|
|
543
|
+
},
|
|
544
|
+
menuItems(): CMenuItem[] {
|
|
545
|
+
return AuthorizationService.provideMenuByPermission({
|
|
546
|
+
roleCode: this.roleCode || '',
|
|
547
|
+
permissions: toRaw(this.permissions) || []
|
|
548
|
+
});
|
|
549
|
+
},
|
|
550
|
+
} as any,
|
|
551
|
+
// 添加持久化設定
|
|
552
|
+
...(persistOptions?.enabled && {
|
|
553
|
+
persist: {
|
|
554
|
+
key: persistOptions.key || `user-session-${uuidv4()}`,
|
|
555
|
+
storage: persistOptions.storage || localStorage,
|
|
556
|
+
paths: persistOptions.paths || ['user', 'token', 'isAuth'],
|
|
557
|
+
serializer: persistOptions.serializer || {
|
|
558
|
+
serialize: (state: UserSessionState<T>): string => {
|
|
559
|
+
return JSON.stringify({
|
|
560
|
+
user: state.user?.toJSON(),
|
|
561
|
+
token: state.token?.toJSON(),
|
|
562
|
+
isAuth: state.isAuth
|
|
563
|
+
});
|
|
564
|
+
},
|
|
565
|
+
deserialize: (str: string): Partial<UserSessionState<T>> => {
|
|
566
|
+
const data = JSON.parse(str);
|
|
567
|
+
return {
|
|
568
|
+
user: data.user ? new UserClassConstructor().loadJSON(data.user) as T : null,
|
|
569
|
+
token: data.token ? new AccessToken().loadJSON(data.token) : null,
|
|
570
|
+
isAuth: data.isAuth
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
})
|
|
576
|
+
};
|
|
577
|
+
return options;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* 創建使用者會話 store 的工廠函數,支援自訂的 SessionUser 類型
|
|
581
|
+
*/
|
|
582
|
+
export function createUserSessionStore<T extends SessionUser = SessionUser>(
|
|
583
|
+
storeId: string = 'session',
|
|
584
|
+
persistOptions?: PersistOptions<T>
|
|
585
|
+
) {
|
|
586
|
+
const options: UserSessionStoreOptionDefine<T> = createUserSessionOptions<T>(persistOptions);
|
|
587
|
+
return defineStore(storeId, options);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* 預設的使用者會話 store(使用 SessionUser)
|
|
592
|
+
*/
|
|
593
|
+
export const useUserSessionStore = createUserSessionStore<SessionUser>();
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
// 防止 Vite/rollup tree-shake 掉 type export
|
|
597
|
+
export const __UserSessionTypes__ = null as unknown as
|
|
598
|
+
UserSessionState &
|
|
599
|
+
UserSessionGetters &
|
|
600
|
+
UserSessionActions &
|
|
601
|
+
UserSessionStoreOptionDefine &
|
|
602
|
+
PersistOptions;
|
|
603
|
+
|
|
604
|
+
// ~ ----------------------------------------------------------
|
|
605
|
+
// ~ Dictionary Store 定義
|
|
606
|
+
|
|
607
|
+
export interface DictionaryPersistOptions<T extends BaseDictionary = CBaseDictionary> {
|
|
608
|
+
enabled?: boolean;
|
|
609
|
+
key?: string;
|
|
610
|
+
storage?: Storage;
|
|
611
|
+
paths?: string[];
|
|
612
|
+
dictConstructor?: new () => T;
|
|
613
|
+
serializer?: {
|
|
614
|
+
serialize: (state: any) => string;
|
|
615
|
+
deserialize: (str: string) => any;
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
export type DictionaryState<T extends BaseDictionary = CBaseDictionary> = {
|
|
620
|
+
dictionary: T | null;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export interface DictionaryActions<T extends BaseDictionary = CBaseDictionary> {
|
|
624
|
+
setDictionary(dict: T): void;
|
|
625
|
+
loadAll(): void;
|
|
626
|
+
loadDictionary(key: string): void;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export interface DictionaryGetters<T extends BaseDictionary = CBaseDictionary> {
|
|
630
|
+
dict(): T | null;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Dictionary store 定義類型
|
|
635
|
+
*/
|
|
636
|
+
export type DictionaryStoreOptionDefine<T extends BaseDictionary = CBaseDictionary> = Omit<DefineStoreOptions<'dictionary', DictionaryState<T>, DictionaryGetters<T>, DictionaryActions<T>>, 'id'>;
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* 創建 Dictionary store 選項的工廠函數
|
|
640
|
+
* @param dictConstructor
|
|
641
|
+
*/
|
|
642
|
+
export function createDictionaryStoreOptions<T extends BaseDictionary = CBaseDictionary>(
|
|
643
|
+
options : {
|
|
644
|
+
dictionary?: T,
|
|
645
|
+
persistOptions?: DictionaryPersistOptions<T>
|
|
646
|
+
}
|
|
647
|
+
): DictionaryStoreOptionDefine<T> {
|
|
648
|
+
|
|
649
|
+
const dictConstructor = options.persistOptions?.dictConstructor || (CBaseDictionary as new () => T);
|
|
650
|
+
|
|
651
|
+
return {
|
|
652
|
+
state: (): DictionaryState<T> => ({
|
|
653
|
+
dictionary: options.dictionary as T | null
|
|
654
|
+
}),
|
|
655
|
+
actions: {
|
|
656
|
+
setDictionary(dict: T | null) {
|
|
657
|
+
this.dictionary = dict as any;
|
|
658
|
+
},
|
|
659
|
+
loadAll() {
|
|
660
|
+
this.dictionary?.loadAll();
|
|
661
|
+
},
|
|
662
|
+
async loadDictionary(key: string) {
|
|
663
|
+
await this.dictionary?.loadDictionary(key);
|
|
664
|
+
}
|
|
665
|
+
},
|
|
666
|
+
getters: {
|
|
667
|
+
dict(): T | null {
|
|
668
|
+
return toRaw(this.dictionary) as T || null;
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
// 添加持久化設定
|
|
672
|
+
...(options.persistOptions?.enabled && {
|
|
673
|
+
persist: {
|
|
674
|
+
key: options.persistOptions.key || `dictionary-store`,
|
|
675
|
+
storage: options.persistOptions.storage || localStorage,
|
|
676
|
+
paths: options.persistOptions.paths || ['dictionary'],
|
|
677
|
+
serializer: options.persistOptions.serializer || {
|
|
678
|
+
serialize: (state: DictionaryState<T>): string => {
|
|
679
|
+
return JSON.stringify({
|
|
680
|
+
dictionary: state.dictionary
|
|
681
|
+
});
|
|
682
|
+
},
|
|
683
|
+
deserialize: (str: string): Partial<DictionaryState<T>> => {
|
|
684
|
+
const data = JSON.parse(str);
|
|
685
|
+
return {
|
|
686
|
+
dictionary: new dictConstructor().loadFromStore(data.dictionary) as T
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
})
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export const __DictionaryStoreTypes__ = null as unknown as
|
|
696
|
+
DictionaryState &
|
|
697
|
+
DictionaryActions &
|
|
698
|
+
DictionaryGetters &
|
|
699
|
+
DictionaryStoreOptionDefine;
|
|
700
|
+
|
|
701
|
+
// ~ ----------------------------------------------------------
|