update
This commit is contained in:
parent
5393559171
commit
b6a9e72500
@ -1,6 +1,7 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import HomeView from '../views/HomeView.vue'
|
import HomeView from '../views/HomeView.vue'
|
||||||
import DownloadView from '../views/DownloadView.vue'
|
import DownloadView from '../views/DownloadView.vue'
|
||||||
|
import StockAnalysisView from '../views/StockAnalysisView.vue'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@ -45,6 +46,24 @@ const router = createRouter({
|
|||||||
component: DownloadView,
|
component: DownloadView,
|
||||||
meta: { standalone: true },
|
meta: { standalone: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/stock-analysis',
|
||||||
|
name: 'stock-analysis',
|
||||||
|
component: StockAnalysisView,
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
title: '股票分析专家',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/crypto-analysis',
|
||||||
|
name: 'crypto-analysis',
|
||||||
|
component: () => import('../views/CryptoAnalysisView.vue'),
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
title: '加密货币分析专家',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
1121
src/views/CryptoAnalysisView.vue
Normal file
1121
src/views/CryptoAnalysisView.vue
Normal file
File diff suppressed because it is too large
Load Diff
512
src/views/StockAnalysisView.vue
Normal file
512
src/views/StockAnalysisView.vue
Normal file
@ -0,0 +1,512 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { useUserStore } from '../stores/user'
|
||||||
|
|
||||||
|
interface StockOption {
|
||||||
|
stock_code: string
|
||||||
|
short_name: string
|
||||||
|
exchange: string
|
||||||
|
list_date: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const selectedStock = ref<StockOption | null>(null)
|
||||||
|
const isAnalyzing = ref(false)
|
||||||
|
const analysisResult = ref(null)
|
||||||
|
const showDropdown = ref(false)
|
||||||
|
const isSearching = ref(false)
|
||||||
|
const stockOptions = ref<StockOption[]>([])
|
||||||
|
|
||||||
|
// 根据环境选择API基础URL
|
||||||
|
const apiBaseUrl =
|
||||||
|
import.meta.env.MODE === 'development' ? 'http://127.0.0.1:8000' : 'https://api.ibtc.work'
|
||||||
|
|
||||||
|
// 监听搜索输入
|
||||||
|
watch(searchQuery, async (newQuery) => {
|
||||||
|
const query = newQuery.trim()
|
||||||
|
if (!query || query.length < 2) {
|
||||||
|
stockOptions.value = []
|
||||||
|
showDropdown.value = false
|
||||||
|
selectedStock.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isSearching.value = true
|
||||||
|
try {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
if (userStore.authHeader) {
|
||||||
|
headers['Authorization'] = userStore.authHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${apiBaseUrl}/adata/stock/search?key=${encodeURIComponent(query)}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('搜索请求失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
stockOptions.value = data
|
||||||
|
showDropdown.value = true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('搜索股票失败:', error)
|
||||||
|
stockOptions.value = []
|
||||||
|
} finally {
|
||||||
|
isSearching.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectStock = (stock: StockOption) => {
|
||||||
|
selectedStock.value = stock
|
||||||
|
searchQuery.value = `${stock.stock_code} - ${stock.short_name}`
|
||||||
|
showDropdown.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = async () => {
|
||||||
|
if (!selectedStock.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isAnalyzing.value = true
|
||||||
|
try {
|
||||||
|
// TODO: 调用股票分析接口
|
||||||
|
// const response = await fetch('API_URL', {
|
||||||
|
// method: 'POST',
|
||||||
|
// headers: {
|
||||||
|
// 'Content-Type': 'application/json'
|
||||||
|
// },
|
||||||
|
// body: JSON.stringify({
|
||||||
|
// stockCode: selectedStock.value.stock_code,
|
||||||
|
// market: selectedStock.value.exchange
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// analysisResult.value = await response.json()
|
||||||
|
|
||||||
|
// 模拟API调用延迟
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('分析请求失败:', error)
|
||||||
|
} finally {
|
||||||
|
isAnalyzing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击外部关闭下拉框
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement
|
||||||
|
if (!target.closest('.search-container')) {
|
||||||
|
showDropdown.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化上市日期
|
||||||
|
const formatListDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString('zh-CN')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="stock-analysis-view" @click="handleClickOutside">
|
||||||
|
<div class="content-container">
|
||||||
|
<div class="header-section">
|
||||||
|
<h1 class="title">股票分析专家</h1>
|
||||||
|
<p class="description">输入股票代码或名称(至少2个字符),获取AI驱动的专业分析报告</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-section">
|
||||||
|
<div class="search-section">
|
||||||
|
<div class="search-container">
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<input
|
||||||
|
v-model="searchQuery"
|
||||||
|
type="text"
|
||||||
|
class="search-input"
|
||||||
|
placeholder="请输入股票代码或名称(如:600519 或 贵州茅台)"
|
||||||
|
@focus="showDropdown = searchQuery.length >= 2 && stockOptions.length > 0"
|
||||||
|
/>
|
||||||
|
<div class="search-icon">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<!-- 加载指示器 -->
|
||||||
|
<div v-if="isSearching" class="loading-indicator">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 股票选项下拉框 -->
|
||||||
|
<div v-if="showDropdown && stockOptions.length > 0" class="stock-options">
|
||||||
|
<div
|
||||||
|
v-for="stock in stockOptions"
|
||||||
|
:key="stock.stock_code"
|
||||||
|
class="stock-option"
|
||||||
|
@click="selectStock(stock)"
|
||||||
|
>
|
||||||
|
<div class="stock-code">{{ stock.stock_code }}</div>
|
||||||
|
<div class="stock-name">{{ stock.short_name }}</div>
|
||||||
|
<div class="stock-market">{{ stock.exchange }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 无搜索结果提示 -->
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
showDropdown && searchQuery.length >= 2 && stockOptions.length === 0 && !isSearching
|
||||||
|
"
|
||||||
|
class="no-results"
|
||||||
|
>
|
||||||
|
未找到匹配的股票
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="analyze-button"
|
||||||
|
@click="handleSearch"
|
||||||
|
:disabled="isAnalyzing || !selectedStock"
|
||||||
|
>
|
||||||
|
<span v-if="!isAnalyzing">AI分析</span>
|
||||||
|
<span v-else class="analyzing">
|
||||||
|
<span class="dot-animation">分析中</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedStock && !analysisResult" class="selected-stock-info">
|
||||||
|
<div class="stock-card">
|
||||||
|
<div class="stock-header">
|
||||||
|
<h3>{{ selectedStock.short_name }}</h3>
|
||||||
|
<span class="stock-code">{{ selectedStock.stock_code }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stock-details">
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">交易所:</span>
|
||||||
|
<span class="detail-value">{{ selectedStock.exchange }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">上市日期:</span>
|
||||||
|
<span class="detail-value">{{ formatListDate(selectedStock.list_date) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-section" v-if="analysisResult">
|
||||||
|
<div class="analysis-card">
|
||||||
|
<h2>AI分析报告</h2>
|
||||||
|
<div class="analysis-content">
|
||||||
|
<!-- 这里将根据API返回的数据结构进行调整 -->
|
||||||
|
<p>分析结果将在这里显示</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stock-analysis-view {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-section {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-section {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
position: relative;
|
||||||
|
background-color: var(--color-bg-secondary);
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 2.5rem 0.75rem 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
position: absolute;
|
||||||
|
right: 1rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-indicator {
|
||||||
|
position: absolute;
|
||||||
|
right: 1rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid var(--color-accent);
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-options {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 1.5rem;
|
||||||
|
right: 1.5rem;
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 10;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-option:hover {
|
||||||
|
background-color: var(--color-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-code {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-right: 1rem;
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-name {
|
||||||
|
flex: 1;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-market {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analyze-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: white;
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analyze-button:hover:not(:disabled) {
|
||||||
|
background-color: var(--color-accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.analyze-button:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-stock-info {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-card {
|
||||||
|
background-color: var(--color-bg-secondary);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-details {
|
||||||
|
margin-top: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
min-width: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.analyzing {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-animation {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-animation::after {
|
||||||
|
content: '...';
|
||||||
|
position: absolute;
|
||||||
|
animation: dots 1.5s infinite;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dots {
|
||||||
|
0%,
|
||||||
|
20% {
|
||||||
|
content: '.';
|
||||||
|
}
|
||||||
|
40%,
|
||||||
|
60% {
|
||||||
|
content: '..';
|
||||||
|
}
|
||||||
|
80%,
|
||||||
|
100% {
|
||||||
|
content: '...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.stock-analysis-view {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-options {
|
||||||
|
left: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.header-section {
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-option {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-code {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue
Block a user