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,623 @@
1
+ import {Ref, watch} from "vue";
2
+ import * as yup from "yup";
3
+ import {FieldContext, FormContext, FormValidationResult, useField, useForm, ValidationResult} from "vee-validate";
4
+ import _ from "lodash";
5
+ import {v4 as uuidv4} from "uuid";
6
+ import dayjs from "dayjs";
7
+ import log from 'loglevel';
8
+
9
+ // 新增介面來擴充 FormValidationResult
10
+ export interface CFormValidationResult extends FormValidationResult<Record<string, any>> {
11
+ yupError?: Array<{ path: string; message: string }>; // 新增 yupError 屬性,存放 Yup 錯誤
12
+ }
13
+
14
+ /**
15
+ * BaseFormDataModel 是一個抽象類別,用於定義表單數據模型的基礎結構。
16
+ * 它包含了表單驗證的 schema、欄位映射和表單上下文等屬性。
17
+ * 子類別需要實現 initFormSchema 方法來定義具體的表單驗證邏輯。
18
+ */
19
+ export abstract class BaseFormDataModel {
20
+
21
+ _debug?: boolean = false; // 調試模式開關
22
+ uid?: string; // 唯一識別符,用於標識該模型實例
23
+ rowId?: string; // 行識別符,用於在表格中標識該行,當不指定時,是隨機產生的 uid
24
+ _formMode?: string; // 表單模型類型,用於區分不同的表單模型
25
+ __rowNumber?: number; // 行號,用於在表格中顯示該行的順序
26
+ __isLastAndLastPage?: boolean; // 是否為最後一頁的最後一筆,用於在刪除最後一筆資料後,重新載入前一頁
27
+
28
+ // ----- properties -----
29
+ /**
30
+ * 表單驗證的 schema,使用 Yup 進行定義。
31
+ */
32
+ formSchema: yup.ObjectSchema<any> | null = null;
33
+ /**
34
+ * 從 formSchema 中展平的欄位名稱列表。
35
+ * formSchema.fields 的 key 值,同時支援 nest object 的欄位名稱。
36
+ * 例如 address.city, address.zipCode
37
+ * 這個屬性會在 initForm 時被初始化。
38
+ */
39
+ formSchemaFieldList: string[] | null = null;
40
+ /**
41
+ * 表單欄位映射,用於 VeeValidate 的表單驗證。
42
+ */
43
+ formFieldMap: Record<string, FieldContext> = {};
44
+ /**
45
+ * 表單上下文,包含驗證和狀態管理。
46
+ */
47
+ formContext: FormContext<Record<string, any>> | null = null;
48
+
49
+ // ----- construct -----
50
+ /**
51
+ * BaseFormDataModel 的建構函數。
52
+ * @param data 可選的初始數據,用於初始化模型屬性。
53
+ */
54
+ protected constructor(data?: Record<string, any>) {
55
+ this.rowId = _.get(data, "_rowId", uuidv4());
56
+ this._formMode = _.get(data, "formMode", null);
57
+ this._debug = _.get(data, "debug", null);
58
+ }
59
+
60
+ // ----- function -----
61
+
62
+ /**
63
+ * 初始化表單驗證的 schema。
64
+ */
65
+ abstract initFormSchema(): yup.ObjectSchema<any>;
66
+
67
+ /**
68
+ * 取得需要套用 form field 欄位名稱的列表。
69
+ * @returns {string[]} 欄位名稱列表
70
+ */
71
+ abstract dataFieldNameList(): string[];
72
+
73
+ /**
74
+ * 在呼叫 loadData 之後的會執行的方法,供子類別可以接續處理。
75
+ * @param data
76
+ */
77
+ afterLoadFormData(data: Record<string, any>) {};
78
+
79
+ /**
80
+ * 載入數據到模型中。
81
+ * 不會檢查欄位名稱是否在 dataFieldNameList 中定義。
82
+ * @param data
83
+ */
84
+ loadData(data: Record<string, any>): this {
85
+ Object.keys(data).forEach((key) => {
86
+ // 使用 lodash 的 set 方法來設定屬性值
87
+ _.set(this, key, data[key]);
88
+ });
89
+ this.applyPropertiesToFormField();
90
+ return this;
91
+ }
92
+
93
+ /**
94
+ * 載入數據到表單模型中。
95
+ * 是從有定義的表單欄位名稱列表中進行載入,同時 properties 也必須要有定義會忽略optional且未賦值的情況。
96
+ *
97
+ * @param data data
98
+ */
99
+ loadFormData(data: Record<string, any>): this {
100
+ // 對 data forEach 走訪,該 key 必須要在 dataFieldNameList 中定義
101
+ const targetFieldNameList = this.dataFieldNameList();
102
+ Object.keys(data).forEach((fieldName) => {
103
+ // 檢查 fieldName 是否在 this 中有定義
104
+ // 如果沒有定義,則顯示警告並跳過該欄位
105
+ if (!Object.hasOwn(this, fieldName)) {
106
+ console.warn(`Field "${fieldName}" is not defined in the model.`);
107
+ return;
108
+ }
109
+ // 檢查 fieldName 是否在 targetFieldNameList 中
110
+ // 如果不在,則跳過該欄位
111
+ if (!targetFieldNameList.includes(fieldName)) {
112
+ return;
113
+ }
114
+ // 如果在,則使用 lodash 的 get 方法來取得 data 中的值
115
+ // 並回寫到 this 中對應的屬性
116
+ // 使用 lodash 的 set 方法來回寫到 dataModel
117
+ const val = _.get(data, fieldName, null);
118
+ _.set(this, fieldName, val);
119
+ });
120
+ this.afterLoadFormData(data);
121
+ this.applyPropertiesToFormField();
122
+ return this;
123
+ }
124
+
125
+ /**
126
+ * 將表單數據轉換為 JSON 格式。
127
+ */
128
+ formToJsonData(): Record<string, any> {
129
+ // 取得所有有定義的欄位名稱
130
+ const targetFieldNameList = this.dataFieldNameList();
131
+ // 使用 BaseFormDataModel 的靜態方法來生成 JSON 數據
132
+ return BaseFormDataModel.makeJsonData(targetFieldNameList, this);
133
+ }
134
+
135
+ /**
136
+ * 將表單數據轉換為 API 請求的 payload 格式。
137
+ * 供子類別可以覆寫此方法來調整 payload 結構。
138
+ * @param key 動作名稱,例如 "create"、"update" 等。
139
+ */
140
+ toPayload(key?: string): Record<string, any> {
141
+ if(key) {
142
+ return this.toPayloadMap()[key];
143
+ }
144
+ return this.formToJsonData();
145
+ }
146
+
147
+ /**
148
+ * 將表單數據轉換為包含 payload 的映射結構。
149
+ * 供子類別可以覆寫此方法來調整 payload 結構。
150
+ * key 動作名稱,例如 "create"、"update" 等。
151
+ */
152
+ toPayloadMap(): Record<string, Record<string,any>> {
153
+ return {
154
+ payload: this.toPayload()
155
+ };
156
+ }
157
+
158
+ /**
159
+ * 初始化表單相關的屬性和方法。
160
+ */
161
+ initForm() {
162
+ this.formSchema = this.initFormSchema();
163
+ // 取得所有有定義的欄位名稱
164
+ // 這邊要支援 nest object 的欄位名稱
165
+ // 例如 address.city, address.zipCode
166
+ // 因此需要使用 Object.keys 來取得所有欄位名稱
167
+ // 讓 targetFileNameList 裡面可以包含 nest object 的欄位名稱,例如 address.city
168
+ const targetFieldNameList = flattenFieldKeys(this.formSchema.fields);
169
+ this.formSchemaFieldList = targetFieldNameList;
170
+
171
+ // 產生 initial value
172
+ const initialValueMap: Record<string, any> = {};
173
+ targetFieldNameList.forEach(fieldName => {
174
+ const val = _.get(this, fieldName, null);
175
+ _.set(initialValueMap, fieldName, val);
176
+ });
177
+ // 使用 VeeValidate 的 useForm 來建立表單上下文
178
+ this.formContext = useForm({
179
+ validationSchema: this.formSchema,
180
+ initialValues: initialValueMap,
181
+ });
182
+
183
+ // 根據 formSchema 建置 formFieldMap
184
+ // 注意需要確保 formContext 已經被初始化
185
+ this.formFieldMap = {};
186
+ targetFieldNameList.forEach(fieldName => {
187
+ const field = useField(fieldName, undefined, {
188
+ validateOnValueUpdate: true,
189
+ validateOnMount: false
190
+ });
191
+ _.set(this.formFieldMap, fieldName, field);
192
+ });
193
+
194
+ // 設定 log level
195
+ const logger = log.getLogger('BaseFormDataModel');
196
+ if(this._debug) {
197
+ logger.setLevel('debug');
198
+ } else {
199
+ logger.setLevel('warn');
200
+ }
201
+
202
+ // 監控 values 異動,回寫到 dataModel
203
+ watch(
204
+ () => this.formContext!.values,
205
+ (newValues) => {
206
+ logger.debug('Form values changed:', newValues);
207
+ targetFieldNameList.forEach(fieldName => {
208
+ // 使用 lodash 的 set 方法來回寫到 dataModel
209
+ const val = _.get(newValues, fieldName, null);
210
+ // 根據 val 型別分別處理
211
+ // if val is array ,需要做深層複製
212
+ if(_.isArray(val)) {
213
+ logger.debug('Updating array field:', fieldName, 'to value:', val, 'with deep clone', _.cloneDeep(val));
214
+ _.set(this, fieldName, _.cloneDeep(val));
215
+ return;
216
+ }
217
+ // if val is Object ,需要做深層複製
218
+ if(_.isPlainObject(val)) {
219
+ logger.debug('Updating object field:', fieldName, 'to value:', val);
220
+ _.set(this, fieldName, _.cloneDeep(val));
221
+ }
222
+ // if 基本型別,直接回寫
223
+ logger.debug('Updating field:', fieldName, 'to value:', val);
224
+ _.set(this, fieldName, val);
225
+ });
226
+ },
227
+ {deep: true}
228
+ )
229
+ }
230
+
231
+ /**
232
+ * 驗證表單數據。
233
+ */
234
+ async validateForm(): Promise<FormValidationResult<Record<string, any>>> {
235
+ if(_.isNil(this.formContext)) {
236
+ return Promise.reject('Form context is not initialized.');
237
+ }
238
+ return this.formContext.validate()
239
+ .then(result => {
240
+ if(!result.valid && result.errors) {
241
+ focusFirstErrorField(result.errors);
242
+ }
243
+ return result;
244
+ });
245
+ }
246
+
247
+ /**
248
+ * 顯示指定欄位的錯誤訊息。
249
+ * @param fieldName
250
+ */
251
+ validateField(fieldName: string): Promise<ValidationResult> {
252
+ const field = this.findFieldContext(fieldName);
253
+ if(!field) {
254
+ return Promise.reject(`Field '${fieldName}' not found`);
255
+ }
256
+ return field.validate();
257
+ }
258
+
259
+ /**
260
+ * 清除表單錯誤訊息。
261
+ */
262
+ clearFormErrors(): void {
263
+ if(_.isNil(this.formContext)) {
264
+ return;
265
+ }
266
+ this.formContext.setErrors({});
267
+ }
268
+
269
+ /**
270
+ * 把本身的 properties 套用到 form field 上。
271
+ */
272
+ applyPropertiesToFormField() {
273
+ // 確保 formFieldMap 已經被初始化
274
+ if (!this.formFieldMap || Object.keys(this.formFieldMap).length === 0) {
275
+ return;
276
+ }
277
+ // 針對有宣告為 data field 的欄位,將其屬性套用到 form field 上
278
+ const targetFieldNameList = this.dataFieldNameList();
279
+ targetFieldNameList.forEach(fieldName => {
280
+ const targetField = _.get(this.formFieldMap, fieldName);
281
+ if(!targetField) {
282
+ return;
283
+ }
284
+ // 使用 lodash 的 get 方法來取得屬性值
285
+ const propertyValue = _.get(this, fieldName, null);
286
+ // 設定到 field 上
287
+ targetField.value.value = propertyValue;
288
+ });
289
+ }
290
+
291
+ /**
292
+ * 重置表單數據。
293
+ */
294
+ resetFormData() {
295
+ if(_.isNil(this.formContext)) {
296
+ return;
297
+ }
298
+ // 重置 formContext 的值
299
+ this.formContext.resetForm();
300
+ }
301
+
302
+ /**
303
+ * 檢查指定的欄位是否為必填欄位。
304
+ * Yup 的 when 條件(.when())會根據當前值動態改變 required 狀態,
305
+ * 這會自動反映在 schema.describe() 的 optional 屬性與 tests 陣列中。
306
+ * @param fieldName 欄位名稱
307
+ */
308
+ fieldIsRequired(fieldName: string): boolean {
309
+ // 檢查欄位是否在 formSchema 中定義
310
+ if (!this.formSchema || !this.formSchemaFieldList?.includes(fieldName)) {
311
+ return false;
312
+ }
313
+ // 需要對 fieldName 進行處理,因為可能是 nest object 的欄位名稱
314
+ const userFieldName = this.parseToYupFieldKey(fieldName);
315
+
316
+ // 取得 Yup schema
317
+ const fieldSchema = _.get(this.formSchema.fields, userFieldName);
318
+ if (fieldSchema instanceof yup.Schema) {
319
+ const desc = fieldSchema.describe();
320
+ // Yup 的 describe().optional 會根據 when 條件動態變化
321
+ if (desc.optional === false) {
322
+ return true;
323
+ }
324
+ // 進一步檢查 tests 陣列中是否有 required 條件
325
+ if (Array.isArray(desc.tests)) {
326
+ const hasRequiredTest = desc.tests.some(test => test.name === 'required');
327
+ if (hasRequiredTest) {
328
+ return true;
329
+ }
330
+ }
331
+ return false;
332
+ }
333
+ return false;
334
+ }
335
+
336
+ /**
337
+ * 取得指定欄位的最大長度限制。
338
+ * @param fieldName
339
+ */
340
+ fieldMaxLength(fieldName: string): number | null {
341
+ // 檢查欄位是否在 formSchema 中定義
342
+ if (!this.formSchema || !this.formSchemaFieldList?.includes(fieldName)) {
343
+ return null;
344
+ }
345
+ // 需要對 fieldName 進行處理,因為可能是 nest object 的欄位名稱
346
+ const userFieldName = this.parseToYupFieldKey(fieldName);
347
+
348
+ // 使用 Yup 的 describe 方法來取得欄位的描述資訊
349
+ const fieldSchema = _.get(this.formSchema.fields, userFieldName);
350
+ if (fieldSchema instanceof yup.StringSchema) {
351
+ const description = fieldSchema.describe();
352
+ const maxLengthTest = description.tests.find(test => test.name === 'max');
353
+ return _.get(maxLengthTest, 'params.max', null) as number;
354
+ }
355
+ return null;
356
+ }
357
+
358
+ /**
359
+ * 將欄位名稱轉換為 Yup schema 中的 key。
360
+ * @param fieldName 欄位名稱
361
+ */
362
+ parseToYupFieldKey(fieldName: string): string {
363
+ // 需要對 fieldName 進行處理,因為可能是 nest object 的欄位名稱
364
+ let userFieldName = fieldName;
365
+ if(fieldName.includes('.')) {
366
+ const parts = fieldName.split('.');
367
+ userFieldName = parts.join('.fields.');
368
+ }
369
+ return userFieldName;
370
+ }
371
+
372
+ /**
373
+ * 設定指定欄位的值。
374
+ * @param fieldName
375
+ * @param toValue
376
+ */
377
+ setFieldValue(fieldName: string, toValue: any): void {
378
+ // 檢查欄位是否在 formFieldMap 中定義
379
+ const fieldContext = this.findFieldContext( fieldName);
380
+ if (_.isNil(fieldContext)) {
381
+ console.warn(`Field "${fieldName}" is not defined in the model.`);
382
+ return;
383
+ }
384
+ // 更新 form field 的值
385
+ fieldContext.value.value = toValue;
386
+ }
387
+
388
+ /**
389
+ * 取得指定欄位的值。
390
+ * @param fieldName
391
+ */
392
+ fieldValue(fieldName: string): any {
393
+ // 檢查欄位是否在 formFieldMap 中定義
394
+ const fieldContext = this.findFieldContext( fieldName);
395
+ if (_.isNil(fieldContext)) {
396
+ console.warn(`Field "${fieldName}" is not defined in the model.`);
397
+ return null;
398
+ }
399
+ return fieldContext.value.value;
400
+ }
401
+
402
+ /**
403
+ * 根據欄位名稱查找對應的 FieldContext。
404
+ * @param fieldName 欄位名稱
405
+ * @returns FieldContext 或 null
406
+ */
407
+ findFieldContext(fieldName: string): FieldContext | null {
408
+ // 檢查欄位是否在 formFieldMap 中定義
409
+ if (!this.formFieldMap || !this.formFieldMap[fieldName]) {
410
+ console.warn(`Field "${fieldName}" is not defined in the model.`);
411
+ return null;
412
+ }
413
+ // 返回對應的 FieldContext
414
+ return this.formFieldMap[fieldName];
415
+ }
416
+
417
+ /**
418
+ * 根據欄位名稱查找對應的 Ref。
419
+ * @param fieldName 欄位名稱
420
+ * @returns Ref<any> 或 null
421
+ */
422
+ findFieldToBind(fieldName: string): Ref<any> | null {
423
+ // 檢查欄位是否在 formFieldMap 中定義
424
+ const fieldContext = this.findFieldContext(fieldName);
425
+ if (_.isNil(fieldContext)) {
426
+ console.warn(`Field "${fieldName}" is not defined in the model.`);
427
+ return null;
428
+ }
429
+ // 返回對應的 Ref
430
+ return fieldContext.value;
431
+ }
432
+
433
+ /**
434
+ * 根據欄位名稱列表查找對應的 Ref 映射。
435
+ * @param fieldNames 欄位名稱列表
436
+ */
437
+ findFieldRefToBind(fieldNames: string[]): Record<string, Ref<any>> {
438
+ const fieldBindMap: Record<string, Ref<any>> = {};
439
+ fieldNames.forEach(fieldName => {
440
+ const fieldContext = this.findFieldContext(fieldName);
441
+ if (fieldContext) {
442
+ fieldBindMap[fieldName] = fieldContext.value;
443
+ }
444
+ });
445
+ return fieldBindMap;
446
+ }
447
+
448
+ /**
449
+ * 將指定的屬性列表中的字串轉換為 Date 物件。
450
+ * @param data 數據物件
451
+ * @param propertyName 屬性名稱列表
452
+ */
453
+ parseStringToDate(data: Record<string, any>, propertyName: string[]) {
454
+ _.forEach(propertyName, (key) => {
455
+ const dateValue = _.get(data, key);
456
+ if (dateValue) {
457
+ const dayObj = dayjs(dateValue);
458
+ // 使用 _.set 更新 data 物件中的值
459
+ _.set(data, key, dayObj.isValid() ? dayObj.toDate() : null);
460
+ }
461
+ });
462
+ }
463
+
464
+ // ~ ----------------------------------------------------------
465
+ // ~ getter and setter
466
+
467
+ set formMode(type: string) {
468
+ this._formMode = type;
469
+ }
470
+
471
+ get formMode(): string | undefined {
472
+ return this._formMode;
473
+ }
474
+
475
+ // ~ ----------------------------------------------------------
476
+ // ~ static methods
477
+
478
+ /**
479
+ * 將指定的屬性列表轉換為 JSON 格式的數據。
480
+ * @param propertyList
481
+ * @param dataModel
482
+ */
483
+ static makeJsonData(propertyList: string[], dataModel: BaseFormDataModel): Record<string, any> {
484
+ const jsonData: Record<string, any> = {};
485
+ propertyList.forEach(property => {
486
+ // 使用 lodash 的 get 方法來取得屬性值
487
+ jsonData[property] = _.get(dataModel, property, null);
488
+ });
489
+ return jsonData;
490
+ }
491
+
492
+ // ~ ----------------------------------------------------------
493
+ }
494
+
495
+ /**
496
+ * 檢查指定的表單欄位是否有錯誤訊息。
497
+ * @param field
498
+ */
499
+ export function hasFormFieldError(field: FieldContext): boolean {
500
+ if(!field || !field.errorMessage) {
501
+ return false;
502
+ }
503
+ return !_.isNil(field.errorMessage.value) && _.trim(field.errorMessage.value) !== '';
504
+ }
505
+
506
+ /**
507
+ * 將焦點設置到第一個有錯誤的表單欄位,並將其滾動到視圖中間位置。
508
+ * @param errors 一個包含欄位名稱和錯誤訊息的物件
509
+ */
510
+ function focusFirstErrorField(errors: Partial<Record<string, string>>) {
511
+ if(_.isEmpty(errors)) {
512
+ return;
513
+ }
514
+ const firstErrorField = Object.keys(errors)[0];
515
+ const el = document.querySelector(`[name="${firstErrorField}"]`) as HTMLElement;
516
+ if (!el) {
517
+ return;
518
+ }
519
+ el.focus();
520
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
521
+ }
522
+
523
+ /**
524
+ * 將物件的欄位名稱展平成一維陣列。
525
+ * @param obj 物件
526
+ * @param prefix 前綴字串
527
+ */
528
+ function flattenFieldKeys(obj: Record<string, any>, prefix = ''): string[] {
529
+ return _.flatMap(Object.keys(obj), key => {
530
+ const fieldItem = obj[key];
531
+ const newKey = prefix ? `${prefix}.${key}` : key;
532
+ if(fieldItem.type === 'object') {
533
+ if(Object.keys(fieldItem.fields).length === 0) {
534
+ return newKey;
535
+ }
536
+ return flattenFieldKeys(fieldItem.fields, newKey);
537
+ }
538
+ return newKey;
539
+ });
540
+ }
541
+
542
+ /**
543
+ * CheckableListViewModel 定義了可檢查的列表視圖模型。
544
+ */
545
+ export interface CheckableModel {
546
+ checked?: boolean; // 是否被選中
547
+ toggleChecked(): void; // 切換選中狀態
548
+ }
549
+
550
+
551
+ /**
552
+ * CheckableDataModel 是一個抽象類別,實現了 CheckableModel 接口。
553
+ */
554
+ export abstract class CheckableDataModel extends BaseFormDataModel implements CheckableModel {
555
+
556
+ checked: boolean = false; // 是否被選中
557
+
558
+ protected constructor(data?: Record<string, any>) {
559
+ super(data);
560
+ }
561
+
562
+ /**
563
+ * 切換選中狀態。
564
+ */
565
+ toggleChecked(): void {
566
+ this.checked = !this.checked;
567
+ }
568
+ }
569
+
570
+ /**
571
+ * FormDataModelMapper 用於將一個資料物件映射到一個 BaseFormDataModel 的子類別實例中,
572
+ * 並且初始化該實例的表單相關功能。
573
+ * 同時會監控表單值的變化,並將變化回寫到原始資料物件中。
574
+ * 目的:網頁控制項的雙向或是單向綁定到 dataInstance 上,而不是直接綁定到 data 上。
575
+ */
576
+ export class FormDataModelMapper<T extends BaseFormDataModel> {
577
+
578
+ dataModelClass: new (data?: Partial<T>) => T;
579
+ dataInstance: T;
580
+
581
+ // ~ ----------------------------------------------------------
582
+ // ~ constructor
583
+
584
+ constructor(dataModelClass: new (data?: Partial<T>) => T, data: Partial<T>) {
585
+ this.dataModelClass = dataModelClass;
586
+ this.dataInstance = new this.dataModelClass(data);
587
+ // 初始化 FormDataModel form 相關功能
588
+ this.dataInstance.initForm();
589
+ // Proxy validateForm method
590
+ data.validateForm = this.dataInstance.validateForm.bind(this.dataInstance);
591
+ // watch dataInstance formContext values to update data
592
+ watch(
593
+ () => this.dataInstance.formContext!.values,
594
+ async (newValues) => {
595
+ // 根據 dataInstance 的 dataFieldNameList 來更新 data
596
+ const targetFieldNameList = this.dataInstance.dataFieldNameList();
597
+ targetFieldNameList.forEach(fieldName => {
598
+ // 使用 lodash 的 set 方法來回寫到 data
599
+ const val = _.get(newValues, fieldName, null);
600
+ _.set(data, fieldName, val);
601
+ // 驗證 field
602
+ this.validateField(fieldName);
603
+ });
604
+ }, {
605
+ deep: true,
606
+ immediate: true
607
+ }
608
+ );
609
+ }
610
+
611
+ /**
612
+ * 驗證指定欄位。
613
+ * @param fieldPath
614
+ */
615
+ validateField(fieldPath: string) {
616
+ const field = _.get(this.dataInstance.formFieldMap, fieldPath) as FieldContext;
617
+ if(!field) {
618
+ return Promise.reject(`Field '${fieldPath}' not found`);
619
+ }
620
+ return field.validate();
621
+ }
622
+
623
+ }