513 lines
11 KiB
Vue
513 lines
11 KiB
Vue
<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>
|