261 lines
7.7 KiB
Vue
261 lines
7.7 KiB
Vue
<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>
|