ch3chi-commons-vue 1.2.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.
Files changed (112) hide show
  1. package/package.json +2 -1
  2. package/src/api/ApiService.ts +869 -0
  3. package/src/auth/AuthorizationService.ts +138 -0
  4. package/src/auth/PermissionDescriptor.ts +99 -0
  5. package/src/auth/keys.ts +5 -0
  6. package/src/components/CAlert.vue +188 -0
  7. package/src/components/CAlertDefine.ts +20 -0
  8. package/src/components/CBSToast.vue +119 -0
  9. package/src/components/CGlobalSpinner.vue +84 -0
  10. package/src/components/CImage.vue +67 -0
  11. package/src/components/CRowCheckBox.vue +75 -0
  12. package/src/components/CRowTextInput.vue +27 -0
  13. package/src/components/CTable.vue +524 -0
  14. package/src/components/CTableDefine.ts +566 -0
  15. package/src/components/CTableTD.vue +28 -0
  16. package/src/components/HasPermission.vue +28 -0
  17. package/src/components/form/CChangePasswordFormField.vue +146 -0
  18. package/src/components/form/CCheckBoxFormField.vue +91 -0
  19. package/src/components/form/CCheckBoxPlatFormField.vue +94 -0
  20. package/src/components/form/CDateFormField.vue +149 -0
  21. package/src/components/form/CDateQueryField.vue +111 -0
  22. package/src/components/form/CDateRangeFormField.vue +138 -0
  23. package/src/components/form/CFilePickerFormField.vue +471 -0
  24. package/src/components/form/CRadioFormField.vue +62 -0
  25. package/src/components/form/CRadioPlatFormField.vue +67 -0
  26. package/src/components/form/CSelectFormField.vue +175 -0
  27. package/src/components/form/CTextAreaFormField.vue +84 -0
  28. package/src/components/form/CTextInputFormField.vue +99 -0
  29. package/src/components/form/CTinyMCEEditorFormField.vue +99 -0
  30. package/src/components/form/SCTextInputFormField.vue +129 -0
  31. package/src/composables/useCheckBoxFormField.ts +126 -0
  32. package/src/composables/useRadioFormField.ts +106 -0
  33. package/src/directive/CBootstrapDirective.ts +83 -0
  34. package/src/directive/CDateFormatterDirective.ts +37 -0
  35. package/src/directive/CFTurnstileDirective.ts +46 -0
  36. package/src/directive/CFormDirective.ts +57 -0
  37. package/src/directive/PermissionDirective.ts +102 -0
  38. package/src/env.d.ts +19 -0
  39. package/src/index.ts +83 -0
  40. package/src/model/BSFieldStyleConfig.ts +349 -0
  41. package/src/model/BaseDictionary.ts +86 -0
  42. package/src/model/BaseFormDataModel.ts +623 -0
  43. package/src/model/BaseListViewModel.ts +392 -0
  44. package/src/model/CBSModalViewModel.ts +91 -0
  45. package/src/model/CFileDataModel.ts +181 -0
  46. package/src/model/CImageViewModel.ts +34 -0
  47. package/src/model/CMenuItem.ts +199 -0
  48. package/src/model/EmailReceiverDataModel.ts +149 -0
  49. package/src/model/EmptyDataModel.ts +25 -0
  50. package/src/model/FormOptions.ts +112 -0
  51. package/src/model/LoginDataModel.ts +51 -0
  52. package/src/model/PasswordDataModel.ts +70 -0
  53. package/src/model/QueryParameter.ts +310 -0
  54. package/src/model/SessionUser.ts +110 -0
  55. package/src/model/ShowMessageDataModel.ts +69 -0
  56. package/src/model/TokenUser.ts +157 -0
  57. package/src/stores/FormDataStore.ts +73 -0
  58. package/src/stores/ViewStore.ts +701 -0
  59. package/src/stores/VueSessionStoreInstaller.ts +22 -0
  60. package/src/types/turnstile.d.ts +8 -0
  61. package/src/utils/CToolUtils.ts +133 -0
  62. package/dist/api/ApiService.d.ts +0 -233
  63. package/dist/auth/AuthorizationService.d.ts +0 -56
  64. package/dist/auth/PermissionDescriptor.d.ts +0 -37
  65. package/dist/components/CAlert.vue.d.ts +0 -17
  66. package/dist/components/CAlertDefine.d.ts +0 -14
  67. package/dist/components/CBSToast.vue.d.ts +0 -6
  68. package/dist/components/CGlobalSpinner.vue.d.ts +0 -13
  69. package/dist/components/CRowCheckBox.vue.d.ts +0 -14
  70. package/dist/components/CRowTextInput.vue.d.ts +0 -10
  71. package/dist/components/CTable.vue.d.ts +0 -24
  72. package/dist/components/CTableDefine.d.ts +0 -201
  73. package/dist/components/CTableTD.vue.d.ts +0 -7
  74. package/dist/components/form/CChangePasswordFormField.vue.d.ts +0 -14
  75. package/dist/components/form/CCheckBoxFormField.vue.d.ts +0 -30
  76. package/dist/components/form/CDateFormField.vue.d.ts +0 -17
  77. package/dist/components/form/CDateQueryField.vue.d.ts +0 -16
  78. package/dist/components/form/CDateRangeFormField.vue.d.ts +0 -17
  79. package/dist/components/form/CFilePickerFormField.vue.d.ts +0 -28
  80. package/dist/components/form/CRadioFormField.vue.d.ts +0 -30
  81. package/dist/components/form/CSelectFormField.vue.d.ts +0 -18
  82. package/dist/components/form/CTextAreaFormField.vue.d.ts +0 -16
  83. package/dist/components/form/CTextInputFormField.vue.d.ts +0 -22
  84. package/dist/directive/CBootstrapDirective.d.ts +0 -17
  85. package/dist/directive/CDateFormatterDirective.d.ts +0 -10
  86. package/dist/directive/CFTurnstileDirective.d.ts +0 -15
  87. package/dist/directive/CFormDirective.d.ts +0 -9
  88. package/dist/directive/PermissionDirective.d.ts +0 -15
  89. package/dist/index.cjs.js +0 -19103
  90. package/dist/index.d.ts +0 -45
  91. package/dist/index.es.js +0 -19086
  92. package/dist/model/BSFieldStyleConfig.d.ts +0 -121
  93. package/dist/model/BaseDictionary.d.ts +0 -34
  94. package/dist/model/BaseFormDataModel.d.ts +0 -199
  95. package/dist/model/BaseListViewModel.d.ts +0 -165
  96. package/dist/model/CBSModalViewModel.d.ts +0 -44
  97. package/dist/model/CFileDataModel.d.ts +0 -74
  98. package/dist/model/CImageViewModel.d.ts +0 -8
  99. package/dist/model/CMenuItem.d.ts +0 -86
  100. package/dist/model/EmailReceiverDataModel.d.ts +0 -57
  101. package/dist/model/EmptyDataModel.d.ts +0 -7
  102. package/dist/model/FormOptions.d.ts +0 -60
  103. package/dist/model/LoginDataModel.d.ts +0 -12
  104. package/dist/model/PasswordDataModel.d.ts +0 -15
  105. package/dist/model/QueryParameter.d.ts +0 -92
  106. package/dist/model/SessionUser.d.ts +0 -45
  107. package/dist/model/ShowMessageDataModel.d.ts +0 -44
  108. package/dist/model/TokenUser.d.ts +0 -50
  109. package/dist/stores/FormDataStore.d.ts +0 -31
  110. package/dist/stores/ViewStore.d.ts +0 -349
  111. package/dist/style.css +0 -223
  112. package/dist/utils/CToolUtils.d.ts +0 -53
@@ -0,0 +1,310 @@
1
+ import _ from 'lodash';
2
+ import {ApiResponse} from "../api/ApiService";
3
+
4
+ export type SortDirection = 'ASC' | 'asc' | 'DESC' | 'desc';
5
+
6
+ /**
7
+ * QuerySort 定義了查詢的排序條件
8
+ */
9
+ export interface QuerySort {
10
+ field: string; // 排序欄位
11
+ direction: SortDirection; // 排序方向,'asc' 或 'desc'
12
+ }
13
+
14
+ /**
15
+ * IQueryPage 定義了查詢的分頁資訊
16
+ */
17
+ export interface IQueryPage {
18
+ pageIndex: number; // 當前頁碼,從 0 開始
19
+ pageSize: number; // 每頁顯示的項目數量
20
+ totalPage: number; // 總頁數
21
+ totalCount: number; // 總項目數量
22
+ }
23
+
24
+ /**
25
+ * QueryPage 實現了 IQueryPage 接口,提供分頁資訊的管理
26
+ */
27
+ export class QueryPage implements IQueryPage {
28
+ pageIndex: number = 0; // 當前頁碼,從 0 開始
29
+ pageSize: number = 10; // 每頁顯示的項目數量
30
+ totalPage: number = 0; // 總頁數
31
+ private _totalCount: number = 0; // 總項目數量
32
+ pageRangeDisplayed: number = 2; // 當前頁面前後要顯示的 n 個頁碼
33
+
34
+ // ~ ----------------------------------------------------------
35
+
36
+ constructor(data: Partial<QueryPage> = {}) {
37
+ Object.assign(this, data);
38
+ }
39
+
40
+ get totalCount(): number {
41
+ return this._totalCount;
42
+ }
43
+
44
+ set totalCount(value: number) {
45
+ this._totalCount = Math.max(value, 0); // 確保總數量不小於 0
46
+ // 計算總頁數
47
+ this.calcTotalPage();
48
+ }
49
+
50
+ get offset(): number {
51
+ return this.pageIndex * this.pageSize;
52
+ }
53
+
54
+ // ~ ----------------------------------------------------------
55
+
56
+
57
+ /**
58
+ * 計算總頁數
59
+ */
60
+ calcTotalPage() {
61
+ if (this.pageSize > 0) {
62
+ this.totalPage = Math.ceil(this._totalCount / this.pageSize);
63
+ } else {
64
+ this.totalPage = 0;
65
+ }
66
+ }
67
+
68
+ // 檢查是否有上一頁
69
+ hasPreviousPage(): boolean {
70
+ return this.pageIndex > 0;
71
+ }
72
+
73
+ // 檢查是否有下一頁
74
+ hasNextPage(): boolean {
75
+ return this.pageIndex < (this.totalPage - 1);
76
+ }
77
+
78
+ // 確保不小於 0
79
+ previous(): void {
80
+ this.pageIndex = Math.max(this.pageIndex - 1, 0);
81
+ }
82
+
83
+ // 確保不超過總頁數
84
+ next(): void {
85
+ this.pageIndex = Math.min(this.pageIndex + 1, this.totalPage - 1);
86
+ }
87
+
88
+ // 檢查是否可以顯示第一頁
89
+ isShowFirstPage(): boolean {
90
+ return this.pageIndex > 0;
91
+ }
92
+
93
+ // 檢查是否可以顯示最後一頁
94
+ isShowLastPage(): boolean {
95
+ return this.pageIndex < (this.totalPage - 1);
96
+ }
97
+
98
+ usePageItemArray(): number[] {
99
+ const rs = [];
100
+ for(let i = 0; i < this.totalPage; i++) {
101
+ rs.push(i);
102
+ }
103
+ return rs;
104
+ }
105
+
106
+ /**
107
+ * 生成分頁的顯示範圍,項目為從 0 開始的頁碼陣列
108
+ *
109
+ * @returns {number[]} 返回一個包含顯示頁碼的陣列
110
+ */
111
+ pageRange(): number[] {
112
+ const range: number[] = [];
113
+ if(this.totalPage == 0) {
114
+ return [];
115
+ }
116
+ if(_.isNil(this.pageRangeDisplayed) || this.pageRangeDisplayed <= 0) {
117
+ this.pageRangeDisplayed = 2; // 預設使用 2 個頁碼
118
+ }
119
+ // 計算顯示範圍的限制
120
+ const limit = (this.pageRangeDisplayed * 2) + 3;
121
+ // 如果總頁數小於或等於顯示範圍,則顯示所有頁碼
122
+ if(this.totalPage <= limit) {
123
+ for (let i = 0; i < this.totalPage; i++) {
124
+ range.push(i);
125
+ }
126
+ return range;
127
+ }
128
+
129
+ // 根據當下的 pageIndex 計算顯示的頁碼範圍
130
+ const start = Math.max(0, this.pageIndex - this.pageRangeDisplayed);
131
+ const end = Math.min(this.totalPage - 1, this.pageIndex + this.pageRangeDisplayed);
132
+ const startOffset = Math.abs((this.pageIndex - this.pageRangeDisplayed) - start);
133
+ const endOffset = Math.abs((this.pageIndex + this.pageRangeDisplayed) - end);
134
+
135
+ // 首頁會顯示
136
+ range.push(0);
137
+
138
+ // 如果有 endOffset 的情況,表示開頭需要補上通等數量的 頁碼
139
+ if(endOffset > 0) {
140
+ // 添加 startOffset 個頁碼到 range 中
141
+ for (let i = 1; i <= endOffset; i++) {
142
+ range.push(i);
143
+ }
144
+ }
145
+
146
+ // 添加省略號:偵測 start 是否大於等於 pageRangeDisplayed
147
+ const isStartConsecutive = start >= this.pageRangeDisplayed;
148
+ if(isStartConsecutive) {
149
+ // 如果 start - 1 大於等於 pageRangeDisplayed,則添加一個省略號
150
+ range.push(-1); // 使用 -1 代表省略號
151
+ }
152
+
153
+ // 添加 start 到 pageIndex 之間的頁碼
154
+ for (let i = start; i < this.pageIndex; i++) {
155
+ if(i == 0) {
156
+ continue;
157
+ }
158
+ range.push(i);
159
+ }
160
+
161
+ // 添加 pageIndex 當前頁碼
162
+ if(this.pageIndex > 0) {
163
+ range.push(this.pageIndex);
164
+ }
165
+
166
+ // 添加 pageIndex 到 end 之間的頁碼
167
+ for (let i = this.pageIndex + 1; i <= end; i++) {
168
+ // 最後一頁跳過,不在這邊添加
169
+ if(i == this.totalPage - 1) {
170
+ continue;
171
+ }
172
+ range.push(i);
173
+ }
174
+
175
+ // 添加省略號:偵測 end 是否小於等於 (totalPage-1) - pageRangeDisplayed
176
+ const isEndConsecutive = (end + 1) < (this.totalPage - 1) - this.pageRangeDisplayed;
177
+ if(isEndConsecutive) {
178
+ // 如果 end + 1 小於等於 (totalPage-1) - pageRangeDisplayed,則添加一個省略號
179
+ range.push(-2); // 使用 -2 代表省略號
180
+ }
181
+
182
+ // 如果有 startOffset 的情況,表示結尾需要補上通等數量的 頁碼
183
+ if(startOffset > 0) {
184
+ // 添加 startOffset 個頁碼到 range 中
185
+ for(let i = startOffset; i > 0; i--) {
186
+ range.push(this.totalPage - 1 - i);
187
+ }
188
+ }
189
+
190
+ // 最後一頁會顯示
191
+ if(this.pageIndex < (this.totalPage - 1)) {
192
+ range.push(this.totalPage - 1);
193
+ }
194
+
195
+ return range;
196
+ }
197
+
198
+ // ~ ----------------------------------------------------------
199
+ }
200
+
201
+ export interface QueryParameter {
202
+ keyword?: string | null; // 關鍵字搜尋
203
+ page?: QueryPage; // 分頁資訊
204
+ sort?: QuerySort[]; // 排序條件,陣列形式
205
+ }
206
+
207
+ export class QueryParameter implements QueryParameter {
208
+ keyword?: string | null = null;
209
+ page?: QueryPage = new QueryPage();
210
+ sort?: QuerySort[] = [];
211
+
212
+ // ~ ----------------------------------------------------------
213
+
214
+ constructor(data: Partial<QueryParameter> = {}) {
215
+ Object.assign(this, {
216
+ keyword: null,
217
+ page: new QueryPage(),
218
+ sort: []
219
+ }, data);
220
+ if (data.page) {
221
+ this.page = new QueryPage(data.page);
222
+ }else {
223
+ this.page = new QueryPage(); // 確保 page 有一個默認值
224
+ }
225
+ }
226
+
227
+ /**
228
+ * 載入查詢參數資料
229
+ * @param data 部分或全部的查詢參數
230
+ */
231
+ load(data: Partial<QueryParameter> = {}) {
232
+ _.merge(this, data);
233
+ }
234
+
235
+ /**
236
+ * 載入 API 回應資料
237
+ * @param response
238
+ */
239
+ loadResponse(response: ApiResponse) {
240
+ this.page!.totalCount = response.paging?.total || 0; // 確保 totalCount 有值
241
+ }
242
+
243
+ /**
244
+ * 產生 API 使用的 payload
245
+ * @returns {Record<string, any>} 返回一個包含查詢參數的物件
246
+ */
247
+ toPayload(): Record<string, any> {
248
+ const payload: Record<string, any> = {};
249
+
250
+ // 針對物件基本屬性進行處理
251
+ const skipProperties = ['page', 'sort'];
252
+ for (const key in this) {
253
+ // 忽略 page 和 sort 等屬性,因為它們已經在之前處理
254
+ if( skipProperties.includes(key)) {
255
+ continue; // 跳過不需要的屬性
256
+ }
257
+ if (!Object.prototype.hasOwnProperty.call(this, key)) {
258
+ continue;
259
+ }
260
+ payload[key] = _.get(this, key);
261
+ }
262
+ return payload;
263
+ }
264
+
265
+ /**
266
+ * 產生 URL 查詢參數
267
+ */
268
+ toQueryStringParam() : Record<string, any> {
269
+ const params: Record<string, any> = {};
270
+ if (this.page) {
271
+ params.limit = this.page.pageSize;
272
+ params.offset = this.page.offset;
273
+ }
274
+ return params;
275
+ }
276
+
277
+ /**
278
+ * 清除查詢參數
279
+ */
280
+ clear() {
281
+ // 清除查詢參數
282
+ this.keyword = '';
283
+ this.sort = [];
284
+ }
285
+
286
+ // ~ ----------------------------------------------------------
287
+ }
288
+
289
+
290
+ // ~ ----------------------------------------------------------
291
+
292
+ /**
293
+ * 適用於 Date Range 的查詢參數
294
+ */
295
+ export class DateRangeParam extends QueryParameter {
296
+ startsAt?: Date | null = null;
297
+ endsAt?: Date | null = null;
298
+
299
+ clear() {
300
+ super.clear();
301
+ this.startsAt = null;
302
+ this.endsAt = null;
303
+ }
304
+ }
305
+
306
+
307
+ // 防止 Vite/rollup tree-shake 掉 type export
308
+ export const __QueryParameterDefine__ = null as unknown as
309
+ SortDirection;
310
+
@@ -0,0 +1,110 @@
1
+ import _ from 'lodash';
2
+
3
+ /**
4
+ * Role model
5
+ */
6
+ export class Role {
7
+ uid?: string;
8
+ name?: string;
9
+ type?: string;
10
+ }
11
+
12
+
13
+ /**
14
+ * 用來描述操作人員的物件
15
+ * 用來保存已登入的使用者資訊
16
+ */
17
+ export class SessionUser {
18
+
19
+ userUid?: string; // 使用者唯一識別碼
20
+ iat?: number; // 簽發的時間戳
21
+ account?: string; // 使用者帳號
22
+ name?: string; // 使用者名稱
23
+ email?: string; // 使用者電子郵件
24
+ roleCode?: string; // 使用者角色代碼
25
+ roleName?: string; // 使用者角色名稱
26
+ permissions?: string[]; // 使用者權限列表
27
+
28
+ constructor(data?: Partial<SessionUser>) {
29
+ if(data) {
30
+ Object.assign(this, data);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * 載入資料
36
+ * @param data
37
+ */
38
+ load(data: Record<string, any>): this {
39
+ data = this.process(data);
40
+ this.userUid = data.userUid || '';
41
+ this.iat = data.iat || 0;
42
+ this.account = data.account || '';
43
+ this.name = data.name || '';
44
+ this.email = data.email || '';
45
+ this.roleCode = data.roleCode || '';
46
+ this.roleName = data.roleName || '';
47
+ this.permissions = data.permissions || [];
48
+ return this;
49
+ }
50
+
51
+ /**
52
+ * 從驗證會話資料載入
53
+ * @param data
54
+ */
55
+ loadFromValidateSession(data: any): this {
56
+ data = this.process(data);
57
+ _.assignWith(this, data, (objValue, srcValue) => {
58
+ // 如果來源值為 null,保留原值不覆蓋
59
+ return srcValue === null ? objValue : srcValue;
60
+ });
61
+ return this;
62
+ }
63
+
64
+ /**
65
+ * 合併資料
66
+ * @param data
67
+ */
68
+ merge(data: Partial<SessionUser>): SessionUser {
69
+ data = this.process(data);
70
+ this.load(data);
71
+ return this;
72
+ }
73
+
74
+ /**
75
+ * 對載入的資料進行預處理
76
+ * @param data
77
+ */
78
+ process(data: Record<string, any>): Record<string, any> {
79
+ // 在這裡可以對資料進行預處理,例如轉換日期格式等
80
+ return data;
81
+ }
82
+
83
+ toJSON(): Record<string, any> {
84
+ return {
85
+ userUid: this.userUid,
86
+ iat: this.iat,
87
+ account: this.account,
88
+ name: this.name,
89
+ email: this.email,
90
+ roleCode: this.roleCode,
91
+ roleName: this.roleName,
92
+ permissions: this.permissions
93
+ }
94
+ }
95
+
96
+ loadJSON(data: Record<string, any>): this {
97
+ _.assign(this, _.pick(data, [
98
+ "userUid",
99
+ "iat",
100
+ "account",
101
+ "name",
102
+ "email",
103
+ "roleCode",
104
+ "roleName",
105
+ "permissions"
106
+ ]));
107
+ return this;
108
+ }
109
+
110
+ }
@@ -0,0 +1,69 @@
1
+ import {COptionItem, DataCategory} from "./FormOptions";
2
+
3
+ /**
4
+ * ShowMessage 訊息類型列舉
5
+ */
6
+ export enum ShowMessageType {
7
+ /** 忘記密碼郵件已發送 */
8
+ FORGOT_PASSWORD_SENT = 'FP_SENT',
9
+ /** 重置密碼成功 */
10
+ RESET_PASSWORD_SUCCESS = 'RP_OK',
11
+ /** 認證過期 */
12
+ AUTH_EXPIRED = 'AUTH_EXP'
13
+ }
14
+
15
+ /**
16
+ * ShowMessage 頁面訊息資料模型
17
+ * 用於定義系統各種訊息的標題、內容、狀態碼等資訊
18
+ */
19
+
20
+ export interface IShowMessageData {
21
+ title: string;
22
+ message: string;
23
+ code?: string;
24
+ showLoginButton?: boolean;
25
+ showHomeButton?: boolean;
26
+ }
27
+
28
+ /**
29
+ * ShowMessage 訊息定義
30
+ */
31
+ export class ShowMessageDataModel {
32
+
33
+ private static _messageMap: Record<ShowMessageType, IShowMessageData>;
34
+
35
+ static set messageMap(map: Record<ShowMessageType, IShowMessageData>) {
36
+ this._messageMap = map;
37
+ }
38
+
39
+ /**
40
+ * 根據訊息類型取得訊息資料
41
+ */
42
+ static getMessageData(type: ShowMessageType): IShowMessageData {
43
+ return ShowMessageDataModel._messageMap?.[type];
44
+ }
45
+
46
+ /**
47
+ * 根據 key 字串取得訊息資料(用於從 query string 解析)
48
+ */
49
+ static getMessageDataByKey(key: string): IShowMessageData | null {
50
+ const type = key as ShowMessageType;
51
+ if (type in ShowMessageDataModel._messageMap) {
52
+ return ShowMessageDataModel._messageMap[type];
53
+ }
54
+ return null;
55
+ }
56
+
57
+ /**
58
+ * 將訊息類型轉換為路由參數(path params)
59
+ */
60
+ static toRouteParams(type: ShowMessageType): { type: string } {
61
+ return {
62
+ type: type
63
+ };
64
+ }
65
+ }
66
+
67
+ export const __ShowMessageDataModelDefine__ = null as unknown as
68
+ ShowMessageType &
69
+ IShowMessageData;
@@ -0,0 +1,157 @@
1
+ import {SessionUser} from "./SessionUser";
2
+ import dayjs from "dayjs";
3
+ import _ from "lodash";
4
+ import {string} from "yup";
5
+
6
+ /**
7
+ * 存放存取權杖相關資訊
8
+ */
9
+ export class AccessToken {
10
+ tokenType?: string;
11
+ accessToken?: string
12
+ accessTokenExpiresAt?: string;
13
+ refreshToken?: string;
14
+ refreshTokenExpiresAt?: string;
15
+ sessionUid?: string;
16
+
17
+ // ~ ----------------------------------------------------------
18
+ // ~ constructor
19
+
20
+ constructor(data?: Partial<AccessToken>) {
21
+ if(data) {
22
+ Object.assign(this, data);
23
+ }
24
+ }
25
+
26
+ // ~ ----------------------------------------------------------
27
+
28
+ /**
29
+ * 存取權杖是否已過期
30
+ */
31
+ isExpired(): boolean {
32
+ if(!this.accessTokenExpiresAt) {
33
+ return true;
34
+ }
35
+ const dayObj = dayjs(this.accessTokenExpiresAt);
36
+ if(!dayObj.isValid()) {
37
+ console.warn('accessTokenExpiresAt is invalid date string:', this.accessTokenExpiresAt);
38
+ return true;
39
+ }
40
+ return dayjs().isAfter(dayObj);
41
+ }
42
+
43
+ /**
44
+ * 刷新權杖是否已過期
45
+ */
46
+ isRefreshTokenExpired(): boolean {
47
+ if(!this.refreshTokenExpiresAt) {
48
+ return true;
49
+ }
50
+ const dayObj = dayjs(this.refreshTokenExpiresAt);
51
+ if(!dayObj.isValid()) {
52
+ console.warn('refreshTokenExpiresAt is invalid date string:', this.refreshTokenExpiresAt);
53
+ return true;
54
+ }
55
+ return dayjs().isAfter(dayObj);
56
+ }
57
+
58
+ /**
59
+ * 載入資料
60
+ * @param data
61
+ */
62
+ loadToken(data: Record<string, any>): this {
63
+ _.assign(this, _.pick(data, [
64
+ "tokenType",
65
+ "accessToken",
66
+ "accessTokenExpiresAt",
67
+ "refreshToken",
68
+ "refreshTokenExpiresAt",
69
+ "sessionUid"
70
+ ]));
71
+ return this;
72
+ }
73
+
74
+ /**
75
+ * 轉換為 JSON 物件
76
+ */
77
+ toJSON(): Record<string, any> {
78
+ return {
79
+ tokenType: this.tokenType,
80
+ accessToken: this.accessToken,
81
+ accessTokenExpiresAt: this.accessTokenExpiresAt,
82
+ sessionUid: this.sessionUid
83
+ };
84
+ }
85
+
86
+ /**
87
+ * 從 JSON 資料載入
88
+ * @param data
89
+ */
90
+ loadJSON(data: Record<string, any>): this {
91
+ _.assign(this, _.pick(data, [
92
+ "tokenType",
93
+ "accessToken",
94
+ "accessTokenExpiresAt",
95
+ "sessionUid"
96
+ ]))
97
+ return this;
98
+ }
99
+
100
+ // ~ ----------------------------------------------------------
101
+
102
+ /**
103
+ * 取得 Bearer Token 字串
104
+ */
105
+ get bearerToken(): string {
106
+ return `${this.tokenType} ${this.accessToken}`;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * 帶有 Token 資訊的使用者物件
112
+ */
113
+ export class TokenUser extends SessionUser {
114
+ tokens?: AccessToken;
115
+ profile?: Record<string, any>;
116
+
117
+ constructor(data?: Partial<AccessToken>) {
118
+ super();
119
+ if(!data) {
120
+ return;
121
+ }
122
+ Object.assign(this, data);
123
+ }
124
+
125
+ load(data: Record<string, any>): this {
126
+ super.load(data);
127
+ if(data.tokens) {
128
+ this.tokens = new AccessToken(data.tokens);
129
+ }
130
+ if(data.profile) {
131
+ this.profile = data.profile;
132
+ }
133
+ return this;
134
+ }
135
+
136
+ toJSON(): Record<string, any> {
137
+ return {
138
+ ...super.toJSON(),
139
+ tokens: this.tokens ? this.tokens.toJSON() : undefined,
140
+ profile: this.profile
141
+ }
142
+ }
143
+
144
+ loadJSON(data: Record<string, any>): this {
145
+ super.loadJSON(data);
146
+ if(data.tokens) {
147
+ this.tokens = new AccessToken().loadJSON(data.tokens);
148
+ }
149
+ if(data.profile) {
150
+ this.profile = data.profile;
151
+ }
152
+ return this;
153
+ }
154
+
155
+ }
156
+
157
+
@@ -0,0 +1,73 @@
1
+ import { defineStore } from 'pinia';
2
+ import _ from 'lodash';
3
+
4
+ /**
5
+ * Backup Form Data Store
6
+ * This store is used to manage the system setting details in a backup form.
7
+ */
8
+ export const backupFormDataStore = defineStore('formData', {
9
+ state: () => ({
10
+ store: {} as Record<string, any>,
11
+ }),
12
+ actions: {
13
+ backupData(dataName: string, data: Record<string, any>) {
14
+ _.set(this.$state.store, dataName, data);
15
+ }
16
+ },
17
+ getters: {
18
+ getBackupData: (state) => (dataName: string): Record<string, any> | null => {
19
+ return _.get(state.store, dataName, null);
20
+ },
21
+ }
22
+ });
23
+
24
+ /**
25
+ * 用來保留查詢表單的資料到 local storage
26
+ */
27
+ export const useQueryFormDataStore = defineStore('queryFormData', {
28
+ state: () => ({
29
+ store: {} as Record<string, any>,
30
+ }),
31
+ actions: {
32
+ save(formName: string, queryParam: Record<string, any>) {
33
+ // 針對 queryParam 做深拷貝,避免外部物件被修改
34
+ const cloneParam = _.cloneDeep(queryParam);
35
+ // 針對 cloneParam.page ,只保留 pageIndex, pageSize 兩個屬性
36
+ if(cloneParam.page) {
37
+ cloneParam.page = {
38
+ pageIndex: cloneParam.page.pageIndex,
39
+ pageSize: cloneParam.page.pageSize
40
+ };
41
+ }
42
+
43
+ // 保存到 Pinia store
44
+ this.$state.store[formName] = cloneParam;
45
+ // 同步到 local storage
46
+ localStorage.setItem(`qfds_${formName}`, JSON.stringify(cloneParam));
47
+ },
48
+ loadAllFromLocalStorage() {
49
+ Object.keys(this.$state.store).forEach((key) => {
50
+ const item = localStorage.getItem(`qfds_${key}`);
51
+ if(item) {
52
+ try {
53
+ const parsed = JSON.parse(item);
54
+ this.$state.store[key] = parsed;
55
+ } catch(e) {
56
+ console.error(`Failed to parse localStorage item for key ${key}:`, e);
57
+ }
58
+ }
59
+ });
60
+ },
61
+ clearQueryParam(formName: string) {
62
+ if(this.$state.store.hasOwnProperty(formName)) {
63
+ this.$state.store[formName] = null;
64
+ localStorage.removeItem(`qfds_${formName}`);
65
+ }
66
+ },
67
+ },
68
+ getters: {
69
+ getQueryParam: (state) => (formName: string): Record<string, any> | null => {
70
+ return state.store[formName];
71
+ },
72
+ }
73
+ });