Compare commits

...

48 Commits

Author SHA1 Message Date
yuanshan
8566012575 echart检测尺寸变动重绘 2025-10-16 15:59:52 +08:00
yuanshan
26e7047359 fix press-releases 2025-10-16 15:41:03 +08:00
yuanshan
b3ab1781c3 fix email-alerts api 2025-10-16 09:29:00 +08:00
yuanshan
d86ad2c832 fix i18n 2025-10-15 18:26:36 +08:00
yuanshan
79210d8402 fix stock-quote i18n 2025-10-15 14:48:23 +08:00
yuanshan
56609fed31 fix quarterlyreports i18n 2025-10-15 14:42:10 +08:00
yuanshan
faac577341 fix quarterlyreports add api 2025-10-15 14:30:38 +08:00
yuanshan
ee596a518f fix email-alerts 2025-10-15 10:27:33 +08:00
yuanshan
bbc63346a1 fix product-introduction 1440 2025-10-14 17:16:36 +08:00
yuanshan
7466bcdcf7 add language change 2025-10-14 15:47:40 +08:00
yuanshan
f6b2956ac3 fix email-alerts 375 2025-10-14 13:32:13 +08:00
yuanshan
6abcba798f fix contacts 375 2025-10-14 10:38:28 +08:00
yuanshan
850a3169c2 fix events-calendar 2025-10-14 10:33:24 +08:00
yuanshan
bd2225f59b fix press-releases 375 2025-10-14 09:42:08 +08:00
yuanshan
4f59eb52e1 fix historic-stock 375,press-releases 375 2025-10-13 17:17:21 +08:00
yuanshan
ad91c54d8d fix stock-quote 375 2025-10-13 10:23:20 +08:00
yuanshan
364a7e4e3e fix quarterlyreports 375 2025-10-13 09:50:19 +08:00
yuanshan
f1af717483 fix quarterlyreports 375 2025-10-11 17:04:28 +08:00
yuanshan
c11364fa42 fix 375 product-introduction 2025-10-11 16:25:06 +08:00
yuanshan
62f9b8f2e1 fix 768 pageheader 2025-10-11 13:26:37 +08:00
yuanshan
7360392044 add 375 img 2025-10-11 13:20:22 +08:00
yuanshan
63c39cfb9a fix product-introduction 768 2025-10-11 11:56:55 +08:00
yuanshan
6679da97f6 fix email-alerts 768 2025-10-11 11:08:28 +08:00
yuanshan
60d228c3d8 fix contacts 768 2025-10-11 10:55:18 +08:00
yuanshan
000b23f4a8 fix events-calendar 768 2025-10-11 10:40:57 +08:00
yuanshan
51d2364e38 fix press-releases 768 2025-10-11 09:20:54 +08:00
yuanshan
52d9083813 统一分辨率切换值 2025-10-10 16:53:47 +08:00
yuanshan
54b1d1551d fix historic-stock 768 2025-10-10 16:44:21 +08:00
yuanshan
009d6d4d67 fix page style 2025-10-10 14:50:16 +08:00
yuanshan
ea9ad3d08e add 768 file 2025-10-10 11:41:54 +08:00
yuanshan
8d38e839ca fix product-introduction 1440 2025-10-10 11:27:00 +08:00
yuanshan
e3196f5619 fix 1440 2025-10-10 10:15:19 +08:00
yuanshan
fd8faedc12 fix 1440 2025-10-09 14:24:15 +08:00
yuanshan
7187503fcf 还原文件 2025-09-30 14:53:36 +08:00
yuanshan
a3209aa170 调整图片路径 2025-09-30 14:53:03 +08:00
yuanshan
df01e9b81b fix 1920 style 2025-09-30 13:44:53 +08:00
yuanshan
be8d2ca4d4 背景图替换 2025-09-29 16:48:29 +08:00
yuanshan
7b4d234c48 add product-introduction 1920 2025-09-29 15:17:21 +08:00
yuanshan
318f850885 del no use 2025-09-29 09:32:24 +08:00
yuanshan
fd8b03ad3e fix email-alerts 2025-09-28 18:28:22 +08:00
yuanshan
7a7751567d 1920文件样式调整 2025-09-28 17:11:45 +08:00
yuanshan
eeabddcf11 Merge branch 'zhangyuanshan-20250826' into zhangyuanshan-20250925 2025-09-25 14:18:27 +08:00
yuanshan
a04fc7cdaf Revert "2025Quarterly"
This reverts commit c7c6e642bd.
2025-09-04 13:39:50 +08:00
yuanshan
c7c6e642bd 2025Quarterly 2025-09-04 13:12:55 +08:00
yuanshan
65c5d29ff3 fix page style 2025-08-26 16:27:27 +08:00
yuanshan
fd40913fa5 fix page style 2025-08-26 14:08:27 +08:00
yuanshan
e9418a7e64 fix page style 2025-08-26 11:37:18 +08:00
yuanshan
7454cd99ab fix page style 2025-08-26 11:09:11 +08:00
220 changed files with 22627 additions and 7399 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -1,10 +1,10 @@
<script setup>
import { ref } from 'vue'
import { ref } from "vue";
import { useI18n } from 'vue-i18n'
import { NConfigProvider, NDropdown } from 'naive-ui'
const { locale } = useI18n()
const primaryColor = ref('#8B59F7')
import { useI18n } from "vue-i18n";
import { NConfigProvider, NDropdown } from "naive-ui";
const { locale } = useI18n();
const primaryColor = ref("#ff7bac");
const themeOverrides = ref({
common: {
primaryColorPressed: primaryColor,
@ -15,7 +15,7 @@ const themeOverrides = ref({
primaryColor: primaryColor,
primaryColorHover: primaryColor,
},
})
});
</script>
<template>

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 851 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 851 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 272 KiB

After

Width:  |  Height:  |  Size: 874 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

View File

@ -0,0 +1,4 @@
<svg width="23" height="22" viewBox="0 0 23 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.6548 8.9774C13.516 8.84467 13.3681 8.72503 13.2156 8.61409L13.2153 8.61433C13.0584 8.46998 12.8453 8.38107 12.6104 8.38107C12.1283 8.38107 11.7375 8.75491 11.7375 9.21604C11.7375 9.42009 11.8142 9.60698 11.9413 9.75202C11.9413 9.75204 11.9413 9.75208 11.9414 9.75211C12.0024 9.82178 12.0751 9.88184 12.1567 9.92939C12.239 9.99432 12.3198 10.0582 12.3953 10.1305L12.4673 10.1994C13.372 11.0637 13.1019 12.5528 12.1972 13.4182L8.33659 17.1098C7.4319 17.9741 5.96108 17.9741 5.05634 17.1098L4.9838 17.0404C4.07904 16.175 4.07904 14.7671 4.9838 13.9039L6.68938 12.273C6.90822 12.1068 7.0487 11.8504 7.0487 11.5624C7.0487 11.0616 6.62422 10.6556 6.10059 10.6556C5.903 10.6556 5.71958 10.7134 5.5677 10.8123C5.56722 10.8114 5.56671 10.8104 5.56621 10.8095L5.54802 10.8257C5.48089 10.8719 5.42019 10.926 5.36776 10.9871L3.59529 12.5735C1.92917 14.1683 1.92917 16.7766 3.59529 18.3691L3.66726 18.4379C5.33337 20.0305 8.05903 20.0305 9.72514 18.4379L13.5846 14.7452C15.2484 13.1516 15.3895 10.6377 13.7257 9.04513L13.6548 8.9774Z" fill="#FF7BAC"/>
<path d="M19.2173 3.56219L19.1454 3.49335C17.4793 1.89968 14.7536 1.89968 13.0875 3.49335L9.22805 7.18608C7.56193 8.77976 7.47018 11.0811 9.1363 12.6758L9.20711 12.7425C9.28278 12.8149 9.36132 12.8831 9.44149 12.9485C9.49952 13.0105 9.56769 13.0636 9.64347 13.1056C9.64405 13.106 9.64465 13.1065 9.64522 13.1069L9.64547 13.1067C9.76626 13.1731 9.90621 13.2113 10.0556 13.2113C10.5107 13.2113 10.8796 12.8584 10.8796 12.4231C10.8796 12.3002 10.8502 12.1839 10.7978 12.0802C10.6888 11.8462 10.4849 11.7038 10.3438 11.5689L10.273 11.5022C9.3683 10.6368 9.71185 9.37962 10.6165 8.51426L14.4783 4.82259C15.3807 3.9572 16.8521 3.9572 17.7568 4.82259L17.8288 4.89033C18.7335 5.75575 18.7335 7.1642 17.8288 8.0285L16.1288 9.65569C15.8975 9.81959 15.7476 10.0825 15.7476 10.3789C15.7476 10.8757 16.1686 11.2785 16.688 11.2785C16.8688 11.2785 17.0376 11.2296 17.1809 11.145C17.182 11.1468 17.183 11.1484 17.1841 11.1502L17.2104 11.1269C17.2917 11.0749 17.3638 11.011 17.4245 10.938L19.2161 9.35669C20.8834 7.76303 20.8835 5.15584 19.2173 3.56219Z" fill="#FF7BAC"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,397 @@
<template>
<div class="picker-mask" @click.self="emit('close')">
<div class="picker-panel">
<div class="picker-title">
Select Time
<svg
@click="emit('close')"
style="cursor: pointer"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0.666016 9.74935C0.666016 4.59469 4.84469 0.416016 9.99935 0.416016C15.154 0.416016 19.3327 4.59469 19.3327 9.74935C19.3327 14.904 15.154 19.0827 9.99935 19.0827C4.84469 19.0827 0.666016 14.904 0.666016 9.74935Z"
fill="#CCCCCC"
/>
<path
d="M12.833 5.84961C13.1273 5.55596 13.6042 5.55565 13.8965 5.84863C14.1907 6.14223 14.1893 6.61848 13.8965 6.91211L11.0615 9.74609L13.9043 12.5898C14.1973 12.8848 14.1986 13.3607 13.9043 13.6543C13.6114 13.947 13.1344 13.9471 12.8408 13.6543L9.99707 10.8105L7.1582 13.6504C6.86386 13.9444 6.38729 13.9446 6.09375 13.6504C5.8002 13.3574 5.80045 12.8809 6.09473 12.5859L8.93359 9.74707L6.10254 6.91602C5.80956 6.62236 5.80889 6.1452 6.10254 5.85156C6.39486 5.55817 6.87209 5.55802 7.16699 5.85156L9.99805 8.68262L12.833 5.84961Z"
fill="white"
/>
</svg>
</div>
<div
class="picker-columns"
:style="{ height: wheelViewportHeight + 'px' }"
>
<div class="center-lines" :style="{ height: wheelItemHeight + 'px' }">
<div class="line"></div>
<div class="line"></div>
</div>
<div
class="picker-col year-col"
ref="yearColRef"
@scroll.passive="(e) => handleWheelScroll('year', e)"
@wheel.prevent="(e) => handleWheelStep('year', e)"
:style="{
scrollPaddingTop: wheelCenterPad + 'px',
scrollPaddingBottom: wheelCenterPad + 'px',
}"
>
<div :style="{ height: wheelCenterPad + 'px' }"></div>
<div
v-for="y in years"
:key="'y' + y"
class="picker-item"
:class="{ active: y === localYear }"
:style="{
height: wheelItemHeight + 'px',
}"
@click="localYear = y"
>
{{ y }}
</div>
<div :style="{ height: wheelCenterPad + 'px' }"></div>
</div>
<div
class="picker-col month-col"
ref="monthColRef"
@scroll.passive="(e) => handleWheelScroll('month', e)"
@wheel.prevent="(e) => handleWheelStep('month', e)"
:style="{
scrollPaddingTop: wheelCenterPad + 'px',
scrollPaddingBottom: wheelCenterPad + 'px',
}"
>
<div :style="{ height: wheelCenterPad + 'px' }"></div>
<div
v-for="m in months"
:key="'m' + m"
class="picker-item"
:class="{ active: m === localMonth }"
:style="{
height: wheelItemHeight + 'px',
}"
@click="localMonth = m"
>
{{ m }}
</div>
<div :style="{ height: wheelCenterPad + 'px' }"></div>
</div>
<div
class="picker-col day-col"
ref="dayColRef"
@scroll.passive="(e) => handleWheelScroll('day', e)"
@wheel.prevent="(e) => handleWheelStep('day', e)"
:style="{
scrollPaddingTop: wheelCenterPad + 'px',
scrollPaddingBottom: wheelCenterPad + 'px',
}"
>
<div :style="{ height: wheelCenterPad + 'px' }"></div>
<div
v-for="d in daysInMonth(localYear, localMonth)"
:key="'d' + d"
class="picker-item"
:class="{ active: d === localDay }"
:style="{
height: wheelItemHeight + 'px',
}"
@click="localDay = d"
>
{{ d }}
</div>
<div :style="{ height: wheelCenterPad + 'px' }"></div>
</div>
</div>
<div class="picker-actions">
<button class="picker-confirm" @click="confirm">
Confirm Selection
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, nextTick } from "vue";
const props = defineProps({
modelValue: { type: Object, required: true }, // { year, month, day }
minYear: { type: Number, default: 2009 },
maxYear: { type: Number, default: new Date().getFullYear() },
});
const emit = defineEmits(["update:modelValue", "close", "confirm"]);
const wheelItemBase = 8 * 5.12;
const wheelItemHeight = Math.round(wheelItemBase); // use integer px to avoid fractional rounding
const wheelViewportHeight = wheelItemHeight * 5;
const wheelCenterPad = (wheelViewportHeight - wheelItemHeight) / 2;
const isUserScrolling = ref(false);
const years = computed(() =>
Array.from(
{ length: props.maxYear - props.minYear + 1 },
(_, i) => props.minYear + i
)
);
const months = Array.from({ length: 12 }, (_, i) => i + 1);
const localYear = ref(props.modelValue.year);
const localMonth = ref(props.modelValue.month);
const localDay = ref(props.modelValue.day);
watch(
() => props.modelValue,
(v) => {
localYear.value = v.year;
localMonth.value = v.month;
localDay.value = v.day;
nextTick(syncWheelPositions);
}
);
const yearColRef = ref(null);
const monthColRef = ref(null);
const dayColRef = ref(null);
function daysInMonth(year, month) {
return new Date(year, month, 0).getDate();
}
watch([localYear, localMonth], () => {
const dim = daysInMonth(localYear.value, localMonth.value);
if (localDay.value > dim) localDay.value = dim;
if (!isUserScrolling.value) {
nextTick(syncWheelPositions);
}
});
function syncWheelPositions() {
const yearIdx = years.value.indexOf(localYear.value);
const monthIdx = localMonth.value - 1;
const dayIdx = localDay.value - 1;
if (yearColRef.value) yearColRef.value.scrollTop = yearIdx * wheelItemHeight;
if (monthColRef.value)
monthColRef.value.scrollTop = monthIdx * wheelItemHeight;
if (dayColRef.value) dayColRef.value.scrollTop = dayIdx * wheelItemHeight;
}
const scrollTimers = { year: null, month: null, day: null };
const isAnimating = { year: false, month: false, day: false };
function getCenteredIndex(el) {
if (!el) return 0;
const raw = (el.scrollTop - wheelCenterPad) / wheelItemHeight;
return Math.round(raw);
}
function clamp(n, min, max) {
return Math.max(min, Math.min(n, max));
}
function getCenteredIndexByRect(el) {
if (!el) return 0;
const items = el.querySelectorAll(".picker-item");
if (!items || items.length === 0) return 0;
const containerRect = el.getBoundingClientRect();
const centerY = containerRect.top + containerRect.height / 2;
let nearestIdx = 0;
let nearestDist = Number.POSITIVE_INFINITY;
for (let i = 0; i < items.length; i++) {
const r = items[i].getBoundingClientRect();
const mid = r.top + r.height / 2;
const dist = Math.abs(mid - centerY);
if (dist < nearestDist) {
nearestDist = dist;
nearestIdx = i;
}
}
return nearestIdx;
}
function handleWheelStep(type, e) {
const refMap = { year: yearColRef, month: monthColRef, day: dayColRef };
const el = refMap[type].value;
if (!el || isAnimating[type]) return;
const direction = e.deltaY > 0 ? 1 : -1;
let idx = getCenteredIndexByRect(el);
if (type === "year") {
idx = clamp(idx + direction, 0, years.value.length - 1);
localYear.value = years.value[idx];
} else if (type === "month") {
idx = clamp(idx + direction, 0, 11);
localMonth.value = idx + 1;
} else {
const dim = daysInMonth(localYear.value, localMonth.value);
idx = clamp(idx + direction, 0, dim - 1);
localDay.value = idx + 1;
}
isAnimating[type] = true;
isUserScrolling.value = true;
el.scrollTo({ top: idx * wheelItemHeight, behavior: "smooth" });
setTimeout(() => {
isAnimating[type] = false;
isUserScrolling.value = false;
}, 180);
}
function handleWheelScroll(type, e) {
clearTimeout(scrollTimers[type]);
isUserScrolling.value = true;
scrollTimers[type] = setTimeout(() => {
const el = e.target;
const idx = getCenteredIndexByRect(el);
if (type === "year") {
localYear.value = years.value[clamp(idx, 0, years.value.length - 1)];
} else if (type === "month") {
localMonth.value = clamp(idx + 1, 1, 12);
} else if (type === "day") {
const dim = daysInMonth(localYear.value, localMonth.value);
localDay.value = clamp(idx + 1, 1, dim);
}
isUserScrolling.value = false;
}, 120);
}
function confirm() {
// 线 DOM
if (yearColRef.value) {
const idx = clamp(
getCenteredIndexByRect(yearColRef.value),
0,
years.value.length - 1
);
localYear.value = years.value[idx];
}
if (monthColRef.value) {
const idx = clamp(getCenteredIndexByRect(monthColRef.value) + 1, 1, 12);
localMonth.value = idx;
}
if (dayColRef.value) {
const dim = daysInMonth(localYear.value, localMonth.value);
const idx = clamp(getCenteredIndexByRect(dayColRef.value) + 1, 1, dim);
localDay.value = idx;
}
//
const payload = {
year: localYear.value,
month: localMonth.value,
day: localDay.value,
};
emit("update:modelValue", payload);
nextTick(() => emit("confirm", payload));
}
nextTick(syncWheelPositions);
</script>
<style scoped lang="scss">
.picker-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.picker-panel {
width: 311 * 5.12px;
background: #fff;
border-radius: 8 * 5.12px;
box-shadow: 0 3 * 5.12px 14 * 5.12px rgba(0, 0, 0, 0.16);
padding: 16 * 5.12px;
}
.picker-title {
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 14 * 5.12px;
color: #455363;
margin-bottom: 16 * 5.12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-columns {
position: relative;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
column-gap: 32 * 5.12px;
align-items: stretch;
justify-items: stretch;
padding: 0;
}
.center-lines {
pointer-events: none;
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
z-index: 0;
}
.center-lines .line {
position: absolute;
left: 16 * 5.12px; /* align with design padding */
right: 16 * 5.12px;
height: 1px; /* crisp hairline */
background: #ededed;
}
.center-lines .line:first-child {
top: 0;
}
.center-lines .line:last-child {
bottom: 0;
}
.picker-col {
position: relative;
overflow-y: auto;
z-index: 1; /* paint above guide lines */
scroll-snap-type: y mandatory;
overscroll-behavior: contain;
}
.picker-col::-webkit-scrollbar {
width: 0;
height: 0;
}
.picker-item {
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
color: #9da3ad;
display: flex;
justify-content: center;
align-items: center;
scroll-snap-align: center;
scroll-snap-stop: always;
}
.year-col .picker-item {
justify-content: flex-start;
padding-left: 16 * 5.12px;
}
.month-col .picker-item {
justify-content: center;
}
.day-col .picker-item {
justify-content: flex-end;
padding-right: 16 * 5.12px;
}
.picker-item.active {
color: #000;
font-weight: 500;
}
.picker-actions {
margin-top: 16 * 5.12px;
}
.picker-confirm {
width: 100%;
height: 44 * 5.12px;
background: #ff7bac;
color: #fff;
border: none;
border-radius: 8 * 5.12px;
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 14 * 5.12px;
}
</style>

View File

@ -0,0 +1,339 @@
<template>
<div class="picker-mask" @click.self="emit('close')">
<div class="picker-panel">
<div class="picker-title">
Select
<svg
@click="emit('close')"
style="cursor: pointer"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0.666016 9.74935C0.666016 4.59469 4.84469 0.416016 9.99935 0.416016C15.154 0.416016 19.3327 4.59469 19.3327 9.74935C19.3327 14.904 15.154 19.0827 9.99935 19.0827C4.84469 19.0827 0.666016 14.904 0.666016 9.74935Z"
fill="#CCCCCC"
/>
<path
d="M12.833 5.84961C13.1273 5.55596 13.6042 5.55565 13.8965 5.84863C14.1907 6.14223 14.1893 6.61848 13.8965 6.91211L11.0615 9.74609L13.9043 12.5898C14.1973 12.8848 14.1986 13.3607 13.9043 13.6543C13.6114 13.947 13.1344 13.9471 12.8408 13.6543L9.99707 10.8105L7.1582 13.6504C6.86386 13.9444 6.38729 13.9446 6.09375 13.6504C5.8002 13.3574 5.80045 12.8809 6.09473 12.5859L8.93359 9.74707L6.10254 6.91602C5.80956 6.62236 5.80889 6.1452 6.10254 5.85156C6.39486 5.55817 6.87209 5.55802 7.16699 5.85156L9.99805 8.68262L12.833 5.84961Z"
fill="white"
/>
</svg>
</div>
<div
class="picker-columns"
:style="{ height: wheelViewportHeight + 'px' }"
>
<div class="center-lines" :style="{ height: wheelItemHeight + 'px' }">
<div class="line"></div>
<div class="line"></div>
</div>
<div
class="picker-col year-col"
ref="yearColRef"
@scroll.passive="(e) => handleWheelScroll('year', e)"
@wheel.prevent="(e) => handleWheelStep('year', e)"
:style="{
scrollPaddingTop: wheelCenterPad + 'px',
scrollPaddingBottom: wheelCenterPad + 'px',
}"
>
<div :style="{ height: wheelCenterPad + 'px' }"></div>
<div
v-for="y in years"
:key="'y' + y"
class="picker-item"
:class="{ active: y === localYear }"
:style="{
height: wheelItemHeight + 'px',
}"
@click="localYear = y"
>
{{ y }}
</div>
<div :style="{ height: wheelCenterPad + 'px' }"></div>
</div>
<div
class="picker-col month-col"
ref="monthColRef"
@scroll.passive="(e) => handleWheelScroll('month', e)"
@wheel.prevent="(e) => handleWheelStep('month', e)"
:style="{
scrollPaddingTop: wheelCenterPad + 'px',
scrollPaddingBottom: wheelCenterPad + 'px',
}"
>
<div :style="{ height: wheelCenterPad + 'px' }"></div>
<div
v-for="m in months"
:key="'m' + m"
class="picker-item"
:class="{ active: m === localMonth }"
:style="{
height: wheelItemHeight + 'px',
}"
@click="localMonth = m"
>
{{ m }}
</div>
<div :style="{ height: wheelCenterPad + 'px' }"></div>
</div>
</div>
<div class="picker-actions">
<button class="picker-confirm" @click="confirm">
Confirm Selection
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, nextTick } from "vue";
const props = defineProps({
modelValue: { type: Object, required: true }, // { year, month }
minYear: { type: Number, default: 2009 },
maxYear: { type: Number, default: new Date().getFullYear() },
});
const emit = defineEmits(["update:modelValue", "close", "confirm"]);
const wheelItemBase = 8 * 5.12;
const wheelItemHeight = Math.round(wheelItemBase); // use integer px to avoid fractional rounding
const wheelViewportHeight = wheelItemHeight * 5;
const wheelCenterPad = (wheelViewportHeight - wheelItemHeight) / 2;
const isUserScrolling = ref(false);
const years = computed(() =>
Array.from(
{ length: props.maxYear - props.minYear + 1 },
(_, i) => props.minYear + i
)
);
const months = Array.from({ length: 12 }, (_, i) => i + 1);
const localYear = ref(props.modelValue.year);
const localMonth = ref(props.modelValue.month);
watch(
() => props.modelValue,
(v) => {
localYear.value = v.year;
localMonth.value = v.month;
nextTick(syncWheelPositions);
}
);
const yearColRef = ref(null);
const monthColRef = ref(null);
watch([localYear, localMonth], () => {
if (!isUserScrolling.value) {
nextTick(syncWheelPositions);
}
});
function syncWheelPositions() {
const yearIdx = years.value.indexOf(localYear.value);
const monthIdx = localMonth.value - 1;
if (yearColRef.value) yearColRef.value.scrollTop = yearIdx * wheelItemHeight;
if (monthColRef.value)
monthColRef.value.scrollTop = monthIdx * wheelItemHeight;
}
const scrollTimers = { year: null, month: null };
const isAnimating = { year: false, month: false };
function clamp(n, min, max) {
return Math.max(min, Math.min(n, max));
}
function getCenteredIndexByRect(el) {
if (!el) return 0;
const items = el.querySelectorAll(".picker-item");
if (!items || items.length === 0) return 0;
const containerRect = el.getBoundingClientRect();
const centerY = containerRect.top + containerRect.height / 2;
let nearestIdx = 0;
let nearestDist = Number.POSITIVE_INFINITY;
for (let i = 0; i < items.length; i++) {
const r = items[i].getBoundingClientRect();
const mid = r.top + r.height / 2;
const dist = Math.abs(mid - centerY);
if (dist < nearestDist) {
nearestDist = dist;
nearestIdx = i;
}
}
return nearestIdx;
}
function handleWheelStep(type, e) {
const refMap = { year: yearColRef, month: monthColRef };
const el = refMap[type].value;
if (!el || isAnimating[type]) return;
const direction = e.deltaY > 0 ? 1 : -1;
let idx = getCenteredIndexByRect(el);
if (type === "year") {
idx = clamp(idx + direction, 0, years.value.length - 1);
localYear.value = years.value[idx];
} else if (type === "month") {
idx = clamp(idx + direction, 0, 11);
localMonth.value = idx + 1;
}
isAnimating[type] = true;
isUserScrolling.value = true;
el.scrollTo({ top: idx * wheelItemHeight, behavior: "smooth" });
setTimeout(() => {
isAnimating[type] = false;
isUserScrolling.value = false;
}, 180);
}
function handleWheelScroll(type, e) {
clearTimeout(scrollTimers[type]);
isUserScrolling.value = true;
scrollTimers[type] = setTimeout(() => {
const el = e.target;
const idx = getCenteredIndexByRect(el);
if (type === "year") {
localYear.value = years.value[clamp(idx, 0, years.value.length - 1)];
} else if (type === "month") {
localMonth.value = clamp(idx + 1, 1, 12);
}
isUserScrolling.value = false;
}, 120);
}
function confirm() {
if (yearColRef.value) {
const idx = clamp(
getCenteredIndexByRect(yearColRef.value),
0,
years.value.length - 1
);
localYear.value = years.value[idx];
}
if (monthColRef.value) {
const idx = clamp(getCenteredIndexByRect(monthColRef.value) + 1, 1, 12);
localMonth.value = idx;
}
const payload = {
year: localYear.value,
month: localMonth.value,
};
emit("update:modelValue", payload);
nextTick(() => emit("confirm", payload));
}
nextTick(syncWheelPositions);
</script>
<style scoped lang="scss">
.picker-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.picker-panel {
width: 311 * 5.12px;
background: #fff;
border-radius: 8 * 5.12px;
box-shadow: 0 3 * 5.12px 14 * 5.12px rgba(0, 0, 0, 0.16);
padding: 16 * 5.12px;
}
.picker-title {
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 14 * 5.12px;
color: #455363;
margin-bottom: 16 * 5.12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-columns {
position: relative;
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 32 * 5.12px;
align-items: stretch;
justify-items: stretch;
padding: 0;
}
.center-lines {
pointer-events: none;
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
z-index: 0;
}
.center-lines .line {
position: absolute;
left: 16 * 5.12px;
right: 16 * 5.12px;
height: 1 * 5.12px;
background: #ededed;
}
.center-lines .line:first-child {
top: 0;
}
.center-lines .line:last-child {
bottom: 0;
}
.picker-col {
position: relative;
overflow-y: auto;
z-index: 1;
scroll-snap-type: y mandatory;
overscroll-behavior: contain;
}
.picker-col::-webkit-scrollbar {
width: 0;
height: 0;
}
.picker-item {
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
color: #9da3ad;
display: flex;
justify-content: center;
align-items: center;
scroll-snap-align: center;
scroll-snap-stop: always;
}
.year-col .picker-item {
justify-content: flex-start;
padding-left: 16 * 5.12px;
}
.month-col .picker-item {
justify-content: center;
}
.picker-item.active {
color: #000;
font-weight: 500;
}
.picker-actions {
margin-top: 16 * 5.12px;
}
.picker-confirm {
width: 100%;
height: 44 * 5.12px;
background: #ff7bac;
color: #fff;
border: none;
border-radius: 8 * 5.12px;
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 14 * 5.12px;
}
</style>

View File

@ -0,0 +1,286 @@
<template>
<Teleport to="body">
<div class="picker-mask" @click.self="emit('close')">
<div class="picker-panel">
<div class="picker-title">
Select
<svg
@click="emit('close')"
style="cursor: pointer"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0.666016 9.74935C0.666016 4.59469 4.84469 0.416016 9.99935 0.416016C15.154 0.416016 19.3327 4.59469 19.3327 9.74935C19.3327 14.904 15.154 19.0827 9.99935 19.0827C4.84469 19.0827 0.666016 14.904 0.666016 9.74935Z"
fill="#CCCCCC"
/>
<path
d="M12.833 5.84961C13.1273 5.55596 13.6042 5.55565 13.8965 5.84863C14.1907 6.14223 14.1893 6.61848 13.8965 6.91211L11.0615 9.74609L13.9043 12.5898C14.1973 12.8848 14.1986 13.3607 13.9043 13.6543C13.6114 13.947 13.1344 13.9471 12.8408 13.6543L9.99707 10.8105L7.1582 13.6504C6.86386 13.9444 6.38729 13.9446 6.09375 13.6504C5.8002 13.3574 5.80045 12.8809 6.09473 12.5859L8.93359 9.74707L6.10254 6.91602C5.80956 6.62236 5.80889 6.1452 6.10254 5.85156C6.39486 5.55817 6.87209 5.55802 7.16699 5.85156L9.99805 8.68262L12.833 5.84961Z"
fill="white"
/>
</svg>
</div>
<div
class="picker-columns"
:style="{ height: wheelViewportHeight + 'px' }"
>
<div class="center-lines" :style="{ height: wheelItemHeight + 'px' }">
<div class="line"></div>
<div class="line"></div>
</div>
<div
class="picker-col year-col"
ref="yearColRef"
@scroll.passive="handleWheelScroll"
@wheel.prevent="handleWheelStep"
:style="{
scrollPaddingTop: wheelCenterPad + 'px',
scrollPaddingBottom: wheelCenterPad + 'px',
}"
>
<div :style="{ height: wheelCenterPad + 'px' }"></div>
<div
v-for="opt in props.options"
:key="opt.value"
class="picker-item"
:class="{ active: opt.value === localYear }"
:style="{ height: wheelItemHeight + 'px' }"
@click="localYear = opt.value"
>
{{ opt.label }}
</div>
<div :style="{ height: wheelCenterPad + 'px' }"></div>
</div>
</div>
<div class="picker-actions">
<button class="picker-confirm" @click="confirm">
Confirm Selection
</button>
</div>
</div>
</div>
</Teleport>
</template>
<script setup>
import { ref, watch, nextTick, Teleport } from "vue";
const props = defineProps({
modelValue: { type: [String, Number], required: true },
options: { type: Array, required: true },
});
const emit = defineEmits(["update:modelValue", "close", "confirm"]);
const wheelItemBase = 8 * 5.12;
const wheelItemHeight = Math.round(wheelItemBase);
const wheelViewportHeight = wheelItemHeight * 5;
const wheelCenterPad = (wheelViewportHeight - wheelItemHeight) / 2;
const localYear = ref(props.modelValue);
watch(
() => props.modelValue,
(v) => {
localYear.value = v;
nextTick(syncWheelPositions);
}
);
const yearColRef = ref(null);
watch(localYear, () => {
if (!isUserScrolling.value) {
nextTick(syncWheelPositions);
}
});
function syncWheelPositions() {
const yearIdx = props.options.findIndex(
(opt) => opt.value === localYear.value
);
if (yearColRef.value) {
yearColRef.value.scrollTop = yearIdx * wheelItemHeight;
}
}
const scrollTimer = ref(null);
const isAnimating = ref(false);
const isUserScrolling = ref(false);
function clamp(n, min, max) {
return Math.max(min, Math.min(n, max));
}
function getCenteredIndexByRect(el) {
if (!el) return 0;
const items = el.querySelectorAll(".picker-item");
if (!items || items.length === 0) return 0;
const containerRect = el.getBoundingClientRect();
const centerY = containerRect.top + containerRect.height / 2;
let nearestIdx = 0;
let nearestDist = Number.POSITIVE_INFINITY;
for (let i = 0; i < items.length; i++) {
const r = items[i].getBoundingClientRect();
const mid = r.top + r.height / 2;
const dist = Math.abs(mid - centerY);
if (dist < nearestDist) {
nearestDist = dist;
nearestIdx = i;
}
}
return nearestIdx;
}
function handleWheelStep(e) {
const el = yearColRef.value;
if (!el || isAnimating.value) return;
const direction = e.deltaY > 0 ? 1 : -1;
let idx = getCenteredIndexByRect(el);
const options = props.options;
idx = clamp(idx + direction, 0, options.length - 1);
localYear.value = options[idx].value;
isAnimating.value = true;
isUserScrolling.value = true;
el.scrollTo({ top: idx * wheelItemHeight, behavior: "smooth" });
setTimeout(() => {
isAnimating.value = false;
isUserScrolling.value = false;
}, 180);
}
function handleWheelScroll(e) {
clearTimeout(scrollTimer.value);
isUserScrolling.value = true;
scrollTimer.value = setTimeout(() => {
const el = e.target;
const idx = getCenteredIndexByRect(el);
const options = props.options;
localYear.value = options[clamp(idx, 0, options.length - 1)].value;
isUserScrolling.value = false;
}, 120);
}
function confirm() {
if (yearColRef.value) {
const idx = getCenteredIndexByRect(yearColRef.value);
const options = props.options;
if (options[idx]) {
localYear.value = options[idx].value;
}
}
emit("update:modelValue", localYear.value);
nextTick(() => emit("confirm", localYear.value));
}
nextTick(syncWheelPositions);
</script>
<style scoped lang="scss">
.picker-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.picker-panel {
width: 311 * 5.12px;
background: #fff;
border-radius: 8 * 5.12px;
box-shadow: 0 3 * 5.12px 14 * 5.12px rgba(0, 0, 0, 0.16);
padding: 16 * 5.12px;
}
.picker-title {
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 14 * 5.12px;
color: #455363;
margin-bottom: 16 * 5.12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-columns {
position: relative;
display: grid;
grid-template-columns: 1fr;
align-items: stretch;
justify-items: stretch;
padding: 0;
}
.center-lines {
pointer-events: none;
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
z-index: 0;
}
.center-lines .line {
position: absolute;
left: 16 * 5.12px;
right: 16 * 5.12px;
height: 1 * 5.12px;
background: #ededed;
}
.center-lines .line:first-child {
top: 0;
}
.center-lines .line:last-child {
bottom: 0;
}
.picker-col {
position: relative;
overflow-y: auto;
z-index: 1;
scroll-snap-type: y mandatory;
overscroll-behavior: contain;
}
.picker-col::-webkit-scrollbar {
width: 0;
height: 0;
}
.picker-item {
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
color: #9da3ad;
display: flex;
justify-content: center;
align-items: center;
scroll-snap-align: center;
scroll-snap-stop: always;
}
.year-col .picker-item {
justify-content: center;
}
.picker-item.active {
color: #000;
font-weight: 500;
}
.picker-actions {
margin-top: 16 * 5.12px;
}
.picker-confirm {
width: 100%;
height: 44 * 5.12px;
background: #ff7bac;
color: #fff;
border: none;
border-radius: 8 * 5.12px;
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 14 * 5.12px;
}
</style>

View File

@ -1,30 +1,30 @@
<script setup>
import { computed } from 'vue'
import { useWindowSize } from '@vueuse/core'
import { computed } from "vue";
import { useWindowSize } from "@vueuse/core";
import size375 from '@/components/customEcharts/size375/index.vue'
import size768 from '@/components/customEcharts/size375/index.vue'
import size1440 from '@/components/customEcharts/size1920/index.vue'
import size1920 from '@/components/customEcharts/size1920/index.vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import size375 from "@/components/customEcharts/size375/index.vue";
import size768 from "@/components/customEcharts/size768/index.vue";
import size1440 from "@/components/customEcharts/size1440/index.vue";
import size1920 from "@/components/customEcharts/size1920/index.vue";
import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
const router = useRouter()
const { width } = useWindowSize()
const { t } = useI18n()
const router = useRouter();
const { width } = useWindowSize();
const { t } = useI18n();
const viewComponent = computed(() => {
const viewWidth = width.value
if (viewWidth <= 500) {
return size375
} else if (viewWidth <= 960) {
return size768
const viewWidth = width.value;
if (viewWidth <= 450) {
return size375;
} else if (viewWidth <= 1100) {
return size768;
} else if (viewWidth <= 1500) {
return size1440
return size1440;
} else if (viewWidth <= 1920 || viewWidth > 1920) {
return size1920
return size1920;
}
})
});
</script>
<template>

View File

@ -0,0 +1,673 @@
<template>
<div class="custom-echarts">
<div>
<div class="echarts-header">
<!-- 标题区域 -->
<div class="title-section">
<div class="title-decoration"></div>
<div class="stock-title">
<span>{{ t("historic_stock.echarts.title") }}</span>
</div>
</div>
<div class="echarts-search-area">
<div class="echarts-search-byRange">
<text style="font-size: 0.9rem; font-weight: 400; color: #666666">
{{ t("historic_stock.echarts.range") }}
</text>
<div class="search-range-list">
<div
class="search-range-list-each"
v-for="(item, index) in searchRangeOptions"
:key="index"
:class="{ activeRange: state.activeRange === item.key }"
@click="changeSearchRange(item.key)"
>
<span>{{ item.label }}</span>
</div>
</div>
</div>
<div class="echarts-search-byDate">
<n-date-picker
v-model:value="state.dateRange"
type="daterange"
:is-date-disabled="isDateDisabled"
@update:value="handleDateRangeChange"
input-readonly
/>
</div>
</div>
</div>
</div>
<div id="myEcharts" class="myChart"></div>
</div>
</template>
<script setup>
import { onMounted, onBeforeUnmount, watch, reactive, computed } from "vue";
import { useI18n } from "vue-i18n";
import * as echarts from "echarts";
import { NDatePicker, NIcon } from "naive-ui";
import { ArrowForwardOutline } from "@vicons/ionicons5";
import axios from "axios";
const { t, locale } = useI18n();
const state = reactive({
searchRange: ["1m", "3m", "YTD", "1Y", "5Y", "10Y", "Max"],
dateRange: [new Date("2009-10-07").getTime(), new Date().getTime()],
activeRange: "",
});
const searchRangeOptions = computed(() => [
{ label: t("historic_stock.echarts.1m"), key: "1m" },
{ label: t("historic_stock.echarts.3m"), key: "3m" },
{ label: t("historic_stock.echarts.ytd_short"), key: "YTD" },
{ label: t("historic_stock.echarts.1y"), key: "1Y" },
{ label: t("historic_stock.echarts.5y"), key: "5Y" },
{ label: t("historic_stock.echarts.10y"), key: "10Y" },
{ label: t("historic_stock.echarts.max"), key: "Max" },
]);
let myCharts = null;
let historicData = [];
let xAxisData = [];
//eCharts
const initEcharts = (data) => {
historicData = data;
xAxisData = data.map((item) => {
return new Date(item.date).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
});
const yAxisData = data.map((item) => item.price);
// console.error(xAxisData, yAxisData)
// domecharts
myCharts = echarts.init(document.getElementById("myEcharts"), null, {
renderer: "canvas",
useDirtyRect: true,
});
//
myCharts.setOption({
animation: false,
progressive: 500,
progressiveThreshold: 3000,
// title: {
// text: 'FiEE, Inc. Stock Price History',
// },
grid: {
left: "8%", // '2%'
right: "12%", // yylabel
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "line",
label: {
backgroundColor: "#6a7985",
},
},
formatter: function (params) {
const p = params[0];
return `<span style="font-size: 1.1rem; font-weight: 600;">${
p.axisValue
}</span><br/><span style="font-size: 0.9rem; font-weight: 400;">${t(
"historic_stock.echarts.price"
)}: ${p.data}</span>`;
},
triggerOn: "mousemove",
confine: true,
hideDelay: 1500,
},
xAxis: {
data: xAxisData,
type: "category",
boundaryGap: false,
inverse: true,
axisLine: {
lineStyle: {
color: "#CCD6EB",
},
},
axisLabel: {
color: "#323232",
fontWeight: "bold",
interval: "auto",
hideOverlap: true,
},
},
yAxis: {
type: "value",
position: "right",
interval: 25,
// max: 75.0,
show: true,
axisLabel: {
color: "#323232",
fontWeight: "bold",
formatter: function (value) {
return value > 0 ? value.toFixed(2) : value;
},
},
},
series: [
{
data: yAxisData,
type: "line",
sampling: "lttb",
symbol: "none",
lineStyle: {
color: "#CC346C",
},
areaStyle: {
color: {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: "#CC346C",
},
{
offset: 1,
color: "#F4F6F8",
},
],
},
},
markPoint: {
symbol: "circle",
symbolSize: 20,
itemStyle: {
color: {
type: "radial",
x: 0.5,
y: 0.5,
r: 0.5,
colorStops: [
{ offset: 0, color: "#CC346C" },
{ offset: 0.4, color: "white" },
{ offset: 0.4, color: "white" },
{ offset: 0.6, color: "rgba(204, 52, 108, 0.30)" },
{ offset: 0.8, color: "rgba(204, 52, 108, 0.30)" },
{ offset: 1, color: "rgba(255, 123, 172, 0)" },
],
},
},
data: [],
},
progressive: 500,
progressiveThreshold: 3000,
large: true,
largeThreshold: 2000,
},
],
dataZoom: [
{
type: "inside",
},
{
type: "slider",
show: true,
dataBackground: {
lineStyle: {
color: "#CC346C",
},
areaStyle: {
color: {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 1, color: "#CC346C" },
{ offset: 0, color: "#F4F6F8" },
],
},
},
},
selectedDataBackground: {
lineStyle: {
color: "#CC346C",
},
areaStyle: {
color: {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 1, color: "#CC346C" },
{ offset: 0, color: "#F4F6F8" },
],
},
},
},
fillerColor: "rgba(44, 98, 136, 0.3)",
realtime: false,
},
],
});
// showTip markPoint
myCharts.on("showTip", function (params) {
if (params) {
const dataIndex = params.dataIndex;
const x = myCharts.getOption().xAxis[0].data[dataIndex];
const y = myCharts.getOption().series[0].data[dataIndex];
myCharts.setOption({
series: [
{
markPoint: {
data: [{ coord: [x, y] }],
},
},
],
});
}
});
// markPoint
myCharts.on("globalout", function () {
myCharts.setOption({
series: [
{
markPoint: {
data: [],
},
},
],
});
});
myCharts.on("dataZoom", function (params) {
// dataZoom
const option = myCharts.getOption();
const xAxisData = option.xAxis[0].data;
const dataZoom = option.dataZoom[1] || option.dataZoom[0];
// dataZoom startValue endValue
let startValue = dataZoom.endValue;
let endValue = dataZoom.startValue;
//
if (typeof startValue === "number") {
startValue = xAxisData[startValue];
}
if (typeof endValue === "number") {
endValue = xAxisData[endValue];
}
//
state.dateRange = [
new Date(startValue).getTime(),
new Date(endValue).getTime(),
];
});
};
//
const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
//
const handleResize = () => {
if (myCharts) {
myCharts.resize();
}
};
// resize
const debouncedResize = debounce(handleResize, 300);
onMounted(() => {
getHistoricalData();
// resize
window.addEventListener("resize", debouncedResize);
});
//
onBeforeUnmount(() => {
// resize
window.removeEventListener("resize", debouncedResize);
// echarts
if (myCharts) {
myCharts.dispose();
myCharts = null;
}
});
//
const getHistoricalData = async () => {
let now = new Date();
let toDate =
now.getFullYear() +
"-" +
String(now.getMonth() + 1).padStart(2, "0") +
"-" +
String(now.getDate()).padStart(2, "0");
let url =
"https://common.szjixun.cn/api/stock/history/base/list?from=2009-10-07&to=" +
toDate;
const res = await axios.get(url);
if (res.status === 200) {
if (res.data.status === 0) {
initEcharts(res.data.data);
}
}
};
//
function findClosestDateIndex(data, targetDateStr) {
let left = 0,
right = data.length - 1;
const target = new Date(targetDateStr).getTime();
let res = data.length - 1; //
while (left <= right) {
const mid = Math.floor((left + right) / 2);
const midTime = new Date(data[mid].date).getTime();
if (midTime > target) {
left = mid + 1;
} else {
res = mid;
right = mid - 1;
}
}
return res;
}
//
function findClosestDateIndexDescLeft(data, targetDateStr) {
let left = 0,
right = data.length - 1;
const target = new Date(targetDateStr).getTime();
let res = -1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
const midTime = new Date(data[mid].date).getTime();
if (midTime > target) {
left = mid + 1; // mid
} else {
res = mid; // mid <= target
right = mid - 1;
}
}
return res;
}
//
const changeSearchRange = (range, dateTime) => {
state.activeRange = range;
const now = new Date();
let startDate = "";
let endDate = "";
if (range === "1m") {
const last = new Date(now);
last.setMonth(now.getMonth() - 1);
startDate = last.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "3m") {
const last = new Date(now);
last.setMonth(now.getMonth() - 3);
startDate = last.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "YTD") {
startDate = new Date(now.getFullYear(), 0, 1).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "1Y") {
const last = new Date(now);
last.setFullYear(now.getFullYear() - 1);
startDate = last.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "5Y") {
const last = new Date(now);
last.setFullYear(now.getFullYear() - 5);
startDate = last.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "10Y") {
const last = new Date(now);
last.setFullYear(now.getFullYear() - 10);
startDate = last.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "Max") {
startDate = "";
endDate = "";
} else if (range === "startDateTime") {
startDate = dateTime;
endDate = "";
} else if (range === "endDateTime") {
startDate = "";
endDate = dateTime;
}
if (startDate || endDate) {
// historicData xAxisData initEcharts
if (
typeof historicData !== "undefined" &&
typeof xAxisData !== "undefined"
) {
const zoomOptions = {};
let newStartTs = state.dateRange[0];
let newEndTs = state.dateRange[1];
if (startDate) {
const idx = findClosestDateIndex(historicData, startDate);
const startValue = new Date(historicData[idx].date).toLocaleDateString(
"en-US",
{
month: "short",
day: "numeric",
year: "numeric",
}
);
zoomOptions.endValue = startValue;
newStartTs = new Date(startValue).getTime();
}
if (endDate) {
const idx = findClosestDateIndexDescLeft(historicData, endDate);
const endValue = new Date(historicData[idx].date).toLocaleDateString(
"en-US",
{
month: "short",
day: "numeric",
year: "numeric",
}
);
zoomOptions.startValue = endValue;
newEndTs = new Date(endValue).getTime();
}
if (Object.keys(zoomOptions).length > 0) {
myCharts.setOption({ dataZoom: [zoomOptions, zoomOptions] });
}
state.dateRange = [newStartTs, newEndTs];
}
} else {
myCharts.setOption({
dataZoom: {
startValue: "",
endValue: "",
},
});
state.dateRange = [new Date("2009-10-07").getTime(), new Date().getTime()];
}
};
//
const isDateDisabled = (ts, type, range) => {
const minDate = new Date("2009-10-06").getTime();
const maxDate = new Date().getTime();
if (ts < minDate || ts > maxDate) {
return true;
}
if (type === "end" && range && range[0]) {
return ts < range[0];
}
return false;
};
//
const handleDateRangeChange = (range) => {
if (range && range[0] && range[1]) {
const startDate = new Date(range[0]).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
const endDate = new Date(range[1]).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
changeSearchRange("startDateTime", startDate);
changeSearchRange("endDateTime", endDate);
}
};
</script>
<style lang="scss" scoped>
.custom-echarts {
.myChart {
width: 100%;
height: 25rem;
}
.echarts-header {
.title-section {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 32px;
padding: 0 16px;
}
.title-decoration {
width: 58px;
height: 7px;
background: #ff7bac;
margin: auto 0;
margin-top: 43px;
}
.stock-title {
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 40px;
line-height: 1.4em;
letter-spacing: 3%;
color: #000000;
}
.echarts-search-area {
padding: 0 16px 0 16px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
.echarts-search-byRange {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
.search-range-list {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
.search-range-list-each {
padding: 5px 10px;
border-radius: 5px;
background-color: #f3f4f6;
cursor: pointer;
span {
font-size: 0.9rem;
}
}
.activeRange {
color: #fff;
background: #ff7bac;
}
}
}
.echarts-search-byDate {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 0.4rem;
}
}
}
}
</style>

View File

@ -2,42 +2,36 @@
<div class="custom-echarts">
<div>
<div class="echarts-header">
<div class="echarts-header-title">
<span>FiEE, Inc. Stock Price History</span>
<!-- 标题区域 -->
<div class="title-section">
<div class="title-decoration"></div>
<div class="stock-title">
<span>{{ t("historic_stock.echarts.title") }}</span>
</div>
</div>
<div class="echarts-search-area">
<div class="echarts-search-byRange">
<text style="font-size: 0.9rem; font-weight: 400; color: #666666;">
Range
<text style="font-size: 0.9rem; font-weight: 400; color: #666666">
{{ t("historic_stock.echarts.range") }}
</text>
<div class="search-range-list">
<div
class="search-range-list-each"
v-for="(item, index) in state.searchRange"
v-for="(item, index) in searchRangeOptions"
:key="index"
@click="changeSearchRange(item)"
:class="{ activeRange: state.activeRange === item.key }"
@click="changeSearchRange(item.key)"
>
<span>{{ item }}</span>
<span>{{ item.label }}</span>
</div>
</div>
</div>
<div class="echarts-search-byDate">
<n-date-picker
v-model:value="state.selectHistoricStartDate"
type="date"
:is-date-disabled="disableAfterDate"
@update:value="changeSearchRangeStartDate"
input-readonly
/>
<!-- <n-icon size="16">
<ArrowForwardOutline />
</n-icon> -->
<span>to</span>
<n-date-picker
v-model:value="state.selectHistoricEndDate"
type="date"
:is-date-disabled="disablePreviousDate"
@update:value="changeSearchRangeEndDate"
v-model:value="state.dateRange"
type="daterange"
:is-date-disabled="isDateDisabled"
@update:value="handleDateRangeChange"
input-readonly
/>
</div>
@ -48,40 +42,50 @@
</div>
</template>
<script setup>
import { onMounted, watch, reactive } from 'vue'
import * as echarts from 'echarts'
import markPointerIcon from '@/assets/image/icon/echarts_markPointer.png'
import axios from 'axios'
import { NDatePicker, NIcon } from 'naive-ui'
import { ArrowForwardOutline } from '@vicons/ionicons5'
import { onMounted, onBeforeUnmount, watch, reactive, computed } from "vue";
import * as echarts from "echarts";
import { NDatePicker, NIcon } from "naive-ui";
import { ArrowForwardOutline } from "@vicons/ionicons5";
import axios from "axios";
import { useI18n } from "vue-i18n";
const { t, locale } = useI18n();
const state = reactive({
searchRange: ['1m', '3m', 'YTD', '1Y', '5Y', '10Y', 'Max'],
selectHistoricStartDate: '2009-10-07',
selectHistoricEndDate: new Date(),
})
searchRange: ["1m", "3m", "YTD", "1Y", "5Y", "10Y", "Max"],
dateRange: [new Date("2009-10-07").getTime(), new Date().getTime()],
activeRange: "",
});
let myCharts = null
let historicData = []
let xAxisData = []
const searchRangeOptions = computed(() => [
{ label: t("historic_stock.echarts.1m"), key: "1m" },
{ label: t("historic_stock.echarts.3m"), key: "3m" },
{ label: t("historic_stock.echarts.ytd_short"), key: "YTD" },
{ label: t("historic_stock.echarts.1y"), key: "1Y" },
{ label: t("historic_stock.echarts.5y"), key: "5Y" },
{ label: t("historic_stock.echarts.10y"), key: "10Y" },
{ label: t("historic_stock.echarts.max"), key: "Max" },
]);
let myCharts = null;
let historicData = [];
let xAxisData = [];
//eCharts
const initEcharts = (data) => {
historicData = data
historicData = data;
xAxisData = data.map((item) => {
return new Date(item.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})
})
const yAxisData = data.map((item) => item.price)
return new Date(item.date).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
});
const yAxisData = data.map((item) => item.price);
// console.error(xAxisData, yAxisData)
// domecharts
myCharts = echarts.init(document.getElementById('myEcharts'), null, {
renderer: 'canvas',
useDirtyRect: true
})
myCharts = echarts.init(document.getElementById("myEcharts"), null, {
renderer: "canvas",
useDirtyRect: true,
});
//
myCharts.setOption({
animation: false,
@ -91,68 +95,72 @@ const initEcharts = (data) => {
// text: 'FiEE, Inc. Stock Price History',
// },
grid: {
left: '8%', // '2%'
right: '12%', // yylabel
left: "8%", // '2%'
right: "12%", // yylabel
},
tooltip: {
trigger: 'axis',
trigger: "axis",
axisPointer: {
type: 'line',
type: "line",
label: {
backgroundColor: '#6a7985',
backgroundColor: "#6a7985",
},
},
formatter: function (params) {
const p = params[0]
return `<span style="font-size: 1.1rem; font-weight: 600;">${p.axisValue}</span><br/><span style="font-size: 0.9rem; font-weight: 400;">Price: ${p.data}</span>`
const p = params[0];
return `<span style="font-size: 1.1rem; font-weight: 600;">${
p.axisValue
}</span><br/><span style="font-size: 0.9rem; font-weight: 400;">${t(
"historic_stock.echarts.price"
)}: ${p.data}</span>`;
},
triggerOn: 'mousemove',
triggerOn: "mousemove",
confine: true,
hideDelay: 1500
hideDelay: 1500,
},
xAxis: {
data: xAxisData,
type: 'category',
type: "category",
boundaryGap: false,
inverse: true,
axisLine: {
lineStyle: {
color: '#CCD6EB',
color: "#CCD6EB",
},
},
axisLabel: {
color: '#323232',
fontWeight: 'bold',
interval: 'auto',
hideOverlap: true
color: "#323232",
fontWeight: "bold",
interval: "auto",
hideOverlap: true,
},
},
yAxis: {
type: 'value',
position: 'right',
type: "value",
position: "right",
interval: 25,
// max: 75.0,
show: true,
axisLabel: {
color: '#323232',
fontWeight: 'bold',
color: "#323232",
fontWeight: "bold",
formatter: function (value) {
return value > 0 ? value.toFixed(2) : value
return value > 0 ? value.toFixed(2) : value;
},
},
},
series: [
{
data: yAxisData,
type: 'line',
sampling: 'lttb',
symbol: 'none',
type: "line",
sampling: "lttb",
symbol: "none",
lineStyle: {
color: '#2c6288',
color: "#CC346C",
},
areaStyle: {
color: {
type: 'linear',
type: "linear",
x: 0,
y: 0,
x2: 0,
@ -160,387 +168,427 @@ const initEcharts = (data) => {
colorStops: [
{
offset: 0,
color: '#2c6288',
color: "#CC346C",
},
{
offset: 1,
color: '#F4F6F8',
color: "#F4F6F8",
},
],
},
},
markPoint: {
symbol: 'image://' + markPointerIcon,
symbolSize: 24,
symbol: "circle",
symbolSize: 20,
itemStyle: {
color: {
type: "radial",
x: 0.5,
y: 0.5,
r: 0.5,
colorStops: [
{ offset: 0, color: "#CC346C" },
{ offset: 0.4, color: "white" },
{ offset: 0.4, color: "white" },
{ offset: 0.6, color: "rgba(204, 52, 108, 0.30)" },
{ offset: 0.8, color: "rgba(204, 52, 108, 0.30)" },
{ offset: 1, color: "rgba(255, 123, 172, 0)" },
],
},
},
data: [],
},
progressive: 500,
progressiveThreshold: 3000,
large: true,
largeThreshold: 2000
largeThreshold: 2000,
},
],
dataZoom: [
{
type: 'inside',
type: "inside",
},
{
type: 'slider',
type: "slider",
show: true,
dataBackground: {
lineStyle: {
color: '#2C6288',
color: "#CC346C",
},
areaStyle: {
color: {
type: 'linear',
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 1, color: '#2c6288' },
{ offset: 0, color: '#F4F6F8' },
{ offset: 1, color: "#CC346C" },
{ offset: 0, color: "#F4F6F8" },
],
},
},
},
selectedDataBackground: {
lineStyle: {
color: '#2C6288',
color: "#CC346C",
},
areaStyle: {
color: {
type: 'linear',
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 1, color: '#2c6288' },
{ offset: 0, color: '#F4F6F8' },
{ offset: 1, color: "#CC346C" },
{ offset: 0, color: "#F4F6F8" },
],
},
},
},
fillerColor: 'rgba(44, 98, 136, 0.3)',
fillerColor: "rgba(44, 98, 136, 0.3)",
realtime: false,
},
],
})
});
// showTip markPoint
myCharts.on('showTip', function (params) {
myCharts.on("showTip", function (params) {
if (params) {
const dataIndex = params.dataIndex
const x = myCharts.getOption().xAxis[0].data[dataIndex]
const y = myCharts.getOption().series[0].data[dataIndex]
const dataIndex = params.dataIndex;
const x = myCharts.getOption().xAxis[0].data[dataIndex];
const y = myCharts.getOption().series[0].data[dataIndex];
myCharts.setOption({
series: [
{
markPoint: {
symbol: 'image://' + markPointerIcon,
symbolSize: 24,
data: [{ coord: [x, y] }],
},
},
],
})
});
}
})
});
// markPoint
myCharts.on('globalout', function () {
myCharts.on("globalout", function () {
myCharts.setOption({
series: [
{
markPoint: {
symbol: 'image://' + markPointerIcon,
symbolSize: 24,
data: [],
},
},
],
})
})
myCharts.on('dataZoom', function (params) {
});
});
myCharts.on("dataZoom", function (params) {
// dataZoom
const option = myCharts.getOption()
const xAxisData = option.xAxis[0].data
const dataZoom = option.dataZoom[1] || option.dataZoom[0]
const option = myCharts.getOption();
const xAxisData = option.xAxis[0].data;
const dataZoom = option.dataZoom[1] || option.dataZoom[0];
// dataZoom startValue endValue
let startValue = dataZoom.endValue
let endValue = dataZoom.startValue
let startValue = dataZoom.endValue;
let endValue = dataZoom.startValue;
//
if (typeof startValue === 'number') {
startValue = xAxisData[startValue]
if (typeof startValue === "number") {
startValue = xAxisData[startValue];
}
if (typeof endValue === 'number') {
endValue = xAxisData[endValue]
if (typeof endValue === "number") {
endValue = xAxisData[endValue];
}
//
state.selectHistoricStartDate = new Date(startValue)
state.selectHistoricEndDate = new Date(endValue)
})
}
state.dateRange = [
new Date(startValue).getTime(),
new Date(endValue).getTime(),
];
});
};
//
const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
//
const handleResize = () => {
if (myCharts) {
myCharts.resize();
}
};
// resize
const debouncedResize = debounce(handleResize, 300);
onMounted(() => {
getHistoricalData()
})
getHistoricalData();
// resize
window.addEventListener("resize", debouncedResize);
});
//
onBeforeUnmount(() => {
// resize
window.removeEventListener("resize", debouncedResize);
// echarts
if (myCharts) {
myCharts.dispose();
myCharts = null;
}
});
//
const getHistoricalData = async () => {
let now = new Date()
let now = new Date();
let toDate =
now.getFullYear() +
'-' +
String(now.getMonth() + 1).padStart(2, '0') +
'-' +
String(now.getDate()).padStart(2, '0')
"-" +
String(now.getMonth() + 1).padStart(2, "0") +
"-" +
String(now.getDate()).padStart(2, "0");
let url =
'https://common.szjixun.cn/api/stock/history/base/list?from=2009-10-07&to=' +
toDate
const res = await axios.get(url)
"https://common.szjixun.cn/api/stock/history/base/list?from=2009-10-07&to=" +
toDate;
const res = await axios.get(url);
if (res.status === 200) {
if (res.data.status === 0) {
initEcharts(res.data.data)
initEcharts(res.data.data);
}
}
}
};
//
function findClosestDateIndex(data, targetDateStr) {
let left = 0,
right = data.length - 1
const target = new Date(targetDateStr).getTime()
let res = data.length - 1 //
right = data.length - 1;
const target = new Date(targetDateStr).getTime();
let res = data.length - 1; //
while (left <= right) {
const mid = Math.floor((left + right) / 2)
const midTime = new Date(data[mid].date).getTime()
const mid = Math.floor((left + right) / 2);
const midTime = new Date(data[mid].date).getTime();
if (midTime > target) {
left = mid + 1
left = mid + 1;
} else {
res = mid
right = mid - 1
res = mid;
right = mid - 1;
}
}
return res
return res;
}
//
function findClosestDateIndexDescLeft(data, targetDateStr) {
let left = 0,
right = data.length - 1
const target = new Date(targetDateStr).getTime()
let res = -1
right = data.length - 1;
const target = new Date(targetDateStr).getTime();
let res = -1;
while (left <= right) {
const mid = Math.floor((left + right) / 2)
const midTime = new Date(data[mid].date).getTime()
const mid = Math.floor((left + right) / 2);
const midTime = new Date(data[mid].date).getTime();
if (midTime > target) {
left = mid + 1 // mid
left = mid + 1; // mid
} else {
res = mid // mid <= target
right = mid - 1
res = mid; // mid <= target
right = mid - 1;
}
}
return res
return res;
}
//
const changeSearchRange = (range, dateTime) => {
const now = new Date()
let startDate = ''
let endDate = ''
if (range === '1m') {
const last = new Date(now)
last.setMonth(now.getMonth() - 1)
startDate = last.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})
endDate = new Date(new Date()).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})
} else if (range === '3m') {
const last = new Date(now)
last.setMonth(now.getMonth() - 3)
startDate = last.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})
endDate = new Date(new Date()).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})
} else if (range === 'YTD') {
startDate = new Date(now.getFullYear(), 0, 1).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})
endDate = new Date(new Date()).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})
} else if (range === '1Y') {
const last = new Date(now)
last.setFullYear(now.getFullYear() - 1)
startDate = last.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})
endDate = new Date(new Date()).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})
} else if (range === '5Y') {
const last = new Date(now)
last.setFullYear(now.getFullYear() - 5)
startDate = last.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})
endDate = new Date(new Date()).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})
} else if (range === '10Y') {
const last = new Date(now)
last.setFullYear(now.getFullYear() - 10)
startDate = last.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})
endDate = new Date(new Date()).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})
} else if (range === 'Max') {
startDate = ''
endDate = ''
} else if (range === 'startDateTime') {
startDate = dateTime
endDate = ''
} else if (range === 'endDateTime') {
startDate = ''
endDate = dateTime
state.activeRange = range;
const now = new Date();
let startDate = "";
let endDate = "";
if (range === "1m") {
const last = new Date(now);
last.setMonth(now.getMonth() - 1);
startDate = last.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "3m") {
const last = new Date(now);
last.setMonth(now.getMonth() - 3);
startDate = last.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "YTD") {
startDate = new Date(now.getFullYear(), 0, 1).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "1Y") {
const last = new Date(now);
last.setFullYear(now.getFullYear() - 1);
startDate = last.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "5Y") {
const last = new Date(now);
last.setFullYear(now.getFullYear() - 5);
startDate = last.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "10Y") {
const last = new Date(now);
last.setFullYear(now.getFullYear() - 10);
startDate = last.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "Max") {
startDate = "";
endDate = "";
} else if (range === "startDateTime") {
startDate = dateTime;
endDate = "";
} else if (range === "endDateTime") {
startDate = "";
endDate = dateTime;
}
if (startDate || endDate) {
// historicData xAxisData initEcharts
if (
typeof historicData !== 'undefined' &&
typeof xAxisData !== 'undefined'
typeof historicData !== "undefined" &&
typeof xAxisData !== "undefined"
) {
let startValue = xAxisData[0]
if (startDate) {
const idx = findClosestDateIndex(historicData, startDate)
// historicData[idx].date xAxisData
startValue = new Date(historicData[idx].date).toLocaleDateString(
'en-US',
{
month: 'short',
day: 'numeric',
year: 'numeric',
},
)
}
let endValue = endDate
if (endDate) {
// console.warn(endDate)
const idx = findClosestDateIndexDescLeft(historicData, endDate)
// console.warn(idx)
// historicData[idx].date xAxisData
endValue = new Date(historicData[idx].date).toLocaleDateString(
'en-US',
{
month: 'short',
day: 'numeric',
year: 'numeric',
},
)
// console.warn(endValue)
}
const zoomOptions = {};
let newStartTs = state.dateRange[0];
let newEndTs = state.dateRange[1];
if (startDate) {
myCharts.setOption({
dataZoom: {
endValue: startValue,
},
})
state.selectHistoricStartDate = new Date(startValue)
const idx = findClosestDateIndex(historicData, startDate);
const startValue = new Date(historicData[idx].date).toLocaleDateString(
"en-US",
{
month: "short",
day: "numeric",
year: "numeric",
}
);
zoomOptions.endValue = startValue;
newStartTs = new Date(startValue).getTime();
}
if (endDate) {
myCharts.setOption({
dataZoom: {
startValue: endValue,
},
})
state.selectHistoricEndDate = new Date(endValue)
const idx = findClosestDateIndexDescLeft(historicData, endDate);
const endValue = new Date(historicData[idx].date).toLocaleDateString(
"en-US",
{
month: "short",
day: "numeric",
year: "numeric",
}
);
zoomOptions.startValue = endValue;
newEndTs = new Date(endValue).getTime();
}
if (Object.keys(zoomOptions).length > 0) {
myCharts.setOption({ dataZoom: [zoomOptions, zoomOptions] });
}
state.dateRange = [newStartTs, newEndTs];
}
} else {
myCharts.setOption({
dataZoom: {
startValue: '',
endValue: '',
startValue: "",
endValue: "",
},
})
});
state.selectHistoricStartDate = new Date('2009-10-07')
state.selectHistoricEndDate = new Date()
state.dateRange = [new Date("2009-10-07").getTime(), new Date().getTime()];
}
}
};
// 2009-10-07
const disableAfterDate = (date) => {
return date < new Date('2009-10-06') || date > new Date()
}
//
const isDateDisabled = (ts, type, range) => {
const minDate = new Date("2009-10-06").getTime();
const maxDate = new Date().getTime();
//
const disablePreviousDate = (date) => {
return date < new Date(state.selectHistoricStartDate) || date > new Date()
}
if (ts < minDate || ts > maxDate) {
return true;
}
//
const changeSearchRangeStartDate = (date) => {
// console.error(date)
changeSearchRange(
'startDateTime',
new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
}),
)
}
if (type === "end" && range && range[0]) {
return ts < range[0];
}
//
const changeSearchRangeEndDate = (date) => {
// console.error(date)
changeSearchRange(
'endDateTime',
new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
}),
)
}
return false;
};
//
const handleDateRangeChange = (range) => {
if (range && range[0] && range[1]) {
const startDate = new Date(range[0]).toLocaleDateString(locale.value, {
month: "short",
day: "numeric",
year: "numeric",
});
const endDate = new Date(range[1]).toLocaleDateString(locale.value, {
month: "short",
day: "numeric",
year: "numeric",
});
changeSearchRange("startDateTime", startDate);
changeSearchRange("endDateTime", endDate);
}
};
</script>
<style lang="scss" scoped>
.custom-echarts {
@ -550,19 +598,38 @@ const changeSearchRangeEndDate = (date) => {
}
.echarts-header {
.echarts-header-title {
span {
font-size: 2rem;
font-weight: 600;
color: #323232;
}
.title-section {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 32px;
padding: 0 16px;
}
.title-decoration {
width: 58px;
height: 7px;
background: #ff7bac;
margin: auto 0;
margin-top: 43px;
}
.stock-title {
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 40px;
line-height: 1.4em;
letter-spacing: 3%;
color: #000000;
}
.echarts-search-area {
padding: 2rem 0 0;
padding: 0 16px 0 16px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
.echarts-search-byRange {
display: flex;
@ -586,6 +653,10 @@ const changeSearchRangeEndDate = (date) => {
font-size: 0.9rem;
}
}
.activeRange {
color: #fff;
background: #ff7bac;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,679 @@
<template>
<div class="custom-echarts">
<div>
<div class="echarts-header">
<!-- 标题区域 -->
<div class="title-section">
<div class="title-decoration"></div>
<div class="title-text">
<span>{{ t("historic_stock.echarts.title") }}</span>
</div>
</div>
<div class="echarts-search-area">
<div class="echarts-search-byRange">
<text style="font-size: 0.9rem; font-weight: 400; color: #666666">
{{ t("historic_stock.echarts.range") }}
</text>
<div class="search-range-list">
<div
class="search-range-list-each"
v-for="(item, index) in searchRangeOptions"
:key="index"
:class="{ activeRange: state.activeRange === item.key }"
@click="changeSearchRange(item.key)"
>
<span>{{ item.label }}</span>
</div>
</div>
</div>
<div class="echarts-search-byDate">
<n-date-picker
v-model:value="state.dateRange"
type="daterange"
:is-date-disabled="isDateDisabled"
@update:value="handleDateRangeChange"
input-readonly
/>
</div>
</div>
</div>
</div>
<div id="myEcharts" class="myChart"></div>
</div>
</template>
<script setup>
import { onMounted, onBeforeUnmount, watch, reactive, computed } from "vue";
import { useI18n } from "vue-i18n";
import * as echarts from "echarts";
import { NDatePicker, NIcon } from "naive-ui";
import { ArrowForwardOutline } from "@vicons/ionicons5";
import axios from "axios";
const { t, locale } = useI18n();
const state = reactive({
searchRange: ["1m", "3m", "YTD", "1Y", "5Y", "10Y", "Max"],
dateRange: [new Date("2009-10-07").getTime(), new Date().getTime()],
activeRange: "",
});
const searchRangeOptions = computed(() => [
{ label: t("historic_stock.echarts.1m"), key: "1m" },
{ label: t("historic_stock.echarts.3m"), key: "3m" },
{ label: t("historic_stock.echarts.ytd_short"), key: "YTD" },
{ label: t("historic_stock.echarts.1y"), key: "1Y" },
{ label: t("historic_stock.echarts.5y"), key: "5Y" },
{ label: t("historic_stock.echarts.10y"), key: "10Y" },
{ label: t("historic_stock.echarts.max"), key: "Max" },
]);
let myCharts = null;
let historicData = [];
let xAxisData = [];
//eCharts
const initEcharts = (data) => {
historicData = data;
xAxisData = data.map((item) => {
return new Date(item.date).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
});
const yAxisData = data.map((item) => item.price);
// console.error(xAxisData, yAxisData)
// domecharts
myCharts = echarts.init(document.getElementById("myEcharts"), null, {
renderer: "canvas",
useDirtyRect: true,
});
//
myCharts.setOption({
animation: false,
progressive: 500,
progressiveThreshold: 3000,
// title: {
// text: 'FiEE, Inc. Stock Price History',
// },
grid: {
left: "8%", // '2%'
right: "12%", // yylabel
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "line",
label: {
backgroundColor: "#6a7985",
},
},
formatter: function (params) {
const p = params[0];
return `<span style="font-size: 1.1rem; font-weight: 600;">${
p.axisValue
}</span><br/><span style="font-size: 0.9rem; font-weight: 400;">${t(
"historic_stock.echarts.price"
)}: ${p.data}</span>`;
},
triggerOn: "mousemove",
confine: true,
hideDelay: 1500,
},
xAxis: {
data: xAxisData,
type: "category",
boundaryGap: false,
inverse: true,
axisLine: {
lineStyle: {
color: "#CCD6EB",
},
},
axisLabel: {
color: "#323232",
fontWeight: "bold",
interval: "auto",
hideOverlap: true,
},
},
yAxis: {
type: "value",
position: "right",
interval: 25,
// max: 75.0,
show: true,
axisLabel: {
color: "#323232",
fontWeight: "bold",
formatter: function (value) {
return value > 0 ? value.toFixed(2) : value;
},
},
},
series: [
{
data: yAxisData,
type: "line",
sampling: "lttb",
symbol: "none",
lineStyle: {
color: "#CC346C",
},
areaStyle: {
color: {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: "#CC346C",
},
{
offset: 1,
color: "#F4F6F8",
},
],
},
},
markPoint: {
symbol: "circle",
symbolSize: 20,
itemStyle: {
color: {
type: "radial",
x: 0.5,
y: 0.5,
r: 0.5,
colorStops: [
{ offset: 0, color: "#CC346C" },
{ offset: 0.4, color: "white" },
{ offset: 0.4, color: "white" },
{ offset: 0.6, color: "rgba(204, 52, 108, 0.30)" },
{ offset: 0.8, color: "rgba(204, 52, 108, 0.30)" },
{ offset: 1, color: "rgba(255, 123, 172, 0)" },
],
},
},
data: [],
},
progressive: 500,
progressiveThreshold: 3000,
large: true,
largeThreshold: 2000,
},
],
dataZoom: [
{
type: "inside",
},
{
type: "slider",
show: true,
dataBackground: {
lineStyle: {
color: "#CC346C",
},
areaStyle: {
color: {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 1, color: "#CC346C" },
{ offset: 0, color: "#F4F6F8" },
],
},
},
},
selectedDataBackground: {
lineStyle: {
color: "#CC346C",
},
areaStyle: {
color: {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 1, color: "#CC346C" },
{ offset: 0, color: "#F4F6F8" },
],
},
},
},
fillerColor: "rgba(44, 98, 136, 0.3)",
realtime: false,
},
],
});
// showTip markPoint
myCharts.on("showTip", function (params) {
if (params) {
const dataIndex = params.dataIndex;
const x = myCharts.getOption().xAxis[0].data[dataIndex];
const y = myCharts.getOption().series[0].data[dataIndex];
myCharts.setOption({
series: [
{
markPoint: {
data: [{ coord: [x, y] }],
},
},
],
});
}
});
// markPoint
myCharts.on("globalout", function () {
myCharts.setOption({
series: [
{
markPoint: {
data: [],
},
},
],
});
});
myCharts.on("dataZoom", function (params) {
// dataZoom
const option = myCharts.getOption();
const xAxisData = option.xAxis[0].data;
const dataZoom = option.dataZoom[1] || option.dataZoom[0];
// dataZoom startValue endValue
let startValue = dataZoom.endValue;
let endValue = dataZoom.startValue;
//
if (typeof startValue === "number") {
startValue = xAxisData[startValue];
}
if (typeof endValue === "number") {
endValue = xAxisData[endValue];
}
//
state.dateRange = [
new Date(startValue).getTime(),
new Date(endValue).getTime(),
];
});
};
//
const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
//
const handleResize = () => {
if (myCharts) {
myCharts.resize();
}
};
// resize
const debouncedResize = debounce(handleResize, 300);
onMounted(() => {
getHistoricalData();
// resize
window.addEventListener("resize", debouncedResize);
});
//
onBeforeUnmount(() => {
// resize
window.removeEventListener("resize", debouncedResize);
// echarts
if (myCharts) {
myCharts.dispose();
myCharts = null;
}
});
//
const getHistoricalData = async () => {
let now = new Date();
let toDate =
now.getFullYear() +
"-" +
String(now.getMonth() + 1).padStart(2, "0") +
"-" +
String(now.getDate()).padStart(2, "0");
let url =
"https://common.szjixun.cn/api/stock/history/base/list?from=2009-10-07&to=" +
toDate;
const res = await axios.get(url);
if (res.status === 200) {
if (res.data.status === 0) {
initEcharts(res.data.data);
}
}
};
//
function findClosestDateIndex(data, targetDateStr) {
let left = 0,
right = data.length - 1;
const target = new Date(targetDateStr).getTime();
let res = data.length - 1; //
while (left <= right) {
const mid = Math.floor((left + right) / 2);
const midTime = new Date(data[mid].date).getTime();
if (midTime > target) {
left = mid + 1;
} else {
res = mid;
right = mid - 1;
}
}
return res;
}
//
function findClosestDateIndexDescLeft(data, targetDateStr) {
let left = 0,
right = data.length - 1;
const target = new Date(targetDateStr).getTime();
let res = -1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
const midTime = new Date(data[mid].date).getTime();
if (midTime > target) {
left = mid + 1; // mid
} else {
res = mid; // mid <= target
right = mid - 1;
}
}
return res;
}
//
const changeSearchRange = (range, dateTime) => {
state.activeRange = range;
const now = new Date();
let startDate = "";
let endDate = "";
if (range === "1m") {
const last = new Date(now);
last.setMonth(now.getMonth() - 1);
startDate = last.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "3m") {
const last = new Date(now);
last.setMonth(now.getMonth() - 3);
startDate = last.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "YTD") {
startDate = new Date(now.getFullYear(), 0, 1).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "1Y") {
const last = new Date(now);
last.setFullYear(now.getFullYear() - 1);
startDate = last.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "5Y") {
const last = new Date(now);
last.setFullYear(now.getFullYear() - 5);
startDate = last.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "10Y") {
const last = new Date(now);
last.setFullYear(now.getFullYear() - 10);
startDate = last.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "Max") {
startDate = "";
endDate = "";
} else if (range === "startDateTime") {
startDate = dateTime;
endDate = "";
} else if (range === "endDateTime") {
startDate = "";
endDate = dateTime;
}
if (startDate || endDate) {
// historicData xAxisData initEcharts
if (
typeof historicData !== "undefined" &&
typeof xAxisData !== "undefined"
) {
const zoomOptions = {};
let newStartTs = state.dateRange[0];
let newEndTs = state.dateRange[1];
if (startDate) {
const idx = findClosestDateIndex(historicData, startDate);
const startValue = new Date(historicData[idx].date).toLocaleDateString(
"en-US",
{
month: "short",
day: "numeric",
year: "numeric",
}
);
zoomOptions.endValue = startValue;
newStartTs = new Date(startValue).getTime();
}
if (endDate) {
const idx = findClosestDateIndexDescLeft(historicData, endDate);
const endValue = new Date(historicData[idx].date).toLocaleDateString(
"en-US",
{
month: "short",
day: "numeric",
year: "numeric",
}
);
zoomOptions.startValue = endValue;
newEndTs = new Date(endValue).getTime();
}
if (Object.keys(zoomOptions).length > 0) {
myCharts.setOption({ dataZoom: [zoomOptions, zoomOptions] });
}
state.dateRange = [newStartTs, newEndTs];
}
} else {
myCharts.setOption({
dataZoom: {
startValue: "",
endValue: "",
},
});
state.dateRange = [new Date("2009-10-07").getTime(), new Date().getTime()];
}
};
//
const isDateDisabled = (ts, type, range) => {
const minDate = new Date("2009-10-06").getTime();
const maxDate = new Date().getTime();
if (ts < minDate || ts > maxDate) {
return true;
}
if (type === "end" && range && range[0]) {
return ts < range[0];
}
return false;
};
//
const handleDateRangeChange = (range) => {
if (range && range[0] && range[1]) {
const startDate = new Date(range[0]).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
const endDate = new Date(range[1]).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
changeSearchRange("startDateTime", startDate);
changeSearchRange("endDateTime", endDate);
}
};
</script>
<style lang="scss" scoped>
.custom-echarts {
.myChart {
width: 100%;
height: 25rem;
}
.echarts-header {
.title-section {
display: flex;
flex-direction: column;
gap: 16 * 2.5px;
margin-bottom: 32 * 2.5px;
margin-top: 43 * 2.5px;
padding: 0 16 * 2.5px;
}
.title-decoration {
width: 58 * 2.5px;
height: 7 * 2.5px;
background: #ff7bac;
margin: auto 0;
margin-top: 0;
}
.title-text {
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 32 * 2.5px;
line-height: 1;
letter-spacing: 0.03em;
color: #000000;
}
.echarts-search-area {
padding: 0 16 * 2.5px 0 16 * 2.5px;
display: flex;
flex-direction: column;
justify-content: center;
gap: 10 * 2.5px;
flex-wrap: wrap;
.echarts-search-byRange {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10 * 2.5px;
.search-range-list {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 16 * 2.5px;
flex-wrap: wrap;
.search-range-list-each {
padding: 7 * 2.5px 22 * 2.5px;
border-radius: 5px;
background-color: #f3f4f6;
cursor: pointer;
span {
font-size: 0.9rem;
}
}
.activeRange {
color: #fff;
background: #ff7bac;
}
}
}
.echarts-search-byDate {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 0.4rem;
.n-date-picker {
width: 100%;
}
}
}
}
}
</style>

View File

@ -1,30 +1,30 @@
<script setup>
import { computed } from 'vue'
import { useWindowSize } from '@vueuse/core'
import { computed } from "vue";
import { useWindowSize } from "@vueuse/core";
import size375 from '@/components/customFooter/size375/index.vue'
import size768 from '@/components/customFooter/size768/index.vue'
import size1440 from '@/components/customFooter/size1920/index.vue'
import size1920 from '@/components/customFooter/size1920/index.vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import size375 from "@/components/customFooter/size375/index.vue";
import size768 from "@/components/customFooter/size768/index.vue";
import size1440 from "@/components/customFooter/size1440/index.vue";
import size1920 from "@/components/customFooter/size1920/index.vue";
import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
const router = useRouter()
const { width } = useWindowSize()
const { t } = useI18n()
const router = useRouter();
const { width } = useWindowSize();
const { t } = useI18n();
const viewComponent = computed(() => {
const viewWidth = width.value
if (viewWidth <= 500) {
return size375
const viewWidth = width.value;
if (viewWidth <= 450) {
return size375;
} else if (viewWidth <= 1100) {
return size768
return size768;
} else if (viewWidth <= 1500) {
return size1440
return size1440;
} else if (viewWidth <= 1920 || viewWidth > 1920) {
return size1920
return size1920;
}
})
});
</script>
<template>

View File

@ -0,0 +1,68 @@
<template>
<!-- 通用页脚 -->
<div class="custom-footer">
<div class="custom-footer-box">
<div class="footer-links">
<span @click="handleLink('privacyPolicy')">Privacy Policy</span>
<span @click="handleLink('termsOfUse')">Terms of use</span>
<span @click="handleLink('siteMap')">Site Map</span>
</div>
<span>&copy; 2025 FiEE, Inc. All Rights Reserved.</span>
</div>
</div>
</template>
<script setup>
import { useRouter } from "vue-router";
const router = useRouter();
import privacyPolicy from "@/assets/file/footer/FiEE, Inc. _ Privacy policy.pdf";
import termsOfUse from "@/assets/file/footer/FiEE, Inc. _ Terms of Use.pdf";
import siteMap from "@/assets/file/footer/FiEE, Inc. _ Site Map.pdf";
//
const handleLink = (link) => {
// if (link === "privacyPolicy") {
// window.open(privacyPolicy, "_blank");
// } else if (link === "termsOfUse") {
// window.open(termsOfUse, "_blank");
// } else if (link === "siteMap") {
// window.open(siteMap, "_blank");
// }
router.push(link);
};
</script>
<style scoped lang="scss">
.custom-footer {
width: 100%;
background: #f5f5f5;
border-top: 2px solid #dbdbdb;
z-index: 100;
height: 80px;
.custom-footer-box {
width: 932px;
margin: 0 auto;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
letter-spacing: 1px;
color: #888;
font-size: 16px;
text-align: center;
height: 100%;
}
.footer-links {
span {
border-right: 1px solid #d2d2d7;
padding: 0 10px;
cursor: pointer;
}
span:nth-last-child(1) {
border: 0;
}
}
}
</style>

View File

@ -2,12 +2,12 @@
<!-- 通用页脚 -->
<div class="custom-footer">
<div class="custom-footer-box">
<span>&copy; 2025 FiEE, Inc. All Rights Reserved.</span>
<div class="footer-links">
<span @click="handleLink('privacyPolicy')">Privacy Policy</span>
<span @click="handleLink('termsOfUse')">Terms of use</span>
<span @click="handleLink('siteMap')">Site Map</span>
</div>
<span>&copy; 2025 FiEE, Inc. All Rights Reserved.</span>
</div>
</div>
</template>
@ -28,19 +28,20 @@ const handleLink = (link) => {
// } else if (link === "siteMap") {
// window.open(siteMap, "_blank");
// }
router.push(link)
router.push(link);
};
</script>
<style scoped lang="scss">
.custom-footer {
width: 100%;
background: #f7f8fa;
border-top: 1px solid #ececec;
background: #f5f5f5;
border-top: 2px solid #dbdbdb;
z-index: 100;
height: 80px;
.custom-footer-box {
max-width: 1700px;
width: 932px;
margin: 0 auto;
display: flex;
flex-direction: row;
@ -50,12 +51,11 @@ const handleLink = (link) => {
color: #888;
// font-size: 15px;
font-size: 1.05rem;
padding: 1rem 40px;
text-align: center;
height: 100%;
}
.footer-links {
margin: 0.4rem 0 0;
span {
border-right: 1px solid #d2d2d7;
padding: 0 10px;

View File

@ -1,7 +1,6 @@
<template>
<!-- 通用页脚 -->
<div class="custom-footer">
<span>&copy; 2025 FiEE, Inc. All Rights Reserved.</span>
<div class="footer-links-box">
<div class="footer-links">
<span @click="handleLink('privacyPolicy')">Privacy Policy</span>
@ -9,6 +8,7 @@
<span @click="handleLink('siteMap')">Site Map</span>
</div>
</div>
<div class="footer-copyright">2025 FiEE, Inc. All Rights Reserved.</div>
</div>
</template>
@ -29,7 +29,7 @@ const handleLink = (link) => {
// } else if (link === "siteMap") {
// window.open(siteMap, "_blank");
// }
router.push(link)
router.push(link);
};
</script>
@ -50,14 +50,13 @@ const handleLink = (link) => {
flex-direction: column;
align-items: center;
justify-content: center;
margin: 0.6rem 0 0;
.footer-links {
span {
border-right: 1px solid #d2d2d7;
padding: 0 0.8rem;
padding: 0 16 * 5.12px;
cursor: pointer;
font-size: 0.75rem;
font-size: 14 * 5.12px;
display: inline-block;
text-align: left;
}
@ -66,5 +65,11 @@ const handleLink = (link) => {
}
}
}
.footer-copyright {
margin-top: 12 * 5.12px;
font-size: 14 * 5.12px;
color: 455363;
}
}
</style>

View File

@ -1,12 +1,12 @@
<template>
<!-- 通用页脚 -->
<div class="custom-footer">
<span>&copy; 2025 FiEE, Inc. All Rights Reserved.</span>
<div class="footer-links">
<span @click="handleLink('privacyPolicy')">Privacy Policy</span>
<span @click="handleLink('termsOfUse')">Terms of use</span>
<span @click="handleLink('siteMap')">Site Map</span>
</div>
<div>&copy; 2025 FiEE, Inc. All Rights Reserved.</div>
</div>
</template>
@ -26,29 +26,28 @@ const handleLink = (link) => {
// } else if (link === "siteMap") {
// window.open(siteMap, "_blank");
// }
router.push(link)
router.push(link);
};
</script>
<style scoped lang="scss">
.custom-footer {
width: 100%;
text-align: center;
padding: 24px 0;
padding: 24 * 2.5px 32 * 2.5px;
color: #888;
// font-size: 15px;
font-size: 1.05rem;
font-size: 14 * 2.5px;
background: #f7f8fa;
letter-spacing: 1px;
border-top: 1px solid #ececec;
z-index: 100;
padding: 1rem 0;
display: flex;
justify-content: space-between;
align-items: center;
.footer-links {
margin: 0.4rem 0 0;
span {
border-right: 1px solid #d2d2d7;
padding: 0 10px;
padding: 0 16 * 2.5px;
cursor: pointer;
}
span:nth-last-child(1) {

View File

@ -1,30 +1,30 @@
<script setup>
import { computed } from 'vue'
import { useWindowSize } from '@vueuse/core'
import { computed } from "vue";
import { useWindowSize } from "@vueuse/core";
import size375 from '@/components/customHeader/size375/index.vue'
import size768 from '@/components/customHeader/size375/index.vue'
import size1440 from '@/components/customHeader/size1440/index.vue'
import size1920 from '@/components/customHeader/size1920/index.vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import size375 from "@/components/customHeader/size375/index.vue";
import size768 from "@/components/customHeader/size768/index.vue";
import size1440 from "@/components/customHeader/size1440/index.vue";
import size1920 from "@/components/customHeader/size1920/index.vue";
import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
const router = useRouter()
const { width } = useWindowSize()
const { t } = useI18n()
const router = useRouter();
const { width } = useWindowSize();
const { t } = useI18n();
const viewComponent = computed(() => {
const viewWidth = width.value
const viewWidth = width.value;
if (viewWidth <= 450) {
return size375
} else if (viewWidth <= 835) {
return size768
} else if (viewWidth <= 1640) {
return size1440
return size375;
} else if (viewWidth <= 1100) {
return size768;
} else if (viewWidth <= 1500) {
return size1440;
} else if (viewWidth <= 1920 || viewWidth > 1920) {
return size1920
return size1920;
}
})
});
</script>
<template>

View File

@ -6,16 +6,46 @@
>
<div class="header-container">
<div class="logo" @click="handleToHome">
<NImage width="80" height="80" :src="FiEELogo" preview-disabled />
<NImage width="140" height="140" :src="FiEELogo" preview-disabled />
</div>
<div class="header-menu">
<NMenu
mode="horizontal"
:options="menuOptions"
:inverted="isScrolled"
v-model:value="selectedKey"
@update:value="handleMenuSelect"
/>
<NConfigProvider :theme-overrides="themeOverrides">
<NMenu
mode="horizontal"
:options="menuOptions"
:inverted="isScrolled"
v-model:value="selectedKey"
@update:value="handleMenuSelect"
/>
</NConfigProvider>
</div>
<div class="header-right">
<div class="lang-switch-container">
<div class="lang-switch" @click.stop="toggleLanguagePicker">
<span class="lang-label">{{ currentLanguageLabel }}</span>
<svg
:class="{ rotated: showLanguagePicker }"
xmlns="http://www.w3.org/2000/svg"
width="7"
height="4"
viewBox="0 0 7 4"
fill="none"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.5 4L7 0L0 0L3.5 4Z"
fill="black"
/>
</svg>
</div>
<LanguagePicker
v-if="showLanguagePicker"
v-model="selectedLanguage"
:options="languageOptions"
@select="handleSelectLanguage"
/>
</div>
</div>
</div>
</NLayoutHeader>
@ -23,21 +53,78 @@
<script setup>
import FiEELogo from "@/assets/image/header/logo.png";
import { ref, onMounted, onUnmounted } from "vue";
import { NMenu, NLayoutHeader, NImage } from "naive-ui";
import { ref, onMounted, onUnmounted, computed } from "vue";
import { NMenu, NLayoutHeader, NImage, NConfigProvider } from "naive-ui";
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
import { useHeaderMenuConfig } from "@/config/headerMenuConfig";
import LanguagePicker from "@/components/languagePicker/size1440/index.vue";
const { t } = useI18n();
const themeOverrides = {
Menu: {
// itemTextColor: "#ff7bac", //
//
// itemTextColorHorizontal: "#ff7bac", //
itemTextColorHoverHorizontal: "#ff7bac", //
itemTextColorActiveHorizontal: "#ff7bac", //
itemTextColorActiveHoverHorizontal: "#ff7bac", //
itemColorHover: "#ff7bac",
itemColorHoverInverted: "#ff7bac",
},
//
Dropdown: {
optionColorHover: "#fddfea", //
optionColorHoverInverted: "#fddfea", //
},
};
const { t, locale } = useI18n();
const router = useRouter();
// 使
const menuOptions = useHeaderMenuConfig();
const { menuOptions } = useHeaderMenuConfig();
const selectedKey = ref(null);
const isScrolled = ref(false);
// language picker
const showLanguagePicker = ref(false);
const selectedLanguage = ref(
localStorage.getItem("language") || locale.value || "en"
);
const languageOptions = computed(() => [
{ label: t("language.ja"), value: "ja", key: "ja" },
{ label: t("language.en"), value: "en", key: "en" },
{ label: t("language.zh"), value: "zh", key: "zh" },
{ label: t("language.zhTW"), value: "zh-TW", key: "zh-TW" },
]);
const currentLanguageLabel = computed(() => {
const found = languageOptions.value.find(
(opt) => opt.value === (locale.value || "en")
);
return found ? found.label : "English";
});
const toggleLanguagePicker = () => {
showLanguagePicker.value = !showLanguagePicker.value;
};
const closeLanguagePicker = () => {
showLanguagePicker.value = false;
};
const handleSelectLanguage = (lang) => {
locale.value = lang;
localStorage.setItem("language", lang);
closeLanguagePicker();
selectedLanguage.value = locale.value;
};
//
function findMenuOptionByKey(options, key) {
for (const option of options) {
@ -52,7 +139,7 @@ function findMenuOptionByKey(options, key) {
//
const handleMenuSelect = (key) => {
const option = findMenuOptionByKey(menuOptions, key);
const option = findMenuOptionByKey(menuOptions.value, key);
if (option && option.href) {
router.push(option.href);
}
@ -66,10 +153,12 @@ const handleScroll = () => {
onMounted(() => {
window.addEventListener("scroll", handleScroll);
window.addEventListener("click", closeLanguagePicker);
});
onUnmounted(() => {
window.removeEventListener("scroll", handleScroll);
window.removeEventListener("click", closeLanguagePicker);
});
//
@ -122,6 +211,12 @@ const handleToHome = () => {
.header-menu {
display: block;
flex: 1;
// :deep(
// .n-menu-item-content:not(.n-menu-item-content--disabled):hover
// .n-menu-item-content-header
// ) {
// color: #ff7bac;
// }
:deep(.n-menu) {
background: transparent;
@ -132,12 +227,13 @@ const handleToHome = () => {
position: relative;
margin: 0 10px;
transition: all 0.3s ease;
font-weight: 700;
// font-size: 16px;
font-size: 0.875rem;
font-family: "PingFang SC";
font-style: normal;
font-weight: 500;
line-height: normal;
font-size: 16px;
min-width: 120px;
text-align: center;
&::after {
content: "";
position: absolute;
@ -145,7 +241,7 @@ const handleToHome = () => {
left: 50%;
width: 0;
height: 2px;
background: var(--primary-color);
background: #ff7bac;
transition: all 0.3s ease;
transform: translateX(-50%);
opacity: 0;
@ -218,6 +314,29 @@ const handleToHome = () => {
transform: translateY(0) scale(1);
}
}
.lang-switch-container {
position: relative;
}
.lang-switch {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
.lang-label {
font-size: 12 * 1.33px;
font-weight: 600;
line-height: normal;
color: #000;
}
svg {
transition: transform 0.2s;
&.rotated {
transform: rotate(180deg);
}
}
}
</style>
<style>
.header-menu .n-menu .n-menu-item-content .n-menu-item-content-header {

View File

@ -6,16 +6,46 @@
>
<div class="header-container">
<div class="logo" @click="handleToHome">
<NImage width="80" height="80" :src="FiEELogo" preview-disabled />
<NImage width="140" height="140" :src="FiEELogo" preview-disabled />
</div>
<div class="header-menu">
<NMenu
mode="horizontal"
:options="menuOptions"
:inverted="isScrolled"
v-model:value="selectedKey"
@update:value="handleMenuSelect"
/>
<NConfigProvider :theme-overrides="themeOverrides">
<NMenu
mode="horizontal"
:options="menuOptions"
:inverted="isScrolled"
v-model:value="selectedKey"
@update:value="handleMenuSelect"
/>
</NConfigProvider>
</div>
<div class="header-right">
<div class="lang-switch-container">
<div class="lang-switch" @click.stop="toggleLanguagePicker">
<span class="lang-label">{{ currentLanguageLabel }}</span>
<svg
:class="{ rotated: showLanguagePicker }"
xmlns="http://www.w3.org/2000/svg"
width="7"
height="4"
viewBox="0 0 7 4"
fill="none"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.5 4L7 0L0 0L3.5 4Z"
fill="black"
/>
</svg>
</div>
<LanguagePicker
v-if="showLanguagePicker"
v-model="selectedLanguage"
:options="languageOptions"
@select="handleSelectLanguage"
/>
</div>
</div>
</div>
</NLayoutHeader>
@ -23,21 +53,75 @@
<script setup>
import FiEELogo from "@/assets/image/header/logo.png";
import { ref, onMounted, onUnmounted } from "vue";
import { NMenu, NLayoutHeader, NImage } from "naive-ui";
import { ref, onMounted, onUnmounted, computed } from "vue";
import { NMenu, NLayoutHeader, NImage, NConfigProvider } from "naive-ui";
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
import { useHeaderMenuConfig } from "@/config/headerMenuConfig";
import LanguagePicker from "@/components/languagePicker/size1920/index.vue";
const { t } = useI18n();
const themeOverrides = {
Menu: {
// itemTextColor: "#ff7bac", //
//
// itemTextColorHorizontal: "#ff7bac", //
itemTextColorHoverHorizontal: "#ff7bac", //
itemTextColorActiveHorizontal: "#ff7bac", //
itemTextColorActiveHoverHorizontal: "#ff7bac", //
itemColorHover: "#ff7bac",
itemColorHoverInverted: "#ff7bac",
},
//
Dropdown: {
optionColorHover: "#fddfea", //
optionColorHoverInverted: "#fddfea", //
},
};
const { t, locale } = useI18n();
const router = useRouter();
// 使
const menuOptions = useHeaderMenuConfig();
const { menuOptions } = useHeaderMenuConfig();
const selectedKey = ref(null);
const isScrolled = ref(false);
const showLanguagePicker = ref(false);
const selectedLanguage = ref(
localStorage.getItem("language") || locale.value || "en"
);
const languageOptions = computed(() => [
{ label: t("language.ja"), value: "ja", key: "ja" },
{ label: t("language.en"), value: "en", key: "en" },
{ label: t("language.zh"), value: "zh", key: "zh" },
{ label: t("language.zhTW"), value: "zh-TW", key: "zh-TW" },
]);
const currentLanguageLabel = computed(() => {
const found = languageOptions.value.find(
(opt) => opt.value === (locale.value || "en")
);
return found ? found.label : "English";
});
const toggleLanguagePicker = () => {
showLanguagePicker.value = !showLanguagePicker.value;
};
const closeLanguagePicker = () => {
showLanguagePicker.value = false;
};
const handleSelectLanguage = (lang) => {
locale.value = lang;
localStorage.setItem("language", lang);
closeLanguagePicker();
selectedLanguage.value = locale.value;
};
//
function findMenuOptionByKey(options, key) {
for (const option of options) {
@ -52,7 +136,7 @@ function findMenuOptionByKey(options, key) {
//
const handleMenuSelect = (key) => {
const option = findMenuOptionByKey(menuOptions, key);
const option = findMenuOptionByKey(menuOptions.value, key);
if (option && option.href) {
router.push(option.href);
}
@ -66,10 +150,12 @@ const handleScroll = () => {
onMounted(() => {
window.addEventListener("scroll", handleScroll);
window.addEventListener("click", closeLanguagePicker);
});
onUnmounted(() => {
window.removeEventListener("scroll", handleScroll);
window.removeEventListener("click", closeLanguagePicker);
});
//
@ -132,12 +218,13 @@ const handleToHome = () => {
position: relative;
margin: 0 20px;
transition: all 0.3s ease;
font-weight: 700;
// font-size: 16px;
font-size: 1.05rem;
min-width: 120px;
text-align: center;
font-family: "PingFang SC";
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: normal;
&::after {
content: "";
position: absolute;
@ -145,7 +232,7 @@ const handleToHome = () => {
left: 50%;
width: 0;
height: 2px;
background: var(--primary-color);
background: #ff7bac;
transition: all 0.3s ease;
transform: translateX(-50%);
opacity: 0;
@ -218,4 +305,32 @@ const handleToHome = () => {
transform: translateY(0) scale(1);
}
}
.header-right {
margin-left: auto;
padding-left: 20px;
}
.lang-switch-container {
position: relative;
}
.lang-switch {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
.lang-label {
font-size: 12px;
font-weight: 600;
line-height: normal;
color: #000;
}
svg {
transition: transform 0.2s;
&.rotated {
transform: rotate(180deg);
}
}
}
</style>

View File

@ -7,56 +7,132 @@
<div class="header-container">
<div class="logo" @click="handleToHome">
<NImage
style="width: 60px; height: 60px; max-width: 100%"
style="width: 100px; max-width: 100%"
:src="FiEELogo"
preview-disabled
/>
</div>
<div
class="menu-btn"
:class="{ 'menu-open': showMenu }"
@click="toggleMenu"
>
<n-icon size="28" class="menu-icon menu-icon-menu">
<menu-sharp />
</n-icon>
<n-icon size="28" class="menu-icon menu-icon-close">
<close-sharp />
</n-icon>
<div class="header-right">
<div v-show="!showMenu" class="lang-switch" @click="openLanguagePicker">
<span class="lang-label">{{ currentLanguageLabel }}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="7"
height="4"
viewBox="0 0 7 4"
fill="none"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.5 4L7 0L0 0L3.5 4Z"
fill="black"
/>
</svg>
</div>
<div
class="menu-btn"
:class="{ 'menu-open': showMenu }"
@click="toggleMenu"
>
<img
v-if="showMenu"
src="@/assets/image/375/menu-close.png"
alt="menu"
class="menu-icon"
/>
<img
v-else
src="@/assets/image/375/menu-open.png"
alt="menu"
class="menu-icon"
/>
</div>
</div>
</div>
</NLayoutHeader>
<transition name="fade-slide">
<div v-if="showMenu" class="mobile-menu-wrapper" @click.self="closeMenu">
<NMenu
mode="vertical"
:options="menuOptions"
:inverted="isScrolled"
class="mobile-menu"
accordion
v-model:value="selectedKey"
@update:value="handleMenuSelect"
/>
<NConfigProvider :theme-overrides="themeOverrides">
<NMenu
mode="vertical"
:options="menuOptions"
:inverted="isScrolled"
class="mobile-menu"
accordion
v-model:value="selectedKey"
@update:value="handleMenuSelect"
/>
</NConfigProvider>
</div>
</transition>
<LanguagePicker
v-if="showLanguagePicker"
v-model="selectedLanguage"
:options="languageOptions"
@close="showLanguagePicker = false"
@confirm="handleConfirmLanguage"
/>
</template>
<script setup>
import FiEELogo from "@/assets/image/header/logo.png";
import { ref, onMounted, onUnmounted } from "vue";
import { NMenu, NLayoutHeader, NImage, NIcon } from "naive-ui";
import { ref, onMounted, onUnmounted, computed } from "vue";
import { NMenu, NLayoutHeader, NImage, NIcon, NConfigProvider } from "naive-ui";
import { MenuSharp, CloseSharp } from "@vicons/ionicons5";
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
import { useHeaderMenuConfig } from "@/config/headerMenuConfig";
const { t } = useI18n();
import LanguagePicker from "@/components/languagePicker/index.vue";
const themeOverrides = {
Menu: {
itemTextColorHover: "#000",
itemTextColorActive: "#FF7BAC",
itemTextColorActiveHover: "#fff8fb",
itemColorHover: "#FDDFE9",
itemColorActive: "#fff",
itemColorActiveHover: "#fff8fb",
},
};
const { t, locale } = useI18n();
const router = useRouter();
const isScrolled = ref(false);
const showMenu = ref(false);
const selectedKey = ref(null);
// language picker
const showLanguagePicker = ref(false);
const selectedLanguage = ref(
localStorage.getItem("language") || locale.value || "en"
);
const languageOptions = computed(() => [
{ label: t("language.ja"), value: "ja", key: "ja" },
{ label: t("language.en"), value: "en", key: "en" },
{ label: t("language.zh"), value: "zh", key: "zh" },
{ label: t("language.zhTW"), value: "zh-TW", key: "zh-TW" },
]);
const currentLanguageLabel = computed(() => {
const found = languageOptions.value.find(
(opt) => opt.value === (locale.value || "en")
);
return found ? found.label : "English";
});
const openLanguagePicker = () => {
if (showMenu.value) {
return;
}
showLanguagePicker.value = true;
selectedLanguage.value = locale.value;
};
const handleConfirmLanguage = (lang) => {
locale.value = lang;
localStorage.setItem("language", lang);
showLanguagePicker.value = false;
};
const toggleMenu = () => {
showMenu.value = !showMenu.value;
};
@ -78,7 +154,7 @@ function findMenuOptionByKey(options, key) {
//
const handleMenuSelect = (key) => {
const option = findMenuOptionByKey(menuOptions, key);
const option = findMenuOptionByKey(menuOptions.value, key);
if (option && option.href) {
router.push(option.href);
showMenu.value = false; //
@ -86,7 +162,7 @@ const handleMenuSelect = (key) => {
};
// 使
const menuOptions = useHeaderMenuConfig();
const { menuOptions } = useHeaderMenuConfig();
//
const handleScroll = () => {
@ -114,7 +190,7 @@ const handleToHome = () => {
.custom-header {
transition: all 0.3s ease;
background: transparent;
height: 320px;
height: 60 * 5.12px;
&.header-scrolled {
background: rgba(255, 255, 255, 0.95);
@ -133,6 +209,19 @@ const handleToHome = () => {
justify-content: space-between;
}
.lang-switch {
display: flex;
align-items: center;
gap: 11 * 5.12px;
font-weight: 600;
font-size: 14 * 5.12px;
cursor: pointer;
user-select: none;
}
.lang-caret {
font-size: 10 * 5.12px;
}
.logo {
flex-shrink: 0;
margin-right: 12px;
@ -141,8 +230,7 @@ const handleToHome = () => {
}
.logo :deep(.n-image) {
max-width: 100px;
height: auto;
width: 94 * 5.12px;
}
.menu-btn {
@ -157,6 +245,7 @@ const handleToHome = () => {
user-select: none;
transition: background 0.2s;
position: relative;
width: 56 * 5.12px;
.menu-icon {
position: absolute;
@ -187,18 +276,40 @@ const handleToHome = () => {
.mobile-menu-wrapper {
position: fixed;
top: 320px;
top: 60 * 5.12px;
left: 0;
width: 100vw;
background: rgba(220, 207, 248, 0.95);
background: #fff;
z-index: 1100;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08);
padding: 40px 0 80px 0;
max-height: 1500px;
max-height: 1500 * 5.12px;
overflow-y: auto;
// CSS
// --n-item-text-color-child-active: #ff7bac;
:deep(.n-menu-item) {
font-weight: 600;
}
// //
// :deep(
// .n-menu
// .n-menu-item-content.n-menu-item-content--child-active
// .n-menu-item-content-header
// ) {
// color: #ff7bac !important;
// }
// //
// :deep(.n-menu-item-content--child-active) {
// color: #ff7bac !important;
// .n-menu-item-content-header {
// color: #ff7bac !important;
// }
// }
}
.fade-slide-enter-active,
@ -210,4 +321,9 @@ const handleToHome = () => {
opacity: 0;
transform: translateY(-50px);
}
.header-right {
display: flex;
align-items: center;
gap: 24 * 5.12px;
}
</style>

View File

@ -0,0 +1,302 @@
<template>
<!-- 通用页头 -->
<NLayoutHeader
class="custom-header"
:class="{ 'header-scrolled': isScrolled }"
>
<div class="header-container">
<div class="logo" @click="handleToHome">
<NImage class="logo-image" :src="FiEELogo" preview-disabled />
</div>
<div class="header-right">
<div v-show="!showMenu" class="lang-switch" @click="openLanguagePicker">
<span class="lang-label">{{ currentLanguageLabel }}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="7"
height="4"
viewBox="0 0 7 4"
fill="none"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.5 4L7 0L0 0L3.5 4Z"
fill="black"
/>
</svg>
</div>
<div
class="menu-btn"
:class="{ 'menu-open': showMenu }"
@click="toggleMenu"
>
<img
v-if="showMenu"
src="@/assets/image/768/menu-close.png"
alt="menu"
class="menu-icon"
/>
<img
v-else
src="@/assets/image/768/menu-open.png"
alt="menu"
class="menu-icon"
/>
</div>
</div>
</div>
</NLayoutHeader>
<transition name="fade-slide">
<div v-if="showMenu" class="mobile-menu-wrapper" @click.self="closeMenu">
<NConfigProvider :theme-overrides="themeOverrides">
<NMenu
mode="vertical"
:options="menuOptions"
:inverted="isScrolled"
class="mobile-menu"
accordion
v-model:value="selectedKey"
@update:value="handleMenuSelect"
/>
</NConfigProvider>
</div>
</transition>
<LanguagePicker
v-if="showLanguagePicker"
v-model="selectedLanguage"
:options="languageOptions"
@close="showLanguagePicker = false"
@confirm="handleConfirmLanguage"
/>
</template>
<script setup>
import FiEELogo from "@/assets/image/header/logo.png";
import { ref, onMounted, onUnmounted, computed } from "vue";
import { NMenu, NLayoutHeader, NImage, NIcon, NConfigProvider } from "naive-ui";
import { MenuSharp, CloseSharp } from "@vicons/ionicons5";
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
import { useHeaderMenuConfig } from "@/config/headerMenuConfig";
import LanguagePicker from "@/components/languagePicker/index.vue";
const themeOverrides = {
Menu: {
itemTextColorHover: "#000",
itemTextColorActive: "#FF7BAC",
itemTextColorActiveHover: "#fff8fb",
itemColorHover: "#FDDFE9",
itemColorActive: "#fff",
itemColorActiveHover: "#fff8fb",
},
};
const { t, locale } = useI18n();
const router = useRouter();
const isScrolled = ref(false);
const showMenu = ref(false);
const selectedKey = ref(null);
// language picker
const showLanguagePicker = ref(false);
const selectedLanguage = ref(
localStorage.getItem("language") || locale.value || "en"
);
const languageOptions = computed(() => [
{ label: t("language.ja"), value: "ja", key: "ja" },
{ label: t("language.en"), value: "en", key: "en" },
{ label: t("language.zh"), value: "zh", key: "zh" },
{ label: t("language.zhTW"), value: "zh-TW", key: "zh-TW" },
]);
const currentLanguageLabel = computed(() => {
const found = languageOptions.value.find(
(opt) => opt.value === (locale.value || "en")
);
return found ? found.label : "English";
});
const openLanguagePicker = () => {
if (showMenu.value) {
return;
}
showLanguagePicker.value = true;
selectedLanguage.value = locale.value;
};
const handleConfirmLanguage = (lang) => {
locale.value = lang;
localStorage.setItem("language", lang);
showLanguagePicker.value = false;
};
const toggleMenu = () => {
showMenu.value = !showMenu.value;
};
const closeMenu = () => {
showMenu.value = false;
};
//
function findMenuOptionByKey(options, key) {
for (const option of options) {
if (option.key === key) return option;
if (option.children) {
const found = findMenuOptionByKey(option.children, key);
if (found) return found;
}
}
return null;
}
//
const handleMenuSelect = (key) => {
const option = findMenuOptionByKey(menuOptions.value, key);
if (option && option.href) {
router.push(option.href);
showMenu.value = false; //
}
};
// 使
const { menuOptions } = useHeaderMenuConfig();
//
const handleScroll = () => {
//100*2.5pxheader
isScrolled.value = window.scrollY >= 100;
};
onMounted(() => {
window.addEventListener("scroll", handleScroll);
});
onUnmounted(() => {
window.removeEventListener("scroll", handleScroll);
});
//
const handleToHome = () => {
router.push("/");
selectedKey.value = null; //
showMenu.value = false; //
};
</script>
<style scoped lang="scss">
.custom-header {
transition: all 0.3s ease;
background: transparent;
height: 60 * 2.5px;
&.header-scrolled {
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 2 * 2.5px 8 * 2.5px rgba(0, 0, 0, 0.1);
}
}
.header-container {
width: 100%;
min-width: 0;
box-sizing: border-box;
padding: 0 59 * 2.5px;
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
.header-right {
display: flex;
align-items: center;
gap: 24 * 2.5px;
}
.lang-switch {
display: flex;
align-items: center;
gap: 11 * 2.5px;
font-weight: 600;
font-size: 14 * 2.5px;
cursor: pointer;
user-select: none;
}
.logo {
flex-shrink: 0;
margin-left: 11 * 2.5px;
display: flex;
align-items: center;
}
.logo-image {
width: 120 * 2.5px;
height: 27 * 2.5px;
}
.menu-btn {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 75 * 2.5px;
padding: 20 * 2.5px;
border-radius: 30 * 2.5px;
background: transparent;
user-select: none;
transition: background 0.2s;
position: relative;
width: 56 * 2.5px;
.menu-icon {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) rotate(0deg);
opacity: 1;
transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
}
.menu-icon-close {
opacity: 0;
transform: translate(-50%, -50%) rotate(-90deg) scale(0.8);
}
&.menu-open {
.menu-icon-menu {
opacity: 0;
transform: translate(-50%, -50%) rotate(90deg) scale(0.8);
}
.menu-icon-close {
opacity: 1;
transform: translate(-50%, -50%) rotate(0deg) scale(1);
}
}
}
.mobile-menu-wrapper {
position: fixed;
top: 60 * 2.5px;
left: 0;
width: 100vw;
background: #fff;
z-index: 1100;
box-shadow: 0 30 * 2.5px 40 * 2.5px rgba(0, 0, 0, 0.08);
padding: 40 * 2.5px;
max-height: 1500 * 2.5px;
overflow-y: auto;
:deep(.n-menu-item) {
font-weight: 600;
}
}
.fade-slide-enter-active,
.fade-slide-leave-active {
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.fade-slide-enter-from,
.fade-slide-leave-to {
opacity: 0;
transform: translateY(-50 * 2.5px);
}
</style>

Some files were not shown because too many files have changed in this diff Show More