253 lines
6.1 KiB
Vue
253 lines
6.1 KiB
Vue
|
|
<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>
|