This commit is contained in:
etoai 2026-05-08 00:51:53 +08:00
parent f23ffa7610
commit 9c703ad75a
4 changed files with 297 additions and 113 deletions

View File

@ -1,10 +1,10 @@
<!doctype html>
<html lang="en">
<html lang="zh-CN" class="dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>vite-project</title>
<title>LOF 基金实时溢价监控</title>
</head>
<body>
<div id="app"></div>

View File

@ -1,5 +1,23 @@
<template>
<div class="lof-app">
<div class="login-overlay" v-if="!authenticated">
<div class="login-card">
<div class="login-icon">🔒</div>
<el-input
v-model="password"
type="password"
placeholder="请输入密码"
show-password
@keyup.enter="verifyPassword"
size="large"
/>
<el-button type="primary" size="large" @click="verifyPassword" :loading="checking" class="login-btn">
验证
</el-button>
<p class="login-error" v-if="errorMsg">{{ errorMsg }}</p>
</div>
</div>
<div class="lof-app" v-else>
<div class="header">
<div class="title">
<h2>LOF 基金实时溢价监控</h2>
@ -7,7 +25,7 @@
</div>
<div class="tool-bar">
<div class="refresh">
<el-button @click="manualRefresh" :loading="loading" type="primary" :icon="Refresh">
<el-button @click="manualRefresh" :loading="loading" type="primary" :icon="Refresh" size="default">
手动刷新
</el-button>
</div>
@ -24,64 +42,87 @@
</div>
<div class="filter-bar">
<el-input v-model="searchCode" placeholder="基金代码" clearable style="width: 160px;" />
<el-input v-model="searchName" placeholder="基金名称" clearable style="width: 200px;" />
<el-select v-model="filterStatus" placeholder="申购状态" clearable style="width: 140px;">
<el-input v-model="searchCode" placeholder="基金代码" clearable />
<el-input v-model="searchName" placeholder="基金名称" clearable />
<el-select v-model="filterStatus" placeholder="申购状态" clearable>
<el-option v-for="s in statusOptions" :key="s" :label="s" :value="s" />
</el-select>
<el-button @click="resetFilter" :icon="RefreshRight">重置</el-button>
<el-button @click="resetFilter" :icon="RefreshRight" size="default">重置</el-button>
</div>
<el-table :data="displayList" v-loading="loading" height="700" border>
<el-table-column prop="fundCode" label="基金代码" align="center" />
<el-table-column prop="fundName" label="基金名称" align="center" />
<el-table-column prop="tradePrice" label="场内价格" align="center" />
<el-table-column prop="netValue" label="场外净值(昨日)" align="center" />
<el-table-column prop="estimateValue" label="估算净值(实时)" align="center" />
<el-table-column label="涨跌幅" align="center">
<div class="table-wrapper">
<el-table :data="displayList" v-loading="loading" border>
<el-table-column prop="fundCode" label="基金代码" align="center" min-width="100" />
<el-table-column prop="fundName" label="基金名称" align="center" min-width="140" />
<el-table-column prop="tradePrice" label="场内价格" align="center" min-width="90" />
<el-table-column prop="netValue" label="场外净值(昨日)" align="center" min-width="120" />
<el-table-column prop="estimateValue" label="估算净值(实时)" align="center" min-width="120" />
<el-table-column label="涨跌幅" align="center" min-width="80">
<template #default="{ row }">
<span :class="getRateClass(row.increaseRate)">
{{ row.increaseRate }}%
</span>
</template>
</el-table-column>
<el-table-column label="溢价率(昨日)" align="center">
<el-table-column label="溢价率(昨日)" align="center" min-width="110">
<template #default="{ row }">
<span :class="getRateClass(row.premiumRate)">
{{ row.premiumRate }}%
</span>
</template>
</el-table-column>
<el-table-column label="溢价率(实时)" align="center">
<el-table-column label="溢价率(实时)" align="center" min-width="110">
<template #default="{ row }">
<span :class="getRateClass(row.estimatePremiumRate)">
{{ row.estimatePremiumRate }}%
</span>
</template>
</el-table-column>
<el-table-column prop="purchaseLimit" label="日限额" align="center" />
<el-table-column label="申购状态" align="center" width="120">
<el-table-column prop="purchaseLimit" label="日限额" align="center" min-width="80" />
<el-table-column label="申购状态" align="center" min-width="100">
<template #default="{ row }">
<el-tag :type="row.purchaseStatus === '暂停申购' ? 'danger' : row.purchaseStatus === '开放申购' ? 'success' : 'info'" size="small" style="white-space: nowrap;">
{{ row.purchaseStatus }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="fundSize" label="基金规模" align="center" />
<el-table-column prop="turnover" label="成交额" align="center" />
<el-table-column prop="fundSize" label="基金规模" align="center" min-width="100" />
<el-table-column prop="turnover" label="成交额" align="center" min-width="80" />
</el-table>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, watch } from 'vue'
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { Refresh, RefreshRight } from '@element-plus/icons-vue'
//
const API_URL = 'http://127.0.0.1:8000/api/lof'
const authenticated = ref(sessionStorage.getItem('auth') === 'true')
const password = ref('')
const checking = ref(false)
const errorMsg = ref('')
function verifyPassword() {
if (checking.value) return
checking.value = true
errorMsg.value = ''
setTimeout(() => {
if (password.value === '88') {
authenticated.value = true
sessionStorage.setItem('auth', 'true')
password.value = ''
} else {
errorMsg.value = '密码错误,请重试'
password.value = ''
}
checking.value = false
}, 300)
}
const fundList = ref([])
const loading = ref(false)
const sortType = ref('desc')
@ -96,13 +137,11 @@ function formatTime(date) {
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
}
//
const statusOptions = computed(() => {
const set = new Set(fundList.value.map(i => i.purchaseStatus).filter(Boolean))
return Array.from(set).sort()
})
// +
const displayList = computed(() => {
let list = [...fundList.value]
if (searchCode.value) {
@ -129,7 +168,6 @@ function resetFilter() {
filterStatus.value = ''
}
//
async function fetchData() {
if (loading.value) return
loading.value = true
@ -148,12 +186,10 @@ async function fetchData() {
}
}
//
function manualRefresh() {
fetchData()
}
//
function toggleSort() {
sortType.value = sortType.value === 'desc' ? 'asc' : 'desc'
}
@ -162,7 +198,6 @@ function toggleSortField() {
sortField.value = sortField.value === 'premiumRate' ? 'estimatePremiumRate' : 'premiumRate'
}
//
function getRateClass(rate) {
const num = parseFloat(rate) || 0
if (num > 0) return 'rate-up'
@ -171,67 +206,223 @@ function getRateClass(rate) {
}
onMounted(() => {
if (authenticated.value) {
fetchData()
}
})
watch(authenticated, (val) => {
if (val) {
fetchData()
}
})
</script>
<style scoped>
.lof-app {
max-width: 100%;
/* margin: 20px auto; */
padding: 24px;
.login-overlay {
position: fixed;
inset: 0;
background: #0d1117;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.login-card {
width: 360px;
max-width: 90vw;
padding: 40px 32px;
background: #161b22;
border: 1px solid #30363d;
border-radius: 12px;
text-align: center;
}
.login-icon {
font-size: 48px;
margin-bottom: 16px;
}
.login-card h2 {
color: #e6edf3;
font-size: 18px;
font-weight: 700;
margin: 0 0 8px 0;
}
.login-desc {
color: #8b949e;
font-size: 13px;
margin: 0 0 24px 0;
}
.login-btn {
width: 100%;
margin-top: 16px;
}
.login-error {
color: #f85149;
font-size: 13px;
margin: 12px 0 0 0;
}
@media (max-width: 480px) {
.login-card {
padding: 32px 20px;
}
.login-card h2 {
font-size: 16px;
}
}
.lof-app {
max-width: 1400px;
margin: 0 auto;
padding: 20px 16px;
min-height: 100vh;
background: #0d1117;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
align-items: flex-start;
margin-bottom: 16px;
gap: 16px;
}
.title {
display: flex;
flex-direction: column;
gap: 4px;
.title h2 {
color: #e6edf3;
font-size: 22px;
font-weight: 700;
margin: 0 0 4px 0;
}
.update-time {
font-size: 13px;
color: #999;
text-align: left;
font-size: 12px;
color: #8b949e;
}
.tool-bar {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
button {
padding: 4px 10px;
cursor: pointer;
border-radius: 4px;
border: 1px solid #ccc;
}
.sort {
display: flex;
align-items: center;
gap: 6px;
}
.sort-label {
font-size: 13px;
color: #666;
color: #8b949e;
white-space: nowrap;
}
.filter-bar {
display: flex;
gap: 10px;
margin-bottom: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.filter-bar .el-input {
width: 150px;
}
.filter-bar .el-select {
width: 130px;
}
.table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
border-radius: 8px;
border: 1px solid #30363d;
}
.table-wrapper :deep(.el-table) {
min-width: 900px;
}
.rate-up {
color: #f53f3f;
color: #f85149;
font-weight: bold;
}
.rate-down {
color: #009944;
color: #3fb950;
font-weight: bold;
}
.rate-zero {
color: #666;
color: #8b949e;
}
@media (max-width: 768px) {
.lof-app {
padding: 12px 8px;
}
.header {
flex-direction: column;
gap: 12px;
}
.title h2 {
font-size: 18px;
}
.tool-bar {
width: 100%;
}
.filter-bar .el-input {
width: calc(50% - 5px);
}
.filter-bar .el-select {
width: calc(50% - 5px);
}
.filter-bar {
gap: 8px;
}
}
@media (max-width: 480px) {
.lof-app {
padding: 8px 4px;
}
.title h2 {
font-size: 16px;
}
.update-time {
font-size: 11px;
}
.filter-bar .el-input {
width: 100%;
}
.filter-bar .el-select {
width: 100%;
}
.tool-bar {
gap: 8px;
}
.sort {
gap: 4px;
}
}
</style>

View File

@ -3,5 +3,6 @@ import './style.css'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
createApp(App).use(ElementPlus).mount('#app')

View File

@ -1,53 +1,45 @@
:root {
--text: #6b6375;
--text-h: #08060d;
--bg: #fff;
--border: #e5e4e7;
--code-bg: #f4f3ec;
--accent: #aa3bff;
--accent-bg: rgba(170, 59, 255, 0.1);
--accent-border: rgba(170, 59, 255, 0.5);
--social-bg: rgba(244, 243, 236, 0.5);
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--text: #9ca3af;
--text-h: #f3f4f6;
--bg: #0d1117;
--bg-secondary: #161b22;
--border: #30363d;
--code-bg: #1f2028;
--accent: #58a6ff;
--accent-bg: rgba(88, 166, 255, 0.15);
--accent-border: rgba(88, 166, 255, 0.5);
--social-bg: rgba(47, 48, 58, 0.5);
--shadow: rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
font: 18px/145% var(--sans);
font: 16px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark;
color-scheme: dark;
color: var(--text);
background: var(--bg);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: 1024px) {
font-size: 16px;
}
}
@media (prefers-color-scheme: dark) {
:root {
--text: #9ca3af;
--text-h: #f3f4f6;
--bg: #16171d;
--border: #2e303a;
--code-bg: #1f2028;
--accent: #c084fc;
--accent-bg: rgba(192, 132, 252, 0.15);
--accent-border: rgba(192, 132, 252, 0.5);
--social-bg: rgba(47, 48, 58, 0.5);
--shadow:
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
#social .button-icon {
filter: invert(1) brightness(2);
body {
min-height: 100vh;
background: var(--bg);
color: var(--text);
}
#app {
min-height: 100vh;
}
body {