tantee-nuxt-commons 0.0.169 → 0.0.170

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.
@@ -1,230 +1,262 @@
1
1
  <script lang="ts" setup>
2
- import {type Ref, ref, watch, onMounted} from 'vue'
3
- import {useAlert} from '../../../composables/alert'
4
- import {isEqual} from 'lodash-es'
5
- import type {FormDialogCallback} from "../../../types/formDialog";
6
-
2
+ import { ref, watch } from 'vue'
3
+ import { isEqual } from 'lodash-es'
4
+ import { useAlert } from '../../../composables/alert'
5
+ import {useAssetFile, type Base64Image, type Base64Asset, type Base64File} from '../../../composables/assetFile'
6
+ import {useRuntimeConfig} from "#imports";
7
7
 
8
8
  const emit = defineEmits<{
9
- (e: "update:modelValue", value: {}): void;
10
- }>();
9
+ (e: 'update:modelValue', value: Base64Image[]): void
10
+ }>()
11
11
 
12
12
  const alert = useAlert()
13
-
14
- interface Image {
15
- title: string;
16
- data: string;
17
- props: {};
18
- }
13
+ const { fileToBase64, hydrateAssetFile } = useAssetFile()
19
14
 
20
15
  interface Props {
21
- modelValue?: any[];
22
- readonly?: boolean;
23
- label?: string;
16
+ modelValue?: Base64Image[]
17
+ readonly?: boolean
18
+ label?: string
19
+ accept?: string
20
+ autoHydrate?: boolean
21
+ maxFileSize?: number
24
22
  }
25
23
  const props = withDefaults(defineProps<Props>(), {
26
- modelValue: () => [] as any[],
24
+ modelValue: () => [] as Base64Image[],
25
+ accept: '.jpg,.jpeg,.png,.webp,.gif,.bmp,.tiff,.tif',
26
+ autoHydrate: false,
27
+ maxFileSize: 10,
27
28
  })
28
29
 
29
- const images = ref<any[]>([]);
30
- // uploadImages = images that are imported by FileBtn
31
- const uploadImages: Ref<any[]> = ref([]);
32
- // dialog = dialog for capturing image
33
- const dialog: Ref<boolean> = ref(false);
34
- // dialogUpdate, dataUpdate = dialog for editing image
35
- const dialogUpdate: Ref<boolean> = ref(false);
36
- const dataUpdate: Ref<Image> = ref({
37
- title: "",
38
- data: "",
39
- props: {},
40
- });
41
-
42
-
43
- // remove selected image
44
- const remove = (index: number) => {
45
- images.value.splice(index, 1);
46
- };
30
+ /** Internal state (always Base64Image[]) */
31
+ const images = ref<Base64Image[]>([])
32
+ const uploadImages = ref<File[]>([])
47
33
 
48
- // open dialog for editing selected image
49
- const setDataUpdate = (data: Image) => {
50
- dataUpdate.value = data;
51
- dialogUpdate.value = true;
52
- };
34
+ /** Dialogs */
35
+ const dialog = ref(false) // capture dialog
36
+ const dialogUpdate = ref(false) // edit dialog
37
+ const dataUpdate = ref<Base64Image>({ imageData: {}, imageTitle: '', imageProps: {} })
53
38
 
39
+ /** Fullscreen preview */
40
+ const dialogImageFullScreen = ref(false)
41
+ const imageFullScreen = ref<{title: string,image: string|undefined}>({ title: '', image: '' })
54
42
 
43
+ /** ---------- Stable keys + guards ---------- */
44
+ let internalSync = false
45
+ let lastEmittedSig = '' // signature of last emitted state
55
46
 
56
- // check if File name is already exist
57
- const checkDuplicationName = async (currentImageName: string) => {
58
- for (const {title} of images.value) {
59
- if (isEqual(title, currentImageName)) return true
60
- }
47
+ function imageKey(im: Base64Image): string {
48
+ const id = im.imageData?.id
49
+ if (id != null) return `id:${id}`
50
+ const title = im.imageTitle ?? ''
51
+ const len = im.imageData?.base64String?.length ?? 0
52
+ return `t:${title}|l:${len}`
61
53
  }
62
- // check if the base64 is image or not
63
- const isImage = (fileBase64: any) => {
64
- const typeFile: string = fileBase64.mineType.substring(5, 10);
65
- return typeFile === "image";
66
- };
67
-
68
- // convert FIle that is uploaded by FileBtn to base64
69
- const convertFileToBase64 = async (file: any) => {
54
+
55
+ function signature(arr: Base64Image[]): string {
56
+ return arr.map(imageKey).join('|')
57
+ }
58
+
59
+ /** Emit helper (guarded + updates last signature) */
60
+ function emitNow(next: Base64Image[]) {
61
+ const sig = signature(next)
62
+ if (sig === lastEmittedSig) return
63
+ internalSync = true
70
64
  try {
71
- const readPromise: any = new Promise((resolve, reject) => {
72
- const reader = new FileReader();
73
- reader.onload = () => {
74
- let result: any = reader.result;
75
- const mineType = result.split(",")[0];
76
- const base64 = result.split(",")[1];
77
- resolve({
78
- base64string: base64,
79
- mineType: mineType,
80
- filename: file.name,
81
- });
82
- };
83
- reader.onerror = (error) => {
84
- reject(error);
85
- };
86
- reader.readAsDataURL(file);
87
- });
88
- return await readPromise;
89
- } catch (error: any) {
90
- alert?.addAlert({message: error, alertType: 'error'})
65
+ emit('update:modelValue', next)
66
+ lastEmittedSig = sig
67
+ } finally {
68
+ queueMicrotask(() => { internalSync = false })
91
69
  }
92
- };
70
+ }
71
+
72
+ /** ---------- Helpers ---------- */
93
73
 
94
- // push image into Field
95
- const addImage = (data: Image) => {
74
+ const addImage = (img: Base64Image) => {
96
75
  images.value.push({
97
- title: data.title,
98
- data: data.data,
99
- props: {},
100
- });
101
- dialog.value = false;
102
- };
103
-
104
- const update = async () => {
105
- const duplicatedFileName = ref("")
106
- for (const image of uploadImages.value) {
107
- if (await checkDuplicationName(image.name)) {
108
- duplicatedFileName.value += `${image.name},`
109
- } else {
110
- const fileBase64: any = await convertFileToBase64(image);
111
- if (isImage(fileBase64)) {
112
- addImage({
113
- title: fileBase64.filename,
114
- data: `${fileBase64.mineType},${fileBase64.base64string}`,
115
- props: {},
116
- })
117
- } else {
118
- alert?.addAlert({
119
- message: `ไฟล์ "${fileBase64.filename}" ไม่ใช่ไฟล์นามสกุล .jpg หรือ .jpeg`,
120
- alertType: 'error'
121
- })
122
- }
76
+ imageData: img.imageData ?? {},
77
+ imageTitle: img.imageTitle ?? '',
78
+ imageProps: img.imageProps ?? {},
79
+ })
80
+ dialog.value = false
81
+ }
82
+
83
+ const remove = (index: number) => {
84
+ images.value.splice(index, 1)
85
+ }
86
+
87
+ const setDataUpdate = (img: Base64Image) => {
88
+ dataUpdate.value = {
89
+ imageData: { ...(img.imageData ?? {}) },
90
+ imageTitle: img.imageTitle ?? '',
91
+ imageProps: { ...(img.imageProps ?? {}) },
92
+ }
93
+ dialogUpdate.value = true
94
+ }
95
+
96
+ const checkDuplicationName = (name: string) => images.value.some(({ imageTitle }) => isEqual(imageTitle, name))
97
+
98
+ const isImageDataUrl = (dataUrl: string) => /^data:image\//i.test(dataUrl)
99
+
100
+ const imageSrcFromImageData = (imageData: Base64Image) => {
101
+ let assetUrl = useRuntimeConfig().public.ASSET_URL as string
102
+ if (imageData?.imageData?.base64String) return useAssetFile().ensureDataUrl(imageData?.imageData?.base64String.trim(),(imageData?.imageData as Base64File).fileType || "image/png")
103
+ if (imageData?.imageData?.id) return (assetUrl || '/asset').replace(/\/+$/, '') +"/"+imageData?.imageData?.id
104
+ return undefined
105
+ }
106
+
107
+ /** File → Base64Image using composable */
108
+ const fileToBase64Image = async (file: File): Promise<Base64Image | null> => {
109
+ try {
110
+ const base64 = await fileToBase64(file,props.maxFileSize)
111
+ const dataUrl = base64.base64String || ''
112
+ if (!isImageDataUrl(dataUrl)) {
113
+ alert?.addAlert({ message: `File "${base64.fileName}" is not supported image type.`, alertType: 'error' })
114
+ return null
123
115
  }
116
+ return {
117
+ imageData: { base64String: dataUrl } as Base64Asset,
118
+ imageTitle: base64.fileName,
119
+ imageProps: {},
120
+ }
121
+ } catch (e: any) {
122
+ alert?.addAlert({ message: String(e), alertType: 'error' })
123
+ return null
124
+ }
125
+ }
126
+
127
+ /** Handle upload button update */
128
+ const uploadImageFile = async () => {
129
+ const duplicated: string[] = []
130
+ for (const file of uploadImages.value) {
131
+ if (checkDuplicationName(file.name)) {
132
+ duplicated.push(file.name)
133
+ continue
134
+ }
135
+ const base64Image = await fileToBase64Image(file)
136
+ if (base64Image) addImage(base64Image)
124
137
  }
125
138
  uploadImages.value = []
126
- if (duplicatedFileName.value !== "") {
127
- alert?.addAlert({message: `ไม่สามารถอัพโหลดไฟล์ ${duplicatedFileName.value}`, alertType: 'error'})
128
- duplicatedFileName.value = ""
139
+ if (duplicated.length) {
140
+ alert?.addAlert({ message: `File(s) are duplicated. ${duplicated.join(', ')}`, alertType: 'error' })
129
141
  }
130
- };
142
+ }
131
143
 
132
- // Capture Image
133
- const modelData = ref();
134
- const captureImage = (image: any, callback: FormDialogCallback) => {
144
+ /** Capture flow (FormDialog) */
145
+ type FormDialogCallback = { done: () => void }
146
+ const modelData = ref()
147
+
148
+ const captureImage = (payload: any, cb: FormDialogCallback) => {
149
+ const dataUrl: string = payload?.imageCapture ?? ''
150
+ if (!dataUrl || !isImageDataUrl(dataUrl)) {
151
+ alert?.addAlert({ message: 'Invalid image.', alertType: 'error' })
152
+ return
153
+ }
135
154
  addImage({
136
- title: Math.random().toString(36).slice(2, 11),
137
- data: image.imageCapture,
138
- props: {}
155
+ imageData: { base64String: dataUrl },
156
+ imageTitle: Math.random().toString(36).slice(2, 11),
157
+ imageProps: {},
139
158
  })
140
- callback.done()
159
+ cb?.done?.()
141
160
  }
142
161
 
143
- // open Fullscreen image
144
- const dialogImageFullScreen = ref(false)
145
- const imageFullScreen = ref({
146
- title: "",
147
- image: ""
148
- })
149
-
150
- const openImageFullScreen = (image: { [key: string]: string }) => {
162
+ /** Fullscreen preview */
163
+ const openImageFullScreen = (img: Base64Image) => {
151
164
  dialogImageFullScreen.value = true
152
- imageFullScreen.value.title = image.title
153
- imageFullScreen.value.image = image.data
165
+ imageFullScreen.value.title = img.imageTitle ?? ''
166
+ imageFullScreen.value.image = imageSrcFromImageData(img)
154
167
  }
155
168
 
169
+ /** ---------- Watchers (signature-based) ---------- */
156
170
 
157
- watch(images, () => {
158
- emit("update:modelValue", images.value);
159
- }, {deep: true});
171
+ /* Parent Internal */
172
+ watch(
173
+ () => props.modelValue,
174
+ async (val) => {
175
+ if (internalSync) return
160
176
 
177
+ const next = Array.isArray(val) ? [...val] : []
178
+ const nextSig = signature(next)
161
179
 
162
- // updating when the parent component adds image to v-model
163
- watch(props.modelValue, () => {
164
- images.value = props.modelValue
165
- }, {deep: true});
180
+ // Only reassign when truly different
181
+ if (nextSig !== signature(images.value)) {
182
+ images.value = next
166
183
 
184
+ // optional hydration
185
+ if (props.autoHydrate && images.value.length) {
186
+ const targets = images.value.filter(
187
+ (im) => im.imageData?.id != null && !im.imageData?.base64String
188
+ )
189
+ if (targets.length) {
190
+ await Promise.allSettled(targets.map((im) => hydrateAssetFile(im.imageData!)))
191
+ // After hydration, emit once (guarded) to update parent
192
+ emitNow(images.value)
193
+ }
194
+ }
167
195
 
168
- onMounted(() => {
169
- // import images by v-model
170
- for (const modelValue of props.modelValue) {
171
- addImage({
172
- title: modelValue.title ?? Math.random().toString(36).slice(2, 11),
173
- data: modelValue.data,
174
- props: {}
175
- })
176
- }
177
- })
196
+ // sync lastEmittedSig to current internal state so next local change emits
197
+ lastEmittedSig = signature(images.value)
198
+ }
199
+ },
200
+ { deep: true, immediate: true }
201
+ )
202
+
203
+ /* Internal → Parent: watch the signature instead of deep structure */
204
+ watch(
205
+ () => signature(images.value),
206
+ () => {
207
+ if (internalSync) return
208
+ // Only emit when signature actually changes
209
+ emitNow(images.value)
210
+ },
211
+ { immediate: false }
212
+ )
178
213
  </script>
179
214
 
180
215
  <template>
181
216
  <VCard>
182
217
  <VToolbar density="compact">
183
218
  <VToolbarTitle>{{ label }}</VToolbarTitle>
184
- <v-spacer></v-spacer>
219
+ <v-spacer />
185
220
  <VToolbarItems v-if="!readonly">
186
221
  <FileBtn
187
- ref="fileBtn"
188
222
  v-model="uploadImages"
189
- accept=".jpg, .jpeg"
223
+ :accept="accept"
190
224
  color="primary"
191
225
  icon="mdi:mdi-image-plus"
192
- iconOnly
226
+ icon-only
193
227
  multiple
194
228
  variant="text"
195
- @update:model-value="update"
229
+ @update:model-value="uploadImageFile"
196
230
  />
197
231
  <v-btn color="primary" icon @click="dialog = true">
198
232
  <v-icon>mdi mdi-camera-plus</v-icon>
199
233
  </v-btn>
200
234
  </VToolbarItems>
201
235
  </VToolbar>
236
+
202
237
  <VCardText>
203
238
  <VRow dense justify="center">
204
- <VCol v-for="(image, index) in images" :key="index" cols="4">
239
+ <VCol v-for="(image, index) in images" :key="`${imageKey(image)}-${index}`" cols="4">
205
240
  <VCard>
206
241
  <VToolbar density="compact">
207
242
  <VToolbarTitle>
208
- {{ image.title }}
243
+ {{ image.imageTitle }}
209
244
  </VToolbarTitle>
210
- <VSpacer></VSpacer>
245
+ <VSpacer />
211
246
  <VToolbarItems v-if="!readonly">
212
247
  <v-btn icon @click="remove(index)">
213
248
  <v-icon>mdi mdi-delete-outline</v-icon>
214
249
  </v-btn>
215
- <v-btn
216
- color="primary"
217
- icon
218
- @click="setDataUpdate(image)"
219
- >
250
+ <v-btn color="primary" icon @click="setDataUpdate(image)" v-if="!image.imageData?.id">
220
251
  <v-icon>mdi mdi-image-edit-outline</v-icon>
221
252
  </v-btn>
222
253
  </VToolbarItems>
223
254
  </VToolbar>
255
+
224
256
  <v-img
225
- :src="image.data"
226
- @click="() => { (props.readonly) ? openImageFullScreen(image) : setDataUpdate(image)}"
257
+ :src="imageSrcFromImageData(image)"
227
258
  height="250"
259
+ @click="() => { (props.readonly || image.imageData?.id) ? openImageFullScreen(image) : setDataUpdate(image) }"
228
260
  />
229
261
  </VCard>
230
262
  </VCol>
@@ -232,32 +264,29 @@ onMounted(() => {
232
264
  </VCardText>
233
265
  </VCard>
234
266
 
235
-
236
- <VDialog
237
- v-model="dialogUpdate"
238
- fullscreen
239
- transition="dialog-bottom-transition"
240
- >
267
+ <!-- Edit dialog -->
268
+ <VDialog v-model="dialogUpdate" fullscreen transition="dialog-bottom-transition">
241
269
  <FormImagesPad
242
- v-model="dataUpdate.data"
270
+ v-model="dataUpdate.imageData.base64String"
243
271
  @closedDialog="dialogUpdate = false"
244
- ></FormImagesPad>
272
+ />
245
273
  </VDialog>
246
274
 
275
+ <!-- Capture dialog -->
247
276
  <FormDialog v-model="dialog" :form-data="modelData" @create="captureImage">
248
277
  <template #default="{ data }">
249
- <FormImagesCapture v-model="data.imageCapture"/>
278
+ <FormImagesCapture v-model="data.imageCapture" />
250
279
  </template>
251
280
  </FormDialog>
252
281
 
282
+ <!-- Fullscreen preview -->
253
283
  <v-dialog v-model="dialogImageFullScreen">
254
284
  <v-toolbar :title="imageFullScreen.title">
255
- <v-spacer/>
256
- <v-btn icon="mdi mdi-close" @click="dialogImageFullScreen = false"/>
285
+ <v-spacer />
286
+ <v-btn icon="mdi mdi-close" @click="dialogImageFullScreen = false" />
257
287
  </v-toolbar>
258
288
  <v-card height="80vh">
259
- <v-img :src="imageFullScreen.image"/>
289
+ <v-img :src="imageFullScreen.image" />
260
290
  </v-card>
261
-
262
291
  </v-dialog>
263
292
  </template>
@@ -2,143 +2,39 @@
2
2
  import { DateTime } from "luxon";
3
3
  import { computed } from "vue";
4
4
 
5
- type Unit = 'years' | 'months' | 'days' | 'hours' | 'minutes' | 'seconds';
6
- type Locale = 'th' | 'en' | 'en-US' | 'th-TH';
5
+ type Locale = "th" | "en" | "en-US" | "th-TH";
7
6
 
8
7
  interface Props {
9
8
  modelValue: DateTime;
10
9
  endDate?: DateTime;
11
10
  locale?: Locale;
12
- showSuffix?: boolean;
13
- units?: Unit[];
14
- zeroBase?: boolean; // true = start at 0, false = start at 1 (inclusive)
11
+ zeroBase?: boolean; // true = start at 0, false = start at 1
15
12
  }
16
13
 
17
14
  const props = withDefaults(defineProps<Props>(), {
18
- locale: 'th',
19
- showSuffix: true,
15
+ locale: "th",
20
16
  zeroBase: true,
21
17
  });
22
18
 
23
- // Fallback map: map complex locale (e.g., en-US) to base (e.g., en)
24
- const normalizeLocale = (locale: Locale): 'en' | 'th' => {
25
- if (locale.startsWith('en')) return 'en';
26
- if (locale.startsWith('th')) return 'th';
27
- return 'th'; // default fallback
28
- };
19
+ const normalizeLocale = (locale: Locale): "en" | "th" =>
20
+ locale.startsWith("en") ? "en" : "th";
29
21
 
30
- const labelsEnPlural: Record<Unit, string> = {
31
- years: 'years',
32
- months: 'months',
33
- days: 'days',
34
- hours: 'hours',
35
- minutes: 'minutes',
36
- seconds: 'seconds',
37
- };
38
- const labelsEnSingular: Record<Unit, string> = {
39
- years: 'year',
40
- months: 'month',
41
- days: 'day',
42
- hours: 'hour',
43
- minutes: 'minute',
44
- seconds: 'second',
45
- };
46
-
47
- const localizedLabels: Record<'en' | 'th', Record<Unit, string>> = {
48
- en: labelsEnPlural, // จะสลับเป็น singular ตามค่าจริงตอน render
49
- th: {
50
- years: 'ปี',
51
- months: 'เดือน',
52
- days: 'วัน',
53
- hours: 'ชั่วโมง',
54
- minutes: 'นาที',
55
- seconds: 'วินาที',
56
- },
57
- };
58
-
59
- const localizedSuffix: Record<'en' | 'th', string> = {
60
- en: 'ago',
61
- th: 'ที่ผ่านมา',
62
- };
63
-
64
- const outputText = computed(() => {
65
- const base = props.endDate ?? DateTime.now(); // ใช้ endDate ถ้ามี ไม่งั้น now
22
+ const countDate = computed(() => {
23
+ const base = props.endDate ?? DateTime.now();
66
24
  const baseLocale = normalizeLocale(props.locale);
67
25
 
68
- const units: Unit[] = props.units?.length
69
- ? props.units
70
- : ['years', 'months', 'days', 'hours', 'minutes', 'seconds'];
71
-
72
- const diffObj = base.diff(props.modelValue, units).toObject();
73
-
74
- // helper: คืน label ตาม singular/plural (เฉพาะ en)
75
- const labelFor = (unit: Unit, value: number) => {
76
- if (baseLocale === 'en') {
77
- return value === 1 ? labelsEnSingular[unit] : labelsEnPlural[unit];
78
- }
79
- return localizedLabels[baseLocale][unit];
80
- };
81
-
82
- // ---------- โหมด single unit ----------
83
- if (!props.units) {
84
- const foundUnit = units.find((unit) => (diffObj[unit] ?? 0) >= 1);
85
- if (foundUnit) {
86
- const raw = Math.floor(diffObj[foundUnit] ?? 0); // >= 1
87
- const value = props.zeroBase ? raw : raw + 1; // inclusive (+1)
88
- const label = labelFor(foundUnit, value);
89
- const suffix = props.showSuffix ? localizedSuffix[baseLocale] : '';
90
- return `${value} ${label}${suffix ? ` ${suffix}` : ''}`;
91
- }
92
-
93
- // ถ้าไม่มีหน่วยใด >= 1 ให้ใช้หน่วยเล็กสุด
94
- const lastUnit = units.at(-1)!;
95
- const raw = Math.max(0, Math.floor(diffObj[lastUnit] ?? 0));
96
- const value = props.zeroBase ? raw : 1; // inclusive: อย่างน้อย 1
97
- const label = labelFor(lastUnit, value);
98
- const suffix = props.showSuffix ? localizedSuffix[baseLocale] : '';
99
- return `${value} ${label}${suffix ? ` ${suffix}` : ''}`;
100
- }
101
-
102
- // ---------- โหมด multi-unit ----------
103
- // เก็บค่าแบบตัวเลขก่อน แล้วค่อยเรนเดอร์
104
- const values = units.map((unit) => ({
105
- unit,
106
- raw: Math.max(0, Math.floor(diffObj[unit] ?? 0)),
107
- }));
108
-
109
- // เลือกหน่วยที่เล็กที่สุด
110
- const smallest = values[values.length - 1];
111
-
112
- // ถ้ามีค่าอย่างน้อยหนึ่งหน่วย > 0 และเป็น inclusive (zeroBase=false) => +1 ที่หน่วยเล็กสุด
113
- const anyPositive = values.some((v) => v.raw > 0);
114
- const adjusted = values.map((v, idx) => {
115
- if (!props.zeroBase && anyPositive && idx === values.length - 1) {
116
- return { ...v, raw: v.raw + 1 };
117
- }
118
- return v;
119
- });
26
+ let days = Math.floor(base.diff(props.modelValue, "days").days ?? 0);
120
27
 
121
- // สร้างข้อความจากหน่วยที่มีค่า > 0
122
- const parts = adjusted
123
- .filter((v) => v.raw > 0)
124
- .map((v) => `${v.raw} ${labelFor(v.unit, v.raw)}`);
28
+ if (!props.zeroBase) days += 1;
125
29
 
126
- // หากทั้งหมดเป็น 0:
127
- if (parts.length === 0) {
128
- const minValue = props.zeroBase ? 0 : 1;
129
- const label = labelFor(smallest.unit, minValue);
130
- const suffix = props.showSuffix ? localizedSuffix[baseLocale] : '';
131
- return `${minValue} ${label}${suffix ? ` ${suffix}` : ''}`;
30
+ if (baseLocale === "en") {
31
+ return days === 1 ? "1 day" : `${days} days`;
132
32
  }
133
33
 
134
- const suffix = props.showSuffix ? localizedSuffix[baseLocale] : '';
135
- return `${parts.join(' ')}${suffix ? ` ${suffix}` : ''}`;
34
+ return `${days} วัน`;
136
35
  });
137
36
  </script>
138
37
 
139
38
  <template>
140
- <span>
141
- <i v-if="props.zeroBase" class="mdi mdi-clock-time-twelve-outline"></i>
142
- <span v-if="props.zeroBase">&nbsp;</span>{{ outputText }}
143
- </span>
39
+ <span>{{ countDate }}</span>
144
40
  </template>