@mptw/skylens-ui 1.0.0

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 (143) hide show
  1. package/.editorconfig +12 -0
  2. package/.eslintrc.cjs +4 -0
  3. package/.nuxtrc +1 -0
  4. package/.playground/app.config.ts +5 -0
  5. package/.playground/nuxt.config.ts +3 -0
  6. package/README.md +73 -0
  7. package/app.config.ts +5 -0
  8. package/app.vue +3 -0
  9. package/assets/css/fonts/MEShPlus-font-icon.eot +0 -0
  10. package/assets/css/fonts/MEShPlus-font-icon.svg +186 -0
  11. package/assets/css/fonts/MEShPlus-font-icon.ttf +0 -0
  12. package/assets/css/fonts/MEShPlus-font-icon.woff +0 -0
  13. package/assets/css/icons.css +555 -0
  14. package/assets/css/layout.styl +98 -0
  15. package/assets/css/tailwind.css +3 -0
  16. package/assets/images/commons/app-logo-white.png +0 -0
  17. package/assets/images/commons/app-logo.png +0 -0
  18. package/assets/images/commons/empty-box.png +0 -0
  19. package/assets/images/commons/watermark.svg +15 -0
  20. package/components/SLAlertModal.vue +99 -0
  21. package/components/SLBreadcrumbs.vue +42 -0
  22. package/components/SLButton.vue +301 -0
  23. package/components/SLCalendarButton.vue +259 -0
  24. package/components/SLCalendarPopup.vue +832 -0
  25. package/components/SLCard.vue +118 -0
  26. package/components/SLChart.vue +181 -0
  27. package/components/SLCheckbox.vue +179 -0
  28. package/components/SLCircleButton.vue +95 -0
  29. package/components/SLCollapsableBlock.vue +116 -0
  30. package/components/SLCollapsableMulitPillSelect.vue +92 -0
  31. package/components/SLConfirmModal.vue +122 -0
  32. package/components/SLDateInput.vue +460 -0
  33. package/components/SLDatePicker.vue +143 -0
  34. package/components/SLDistributor.vue +30 -0
  35. package/components/SLDownloadButton.vue +410 -0
  36. package/components/SLDraggable.vue +151 -0
  37. package/components/SLDropdown.vue +277 -0
  38. package/components/SLDropdownItem.vue +31 -0
  39. package/components/SLElementWithTitle.vue +181 -0
  40. package/components/SLEyeCheckbox.vue +162 -0
  41. package/components/SLFileInput.vue +101 -0
  42. package/components/SLGenderAgeSelect.vue +279 -0
  43. package/components/SLHeatMap.vue +151 -0
  44. package/components/SLHourRangeInput.vue +211 -0
  45. package/components/SLIOSSwitch.vue +235 -0
  46. package/components/SLIcon.vue +35 -0
  47. package/components/SLIconButton.vue +130 -0
  48. package/components/SLInfoTip.vue +221 -0
  49. package/components/SLInsightSitePage.vue +27 -0
  50. package/components/SLLegend.vue +76 -0
  51. package/components/SLLinearProgressBar.vue +35 -0
  52. package/components/SLLink.vue +162 -0
  53. package/components/SLLoading.vue +127 -0
  54. package/components/SLMapChart.vue +151 -0
  55. package/components/SLModal.vue +377 -0
  56. package/components/SLMonthCalendarButton.vue +378 -0
  57. package/components/SLMonthPicker.vue +151 -0
  58. package/components/SLMultiSelect.vue +121 -0
  59. package/components/SLNoData.vue +43 -0
  60. package/components/SLPageModal.vue +64 -0
  61. package/components/SLPagination.vue +192 -0
  62. package/components/SLPillCheckbox.vue +207 -0
  63. package/components/SLPillLabel.vue +101 -0
  64. package/components/SLPopupMenuButton.vue +158 -0
  65. package/components/SLProfileButton.vue +186 -0
  66. package/components/SLRadioButton.vue +97 -0
  67. package/components/SLRadioButtonGroup.vue +113 -0
  68. package/components/SLRadioGroup.vue +201 -0
  69. package/components/SLRangeInput.vue +209 -0
  70. package/components/SLRegionsMapChart.vue +151 -0
  71. package/components/SLRightSider.vue +242 -0
  72. package/components/SLSearchInput.vue +87 -0
  73. package/components/SLSelect.vue +545 -0
  74. package/components/SLSelectAllToggle.vue +118 -0
  75. package/components/SLSiderbarNav.vue +394 -0
  76. package/components/SLSiderbarNavItem.vue +204 -0
  77. package/components/SLSitePage.vue +96 -0
  78. package/components/SLSkyInsightSitePage.vue +27 -0
  79. package/components/SLSortByDropdown.vue +189 -0
  80. package/components/SLSpinIcon.vue +24 -0
  81. package/components/SLStayRangeInput.vue +306 -0
  82. package/components/SLTab.vue +159 -0
  83. package/components/SLTable.vue +409 -0
  84. package/components/SLTableTooltip.vue +111 -0
  85. package/components/SLTabs.vue +388 -0
  86. package/components/SLTextInput.vue +362 -0
  87. package/components/SLTextarea.vue +134 -0
  88. package/components/SLToast.vue +71 -0
  89. package/components/SLToggleButton.vue +78 -0
  90. package/components/SLTwoLayerMultiSelect.vue +428 -0
  91. package/components/SLTwoLayerSelect.vue +265 -0
  92. package/components/SLWarnModal.vue +75 -0
  93. package/interface/IDateRange.ts +5 -0
  94. package/interface/IHourRange.ts +4 -0
  95. package/interface/IInputOption.ts +6 -0
  96. package/interface/ILegendItem.ts +7 -0
  97. package/interface/IPopupMenuButton.ts +5 -0
  98. package/interface/IStayRange.ts +15 -0
  99. package/interface/ITableField.ts +19 -0
  100. package/interface/IWeb.ts +5 -0
  101. package/interface/NavConfigType.ts +14 -0
  102. package/interface/commons.ts +7 -0
  103. package/models/DateRange.ts +21 -0
  104. package/nuxt.config.ts +22 -0
  105. package/package.json +52 -0
  106. package/plugins/googlemaps.ts +17 -0
  107. package/plugins/v-calendar.ts +5 -0
  108. package/public/assets/files/taiwan-regions.json +7 -0
  109. package/public/assets/files/taiwan.json +24 -0
  110. package/public/assets/images/commons/default-favico.png +0 -0
  111. package/public/assets/images/dashboard/closure-button-icon.png +0 -0
  112. package/public/assets/images/dashboard/dashboard-widget-bar-chart-icon-disabled.png +0 -0
  113. package/public/assets/images/dashboard/dashboard-widget-bar-chart-icon-poi.png +0 -0
  114. package/public/assets/images/dashboard/dashboard-widget-bar-chart-icon-web.png +0 -0
  115. package/public/assets/images/dashboard/dashboard-widget-country-heatmap-chart-icon-disabled.png +0 -0
  116. package/public/assets/images/dashboard/dashboard-widget-country-heatmap-chart-icon.png +0 -0
  117. package/public/assets/images/dashboard/dashboard-widget-geo-list-icon-disabled.png +0 -0
  118. package/public/assets/images/dashboard/dashboard-widget-geo-list-icon-poi.png +0 -0
  119. package/public/assets/images/dashboard/dashboard-widget-geo-list-icon-web.png +0 -0
  120. package/public/assets/images/dashboard/dashboard-widget-heatmap-chart-icon-disabled.png +0 -0
  121. package/public/assets/images/dashboard/dashboard-widget-heatmap-chart-icon-web.png +0 -0
  122. package/public/assets/images/dashboard/dashboard-widget-line-chart-icon-disabled.png +0 -0
  123. package/public/assets/images/dashboard/dashboard-widget-line-chart-icon-poi.png +0 -0
  124. package/public/assets/images/dashboard/dashboard-widget-list-icon-disabled.png +0 -0
  125. package/public/assets/images/dashboard/dashboard-widget-list-icon-poi.png +0 -0
  126. package/public/assets/images/dashboard/dashboard-widget-list-icon-web.png +0 -0
  127. package/public/assets/images/dashboard/dashboard-widget-pie-chart-icon-disabled.png +0 -0
  128. package/public/assets/images/dashboard/dashboard-widget-pie-chart-icon-poi.png +0 -0
  129. package/public/assets/images/dashboard/dashboard-widget-pie-chart-icon-web.png +0 -0
  130. package/public/assets/images/dashboard/drag-hint-icon.svg +3 -0
  131. package/public/assets/images/dashboard/extend-button-icon.png +0 -0
  132. package/public/assets/images/dashboard/map-list-chart-icon.svg +17 -0
  133. package/public/assets/images/dashboard/map-marker-chart-icon.svg +32 -0
  134. package/public/assets/images/dashboard/pie-chart-icon.svg +4 -0
  135. package/public/assets/images/dashboard/poi-gender-age-chart-icon.svg +20 -0
  136. package/public/assets/images/dashboard/poi-nav-icon.png +0 -0
  137. package/public/assets/images/dashboard/web-gender-age-chart-icon.svg +20 -0
  138. package/public/assets/images/dashboard/web-nav-icon.png +0 -0
  139. package/tailwind.config.js +300 -0
  140. package/tsconfig.json +8 -0
  141. package/utils/constants.ts +160 -0
  142. package/utils/generateSheet.ts +36 -0
  143. package/utils/index.ts +539 -0
@@ -0,0 +1,832 @@
1
+ <template lang="pug">
2
+ .calendar-popup(ref='el')
3
+ .popup-body
4
+ .quicks(v-if="showQuicks")
5
+ a(
6
+ href="javascript:;"
7
+ :class="{ selected: status1 === 'custom' }"
8
+ @click="onClickedQuick('custom')"
9
+ ).quick-button
10
+ .quick-button-title 自訂
11
+ .quick-button-notice ({{ minDateNotice }})
12
+ .quicks-divide
13
+ a(
14
+ v-for="quick in quicks"
15
+ href="javascript:;"
16
+ :class="{ selected: status1 === quick.key }"
17
+ :key="quick.key"
18
+ @click="onClickedQuick(quick.key)"
19
+ ).quick-button
20
+ .quick-button-title {{ quick.label }}
21
+ SLIOSSwitch(v-if="showCompareSwitch" v-model="isCompare" trueLabel="比較" falseLabel="比較").compare-switch
22
+ .calendar.calendar-compare(v-show="isCompare && showCompareSwitch")
23
+ .calendar-header
24
+ .date-range-status(:class='{ "text-danger": rangeInput2.start.invalid || rangeInput2.end.invalid }') 比較 {{ range2Status }}
25
+ .date-range
26
+ SLTextInput(
27
+ size='sm',
28
+ :lazy='1000',
29
+ :modelValue='rangeInput2.start.value'
30
+ :isInvalid="rangeInput2.start.invalid"
31
+ @update:modelValue='(value) => onRangeInputUpdated(2, "start", value)'
32
+ )
33
+ span -
34
+ SLTextInput(
35
+ size='sm',
36
+ :lazy='1000',
37
+ :modelValue='rangeInput2.end.value'
38
+ :isInvalid="rangeInput2.end.invalid"
39
+ @update:modelValue='(value) => onRangeInputUpdated(2, "end", value)'
40
+ )
41
+ SLElementWithTitle(
42
+ v-if='rangeInput2.start.invalid || rangeInput2.end.invalid'
43
+ title='日期格式有誤或選取超過365天'
44
+ )
45
+ SLIcon.text-xs.text-danger(icon='exclamation-circle')
46
+ .calendar-body
47
+ SLDatePicker(
48
+ v-model="dateRange2"
49
+ color="skylens-gray"
50
+ :is-range="true"
51
+ :min-date="minDateProxy"
52
+ :max-date="maxDate2"
53
+ :max-days="maxDaysProxy"
54
+ )
55
+ .calendar
56
+ .calendar-header
57
+ .date-range-status(:class='{ "text-danger": rangeInput1.start.invalid || rangeInput1.end.invalid }') {{ range1Status }}
58
+ .date-range
59
+ SLTextInput(
60
+ size='sm',
61
+ :lazy='1000',
62
+ :modelValue='rangeInput1.start.value'
63
+ :isInvalid="rangeInput1.start.invalid"
64
+ @update:modelValue='(value) => onRangeInputUpdated(1, "start", value)'
65
+ )
66
+ span -
67
+ SLTextInput(
68
+ size='sm',
69
+ :lazy='1000',
70
+ :modelValue='rangeInput1.end.value'
71
+ :isInvalid="rangeInput1.end.invalid"
72
+ @update:modelValue='(value) => onRangeInputUpdated(1, "end", value)'
73
+ )
74
+ SLElementWithTitle(
75
+ v-if='rangeInput1.start.invalid || rangeInput1.end.invalid'
76
+ title='日期格式有誤或選取超過365天'
77
+ )
78
+ SLIcon.text-xs.text-danger(icon='exclamation-circle')
79
+ .calendar-body
80
+ SLDatePicker(
81
+ v-model="dateRange1"
82
+ color='skylens-purple'
83
+ :is-range="true"
84
+ :min-date="minDateProxy"
85
+ :max-date="maxDate1"
86
+ :max-days="maxDaysProxy"
87
+ )
88
+ .data-date 最新資料:{{ dataDateString }}({{ dataSourceTypeString }})
89
+ .popup-footer
90
+ //- .compare(v-if="isCompare") 比較 #[span.range1 {{ dateRange1Style2 }}] 與 #[span.range2 {{ dateRange2Style2 }}]
91
+ .actions
92
+ SLButton(
93
+ size="lg"
94
+ color="cancel"
95
+ title="取消"
96
+ @click="onClickedCancel"
97
+ )
98
+ SLButton(
99
+ size="lg"
100
+ color="primary"
101
+ title="套用"
102
+ :disabled="isApplyButtonDisabled"
103
+ @click="onClickedApply"
104
+ )
105
+ </template>
106
+
107
+ <script lang="ts">
108
+ import type { PropType } from "vue";
109
+ import {
110
+ parse as parseDateString,
111
+ format as formatDate,
112
+ addDays,
113
+ subDays,
114
+ differenceInCalendarDays,
115
+ isBefore,
116
+ subYears,
117
+ startOfMonth,
118
+ } from "date-fns";
119
+ import {
120
+ dateRangeToStatus,
121
+ statusToDateRange,
122
+ isPreviousDateRange,
123
+ previousDateRange,
124
+ isValidDate,
125
+ } from "~ui/utils";
126
+ import { DEFAULT_END_DATE_BEFORE_NOW } from "~ui/utils/constants";
127
+ import type IDateRange from "~ui/interface/IDateRange";
128
+ import { OnlineSourceType } from "~ui/interface/IWeb";
129
+
130
+ const DateStringRegexp = /[0-9]{4}\/[0-9]{2}\/[0-9]{2}/;
131
+
132
+ export default defineComponent({
133
+ name: "SLCalendarPopup",
134
+ components: {},
135
+ props: {
136
+ dataSourceType: {
137
+ type: Number as PropType<OnlineSourceType>,
138
+ default: OnlineSourceType.Mobile,
139
+ },
140
+ showQuicks: {
141
+ type: Boolean,
142
+ default: true,
143
+ },
144
+ showCompareSwitch: {
145
+ type: Boolean,
146
+ default: true,
147
+ },
148
+ minDate: {
149
+ type: Date,
150
+ default: () => null,
151
+ },
152
+ maxDate: {
153
+ type: Date,
154
+ default: () => subDays(Date.now(), 1),
155
+ },
156
+ maxDays: {
157
+ type: Number,
158
+ default: -1,
159
+ },
160
+ modelValue: {
161
+ type: Array as PropType<Array<IDateRange | null>>,
162
+ default: () => [
163
+ { start: new Date(2021, 1, 11), end: new Date(2021, 1, 17) },
164
+ ],
165
+ },
166
+ },
167
+ emits: ["update:modelValue", "compare:change"],
168
+ setup(props, { emit }) {
169
+ const { dataSourceType, minDate, maxDate, maxDays, modelValue } =
170
+ toRefs(props);
171
+
172
+ const el = shallowRef<HTMLElement | null>(null);
173
+ const dateRange1 = ref<IDateRange | null>(modelValue.value[0]);
174
+ const dateRange2 = ref<IDateRange | null>(modelValue.value[1]);
175
+ const isCompare = ref(!!modelValue.value[1]);
176
+ const rangeInput1 = reactive({
177
+ start: {
178
+ value: formatDate(
179
+ modelValue.value && modelValue.value[0]
180
+ ? modelValue.value[0].start
181
+ : new Date(),
182
+ "yyyy/MM/dd"
183
+ ),
184
+ invalid: false,
185
+ },
186
+ end: {
187
+ value: formatDate(
188
+ modelValue.value && modelValue.value[0]
189
+ ? modelValue.value[0].end
190
+ : new Date(),
191
+ "yyyy/MM/dd"
192
+ ),
193
+ invalid: false,
194
+ },
195
+ });
196
+ const rangeInput2 = reactive({
197
+ start: {
198
+ value: modelValue.value[1]
199
+ ? formatDate(modelValue.value[1].start, "yyyy/MM/dd")
200
+ : "",
201
+ invalid: false,
202
+ },
203
+ end: {
204
+ value: modelValue.value[1]
205
+ ? formatDate(modelValue.value[1].end, "yyyy/MM/dd")
206
+ : "",
207
+ invalid: false,
208
+ },
209
+ });
210
+
211
+ const minDateProxy = computed(() => {
212
+ if (!minDate.value) {
213
+ return subYears(startOfMonth(new Date()), 3);
214
+ }
215
+
216
+ return minDate.value;
217
+ });
218
+ const dataSourceTypeString = computed(() => {
219
+ switch (dataSourceType.value) {
220
+ case OnlineSourceType.Mobile:
221
+ return "行動";
222
+ default:
223
+ return "固網";
224
+ }
225
+ });
226
+ const rangeInputDate1 = computed(() => {
227
+ const inputStartDate = parseDateString(
228
+ rangeInput1.start.value,
229
+ "yyyy/MM/dd",
230
+ new Date()
231
+ );
232
+ const inputEndDate = parseDateString(
233
+ rangeInput1.end.value,
234
+ "yyyy/MM/dd",
235
+ new Date()
236
+ );
237
+ const invalidInputStartDate = !DateStringRegexp.test(
238
+ rangeInput1.start.value
239
+ );
240
+ const invalidInputEndDate = !DateStringRegexp.test(rangeInput1.end.value);
241
+
242
+ if (
243
+ !isValidDate(inputStartDate) ||
244
+ !isValidDate(inputEndDate) ||
245
+ invalidInputStartDate ||
246
+ invalidInputEndDate ||
247
+ differenceInCalendarDays(inputEndDate, inputStartDate) < 0
248
+ ) {
249
+ return null;
250
+ }
251
+
252
+ return {
253
+ start: inputStartDate,
254
+ end: inputEndDate,
255
+ };
256
+ });
257
+ const rangeInputDate2 = computed(() => {
258
+ const inputStartDate = parseDateString(
259
+ rangeInput2.start.value,
260
+ "yyyy/MM/dd",
261
+ new Date()
262
+ );
263
+ const inputEndDate = parseDateString(
264
+ rangeInput2.end.value,
265
+ "yyyy/MM/dd",
266
+ new Date()
267
+ );
268
+ const invalidInputStartDate = !DateStringRegexp.test(
269
+ rangeInput2.start.value
270
+ );
271
+ const invalidInputEndDate = !DateStringRegexp.test(rangeInput2.end.value);
272
+
273
+ if (
274
+ !isValidDate(inputStartDate) ||
275
+ !isValidDate(inputEndDate) ||
276
+ invalidInputStartDate ||
277
+ invalidInputEndDate ||
278
+ differenceInCalendarDays(inputEndDate, inputStartDate) < 0
279
+ ) {
280
+ return null;
281
+ }
282
+
283
+ return {
284
+ start: inputStartDate,
285
+ end: inputEndDate,
286
+ };
287
+ });
288
+ const dataDateString = computed(() => {
289
+ switch (dataSourceType.value) {
290
+ case OnlineSourceType.Mobile:
291
+ return formatDate(
292
+ subDays(new Date(), DEFAULT_END_DATE_BEFORE_NOW),
293
+ "yyyy/MM/dd"
294
+ );
295
+ default:
296
+ return formatDate(
297
+ subDays(new Date(), DEFAULT_END_DATE_BEFORE_NOW + 2),
298
+ "yyyy/MM/dd"
299
+ );
300
+ }
301
+ });
302
+ const maxDaysProxy = computed(() => {
303
+ if (minDateProxy.value) {
304
+ const diff =
305
+ differenceInCalendarDays(maxDate.value, minDateProxy.value) + 1;
306
+ return maxDays.value === -1 ? diff : Math.min(diff, maxDays.value);
307
+ }
308
+
309
+ return maxDays.value;
310
+ });
311
+ const diffNow = computed(() => {
312
+ const now = new Date();
313
+ const diffNow = maxDate.value
314
+ ? differenceInCalendarDays(now, maxDate.value)
315
+ : 0;
316
+ return diffNow;
317
+ });
318
+ const baseDate = computed(() => {
319
+ if (diffNow.value <= 0) {
320
+ return new Date();
321
+ }
322
+
323
+ return addDays(maxDate.value, 1);
324
+ });
325
+ const baseDateString = computed(() => {
326
+ return formatDate(baseDate.value, "yyyy/MM/dd");
327
+ });
328
+ const quicks = computed(() => {
329
+ const quicks = [
330
+ { key: "last7Days", label: "過去7天" },
331
+ { key: "last30Days", label: "過去30天" },
332
+ ];
333
+
334
+ if (maxDaysProxy.value >= 90) {
335
+ quicks.push({ key: "last90Days", label: "過去90天" });
336
+ }
337
+
338
+ // if (diffNow.value <= 0) {
339
+ // quicks.push({ key: 'yesterday', label: '昨天' });
340
+ // } else {
341
+ // quicks.push({ key: 'yesterday', label: '過去1天' });
342
+ // }
343
+
344
+ return [...quicks, { key: "lastMonth", label: "上個月" }];
345
+ });
346
+ const maxDate1 = computed(() => {
347
+ return maxDate.value;
348
+ });
349
+ const status1 = computed(() => {
350
+ if (isCompare.value) {
351
+ return status2.value === "custom"
352
+ ? "custom"
353
+ : dateRangeToStatus(dateRange1.value, baseDate.value);
354
+ } else {
355
+ return dateRangeToStatus(dateRange1.value, baseDate.value);
356
+ }
357
+ });
358
+ const range1Status = computed(() => {
359
+ if (status1.value === "custom") {
360
+ const days =
361
+ !rangeInputDate1.value ||
362
+ !rangeInputDate1.value.start ||
363
+ !rangeInputDate1.value.end
364
+ ? -1
365
+ : differenceInCalendarDays(
366
+ rangeInputDate1.value.end,
367
+ rangeInputDate1.value.start
368
+ ) + 1;
369
+ return `自訂 ${days === -1 ? "" : `${days}天`}`;
370
+ }
371
+
372
+ const quick = quicks.value.find((q) => q.key === status1.value);
373
+ if (!quick) {
374
+ return "";
375
+ }
376
+ return quick.label;
377
+ });
378
+ const dateRange1Style1 = computed(() => {
379
+ if (!dateRange1.value) return "";
380
+
381
+ return `${
382
+ dateRange1.value.start
383
+ ? formatDate(dateRange1.value.start, "yyyy/MM/dd")
384
+ : ""
385
+ }${
386
+ dateRange1.value.end
387
+ ? `-${formatDate(dateRange1.value.end, "yyyy/MM/dd")}`
388
+ : ""
389
+ }`;
390
+ });
391
+ const dateRange1Style2 = computed(() => {
392
+ if (!dateRange1.value) return "";
393
+
394
+ return `${
395
+ dateRange1.value ? formatDate(dateRange1.value.start, "yyyy/MM/dd") : ""
396
+ }${
397
+ dateRange1.value.end
398
+ ? ` - ${formatDate(dateRange1.value.end, "yyyy/MM/dd")}`
399
+ : ""
400
+ }`;
401
+ });
402
+ const maxDate2 = computed(() => {
403
+ if (!dateRange1.value) return maxDate1.value;
404
+ const { start } = dateRange1.value as IDateRange;
405
+ return subDays(start, 1);
406
+ });
407
+ const status2 = computed(() => {
408
+ return isPreviousDateRange(dateRange1.value, rangeInputDate2.value);
409
+ });
410
+ const range2Status = computed(() => {
411
+ if (status2.value === "previous") {
412
+ return "上一個時段";
413
+ }
414
+
415
+ const days =
416
+ !rangeInputDate2.value ||
417
+ !rangeInputDate2.value.start ||
418
+ !rangeInputDate2.value.end
419
+ ? -1
420
+ : differenceInCalendarDays(
421
+ rangeInputDate2.value.end,
422
+ rangeInputDate2.value.start
423
+ ) + 1;
424
+ return `自訂 ${days === -1 ? "" : `${days}天`}`;
425
+ });
426
+ const dateRange2Style1 = computed(() => {
427
+ if (!dateRange2.value) return "";
428
+
429
+ return `${
430
+ dateRange2.value.start
431
+ ? formatDate(dateRange2.value.start, "yyyy/MM/dd")
432
+ : ""
433
+ }${
434
+ dateRange2.value.end
435
+ ? `-${formatDate(dateRange2.value.end, "yyyy/MM/dd")}`
436
+ : ""
437
+ }`;
438
+ });
439
+ const dateRange2Style2 = computed(() => {
440
+ if (!dateRange2.value) return "";
441
+ return `${
442
+ dateRange2.value.start
443
+ ? formatDate(dateRange2.value.start, "yyyy/MM/dd")
444
+ : ""
445
+ }${
446
+ dateRange2.value.end
447
+ ? ` - ${formatDate(dateRange2.value.end, "yyyy/MM/dd")}`
448
+ : ""
449
+ }`;
450
+ });
451
+ const isApplyButtonDisabled = computed(() => {
452
+ return (
453
+ !dateRange1.value ||
454
+ !dateRange1.value.start ||
455
+ !dateRange1.value.end ||
456
+ rangeInput1.start.invalid ||
457
+ rangeInput1.end.invalid ||
458
+ (isCompare.value &&
459
+ (!dateRange2.value ||
460
+ !dateRange2.value.start ||
461
+ !dateRange2.value.end ||
462
+ rangeInput2.start.invalid ||
463
+ rangeInput2.end.invalid))
464
+ );
465
+ });
466
+ const minDateNotice = computed(() => {
467
+ const threeYearsAgo = subYears(startOfMonth(new Date()), 3);
468
+
469
+ if (
470
+ minDateProxy.value.getFullYear() === threeYearsAgo.getFullYear() &&
471
+ minDateProxy.value.getMonth() === threeYearsAgo.getMonth() &&
472
+ minDateProxy.value.getDate() === threeYearsAgo.getDate()
473
+ ) {
474
+ return "過去3年資料";
475
+ }
476
+
477
+ return `${formatDate(minDateProxy.value, "yyyy/MM/dd")}起`;
478
+ });
479
+
480
+ function onClickedQuick(key: string) {
481
+ const dateRange = statusToDateRange(key, baseDate.value);
482
+
483
+ // if (dateRange) {
484
+ dateRange1.value = dateRange;
485
+ // }
486
+ }
487
+
488
+ function onClickedCancel() {
489
+ emit("update:modelValue", modelValue.value);
490
+ reset();
491
+ }
492
+
493
+ function onClickedApply() {
494
+ if (isApplyButtonDisabled.value) {
495
+ return;
496
+ }
497
+
498
+ const applyValue = [dateRange1.value];
499
+
500
+ if (isCompare.value && dateRange2.value) {
501
+ applyValue.push(dateRange2.value);
502
+ }
503
+
504
+ emit("update:modelValue", applyValue);
505
+ }
506
+
507
+ function onRangeInputUpdated(
508
+ rangeIndex: 1 | 2,
509
+ rangeDateKey: "start" | "end",
510
+ inputValue: string
511
+ ) {
512
+ const pMaxDays = maxDaysProxy.value > -1 ? maxDaysProxy.value : 365;
513
+
514
+ if (rangeIndex === 1) {
515
+ rangeInput1[rangeDateKey].value = inputValue;
516
+ }
517
+
518
+ rangeInput1.start.invalid = false;
519
+ rangeInput1.end.invalid = false;
520
+
521
+ const input1StartDate = parseDateString(
522
+ rangeInput1.start.value,
523
+ "yyyy/MM/dd",
524
+ new Date()
525
+ );
526
+ const input1EndDate = parseDateString(
527
+ rangeInput1.end.value,
528
+ "yyyy/MM/dd",
529
+ new Date()
530
+ );
531
+ const invalidInput1StartDate = !DateStringRegexp.test(
532
+ rangeInput1.start.value
533
+ );
534
+ const invalidInput1EndDate = !DateStringRegexp.test(
535
+ rangeInput1.end.value
536
+ );
537
+
538
+ if (
539
+ invalidInput1StartDate ||
540
+ isBefore(input1StartDate, minDateProxy.value) ||
541
+ isBefore(maxDate.value, input1StartDate) ||
542
+ (!invalidInput1EndDate && isBefore(input1EndDate, input1StartDate))
543
+ ) {
544
+ rangeInput1.start.invalid = true;
545
+ }
546
+
547
+ if (
548
+ invalidInput1EndDate ||
549
+ isBefore(input1EndDate, minDateProxy.value) ||
550
+ isBefore(maxDate.value, input1EndDate) ||
551
+ (!invalidInput1StartDate && isBefore(input1EndDate, input1StartDate))
552
+ ) {
553
+ rangeInput1.end.invalid = true;
554
+ }
555
+
556
+ if (
557
+ !invalidInput1StartDate &&
558
+ !invalidInput1EndDate &&
559
+ differenceInCalendarDays(input1EndDate, input1StartDate) >= pMaxDays
560
+ ) {
561
+ rangeInput1.start.invalid = true;
562
+ rangeInput1.end.invalid = true;
563
+ }
564
+
565
+ if (
566
+ !rangeInput1.start.invalid &&
567
+ !rangeInput1.end.invalid &&
568
+ rangeIndex === 1
569
+ ) {
570
+ dateRange1.value = {
571
+ start: input1StartDate,
572
+ end: input1EndDate,
573
+ };
574
+ }
575
+
576
+ if (!isCompare.value) return;
577
+
578
+ if (rangeIndex === 2) {
579
+ rangeInput2[rangeDateKey].value = inputValue;
580
+ }
581
+
582
+ rangeInput2.start.invalid = false;
583
+ rangeInput2.end.invalid = false;
584
+
585
+ const input2StartDate = parseDateString(
586
+ rangeInput2.start.value,
587
+ "yyyy/MM/dd",
588
+ new Date()
589
+ );
590
+ const input2EndDate = parseDateString(
591
+ rangeInput2.end.value,
592
+ "yyyy/MM/dd",
593
+ new Date()
594
+ );
595
+ const invalidInput2StartDate = !DateStringRegexp.test(
596
+ rangeInput2.start.value
597
+ );
598
+ const invalidInput2EndDate = !DateStringRegexp.test(
599
+ rangeInput2.end.value
600
+ );
601
+
602
+ if (
603
+ !isValidDate(input2StartDate) ||
604
+ invalidInput2StartDate ||
605
+ isBefore(input2StartDate, minDateProxy.value) ||
606
+ isBefore(maxDate.value, input2StartDate) ||
607
+ (!invalidInput2EndDate && isBefore(input2EndDate, input2StartDate)) ||
608
+ (!invalidInput1StartDate && isBefore(input1StartDate, input2StartDate))
609
+ ) {
610
+ rangeInput2.start.invalid = true;
611
+ }
612
+
613
+ if (
614
+ !isValidDate(input2EndDate) ||
615
+ invalidInput2EndDate ||
616
+ isBefore(input2EndDate, minDateProxy.value) ||
617
+ isBefore(maxDate.value, input2EndDate) ||
618
+ (!invalidInput2StartDate && isBefore(input2EndDate, input2StartDate)) ||
619
+ (!invalidInput1StartDate && isBefore(input1StartDate, input2EndDate)) ||
620
+ (!invalidInput1EndDate && isBefore(input1EndDate, input2EndDate))
621
+ ) {
622
+ rangeInput2.end.invalid = true;
623
+ }
624
+
625
+ if (
626
+ (!isValidDate(input2StartDate) && !isValidDate(input2EndDate)) ||
627
+ (!invalidInput2StartDate &&
628
+ !invalidInput2EndDate &&
629
+ differenceInCalendarDays(input2EndDate, input2StartDate) >= pMaxDays)
630
+ ) {
631
+ rangeInput2.start.invalid = true;
632
+ rangeInput2.end.invalid = true;
633
+ }
634
+
635
+ if (
636
+ !rangeInput2.start.invalid &&
637
+ !rangeInput2.end.invalid &&
638
+ rangeIndex === 2
639
+ ) {
640
+ dateRange2.value = {
641
+ start: input2StartDate,
642
+ end: input2EndDate,
643
+ };
644
+ }
645
+ }
646
+
647
+ function reset() {
648
+ const [orginalDateRange1, orginalDateRange2] = modelValue.value;
649
+
650
+ dateRange1.value = orginalDateRange1;
651
+ dateRange2.value = orginalDateRange2;
652
+ isCompare.value = !!orginalDateRange2;
653
+
654
+ rangeInput1.start = {
655
+ value: formatDate(
656
+ modelValue.value && modelValue.value[0]
657
+ ? modelValue.value[0].start
658
+ : new Date(),
659
+ "yyyy/MM/dd"
660
+ ),
661
+ invalid: false,
662
+ };
663
+ rangeInput1.end = {
664
+ value: formatDate(
665
+ modelValue.value && modelValue.value[0]
666
+ ? modelValue.value[0].end
667
+ : new Date(),
668
+ "yyyy/MM/dd"
669
+ ),
670
+ invalid: false,
671
+ };
672
+ rangeInput2.start = {
673
+ value: modelValue.value[1]
674
+ ? formatDate(modelValue.value[1].start, "yyyy/MM/dd")
675
+ : "",
676
+ invalid: false,
677
+ };
678
+ rangeInput2.end = {
679
+ value: modelValue.value[1]
680
+ ? formatDate(modelValue.value[1].end, "yyyy/MM/dd")
681
+ : "",
682
+ invalid: false,
683
+ };
684
+ }
685
+
686
+ watch(modelValue, () => {
687
+ dateRange1.value = modelValue.value[0];
688
+ dateRange2.value = modelValue.value[1];
689
+ isCompare.value = !!modelValue.value[1];
690
+ });
691
+ watch(dateRange1, () => {
692
+ rangeInput1.start.value =
693
+ dateRange1.value && dateRange1.value.start
694
+ ? formatDate(dateRange1.value.start, "yyyy/MM/dd")
695
+ : "";
696
+ rangeInput1.start.invalid = !dateRange1.value || !dateRange1.value.start;
697
+ rangeInput1.end.value =
698
+ dateRange1.value && dateRange1.value.end
699
+ ? formatDate(dateRange1.value.end, "yyyy/MM/dd")
700
+ : "";
701
+ rangeInput1.end.invalid = !dateRange1.value || !dateRange1.value.end;
702
+
703
+ if (isCompare.value) {
704
+ dateRange2.value = previousDateRange(dateRange1.value);
705
+ }
706
+ });
707
+ watch(dateRange2, () => {
708
+ rangeInput2.start.value =
709
+ dateRange2.value && dateRange2.value.start
710
+ ? formatDate(dateRange2.value.start, "yyyy/MM/dd")
711
+ : "";
712
+ rangeInput2.start.invalid = !dateRange2.value || !dateRange2.value.start;
713
+ rangeInput2.end.value =
714
+ dateRange2.value && dateRange2.value.end
715
+ ? formatDate(dateRange2.value.end, "yyyy/MM/dd")
716
+ : "";
717
+ rangeInput2.end.invalid = !dateRange2.value || !dateRange2.value.end;
718
+ });
719
+ watch(isCompare, () => {
720
+ emit("compare:change", isCompare.value);
721
+ dateRange2.value = isCompare.value
722
+ ? previousDateRange(dateRange1.value)
723
+ : null;
724
+ });
725
+
726
+ return {
727
+ el,
728
+ status1,
729
+ dateRange1,
730
+ dateRange2,
731
+ rangeInput1,
732
+ rangeInput2,
733
+ isCompare,
734
+ baseDateString,
735
+ quicks,
736
+ maxDate1,
737
+ range1Status,
738
+ dateRange1Style1,
739
+ dateRange1Style2,
740
+ maxDate2,
741
+ range2Status,
742
+ dateRange2Style1,
743
+ dateRange2Style2,
744
+ isApplyButtonDisabled,
745
+ maxDaysProxy,
746
+ dataSourceTypeString,
747
+ dataDateString,
748
+ minDateNotice,
749
+ minDateProxy,
750
+ onClickedQuick,
751
+ onClickedCancel,
752
+ onClickedApply,
753
+ onRangeInputUpdated,
754
+ reset,
755
+ };
756
+ },
757
+ });
758
+ </script>
759
+
760
+ <style lang="stylus">
761
+ .calendar-popup
762
+ @apply w-auto py-4 border border-bg rounded-lg bg-white shadow-md overflow-hidden
763
+
764
+ .popup-body
765
+ @apply flex s('mb-2.5') divide-x divide-gray-4
766
+
767
+ .popup-footer
768
+ @apply s('mx-3.75') border-t border-gray-4 text-center
769
+
770
+ .compare
771
+ @apply pt-4 text-base leading-normal text-gray-2
772
+
773
+ .range1
774
+ @apply text-primary font-bold
775
+
776
+ .range2
777
+ @apply text-gray-2 font-bold
778
+
779
+ .actions
780
+ @apply pt-4 space-x-8
781
+
782
+ .quicks
783
+ @apply relative
784
+
785
+ .quick-button
786
+ @apply relative block s('w-[7.4375rem]') s('pl-6.5') s('py-2') space-y-1 bg-white text-gray-3 s('hover:bg-bg') s('hover:text-gray-2')
787
+
788
+ &.selected
789
+ @apply bg-bg text-primary s('hover:text-primary')
790
+
791
+ &:before
792
+ content ''
793
+ @apply block absolute s('top-1/2') s('left-0') s('w-1.5') s('h-1.5') s('ml-3') rounded-full bg-primary transform s('-translate-y-1/2')
794
+
795
+ &-title
796
+ @apply text-base
797
+
798
+ &-notice
799
+ @apply text-xs
800
+
801
+ .quicks-divide
802
+ @apply h-px ml-1 my-1 bg-gray-4
803
+ width calc(100% - theme('spacing.1'))
804
+
805
+ .compare-switch
806
+ @apply absolute bottom-0 left-0 s('ml-4.75')
807
+
808
+ .calendar
809
+ @apply s('pl-[1.4375rem]') s('pr-6')
810
+
811
+ .vc-header
812
+ @apply pt-2
813
+
814
+ &-compare
815
+ .calendar-header
816
+ @apply text-gray-2
817
+
818
+ &-header
819
+ @apply s('pt-1.5') s('pb-1.75') s('space-y-0.5') border-b border-primary text-sm s('leading-4.75') text-primary
820
+
821
+ .date-range
822
+ @apply flex justify-start items-center space-x-1 text-sm
823
+
824
+ .text-input
825
+ @apply flex-grow-0 flex-shrink-0 s('w-[6.5rem]')
826
+
827
+ &-body
828
+ @apply space-y-2
829
+
830
+ .data-date
831
+ @apply text-xs text-gray-2 text-right
832
+ </style>