@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,162 @@
1
+ <template lang="pug">
2
+ .eye-checkbox(:class="checkboxClass")
3
+ input(
4
+ type="checkbox"
5
+ :id="uid"
6
+ :checked="isChecked"
7
+ :value="value"
8
+ :disabled="disabled"
9
+ @change="onChanged"
10
+ )
11
+ label(:for="uid")
12
+ .status-icon
13
+ SLIcon(icon="eye-slash")
14
+ SLIcon(icon="eye")
15
+ slot
16
+ span(v-if="label !== ''") {{ label }}
17
+ </template>
18
+
19
+ <script lang="ts">
20
+ import { isEqual } from "lodash-es";
21
+ import { getUID } from "~ui/utils";
22
+
23
+ export default defineComponent({
24
+ name: "SLEyeCheckbox",
25
+ components: {},
26
+ props: {
27
+ label: {
28
+ type: String,
29
+ default: "",
30
+ },
31
+ value: {
32
+ type: [Number, String, Boolean, Object],
33
+ default: true,
34
+ },
35
+ modelValue: {
36
+ type: [Number, String, Boolean, Array],
37
+ default: false,
38
+ },
39
+ trueValue: {
40
+ type: [Number, String, Boolean],
41
+ default: true,
42
+ },
43
+ falseValue: {
44
+ type: [Number, String, Boolean],
45
+ default: false,
46
+ },
47
+ disabled: {
48
+ type: Boolean,
49
+ default: false,
50
+ },
51
+ size: {
52
+ type: String,
53
+ default: "",
54
+ validator: (value: string) => ["", "sm"].includes(value),
55
+ },
56
+ },
57
+ emits: ["update:modelValue"],
58
+ setup(props, { emit }) {
59
+ const { disabled, size, modelValue, value, trueValue, falseValue } =
60
+ toRefs(props);
61
+
62
+ const uid = ref<string | undefined>(undefined);
63
+
64
+ const checkboxClass = computed(() => {
65
+ if (size.value === "") return "";
66
+
67
+ return `checkbox-${size.value}`;
68
+ });
69
+ const isChecked = computed(() => {
70
+ if (Array.isArray(modelValue.value)) {
71
+ let found = false;
72
+ modelValue.value.forEach((v) => {
73
+ if (isEqual(v, value.value)) {
74
+ found = true;
75
+ }
76
+ });
77
+ return found;
78
+ }
79
+ return modelValue.value === trueValue.value;
80
+ });
81
+
82
+ function onChanged(e: Event) {
83
+ if (disabled.value) {
84
+ return;
85
+ }
86
+
87
+ const isChecked = (e.target as HTMLInputElement).checked;
88
+
89
+ if (Array.isArray(modelValue.value)) {
90
+ let newValue = [...modelValue.value];
91
+
92
+ if (isChecked) {
93
+ newValue.push(value.value);
94
+ } else {
95
+ newValue = newValue.filter((v) => !isEqual(v, value.value));
96
+ }
97
+
98
+ emit("update:modelValue", newValue);
99
+ } else {
100
+ emit(
101
+ "update:modelValue",
102
+ isChecked ? trueValue.value : falseValue.value
103
+ );
104
+ }
105
+ }
106
+
107
+ onMounted(() => {
108
+ uid.value = getUID();
109
+ });
110
+
111
+ return {
112
+ uid,
113
+ checkboxClass,
114
+ isChecked,
115
+ onChanged,
116
+ };
117
+ },
118
+ });
119
+ </script>
120
+
121
+ <style lang="stylus">
122
+ .eye-checkbox
123
+ @apply inline-block relative text-base align-top overflow-hidden
124
+
125
+ &.checkbox-sm
126
+ @apply text-sm
127
+
128
+ label
129
+ @apply text-sm
130
+
131
+ input
132
+ @apply absolute s('-top-10') s('-left-10')
133
+
134
+ label
135
+ @apply relative inline-flex items-center text-base text-gray-2 cursor-pointer
136
+
137
+ .icon
138
+ @apply hidden text-primary
139
+
140
+ span
141
+ @apply ml-2
142
+
143
+ input:checked + label
144
+ .icon-eye
145
+ @apply inline-block
146
+
147
+ input:not(:checked) + label
148
+ .icon-eye-slash
149
+ @apply inline-block
150
+
151
+ input:not(:checked):disabled + label
152
+ @apply text-gray-4 cursor-default
153
+
154
+ .icon-eye-slash
155
+ @apply inline-block text-gray-4
156
+
157
+ input:checked:disabled + label
158
+ @apply text-gray-3 cursor-default
159
+
160
+ .icon-eye
161
+ @apply inline-block text-gray-3
162
+ </style>
@@ -0,0 +1,101 @@
1
+ <template lang="pug">
2
+ .custom-file-input
3
+ .file-button(:class='{ uploaded: isUploaded }')
4
+ input(
5
+ :id='uid',
6
+ type='file',
7
+ :accept='accept',
8
+ @change='onFileChanged'
9
+ ref='input'
10
+ )
11
+ label(:for='uid') {{ uploadButtonTitle }}
12
+ .filename(v-if='isUploaded && filename !== ""') {{ filename }}
13
+ </template>
14
+
15
+ <script lang="ts">
16
+ import { getUID } from "~ui/utils";
17
+
18
+ export default defineComponent({
19
+ name: "SLFileInput",
20
+ components: {},
21
+ props: {
22
+ accept: {
23
+ type: String,
24
+ default: ".jpg, .jpeg, .png, .gif",
25
+ },
26
+ },
27
+ emits: ["update:modelValue"],
28
+ setup(_, { emit }) {
29
+ const input = shallowRef<HTMLInputElement | null>(null);
30
+ const uid = ref<string | undefined>(undefined);
31
+ const isUploaded = ref(false);
32
+ const filename = ref("");
33
+
34
+ const uploadButtonTitle = computed(() => {
35
+ if (isUploaded.value) {
36
+ return "重新上傳";
37
+ }
38
+
39
+ return "選擇檔案";
40
+ });
41
+
42
+ function onFileChanged(e: Event) {
43
+ const { target } = e;
44
+ const file = ((target as HTMLInputElement).files || [])[0];
45
+
46
+ filename.value = file.name;
47
+ isUploaded.value = true;
48
+
49
+ emit("update:modelValue", file);
50
+
51
+ if (target) {
52
+ (target as HTMLInputElement).value = "";
53
+ }
54
+ }
55
+
56
+ function reset() {
57
+ if (input.value) {
58
+ input.value.value = "";
59
+ }
60
+ filename.value = "";
61
+ isUploaded.value = false;
62
+ }
63
+
64
+ onMounted(() => {
65
+ uid.value = getUID();
66
+ });
67
+
68
+ return {
69
+ input,
70
+ uid,
71
+ isUploaded,
72
+ filename,
73
+ uploadButtonTitle,
74
+ onFileChanged,
75
+ reset,
76
+ };
77
+ },
78
+ });
79
+ </script>
80
+
81
+ <style lang="stylus">
82
+ .custom-file-input
83
+ @apply inline-flex items-center s('space-x-3.5')
84
+
85
+ .file-button
86
+ @apply relative overflow-hidden
87
+
88
+ &.uploaded
89
+ label
90
+ @apply bg-white border text-primary s("hover:text-primary-hover") s("hover:bg-white")
91
+
92
+ label
93
+ @apply relative inline-block s("py-1.75") s("px-3.75") border border-transparent rounded bg-transparent text-base leading-normal space-x-2 cursor-pointer s("focus:outline-none")
94
+ @apply bg-primary border-primary text-gray s("hover:bg-primary-hover") s("hover:border-primary-hover")
95
+
96
+ input[type="file"]
97
+ @apply absolute top-0 left-0
98
+
99
+ .filename
100
+ @apply s("p-1.75") border border-primary-lighter rounded bg-bg-middle text-base text-gray-2
101
+ </style>
@@ -0,0 +1,279 @@
1
+ <template lang="pug">
2
+ .gender-age-select(ref="el")
3
+ a.toggle-button(
4
+ href="javascript:;"
5
+ ref="toggleEl"
6
+ @click="onToggleSelect"
7
+ )
8
+ .toggle-title {{ placeholder }}
9
+ SLIcon(icon="caret-down")
10
+ ClientOnly
11
+ Teleport(to="body")
12
+ .options-popup(ref="popupEl", :style="`min-width: ${maxPopupWidth}px`")
13
+ .options-block.options-block-gender
14
+ .options-block-header 性別
15
+ .options-block-body
16
+ .custom-scrollbar-y.max-h-full
17
+ .options-list
18
+ SLCheckbox(
19
+ v-for="option in genderOptions"
20
+ :label="option.label"
21
+ :value="option.value"
22
+ v-model="genderProxy"
23
+ :key="option.value"
24
+ )
25
+ .options-divide
26
+ .options-block.options-block-age
27
+ .options-block-header 年齡
28
+ .options-block-body
29
+ .custom-scrollbar-y.max-h-full
30
+ .options-list
31
+ SLCheckbox(
32
+ v-for="option in ageOptions"
33
+ :label="option.label"
34
+ :value="option.value"
35
+ v-model="ageProxy"
36
+ :key="option.value"
37
+ )
38
+ </template>
39
+
40
+ <script lang="ts">
41
+ import type { PropType } from "vue";
42
+ import { fromEvent, Subscription } from "rxjs";
43
+ import { createPopper } from "@popperjs/core";
44
+ import type { Instance as PopperInstance } from "@popperjs/core";
45
+ import { AGE_RANGE_NAME } from "~ui/utils/constants";
46
+
47
+ export default defineComponent({
48
+ name: "SLGenderAgeSelect",
49
+ components: {},
50
+ props: {
51
+ placement: {
52
+ type: String,
53
+ default: "bottom-start",
54
+ },
55
+ modelValue: {
56
+ type: Object as PropType<{ gender: string[]; age: number[] }>,
57
+ default: null,
58
+ },
59
+ },
60
+ emits: ["update:modelValue", "update:selecting"],
61
+ setup(props, { emit }) {
62
+ const { modelValue, placement } = toRefs(props);
63
+
64
+ const el = shallowRef<HTMLElement | null>(null);
65
+ const toggleEl = shallowRef<HTMLElement | null>(null);
66
+ const popupEl = shallowRef<HTMLElement | null>(null);
67
+ const isSelecting = ref(false);
68
+ const maxPopupWidth = ref(200);
69
+
70
+ let popperInstance: PopperInstance | null = null;
71
+ let clickSubscriber: Subscription | null = null;
72
+
73
+ const placeholder = computed(() => {
74
+ if (
75
+ !modelValue.value ||
76
+ ((!modelValue.value.gender || modelValue.value.gender.length === 0) &&
77
+ (!modelValue.value.age || modelValue.value.age.length === 0))
78
+ )
79
+ return "所有受眾";
80
+
81
+ return "已篩選受眾";
82
+ });
83
+ const genderOptions = computed(() => [
84
+ { label: "男性", value: "male" },
85
+ { label: "女性", value: "female" },
86
+ ]);
87
+ const ageOptions = computed(() =>
88
+ AGE_RANGE_NAME.map((name, i) => ({
89
+ label: name,
90
+ value: i + 1,
91
+ }))
92
+ );
93
+ const genderProxy = computed({
94
+ get: () => {
95
+ return modelValue.value?.gender || [];
96
+ },
97
+ set: (newValue) => {
98
+ emit("update:modelValue", {
99
+ gender: newValue,
100
+ age: modelValue.value?.age || [],
101
+ });
102
+ },
103
+ });
104
+ const ageProxy = computed({
105
+ get: () => {
106
+ return modelValue.value?.age || [];
107
+ },
108
+ set: (newValue) => {
109
+ emit("update:modelValue", {
110
+ gender: modelValue.value?.gender || [],
111
+ age: newValue,
112
+ });
113
+ },
114
+ });
115
+
116
+ function subscribeClick() {
117
+ clickSubscriber = fromEvent(window, "click").subscribe((e) => {
118
+ const { target } = e;
119
+ if (
120
+ el.value &&
121
+ !el.value.contains(target as HTMLElement) &&
122
+ popupEl.value &&
123
+ !popupEl.value.contains(target as HTMLElement)
124
+ ) {
125
+ hide();
126
+ }
127
+ });
128
+ }
129
+
130
+ function unsubscribeClick() {
131
+ if (clickSubscriber) {
132
+ clickSubscriber.unsubscribe();
133
+ }
134
+ clickSubscriber = null;
135
+ }
136
+
137
+ function show() {
138
+ if (!popupEl.value || !popperInstance) return;
139
+
140
+ if (popupEl.value) {
141
+ popupEl.value.setAttribute("data-show", "");
142
+ }
143
+
144
+ maxPopupWidth.value =
145
+ toggleEl.value?.getBoundingClientRect().width || 200;
146
+ isSelecting.value = true;
147
+ subscribeClick();
148
+ popperInstance.setOptions({
149
+ modifiers: [
150
+ {
151
+ name: "offset",
152
+ options: {
153
+ offset: [0, 8],
154
+ },
155
+ },
156
+ { name: "eventListeners", enabled: true },
157
+ ],
158
+ });
159
+ }
160
+
161
+ function hide() {
162
+ if (
163
+ !popupEl.value ||
164
+ popupEl.value.getAttribute("data-show") === null ||
165
+ !popperInstance
166
+ )
167
+ return;
168
+
169
+ unsubscribeClick();
170
+
171
+ if (popupEl.value) {
172
+ popupEl.value.removeAttribute("data-show");
173
+ }
174
+ popperInstance.setOptions({
175
+ modifiers: [{ name: "eventListeners", enabled: false }],
176
+ });
177
+ isSelecting.value = false;
178
+ }
179
+
180
+ function onToggleSelect() {
181
+ if (!popupEl.value || popupEl.value.getAttribute("data-show") !== null) {
182
+ hide();
183
+ return;
184
+ }
185
+
186
+ show();
187
+ }
188
+
189
+ watch(isSelecting, () => {
190
+ emit("update:selecting", isSelecting.value);
191
+ });
192
+
193
+ onMounted(() => {
194
+ nextTick(() => {
195
+ if (toggleEl.value && popupEl.value) {
196
+ popperInstance = createPopper(toggleEl.value, popupEl.value, {
197
+ placement: "bottom-end",
198
+ strategy: "absolute",
199
+ modifiers: [{ name: "eventListeners", enabled: false }],
200
+ });
201
+ }
202
+ });
203
+ });
204
+
205
+ onBeforeUnmount(() => {
206
+ if (popperInstance) {
207
+ popperInstance.destroy();
208
+ }
209
+ popperInstance = null;
210
+
211
+ if (clickSubscriber) {
212
+ clickSubscriber.unsubscribe();
213
+ }
214
+ clickSubscriber = null;
215
+ });
216
+
217
+ return {
218
+ el,
219
+ toggleEl,
220
+ popupEl,
221
+ maxPopupWidth,
222
+ placeholder,
223
+ genderOptions,
224
+ ageOptions,
225
+ genderProxy,
226
+ ageProxy,
227
+ onToggleSelect,
228
+ };
229
+ },
230
+ });
231
+ </script>
232
+
233
+ <style lang="stylus">
234
+ .gender-age-select
235
+ @apply relative inline-flex
236
+
237
+ .toggle-button
238
+ @apply inline-flex items-center justify-between w-full s('p-1.75') border border-primary-light rounded space-x-4 text-sm text-gray-2
239
+
240
+ .toggle-title
241
+ @apply flex-grow truncate
242
+
243
+ .icon
244
+ @apply flex-grow-0 flex-shrink-0 text-xs leading-none
245
+
246
+ .options-popup
247
+ @apply z-60 absolute flex s('max-h-[196px]') s('p-2.75') bg-white border border-primary-light rounded shadow pointer-events-none invisible
248
+
249
+ &[data-show]
250
+ @apply pointer-events-auto visible
251
+
252
+ .options-block
253
+ @apply flex flex-col
254
+ &-gender
255
+ @apply flex-grow-0 flex-shrink-0
256
+ width 37%
257
+
258
+ &-age
259
+ @apply flex-grow
260
+
261
+ &-header
262
+ @apply s('pb-1.75') border-b border-primary text-base text-primary
263
+
264
+ &-body
265
+ @apply overflow-hidden
266
+ .options-list
267
+ @apply flex flex-col
268
+
269
+ .checkbox
270
+ label
271
+ @apply w-full s('py-1.5') px-2 text-sm
272
+ @apply s('hover:bg-bg')
273
+
274
+ span
275
+ @apply ml-1
276
+
277
+ .options-divide
278
+ @apply flex-grow-0 flex-shrink-0 w-px ml-3 s('mr-2.75') bg-gray-4
279
+ </style>
@@ -0,0 +1,151 @@
1
+ <template lang="pug">
2
+ .heat-map(ref="mapEl")
3
+ </template>
4
+
5
+ <script lang="ts">
6
+ import type { PropType } from "vue";
7
+ const RADIUS = 30;
8
+
9
+ export default defineComponent({
10
+ name: "SLHeatMap",
11
+ components: {},
12
+ props: {
13
+ latlons: {
14
+ type: Array as PropType<{ lat: string; lon: string }[]>,
15
+ default: () => [],
16
+ },
17
+ weights: {
18
+ type: Array as PropType<{ lat: string; lon: string; weight: number }[]>,
19
+ default: () => [],
20
+ },
21
+ isShowMarker: {
22
+ type: Boolean,
23
+ default: false,
24
+ },
25
+ },
26
+ setup(props) {
27
+ const { latlons, weights, isShowMarker } = toRefs(props);
28
+
29
+ const { $googlemapsLoader } = useNuxtApp();
30
+ const mapEl = shallowRef<HTMLElement | null>(null);
31
+
32
+ let map: google.maps.Map | null = null;
33
+ let heatmap: google.maps.visualization.HeatmapLayer | null = null;
34
+ let markers: google.maps.Marker[] = [];
35
+
36
+ async function loadGoogleMapsApi() {
37
+ if (!$googlemapsLoader) return;
38
+
39
+ await $googlemapsLoader.importLibrary("core");
40
+ await $googlemapsLoader.importLibrary("maps");
41
+ await $googlemapsLoader.importLibrary("visualization");
42
+
43
+ setupMap();
44
+ }
45
+
46
+ function setupMap() {
47
+ if (!mapEl.value) return;
48
+
49
+ if (!map) {
50
+ map = new google.maps.Map(mapEl.value, {
51
+ disableDefaultUI: true,
52
+ styles: [
53
+ {
54
+ featureType: "poi",
55
+ stylers: [{ visibility: "off" }],
56
+ },
57
+ {
58
+ featureType: "transit",
59
+ stylers: [{ visibility: "off" }],
60
+ },
61
+ ],
62
+ });
63
+ }
64
+
65
+ map.setCenter({ lat: 23.586114913594802, lng: 120.87814086119803 });
66
+
67
+ setupHeatmapData();
68
+ }
69
+
70
+ function setupMarkers() {
71
+ const bounds = new google.maps.LatLngBounds();
72
+ markers.forEach((marker) => marker.setMap(null));
73
+ markers = [];
74
+
75
+ latlons.value.forEach((latlon) => {
76
+ const position = new google.maps.LatLng(
77
+ Number(latlon.lat),
78
+ Number(latlon.lon)
79
+ );
80
+ bounds.extend(position);
81
+
82
+ markers.push(
83
+ new google.maps.Marker({
84
+ position,
85
+ map: isShowMarker.value ? map : null,
86
+ })
87
+ );
88
+ });
89
+
90
+ if (map) {
91
+ map.fitBounds(bounds, 0);
92
+ }
93
+ }
94
+
95
+ function setupHeatmapData() {
96
+ if (!map || latlons.value.length === 0) return;
97
+
98
+ const heatMapData = weights.value
99
+ .filter((weight) => weight.weight > 0)
100
+ .map((weight) => ({
101
+ location: new google.maps.LatLng(
102
+ Number(weight.lat),
103
+ Number(weight.lon)
104
+ ),
105
+ weight: weight.weight,
106
+ }));
107
+
108
+ if (!heatmap) {
109
+ heatmap = new google.maps.visualization.HeatmapLayer({
110
+ radius: RADIUS,
111
+ });
112
+ heatmap.setMap(map);
113
+ }
114
+
115
+ heatmap.setData(heatMapData);
116
+ }
117
+
118
+ watch(latlons, () => {
119
+ setupMarkers();
120
+ });
121
+ watch(weights, () => {
122
+ setupHeatmapData();
123
+ });
124
+ watch(isShowMarker, () => {
125
+ markers.forEach((marker) => {
126
+ marker.setMap(isShowMarker.value ? map : null);
127
+ });
128
+ });
129
+
130
+ onMounted(() => {
131
+ loadGoogleMapsApi();
132
+ });
133
+
134
+ onBeforeUnmount(() => {
135
+ if (heatmap) {
136
+ heatmap.setMap(null);
137
+ heatmap = null;
138
+ }
139
+ });
140
+
141
+ return {
142
+ mapEl,
143
+ };
144
+ },
145
+ });
146
+ </script>
147
+
148
+ <style lang="stylus">
149
+ .heat-map
150
+ @apply w-full h-full
151
+ </style>