594 lines
16 KiB
Vue
594 lines
16 KiB
Vue
<template>
|
||
<div class="custom-echarts">
|
||
<div>
|
||
<div class="echarts-header">
|
||
<div class="echarts-header-title">
|
||
<span>FiEE, Inc. Stock Price History</span>
|
||
</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"
|
||
@click="changeSearchRange(item)"
|
||
>
|
||
<span>{{ item }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="echarts-search-byDate">
|
||
<n-date-picker
|
||
v-model:value="state.selectHistoricStartDate"
|
||
type="date"
|
||
:is-date-disabled="disableAfterDate"
|
||
@update:value="changeSearchRangeStartDate"
|
||
input-readonly
|
||
/>
|
||
<!-- <n-icon size="16">
|
||
<ArrowForwardOutline />
|
||
</n-icon> -->
|
||
<span>to</span>
|
||
<n-date-picker
|
||
v-model:value="state.selectHistoricEndDate"
|
||
type="date"
|
||
:is-date-disabled="disablePreviousDate"
|
||
@update:value="changeSearchRangeEndDate"
|
||
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 markPointerIcon from '@/assets/image/icon/echarts_markPointer.png'
|
||
import axios from 'axios'
|
||
import { NDatePicker, NIcon } from 'naive-ui'
|
||
import { ArrowForwardOutline } from '@vicons/ionicons5'
|
||
|
||
const state = reactive({
|
||
searchRange: ['1m', '3m', 'YTD', '1Y', '5Y', '10Y', 'Max'],
|
||
selectHistoricStartDate: '2009-10-07',
|
||
selectHistoricEndDate: new Date(),
|
||
})
|
||
|
||
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'))
|
||
// 绘制图表
|
||
myCharts.setOption({
|
||
// title: {
|
||
// text: 'FiEE, Inc. Stock Price History',
|
||
// },
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
axisPointer: {
|
||
type: 'line',
|
||
snap: true,
|
||
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>`
|
||
},
|
||
},
|
||
xAxis: {
|
||
data: xAxisData,
|
||
type: 'category',
|
||
boundaryGap: false,
|
||
inverse: true,
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: '#CCD6EB',
|
||
},
|
||
},
|
||
axisLabel: {
|
||
color: '#323232',
|
||
fontWeight: 'bold',
|
||
// formatter: function (value) {
|
||
// return value ? value.split('-')[0] : ''
|
||
// },
|
||
// interval: function (index, value) {
|
||
// if (index === 0) return true;
|
||
// const axisData = this && this.axis && this.axis.data ? this.axis.data : [];
|
||
// if (!axisData[index - 1]) return true;
|
||
// return value.split('-')[0] !== axisData[index - 1].split('-')[0];
|
||
// },
|
||
},
|
||
},
|
||
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: '#2c6288',
|
||
},
|
||
areaStyle: {
|
||
color: {
|
||
type: 'linear',
|
||
x: 0,
|
||
y: 0,
|
||
x2: 0,
|
||
y2: 1,
|
||
colorStops: [
|
||
{
|
||
offset: 0,
|
||
color: '#2c6288',
|
||
},
|
||
{
|
||
offset: 1,
|
||
color: '#F4F6F8',
|
||
},
|
||
],
|
||
},
|
||
},
|
||
markPoint: {
|
||
symbol: 'image://' + markPointerIcon,
|
||
symbolSize: 24,
|
||
data: [],
|
||
},
|
||
},
|
||
],
|
||
|
||
dataZoom: [
|
||
{
|
||
type: 'inside',
|
||
},
|
||
{
|
||
type: 'slider',
|
||
show: true,
|
||
dataBackground: {
|
||
lineStyle: {
|
||
color: '#2C6288',
|
||
},
|
||
areaStyle: {
|
||
color: {
|
||
type: 'linear',
|
||
x: 0,
|
||
y: 0,
|
||
x2: 0,
|
||
y2: 1,
|
||
colorStops: [
|
||
{ offset: 1, color: '#2c6288' },
|
||
{ offset: 0, color: '#F4F6F8' },
|
||
],
|
||
},
|
||
},
|
||
},
|
||
selectedDataBackground: {
|
||
lineStyle: {
|
||
color: '#2C6288',
|
||
},
|
||
areaStyle: {
|
||
color: {
|
||
type: 'linear',
|
||
x: 0,
|
||
y: 0,
|
||
x2: 0,
|
||
y2: 1,
|
||
colorStops: [
|
||
{ offset: 1, color: '#2c6288' },
|
||
{ 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: {
|
||
symbol: 'image://' + markPointerIcon,
|
||
symbolSize: 24,
|
||
data: [{ coord: [x, y] }],
|
||
},
|
||
},
|
||
],
|
||
})
|
||
}
|
||
})
|
||
// 鼠标移出时,清除 markPoint
|
||
myCharts.on('globalout', function () {
|
||
myCharts.setOption({
|
||
series: [
|
||
{
|
||
markPoint: {
|
||
symbol: 'image://' + markPointerIcon,
|
||
symbolSize: 24,
|
||
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.selectHistoricStartDate = new Date(startValue)
|
||
state.selectHistoricEndDate = new Date(endValue)
|
||
})
|
||
}
|
||
|
||
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 // 默认返回-1(找不到)
|
||
while (left <= right) {
|
||
const mid = Math.floor((left + right) / 2)
|
||
const midTime = new Date(data[mid].date).getTime()
|
||
if (midTime < target) {
|
||
right = mid - 1 // 向左搜索,因为我们要找的是小于等于目标日期的最近一天
|
||
} else {
|
||
res = mid // 记录当前找到的索引
|
||
left = mid + 1 // 向右搜索,因为更早的日期在数组后面
|
||
}
|
||
}
|
||
return res
|
||
}
|
||
|
||
//点击切换搜索区间
|
||
const changeSearchRange = (range, dateTime) => {
|
||
const now = new Date()
|
||
let startDate = ''
|
||
let endDate = ''
|
||
if (range === '1m') {
|
||
const last = new Date(now)
|
||
last.setMonth(now.getMonth() - 1)
|
||
startDate = last.toLocaleDateString('en-US', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: 'numeric',
|
||
})
|
||
endDate = new Date(new Date()).toLocaleDateString('en-US', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: 'numeric',
|
||
})
|
||
} else if (range === '3m') {
|
||
const last = new Date(now)
|
||
last.setMonth(now.getMonth() - 3)
|
||
startDate = last.toLocaleDateString('en-US', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: 'numeric',
|
||
})
|
||
endDate = new Date(new Date()).toLocaleDateString('en-US', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: 'numeric',
|
||
})
|
||
} else if (range === 'YTD') {
|
||
startDate = new Date(now.getFullYear(), 0, 1).toLocaleDateString('en-US', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: 'numeric',
|
||
})
|
||
endDate = new Date(new Date()).toLocaleDateString('en-US', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: 'numeric',
|
||
})
|
||
} else if (range === '1Y') {
|
||
const last = new Date(now)
|
||
last.setFullYear(now.getFullYear() - 1)
|
||
startDate = last.toLocaleDateString('en-US', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: 'numeric',
|
||
})
|
||
endDate = new Date(new Date()).toLocaleDateString('en-US', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: 'numeric',
|
||
})
|
||
} else if (range === '5Y') {
|
||
const last = new Date(now)
|
||
last.setFullYear(now.getFullYear() - 5)
|
||
startDate = last.toLocaleDateString('en-US', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: 'numeric',
|
||
})
|
||
endDate = new Date(new Date()).toLocaleDateString('en-US', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: 'numeric',
|
||
})
|
||
} else if (range === '10Y') {
|
||
const last = new Date(now)
|
||
last.setFullYear(now.getFullYear() - 10)
|
||
startDate = last.toLocaleDateString('en-US', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: 'numeric',
|
||
})
|
||
endDate = new Date(new Date()).toLocaleDateString('en-US', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: 'numeric',
|
||
})
|
||
} else if (range === 'Max') {
|
||
startDate = ''
|
||
endDate = ''
|
||
} else if (range === 'startDateTime') {
|
||
startDate = dateTime
|
||
endDate = ''
|
||
} else if (range === 'endDateTime') {
|
||
startDate = ''
|
||
endDate = dateTime
|
||
}
|
||
if (startDate || endDate) {
|
||
// historicData 和 xAxisData 需在 initEcharts 作用域可用
|
||
if (
|
||
typeof historicData !== 'undefined' &&
|
||
typeof xAxisData !== 'undefined'
|
||
) {
|
||
let startValue = xAxisData[0]
|
||
if (startDate) {
|
||
const idx = findClosestDateIndex(historicData, startDate)
|
||
// 用 historicData[idx].date 格式化为 xAxisData 的格式
|
||
startValue = new Date(historicData[idx].date).toLocaleDateString(
|
||
'en-US',
|
||
{
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: 'numeric',
|
||
},
|
||
)
|
||
}
|
||
let endValue = endDate
|
||
if (endDate) {
|
||
console.warn(endDate)
|
||
const idx = findClosestDateIndexDescLeft(historicData, endDate)
|
||
console.warn(idx)
|
||
// 用 historicData[idx].date 格式化为 xAxisData 的格式
|
||
endValue = new Date(historicData[idx].date).toLocaleDateString(
|
||
'en-US',
|
||
{
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: 'numeric',
|
||
},
|
||
)
|
||
console.warn(endValue)
|
||
}
|
||
|
||
if (startDate) {
|
||
myCharts.setOption({
|
||
dataZoom: {
|
||
endValue: startValue,
|
||
},
|
||
})
|
||
state.selectHistoricStartDate = new Date(startValue)
|
||
}
|
||
if (endDate) {
|
||
myCharts.setOption({
|
||
dataZoom: {
|
||
startValue: endValue,
|
||
},
|
||
})
|
||
state.selectHistoricEndDate = new Date(endValue)
|
||
}
|
||
}
|
||
} else {
|
||
myCharts.setOption({
|
||
dataZoom: {
|
||
startValue: '',
|
||
endValue: '',
|
||
},
|
||
})
|
||
|
||
state.selectHistoricStartDate = new Date('2009-10-07')
|
||
state.selectHistoricEndDate = new Date()
|
||
}
|
||
}
|
||
|
||
// 禁用2009-10-07之后的日期
|
||
const disableAfterDate = (date) => {
|
||
return date < new Date('2009-10-06') || date > new Date()
|
||
}
|
||
|
||
// 禁用过去的日期
|
||
const disablePreviousDate = (date) => {
|
||
return date < new Date(state.selectHistoricStartDate) || date > new Date()
|
||
}
|
||
|
||
// 切换搜索区间开始日期
|
||
const changeSearchRangeStartDate = (date) => {
|
||
console.error(date)
|
||
changeSearchRange(
|
||
'startDateTime',
|
||
new Date(date).toLocaleDateString('en-US', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: 'numeric',
|
||
}),
|
||
)
|
||
}
|
||
|
||
// 切换搜索区间结束日期
|
||
const changeSearchRangeEndDate = (date) => {
|
||
console.error(date)
|
||
changeSearchRange(
|
||
'endDateTime',
|
||
new Date(date).toLocaleDateString('en-US', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: 'numeric',
|
||
}),
|
||
)
|
||
}
|
||
</script>
|
||
<style lang="scss" scoped>
|
||
.custom-echarts {
|
||
.myChart {
|
||
width: 100%;
|
||
height: 25rem;
|
||
}
|
||
|
||
.echarts-header {
|
||
.echarts-header-title {
|
||
span {
|
||
font-size: 2rem;
|
||
font-weight: 600;
|
||
color: #323232;
|
||
}
|
||
}
|
||
.echarts-search-area {
|
||
padding: 2rem 0 0;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
|
||
.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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.echarts-search-byDate {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
gap: 0.4rem;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|