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,392 @@
1
+ import {QueryPage, QueryParameter} from "./QueryParameter";
2
+ import {BaseFormDataModel} from "./BaseFormDataModel";
3
+ import {
4
+ CTableColumn,
5
+ CTableColumnActionMeta,
6
+ CTableColumnActionType,
7
+ CTableColumnType
8
+ } from "../components/CTableDefine";
9
+ import {isReactive, reactive, toRaw} from "vue";
10
+ import {Router} from "vue-router";
11
+ import {useQueryFormDataStore} from "../stores/FormDataStore";
12
+ import type {GlobalViewActions} from '../stores/ViewStore'
13
+ import _ from "lodash";
14
+ import {ApiResponse, ApiService} from "../api/ApiService";
15
+
16
+ export interface ListApiEndpointMeta {
17
+ key: string; // API 端點鍵值
18
+ pathParam?: Record<string, any>;
19
+ dataListFinder?: (response: ApiResponse) => any[]; // 用於從 API 回應中提取資料列表的函式
20
+ dataParser: (dataItem: any) => any; // 用於解析 API 回
21
+ }
22
+
23
+ /**
24
+ * BaseListViewModel 是一個抽象類別,用於定義列表視圖模型的基本結構和行為。
25
+ */
26
+ export abstract class BaseListViewModel<Q extends QueryParameter, V extends BaseFormDataModel> {
27
+ queryParam: Q; // 查詢參數
28
+ dataList: V[]; // 顯示資料
29
+ tableColumnArray: CTableColumn[]; // 表格欄位定義
30
+ tableActionFilter?: (action: CTableColumnActionMeta, dataItem?: V) => boolean; // 權限過濾函式
31
+ router?: Router;
32
+ storeAction?: GlobalViewActions;
33
+ enableQueryParamDefault?: boolean = false; // 是否啟用查詢參數的默認值
34
+ queryParamDefaultValues?: Record<string, any>; // 用於存儲屬性的默認值
35
+ saveOnQueryFormDataStore?: boolean = false; // 是否在查詢表單數據存儲中保存, 預設為 false
36
+ autoSearchOnClear?: boolean = true; // 是否啟用清除查詢條件後重新再呼叫search功能, 預設為 true
37
+
38
+ protected constructor(data: Partial<BaseListViewModel<Q, V>> = {}) {
39
+ this.tableColumnArray = this.useTableColumnArray() || [];
40
+ this.dataList = reactive([]);
41
+ this.queryParam = data.queryParam as Q;
42
+ this.onSortChange = this.onSortChange.bind(this);
43
+ this.onPageChange = this.onPageChange.bind(this);
44
+ // 設定 table action filter
45
+ this.tableActionFilter = data.tableActionFilter;
46
+ // 針對 tableColumnArray 設定 action 的過濾函式
47
+ if(this.tableActionFilter) {
48
+ this.tableColumnArray
49
+ .filter(col => col.type === CTableColumnType.Action)
50
+ .forEach((col) => {
51
+ col.actionFilter = (action: CTableColumnActionMeta, rowData?: V) => {
52
+ return this.tableActionFilter!(action, rowData);
53
+ }
54
+ });
55
+ }
56
+ }
57
+
58
+ /**
59
+ * 初始化方法
60
+ * @param options
61
+ */
62
+ init(options: {
63
+ router: Router,
64
+ storeAction?: GlobalViewActions
65
+ }) : this {
66
+ this.router = options.router;
67
+ this.storeAction = options.storeAction;
68
+ return this;
69
+ }
70
+
71
+ // ~ ----------------------------------------------------------
72
+ // ~ abstract methods or 可選的覆寫方法
73
+ // 抽象方法,子類別必須實現 或是 可選覆寫
74
+
75
+ /**
76
+ * 查詢列表的 API 端點元資料
77
+ */
78
+ queryListApiEndpointMeta() : ListApiEndpointMeta {
79
+ return {
80
+ key: '',
81
+ dataParser: (dataItem: any) => {}
82
+ }
83
+ }
84
+
85
+ /**
86
+ * 生成新增的 URL
87
+ * @returns {string} 新增頁面的 URL
88
+ */
89
+ abstract toCreateUrl(): string;
90
+
91
+ /**
92
+ * 生成編輯的 URL
93
+ * @param rowData 資料模型
94
+ * @returns {string} 編輯頁面的 URL
95
+ */
96
+ abstract toEditUrl(rowData: V): string;
97
+
98
+ /**
99
+ * 使用表格欄位定義
100
+ */
101
+ abstract useTableColumnArray(): CTableColumn[];
102
+
103
+ // ~ ----------------------------------------------------------
104
+ // ~ methods
105
+
106
+ /**
107
+ * 在組件掛載時調用的函式
108
+ */
109
+ async callOnMounted() {
110
+ this.readQueryParamFromStore();
111
+ await this.doSearch();
112
+ }
113
+
114
+ /**
115
+ * 儲存目前 query param 的屬性值為預設值
116
+ */
117
+ storeQueryDefaultValues(defValues: Record<string, any>) {
118
+ this.queryParamDefaultValues = defValues;
119
+ if(this.queryParam) {
120
+ _.merge(this.queryParam, _.cloneDeep(this.queryParamDefaultValues));
121
+ }
122
+ this.enableQueryParamDefault = true;
123
+ }
124
+
125
+ /**
126
+ * 重設查詢參數為預設值
127
+ */
128
+ resetToDefaultValues() {
129
+ if(_.isNil(this.queryParamDefaultValues) || _.isEmpty(this.queryParamDefaultValues)) {
130
+ return;
131
+ }
132
+ _.merge(this.queryParam, _.cloneDeep(this.queryParamDefaultValues));
133
+ }
134
+
135
+ /**
136
+ * 載入 API 回應資料
137
+ * @param response API 回應物件
138
+ * @param options 載入選項
139
+ */
140
+ loadApiResponse(response:ApiResponse, options: {
141
+ dataListFinder?: (response: ApiResponse) => any[],
142
+ parser?: (data: any) => V,
143
+ }) {
144
+ this.queryParam.loadResponse(response);
145
+ this.cleanDataList();
146
+ const dataList = _.invoke(options, 'dataListFinder', response) || _.get(response.data, 'items', response.data);
147
+ if(!_.isArray(dataList)) {
148
+ return;
149
+ }
150
+ let dataListToAdd = dataList as V[];
151
+ if(options.parser) {
152
+ dataListToAdd = dataList.map((dataItem,idx) => {
153
+ const b = options.parser!(dataItem);
154
+ this.setupRowNumber(b, idx);
155
+ return b;
156
+ });
157
+ }
158
+ this.addDataList(dataListToAdd);
159
+ }
160
+
161
+ /**
162
+ * 設定列號 根據當下的分頁資訊計算
163
+ * @param dataItem 當前資料項目
164
+ * @param dataIndex 當前資料的索引
165
+ */
166
+ setupRowNumber(dataItem: V, dataIndex: number) {
167
+ // __rowNumber
168
+ const pageObj = this.queryParam.page;
169
+ const pageIndex = pageObj?.pageIndex || 0;
170
+ const pageSize = pageObj?.pageSize || 0;
171
+ const rowNumber = (pageObj?.offset ?? 0) + (dataIndex + 1);
172
+ dataItem.__rowNumber = rowNumber;
173
+ // 判斷是否為最後一頁的最後一筆
174
+ dataItem.__isLastAndLastPage = pageObj?.totalCount === rowNumber && pageObj?.totalPage === (pageIndex + 1);
175
+ }
176
+
177
+ /**
178
+ * 添加單筆資料到列表中
179
+ * @param dataToAdd
180
+ */
181
+ addData(dataToAdd: V) {
182
+ // 添加單筆資料到列表中
183
+ this.dataList.push(dataToAdd);
184
+ }
185
+
186
+ /**
187
+ * 添加多筆資料到列表中
188
+ * @param dataListToAdd
189
+ */
190
+ addDataList(dataListToAdd: V[]) {
191
+ // 添加多筆資料到列表中
192
+ this.dataList.push(...dataListToAdd);
193
+ }
194
+
195
+ /**
196
+ * 清除資料列表
197
+ */
198
+ cleanDataList() {
199
+ // 清除資料列表
200
+ this.dataList.splice(0, this.dataList.length);
201
+ }
202
+
203
+ /**
204
+ * 顯示錯誤訊息對話框
205
+ * @param content 錯誤訊息內容
206
+ * @param onOk 確認按鈕點擊事件
207
+ * @param onCancel 取消按鈕點擊事件
208
+ */
209
+ showConfirm(params: { content: string, onOk: () => void, onCancel?: () => void }) {
210
+ // 顯示確認對話框
211
+ this.storeAction?.showModalConfirm({
212
+ content: params.content,
213
+ onOk: params.onOk,
214
+ onCancel: params.onCancel
215
+ });
216
+ }
217
+
218
+ makeQueryParamStoreKey(): string {
219
+ return `R-${this.router?.currentRoute.value.name as string}`;
220
+ }
221
+
222
+ /**
223
+ * 將查詢參數保存到查詢表單數據存儲中
224
+ */
225
+ saveQueryParamToStore() {
226
+ if(!this.saveOnQueryFormDataStore) {
227
+ return;
228
+ }
229
+ const store = useQueryFormDataStore();
230
+ store.save(this.makeQueryParamStoreKey(), this.queryParam);
231
+ }
232
+
233
+ /**
234
+ * 從查詢表單數據存儲中讀取查詢參數
235
+ */
236
+ readQueryParamFromStore() {
237
+ if(!this.saveOnQueryFormDataStore) {
238
+ return;
239
+ }
240
+ const store = useQueryFormDataStore();
241
+ const savedData = store.getQueryParam(this.makeQueryParamStoreKey());
242
+ if(savedData) {
243
+ this.queryParam.load(savedData);
244
+ }
245
+ }
246
+
247
+ // ~ ----------------------------------------------------------
248
+ // ~ event handlers
249
+
250
+ /**
251
+ * 前往新增頁面
252
+ */
253
+ goToCreatePage() {
254
+ console.log('goToCreatePage', this.toCreateUrl());
255
+ if(!this.router) {
256
+ console.warn('Router is not initialized.');
257
+ }
258
+ this.router?.push({path: this.toCreateUrl()});
259
+ }
260
+
261
+ /**
262
+ * 執行搜尋操作
263
+ * 查詢時會將查詢參數保存到 store 中,並呼叫 API 獲取資料
264
+ */
265
+ async doSearch(event?: Event) {
266
+ const {key, dataListFinder, dataParser, pathParam} = this.queryListApiEndpointMeta();
267
+ if(_.isNil(key) || _.isEmpty(key)) {
268
+ return;
269
+ }
270
+
271
+ // 如果是從表單觸發的 doSearch,則把 pageIndex 重設為 0
272
+ if(event) {
273
+ if(this.queryParam.page) {
274
+ this.queryParam.page.pageIndex = 0;
275
+ }
276
+ }
277
+
278
+ // 儲存查詢參數到 store
279
+ this.saveQueryParamToStore();
280
+ // 呼叫 API 取得資料
281
+ const resp = await ApiService.call({
282
+ endpointKey: key,
283
+ pathParam: pathParam,
284
+ queryParam: this.queryParam.toQueryStringParam(),
285
+ postBody: this.queryParam.toPayload()
286
+ });
287
+ if(!resp.isOk()) {
288
+ return;
289
+ }
290
+ // 載入 API 回應資料
291
+ this.loadApiResponse(resp, {
292
+ parser: dataParser,
293
+ dataListFinder: dataListFinder
294
+ });
295
+ }
296
+
297
+
298
+ /**
299
+ * 處理清除事件
300
+ * 清除查詢參數,並根據設定決定是否重新執行搜尋
301
+ */
302
+ async doClear() {
303
+ // 清除查詢參數
304
+ this.queryParam.clear();
305
+ // 如果啟用預設值,則重設為預設值
306
+ if(this.enableQueryParamDefault) {
307
+ this.resetToDefaultValues();
308
+ }
309
+ // 視設定再次執行搜尋
310
+ if(this.autoSearchOnClear) {
311
+ await this.doSearch();
312
+ }
313
+ }
314
+
315
+ /**
316
+ * 處理排序變更事件
317
+ */
318
+ async onSortChange() {
319
+ await this.doSearch();
320
+ }
321
+
322
+ /**
323
+ * 處理分頁變更事件
324
+ * @param params
325
+ */
326
+ async onPageChange(params: QueryPage) {
327
+ await this.doSearch();
328
+ }
329
+
330
+ /**
331
+ * 處理 row action編輯事件
332
+ * @param rowMeta
333
+ */
334
+ async onColumnActionEdit(rowMeta: {actionType:CTableColumnActionType, rowData: V}) {
335
+ // 前往編輯頁面
336
+ await this.router?.push({path: this.toEditUrl(rowMeta.rowData)});
337
+ }
338
+
339
+ /**
340
+ * 刪除列後的處理
341
+ * @param rowData
342
+ */
343
+ async callOnAfterDeleteRow(rowData: V) {
344
+ // 刪除後的處理
345
+ // 如果是最後一頁的最後一筆,則把分頁往前一頁
346
+ if(rowData.__isLastAndLastPage) {
347
+ this.queryParam.page?.previous();
348
+ }
349
+ return await this.doSearch();
350
+ }
351
+
352
+ // ~ ----------------------------------------------------------
353
+ }
354
+
355
+
356
+ /**
357
+ * CheckableListViewModel 是一個抽象類別,擴展自 BaseListViewModel,提供了查找被選中資料的功能。
358
+ */
359
+ export abstract class CheckableListViewModel<Q extends QueryParameter, V extends BaseFormDataModel> extends BaseListViewModel<Q, V> {
360
+
361
+ /**
362
+ * 查找所有被選中的資料
363
+ */
364
+ findCheckedData(): V[] {
365
+ // 查找所有被選中的資料
366
+ if (isReactive(this.dataList)) {
367
+ const rawDataList = toRaw(this.dataList) as unknown as V[];
368
+ return rawDataList.filter((data) => {
369
+ return _.get(data, 'checked', false) === true;
370
+ });
371
+ }
372
+ return [];
373
+ }
374
+
375
+ /**
376
+ * 重置所有資料的選中狀態
377
+ */
378
+ resetCheckedData() {
379
+ // 重置所有資料的選中狀態
380
+ this.dataList.forEach((data) => {
381
+ _.set(data, 'checked', false);
382
+ })
383
+ }
384
+ }
385
+
386
+
387
+ // ~ ----------------------------------------------------------
388
+
389
+ // 防止 Vite/rollup tree-shake 掉 type export
390
+ export const __BaseListViewModelDefine__ = null as unknown as
391
+ ListApiEndpointMeta
392
+ ;
@@ -0,0 +1,91 @@
1
+ import {ref} from 'vue';
2
+ import {Modal} from 'bootstrap';
3
+ import {v4 as uuid} from 'uuid';
4
+
5
+ export enum ICBSModalViewType {
6
+ Info = 'infoModal',
7
+ Form = 'formModal',
8
+ Table = 'tableModal',
9
+ }
10
+
11
+ export interface ICBSModalViewModel {
12
+ type: ICBSModalViewType;
13
+ title?: string;
14
+ config?: Record<string, any>; // 額外的配置選項與 Modal config 一致
15
+ onOpen?: () => void;
16
+ onClose?: () => void;
17
+ }
18
+
19
+ export class CBSModalViewModel {
20
+
21
+ modalInstance = ref<Modal | null>(null);
22
+ elt: HTMLElement | null = null;
23
+ type: ICBSModalViewType;
24
+ modalId: string;
25
+ labelId: string;
26
+ title?: string;
27
+ config?: Record<string, any>; // 額外的配置選項與 Modal config 一致
28
+ onOpen?: () => void;
29
+ onClose?: () => void;
30
+
31
+ constructor(data?: Partial<ICBSModalViewModel>) {
32
+ this.type = data?.type || ICBSModalViewType.Info;
33
+ this.modalId = `${this.type}-${uuid()}`;
34
+ this.labelId = `${this.modalId}-label`;
35
+ this.title = data?.title || this.defaultTitle();
36
+ this.onOpen = data?.onOpen;
37
+ this.onClose = data?.onClose;
38
+ this.config = Object.assign({
39
+ backdrop: 'static',
40
+ keyboard: false
41
+ }, data?.config);
42
+ }
43
+
44
+ defaultTitle(): string {
45
+ switch(this.type) {
46
+ case ICBSModalViewType.Info:
47
+ return '資訊';
48
+ case ICBSModalViewType.Form:
49
+ return '請填寫表單';
50
+ case ICBSModalViewType.Table:
51
+ return '請選擇';
52
+ default:
53
+ return '';
54
+ }
55
+ }
56
+
57
+ show() {
58
+ this.modalInstance.value?.show();
59
+ }
60
+
61
+ hide() {
62
+ this.modalInstance.value?.hide();
63
+ }
64
+
65
+
66
+ init({elt} : { elt: HTMLElement }) {
67
+ if(!elt) {
68
+ return;
69
+ }
70
+ this.elt = elt;
71
+ this.modalInstance.value = new Modal(this.elt, {
72
+ ...this.config
73
+ });
74
+
75
+ this.elt.addEventListener('show.bs.modal', () => {
76
+ this.onOpen?.();
77
+ });
78
+
79
+ this.elt.addEventListener('hidden.bs.modal', () => {
80
+ this.onClose?.();
81
+ });
82
+ }
83
+
84
+ unmount() {
85
+ if (this.modalInstance.value) {
86
+ this.modalInstance.value.dispose();
87
+ this.modalInstance.value = null;
88
+ }
89
+ this.elt = null;
90
+ }
91
+ }
@@ -0,0 +1,181 @@
1
+ import _ from 'lodash';
2
+
3
+ /**
4
+ * CFileDataModel.ts
5
+ * 用來定義檔案資料模型的介面和實作類別
6
+ */
7
+
8
+ /**
9
+ * CFileDataModel 介面
10
+ */
11
+ export interface CFileDataModel {
12
+ uid?: string; // 檔案 ID
13
+ originalFilename?: string; // 原始檔案名稱
14
+ name?: string; // 檔案名稱
15
+ mimeType?: string; // 檔案 MIME 類型
16
+ ext?: string; // 檔案類型(副檔名)
17
+ size?: number; // 檔案大小 (以位元組為單位)
18
+ url?: string; // 檔案 URL
19
+ createdAt?: Date; // 創建時間
20
+ fileObj?: File; // 檔案物件 native File object
21
+ icon?: string; // 檔案圖示 class (可選)
22
+ fontIcon?: string // font awesome icon 設定 (可選)
23
+ badgeClass?: string; // 檔案徽章樣式 (可選)
24
+ isAssociated?: boolean; // 是否已關聯到其他資料 (如新聞、活動等)
25
+ description?: string; // 檔案描述 (可選)
26
+ }
27
+
28
+ /**
29
+ * 用來標示檔案資料模型的實作類別
30
+ */
31
+ export class CFileDataModel implements CFileDataModel {
32
+ uid?: string;
33
+ originalFilename?: string; // 原始檔案名稱
34
+ name?: string;
35
+ mimeType?: string; // 檔案 MIME 類型
36
+ ext?: string;
37
+ size?: number;
38
+ url?: string;
39
+ createdAt?: Date;
40
+ fileObj?: File;
41
+ icon?: string;
42
+ fontIcon?: string // font awesome icon 設定 (可選)
43
+ badgeClass?: string;
44
+ isAssociated?: boolean = false; // 是否已關聯到其他資料 (如新聞、活動等)
45
+ description?: string; // 檔案描述 (可選)
46
+
47
+ constructor(data?: Partial<CFileDataModel>) {
48
+ if (data) {
49
+ Object.assign(this, data);
50
+ }
51
+ }
52
+
53
+ init(): this {
54
+ if (_.isNil(this.name) && !_.isNil(this.originalFilename)) {
55
+ this.name = this.originalFilename;
56
+ }
57
+ if (this.originalFilename) {
58
+ const parts = _.split(this.originalFilename, '.');
59
+ if (parts.length > 1) {
60
+ this.ext = _.last(parts)?.toLowerCase();
61
+ }
62
+ // 基於副檔名來決定圖示與徽章樣式
63
+ switch (this.ext) {
64
+ case 'jpg':
65
+ case 'jpeg':
66
+ case 'png':
67
+ case 'gif':
68
+ case 'bmp':
69
+ case 'webp':
70
+ case 'svg':
71
+ this.icon = 'text-warning';
72
+ this.fontIcon = 'file-image';
73
+ this.badgeClass = 'bg-warning';
74
+ break;
75
+ case 'pdf':
76
+ this.icon = 'text-danger';
77
+ this.fontIcon = 'file-pdf';
78
+ this.badgeClass = 'bg-danger';
79
+ break;
80
+ case 'doc':
81
+ case 'docx':
82
+ this.icon = 'text-info';
83
+ this.fontIcon = 'file-word';
84
+ this.badgeClass = 'bg-info';
85
+ break;
86
+ case 'xls':
87
+ case 'xlsx':
88
+ this.icon = 'text-success';
89
+ this.fontIcon = 'file-excel';
90
+ this.badgeClass = 'bg-success';
91
+ break;
92
+ case 'mp4':
93
+ case 'mov':
94
+ case 'avi':
95
+ case 'wmv':
96
+ case 'webm':
97
+ this.icon = 'text-primary';
98
+ this.fontIcon = 'file-video';
99
+ this.badgeClass = 'bg-primary';
100
+ break;
101
+ default:
102
+ this.icon = 'text-secondary';
103
+ this.fontIcon = 'file-alt';
104
+ this.badgeClass = 'bg-secondary';
105
+ break;
106
+ }
107
+ this.isAssociated = true;
108
+ }
109
+ return this;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * CPhotoDataModel 介面
115
+ */
116
+ export class CPhotoDataModel extends CFileDataModel {
117
+ width?: number; // 圖片寬度
118
+ height?: number; // 圖片高度
119
+ loaded?: boolean = false; // 圖片是否已經載入完成
120
+
121
+ constructor(data?: Partial<CPhotoDataModel>) {
122
+ super(data);
123
+ if (data) {
124
+ Object.assign(this, data);
125
+ }
126
+ }
127
+ }
128
+
129
+ // ~ ----------------------------------------------------------
130
+
131
+ export interface FileUploadProgress {
132
+ uid?: string; // 檔案 ID
133
+ name?: string; // 檔案名稱
134
+ progress?: number; // 上傳進度百分比 (0-100)
135
+ status?: 'ready' | 'uploading' | 'completed' | 'failed'; // 上傳
136
+ error?: string; // 錯誤訊息 (如果有的話)
137
+ }
138
+
139
+ export class FileUploadProgress implements FileUploadProgress {
140
+ uid?: string;
141
+ name?: string;
142
+ progress?: number = 0;
143
+ status?: 'ready' | 'uploading' | 'completed' | 'failed' = 'ready';
144
+ error?: string;
145
+
146
+ constructor(data?: Partial<FileUploadProgress>) {
147
+ if (data) {
148
+ Object.assign(this, data);
149
+ }
150
+ }
151
+
152
+ readyToUpload(): boolean {
153
+ return this.status === 'ready' || this.status === 'failed';
154
+ }
155
+
156
+ isShowProgress(): boolean {
157
+ return this.status === 'uploading';
158
+ }
159
+
160
+ isDone(): boolean {
161
+ return this.status === 'completed' || this.status === 'failed';
162
+ }
163
+
164
+ start() {
165
+ this.status = 'uploading';
166
+ this.progress = 0;
167
+ }
168
+
169
+ complete() {
170
+ this.status = 'completed';
171
+ this.progress = 100;
172
+ }
173
+
174
+ fail(errorMessage: string) {
175
+ this.status = 'failed';
176
+ this.error = errorMessage;
177
+ this.progress = 0;
178
+ }
179
+
180
+ // ~ ----------------------------------------------------------
181
+ }
@@ -0,0 +1,34 @@
1
+
2
+ export class CImageViewModel {
3
+
4
+ onLoad?: (event: Event) => void; // 圖片加載完成
5
+ onError?: (event: Event | string) => void; // 圖片加載錯
6
+ placeholderImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/w8AAgMBAp6Ff9cAAAAASUVORK5CYII=';
7
+
8
+ constructor(data?: Partial<CImageViewModel>) {
9
+ if(data) {
10
+ Object.assign(this, data);
11
+ }
12
+ }
13
+
14
+ onLoadHandler(event: Event) {
15
+ // 確保圖片載入完成後添加 loaded 類別
16
+ const imgElement = event.target as HTMLImageElement;
17
+ imgElement.classList.add('loaded');
18
+ // 如果有 onLoad 回調,則調用它
19
+ this.onLoad?.(event);
20
+ }
21
+
22
+ onErrorHandler(event: Event | string) {
23
+ const eventTarget = (event as Event).target;
24
+ if (eventTarget && eventTarget instanceof HTMLImageElement) {
25
+ // 避免無限迴圈的錯誤觸發
26
+ if (eventTarget.src !== this.placeholderImage) {
27
+ eventTarget.src = this.placeholderImage;
28
+ eventTarget.classList.add('loaded', 'error'); // 添加錯誤類別
29
+ }
30
+ }
31
+ // 如果有 onError 回調,則調用它
32
+ this.onError?.(event);
33
+ }
34
+ }