美化了页面布局,并解决了页面抖动的问题

This commit is contained in:
Sebastian
2026-02-05 14:29:13 +08:00
parent 919b65a5ac
commit ddfa56a800
5 changed files with 1023 additions and 634 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -7,23 +7,20 @@
<h1>GoFundBot</h1>
<p>智能基金分析 · 实时市场追踪</p>
</div>
<!-- 顶部搜索框 -->
<div class="header-search">
<FundSearch @fund-selected="handleHeaderSearch" :compact="true" />
</div>
<!-- 模式切换 -->
<div class="header-right">
<div class="mode-switch">
<button
class="mode-btn"
:class="{ active: viewMode === 'dashboard' }"
@click="viewMode = 'dashboard'"
@click="resetToDashboard"
>
🏠 市场大盘
</button>
<button
class="mode-btn"
:class="{ active: viewMode === 'detail' }"
@click="viewMode = 'detail'"
>
📋 基金详情
</button>
<button
class="mode-btn"
:class="{ active: viewMode === 'screening' }"
@@ -31,13 +28,6 @@
>
🔍 基金筛选
</button>
<button
class="mode-btn"
:class="{ active: viewMode === 'compare' }"
@click="viewMode = 'compare'"
>
📈 基金对比
</button>
<button
class="mode-btn"
:class="{ active: viewMode === 'backtest' }"
@@ -52,28 +42,40 @@
<main class="app-main">
<!-- 市场大盘模式 -->
<div v-if="viewMode === 'dashboard'" class="dashboard-layout">
<div v-if="viewMode === 'dashboard'" class="dashboard-layout" :class="{ 'full-content': showFullContent }">
<!-- 左侧自选列表 -->
<aside class="dashboard-sidebar">
<FundWatchlist
@view-fund="handleDashboardFundView"
@add-to-compare="handleAddToCompare"
:compareMode="false"
:compareMode="compareMode"
:compareFunds="compareFunds"
:showCompareToggle="true"
@toggle-compare="toggleCompareMode"
/>
</aside>
<!-- 中间核心内容 -->
<div class="dashboard-main">
<!-- 基金对比页面选中多只基金时显示 -->
<FundComparison
v-if="compareMode && compareFunds.length >= 2"
:compareFunds="compareFunds"
@remove-fund="handleRemoveFromCompare"
@clear-funds="handleClearCompare"
/>
<!-- 基金详情选中时显示 -->
<FundDetail v-else-if="selectedFundCode && !compareMode" :fundCode="selectedFundCode" />
<!-- 市场指数 + 金价 -->
<MarketOverview
v-else
:showGoldHistory="true"
:showSSE30Min="true"
/>
</div>
<!-- 右侧快讯 + 板块 -->
<aside class="dashboard-right">
<!-- 右侧快讯 + 板块显示详情/对比时隐藏 -->
<aside class="dashboard-right" v-if="!showFullContent">
<FlashNews :count="15" :refreshInterval="60000" />
<SectorRank :limit="50" :initialDisplay="12" />
</aside>
@@ -86,8 +88,10 @@
<FundWatchlist
@view-fund="handleFundSelected"
@add-to-compare="handleAddToCompare"
:compareMode="viewMode === 'compare'"
:compareMode="compareMode"
:compareFunds="compareFunds"
:showCompareToggle="true"
@toggle-compare="toggleCompareMode"
/>
</aside>
@@ -101,32 +105,12 @@
/>
</template>
<!-- 对比模式 -->
<template v-else-if="viewMode === 'compare'">
<FundComparison
:compareFunds="compareFunds"
@remove-fund="handleRemoveFromCompare"
@clear-funds="handleClearCompare"
/>
</template>
<!-- 回测模式 -->
<template v-else-if="viewMode === 'backtest'">
<FundBacktest
:fundCode="selectedFundCode"
/>
</template>
<!-- 详情模式 -->
<template v-else>
<FundSearch @fund-selected="handleFundSelected" />
<FundDetail v-if="selectedFundCode" :fundCode="selectedFundCode" />
<div v-else class="welcome-container">
<div class="welcome-icon">🔍</div>
<h3>搜索基金开始分析</h3>
<p>在上方搜索框输入基金代码或名称</p>
</div>
</template>
</div>
</div>
</main>
@@ -138,7 +122,7 @@
</template>
<script>
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import FundSearch from './components/FundSearch.vue'
import FundDetail from './components/FundDetail.vue'
import FundWatchlist from './components/FundWatchlist.vue'
@@ -167,8 +151,16 @@ export default {
const currentTime = ref('')
const viewMode = ref('dashboard') // 默认显示市场大盘
const compareFunds = ref([]) // 用于对比的基金列表
const compareMode = ref(false) // 是否处于对比模式
// 是否显示全宽内容(详情页或对比页时隐藏右侧栏)
const showFullContent = computed(() => {
return (compareMode.value && compareFunds.value.length >= 2) ||
(selectedFundCode.value && !compareMode.value)
})
const handleFundSelected = (fundOrCode) => {
if (compareMode.value) return // 对比模式下不切换基金
if (fundOrCode && typeof fundOrCode === 'object') {
selectedFundCode.value = fundOrCode.CODE || fundOrCode.fund_code || fundOrCode.code
} else {
@@ -176,16 +168,31 @@ export default {
}
}
// 从仪表盘点击基金,切换到详情模式
const handleDashboardFundView = (fundOrCode) => {
// 顶部搜索框选中基金
const handleHeaderSearch = (fundOrCode) => {
compareMode.value = false // 退出对比模式
handleFundSelected(fundOrCode)
viewMode.value = 'detail'
}
// 从仪表盘/自选点击基金
const handleDashboardFundView = (fundOrCode) => {
if (compareMode.value) return // 对比模式下不切换
handleFundSelected(fundOrCode)
}
// 切换对比模式
const toggleCompareMode = () => {
compareMode.value = !compareMode.value
if (!compareMode.value) {
// 退出对比模式时清空对比列表
compareFunds.value = []
}
}
// 从筛选页面查看基金详情
const handleScreeningFundView = (fundCode) => {
selectedFundCode.value = fundCode
viewMode.value = 'detail'
viewMode.value = 'dashboard'
}
// 添加基金到对比列表
@@ -217,6 +224,17 @@ export default {
compareFunds.value = []
}
// 重置到市场大盘(点击菜单栏"市场大盘"时)
const resetToDashboard = () => {
viewMode.value = 'dashboard'
selectedFundCode.value = ''
// 如果处于对比模式,也退出
if (compareMode.value) {
compareMode.value = false
compareFunds.value = []
}
}
// 更新时间
const updateTime = () => {
const now = new Date()
@@ -234,12 +252,17 @@ export default {
currentTime,
viewMode,
compareFunds,
compareMode,
handleFundSelected,
handleHeaderSearch,
handleDashboardFundView,
handleScreeningFundView,
handleAddToCompare,
handleRemoveFromCompare,
handleClearCompare
handleClearCompare,
toggleCompareMode,
showFullContent,
resetToDashboard
}
}
}
@@ -313,6 +336,79 @@ export default {
font-size: 0.85rem;
}
/* 顶部搜索框 */
.header-search {
flex: 1;
max-width: 600px;
margin: 0 40px;
}
.header-search :deep(.fund-search) {
margin-bottom: 0;
background: transparent;
padding: 0;
box-shadow: none;
}
.header-search :deep(.search-header) {
gap: 8px;
}
.header-search :deep(.search-box) {
min-width: 300px;
flex: 1;
}
.header-search :deep(.search-input) {
background: rgba(255, 255, 255, 0.95);
border: 2px solid transparent;
height: 40px;
flex: 1;
}
.header-search :deep(.search-input:focus) {
border-color: rgba(255, 255, 255, 0.5);
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.2);
}
.header-search :deep(.search-btn) {
background: linear-gradient(135deg, #ff9f43 0%, #f39c12 100%);
height: 40px;
padding: 0 24px;
color: white;
font-weight: 600;
border: none;
box-shadow: 0 2px 8px rgba(243, 156, 18, 0.35);
}
.header-search :deep(.search-btn:hover) {
background: linear-gradient(135deg, #ffb366 0%, #f5a623 100%);
box-shadow: 0 4px 12px rgba(243, 156, 18, 0.45);
transform: translateY(-1px);
}
.header-search :deep(.refresh-btn) {
background: rgba(255, 255, 255, 0.2);
color: white;
width: 40px;
height: 40px;
border-radius: 8px;
}
.header-search :deep(.refresh-btn:hover:not(:disabled)) {
background: rgba(255, 255, 255, 0.3);
}
.header-search :deep(.search-results) {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
margin-top: 4px;
max-height: 300px;
}
.header-right {
display: flex;
align-items: center;
@@ -361,9 +457,14 @@ export default {
/* ==================== 仪表盘布局 ==================== */
.dashboard-layout {
display: grid;
grid-template-columns: 320px 1fr 380px;
grid-template-columns: 380px 1fr 380px;
gap: 20px;
min-height: calc(100vh - 140px);
transition: grid-template-columns 0.3s ease;
}
.dashboard-layout.full-content {
grid-template-columns: 380px 1fr;
}
.dashboard-sidebar {
@@ -397,7 +498,7 @@ export default {
}
.sidebar-left {
width: 360px;
width: 400px;
flex-shrink: 0;
}
@@ -410,6 +511,15 @@ export default {
width: 100%;
}
/* ==================== 对比面板 ==================== */
.compare-panel {
margin-bottom: 20px;
border-radius: var(--radius-lg);
background: var(--bg-card);
box-shadow: var(--shadow-md);
overflow: hidden;
}
/* ==================== 欢迎页面 ==================== */
.welcome-container {
display: flex;
@@ -456,9 +566,24 @@ export default {
}
/* ==================== 响应式设计 ==================== */
@media (max-width: 1600px) {
.header-search {
max-width: 400px;
margin: 0 20px;
}
}
@media (max-width: 1400px) {
.dashboard-layout {
grid-template-columns: 280px 1fr 340px;
grid-template-columns: 340px 1fr 340px;
}
.sidebar-left {
width: 360px;
}
.header-search {
max-width: 350px;
}
}
@@ -470,6 +595,15 @@ export default {
.dashboard-sidebar {
display: none;
}
.sidebar-left {
width: 320px;
}
.header-search {
max-width: 280px;
margin: 0 15px;
}
}
@media (max-width: 1024px) {
@@ -494,6 +628,13 @@ export default {
flex-wrap: wrap;
justify-content: center;
}
.header-search {
order: 3;
width: 100%;
max-width: 100%;
margin: 10px 0 0 0;
}
}
@media (max-width: 768px) {
@@ -518,6 +659,10 @@ export default {
.app-main {
padding: 12px;
}
.header-search :deep(.db-status) {
display: none;
}
}
/* ==================== 滚动条美化 ==================== */

View File

@@ -1,24 +1,30 @@
<template>
<div class="fund-comparison" v-if="selectedFunds.length > 0 || compareFunds.length > 0">
<div class="fund-comparison" :class="{ 'compact-mode': compact }">
<div class="comparison-header">
<h2>
<span class="header-icon">📈</span>
基金对比
<span class="count-badge" v-if="selectedFunds.length">{{ selectedFunds.length }}/{{ maxFunds }}</span>
</h2>
<div class="header-actions">
<div class="header-actions" v-if="selectedFunds.length > 0">
<button
class="btn btn-clear"
@click="clearSelection"
:disabled="selectedFunds.length === 0"
>
清空对比
清空
</button>
</div>
</div>
<!-- 空状态提示 -->
<div v-if="selectedFunds.length === 0" class="empty-compare-hint">
<span class="hint-icon">👆</span>
<span>点击自选基金的 <strong>+</strong> 按钮添加对比</span>
</div>
<!-- 基金选择区域 -->
<div class="selection-area">
<div class="selection-area" v-if="selectedFunds.length > 0">
<div class="selection-tags">
<div
v-for="fund in selectedFunds"
@@ -31,14 +37,14 @@
<span class="tag-code">({{ fund.code }})</span>
<button class="tag-remove" @click="removeFund(fund.code)">×</button>
</div>
<div v-if="selectedFunds.length < maxFunds && selectedFunds.length > 0" class="add-fund-hint">
<span>👈 继续从左侧添加基金 (还可添加{{ maxFunds - selectedFunds.length }})</span>
<div v-if="selectedFunds.length < maxFunds" class="add-fund-hint">
<span>还可添加{{ maxFunds - selectedFunds.length }}</span>
</div>
</div>
</div>
<!-- 对比内容区域 -->
<div class="comparison-content" v-if="selectedFunds.length >= 2">
<!-- 对比内容区域紧凑模式下隐藏详细内容 -->
<div class="comparison-content" v-if="selectedFunds.length >= 2 && !compact">
<!-- 净值走势图表 -->
<div class="chart-section">
<div class="section-header">
@@ -266,6 +272,10 @@ export default {
compareFunds: {
type: Array,
default: () => []
},
compact: {
type: Boolean,
default: false
}
},
emits: ['remove-fund', 'clear-funds'],
@@ -700,6 +710,60 @@ export default {
overflow: hidden;
}
/* 紧凑模式样式 */
.fund-comparison.compact-mode {
margin-bottom: 0;
box-shadow: none;
border-radius: 0;
}
.fund-comparison.compact-mode .comparison-header {
padding: 10px 12px;
}
.fund-comparison.compact-mode .comparison-header h2 {
font-size: 14px;
}
.fund-comparison.compact-mode .selection-area {
padding: 8px 12px;
}
.fund-comparison.compact-mode .fund-tag {
padding: 4px 8px;
font-size: 12px;
}
.fund-comparison.compact-mode .tag-code {
display: none;
}
.fund-comparison.compact-mode .add-fund-hint {
font-size: 11px;
}
/* 空状态提示 */
.empty-compare-hint {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 16px;
background: #f8fafc;
color: #64748b;
font-size: 13px;
border-top: 1px solid #f0f0f0;
}
.empty-compare-hint .hint-icon {
font-size: 16px;
}
.fund-comparison.compact-mode .empty-compare-hint {
padding: 10px 12px;
font-size: 12px;
}
.comparison-header {
display: flex;
justify-content: space-between;

View File

@@ -1,28 +1,20 @@
<template>
<div class="fund-search">
<div class="fund-search" :class="{ 'compact-mode': compact }">
<div class="search-header">
<div class="search-box">
<input
v-model="searchKeyword"
@input="handleSearch"
@keyup.enter="performSearch"
placeholder="输入基金代码或名称搜索..."
:placeholder="compact ? '输入基金代码或名称搜索...' : '输入基金代码或名称搜索...'"
class="search-input"
/>
<button @click="performSearch" class="search-btn">搜索</button>
</div>
<div class="db-status">
<span v-if="dbStatus.has_cache" class="status-text">
📊 {{ dbStatus.count }} 只基金 | 更新: {{ formatDate(dbStatus.last_update) }}
</span>
<span v-else class="status-text status-empty">
暂无数据
</span>
<button
@click="updateDatabase"
:disabled="updating"
class="update-btn"
:title="updating ? '更新中...' : '更新基金数据库'"
class="refresh-btn"
:title="dbStatus.has_cache ? `${dbStatus.count}只基金 | 更新: ${formatDate(dbStatus.last_update)}` : '更新基金数据库'"
>
<span v-if="updating" class="spinner"></span>
<span v-else>🔄</span>
@@ -52,6 +44,12 @@ import { fundAPI } from '../services/api'
export default {
name: 'FundSearch',
props: {
compact: {
type: Boolean,
default: false
}
},
emits: ['fund-selected'],
data() {
return {
@@ -151,6 +149,36 @@ export default {
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
position: relative;
}
.fund-search.compact-mode {
margin-bottom: 0;
background: transparent;
padding: 0;
box-shadow: none;
}
.fund-search.compact-mode .search-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
margin-top: 4px;
}
.fund-search.compact-mode .loading {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
margin-top: 4px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.search-header {
@@ -162,9 +190,10 @@ export default {
.search-box {
display: flex;
gap: 10px;
gap: 8px;
flex: 1;
min-width: 280px;
min-width: 200px;
align-items: center;
}
.search-input {
@@ -174,6 +203,7 @@ export default {
border-radius: 8px;
font-size: 14px;
transition: all 0.2s;
min-width: 150px;
}
.search-input:focus {
@@ -184,7 +214,7 @@ export default {
.search-btn {
padding: 10px 20px;
background: linear-gradient(135deg, #1677ff 0%, #0958d9 100%);
background: linear-gradient(135deg, #ff9f43 0%, #f39c12 100%);
color: white;
border: none;
border-radius: 8px;
@@ -196,51 +226,30 @@ export default {
.search-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(22, 119, 255, 0.3);
box-shadow: 0 4px 12px rgba(243, 156, 18, 0.4);
}
.db-status {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #6b7280;
background: #f9fafb;
padding: 6px 12px;
border-radius: 20px;
border: 1px solid #e5e7eb;
}
.status-text {
white-space: nowrap;
}
.status-empty {
color: #ef4444;
}
.update-btn {
width: 28px;
height: 28px;
.refresh-btn {
width: 38px;
height: 38px;
border: none;
background: white;
border-radius: 50%;
background: rgba(255, 255, 255, 0.15);
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-size: 16px;
transition: all 0.3s;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
flex-shrink: 0;
}
.update-btn:hover:not(:disabled) {
background: #1677ff;
color: white;
.refresh-btn:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.25);
transform: rotate(180deg);
}
.update-btn:disabled {
.refresh-btn:disabled {
cursor: not-allowed;
opacity: 0.7;
}

View File

@@ -1,5 +1,30 @@
<template>
<div class="watchlist-container">
<!-- 对比模式切换按钮 -->
<div v-if="showCompareToggle" class="compare-toggle-bar">
<button
class="btn-compare-toggle"
:class="{ active: compareMode }"
@click="$emit('toggle-compare')"
>
<span class="toggle-icon">📈</span>
<span>{{ compareMode ? '退出对比' : '基金对比' }}</span>
<span v-if="compareFunds.length && compareMode" class="compare-count">{{ compareFunds.length }}</span>
</button>
<!-- 对比模式下显示已选基金 -->
<div v-if="compareMode && compareFunds.length > 0" class="compare-selected">
<div v-for="fund in compareFunds" :key="fund.code" class="compare-tag">
<span class="tag-name">{{ fund.name }}</span>
</div>
</div>
<div v-if="compareMode && compareFunds.length === 0" class="compare-hint">
👆 点击下方基金的 <strong>+</strong> 按钮添加对比
</div>
<div v-if="compareMode && compareFunds.length === 1" class="compare-hint">
还需选择至少 <strong>1</strong> 只基金才能对比
</div>
</div>
<!-- 头部操作栏 -->
<div class="watchlist-header">
<h2>
@@ -166,9 +191,10 @@ export default {
components: { FundListItems },
props: {
compareMode: { type: Boolean, default: false },
compareFunds: { type: Array, default: () => [] }
compareFunds: { type: Array, default: () => [] },
showCompareToggle: { type: Boolean, default: false }
},
emits: ['view-fund', 'add-to-compare'],
emits: ['view-fund', 'add-to-compare', 'toggle-compare'],
setup(props, { emit }) {
const watchlist = ref([])
const groups = ref([])
@@ -585,6 +611,81 @@ export default {
flex-direction: column;
}
/* 对比切换按钮区域 */
.compare-toggle-bar {
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #e5e7eb;
}
.btn-compare-toggle {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 16px;
border: 2px solid #e5e7eb;
border-radius: 10px;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
cursor: pointer;
font-size: 14px;
font-weight: 600;
color: #64748b;
transition: all 0.2s ease;
}
.btn-compare-toggle:hover {
border-color: #1677ff;
color: #1677ff;
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
}
.btn-compare-toggle.active {
border-color: #1677ff;
background: linear-gradient(135deg, #1677ff 0%, #0958d9 100%);
color: white;
box-shadow: 0 4px 12px rgba(22, 119, 255, 0.3);
}
.toggle-icon {
font-size: 18px;
}
.compare-count {
background: rgba(255, 255, 255, 0.25);
padding: 2px 10px;
border-radius: 10px;
font-size: 12px;
font-weight: 600;
}
.compare-selected {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
}
.compare-tag {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 4px 10px;
border-radius: 15px;
font-size: 12px;
font-weight: 500;
}
.compare-hint {
margin-top: 10px;
text-align: center;
color: #64748b;
font-size: 13px;
padding: 8px;
background: #f8fafc;
border-radius: 8px;
}
/* 头部 */
.watchlist-header {
display: flex;