fiee-official-website/src/components/customEcharts/size1920/index.vue
2025-10-16 15:59:52 +08:00

674 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="custom-echarts">
<div>
<div class="echarts-header">
<!-- 标题区域 -->
<div class="title-section">
<div class="title-decoration"></div>
<div class="stock-title">
<span>{{ t("historic_stock.echarts.title") }}</span>
</div>
</div>
<div class="echarts-search-area">
<div class="echarts-search-byRange">
<text style="font-size: 0.9rem; font-weight: 400; color: #666666">
{{ t("historic_stock.echarts.range") }}
</text>
<div class="search-range-list">
<div
class="search-range-list-each"
v-for="(item, index) in searchRangeOptions"
:key="index"
:class="{ activeRange: state.activeRange === item.key }"
@click="changeSearchRange(item.key)"
>
<span>{{ item.label }}</span>
</div>
</div>
</div>
<div class="echarts-search-byDate">
<n-date-picker
v-model:value="state.dateRange"
type="daterange"
:is-date-disabled="isDateDisabled"
@update:value="handleDateRangeChange"
input-readonly
/>
</div>
</div>
</div>
</div>
<div id="myEcharts" class="myChart"></div>
</div>
</template>
<script setup>
import { onMounted, onBeforeUnmount, watch, reactive, computed } from "vue";
import * as echarts from "echarts";
import { NDatePicker, NIcon } from "naive-ui";
import { ArrowForwardOutline } from "@vicons/ionicons5";
import axios from "axios";
import { useI18n } from "vue-i18n";
const { t, locale } = useI18n();
const state = reactive({
searchRange: ["1m", "3m", "YTD", "1Y", "5Y", "10Y", "Max"],
dateRange: [new Date("2009-10-07").getTime(), new Date().getTime()],
activeRange: "",
});
const searchRangeOptions = computed(() => [
{ label: t("historic_stock.echarts.1m"), key: "1m" },
{ label: t("historic_stock.echarts.3m"), key: "3m" },
{ label: t("historic_stock.echarts.ytd_short"), key: "YTD" },
{ label: t("historic_stock.echarts.1y"), key: "1Y" },
{ label: t("historic_stock.echarts.5y"), key: "5Y" },
{ label: t("historic_stock.echarts.10y"), key: "10Y" },
{ label: t("historic_stock.echarts.max"), key: "Max" },
]);
let myCharts = null;
let historicData = [];
let xAxisData = [];
//初始化eCharts
const initEcharts = (data) => {
historicData = data;
xAxisData = data.map((item) => {
return new Date(item.date).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
});
const yAxisData = data.map((item) => item.price);
// console.error(xAxisData, yAxisData)
// 基于准备好的dom初始化echarts实例
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%", // 给右侧y轴留空间数值可根据y轴label宽度调整
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "line",
label: {
backgroundColor: "#6a7985",
},
},
formatter: function (params) {
const p = params[0];
return `<span style="font-size: 1.1rem; font-weight: 600;">${
p.axisValue
}</span><br/><span style="font-size: 0.9rem; font-weight: 400;">${t(
"historic_stock.echarts.price"
)}: ${p.data}</span>`;
},
triggerOn: "mousemove",
confine: true,
hideDelay: 1500,
},
xAxis: {
data: xAxisData,
type: "category",
boundaryGap: false,
inverse: true,
axisLine: {
lineStyle: {
color: "#CCD6EB",
},
},
axisLabel: {
color: "#323232",
fontWeight: "bold",
interval: "auto",
hideOverlap: true,
},
},
yAxis: {
type: "value",
position: "right",
interval: 25,
// max: 75.0,
show: true,
axisLabel: {
color: "#323232",
fontWeight: "bold",
formatter: function (value) {
return value > 0 ? value.toFixed(2) : value;
},
},
},
series: [
{
data: yAxisData,
type: "line",
sampling: "lttb",
symbol: "none",
lineStyle: {
color: "#CC346C",
},
areaStyle: {
color: {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: "#CC346C",
},
{
offset: 1,
color: "#F4F6F8",
},
],
},
},
markPoint: {
symbol: "circle",
symbolSize: 20,
itemStyle: {
color: {
type: "radial",
x: 0.5,
y: 0.5,
r: 0.5,
colorStops: [
{ offset: 0, color: "#CC346C" },
{ offset: 0.4, color: "white" },
{ offset: 0.4, color: "white" },
{ offset: 0.6, color: "rgba(204, 52, 108, 0.30)" },
{ offset: 0.8, color: "rgba(204, 52, 108, 0.30)" },
{ offset: 1, color: "rgba(255, 123, 172, 0)" },
],
},
},
data: [],
},
progressive: 500,
progressiveThreshold: 3000,
large: true,
largeThreshold: 2000,
},
],
dataZoom: [
{
type: "inside",
},
{
type: "slider",
show: true,
dataBackground: {
lineStyle: {
color: "#CC346C",
},
areaStyle: {
color: {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 1, color: "#CC346C" },
{ offset: 0, color: "#F4F6F8" },
],
},
},
},
selectedDataBackground: {
lineStyle: {
color: "#CC346C",
},
areaStyle: {
color: {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 1, color: "#CC346C" },
{ offset: 0, color: "#F4F6F8" },
],
},
},
},
fillerColor: "rgba(44, 98, 136, 0.3)",
realtime: false,
},
],
});
// 监听 showTip 事件,动态显示 markPoint
myCharts.on("showTip", function (params) {
if (params) {
const dataIndex = params.dataIndex;
const x = myCharts.getOption().xAxis[0].data[dataIndex];
const y = myCharts.getOption().series[0].data[dataIndex];
myCharts.setOption({
series: [
{
markPoint: {
data: [{ coord: [x, y] }],
},
},
],
});
}
});
// 鼠标移出时,清除 markPoint
myCharts.on("globalout", function () {
myCharts.setOption({
series: [
{
markPoint: {
data: [],
},
},
],
});
});
myCharts.on("dataZoom", function (params) {
// 获取当前 dataZoom 范围
const option = myCharts.getOption();
const xAxisData = option.xAxis[0].data;
const dataZoom = option.dataZoom[1] || option.dataZoom[0];
// 获取 dataZoom 的 startValue 和 endValue
let startValue = dataZoom.endValue;
let endValue = dataZoom.startValue;
// 如果是索引,转为日期
if (typeof startValue === "number") {
startValue = xAxisData[startValue];
}
if (typeof endValue === "number") {
endValue = xAxisData[endValue];
}
// 更新日期选择器
state.dateRange = [
new Date(startValue).getTime(),
new Date(endValue).getTime(),
];
});
};
// 防抖函数
const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
// 处理窗口大小变化
const handleResize = () => {
if (myCharts) {
myCharts.resize();
}
};
// 创建防抖后的 resize 处理函数
const debouncedResize = debounce(handleResize, 300);
onMounted(() => {
getHistoricalData();
// 添加窗口 resize 监听
window.addEventListener("resize", debouncedResize);
});
// 组件卸载时清理
onBeforeUnmount(() => {
// 移除 resize 监听
window.removeEventListener("resize", debouncedResize);
// 销毁 echarts 实例
if (myCharts) {
myCharts.dispose();
myCharts = null;
}
});
//获取历史数据
const getHistoricalData = async () => {
let now = new Date();
let toDate =
now.getFullYear() +
"-" +
String(now.getMonth() + 1).padStart(2, "0") +
"-" +
String(now.getDate()).padStart(2, "0");
let url =
"https://common.szjixun.cn/api/stock/history/base/list?from=2009-10-07&to=" +
toDate;
const res = await axios.get(url);
if (res.status === 200) {
if (res.data.status === 0) {
initEcharts(res.data.data);
}
}
};
// 适配倒序数据,返回大于等于目标日期的最近一天索引
function findClosestDateIndex(data, targetDateStr) {
let left = 0,
right = data.length - 1;
const target = new Date(targetDateStr).getTime();
let res = data.length - 1; // 默认返回最后一个
while (left <= right) {
const mid = Math.floor((left + right) / 2);
const midTime = new Date(data[mid].date).getTime();
if (midTime > target) {
left = mid + 1;
} else {
res = mid;
right = mid - 1;
}
}
return res;
}
// 适配倒序数据,返回小于等于目标日期的最近一天索引
function findClosestDateIndexDescLeft(data, targetDateStr) {
let left = 0,
right = data.length - 1;
const target = new Date(targetDateStr).getTime();
let res = -1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
const midTime = new Date(data[mid].date).getTime();
if (midTime > target) {
left = mid + 1; // mid 比目标新,往更旧的方向找
} else {
res = mid; // mid <= target记录下来继续往更新的方向找
right = mid - 1;
}
}
return res;
}
//点击切换搜索区间
const changeSearchRange = (range, dateTime) => {
state.activeRange = range;
const now = new Date();
let startDate = "";
let endDate = "";
if (range === "1m") {
const last = new Date(now);
last.setMonth(now.getMonth() - 1);
startDate = last.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "3m") {
const last = new Date(now);
last.setMonth(now.getMonth() - 3);
startDate = last.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "YTD") {
startDate = new Date(now.getFullYear(), 0, 1).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "1Y") {
const last = new Date(now);
last.setFullYear(now.getFullYear() - 1);
startDate = last.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "5Y") {
const last = new Date(now);
last.setFullYear(now.getFullYear() - 5);
startDate = last.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "10Y") {
const last = new Date(now);
last.setFullYear(now.getFullYear() - 10);
startDate = last.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
endDate = new Date(new Date()).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} else if (range === "Max") {
startDate = "";
endDate = "";
} else if (range === "startDateTime") {
startDate = dateTime;
endDate = "";
} else if (range === "endDateTime") {
startDate = "";
endDate = dateTime;
}
if (startDate || endDate) {
// historicData 和 xAxisData 需在 initEcharts 作用域可用
if (
typeof historicData !== "undefined" &&
typeof xAxisData !== "undefined"
) {
const zoomOptions = {};
let newStartTs = state.dateRange[0];
let newEndTs = state.dateRange[1];
if (startDate) {
const idx = findClosestDateIndex(historicData, startDate);
const startValue = new Date(historicData[idx].date).toLocaleDateString(
"en-US",
{
month: "short",
day: "numeric",
year: "numeric",
}
);
zoomOptions.endValue = startValue;
newStartTs = new Date(startValue).getTime();
}
if (endDate) {
const idx = findClosestDateIndexDescLeft(historicData, endDate);
const endValue = new Date(historicData[idx].date).toLocaleDateString(
"en-US",
{
month: "short",
day: "numeric",
year: "numeric",
}
);
zoomOptions.startValue = endValue;
newEndTs = new Date(endValue).getTime();
}
if (Object.keys(zoomOptions).length > 0) {
myCharts.setOption({ dataZoom: [zoomOptions, zoomOptions] });
}
state.dateRange = [newStartTs, newEndTs];
}
} else {
myCharts.setOption({
dataZoom: {
startValue: "",
endValue: "",
},
});
state.dateRange = [new Date("2009-10-07").getTime(), new Date().getTime()];
}
};
// 禁用日期
const isDateDisabled = (ts, type, range) => {
const minDate = new Date("2009-10-06").getTime();
const maxDate = new Date().getTime();
if (ts < minDate || ts > maxDate) {
return true;
}
if (type === "end" && range && range[0]) {
return ts < range[0];
}
return false;
};
// 切换搜索区间
const handleDateRangeChange = (range) => {
if (range && range[0] && range[1]) {
const startDate = new Date(range[0]).toLocaleDateString(locale.value, {
month: "short",
day: "numeric",
year: "numeric",
});
const endDate = new Date(range[1]).toLocaleDateString(locale.value, {
month: "short",
day: "numeric",
year: "numeric",
});
changeSearchRange("startDateTime", startDate);
changeSearchRange("endDateTime", endDate);
}
};
</script>
<style lang="scss" scoped>
.custom-echarts {
.myChart {
width: 100%;
height: 25rem;
}
.echarts-header {
.title-section {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 32px;
padding: 0 16px;
}
.title-decoration {
width: 58px;
height: 7px;
background: #ff7bac;
margin: auto 0;
margin-top: 43px;
}
.stock-title {
font-family: "PingFang SC", sans-serif;
font-weight: 500;
font-size: 40px;
line-height: 1.4em;
letter-spacing: 3%;
color: #000000;
}
.echarts-search-area {
padding: 0 16px 0 16px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
.echarts-search-byRange {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
.search-range-list {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
.search-range-list-each {
padding: 5px 10px;
border-radius: 5px;
background-color: #f3f4f6;
cursor: pointer;
span {
font-weight: 600;
font-size: 0.9rem;
}
}
.activeRange {
color: #fff;
background: #ff7bac;
}
}
}
.echarts-search-byDate {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 0.4rem;
}
}
}
}
</style>