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,199 @@
|
|
|
1
|
+
import {ref, Ref} from "vue";
|
|
2
|
+
import _ from 'lodash';
|
|
3
|
+
import {writeVueRefValue} from "../utils/CToolUtils";
|
|
4
|
+
import {PermissionDescriptor} from "../auth/PermissionDescriptor";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* MenuItem 介面用於描述左側選單項目結構
|
|
8
|
+
* name: 顯示名稱
|
|
9
|
+
* icon: 圖示 class 或路徑(可選)
|
|
10
|
+
* path: 路由路徑
|
|
11
|
+
* children: 子選單項目(可選,型別為 CMenuItem 陣列)
|
|
12
|
+
*/
|
|
13
|
+
export interface MenuItem {
|
|
14
|
+
id: string
|
|
15
|
+
name: string;
|
|
16
|
+
icon?: string; // icon class name
|
|
17
|
+
fontIcon?: string; // awesome font icon name
|
|
18
|
+
path?: string;
|
|
19
|
+
children?: MenuItem[];
|
|
20
|
+
needRoles?: string[]; // 允許的權限列表
|
|
21
|
+
permission?: string | string[]; // 需要的權限
|
|
22
|
+
activePaths?: string[]; // 當下路徑匹配列表,當前路徑符合其中一個即視為 active
|
|
23
|
+
|
|
24
|
+
// 提供方法告知是否有子選單
|
|
25
|
+
hasChildren?: () => boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* CMenuItem 類別實現 MenuItem 介面
|
|
30
|
+
*/
|
|
31
|
+
export class CMenuItem implements MenuItem {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
icon?: string;
|
|
35
|
+
fontIcon?: string;
|
|
36
|
+
path?: string;
|
|
37
|
+
needRoles?: string[];
|
|
38
|
+
permission?: string | string[];
|
|
39
|
+
permissionDescriptors?: PermissionDescriptor[];
|
|
40
|
+
activePaths?: string[];
|
|
41
|
+
children?: CMenuItem[];
|
|
42
|
+
isOpen?: Ref<boolean>; // 是否展開子選單
|
|
43
|
+
isActive?: Ref<boolean>; // 是否為當前選中項目
|
|
44
|
+
|
|
45
|
+
constructor(params: MenuItem) {
|
|
46
|
+
this.id = params.id;
|
|
47
|
+
this.name = params.name;
|
|
48
|
+
this.icon = params.icon;
|
|
49
|
+
this.fontIcon = params.fontIcon;
|
|
50
|
+
this.path = params.path;
|
|
51
|
+
this.needRoles = params.needRoles;
|
|
52
|
+
this.permission = params.permission;
|
|
53
|
+
if(params.permission) {
|
|
54
|
+
if(_.isArray(params.permission)) {
|
|
55
|
+
this.permissionDescriptors = params.permission.map(perm => new PermissionDescriptor(perm));
|
|
56
|
+
} else {
|
|
57
|
+
this.permissionDescriptors = [new PermissionDescriptor(params.permission)];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
this.activePaths = params.activePaths || [];
|
|
61
|
+
if(params.children) {
|
|
62
|
+
this.children = params.children.map(child => CMenuItem.parse(child));
|
|
63
|
+
this.isOpen = ref(false); // 初始化折疊狀態
|
|
64
|
+
}
|
|
65
|
+
this.isActive = ref(false); // 初始化選中狀態
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 判斷是否有子選單項目
|
|
70
|
+
* @returns {boolean} 如果有子選單項目則返回 true,否則返回 false
|
|
71
|
+
*/
|
|
72
|
+
hasChildren(): boolean {
|
|
73
|
+
// 使用 lodash
|
|
74
|
+
return _.isArray(this.children) && !_.isEmpty(this.children);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 使用 collapse 屬性來控制子選單的展開或收起狀態
|
|
79
|
+
* 這個方法可以在 Vue 模板中使用 v-bind 指令來
|
|
80
|
+
* 綁定到 collapse 屬性上,以實現子選單的折
|
|
81
|
+
*/
|
|
82
|
+
useCollapseAttribute(): Record<string, any> {
|
|
83
|
+
if (!this.hasChildren()) return {};
|
|
84
|
+
const collapseId = this.useCollapseId();
|
|
85
|
+
return {
|
|
86
|
+
href: `#${collapseId}`,
|
|
87
|
+
role: 'button',
|
|
88
|
+
'aria-expanded': 'false',
|
|
89
|
+
'aria-controls': collapseId
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 使用 collapseId 方法來生成唯一的折疊 ID
|
|
95
|
+
*/
|
|
96
|
+
useCollapseId(): string {
|
|
97
|
+
return `collapse-sidebar-${this.id}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 使用 useClassName 方法來生成 CSS 類名
|
|
102
|
+
*/
|
|
103
|
+
useClassNameForNavLink(): string {
|
|
104
|
+
let className = 'c-nav-link';
|
|
105
|
+
// 根據 isActive 狀態返回不同的 CSS 類名
|
|
106
|
+
if (this.isActive?.value) {
|
|
107
|
+
className += ' c-active';
|
|
108
|
+
}
|
|
109
|
+
return className;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 使用 useClassName 方法來生成 CSS 類名
|
|
114
|
+
*/
|
|
115
|
+
useClassNameForLi(): string {
|
|
116
|
+
let className = '';
|
|
117
|
+
// 根據 isActive 狀態返回不同的 CSS 類名
|
|
118
|
+
if (this.isActive?.value) {
|
|
119
|
+
className += ' c-active';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 如果有子選單項目,則返回展合狀態的 CSS 類名
|
|
123
|
+
if(this.hasChildren()) {
|
|
124
|
+
className += ' has-children';
|
|
125
|
+
}
|
|
126
|
+
return className;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 計算箭頭圖示的 CSS 類名
|
|
131
|
+
*/
|
|
132
|
+
computeArrowClass(): string {
|
|
133
|
+
// 根據 isOpen 狀態返回不同的箭頭圖示類名
|
|
134
|
+
return this.isOpen?.value ? 'caret-down' : 'caret-left';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 計算折疊顯示的 CSS 類名
|
|
139
|
+
*/
|
|
140
|
+
computeCollapseShowClass(): string{
|
|
141
|
+
return this.isOpen?.value ?'show' : '';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 計算折疊的 CSS 類名
|
|
146
|
+
* @returns {string} 返回折疊的 CSS 類名
|
|
147
|
+
*/
|
|
148
|
+
collapseClassName(): string {
|
|
149
|
+
// 根據 isOpen 狀態返回不同的折疊類名
|
|
150
|
+
return this.isOpen?.value ? 'show' : '';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 檢查當前路徑是否與此選單項目的路徑匹配
|
|
155
|
+
* @param currentPath
|
|
156
|
+
*/
|
|
157
|
+
checkCurrentPath(currentPath: string): boolean {
|
|
158
|
+
let matched = false;
|
|
159
|
+
// 檢查當前路徑是否與此選單項目的路徑匹配
|
|
160
|
+
if (this.path === currentPath) {
|
|
161
|
+
writeVueRefValue(this.isActive, true);
|
|
162
|
+
matched = true;
|
|
163
|
+
}
|
|
164
|
+
// 檢查當前路徑是否在 activePaths 列表中,符合規則為前綴匹配
|
|
165
|
+
if (!matched && this.activePaths) {
|
|
166
|
+
writeVueRefValue(this.isActive, _.some(this.activePaths, (p) => currentPath.startsWith(p)))
|
|
167
|
+
matched = this.isActive?.value || false;
|
|
168
|
+
}
|
|
169
|
+
// 如果有子選單,遞迴檢查子選單項目
|
|
170
|
+
if (this.children) {
|
|
171
|
+
for (const child of this.children) {
|
|
172
|
+
if (child.checkCurrentPath(currentPath)) {
|
|
173
|
+
writeVueRefValue(this.isActive, true);
|
|
174
|
+
writeVueRefValue(this.isOpen, true);
|
|
175
|
+
matched = true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if(!matched) {
|
|
180
|
+
writeVueRefValue(this.isActive, false);
|
|
181
|
+
}
|
|
182
|
+
return matched;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 靜態方法用於從 MenuItem 物件解析出 CMenuItem 實例
|
|
187
|
+
* @param item {MenuItem} 要解析的 MenuItem 物件
|
|
188
|
+
* @return {CMenuItem} 返回解析後的 CMenuItem 實例
|
|
189
|
+
*/
|
|
190
|
+
static parse(item: MenuItem): CMenuItem {
|
|
191
|
+
return new CMenuItem({
|
|
192
|
+
...item,
|
|
193
|
+
children: item.children ? item.children.map(CMenuItem.parse) : []
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ~ ----------------------------------------------------------
|
|
199
|
+
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {BaseFormDataModel} from "./BaseFormDataModel";
|
|
2
|
+
import * as yup from "yup";
|
|
3
|
+
import _ from "lodash";
|
|
4
|
+
import {computed, ComputedRef} from "vue";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Email 收件人資料
|
|
8
|
+
*/
|
|
9
|
+
export class EmailRecipientData extends BaseFormDataModel {
|
|
10
|
+
email: string
|
|
11
|
+
type: 'receiver' | 'cc' | 'bcc'
|
|
12
|
+
|
|
13
|
+
constructor(data?: Partial<EmailRecipientData>) {
|
|
14
|
+
super();
|
|
15
|
+
this.email = data?.email || '';
|
|
16
|
+
this.type = data?.type || 'receiver';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ~ ----------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
dataFieldNameList(): string[] {
|
|
22
|
+
return [
|
|
23
|
+
'email',
|
|
24
|
+
'type',
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
initFormSchema() {
|
|
29
|
+
return yup.object({
|
|
30
|
+
email: yup.string().email('收件人 Email 格式不正確').required('收件人 Email 為必填項'),
|
|
31
|
+
type: yup.string().oneOf(['receiver', 'cc', 'bcc'], '收件人類型不正確').required('收件人類型為必填項'),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Email 收件人資料模型
|
|
39
|
+
*
|
|
40
|
+
*/
|
|
41
|
+
export class EmailReceiverDataModel extends BaseFormDataModel {
|
|
42
|
+
|
|
43
|
+
recipientList: Array<EmailRecipientData> = []; // 收件人 Email 清單
|
|
44
|
+
|
|
45
|
+
// ~ ----------------------------------------------------------
|
|
46
|
+
// ~ constructor
|
|
47
|
+
|
|
48
|
+
constructor(data?: Partial<EmailReceiverDataModel>) {
|
|
49
|
+
super();
|
|
50
|
+
if (data) {
|
|
51
|
+
Object.assign(this, data);
|
|
52
|
+
}
|
|
53
|
+
if(_.isNil(this.recipientList)) {
|
|
54
|
+
this.recipientList = [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ~ ----------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
dataFieldNameList(): string[] {
|
|
61
|
+
return [
|
|
62
|
+
'recipientList',
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
initFormSchema() {
|
|
67
|
+
return yup.object({
|
|
68
|
+
recipientList: yup.array().of(
|
|
69
|
+
yup.object().shape({
|
|
70
|
+
email: yup.string().email('收件人 Email 格式不正確').required('收件人 Email 為必填項'),
|
|
71
|
+
type: yup.string().oneOf(['receiver', 'cc', 'bcc'], '收件人類型不正確').required('收件人類型為必填項'),
|
|
72
|
+
})
|
|
73
|
+
).min(1, '請至少提供一個收件人 Email')
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
toPayload(key?: string): Record<string, any> {
|
|
79
|
+
return {
|
|
80
|
+
recipientEmailList: _.chain(this.recipientList)
|
|
81
|
+
.map(item => _.trim(item.email))
|
|
82
|
+
.filter(email => !_.isEmpty(email))
|
|
83
|
+
.uniq()
|
|
84
|
+
.value(),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ~ ----------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 收件人列表
|
|
92
|
+
* @return {ComputedRef<Array<EmailRecipientData>>}
|
|
93
|
+
*/
|
|
94
|
+
dataList() : ComputedRef<Array<EmailRecipientData>> {
|
|
95
|
+
return computed(() => {
|
|
96
|
+
const value = _.get(this.formFieldMap, 'recipientList.value.value', []);
|
|
97
|
+
if(_.isArray(value)) {
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
return [];
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 新增收件人
|
|
106
|
+
* @param data
|
|
107
|
+
*/
|
|
108
|
+
add(data?: Partial<EmailRecipientData>) {
|
|
109
|
+
// field 更新
|
|
110
|
+
const field = this.recipientListField;
|
|
111
|
+
if(!field) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const newItem = new EmailRecipientData(data);
|
|
115
|
+
_.invoke((field.value.value as Array<EmailRecipientData>), 'push', newItem);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 刪除收件人
|
|
120
|
+
* @param index
|
|
121
|
+
*/
|
|
122
|
+
remove(index: number) {
|
|
123
|
+
// field 更新
|
|
124
|
+
const field = this.recipientListField;
|
|
125
|
+
if(!field) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if(_.isArray(field.value.value) && index >= 0 && index < field.value.value.length) {
|
|
129
|
+
field.value.value.splice(index, 1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 清空表單
|
|
135
|
+
*/
|
|
136
|
+
clean() {
|
|
137
|
+
// field 清空
|
|
138
|
+
const field = this.recipientListField;
|
|
139
|
+
if(field) {
|
|
140
|
+
field.value.value = [];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ~ ----------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
get recipientListField() {
|
|
147
|
+
return _.get(this.formFieldMap, 'recipientList');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as yup from 'yup'
|
|
2
|
+
import {BaseFormDataModel} from "./BaseFormDataModel";
|
|
3
|
+
|
|
4
|
+
export class EmptyDataModel extends BaseFormDataModel{
|
|
5
|
+
|
|
6
|
+
// 空的數據模型,沒有任何屬性
|
|
7
|
+
|
|
8
|
+
constructor(data?: Partial<EmptyDataModel>) {
|
|
9
|
+
super();
|
|
10
|
+
if (data) {
|
|
11
|
+
Object.assign(this, data);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 初始化表單驗證的 schema
|
|
16
|
+
initFormSchema() {
|
|
17
|
+
return yup.object({});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 取得需要套用 form field 欄位名稱的列表
|
|
21
|
+
dataFieldNameList(): string[] {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 表單選項相關的 TypeScript 接口定義
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as _ from 'lodash';
|
|
6
|
+
|
|
7
|
+
// ~ ----------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* COptionItem 接口定義
|
|
11
|
+
*/
|
|
12
|
+
export interface COptionItem {
|
|
13
|
+
id: string; // 可選的唯一識別符
|
|
14
|
+
text: string;
|
|
15
|
+
value: string | number | boolean;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
selected?: boolean; // 是否被選中
|
|
18
|
+
children?: CCOptionItem[]; // 可選的子選項,用於多層級選單
|
|
19
|
+
meta?: Record<string, any>; // 可選的額外元資料
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* COptionItem 類別實現
|
|
24
|
+
*/
|
|
25
|
+
export class CCOptionItem implements COptionItem {
|
|
26
|
+
id: string = '';
|
|
27
|
+
text: string = '';
|
|
28
|
+
value: string | number | boolean = '';
|
|
29
|
+
disabled?: boolean = false;
|
|
30
|
+
selected?: boolean = false;
|
|
31
|
+
children?: CCOptionItem[] = []; // 可選的子選項,用於多層級選單
|
|
32
|
+
meta?: Record<string, any>; // 可選的額外元資料
|
|
33
|
+
|
|
34
|
+
constructor(data: Partial<COptionItem> = {}) {
|
|
35
|
+
Object.assign(this, data);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* DataCategory 資料類別
|
|
41
|
+
*/
|
|
42
|
+
export class DataCategory {
|
|
43
|
+
slug?: string; // 類別標識符
|
|
44
|
+
name?: string; // 類別名稱
|
|
45
|
+
|
|
46
|
+
constructor(data: Partial<DataCategory> = {}) {
|
|
47
|
+
Object.assign(this, data);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ~ ----------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 基本狀態選項
|
|
55
|
+
*/
|
|
56
|
+
export const ToggleStatusOptions: COptionItem[] = [
|
|
57
|
+
{ id: 'enable', text: '開放', value: true },
|
|
58
|
+
{ id: 'disable', text: '關閉', value: false }
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 通用選項,適用當 value 為 boolean 時
|
|
63
|
+
*/
|
|
64
|
+
export const CommonStatusOptions: COptionItem[] = [
|
|
65
|
+
{ id: 'enable', text: '啟用', value: true },
|
|
66
|
+
{ id: 'disable', text: '停用', value: false }
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 通用選項,適用當 value 為字串時
|
|
71
|
+
*/
|
|
72
|
+
export const CommonStatusStrOptions: COptionItem[] = [
|
|
73
|
+
{ id: 'enable', text: '啟用', value: 'ENABLED' },
|
|
74
|
+
{ id: 'disable', text: '停用', value: 'DISABLED' }
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 是/否 選項
|
|
79
|
+
*/
|
|
80
|
+
export const YesNoStatusOptions: COptionItem[] = [
|
|
81
|
+
{ id: 'enable', text: '是', value: true },
|
|
82
|
+
{ id: 'disable', text: '否', value: false }
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
// ~ ----------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
// 工具方法,根據 CommonStatusOptions 產生對應的 狀態標籤 HTML
|
|
88
|
+
export const OptionUtils = {
|
|
89
|
+
/**
|
|
90
|
+
* 根據 CommonStatusOptions 產生對應的 狀態標籤 HTML
|
|
91
|
+
* @param value 狀態值
|
|
92
|
+
*/
|
|
93
|
+
makeCommonStatusLabelText: (value: string | boolean): string => {
|
|
94
|
+
const matchOption = CommonStatusOptions.find(option => option.value === value);
|
|
95
|
+
if(!matchOption) {
|
|
96
|
+
return '';
|
|
97
|
+
}
|
|
98
|
+
switch(matchOption) {
|
|
99
|
+
case CommonStatusOptions[0]: // 啟用
|
|
100
|
+
return `<span class="badge bg-success fs-6 py-2 px-3">啟用</span>`;
|
|
101
|
+
case CommonStatusOptions[1]: // 停用
|
|
102
|
+
return `<span class="badge bg-danger fs-6 py-2 px-3">停用</span>`;
|
|
103
|
+
default:
|
|
104
|
+
return matchOption.text; // 預設返回原始文本
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 防止 Vite/rollup tree-shake 掉 type export
|
|
110
|
+
export const __FormOptionsTypes__ = null as unknown as
|
|
111
|
+
COptionItem &
|
|
112
|
+
DataCategory;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {BaseFormDataModel} from "./BaseFormDataModel";
|
|
2
|
+
import * as yup from "yup";
|
|
3
|
+
import {ObjectSchema} from "yup";
|
|
4
|
+
import _ from "lodash";
|
|
5
|
+
|
|
6
|
+
export class LoginDataModel extends BaseFormDataModel{
|
|
7
|
+
|
|
8
|
+
account?:string; // 帳號
|
|
9
|
+
password?:string; // 密碼
|
|
10
|
+
turnstileToken?:string; // turnstile token
|
|
11
|
+
enableCFTurnstile: boolean = false; // 是否啟用 Cloudflare Turnstile
|
|
12
|
+
|
|
13
|
+
constructor(data?: Partial<LoginDataModel>) {
|
|
14
|
+
super();
|
|
15
|
+
if(data) {
|
|
16
|
+
Object.assign(this, data);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
dataFieldNameList(): string[] {
|
|
21
|
+
return [
|
|
22
|
+
"account",
|
|
23
|
+
"password",
|
|
24
|
+
"codeChallenge",
|
|
25
|
+
"turnstileToken"
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
initFormSchema(): ObjectSchema<any> {
|
|
30
|
+
return yup.object({
|
|
31
|
+
account: yup.string()
|
|
32
|
+
.required("帳號為必填欄位"),
|
|
33
|
+
password: yup.string().required("密碼為必填欄位"),
|
|
34
|
+
codeChallenge: yup.string().nullable().optional(),
|
|
35
|
+
enableCFTurnstile: yup.boolean(),
|
|
36
|
+
turnstileToken: yup.string().when('enableCFTurnstile', {
|
|
37
|
+
is: true,
|
|
38
|
+
then: (schema) => schema.required("請完成驗證"),
|
|
39
|
+
otherwise: (schema) => schema.nullable().optional()
|
|
40
|
+
})
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
formToJsonData(): Record<string, any> {
|
|
45
|
+
return {
|
|
46
|
+
account : _.trim(this.account),
|
|
47
|
+
password : this.password,
|
|
48
|
+
...(this.enableCFTurnstile ? { turnstileToken: this.turnstileToken } : {})
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {BaseFormDataModel} from "./BaseFormDataModel";
|
|
2
|
+
import * as yup from "yup";
|
|
3
|
+
import {ObjectSchema, ObjectShape} from "yup";
|
|
4
|
+
import * as _ from "lodash";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 密碼數據模型
|
|
8
|
+
*/
|
|
9
|
+
export class PasswordDataModel extends BaseFormDataModel {
|
|
10
|
+
oldPassword: string | null = null; // 舊密碼
|
|
11
|
+
newPassword: string | null = null; // 新密碼
|
|
12
|
+
confirmNewPassword: string | null = null; // 確認新密碼
|
|
13
|
+
requiredOldPassword: boolean = true; // 是否需要舊密碼
|
|
14
|
+
|
|
15
|
+
constructor(data?: Partial<PasswordDataModel>) {
|
|
16
|
+
super();
|
|
17
|
+
if (data) {
|
|
18
|
+
Object.assign(this, data);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
dataFieldNameList(): string[] {
|
|
23
|
+
if(!this.requiredOldPassword) {
|
|
24
|
+
return ["newPassword", "confirmNewPassword"];
|
|
25
|
+
}
|
|
26
|
+
return ["oldPassword", "newPassword", "confirmNewPassword"];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
initFormSchema(): ObjectSchema<any> {
|
|
30
|
+
const shema : ObjectShape = {
|
|
31
|
+
newPassword: yup.string()
|
|
32
|
+
.required('新密碼為必填項')
|
|
33
|
+
.max(100, '新密碼長度不可超過100個字')
|
|
34
|
+
.min(12, '新密碼長度至少為12個字符')
|
|
35
|
+
.matches(/^(?=.*[A-Za-z])(?=.*\d)(?=.*[^\w\s]).+$/, '新密碼必須包含英數及符號'),
|
|
36
|
+
confirmNewPassword: yup.string()
|
|
37
|
+
.required('確認新密碼為必填項')
|
|
38
|
+
.max(100, '確認新密碼長度不可超過100個字')
|
|
39
|
+
.oneOf([yup.ref('newPassword')], '新密碼和確認新密碼必須相同')
|
|
40
|
+
};
|
|
41
|
+
if(this.requiredOldPassword) {
|
|
42
|
+
_.set(shema, 'requiredOldPassword', yup.boolean().nullable().optional());
|
|
43
|
+
_.set(shema, 'oldPassword', yup.string()
|
|
44
|
+
.when('requiredOldPassword', {
|
|
45
|
+
is: true,
|
|
46
|
+
then: s => s.required('舊密碼為必填項')
|
|
47
|
+
.max(100, '舊密碼長度不可超過100個字')
|
|
48
|
+
.min(12, '舊密碼長度至少為12個字符')
|
|
49
|
+
.matches(/^(?=.*[A-Za-z])(?=.*\d)(?=.*[^\w\s]).+$/, '舊密碼必須包含英數及符號'),
|
|
50
|
+
otherwise: s => s.notRequired()
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return yup.object(shema);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
toPayload(): Record<string, any> {
|
|
58
|
+
if(this.requiredOldPassword) {
|
|
59
|
+
return {
|
|
60
|
+
oldPassword: this.oldPassword,
|
|
61
|
+
newPassword: this.newPassword,
|
|
62
|
+
newPasswordConfirm: this.confirmNewPassword
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
newPassword: this.newPassword,
|
|
67
|
+
newPasswordConfirm: this.confirmNewPassword
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|