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,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
|
+
}
|