ch3chi-commons-vue 1.2.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/package.json +2 -1
  2. package/src/api/ApiService.ts +869 -0
  3. package/src/auth/AuthorizationService.ts +138 -0
  4. package/src/auth/PermissionDescriptor.ts +99 -0
  5. package/src/auth/keys.ts +5 -0
  6. package/src/components/CAlert.vue +188 -0
  7. package/src/components/CAlertDefine.ts +20 -0
  8. package/src/components/CBSToast.vue +119 -0
  9. package/src/components/CGlobalSpinner.vue +84 -0
  10. package/src/components/CImage.vue +67 -0
  11. package/src/components/CRowCheckBox.vue +75 -0
  12. package/src/components/CRowTextInput.vue +27 -0
  13. package/src/components/CTable.vue +524 -0
  14. package/src/components/CTableDefine.ts +566 -0
  15. package/src/components/CTableTD.vue +28 -0
  16. package/src/components/HasPermission.vue +28 -0
  17. package/src/components/form/CChangePasswordFormField.vue +146 -0
  18. package/src/components/form/CCheckBoxFormField.vue +91 -0
  19. package/src/components/form/CCheckBoxPlatFormField.vue +94 -0
  20. package/src/components/form/CDateFormField.vue +149 -0
  21. package/src/components/form/CDateQueryField.vue +111 -0
  22. package/src/components/form/CDateRangeFormField.vue +138 -0
  23. package/src/components/form/CFilePickerFormField.vue +471 -0
  24. package/src/components/form/CRadioFormField.vue +62 -0
  25. package/src/components/form/CRadioPlatFormField.vue +67 -0
  26. package/src/components/form/CSelectFormField.vue +175 -0
  27. package/src/components/form/CTextAreaFormField.vue +84 -0
  28. package/src/components/form/CTextInputFormField.vue +99 -0
  29. package/src/components/form/CTinyMCEEditorFormField.vue +99 -0
  30. package/src/components/form/SCTextInputFormField.vue +129 -0
  31. package/src/composables/useCheckBoxFormField.ts +126 -0
  32. package/src/composables/useRadioFormField.ts +106 -0
  33. package/src/directive/CBootstrapDirective.ts +83 -0
  34. package/src/directive/CDateFormatterDirective.ts +37 -0
  35. package/src/directive/CFTurnstileDirective.ts +46 -0
  36. package/src/directive/CFormDirective.ts +57 -0
  37. package/src/directive/PermissionDirective.ts +102 -0
  38. package/src/env.d.ts +19 -0
  39. package/src/index.ts +83 -0
  40. package/src/model/BSFieldStyleConfig.ts +349 -0
  41. package/src/model/BaseDictionary.ts +86 -0
  42. package/src/model/BaseFormDataModel.ts +623 -0
  43. package/src/model/BaseListViewModel.ts +392 -0
  44. package/src/model/CBSModalViewModel.ts +91 -0
  45. package/src/model/CFileDataModel.ts +181 -0
  46. package/src/model/CImageViewModel.ts +34 -0
  47. package/src/model/CMenuItem.ts +199 -0
  48. package/src/model/EmailReceiverDataModel.ts +149 -0
  49. package/src/model/EmptyDataModel.ts +25 -0
  50. package/src/model/FormOptions.ts +112 -0
  51. package/src/model/LoginDataModel.ts +51 -0
  52. package/src/model/PasswordDataModel.ts +70 -0
  53. package/src/model/QueryParameter.ts +310 -0
  54. package/src/model/SessionUser.ts +110 -0
  55. package/src/model/ShowMessageDataModel.ts +69 -0
  56. package/src/model/TokenUser.ts +157 -0
  57. package/src/stores/FormDataStore.ts +73 -0
  58. package/src/stores/ViewStore.ts +701 -0
  59. package/src/stores/VueSessionStoreInstaller.ts +22 -0
  60. package/src/types/turnstile.d.ts +8 -0
  61. package/src/utils/CToolUtils.ts +133 -0
  62. package/dist/api/ApiService.d.ts +0 -233
  63. package/dist/auth/AuthorizationService.d.ts +0 -56
  64. package/dist/auth/PermissionDescriptor.d.ts +0 -37
  65. package/dist/components/CAlert.vue.d.ts +0 -17
  66. package/dist/components/CAlertDefine.d.ts +0 -14
  67. package/dist/components/CBSToast.vue.d.ts +0 -6
  68. package/dist/components/CGlobalSpinner.vue.d.ts +0 -13
  69. package/dist/components/CRowCheckBox.vue.d.ts +0 -14
  70. package/dist/components/CRowTextInput.vue.d.ts +0 -10
  71. package/dist/components/CTable.vue.d.ts +0 -24
  72. package/dist/components/CTableDefine.d.ts +0 -201
  73. package/dist/components/CTableTD.vue.d.ts +0 -7
  74. package/dist/components/form/CChangePasswordFormField.vue.d.ts +0 -14
  75. package/dist/components/form/CCheckBoxFormField.vue.d.ts +0 -30
  76. package/dist/components/form/CDateFormField.vue.d.ts +0 -17
  77. package/dist/components/form/CDateQueryField.vue.d.ts +0 -16
  78. package/dist/components/form/CDateRangeFormField.vue.d.ts +0 -17
  79. package/dist/components/form/CFilePickerFormField.vue.d.ts +0 -28
  80. package/dist/components/form/CRadioFormField.vue.d.ts +0 -30
  81. package/dist/components/form/CSelectFormField.vue.d.ts +0 -18
  82. package/dist/components/form/CTextAreaFormField.vue.d.ts +0 -16
  83. package/dist/components/form/CTextInputFormField.vue.d.ts +0 -22
  84. package/dist/directive/CBootstrapDirective.d.ts +0 -17
  85. package/dist/directive/CDateFormatterDirective.d.ts +0 -10
  86. package/dist/directive/CFTurnstileDirective.d.ts +0 -15
  87. package/dist/directive/CFormDirective.d.ts +0 -9
  88. package/dist/directive/PermissionDirective.d.ts +0 -15
  89. package/dist/index.cjs.js +0 -19103
  90. package/dist/index.d.ts +0 -45
  91. package/dist/index.es.js +0 -19086
  92. package/dist/model/BSFieldStyleConfig.d.ts +0 -121
  93. package/dist/model/BaseDictionary.d.ts +0 -34
  94. package/dist/model/BaseFormDataModel.d.ts +0 -199
  95. package/dist/model/BaseListViewModel.d.ts +0 -165
  96. package/dist/model/CBSModalViewModel.d.ts +0 -44
  97. package/dist/model/CFileDataModel.d.ts +0 -74
  98. package/dist/model/CImageViewModel.d.ts +0 -8
  99. package/dist/model/CMenuItem.d.ts +0 -86
  100. package/dist/model/EmailReceiverDataModel.d.ts +0 -57
  101. package/dist/model/EmptyDataModel.d.ts +0 -7
  102. package/dist/model/FormOptions.d.ts +0 -60
  103. package/dist/model/LoginDataModel.d.ts +0 -12
  104. package/dist/model/PasswordDataModel.d.ts +0 -15
  105. package/dist/model/QueryParameter.d.ts +0 -92
  106. package/dist/model/SessionUser.d.ts +0 -45
  107. package/dist/model/ShowMessageDataModel.d.ts +0 -44
  108. package/dist/model/TokenUser.d.ts +0 -50
  109. package/dist/stores/FormDataStore.d.ts +0 -31
  110. package/dist/stores/ViewStore.d.ts +0 -349
  111. package/dist/style.css +0 -223
  112. package/dist/utils/CToolUtils.d.ts +0 -53
@@ -0,0 +1,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>