251 lines
6.1 KiB
Vue
251 lines
6.1 KiB
Vue
<template>
|
|
<div class="chart-panel card">
|
|
<el-tabs v-model="period" class="tabs">
|
|
<el-tab-pane label="分时" name="intraday" />
|
|
<el-tab-pane label="五日" name="five" />
|
|
<el-tab-pane label="日K" name="day" />
|
|
<el-tab-pane label="周K" name="week" />
|
|
<el-tab-pane label="月K" name="month" />
|
|
</el-tabs>
|
|
<v-chart class="chart" :option="option" autoresize />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref } from 'vue'
|
|
import { use } from 'echarts/core'
|
|
import { CanvasRenderer } from 'echarts/renderers'
|
|
import { LineChart, BarChart, CandlestickChart } from 'echarts/charts'
|
|
import {
|
|
GridComponent,
|
|
TooltipComponent,
|
|
DataZoomComponent,
|
|
LegendComponent,
|
|
} from 'echarts/components'
|
|
import type { EChartsOption } from 'echarts'
|
|
import VChart from 'vue-echarts'
|
|
import type { Candle, QuoteData } from '../../types'
|
|
|
|
use([
|
|
CanvasRenderer,
|
|
LineChart,
|
|
BarChart,
|
|
CandlestickChart,
|
|
GridComponent,
|
|
TooltipComponent,
|
|
DataZoomComponent,
|
|
LegendComponent,
|
|
])
|
|
|
|
type ChartPeriod = 'intraday' | 'five' | 'day' | 'week' | 'month'
|
|
|
|
interface ColorParam {
|
|
dataIndex?: number
|
|
}
|
|
|
|
const props = defineProps<{
|
|
quote: QuoteData
|
|
}>()
|
|
|
|
const period = ref<ChartPeriod>('intraday')
|
|
|
|
const option = computed((): EChartsOption => {
|
|
if (period.value === 'intraday' || period.value === 'five') {
|
|
return buildIntradayOption(props.quote, period.value === 'five')
|
|
}
|
|
const key = period.value === 'day' ? 'day' : period.value === 'week' ? 'week' : 'month'
|
|
return buildCandleOption(props.quote.candles[key])
|
|
})
|
|
|
|
function buildIntradayOption(quote: QuoteData, fiveDay: boolean): EChartsOption {
|
|
const points = fiveDay
|
|
? [...quote.intraday, ...quote.intraday.map((p) => ({ ...p, price: p.price + 1.5 }))]
|
|
: quote.intraday
|
|
const times = points.map((p) => p.time)
|
|
const prices = points.map((p) => p.price)
|
|
const avgs = points.map((p) => p.avg)
|
|
const vols = points.map((p) => p.volume)
|
|
const base = quote.prevSettlement
|
|
|
|
return {
|
|
animation: false,
|
|
legend: { data: ['分时', '均价'], top: 0, textStyle: { fontSize: 11 } },
|
|
tooltip: { trigger: 'axis' },
|
|
axisPointer: { link: [{ xAxisIndex: 'all' }] },
|
|
grid: [
|
|
{ left: 48, right: 48, top: 28, height: '58%' },
|
|
{ left: 48, right: 48, top: '78%', height: '14%' },
|
|
],
|
|
xAxis: [
|
|
{
|
|
type: 'category',
|
|
data: times,
|
|
boundaryGap: false,
|
|
axisLabel: { fontSize: 10 },
|
|
gridIndex: 0,
|
|
},
|
|
{
|
|
type: 'category',
|
|
data: times,
|
|
boundaryGap: false,
|
|
axisLabel: { show: false },
|
|
gridIndex: 1,
|
|
},
|
|
],
|
|
yAxis: [
|
|
{
|
|
type: 'value',
|
|
scale: true,
|
|
axisLabel: { fontSize: 10 },
|
|
splitLine: { lineStyle: { type: 'dashed', color: '#eee' } },
|
|
gridIndex: 0,
|
|
},
|
|
{
|
|
type: 'value',
|
|
scale: true,
|
|
position: 'right',
|
|
axisLabel: {
|
|
fontSize: 10,
|
|
formatter: (v: number) => `${(((v - base) / base) * 100).toFixed(2)}%`,
|
|
},
|
|
splitLine: { show: false },
|
|
gridIndex: 0,
|
|
},
|
|
{
|
|
type: 'value',
|
|
axisLabel: { show: false },
|
|
splitLine: { show: false },
|
|
gridIndex: 1,
|
|
},
|
|
],
|
|
dataZoom: [{ type: 'inside', xAxisIndex: [0, 1] }],
|
|
series: [
|
|
{
|
|
name: '分时',
|
|
type: 'line',
|
|
data: prices,
|
|
showSymbol: false,
|
|
lineStyle: { width: 1.5, color: '#1677ff' },
|
|
areaStyle: {
|
|
color: {
|
|
type: 'linear',
|
|
x: 0,
|
|
y: 0,
|
|
x2: 0,
|
|
y2: 1,
|
|
colorStops: [
|
|
{ offset: 0, color: 'rgba(22,119,255,0.25)' },
|
|
{ offset: 1, color: 'rgba(22,119,255,0.02)' },
|
|
],
|
|
},
|
|
},
|
|
xAxisIndex: 0,
|
|
yAxisIndex: 0,
|
|
},
|
|
{
|
|
name: '均价',
|
|
type: 'line',
|
|
data: avgs,
|
|
showSymbol: false,
|
|
lineStyle: { width: 1, color: '#fa8c16' },
|
|
xAxisIndex: 0,
|
|
yAxisIndex: 0,
|
|
},
|
|
{
|
|
name: '成交量',
|
|
type: 'bar',
|
|
data: vols,
|
|
itemStyle: {
|
|
color: (p: ColorParam) => {
|
|
const idx = p.dataIndex ?? 0
|
|
return prices[idx] >= (prices[idx - 1] ?? prices[idx]) ? '#e54545' : '#12b37b'
|
|
},
|
|
},
|
|
xAxisIndex: 1,
|
|
yAxisIndex: 2,
|
|
},
|
|
],
|
|
}
|
|
}
|
|
|
|
function buildCandleOption(candles: Candle[]): EChartsOption {
|
|
const dates = candles.map((c) => c.date)
|
|
const ohlc = candles.map((c) => [c.open, c.close, c.low, c.high])
|
|
const vols = candles.map((c) => c.volume)
|
|
|
|
return {
|
|
animation: false,
|
|
tooltip: { trigger: 'axis' },
|
|
grid: [
|
|
{ left: 48, right: 16, top: 16, height: '58%' },
|
|
{ left: 48, right: 16, top: '78%', height: '14%' },
|
|
],
|
|
xAxis: [
|
|
{ type: 'category', data: dates, axisLabel: { fontSize: 10 }, gridIndex: 0 },
|
|
{
|
|
type: 'category',
|
|
data: dates,
|
|
axisLabel: { show: false },
|
|
gridIndex: 1,
|
|
},
|
|
],
|
|
yAxis: [
|
|
{
|
|
scale: true,
|
|
axisLabel: { fontSize: 10 },
|
|
splitLine: { lineStyle: { type: 'dashed', color: '#eee' } },
|
|
gridIndex: 0,
|
|
},
|
|
{
|
|
scale: true,
|
|
axisLabel: { show: false },
|
|
splitLine: { show: false },
|
|
gridIndex: 1,
|
|
},
|
|
],
|
|
dataZoom: [{ type: 'inside', xAxisIndex: [0, 1] }],
|
|
series: [
|
|
{
|
|
type: 'candlestick',
|
|
data: ohlc,
|
|
itemStyle: {
|
|
color: '#e54545',
|
|
color0: '#12b37b',
|
|
borderColor: '#e54545',
|
|
borderColor0: '#12b37b',
|
|
},
|
|
xAxisIndex: 0,
|
|
yAxisIndex: 0,
|
|
},
|
|
{
|
|
type: 'bar',
|
|
data: vols,
|
|
itemStyle: {
|
|
color: (p: ColorParam) => {
|
|
const d = ohlc[p.dataIndex ?? 0]
|
|
return d[1] >= d[0] ? '#e54545' : '#12b37b'
|
|
},
|
|
},
|
|
xAxisIndex: 1,
|
|
yAxisIndex: 1,
|
|
},
|
|
],
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.chart-panel {
|
|
padding: 8px 12px 12px;
|
|
}
|
|
|
|
.tabs :deep(.el-tabs__header) {
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.chart {
|
|
height: 420px;
|
|
width: 100%;
|
|
}
|
|
</style>
|