Files
zjjk/frontend/src/components/dashboard/OverviewPanel.vue
2026-03-20 21:47:30 +08:00

253 lines
6.1 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 } from 'vue'
import type { OverviewResponse } from '../../types/api'
import { formatAmount, formatPrecision, formatTimestamp } from '../../utils/formatters'
import MetricCard from './MetricCard.vue'
import TrendChart from './TrendChart.vue'
const props = defineProps<{
overview: OverviewResponse
}>()
const minuteChartSeries = computed(() => {
const southboundPoints = props.overview.minute_timeline
.filter((item) => item.amount_hkd_billion !== null)
.map((item) => ({
label: item.timestamp.slice(11, 16),
value: item.amount_hkd_billion ?? 0
}))
const benchmarkSeries = props.overview.benchmark_series.map((series) => ({
key: series.key,
label: series.label,
color: series.key === 'hsi' ? '#6fd0ff' : '#9bffb0',
axis: 'right' as const,
dashed: series.key === 'hsi',
formatter: (value: number) => `${value.toFixed(2)} ${series.unit}`,
points: series.points
.filter((item) => item.value !== null)
.map((item) => ({
label: item.timestamp.slice(11, 16),
value: item.value ?? 0
}))
}))
return [
{
key: 'southbound',
label: '南向净流入',
color: '#d6ad47',
axis: 'left' as const,
fill: true,
formatter: (value: number) => `${value.toFixed(2)} 亿港元`,
points: southboundPoints
},
...benchmarkSeries
].filter((item) => item.points.length)
})
</script>
<template>
<section class="panel">
<div class="panel__header">
<div>
<p class="panel__eyebrow">Realtime Overview</p>
<h2 class="panel__title">实时总览</h2>
</div>
<div class="panel__meta">
<span>更新 {{ formatTimestamp(overview.snapshot.updated_at) }}</span>
<span>{{ overview.snapshot.source_name }}</span>
<span>{{ formatPrecision(overview.snapshot.total_net_inflow.precision) }}</span>
</div>
</div>
<div class="overview-grid">
<MetricCard :metric="overview.snapshot.total_net_inflow" />
<MetricCard :metric="overview.snapshot.cumulative_net_inflow" />
<MetricCard :metric="overview.snapshot.shanghai_net_inflow" />
<MetricCard :metric="overview.snapshot.shenzhen_net_inflow" />
<MetricCard :metric="overview.snapshot.one_min_change" />
<MetricCard :metric="overview.snapshot.five_min_change" />
</div>
<div class="overview-body">
<article class="overview-card overview-card--chart">
<div class="overview-card__head">
<h3>分钟级净流入与港股指数叠加</h3>
<span>左轴为南向资金右轴为恒生指数与恒生科技</span>
</div>
<div class="overview-card__chart">
<TrendChart :series="minuteChartSeries" />
</div>
</article>
<article class="overview-card">
<div class="overview-card__head">
<h3>交易状态</h3>
<span>{{ formatPrecision(overview.snapshot.total_net_inflow.precision) }}</span>
</div>
<div class="overview-status">
<div class="overview-status__row">
<span>下一阈值</span>
<strong>{{ formatAmount(overview.snapshot.next_threshold_hkd_billion) }}</strong>
</div>
<div class="overview-status__row">
<span>阈值进度</span>
<strong>{{ (overview.snapshot.threshold_progress * 100).toFixed(1) }}%</strong>
</div>
<div class="overview-status__track">
<span class="overview-status__fill" :style="{ width: `${overview.snapshot.threshold_progress * 100}%` }"></span>
</div>
<p class="overview-status__reason">
{{
overview.snapshot.unavailable_reason ??
'当前已接入东方财富真实接口,页面与数据库快照保持一致。'
}}
</p>
</div>
</article>
</div>
</section>
</template>
<style scoped>
.panel {
display: grid;
gap: 0.7rem;
min-height: 100%;
padding: 0.9rem;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(8, 14, 26, 0.88);
}
.panel__header {
display: flex;
justify-content: space-between;
gap: 0.8rem;
align-items: flex-start;
}
.panel__eyebrow {
margin: 0;
color: var(--color-accent);
letter-spacing: 0.15em;
text-transform: uppercase;
font-size: 0.74rem;
}
.panel__title {
margin: 0.2rem 0 0;
font-family: var(--font-display);
font-size: 1.18rem;
}
.panel__meta {
display: flex;
gap: 0.8rem;
flex-wrap: wrap;
justify-content: flex-end;
color: var(--color-text-subtle);
font-size: 0.82rem;
}
.overview-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.65rem;
}
.overview-body {
display: grid;
grid-template-columns: minmax(0, 1.25fr) minmax(280px, 0.75fr);
gap: 0.7rem;
}
.overview-card {
display: grid;
gap: 0.65rem;
padding: 0.8rem;
background: rgba(13, 20, 36, 0.75);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.overview-card--chart {
min-height: 27rem;
}
.overview-card__head {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: baseline;
}
.overview-card__head h3 {
margin: 0;
font-size: 1rem;
}
.overview-card__head span {
color: var(--color-text-subtle);
font-size: 0.78rem;
}
.overview-card__chart {
min-height: 21rem;
}
.overview-status {
display: grid;
gap: 0.85rem;
align-content: start;
}
.overview-status__row {
display: flex;
justify-content: space-between;
gap: 0.9rem;
}
.overview-status__row span {
color: var(--color-text-subtle);
}
.overview-status__track {
height: 0.55rem;
background: rgba(255, 255, 255, 0.08);
border-radius: 999px;
overflow: hidden;
}
.overview-status__fill {
display: block;
height: 100%;
background: linear-gradient(90deg, #d7ad47, #e8d4a3);
}
.overview-status__reason {
margin: 0;
color: var(--color-text-muted);
line-height: 1.6;
}
@media (max-width: 1100px) {
.overview-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.overview-body {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.overview-grid {
grid-template-columns: 1fr;
}
.panel__header {
flex-direction: column;
}
}
</style>