This commit is contained in:
etoai 2026-05-18 11:35:20 +08:00
parent 3ae68e083f
commit 9bcb8a2b9b
12 changed files with 77 additions and 32 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
import{C as e,E as t,S as n,T as r,_ as i,b as a,d as o,f as s,g as c,h as l,i as u,n as d,p as f,r as p,t as m,x as h,y as g}from"./index-CWE7ZHKi.js";import{t as _}from"./axios-C2kjobOc.js";var v={class:`history-app`},y={class:`history-header`},b={class:`header-left`},x=`/api/lof/history`,S=m({__name:`HistoryDetail`,setup(m){let S=d(),C=p(),w=e(S.query.fundCode||``),T=e(S.query.fundName||``),E=e([]),D=e(!1);function O(){C.push(`/`)}async function k(){if(!w.value){u.error(`缺少基金代码参数`);return}D.value=!0;try{let e=await _.get(x,{params:{fund_code:w.value,fund_name:T.value}});e.data.code===200?(E.value=e.data.data,T.value=e.data.fundName||T.value):u.error(e.data.msg||`获取历史数据失败`)}catch{u.error(`请求失败:请确认 Python 后端已启动`)}finally{D.value=!1}}function A(e){if(e==null)return`-`;let t=parseFloat(e);return isNaN(t)?`-`:(t>0?`+`:``)+t+`%`}function j(e){if(e==null)return`-`;let t=parseFloat(e);return isNaN(t)?`-`:(t>0?`+`:``)+t.toFixed(3)+`%`}function M(e){if(e==null)return`-`;let t=parseFloat(e);return isNaN(t)?`-`:(t>0?`+`:``)+t.toFixed(2)}function N(e){let t=parseFloat(e)||0;return t>0?`rate-up`:t<0?`rate-down`:`rate-zero`}return c(()=>{k()}),(e,c)=>{let u=g(`el-table-column`),d=g(`el-table`),p=a(`loading`);return i(),f(`div`,v,[o(`div`,y,[o(`div`,b,[o(`h2`,null,t(T.value)+` (`+t(w.value)+`) 历史数据`,1)]),o(`a`,{class:`back-link`,onClick:O},`← 返回看板`)]),n((i(),s(d,{data:E.value,border:``,height:`calc(100vh - 200px)`,style:{width:`100%`}},{default:h(()=>[l(u,{prop:`date`,label:`价格日期`,align:`center`,"min-width":`110`}),l(u,{prop:`price`,label:`收盘价`,align:`center`,"min-width":`90`}),l(u,{prop:`navDate`,label:`净值日期`,align:`center`,"min-width":`110`}),l(u,{prop:`nav`,label:`净值`,align:`center`,"min-width":`90`}),l(u,{label:`溢价率`,align:`center`,"min-width":`100`},{default:h(({row:e})=>[o(`span`,{class:r(N(e.premiumRate))},t(A(e.premiumRate)),3)]),_:1}),l(u,{prop:`turnover`,label:`成交额(万元)`,align:`center`,"min-width":`120`}),l(u,{prop:`shareVolume`,label:`场内份额(万份)`,align:`center`,"min-width":`130`}),l(u,{label:`场内新增(万份)`,align:`center`,"min-width":`120`},{default:h(({row:e})=>[o(`span`,{class:r(N(e.changeAmount))},t(e.changeAmount==null?`-`:M(e.changeAmount)),3)]),_:1}),l(u,{label:`份额涨幅`,align:`center`,"min-width":`100`},{default:h(({row:e})=>[o(`span`,{class:r(N(e.changePct))},t(e.changePct==null?`-`:j(e.changePct)),3)]),_:1})]),_:1},8,[`data`])),[[p,D.value]])])}}},[[`__scopeId`,`data-v-18b3266c`]]);export{S as default};

View File

@ -1 +0,0 @@
import{C as e,_ as t,b as n,d as r,h as i,i as a,l as o,m as s,n as c,p as l,r as u,t as d,u as f,v as p,w as m,x as h,y as g}from"./index-elGRMxOZ.js";import{t as _}from"./axios-AStdilPR.js";var v={class:`history-app`},y={class:`history-header`},b={class:`header-left`},x=`/api/lof/history`,S=d({__name:`HistoryDetail`,setup(d){let S=c(),C=u(),w=h(S.query.fundCode||``),T=h(S.query.fundName||``),E=h([]),D=h(!1);function O(){C.push(`/`)}async function k(){if(!w.value){a.error(`缺少基金代码参数`);return}D.value=!0;try{let e=await _.get(x,{params:{fund_code:w.value,fund_name:T.value}});e.data.code===200?(E.value=e.data.data,T.value=e.data.fundName||T.value):a.error(e.data.msg||`获取历史数据失败`)}catch{a.error(`请求失败:请确认 Python 后端已启动`)}finally{D.value=!1}}function A(e){if(e==null)return`-`;let t=parseFloat(e);return isNaN(t)?`-`:(t>0?`+`:``)+t+`%`}function j(e){if(e==null)return`-`;let t=parseFloat(e);return isNaN(t)?`-`:(t>0?`+`:``)+t.toFixed(3)+`%`}function M(e){if(e==null)return`-`;let t=parseFloat(e);return isNaN(t)?`-`:(t>0?`+`:``)+t.toFixed(2)}function N(e){let t=parseFloat(e)||0;return t>0?`rate-up`:t<0?`rate-down`:`rate-zero`}return s(()=>{k()}),(a,s)=>{let c=t(`el-table-column`),u=t(`el-table`),d=p(`loading`);return i(),r(`div`,v,[o(`div`,y,[o(`div`,b,[o(`h2`,null,m(T.value)+` (`+m(w.value)+`) 历史数据`,1)]),o(`a`,{class:`back-link`,onClick:O},`← 返回看板`)]),n((i(),f(u,{data:E.value,border:``,height:`calc(100vh - 200px)`,style:{width:`100%`}},{default:g(()=>[l(c,{prop:`date`,label:`价格日期`,align:`center`,"min-width":`110`}),l(c,{prop:`price`,label:`收盘价`,align:`center`,"min-width":`90`}),l(c,{prop:`navDate`,label:`净值日期`,align:`center`,"min-width":`110`}),l(c,{prop:`nav`,label:`净值`,align:`center`,"min-width":`90`}),l(c,{label:`溢价率`,align:`center`,"min-width":`100`},{default:g(({row:t})=>[o(`span`,{class:e(N(t.premiumRate))},m(A(t.premiumRate)),3)]),_:1}),l(c,{prop:`turnover`,label:`成交额(万元)`,align:`center`,"min-width":`120`}),l(c,{prop:`shareVolume`,label:`场内份额(万份)`,align:`center`,"min-width":`130`}),l(c,{label:`场内新增(万份)`,align:`center`,"min-width":`120`},{default:g(({row:t})=>[o(`span`,{class:e(N(t.changeAmount))},m(t.changeAmount==null?`-`:M(t.changeAmount)),3)]),_:1}),l(c,{label:`份额涨幅`,align:`center`,"min-width":`100`},{default:g(({row:t})=>[o(`span`,{class:e(N(t.changePct))},m(t.changePct==null?`-`:j(t.changePct)),3)]),_:1})]),_:1},8,[`data`])),[[d,D.value]])])}}},[[`__scopeId`,`data-v-18b3266c`]]);export{S as default};

View File

@ -0,0 +1 @@
.lof-app[data-v-f13f868e]{box-sizing:border-box;width:100%;max-width:1600px;margin:0 auto;padding:24px;overflow-x:auto}.header[data-v-f13f868e]{justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px;display:flex}.title[data-v-f13f868e]{flex-direction:column;flex-shrink:0;gap:4px;display:flex}.update-time[data-v-f13f868e]{color:var(--text-muted);text-align:left;white-space:nowrap;font-size:13px}.tool-bar[data-v-f13f868e]{flex-shrink:0;gap:12px;display:flex}.filter-bar[data-v-f13f868e]{flex-wrap:wrap;align-items:center;gap:10px;margin-bottom:12px;display:flex}.filter-input[data-v-f13f868e]{width:200px}.filter-input-sm[data-v-f13f868e]{width:160px}.filter-select[data-v-f13f868e]{width:140px}.filter-toggle-btn[data-v-f13f868e]{display:none}.rate-up[data-v-f13f868e]{color:var(--rate-up);font-weight:700}.rate-down[data-v-f13f868e]{color:var(--rate-down);font-weight:700}.rate-zero[data-v-f13f868e]{color:var(--text-muted)}.fund-code-link[data-v-f13f868e]{color:var(--accent);cursor:pointer;text-decoration:none}.fund-code-link[data-v-f13f868e]:hover{text-decoration:underline}.lof-app .el-table .cell[data-v-f13f868e]{padding:2px 3px}.lof-app .el-table th.el-table__cell .cell[data-v-f13f868e]{padding:3px}@media (width<=768px){.lof-app[data-v-f13f868e]{padding:8px}.header[data-v-f13f868e]{flex-direction:column;align-items:flex-start}.tool-bar[data-v-f13f868e]{gap:8px;width:100%;display:flex}.tool-bar .el-button[data-v-f13f868e]{flex:1}.filter-toggle-btn[data-v-f13f868e]{display:inline-flex}.filter-bar[data-v-f13f868e]{flex-direction:column;align-items:stretch}.filter-input[data-v-f13f868e],.filter-input-sm[data-v-f13f868e],.filter-select[data-v-f13f868e]{width:100%}.filter-checkbox[data-v-f13f868e]{justify-content:center;display:flex}.filter-radio[data-v-f13f868e]{width:100%;display:flex}.filter-radio .el-radio-button[data-v-f13f868e]{flex:1}.filter-radio .el-radio-button__inner[data-v-f13f868e]{justify-content:center;width:100%}.filter-reset-btn[data-v-f13f868e]{width:100%}.update-time[data-v-f13f868e]{white-space:normal}.lof-app .el-table[data-v-f13f868e],.lof-app .fund-code-link[data-v-f13f868e]{font-size:12px}.lof-app .el-tag[data-v-f13f868e]{height:22px;padding:0 4px;font-size:11px;line-height:20px}.lof-app .el-switch[data-v-f13f868e]{--el-switch-height:16px;--el-switch-button-size:12px;--el-switch-font-size:11px}}

View File

@ -1 +0,0 @@
.lof-app[data-v-0da2059b]{box-sizing:border-box;width:100%;max-width:1600px;margin:0 auto;padding:24px;overflow-x:auto}.header[data-v-0da2059b]{justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px;display:flex}.title[data-v-0da2059b]{flex-direction:column;flex-shrink:0;gap:4px;display:flex}.update-time[data-v-0da2059b]{color:var(--text-muted);text-align:left;white-space:nowrap;font-size:13px}.tool-bar[data-v-0da2059b]{flex-shrink:0;gap:12px;display:flex}.filter-bar[data-v-0da2059b]{flex-wrap:wrap;align-items:center;gap:10px;margin-bottom:12px;display:flex}.filter-input[data-v-0da2059b]{width:200px}.filter-input-sm[data-v-0da2059b]{width:160px}.filter-select[data-v-0da2059b]{width:140px}.rate-up[data-v-0da2059b]{color:var(--rate-up);font-weight:700}.rate-down[data-v-0da2059b]{color:var(--rate-down);font-weight:700}.rate-zero[data-v-0da2059b]{color:var(--text-muted)}.fund-code-link[data-v-0da2059b]{color:var(--accent);cursor:pointer;text-decoration:none}.fund-code-link[data-v-0da2059b]:hover{text-decoration:underline}@media (width<=768px){.lof-app[data-v-0da2059b]{padding:12px}.header[data-v-0da2059b]{flex-direction:column;align-items:flex-start}.tool-bar[data-v-0da2059b],.tool-bar .el-button[data-v-0da2059b]{width:100%}.filter-bar[data-v-0da2059b]{flex-direction:column;align-items:stretch}.filter-input[data-v-0da2059b],.filter-input-sm[data-v-0da2059b],.filter-select[data-v-0da2059b]{width:100%}.filter-checkbox[data-v-0da2059b]{justify-content:center;display:flex}.filter-radio[data-v-0da2059b]{width:100%;display:flex}.filter-radio .el-radio-button[data-v-0da2059b]{flex:1}.filter-radio .el-radio-button__inner[data-v-0da2059b]{justify-content:center;width:100%}.filter-reset-btn[data-v-0da2059b]{width:100%}.update-time[data-v-0da2059b]{white-space:normal}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,7 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>LOF 基金监控</title> <title>LOF 基金监控</title>
<script type="module" crossorigin src="/assets/index-elGRMxOZ.js"></script> <script type="module" crossorigin src="/assets/index-CWE7ZHKi.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Ci_MFdXL.css"> <link rel="stylesheet" crossorigin href="/assets/index-Ci_MFdXL.css">
</head> </head>

View File

@ -5,13 +5,16 @@
<span class="update-time">数据更新时间{{ lastUpdateTime }}</span> <span class="update-time">数据更新时间{{ lastUpdateTime }}</span>
</div> </div>
<div class="tool-bar"> <div class="tool-bar">
<el-button @click="showFilters = !showFilters" class="filter-toggle-btn" :icon="showFilters ? RefreshRight : Search">
{{ showFilters ? '收起筛选' : '筛选' }}
</el-button>
<el-button @click="manualRefresh" :loading="loading" type="primary" :icon="Refresh"> <el-button @click="manualRefresh" :loading="loading" type="primary" :icon="Refresh">
手动刷新 手动刷新
</el-button> </el-button>
</div> </div>
</div> </div>
<div class="filter-bar"> <div class="filter-bar" v-show="showFilters">
<el-input v-model="searchCode" placeholder="基金代码" clearable class="filter-input filter-input-sm" /> <el-input v-model="searchCode" placeholder="基金代码" clearable class="filter-input filter-input-sm" />
<el-input v-model="searchName" placeholder="基金名称" clearable class="filter-input" /> <el-input v-model="searchName" placeholder="基金名称" clearable class="filter-input" />
<el-select v-model="filterStatus" placeholder="申购状态" clearable class="filter-select"> <el-select v-model="filterStatus" placeholder="申购状态" clearable class="filter-select">
@ -25,48 +28,48 @@
<el-button @click="resetFilter" :icon="RefreshRight" class="filter-reset-btn">重置</el-button> <el-button @click="resetFilter" :icon="RefreshRight" class="filter-reset-btn">重置</el-button>
</div> </div>
<el-table :data="displayList" v-loading="loading" height="calc(100vh - 180px)" border :default-sort="{ prop: 'premiumRate', order: 'descending' }" style="width: 100%"> <el-table :data="displayList" v-loading="loading" height="calc(100vh - 180px)" border :default-sort="{ prop: 'premiumRate', order: 'descending' }" style="width: 100%" size="small">
<el-table-column prop="fundCode" label="基金代码" align="center" min-width="100"> <el-table-column prop="fundCode" label="基金代码" align="center" min-width="80">
<template #default="{ row }"> <template #default="{ row }">
<a class="fund-code-link" @click="goHistory(row)">{{ row.fundCode }}</a> <a class="fund-code-link" @click="goHistory(row)">{{ row.fundCode }}</a>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="fundName" label="基金名称" align="center" min-width="130" /> <el-table-column prop="fundName" label="基金名称" align="center" min-width="100" />
<el-table-column prop="tradePrice" label="场内价格" align="center" min-width="110" sortable :sort-method="(a, b) => numericSort(a, b, 'tradePrice')" /> <el-table-column prop="premiumRate" label="溢价率(昨日)" align="center" min-width="95" sortable :sort-method="(a, b) => numericSort(a, b, 'premiumRate')">
<el-table-column prop="netValue" label="场外净值(昨日)" align="center" min-width="140" sortable :sort-method="(a, b) => numericSort(a, b, 'netValue')" />
<el-table-column prop="estimateValue" label="估算净值(实时)" align="center" min-width="140" sortable :sort-method="(a, b) => numericSort(a, b, 'estimateValue')" />
<el-table-column prop="increaseRate" label="涨跌幅" align="center" min-width="110" sortable :sort-method="(a, b) => numericSort(a, b, 'increaseRate')">
<template #default="{ row }">
<span :class="getRateClass(row.increaseRate)">
{{ row.increaseRate }}%
</span>
</template>
</el-table-column>
<el-table-column prop="premiumRate" label="溢价率(昨日)" align="center" min-width="130" sortable :sort-method="(a, b) => numericSort(a, b, 'premiumRate')">
<template #default="{ row }"> <template #default="{ row }">
<span :class="getRateClass(row.premiumRate)"> <span :class="getRateClass(row.premiumRate)">
{{ row.premiumRate }}% {{ row.premiumRate }}%
</span> </span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="estimatePremiumRate" label="溢价率(实时)" align="center" min-width="130" sortable :sort-method="(a, b) => numericSort(a, b, 'estimatePremiumRate')"> <el-table-column prop="estimatePremiumRate" label="溢价率(实时)" align="center" min-width="95" sortable :sort-method="(a, b) => numericSort(a, b, 'estimatePremiumRate')">
<template #default="{ row }"> <template #default="{ row }">
<span :class="getRateClass(row.estimatePremiumRate)"> <span :class="getRateClass(row.estimatePremiumRate)">
{{ row.estimatePremiumRate }}% {{ row.estimatePremiumRate }}%
</span> </span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="purchaseLimit" label="日限额" align="center" min-width="110" sortable :sort-method="(a, b) => numericSort(a, b, 'purchaseLimit')" /> <el-table-column prop="tradePrice" label="场内价格" align="center" min-width="80" sortable :sort-method="(a, b) => numericSort(a, b, 'tradePrice')" />
<el-table-column label="申购状态" align="center" width="120"> <el-table-column prop="netValue" label="场外净值(昨日)" align="center" min-width="105" sortable :sort-method="(a, b) => numericSort(a, b, 'netValue')" />
<el-table-column prop="estimateValue" label="估算净值(实时)" align="center" min-width="105" sortable :sort-method="(a, b) => numericSort(a, b, 'estimateValue')" />
<el-table-column prop="increaseRate" label="涨跌幅" align="center" min-width="80" sortable :sort-method="(a, b) => numericSort(a, b, 'increaseRate')">
<template #default="{ row }">
<span :class="getRateClass(row.increaseRate)">
{{ row.increaseRate }}%
</span>
</template>
</el-table-column>
<el-table-column prop="purchaseLimit" label="日限额" align="center" min-width="85" sortable :sort-method="(a, b) => numericSort(a, b, 'purchaseLimit')" />
<el-table-column label="申购状态" align="center" width="85">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="row.purchaseStatus === '暂停申购' ? 'danger' : row.purchaseStatus === '开放申购' ? 'success' : 'info'" size="small" style="white-space: nowrap;"> <el-tag :type="row.purchaseStatus === '暂停申购' ? 'danger' : row.purchaseStatus === '开放申购' ? 'success' : 'info'" size="small" style="white-space: nowrap;">
{{ row.purchaseStatus }} {{ row.purchaseStatus }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="fundSize" label="基金规模" align="center" min-width="110" sortable :sort-method="(a, b) => numericSort(a, b, 'fundSize')" /> <el-table-column prop="fundSize" label="基金规模" align="center" min-width="85" sortable :sort-method="(a, b) => numericSort(a, b, 'fundSize')" />
<el-table-column prop="turnover" label="成交额" align="center" min-width="110" sortable :sort-method="(a, b) => numericSort(a, b, 'turnover')" /> <el-table-column prop="turnover" label="成交额" align="center" min-width="85" sortable :sort-method="(a, b) => numericSort(a, b, 'turnover')" />
<el-table-column label="关注" align="center" width="70" fixed="right"> <el-table-column label="关注" align="center" width="55">
<template #default="{ row }"> <template #default="{ row }">
<el-switch <el-switch
:model-value="favorites.has(row.fundCode)" :model-value="favorites.has(row.fundCode)"
@ -83,7 +86,7 @@ import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import axios from 'axios' import axios from 'axios'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { Refresh, RefreshRight } from '@element-plus/icons-vue' import { Refresh, RefreshRight, Search } from '@element-plus/icons-vue'
// //
const API_URL = '/api/lof' const API_URL = '/api/lof'
@ -150,6 +153,7 @@ const lastUpdateTime = ref('')
const favorites = ref(new Set()) const favorites = ref(new Set())
const showOnlyFavorites = ref(false) const showOnlyFavorites = ref(false)
const filterByScale = ref(true) const filterByScale = ref(true)
const showFilters = ref(false)
const router = useRouter() const router = useRouter()
function parseFundSize(sizeStr) { function parseFundSize(sizeStr) {
@ -336,6 +340,9 @@ onMounted(() => {
.filter-select { .filter-select {
width: 140px; width: 140px;
} }
.filter-toggle-btn {
display: none;
}
.rate-up { .rate-up {
color: var(--rate-up); color: var(--rate-up);
font-weight: bold; font-weight: bold;
@ -356,9 +363,16 @@ onMounted(() => {
text-decoration: underline; text-decoration: underline;
} }
.lof-app .el-table .cell {
padding: 2px 3px;
}
.lof-app .el-table th.el-table__cell .cell {
padding: 3px 3px;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.lof-app { .lof-app {
padding: 12px; padding: 8px;
} }
.header { .header {
flex-direction: column; flex-direction: column;
@ -366,9 +380,14 @@ onMounted(() => {
} }
.tool-bar { .tool-bar {
width: 100%; width: 100%;
display: flex;
gap: 8px;
} }
.tool-bar .el-button { .tool-bar .el-button {
width: 100%; flex: 1;
}
.filter-toggle-btn {
display: inline-flex;
} }
.filter-bar { .filter-bar {
flex-direction: column; flex-direction: column;
@ -400,5 +419,23 @@ onMounted(() => {
.update-time { .update-time {
white-space: normal; white-space: normal;
} }
.lof-app .el-table {
font-size: 12px;
}
.lof-app .fund-code-link {
font-size: 12px;
}
.lof-app .el-tag {
font-size: 11px;
padding: 0 4px;
height: 22px;
line-height: 20px;
}
.lof-app .el-switch {
--el-switch-height: 16px;
--el-switch-button-size: 12px;
--el-switch-font-size: 11px;
}
} }
</style> </style>

View File

@ -5,4 +5,12 @@ import vue from '@vitejs/plugin-vue'
export default defineConfig({ export default defineConfig({
base: '/', base: '/',
plugins: [vue()], plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
},
},
},
}) })