@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,143 @@
1
+ <template lang="pug">
2
+ .custom-date-picker
3
+ DatePicker(
4
+ v-model="modelValueProxy"
5
+ :model-modifiers="{ range: isRange }"
6
+ mode='date'
7
+ :color="color"
8
+ :min-date="minDateByDays"
9
+ :max-date="maxDateByDays"
10
+ :update-on-input="false"
11
+ :popover="false"
12
+ :borderless='true'
13
+ @drag="onDrag"
14
+ ref="datePickerComp"
15
+ )
16
+ template(v-if="$slots.default" v-slot="{inputValue, inputEvents}")
17
+ slot(v-bind="{ inputValue, inputEvents }")
18
+ </template>
19
+
20
+ <script lang="ts">
21
+ import type { PropType } from "vue";
22
+ import { defineComponent, ref, computed, watch, toRefs } from "#imports";
23
+ import { DatePicker } from "v-calendar";
24
+ import "v-calendar/style.css";
25
+ import {
26
+ addDays,
27
+ subDays,
28
+ min as dateMin,
29
+ max as dateMax,
30
+ isDate,
31
+ } from "date-fns";
32
+ import type IDateRange from "~ui/interface/IDateRange";
33
+
34
+ type DatePickerType = InstanceType<typeof DatePicker>;
35
+
36
+ export default defineComponent({
37
+ name: "SLDatePicker",
38
+ components: {
39
+ DatePicker,
40
+ },
41
+ props: {
42
+ modelValue: {
43
+ type: [Date, Object] as PropType<Date | IDateRange | null>,
44
+ default: null,
45
+ },
46
+ color: {
47
+ type: String,
48
+ default: "purple",
49
+ },
50
+ isRange: {
51
+ type: Boolean,
52
+ default: false,
53
+ },
54
+ minDate: {
55
+ type: Date,
56
+ default: () => null,
57
+ },
58
+ maxDate: {
59
+ type: Date,
60
+ default: () => null,
61
+ },
62
+ maxDays: {
63
+ type: Number,
64
+ default: -1,
65
+ },
66
+ },
67
+ emits: ["update:modelValue"],
68
+ setup(props, { emit }) {
69
+ const { minDate, maxDate, maxDays, modelValue } = toRefs(props);
70
+
71
+ const datePickerComp = ref<DatePickerType | null>(null);
72
+ const draggingStartDate = ref<Date | null>(null);
73
+
74
+ const modelValueProxy = computed({
75
+ get: () => modelValue.value,
76
+ set: (newValue) => {
77
+ draggingStartDate.value = null;
78
+ emit("update:modelValue", newValue);
79
+ },
80
+ });
81
+ const minDateByDays = computed(() => {
82
+ if (!draggingStartDate.value || maxDays.value <= 0) return minDate.value;
83
+
84
+ const draggingMinDate = subDays(
85
+ draggingStartDate.value as Date,
86
+ maxDays.value - 1
87
+ );
88
+
89
+ return minDate.value
90
+ ? dateMax([minDate.value, draggingMinDate])
91
+ : draggingMinDate;
92
+ });
93
+ const maxDateByDays = computed(() => {
94
+ if (!draggingStartDate.value || maxDays.value <= 0) return maxDate.value;
95
+
96
+ const draggingMaxDate = addDays(
97
+ draggingStartDate.value as Date,
98
+ maxDays.value - 1
99
+ );
100
+
101
+ return maxDate.value
102
+ ? dateMin([maxDate.value, draggingMaxDate])
103
+ : draggingMaxDate;
104
+ });
105
+
106
+ function onDrag(e: IDateRange) {
107
+ if (draggingStartDate.value) return;
108
+
109
+ draggingStartDate.value = e.start;
110
+ }
111
+
112
+ async function moveToStartDate() {
113
+ const moveTo: Date | null = isDate(modelValue.value)
114
+ ? (modelValue.value as Date)
115
+ : (modelValue.value as IDateRange)?.start;
116
+
117
+ if (datePickerComp.value && datePickerComp.value.calendarRef && moveTo) {
118
+ await datePickerComp.value.calendarRef.move(moveTo);
119
+ }
120
+ }
121
+
122
+ watch(modelValue, () => {
123
+ if (modelValue.value) {
124
+ moveToStartDate();
125
+ }
126
+ });
127
+
128
+ return {
129
+ datePickerComp,
130
+ modelValueProxy,
131
+ minDateByDays,
132
+ maxDateByDays,
133
+ onDrag,
134
+ };
135
+ },
136
+ });
137
+ </script>
138
+
139
+ <style lang="stylus">
140
+ .custom-date-picker
141
+ .vc-pane-container
142
+ @apply border-0
143
+ </style>
@@ -0,0 +1,30 @@
1
+ <template lang="pug">
2
+ component(:is="tag")
3
+ slot
4
+ </template>
5
+
6
+ <script lang="ts">
7
+ export default defineComponent({
8
+ name: "SLDirtributor",
9
+ inheritAttrs: false,
10
+ props: {
11
+ tag: {
12
+ type: String,
13
+ default: "div",
14
+ },
15
+ },
16
+ setup(_, { attrs }) {
17
+ const distributed = computed(() => {
18
+ return attrs;
19
+ });
20
+
21
+ provide("distributed", readonly(distributed.value));
22
+
23
+ return {
24
+ distributed,
25
+ };
26
+ },
27
+ });
28
+ </script>
29
+
30
+ <style lang="stylus"></style>
@@ -0,0 +1,410 @@
1
+ <template lang="pug">
2
+ .download-button(
3
+ :class='{ disabled }'
4
+ ref="buttonEl"
5
+ data-skip-download
6
+ )
7
+ a.download-button-toggle(
8
+ href="javascript:;"
9
+ @click="onClickedToggle"
10
+ )
11
+ SLElementWithTitle(title="下載")
12
+ SLIcon(icon="download")
13
+ ClientOnly
14
+ Teleport(to="body")
15
+ .download-button-popup(ref="popupEl")
16
+ a.download-option(
17
+ v-if="sheets"
18
+ href="javascript:;"
19
+ @click="onClickedOption('excel')"
20
+ ) Excel (.xlsx)
21
+ a.download-option(
22
+ v-if="imageTarget"
23
+ href="javascript:;"
24
+ @click="onClickedOption('png')"
25
+ ) 圖片 (.png)
26
+ </template>
27
+
28
+ <script lang="ts">
29
+ import type { PropType } from "vue";
30
+ import { fromEvent, Subscription } from "rxjs";
31
+ import { createPopper } from "@popperjs/core";
32
+ import type { Instance as PopperInstance, Placement } from "@popperjs/core";
33
+ import * as htmlToImage from "html-to-image";
34
+ import ExcelJS from "exceljs";
35
+ import {
36
+ formatDateRange,
37
+ downloadURL,
38
+ downloadBlob,
39
+ gaev,
40
+ ga4ev,
41
+ } from "~ui/utils";
42
+ import type IDateRange from "~ui/interface/IDateRange";
43
+ import watermarkImg from "images/commons/watermark.svg";
44
+
45
+ // http://www.excelcodex.com/2012/06/worksheets-naming-conventions/
46
+ function safeSheetName(name: string) {
47
+ return name.replace(/[\\/*?:[\]]/g, "").substring(0, 31);
48
+ }
49
+
50
+ function loadImage(url: string) {
51
+ return new Promise<HTMLImageElement>((resolve) => {
52
+ const img = new Image();
53
+ img.onload = function () {
54
+ resolve(img);
55
+ };
56
+ img.src = url;
57
+ });
58
+ }
59
+
60
+ const DEFAULT_STYLE = {
61
+ marginTop: 0,
62
+ marginBottom: 9,
63
+ border: 0,
64
+ };
65
+
66
+ export default defineComponent({
67
+ name: "SLDownloadButton",
68
+ components: {},
69
+ props: {
70
+ disabled: {
71
+ type: Boolean,
72
+ default: false,
73
+ },
74
+ placement: {
75
+ type: String as PropType<Placement>,
76
+ default: "bottom-end",
77
+ },
78
+ fileName: {
79
+ type: String,
80
+ default: "",
81
+ },
82
+ sheets: {
83
+ type: Array as PropType<
84
+ Array<{
85
+ sheetName: string;
86
+ sheetData: Array<Array<string | number>>;
87
+ } | null>
88
+ >,
89
+ default: null,
90
+ },
91
+ imageTarget: {
92
+ type: Object as PropType<HTMLElement | null>,
93
+ default: () => null,
94
+ },
95
+ dateRanges: {
96
+ type: Array as PropType<(IDateRange | null)[]>,
97
+ default: () => [],
98
+ },
99
+ padding: {
100
+ type: Number,
101
+ default: 0,
102
+ },
103
+ watermark: {
104
+ type: Boolean,
105
+ default: true,
106
+ },
107
+ customFilter: {
108
+ type: Function,
109
+ default: null,
110
+ },
111
+ customStyle: {
112
+ type: Object as PropType<Partial<CSSStyleDeclaration>>,
113
+ default: () => {},
114
+ },
115
+ evCategory: {
116
+ type: String,
117
+ default: "",
118
+ },
119
+ },
120
+ setup(props, { emit }) {
121
+ const {
122
+ disabled,
123
+ placement,
124
+ fileName,
125
+ sheets,
126
+ imageTarget,
127
+ dateRanges,
128
+ padding,
129
+ watermark,
130
+ customFilter,
131
+ customStyle,
132
+ evCategory,
133
+ } = toRefs(props);
134
+
135
+ const buttonEl = shallowRef<HTMLElement | null>(null);
136
+ const popupEl = shallowRef<HTMLElement | null>(null);
137
+
138
+ let clickSubscriber: Subscription | null = null;
139
+ let popperInstance: PopperInstance | null = null;
140
+
141
+ const styleOption = computed(() => {
142
+ return Object.assign({}, DEFAULT_STYLE, customStyle.value);
143
+ });
144
+ const filterOption = computed(() => {
145
+ return (node: HTMLElement) => {
146
+ return (
147
+ !(
148
+ node?.getAttribute && node.getAttribute("data-skip-download") === ""
149
+ ) &&
150
+ (typeof customFilter.value !== "function" ||
151
+ (customFilter.value && customFilter.value(node)))
152
+ );
153
+ };
154
+ });
155
+
156
+ function subscribeClick() {
157
+ clickSubscriber = fromEvent(window, "click").subscribe((e) => {
158
+ const { target } = e;
159
+ if (buttonEl.value && !buttonEl.value.contains(target as Element)) {
160
+ hide();
161
+ }
162
+ });
163
+ }
164
+
165
+ function unsubscribeClick() {
166
+ if (clickSubscriber) {
167
+ clickSubscriber.unsubscribe();
168
+ }
169
+ clickSubscriber = null;
170
+ }
171
+
172
+ function show() {
173
+ if (!popupEl.value || !popperInstance) return;
174
+
175
+ subscribeClick();
176
+ popupEl.value.setAttribute("data-show", "");
177
+
178
+ popperInstance.setOptions({
179
+ modifiers: [
180
+ {
181
+ name: "offset",
182
+ options: {
183
+ offset: [0, 8],
184
+ },
185
+ },
186
+ { name: "eventListeners", enabled: true },
187
+ ],
188
+ });
189
+
190
+ popperInstance.update();
191
+ }
192
+
193
+ function hide() {
194
+ if (
195
+ !popupEl.value ||
196
+ popupEl.value.getAttribute("data-show") === null ||
197
+ !popperInstance
198
+ ) {
199
+ return;
200
+ }
201
+
202
+ unsubscribeClick();
203
+ popupEl.value.removeAttribute("data-show");
204
+ popperInstance.setOptions({
205
+ modifiers: [{ name: "eventListeners", enabled: false }],
206
+ });
207
+ }
208
+
209
+ function onClickedToggle(e: Event) {
210
+ e.stopPropagation();
211
+ if (disabled.value) return;
212
+
213
+ if (!popupEl.value || popupEl.value.getAttribute("data-show") !== null) {
214
+ hide();
215
+ return;
216
+ }
217
+ show();
218
+ }
219
+
220
+ async function onClickedOption(option: string) {
221
+ hide();
222
+
223
+ if (option === "excel") {
224
+ if (evCategory.value !== "") {
225
+ gaev({
226
+ category: evCategory.value,
227
+ action: "click",
228
+ label: "下載 Excel",
229
+ });
230
+ ga4ev({
231
+ category: evCategory.value,
232
+ action: `${evCategory.value}-下載 Excel`,
233
+ label: "下載 Excel",
234
+ });
235
+ }
236
+ const workbook = new ExcelJS.Workbook();
237
+ sheets.value.map((sheetData) => {
238
+ const sheet = workbook.addWorksheet(
239
+ sheetData?.sheetName ? safeSheetName(sheetData.sheetName) : ""
240
+ );
241
+ (sheetData?.sheetData || []).forEach((rowData) => {
242
+ sheet.addRow(rowData);
243
+ });
244
+ });
245
+
246
+ const buffer = await workbook.xlsx.writeBuffer();
247
+ downloadBlob(
248
+ buffer,
249
+ `${fileName.value}.xlsx`,
250
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
251
+ );
252
+ return;
253
+ }
254
+
255
+ if (!imageTarget.value) {
256
+ console.log("Can not find target of htmlToImage!");
257
+ return;
258
+ }
259
+
260
+ if (evCategory.value !== "") {
261
+ gaev({
262
+ category: evCategory.value,
263
+ action: "click",
264
+ label: "下載圖片",
265
+ });
266
+ ga4ev({
267
+ category: evCategory.value,
268
+ action: `${evCategory.value}-下載圖片`,
269
+ label: "下載圖片",
270
+ });
271
+ }
272
+
273
+ const options = {
274
+ pixelRatio: 1,
275
+ filter: filterOption.value,
276
+ style: styleOption.value,
277
+ };
278
+
279
+ let dataURL = await htmlToImage.toPng(imageTarget.value, options);
280
+ if (padding.value) dataURL = await addPadding(dataURL, padding.value);
281
+ if (dateRanges.value?.length > 0)
282
+ dataURL = await addDateRanges(dataURL, [...dateRanges.value].reverse());
283
+ if (watermark.value) dataURL = await addWatermark(dataURL, watermarkImg);
284
+ downloadURL(dataURL, `${fileName.value}.png`);
285
+ }
286
+
287
+ async function addPadding(dataURL: string, padding: number) {
288
+ const canvas = document.createElement("canvas");
289
+ const ctx = canvas.getContext("2d");
290
+ if (!ctx) return "";
291
+
292
+ const img = await loadImage(dataURL);
293
+ const { width, height } = img;
294
+ canvas.width = width + padding * 2;
295
+ canvas.height = height + padding * 2;
296
+ ctx.fillStyle = "#FFFFFF";
297
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
298
+ ctx.drawImage(img, padding, padding);
299
+
300
+ return canvas.toDataURL();
301
+ }
302
+
303
+ async function addDateRanges(
304
+ dataURL: string,
305
+ dateRanges: (IDateRange | null)[]
306
+ ) {
307
+ const fontSize = 16;
308
+ const canvas = document.createElement("canvas");
309
+ const ctx = canvas.getContext("2d");
310
+ if (!ctx) return "";
311
+
312
+ const img = await loadImage(dataURL);
313
+ const { width, height } = img;
314
+ canvas.width = width;
315
+ canvas.height = height + fontSize * 3;
316
+ ctx.fillStyle = "#FFFFFF";
317
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
318
+ ctx.drawImage(img, 0, 0);
319
+
320
+ const text = `時間區間:${dateRanges
321
+ .filter((dateRange) => !!dateRange)
322
+ .map((dateRange) => formatDateRange(dateRange))
323
+ .join(" v.s. ")}`;
324
+ ctx.fillStyle = "#333333";
325
+ ctx.font = `${fontSize}px "Microsoft JhengHei"`;
326
+ ctx.fillText(text, fontSize, canvas.height - fontSize);
327
+
328
+ return canvas.toDataURL();
329
+ }
330
+
331
+ async function addWatermark(dataURL: string, watermarkImg: any) {
332
+ const canvas = document.createElement("canvas");
333
+ const ctx = canvas.getContext("2d");
334
+ if (!ctx) return "";
335
+
336
+ const img = await loadImage(dataURL);
337
+ const watermark = await loadImage(watermarkImg);
338
+
339
+ const { width, height } = img;
340
+ canvas.width = width;
341
+ canvas.height = height;
342
+
343
+ ctx.fillStyle = "#FFFFFF";
344
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
345
+ ctx.drawImage(img, 0, 0);
346
+
347
+ const matrix = new DOMMatrix();
348
+ const pattern = ctx.createPattern(watermark, "repeat");
349
+ if (!pattern) return "";
350
+
351
+ pattern.setTransform(matrix.rotate(-20).scale(0.5));
352
+ ctx.globalAlpha = 0.05;
353
+ ctx.fillStyle = pattern;
354
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
355
+
356
+ return canvas.toDataURL();
357
+ }
358
+
359
+ onMounted(() => {
360
+ nextTick(() => {
361
+ if (buttonEl.value && popupEl.value) {
362
+ popperInstance = createPopper(buttonEl.value, popupEl.value, {
363
+ placement: placement.value,
364
+ modifiers: [{ name: "eventListeners", enabled: false }],
365
+ });
366
+ }
367
+ });
368
+ });
369
+
370
+ onBeforeUnmount(() => {
371
+ if (popperInstance) {
372
+ popperInstance.destroy();
373
+ }
374
+ popperInstance = null;
375
+ });
376
+
377
+ return {
378
+ buttonEl,
379
+ popupEl,
380
+ onClickedToggle,
381
+ onClickedOption,
382
+ };
383
+ },
384
+ });
385
+ </script>
386
+
387
+ <style lang="stylus">
388
+ .download-button
389
+ @apply inline-block
390
+
391
+ &.disabled
392
+ .download-button-toggle
393
+ @apply border-cancel text-cancel cursor-default
394
+
395
+ &-toggle
396
+ @apply inline-flex justify-center items-center s('w-[28px]') s('h-[28px]') bg-white border border-primary-hover rounded text-lg text-primary-hover
397
+
398
+ &.active
399
+ @apply bg-primary-hover text-white
400
+
401
+ .download-button-popup
402
+ @apply z-60 absolute flex flex-col bg-white border border-primary-light rounded overflow-hidden shadow invisible pointer-events-none
403
+
404
+ &[data-show]
405
+ @apply visible pointer-events-auto
406
+
407
+ .download-option
408
+ @apply block p-2 bg-white text-sm text-gray-3
409
+ @apply s('hover:bg-bg') s('hover:text-primary')
410
+ </style>