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,67 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useRadioFormField } from '../../composables/useRadioFormField'
|
|
3
|
+
import { COptionItem } from '../../model/FormOptions'
|
|
4
|
+
import { IBSFieldStyleConfig } from '../../model/BSFieldStyleConfig'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
label?: string
|
|
8
|
+
required?: boolean
|
|
9
|
+
name: string
|
|
10
|
+
optionList: COptionItem[]
|
|
11
|
+
readMode?: boolean
|
|
12
|
+
styleConfig?: IBSFieldStyleConfig
|
|
13
|
+
}>()
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
useOptionList,
|
|
17
|
+
bStyleConfig,
|
|
18
|
+
fieldModel,
|
|
19
|
+
fieldRef,
|
|
20
|
+
isRequired,
|
|
21
|
+
isReadMode,
|
|
22
|
+
selectedText,
|
|
23
|
+
hasFormFieldError
|
|
24
|
+
} = useRadioFormField(props)
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<div :class="bStyleConfig.containerClass">
|
|
29
|
+
<label v-if="props.label && !bStyleConfig.hideLabel" :class="bStyleConfig.labelClass">
|
|
30
|
+
{{props.label}}
|
|
31
|
+
<span v-if="isRequired" :class="bStyleConfig.requiredLabelClass">{{bStyleConfig.requiredLabelText}}</span>
|
|
32
|
+
</label>
|
|
33
|
+
<template v-if="isReadMode">
|
|
34
|
+
<div>
|
|
35
|
+
<span :class="bStyleConfig.plainTextClass">
|
|
36
|
+
{{ selectedText }}
|
|
37
|
+
</span>
|
|
38
|
+
<input type="hidden" :name="props.name" v-model="fieldRef"/>
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
41
|
+
<template v-else>
|
|
42
|
+
<div>
|
|
43
|
+
<div class="c-platradio-wrapper" :class="bStyleConfig.radioWrapperClass"
|
|
44
|
+
v-for="optionItem in useOptionList" :key="optionItem.id">
|
|
45
|
+
<label :class="bStyleConfig.radioLabelClass" :for="`radio-${props.name}-${optionItem.id}`">
|
|
46
|
+
<input type="radio" :class="bStyleConfig.radioClass"
|
|
47
|
+
:id="`radio-${props.name}-${optionItem.id}`"
|
|
48
|
+
:name="props.name" :value="optionItem.value"
|
|
49
|
+
v-model="fieldRef"
|
|
50
|
+
v-form-invalid="fieldModel" />
|
|
51
|
+
{{optionItem.text}}
|
|
52
|
+
<i :class="bStyleConfig.radioHelperIconClass"></i>
|
|
53
|
+
</label>
|
|
54
|
+
</div>
|
|
55
|
+
<div v-if="hasFormFieldError(fieldModel)" :class="bStyleConfig.errorClass">
|
|
56
|
+
{{ fieldModel.errorMessage }}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
60
|
+
</div>
|
|
61
|
+
</template>
|
|
62
|
+
|
|
63
|
+
<style scoped>
|
|
64
|
+
div.c-platradio-wrapper label {
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
}
|
|
67
|
+
</style>
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {computed, inject, onMounted, ref, Ref, watch, watchEffect} from 'vue';
|
|
3
|
+
import {BaseFormDataModel, hasFormFieldError} from "../../model/BaseFormDataModel";
|
|
4
|
+
import {COptionItem} from "../../model/FormOptions";
|
|
5
|
+
import {v4 as uuidv4} from 'uuid';
|
|
6
|
+
import _ from "lodash";
|
|
7
|
+
import {EmptyDataModel} from "../../model/EmptyDataModel";
|
|
8
|
+
import {BSFieldStyleConfig, IBSFieldStyleConfig} from "../../model/BSFieldStyleConfig";
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(defineProps<{
|
|
11
|
+
id?: string; // 欄位 ID
|
|
12
|
+
label?: string; // 欄位標籤
|
|
13
|
+
required?: boolean; // 是否為必填
|
|
14
|
+
requiredReactive?: boolean; // 是否為必填 (響應式)
|
|
15
|
+
placeholder?: string; // 輸入提示文字
|
|
16
|
+
name: string; // 欄位名稱
|
|
17
|
+
optionList?: COptionItem[] | Promise<COptionItem[]>; // 選項列表
|
|
18
|
+
readMode?: boolean; // 是否為閱讀模式,只會顯示文字
|
|
19
|
+
dependentField?: string; // 連動欄位名稱
|
|
20
|
+
fetchOptions?: (linkedValue: any) => Promise<COptionItem[]>; // 取得動態選項的函式
|
|
21
|
+
styleConfig?: IBSFieldStyleConfig; // BS 欄位樣式設定
|
|
22
|
+
}>(), {
|
|
23
|
+
placeholder: '請選擇項目',
|
|
24
|
+
requiredReactive: undefined
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// ----- view model -----
|
|
28
|
+
const viewModel = inject('c-formViewModel', new EmptyDataModel()) as BaseFormDataModel;
|
|
29
|
+
if (!viewModel) {
|
|
30
|
+
console.error('CSelectFormField: viewModel not found, please provide "c-formViewModel" in parent component');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// BSFieldStyleConfig
|
|
34
|
+
const bStyleConfig = BSFieldStyleConfig.mix({
|
|
35
|
+
plainTextClass: 'form-control-plaintext bg-light border rounded px-3 d-flex align-items-center'
|
|
36
|
+
}, props.styleConfig);
|
|
37
|
+
|
|
38
|
+
// field model
|
|
39
|
+
const formFieldMap = viewModel.formFieldMap;
|
|
40
|
+
const fieldModel = _.get(formFieldMap, props.name);
|
|
41
|
+
const fieldRef = fieldModel?.value as Ref<string | null>;
|
|
42
|
+
|
|
43
|
+
// ----- 與 option list 相關 -----
|
|
44
|
+
const optionListRef = ref<COptionItem[]>([]);
|
|
45
|
+
|
|
46
|
+
// 處理 optionList (支援 Promise)
|
|
47
|
+
const initOptionList = async () => {
|
|
48
|
+
if (!props.optionList) {
|
|
49
|
+
optionListRef.value = [];
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 檢查是否為 Promise
|
|
54
|
+
if (props.optionList instanceof Promise) {
|
|
55
|
+
optionListRef.value = await props.optionList;
|
|
56
|
+
} else {
|
|
57
|
+
optionListRef.value = props.optionList;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// 監聽 optionList 變化
|
|
62
|
+
watch(() => props.optionList, async () => {
|
|
63
|
+
await initOptionList();
|
|
64
|
+
}, { immediate: true });
|
|
65
|
+
|
|
66
|
+
// 監聽連動欄位變化,更新選項
|
|
67
|
+
initForDependentFieldIfNeed();
|
|
68
|
+
|
|
69
|
+
// ----- 控制項 attribute -----
|
|
70
|
+
const componentId = props.id || 'selectPicker-' + uuidv4();
|
|
71
|
+
const isRequired = computed(() => {
|
|
72
|
+
if (typeof props.requiredReactive === 'boolean') {
|
|
73
|
+
return props.requiredReactive;
|
|
74
|
+
}
|
|
75
|
+
return props.required || viewModel.fieldIsRequired(props.name);
|
|
76
|
+
});
|
|
77
|
+
const isReadMode = props.readMode == true || false;
|
|
78
|
+
const displayText = computed(() => {
|
|
79
|
+
return optionListRef.value.filter((item: COptionItem) => item.value === fieldRef.value)[0]?.text || '';
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// watch(() => fieldRef.value, () => {
|
|
83
|
+
// console.log(`CSelectFormField: [${props.name}] fieldRef changed`, fieldRef.value, optionListRef.value);
|
|
84
|
+
// });
|
|
85
|
+
|
|
86
|
+
// 自動追蹤 fieldRef 和 optionListRef 的變化,確保值和選項同步
|
|
87
|
+
watchEffect(() => {
|
|
88
|
+
// 自動追蹤 fieldRef 和 optionListRef 的變化
|
|
89
|
+
const value = fieldRef.value;
|
|
90
|
+
const options = optionListRef.value;
|
|
91
|
+
// 確保值和選項同步
|
|
92
|
+
if (value && options.length > 0) {
|
|
93
|
+
const found = options.find(item => item.value === value);
|
|
94
|
+
if (!found) {
|
|
95
|
+
console.warn(`Value "${value}" not found in options`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}, {flush: 'post'});
|
|
99
|
+
|
|
100
|
+
// ----- LifeCycle -----
|
|
101
|
+
onMounted(() => {
|
|
102
|
+
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ----- private functions -----
|
|
106
|
+
function initForDependentFieldIfNeed() {
|
|
107
|
+
// 如果有設定 dependentField 和 fetchOptions,則監聽連動欄位變化
|
|
108
|
+
if(!props.dependentField || !props.fetchOptions || !_.isFunction(props.fetchOptions)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const dependentFieldModel = formFieldMap[props.dependentField];
|
|
112
|
+
if(!dependentFieldModel) {
|
|
113
|
+
console.warn(`CSelectFormField: dependentField "${props.dependentField}" not found in formFieldMap`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 監聽連動欄位變化
|
|
118
|
+
watch(dependentFieldModel.value, async (newVal, oldVal) => {
|
|
119
|
+
// 取得新的選項列表
|
|
120
|
+
const newOptions = await props.fetchOptions?.(newVal);
|
|
121
|
+
optionListRef.value = newOptions || [];
|
|
122
|
+
|
|
123
|
+
// 決定是否要清空目前的欄位值 (非第一次呼叫且值有變更時)
|
|
124
|
+
const isFirstCall = oldVal === undefined;
|
|
125
|
+
if(!isFirstCall && oldVal !== newVal) {
|
|
126
|
+
// 判斷目前的值是否在新的選項中
|
|
127
|
+
const found = newOptions?.find(item => item.value === fieldRef.value);
|
|
128
|
+
if(!found) {
|
|
129
|
+
fieldRef.value = null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}, {immediate: true});
|
|
133
|
+
}
|
|
134
|
+
</script>
|
|
135
|
+
|
|
136
|
+
<template>
|
|
137
|
+
<div :class="bStyleConfig.containerClass">
|
|
138
|
+
<label v-if="props.label && !bStyleConfig.hideLabel" :for="componentId" :class="bStyleConfig.labelClass">
|
|
139
|
+
{{ props.label }}
|
|
140
|
+
<span v-if="isRequired" :class="bStyleConfig.requiredLabelClass">{{bStyleConfig.requiredLabelText}}</span>
|
|
141
|
+
</label>
|
|
142
|
+
<template v-if="isReadMode">
|
|
143
|
+
<div :id="componentId">
|
|
144
|
+
<span :class="bStyleConfig.plainTextClass">
|
|
145
|
+
{{ displayText }}
|
|
146
|
+
</span>
|
|
147
|
+
</div>
|
|
148
|
+
<input type="hidden" :name="props.name" v-model="fieldRef"/>
|
|
149
|
+
</template>
|
|
150
|
+
<template v-else>
|
|
151
|
+
<select :id="componentId" :class="bStyleConfig.selectClass"
|
|
152
|
+
:name="props.name"
|
|
153
|
+
:required="isRequired"
|
|
154
|
+
:aria-label="props.placeholder"
|
|
155
|
+
:multiple="bStyleConfig.selectMultiple"
|
|
156
|
+
v-model="fieldRef"
|
|
157
|
+
v-form-invalid="fieldModel">
|
|
158
|
+
<option selected :value="null">{{ bStyleConfig.selectPlaceholder || props.placeholder }}</option>
|
|
159
|
+
<template v-for="optionItem in optionListRef" :key="optionItem.id" >
|
|
160
|
+
<option :value="optionItem.value"
|
|
161
|
+
:disabled="optionItem.disabled">
|
|
162
|
+
{{ optionItem.text }}
|
|
163
|
+
</option>
|
|
164
|
+
</template>
|
|
165
|
+
</select>
|
|
166
|
+
<div v-if="hasFormFieldError(fieldModel)" :class="bStyleConfig.errorClass">
|
|
167
|
+
{{ fieldModel.errorMessage }}
|
|
168
|
+
</div>
|
|
169
|
+
</template>
|
|
170
|
+
</div>
|
|
171
|
+
</template>
|
|
172
|
+
|
|
173
|
+
<style scoped>
|
|
174
|
+
|
|
175
|
+
</style>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
|
|
3
|
+
import {inject, Ref, ref, ComputedRef} from 'vue';
|
|
4
|
+
import {v4 as uuidv4} from 'uuid';
|
|
5
|
+
import {BaseFormDataModel, hasFormFieldError} from "../../model/BaseFormDataModel";
|
|
6
|
+
import * as _ from 'lodash';
|
|
7
|
+
import {EmptyDataModel} from "../../model/EmptyDataModel";
|
|
8
|
+
import {BSFieldStyleConfig, IBSFieldStyleConfig} from "../../model/BSFieldStyleConfig";
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{
|
|
11
|
+
id?: string; // 欄位 ID
|
|
12
|
+
label?: string; // 欄位標籤
|
|
13
|
+
name: string; // 欄位名稱
|
|
14
|
+
required?: boolean; // 是否為必填
|
|
15
|
+
placeholder?: string; // 輸入提示文字
|
|
16
|
+
rows?: number; // 行數,預設為 3
|
|
17
|
+
maxlength?: number; // 最大長度
|
|
18
|
+
disabledState?: ComputedRef<boolean>; // 禁用狀態的引用
|
|
19
|
+
readMode?: boolean; // 是否為閱讀模式,只會顯示文字
|
|
20
|
+
styleConfig?: IBSFieldStyleConfig; // BS 欄位樣式設定
|
|
21
|
+
}>();
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
// ----- view model -----
|
|
25
|
+
const viewModel = inject('c-formViewModel', new EmptyDataModel()) as BaseFormDataModel;
|
|
26
|
+
if (!viewModel) {
|
|
27
|
+
console.error('CTextAreaFormField: viewModel not found, please provide "c-formViewModel" in parent component');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// BSFieldStyleConfig
|
|
31
|
+
const bStyleConfig = BSFieldStyleConfig.mix({
|
|
32
|
+
plainTextClass: 'form-control disabled c-textarea-readonly'
|
|
33
|
+
}, props.styleConfig);
|
|
34
|
+
|
|
35
|
+
// field model
|
|
36
|
+
const formFieldMap = viewModel.formFieldMap;
|
|
37
|
+
const fieldModel = _.get(formFieldMap, props.name);
|
|
38
|
+
const fieldRef = fieldModel?.value as Ref<string>;
|
|
39
|
+
|
|
40
|
+
// ----- 控制項 attribute -----
|
|
41
|
+
const componentId = props.id || 'textarea-' + uuidv4();
|
|
42
|
+
const isRequired = props.required || viewModel.fieldIsRequired(props.name);
|
|
43
|
+
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<template>
|
|
47
|
+
<div :class="bStyleConfig.containerClass">
|
|
48
|
+
<label v-if="props.label && !bStyleConfig.hideLabel" :for="componentId" :class="bStyleConfig.labelClass">
|
|
49
|
+
{{ props.label }}
|
|
50
|
+
<span v-if="isRequired" :class="bStyleConfig.requiredLabelClass">{{bStyleConfig.requiredLabelText}}</span>
|
|
51
|
+
</label>
|
|
52
|
+
<template v-if="props.readMode">
|
|
53
|
+
<div :class="bStyleConfig.plainTextClass" :id="componentId">
|
|
54
|
+
{{ fieldRef }}
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
<template v-else>
|
|
58
|
+
<textarea :class="bStyleConfig.textareaClass"
|
|
59
|
+
:id="componentId"
|
|
60
|
+
:name="props.name"
|
|
61
|
+
:required="isRequired"
|
|
62
|
+
:rows="props.rows || bStyleConfig.textareaRows || 3"
|
|
63
|
+
:maxlength="props.maxlength"
|
|
64
|
+
:style="bStyleConfig.textareaResizeStyle"
|
|
65
|
+
:placeholder="props.placeholder"
|
|
66
|
+
:disabled="props.disabledState?.value || false"
|
|
67
|
+
v-model="fieldRef"
|
|
68
|
+
v-form-invalid="fieldModel">
|
|
69
|
+
</textarea>
|
|
70
|
+
<div v-if="hasFormFieldError(fieldModel)" :class="bStyleConfig.errorClass">
|
|
71
|
+
{{ fieldModel.errorMessage }}
|
|
72
|
+
</div>
|
|
73
|
+
</template>
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
77
|
+
<style scoped>
|
|
78
|
+
.c-textarea-readonly {
|
|
79
|
+
background-color: #e9ecef;
|
|
80
|
+
opacity: 1;
|
|
81
|
+
min-height: 90px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
</style>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { InputHTMLAttributes } from '@vue/runtime-dom';
|
|
3
|
+
import {inject, Ref, ref, ComputedRef, computed} from 'vue';
|
|
4
|
+
import {v4 as uuidv4} from 'uuid';
|
|
5
|
+
import {BaseFormDataModel, hasFormFieldError} from "../../model/BaseFormDataModel";
|
|
6
|
+
import _ from "lodash";
|
|
7
|
+
import {EmptyDataModel} from "../../model/EmptyDataModel";
|
|
8
|
+
import {BSFieldStyleConfig, IBSFieldStyleConfig} from "../../model/BSFieldStyleConfig";
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(defineProps<Partial< /* @vue-ignore */ InputHTMLAttributes> & {
|
|
11
|
+
id?: string; // 欄位 ID
|
|
12
|
+
label?: string; // 欄位標籤
|
|
13
|
+
name: string; // 欄位名稱
|
|
14
|
+
type?: string; // 欄位類型,預設為 'text'
|
|
15
|
+
required?: boolean; // 是否為必填
|
|
16
|
+
requiredReactive?: boolean; // 是否為必填 (響應式)
|
|
17
|
+
placeholder?: string; // 輸入提示文字
|
|
18
|
+
autocomplete?: string; // 自動完成提示 預設為 'off'
|
|
19
|
+
disabledState?: ComputedRef<boolean>; // 禁用狀態的引用
|
|
20
|
+
readMode?: boolean; // 是否為閱讀模式,只會顯示文字
|
|
21
|
+
min?: number;// min attribute for a number type
|
|
22
|
+
max?: number;// max attribute for a number type
|
|
23
|
+
styleConfig?: IBSFieldStyleConfig; // BS 欄位樣式設定
|
|
24
|
+
}>(), {
|
|
25
|
+
requiredReactive: undefined
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
// ----- view model -----
|
|
30
|
+
const viewModel = inject('c-formViewModel', new EmptyDataModel()) as BaseFormDataModel;
|
|
31
|
+
if(!viewModel) {
|
|
32
|
+
console.error('CTextInputFormField: viewModel not found, please provide "c-formViewModel" in parent component');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// BSFieldStyleConfig
|
|
36
|
+
const bStyleConfig = BSFieldStyleConfig.mix({
|
|
37
|
+
plainTextClass: 'form-control-plaintext bg-light border rounded px-3 d-flex align-items-center'
|
|
38
|
+
}, props.styleConfig);
|
|
39
|
+
|
|
40
|
+
// field model
|
|
41
|
+
const formFieldMap = viewModel.formFieldMap;
|
|
42
|
+
const fieldModel = _.get(formFieldMap, props.name);
|
|
43
|
+
const fieldRef = fieldModel?.value as Ref<string>;
|
|
44
|
+
|
|
45
|
+
// ----- 控制項 attribute -----
|
|
46
|
+
const componentId = props.id || 'textInput-' + uuidv4();
|
|
47
|
+
// 使用 computed 讓 isRequired 具備響應式,根據 props.required 與 yup schema 動態判斷
|
|
48
|
+
const isRequired = computed(() => {
|
|
49
|
+
// 若父元件有傳 requiredReactive,優先採用
|
|
50
|
+
if (typeof props.requiredReactive === 'boolean') {
|
|
51
|
+
return props.requiredReactive;
|
|
52
|
+
}
|
|
53
|
+
// 直接使用 props.required || viewModel.fieldIsRequired(props.name)
|
|
54
|
+
return props.required || viewModel.fieldIsRequired(props.name);
|
|
55
|
+
});
|
|
56
|
+
const isReadMode = props.readMode == true || false;
|
|
57
|
+
const maxLengthAttr = viewModel.fieldMaxLength(props.name) || undefined;
|
|
58
|
+
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<template>
|
|
62
|
+
|
|
63
|
+
<div :class="bStyleConfig.containerClass">
|
|
64
|
+
<label v-if="props.label && !bStyleConfig.hideLabel" :for="componentId" :class="bStyleConfig.labelClass">
|
|
65
|
+
{{props.label}}
|
|
66
|
+
<span v-if="isRequired" :class="bStyleConfig.requiredLabelClass">{{bStyleConfig.requiredLabelText}}</span>
|
|
67
|
+
</label>
|
|
68
|
+
<template v-if="isReadMode">
|
|
69
|
+
<div :id="componentId">
|
|
70
|
+
<span :class="bStyleConfig.plainTextClass">
|
|
71
|
+
{{ fieldRef }}
|
|
72
|
+
</span>
|
|
73
|
+
</div>
|
|
74
|
+
<input type="hidden" :name="props.name" v-model="fieldRef"/>
|
|
75
|
+
</template>
|
|
76
|
+
<template v-else>
|
|
77
|
+
<input :class="bStyleConfig.inputClass"
|
|
78
|
+
:id="componentId"
|
|
79
|
+
:name="props.name"
|
|
80
|
+
:type="props.type || 'text'"
|
|
81
|
+
:min="props.min"
|
|
82
|
+
:max="props.max"
|
|
83
|
+
:placeholder="props.placeholder || ''"
|
|
84
|
+
:autocomplete="props.autocomplete"
|
|
85
|
+
:required="isRequired"
|
|
86
|
+
:disabled="props.disabledState?.value || false"
|
|
87
|
+
:maxlength="maxLengthAttr"
|
|
88
|
+
v-model="fieldRef"
|
|
89
|
+
v-form-invalid="fieldModel"/>
|
|
90
|
+
<div v-if="hasFormFieldError(fieldModel)" :class="bStyleConfig.errorClass">
|
|
91
|
+
{{ fieldModel.errorMessage }}
|
|
92
|
+
</div>
|
|
93
|
+
</template>
|
|
94
|
+
</div>
|
|
95
|
+
</template>
|
|
96
|
+
|
|
97
|
+
<style scoped>
|
|
98
|
+
|
|
99
|
+
</style>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { inject, Ref, computed, ref } from 'vue';
|
|
3
|
+
import {BaseFormDataModel, hasFormFieldError} from "../../model/BaseFormDataModel";
|
|
4
|
+
import {v4 as uuidv4} from 'uuid';
|
|
5
|
+
import _ from 'lodash';
|
|
6
|
+
import Editor from '@tinymce/tinymce-vue';
|
|
7
|
+
import {EmptyDataModel} from "../../model/EmptyDataModel";
|
|
8
|
+
import {BSFieldStyleConfig, IBSFieldStyleConfig} from "../../model/BSFieldStyleConfig";
|
|
9
|
+
|
|
10
|
+
// ----- props -----
|
|
11
|
+
const props = withDefaults(defineProps<{
|
|
12
|
+
label?: string; // 欄位標籤
|
|
13
|
+
name: string; // 欄位 data name
|
|
14
|
+
required?: boolean // 是否為必填
|
|
15
|
+
requiredReactive?: boolean // 是否為必填 (響應式)
|
|
16
|
+
styleConfig?: IBSFieldStyleConfig; // BS 欄位樣式設定
|
|
17
|
+
}>(), {
|
|
18
|
+
requiredReactive: undefined
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// ----- view model -----
|
|
22
|
+
const viewModel = inject('c-formViewModel', new EmptyDataModel()) as BaseFormDataModel;
|
|
23
|
+
if(!viewModel) {
|
|
24
|
+
console.error('CTextInputFormField: viewModel not found, please provide "c-formViewModel" in parent component');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// BSFieldStyleConfig
|
|
28
|
+
const bStyleConfig = BSFieldStyleConfig.mix({}, props.styleConfig);
|
|
29
|
+
const formFieldMap = viewModel.formFieldMap;
|
|
30
|
+
const fieldModel = _.get(formFieldMap, props.name);
|
|
31
|
+
const fieldRef = fieldModel?.value as Ref<string>;
|
|
32
|
+
|
|
33
|
+
// ----- attribute -----
|
|
34
|
+
const componentId = `tinymceEditor-${uuidv4()}`; // 生成唯一的 ID
|
|
35
|
+
const isRequired = computed(() => {
|
|
36
|
+
if (typeof props.requiredReactive === 'boolean') {
|
|
37
|
+
return props.requiredReactive;
|
|
38
|
+
}
|
|
39
|
+
return props.required || viewModel.fieldIsRequired(props.name);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ----- TinyMCE Editor Config -----
|
|
43
|
+
const apiKey = import.meta.env.VITE_TINYMCE_API_KEY;
|
|
44
|
+
const editorBoxElt = ref<HTMLDivElement | null>(null);
|
|
45
|
+
const editorRef = ref<InstanceType<typeof Editor> | null>(null);
|
|
46
|
+
const tinymceOptions = ref({
|
|
47
|
+
height: 500,
|
|
48
|
+
menubar: false,
|
|
49
|
+
language: 'zh-TW', // 設定語言
|
|
50
|
+
plugins: [
|
|
51
|
+
'code'
|
|
52
|
+
],
|
|
53
|
+
toolbar: 'undo redo | code'
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ----- event handler -----
|
|
57
|
+
// 當編輯器初始化完成時觸發
|
|
58
|
+
function onEditorInit(editor: InstanceType<typeof Editor>) {
|
|
59
|
+
if(!editorBoxElt.value) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// 移除隱藏類別
|
|
63
|
+
editorBoxElt.value.classList.remove('d-none');
|
|
64
|
+
}
|
|
65
|
+
// 注目編輯器
|
|
66
|
+
function doFocusEditor() {
|
|
67
|
+
const editorInstance = _.invoke(editorRef.value, 'getEditor');
|
|
68
|
+
editorInstance?.focus();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<template>
|
|
74
|
+
<div :class="bStyleConfig.containerClass">
|
|
75
|
+
<label v-if="props.label && !bStyleConfig.hideLabel" :for="componentId" :class="bStyleConfig.labelClass" @click="doFocusEditor">
|
|
76
|
+
{{props.label}}
|
|
77
|
+
<span v-if="isRequired" :class="bStyleConfig.requiredLabelClass">{{bStyleConfig.requiredLabelText}}</span>
|
|
78
|
+
</label>
|
|
79
|
+
<div class="c-tinymce-editor-box d-none" ref="editorBoxElt" v-form-invalid="fieldModel">
|
|
80
|
+
<editor
|
|
81
|
+
:id="componentId" ref="editorRef"
|
|
82
|
+
:api-key="apiKey"
|
|
83
|
+
v-model="fieldRef"
|
|
84
|
+
:init="tinymceOptions"
|
|
85
|
+
@init="onEditorInit"
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
<div v-if="hasFormFieldError(fieldModel)" :class="bStyleConfig.errorClass">
|
|
89
|
+
{{ fieldModel.errorMessage }}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</template>
|
|
93
|
+
|
|
94
|
+
<style scoped>
|
|
95
|
+
.c-tinymce-editor-box.is-invalid {
|
|
96
|
+
border: 1px solid red;
|
|
97
|
+
border-radius: 10px;
|
|
98
|
+
}
|
|
99
|
+
</style>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { InputHTMLAttributes } from '@vue/runtime-dom';
|
|
3
|
+
import {inject, Ref, ref, ComputedRef, computed} from 'vue';
|
|
4
|
+
import {v4 as uuidv4} from 'uuid';
|
|
5
|
+
import {BaseFormDataModel} from "../../model/BaseFormDataModel";
|
|
6
|
+
import _ from "lodash";
|
|
7
|
+
import {EmptyDataModel} from "../../model/EmptyDataModel";
|
|
8
|
+
|
|
9
|
+
const props = defineProps<Partial< /* @vue-ignore */ InputHTMLAttributes> & {
|
|
10
|
+
id?: string; // 欄位 ID
|
|
11
|
+
label?: string; // 欄位標籤
|
|
12
|
+
name: string; // 欄位名稱
|
|
13
|
+
type?: string; // 欄位類型,預設為 'text'
|
|
14
|
+
required?: boolean; // 是否為必填
|
|
15
|
+
requiredReactive?: boolean; // 是否為必填 (響應式)
|
|
16
|
+
placeholder?: string; // 輸入提示文字
|
|
17
|
+
autocomplete?: string; // 自動完成提示 預設為 'off'
|
|
18
|
+
disabledState?: ComputedRef<boolean>; // 禁用狀態的引用
|
|
19
|
+
readMode?: boolean; // 是否為閱讀模式,只會顯示文字
|
|
20
|
+
min?: number;// min attribute for number type
|
|
21
|
+
max?: number;// max attribute for number type
|
|
22
|
+
}>();
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
// ----- view model -----
|
|
26
|
+
const viewModel = inject('c-formViewModel', new EmptyDataModel()) as BaseFormDataModel;
|
|
27
|
+
if(!viewModel) {
|
|
28
|
+
console.error('CTextInputFormField: viewModel not found, please provide "c-formViewModel" in parent component');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// field model
|
|
32
|
+
const formFieldMap = viewModel.formFieldMap;
|
|
33
|
+
const fieldModel = _.get(formFieldMap, props.name);
|
|
34
|
+
const fieldRef = fieldModel?.value as Ref<string>;
|
|
35
|
+
|
|
36
|
+
// ----- 控制項 attribute -----
|
|
37
|
+
const componentId = props.id || 'textInput-' + uuidv4();
|
|
38
|
+
const isRequired = computed(() => {
|
|
39
|
+
if (typeof props.requiredReactive === 'boolean') {
|
|
40
|
+
return props.requiredReactive;
|
|
41
|
+
}
|
|
42
|
+
return props.required || viewModel.fieldIsRequired(props.name);
|
|
43
|
+
});
|
|
44
|
+
const isReadMode = props.readMode || false;
|
|
45
|
+
const maxLengthAttr = viewModel.fieldMaxLength(props.name) || undefined;
|
|
46
|
+
|
|
47
|
+
// 預設的 input attributes,方便使用者用 v-bind 一次綁定
|
|
48
|
+
const inputAttrs = computed(() => ({
|
|
49
|
+
id: componentId,
|
|
50
|
+
name: props.name,
|
|
51
|
+
type: props.type || 'text',
|
|
52
|
+
min: props.min,
|
|
53
|
+
max: props.max,
|
|
54
|
+
placeholder: props.placeholder || '',
|
|
55
|
+
autocomplete: props.autocomplete,
|
|
56
|
+
required: isRequired.value,
|
|
57
|
+
disabled: props.disabledState?.value || false,
|
|
58
|
+
maxlength: maxLengthAttr,
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
// 這邊先開出去讓父元件可以控制 isRequired 的值
|
|
62
|
+
function setIsRequired(value: boolean): void {
|
|
63
|
+
// 建議由父元件直接控制 props.required
|
|
64
|
+
}
|
|
65
|
+
defineExpose({
|
|
66
|
+
setIsRequired,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<template>
|
|
72
|
+
<slot name="default"
|
|
73
|
+
:componentId="componentId"
|
|
74
|
+
:fieldRef="fieldRef"
|
|
75
|
+
:fieldModel="fieldModel"
|
|
76
|
+
:isRequired="isRequired"
|
|
77
|
+
:isReadMode="isReadMode"
|
|
78
|
+
:maxLengthAttr="maxLengthAttr">
|
|
79
|
+
<!-- 預設實作 -->
|
|
80
|
+
<div>
|
|
81
|
+
<slot name="label"
|
|
82
|
+
:componentId="componentId"
|
|
83
|
+
:label="props.label"
|
|
84
|
+
:isRequired="isRequired">
|
|
85
|
+
<label v-if="props.label" :for="componentId" class="form-label">
|
|
86
|
+
{{props.label}}
|
|
87
|
+
<span v-if="isRequired" class="text-danger px-1">*</span>
|
|
88
|
+
</label>
|
|
89
|
+
</slot>
|
|
90
|
+
|
|
91
|
+
<slot name="input"
|
|
92
|
+
:componentId="componentId"
|
|
93
|
+
:fieldRef="fieldRef"
|
|
94
|
+
:fieldModel="fieldModel"
|
|
95
|
+
:isRequired="isRequired"
|
|
96
|
+
:isReadMode="isReadMode"
|
|
97
|
+
:maxLengthAttr="maxLengthAttr"
|
|
98
|
+
:inputAttrs="inputAttrs"
|
|
99
|
+
:props="props">
|
|
100
|
+
<template v-if="isReadMode">
|
|
101
|
+
<div :id="componentId">
|
|
102
|
+
<span class="form-control-plaintext bg-light border rounded px-3 d-flex align-items-center">
|
|
103
|
+
{{ fieldRef }}
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
<input type="hidden" :name="props.name" v-model="fieldRef"/>
|
|
107
|
+
</template>
|
|
108
|
+
<template v-else>
|
|
109
|
+
<input class="form-control"
|
|
110
|
+
v-bind="inputAttrs"
|
|
111
|
+
v-model="fieldRef"
|
|
112
|
+
v-form-invalid="fieldModel"/>
|
|
113
|
+
</template>
|
|
114
|
+
</slot>
|
|
115
|
+
|
|
116
|
+
<slot name="error"
|
|
117
|
+
:fieldModel="fieldModel"
|
|
118
|
+
:errorMessage="fieldModel?.errorMessage">
|
|
119
|
+
<div v-if="fieldModel?.errorMessage" class="invalid-feedback d-block mt-1">
|
|
120
|
+
{{ fieldModel.errorMessage }}
|
|
121
|
+
</div>
|
|
122
|
+
</slot>
|
|
123
|
+
</div>
|
|
124
|
+
</slot>
|
|
125
|
+
</template>
|
|
126
|
+
|
|
127
|
+
<style scoped>
|
|
128
|
+
|
|
129
|
+
</style>
|