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

770 lines
24 KiB
Vue

<template>
<div class="page-div">
<back-button />
<div class="header">
<div class="header-content">
<h1 class="title">{{ fundData?.fundName }}</h1>
</div>
</div>
<div class="info-card">
<div class="d-flex justify-content-between align-items-center">
<div class="f-b">相关文件</div>
<div v-if="fileList.length > 0" @click="showFileList" class="blue-color">
查看详情
</div>
<div v-else class="red-color">暂无数据</div>
</div>
</div>
<div class="info-card" v-if="chartList.length > 0">
<div class="chart-container">
<van-loading v-show="chartList[0].loading" type="spinner" color="#1989fa" class="loading" />
<div :id="`${chartList[0].id}`" class="chartDiv"></div>
</div>
</div>
<div class="info-card" v-if="chartList.length > 1">
<div class="chart-container">
<van-loading v-show="chartList[1].loading" type="spinner" color="#1989fa" class="loading" />
<div :id="`${chartList[1].id}`" class="chartDiv"></div>
</div>
</div>
<div class="listDiv">
<div class="data-list-container">
<div v-if="tableList.length > 0">
<div class="list-header">
<div class="list-item">
<div class="list-col date-col">日期</div>
<div class="list-col value-col">净值</div>
<div class="list-col value-col">月涨幅</div>
<div class="list-col value-col">今年以来收益</div>
</div>
</div>
<RecycleScroller
class="list-body"
:items="tableList"
:item-size="1"
key-field="realSysDate"
>
<template v-slot="{ item }">
<div class="list-item">
<div class="list-col date-col" :class="{ 'f-b' : item.boldFlag }">{{ item.realSysDate }}</div>
<div class="list-col value-col">
<div v-if="item.budget" class="tag-div"><van-tag type="success" plain>预测</van-tag></div>
<span :class="{ 'f-b' : item.boldFlag, 'budgetHeight': item.budget }">{{ item.nav }}</span>
</div>
<div class="list-col value-col">
<div v-if="item.budget" class="tag-div"><van-tag type="success" plain>预测</van-tag></div>
<span :class="{
'f-b' : item.boldFlag,
'budgetHeight': item.budget,
'red-color': item.navPercent > 0,
'green-color' : item.navPercent < 0
}">{{ item.navPercent || 0 }}%</span>
</div>
<div class="list-col value-col">
<div v-if="item.budget" class="tag-div"><van-tag type="success" plain>预测</van-tag></div>
<span :class="{
'f-b' : item.boldFlag,
'budgetHeight': item.budget,
'red-color': item.navPercent > 0,
'green-color' : item.navPercent < 0
}">{{ item.yearIncomeRate || 0 }}%</span>
</div>
</div>
</template>
</RecycleScroller>
</div>
<div class="no-data" v-else>暂无数据</div>
</div>
</div>
</div>
<van-popup v-model:show="showFileListPopup" position="bottom" :style="{ height: '70%' }">
<div class="p-16">
<div class="d-flex justify-content-between items-center mb-16">
<div class="sub-title f-b">文件列表</div>
<van-icon name="close" @click="showFileListPopup = false" />
</div>
<div class="file-list">
<div v-for="(file, index) in fileList" :key="index" class="file-item">
<a :href="file.fileUrl" target="_blank" class="file-link">
<i class="iconfont" :class="`icon-${file.type}`"></i>
<span class="file-title van-ellipsis">{{ file.fileName }}</span>
</a>
</div>
</div>
</div>
</van-popup>
</template>
<script setup lang='ts'>
import BackButton from '@/components/back-button.vue'
import { ref, onMounted, nextTick } from 'vue'
import { RecycleScroller } from 'vue-virtual-scroller'
import { useRoute } from 'vue-router'
const route = useRoute()
import { stockAFundInfo, stockAFundFileList, stockAFundHisFundHisList, stockAFundListFund } from '@/utils/api'
const fundData: any = ref(null)
const fileList = ref<any[]>([])
import { padZeroAfterDecimal } from '@/utils'
const init = async () => {
try {
const data: any = await stockAFundInfo(route.params.id + '')
fundData.value = data.data
const { data: file } = await stockAFundFileList({ fundId: route.params.id + '', curPage: 1, limit: 999 })
const reg1 = /(\.xls|\.xlsx)$/
const reg2 = /(\.pdf)$/
const reg3 = /(\.doc|\.docx)$/
const reg4 = /(\.ppt|\.pptx)$/
file.list.map((ele: any) => {
if (ele.fileUrl) {
ele.type = 'lianjie'
} else {
if (ele.fileId) {
if (reg1.test(ele.fileName)) {
ele.type = 'excel'
}
if (reg2.test(ele.fileName)) {
ele.type = 'pdf'
}
if (reg3.test(ele.fileName)) {
ele.type = 'word'
}
if (reg4.test(ele.fileName)) {
ele.type = 'ppt'
}
}
ele.fileUrl = `${import.meta.env.VITE_BASE_URL}/file/${ele.fileId}`
}
})
fileList.value = file.list
const { data: nav } = await stockAFundHisFundHisList({ fundId: route.params.id + '' })
nav.map((ele: any) => {
ele.selectId = ele.id
ele.nav = ele.nav ? padZeroAfterDecimal(ele.nav) : ''
const month = ele.sysDate.slice(5, 7)
ele.boldFlag = false
if (month === '12') {
ele.boldFlag = true
}
ele.budget = false
ele.realSysDate = ele.sysDate
ele.holdingList = []
})
const info2 = data.preData && data.preData.sysDate ? data.preData : null
if (info2 && nav.length > 0) {
let sysDate = ''
let fundId = ''
nav.forEach((ele: any) => {
if (ele.holdingNum > 0) {
sysDate = ele.sysDate
fundId = ele.fundId
}
})
const month = info2.sysDate.slice(5, 7)
nav.push({
sysDate: sysDate,
fundId: fundId,
realSysDate: info2.sysDate,
nav: info2.price ? padZeroAfterDecimal(info2.price) : '',
navPercent: info2.percent,
yearIncomeRate: info2.yearIncomeRate,
holdingNum: info2.holdingList.length,
boldFlag: month === '12',
budget: true,
holdingList: info2.holdingList
})
}
setLineChart(nav, data.fundConfig)
} catch (error) {
console.error('初始化数据失败:', error)
}
}
let chartList = ref<any[]>([])
import { chartMixins } from '@/mixins/chart-mixins'
const { charts, destroyedFlag } = chartMixins()
import * as echarts from 'echarts'
import { getLineSeries, getBaseOption, getXAxis, getYAxis } from '@/utils/chart'
const tableList = ref<any[]>([])
const setLineChart = (nav: any, fundConfig: any) => {
if (fundData.value.configFlag === 1 && fundData.value.fundType === 'fund' && fundConfig.length > 0) {
charts.value = [null, null]
chartList.value = [{
id: 'nav_chart',
loading: true,
}, {
id: 'nav_list_chart',
loading: true,
}]
} else {
charts.value = [null]
chartList.value = [{
id: 'nav_chart',
loading: true,
}]
}
const list = nav.sort(function (a: any, b: any) {
return new Date(a.realSysDate).getTime() - new Date(b.realSysDate).getTime()
})
const drawdowns: any = []
let maxDrawdown = 0
const tabledata: any = []
for (let j = 0; j < list.length; j++) {
tabledata.unshift(list[j])
let drawdown = 0
const nav = Number(list[j].nav)
if (nav > maxDrawdown) {
maxDrawdown = nav
drawdown = 0
} else {
drawdown = Math.round((nav - maxDrawdown) / maxDrawdown * 10000) / 100
}
drawdowns.push(drawdown)
}
const sysDate: any = []
const seriesData: any = []
const drawdownsData: any = []
list.forEach((ele: any, index: number) => {
sysDate.push(ele.realSysDate)
seriesData.push({ value: ele.nav, drawdown: drawdowns[index] })
drawdownsData.push({ value: drawdowns[index], nav: ele.nav, sysDate: ele.realSysDate })
})
const series = [
getLineSeries({ data: seriesData, name: '历史净值', areaStyle: { color: '#FAE5E3' }, color: '#FF3732' }),
getLineSeries({ data: drawdownsData, yAxisIndex: 1, xAxisIndex: 1, name: '回撤走势', areaStyle: { color: '#FAE5E3' }, color: '#FF3732' })
]
if (drawdownsData.length > 3) {
const minTime = new Date(drawdownsData[0].sysDate).getTime()
const maxTime = new Date(drawdownsData[drawdownsData.length - 1].sysDate).getTime()
const range = Math.round((maxTime - minTime) / 6)
const min1 = getPoint(drawdownsData)
const left = []
const right = []
for (let i = 0; i < drawdownsData.length; i++) {
if (new Date(min1.sysDate).getTime() - new Date(drawdownsData[i].sysDate).getTime() - range > 0) {
left.push(drawdownsData[i])
}
if (new Date(drawdownsData[i].sysDate).getTime() - range - new Date(min1.sysDate).getTime() > 0) {
right.push(drawdownsData[i])
}
}
let min2: any = {}
let min3: any = {}
if (left.length > 0 && right.length > 0) {
min2 = getPoint(left)
min3 = getPoint(right)
} else if (left.length > 0 && right.length === 0) {
min2 = getPoint(left)
const left1 = []
const right1 = []
for (let i = 0; i < left.length; i++) {
if (new Date(min2.sysDate).getTime() - new Date(left[i].sysDate).getTime() - range > 0) {
left1.push(left[i])
}
if (new Date(left[i].sysDate).getTime() - range - new Date(min2.sysDate).getTime() > 0) {
right1.push(left[i])
}
}
min3 = getPoint([...left1, ...right1])
} else if (left.length === 0 && right.length > 0) {
min2 = getPoint(right)
const left2 = []
const right2 = []
for (let i = 0; i < right.length; i++) {
if (new Date(min2.sysDate).getTime() - new Date(right[i].sysDate).getTime() - range > 0) {
left2.push(right[i])
}
if (new Date(right[i].sysDate).getTime() - range - new Date(min2.sysDate).getTime() > 0) {
right2.push(right[i])
}
}
min3 = getPoint([...left2, ...right2])
}
series[1].markPoint = {
data: [{
name: 1,
value: min1.num,
coord: [min1.sysDate, min1.num],
itemStyle: {
color: 'rgba(0,0,0,0)'
},
label: {
position: 'bottom',
color: '#f58220'
}
}, {
name: 2,
value: min2.num,
coord: [min2.sysDate, min2.num],
itemStyle: {
color: 'rgba(0,0,0,0)'
},
label: {
position: 'bottom',
color: '#0000ff'
}
}, {
name: 3,
value: min3.num,
coord: [min3.sysDate, min3.num],
itemStyle: {
color: 'rgba(0,0,0,0)'
},
label: {
position: 'bottom',
color: '#65c294'
}
}]
}
} else {
series[1].markPoint = {
data: drawdownsData.map((ele: any, i: number) => ({
name: `${i + 1}`,
value: ele.value,
coord: [sysDate[i], ele.value],
itemStyle: {
color: 'rgba(0,0,0,0)'
},
label: {
position: 'bottom',
color: i === 0 ? '#f58220' : i === 1 ? '#0000ff' : '#65c294'
}
}))
}
}
const option = getBaseOption({
yAxis: [getYAxis({ }), getYAxis({ gridIndex: 1, axisLabel: '{value}%' })],
xAxis: [getXAxis({ data: sysDate }), getXAxis({ data: sysDate, gridIndex: 1, showFlag: false })],
series: series,
title: [{ text: '历史净值' }, { text: '回撤情况', top: '54%' }],
grid: [{ bottom: '54%', left: '40px', right: '40px', top: '40px' }, { top: '66%', bottom: '20px', left: '40px', right: '40px' }],
formatter: function (data: any) {
let res = `${data[0].name}<br/>`
data.forEach((ele: any) => {
res += `历史净值:${ele.seriesName === '历史净值' ? ele.data.value : ele.data.nav}<br/>`
res += `回撤:${ele.seriesName === '历史净值' ? ele.data.drawdown : ele.data.value}%<br/>`
})
return res
}
})
nextTick(() =>{
if (!destroyedFlag.value && document.getElementById(chartList.value[0].id)) {
const chart = echarts.init(document.getElementById(chartList.value[0].id))
chart.setOption(option)
charts.value[0] = chart
chartList.value[0].loading = false
}
if (fundData.value.configFlag === 1 && fundData.value.fundType === 'fund' && fundConfig.length > 0) {
getFundList(fundConfig)
tableList.value = tabledata
} else {
tableList.value = tabledata
}
})
}
const getPoint = (drawdownsData: any) => {
const minValue = {
num: drawdownsData[0].value,
sysDate: drawdownsData[0].sysDate
}
for (let i = 1; i < drawdownsData.length; i++) {
if (drawdownsData[i].value < minValue.num) {
minValue.num = drawdownsData[i].value
minValue.sysDate = drawdownsData[i].sysDate
}
}
return minValue
}
const getFundList = async(fundConfig: any) => {
const { data } = await stockAFundListFund()
const multipleSelection: any = []
data.fundList.forEach((ele: any) => {
const fund = fundConfig.find((res: any) => {
return ele.fundId === res.configFundId
})
if (fund && fund.sysDate && fund.weight) {
multipleSelection.push({
weight: fund.weight,
sysDate: fund.sysDate,
stockPriceList: ele.stockPriceList,
fundName: ele.fundName,
fundId: ele.fundId
})
}
})
getChart(multipleSelection)
}
const getChart = (multipleSelection: any) => {
const colorPalette = generateColors(multipleSelection.length + 1)
const legend: any = []
const seriesList: any = []
const sysDate: any = []
multipleSelection.sort((a: any, b: any) => new Date(a.sysDate).getTime() - new Date(b.sysDate).getTime())
multipleSelection.forEach((ele: any, index: number) => {
legend.push(ele.fundName)
seriesList.push(getLineSeries({ name: ele.fundName, color: colorPalette[index] }))
seriesList.push(getLineSeries({ name: ele.fundName, yAxisIndex: 1, xAxisIndex: 1, color: colorPalette[index] }))
const priceIndex = ele.stockPriceList.findIndex((res: any) => {
return new Date(res.sysDate).getTime() >= new Date(ele.sysDate).getTime()
})
const stockPriceList = ele.stockPriceList.slice(priceIndex)
const per = stockPriceList.length > 0 ? stockPriceList[0].nav : 1
let maxDrawdown = 0
stockPriceList.forEach((res: any) => {
if (!sysDate.includes(res.sysDate)) {
sysDate.push(res.sysDate)
}
const nav = res.nav ? Math.round(res.nav / per * 10000) / 10000 : 0
let drawdown = 0
if (nav > maxDrawdown) {
maxDrawdown = nav
drawdown = 0
} else {
drawdown = Math.round((nav - maxDrawdown) / maxDrawdown * 10000) / 100
}
seriesList[index * 2].data.push([res.sysDate, res.nav, drawdown])
seriesList[index * 2 + 1].data.push([res.sysDate, drawdown, res.nav])
})
})
sysDate.sort((a: any, b: any) => new Date(a).getTime() - new Date(b).getTime())
const moneyList: any = []
const navList: any = []
sysDate.forEach((ele: any) => {
const countList: any = []
seriesList.forEach((res: any, index: number) => {
if (index % 2 === 0) {
if (new Date(multipleSelection[index / 2].sysDate).getTime() <= new Date(ele).getTime()) {
const navItem = res.data.find((item: any) => {
return item[0] === ele
})
if (navItem) {
countList.push({
weight: Number(multipleSelection[index / 2].weight),
nav: navItem[1],
marketValue: 0,
fundId: multipleSelection[index / 2].fundId
})
} else {
countList.push({
weight: Number(multipleSelection[index / 2].weight),
nav: 0,
marketValue: 0,
fundId: multipleSelection[index / 2].fundId
})
}
}
}
})
let weights = 0
countList.forEach((ele: any) => {
weights += ele.weight
})
let marketValue = 0
if (moneyList.length === 0) {
countList.map((res: any) => {
res.marketValue = Math.round(Number(fundData.value.initAmount) * res.weight / weights * 100) / 100
marketValue += res.marketValue
})
} else {
if (countList.length > moneyList[moneyList.length - 1].length) {
countList.forEach((res: any, index: number) => {
if (index < moneyList[moneyList.length - 1].length) {
if (res.nav) {
marketValue += Math.round(moneyList[moneyList.length - 1][index].marketValue / moneyList[moneyList.length - 1][index].nav * res.nav * 100) / 100
} else {
marketValue += moneyList[moneyList.length - 1][index].marketValue
}
}
})
countList.map((res: any, index: number) => {
if (index >= moneyList[moneyList.length - 1].length) {
res.marketValue = Math.round(marketValue * res.weight / weights * 100) / 100
} else {
res.marketValue = Math.round(marketValue * res.weight / weights * 100) / 100
}
})
} else {
countList.map((res: any, index: number) => {
if (res.nav) {
res.marketValue = Math.round(moneyList[moneyList.length - 1][index].marketValue / moneyList[moneyList.length - 1][index].nav * res.nav * 100) / 100
} else {
res.marketValue = moneyList[moneyList.length - 1][index].marketValue
}
marketValue += res.marketValue
})
}
}
moneyList.push(countList)
navList.push({ sysDate: sysDate[0], nav: Math.round(marketValue / Number(fundData.value.initAmount) * 10000) / 10000 })
})
legend.push('本基金')
seriesList.push(getLineSeries({ name: '本基金', color: colorPalette[colorPalette.length - 1] }))
seriesList.push(getLineSeries({ name: '本基金', yAxisIndex: 1, xAxisIndex: 1, color: colorPalette[colorPalette.length - 1] }))
let maxDrawdown = 0
navList.forEach((ele: any) => {
let drawdown = 0
const nav = Number(ele.nav)
if (nav > maxDrawdown) {
maxDrawdown = nav
drawdown = 0
} else {
drawdown = Math.round((nav - maxDrawdown) / maxDrawdown * 10000) / 100
}
seriesList[seriesList.length - 2].data.push([ele.sysDate, nav, drawdown])
seriesList[seriesList.length - 1].data.push([ele.sysDate, drawdown, nav])
})
const option = getBaseOption({
yAxis: [getYAxis({ }), getYAxis({ gridIndex: 1, axisLabel: '{value}%' })],
xAxis: [getXAxis({ data: sysDate }), getXAxis({ data: sysDate, gridIndex: 1, showFlag: false })],
series: seriesList,
formatter: function (data: any) {
let res = `${data[0].value[0]}<br/>`
data.forEach((ele: any) => {
res += `<span style="color: ${ele.color}">${ele.seriesName}:历史净值${ele.axisIndex % 2 === 0 ? ele.value[1] : ele.value[2]},回撤${ele.axisIndex % 2 === 0 ? ele.value[2] : ele.value[1]}%</span><br/>`
})
return res
},
legend: { data: legend, top: '40px' },
title: [{ text: '历史净值' }, { text: '回撤情况', top: '54%' }],
grid: [{ bottom: '54%', left: '40px', right: '40px', top: '90px' }, { top: '66%', bottom: '20px', left: '40px', right: '40px' }],
color: colorPalette
})
if (!destroyedFlag.value && document.getElementById(chartList.value[1].id)) {
const chart = echarts.init(document.getElementById(chartList.value[1].id))
chart.setOption(option)
charts.value[1] = chart
chartList.value[1].loading = false
}
}
const generateColors = (numLines: number) => {
const colors = []
const m = numLines - 1
if (m > 0) {
const startHue = 30
const endHue = 330
const step = m === 1 ? 0 : (endHue - startHue) / (m - 1)
for (let i = 0; i < m; i++) {
const h = startHue + (step * i)
const s = 70
const l = 70
colors.push(hslToHex(h, s, l))
}
}
colors.push('#ff0000')
return colors
}
const hslToHex = (h : number, s: number, l: number) => {
h /= 360
s /= 100
l /= 100
const c = (1 - Math.abs(2 * l - 1)) * s
const x = c * (1 - Math.abs((h * 6) % 2 - 1))
const m = l - c / 2
let r, g, b
const i = Math.floor(h * 6)
switch (i) {
case 0: r = c; g = x; b = 0; break
case 1: r = x; g = c; b = 0; break
case 2: r = 0; g = c; b = x; break
case 3: r = 0; g = x; b = c; break
case 4: r = x; g = 0; b = c; break
default: r = c; g = 0; b = x; break
}
r = Math.round((r + m) * 255)
g = Math.round((g + m) * 255)
b = Math.round((b + m) * 255)
return '#' + [r, g, b].map(v => v.toString(16).padStart(2, '0')).join('')
}
const showFileListPopup = ref(false)
const showFileList = () => {
showFileListPopup.value = true
}
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: 86px;
.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-container {
height: 40vh;
@media screen and (min-width: 768px) {
height: 60vh;
}
}
.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: 12px 16px;
border-bottom: 1px solid #e8ebed;
height: 48px;
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;
position: relative;
color: #333;
&.date-col {
flex: 0 0 30%;
text-align: left;
font-weight: 500;
}
.budgetHeight {
height: 48px;
line-height: 48px;
}
.tag-div {
position: absolute;
right: 0;
top: 2px;
}
}
.no-data {
padding: 30px 0;
text-align: center;
color: #999;
background-color: #fafafa;
}
}
}
@media screen and (max-width: 768px) {
.listDiv {
padding: 0 12px;
.data-list-container {
.list-item {
padding: 10px 12px;
height: 44px;
}
.list-col {
font-size: 12px;
padding: 0 4px;
color: #333;
&.date-col {
flex: 0 0 25%;
}
}
}
}
}
}
.file-list {
max-height: calc(100% - 60px);
overflow-y: auto;
padding-bottom: 20px;
.file-item {
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
}
.file-link {
display: flex;
align-items: center;
color: #333;
text-decoration: none;
i {
font-size: 20px;
margin-right: 10px;
}
.file-title {
flex: 1;
font-size: 14px;
}
}
@media screen and (min-width: 768px) {
padding: 0 16px 20px;
.file-item {
padding: 16px 0;
}
.file-link {
i {
font-size: 24px;
margin-right: 16px;
}
.file-title {
font-size: 16px;
}
}
}
}
</style>