web/src/views/StockAnalysisView.vue
2025-05-15 01:07:09 +08:00

513 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>