This commit is contained in:
aaron 2025-05-15 00:58:54 +08:00
parent 35448085c5
commit 398ec0306e
5 changed files with 377 additions and 254 deletions

View File

@ -5,7 +5,7 @@ services:
build:
context: .
dockerfile: Dockerfile
image: tradus-web:1.11
image: tradus-web:1.12
container_name: tradus-web
ports:
- '6000:80'

View File

@ -1159,7 +1159,6 @@ body {
.chat-container {
margin-left: 0;
padding-top: 4.5rem;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

View File

@ -96,3 +96,22 @@ body {
display: flex;
flex-direction: column;
}
/* 全局滚动条样式 */
*::-webkit-scrollbar {
width: 6px;
height: 6px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: rgba(125, 125, 125, 0.2);
border-radius: 3px;
}
*::-webkit-scrollbar-thumb:hover {
background-color: rgba(125, 125, 125, 0.3);
}

View File

@ -9,6 +9,7 @@ const analysisContent = ref('')
const analysisContainer = ref<HTMLElement | null>(null)
const currentThought = ref('')
const showInitialView = ref(true)
const copySuccess = ref(false)
// APIURL
const apiBaseUrl =
@ -138,6 +139,24 @@ const clearInput = () => {
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('复制失败')
}
}
</script>
<template>
@ -201,7 +220,7 @@ const clearInput = () => {
<div v-else class="analysis-view">
<div class="analysis-header">
<div class="target-info">
<span class="label">正在分析</span>
<span class="label">正在分析股票</span>
<span class="value">{{ stockCode }}</span>
</div>
<button class="new-analysis-button" @click="resetView" :disabled="isAnalyzing">
@ -231,8 +250,8 @@ const clearInput = () => {
<span></span>
</div>
<div class="status-text">
<div class="status-label">AI分析进行中</div>
<div v-if="currentThought" class="thought-text">{{ currentThought }}</div>
<div class="status-label">AI 正在分析</div>
<!-- <div v-if="currentThought" class="thought-text">{{ currentThought }}</div> -->
</div>
</div>
@ -242,6 +261,12 @@ const clearInput = () => {
:class="{ 'fade-in': analysisContent }"
>
<div class="analysis-content" v-html="analysisContent"></div>
<div v-if="analysisContent && !isAnalyzing" class="copy-button-container">
<button class="copy-button" @click="copyAnalysis" :class="{ success: copySuccess }">
<span v-if="!copySuccess">复制分析结果</span>
<span v-else>复制成功</span>
</button>
</div>
</div>
</div>
</div>
@ -266,14 +291,15 @@ const clearInput = () => {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
justify-content: center;
}
.initial-content {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 15vh;
margin-bottom: 15vh;
gap: 2rem;
transition: all 0.5s ease;
}
@ -284,7 +310,7 @@ const clearInput = () => {
}
.title {
font-size: 2.5rem;
font-size: 2rem;
font-weight: 700;
color: var(--color-text-primary);
margin-bottom: 0.5rem;
@ -292,7 +318,7 @@ const clearInput = () => {
}
.description {
font-size: 1rem;
font-size: 0.95rem;
color: var(--color-text-secondary);
transition: all 0.5s ease;
}
@ -338,7 +364,7 @@ const clearInput = () => {
border: none;
background: none;
padding: 0.5rem;
font-size: 1.1rem;
font-size: 0.95rem;
color: var(--color-text-primary);
outline: none;
height: 2.5rem;
@ -370,7 +396,7 @@ const clearInput = () => {
.analyze-button {
width: 100%;
padding: 0.75rem;
font-size: 1rem;
font-size: 0.9rem;
font-weight: 500;
color: white;
background-color: var(--color-accent);
@ -391,21 +417,20 @@ const clearInput = () => {
.analysis-status {
display: flex;
flex-direction: column;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 2rem 1rem;
padding: 1rem;
text-align: center;
background-color: var(--color-bg-secondary);
border-radius: var(--border-radius);
gap: 1rem;
margin-top: 1rem;
}
.progress-dots {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-bottom: 1rem;
gap: 6px;
}
.progress-dots span {
@ -427,7 +452,7 @@ const clearInput = () => {
.status-text {
display: flex;
flex-direction: column;
flex-direction: row;
align-items: center;
gap: 0.5rem;
}
@ -435,35 +460,39 @@ const clearInput = () => {
.status-label {
font-weight: 500;
color: var(--color-accent);
font-size: 1.1rem;
font-size: 0.85rem;
}
.thought-text {
font-size: 0.95rem;
font-size: 0.9rem;
color: var(--color-text-secondary);
max-width: 600px;
text-align: center;
line-height: 1.5;
}
.analysis-container {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 1rem 1.5rem;
border-radius: var(--border-radius);
margin-top: 1rem;
margin-bottom: 1rem;
display: flex;
flex-direction: column;
}
.analysis-container.fade-in {
background-color: var(--color-bg-secondary);
}
.analysis-content {
color: var(--color-text-primary);
line-height: 1.8;
font-size: 1.1rem;
line-height: 1.6;
font-size: 0.95rem;
white-space: pre-wrap;
padding-bottom: 2rem;
flex: 1;
padding: 1rem 1.5rem 4.5rem 1.5rem;
padding: 1.5rem 2rem;
overflow-y: auto;
}
.analysis-content::-webkit-scrollbar {
display: none;
}
:deep(.error-message) {
@ -505,7 +534,7 @@ const clearInput = () => {
@media (max-width: 768px) {
.initial-content {
padding-top: 12vh;
margin-bottom: 12vh;
gap: 1.5rem;
}
@ -518,23 +547,49 @@ const clearInput = () => {
}
.search-input {
font-size: 1rem;
}
.analysis-container {
padding: 0.75rem 1rem;
margin-bottom: 0.75rem;
font-size: 0.9rem;
}
.analysis-content {
font-size: 1rem;
padding: 1rem 1rem 4.5rem 1rem;
padding: 1.25rem 1.5rem;
font-size: 0.9rem;
}
.analysis-view {
padding: 0.5rem;
margin-top: 4.5rem;
}
.analysis-header {
padding: 0.75rem;
flex-direction: column;
gap: 0.75rem;
align-items: flex-start;
}
.new-analysis-button {
width: 100%;
justify-content: center;
}
.analysis-container {
margin-top: 1rem;
}
.analysis-content {
padding: 1.25rem 1.5rem;
font-size: 0.9rem;
}
.copy-button-container {
bottom: 1.5rem;
right: 1.5rem;
}
}
@media (max-width: 480px) {
.initial-content {
padding-top: 10vh;
margin-bottom: 10vh;
gap: 1rem;
}
@ -543,33 +598,78 @@ const clearInput = () => {
}
.description {
font-size: 0.9rem;
font-size: 0.85rem;
}
.search-input {
font-size: 0.95rem;
font-size: 0.85rem;
}
.analyze-button {
font-size: 0.95rem;
font-size: 0.85rem;
padding: 0.6rem;
}
.analysis-content {
padding: 1rem;
font-size: 0.85rem;
line-height: 1.5;
}
.target-info .label {
font-size: 0.8rem;
}
.target-info .value {
font-size: 0.85rem;
}
.stock-analysis-view {
padding: 0.5rem;
}
.analysis-view {
padding: 0.5rem;
height: calc(100% - 1rem);
}
.analysis-header {
padding: 0.75rem;
}
.analysis-container {
margin-bottom: 0.5rem;
margin-top: 1rem;
}
.analysis-content {
padding: 0.75rem 0.75rem 4.5rem 0.75rem;
padding: 1rem;
font-size: 0.85rem;
line-height: 1.5;
}
.analysis-status {
margin-top: 0.5rem;
padding: 0.75rem;
}
.copy-button-container {
bottom: 1rem;
right: 1rem;
}
.copy-button {
padding: 0.6rem 1.2rem;
font-size: 0.85rem;
}
}
.analysis-view {
display: flex;
flex-direction: column;
height: 100vh;
padding: 1rem;
height: 100%;
padding: 0.5rem;
animation: fadeIn 0.3s ease-out;
min-height: 0;
}
.analysis-header {
@ -579,7 +679,6 @@ const clearInput = () => {
padding: 1rem;
background-color: var(--color-bg-secondary);
border-radius: var(--border-radius);
margin-bottom: 1rem;
}
.target-info {
@ -590,13 +689,13 @@ const clearInput = () => {
.target-info .label {
color: var(--color-text-secondary);
font-size: 0.95rem;
font-size: 0.85rem;
}
.target-info .value {
color: var(--color-accent);
font-weight: 500;
font-size: 1.1rem;
font-size: 0.9rem;
}
.new-analysis-button {
@ -627,43 +726,53 @@ const clearInput = () => {
height: 16px;
}
.analysis-container {
flex: 1;
overflow-y: auto;
background-color: var(--color-bg-primary);
border-radius: var(--border-radius);
margin-top: 1rem;
:deep(*::-webkit-scrollbar) {
width: 6px;
height: 6px;
}
@media (max-width: 768px) {
.analysis-view {
padding: 0.5rem;
}
:deep(*::-webkit-scrollbar-track) {
background: transparent;
}
.analysis-header {
padding: 0.75rem;
flex-direction: column;
gap: 0.75rem;
align-items: flex-start;
}
:deep(*::-webkit-scrollbar-thumb) {
background-color: rgba(125, 125, 125, 0.2);
border-radius: 3px;
}
.new-analysis-button {
width: 100%;
:deep(*::-webkit-scrollbar-thumb:hover) {
background-color: rgba(125, 125, 125, 0.3);
}
.copy-button-container {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 10;
}
.copy-button {
display: flex;
align-items: center;
justify-content: center;
}
padding: 0.75rem 1.5rem;
border: none;
border-radius: var(--border-radius);
background-color: var(--color-accent);
color: white;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
@media (max-width: 480px) {
.analysis-header {
padding: 0.5rem;
}
.copy-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.target-info .label {
font-size: 0.9rem;
}
.target-info .value {
font-size: 1rem;
}
.copy-button.success {
background-color: #4caf50;
}
</style>

View File

@ -13,11 +13,11 @@ const isAnalyzing = ref(false)
const analysisContent = ref('')
const analysisContainer = ref<HTMLElement | null>(null)
const currentThought = ref('')
const selectedTimeframe = ref('all')
const selectedTimeframe = ref('15m')
const showInitialView = ref(true)
const copySuccess = ref(false)
const timeframes = [
{ label: '全部', value: 'all' },
{ label: '15分钟', value: '15m' },
{ label: '1小时', value: '1h' },
{ label: '4小时', value: '4h' },
@ -61,11 +61,7 @@ const handleAnalysis = async () => {
const requestData: AnalysisRequest = {
symbol: cryptoCode.value.trim().toUpperCase(),
}
// 'all' timeframe
if (selectedTimeframe.value !== 'all') {
requestData.timeframe = selectedTimeframe.value
timeframe: selectedTimeframe.value,
}
const response = await fetch(`${apiBaseUrl}/crypto/crypto/analysis`, {
@ -124,15 +120,13 @@ const handleAnalysis = async () => {
await scrollToBottom()
break
}
} catch (e) {
console.error('解析响应数据出错:', e)
} catch {
analysisContent.value = '<div class="error-message">解析响应数据时出错,请稍后重试</div>'
currentThought.value = '数据解析出错'
}
}
}
} catch (error) {
console.error('分析请求失败:', error)
} catch {
analysisContent.value = '<div class="error-message">抱歉,分析请求失败,请稍后重试</div>'
currentThought.value = '请求失败'
} finally {
@ -155,6 +149,24 @@ const clearInput = () => {
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('复制失败')
}
}
</script>
<template>
@ -261,8 +273,7 @@ const clearInput = () => {
<span></span>
</div>
<div class="status-text">
<div class="status-label">AI分析进行中</div>
<div v-if="currentThought" class="thought-text">{{ currentThought }}</div>
<div class="status-label">AI 正在分析</div>
</div>
</div>
@ -272,6 +283,12 @@ const clearInput = () => {
:class="{ 'fade-in': analysisContent }"
>
<div class="analysis-content" v-html="analysisContent"></div>
<div v-if="analysisContent && !isAnalyzing" class="copy-button-container">
<button class="copy-button" @click="copyAnalysis" :class="{ success: copySuccess }">
<span v-if="!copySuccess">复制分析结果</span>
<span v-else>复制成功</span>
</button>
</div>
</div>
</div>
</div>
@ -302,13 +319,14 @@ const clearInput = () => {
display: flex;
flex-direction: column;
overflow: hidden;
justify-content: center;
}
.initial-content {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 15vh;
margin-bottom: 15vh;
gap: 2rem;
transition: all 0.5s ease;
}
@ -319,7 +337,7 @@ const clearInput = () => {
}
.title {
font-size: 2.5rem;
font-size: 2rem;
font-weight: 700;
color: var(--color-text-primary);
margin-bottom: 0.5rem;
@ -327,7 +345,7 @@ const clearInput = () => {
}
.description {
font-size: 1rem;
font-size: 0.95rem;
color: var(--color-text-secondary);
transition: all 0.5s ease;
}
@ -445,7 +463,7 @@ const clearInput = () => {
border: none;
background: none;
padding: 0;
font-size: 1.1rem;
font-size: 0.95rem;
color: var(--color-text-primary);
outline: none;
height: 2.5rem;
@ -489,7 +507,7 @@ const clearInput = () => {
.analyze-button {
width: 100%;
padding: 0.75rem;
font-size: 1rem;
font-size: 0.9rem;
font-weight: 500;
color: white;
background-color: var(--color-accent);
@ -553,21 +571,20 @@ const clearInput = () => {
.analysis-status {
display: flex;
flex-direction: column;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 2rem 1rem;
padding: 1rem;
text-align: center;
background-color: var(--color-bg-secondary);
border-radius: var(--border-radius);
gap: 1rem;
margin-top: 1rem;
}
.progress-dots {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-bottom: 1rem;
gap: 6px;
}
.progress-dots span {
@ -589,7 +606,7 @@ const clearInput = () => {
.status-text {
display: flex;
flex-direction: column;
flex-direction: row;
align-items: center;
gap: 0.5rem;
}
@ -597,15 +614,12 @@ const clearInput = () => {
.status-label {
font-weight: 500;
color: var(--color-accent);
font-size: 1.1rem;
font-size: 0.85rem;
}
.thought-text {
font-size: 0.95rem;
font-size: 0.9rem;
color: var(--color-text-secondary);
max-width: 600px;
text-align: center;
line-height: 1.5;
}
.no-results {
@ -620,20 +634,25 @@ const clearInput = () => {
.analysis-container {
flex: 1;
min-height: 0;
overflow-y: auto;
border-radius: var(--border-radius);
margin-top: 1rem;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.analysis-container.fade-in {
background-color: var(--color-bg-secondary);
}
.analysis-content {
flex: 1;
overflow-y: auto;
color: var(--color-text-primary);
line-height: 1.8;
font-size: 1.1rem;
line-height: 1.6;
font-size: 0.9rem;
white-space: pre-wrap;
padding: 1rem 1.5rem 4.5rem 1.5rem;
padding: 1.5rem 2rem;
overflow-y: auto;
}
.analysis-content :deep(h1),
@ -744,29 +763,38 @@ const clearInput = () => {
}
.analysis-content::-webkit-scrollbar {
width: 6px;
}
.analysis-content::-webkit-scrollbar-track {
background: transparent;
}
.analysis-content::-webkit-scrollbar-thumb {
background-color: var(--color-border);
border-radius: 3px;
}
.analysis-content::-webkit-scrollbar-thumb:hover {
background-color: var(--color-accent);
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;
}
.new-analysis-button {
width: 100%;
justify-content: center;
}
.analysis-content {
padding: 1.25rem 1.5rem;
font-size: 0.85rem;
}
.initial-content {
padding-top: 12vh;
margin-bottom: 12vh;
gap: 1.5rem;
}
@ -774,76 +802,60 @@ const clearInput = () => {
font-size: 1.75rem;
}
.search-container {
padding: 1rem;
margin: 0 -0.5rem;
.description {
font-size: 0.9rem;
}
.search-input {
height: 3rem;
font-size: 1rem;
padding: 0.75rem 2rem 0.75rem 1rem;
border-radius: 0;
}
.analysis-container {
margin: 0.5rem -0.5rem 0;
border-left: none;
border-right: none;
border-radius: 0;
padding: 1rem;
}
.analysis-content {
font-size: 1rem;
padding: 1rem 1rem 4.5rem 1rem;
}
.crypto-card {
margin: 0;
}
.status-text {
font-size: 0.95rem;
gap: 0.75rem;
}
.status-label {
font-size: 1rem;
}
.thought-text {
font-size: 0.9rem;
max-width: 95%;
}
.selected-tag {
padding: 0.4rem 0.6rem;
font-size: 0.95rem;
}
.tag-remove {
padding: 0.12rem;
}
.tag-remove svg {
width: 12px;
height: 12px;
}
.timeframe-selector {
gap: 0.35rem;
}
.timeframe-selector .timeframe-label {
padding: 0.2rem 0.5rem;
font-size: 0.85rem;
}
.analyze-button {
font-size: 0.85rem;
}
}
@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.8rem;
line-height: 1.5;
}
.initial-content {
padding-top: 10vh;
margin-bottom: 10vh;
gap: 1rem;
}
@ -852,34 +864,22 @@ const clearInput = () => {
}
.description {
font-size: 0.9rem;
font-size: 0.85rem;
}
.crypto-option {
padding: 0.5rem 0.75rem;
}
.analysis-container {
padding: 1rem;
}
.timeframe-selector {
gap: 0.25rem;
.search-input {
font-size: 0.85rem;
}
.timeframe-selector .timeframe-label {
padding: 0.15rem 0.4rem;
font-size: 0.8rem;
padding: 0.2rem 0.6rem;
}
.analyze-button {
font-size: 0.95rem;
font-size: 0.85rem;
padding: 0.6rem;
}
.analysis-content {
padding: 0.75rem 0.75rem 4.5rem 0.75rem;
}
}
.search-container.is-analyzing {
@ -1132,8 +1132,8 @@ const clearInput = () => {
.analysis-view {
display: flex;
flex-direction: column;
height: 100vh;
padding: 1rem;
height: calc(100vh - 2rem);
padding: 0.5rem;
animation: fadeIn 0.3s ease-out;
}
@ -1144,7 +1144,21 @@ const clearInput = () => {
padding: 1rem;
background-color: var(--color-bg-secondary);
border-radius: var(--border-radius);
margin-bottom: 1rem;
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 {
@ -1155,18 +1169,18 @@ const clearInput = () => {
.target-info .label {
color: var(--color-text-secondary);
font-size: 0.95rem;
font-size: 0.85rem;
}
.target-info .value {
color: var(--color-accent);
font-weight: 500;
font-size: 1.1rem;
font-size: 0.9rem;
}
.target-info .timeframe {
color: var(--color-text-secondary);
font-size: 0.9rem;
font-size: 0.8rem;
padding: 0.15rem 0.5rem;
background-color: var(--color-bg-primary);
border-radius: var(--border-radius);
@ -1200,51 +1214,33 @@ const clearInput = () => {
height: 16px;
}
.analysis-container {
flex: 1;
overflow-y: auto;
background-color: var(--color-bg-primary);
border-radius: var(--border-radius);
margin-top: 1rem;
}
@media (max-width: 768px) {
.analysis-view {
padding: 0.5rem;
}
.analysis-header {
padding: 0.75rem;
flex-direction: column;
gap: 0.75rem;
align-items: flex-start;
}
.new-analysis-button {
width: 100%;
justify-content: center;
height: calc(100vh - 1.5rem);
}
}
@media (max-width: 480px) {
.analysis-header {
padding: 0.5rem;
}
.target-info {
flex-wrap: wrap;
}
.target-info .label {
font-size: 0.9rem;
}
.target-info .value {
font-size: 1rem;
}
.target-info .timeframe {
font-size: 0.85rem;
.analysis-view {
height: calc(100vh - 1rem);
}
}
: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);
}
</style>