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.
- 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
- package/dist/api/ApiService.d.ts +0 -233
- package/dist/auth/AuthorizationService.d.ts +0 -56
- package/dist/auth/PermissionDescriptor.d.ts +0 -37
- package/dist/components/CAlert.vue.d.ts +0 -17
- package/dist/components/CAlertDefine.d.ts +0 -14
- package/dist/components/CBSToast.vue.d.ts +0 -6
- package/dist/components/CGlobalSpinner.vue.d.ts +0 -13
- package/dist/components/CRowCheckBox.vue.d.ts +0 -14
- package/dist/components/CRowTextInput.vue.d.ts +0 -10
- package/dist/components/CTable.vue.d.ts +0 -24
- package/dist/components/CTableDefine.d.ts +0 -201
- package/dist/components/CTableTD.vue.d.ts +0 -7
- package/dist/components/form/CChangePasswordFormField.vue.d.ts +0 -14
- package/dist/components/form/CCheckBoxFormField.vue.d.ts +0 -30
- package/dist/components/form/CDateFormField.vue.d.ts +0 -17
- package/dist/components/form/CDateQueryField.vue.d.ts +0 -16
- package/dist/components/form/CDateRangeFormField.vue.d.ts +0 -17
- package/dist/components/form/CFilePickerFormField.vue.d.ts +0 -28
- package/dist/components/form/CRadioFormField.vue.d.ts +0 -30
- package/dist/components/form/CSelectFormField.vue.d.ts +0 -18
- package/dist/components/form/CTextAreaFormField.vue.d.ts +0 -16
- package/dist/components/form/CTextInputFormField.vue.d.ts +0 -22
- package/dist/directive/CBootstrapDirective.d.ts +0 -17
- package/dist/directive/CDateFormatterDirective.d.ts +0 -10
- package/dist/directive/CFTurnstileDirective.d.ts +0 -15
- package/dist/directive/CFormDirective.d.ts +0 -9
- package/dist/directive/PermissionDirective.d.ts +0 -15
- package/dist/index.cjs.js +0 -19103
- package/dist/index.d.ts +0 -45
- package/dist/index.es.js +0 -19086
- package/dist/model/BSFieldStyleConfig.d.ts +0 -121
- package/dist/model/BaseDictionary.d.ts +0 -34
- package/dist/model/BaseFormDataModel.d.ts +0 -199
- package/dist/model/BaseListViewModel.d.ts +0 -165
- package/dist/model/CBSModalViewModel.d.ts +0 -44
- package/dist/model/CFileDataModel.d.ts +0 -74
- package/dist/model/CImageViewModel.d.ts +0 -8
- package/dist/model/CMenuItem.d.ts +0 -86
- package/dist/model/EmailReceiverDataModel.d.ts +0 -57
- package/dist/model/EmptyDataModel.d.ts +0 -7
- package/dist/model/FormOptions.d.ts +0 -60
- package/dist/model/LoginDataModel.d.ts +0 -12
- package/dist/model/PasswordDataModel.d.ts +0 -15
- package/dist/model/QueryParameter.d.ts +0 -92
- package/dist/model/SessionUser.d.ts +0 -45
- package/dist/model/ShowMessageDataModel.d.ts +0 -44
- package/dist/model/TokenUser.d.ts +0 -50
- package/dist/stores/FormDataStore.d.ts +0 -31
- package/dist/stores/ViewStore.d.ts +0 -349
- package/dist/style.css +0 -223
- 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
|
+
});
|