update
This commit is contained in:
parent
b6a9e72500
commit
ef33c1a3b3
22
src/App.vue
22
src/App.vue
@ -278,9 +278,21 @@ onUnmounted(() => {
|
|||||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="agent-name">Home</span>
|
<span class="agent-name">首页</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<RouterLink to="/ai-agent" class="agent-item" @click="showMobileMenu = false">
|
<RouterLink to="/crypto-analysis" class="agent-item" @click="showMobileMenu = false">
|
||||||
|
<svg
|
||||||
|
class="agent-icon"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M7 12l5-5 5 5M7 17l5-5 5 5" />
|
||||||
|
</svg>
|
||||||
|
<span class="agent-name">加密分析</span>
|
||||||
|
</RouterLink>
|
||||||
|
<RouterLink to="/astock-analysis" class="agent-item" @click="showMobileMenu = false">
|
||||||
<svg
|
<svg
|
||||||
class="agent-icon"
|
class="agent-icon"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -291,10 +303,12 @@ onUnmounted(() => {
|
|||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"
|
||||||
/>
|
/>
|
||||||
|
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
|
||||||
|
<line x1="12" y1="22.08" x2="12" y2="12"></line>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="agent-name">AI Agent</span>
|
<span class="agent-name">A股分析</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,14 @@ const router = createRouter({
|
|||||||
name: 'home',
|
name: 'home',
|
||||||
component: HomeView,
|
component: HomeView,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/ai-agents',
|
||||||
|
name: 'ai-agents',
|
||||||
|
component: () => import('../views/AIAgentsView.vue'),
|
||||||
|
meta: {
|
||||||
|
title: 'AI 专家团队',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/tools',
|
path: '/tools',
|
||||||
name: 'tools',
|
name: 'tools',
|
||||||
@ -64,6 +72,15 @@ const router = createRouter({
|
|||||||
title: '加密货币分析专家',
|
title: '加密货币分析专家',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/astock-analysis',
|
||||||
|
name: 'astock-analysis',
|
||||||
|
component: () => import('../views/AStockAnalysisView.vue'),
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
title: 'A股分析专家',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
240
src/views/AIAgentsView.vue
Normal file
240
src/views/AIAgentsView.vue
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const agents = [
|
||||||
|
{
|
||||||
|
id: 'crypto-analysis',
|
||||||
|
name: '加密货币AI分析专家',
|
||||||
|
description: '通过 AI 技术,获取加密货币的深度分析报告',
|
||||||
|
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M7 12l5-5 5 5M7 17l5-5 5 5"/>
|
||||||
|
</svg>`,
|
||||||
|
route: '/crypto-analysis',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'astock-analysis',
|
||||||
|
name: 'A股AI分析专家',
|
||||||
|
description: '通过 AI 技术,获取 A 股上市公司的深度分析报告',
|
||||||
|
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
|
||||||
|
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
|
||||||
|
<line x1="12" y1="22.08" x2="12" y2="12"></line>
|
||||||
|
</svg>`,
|
||||||
|
route: '/astock-analysis',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const navigateToAgent = (route: string) => {
|
||||||
|
router.push(route)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="ai-agents-view">
|
||||||
|
<div class="content-container">
|
||||||
|
<div class="header-section">
|
||||||
|
<h1 class="title">AI 专家团队</h1>
|
||||||
|
<p class="description">选择专业的 AI 助手,获取精准的分析和建议</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="agents-grid">
|
||||||
|
<div
|
||||||
|
v-for="agent in agents"
|
||||||
|
:key="agent.id"
|
||||||
|
class="agent-card"
|
||||||
|
@click="navigateToAgent(agent.route)"
|
||||||
|
>
|
||||||
|
<div class="agent-icon" v-html="agent.icon"></div>
|
||||||
|
<div class="agent-info">
|
||||||
|
<h3 class="agent-name">{{ agent.name }}</h3>
|
||||||
|
<p class="agent-description">{{ agent.description }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-arrow">
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M5 12h14M12 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ai-agents-view {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agents-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card {
|
||||||
|
background-color: var(--color-bg-secondary);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 1.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
box-shadow: 0 4px 12px rgba(var(--color-accent-rgb), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-name {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-description {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-arrow {
|
||||||
|
position: absolute;
|
||||||
|
right: 1.25rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card:hover .card-arrow {
|
||||||
|
color: var(--color-accent);
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(4px, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.ai-agents-view {
|
||||||
|
padding: 1rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agents-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-name {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-description {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.ai-agents-view {
|
||||||
|
padding: 0.75rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card {
|
||||||
|
padding: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-arrow {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.agent-card:hover {
|
||||||
|
background-color: rgba(var(--color-accent-rgb), 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
555
src/views/AStockAnalysisView.vue
Normal file
555
src/views/AStockAnalysisView.vue
Normal file
@ -0,0 +1,555 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, nextTick } from 'vue'
|
||||||
|
import { useUserStore } from '../stores/user'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const stockCode = ref('')
|
||||||
|
const isAnalyzing = ref(false)
|
||||||
|
const analysisContent = ref('')
|
||||||
|
const analysisContainer = ref<HTMLElement | null>(null)
|
||||||
|
const currentThought = ref('')
|
||||||
|
|
||||||
|
// 根据环境选择API基础URL
|
||||||
|
const apiBaseUrl =
|
||||||
|
import.meta.env.MODE === 'development' ? 'http://127.0.0.1:8000' : 'https://api.ibtc.work'
|
||||||
|
|
||||||
|
const scrollToBottom = async () => {
|
||||||
|
await nextTick()
|
||||||
|
if (analysisContainer.value) {
|
||||||
|
analysisContainer.value.scrollTop = analysisContainer.value.scrollHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAnalysis = async () => {
|
||||||
|
if (!stockCode.value || isAnalyzing.value) return
|
||||||
|
|
||||||
|
const code = stockCode.value.trim()
|
||||||
|
// 验证股票代码格式
|
||||||
|
if (!/^\d{6}$/.test(code)) {
|
||||||
|
analysisContent.value = '<div class="error-message">请输入正确的6位股票代码</div>'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isAnalyzing.value = true
|
||||||
|
analysisContent.value = ''
|
||||||
|
currentThought.value = '准备开始分析...'
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
if (userStore.authHeader) {
|
||||||
|
headers['Authorization'] = userStore.authHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${apiBaseUrl}/adata/${code}/analysis`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body?.getReader()
|
||||||
|
if (!reader) {
|
||||||
|
throw new Error('无法获取响应流')
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoder = new TextDecoder()
|
||||||
|
let buffer = ''
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read()
|
||||||
|
if (done) break
|
||||||
|
|
||||||
|
const chunk = decoder.decode(value, { stream: true })
|
||||||
|
buffer += chunk
|
||||||
|
|
||||||
|
const lines = buffer.split('\n')
|
||||||
|
buffer = lines.pop() || ''
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.trim() || !line.startsWith('data: ')) continue
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jsonStr = line.slice(6)
|
||||||
|
const data = JSON.parse(jsonStr)
|
||||||
|
|
||||||
|
switch (data.event) {
|
||||||
|
case 'workflow_started':
|
||||||
|
currentThought.value = '开始分析...'
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'text_chunk':
|
||||||
|
if (data.data && data.data.text) {
|
||||||
|
analysisContent.value += data.data.text
|
||||||
|
await scrollToBottom()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'workflow_finished':
|
||||||
|
if (data.data && data.data.outputs && data.data.outputs.text) {
|
||||||
|
// 如果最终结果与当前内容不同,则更新
|
||||||
|
if (analysisContent.value !== data.data.outputs.text) {
|
||||||
|
analysisContent.value = data.data.outputs.text
|
||||||
|
await scrollToBottom()
|
||||||
|
}
|
||||||
|
currentThought.value = `分析完成 (用时 ${Math.round(data.data.elapsed_time)}秒)`
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'error':
|
||||||
|
const errorMessage = data.error || '未知错误'
|
||||||
|
analysisContent.value = `<div class="error-message">分析过程中出现错误:${errorMessage}</div>`
|
||||||
|
currentThought.value = '分析过程出现错误'
|
||||||
|
await scrollToBottom()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析响应数据出错:', e)
|
||||||
|
analysisContent.value = '<div class="error-message">解析响应数据时出错,请稍后重试</div>'
|
||||||
|
currentThought.value = '数据解析出错'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('分析请求失败:', error)
|
||||||
|
analysisContent.value = '<div class="error-message">抱歉,分析请求失败,请稍后重试</div>'
|
||||||
|
currentThought.value = '请求失败'
|
||||||
|
} finally {
|
||||||
|
isAnalyzing.value = false
|
||||||
|
await scrollToBottom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearInput = () => {
|
||||||
|
stockCode.value = ''
|
||||||
|
analysisContent.value = ''
|
||||||
|
currentThought.value = ''
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="stock-analysis-view">
|
||||||
|
<div class="content-container">
|
||||||
|
<div class="initial-content" :class="{ minimized: analysisContent }">
|
||||||
|
<div class="header-section" :class="{ minimized: analysisContent }">
|
||||||
|
<h1 class="title">A股AI分析专家</h1>
|
||||||
|
<p class="description">通过 AI 技术,获取 A 股上市公司的深度分析报告</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search-section">
|
||||||
|
<div class="search-container" :class="{ 'is-analyzing': isAnalyzing }">
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<div class="input-container" :class="{ 'has-input': stockCode }">
|
||||||
|
<div class="input-area">
|
||||||
|
<input
|
||||||
|
v-model="stockCode"
|
||||||
|
type="text"
|
||||||
|
class="search-input"
|
||||||
|
:class="{ 'is-selected': stockCode }"
|
||||||
|
placeholder="请输入6位股票代码"
|
||||||
|
maxlength="6"
|
||||||
|
@keyup.enter="handleAnalysis"
|
||||||
|
:disabled="isAnalyzing"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="stockCode"
|
||||||
|
class="clear-button"
|
||||||
|
@click.stop="clearInput"
|
||||||
|
:disabled="isAnalyzing"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="analyze-button"
|
||||||
|
@click="handleAnalysis"
|
||||||
|
:disabled="!stockCode || isAnalyzing"
|
||||||
|
>
|
||||||
|
{{ isAnalyzing ? '分析中...' : '开始分析' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分析状态和结果区域 -->
|
||||||
|
<div v-if="isAnalyzing" class="analysis-status fade-in">
|
||||||
|
<div class="progress-dots">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
<p class="status-text">
|
||||||
|
<span class="status-label">AI分析进行中</span>
|
||||||
|
<span class="thought-text" v-if="currentThought">{{ currentThought }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="analysisContent"
|
||||||
|
class="analysis-container"
|
||||||
|
ref="analysisContainer"
|
||||||
|
:class="{ 'fade-in': analysisContent }"
|
||||||
|
>
|
||||||
|
<div class="analysis-content" v-html="analysisContent"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stock-analysis-view {
|
||||||
|
min-height: 100vh;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section {
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section.minimized {
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.initial-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100%;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
gap: 2rem;
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.initial-content.minimized {
|
||||||
|
min-height: auto;
|
||||||
|
padding: 0;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section.minimized .title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section.minimized .description {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-section {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
position: relative;
|
||||||
|
padding: 1.5rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-container {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
border: 2px solid var(--color-accent);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-area {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
outline: none;
|
||||||
|
height: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.5rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-button svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-status {
|
||||||
|
text-align: center;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-dots {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-dots span {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.3;
|
||||||
|
animation: pulse-dot 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-dots span:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-dots span:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thought-text {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-content {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
line-height: 1.8;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.error-message) {
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background-color: rgba(255, 0, 0, 0.05);
|
||||||
|
border: 1px solid rgba(255, 0, 0, 0.1);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
color: #ff4444;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-dot {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.stock-analysis-view {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.initial-content {
|
||||||
|
padding: 1rem 0.5rem;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.initial-content.minimized {
|
||||||
|
padding: 0;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section.minimized .title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-container {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-content {
|
||||||
|
font-size: 1rem;
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.initial-content {
|
||||||
|
padding: 0.75rem 0.5rem;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analyze-button {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
padding: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-container {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-content {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,29 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, nextTick } from 'vue'
|
import { ref, nextTick } from 'vue'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
|
|
||||||
interface CryptoOption {
|
|
||||||
symbol: string
|
|
||||||
base_asset: string
|
|
||||||
quote_asset: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AnalysisRequest {
|
interface AnalysisRequest {
|
||||||
symbol: string
|
symbol: string
|
||||||
timeframe?: string
|
timeframe?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const searchQuery = ref('')
|
const cryptoCode = ref('')
|
||||||
const selectedCrypto = ref<CryptoOption | null>(null)
|
|
||||||
const isAnalyzing = ref(false)
|
const isAnalyzing = ref(false)
|
||||||
const showDropdown = ref(false)
|
|
||||||
const isSearching = ref(false)
|
|
||||||
const cryptoOptions = ref<CryptoOption[]>([])
|
|
||||||
const analysisContent = ref('')
|
const analysisContent = ref('')
|
||||||
const analysisContainer = ref<HTMLElement | null>(null)
|
const analysisContainer = ref<HTMLElement | null>(null)
|
||||||
const showSearchTips = ref(false)
|
|
||||||
const isFirstAnalysis = ref(true)
|
|
||||||
const currentThought = ref('')
|
const currentThought = ref('')
|
||||||
const selectedTimeframe = ref('all')
|
const selectedTimeframe = ref('all')
|
||||||
|
|
||||||
@ -39,73 +27,6 @@ const timeframes = [
|
|||||||
const apiBaseUrl =
|
const apiBaseUrl =
|
||||||
import.meta.env.MODE === 'development' ? 'http://127.0.0.1:8000' : 'https://api.ibtc.work'
|
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.length === 1) {
|
|
||||||
showSearchTips.value = true
|
|
||||||
setTimeout(() => {
|
|
||||||
showSearchTips.value = false
|
|
||||||
}, 3000)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!query || query.length < 2) {
|
|
||||||
cryptoOptions.value = []
|
|
||||||
showDropdown.value = false
|
|
||||||
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}/crypto/crypto/search/${encodeURIComponent(query)}`,
|
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
headers,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('搜索请求失败')
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
cryptoOptions.value = data
|
|
||||||
showDropdown.value = true
|
|
||||||
} catch (error) {
|
|
||||||
console.error('搜索加密货币失败:', error)
|
|
||||||
cryptoOptions.value = []
|
|
||||||
} finally {
|
|
||||||
isSearching.value = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const selectCrypto = (crypto: CryptoOption) => {
|
|
||||||
selectedCrypto.value = crypto
|
|
||||||
searchQuery.value = `${crypto.base_asset}/${crypto.quote_asset}`
|
|
||||||
showDropdown.value = false
|
|
||||||
|
|
||||||
// 自动触发分析
|
|
||||||
if (isFirstAnalysis.value) {
|
|
||||||
// 首次选择时显示提示,并延迟开始分析
|
|
||||||
isFirstAnalysis.value = false
|
|
||||||
setTimeout(() => {
|
|
||||||
handleAnalysis()
|
|
||||||
}, 500)
|
|
||||||
} else {
|
|
||||||
handleAnalysis()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollToBottom = async () => {
|
const scrollToBottom = async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
if (analysisContainer.value) {
|
if (analysisContainer.value) {
|
||||||
@ -113,8 +34,16 @@ const scrollToBottom = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理回车键
|
||||||
|
const handleKeydown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault()
|
||||||
|
handleAnalysis()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleAnalysis = async () => {
|
const handleAnalysis = async () => {
|
||||||
if (!selectedCrypto.value || isAnalyzing.value) return
|
if (!cryptoCode.value.trim() || isAnalyzing.value) return
|
||||||
|
|
||||||
isAnalyzing.value = true
|
isAnalyzing.value = true
|
||||||
analysisContent.value = ''
|
analysisContent.value = ''
|
||||||
@ -129,7 +58,7 @@ const handleAnalysis = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const requestData: AnalysisRequest = {
|
const requestData: AnalysisRequest = {
|
||||||
symbol: selectedCrypto.value.base_asset,
|
symbol: cryptoCode.value.trim().toUpperCase(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只有当不是 'all' 时才添加 timeframe
|
// 只有当不是 'all' 时才添加 timeframe
|
||||||
@ -206,75 +135,58 @@ const handleAnalysis = async () => {
|
|||||||
currentThought.value = '请求失败'
|
currentThought.value = '请求失败'
|
||||||
} finally {
|
} finally {
|
||||||
isAnalyzing.value = false
|
isAnalyzing.value = false
|
||||||
currentThought.value = ''
|
|
||||||
await scrollToBottom()
|
await scrollToBottom()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 点击外部关闭下拉框
|
// 清除输入
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const clearInput = () => {
|
||||||
const target = event.target as HTMLElement
|
cryptoCode.value = ''
|
||||||
if (!target.closest('.search-container')) {
|
|
||||||
showDropdown.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在 script setup 中添加清除选择的方法
|
|
||||||
const clearSelection = () => {
|
|
||||||
selectedCrypto.value = null
|
|
||||||
searchQuery.value = ''
|
|
||||||
analysisContent.value = ''
|
analysisContent.value = ''
|
||||||
currentThought.value = ''
|
currentThought.value = ''
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="crypto-analysis-view" @click="handleClickOutside">
|
<div class="crypto-analysis-view">
|
||||||
<div class="content-container">
|
<div class="content-container">
|
||||||
<div class="initial-content" :class="{ minimized: selectedCrypto }">
|
<div class="initial-content" :class="{ minimized: analysisContent }">
|
||||||
<div class="header-section" :class="{ minimized: selectedCrypto }">
|
<div class="header-section" :class="{ minimized: analysisContent }">
|
||||||
<h1 class="title">加密货币分析专家</h1>
|
<h1 class="title">加密货币AI分析专家</h1>
|
||||||
<p class="description">
|
<p class="description">通过 AI 技术,获取加密货币的深度分析报告</p>
|
||||||
输入加密货币名称或代号(如:BTC、ETH),获取AI驱动的专业分析报告
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-section">
|
<div class="search-section">
|
||||||
<div class="search-container" :class="{ 'is-analyzing': isAnalyzing }">
|
<div class="search-container" :class="{ 'is-analyzing': isAnalyzing }">
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<div class="input-container" :class="{ 'has-selection': selectedCrypto }">
|
<div class="input-container">
|
||||||
<div class="input-area">
|
<div class="input-area">
|
||||||
<template v-if="selectedCrypto">
|
|
||||||
<div class="selected-tag">
|
|
||||||
<span class="tag-text">{{ selectedCrypto.base_asset }}</span>
|
|
||||||
<button class="tag-remove" @click.stop="clearSelection">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
>
|
|
||||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
||||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<input
|
<input
|
||||||
v-show="!selectedCrypto"
|
v-model="cryptoCode"
|
||||||
v-model="searchQuery"
|
|
||||||
type="text"
|
type="text"
|
||||||
class="search-input"
|
class="search-input"
|
||||||
:class="{ 'is-selected': selectedCrypto }"
|
placeholder="请输入加密货币代码,如 BTC"
|
||||||
placeholder="请输入加密货币名称或代号(如:BTC、ETH)"
|
@keydown="handleKeydown"
|
||||||
@focus="showDropdown = searchQuery.length >= 2 && cryptoOptions.length > 0"
|
|
||||||
:disabled="isAnalyzing"
|
:disabled="isAnalyzing"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
v-if="cryptoCode.trim() || analysisContent"
|
||||||
|
class="clear-button"
|
||||||
|
@click="clearInput"
|
||||||
|
:disabled="isAnalyzing"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- 时间周期选择 -->
|
<!-- 时间周期选择 -->
|
||||||
<div class="timeframe-selector">
|
<div class="timeframe-selector">
|
||||||
@ -288,61 +200,30 @@ const clearSelection = () => {
|
|||||||
<span class="timeframe-label">{{ timeframe.label }}</span>
|
<span class="timeframe-label">{{ timeframe.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
class="analyze-button"
|
||||||
|
@click="handleAnalysis"
|
||||||
|
:disabled="isAnalyzing || !cryptoCode.trim()"
|
||||||
|
>
|
||||||
|
{{ isAnalyzing ? '分析中...' : '开始分析' }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 搜索提示 -->
|
|
||||||
<div v-if="showSearchTips" class="search-tips">继续输入以搜索加密货币...</div>
|
|
||||||
|
|
||||||
<!-- 加密货币选项下拉框 -->
|
|
||||||
<div
|
|
||||||
v-if="showDropdown && cryptoOptions.length > 0"
|
|
||||||
class="crypto-options"
|
|
||||||
:class="{ 'fade-in': showDropdown }"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="crypto in cryptoOptions"
|
|
||||||
:key="crypto.symbol"
|
|
||||||
class="crypto-option"
|
|
||||||
@click="selectCrypto(crypto)"
|
|
||||||
>
|
|
||||||
<div class="crypto-name">{{ crypto.base_asset }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 无搜索结果提示 -->
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
showDropdown &&
|
|
||||||
searchQuery.length >= 2 &&
|
|
||||||
cryptoOptions.length === 0 &&
|
|
||||||
!isSearching
|
|
||||||
"
|
|
||||||
class="no-results fade-in"
|
|
||||||
>
|
|
||||||
未找到匹配的加密货币
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分析状态和结果区域 -->
|
<!-- 分析状态和结果区域 -->
|
||||||
<div v-if="selectedCrypto && isAnalyzing" class="selected-crypto-info fade-in">
|
<div v-if="isAnalyzing" class="analysis-status fade-in">
|
||||||
<div class="crypto-card" :class="{ 'is-analyzing': isAnalyzing }">
|
<div class="progress-dots">
|
||||||
<div class="analysis-status">
|
<span></span>
|
||||||
<div class="analysis-progress">
|
<span></span>
|
||||||
<div class="progress-dots">
|
<span></span>
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="status-text">
|
|
||||||
<span class="status-label">AI分析进行中</span>
|
|
||||||
<span class="thought-text" v-if="currentThought">{{ currentThought }}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="status-text">
|
||||||
|
<span class="status-label">AI分析进行中</span>
|
||||||
|
<span class="thought-text" v-if="currentThought">{{ currentThought }}</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -484,7 +365,7 @@ const clearSelection = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 2.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-container:hover {
|
.input-container:hover {
|
||||||
@ -607,6 +488,34 @@ const clearSelection = () => {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clear-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.5rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-button:hover:not(:disabled) {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-button:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-button svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.selected-crypto-info {
|
.selected-crypto-info {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
@ -926,6 +835,15 @@ const clearSelection = () => {
|
|||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.timeframe-selector {
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeframe-selector .timeframe-label {
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
@ -961,6 +879,20 @@ const clearSelection = () => {
|
|||||||
.analysis-container {
|
.analysis-container {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.timeframe-selector {
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeframe-selector .timeframe-label {
|
||||||
|
padding: 0.15rem 0.4rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analyze-button {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
padding: 0.6rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-container.is-analyzing {
|
.search-container.is-analyzing {
|
||||||
@ -1080,13 +1012,23 @@ const clearSelection = () => {
|
|||||||
.timeframe-selector {
|
.timeframe-selector {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏滚动条 */
|
||||||
|
.timeframe-selector::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeframe-selector .timeframe-option {
|
.timeframe-selector .timeframe-option {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeframe-selector .timeframe-label {
|
.timeframe-selector .timeframe-label {
|
||||||
@ -1097,16 +1039,20 @@ const clearSelection = () => {
|
|||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
background-color: var(--color-bg-secondary);
|
background-color: var(--color-bg-secondary);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeframe-selector .timeframe-option:not(.active):hover .timeframe-label {
|
.timeframe-selector .timeframe-option:not(.active):hover .timeframe-label {
|
||||||
background-color: rgba(var(--color-accent-rgb), 0.1);
|
border-color: var(--color-accent);
|
||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeframe-selector .timeframe-option.active .timeframe-label {
|
.timeframe-selector .timeframe-option.active .timeframe-label {
|
||||||
background-color: var(--color-accent);
|
background-color: transparent;
|
||||||
color: white;
|
color: var(--color-accent);
|
||||||
|
border: 1px solid var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
@ -1115,7 +1061,84 @@ const clearSelection = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timeframe-selector .timeframe-option:not(.active):hover .timeframe-label {
|
.timeframe-selector .timeframe-option:not(.active):hover .timeframe-label {
|
||||||
background-color: rgba(var(--color-accent-rgb), 0.2);
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-area {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button:hover:not(:disabled) {
|
||||||
|
background-color: var(--color-accent-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
stroke: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button-text {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.search-button {
|
||||||
|
padding: 0.4rem 0.75rem;
|
||||||
|
min-width: 80px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-button:hover:not(:disabled) {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const isAuthenticated = computed(() => userStore.isAuthenticated)
|
const isAuthenticated = computed(() => userStore.isAuthenticated)
|
||||||
@ -25,7 +24,7 @@ const openAuthModal = (mode: 'login' | 'register') => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="hero-actions" v-else>
|
<div class="hero-actions" v-else>
|
||||||
<div class="btn-wrapper">
|
<div class="btn-wrapper">
|
||||||
<RouterLink to="/ai-agent" class="btn btn-primary">开始提问</RouterLink>
|
<RouterLink to="/ai-agents" class="btn btn-primary">开始分析</RouterLink>
|
||||||
<span class="free-tag">Beta</span>
|
<span class="free-tag">Beta</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user