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"
|
||||
/>
|
||||
</svg>
|
||||
<span class="agent-name">Home</span>
|
||||
<span class="agent-name">首页</span>
|
||||
</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
|
||||
class="agent-icon"
|
||||
viewBox="0 0 24 24"
|
||||
@ -291,10 +303,12 @@ onUnmounted(() => {
|
||||
<path
|
||||
stroke-linecap="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>
|
||||
<span class="agent-name">AI Agent</span>
|
||||
<span class="agent-name">A股分析</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
|
||||
@ -12,6 +12,14 @@ const router = createRouter({
|
||||
name: 'home',
|
||||
component: HomeView,
|
||||
},
|
||||
{
|
||||
path: '/ai-agents',
|
||||
name: 'ai-agents',
|
||||
component: () => import('../views/AIAgentsView.vue'),
|
||||
meta: {
|
||||
title: 'AI 专家团队',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/tools',
|
||||
name: 'tools',
|
||||
@ -64,6 +72,15 @@ const router = createRouter({
|
||||
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">
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
interface CryptoOption {
|
||||
symbol: string
|
||||
base_asset: string
|
||||
quote_asset: string
|
||||
}
|
||||
|
||||
interface AnalysisRequest {
|
||||
symbol: string
|
||||
timeframe?: string
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
const searchQuery = ref('')
|
||||
const selectedCrypto = ref<CryptoOption | null>(null)
|
||||
const cryptoCode = ref('')
|
||||
const isAnalyzing = ref(false)
|
||||
const showDropdown = ref(false)
|
||||
const isSearching = ref(false)
|
||||
const cryptoOptions = ref<CryptoOption[]>([])
|
||||
const analysisContent = ref('')
|
||||
const analysisContainer = ref<HTMLElement | null>(null)
|
||||
const showSearchTips = ref(false)
|
||||
const isFirstAnalysis = ref(true)
|
||||
const currentThought = ref('')
|
||||
const selectedTimeframe = ref('all')
|
||||
|
||||
@ -39,73 +27,6 @@ const timeframes = [
|
||||
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.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 () => {
|
||||
await nextTick()
|
||||
if (analysisContainer.value) {
|
||||
@ -113,8 +34,16 @@ const scrollToBottom = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理回车键
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
handleAnalysis()
|
||||
}
|
||||
}
|
||||
|
||||
const handleAnalysis = async () => {
|
||||
if (!selectedCrypto.value || isAnalyzing.value) return
|
||||
if (!cryptoCode.value.trim() || isAnalyzing.value) return
|
||||
|
||||
isAnalyzing.value = true
|
||||
analysisContent.value = ''
|
||||
@ -129,7 +58,7 @@ const handleAnalysis = async () => {
|
||||
}
|
||||
|
||||
const requestData: AnalysisRequest = {
|
||||
symbol: selectedCrypto.value.base_asset,
|
||||
symbol: cryptoCode.value.trim().toUpperCase(),
|
||||
}
|
||||
|
||||
// 只有当不是 'all' 时才添加 timeframe
|
||||
@ -206,75 +135,58 @@ const handleAnalysis = async () => {
|
||||
currentThought.value = '请求失败'
|
||||
} finally {
|
||||
isAnalyzing.value = false
|
||||
currentThought.value = ''
|
||||
await scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
// 点击外部关闭下拉框
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement
|
||||
if (!target.closest('.search-container')) {
|
||||
showDropdown.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 在 script setup 中添加清除选择的方法
|
||||
const clearSelection = () => {
|
||||
selectedCrypto.value = null
|
||||
searchQuery.value = ''
|
||||
// 清除输入
|
||||
const clearInput = () => {
|
||||
cryptoCode.value = ''
|
||||
analysisContent.value = ''
|
||||
currentThought.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="crypto-analysis-view" @click="handleClickOutside">
|
||||
<div class="crypto-analysis-view">
|
||||
<div class="content-container">
|
||||
<div class="initial-content" :class="{ minimized: selectedCrypto }">
|
||||
<div class="header-section" :class="{ minimized: selectedCrypto }">
|
||||
<h1 class="title">加密货币分析专家</h1>
|
||||
<p class="description">
|
||||
输入加密货币名称或代号(如:BTC、ETH),获取AI驱动的专业分析报告
|
||||
</p>
|
||||
<div class="initial-content" :class="{ minimized: analysisContent }">
|
||||
<div class="header-section" :class="{ minimized: analysisContent }">
|
||||
<h1 class="title">加密货币AI分析专家</h1>
|
||||
<p class="description">通过 AI 技术,获取加密货币的深度分析报告</p>
|
||||
</div>
|
||||
|
||||
<div class="search-section">
|
||||
<div class="search-container" :class="{ 'is-analyzing': isAnalyzing }">
|
||||
<div class="input-wrapper">
|
||||
<div class="input-container" :class="{ 'has-selection': selectedCrypto }">
|
||||
<div class="input-container">
|
||||
<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
|
||||
v-show="!selectedCrypto"
|
||||
v-model="searchQuery"
|
||||
v-model="cryptoCode"
|
||||
type="text"
|
||||
class="search-input"
|
||||
:class="{ 'is-selected': selectedCrypto }"
|
||||
placeholder="请输入加密货币名称或代号(如:BTC、ETH)"
|
||||
@focus="showDropdown = searchQuery.length >= 2 && cryptoOptions.length > 0"
|
||||
placeholder="请输入加密货币代码,如 BTC"
|
||||
@keydown="handleKeydown"
|
||||
: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 class="timeframe-selector">
|
||||
@ -288,61 +200,30 @@ const clearSelection = () => {
|
||||
<span class="timeframe-label">{{ timeframe.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="analyze-button"
|
||||
@click="handleAnalysis"
|
||||
:disabled="isAnalyzing || !cryptoCode.trim()"
|
||||
>
|
||||
{{ isAnalyzing ? '分析中...' : '开始分析' }}
|
||||
</button>
|
||||
</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 v-if="selectedCrypto && isAnalyzing" class="selected-crypto-info fade-in">
|
||||
<div class="crypto-card" :class="{ 'is-analyzing': isAnalyzing }">
|
||||
<div class="analysis-status">
|
||||
<div class="analysis-progress">
|
||||
<div class="progress-dots">
|
||||
<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 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
|
||||
@ -484,7 +365,7 @@ const clearSelection = () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 2.5rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.input-container:hover {
|
||||
@ -607,6 +488,34 @@ const clearSelection = () => {
|
||||
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 {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
@ -926,6 +835,15 @@ const clearSelection = () => {
|
||||
width: 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) {
|
||||
@ -961,6 +879,20 @@ const clearSelection = () => {
|
||||
.analysis-container {
|
||||
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 {
|
||||
@ -1080,13 +1012,23 @@ const clearSelection = () => {
|
||||
.timeframe-selector {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
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 {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.timeframe-selector .timeframe-label {
|
||||
@ -1097,16 +1039,20 @@ const clearSelection = () => {
|
||||
color: var(--color-text-secondary);
|
||||
background-color: var(--color-bg-secondary);
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.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);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.timeframe-selector .timeframe-option.active .timeframe-label {
|
||||
background-color: var(--color-accent);
|
||||
color: white;
|
||||
background-color: transparent;
|
||||
color: var(--color-accent);
|
||||
border: 1px solid var(--color-accent);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@ -1115,7 +1061,84 @@ const clearSelection = () => {
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const isAuthenticated = computed(() => userStore.isAuthenticated)
|
||||
@ -25,7 +24,7 @@ const openAuthModal = (mode: 'login' | 'register') => {
|
||||
</div>
|
||||
<div class="hero-actions" v-else>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user