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,146 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {onMounted, provide} from 'vue';
|
|
3
|
+
import {CBSModalViewModel, ICBSModalViewType} from "../../model/CBSModalViewModel";
|
|
4
|
+
import {PasswordDataModel} from "../../model/PasswordDataModel";
|
|
5
|
+
import CTextInputFormField from "../../components/form/CTextInputFormField.vue";
|
|
6
|
+
import {ApiResponse} from "../../api/ApiService";
|
|
7
|
+
import {BSFieldStyleConfig, IBSFieldStyleConfig} from "../../model/BSFieldStyleConfig";
|
|
8
|
+
import * as _ from 'lodash';
|
|
9
|
+
|
|
10
|
+
// ----- properties -----
|
|
11
|
+
const props = withDefaults(defineProps<{
|
|
12
|
+
userUid: string; // 使用者的 uid
|
|
13
|
+
requireOldPassword?: boolean; // 是否需要舊密碼欄位
|
|
14
|
+
dataModelConstructor?: new () => PasswordDataModel; // 密碼資料模型建構子
|
|
15
|
+
callApi: (dataModel: PasswordDataModel) => Promise<ApiResponse>; // 儲存密碼時要呼叫的 API 函式
|
|
16
|
+
styleConfig?: IBSFieldStyleConfig; // BS 欄位樣式設定,會傳遞給子組件
|
|
17
|
+
}>(), {
|
|
18
|
+
requireOldPassword: true,
|
|
19
|
+
});
|
|
20
|
+
const isRequireOldPassword = props.requireOldPassword == true;
|
|
21
|
+
|
|
22
|
+
// ----- viewModel -----
|
|
23
|
+
const viewModel = new CBSModalViewModel({
|
|
24
|
+
type: ICBSModalViewType.Form,
|
|
25
|
+
title: '編輯密碼',
|
|
26
|
+
onClose
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// ----- dataViewModel -----
|
|
30
|
+
const useDataModelConstructor = props.dataModelConstructor || PasswordDataModel;
|
|
31
|
+
const dataViewModel = new useDataModelConstructor({
|
|
32
|
+
requiredOldPassword: isRequireOldPassword
|
|
33
|
+
});
|
|
34
|
+
dataViewModel.initForm();
|
|
35
|
+
provide('c-formViewModel', dataViewModel);
|
|
36
|
+
|
|
37
|
+
// BSFieldStyleConfig
|
|
38
|
+
const bStyleConfig = BSFieldStyleConfig.mix({
|
|
39
|
+
plainTextClass: ''
|
|
40
|
+
}, props.styleConfig);
|
|
41
|
+
|
|
42
|
+
// ----- event -----
|
|
43
|
+
// 顯示表單
|
|
44
|
+
function doShowForm() {
|
|
45
|
+
viewModel.show();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function onClose() {
|
|
49
|
+
// dataViewModel reset form data
|
|
50
|
+
dataViewModel.resetFormData();
|
|
51
|
+
viewModel.hide();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// save 密碼
|
|
55
|
+
async function doSave() {
|
|
56
|
+
if(!props.callApi) {
|
|
57
|
+
console.error('請提供 callApi 屬性以處理密碼儲存邏輯');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 驗證表單
|
|
62
|
+
const {valid, errors} = await dataViewModel.validateForm();
|
|
63
|
+
if (!valid) {
|
|
64
|
+
console.error('表單驗證失敗:', errors);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 在這裡處理密碼儲存邏輯
|
|
69
|
+
const response = await props.callApi(dataViewModel);
|
|
70
|
+
if(!response.isOk()) {
|
|
71
|
+
console.error('儲存密碼失敗:', response.nativeError);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
viewModel.hide();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ----- LifeCycle -----
|
|
78
|
+
onMounted(() => {
|
|
79
|
+
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<template>
|
|
85
|
+
<div :class="bStyleConfig.containerClass">
|
|
86
|
+
<label :class="bStyleConfig.checkboxLabelClass">密碼</label>
|
|
87
|
+
<div class="mb-2">
|
|
88
|
+
<button type="button" :class="bStyleConfig.changePasswordButtonClass" @click.prevent.stop="doShowForm">
|
|
89
|
+
<i :class="bStyleConfig.changePasswordButtonIcon"></i>編輯密碼
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<teleport to="body">
|
|
94
|
+
<div :id="viewModel.modalId" class="modal fade c-modal-form c-changePwd-form" tabindex="-1"
|
|
95
|
+
:aria-labelledby="viewModel.labelId"
|
|
96
|
+
aria-hidden="true" v-cbs-modal="viewModel">
|
|
97
|
+
<div class="modal-dialog">
|
|
98
|
+
<div class="modal-content">
|
|
99
|
+
<div class="modal-header">
|
|
100
|
+
<h5 class="modal-title" :id='viewModel.labelId'>{{viewModel.title}}</h5>
|
|
101
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="modal-body">
|
|
104
|
+
<div class="mb-3">
|
|
105
|
+
<form>
|
|
106
|
+
<div v-if="isRequireOldPassword" class="row mb-3">
|
|
107
|
+
<div class="col-md-12">
|
|
108
|
+
<CTextInputFormField label="舊密碼" name="oldPassword"
|
|
109
|
+
type="password"
|
|
110
|
+
autocomplete="off"
|
|
111
|
+
:styleConfig="props.styleConfig"/>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
<div class="row mb-3">
|
|
115
|
+
<div class="col-md-12">
|
|
116
|
+
<CTextInputFormField label="新密碼" name="newPassword"
|
|
117
|
+
type="password"
|
|
118
|
+
autocomplete="new-password"
|
|
119
|
+
:styleConfig="props.styleConfig"/>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="row mb-3">
|
|
123
|
+
<div class="col-md-12">
|
|
124
|
+
<CTextInputFormField label="確認新密碼" name="confirmNewPassword"
|
|
125
|
+
type="password"
|
|
126
|
+
autocomplete="new-password"
|
|
127
|
+
:styleConfig="props.styleConfig"
|
|
128
|
+
@keydown.enter="doSave"/>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</form>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="modal-footer">
|
|
135
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
|
136
|
+
<button type="button" class="btn btn-primary" @click.prevent.stop="doSave">送出</button>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</teleport>
|
|
142
|
+
</template>
|
|
143
|
+
|
|
144
|
+
<style scoped>
|
|
145
|
+
|
|
146
|
+
</style>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useCheckBoxFormField } from '../../composables/useCheckBoxFormField'
|
|
3
|
+
import { COptionItem } from '../../model/FormOptions'
|
|
4
|
+
import { IBSFieldStyleConfig } from '../../model/BSFieldStyleConfig'
|
|
5
|
+
|
|
6
|
+
const props = withDefaults(defineProps<{
|
|
7
|
+
/**
|
|
8
|
+
* Checkbox 欄位的標籤文字
|
|
9
|
+
*/
|
|
10
|
+
label?: string
|
|
11
|
+
/**
|
|
12
|
+
* Checkbox 欄位是否為必填
|
|
13
|
+
*/
|
|
14
|
+
required?: boolean
|
|
15
|
+
/**
|
|
16
|
+
* Checkbox 欄位是否為必填 (響應式)
|
|
17
|
+
*/
|
|
18
|
+
requiredReactive?: boolean
|
|
19
|
+
/**
|
|
20
|
+
* Checkbox 欄位的名稱,為 form field 的 name 屬性
|
|
21
|
+
*/
|
|
22
|
+
name: string
|
|
23
|
+
/**
|
|
24
|
+
* Checkbox 選項集
|
|
25
|
+
*/
|
|
26
|
+
optionList: COptionItem[]
|
|
27
|
+
/**
|
|
28
|
+
* 是否為閱讀模式,若為 true 則只顯示選項文字,不顯示 Checkbox 按鈕
|
|
29
|
+
*/
|
|
30
|
+
readMode?: boolean
|
|
31
|
+
/**
|
|
32
|
+
* BS 欄位樣式設定
|
|
33
|
+
*/
|
|
34
|
+
styleConfig?: IBSFieldStyleConfig
|
|
35
|
+
}>(), {
|
|
36
|
+
requiredReactive: undefined
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const {
|
|
40
|
+
useOptionList,
|
|
41
|
+
viewModel,
|
|
42
|
+
bStyleConfig,
|
|
43
|
+
fieldModel,
|
|
44
|
+
fieldRef,
|
|
45
|
+
isRequired,
|
|
46
|
+
isReadMode,
|
|
47
|
+
selectedText,
|
|
48
|
+
hasFormFieldError
|
|
49
|
+
} = useCheckBoxFormField(props)
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<div :class="bStyleConfig.containerClass">
|
|
54
|
+
<label v-if="props.label && !bStyleConfig.hideLabel" :class="bStyleConfig.labelClass">
|
|
55
|
+
{{props.label}}
|
|
56
|
+
<span v-if="isRequired" :class="bStyleConfig.requiredLabelClass">{{bStyleConfig.requiredLabelText}}</span>
|
|
57
|
+
</label>
|
|
58
|
+
<template v-if="isReadMode">
|
|
59
|
+
<div>
|
|
60
|
+
<span :class="bStyleConfig.plainTextClass">
|
|
61
|
+
{{ selectedText }}
|
|
62
|
+
</span>
|
|
63
|
+
<input type="hidden" :name="props.name" v-model="fieldRef"/>
|
|
64
|
+
</div>
|
|
65
|
+
</template>
|
|
66
|
+
<template v-else>
|
|
67
|
+
<div>
|
|
68
|
+
<div :class="bStyleConfig.checkboxWrapperClass"
|
|
69
|
+
v-for="optionItem in useOptionList" :key="optionItem.id">
|
|
70
|
+
<input type="checkbox" :class="bStyleConfig.checkboxClass"
|
|
71
|
+
:id="optionItem.id"
|
|
72
|
+
:name="props.name"
|
|
73
|
+
:value="optionItem.value"
|
|
74
|
+
v-model="fieldRef"
|
|
75
|
+
v-form-invalid="fieldModel">
|
|
76
|
+
<label :class="bStyleConfig.checkboxLabelClass" :for="optionItem.id">{{optionItem.text}}</label>
|
|
77
|
+
</div>
|
|
78
|
+
<div v-if="hasFormFieldError(fieldModel)" :class="bStyleConfig.errorClass">
|
|
79
|
+
{{ fieldModel.errorMessage }}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</template>
|
|
83
|
+
</div>
|
|
84
|
+
</template>
|
|
85
|
+
|
|
86
|
+
<style scoped>
|
|
87
|
+
.c-multiline-text {
|
|
88
|
+
white-space: pre-wrap;
|
|
89
|
+
word-break: break-word;
|
|
90
|
+
}
|
|
91
|
+
</style>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useCheckBoxFormField } from '../../composables/useCheckBoxFormField'
|
|
3
|
+
import { COptionItem } from '../../model/FormOptions'
|
|
4
|
+
import { IBSFieldStyleConfig } from '../../model/BSFieldStyleConfig'
|
|
5
|
+
|
|
6
|
+
const props = withDefaults(defineProps<{
|
|
7
|
+
/**
|
|
8
|
+
* Checkbox 欄位的標籤文字
|
|
9
|
+
*/
|
|
10
|
+
label?: string
|
|
11
|
+
/**
|
|
12
|
+
* Checkbox 欄位是否為必填
|
|
13
|
+
*/
|
|
14
|
+
required?: boolean
|
|
15
|
+
/**
|
|
16
|
+
* Checkbox 欄位是否為必填 (響應式)
|
|
17
|
+
*/
|
|
18
|
+
requiredReactive?: boolean
|
|
19
|
+
/**
|
|
20
|
+
* Checkbox 欄位的名稱,為 form field 的 name 屬性
|
|
21
|
+
*/
|
|
22
|
+
name: string
|
|
23
|
+
/**
|
|
24
|
+
* Checkbox 選項集
|
|
25
|
+
*/
|
|
26
|
+
optionList: COptionItem[]
|
|
27
|
+
/**
|
|
28
|
+
* 是否為閱讀模式,若為 true 則只顯示選項文字,不顯示 Checkbox 按鈕
|
|
29
|
+
*/
|
|
30
|
+
readMode?: boolean
|
|
31
|
+
/**
|
|
32
|
+
* BS 欄位樣式設定
|
|
33
|
+
*/
|
|
34
|
+
styleConfig?: IBSFieldStyleConfig
|
|
35
|
+
}>(), {
|
|
36
|
+
requiredReactive: undefined
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const {
|
|
40
|
+
useOptionList,
|
|
41
|
+
viewModel,
|
|
42
|
+
bStyleConfig,
|
|
43
|
+
fieldModel,
|
|
44
|
+
fieldRef,
|
|
45
|
+
isRequired,
|
|
46
|
+
isReadMode,
|
|
47
|
+
selectedText,
|
|
48
|
+
hasFormFieldError
|
|
49
|
+
} = useCheckBoxFormField(props)
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<div :class="bStyleConfig.containerClass">
|
|
54
|
+
<label v-if="props.label && !bStyleConfig.hideLabel" :class="bStyleConfig.labelClass">
|
|
55
|
+
{{props.label}}
|
|
56
|
+
<span v-if="isRequired" :class="bStyleConfig.requiredLabelClass">{{bStyleConfig.requiredLabelText}}</span>
|
|
57
|
+
</label>
|
|
58
|
+
<template v-if="isReadMode">
|
|
59
|
+
<div>
|
|
60
|
+
<span :class="bStyleConfig.plainTextClass">
|
|
61
|
+
{{ selectedText }}
|
|
62
|
+
</span>
|
|
63
|
+
<input type="hidden" :name="props.name" v-model="fieldRef"/>
|
|
64
|
+
</div>
|
|
65
|
+
</template>
|
|
66
|
+
<template v-else>
|
|
67
|
+
<div>
|
|
68
|
+
<div :class="bStyleConfig.checkboxWrapperClass"
|
|
69
|
+
v-for="optionItem in useOptionList" :key="optionItem.id">
|
|
70
|
+
<label :class="bStyleConfig.checkboxLabelClass" :for="optionItem.id">
|
|
71
|
+
<input type="checkbox" :class="bStyleConfig.checkboxClass"
|
|
72
|
+
:id="optionItem.id"
|
|
73
|
+
:name="props.name"
|
|
74
|
+
:value="optionItem.value"
|
|
75
|
+
v-model="fieldRef"
|
|
76
|
+
v-form-invalid="fieldModel">
|
|
77
|
+
{{optionItem.text}}
|
|
78
|
+
<i :class="bStyleConfig.checkboxHelperIconClass"></i>
|
|
79
|
+
</label>
|
|
80
|
+
</div>
|
|
81
|
+
<div v-if="hasFormFieldError(fieldModel)" :class="bStyleConfig.errorClass">
|
|
82
|
+
{{ fieldModel.errorMessage }}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</template>
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|
|
88
|
+
|
|
89
|
+
<style scoped>
|
|
90
|
+
.c-multiline-text {
|
|
91
|
+
white-space: pre-wrap;
|
|
92
|
+
word-break: break-word;
|
|
93
|
+
}
|
|
94
|
+
</style>
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {computed, inject, nextTick, onMounted, ref, Ref, watch} from "vue";
|
|
3
|
+
import flatpickr from "flatpickr";
|
|
4
|
+
import "flatpickr/dist/flatpickr.min.css";
|
|
5
|
+
import {Mandarin} from "flatpickr/dist/l10n/zh.js";
|
|
6
|
+
import {v4 as uuidv4} from 'uuid';
|
|
7
|
+
import {BaseFormDataModel, hasFormFieldError} from "../../model/BaseFormDataModel";
|
|
8
|
+
import {EmptyDataModel} from "../../model/EmptyDataModel";
|
|
9
|
+
import * as _ from 'lodash';
|
|
10
|
+
import {BSFieldStyleConfig, IBSFieldStyleConfig} from "../../model/BSFieldStyleConfig";
|
|
11
|
+
|
|
12
|
+
// ----- props -----
|
|
13
|
+
const props = withDefaults(defineProps<{
|
|
14
|
+
id?: string; // 欄位 ID
|
|
15
|
+
label?: string; // 欄位標籤
|
|
16
|
+
name: string; // 欄位名稱
|
|
17
|
+
required?: boolean; // 是否為必填
|
|
18
|
+
requiredReactive?: boolean; // 是否為必填 (響應式)
|
|
19
|
+
placeholder?: string; // placeholder
|
|
20
|
+
readMode?: boolean; // 是否為閱讀模式
|
|
21
|
+
timeType?: 'startOfDay' | 'endOfDay'; // 時間型態
|
|
22
|
+
styleConfig?: IBSFieldStyleConfig; // 樣式設定
|
|
23
|
+
}>(), {
|
|
24
|
+
requiredReactive: undefined
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// ----- view model -----
|
|
28
|
+
const viewModel = inject('c-formViewModel', new EmptyDataModel()) as BaseFormDataModel;
|
|
29
|
+
if(!viewModel) {
|
|
30
|
+
console.error('CDateRangeFormField: viewModel not found, please provide "c-formViewModel" in parent component');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// BSFieldStyleConfig
|
|
34
|
+
const bStyleConfig = BSFieldStyleConfig.mix({}, props.styleConfig);
|
|
35
|
+
|
|
36
|
+
// ----- form field -----
|
|
37
|
+
const formFieldMap = viewModel.formFieldMap;
|
|
38
|
+
const dataField = _.get(formFieldMap, props.name);
|
|
39
|
+
const dateFieldRef = dataField?.value as Ref<Date | null>;
|
|
40
|
+
|
|
41
|
+
// ----- 控制項 attribute -----
|
|
42
|
+
const componentId = props.id || 'datePicker-' + uuidv4();
|
|
43
|
+
const triggerBtnId = props.id || `date-picker-btn-${uuidv4()}`;
|
|
44
|
+
const dateBtn = ref<HTMLButtonElement | null>(null);
|
|
45
|
+
const flatpickrInstance = ref<any>(null);
|
|
46
|
+
const isRequired = computed(() => {
|
|
47
|
+
if (typeof props.requiredReactive === 'boolean') {
|
|
48
|
+
return props.requiredReactive;
|
|
49
|
+
}
|
|
50
|
+
return props.required || viewModel.fieldIsRequired(props.name);
|
|
51
|
+
});
|
|
52
|
+
const isReadMode = props.readMode == true || false;
|
|
53
|
+
|
|
54
|
+
// ----- event handler -----
|
|
55
|
+
// 切換 flatpickr 的開啟/關閉狀態
|
|
56
|
+
function toggleFlatpickr() {
|
|
57
|
+
if(flatpickrInstance.value?.isOpen) {
|
|
58
|
+
flatpickrInstance.value?.close();
|
|
59
|
+
} else {
|
|
60
|
+
flatpickrInstance.value?.open();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 清除選取的日期
|
|
65
|
+
function clearDate() {
|
|
66
|
+
dateFieldRef.value = null;
|
|
67
|
+
if (flatpickrInstance.value) {
|
|
68
|
+
flatpickrInstance.value.clear();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ----- watch -----
|
|
73
|
+
// 監聽 dateFieldRef 的變化,更新 flatpickr 的選取日期
|
|
74
|
+
watch(dateFieldRef, (newDate) => {
|
|
75
|
+
const instance = flatpickrInstance.value;
|
|
76
|
+
if(!instance || _.isEmpty(instance)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
instance?.setDate(newDate, false); // 第二個參數 false 表示不觸發 onChange 事件
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ----- LifeCycle -----
|
|
83
|
+
onMounted(() => {
|
|
84
|
+
nextTick(() => {
|
|
85
|
+
flatpickrInstance.value = flatpickr(`#${triggerBtnId}`, {
|
|
86
|
+
mode: "single", // 啟用日期區間選擇
|
|
87
|
+
dateFormat: "Y-m-d", // 日期格式
|
|
88
|
+
locale: Mandarin, // 設定語系為中文
|
|
89
|
+
onChange: (selectedDates) => {
|
|
90
|
+
if (selectedDates.length === 1) {
|
|
91
|
+
// 更新開始和結束日期
|
|
92
|
+
const selectedDate = selectedDates[0];
|
|
93
|
+
if(props.timeType === 'startOfDay') {
|
|
94
|
+
selectedDate.setHours(0, 0, 0, 0); // 設定時間為 00:00:00
|
|
95
|
+
}
|
|
96
|
+
else if(props.timeType === 'endOfDay') {
|
|
97
|
+
selectedDate.setHours(23, 59, 59, 999); // 設定時間為 23:59:59
|
|
98
|
+
}
|
|
99
|
+
dateFieldRef.value = selectedDate;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
})
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<template>
|
|
109
|
+
<div :class="bStyleConfig.containerClass">
|
|
110
|
+
<label v-if="props.label && !bStyleConfig.hideLabel" :for="triggerBtnId" :class="bStyleConfig.labelClass">
|
|
111
|
+
{{ props.label }}
|
|
112
|
+
<span v-if="isRequired" :class="bStyleConfig.requiredLabelClass">{{bStyleConfig.requiredLabelText}}</span>
|
|
113
|
+
</label>
|
|
114
|
+
<template v-if="isReadMode">
|
|
115
|
+
<div :id="componentId">
|
|
116
|
+
<span :class="bStyleConfig.plainTextClass" v-date-formatter="dateFieldRef"></span>
|
|
117
|
+
</div>
|
|
118
|
+
<input type="hidden" :name="props.name" v-model="dateFieldRef"/>
|
|
119
|
+
</template>
|
|
120
|
+
<template v-else>
|
|
121
|
+
<div :class="bStyleConfig.wrapperClass">
|
|
122
|
+
<button type="button" class="btn btn-outline-secondary c-calendar-btn"
|
|
123
|
+
ref="dateBtn"
|
|
124
|
+
:id="triggerBtnId" @click.prevent.stop>
|
|
125
|
+
<i :class="bStyleConfig.calendarIconClass"></i>
|
|
126
|
+
</button>
|
|
127
|
+
<input type="text" :class="bStyleConfig.inputClass"
|
|
128
|
+
:placeholder="props.placeholder"
|
|
129
|
+
readonly
|
|
130
|
+
v-date-formatter="dateFieldRef"
|
|
131
|
+
v-form-invalid="dataField"
|
|
132
|
+
@click.prevent.stop="toggleFlatpickr">
|
|
133
|
+
<!-- 當 valueRef 有值時,顯示清除按鈕 -->
|
|
134
|
+
<button v-if="dateFieldRef" type="button" class="btn btn-outline-secondary c-calendar-btn"
|
|
135
|
+
title="清除"
|
|
136
|
+
@click.prevent.stop="clearDate">
|
|
137
|
+
<i :class="bStyleConfig.calendarClearIconClass"></i>
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
<div v-if="hasFormFieldError(dataField)" :class="bStyleConfig.errorClass" >
|
|
141
|
+
{{ dataField?.errorMessage }}
|
|
142
|
+
</div>
|
|
143
|
+
</template>
|
|
144
|
+
</div>
|
|
145
|
+
</template>
|
|
146
|
+
|
|
147
|
+
<style scoped>
|
|
148
|
+
|
|
149
|
+
</style>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {nextTick, onMounted, Ref, ref, watch} from "vue";
|
|
3
|
+
import flatpickr from "flatpickr";
|
|
4
|
+
import "flatpickr/dist/flatpickr.min.css";
|
|
5
|
+
import {Mandarin} from "flatpickr/dist/l10n/zh.js";
|
|
6
|
+
import {v4 as uuidv4} from 'uuid';
|
|
7
|
+
import _ from "lodash";
|
|
8
|
+
import {BSFieldStyleConfig, IBSFieldStyleConfig} from "../../model/BSFieldStyleConfig";
|
|
9
|
+
|
|
10
|
+
// ----- props -----
|
|
11
|
+
const props = withDefaults(defineProps<{
|
|
12
|
+
id?: string; // 欄位 ID
|
|
13
|
+
label?: string; // 欄位標籤
|
|
14
|
+
model: any; // query parameter 實體
|
|
15
|
+
name: string; // 欄位名稱
|
|
16
|
+
placeholder?: string; // placeholder
|
|
17
|
+
timeType?: 'startOfDay' | 'endOfDay'; // 時間類型,決定時間部分的預設值
|
|
18
|
+
styleConfig?: IBSFieldStyleConfig; // BS 欄位樣式設定
|
|
19
|
+
}>(), {
|
|
20
|
+
label: "選擇日期",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// BSFieldStyleConfig
|
|
24
|
+
const bStyleConfig = BSFieldStyleConfig.mix({}, props.styleConfig);
|
|
25
|
+
|
|
26
|
+
// ----- value ref -----
|
|
27
|
+
const valueRef = ref(_.get(props.model, props.name) || null) as Ref<Date | null>;
|
|
28
|
+
watch(valueRef, (newValue) => {
|
|
29
|
+
_.set(props.model, props.name, newValue);
|
|
30
|
+
});
|
|
31
|
+
//
|
|
32
|
+
watch(() => props.model, (newModel) => {
|
|
33
|
+
valueRef.value = _.get(newModel, props.name) || null;
|
|
34
|
+
}, {deep: true});
|
|
35
|
+
|
|
36
|
+
// ----- 控制項 attribute -----
|
|
37
|
+
const triggerBtnId = props.id || `date-picker-btn-${uuidv4()}`;
|
|
38
|
+
const dateBtn = ref<HTMLButtonElement | null>(null);
|
|
39
|
+
const flatpickrInstance = ref<any>(null);
|
|
40
|
+
|
|
41
|
+
// ----- event handler -----
|
|
42
|
+
function toggleFlatpickr() {
|
|
43
|
+
if(flatpickrInstance.value?.isOpen) {
|
|
44
|
+
flatpickrInstance.value?.close();
|
|
45
|
+
} else {
|
|
46
|
+
flatpickrInstance.value?.open();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function clearDate() {
|
|
51
|
+
valueRef.value = null;
|
|
52
|
+
if (flatpickrInstance.value) {
|
|
53
|
+
flatpickrInstance.value.clear();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ----- LifeCycle -----
|
|
58
|
+
onMounted(() => {
|
|
59
|
+
nextTick(() => {
|
|
60
|
+
flatpickrInstance.value = flatpickr(`#${triggerBtnId}`, {
|
|
61
|
+
mode: "single", // 啟用日期區間選擇
|
|
62
|
+
dateFormat: "Y-m-d", // 日期格式
|
|
63
|
+
locale: Mandarin, // 設定語系為中文
|
|
64
|
+
onChange: (selectedDates) => {
|
|
65
|
+
console.log('Selected Dates:', selectedDates);
|
|
66
|
+
if (selectedDates.length === 1) {
|
|
67
|
+
// 更新開始和結束日期
|
|
68
|
+
const selectedDate = selectedDates[0];
|
|
69
|
+
if(props.timeType === 'startOfDay') {
|
|
70
|
+
selectedDate.setHours(0, 0, 0, 0); // 設定時間為 00:00:00
|
|
71
|
+
}
|
|
72
|
+
else if(props.timeType === 'endOfDay') {
|
|
73
|
+
selectedDate.setHours(23, 59, 59, 999); // 設定時間為 23:59:59
|
|
74
|
+
}
|
|
75
|
+
valueRef.value = selectedDate;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
})
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<template>
|
|
85
|
+
<div :class="bStyleConfig.containerClass">
|
|
86
|
+
<label v-if="props.label && !bStyleConfig.hideLabel" :for="triggerBtnId" :class="bStyleConfig.labelClass">
|
|
87
|
+
{{ props.label }}
|
|
88
|
+
</label>
|
|
89
|
+
<div :class="bStyleConfig.wrapperClass">
|
|
90
|
+
<button type="button" class="btn btn-outline-secondary"
|
|
91
|
+
ref="dateBtn"
|
|
92
|
+
:id="triggerBtnId" @click.prevent.stop>
|
|
93
|
+
<font-icon icon="calendar-days"/>
|
|
94
|
+
</button>
|
|
95
|
+
<input type="text" :class="bStyleConfig.inputClass" :placeholder="props.placeholder" readonly
|
|
96
|
+
v-date-formatter="valueRef"
|
|
97
|
+
v-form-invalid="valueRef"
|
|
98
|
+
@click.prevent.stop="toggleFlatpickr" />
|
|
99
|
+
<!-- 當 valueRef 有值時,顯示清除按鈕 -->
|
|
100
|
+
<button v-if="valueRef" type="button" class="btn btn-outline-secondary"
|
|
101
|
+
title="清除日期"
|
|
102
|
+
@click.prevent.stop="clearDate">
|
|
103
|
+
<font-icon icon="times"/>
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</template>
|
|
108
|
+
|
|
109
|
+
<style scoped>
|
|
110
|
+
|
|
111
|
+
</style>
|