Compare commits

...

19 Commits

Author SHA1 Message Date
e75e8adcd3 Merge branch 'newmain-20250926' of http://gitea.tools.fontree.cn:3000/scout666/fiee-official-website into yk-20250926 2025-10-14 11:01:36 +08:00
yuanshan
75344b2ee3 Merge branch 'zhangyuanshan-20250925' into newmain-20250926 2025-10-14 10:52:23 +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
63 changed files with 7466 additions and 2186 deletions

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: 12 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

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

@ -3,7 +3,7 @@ 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 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";
@ -15,9 +15,9 @@ const { t } = useI18n();
const viewComponent = computed(() => {
const viewWidth = width.value;
if (viewWidth <= 500) {
if (viewWidth <= 450) {
return size375;
} else if (viewWidth <= 960) {
} else if (viewWidth <= 1100) {
return size768;
} else if (viewWidth <= 1500) {
return size1440;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,624 @@
<template>
<div class="custom-echarts">
<div>
<div class="echarts-header">
<!-- 标题区域 -->
<div class="title-section">
<div class="title-decoration"></div>
<div class="title-text">
<span>FiEE, Inc. Stock Price History</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>
<div class="search-range-list">
<div
class="search-range-list-each"
v-for="(item, index) in state.searchRange"
:key="index"
:class="{ activeRange: state.activeRange === item }"
@click="changeSearchRange(item)"
>
<span>{{ item }}</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, watch, reactive } from "vue";
import * as echarts from "echarts";
import { NDatePicker, NIcon } from "naive-ui";
import { ArrowForwardOutline } from "@vicons/ionicons5";
import axios from "axios";
const state = reactive({
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 = [];
//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;">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(),
];
});
};
onMounted(() => {
getHistoricalData();
});
//
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;
.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;
.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

@ -15,7 +15,7 @@ const { t } = useI18n();
const viewComponent = computed(() => {
const viewWidth = width.value;
if (viewWidth <= 500) {
if (viewWidth <= 450) {
return size375;
} else if (viewWidth <= 1100) {
return size768;

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

@ -160,13 +160,13 @@ const handleToHome = () => {
position: relative;
margin: 0 10px;
transition: all 0.3s ease;
font-size: 12px;
min-width: 120px;
text-align: center;
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;

View File

@ -153,12 +153,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;

View File

@ -17,12 +17,18 @@
: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>
<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>
</NLayoutHeader>
@ -53,11 +59,11 @@ import { useRouter } from "vue-router";
import { useHeaderMenuConfig } from "@/config/headerMenuConfig";
const themeOverrides = {
Menu: {
itemTextColorHover: "#ff7bac",
itemTextColorActive: "#ff7bac",
itemTextColorActiveHover: "#ff7bac",
itemColorHover: "#fff8fb",
itemColorActive: "#fff8fb",
itemTextColorHover: "#000",
itemTextColorActive: "#FF7BAC",
itemTextColorActiveHover: "#fff8fb",
itemColorHover: "#FDDFE9",
itemColorActive: "#fff",
itemColorActiveHover: "#fff8fb",
},
};
@ -125,7 +131,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);
@ -152,8 +158,7 @@ const handleToHome = () => {
}
.logo :deep(.n-image) {
max-width: 100px;
height: auto;
width: 94 * 5.12px;
}
.menu-btn {
@ -168,6 +173,7 @@ const handleToHome = () => {
user-select: none;
transition: background 0.2s;
position: relative;
width: 56 * 5.12px;
.menu-icon {
position: absolute;
@ -198,15 +204,14 @@ const handleToHome = () => {
.mobile-menu-wrapper {
position: fixed;
top: 320px;
top: 60 * 5.12px;
left: 0;
width: 100vw;
bottom: 0;
background: #fff;
z-index: 1100;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08);
padding: 40px 0 80px 0;
// max-height: 1500px;
// box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08);
padding: 40 * 5.12px 0;
max-height: 1500 * 5.12px;
overflow-y: auto;
// CSS
@ -223,7 +228,7 @@ const handleToHome = () => {
// n-submenu
:deep(.mobile-menu > div:last-child) {
border-bottom: 3px dashed #e0e0e0;
border-bottom: 3px dashed #e0e0e0;
}
// //

View File

@ -13,12 +13,18 @@
: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>
<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>
</NLayoutHeader>
@ -164,6 +170,7 @@ const handleToHome = () => {
user-select: none;
transition: background 0.2s;
position: relative;
width: 56 * 2.5px;
.menu-icon {
position: absolute;

View File

@ -74,7 +74,7 @@ function copyEmail() {
gap: 4px;
background-color: white;
border-radius: 1rem;
background-image: url("@/assets/image/1920/contacts-bg.png");
background-image: url("@/assets/image/1440/contacts-bg.png");
background-size: 64% auto;
background-position: center;
background-repeat: no-repeat;

View File

@ -1,73 +1,162 @@
<script setup>
import { NCarousel, NDivider, NMarquee, NPopselect } from "naive-ui";
import { onUnmounted, ref, watch, onMounted, computed } from "vue";
function copyEmail() {
navigator.clipboard.writeText("fiee@dlkadvisory.com");
}
</script>
<template>
<main
ref="main"
class="flex flex-col items-center from-primary to-accent w-[100vw] mt-8 animate-fade-in px-4 py-8 pt-500px"
>
<div class="w-full flex flex-col items-center gap-4 px-2">
<h1
class="text-2xl font-bold text-primary animate-fade-in-down animate-delay-0"
>
Investor Contacts
</h1>
<div
class="text-lg font-semibold text-gray-800 animate-fade-in-down animate-delay-200"
>
FiEE Inc.
</div>
<div
class="text-base text-#ff7bac animate-fade-in-down animate-delay-400"
>
Investor Relations
</div>
<div
class="text-sm text-gray-600 flex items-center gap-1 animate-fade-in-down animate-delay-600"
>
<div class="contact-container">
<!-- Title Section -->
<div class="title-section">
<div class="title-decoration"></div>
<div class="contact-title">Investor Contacts</div>
</div>
<!-- Card Section -->
<div class="contact-card">
<img
class="card-overlay"
src="@/assets/image/375/contacts-bg.png"
alt=""
/>
<div class="logo-text">FiEE Inc.</div>
<div class="relation-text">Investor Relations</div>
<div class="email-section">
<span>Email:</span>
<span
class="transition-colors duration-300 cursor-pointer text-#00baff hover:text-primary active:text-secondary select-all"
@click="copyEmail"
<span class="email-address" @click="copyEmail"
>fiee@dlkadvisory.com</span
>
</div>
</div>
</main>
</div>
</template>
<style scoped lang="scss">
.contact-container {
width: 343 * 5.12px;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
}
.title-section {
width: 100%;
display: flex;
flex-direction: column;
gap: 16 * 5.12px;
padding: 0 16 * 5.12px;
margin-top: 43 * 5.12px;
margin-bottom: 32 * 5.12px;
}
.title-decoration {
width: 58 * 5.12px;
height: 7 * 5.12px;
background: #ff7bac;
margin: auto 0;
}
.contact-title {
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 24 * 5.12px;
line-height: normal;
letter-spacing: 0 * 5.12px;
color: #000000;
}
.contact-card {
display: flex;
width: 100%;
height: 524 * 5.12px;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 4 * 5.12px;
background-color: white;
border-radius: 16 * 5.12px;
box-shadow: 0 * 5.12px 3 * 5.12px 14 * 5.12px 0 * 5.12px rgba(0, 0, 0, 0.16);
position: relative;
margin: 0 16 * 5.12px;
animation: fade-in 0.8s cubic-bezier(0.23, 1, 0.32, 1) both;
}
.logo-text {
width: 100%;
text-align: center;
font-feature-settings: "liga" off, "clig" off;
font-family: "PingFang SC";
font-size: 64 * 5.12px;
font-style: normal;
font-weight: 600;
line-height: normal;
letter-spacing: 0.48 * 5.12px;
background: linear-gradient(90deg, #ff7bac 0%, #0ff 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
animation: fade-in-down 0.8s cubic-bezier(0.23, 1, 0.32, 1) both;
animation-delay: 0.2s;
}
.relation-text {
font-family: "PingFang SC", sans-serif;
font-size: 20 * 5.12px;
color: #000000;
font-weight: 600;
animation: fade-in-down 0.8s cubic-bezier(0.23, 1, 0.32, 1) both;
animation-delay: 0.4s;
}
.email-section {
font-size: 16 * 5.12px;
color: #455363;
display: flex;
align-items: center;
gap: 8 * 5.12px;
letter-spacing: 1.2 * 5.12px;
animation: fade-in-down 0.8s cubic-bezier(0.23, 1, 0.32, 1) both;
animation-delay: 0.6s;
margin-top: 0 * 5.12px;
}
.email-address {
color: #ff7bac;
cursor: pointer;
}
.card-overlay {
position: absolute;
left: 43 * 5.12px;
top: 174 * 5.12px;
width: 258 * 5.12px;
height: 176 * 5.12px;
pointer-events: none;
user-select: none;
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fade-in-down {
0% {
opacity: 0;
transform: translateY(-20px);
transform: translateY(-20 * 5.12px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in-down {
animation: fade-in-down 0.8s cubic-bezier(0.23, 1, 0.32, 1) both;
}
.animate-delay-0 {
animation-delay: 0s;
}
.animate-delay-200 {
animation-delay: 0.2s;
}
.animate-delay-400 {
animation-delay: 0.4s;
}
.animate-delay-600 {
animation-delay: 0.6s;
}
.max-w-343px {
max-width: 343px;
}
</style>

View File

@ -1,67 +1,148 @@
<script setup>
import { NCarousel, NDivider, NMarquee, NPopselect } from "naive-ui";
import { onUnmounted, ref, watch, onMounted, computed } from "vue";
function copyEmail() {
navigator.clipboard.writeText("fiee@dlkadvisory.com");
}
</script>
<template>
<main
ref="main"
class="flex flex-col items-center from-primary to-accent w-[100vw] mt-12 animate-fade-in px-6 py-10 pt-500px"
>
<div class="w-full flex flex-col items-center gap-5 px-4">
<h1
class="text-3xl font-bold text-primary animate-fade-in-down animate-delay-0"
>
Investor Contacts
</h1>
<div
class="text-xl font-semibold text-gray-800 animate-fade-in-down animate-delay-200"
>
FiEE Inc.
</div>
<div class="text-lg text-#ff7bac animate-fade-in-down animate-delay-400">
Investor Relations
</div>
<div
class="text-base text-gray-600 flex items-center gap-2 animate-fade-in-down animate-delay-600"
>
<div class="contact-container">
<!-- Title Section -->
<div class="title-section">
<div class="title-decoration"></div>
<div class="contact-title">Investor Contacts</div>
</div>
<!-- Card Section -->
<div class="contact-card">
<div class="logo-text">FiEE Inc.</div>
<div class="relation-text">Investor Relations</div>
<div class="email-section">
<span>Email:</span>
<span
class="transition-colors duration-300 cursor-pointer text-#00baff hover:text-primary active:text-secondary select-all"
@click="copyEmail"
<span class="email-address" @click="copyEmail"
>fiee@dlkadvisory.com</span
>
</div>
</div>
</main>
</div>
</template>
<style scoped lang="scss">
.contact-container {
width: 650 * 2.5px;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
}
.title-section {
width: 100%;
display: flex;
flex-direction: column;
gap: 16 * 2.5px;
padding: 0 16 * 2.5px;
margin-bottom: 32 * 2.5px;
}
.title-decoration {
width: 58 * 2.5px;
height: 7 * 2.5px;
background: #ff7bac;
margin: auto 0;
margin-top: 43 * 2.5px;
}
.contact-title {
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 32 * 2.5px;
line-height: 1.4em;
letter-spacing: 0.03em;
color: #000000;
}
.contact-card {
display: flex;
width: 100%;
height: 511 * 2.5px;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 4px;
background-color: white;
border-radius: 1rem;
background-image: url("@/assets/image/768/contacts-bg.png");
background-size: 64% auto;
background-position: center;
background-repeat: no-repeat;
box-shadow: 0px 3 * 2.5px 14 * 2.5px 0px rgba(0, 0, 0, 0.16);
animation: fade-in 0.8s cubic-bezier(0.23, 1, 0.32, 1) both;
}
.logo-text {
width: 100%;
text-align: center;
font-feature-settings: "liga" off, "clig" off;
font-family: "PingFang SC";
font-size: 110 * 2.5px;
font-style: normal;
font-weight: 600;
line-height: normal;
letter-spacing: 0.48 * 2.5px;
background: linear-gradient(90deg, #ff7bac 0%, #0ff 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
animation: fade-in-down 0.8s cubic-bezier(0.23, 1, 0.32, 1) both;
animation-delay: 0.2s;
}
.relation-text {
font-family: "PingFang SC", sans-serif;
font-size: 24 * 2.5px;
color: #000000;
font-weight: 600;
animation: fade-in-down 0.8s cubic-bezier(0.23, 1, 0.32, 1) both;
animation-delay: 0.4s;
}
.email-section {
font-size: 18 * 2.5px;
color: #4a5568;
display: flex;
align-items: center;
gap: 8 * 2.5px;
animation: fade-in-down 0.8s cubic-bezier(0.23, 1, 0.32, 1) both;
animation-delay: 0.6s;
margin-top: 16 * 2.5px;
}
.email-address {
color: #ff7bac;
cursor: pointer;
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fade-in-down {
0% {
opacity: 0;
transform: translateY(-20px);
transform: translateY(-20 * 2.5px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in-down {
animation: fade-in-down 0.8s cubic-bezier(0.23, 1, 0.32, 1) both;
}
.animate-delay-0 {
animation-delay: 0s;
}
.animate-delay-200 {
animation-delay: 0.2s;
}
.animate-delay-400 {
animation-delay: 0.4s;
}
.animate-delay-600 {
animation-delay: 0.6s;
}
</style>

View File

@ -245,7 +245,7 @@ async function handleSubmit(e) {
left: 0;
width: 100%;
height: 100%;
background-image: url("@/assets/image/1920/email-alerts-submit.png");
background-image: url("@/assets/image/1440/email-alerts-submit.png");
background-repeat: no-repeat;
background-position: bottom;
background-size: 100%;

View File

@ -1,6 +1,6 @@
<script setup>
import { NCarousel, NDivider, NMarquee, NPopselect } from "naive-ui";
import { onUnmounted, ref, watch, onMounted, computed } from "vue";
import { ref } from "vue";
import axios from "axios";
const form = ref({
firstName: "",
@ -8,9 +8,8 @@ const form = ref({
email: "",
company: "",
phone: "",
alertType: "all",
});
const submitted = ref(false);
const submitted = ref(true);
async function handleSubmit(e) {
e.preventDefault();
@ -26,132 +25,271 @@ async function handleSubmit(e) {
</script>
<template>
<main ref="main">
<main
class="min-h-70vh flex flex-col items-center justify-center relative px-6 py-10"
<div class="alerts-container">
<!-- Title Section -->
<!-- 未提交 -->
<div v-if="!submitted" class="title-section">
<div class="title-decoration"></div>
<div class="title">E-Mail Alerts</div>
<div class="subtitle">* Required Fields</div>
</div>
<!-- 已提交 -->
<div v-else class="title-section">
<div class="title-decoration"></div>
<div class="title">Submitted successfully!</div>
<div class="subtitle">The information you submitted is as follows:</div>
</div>
<!-- Form Card -->
<div
class="form-card relative"
:style="{
width: '100%',
height: submitted ? '593px' : 'auto',
}"
>
<div
class="w-[840px] max-w-90vw p-6 bg-white/95 rounded-2xl shadow-lg animate-bounce-in"
>
<template v-if="!submitted">
<h2
class="text-2xl font-bold text-#ff7bac mb-3 text-center tracking-wide"
>
E-Mail Alerts
</h2>
<p class="text-sm text-gray-500 mb-5 text-center">
* Required Fields
</p>
<form class="flex flex-col gap-4" @submit="handleSubmit">
<div>
<label class="block text-gray-700 font-semibold mb-1.5 text-base"
>* First Name</label
>
<input
v-model="form.firstName"
type="text"
class="w-full px-3 py-2 rounded-lg ring-4 ring-#ff7bac/20) transition-all duration-300 outline-none bg-white/90 border-none"
/>
<template v-if="!submitted">
<form class="form-content" @submit="handleSubmit">
<div class="form-group mt-[36px]">
<label for="firstName">* First Name</label>
<input
id="firstName"
v-model="form.firstName"
type="text"
required
/>
</div>
<div class="form-group">
<label for="lastName">* Last Name</label>
<input id="lastName" v-model="form.lastName" type="text" required />
</div>
<div class="form-group">
<label for="email">* Email</label>
<input id="email" v-model="form.email" type="email" required />
</div>
<div class="form-group">
<label for="company">* Company</label>
<input id="company" v-model="form.company" type="text" required />
</div>
<div class="form-group">
<label for="phone">* Phone</label>
<input id="phone" v-model="form.phone" type="tel" />
</div>
<button type="submit" class="submit-btn">Submit</button>
</form>
</template>
<template v-else>
<div class="submitted-data">
<div class="submitted-data-content">
<div class="submitted-row">
<span class="label">First Name</span>
<span class="value">{{ form.firstName || "Not filled in" }}</span>
</div>
<div>
<label class="block text-gray-700 font-semibold mb-1.5 text-base"
>* Last Name</label
>
<input
v-model="form.lastName"
type="text"
class="w-full px-3 py-2 rounded-lg ring-4 ring-#ff7bac/20) transition-all duration-300 outline-none bg-white/90 border-none"
/>
<div class="submitted-row">
<span class="label">Last Name</span>
<span class="value">{{ form.lastName || "Not filled in" }}</span>
</div>
<div>
<label class="block text-gray-700 font-semibold mb-1.5 text-base"
>* Email</label
>
<input
v-model="form.email"
type="email"
class="w-full px-3 py-2 rounded-lg ring-4 ring-#ff7bac/20) transition-all duration-300 outline-none bg-white/90 border-none"
/>
<div class="submitted-row">
<span class="label">Email</span>
<span class="value">{{ form.email || "Not filled in" }}</span>
</div>
<div>
<label class="block text-gray-700 font-semibold mb-1.5 text-base"
>* Company</label
>
<input
v-model="form.company"
type="text"
class="w-full px-3 py-2 rounded-lg ring-4 ring-#ff7bac/20) transition-all duration-300 outline-none bg-white/90 border-none"
/>
<div class="submitted-row">
<span class="label">Company</span>
<span class="value">{{ form.company || "Not filled in" }}</span>
</div>
<div>
<label class="block text-gray-700 font-semibold mb-1.5 text-base"
>Phone</label
>
<input
v-model="form.phone"
type="tel"
class="w-full px-3 py-2 rounded-lg ring-4 ring-#ff7bac/20) transition-all duration-300 outline-none bg-white/90 border-none"
/>
</div>
<button
type="submit"
class="w-full py-3.5 rounded-xl text-white font-bold text-lg active:scale-95 transition-all duration-200 animate-bounce-in animate-delay-200 mt-3 submit-btn"
>
Submit
</button>
</form>
</template>
<template v-else>
<div
class="flex flex-col items-center justify-center min-h-[240px] animate-bounce-in"
>
<span
class="i-mdi:check-circle-outline text-green-500 text-5xl mb-4"
></span>
<h2 class="text-xl font-bold text-#ff7bac mb-3">
Submitted successfully!
</h2>
<div class="text-gray-700 text-base mb-4">
The information you submitted is as follows:
</div>
<div
class="w-full bg-white/90 rounded-xl shadow p-4 space-y-2 text-gray-800 text-base"
>
<div>
<span class="font-semibold">First Name</span
>{{ form.firstName }}
</div>
<div>
<span class="font-semibold">Last Name</span
>{{ form.lastName }}
</div>
<div>
<span class="font-semibold">Email</span>{{ form.email }}
</div>
<div>
<span class="font-semibold">Company</span>{{ form.company }}
</div>
<div>
<span class="font-semibold">Phone</span
>{{ form.phone || "(Not filled)" }}
</div>
<div>
<span class="font-semibold">Alert Type</span
>{{
form.alertType === "all" ? "All Alerts" : "Customize Alerts"
}}
</div>
<div class="submitted-row">
<span class="label">Phone</span>
<span class="value">{{ form.phone || "Not filled in" }}</span>
</div>
</div>
</template>
</div>
</main>
</main>
</div>
<div class="submitted-bg"></div>
</template>
</div>
</div>
</template>
<style scoped lang="scss">
/* Keep tablet background simple */
.alerts-container {
width: 650 * 2.5px;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.title-section {
margin-top: 38 * 2.5px;
display: flex;
flex-direction: column;
gap: 8 * 2.5px;
padding: 0 16 * 2.5px;
margin-bottom: 32 * 2.5px;
align-self: center;
}
.title-decoration {
width: 58 * 2.5px;
height: 7 * 2.5px;
background: #ff7bac;
margin: auto 0;
align-self: center;
}
.title {
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 32 * 2.5px;
line-height: 1.4em;
color: #000000;
letter-spacing: 0.03em;
}
.subtitle {
font-family: "PingFang SC", sans-serif;
font-size: 14 * 2.5px;
line-height: 1.375em;
color: #455363;
letter-spacing: 0.48 * 2.5px;
align-self: center;
}
.form-card {
width: 100%;
background-color: white;
border-radius: 16 * 2.5px;
padding: 24 * 2.5px;
box-shadow: 0px 3 * 2.5px 14 * 2.5px 0px rgba(0, 0, 0, 0.16);
animation: fade-in 0.8s cubic-bezier(0.23, 1, 0.32, 1) both;
display: flex;
align-items: center;
justify-content: center;
}
.form-content {
width: 100%;
display: flex;
flex-direction: column;
gap: 16 * 2.5px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8 * 2.5px;
label {
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 14 * 2.5px;
line-height: 1.375em;
color: #000000;
letter-spacing: 0.48 * 2.5px;
}
input {
height: 38 * 2.5px;
border: 1 * 2.5px solid #e0e0e6;
border-radius: 8 * 2.5px;
padding: 0 12 * 2.5px;
font-size: 14 * 2.5px;
outline: none;
transition: border-color 0.3s;
&:focus {
border-color: #ff7bac;
}
}
}
.submit-btn {
background: linear-gradient(to right, #ff7bac, #00ffff);
height: 60 * 2.5px;
background: #ff7bac;
border-radius: 8 * 2.5px;
border: none;
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 24 * 2.5px;
line-height: 32 * 2.5px;
color: white;
letter-spacing: 1.2 * 2.5px;
cursor: pointer;
transition: opacity 0.3s;
margin-top: 8 * 2.5px; // 16px (from figma form-group gap) + 8px = 24px
margin-bottom: 16 * 2.5px;
&:hover {
opacity: 0.9;
}
}
.success-title {
font-size: 24px;
font-weight: bold;
color: #ff7bac;
margin-bottom: 16px;
}
.success-info {
margin-bottom: 24px;
color: #455363;
}
.submitted-data {
padding: 36 * 2.5px 24 * 2.5px 24 * 2.5px;
border-radius: 16 * 2.5px;
width: 678 * 2.5px;
height: 428 * 2.5px;
flex-shrink: 0;
}
.submitted-bg {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url("@/assets/image/768/email-alerts-submit.png");
background-repeat: no-repeat;
background-position: bottom;
background-size: 100%;
}
.submitted-data-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 16 * 2.5px;
padding: 16 * 2.5px 0;
}
.submitted-row {
display: flex;
font-family: "PingFang SC", sans-serif;
font-size: 14 * 2.5px;
line-height: 1.375em;
letter-spacing: 0.48 * 2.5px;
width: 280 * 2.5px;
}
.label {
font-weight: 500;
color: #000000;
width: 110 * 2.5px;
flex-shrink: 0;
}
.value {
font-weight: 400;
color: #455363;
}
@keyframes fade-in {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@ -5,7 +5,7 @@
</template>
<script setup>
import size375 from "@/views/events-calendar/size375/index.vue";
import size768 from "@/views/events-calendar/size375/index.vue";
import size768 from "@/views/events-calendar/size768/index.vue";
import size1440 from "@/views/events-calendar/size1440/index.vue";
import size1920 from "@/views/events-calendar/size1920/index.vue";
import { computed } from "vue";
@ -16,7 +16,7 @@ const viewComponent = computed(() => {
const viewWidth = width.value;
if (viewWidth <= 450) {
return size375;
} else if (viewWidth <= 768) {
} else if (viewWidth <= 1100) {
return size768;
} else if (viewWidth <= 1500) {
return size1440;

View File

@ -79,7 +79,7 @@
<!-- 背景图片区域 -->
<div class="background-image-container">
<img
src="@/assets/image/1920/events-calendar-bg.png"
src="@/assets/image/1440/events-calendar-bg.png"
alt="Events Calendar Background"
class="background-image"
/>
@ -200,7 +200,7 @@ const handleSearch = () => {
.background-image-container {
background: #fff;
width: 100%;
height: 800px;
height: 518 * 1.33px;
box-shadow: 0px 3px 14px 0px rgba(0, 0, 0, 0.16);
border-radius: 16px;
display: flex;

View File

@ -1,32 +1,57 @@
<template>
<div class="events-calendar-page">
<customDefaultPage>
<template #content>
<main class="p-[35px] max-w-[1800px] mx-auto">
<div class="title mb-[20px]">
{{ t("events_calendar.title") }}
</div>
<div class="search-container">
<n-date-picker
v-model:value="state.selectedDateValue"
type="date"
class="search-date-picker"
></n-date-picker>
<n-button @click="handleSearch" class="search-button">
{{ t("events_calendar.search.button") }}
</n-button>
</div>
</main>
</template>
</customDefaultPage>
</div>
<main class="page-container">
<!-- 标题区域 -->
<div class="title-section">
<div class="title-decoration"></div>
<div class="events-title">
{{ t("events_calendar.title") }}
</div>
</div>
<!-- 日期选择区域 -->
<div class="date-selector" @click="showPicker = true">
<span class="date-label">Date</span>
<div class="date-value">
<span>{{ selectedDateText }}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="10"
height="10"
viewBox="0 0 10 10"
fill="none"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M2.58687 9.69047C2.38438 9.48798 2.36188 9.17365 2.51937 8.9463L2.58687 8.86552L6.45252 5.00022L2.58687 1.13492C2.38438 0.932428 2.36188 0.618098 2.51937 0.390748L2.58687 0.309958C2.78936 0.107469 3.10369 0.0849685 3.33104 0.242458L3.41183 0.309958L7.68961 4.58774C7.8921 4.79023 7.9146 5.10456 7.75711 5.33191L7.68961 5.4127L3.41183 9.69047C3.18402 9.91828 2.81468 9.91828 2.58687 9.69047Z"
fill="#B6B6B6"
/>
</svg>
</div>
</div>
<!-- 内容区域 -->
<div class="content-area">
<img
src="@/assets/image/375/events-calendar-bg.png"
alt="No Events"
class="empty-state-image"
/>
</div>
<YearMonthWheelPicker
v-if="showPicker"
v-model="selectedDate"
@close="showPicker = false"
@confirm="handleDateConfirm"
/>
</main>
</template>
<script setup>
import customDefaultPage from "@/components/customDefaultPage/index.vue";
import { reactive } from "vue";
import { NDatePicker, NButton } from "naive-ui";
import { reactive, ref, computed } from "vue";
import { useI18n } from "vue-i18n";
import YearMonthWheelPicker from "@/components/YearMonthWheelPicker.vue";
const { t } = useI18n();
@ -34,55 +59,104 @@ const state = reactive({
selectedDateValue: null, //
});
const handleSearch = () => {
//
// console.log(':', state.selectedDateValue)
};
const showPicker = ref(false);
const now = new Date();
const selectedDate = ref({
year: now.getFullYear(),
month: now.getMonth() + 1,
});
const selectedDateText = computed(() => {
if (selectedDate.value) {
return `${selectedDate.value.year}-${String(
selectedDate.value.month
).padStart(2, "0")}`;
}
return "Select Date";
});
function handleDateConfirm(date) {
selectedDate.value = date;
showPicker.value = false;
// TODO: Add logic to fetch events for the selected date
}
</script>
<style scoped lang="scss">
.title {
font-size: 113px;
font-weight: bold;
color: #333;
text-align: center;
margin-top: 8px;
.page-container {
width: 343 * 5.12px;
margin: 0 auto;
}
.search-container {
margin-bottom: 24px;
.title-section {
display: flex;
flex-direction: row;
align-items: center;
background-color: #f6f7f9;
border-radius: 8px;
padding: 8px;
gap: 16px;
flex-direction: column;
gap: 16 * 5.12px;
margin-top: 43 * 5.12px;
padding: 0 16 * 5.12px;
}
.search-date-picker {
width: 100%;
}
:deep(.n-date-picker) {
width: 100%;
.n-input__input {
padding: 4px 0;
border-radius: 4px;
}
}
:deep(.n-button) {
width: 260px;
padding: 20px 16px;
border-radius: 4px;
}
.search-button {
.title-decoration {
width: 58 * 5.12px;
height: 7 * 5.12px;
background: #ff7bac;
color: #fff;
&:hover {
background: #ff7bac;
color: #fff;
margin: auto 0;
margin-top: 0;
}
.events-title {
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 24 * 5.12px;
line-height: 1;
color: #000000;
}
.date-selector {
display: flex;
justify-content: space-between;
align-items: center;
height: 20 * 5.12px;
margin: 32 * 5.12px 0;
padding: 0 16 * 5.12px;
.date-label {
font-family: "PingFang SC", sans-serif;
font-size: 14 * 5.12px;
font-weight: 400;
line-height: 1;
letter-spacing: 0.48 * 5.12px;
color: #000;
}
.date-value {
display: flex;
align-items: center;
gap: 16 * 5.12px;
span {
font-family: "PingFang SC", sans-serif;
font-size: 14 * 5.12px;
font-weight: 400;
line-height: 1;
letter-spacing: 0.48 * 5.12px;
color: #b6b6b6;
}
img {
width: 10 * 5.12px;
height: 10 * 5.12px;
}
}
}
.content-area {
background: #fff;
height: 456 * 5.12px;
box-shadow: 0 * 5.12px 3 * 5.12px 14 * 5.12px 0 * 5.12px rgba(0, 0, 0, 0.16);
border-radius: 16 * 5.12px;
display: flex;
justify-content: center;
align-items: center;
}
.empty-state-image {
width: 243 * 5.12px;
height: 138 * 5.12px;
}
</style>

View File

@ -0,0 +1,218 @@
<template>
<div class="events-calendar-page">
<main class="page-container">
<div class="events-container">
<!-- 标题区域 -->
<div class="title-section">
<div class="title-decoration"></div>
<div class="events-title">
{{ t("events_calendar.title") }}
</div>
</div>
<!-- 搜索区域 -->
<div class="search-container">
<div class="date-picker-wrapper">
<n-date-picker
v-model:value="state.selectedDateValue"
type="date"
class="search-date-picker"
placeholder="Select Date"
>
<template #prefix>
<svg
class="calendar-icon"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
>
<g clip-path="url(#clip0_134_3094)">
<path
d="M17.5 3.33398H2.5C2.08333 3.33398 1.66667 3.66732 1.66667 4.08398V17.5007C1.66667 17.9173 2.08333 18.2507 2.5 18.2507H17.5C17.9167 18.2507 18.3333 17.9173 18.3333 17.5007V4.08398C18.3333 3.66732 17.9167 3.33398 17.5 3.33398Z"
stroke="#78777B"
stroke-width="1.25"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M13.3333 1.66602V5.00018"
stroke="#78777B"
stroke-width="1.25"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M6.66669 1.66602V5.00018"
stroke="#78777B"
stroke-width="1.25"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M1.66669 8.33398H18.3334"
stroke="#78777B"
stroke-width="1.25"
stroke-linecap="round"
stroke-linejoin="round"
></path>
</g>
<defs>
<clipPath id="clip0_134_3094">
<rect
width="20"
height="20"
fill="white"
transform="translate(0 0.000976562)"
></rect>
</clipPath>
</defs>
</svg>
</template>
</n-date-picker>
</div>
<button @click="handleSearch" class="search-button">
{{ t("events_calendar.search.button") }}
</button>
</div>
<!-- 背景图片区域 -->
<div class="background-image-container">
<img
src="@/assets/image/768/events-calendar-bg.png"
alt="Events Calendar Background"
class="background-image"
/>
</div>
</div>
</main>
</div>
</template>
<script setup>
import { reactive } from "vue";
import { NDatePicker, NButton } from "naive-ui";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const state = reactive({
selectedDateValue: null, //
});
const handleSearch = () => {
//
// console.log(':', state.selectedDateValue)
};
</script>
<style scoped lang="scss">
.page-container {
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
.events-container {
width: 650 * 2.5px;
margin: 0 auto;
}
.title-section {
display: flex;
flex-direction: column;
gap: 16 * 2.5px;
margin-bottom: 32 * 2.5px;
padding: 0 16 * 2.5px;
}
.title-decoration {
width: 58 * 2.5px;
height: 7 * 2.5px;
background: #ff7bac;
margin: auto 0;
margin-top: 43 * 2.5px;
}
.events-title {
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 32 * 2.5px;
line-height: 1.4em;
letter-spacing: 0.03em;
color: #000000;
}
.search-container {
margin-bottom: 20 * 2.5px;
display: flex;
align-items: center;
gap: 16 * 2.5px;
padding: 0 16 * 2.5px;
}
.date-picker-wrapper {
flex: 1;
position: relative;
}
.search-date-picker {
width: 100%;
}
:deep(.n-input) {
height: 34 * 2.5px;
border-radius: 3 * 2.5px;
border: 1 * 2.5px solid #e0e0e6;
&:hover {
border-color: #ff7bac;
}
}
:deep(.n-input--focus) {
border-color: #ff7bac;
box-shadow: 0 0 0 2px rgba(255, 123, 172, 0.2);
}
.calendar-icon {
margin-left: 12 * 2.5px;
}
.search-button {
height: 34 * 2.5px;
padding: 7 * 2.5px 12 * 2.5px;
color: #fff;
background-color: #ff7bac;
border: none;
border-radius: 3 * 2.5px;
cursor: pointer;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 2.5px;
line-height: 1.375em;
letter-spacing: 0.03em;
min-width: 160 * 2.5px;
&:hover {
background-color: #e66f9a;
}
}
.background-image-container {
background: #fff;
width: 100%;
height: 511 * 2.5px;
margin-top: 20 * 2.5px;
box-shadow: 0px 3px 14px 0px rgba(0, 0, 0, 0.16);
border-radius: 16 * 2.5px;
display: flex;
justify-content: center;
align-items: center;
}
.background-image {
width: 300 * 2.5px;
height: 170 * 2.5px;
flex-shrink: 0;
aspect-ratio: 83/47;
}
</style>

View File

@ -498,7 +498,7 @@ const handleClickOutside = (event) => {
letter-spacing: 0.03em;
color: #455363;
margin: 0;
padding-left: 16px;
padding: 0 16px;
margin-top: 8px;
}

View File

@ -496,7 +496,7 @@ const handleClickOutside = (event) => {
letter-spacing: 3%;
color: #455363;
margin: 0;
padding-left: 16px;
padding: 0 16px;
margin-top: 8px;
}

View File

@ -493,7 +493,7 @@ const handleClickOutside = (event) => {
letter-spacing: 0.03em;
color: #455363;
margin: 0;
padding-left: 16 * 2.5px;
padding: 0 16 * 2.5px;
margin-top: 8 * 2.5px;
}

View File

@ -1,94 +1,184 @@
<template>
<div class="historic-data-container" style="margin-bottom: 40px">
<div class="historic-data-container">
<div class="echarts-container">
<customEcharts></customEcharts>
</div>
<div class="header mt-[80px]">
<div class="title">Historical Data</div>
<div class="filter-container">
<!-- <n-dropdown
trigger="click"
:options="periodOptions"
@select="handlePeriodChange"
:value="state.selectedPeriod"
>
<n-button>
{{ state.selectedPeriod }}
<n-icon><chevron-down-outline /></n-icon>
</n-button>
</n-dropdown> -->
<n-dropdown
trigger="click"
:options="durationOptions"
@select="handleDurationChange"
:value="state.selectedDuration"
>
<n-button>
{{ state.selectedDuration }}
<n-icon><chevron-down-outline /></n-icon>
</n-button>
</n-dropdown>
<div class="header">
<!-- 标题区域 -->
<div class="title-section">
<div class="title-decoration"></div>
<div class="title-text">Historical Data</div>
</div>
</div>
<n-data-table
:columns="columns"
:data="paginatedData"
:bordered="false"
:single-line="false"
:scroll-x="600"
/>
<div class="filter-container">
<span class="range-label">Range</span>
<div class="filter-row">
<div
v-for="option in durationOptions"
:key="option.key"
class="filter-option"
:class="{ active: state.selectedDuration === option.key }"
@click="handleDurationChange(option.key)"
>
{{
option.label
.replace(" Months", "m")
.replace(" Years", "Y")
.replace(" Year", "Y")
.replace(" to Date", "TD")
}}
</div>
</div>
</div>
<!-- reports-table from annualreports -->
<div class="reports-table">
<div class="table-container">
<div class="table-header">
<div
class="column"
v-for="col in columns"
:key="col.key"
:style="{
width: col.width ? col.width + 'px' : 'auto',
flex: col.width ? '0 0 ' + col.width + 'px' : '1 0 116px',
'text-align': col.align,
}"
>
{{ col.title }}
</div>
</div>
<div class="reports-list">
<div
class="table-row"
v-for="(row, index) in paginatedData"
:key="index"
>
<div
class="column"
v-for="col in columns"
:key="col.key"
:style="{
width: col.width ? col.width + 'px' : 'auto',
flex: col.width ? '0 0 ' + col.width + 'px' : '1 0 116px',
'text-align': col.align,
}"
>
<span
v-if="col.key === 'change'"
:style="{
color:
parseFloat(row.change) < 0
? '#cf3050'
: parseFloat(row.change) > 0
? '#18a058'
: '',
}"
>
{{ row[col.key] }}
</span>
<span v-else>
{{ row[col.key] }}
</span>
</div>
</div>
</div>
</div>
</div>
<!-- pagination-container from annualreports -->
<div class="pagination-container">
<n-button class="page-btn prev-btn" @click="handlePrevPage">
<n-icon><chevron-back-outline /></n-icon>
</n-button>
<div class="pagination-controls">
<div class="pagination-buttons">
<button
class="page-btn prev-btn"
:disabled="state.currentPage === 1"
@click="goToPrevPage"
>
<svg width="5" height="9" viewBox="0 0 5 9" fill="none">
<path
d="M4 1L1 4.5L4 8"
stroke="#455363"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
<div class="page-info mr-[40px]">
{{ state.currentPage }} of {{ totalPages }}
</div>
<template v-for="page in getVisiblePages()" :key="page">
<button
v-if="page !== '...'"
class="page-btn"
:class="{ active: page === state.currentPage }"
@click="goToPage(page)"
>
{{ page }}
</button>
<button v-else class="page-btn disabled" disabled>...</button>
</template>
<div class="right-controls">
<n-dropdown
trigger="click"
:options="pageSizeOptions"
@select="handlePageSizeChange"
>
<n-button class="rows-dropdown">
{{ state.pageSize }} Rows
<n-icon><chevron-down-outline /></n-icon>
</n-button>
</n-dropdown>
<n-button class="page-btn next-btn" @click="handleNextPage">
<n-icon><chevron-forward-outline /></n-icon>
</n-button>
<button
class="page-btn next-btn"
:disabled="state.currentPage === totalPages"
@click="goToNextPage"
>
<svg width="5" height="9" viewBox="0 0 5 9" fill="none">
<path
d="M1 1L4 4.5L1 8"
stroke="#455363"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
<div class="page-size-selector" @click="togglePageSizeMenu">
<span>{{ state.pageSize }}/page</span>
<svg width="10" height="5" viewBox="0 0 10 5" fill="none">
<path
d="M1 1L5 4L9 1"
stroke="#455363"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<div v-if="showPageSizeMenu" class="page-size-menu">
<div
v-for="size in [10, 50, 100, 500, 1000]"
:key="size"
class="page-size-option"
:class="{ active: state.pageSize === size }"
@click="handlePageSizeChange(size)"
>
{{ size }}/page
</div>
</div>
</div>
</div>
</div>
<div class="pagination-info">
Displaying {{ displayRange.start }} - {{ displayRange.end }} of
{{ state.tableData.length }} results
</div>
<div class="back-to-top-link">
<!-- <div class="back-to-top-link">
<a href="#" @click.prevent="scrollToTop">
Back to Top
<n-icon><arrow-up-outline /></n-icon>
</a>
</div>
</div> -->
</div>
</template>
<script setup>
import { NDataTable, NButton, NDropdown, NIcon } from "naive-ui";
import { reactive, onMounted, h, computed } from "vue";
import { NDropdown, NIcon } from "naive-ui";
import { reactive, onMounted, h, computed, ref, watch, onUnmounted } from "vue";
import axios from "axios";
import {
ChevronDownOutline,
ChevronBackOutline,
ChevronForwardOutline,
ArrowUpOutline,
} from "@vicons/ionicons5";
import { ChevronDownOutline, ArrowUpOutline } from "@vicons/ionicons5";
import defaultTableData from "../data";
// console.log('defaultTableData', defaultTableData)
import customEcharts from "@/components/customEcharts/index.vue";
//
@ -103,15 +193,15 @@ const periodOptions = [
const durationOptions = [
{ label: "3 Months", key: "3 Months" },
{ label: "6 Months", key: "6 Months" },
{ label: "Year to Date", key: "Year to Date" },
{ label: "YTD", key: "Year to Date" },
{ label: "1 Year", key: "1 Year" },
{ label: "5 Years", key: "5 Years" },
{ label: "10 Years", key: "10 Years" },
// { label: 'Full History', key: 'Full History', disabled: true },
];
//
const pageSizeOptions = [
{ label: "10", key: 10 },
{ label: "50", key: 50 },
{ label: "100", key: 100 },
{ label: "500", key: 500 },
@ -123,9 +213,12 @@ const state = reactive({
selectedDuration: "6 Months",
tableData: [],
currentPage: 1,
pageSize: 50,
pageSize: 10,
gotoPage: 1,
});
const showPageSizeMenu = ref(false);
//
const totalPages = computed(() => {
return Math.ceil(state.tableData.length / state.pageSize);
@ -145,47 +238,49 @@ const columns = [
key: "date",
align: "left",
fixed: "left",
width: 150,
width: 152,
},
{
title: "Open",
key: "open",
align: "center",
width: 116,
},
{
title: "High",
key: "high",
align: "center",
width: 116,
},
{
title: "Low",
key: "low",
align: "center",
width: 116,
},
{
title: "Close",
key: "close",
align: "center",
width: 116,
},
{
title: "Adj. Close",
key: "adjClose",
align: "center",
width: 116,
},
{
title: "Change",
key: "change",
align: "center",
render(row) {
const value = parseFloat(row.change);
const color = value < 0 ? "#ff4d4f" : value > 0 ? "#52c41a" : "";
return h("span", { style: { color } }, row.change);
},
width: 116,
},
{
title: "Volume",
key: "volume",
align: "center",
width: 116,
},
];
@ -213,26 +308,62 @@ const handleDurationChange = (key) => {
getPageData();
};
const displayRange = computed(() => {
const start = (state.currentPage - 1) * state.pageSize + 1;
const end = Math.min(
state.currentPage * state.pageSize,
state.tableData.length
);
return { start, end };
});
const goToPage = (page) => {
if (page >= 1 && page <= totalPages.value) {
state.currentPage = page;
}
};
const goToPrevPage = () => {
if (state.currentPage > 1) {
state.currentPage--;
}
};
const goToNextPage = () => {
if (state.currentPage < totalPages.value) {
state.currentPage++;
}
};
//
const handlePrevPage = () => {
if (state.currentPage === 1) {
return;
}
state.currentPage--;
};
const handleNextPage = () => {
if (state.currentPage >= totalPages.value) {
return;
}
state.currentPage++;
};
const handlePageSizeChange = (size) => {
state.pageSize = size;
state.currentPage = 1; //
};
const handleGoto = () => {
const page = parseInt(state.gotoPage);
if (page >= 1 && page <= totalPages.value) {
state.currentPage = page;
}
};
const togglePageSizeMenu = () => {
showPageSizeMenu.value = !showPageSizeMenu.value;
};
const getVisiblePages = () => {
const total = totalPages.value;
if (total <= 4) {
const pages = [];
for (let i = 1; i <= total; i++) {
pages.push(i);
}
return pages;
}
return [1, 2, "...", total];
};
//
const scrollToTop = () => {
//
@ -248,8 +379,32 @@ const scrollToTop = () => {
};
onMounted(() => {
getPageData();
document.addEventListener("click", handleClickOutside);
});
onUnmounted(() => {
document.removeEventListener("click", handleClickOutside);
});
const handleClickOutside = (event) => {
if (!event.target.closest(".page-size-selector")) {
showPageSizeMenu.value = false;
}
};
watch(
() => state.pageSize,
() => {
state.currentPage = 1;
}
);
watch(
() => state.currentPage,
(newPage) => {
state.gotoPage = newPage;
}
);
const getPageDefaultData = async () => {
try {
let url =
@ -341,24 +496,25 @@ const getPageData = async () => {
String(fromDate.getMonth() + 1).padStart(2, "0") +
"-" +
String(fromDate.getDate()).padStart(2, "0");
// let url = `https://stockanalysis.com/api/symbol/a/OTC-MINM/history?period=${state.selectedPeriod}&range=${range}`
let url =
"https://common.szjixun.cn/api/stock/history/list?from=" +
finalFromDate +
"&to=" +
toDate;
const res = await axios.get(url);
// console.error(res)
if (res.status === 200) {
if (res.data.status === 0) {
// "Nov 26, 2024"
let resultData = res.data.data.map((item) => {
return {
date: new Date(item.date).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
}),
date: new Date(item.date.replace(/-/g, "/")).toLocaleDateString(
"en-US",
{
month: "short",
day: "numeric",
year: "numeric",
}
),
open: item.open != null ? Number(item.open).toFixed(2) : "",
high: item.high != null ? Number(item.high).toFixed(2) : "",
low: item.low != null ? Number(item.low).toFixed(2) : "",
@ -379,82 +535,83 @@ const getPageData = async () => {
<style scoped lang="scss">
.historic-data-container {
padding: 80px;
width: 343 * 5.12px;
margin: 0 auto;
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.title {
font-size: 80px;
font-size: 40 * 2.5 * 5.12px;
font-weight: bold;
margin: 0;
}
.filter-container {
display: flex;
gap: 40px;
}
}
.pagination-container {
.filter-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 60px;
padding: 10px 16px;
border-radius: 4px;
background-color: #ffffff;
flex-direction: column;
align-items: flex-start;
gap: 8 * 5.12px;
padding: 0 16 * 5.12px;
margin-bottom: 32 * 5.12px;
.page-btn {
display: flex;
align-items: center;
gap: 5px;
padding: 6px 12px;
font-size: 92px;
&.prev-btn {
margin-right: auto;
}
&.next-btn {
margin-left: 10px;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.range-label {
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
line-height: 1.4;
letter-spacing: 0.48 * 5.12px;
color: #455363;
}
.page-info {
font-size: 72px;
color: #374151;
.filter-row {
width: 311 * 5.12px;
display: flex;
flex-wrap: wrap;
column-gap: 16 * 5.12px;
row-gap: 8 * 5.12px;
}
.right-controls {
.filter-option {
display: flex;
align-items: center;
justify-content: center;
height: 34 * 5.12px;
border-radius: 3 * 5.12px;
background-color: #efefef;
cursor: pointer;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
line-height: 1.4;
color: #000000;
transition: all 0.2s ease;
width: 93 * 5.12px;
.rows-dropdown {
font-size: 72px;
&:hover {
background-color: #e0e0e0;
}
&.active {
background-color: #ff7bac;
color: #ffffff;
}
}
}
.back-to-top-link {
display: flex;
justify-content: center;
margin-top: 56px;
margin-top: 16 * 5.12px;
a {
display: flex;
align-items: center;
gap: 5px;
gap: 5 * 5.12px;
color: #2563eb;
font-size: 92px;
font-size: 20 * 5.12px;
font-weight: bold;
text-decoration: none;
@ -463,11 +620,300 @@ const getPageData = async () => {
}
}
}
}
:deep(.n-data-table) {
.n-data-table-td {
padding: 12px 8px;
}
.reports-table {
width: 100%;
background: #ffffff;
border-radius: 16 * 5.12px;
box-shadow: 0 * 5.12px 3 * 5.12px 14 * 5.12px 0 * 5.12px rgba(0, 0, 0, 0.16);
padding: 16 * 5.12px;
}
.table-container {
width: 100%;
overflow-x: auto;
}
.table-container::-webkit-scrollbar {
height: 8 * 5.12px;
}
.table-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4 * 5.12px;
}
.table-container::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 4 * 5.12px;
}
.table-container::-webkit-scrollbar-thumb:hover {
background: #fff0f5;
cursor: pointer;
}
.column {
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
line-height: 1.4;
letter-spacing: 0.48 * 5.12px;
color: #455363;
padding: 16 * 5.12px;
position: relative;
font-variant-numeric: tabular-nums; /* 让数字等宽对齐 */
}
.table-header {
display: flex;
border-radius: 8 * 5.12px;
margin-bottom: 4 * 5.12px;
align-items: center;
position: sticky;
top: 0;
z-index: 2;
.column {
background: #fff0f5;
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 14 * 5.12px;
line-height: 1.4;
letter-spacing: 0.48 * 5.12px;
color: #000000;
}
}
.table-row {
display: flex;
align-items: center;
position: relative;
border-radius: 8 * 5.12px;
&:hover .column {
background: #fff8fb;
}
// &:last-child {
// border-bottom: none;
// }
&:nth-child(even) {
margin: 4 * 5.12px 0;
}
}
.reports-list {
display: flex;
flex-direction: column;
gap: 4 * 5.12px;
}
.table-row .column:not(:last-child)::after {
content: "";
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
height: 40 * 5.12px;
border-right: 1 * 5.12px dashed #e0e0e6;
}
.table-header .column:first-child {
border-radius: 8 * 5.12px 0 0 8 * 5.12px;
}
.table-header .column:last-child,
.table-row .column:last-child {
border-radius: 0 8 * 5.12px 8 * 5.12px 0;
}
.table-row .column:first-child {
background: #ffffff;
}
.table-row:hover .column:first-child {
background: #fff8fb;
}
//
.pagination-container {
display: flex;
align-items: center;
margin-top: 16 * 5.12px;
justify-content: flex-end;
padding: 0 4 * 5.12px;
}
.pagination-info {
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
line-height: 1.4;
color: #455363;
text-align: right;
margin-top: 16 * 5.12px;
margin-bottom: 16 * 5.12px;
padding: 0 4 * 5.12px;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 8 * 5.12px;
}
.pagination-buttons {
display: flex;
align-items: center;
gap: 8 * 5.12px;
}
.page-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28 * 5.12px;
height: 28 * 5.12px;
border: 1 * 5.12px solid #e0e0e6;
border-radius: 3 * 5.12px;
background: #ffffff;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
line-height: 1.4;
color: #455363;
cursor: pointer;
transition: all 0.2s ease;
&:hover:not(:disabled) {
border-color: #ff7bac;
color: #ff7bac;
}
&.active {
border-color: #ff7bac;
color: #ff7bac;
background: #fff0f5;
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
.page-size-selector {
position: relative;
display: flex;
align-items: center;
gap: 18 * 5.12px;
padding: 4 * 5.12px 12 * 5.12px;
height: 28 * 5.12px;
border: 1 * 5.12px solid #e0e0e6;
border-radius: 3 * 5.12px;
background: #ffffff;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
line-height: 1.4;
color: #455363;
cursor: pointer;
&:hover {
border-color: #ff7bac;
}
}
.page-size-menu {
position: absolute;
bottom: 100%;
left: 0;
right: 0;
background: #ffffff;
border: 1 * 5.12px solid #e0e0e6;
border-radius: 3 * 5.12px;
box-shadow: 0 2 * 5.12px 8 * 5.12px rgba(0, 0, 0, 0.1);
z-index: 1000;
margin-bottom: 5 * 5.12px;
}
.page-size-option {
padding: 8 * 5.12px 12 * 5.12px;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
line-height: 1.4;
color: #455363;
cursor: pointer;
transition: background-color 0.2s ease;
&:hover {
background: #fff0f5;
}
&.active {
background: #fff0f5;
color: #ff7bac;
}
}
.goto-section {
display: flex;
align-items: center;
gap: 8 * 5.12px;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
line-height: 1.4;
color: #455363;
margin-right: 16 * 5.12px;
}
.goto-input {
width: 60 * 5.12px;
height: 28 * 5.12px;
padding: 4 * 5.12px 12 * 5.12px;
border: 1 * 5.12px solid #e0e0e6;
border-radius: 3 * 5.12px;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
line-height: 1.4;
color: #455363;
text-align: center;
&:focus {
outline: none;
border-color: #ff7bac;
}
}
.title-section {
display: flex;
flex-direction: column;
gap: 16 * 5.12px;
margin-bottom: 32 * 5.12px;
margin-top: 43 * 5.12px;
padding: 0 16 * 5.12px;
}
.title-decoration {
width: 58 * 5.12px;
height: 7 * 5.12px;
background: #ff7bac;
margin: auto 0;
margin-top: 0;
}
.title-text {
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 24 * 5.12px;
line-height: 1.4;
letter-spacing: 0.03em;
color: #000000;
}
</style>

View File

@ -1,96 +1,193 @@
<template>
<div class="historic-data-container" style="margin-bottom: 40px">
<div class="historic-data-container">
<div class="echarts-container">
<customEcharts></customEcharts>
</div>
<div class="header mt-[20px]">
<div class="title">Historical Data</div>
<div class="filter-container">
<!-- <n-dropdown
trigger="click"
:options="periodOptions"
@select="handlePeriodChange"
:value="state.selectedPeriod"
>
<n-button>
{{ state.selectedPeriod }}
<n-icon><chevron-down-outline /></n-icon>
</n-button>
</n-dropdown> -->
<n-dropdown
trigger="click"
:options="durationOptions"
@select="handleDurationChange"
:value="state.selectedDuration"
>
<n-button>
{{ state.selectedDuration }}
<n-icon><chevron-down-outline /></n-icon>
</n-button>
</n-dropdown>
<div class="header">
<!-- 标题区域 -->
<div class="title-section">
<div class="title-decoration"></div>
<div class="title-text">Historical Data</div>
</div>
</div>
<n-data-table
:columns="columns"
:data="paginatedData"
:bordered="false"
:single-line="false"
:scroll-x="1200"
/>
<div class="filter-container">
<span class="range-label">Range</span>
<div
v-for="option in durationOptions"
:key="option.key"
class="filter-option"
:class="{ active: state.selectedDuration === option.key }"
@click="handleDurationChange(option.key)"
>
{{
option.label
.replace(" Months", "m")
.replace(" Years", "Y")
.replace(" Year", "Y")
.replace(" to Date", "TD")
}}
</div>
</div>
<!-- reports-table from annualreports -->
<div class="reports-table">
<div class="table-container">
<div class="table-header">
<div
class="column"
v-for="col in columns"
:key="col.key"
:style="{
width: col.width ? col.width + 'px' : 'auto',
flex: col.width ? 'none' : '1 0 120px',
'text-align': col.align,
}"
>
{{ col.title }}
</div>
</div>
<div class="reports-list">
<div
class="table-row"
v-for="(row, index) in paginatedData"
:key="index"
>
<div
class="column"
v-for="col in columns"
:key="col.key"
:style="{
width: col.width ? col.width + 'px' : 'auto',
flex: col.width ? 'none' : '1 0 120px',
'text-align': col.align,
}"
>
<span
v-if="col.key === 'change'"
:style="{
color:
parseFloat(row.change) < 0
? '#cf3050'
: parseFloat(row.change) > 0
? '#18a058'
: '',
}"
>
{{ row[col.key] }}
</span>
<span v-else>
{{ row[col.key] }}
</span>
</div>
</div>
</div>
</div>
</div>
<!-- pagination-container from annualreports -->
<div class="pagination-container">
<n-button class="page-btn prev-btn" @click="handlePrevPage">
<n-icon><chevron-back-outline /></n-icon>
Previous
</n-button>
<div class="pagination-controls">
<div class="pagination-buttons">
<button
class="page-btn prev-btn"
:disabled="state.currentPage === 1"
@click="goToPrevPage"
>
<svg width="5" height="9" viewBox="0 0 5 9" fill="none">
<path
d="M4 1L1 4.5L4 8"
stroke="#455363"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
<div class="page-info">
Page {{ state.currentPage }} of {{ totalPages }}
</div>
<template v-for="page in getVisiblePages()" :key="page">
<button
v-if="page !== '...'"
class="page-btn"
:class="{ active: page === state.currentPage }"
@click="goToPage(page)"
>
{{ page }}
</button>
<button v-else class="page-btn disabled" disabled>...</button>
</template>
<div class="right-controls">
<n-dropdown
trigger="click"
:options="pageSizeOptions"
@select="handlePageSizeChange"
>
<n-button class="rows-dropdown">
{{ state.pageSize }} Rows
<n-icon><chevron-down-outline /></n-icon>
</n-button>
</n-dropdown>
<n-button class="page-btn next-btn" @click="handleNextPage">
Next
<n-icon><chevron-forward-outline /></n-icon>
</n-button>
<button
class="page-btn next-btn"
:disabled="state.currentPage === totalPages"
@click="goToNextPage"
>
<svg width="5" height="9" viewBox="0 0 5 9" fill="none">
<path
d="M1 1L4 4.5L1 8"
stroke="#455363"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
<div class="page-size-selector" @click="togglePageSizeMenu">
<span>{{ state.pageSize }}/page</span>
<svg width="10" height="5" viewBox="0 0 10 5" fill="none">
<path
d="M1 1L5 4L9 1"
stroke="#455363"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<div v-if="showPageSizeMenu" class="page-size-menu">
<div
v-for="size in [10, 50, 100, 500, 1000]"
:key="size"
class="page-size-option"
:class="{ active: state.pageSize === size }"
@click="handlePageSizeChange(size)"
>
{{ size }}/page
</div>
</div>
</div>
<div class="goto-section">
<span>Goto</span>
<input
type="number"
v-model="state.gotoPage"
class="goto-input"
:min="1"
:max="totalPages"
@keyup.enter="handleGoto"
/>
</div>
</div>
</div>
<div class="pagination-info">
Displaying {{ displayRange.start }} - {{ displayRange.end }} of
{{ state.tableData.length }} results
</div>
<div class="back-to-top-link">
<!-- <div class="back-to-top-link">
<a href="#" @click.prevent="scrollToTop">
Back to Top
<n-icon><arrow-up-outline /></n-icon>
</a>
</div>
</div> -->
</div>
</template>
<script setup>
import { NDataTable, NButton, NDropdown, NIcon } from "naive-ui";
import { reactive, onMounted, h, computed } from "vue";
import { NDropdown, NIcon } from "naive-ui";
import { reactive, onMounted, h, computed, ref, watch, onUnmounted } from "vue";
import axios from "axios";
import {
ChevronDownOutline,
ChevronBackOutline,
ChevronForwardOutline,
ArrowUpOutline,
} from "@vicons/ionicons5";
import { ChevronDownOutline, ArrowUpOutline } from "@vicons/ionicons5";
import defaultTableData from "../data";
// console.log('defaultTableData', defaultTableData)
import customEcharts from "@/components/customEcharts/index.vue";
//
@ -105,15 +202,15 @@ const periodOptions = [
const durationOptions = [
{ label: "3 Months", key: "3 Months" },
{ label: "6 Months", key: "6 Months" },
{ label: "Year to Date", key: "Year to Date" },
{ label: "YTD", key: "Year to Date" },
{ label: "1 Year", key: "1 Year" },
{ label: "5 Years", key: "5 Years" },
{ label: "10 Years", key: "10 Years" },
// { label: 'Full History', key: 'Full History', disabled: true },
];
//
const pageSizeOptions = [
{ label: "10", key: 10 },
{ label: "50", key: 50 },
{ label: "100", key: 100 },
{ label: "500", key: 500 },
@ -125,9 +222,12 @@ const state = reactive({
selectedDuration: "6 Months",
tableData: [],
currentPage: 1,
pageSize: 50,
pageSize: 10,
gotoPage: 1,
});
const showPageSizeMenu = ref(false);
//
const totalPages = computed(() => {
return Math.ceil(state.tableData.length / state.pageSize);
@ -173,16 +273,12 @@ const columns = [
title: "Adj. Close",
key: "adjClose",
align: "center",
width: 115,
},
{
title: "Change",
key: "change",
align: "center",
render(row) {
const value = parseFloat(row.change);
const color = value < 0 ? "#ff4d4f" : value > 0 ? "#52c41a" : "";
return h("span", { style: { color } }, row.change);
},
},
{
title: "Volume",
@ -215,26 +311,91 @@ const handleDurationChange = (key) => {
getPageData();
};
const displayRange = computed(() => {
const start = (state.currentPage - 1) * state.pageSize + 1;
const end = Math.min(
state.currentPage * state.pageSize,
state.tableData.length
);
return { start, end };
});
const goToPage = (page) => {
if (page >= 1 && page <= totalPages.value) {
state.currentPage = page;
}
};
const goToPrevPage = () => {
if (state.currentPage > 1) {
state.currentPage--;
}
};
const goToNextPage = () => {
if (state.currentPage < totalPages.value) {
state.currentPage++;
}
};
//
const handlePrevPage = () => {
if (state.currentPage === 1) {
return;
}
state.currentPage--;
};
const handleNextPage = () => {
if (state.currentPage >= totalPages.value) {
return;
}
state.currentPage++;
};
const handlePageSizeChange = (size) => {
state.pageSize = size;
state.currentPage = 1; //
};
const handleGoto = () => {
const page = parseInt(state.gotoPage);
if (page >= 1 && page <= totalPages.value) {
state.currentPage = page;
}
};
const togglePageSizeMenu = () => {
showPageSizeMenu.value = !showPageSizeMenu.value;
};
const getVisiblePages = () => {
const current = state.currentPage;
const total = totalPages.value;
const pages = [];
if (total <= 7) {
// 7
for (let i = 1; i <= total; i++) {
pages.push(i);
}
} else {
//
pages.push(1);
if (current <= 4) {
// 4
for (let i = 2; i <= 5; i++) {
pages.push(i);
}
pages.push("...");
pages.push(total);
} else if (current >= total - 3) {
// 4
pages.push("...");
for (let i = total - 4; i <= total; i++) {
pages.push(i);
}
} else {
//
pages.push("...");
for (let i = current - 1; i <= current + 1; i++) {
pages.push(i);
}
pages.push("...");
pages.push(total);
}
}
return pages;
};
//
const scrollToTop = () => {
//
@ -250,8 +411,32 @@ const scrollToTop = () => {
};
onMounted(() => {
getPageData();
document.addEventListener("click", handleClickOutside);
});
onUnmounted(() => {
document.removeEventListener("click", handleClickOutside);
});
const handleClickOutside = (event) => {
if (!event.target.closest(".page-size-selector")) {
showPageSizeMenu.value = false;
}
};
watch(
() => state.pageSize,
() => {
state.currentPage = 1;
}
);
watch(
() => state.currentPage,
(newPage) => {
state.gotoPage = newPage;
}
);
const getPageDefaultData = async () => {
try {
let url =
@ -343,14 +528,12 @@ const getPageData = async () => {
String(fromDate.getMonth() + 1).padStart(2, "0") +
"-" +
String(fromDate.getDate()).padStart(2, "0");
// let url = `https://stockanalysis.com/api/symbol/a/OTC-MINM/history?period=${state.selectedPeriod}&range=${range}`
let url =
"https://common.szjixun.cn/api/stock/history/list?from=" +
finalFromDate +
"&to=" +
toDate;
const res = await axios.get(url);
// console.error(res)
if (res.status === 200) {
if (res.data.status === 0) {
// "Nov 26, 2024"
@ -381,83 +564,71 @@ const getPageData = async () => {
<style scoped lang="scss">
.historic-data-container {
max-width: calc(100% - 300px);
width: 650 * 2.5px;
margin: 0 auto;
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.title {
font-size: 85px;
font-size: 40 * 2.5px;
font-weight: bold;
margin: 0;
}
.filter-container {
display: flex;
gap: 10px;
}
}
.pagination-container {
.filter-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
padding: 10px 16px;
border-radius: 4px;
background-color: #ffffff;
gap: 16 * 2.5px;
padding: 0 16 * 2.5px;
margin-bottom: 32 * 2.5px;
margin-top: 32 * 2.5px;
.page-btn {
display: flex;
align-items: center;
gap: 5px;
padding: 6px 12px;
font-size: 50px;
&.prev-btn {
margin-right: auto;
}
&.next-btn {
margin-left: 10px;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.range-label {
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 16 * 2.5px;
line-height: 1.375em;
letter-spacing: 3%;
color: #455363;
}
.page-info {
font-size: 40px;
color: #374151;
margin: 0 10px;
}
.filter-option {
padding: 7 * 2.5px 28 * 2.5px;
border-radius: 3 * 2.5px;
background-color: #efefef;
cursor: pointer;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 16 * 2.5px;
line-height: 1.375em;
color: #000000;
transition: all 0.2s ease;
.right-controls {
display: flex;
align-items: center;
&:hover {
background-color: #e0e0e0;
}
.rows-dropdown {
font-size: 40px;
&.active {
background-color: #ff7bac;
color: #ffffff;
}
}
}
.back-to-top-link {
display: flex;
justify-content: center;
margin-top: 16px;
margin-top: 16 * 2.5px;
a {
display: flex;
align-items: center;
gap: 5px;
gap: 5 * 2.5px;
color: #2563eb;
font-size: 50px;
font-size: 20 * 2.5px;
font-weight: bold;
text-decoration: none;
@ -466,11 +637,305 @@ const getPageData = async () => {
}
}
}
}
:deep(.n-data-table) {
.n-data-table-td {
padding: 12px 8px;
}
.reports-table {
width: 100%;
background: #ffffff;
border-radius: 16 * 2.5px;
box-shadow: 0px 3px 14px 0px rgba(0, 0, 0, 0.16);
padding: 16 * 2.5px;
}
.table-container {
width: 100%;
overflow-x: auto;
}
.table-container::-webkit-scrollbar {
height: 8px;
}
.table-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.table-container::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 4px;
}
.table-container::-webkit-scrollbar-thumb:hover {
background: #fff0f5;
cursor: pointer;
}
.column {
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 16 * 2.5px;
line-height: 1.375em;
letter-spacing: 3%;
color: #455363;
padding: 16 * 2.5px 16 * 2.5px;
position: relative;
}
.table-header {
display: flex;
border-radius: 8 * 2.5px;
margin-bottom: 4 * 2.5px;
align-items: center;
position: sticky;
top: 0;
z-index: 2;
.column {
background: #fff0f5;
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 16 * 2.5px;
line-height: 1.375em;
letter-spacing: 3%;
color: #000000;
}
}
.table-row {
display: flex;
align-items: center;
position: relative;
border-radius: 8 * 2.5px;
&:hover .column {
background: #fff8fb;
}
// &:last-child {
// border-bottom: none;
// }
&:nth-child(even) {
margin: 4px 0;
}
}
.reports-list {
display: flex;
flex-direction: column;
gap: 4 * 2.5px;
}
.table-row .column:not(:last-child)::after {
content: "";
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
height: 24 * 2.5px;
border-right: 1px dashed #e0e0e6;
}
.table-header .column:first-child,
.table-row .column:first-child {
position: sticky;
left: 0;
z-index: 1;
}
.table-header .column:first-child {
border-radius: 8 * 2.5px 0 0 8 * 2.5px;
}
.table-header .column:last-child,
.table-row .column:last-child {
border-radius: 0 8 * 2.5px 8 * 2.5px 0;
}
.table-row .column:first-child {
background: #ffffff;
}
.table-row:hover .column:first-child {
background: #fff8fb;
}
//
.pagination-container {
display: flex;
align-items: center;
margin-top: 20 * 2.5px;
justify-content: flex-end;
}
.pagination-info {
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 16 * 2.5px;
line-height: 1.4375em;
color: #455363;
text-align: right;
padding: 0 16 * 2.5px;
margin-top: 16 * 2.5px;
margin-bottom: 16 * 2.5px;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 8 * 2.5px;
}
.pagination-buttons {
display: flex;
align-items: center;
gap: 8 * 2.5px;
}
.page-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28 * 2.5px;
height: 28 * 2.5px;
border: 1px solid #e0e0e6;
border-radius: 3 * 2.5px;
background: #ffffff;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 2.5px;
line-height: 1.428em;
color: #455363;
cursor: pointer;
transition: all 0.2s ease;
&:hover:not(:disabled) {
border-color: #ff7bac;
color: #ff7bac;
}
&.active {
border-color: #ff7bac;
color: #ff7bac;
background: #fff0f5;
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
.page-size-selector {
position: relative;
display: flex;
align-items: center;
gap: 18 * 2.5px;
padding: 4px 12 * 2.5px;
height: 28 * 2.5px;
border: 1px solid #e0e0e6;
border-radius: 3 * 2.5px;
background: #ffffff;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 2.5px;
line-height: 1.428em;
color: #455363;
cursor: pointer;
&:hover {
border-color: #ff7bac;
}
}
.page-size-menu {
position: absolute;
bottom: 100%;
left: 0;
right: 0;
background: #ffffff;
border: 1px solid #e0e0e6;
border-radius: 3 * 2.5px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 1000;
margin-bottom: 2 * 2.5px;
}
.page-size-option {
padding: 8px 12 * 2.5px;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 2.5px;
line-height: 1.428em;
color: #455363;
cursor: pointer;
transition: background-color 0.2s ease;
&:hover {
background: #fff0f5;
}
&.active {
background: #fff0f5;
color: #ff7bac;
}
}
.goto-section {
display: flex;
align-items: center;
gap: 8 * 2.5px;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 2.5px;
line-height: 1.428em;
color: #455363;
margin-right: 16 * 2.5px;
}
.goto-input {
width: 60 * 2.5px;
height: 28 * 2.5px;
padding: 4px 12 * 2.5px;
border: 1px solid #e0e0e6;
border-radius: 3 * 2.5px;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 2.5px;
line-height: 1.428em;
color: #455363;
text-align: center;
&:focus {
outline: none;
border-color: #ff7bac;
}
}
.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;
}
</style>

View File

@ -1,24 +1,21 @@
<script setup>
import size375 from '@/views/index/size375/index.vue'
import size768 from '@/views/index/size1920/index.vue'
import size1440 from '@/views/index/size1920/index.vue'
import size1920 from '@/views/index/size1920/index.vue'
import { computed } from 'vue'
import { useWindowSize } from '@vueuse/core'
import size1920 from "@/views/index/size1920/index.vue";
import { computed } from "vue";
import { useWindowSize } from "@vueuse/core";
const { width } = useWindowSize()
const { width } = useWindowSize();
const viewComponent = computed(() => {
const viewWidth = width.value
if (viewWidth <= 450) {
return size375
} else if (viewWidth <= 1100) {
return size768
} else if (viewWidth <= 1500) {
return size1440
} else if (viewWidth <= 1920 || viewWidth > 1920) {
return size1920
}
})
// const viewWidth = width.value
// if (viewWidth <= 450) {
// return size375
// } else if (viewWidth <= 1100) {
// return size768
// } else if (viewWidth <= 1500) {
// return size1440
// } else if (viewWidth <= 1920 || viewWidth > 1920) {
return size1920;
// }
});
</script>
<template>

View File

@ -1,21 +0,0 @@
<script setup>
import customHeader from "@/components/customHeader/index.vue";
import customFooter from "@/components/customFooter/index.vue";
import { NScrollbar } from "naive-ui";
</script>
<template>
<div class="flex flex-col h-100svh">
<customHeader />
<n-scrollbar
class="bg-[url('@/assets/image/bg-mobile.png')] bg-cover bg-center flex-1"
>
<div>
<router-view />
</div>
</n-scrollbar>
<customFooter />
</div>
</template>
<style scoped lang="scss"></style>

View File

@ -16,7 +16,7 @@ const viewComponent = computed(() => {
const viewWidth = width.value;
if (viewWidth <= 450) {
return size375;
} else if (viewWidth <= 768) {
} else if (viewWidth <= 1100) {
return size768;
} else if (viewWidth <= 1500) {
return size1440;

View File

@ -16,7 +16,7 @@ const viewComponent = computed(() => {
const viewWidth = width.value;
if (viewWidth <= 450) {
return size375;
} else if (viewWidth <= 768) {
} else if (viewWidth <= 1100) {
return size768;
} else if (viewWidth <= 1500) {
return size1440;

View File

@ -619,7 +619,7 @@ const handleClickOutside = (event) => {
letter-spacing: 0.03em;
color: #455363;
margin: 0;
padding-left: 21px;
padding: 0 21px;
margin-top: 4px;
}

View File

@ -615,7 +615,7 @@ const handleClickOutside = (event) => {
letter-spacing: 3%;
color: #455363;
margin: 0;
padding-left: 17px;
padding: 0 16px;
margin-top: 8px;
}

View File

@ -1,59 +1,95 @@
<template>
<div class="press-releases-page">
<n-infinite-scroll :distance="0" @load="doLoadMore">
<main class="p-[80px] mx-auto" style="max-width: 100vw; min-width: 285px">
<div class="title mb-[24px]">
{{ t("press_releases.title") }}
<div class="title-section">
<div class="title-decoration"></div>
<div class="title">
{{ t("press_releases.title") }}
</div>
</div>
<div class="search-container">
<div class="search-select" @click="openYearPicker">
<div class="search-select-label">Year</div>
<div class="search-select-icon">
<span class="selected-year-label">{{ selectedYearLabel }}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="10"
height="10"
viewBox="0 0 10 10"
fill="none"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M2.58882 9.69047C2.38633 9.48798 2.36383 9.17365 2.52132 8.9463L2.58882 8.86552L6.45447 5.00022L2.58882 1.13492C2.38633 0.932428 2.36383 0.618098 2.52132 0.390748L2.58882 0.309958C2.79132 0.107469 3.10565 0.0849685 3.33299 0.242458L3.41378 0.309958L7.69156 4.58774C7.89405 4.79023 7.91655 5.10456 7.75906 5.33191L7.69156 5.4127L3.41378 9.69047C3.18597 9.91828 2.81663 9.91828 2.58882 9.69047Z"
fill="black"
/>
</svg>
</div>
<div class="search-container">
<n-select
:options="state.selectOptions"
v-model:value="state.selectedValue"
class="search-select"
:font-size="72"
/>
<n-input
v-model:value="state.inputValue"
type="text"
:placeholder="t('press_releases.search.placeholder')"
class="search-input"
clearable
:font-size="72"
/>
<n-button @click="handleSearch" class="search-button" :font-size="72">
{{ t("press_releases.search.button") }}
</n-button>
</div>
<div v-for="(item, idx) in state.filterNewsData" :key="idx">
<div class="news-item mt-[10px]">
<div class="news-item-date">{{ item.date }}</div>
<div
class="news-item-title text-[#0078d7] cursor-pointer"
style="
word-break: break-word;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
"
@click="handleNewClick(item)"
>
{{ item.title }}
</div>
<input
v-model="state.inputValue"
type="text"
:placeholder="t('press_releases.search.placeholder')"
class="search-input"
/>
<button @click="handleSearch" class="search-button">
{{ t("press_releases.search.button") }}
</button>
</div>
<div class="reports-list">
<div
v-for="(item, idx) in state.filterNewsData"
:key="idx"
class="news-item table-row"
>
<div class="content">
<div class="file-content">
<div class="file-info">
<div class="vertical-line"></div>
<div
class="news-item-title text-[#000] cursor-pointer"
style="
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 1;
line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
"
@click="handleNewClick(item)"
>
{{ item.title }}
</div>
<svg
class="arrow-icon"
width="7"
height="14"
viewBox="0 0 7 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@click="handleNewClick(item)"
>
<path
d="M1 1L6 7L1 13"
stroke="#FF7BAC"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
<n-tooltip
trigger="click"
:disabled="!item.showTooltip"
width="trigger"
>
<n-tooltip trigger="hover" :disabled="true" width="trigger">
<template #trigger>
<div
:ref="(el) => setTitleRef(el, idx)"
class="news-item-content"
class="news-item-content file-description"
style="
word-break: break-word;
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
@ -66,17 +102,115 @@
{{ item.summary }}
</div>
</n-tooltip>
<div class="download-section">
<div class="news-item-date">{{ item.date }}</div>
</div>
</div>
</div>
</main>
</n-infinite-scroll>
<div class="separator-line"></div>
</div>
</div>
<!-- 分页器 -->
<div class="pagination-container" v-if="state.total > 0">
<div class="pagination-controls">
<div class="pagination-buttons">
<button
class="page-btn prev-btn"
:disabled="state.currentPage === 1"
@click="goToPrevPage"
>
<svg width="5" height="9" viewBox="0 0 5 9" fill="none">
<path
d="M4 1L1 4.5L4 8"
stroke="#455363"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
<template v-for="page in getVisiblePages()" :key="page">
<button
v-if="page !== '...'"
class="page-btn"
:class="{ active: page === state.currentPage }"
@click="goToPage(page)"
>
{{ page }}
</button>
<button v-else class="page-btn disabled" disabled>...</button>
</template>
<button
class="page-btn next-btn"
:disabled="state.currentPage === totalPages"
@click="goToNextPage"
>
<svg width="5" height="9" viewBox="0 0 5 9" fill="none">
<path
d="M1 1L4 5L1 8"
stroke="#455363"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
<div class="page-size-selector" @click="togglePageSizeMenu">
<span>{{ state.pageSize }}/page</span>
<svg width="10" height="5" viewBox="0 0 10 5" fill="none">
<path
d="M1 1L5 4L9 1"
stroke="#455363"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<div v-if="showPageSizeMenu" class="page-size-menu">
<div
v-for="size in [10, 20, 50]"
:key="size"
class="page-size-option"
:class="{ active: state.pageSize === size }"
@click="changePageSize(size)"
>
{{ size }}/page
</div>
</div>
</div>
</div>
</div>
<div class="pagination-info" v-if="state.total > 0">
Displaying {{ displayRange.start }} - {{ displayRange.end }} of
{{ state.total }} results
</div>
</div>
<year-wheel-picker
v-if="state.showYearPicker"
v-model="state.selectedValue"
:options="state.selectOptions"
@close="closeYearPicker"
@confirm="onYearConfirm"
></year-wheel-picker>
</template>
<script setup>
import customDefaultPage from "@/components/customDefaultPage/index.vue";
import { reactive, onMounted, watch, nextTick, ref } from "vue";
import { NSelect, NInput, NButton, NInfiniteScroll, NTooltip } from "naive-ui";
import YearWheelPicker from "@/components/YearWheelPicker.vue";
import {
reactive,
onMounted,
watch,
nextTick,
ref,
computed,
onUnmounted,
} from "vue";
import { NInput, NButton, NTooltip } from "naive-ui";
import { useI18n } from "vue-i18n";
import axios from "axios";
@ -97,34 +231,39 @@ const state = reactive({
}),
], //
inputValue: "", //
newsData: [
{
date: "June 3, 2025",
title: "FiEE, Inc. seized market opportunities through 2025 Osaka Expo",
content:
"Hong Kong, 3 June 2025 — FiEE, Inc. (NASDAQ:FIEE) (“FiEE, Inc.” or the “Company”), a technology company integrating IoT, connectivity and AI to redefine brand management solutions in the digital era, is pleased to announce significant business updates....",
},
{
date: "June 2, 2025",
title: "FiEE, Inc. Closes Its First Day of Trading on NASDAQ",
content:
"Hong Kong, 2 June 2025 — FiEE, Inc. (NASDAQ:FIEE) (“FiEE, Inc.” or the “Company”), a technology company integrating IoT, connectivity and AI to redefine brand management solutions in the digital era, commenced...",
},
{
date: "May 30, 2025",
title: "FiEE, Inc. Announces Reinitiation of Trading on Nasdaq",
content:
"Hong Kong, May 30, 2025 — FiEE, Inc. (“FiEE, Inc.” or the “Company”), a technology company integrating IoT, connectivity and AI to redefine brand management solutions...",
},
],
filterNewsData: [],
loading: false, //
hasMore: true, //
currentPage: 1, //
pageSize: 10,
total: 0,
gotoPage: 1,
showYearPicker: false,
});
const showPageSizeMenu = ref(false);
const titleRefs = ref([]);
const selectedYearLabel = computed(() => {
const option = state.selectOptions.find(
(opt) => opt.value === state.selectedValue
);
return option ? option.label : "";
});
const openYearPicker = () => {
state.showYearPicker = true;
};
const closeYearPicker = () => {
state.showYearPicker = false;
};
const onYearConfirm = (year) => {
state.selectedValue = year;
closeYearPicker();
};
const setTitleRef = (el, idx) => {
if (el) titleRefs.value[idx] = el;
};
@ -142,14 +281,18 @@ const checkAllTitleOverflow = () => {
};
onMounted(() => {
// state.filterNewsData = state.newsData;
getPressReleasesDisplay();
document.addEventListener("click", handleClickOutside);
nextTick(() => {
checkAllTitleOverflow();
});
});
onUnmounted(() => {
document.removeEventListener("click", handleClickOutside);
});
watch(
() => state.filterNewsData,
() => {
@ -162,93 +305,68 @@ watch(
//
const getPressReleasesDisplay = () => {
state.loading = true;
let url = "https://erpapi.fiee.com/api/fiee/pressreleases/display";
let params = {
query: state.inputValue,
page: state.currentPage,
pageSize: 10,
pageSize: state.pageSize,
timeStart: state.selectedValue
? state.selectedValue === "all_years"
? null
: new Date(state.selectedValue).getTime()
: null,
};
// console.log(params)
axios.post(url, params).then((res) => {
// console.log(res)
if (res.status === 200) {
if (res.data.status === 0) {
res.data.data?.data?.forEach((item) => {
item.date = new Date(item.createdAt).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
axios
.post(url, params)
.then((res) => {
if (res.status === 200) {
if (res.data.status === 0) {
res.data.data?.data?.forEach((item) => {
item.date = new Date(item.createdAt).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
});
});
if (state.currentPage === 1) {
state.filterNewsData = res.data.data?.data || [];
} else {
state.filterNewsData = [
...state.filterNewsData,
...(res.data.data?.data || []),
];
}
if (state.filterNewsData.length < (res.data.data?.total || 0)) {
state.hasMore = true;
} else {
state.hasMore = false;
state.total = res.data.data?.total || 0;
}
}
}
});
};
const handleFilter = () => {
//
let filteredData = [...state.newsData];
//
if (state.selectedValue !== "all_years") {
filteredData = filteredData.filter((item) => {
// "May 30, 2025"
const dateMatch = item.date.match(/\b\d{4}\b/);
if (dateMatch) {
const year = dateMatch[0];
return year === state.selectedValue;
}
return false;
})
.finally(() => {
state.loading = false;
});
}
// title content
if (state.inputValue && state.inputValue.trim() !== "") {
const searchText = state.inputValue.toLowerCase().trim();
filteredData = filteredData.filter((item) => {
const titleMatch = item.title.toLowerCase().includes(searchText);
const contentMatch = item.content.toLowerCase().includes(searchText);
return titleMatch || contentMatch;
});
}
state.filterNewsData = filteredData;
};
// watcher
watch(
() => [state.selectedValue, state.inputValue],
() => {
// handleFilter();
state.currentPage = 1;
getPressReleasesDisplay();
}
);
watch(
() => state.pageSize,
() => {
state.currentPage = 1;
getPressReleasesDisplay();
}
);
watch(
() => state.currentPage,
(newPage) => {
state.gotoPage = newPage;
getPressReleasesDisplay();
}
);
const handleSearch = () => {
//
// handleFilter();
state.currentPage = 1;
getPressReleasesDisplay();
// console.log(":", state.filterNewsData);
};
const handleNewClick = (item) => {
@ -260,102 +378,472 @@ const handleNewClick = (item) => {
});
};
//
const doLoadMore = () => {
if (!state.hasMore || state.loading) {
return;
const totalPages = computed(() => {
return Math.ceil(state.total / state.pageSize) || 1;
});
const displayRange = computed(() => {
if (state.total === 0) return { start: 0, end: 0 };
const start = (state.currentPage - 1) * state.pageSize + 1;
const end = Math.min(state.currentPage * state.pageSize, state.total);
return { start, end };
});
const goToPage = (page) => {
if (page >= 1 && page <= totalPages.value) {
state.currentPage = page;
}
};
const goToPrevPage = () => {
if (state.currentPage > 1) {
state.currentPage--;
}
};
const goToNextPage = () => {
if (state.currentPage < totalPages.value) {
state.currentPage++;
}
};
const changePageSize = (size) => {
state.pageSize = size;
};
const handleGoto = () => {
const page = parseInt(state.gotoPage);
if (page >= 1 && page <= totalPages.value) {
state.currentPage = page;
}
};
const togglePageSizeMenu = () => {
showPageSizeMenu.value = !showPageSizeMenu.value;
};
const getVisiblePages = () => {
const total = totalPages.value;
if (total <= 4) {
const pages = [];
for (let i = 1; i <= total; i++) {
pages.push(i);
}
return pages;
}
return [1, 2, "...", total];
};
//
const handleClickOutside = (event) => {
if (!event.target.closest || !event.target.closest(".page-size-selector")) {
showPageSizeMenu.value = false;
}
// console.log('')
state.loading = true;
state.currentPage++;
getPressReleasesDisplay().finally(() => {
state.loading = false;
});
};
</script>
<style scoped lang="scss">
.press-releases-page {
width: 343 * 5.12px;
margin: 0 auto;
background: #fff;
}
.title-section {
display: flex;
flex-direction: column;
gap: 16 * 5.12px;
padding: 0 16 * 5.12px;
}
.title-decoration {
width: 58 * 5.12px;
height: 7 * 5.12px;
background: #ff7bac;
margin-top: 43 * 5.12px;
}
.title {
font-size: 113px;
font-weight: bold;
color: #333;
text-align: center;
margin-top: 8px;
font-size: 32 * 5.12px;
color: #000;
}
.search-container {
margin-bottom: 24px;
margin-top: 32 * 5.12px;
margin-bottom: 20 * 5.12px;
display: flex;
flex-direction: row;
align-items: center;
background-color: #f6f7f9;
border-radius: 8px;
padding: 8px;
gap: 16px;
justify-content: space-between;
flex-flow: wrap;
gap: 16 * 5.12px;
padding: 0 16 * 5.12px;
}
.search-select {
width: 1000px;
:deep(.n-base-selection) {
padding: 4px 0;
}
}
.search-input {
width: 100%;
height: 34 * 5.12px;
padding: 0 12px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
color: #455363;
}
.search-select-icon {
display: flex;
align-items: center;
}
.search-input {
width: 191 * 5.12px;
height: 34 * 5.12px;
padding: 7 * 5.12px 12 * 5.12px;
border: 1 * 5.12px solid #e0e0e6;
border-radius: 3 * 5.12px;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
line-height: 1.375em;
letter-spacing: 0.48 * 5.12px;
color: #455363;
&::placeholder {
color: #b6b6b6;
}
}
:deep(.n-input) {
.n-input__input {
padding: 4px 0;
border-radius: 4px;
padding: 4 * 5.12px 0;
// border: 1*5.12px solid #ccc;
border-radius: 4 * 5.12px;
}
}
:deep(.n-select) {
.n-select__input {
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 8 * 5.12px 12 * 5.12px;
border: 1 * 5.12px solid #ccc;
border-radius: 4 * 5.12px;
}
}
:deep(.n-button) {
width: 260px;
padding: 20px 16px;
border-radius: 4px;
}
.news-item {
padding: 16px;
border-bottom: 1px solid #eee;
margin-bottom: 16px;
}
.news-item-date {
font-size: 72px;
color: #666;
margin-bottom: 8px;
}
.news-item-title {
font-size: 92px;
font-weight: bold;
margin-bottom: 8px;
line-height: 1.4;
}
.news-item-content {
font-size: 72px;
color: #333;
line-height: 1.6;
padding: 20 * 5.12px 16 * 5.12px;
border-radius: 4 * 5.12px;
}
.search-button {
width: 104 * 5.12px;
height: 34 * 5.12px;
padding: 7 * 5.12px 12 * 5.12px;
background: #ff7bac;
color: #fff;
border: none;
border-radius: 3 * 5.12px;
cursor: pointer;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
line-height: 1.375em;
letter-spacing: 0.48 * 5.12px;
&:hover {
background: #ff7bac;
color: #fff;
}
}
.reports-list {
display: flex;
flex-direction: column;
gap: 4 * 5.12px;
background: #fff;
width: 100%;
margin-top: 40 * 5.12px;
}
.table-row {
display: flex;
flex-direction: column;
position: relative;
border-radius: 8 * 5.12px;
// &:last-child {
// .separator-line {
// display: none;
// }
// }
}
.content {
flex: 1;
display: flex;
flex-direction: column;
gap: 16 * 5.12px;
}
.table-row .content {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 0;
&:hover {
background: #fff8fb;
}
}
.file-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.file-info {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16 * 5.12px;
flex: 1;
}
.news-item-title {
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 14 * 5.12px;
line-height: 1.375em;
letter-spacing: 0.48 * 5.12px;
color: #000000;
}
.arrow-icon {
margin-right: 16 * 5.12px;
flex-shrink: 0;
cursor: pointer;
}
.vertical-line {
width: 1 * 5.12px;
height: 20 * 5.12px;
background: #ff7bac;
flex-shrink: 0;
}
.file-description {
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
line-height: 1.375em;
letter-spacing: 0.48 * 5.12px;
color: #455363;
margin: 8 * 5.12px 0;
padding: 0 16 * 5.12px;
}
.news-item-date {
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
line-height: 1.375em;
letter-spacing: 0.48 * 5.12px;
color: #455363;
}
.download-section {
display: flex;
align-items: center;
justify-content: flex-start;
width: 100%;
padding: 0 16 * 5.12px;
margin-bottom: 8 * 5.12px;
}
.separator-line {
width: 100%;
height: 1 * 5.12px;
background: repeating-linear-gradient(
to right,
#e6eaee 0 * 5.12px,
#e6eaee 2 * 5.12px,
transparent 2 * 5.12px,
transparent 4 * 5.12px
);
margin-top: 16 * 5.12px;
}
//
.pagination-container {
display: flex;
align-items: center;
margin: 16 * 5.12px 0;
justify-content: flex-end;
padding: 0 16 * 5.12px;
flex-wrap: wrap;
gap: 16 * 5.12px;
}
.pagination-info {
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
line-height: 1.4375em;
color: #455363;
text-align: right;
margin-bottom: 30 * 5.12px;
padding: 0 16 * 5.12px;
width: 100%;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 8 * 5.12px;
flex-wrap: wrap;
justify-content: flex-end;
}
.pagination-buttons {
display: flex;
align-items: center;
gap: 8 * 5.12px;
}
.page-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28 * 5.12px;
height: 28 * 5.12px;
border: 1 * 5.12px solid #e0e0e6;
border-radius: 3 * 5.12px;
background: #ffffff;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
line-height: 1.428em;
color: #455363;
cursor: pointer;
transition: all 0.2s ease;
&:hover:not(:disabled) {
border-color: #ff7bac;
color: #ff7bac;
}
&.active {
border-color: #ff7bac;
color: #ff7bac;
background: #fff0f5;
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
.page-size-selector {
position: relative;
display: flex;
align-items: center;
gap: 18 * 5.12px;
padding: 4 * 5.12px 12 * 5.12px;
height: 28 * 5.12px;
border: 1 * 5.12px solid #e0e0e6;
border-radius: 3 * 5.12px;
background: #ffffff;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
line-height: 1.428em;
color: #455363;
cursor: pointer;
&:hover {
border-color: #ff7bac;
}
}
.page-size-menu {
position: absolute;
bottom: 100%;
left: 0;
right: 0;
background: #ffffff;
border: 1 * 5.12px solid #e0e0e6;
border-radius: 3 * 5.12px;
box-shadow: 0 2 * 5.12px 8 * 5.12px rgba(0, 0, 0, 0.1);
z-index: 1000;
margin-bottom: 2 * 5.12px;
}
.page-size-option {
padding: 8 * 5.12px 12 * 5.12px;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
line-height: 1.428em;
color: #455363;
cursor: pointer;
transition: background-color 0.2s ease;
&:hover {
background: #fff0f5;
}
&.active {
background: #fff0f5;
color: #ff7bac;
}
}
.goto-section {
display: flex;
align-items: center;
gap: 8 * 5.12px;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
line-height: 1.428em;
color: #455363;
}
.goto-input {
width: 60 * 5.12px;
height: 28 * 5.12px;
padding: 4 * 5.12px 12 * 5.12px;
border: 1 * 5.12px solid #e0e0e6;
border-radius: 3 * 5.12px;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
line-height: 1.428em;
color: #455363;
text-align: center;
&:focus {
outline: none;
border-color: #ff7bac;
}
}
.selected-year-label {
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
line-height: 1.428em;
color: #000;
margin-right: 16 * 5.12px;
}
.search-select-label {
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 5.12px;
line-height: 1.428em;
color: #000;
}
</style>

View File

@ -1,79 +1,206 @@
<template>
<div class="press-releases-page">
<n-infinite-scroll :distance="0" @load="doLoadMore">
<main class="p-[35px] mx-auto" style="max-width: calc(100% - 100px)">
<div class="title mb-[20px]">
<main class="mx-auto">
<div class="title-section">
<div class="title-decoration"></div>
<div class="title">
{{ t("press_releases.title") }}
</div>
<div class="search-container">
<n-select
:options="state.selectOptions"
v-model:value="state.selectedValue"
class="search-select"
/>
<n-input
v-model:value="state.inputValue"
type="text"
:placeholder="t('press_releases.search.placeholder')"
class="search-input"
/>
<n-button @click="handleSearch" class="search-button w-[120px]">
{{ t("press_releases.search.button") }}
</n-button>
</div>
<div v-for="(item, idx) in state.filterNewsData" :key="idx">
<div class="news-item mt-[10px]">
<div class="news-item-date">{{ item.date }}</div>
<div
class="news-item-title text-[#0078d7] cursor-pointer"
style="
word-break: break-word;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
"
@click="handleNewClick(item)"
>
{{ item.title }}
</div>
<n-tooltip
trigger="click"
:disabled="!item.showTooltip"
width="trigger"
>
<template #trigger>
</div>
<div class="search-container">
<n-select
:options="state.selectOptions"
v-model:value="state.selectedValue"
class="search-select"
/>
<input
v-model="state.inputValue"
type="text"
:placeholder="t('press_releases.search.placeholder')"
class="search-input"
/>
<button @click="handleSearch" class="search-button">
{{ t("press_releases.search.button") }}
</button>
</div>
<div class="reports-list">
<div
v-for="(item, idx) in state.filterNewsData"
:key="idx"
class="news-item table-row"
>
<div class="content">
<div class="file-content">
<div class="file-info">
<div class="vertical-line"></div>
<div
:ref="(el) => setTitleRef(el, idx)"
class="news-item-content"
class="news-item-title text-[#000] cursor-pointer"
style="
word-break: break-word;
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-line-clamp: 1;
line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
"
@click="handleNewClick(item)"
>
{{ item.title }}
</div>
<svg
class="arrow-icon"
width="7"
height="14"
viewBox="0 0 7 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@click="handleNewClick(item)"
>
<path
d="M1 1L6 7L1 13"
stroke="#FF7BAC"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
<n-tooltip trigger="hover" :disabled="true" width="trigger">
<template #trigger>
<div
:ref="(el) => setTitleRef(el, idx)"
class="news-item-content file-description"
style="
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
"
>
{{ item.summary }}
</div>
</template>
<div slot="content">
{{ item.summary }}
</div>
</template>
<div slot="content">
{{ item.summary }}
</n-tooltip>
<div class="download-section">
<div class="news-item-date">{{ item.date }}</div>
</div>
</n-tooltip>
</div>
</div>
<div class="separator-line"></div>
</div>
</div>
<!-- 分页器 -->
<div class="pagination-container" v-if="state.total > 0">
<div class="pagination-controls">
<div class="pagination-buttons">
<button
class="page-btn prev-btn"
:disabled="state.currentPage === 1"
@click="goToPrevPage"
>
<svg width="5" height="9" viewBox="0 0 5 9" fill="none">
<path
d="M4 1L1 4.5L4 8"
stroke="#455363"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
<template v-for="page in getVisiblePages()" :key="page">
<button
v-if="page !== '...'"
class="page-btn"
:class="{ active: page === state.currentPage }"
@click="goToPage(page)"
>
{{ page }}
</button>
<button v-else class="page-btn disabled" disabled>...</button>
</template>
<button
class="page-btn next-btn"
:disabled="state.currentPage === totalPages"
@click="goToNextPage"
>
<svg width="5" height="9" viewBox="0 0 5 9" fill="none">
<path
d="M1 1L4 5L1 8"
stroke="#455363"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
<div class="page-size-selector" @click="togglePageSizeMenu">
<span>{{ state.pageSize }}/page</span>
<svg width="10" height="5" viewBox="0 0 10 5" fill="none">
<path
d="M1 1L5 4L9 1"
stroke="#455363"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<div v-if="showPageSizeMenu" class="page-size-menu">
<div
v-for="size in [10, 20, 50]"
:key="size"
class="page-size-option"
:class="{ active: state.pageSize === size }"
@click="changePageSize(size)"
>
{{ size }}/page
</div>
</div>
</div>
<div class="goto-section">
<span>Goto</span>
<input
type="number"
v-model="state.gotoPage"
class="goto-input"
:min="1"
:max="totalPages"
@keyup.enter="handleGoto"
/>
</div>
</div>
</main>
</n-infinite-scroll>
</div>
<div class="pagination-info" v-if="state.total > 0">
Displaying {{ displayRange.start }} - {{ displayRange.end }} of
{{ state.total }} results
</div>
</main>
</div>
</template>
<script setup>
import customDefaultPage from "@/components/customDefaultPage/index.vue";
import { reactive, onMounted, watch, nextTick, ref } from "vue";
import { NSelect, NInput, NButton, NInfiniteScroll, NTooltip } from "naive-ui";
import {
reactive,
onMounted,
watch,
nextTick,
ref,
computed,
onUnmounted,
} from "vue";
import { NSelect, NInput, NButton, NTooltip } from "naive-ui";
import { useI18n } from "vue-i18n";
import axios from "axios";
@ -94,32 +221,16 @@ const state = reactive({
}),
], //
inputValue: "", //
newsData: [
{
date: "June 3, 2025",
title: "FiEE, Inc. seized market opportunities through 2025 Osaka Expo",
content:
"Hong Kong, 3 June 2025 — FiEE, Inc. (NASDAQ:FIEE) (“FiEE, Inc.” or the “Company”), a technology company integrating IoT, connectivity and AI to redefine brand management solutions in the digital era, is pleased to announce significant business updates....",
},
{
date: "June 2, 2025",
title: "FiEE, Inc. Closes Its First Day of Trading on NASDAQ",
content:
"Hong Kong, 2 June 2025 — FiEE, Inc. (NASDAQ:FIEE) (“FiEE, Inc.” or the “Company”), a technology company integrating IoT, connectivity and AI to redefine brand management solutions in the digital era, commenced...",
},
{
date: "May 30, 2025",
title: "FiEE, Inc. Announces Reinitiation of Trading on Nasdaq",
content:
"Hong Kong, May 30, 2025 — FiEE, Inc. (“FiEE, Inc.” or the “Company”), a technology company integrating IoT, connectivity and AI to redefine brand management solutions...",
},
],
filterNewsData: [],
loading: false, //
hasMore: true, //
currentPage: 1, //
pageSize: 10,
total: 0,
gotoPage: 1,
});
const showPageSizeMenu = ref(false);
const titleRefs = ref([]);
const setTitleRef = (el, idx) => {
@ -139,14 +250,18 @@ const checkAllTitleOverflow = () => {
};
onMounted(() => {
// state.filterNewsData = state.newsData;
getPressReleasesDisplay();
document.addEventListener("click", handleClickOutside);
nextTick(() => {
checkAllTitleOverflow();
});
});
onUnmounted(() => {
document.removeEventListener("click", handleClickOutside);
});
watch(
() => state.filterNewsData,
() => {
@ -159,93 +274,68 @@ watch(
//
const getPressReleasesDisplay = () => {
state.loading = true;
let url = "https://erpapi.fiee.com/api/fiee/pressreleases/display";
let params = {
query: state.inputValue,
page: state.currentPage,
pageSize: 10,
pageSize: state.pageSize,
timeStart: state.selectedValue
? state.selectedValue === "all_years"
? null
: new Date(state.selectedValue).getTime()
: null,
};
// console.log(params)
axios.post(url, params).then((res) => {
// console.log(res)
if (res.status === 200) {
if (res.data.status === 0) {
res.data.data?.data?.forEach((item) => {
item.date = new Date(item.createdAt).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
axios
.post(url, params)
.then((res) => {
if (res.status === 200) {
if (res.data.status === 0) {
res.data.data?.data?.forEach((item) => {
item.date = new Date(item.createdAt).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
});
});
if (state.currentPage === 1) {
state.filterNewsData = res.data.data?.data || [];
} else {
state.filterNewsData = [
...state.filterNewsData,
...(res.data.data?.data || []),
];
}
if (state.filterNewsData.length < (res.data.data?.total || 0)) {
state.hasMore = true;
} else {
state.hasMore = false;
state.total = res.data.data?.total || 0;
}
}
}
});
};
const handleFilter = () => {
//
let filteredData = [...state.newsData];
//
if (state.selectedValue !== "all_years") {
filteredData = filteredData.filter((item) => {
// "May 30, 2025"
const dateMatch = item.date.match(/\b\d{4}\b/);
if (dateMatch) {
const year = dateMatch[0];
return year === state.selectedValue;
}
return false;
})
.finally(() => {
state.loading = false;
});
}
// title content
if (state.inputValue && state.inputValue.trim() !== "") {
const searchText = state.inputValue.toLowerCase().trim();
filteredData = filteredData.filter((item) => {
const titleMatch = item.title.toLowerCase().includes(searchText);
const contentMatch = item.content.toLowerCase().includes(searchText);
return titleMatch || contentMatch;
});
}
state.filterNewsData = filteredData;
};
// watcher
watch(
() => [state.selectedValue, state.inputValue],
() => {
// handleFilter();
state.currentPage = 1;
getPressReleasesDisplay();
}
);
watch(
() => state.pageSize,
() => {
state.currentPage = 1;
getPressReleasesDisplay();
}
);
watch(
() => state.currentPage,
(newPage) => {
state.gotoPage = newPage;
getPressReleasesDisplay();
}
);
const handleSearch = () => {
//
// handleFilter();
state.currentPage = 1;
getPressReleasesDisplay();
// console.log(":", state.filterNewsData);
};
const handleNewClick = (item) => {
@ -257,72 +347,461 @@ const handleNewClick = (item) => {
});
};
//
const doLoadMore = () => {
if (!state.hasMore || state.loading) {
return;
const totalPages = computed(() => {
return Math.ceil(state.total / state.pageSize) || 1;
});
const displayRange = computed(() => {
if (state.total === 0) return { start: 0, end: 0 };
const start = (state.currentPage - 1) * state.pageSize + 1;
const end = Math.min(state.currentPage * state.pageSize, state.total);
return { start, end };
});
const goToPage = (page) => {
if (page >= 1 && page <= totalPages.value) {
state.currentPage = page;
}
};
const goToPrevPage = () => {
if (state.currentPage > 1) {
state.currentPage--;
}
};
const goToNextPage = () => {
if (state.currentPage < totalPages.value) {
state.currentPage++;
}
};
const changePageSize = (size) => {
state.pageSize = size;
};
const handleGoto = () => {
const page = parseInt(state.gotoPage);
if (page >= 1 && page <= totalPages.value) {
state.currentPage = page;
}
};
const togglePageSizeMenu = () => {
showPageSizeMenu.value = !showPageSizeMenu.value;
};
const getVisiblePages = () => {
const current = state.currentPage;
const total = totalPages.value;
const pages = [];
if (total <= 7) {
for (let i = 1; i <= total; i++) {
pages.push(i);
}
} else {
pages.push(1);
if (current <= 4) {
for (let i = 2; i <= 5; i++) {
pages.push(i);
}
pages.push("...");
pages.push(total);
} else if (current >= total - 3) {
pages.push("...");
for (let i = total - 4; i <= total; i++) {
pages.push(i);
}
} else {
pages.push("...");
for (let i = current - 1; i <= current + 1; i++) {
pages.push(i);
}
pages.push("...");
pages.push(total);
}
}
return pages;
};
//
const handleClickOutside = (event) => {
if (!event.target.closest || !event.target.closest(".page-size-selector")) {
showPageSizeMenu.value = false;
}
// console.log('')
state.loading = true;
state.currentPage++;
getPressReleasesDisplay().finally(() => {
state.loading = false;
});
};
</script>
<style scoped lang="scss">
.press-releases-page {
width: 650 * 2.5px;
margin: 0 auto;
}
.title-section {
display: flex;
flex-direction: column;
gap: 16 * 2.5px;
padding: 0 16 * 2.5px;
}
.title-decoration {
width: 58 * 2.5px;
height: 7 * 2.5px;
background: #ff7bac;
margin: auto 0;
margin-top: 43 * 2.5px;
}
.title {
font-size: 63px;
color: #333;
font-size: 32 * 2.5px;
color: #000;
}
.search-container {
margin-bottom: 20px;
margin-bottom: 20 * 2.5px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 25px;
gap: 16 * 2.5px;
padding: 0 16 * 2.5px;
}
.search-select {
width: 360px;
:deep(.n-base-selection) {
padding: 4px 0;
}
width: 134 * 2.5px;
height: 34 * 2.5px;
}
.search-input {
width: 360px;
width: 292 * 2.5px;
height: 34 * 2.5px;
padding: 7 * 2.5px 12 * 2.5px;
border: 1 * 2.5px solid #e0e0e6;
border-radius: 3 * 2.5px;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 2.5px;
line-height: 1.375em;
letter-spacing: 0.48 * 2.5px;
color: #455363;
&::placeholder {
color: #b6b6b6;
}
}
:deep(.n-input) {
.n-input__input {
padding: 4px 0;
// border: 1px solid #ccc;
border-radius: 4px;
padding: 4 * 2.5px 0;
// border: 1*2.5px solid #ccc;
border-radius: 4 * 2.5px;
}
}
:deep(.n-select) {
.n-select__input {
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 8 * 2.5px 12 * 2.5px;
border: 1 * 2.5px solid #ccc;
border-radius: 4 * 2.5px;
}
}
:deep(.n-button) {
padding: 20px 16px;
border-radius: 4px;
padding: 20 * 2.5px 16 * 2.5px;
border-radius: 4 * 2.5px;
}
.search-button {
height: 34 * 2.5px;
padding: 7 * 2.5px 12 * 2.5px;
min-width: 160 * 2.5px;
background: #ff7bac;
color: #fff;
border: none;
border-radius: 3 * 2.5px;
cursor: pointer;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 2.5px;
line-height: 1.375em;
letter-spacing: 0.48 * 2.5px;
&:hover {
background: #ff7bac;
color: #fff;
}
}
.reports-list {
display: flex;
flex-direction: column;
gap: 4 * 2.5px;
background: #fff;
width: 650 * 2.5px;
}
.table-row {
display: flex;
flex-direction: column;
position: relative;
border-radius: 8 * 2.5px;
// &:last-child {
// .separator-line {
// display: none;
// }
// }
}
.content {
flex: 1;
display: flex;
flex-direction: column;
gap: 16 * 2.5px;
}
.table-row .content {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 0;
&:hover {
background: #fff8fb;
}
}
.file-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding-right: 16 * 2.5px;
}
.file-info {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16 * 2.5px;
flex: 1;
}
.news-item-title {
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 14 * 2.5px;
line-height: 1.375em;
letter-spacing: 0.48 * 2.5px;
color: #000000;
}
.arrow-icon {
margin-left: auto;
flex-shrink: 0;
cursor: pointer;
}
.vertical-line {
width: 1 * 2.5px;
height: 20 * 2.5px;
background: #ff7bac;
flex-shrink: 0;
}
.file-description {
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 2.5px;
line-height: 1.375em;
letter-spacing: 0.48 * 2.5px;
color: #455363;
margin: 0;
padding: 0 16 * 2.5px;
margin-top: 8 * 2.5px;
}
.news-item-date {
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 2.5px;
line-height: 1.375em;
letter-spacing: 0.48 * 2.5px;
color: #455363;
}
.download-section {
display: flex;
align-items: center;
justify-content: flex-start;
width: 100%;
padding: 4 * 2.5px 16 * 2.5px;
}
.separator-line {
width: 100%;
height: 1 * 2.5px;
background: repeating-linear-gradient(
to right,
#e6eaee 0 * 2.5px,
#e6eaee 2 * 2.5px,
transparent 2 * 2.5px,
transparent 4 * 2.5px
);
margin-top: 16 * 2.5px;
}
//
.pagination-container {
display: flex;
align-items: center;
margin: 20 * 2.5px 0;
justify-content: flex-end;
padding: 0 16 * 2.5px;
}
.pagination-info {
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 2.5px;
line-height: 1.4375em;
color: #455363;
text-align: right;
margin-bottom: 30 * 2.5px;
padding: 0 16 * 2.5px;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 8 * 2.5px;
}
.pagination-buttons {
display: flex;
align-items: center;
gap: 8 * 2.5px;
}
.page-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28 * 2.5px;
height: 28 * 2.5px;
border: 1 * 2.5px solid #e0e0e6;
border-radius: 3 * 2.5px;
background: #ffffff;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 2.5px;
line-height: 1.428em;
color: #455363;
cursor: pointer;
transition: all 0.2s ease;
&:hover:not(:disabled) {
border-color: #ff7bac;
color: #ff7bac;
}
&.active {
border-color: #ff7bac;
color: #ff7bac;
background: #fff0f5;
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
.page-size-selector {
position: relative;
display: flex;
align-items: center;
gap: 18 * 2.5px;
padding: 4 * 2.5px 12 * 2.5px;
height: 28 * 2.5px;
border: 1 * 2.5px solid #e0e0e6;
border-radius: 3 * 2.5px;
background: #ffffff;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 2.5px;
line-height: 1.428em;
color: #455363;
cursor: pointer;
&:hover {
border-color: #ff7bac;
}
}
.page-size-menu {
position: absolute;
bottom: 100%;
left: 0;
right: 0;
background: #ffffff;
border: 1 * 2.5px solid #e0e0e6;
border-radius: 3 * 2.5px;
box-shadow: 0 2 * 2.5px 8 * 2.5px rgba(0, 0, 0, 0.1);
z-index: 1000;
margin-bottom: 2 * 2.5px;
}
.page-size-option {
padding: 8 * 2.5px 12 * 2.5px;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 2.5px;
line-height: 1.428em;
color: #455363;
cursor: pointer;
transition: background-color 0.2s ease;
&:hover {
background: #fff0f5;
}
&.active {
background: #fff0f5;
color: #ff7bac;
}
}
.goto-section {
display: flex;
align-items: center;
gap: 8 * 2.5px;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 2.5px;
line-height: 1.428em;
color: #455363;
}
.goto-input {
width: 60 * 2.5px;
height: 28 * 2.5px;
padding: 4 * 2.5px 12 * 2.5px;
border: 1 * 2.5px solid #e0e0e6;
border-radius: 3 * 2.5px;
font-family: "PingFang SC", sans-serif;
font-weight: 400;
font-size: 14 * 2.5px;
line-height: 1.428em;
color: #455363;
text-align: center;
&:focus {
outline: none;
border-color: #ff7bac;
}
}
</style>

View File

@ -2,7 +2,7 @@
import { computed } from "vue";
import { useWindowSize } from "@vueuse/core";
// import size375 from "./size375/index.vue";
import size375 from "./size375/index.vue";
import size768 from "./size768/index.vue";
import size1440 from "./size1440/index.vue";
import size1920 from "./size1920/index.vue";

View File

@ -262,11 +262,10 @@
</div>
</template>
<style scoped>
<style scoped lang="scss">
.page-container {
background-color: #fff;
font-family: "PingFang SC", sans-serif;
/* width:932px */
margin: 0 auto;
position: relative;
}

View File

@ -0,0 +1,589 @@
<script setup></script>
<template>
<div class="page-container">
<div class="grid-lines">
<div class="line solid line-1"></div>
<div class="line solid line-5"></div>
</div>
<section class="hero-section relative">
<div class="hero-content">
<div class="hero-title">
More than just a tool<br />
Comprehensive growth <br />solutions, providing a one- <br />stop
solution for content <br />creation, publishing, analysis,<br />
and monetization
</div>
</div>
<div class="core-value-card">
<div class="card-content">
<div class="card-title">Core Value</div>
<div class="card-text">
The FIEE-SAAS platform is a one-stop content operation solution
tailored for creators in the digital era. The platform utilizes
intelligent distribution technology, A1 empowerment tools, and
full-chain services,Assist you in efficiently reaching audiences on
global mainstream platforms such as TikTok, YouTube, and Instagram,
creating a KOL brand effect, unlocking content value, and achieving
sustainable growth.
</div>
</div>
</div>
<img
src="@/assets/image/375/product-introduction-img2.png"
alt="background"
class="hero-bg-img"
/>
</section>
<section class="features-section">
<div class="section-header">
<div class="decorator-bar"></div>
<div class="section-title">Product Features</div>
</div>
<div class="features-list">
<div class="feature-item">
<div class="feature-title">
<div class="vertical-line"></div>
One-click Synchronous Publishing
</div>
<div class="feature-description">
Synchronize graphic and video content to TikTok, YouTube, and
Instagram platforms at once, saving time on repetitive operations.
</div>
</div>
<div class="feature-item">
<div class="feature-title">
<div class="vertical-line"></div>
Intelligent Scheduled Publishing
</div>
<div class="feature-description">
Plan the content release time in advance, support batch scheduling,
and accurately grasp the optimal release time of each platform.
</div>
</div>
<div class="feature-item">
<div class="feature-title">
<div class="vertical-line"></div>
Unified Management of Multiple Accounts
</div>
<div class="feature-description">
Easily manage multiple accounts on one platform without the need for
repeated login and switching, improving team collaboration
efficiency.
</div>
</div>
<div class="feature-item">
<div class="feature-title">
<div class="vertical-line"></div>
Cloud Content Library
</div>
<div class="feature-description">
Safely store and manage all creative materials, access and use them
anytime, anywhere, and support quick retrieval and reuse.
</div>
</div>
<div class="feature-item">
<div class="feature-title">
<div class="vertical-line"></div>
Basic Data Tracking
</div>
<div class="feature-description">
Visually view the content performance of various platforms,
understand core data indicators, and provide a basis for optimizing
strategies.
</div>
</div>
</div>
</section>
<section class="solutions-section">
<div class="section-header">
<div class="decorator-bar"></div>
<div class="section-title">Value Added Solutions</div>
</div>
<div class="solutions-content">
<div class="solution-image-container">
<img
src="@/assets/image/375/product-introduction-img1.png"
alt="Value Added Solutions"
class="solution-image"
/>
</div>
<div class="solutions-list">
<div class="solution-item">
<img
src="@/assets/image/375/product-introduction-icon1.png"
alt="KOL Brand Promotion"
class="solution-icon"
/>
<div class="solution-title">
<div class="vertical-line"></div>
KOL Brand Promotion Services
</div>
<div class="solution-description">
Efficiently connect high-quality business cooperation
opportunities and complete the entire process management from
order acceptance to publication within the platform.
</div>
</div>
<div class="solution-item">
<img
src="@/assets/image/375/product-introduction-icon2.png"
alt="Content Creation Support"
class="solution-icon"
/>
<div class="solution-title">
<div class="vertical-line"></div>
Professional Content Creation Support
</div>
<div class="solution-description">
Connect professional shooting and post production teams for you,
create high-quality "art+story" content, and strengthen IP
influence.
</div>
</div>
<div class="solution-item">
<img
src="@/assets/image/375/product-introduction-icon3.png"
alt="Account Operation"
class="solution-icon"
/>
<div class="solution-title">
<div class="vertical-line"></div>
Account Operation and Hosting Services
</div>
<div class="solution-description">
From 0 to 1 account positioning, follower growth strategy to
monetization cycle, operation experts provide full cycle running
and hosting services.
</div>
</div>
</div>
</div>
</section>
<section class="advantages-section">
<div class="advantages-content">
<div class="advantages-header">
<div class="decorator-bar"></div>
<div class="section-title text-white">Our Advantages</div>
</div>
<div class="advantages-list">
<div class="advantage-item">
<div class="advantage-title">
<div class="vertical-line"></div>
Time Saving
</div>
<div class="advantage-description">
Multi platform publishing efficiency improvement, allowing you to
focus on content creation.
</div>
</div>
<div class="advantage-item">
<div class="advantage-title">
<div class="vertical-line"></div>
Safe and Reliable
</div>
<div class="advantage-description">
Enterprise level data encryption and permission control ensure
account and content security.
</div>
</div>
<div class="advantage-item">
<div class="advantage-title">
<div class="vertical-line"></div>
Maintain Consistency
</div>
<div class="advantage-description">
Ensure that brand information is presented uniformly on all
platforms.
</div>
</div>
<div class="advantage-item">
<div class="advantage-title">
<div class="vertical-line"></div>
Data Driven
</div>
<div class="advantage-description">
Optimizing Content Strategies Based on Actual Performance.
</div>
</div>
<div class="advantage-item">
<div class="advantage-title">
<div class="vertical-line"></div>
Easy to Use
</div>
<div class="advantage-description">
Intuitive interface design, no need for professional technical
background.
</div>
</div>
</div>
</div>
</section>
<section class="cta-section">
<div class="cta-content">
<div class="cta-title">Get customized<br />solutions for free</div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="60"
viewBox="0 0 32 60"
fill="none"
>
<path
d="M31.3636 42.4968C32.2121 43.3437 32.2121 44.7165 31.3636 45.5635L17.5362 59.3648C16.6877 60.2117 15.3123 60.2117 14.4638 59.3648L0.636387 45.5635C-0.212133 44.7165 -0.212133 43.3437 0.636387 42.4968C1.48491 41.6499 2.8603 41.6499 3.70883 42.4968L13.8272 52.5962L13.8272 2.16868C13.8272 0.970951 14.8 -7.51835e-07 16 -6.99382e-07C17.2 -6.46929e-07 18.1728 0.970951 18.1728 2.16868L18.1728 52.5962L28.2912 42.4968C29.1397 41.6499 30.5151 41.6499 31.3636 42.4968Z"
fill="#FF7BAC"
/>
</svg>
</div>
<div class="cta-qr-code">
<img
src="@/assets/image/375/product-introduction-img6.png"
alt="QR Code"
/>
</div>
<img
src="@/assets/image/375/product-introduction-img5.png"
alt="background"
class="cta-bg-img"
/>
</section>
</div>
</template>
<style scoped lang="scss">
.page-container {
background-color: #fff;
font-family: "PingFang SC", sans-serif;
margin: 0 auto;
position: relative;
}
.hero-section {
text-align: center;
position: relative;
background-image: url("@/assets/image/375/product-introduction-img3.png");
background-repeat: no-repeat;
background-size: 100% auto;
background-position: top;
}
.hero-content {
position: relative;
z-index: 2;
}
.hero-title {
font-size: 24 * 5.12px;
font-weight: 500;
line-height: 34 * 5.12px;
letter-spacing: 0.2 * 5.12px;
padding: 153 * 5.12px 0 163 * 5.12px 0;
color: #000;
z-index: 2;
}
.hero-bg-img {
position: absolute;
bottom: -25 * 5.12px;
left: 0;
width: 100%;
/* height: 100%; */
z-index: 1;
}
.core-value-card {
width: 346 * 5.12px;
padding: 40 * 5.12px 32 * 5.12px;
margin: 0 auto;
background-color: #fff;
border-radius: 16 * 5.12px;
box-shadow: 0 * 5.12px 3 * 5.12px 14 * 5.12px 0 * 5.12px rgba(0, 0, 0, 0.16);
text-align: left;
z-index: 2;
position: relative;
}
.card-content {
display: flex;
flex-direction: column;
gap: 24 * 5.12px;
}
.card-title {
font-size: 32 * 5.12px;
font-weight: 500;
letter-spacing: 0.8 * 5.12px;
}
.card-text {
font-size: 14 * 5.12px;
color: #455363;
letter-spacing: 0.48 * 5.12px;
text-align: justify;
font-feature-settings: "liga" off, "clig" off;
}
.section-header {
margin-bottom: 32 * 5.12px;
padding: 0 16 * 5.12px;
}
.decorator-bar {
width: 58 * 5.12px;
height: 7 * 5.12px;
background-color: #ff7bac;
margin-bottom: 16 * 5.12px;
}
.section-title {
font-family: "PingFang SC";
font-size: 24 * 5.12px;
font-style: normal;
font-weight: 500;
line-height: normal;
}
.features-section {
padding-top: 64 * 5.12px;
width: 346 * 5.12px;
margin: 0 auto;
}
.features-list {
display: flex;
flex-direction: column;
gap: 24 * 5.12px;
}
.feature-item {
display: flex;
flex-direction: column;
gap: 16 * 5.12px;
}
.feature-title {
font-size: 24 * 5.12px;
font-weight: 500;
line-height: 32 * 5.12px;
letter-spacing: 1.2 * 5.12px;
display: flex;
align-items: flex-start;
gap: 16 * 5.12px;
}
.feature-description {
font-size: 16 * 5.12px;
line-height: 22 * 5.12px;
color: #455363;
letter-spacing: 0.48 * 5.12px;
padding: 0 16 * 5.12px;
text-align: justify;
font-feature-settings: "liga" off, "clig" off;
}
.solutions-section {
padding-top: 64 * 5.12px;
width: 346 * 5.12px;
margin: 0 auto;
}
.solutions-content {
display: flex;
gap: 32 * 5.12px;
flex-direction: column;
align-items: stretch;
}
.solutions-list {
display: flex;
flex-direction: column;
gap: 24 * 5.12px;
width: 100%;
}
.solution-item {
text-align: left;
display: flex;
flex-direction: column;
}
.solution-icon {
width: 92 * 5.12px;
height: 76 * 5.12px;
padding-left: 16 * 5.12px;
}
.solution-title {
font-family: "PingFang SC";
font-size: 24 * 5.12px;
font-style: normal;
font-weight: 500;
line-height: 32 * 5.12px; /* 133.333% */
letter-spacing: 1.2 * 5.12px;
display: flex;
gap: 16 * 5.12px;
align-items: flex-start;
margin-bottom: 16 * 5.12px;
}
.solution-description {
font-size: 14 * 5.12px;
letter-spacing: 0.48 * 5.12px;
padding: 0 16 * 5.12px;
text-align: justify;
font-feature-settings: "liga" off, "clig" off;
font-family: "PingFang SC";
font-style: normal;
font-weight: 400;
line-height: normal;
color: #455363;
}
.solution-image-container {
width: 100%;
border-radius: 16 * 5.12px;
}
.solution-image {
width: 100%;
height: auto;
object-fit: cover;
border-radius: 16 * 5.12px;
}
.advantages-section {
margin-top: 64 * 5.12px;
padding: 64 * 5.12px 0;
background-image: url("@/assets/image/375/product-introduction-img4.png");
background-size: cover;
background-position: center;
color: #fff;
position: relative;
}
.advantages-content {
width: 346 * 5.12px;
margin: 0 auto;
display: flex;
gap: 16 * 5.12px;
position: relative;
z-index: 1;
flex-direction: column;
}
.advantages-header {
width: 100%;
padding: 0 16 * 5.12px;
}
.advantages-list {
width: 100%;
display: flex;
flex-direction: column;
gap: 24 * 5.12px;
}
.advantage-item {
display: flex;
flex-direction: column;
gap: 16 * 5.12px;
}
.advantage-title {
font-size: 24 * 5.12px;
font-weight: 500;
line-height: 32 * 5.12px;
letter-spacing: 1.2 * 5.12px;
display: flex;
gap: 16 * 5.12px;
align-items: flex-start;
}
.advantage-description {
font-size: 16 * 5.12px;
line-height: 22 * 5.12px;
letter-spacing: 0.48 * 5.12px;
opacity: 0.7;
padding: 0 16 * 5.12px;
}
.text-white {
color: #fff;
}
.cta-section {
padding: 80 * 5.12px 0 21 * 5.12px 0;
width: 346 * 5.12px;
margin: 0 auto;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.cta-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16 * 5.12px;
position: relative;
z-index: 1;
width: 100%;
}
.cta-title {
font-family: "PingFang SC";
font-size: 24 * 5.12px;
font-style: normal;
font-weight: 500;
line-height: normal;
}
.cta-qr-code {
width: 188 * 5.12px;
height: 188 * 5.12px;
background-color: #90ffff;
border-radius: 16 * 5.12px;
padding: 14 * 5.12px;
margin: 20 * 5.12px 0;
}
.cta-qr-code img {
width: 100%;
height: 100%;
object-fit: contain;
}
.cta-bg-img {
width: 480 * 5.12px;
height: auto;
opacity: 0.8;
z-index: 0;
}
.vertical-line {
width: 1 * 5.12px;
height: 20 * 5.12px;
background: #ff7bac;
flex-shrink: 0;
margin-top: 6 * 5.12px;
}
.grid-lines {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 346 * 5.12px;
height: 100%;
pointer-events: none;
z-index: 0;
}
.grid-lines .line {
position: absolute;
top: 0;
bottom: 0;
}
.grid-lines .line.solid {
width: 1 * 5.12px;
background-color: rgba(0, 0, 0, 0.08);
}
.grid-lines .line.dashed {
width: 0;
border-left: 1 * 5.12px dotted rgba(0, 0, 0, 0.12);
}
.grid-lines .line-1 {
left: 0;
}
.grid-lines .line-5 {
right: 0;
}
</style>

View File

@ -2,23 +2,21 @@
<template>
<div class="page-container">
<div class="grid-lines px-fixed">
<div class="grid-lines">
<div class="line solid line-1"></div>
<div class="line dashed line-2"></div>
<div class="line dashed line-3"></div>
<div class="line dashed line-4"></div>
<div class="line solid line-5"></div>
</div>
<section class="hero-section px-fixed relative">
<section class="hero-section relative">
<div class="hero-content">
<div class="hero-title">
More than just a tool<br />
Comprehensive growth solutions, <br />
providing a one-stop solution for content creation,<br />
publishing, analysis, and monetization
Comprehensive growth solutions, <br />providing a one-stop solution
for <br />content creation, publishing, analysis, <br />and
monetization
</div>
</div>
<div class="core-value-card px-fixed">
<div class="core-value-card">
<div class="card-content">
<div class="card-title">Core Value</div>
<div class="card-text">
@ -33,13 +31,13 @@
</div>
</div>
<img
src="@/assets/image/1440/product-introduction-img2.png"
src="@/assets/image/768/product-introduction-img2.png"
alt="background"
class="hero-bg-img"
/>
</section>
<section class="features-section px-fixed">
<section class="features-section">
<div class="section-header">
<div class="decorator-bar"></div>
<div class="section-title">Product Features</div>
@ -47,7 +45,7 @@
<div class="features-list">
<div class="feature-item">
<div class="feature-title">
<div class="vertical-line px-fixed"></div>
<div class="vertical-line"></div>
One-click Synchronous Publishing
</div>
<div class="feature-description">
@ -57,7 +55,7 @@
</div>
<div class="feature-item">
<div class="feature-title">
<div class="vertical-line px-fixed"></div>
<div class="vertical-line"></div>
Intelligent Scheduled Publishing
</div>
<div class="feature-description">
@ -67,7 +65,7 @@
</div>
<div class="feature-item">
<div class="feature-title">
<div class="vertical-line px-fixed"></div>
<div class="vertical-line"></div>
Unified Management of Multiple Accounts
</div>
<div class="feature-description">
@ -78,7 +76,7 @@
</div>
<div class="feature-item">
<div class="feature-title">
<div class="vertical-line px-fixed"></div>
<div class="vertical-line"></div>
Cloud Content Library
</div>
<div class="feature-description">
@ -88,7 +86,7 @@
</div>
<div class="feature-item">
<div class="feature-title">
<div class="vertical-line px-fixed"></div>
<div class="vertical-line"></div>
Basic Data Tracking
</div>
<div class="feature-description">
@ -100,21 +98,28 @@
</div>
</section>
<section class="solutions-section px-fixed">
<section class="solutions-section">
<div class="section-header">
<div class="decorator-bar"></div>
<div class="section-title">Value Added Solutions</div>
</div>
<div class="solutions-content">
<div class="solution-image-container">
<img
src="@/assets/image/768/product-introduction-img1.png"
alt="Value Added Solutions"
class="solution-image"
/>
</div>
<div class="solutions-list">
<div class="solution-item">
<img
src="@/assets/image/1440/product-introduction-icon1.png"
src="@/assets/image/768/product-introduction-icon1.png"
alt="KOL Brand Promotion"
class="solution-icon"
/>
<div class="solution-title">
<div class="vertical-line px-fixed"></div>
<div class="vertical-line"></div>
KOL Brand Promotion Services
</div>
<div class="solution-description">
@ -125,12 +130,12 @@
</div>
<div class="solution-item">
<img
src="@/assets/image/1440/product-introduction-icon2.png"
src="@/assets/image/768/product-introduction-icon2.png"
alt="Content Creation Support"
class="solution-icon"
/>
<div class="solution-title">
<div class="vertical-line px-fixed"></div>
<div class="vertical-line"></div>
Professional Content Creation Support
</div>
<div class="solution-description">
@ -141,12 +146,12 @@
</div>
<div class="solution-item">
<img
src="@/assets/image/1440/product-introduction-icon3.png"
src="@/assets/image/768/product-introduction-icon3.png"
alt="Account Operation"
class="solution-icon"
/>
<div class="solution-title">
<div class="vertical-line px-fixed"></div>
<div class="vertical-line"></div>
Account Operation and Hosting Services
</div>
<div class="solution-description">
@ -156,84 +161,72 @@
</div>
</div>
</div>
<div class="solution-image-container">
<img
src="@/assets/image/1440/product-introduction-img1.png"
alt="Value Added Solutions"
class="solution-image"
style="width: 434px"
/>
</div>
</div>
</section>
<section class="advantages-section px-fixed">
<div class="advantages-content px-fixed">
<section class="advantages-section">
<div class="advantages-content">
<div class="advantages-header">
<div class="decorator-bar"></div>
<div class="section-title text-white">Our Advantages</div>
</div>
<div style="width: 50%">
<div class="advantages-list">
<div class="advantage-item">
<div class="advantage-title">
<div class="vertical-line px-fixed"></div>
Time Saving
</div>
<div class="advantage-description">
Multi platform publishing efficiency improvement, allowing you
to focus on content creation.
</div>
<div class="advantages-list">
<div class="advantage-item">
<div class="advantage-title">
<div class="vertical-line"></div>
Time Saving
</div>
<div class="advantage-item">
<div class="advantage-title">
<div class="vertical-line px-fixed"></div>
Safe and Reliable
</div>
<div class="advantage-description">
Enterprise level data encryption and permission control ensure
account and content security.
</div>
<div class="advantage-description">
Multi platform publishing efficiency improvement, allowing you to
focus on content creation.
</div>
<div class="advantage-item">
<div class="advantage-title">
<div class="vertical-line px-fixed"></div>
Maintain Consistency
</div>
<div class="advantage-description">
Ensure that brand information is presented uniformly on all
platforms.
</div>
</div>
<div class="advantage-item">
<div class="advantage-title">
<div class="vertical-line"></div>
Safe and Reliable
</div>
<div class="advantage-item">
<div class="advantage-title">
<div class="vertical-line px-fixed"></div>
Data Driven
</div>
<div class="advantage-description">
Optimizing Content Strategies Based on Actual Performance.
</div>
<div class="advantage-description">
Enterprise level data encryption and permission control ensure
account and content security.
</div>
<div class="advantage-item">
<div class="advantage-title">
<div class="vertical-line px-fixed"></div>
Easy to Use
</div>
<div class="advantage-description">
Intuitive interface design, no need for professional technical
background.
</div>
</div>
<div class="advantage-item">
<div class="advantage-title">
<div class="vertical-line"></div>
Maintain Consistency
</div>
<div class="advantage-description">
Ensure that brand information is presented uniformly on all
platforms.
</div>
</div>
<div class="advantage-item">
<div class="advantage-title">
<div class="vertical-line"></div>
Data Driven
</div>
<div class="advantage-description">
Optimizing Content Strategies Based on Actual Performance.
</div>
</div>
<div class="advantage-item">
<div class="advantage-title">
<div class="vertical-line"></div>
Easy to Use
</div>
<div class="advantage-description">
Intuitive interface design, no need for professional technical
background.
</div>
</div>
</div>
</div>
</section>
<section class="cta-section px-fixed">
<section class="cta-section">
<img
src="@/assets/image/1440/product-introduction-img5.png"
src="@/assets/image/768/product-introduction-img5.png"
alt="background"
class="cta-bg-img"
/>
@ -258,7 +251,7 @@
</div>
<div class="cta-qr-code">
<img
src="@/assets/image/1440/product-introduction-img6.png"
src="@/assets/image/768/product-introduction-img6.png"
alt="QR Code"
/>
</div>
@ -267,7 +260,7 @@
</div>
</template>
<style scoped>
<style scoped lang="scss">
.page-container {
background-color: #fff;
font-family: "PingFang SC", sans-serif;
@ -275,10 +268,10 @@
position: relative;
}
.hero-section.px-fixed {
.hero-section {
text-align: center;
position: relative;
background-image: url("@/assets/image/1440/product-introduction-img3.png");
background-image: url("@/assets/image/768/product-introduction-img3.png");
background-repeat: no-repeat;
background-size: 100% auto;
background-position: top;
@ -290,28 +283,29 @@
}
.hero-title {
font-size: 40px;
font-size: 32 * 2.5px;
font-weight: 500;
line-height: 56px;
letter-spacing: 1.2px;
padding: 153px 0;
line-height: 56 * 2.5px;
letter-spacing: 1.2 * 2.5px;
padding: 153 * 2.5px 0 133 * 2.5px 0;
color: #000;
z-index: 2;
}
.hero-bg-img {
position: absolute;
bottom: -204px;
bottom: -24 * 2.5px;
left: 0;
width: 100%;
/* height: 100%; */
z-index: 1;
}
.core-value-card.px-fixed {
width: 932px;
padding: 40px 32px;
.core-value-card {
width: 662 * 2.5px;
padding: 40 * 2.5px 32 * 2.5px;
margin: 0 auto;
background-color: #fff;
border-radius: 16px;
box-shadow: 0px 3px 14px 0px rgba(0, 0, 0, 0.16);
border-radius: 16 * 2.5px;
box-shadow: 0 * 2.5px 3 * 2.5px 14 * 2.5px 0 * 2.5px rgba(0, 0, 0, 0.16);
text-align: left;
z-index: 2;
position: relative;
@ -320,204 +314,191 @@
.card-content {
display: flex;
flex-direction: column;
gap: 32px;
gap: 24 * 2.5px;
}
.card-title {
font-size: 40px;
font-size: 40 * 2.5px;
font-weight: 500;
line-height: 56px;
letter-spacing: 1.2px;
line-height: 56 * 2.5px;
letter-spacing: 1.2 * 2.5px;
}
.card-text {
font-size: 16px;
line-height: 22px;
font-size: 16 * 2.5px;
line-height: 22 * 2.5px;
color: #455363;
letter-spacing: 0.48px;
letter-spacing: 0.48 * 2.5px;
}
.section-header {
margin-bottom: 32px;
padding: 0 16px;
margin-bottom: 32 * 2.5px;
padding: 0 16 * 2.5px;
}
.decorator-bar {
width: 58px;
height: 7px;
width: 58 * 2.5px;
height: 7 * 2.5px;
background-color: #ff7bac;
margin-bottom: 16px;
margin-bottom: 16 * 2.5px;
}
.section-title {
font-size: 40px;
font-size: 40 * 2.5px;
font-weight: 500;
line-height: 56px;
letter-spacing: 1.2px;
line-height: 56 * 2.5px;
letter-spacing: 1.2 * 2.5px;
color: #000;
}
.features-section.px-fixed {
padding-top: 200px;
width: 932px;
.features-section {
padding-top: 100 * 2.5px;
width: 662 * 2.5px;
margin: 0 auto;
}
.features-list {
display: flex;
flex-direction: column;
gap: 32px;
gap: 24 * 2.5px;
}
.feature-item {
display: flex;
flex-direction: column;
gap: 16px;
gap: 16 * 2.5px;
}
.feature-title {
font-size: 24px;
font-size: 24 * 2.5px;
font-weight: 500;
line-height: 32px;
letter-spacing: 1.2px;
line-height: 32 * 2.5px;
letter-spacing: 1.2 * 2.5px;
display: flex;
align-items: flex-start;
gap: 16px;
gap: 16 * 2.5px;
}
.feature-description {
font-size: 16px;
line-height: 22px;
font-size: 16 * 2.5px;
line-height: 22 * 2.5px;
color: #455363;
letter-spacing: 0.48px;
padding: 0 16px;
text-align: justify;
font-feature-settings: "liga" off, "clig" off;
letter-spacing: 0.48 * 2.5px;
padding: 0 16 * 2.5px;
}
.solutions-section.px-fixed {
padding-top: 80px;
width: 932px;
.solutions-section {
padding-top: 80 * 2.5px;
width: 662 * 2.5px;
margin: 0 auto;
}
.solutions-content {
display: flex;
gap: 16px;
align-items: center;
gap: 32 * 2.5px;
flex-direction: column;
align-items: stretch;
}
.solutions-list {
display: flex;
flex-direction: column;
gap: 24px;
width: 466px;
gap: 24 * 2.5px;
width: 100%;
}
.solution-item {
text-align: left;
display: flex;
flex-direction: column;
gap: 14px;
}
.solution-icon {
width: 92px;
height: 76px;
padding-left: 16px;
width: 92 * 2.5px;
height: 76 * 2.5px;
padding-left: 16 * 2.5px;
}
.solution-title {
font-family: "PingFang SC";
font-size: 24px;
font-size: 24 * 2.5px;
font-style: normal;
font-weight: 500;
line-height: 32px; /* 133.333% */
letter-spacing: 1.2px;
line-height: 32 * 2.5px; /* 133.333% */
letter-spacing: 1.2 * 2.5px;
display: flex;
gap: 16px;
gap: 16 * 2.5px;
align-items: flex-start;
margin-bottom: 16 * 2.5px;
}
.solution-description {
align-self: stretch;
font-size: 16 * 2.5px;
line-height: 22 * 2.5px;
color: #455363;
padding: 0 16px;
text-align: justify;
font-feature-settings: "liga" off, "clig" off;
/* 正文 */
font-family: "PingFang SC";
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 137.5% */
letter-spacing: 0.48px;
letter-spacing: 0.48 * 2.5px;
padding: 0 16 * 2.5px;
}
.solution-image-container {
width: 434px;
border-radius: 16px;
margin: 0 auto;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
border-radius: 16 * 2.5px;
}
.solution-image {
width: 100%;
height: 100%;
height: auto;
object-fit: cover;
border-radius: 16px;
border-radius: 16 * 2.5px;
}
.advantages-section.px-fixed {
margin-top: 80px;
padding: 80px 0;
background-image: url("@/assets/image/1440/product-introduction-img4.png");
.advantages-section {
margin-top: 80 * 2.5px;
padding: 80 * 2.5px 0;
background-image: url("@/assets/image/768/product-introduction-img4.png");
background-size: cover;
background-position: center;
color: #fff;
position: relative;
}
.advantages-content.px-fixed {
width: 932px;
.advantages-content {
width: 662 * 2.5px;
margin: 0 auto;
display: flex;
justify-content: space-between;
gap: 16 * 2.5px;
position: relative;
z-index: 1;
flex-direction: column;
}
.advantages-header {
width: 466px;
padding: 0 16px;
width: 100%;
padding: 0 16 * 2.5px;
}
.advantages-list {
width: 466px;
width: 100%;
display: flex;
flex-direction: column;
gap: 32px;
gap: 24 * 2.5px;
}
.advantage-item {
display: flex;
flex-direction: column;
gap: 16px;
gap: 16 * 2.5px;
}
.advantage-title {
font-size: 24px;
font-size: 24 * 2.5px;
font-weight: 500;
line-height: 32px;
letter-spacing: 1.2px;
line-height: 32 * 2.5px;
letter-spacing: 1.2 * 2.5px;
display: flex;
gap: 16px;
gap: 16 * 2.5px;
align-items: flex-start;
}
.advantage-description {
font-size: 16px;
line-height: 22px;
letter-spacing: 0.48px;
font-size: 16 * 2.5px;
line-height: 22 * 2.5px;
letter-spacing: 0.48 * 2.5px;
opacity: 0.7;
padding: 0 16px;
}
.text-white {
color: #fff;
}
.cta-section.px-fixed {
padding: 60px 0;
.cta-section {
padding: 80 * 2.5px 0;
position: relative;
width: 932px;
width: 662 * 2.5px;
margin: 0 auto;
overflow: hidden;
}
@ -525,7 +506,7 @@
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 16px;
padding: 0 16 * 2.5px;
position: relative;
z-index: 1;
}
@ -533,24 +514,24 @@
display: flex;
flex-direction: column;
justify-content: space-between;
height: 188px;
height: 188 * 2.5px;
}
.cta-arrow {
width: 60px;
height: 32px;
width: 60 * 2.5px;
height: 32 * 2.5px;
}
.cta-title {
font-size: 40px;
font-size: 40 * 2.5px;
font-weight: 500;
line-height: 56px;
letter-spacing: 1.2px;
line-height: 56 * 2.5px;
letter-spacing: 1.2 * 2.5px;
}
.cta-qr-code {
width: 188px;
height: 188px;
width: 188 * 2.5px;
height: 188 * 2.5px;
background-color: #90ffff;
border-radius: 16px;
padding: 14px;
border-radius: 16 * 2.5px;
padding: 14 * 2.5px;
}
.cta-qr-code img {
width: 100%;
@ -559,27 +540,27 @@
}
.cta-bg-img {
position: absolute;
top: 80px;
left: 355px;
width: 530px;
height: 268px;
top: 80 * 2.5px;
left: 60 * 2.5px;
width: 530 * 2.5px;
height: 268 * 2.5px;
opacity: 0.8;
z-index: 0;
}
.vertical-line.px-fixed {
width: 1px;
height: 16px;
.vertical-line {
width: 1 * 2.5px;
height: 20 * 2.5px;
background: #ff7bac;
flex-shrink: 0;
margin-top: 6px;
margin-top: 6 * 2.5px;
}
.grid-lines.px-fixed {
.grid-lines {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 932px;
width: 662 * 2.5px;
height: 100%;
pointer-events: none;
z-index: 0;
@ -592,26 +573,26 @@
}
.grid-lines .line.solid {
width: 1px;
width: 1 * 2.5px;
background-color: rgba(0, 0, 0, 0.08);
}
.grid-lines .line.dashed {
width: 0;
border-left: 1px dotted rgba(0, 0, 0, 0.12);
border-left: 1 * 2.5px dotted rgba(0, 0, 0, 0.12);
}
.grid-lines .line-1 {
left: 0;
}
.grid-lines .line-2 {
left: 310px;
left: 162.5 * 2.5px;
}
.grid-lines .line-3 {
left: 620px;
left: 325 * 2.5px;
}
.grid-lines .line-4 {
left: 930px;
left: 487.5 * 2.5px;
}
.grid-lines .line-5 {
right: 0;

View File

@ -1,7 +1,6 @@
<script setup>
import { useStockQuote } from "@/store/stock-quote/index.js";
const { getStockQuate, stockQuote, formatted } = useStockQuote();
console.log(stockQuote);
getStockQuate();
</script>

View File

@ -1,82 +1,211 @@
<script setup>
import { onMounted } from "vue";
import { useStockQuote } from "@/store/stock-quote/index.js";
const { getStockQuate, stockQuote, formatted } = useStockQuote();
getStockQuate();
onMounted(() => {
getStockQuate();
});
</script>
<template>
<main
class="min-h-60vh flex flex-col items-center justify-start px-2 py-5 pt-500px"
>
<!-- 价格卡片 -->
<section
class="w-full max-w-90vw flex flex-col items-center justify-center glass-card p-4 rounded-2xl shadow mb-5"
>
<div
class="text-4xl font-extrabold text-#ff7bac animate-bg-move select-none drop-shadow-lg"
>
${{ stockQuote.price }}
<main class="stock-quote-container-375">
<div class="content-wrapper">
<div class="title-section">
<div class="title-decoration"></div>
<div class="title-text">Stock Quote</div>
</div>
<div
class="mt-2 text-sm text-gray-500 font-semibold tracking-widest mb-0px"
>
NASDAQ: <span class="text-black">FIEE</span>
</div>
<div class="text-gray-500 text-60px">{{ formatted }}</div>
</section>
<!-- 信息卡片列表 -->
<section class="w-full max-w-90vw grid grid-cols-2 gap-2">
<div class="info-card">
<div class="text-xs text-gray-400">Open</div>
<div class="text-lg font-bold">{{ stockQuote.open }}</div>
</div>
<div class="info-card">
<div class="text-xs text-gray-400">% Change</div>
<div
class="text-lg font-bold"
:class="
stockQuote.change?.includes('-') ? 'text-green-500' : 'text-red-500'
"
>
{{ stockQuote.change }}
<div class="data-section">
<div class="price-card">
<div class="price-value">${{ stockQuote.price }}</div>
<div class="price-nasdaq">NASDAQ: FIEE</div>
<div class="price-date">{{ formatted }}</div>
</div>
<div class="details-grid">
<div class="details-column">
<div class="detail-item">
<span class="detail-label">Open</span>
<span class="detail-value">{{ stockQuote.open }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Day's Range</span>
<span class="detail-value">{{ stockQuote.daysRange }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Volume</span>
<span class="detail-value">{{ stockQuote.volume }}</span>
</div>
</div>
<div class="details-column">
<div class="detail-item">
<span class="detail-label">% Change</span>
<span
class="detail-value"
:class="{
'text-red': String(stockQuote.change).includes('-'),
'text-green': String(stockQuote.change).includes('+'),
}"
>
{{ stockQuote.change }}
</span>
</div>
<div class="detail-item">
<span class="detail-label">52-Week Range</span>
<span class="detail-value">{{ stockQuote.week52Range }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Market Cap</span>
<span class="detail-value">{{ stockQuote.marketCap }}</span>
</div>
</div>
</div>
</div>
<div class="info-card">
<div class="text-xs text-gray-400">Day's Range</div>
<div class="text-lg font-bold">{{ stockQuote.daysRange }}</div>
</div>
<div class="info-card">
<div class="text-xs text-gray-400">52-Week Range</div>
<div class="text-lg font-bold">{{ stockQuote.week52Range }}</div>
</div>
<div class="info-card">
<div class="text-xs text-gray-400">Volume</div>
<div class="text-lg font-bold">{{ stockQuote.volume }}</div>
</div>
<div class="info-card">
<div class="text-xs text-gray-400">Market Cap</div>
<div class="text-lg font-bold">{{ stockQuote.marketCap }}</div>
</div>
</section>
</div>
</main>
</template>
<style scoped>
.glass-card {
backdrop-filter: blur(10px);
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(200, 200, 255, 0.18);
box-shadow: 0 2px 8px 0 rgba(255, 123, 172, 0.08);
<style scoped lang="scss">
.stock-quote-container-375 {
display: flex;
flex-direction: column;
align-items: center;
font-family: "PingFang SC", sans-serif;
background-image: url("@/assets/image/375/bg-stock-quote.png");
background-size: cover;
background-position: center;
height: 100vh;
}
.info-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
box-shadow: 0 1px 4px 0 rgba(255, 123, 172, 0.06);
padding: 12px 10px;
.content-wrapper {
width: 343 * 5.12px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
}
.title-section {
display: flex;
flex-direction: column;
gap: 8 * 5.12px;
margin-bottom: 32 * 5.12px;
margin-top: 43 * 5.12px;
padding: 0 16 * 5.12px;
}
.title-decoration {
width: 58 * 5.12px;
height: 7 * 5.12px;
background: #ff7bac;
margin: auto 0;
margin-top: 0;
}
.title-text {
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 24 * 5.12px;
line-height: 1;
letter-spacing: 0.03em;
color: #000000;
}
.data-section {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 16 * 5.12px;
}
.price-card {
width: 100%;
height: 280 * 5.12px;
background-color: white;
border-radius: 16 * 5.12px;
box-shadow: 0 3 * 5.12px 14 * 5.12px 0 rgba(0, 0, 0, 0.16);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 16 * 5.12px;
gap: 4 * 5.12px;
text-align: center;
}
.price-value {
width: 100%;
font-size: 88 * 5.12px;
font-weight: 600;
line-height: 1;
letter-spacing: 0.48 * 5.12px;
background: linear-gradient(to right, #ff7bac, #00ffff);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.price-nasdaq {
width: 100%;
font-size: 20 * 5.12px;
color: black;
font-weight: 600;
letter-spacing: 1.2 * 5.12px;
line-height: 1;
}
.price-date {
width: 100%;
font-size: 16 * 5.12px;
color: #455363;
font-weight: 500;
letter-spacing: 1.2 * 5.12px;
}
.details-grid {
width: 343 * 5.12px;
display: flex;
align-items: center;
margin: 0 auto;
}
.details-column {
width: 172 * 5.12px;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.detail-item {
width: 100%;
height: 155 * 5.12px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
// padding: 16 * 5.12px 32 * 5.12px;
text-align: center;
}
.detail-label {
width: 100%;
font-size: 16 * 5.12px;
color: #455363;
font-weight: 500;
}
.detail-value {
width: 100%;
font-size: 20 * 5.12px;
color: black;
font-weight: 600;
letter-spacing: 1.2 * 5.12px;
}
.text-red {
color: #cf3050;
}
.text-green {
color: #00c48c;
}
</style>

View File

@ -7,81 +7,201 @@ getStockQuate();
</script>
<template>
<main ref="main">
<main
class="min-h-60vh flex flex-col items-center justify-start px-4 py-6 pt-500px"
>
<!-- 价格卡片 -->
<section
class="w-full max-w-80vw flex flex-col items-center justify-center glass-card p-6 rounded-2xl shadow mb-6"
>
<div
class="text-5xl font-extrabold text-#ff7bac animate-bg-move select-none drop-shadow-lg"
>
${{ stockQuote.price }}
<main class="stock-quote-container-768">
<div class="content-wrapper">
<div class="title-section">
<div class="title-decoration"></div>
<div class="title-text">Stock Quote</div>
</div>
<div class="data-section">
<div class="price-card">
<div class="price-value">${{ stockQuote.price }}</div>
<div class="price-nasdaq">NASDAQ: FIEE</div>
<div class="price-date">{{ formatted }}</div>
</div>
<div
class="mt-3 text-base text-gray-500 font-semibold tracking-widest mb-0px"
>
NASDAQ: <span class="text-black">FIEE</span>
</div>
<div class="text-gray-500 text-70px">{{ formatted }}</div>
</section>
<!-- 信息卡片列表 -->
<section class="w-full max-w-80vw grid grid-cols-3 gap-4">
<div class="info-card">
<div class="text-sm text-gray-400">Open</div>
<div class="text-xl font-bold">{{ stockQuote.open }}</div>
</div>
<div class="info-card">
<div class="text-sm text-gray-400">% Change</div>
<div
class="text-xl font-bold"
:class="
stockQuote.change?.includes('-')
? 'text-green-500'
: 'text-red-500'
"
>
{{ stockQuote.change }}
<div class="details-grid">
<div class="details-column">
<div class="detail-item">
<span class="detail-label">Open</span>
<span class="detail-value">{{ stockQuote.open }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Day's Range</span>
<span class="detail-value">{{ stockQuote.daysRange }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Volume</span>
<span class="detail-value">{{ stockQuote.volume }}</span>
</div>
</div>
<div class="details-column">
<div class="detail-item">
<span class="detail-label">% Change</span>
<span
class="detail-value"
:class="{
'text-red': String(stockQuote.change).includes('-'),
'text-green': String(stockQuote.change).includes('+'),
}"
>
{{ stockQuote.change }}
</span>
</div>
<div class="detail-item">
<span class="detail-label">52-Week Range</span>
<span class="detail-value">{{ stockQuote.week52Range }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Market Cap</span>
<span class="detail-value">{{ stockQuote.marketCap }}</span>
</div>
</div>
</div>
<div class="info-card">
<div class="text-sm text-gray-400">Day's Range</div>
<div class="text-xl font-bold">{{ stockQuote.daysRange }}</div>
</div>
<div class="info-card">
<div class="text-sm text-gray-400">52-Week Range</div>
<div class="text-xl font-bold">{{ stockQuote.week52Range }}</div>
</div>
<div class="info-card">
<div class="text-sm text-gray-400">Volume</div>
<div class="text-xl font-bold">{{ stockQuote.volume }}</div>
</div>
<div class="info-card">
<div class="text-sm text-gray-400">Market Cap</div>
<div class="text-xl font-bold">{{ stockQuote.marketCap }}</div>
</div>
</section>
</main>
</div>
</div>
</main>
</template>
<style scoped lang="scss">
.glass-card {
backdrop-filter: blur(10px);
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(200, 200, 255, 0.18);
box-shadow: 0 3px 12px 0 rgba(255, 123, 172, 0.08);
.stock-quote-container-768 {
display: flex;
flex-direction: column;
align-items: center;
font-family: "PingFang SC", sans-serif;
background-image: url("@/assets/image/768/bg-stock-quote.png");
background-size: cover;
background-position: center;
}
.info-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
box-shadow: 0 2px 6px 0 rgba(255, 123, 172, 0.06);
padding: 16px 14px;
.content-wrapper {
width: 650 * 2.5px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
color: #fff;
}
.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;
}
.data-section {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.price-card {
width: 100%;
height: 355 * 2.5px;
background-color: white;
border-radius: 16 * 2.5px;
box-shadow: 0 * 2.5px 3 * 2.5px 14 * 2.5px 0 * 2.5px rgba(0, 0, 0, 0.16);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 16 * 2.5px;
gap: 30 * 2.5px;
text-align: center;
}
.price-value {
width: 100%;
font-size: 110 * 2.5px;
font-weight: 600;
line-height: 1;
letter-spacing: 0.48 * 2.5px;
background: linear-gradient(to right, #ff7bac, #00ffff);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.price-nasdaq {
width: 100%;
font-size: 24 * 2.5px;
color: black;
font-weight: 600;
letter-spacing: 1.2 * 2.5px;
line-height: 1;
}
.price-date {
width: 100%;
font-size: 18 * 2.5px;
color: #455363;
font-weight: 500;
}
.details-grid {
width: 100%;
display: flex;
align-items: center;
}
.details-column {
width: 325 * 2.5px;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.detail-item {
width: 100%;
height: 155 * 2.5px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 16 * 2.5px 32 * 2.5px;
text-align: center;
}
.detail-label {
width: 100%;
font-size: 18 * 2.5px;
color: #455363;
font-weight: 500;
}
.detail-value {
width: 100%;
font-size: 24 * 2.5px;
color: black;
font-weight: 600;
letter-spacing: 1.2 * 2.5px;
}
.text-red {
color: #cf3050;
}
.text-green {
color: #00c48c;
}
</style>