Files
stock-h5/src/views/target/fred/fred-detail.vue
2026-03-19 15:02:23 +08:00

363 lines
9.5 KiB
Vue

<template>
<div class="page-div">
<back-button />
<div class="header">
<h1 class="title">{{ fredCode }}</h1>
<p class="subtitle">{{ fredName }}</p>
</div>
<div class="chart-header">
<div class="time-range-buttons">
<van-button
v-for="range in timeRanges"
:key="range.value"
:plain="selectedTimeRange !== range.value"
:hairline="selectedTimeRange !== range.value"
:type="selectedTimeRange === range.value ? 'primary' : 'default'"
@click="setTimeRange(range.value)"
style="margin-left: 8px;"
>{{ range.label }}</van-button>
</div>
</div>
<div class="info-card">
<div class="chart-container">
<van-loading v-show="loading" type="spinner" color="#1989fa" class="loading" />
<div :id="`${fredCode}_chart`" class="chartDiv"></div>
</div>
</div>
<div class="chartItem d-flex align-items-center justify-content-center">
</div>
<div class="listDiv">
<div class="data-list-container" v-if="!loading">
<div v-if="list.length > 0">
<div class="list-header">
<div class="list-item">
<div class="list-col date-col">日期</div>
<template v-if="route.params.id">
<div class="list-col value-col">(%)</div>
</template>
<template v-else>
<div class="list-col aaa-col">AAA(%)</div>
<div class="list-col bbb-col">BBB(%)</div>
<div class="list-col spread-col">BBB-AAA(%)</div>
</template>
</div>
</div>
<RecycleScroller
class="list-body"
:items="list"
:item-size="1"
key-field="sysDate"
>
<template v-slot="{ item }">
<div class="list-item">
<div class="list-col date-col">{{ item.sysDate }}</div>
<template v-if="route.params.id">
<div class="list-col value-col">{{ item.fredValue }}</div>
</template>
<template v-else>
<div class="list-col aaa-col">{{ item.AaaFredValue }}</div>
<div class="list-col bbb-col">{{ item.BbbFredValue }}</div>
<div class="list-col spread-col">{{ (item.BbbFredValue - item.AaaFredValue).toFixed(2) }}</div>
</template>
</div>
</template>
</RecycleScroller>
</div>
<div class="no-data" v-else>暂无数据</div>
</div>
</div>
</div>
</template>
<script setup lang='ts'>
import BackButton from '@/components/back-button.vue'
import { ref, nextTick, onMounted } from 'vue'
import { RecycleScroller } from 'vue-virtual-scroller'
import { useRoute } from 'vue-router'
import { fredInfoInfo, fredDetailQueryFred, fredDetailQueryAaaAndBbb } from '@/utils/api'
let fredCode = ref('')
let fredName = ref('')
let beginTime = ref('')
let endTime = ref('')
let selectedTimeRange = ref('3Y')
const timeRanges = [
{ label: '1M', value: '1M', unit: 'month', offset: -1 },
{ label: '3M', value: '3M', unit: 'month', offset: -3 },
{ label: '1Y', value: '1Y', unit: 'year', offset: -1 },
{ label: '3Y', value: '3Y', unit: 'year', offset: -3 },
]
const route = useRoute()
const init = async () => {
const id = route.params.id
if (id) {
const { data } = await fredInfoInfo(id + '')
fredCode.value = data.fredCode
fredName.value = data.fredName
} else {
fredCode.value = 'BBB-AAA'
fredName.value = 'Option-Adjusted Spread'
}
setTimeRange('3Y')
charts.value = [null]
}
import { addOrSubtractTime } from '@/utils'
const setTimeRange = (range: string) => {
selectedTimeRange.value = range
endTime.value = new Date().toISOString().split('T')[0]
const rangeConfig = timeRanges.find(item => item.value === range)
if (rangeConfig) {
beginTime.value = addOrSubtractTime(new Date(), rangeConfig.offset, rangeConfig.unit as 'month' | 'year')
}
disposeAll()
nextTick(() => {
const id = route.params.id
getData(id ? 1 : 0)
})
}
let loading = ref(true)
let list = ref<any[]>([])
import * as echarts from 'echarts'
import { chartMixins } from '@/mixins/chart-mixins'
const { charts, destroyedFlag, disposeAll } = chartMixins()
import { getLineSeries, getBaseOption, getXAxis, getYAxis } from '@/utils/chart'
const getData = async (index: number) => {
loading.value = true
const sysDate: any = []
let seriesList: any = []
let color = []
const legend = {
data: [] as string[],
top: '30px',
show: false
}
if (index === 0) {
const { data } = await fredDetailQueryAaaAndBbb({
begin: beginTime.value,
end: endTime.value
})
color = ['green', 'blue', 'red']
legend.show = true
legend.data = ['AAA', 'BBB', 'BBB-AAA']
seriesList = [getLineSeries({ name: 'AAA' }), getLineSeries({ name: 'BBB' }), getLineSeries({ name: 'BBB-AAA' })]
data.forEach((ele: any) => {
sysDate.push(ele.sysDate)
seriesList[0].data.push({ value: ele.AaaFredValue })
seriesList[1].data.push({ value: ele.BbbFredValue })
seriesList[2].data.push({ value: Math.round((ele.BbbFredValue - ele.AaaFredValue) * 100) / 100 })
list.value.unshift(ele)
})
} else{
const { data } = await fredDetailQueryFred({
fredCode: fredCode.value,
begin: beginTime.value,
end: endTime.value
})
color = ['blue']
seriesList = [getLineSeries({ name: fredCode.value })]
data.forEach((ele: any) => {
sysDate.push(ele.sysDate)
seriesList[0].data.push({ value: ele.fredValue })
list.value.unshift(ele)
})
}
if (!destroyedFlag.value) {
let chart = echarts.init(document.getElementById(`${fredCode.value}_chart`))
const option = getBaseOption({
series: seriesList,
yAxis: [getYAxis({ axisLabel: '{value} %' })],
xAxis: [getXAxis({ data: sysDate })],
tooltipFormatter: function (params: any) {
const data = Array.isArray(params) ? params : [params]
let backText = `日期:${data[0].name}<br/>`
data.forEach((ele) => {
backText += `<span style="color: ${ele.color}">${ele.seriesName}</span>:<span class="f-b">${ele.value}%</span><br/>`
})
return backText
},
grid: [{ top: '10px', left: '40px', right: '40px', bottom: '20px' }],
legend: legend,
color: color
})
chart.setOption(option)
charts.value[0] = chart
loading.value = false
}
}
onMounted (() =>{
init()
})
</script>
<style lang='scss' scoped>
@import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
.page-div {
box-sizing: border-box;
overflow-y: auto;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
padding-top: 102px;
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
@media screen and (min-width: 992px) {
left: calc(50vw - 496px);
right: calc(50vw - 496px);
}
}
.chart-header {
display: flex;
flex-direction: column;
align-items: flex-end;
width: 100%;
margin-bottom: 16px;
.chart-title {
display: none;
}
.time-range-buttons {
display: flex;
justify-content: flex-end;
padding: 0 16px;
flex-wrap: wrap;
gap: 8px;
}
}
.chart-container {
width: 100%;
height: 30vh;
@media screen and (min-width: 768px) {
height: 40vh;
}
}
.listDiv {
padding: 0 16px;
.data-list-container {
width: 100%;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
background-color: #fff;
max-height: none !important;
overflow: visible !important;
height: auto !important;
-webkit-overflow-scrolling: auto;
.list-header {
background-color: #f5f7fa;
border-bottom: 1px solid #e8ebed;
}
.list-item {
display: flex;
align-items: center;
padding: 8px 16px;
border-bottom: 1px solid #e8ebed;
height: 40px;
box-sizing: border-box;
&:last-child {
border-bottom: none;
}
}
.list-body {
overflow: visible !important;
max-height: none !important;
height: auto !important;
}
.list-col {
flex: 1;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 0 8px;
font-size: 14px;
color: #333;
&.date-col {
flex: 0 0 30%;
text-align: left;
font-weight: 500;
}
&.value-col,
&.aaa-col,
&.bbb-col,
&.spread-col {
font-family: monospace;
}
&.spread-col {
color: #1989fa;
font-weight: 500;
}
}
.no-data {
padding: 30px 0;
text-align: center;
color: #999;
background-color: #fafafa;
}
}
}
@media screen and (max-width: 768px) {
.chart-header {
margin-bottom: 12px;
.time-range-buttons {
padding: 0 12px;
button {
font-size: 12px !important;
padding: 4px 8px !important;
}
}
}
.chart-container {
height: 25vh;
}
.listDiv {
padding: 0 12px;
.data-list-container {
.list-item {
padding: 10px 12px;
height: 44px;
}
.list-col {
font-size: 12px;
padding: 0 4px;
&.date-col {
flex: 0 0 35%;
}
}
}
}
}
}
</style>