Files
lhbfx/frontend/src/App.vue
2026-05-02 18:27:36 +08:00

261 lines
7.7 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 { computed, onMounted, onUnmounted, shallowRef, watch } from 'vue'
import AppHero from './components/AppHero.vue'
import HomeControlScreen from './components/HomeControlScreen.vue'
import StockDetailScreen from './components/StockDetailScreen.vue'
import TraderDetailScreen from './components/TraderDetailScreen.vue'
import WarningCenterScreen from './components/WarningCenterScreen.vue'
import { useDashboardData } from './composables/useDashboardData'
import type { StockSearchItem, WarningItem } from './types'
const dashboard = useDashboardData()
type PageKey = 'home' | 'trader' | 'stock' | 'warning'
const currentPage = shallowRef<PageKey>('home')
const selectedTraderId = computed(() => dashboard.selectedTraderId.value)
const stockSearchQuery = shallowRef('')
const stockSearchResults = shallowRef<StockSearchItem[]>([])
const stockSearchLoading = shallowRef(false)
let stockSearchTimer: ReturnType<typeof window.setTimeout> | null = null
let latestStockSearchToken = 0
const navItems: Array<{ key: PageKey; label: string }> = [
{ key: 'home', label: '首页总控台' },
{ key: 'trader', label: '游资详情' },
{ key: 'stock', label: '个股详情' },
{ key: 'warning', label: '预警中心' },
]
function pageFromHash(): PageKey {
const hash = window.location.hash.replace(/^#\/?/, '')
if (hash === 'trader' || hash === 'stock' || hash === 'warning') return hash
return 'home'
}
function syncPageFromHash() {
currentPage.value = pageFromHash()
}
async function ensurePageData(page: PageKey) {
if (dashboard.isBooting.value) return
if (page === 'trader' && dashboard.selectedTraderId.value !== null) {
await dashboard.selectTrader(dashboard.selectedTraderId.value)
}
if (page === 'stock' && dashboard.selectedStockCode.value) {
await dashboard.selectStock(dashboard.selectedStockCode.value)
}
}
function navigate(page: PageKey) {
const nextHash = `#/${page}`
if (window.location.hash !== nextHash) {
window.location.hash = nextHash
}
currentPage.value = page
void ensurePageData(page)
}
async function handleSelectTrader(traderId: number) {
await dashboard.selectTrader(traderId)
navigate('trader')
}
async function handleSelectStock(stockCode: string) {
await dashboard.selectStock(stockCode)
navigate('stock')
}
async function handleSearchSelectStock(stock: Pick<StockSearchItem, 'stock_code' | 'stock_name'>) {
stockSearchQuery.value = stock.stock_name
stockSearchResults.value = []
await handleSelectStock(stock.stock_code)
}
async function handleSelectWarningInCenter(warning: WarningItem) {
await dashboard.selectWarning(warning)
}
async function handleSelectTradeDateRange(payload: { dateFrom: string; dateTo: string }) {
await dashboard.selectTradeDateRange(payload.dateFrom, payload.dateTo)
}
function handleUpdateTraderFilter(traderName: string) {
dashboard.selectedTraderFilter.value = traderName
void dashboard.loadActions()
}
function handleUpdateActionFilter(actionFilter: 'all' | 'buy' | 'sell' | 'net_buy' | 'net_sell') {
dashboard.selectedActionFilter.value = actionFilter
}
async function handleFollowStock(payload: {
stock_code: string
stock_name: string
source_trade_date: string | null
source_trader_name: string | null
}) {
await dashboard.addToWatchlist(payload)
}
async function handleUnfollowStock(stockCode: string) {
await dashboard.removeFromWatchlist(stockCode)
}
onMounted(() => {
syncPageFromHash()
window.addEventListener('hashchange', syncPageFromHash)
void (async () => {
await dashboard.initialize()
await ensurePageData(currentPage.value)
})()
})
onUnmounted(() => {
window.removeEventListener('hashchange', syncPageFromHash)
if (stockSearchTimer !== null) {
window.clearTimeout(stockSearchTimer)
}
})
watch(currentPage, (page) => {
void ensurePageData(page)
})
watch(stockSearchQuery, (nextQuery) => {
const keyword = nextQuery.trim()
if (stockSearchTimer !== null) {
window.clearTimeout(stockSearchTimer)
stockSearchTimer = null
}
if (!keyword) {
stockSearchResults.value = []
stockSearchLoading.value = false
return
}
stockSearchTimer = window.setTimeout(() => {
const currentToken = ++latestStockSearchToken
stockSearchLoading.value = true
void dashboard
.searchStocks(keyword, 8)
.then((results) => {
if (currentToken !== latestStockSearchToken) return
stockSearchResults.value = results
})
.catch(() => {
if (currentToken !== latestStockSearchToken) return
stockSearchResults.value = []
})
.finally(() => {
if (currentToken !== latestStockSearchToken) return
stockSearchLoading.value = false
})
}, 180)
})
</script>
<template>
<main class="page-shell">
<div v-if="dashboard.errorMessage.value" class="error-banner">
数据加载失败{{ dashboard.errorMessage.value }}
</div>
<AppHero
:summary="dashboard.summary.value"
:status="dashboard.status.value"
:nav-items="navItems"
:current-page="currentPage"
:search-query="stockSearchQuery"
:search-results="stockSearchResults"
:search-loading="stockSearchLoading"
@navigate="navigate"
@update-search-query="stockSearchQuery = $event"
@select-stock="handleSearchSelectStock"
/>
<div v-if="dashboard.isBooting.value" class="loading-state">
正在加载龙虎榜游资和预警数据...
</div>
<section v-else class="page-stage">
<HomeControlScreen
v-if="currentPage === 'home'"
:trade-dates="dashboard.availableTradeDates.value"
:selected-date-from="dashboard.selectedDateFrom.value"
:selected-date-to="dashboard.selectedDateTo.value"
:traders="dashboard.traders.value"
:selected-trader-filter="dashboard.selectedTraderFilter.value"
:selected-action-filter="dashboard.selectedActionFilter.value"
:watched-actions="dashboard.watchedActionRows.value"
:candidate-actions="dashboard.candidateActionRows.value"
:watchlist="dashboard.watchlistStocksForDisplay.value"
:metrics="dashboard.watchlistMetrics.value"
@select-trade-date-range="handleSelectTradeDateRange"
@update-trader-filter="handleUpdateTraderFilter"
@update-action-filter="handleUpdateActionFilter"
@select-stock="handleSelectStock"
@follow-stock="handleFollowStock"
@unfollow-stock="handleUnfollowStock"
/>
<TraderDetailScreen
v-else-if="currentPage === 'trader'"
:traders="dashboard.traders.value"
:trader-detail="dashboard.traderDetail.value"
:selected-trader-id="selectedTraderId"
@select-trader="handleSelectTrader"
@select-stock="handleSelectStock"
/>
<StockDetailScreen
v-else-if="currentPage === 'stock'"
:stock-detail="dashboard.stockDetail.value"
:active-warning="dashboard.activeWarning.value"
/>
<WarningCenterScreen
v-else
:warnings="dashboard.warnings.value"
:traders="dashboard.traders.value"
:active-warning="dashboard.activeWarning.value"
@select-warning="handleSelectWarningInCenter"
/>
</section>
</main>
</template>
<style scoped>
.page-shell {
display: grid;
grid-template-rows: auto auto minmax(0, 1fr);
gap: 10px;
height: calc(100vh - 12px);
overflow: hidden;
}
.loading-state,
.error-banner {
padding: 14px 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
background: rgba(255, 255, 255, 0.04);
color: var(--color-muted);
}
.error-banner {
border-color: rgba(255, 93, 93, 0.2);
color: #ffb4b4;
background: rgba(255, 93, 93, 0.08);
}
.page-stage {
min-height: 0;
overflow: hidden;
}
</style>