This commit is contained in:
2026-04-25 01:23:38 +08:00
parent 221f50fe50
commit 9fdf0e1c25
10 changed files with 215 additions and 253 deletions
+8 -153
View File
@@ -1,160 +1,15 @@
<template>
<div class="container">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>📈 股票涨停回落分析工具</span>
</div>
</template>
<div class="filter-bar">
<el-form :inline="true">
<el-form-item label="开始日期">
<el-date-picker
v-model="startDate"
type="date"
placeholder="请选择"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:disabled-date="disabledStartDate"
/>
</el-form-item>
<el-form-item label="结束日期">
<el-date-picker
v-model="endDate"
type="date"
placeholder="请选择"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:disabled-date="disabledEndDate"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="handleSearch"
:loading="loading"
icon="Search"
>
查询分析
</el-button>
<el-button @click="resetFilter">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-table
:data="tableData"
style="width: 100%; margin-top: 10px"
v-loading="loading"
stripe
:default-sort="{ prop: 'drop_pct', order: 'descending' }"
>
<el-table-column prop="code" label="代码" width="100" />
<el-table-column prop="name" label="名称" width="120" />
<el-table-column prop="day_open" label="当日开盘" />
<el-table-column prop="max_high" label="区间最高" sortable />
<el-table-column prop="max_date" label="最高日期" width="120" />
<el-table-column prop="post_min_low" label="见顶后最低" />
<el-table-column prop="drop_pct" label="最大回落幅%" sortable>
<template #default="scope">
<b style="color: #f56c6c">{{ scope.row.drop_pct?.toFixed(2) }}%</b>
</template>
</el-table-column>
<el-table-column prop="rebound_pct" label="低位反弹%" sortable>
<template #default="scope">
<span
:style="{
color: scope.row.rebound_pct > 0 ? '#67c23a' : '#909399',
}"
>
{{ scope.row.rebound_pct?.toFixed(2) }}%
</span>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
<router-view />
</template>
<script setup>
import { ref } from "vue";
import axios from "axios";
import { ElMessage } from "element-plus";
// 1. 将范围数组拆分为两个独立状态
const startDate = ref("2024-09-24");
const endDate = ref("2026-04-23");
const tableData = ref([]);
const loading = ref(false);
// 2. 逻辑约束:开始日期不能晚于结束日期
const disabledStartDate = (time) => {
if (!endDate.value) return false;
return time.getTime() > new Date(endDate.value).getTime();
};
// 3. 逻辑约束:结束日期不能早于开始日期
const disabledEndDate = (time) => {
if (!startDate.value) return false;
return time.getTime() < new Date(startDate.value).getTime();
};
// 重置功能
const resetFilter = () => {
startDate.value = "";
endDate.value = "";
};
const handleSearch = async () => {
// 校验非空
if (!startDate.value || !endDate.value) {
ElMessage.warning("请完整选择开始和结束日期");
return;
}
loading.value = true;
try {
const response = await axios.get(
"http://localhost:18080/api/v1/stock/analyze",
{
params: {
start_date: startDate.value,
end_date: endDate.value,
},
},
);
tableData.value = response.data;
ElMessage.success(`查询成功,共计 ${tableData.value.length} 条记录`);
} catch (error) {
console.error(error);
ElMessage.error("获取数据失败,请检查后端服务");
} finally {
loading.value = false;
}
};
// 全局入口,无需额外逻辑
</script>
<style scoped>
.container {
padding: 20px;
background-color: #f5f7fa;
min-height: 100vh;
<style>
body {
margin: 0;
background-color: #f0f2f5;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.card-header {
font-size: 18px;
font-weight: bold;
}
.filter-bar {
margin-bottom: 10px;
}
/* 调整表单间距 */
:deep(.el-form-item) {
margin-right: 20px;
margin-bottom: 10px;
}
</style>
</style>
+11
View File
@@ -0,0 +1,11 @@
import axios from 'axios'
const request = axios.create({
baseURL: 'http://localhost:18080/api/v1', // 指向你的 Go 后端
timeout: 10000
})
// 获取涨停回落分析数据
export const getAnalyzeData = (params) => {
return request.get('/stock/analyze', { params })
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.5 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 496 B

-95
View File
@@ -1,95 +0,0 @@
<script setup>
import { ref } from 'vue'
import viteLogo from '../assets/vite.svg'
import heroImg from '../assets/hero.png'
import vueLogo from '../assets/vue.svg'
const count = ref(0)
</script>
<template>
<section id="center">
<div class="hero">
<img :src="heroImg" class="base" width="170" height="179" alt="" />
<img :src="vueLogo" class="framework" alt="Vue logo" />
<img :src="viteLogo" class="vite" alt="Vite logo" />
</div>
<div>
<h1>Get started</h1>
<p>Edit <code>src/App.vue</code> and save to test <code>HMR</code></p>
</div>
<button type="button" class="counter" @click="count++">
Count is {{ count }}
</button>
</section>
<div class="ticks"></div>
<section id="next-steps">
<div id="docs">
<svg class="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#documentation-icon"></use>
</svg>
<h2>Documentation</h2>
<p>Your questions, answered</p>
<ul>
<li>
<a href="https://vite.dev/" target="_blank">
<img class="logo" :src="viteLogo" alt="" />
Explore Vite
</a>
</li>
<li>
<a href="https://vuejs.org/" target="_blank">
<img class="button-icon" :src="vueLogo" alt="" />
Learn more
</a>
</li>
</ul>
</div>
<div id="social">
<svg class="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#social-icon"></use>
</svg>
<h2>Connect with us</h2>
<p>Join the Vite community</p>
<ul>
<li>
<a href="https://github.com/vitejs/vite" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#github-icon"></use>
</svg>
GitHub
</a>
</li>
<li>
<a href="https://chat.vite.dev/" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#discord-icon"></use>
</svg>
Discord
</a>
</li>
<li>
<a href="https://x.com/vite_js" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#x-icon"></use>
</svg>
X.com
</a>
</li>
<li>
<a href="https://bsky.app/profile/vite.dev" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#bluesky-icon"></use>
</svg>
Bluesky
</a>
</li>
</ul>
</div>
</section>
<div class="ticks"></div>
<section id="spacer"></section>
</template>
+10 -1
View File
@@ -1,8 +1,17 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// 引入中文语言包
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
const app = createApp(App)
app.use(ElementPlus)
app.use(router)
// 全局配置 Element Plus 使用中文
app.use(ElementPlus, {
locale: zhCn,
})
app.mount('#app')
+20
View File
@@ -0,0 +1,20 @@
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
redirect: '/stock/analyze'
},
{
path: '/stock/analyze',
name: 'StockAnalyze',
component: () => import('../views/stock/Analyze.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
+156
View File
@@ -0,0 +1,156 @@
<template>
<div class="analyze-container">
<el-card shadow="never">
<template #header>
<div class="header-title">📊 涨停后回落分析</div>
</template>
<div class="filter-section">
<div class="filter-item">
<span class="label-text">选定日期:</span>
<el-date-picker
v-model="startDate"
type="date"
placeholder="请选择"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 140px"
/>
</div>
<div class="filter-item">
<span class="label-text">结束日期:</span>
<el-date-picker
v-model="endDate"
type="date"
placeholder="请选择"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 140px"
/>
</div>
<el-button
type="primary"
@click="fetchData"
:loading="loading"
style="margin-left: 12px"
>
查询数据
</el-button>
</div>
<el-table
:data="tableData"
v-loading="loading"
stripe
style="width: 100%; margin-top: 20px"
:header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
:default-sort="{ prop: 'drop_pct', order: 'descending' }"
>
<el-table-column prop="code" label="代码" width="100" />
<el-table-column prop="name" label="名称" width="120" />
<el-table-column prop="day_open" label="当日开盘" align="right" />
<el-table-column prop="max_high" label="区间最高" sortable align="right" />
<el-table-column prop="max_date" label="最高日期" width="120" align="center" />
<el-table-column prop="post_min_low" label="见顶后最低" align="right" />
<el-table-column prop="drop_pct" label="最大回落幅度%" sortable align="right">
<template #default="scope">
<span class="text-success">{{ scope.row.drop_pct.toFixed(2) }}%</span>
</template>
</el-table-column>
<el-table-column prop="rebound_pct" label="低位反弹%" sortable align="right">
<template #default="scope">
<span :class="scope.row.rebound_pct > 0 ? 'text-danger' : 'text-gray'">
{{ scope.row.rebound_pct.toFixed(2) }}%
</span>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { getAnalyzeData } from '@/api/stock'
import { ElMessage } from 'element-plus'
// 修改默认日期为 2026-04-22
const startDate = ref('2026-04-22')
const endDate = ref('2026-04-23')
const tableData = ref([])
const loading = ref(false)
const fetchData = async () => {
if (!startDate.value || !endDate.value) {
return ElMessage.warning('请选择日期')
}
if (new Date(startDate.value) > new Date(endDate.value)) {
return ElMessage.warning('选定日期不能晚于结束日期')
}
loading.value = true
try {
const res = await getAnalyzeData({
start_date: startDate.value,
end_date: endDate.value
})
tableData.value = res.data
ElMessage.success(`加载成功: ${res.data.length} 条数据`)
} catch (err) {
ElMessage.error('数据获取失败')
} finally {
loading.value = false
}
}
</script>
<style scoped>
.analyze-container {
padding: 24px;
background-color: #f0f2f5;
min-height: calc(100vh - 48px);
}
.header-title {
font-weight: 600;
font-size: 18px;
color: #303133;
}
.filter-section {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 20px;
}
.filter-item {
display: flex;
align-items: center;
}
.label-text {
font-size: 14px;
color: #606266;
margin-right: 8px;
}
.text-success {
color: #67c23a;
font-weight: bold;
}
.text-danger {
color: #f56c6c;
font-weight: bold;
}
.text-gray {
color: #909399;
}
:deep(.el-table .cell) {
padding: 0 12px;
}
</style>