web/src/views/CryptoAnalysisView.vue
2025-05-17 20:59:12 +08:00

1479 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<script setup lang="ts">
import { ref, nextTick } from 'vue'
import { useUserStore } from '../stores/user'
interface AnalysisRequest {
symbol: string
timeframe?: string
}
const userStore = useUserStore()
const cryptoCode = ref('')
const isAnalyzing = ref(false)
const analysisContent = ref('')
const analysisContainer = ref<HTMLElement | null>(null)
const currentThought = ref('')
const selectedTimeframe = ref('15m')
const showInitialView = ref(true)
const copySuccess = ref(false)
const timeframes = [
{ label: '15分钟', value: '15m' },
{ label: '1小时', value: '1h' },
{ label: '4小时', value: '4h' },
{ label: '1天', value: '1d' },
]
// 根据环境选择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 handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault()
handleAnalysis()
}
}
const handleAnalysis = async () => {
if (!cryptoCode.value.trim() || isAnalyzing.value) return
showInitialView.value = false
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 requestData: AnalysisRequest = {
symbol: cryptoCode.value.trim().toUpperCase(),
timeframe: selectedTimeframe.value,
}
const response = await fetch(`${apiBaseUrl}/crypto/analysis_v2`, {
method: 'POST',
headers,
body: JSON.stringify(requestData),
})
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 (error) {
console.error('解析响应数据出错:', error)
analysisContent.value = '<div class="error-message">解析响应数据时出错,请稍后重试</div>'
currentThought.value = '数据解析出错'
}
}
}
} catch {
analysisContent.value = '<div class="error-message">抱歉,分析请求失败,请稍后重试</div>'
currentThought.value = '请求失败'
} finally {
isAnalyzing.value = false
await scrollToBottom()
}
}
const resetView = () => {
showInitialView.value = true
cryptoCode.value = ''
analysisContent.value = ''
currentThought.value = ''
isAnalyzing.value = false
}
// 清除输入
const clearInput = () => {
cryptoCode.value = ''
analysisContent.value = ''
currentThought.value = ''
}
const copyAnalysis = async () => {
if (!analysisContent.value) return
try {
const tempDiv = document.createElement('div')
tempDiv.innerHTML = analysisContent.value
const textContent = tempDiv.textContent || tempDiv.innerText || ''
await navigator.clipboard.writeText(textContent)
copySuccess.value = true
setTimeout(() => {
copySuccess.value = false
}, 2000)
} catch {
console.error('复制失败')
}
}
// 添加快速分析列表数据
const commonAnalysisList = [
{ code: 'BTC', timeframe: '15m', label: 'BTC 15分钟' },
{ code: 'BTC', timeframe: '1h', label: 'BTC 1小时' },
{ code: 'BTC', timeframe: '4h', label: 'BTC 4小时' },
{ code: 'BTC', timeframe: '1d', label: 'BTC 日线' },
{ code: 'ETH', timeframe: '15m', label: 'ETH 15分钟' },
{ code: 'ETH', timeframe: '1h', label: 'ETH 1小时' },
{ code: 'ETH', timeframe: '4h', label: 'ETH 4小时' },
{ code: 'ETH', timeframe: '1d', label: 'ETH 日线' },
]
// 处理快速分析项的点击
const handleCommonAnalysis = (item: { code: string; timeframe: string }) => {
cryptoCode.value = item.code
selectedTimeframe.value = item.timeframe
handleAnalysis()
}
</script>
<template>
<div class="crypto-analysis-view">
<div class="content-container">
<!-- 初始视图 -->
<div v-if="showInitialView" class="initial-content">
<div class="header-section">
<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">
<div class="input-area">
<input
v-model="cryptoCode"
type="text"
class="search-input"
placeholder="请输入BTC、ETH仅支持输入一个分析。"
@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">
<div
v-for="timeframe in timeframes"
:key="timeframe.value"
class="timeframe-option"
:class="{ active: selectedTimeframe === timeframe.value }"
@click="selectedTimeframe = timeframe.value"
>
<span class="timeframe-label">{{ timeframe.label }}</span>
</div>
</div>
<button
class="analyze-button"
@click="handleAnalysis"
:disabled="isAnalyzing || !cryptoCode.trim()"
>
{{ isAnalyzing ? '分析中...' : '开始分析' }}
</button>
</div>
</div>
</div>
</div>
<!-- 快速分析列表 -->
<div class="common-analysis-section">
<div class="common-analysis-container">
<div class="section-header">
<span class="section-title">快速分析</span>
</div>
<div class="common-analysis-list">
<button
v-for="item in commonAnalysisList"
:key="item.label"
class="common-analysis-item"
@click="handleCommonAnalysis(item)"
:disabled="isAnalyzing"
>
{{ item.label }}
</button>
</div>
</div>
</div>
</div>
<!-- 分析视图 -->
<div v-else class="analysis-view">
<div class="analysis-header">
<div class="target-info">
<span class="label">正在分析:</span>
<span class="value">{{ cryptoCode.toUpperCase() }}</span>
<span class="timeframe">{{
timeframes.find((t) => t.value === selectedTimeframe)?.label
}}</span>
</div>
<div class="action-buttons" v-if="!isAnalyzing">
<button class="action-button" @click="resetView">
<svg
class="icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="22 12 18 12 15 12"></polyline>
<path d="M11.5 3a17 17 0 0 0-6.5 2.5L2 8"></path>
<path d="M2 3v5h5"></path>
<path d="M13.5 21a17 17 0 0 0 6.5-2.5L22 16"></path>
<path d="M22 21v-5h-5"></path>
</svg>
<span>重新分析</span>
</button>
<button
class="action-button copy"
@click="copyAnalysis"
:class="{ success: copySuccess }"
>
<span v-if="!copySuccess">复制结果</span>
<span v-else>复制成功</span>
</button>
</div>
</div>
<div v-if="isAnalyzing" class="analysis-status">
<div class="progress-dots">
<span></span>
<span></span>
<span></span>
</div>
<div class="status-text">
<div class="status-label">AI 正在分析</div>
</div>
</div>
<div
class="analysis-container"
ref="analysisContainer"
:class="{ 'fade-in': analysisContent }"
>
<div class="analysis-content" v-html="analysisContent"></div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
/* 移除不需要的时间周期选择样式 */
.timeframe-section {
display: none;
}
.crypto-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: 800px;
margin: 0 auto;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
justify-content: center;
}
.initial-content {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 15vh;
gap: 1.5rem;
transition: all 0.5s ease;
}
.header-section {
text-align: center;
transition: all 0.5s ease;
}
.title {
font-size: 2rem;
font-weight: 700;
color: var(--color-text-primary);
margin-bottom: 0.5rem;
transition: all 0.5s ease;
}
.description {
font-size: 0.95rem;
color: var(--color-text-secondary);
transition: all 0.5s ease;
}
.search-section {
width: 100%;
max-width: 800px;
transition: all 0.5s ease;
padding: 0 1.5rem;
}
.main-section {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.main-section.centered {
justify-content: center;
padding: 2rem 0;
}
.main-section.top-aligned {
justify-content: flex-start;
padding-top: 1rem;
}
.search-container {
position: relative;
padding: 1.5rem;
transition: all 0.3s ease;
}
.input-wrapper {
position: relative;
margin-bottom: 0;
}
.input-container {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
min-height: 5.5rem;
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%;
gap: 0.5rem;
}
.input-container:hover {
border-color: var(--color-accent);
opacity: 0.8;
}
.input-container.has-selection {
background-color: transparent;
}
.selected-tag {
display: flex;
align-items: center;
background-color: var(--color-accent);
color: white;
padding: 0.5rem 0.75rem;
border-radius: var(--border-radius);
margin-right: 0.5rem;
font-size: 1rem;
font-weight: 500;
transition: all 0.3s ease;
gap: 0.5rem;
}
.tag-text {
margin-right: 0.25rem;
}
.tag-remove {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.15rem;
border: none;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
line-height: 0;
}
.tag-remove:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
.tag-remove svg {
width: 14px;
height: 14px;
stroke: currentColor;
stroke-width: 2.5;
}
.search-input {
flex: 1;
border: none;
background: none;
padding: 0;
font-size: 0.95rem;
color: var(--color-text-primary);
outline: none;
height: 2.5rem;
}
.crypto-options {
position: absolute;
top: calc(100% + 5px);
left: 0;
right: 0;
background-color: var(--color-bg-primary);
border: 2px solid var(--color-border);
border-radius: var(--border-radius);
max-height: 300px;
overflow-y: auto;
z-index: 10;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
margin: 0 1.5rem;
}
.crypto-option {
padding: 1rem 1.5rem;
cursor: pointer;
transition: background-color 0.2s ease;
border-bottom: 1px solid var(--color-border);
}
.crypto-option:last-child {
border-bottom: none;
}
.crypto-option:hover {
background-color: var(--color-bg-secondary);
}
.crypto-name {
font-weight: 500;
color: var(--color-text-primary);
}
.analyze-button {
width: 100%;
padding: 0.75rem;
font-size: 0.9rem;
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;
}
.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;
}
.crypto-card {
width: 100%;
max-width: 800px;
margin: 0 auto;
}
.crypto-card.is-analyzing {
position: relative;
overflow: hidden;
}
.analysis-status {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 1rem;
text-align: center;
gap: 1rem;
margin-top: 1rem;
}
.progress-dots {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.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 {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
}
.status-label {
font-weight: 500;
color: var(--color-accent);
font-size: 0.85rem;
}
.thought-text {
font-size: 0.9rem;
color: var(--color-text-secondary);
}
.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;
}
.analysis-container {
flex: 1;
min-height: 0;
overflow-y: auto;
border-radius: var(--border-radius);
margin-top: 1rem;
display: flex;
flex-direction: column;
}
.analysis-container.fade-in {
background-color: var(--color-bg-secondary);
}
.analysis-content {
font-size: 1rem;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
color: var(--color-text-primary);
padding: 1.5rem 2rem;
}
.analysis-content :deep(h1),
.analysis-content :deep(h2),
.analysis-content :deep(h3),
.analysis-content :deep(h4) {
margin-top: 1.5em;
margin-bottom: 0.8em;
font-weight: 600;
line-height: 1.4;
}
.analysis-content :deep(p) {
margin-bottom: 1em;
white-space: pre-wrap;
}
.analysis-content :deep(ul),
.analysis-content :deep(ol) {
margin: 1em 0;
padding-left: 2em;
}
.analysis-content :deep(li) {
margin: 0.5em 0;
line-height: 1.6;
}
.analysis-content :deep(code) {
font-family: monospace;
background-color: rgba(var(--color-accent-rgb), 0.1);
padding: 0.2em 0.4em;
border-radius: 3px;
font-size: 0.9em;
}
.analysis-content :deep(pre) {
background-color: rgba(var(--color-accent-rgb), 0.05);
padding: 1em;
border-radius: var(--border-radius);
overflow-x: auto;
margin: 1em 0;
}
.analysis-content :deep(pre code) {
background-color: transparent;
padding: 0;
font-size: 0.9em;
line-height: 1.5;
}
.analysis-content :deep(blockquote) {
border-left: 4px solid var(--color-accent);
margin: 1em 0;
padding: 0.5em 0 0.5em 1em;
background-color: rgba(var(--color-accent-rgb), 0.05);
}
.analysis-content :deep(table) {
width: 100%;
border-collapse: collapse;
margin: 1em 0;
}
.analysis-content :deep(th),
.analysis-content :deep(td) {
border: 1px solid var(--color-border);
padding: 0.5em;
text-align: left;
}
.analysis-content :deep(th) {
background-color: rgba(var(--color-accent-rgb), 0.05);
font-weight: 600;
}
:deep(.thought-step) {
display: flex;
align-items: flex-start;
gap: 0.75rem;
margin: 1rem 0;
padding: 0.75rem;
background-color: rgba(var(--color-accent-rgb), 0.05);
border-radius: 0.5rem;
border: 1px solid rgba(var(--color-accent-rgb), 0.1);
}
:deep(.thought-icon) {
font-size: 1.2rem;
line-height: 1;
flex-shrink: 0;
}
:deep(.thought-content) {
color: var(--color-text-secondary);
font-size: 0.95rem;
line-height: 1.5;
}
: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: 0.5rem;
color: #ff4444;
font-size: 0.95rem;
}
.analysis-content::-webkit-scrollbar {
display: none;
}
@media (max-width: 768px) {
.crypto-analysis-view {
padding: 0.75rem;
}
.analysis-view {
padding: 0.5rem;
margin-top: 4.5rem;
}
.analysis-header {
padding: 1rem;
flex-direction: column;
gap: 0.75rem;
align-items: flex-start;
}
.action-buttons {
width: 100%;
flex-direction: row;
gap: 0.75rem;
}
.action-button {
flex: 1;
justify-content: center;
padding: 0.6rem;
font-size: 0.85rem;
}
.analysis-content {
padding: 1.25rem 1.5rem;
font-size: 0.95rem;
}
.initial-content {
margin-bottom: 12vh;
gap: 1.5rem;
}
.title {
font-size: 1.75rem;
}
.description {
font-size: 0.9rem;
}
.search-input {
font-size: 0.9rem;
}
.timeframe-selector .timeframe-label {
font-size: 0.85rem;
}
.analyze-button {
font-size: 0.85rem;
}
.search-container {
padding: 1rem;
}
.search-section {
padding: 0 1rem;
}
.search-history-container {
padding: 1rem;
}
.search-history-section {
padding: 0 1rem;
}
.history-code {
padding: 0.25rem 0.6rem;
font-size: 0.85rem;
}
.history-code .timeframe {
font-size: 0.8rem;
padding: 0.1rem 0.3rem;
}
}
@media (max-width: 480px) {
.crypto-analysis-view {
padding: 0.5rem;
}
.analysis-view {
padding: 0.5rem;
}
.analysis-header {
padding: 1rem;
}
.target-info {
flex-wrap: wrap;
}
.target-info .label {
font-size: 0.8rem;
}
.target-info .value {
font-size: 0.85rem;
}
.target-info .timeframe {
font-size: 0.75rem;
}
.analysis-content {
padding: 1rem;
font-size: 0.9rem;
line-height: 1.5;
}
.initial-content {
margin-bottom: 10vh;
gap: 1rem;
}
.title {
font-size: 1.5rem;
}
.description {
font-size: 0.85rem;
}
.search-input {
font-size: 0.85rem;
}
.timeframe-selector .timeframe-label {
font-size: 0.8rem;
padding: 0.2rem 0.6rem;
}
.analyze-button {
font-size: 0.85rem;
padding: 0.6rem;
}
.action-buttons {
gap: 0.5rem;
}
.action-button {
padding: 0.5rem;
font-size: 0.8rem;
}
.search-container {
padding: 0.75rem;
}
.search-section {
padding: 0 0.75rem;
}
.search-history-container {
padding: 1rem;
}
.search-history-section {
padding: 0 0.75rem;
}
.history-title {
font-size: 0.85rem;
}
.clear-history {
font-size: 0.8rem;
}
.history-code {
padding: 0.2rem 0.5rem;
font-size: 0.8rem;
}
.history-code .timeframe {
font-size: 0.75rem;
padding: 0.1rem 0.25rem;
}
.remove-icon {
width: 12px;
height: 12px;
}
}
.search-container.is-analyzing {
opacity: 0.8;
pointer-events: none;
}
.search-input.is-selected {
border-color: var(--color-accent);
background-color: rgba(var(--color-accent-rgb), 0.05);
}
.search-tips {
position: absolute;
top: 100%;
left: 1.5rem;
right: 1.5rem;
margin-top: 0.5rem;
padding: 0.75rem;
background-color: rgba(var(--color-accent-rgb), 0.1);
border-radius: var(--border-radius);
color: var(--color-accent);
font-size: 0.9rem;
text-align: center;
animation: fadeInOut 3s forwards;
}
@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);
}
}
@keyframes fadeInOut {
0% {
opacity: 0;
transform: translateY(-10px);
}
10% {
opacity: 1;
transform: translateY(0);
}
90% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-10px);
}
}
@media (prefers-color-scheme: dark) {
.search-tips {
background-color: rgba(var(--color-accent-rgb), 0.2);
}
.progress-dots span {
background-color: var(--color-accent);
opacity: 0.4;
}
.status-label {
color: var(--color-accent);
}
.thought-text {
color: var(--color-text-secondary);
}
:deep(.thought-step) {
background-color: rgba(var(--color-accent-rgb), 0.1);
border-color: rgba(var(--color-accent-rgb), 0.2);
}
:deep(.error-message) {
background-color: rgba(255, 0, 0, 0.1);
border-color: rgba(255, 0, 0, 0.2);
}
.search-container {
border-color: var(--color-border);
}
.search-input {
background-color: rgba(255, 255, 255, 0.05);
border-color: var(--color-accent);
}
.search-input:focus,
.search-input:hover {
border-color: var(--color-accent);
}
}
.timeframe-selector {
display: flex;
gap: 0.5rem;
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 {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: var(--border-radius);
/* font-size: 0.9rem; */
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 {
border-color: var(--color-accent);
color: var(--color-accent);
background-color: transparent;
}
.timeframe-selector .timeframe-option.active .timeframe-label {
background-color: transparent;
color: var(--color-accent);
border: 1px solid var(--color-accent);
}
@media (prefers-color-scheme: dark) {
.timeframe-selector .timeframe-label {
background-color: rgba(255, 255, 255, 0.05);
}
.timeframe-selector .timeframe-option:not(.active):hover .timeframe-label {
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;
}
.analysis-view {
display: flex;
flex-direction: column;
height: calc(100vh - 2rem);
padding: 0.5rem;
animation: fadeIn 0.3s ease-out;
}
.analysis-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background-color: var(--color-bg-secondary);
border-radius: var(--border-radius);
flex-shrink: 0;
}
.analysis-status {
flex-shrink: 0;
}
.analysis-container {
flex: 1;
min-height: 0;
overflow-y: auto;
border-radius: var(--border-radius);
margin-top: 1rem;
display: flex;
flex-direction: column;
}
.target-info {
display: flex;
align-items: center;
gap: 0.5rem;
}
.target-info .label {
color: var(--color-text-secondary);
font-size: 0.85rem;
}
.target-info .value {
color: var(--color-accent);
font-weight: 500;
font-size: 0.9rem;
}
.target-info .timeframe {
color: var(--color-text-secondary);
font-size: 0.8rem;
padding: 0.15rem 0.5rem;
background-color: var(--color-bg-primary);
border-radius: var(--border-radius);
}
.action-buttons {
display: flex;
gap: 1rem;
align-items: center;
}
.action-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border: 1px solid var(--color-accent);
border-radius: var(--border-radius);
background: transparent;
color: var(--color-accent);
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.9rem;
font-weight: 500;
}
.action-button:hover {
background-color: var(--color-accent);
color: white;
}
.action-button.copy {
background-color: var(--color-accent);
color: white;
}
.action-button.copy:hover {
opacity: 0.9;
}
.action-button.copy.success {
background-color: #4caf50;
border-color: #4caf50;
}
.action-button .icon {
width: 16px;
height: 16px;
}
@media (max-width: 768px) {
.analysis-view {
height: calc(100vh - 4.5rem);
}
}
@media (max-width: 480px) {
.analysis-view {
height: calc(100vh - 4.5rem);
}
}
:deep(*::-webkit-scrollbar) {
width: 6px;
height: 6px;
}
:deep(*::-webkit-scrollbar-track) {
background: transparent;
}
:deep(*::-webkit-scrollbar-thumb) {
background-color: rgba(125, 125, 125, 0.2);
border-radius: 3px;
}
:deep(*::-webkit-scrollbar-thumb:hover) {
background-color: rgba(125, 125, 125, 0.3);
}
/* 快速分析列表样式 */
.common-analysis-section {
width: 100%;
max-width: 800px;
margin: 0 auto;
padding: 0 1.5rem;
}
.common-analysis-container {
width: 100%;
padding: 0 1.5rem;
margin-bottom: 1.5rem;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.section-title {
font-size: 0.9rem;
color: var(--color-text-secondary);
}
.common-analysis-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.common-analysis-item {
padding: 0.35rem 0.75rem;
font-size: 0.9rem;
color: var(--color-accent);
background-color: var(--color-bg-secondary);
border: 1px solid var(--color-accent);
border-radius: var(--border-radius);
cursor: pointer;
transition: all 0.2s ease;
}
.common-analysis-item:hover:not(:disabled) {
background-color: var(--color-accent);
color: white;
}
.common-analysis-item:disabled {
opacity: 0.6;
cursor: not-allowed;
border-color: var(--color-border);
color: var(--color-text-secondary);
}
@media (max-width: 768px) {
.common-analysis-section {
padding: 0 1rem;
}
.common-analysis-container {
padding: 0 1rem;
}
}
@media (max-width: 480px) {
.common-analysis-section {
padding: 0 0.75rem;
}
.common-analysis-container {
padding: 0 0.75rem;
}
.common-analysis-item {
font-size: 0.85rem;
padding: 0.3rem 0.6rem;
}
}
</style>