Files
stock/frontend/src/components/Sparkline.vue
2026-09-07 18:07:31 +08:00

91 lines
3.6 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">
// 迷你走势归一化折线SVG viewBox=1x1 + preserveAspectRatio=none 拉伸,
// 描边 vector-effect=non-scaling-stroke 保证粗细不缩放;
// 端点/悬停点用 HTML 圆点(非等比 viewBox 会把 SVG 圆拉成椭圆)。
// 从主页大盘总览卡片抽出指数卡片两处共用。hover 出十字点与「日期 数值」提示。
import { computed, ref } from 'vue';
const props = defineProps<{
values: number[];
dates?: string[]; // 与 values 对齐的交易日YYYYMMDDhover 提示用
pct?: number | null; // 涨跌幅决定配色(正=涨色/负=跌色/缺=灰)
}>();
const PAD = 0.08; // 上下留白,避免贴边
const geom = computed(() => {
const vals = props.values;
const n = vals.length;
if (n < 2) return null;
const lo = Math.min(...vals);
const hi = Math.max(...vals);
const span = hi - lo || Math.abs(hi) || 1;
const pts = vals.map((v, i) => ({
x: i / (n - 1),
y: 1 - ((v - lo) / span) * (1 - 2 * PAD) - PAD,
}));
const line = pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x.toFixed(4)},${p.y.toFixed(4)}`).join(' ');
const area = `${line} L1,1 L0,1 Z`;
return { pts, line, area, last: pts[n - 1] };
});
const color = computed(() => {
const p = props.pct;
if (p == null || p === 0) return '#A8AFB8';
return p > 0 ? 'var(--color-up)' : 'var(--color-down)';
});
// hover相对坐标记录点索引tooltip 跟随
const hover = ref<{ i: number; x: number; y: number } | null>(null);
function onMove(e: MouseEvent) {
if (!geom.value) return;
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
const t = Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width));
const i = Math.round(t * (props.values.length - 1));
hover.value = { i, x: geom.value.pts[i].x, y: geom.value.pts[i].y };
}
const hoverText = computed(() => {
const h = hover.value;
if (!h) return '';
const d = props.dates?.[h.i] ?? '';
const iso = d ? `${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}` : '';
const v = props.values[h.i];
return `${iso} ${v != null ? v.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '--'}`;
});
</script>
<template>
<div class="relative h-9" @mousemove="onMove" @mouseleave="hover = null">
<svg v-if="geom" class="h-full w-full" viewBox="0 0 1 1" preserveAspectRatio="none" aria-hidden="true">
<path :d="geom.area" :fill="color" fill-opacity="0.1" />
<path
:d="geom.line"
fill="none"
:stroke="color"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
vector-effect="non-scaling-stroke"
/>
</svg>
<!-- 端点 2px 表面环与悬停点 -->
<span
v-if="geom"
class="pointer-events-none absolute h-2.5 w-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-[#101014]"
:style="{ left: `${geom.last.x * 100}%`, top: `${geom.last.y * 100}%`, backgroundColor: color }"
/>
<template v-if="hover && geom">
<span
class="pointer-events-none absolute h-2.5 w-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-[#101014]"
:style="{ left: `${hover.x * 100}%`, top: `${hover.y * 100}%`, backgroundColor: color }"
/>
<span
class="pointer-events-none absolute -top-1 z-10 -translate-y-full whitespace-nowrap rounded border border-[#3A3D46] bg-[#1A1B21] px-1.5 py-0.5 font-mono text-[10px] tabular-nums text-[#E5E7EB]"
:style="{ left: `${Math.min(82, Math.max(18, hover.x * 100))}%` }"
>{{ hoverText }}</span>
</template>
</div>
</template>