@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,378 @@
1
+ <template lang="pug">
2
+ .month-calendar-button(ref='el')
3
+ a.month-calendar-button-toggle(href='javascript:;', ref='toggleEl', @click='onTogglePopup')
4
+ .toggle-wrapper
5
+ SLIcon(icon='calendar')
6
+ .toggle-divide
7
+ .button-title
8
+ .current-month {{ currentMonthString }}
9
+ .compare-month(v-if='compareMonthString !== ""') v.s. {{ compareMonthString }}
10
+ ClientOnly
11
+ Teleport(to="body")
12
+ .month-calendar-popup(ref='popupEl')
13
+ .popup-header
14
+ .compare-switch
15
+ a.compare-switch-button(
16
+ v-if='enableCompare',
17
+ href='javascript:;',
18
+ :class='{ selected: isCompare }',
19
+ @click='onClickedCompare'
20
+ ) 比較
21
+ a.compare-switch-button(
22
+ href='javascript:;',
23
+ :class='{ selected: !isCompare }',
24
+ @click='onClickedSingle'
25
+ ) 單月
26
+ .popup-body
27
+ SLMonthPicker(
28
+ v-if='isCompare',
29
+ color='primary-hover',
30
+ :min-date='minDate',
31
+ :max-date='maxCompareDate',
32
+ v-model='compareDate',
33
+ )
34
+ .calendar-divide(v-if='isCompare')
35
+ SLMonthPicker(
36
+ color='insight-title',
37
+ :min-date='minDate',
38
+ :max-date="maxDate",
39
+ v-model='currentDate',
40
+ )
41
+ .popup-footer
42
+ a.cancel-button(
43
+ href='javascript:;'
44
+ @click='onClickedCancel'
45
+ ) 取消
46
+ SLButton(
47
+ title='套用',
48
+ size='lg',
49
+ color='primary',
50
+ @click='onClickedApply'
51
+ )
52
+ </template>
53
+
54
+ <script lang="ts">
55
+ import type { PropType } from "vue";
56
+ import { fromEvent, Subscription } from "rxjs";
57
+ import {
58
+ subMonths,
59
+ differenceInCalendarMonths,
60
+ format as formatDate,
61
+ } from "date-fns";
62
+ import { createPopper } from "@popperjs/core";
63
+ import type { Instance as PopperInstance } from "@popperjs/core";
64
+ import { gaev, ga4ev } from "~ui/utils";
65
+
66
+ export default defineComponent({
67
+ name: "SLMonthCalendarButton",
68
+ components: {},
69
+ props: {
70
+ minDate: {
71
+ type: Date as PropType<Date | null>,
72
+ default: () => null,
73
+ },
74
+ maxDate: {
75
+ type: Date as PropType<Date | null>,
76
+ default: () => new Date(),
77
+ },
78
+ modelValue: {
79
+ type: Array as PropType<(Date | null)[]>,
80
+ default: () => [new Date()],
81
+ },
82
+ enableCompare: {
83
+ type: Boolean,
84
+ default: false,
85
+ },
86
+ },
87
+ emits: ["update:modelValue"],
88
+ setup(props, { emit }) {
89
+ const { minDate, maxDate, modelValue, enableCompare } = toRefs(props);
90
+ const el = shallowRef<HTMLDivElement | null>(null);
91
+ const toggleEl = shallowRef<HTMLLinkElement | null>(null);
92
+ const popupEl = shallowRef<HTMLDivElement | null>(null);
93
+ const isShowPopup = ref(false);
94
+ const modelValueProxy = ref<(Date | null)[]>([...modelValue.value]);
95
+ const isCompare = ref(
96
+ modelValue.value[1] && enableCompare.value ? true : false
97
+ );
98
+
99
+ let popperInstance: PopperInstance | null = null;
100
+ let clickSubscription: Subscription | null = null;
101
+
102
+ const currentDate = computed({
103
+ get: () => modelValueProxy.value[0] || null,
104
+ set: (newValue) => {
105
+ modelValueProxy.value = [newValue, modelValueProxy.value[1] || null];
106
+ },
107
+ });
108
+ const compareDate = computed({
109
+ get: () => modelValueProxy.value[1] || null,
110
+ set: (newValue) => {
111
+ modelValueProxy.value = [modelValueProxy.value[0] || null, newValue];
112
+ },
113
+ });
114
+ const maxCompareDate = computed(() => {
115
+ let maxCompareDate: Date | null = null;
116
+
117
+ if (currentDate.value) {
118
+ maxCompareDate = subMonths(currentDate.value, 1);
119
+ }
120
+
121
+ if (
122
+ maxDate.value &&
123
+ maxCompareDate &&
124
+ differenceInCalendarMonths(maxDate.value, maxCompareDate) < 0
125
+ ) {
126
+ maxCompareDate = maxDate.value;
127
+ }
128
+
129
+ return maxCompareDate;
130
+ });
131
+ const currentMonthString = computed(() =>
132
+ modelValue.value[0] ? formatDate(modelValue.value[0], "yyyy/ MM") : ""
133
+ );
134
+ const compareMonthString = computed(() =>
135
+ modelValue.value[1] ? formatDate(modelValue.value[1], "yyyy/ MM") : ""
136
+ );
137
+
138
+ function onClickedCompare() {
139
+ gaev({
140
+ category: "Month picker",
141
+ action: "Month picker 切換比較",
142
+ label: "切換比較",
143
+ });
144
+ ga4ev({
145
+ category: "Month picker",
146
+ action: "Month picker 切換比較",
147
+ label: "切換比較",
148
+ });
149
+ isCompare.value = true;
150
+ }
151
+
152
+ function onClickedSingle() {
153
+ gaev({
154
+ category: "Month picker",
155
+ action: "Month picker 切換單月",
156
+ label: "切換單月",
157
+ });
158
+ ga4ev({
159
+ category: "Month picker",
160
+ action: "Month picker 切換單月",
161
+ label: "切換單月",
162
+ });
163
+ isCompare.value = false;
164
+ }
165
+
166
+ function subscribeClick() {
167
+ clickSubscription = fromEvent(window, "click").subscribe((e) => {
168
+ const { target } = e;
169
+ if (
170
+ el.value &&
171
+ !el.value.contains(target as Node) &&
172
+ popupEl.value &&
173
+ !popupEl.value.contains(target as Node)
174
+ ) {
175
+ isShowPopup.value = false;
176
+ }
177
+ });
178
+ }
179
+
180
+ function unsubscribeClick() {
181
+ if (clickSubscription) {
182
+ clickSubscription.unsubscribe();
183
+ }
184
+ clickSubscription = null;
185
+ }
186
+
187
+ function setupPopper() {
188
+ nextTick(() => {
189
+ if (!toggleEl.value || !popupEl.value || popperInstance) return;
190
+
191
+ popperInstance = createPopper(toggleEl.value, popupEl.value, {
192
+ placement: "bottom-end",
193
+ modifiers: [
194
+ { name: "offset", options: { offset: [0, 4] } },
195
+ { name: "eventListeners", enabled: false },
196
+ ],
197
+ });
198
+ });
199
+ }
200
+
201
+ function destoryPopper() {
202
+ if (popperInstance) {
203
+ popperInstance.destroy();
204
+ }
205
+ popperInstance = null;
206
+ }
207
+
208
+ function showPopup() {
209
+ if (!popperInstance || !popupEl.value) return;
210
+
211
+ subscribeClick();
212
+ popupEl.value.setAttribute("data-show", "");
213
+ popperInstance.setOptions((options) => ({
214
+ ...options,
215
+ modifiers: [
216
+ ...(options.modifiers || []),
217
+ { name: "eventListeners", enabled: true },
218
+ ],
219
+ }));
220
+ popperInstance.update();
221
+ }
222
+
223
+ function hidePopup() {
224
+ if (!popperInstance || !popupEl.value) return;
225
+
226
+ unsubscribeClick();
227
+ popupEl.value.removeAttribute("data-show");
228
+ popperInstance.setOptions((options) => ({
229
+ ...options,
230
+ modifiers: [
231
+ ...(options.modifiers || []),
232
+ { name: "eventListeners", enabled: false },
233
+ ],
234
+ }));
235
+ }
236
+
237
+ function onTogglePopup() {
238
+ isShowPopup.value = !isShowPopup.value;
239
+ }
240
+
241
+ function onClickedCancel() {
242
+ modelValueProxy.value = [...modelValue.value];
243
+ isShowPopup.value = false;
244
+ }
245
+
246
+ function onClickedApply() {
247
+ if (isCompare.value) {
248
+ const compareDate = modelValueProxy.value[1];
249
+ if (minDate.value && compareDate && compareDate < minDate.value) {
250
+ isCompare.value = false;
251
+ emit("update:modelValue", [modelValueProxy.value[0]]);
252
+ return;
253
+ }
254
+ emit("update:modelValue", [...modelValueProxy.value]);
255
+ } else {
256
+ emit("update:modelValue", [modelValueProxy.value[0]]);
257
+ }
258
+ isShowPopup.value = false;
259
+ }
260
+
261
+ function setupCompareMonth() {
262
+ if (!currentDate.value) {
263
+ modelValueProxy.value = [];
264
+ return;
265
+ }
266
+
267
+ if (!isCompare.value) {
268
+ modelValueProxy.value = [modelValueProxy.value[0]];
269
+ return;
270
+ }
271
+
272
+ // if (
273
+ // compareMonth.value &&
274
+ // differenceInCalendarMonths(currentMonth.value, compareMonth.value) > 0
275
+ // ) {
276
+ // return;
277
+ // }
278
+
279
+ modelValueProxy.value = [
280
+ modelValueProxy.value[0],
281
+ subMonths(currentDate.value, 1),
282
+ ];
283
+ }
284
+
285
+ watch(modelValue, () => {
286
+ modelValueProxy.value = [...modelValue.value];
287
+ isCompare.value =
288
+ modelValue.value[1] && enableCompare.value ? true : false;
289
+ });
290
+ watch(isShowPopup, () => {
291
+ if (isShowPopup.value) {
292
+ showPopup();
293
+ return;
294
+ }
295
+
296
+ hidePopup();
297
+ });
298
+ watch(currentDate, () => {
299
+ setupCompareMonth();
300
+ });
301
+ watch(isCompare, () => {
302
+ setupCompareMonth();
303
+ });
304
+
305
+ onMounted(() => {
306
+ setupPopper();
307
+ });
308
+
309
+ onBeforeUnmount(() => {
310
+ destoryPopper();
311
+ });
312
+
313
+ return {
314
+ el,
315
+ toggleEl,
316
+ popupEl,
317
+ isCompare,
318
+ currentDate,
319
+ compareDate,
320
+ maxCompareDate,
321
+ currentMonthString,
322
+ compareMonthString,
323
+ onTogglePopup,
324
+ onClickedCompare,
325
+ onClickedSingle,
326
+ onClickedCancel,
327
+ onClickedApply,
328
+ };
329
+ },
330
+ });
331
+ </script>
332
+
333
+ <style lang="stylus" scoped>
334
+ .month-calendar-button
335
+ @apply inline-flex
336
+
337
+ &-toggle
338
+ @apply inline-flex items-center justify-center s('min-w-[163px]') s('min-h-[58px]') py-1 px-4 bg-white border border-primary-light rounded shadow
339
+ .toggle-wrapper
340
+ @apply inline-flex items-center
341
+ .icon
342
+ @apply text-base leading-none text-primary
343
+ .toggle-divide
344
+ @apply self-stretch w-px ml-4 s('mr-3.75') bg-primary rounded-full
345
+ .button-title
346
+ @apply space-y-1 text-end
347
+ .current-month
348
+ @apply text-lg text-gray-2
349
+ .compare-month
350
+ @apply text-xs text-gray-3
351
+ .month-calendar-popup
352
+ @apply z-50 s('p-3.75') space-y-4 bg-white border border-bg rounded-lg shadow-md hidden
353
+
354
+ &[data-show]
355
+ @apply block
356
+ .popup-header
357
+ @apply text-center
358
+ .compare-switch
359
+ @apply inline-flex items-center space-x-px
360
+ .compare-switch-button
361
+ @apply block s('py-0.75') s('px-3.75') bg-white border border-primary-lighter text-sm text-insight-title
362
+
363
+ &:first-child
364
+ @apply rounded-l s('rounded-r-[1px]')
365
+
366
+ &:last-child
367
+ @apply s('rounded-l-[1px]') rounded-r
368
+ &.selected
369
+ @apply bg-insight-title border-insight-title text-white
370
+ .popup-body
371
+ @apply flex
372
+ .calendar-divide
373
+ @apply w-px s('ml-4') s('mr-3.75') bg-gray-4
374
+ .popup-footer
375
+ @apply pt-4 space-x-6 border-t border-gray-4 text-center
376
+ .cancel-button
377
+ @apply text-base text-gray-3
378
+ </style>
@@ -0,0 +1,151 @@
1
+ <template lang="pug">
2
+ .month-picker(:class='{ "month-picker--insight-title": color === "insight-title", "month-picker--primary-hover": color === "primary-hover" }')
3
+ .year-select
4
+ a.prev-button(
5
+ href='javascript:;'
6
+ :class='{ disabled: minYear !== null && yearProxy <= minYear }',
7
+ @click='onClickedPrevYear'
8
+ )
9
+ SLIcon(icon='chevron-left')
10
+ .year {{ yearProxy }}
11
+ a.next-button(
12
+ href='javascript:;'
13
+ :class='{ disabled: maxYear !== null && yearProxy >= maxYear }',
14
+ @click='onClickedNextYear'
15
+ )
16
+ SLIcon(icon='chevron-right')
17
+ .month-select
18
+ .month-select-wrapper
19
+ a.month-button(
20
+ v-for='m in 12'
21
+ href='javascript:;'
22
+ :class='{ selected: yearProxy === year && month === m, disabled: (maxYear !== null && maxYear <= yearProxy && maxMonth !== null && m > maxMonth) || (minYear !== null && minYear >= yearProxy && minMonth !== null && m < minMonth)}'
23
+ @click='onClickedMonth(m)'
24
+ ) {{ m }} 月
25
+ </template>
26
+
27
+ <script lang="ts">
28
+ import type { PropType } from "vue";
29
+
30
+ export default defineComponent({
31
+ name: "SLMonthPicker",
32
+ components: {},
33
+ props: {
34
+ color: {
35
+ type: String as PropType<"insight-title" | "primary-hover">,
36
+ validate: (value: string) =>
37
+ ["insight-title", "primary-hover"].includes(value),
38
+ },
39
+ minDate: {
40
+ type: Date as PropType<Date | null>,
41
+ default: () => null,
42
+ },
43
+ maxDate: {
44
+ type: Date as PropType<Date | null>,
45
+ default: () => null,
46
+ },
47
+ modelValue: {
48
+ type: Date as PropType<Date | null>,
49
+ default: () => null,
50
+ },
51
+ },
52
+ emits: ["update:modelValue"],
53
+ setup(props, { emit }) {
54
+ const { minDate, maxDate, modelValue } = toRefs(props);
55
+
56
+ const yearProxy = ref(
57
+ modelValue.value?.getFullYear() || new Date().getFullYear()
58
+ );
59
+
60
+ const minYear = computed(() => minDate.value?.getFullYear() || null);
61
+ const minMonth = computed(() =>
62
+ minDate.value ? minDate.value.getMonth() + 1 : null
63
+ );
64
+ const maxYear = computed(() => maxDate.value?.getFullYear() || null);
65
+ const maxMonth = computed(() =>
66
+ maxDate.value ? maxDate.value.getMonth() + 1 : null
67
+ );
68
+ const year = computed(() => modelValue.value?.getFullYear() || null);
69
+ const month = computed(() =>
70
+ modelValue.value ? modelValue.value.getMonth() + 1 : null
71
+ );
72
+
73
+ function onClickedPrevYear() {
74
+ if (minYear.value !== null && yearProxy.value <= minYear.value) return;
75
+ yearProxy.value -= 1;
76
+ }
77
+
78
+ function onClickedNextYear() {
79
+ if (maxYear.value !== null && yearProxy.value >= maxYear.value) return;
80
+ yearProxy.value += 1;
81
+ }
82
+
83
+ function onClickedMonth(m: number) {
84
+ emit("update:modelValue", new Date(yearProxy.value, m - 1, 1));
85
+ }
86
+
87
+ watch(modelValue, () => {
88
+ yearProxy.value =
89
+ modelValue.value?.getFullYear() || new Date().getFullYear();
90
+ });
91
+
92
+ return {
93
+ yearProxy,
94
+ minYear,
95
+ minMonth,
96
+ maxYear,
97
+ maxMonth,
98
+ year,
99
+ month,
100
+ onClickedPrevYear,
101
+ onClickedNextYear,
102
+ onClickedMonth,
103
+ };
104
+ },
105
+ });
106
+ </script>
107
+
108
+ <style lang="stylus" scoped>
109
+ .month-picker
110
+ @apply s('w-[190px]') space-y-4
111
+
112
+ &--insight-title
113
+ .month-select
114
+ &-wrapper
115
+ .month-button
116
+ &.selected
117
+ @apply bg-insight-title
118
+
119
+ &--primary-hover
120
+ .month-select
121
+ &-wrapper
122
+ .month-button
123
+ &.selected
124
+ @apply bg-primary-hover
125
+
126
+ .year-select
127
+ @apply flex items-center justify-center space-x-10
128
+
129
+ .prev-button, .next-button
130
+ @apply inline-flex items-center text-xs text-gray-3
131
+
132
+ &.disabled
133
+ @apply text-gray-5 cursor-default
134
+
135
+ .year
136
+ @apply text-sm text-gray-2
137
+
138
+ .month-select
139
+ &-wrapper
140
+ @apply flex flex-wrap s('-my-2') s('-mx-4')
141
+
142
+ .month-button
143
+ @apply flex items-center justify-center s('w-[42px]') s('h-[27px]') s('my-2') s('mx-4') bg-white rounded text-sm text-gray-3
144
+ @apply s('hover:bg-primary-lighter') s('hover:text-white')
145
+
146
+ &.selected
147
+ @apply text-white
148
+
149
+ &.disabled
150
+ @apply bg-white text-cancel
151
+ </style>
@@ -0,0 +1,121 @@
1
+ <template lang="pug">
2
+ .custom-multi-select
3
+ .options-wrapper.custom-scrollbar-y(ref='listWrapperEl')
4
+ .options
5
+ a.option-button(
6
+ v-for='option in formatedOptions',
7
+ href='javascript:;',
8
+ :class='{ selected: option.selected }',
9
+ @click='(e) => onClickedOption(e, option.value)'
10
+ )
11
+ span.option-label {{ option.label }}
12
+ span.btn.btn-apply(v-if='!option.selected') {{ applyTitle }}
13
+ span.btn.btn-cancel(v-else) {{ cancelTitle }}
14
+ </template>
15
+
16
+ <script lang="ts">
17
+ import type { PropType } from "vue";
18
+ import { isEqual } from "lodash-es";
19
+ import type IInputOption from "~ui/interface/IInputOption";
20
+
21
+ export default defineComponent({
22
+ name: "SLMultiSelect",
23
+ components: {},
24
+ props: {
25
+ modelValue: {
26
+ type: Array,
27
+ default: null,
28
+ },
29
+ options: {
30
+ type: Array as PropType<Array<IInputOption>>,
31
+ default: () => [],
32
+ },
33
+ applyTitle: {
34
+ type: String,
35
+ default: "套用",
36
+ },
37
+ cancelTitle: {
38
+ type: String,
39
+ default: "取消",
40
+ },
41
+ },
42
+ emits: ["update:modelValue"],
43
+ setup(props, { emit }) {
44
+ const { modelValue, options } = toRefs(props);
45
+
46
+ const listWrapperEl = ref(null);
47
+ const listScrollbarEl = ref(null);
48
+ const modelProxy = ref(modelValue.value);
49
+
50
+ const formatedOptions = computed(() => {
51
+ return options.value.map((option) => {
52
+ return {
53
+ ...option,
54
+ selected:
55
+ modelValue.value.findIndex((v) => isEqual(v, option.value)) > -1,
56
+ };
57
+ });
58
+ });
59
+
60
+ function onClickedOption(_: Event, optionValue: any) {
61
+ const found = modelValue.value.find((v) => isEqual(v, optionValue));
62
+ if (found) {
63
+ emit(
64
+ "update:modelValue",
65
+ modelValue.value.filter((v) => !isEqual(v, optionValue))
66
+ );
67
+ return;
68
+ }
69
+
70
+ emit("update:modelValue", [...modelValue.value, optionValue]);
71
+ }
72
+
73
+ watch(modelProxy, () => {
74
+ emit("update:modelValue", modelProxy.value);
75
+ });
76
+
77
+ return {
78
+ listWrapperEl,
79
+ listScrollbarEl,
80
+ modelProxy,
81
+ formatedOptions,
82
+ onClickedOption,
83
+ };
84
+ },
85
+ });
86
+ </script>
87
+
88
+ <style lang="stylus">
89
+ .custom-multi-select
90
+ @apply px-3 py-1 border border-primary-light rounded
91
+
92
+ .options-wrapper
93
+ @apply relative overflow-auto
94
+
95
+ .options
96
+ @apply flex flex-col
97
+
98
+ .option-button
99
+ @apply flex justify-between items-center px-2 py-1 bg-white shadow-inline s("hover:bg-card-primary-light")
100
+
101
+ &:hover
102
+ .btn
103
+ @apply opacity-100
104
+
105
+ &.selected
106
+ .option-label
107
+ @apply text-primary font-bold
108
+
109
+ .btn
110
+ @apply bg-primary-hover opacity-100
111
+
112
+ &:hover, &:active, &:focus
113
+ .btn
114
+ @apply bg-primary
115
+
116
+ .option-label
117
+ @apply flex-grow inline-block text-base leading-normal text-gray-2 truncate
118
+
119
+ .btn
120
+ @apply flex-grow-0 flex-shrink-0 inline-block px-2 py-1 bg-primary rounded text-base leading-normal text-white opacity-0
121
+ </style>
@@ -0,0 +1,43 @@
1
+ <template lang="pug">
2
+ .no-data(:class='{ "no-data--sm": size === "sm" }')
3
+ img(src="images/commons/empty-box.png")
4
+ .no-data-title {{ title }}
5
+ .no-data-reason(v-if='reason') 可能的原因:{{ reason }}
6
+ </template>
7
+
8
+ <script lang="ts">
9
+ export default defineComponent({
10
+ name: "SLNoData",
11
+ components: {},
12
+ props: {
13
+ title: {
14
+ type: String,
15
+ default: "無資料",
16
+ },
17
+ reason: {
18
+ type: String,
19
+ default: "數據未匯入,或是未選擇網站",
20
+ },
21
+ size: {
22
+ type: String as PropType<"" | "sm">,
23
+ default: "",
24
+ },
25
+ },
26
+ setup() {},
27
+ });
28
+ </script>
29
+
30
+ <style lang="stylus" scoped>
31
+ .no-data
32
+ @apply flex flex-col items-center w-full py-8 px-4 space-y-3 bg-white rounded-lg
33
+
34
+ &--sm
35
+ .no-data-title
36
+ @apply text-sm
37
+
38
+ &-title
39
+ @apply text-lg text-primary-light
40
+
41
+ &-reason
42
+ @apply text-sm text-gray-3
43
+ </style>