180 lines
3.7 KiB
Vue
180 lines
3.7 KiB
Vue
<template>
|
||
<div class="header-inner">
|
||
<div class="symbol">
|
||
<span class="name">{{ quote.name }}</span>
|
||
<span class="code">{{ quote.code }}</span>
|
||
<span class="exchange">{{ quote.exchange }}</span>
|
||
<el-tag size="small" type="info" effect="plain">{{ quote.status }}</el-tag>
|
||
<span class="ws-status" :class="wsStatusClass" :title="wsStatusTitle">
|
||
<span class="ws-dot" aria-hidden="true" />
|
||
<span class="ws-label">{{ wsStatusLabel }}</span>
|
||
</span>
|
||
</div>
|
||
<div class="price-block">
|
||
<span class="last" :class="priceClass">{{ quote.last.toFixed(2) }}</span>
|
||
<span class="chg" :class="priceClass">
|
||
{{ formatSigned(quote.change) }}
|
||
({{ formatSigned(quote.changePercent) }}%)
|
||
</span>
|
||
<span class="time">更新 {{ quote.updatedAt }}</span>
|
||
</div>
|
||
<div class="actions">
|
||
<el-button :icon="Setting" circle title="设置" @click="settingsVisible = true" />
|
||
</div>
|
||
<SettingsDialog v-model="settingsVisible" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, ref } from 'vue'
|
||
import { Setting } from '@element-plus/icons-vue'
|
||
import type { QuoteData } from '../../types'
|
||
import type { WsConnectionStatus } from '../../api/baidu/ws'
|
||
import SettingsDialog from './SettingsDialog.vue'
|
||
|
||
const props = defineProps<{
|
||
quote: QuoteData
|
||
wsStatus: WsConnectionStatus
|
||
wsInSession: boolean
|
||
}>()
|
||
|
||
const settingsVisible = ref(false)
|
||
|
||
const priceClass = computed(() =>
|
||
props.quote.change >= 0 ? 'price-up' : 'price-down',
|
||
)
|
||
|
||
const wsStatusLabel = computed(() => {
|
||
if (!props.wsInSession) return '休市'
|
||
if (props.wsStatus === 'connected') return '已连接'
|
||
if (props.wsStatus === 'connecting') return '连接中'
|
||
return '未连接'
|
||
})
|
||
|
||
const wsStatusClass = computed(() => {
|
||
if (!props.wsInSession) return 'is-off'
|
||
if (props.wsStatus === 'connected') return 'is-on'
|
||
if (props.wsStatus === 'connecting') return 'is-pending'
|
||
return 'is-off'
|
||
})
|
||
|
||
const wsStatusTitle = computed(() => {
|
||
if (!props.wsInSession) {
|
||
return '非交易时段(09:00–11:30 / 13:30–15:00 / 21:00–23:00)'
|
||
}
|
||
return `WebSocket ${wsStatusLabel.value}`
|
||
})
|
||
|
||
function formatSigned(n: number) {
|
||
if (n > 0) return `+${Number(n).toFixed(2)}`
|
||
return Number(n).toFixed(2)
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.header-inner {
|
||
height: 100%;
|
||
max-width: 1440px;
|
||
margin: 0 auto;
|
||
padding: 0 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 24px;
|
||
}
|
||
|
||
.symbol {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-width: 220px;
|
||
}
|
||
|
||
.name {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.code,
|
||
.exchange {
|
||
color: var(--text-secondary);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.ws-status {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
margin-left: 4px;
|
||
padding: 2px 8px;
|
||
border-radius: 999px;
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
background: #f0f2f5;
|
||
color: var(--text-secondary);
|
||
user-select: none;
|
||
}
|
||
|
||
.ws-dot {
|
||
width: 7px;
|
||
height: 7px;
|
||
border-radius: 50%;
|
||
background: currentColor;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.ws-status.is-on {
|
||
color: var(--down);
|
||
background: color-mix(in srgb, var(--down) 12%, #fff);
|
||
}
|
||
|
||
.ws-status.is-pending {
|
||
color: #d48806;
|
||
background: color-mix(in srgb, #d48806 12%, #fff);
|
||
}
|
||
|
||
.ws-status.is-pending .ws-dot {
|
||
animation: ws-pulse 1.2s ease-in-out infinite;
|
||
}
|
||
|
||
.ws-status.is-off {
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
@keyframes ws-pulse {
|
||
0%,
|
||
100% {
|
||
opacity: 1;
|
||
}
|
||
50% {
|
||
opacity: 0.35;
|
||
}
|
||
}
|
||
|
||
.price-block {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 12px;
|
||
flex: 1;
|
||
}
|
||
|
||
.last {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.chg {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.time {
|
||
color: var(--text-secondary);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.actions {
|
||
margin-left: auto;
|
||
}
|
||
</style>
|