2026-04-18 22:16:03 +08:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, onMounted, onUnmounted, shallowRef, useTemplateRef } from 'vue'
|
|
|
|
|
|
|
|
|
|
|
|
import type { TraderAction } from '../types'
|
|
|
|
|
|
import { formatSignedWanAmount, formatWanAmount, numberFromText } from '../utils/format'
|
|
|
|
|
|
|
|
|
|
|
|
type AggregatedActionRow = {
|
|
|
|
|
|
trade_date: string
|
|
|
|
|
|
buyTotalWan: number
|
|
|
|
|
|
sellTotalWan: number
|
|
|
|
|
|
netTotalWan: number
|
|
|
|
|
|
cumulativeNetWan: number
|
|
|
|
|
|
traderCount: number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
|
actions: TraderAction[]
|
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
|
|
const chartContainerRef = useTemplateRef<HTMLDivElement>('chartContainerRef')
|
|
|
|
|
|
const hoveredIndex = shallowRef<number | null>(null)
|
|
|
|
|
|
const containerWidth = shallowRef(0)
|
|
|
|
|
|
|
|
|
|
|
|
const aggregatedRows = computed<AggregatedActionRow[]>(() => {
|
|
|
|
|
|
const grouped = new Map<
|
|
|
|
|
|
string,
|
|
|
|
|
|
{
|
|
|
|
|
|
trade_date: string
|
|
|
|
|
|
buyTotalWan: number
|
|
|
|
|
|
sellTotalWan: number
|
|
|
|
|
|
netTotalWan: number
|
|
|
|
|
|
traders: Set<string>
|
|
|
|
|
|
}
|
|
|
|
|
|
>()
|
|
|
|
|
|
|
|
|
|
|
|
for (const action of props.actions) {
|
|
|
|
|
|
const buyTotalWan = numberFromText(action.buy_amount_wan) ?? 0
|
|
|
|
|
|
const sellTotalWan = numberFromText(action.sell_amount_wan) ?? 0
|
|
|
|
|
|
const netTotalWan = numberFromText(action.net_amount_wan) ?? buyTotalWan - sellTotalWan
|
|
|
|
|
|
const current = grouped.get(action.trade_date) ?? {
|
|
|
|
|
|
trade_date: action.trade_date,
|
|
|
|
|
|
buyTotalWan: 0,
|
|
|
|
|
|
sellTotalWan: 0,
|
|
|
|
|
|
netTotalWan: 0,
|
|
|
|
|
|
traders: new Set<string>(),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
current.buyTotalWan += buyTotalWan
|
|
|
|
|
|
current.sellTotalWan += sellTotalWan
|
|
|
|
|
|
current.netTotalWan += netTotalWan
|
|
|
|
|
|
if (action.matched_trader_name) {
|
|
|
|
|
|
current.traders.add(action.matched_trader_name)
|
|
|
|
|
|
}
|
|
|
|
|
|
grouped.set(action.trade_date, current)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let cumulativeNetWan = 0
|
|
|
|
|
|
return [...grouped.values()]
|
|
|
|
|
|
.sort((left, right) => left.trade_date.localeCompare(right.trade_date))
|
|
|
|
|
|
.map((item) => {
|
|
|
|
|
|
cumulativeNetWan += item.netTotalWan
|
|
|
|
|
|
return {
|
|
|
|
|
|
trade_date: item.trade_date,
|
|
|
|
|
|
buyTotalWan: item.buyTotalWan,
|
|
|
|
|
|
sellTotalWan: item.sellTotalWan,
|
|
|
|
|
|
netTotalWan: item.netTotalWan,
|
|
|
|
|
|
cumulativeNetWan,
|
|
|
|
|
|
traderCount: item.traders.size,
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const chartModel = computed(() => {
|
|
|
|
|
|
const rows = aggregatedRows.value
|
|
|
|
|
|
const measuredWidth = containerWidth.value || 0
|
|
|
|
|
|
const width = Math.max(measuredWidth, 320, rows.length * 54)
|
|
|
|
|
|
const height = 260
|
|
|
|
|
|
const left = 56
|
|
|
|
|
|
const right = 64
|
|
|
|
|
|
const top = 18
|
|
|
|
|
|
const bottom = 34
|
|
|
|
|
|
const innerWidth = width - left - right
|
|
|
|
|
|
const innerHeight = height - top - bottom
|
|
|
|
|
|
|
|
|
|
|
|
const emptyModel = {
|
|
|
|
|
|
width,
|
|
|
|
|
|
height,
|
|
|
|
|
|
left,
|
|
|
|
|
|
right,
|
|
|
|
|
|
top,
|
|
|
|
|
|
bottom,
|
|
|
|
|
|
zeroY: top + innerHeight / 2,
|
|
|
|
|
|
stepX: 0,
|
|
|
|
|
|
buyBars: [] as Array<{ x: number; y: number; height: number; title: string }>,
|
|
|
|
|
|
sellBars: [] as Array<{ x: number; y: number; height: number; title: string }>,
|
|
|
|
|
|
netPoints: [] as Array<{ x: number; y: number }>,
|
|
|
|
|
|
cumulativePoints: [] as Array<{ x: number; y: number }>,
|
|
|
|
|
|
labels: [] as Array<{ x: number; label: string; visible: boolean }>,
|
|
|
|
|
|
leftAxis: [] as Array<{ y: number; label: string }>,
|
|
|
|
|
|
hoverColumns: [] as Array<{ x: number; width: number }>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!rows.length) {
|
|
|
|
|
|
return emptyModel
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 21:41:31 +08:00
|
|
|
|
const valueMax = Math.max(
|
|
|
|
|
|
...rows.flatMap((row) => [
|
|
|
|
|
|
Math.abs(row.buyTotalWan),
|
|
|
|
|
|
Math.abs(row.sellTotalWan),
|
|
|
|
|
|
Math.abs(row.netTotalWan),
|
|
|
|
|
|
Math.abs(row.cumulativeNetWan),
|
|
|
|
|
|
]),
|
2026-04-18 22:16:03 +08:00
|
|
|
|
1,
|
|
|
|
|
|
)
|
|
|
|
|
|
const zeroY = top + innerHeight / 2
|
2026-04-20 21:41:31 +08:00
|
|
|
|
const yOfValue = (value: number) => zeroY - (value / valueMax) * (innerHeight / 2)
|
2026-04-18 22:16:03 +08:00
|
|
|
|
|
|
|
|
|
|
const stepX = rows.length === 1 ? innerWidth / 2 : innerWidth / (rows.length - 1)
|
|
|
|
|
|
const groupWidth = Math.max(20, Math.min(28, stepX * 0.58))
|
|
|
|
|
|
const barWidth = Math.max(7, groupWidth / 2 - 2)
|
|
|
|
|
|
const labelStep = Math.max(1, Math.ceil(rows.length / 6))
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
width,
|
|
|
|
|
|
height,
|
|
|
|
|
|
left,
|
|
|
|
|
|
right,
|
|
|
|
|
|
top,
|
|
|
|
|
|
bottom,
|
|
|
|
|
|
zeroY,
|
|
|
|
|
|
stepX,
|
|
|
|
|
|
buyBars: rows.map((row, index) => {
|
|
|
|
|
|
const x = left + (rows.length === 1 ? innerWidth / 2 : index * stepX)
|
2026-04-20 21:41:31 +08:00
|
|
|
|
const y = yOfValue(row.buyTotalWan)
|
2026-04-18 22:16:03 +08:00
|
|
|
|
return {
|
|
|
|
|
|
x: x - barWidth - 2,
|
|
|
|
|
|
y,
|
|
|
|
|
|
height: Math.max(2, zeroY - y),
|
|
|
|
|
|
title: `${row.trade_date}\n买入 ${formatWanAmount(row.buyTotalWan)}\n卖出 ${formatWanAmount(row.sellTotalWan)}\n净额 ${formatSignedWanAmount(row.netTotalWan)}`,
|
|
|
|
|
|
}
|
|
|
|
|
|
}),
|
|
|
|
|
|
sellBars: rows.map((row, index) => {
|
|
|
|
|
|
const x = left + (rows.length === 1 ? innerWidth / 2 : index * stepX)
|
|
|
|
|
|
const y = zeroY
|
2026-04-20 21:41:31 +08:00
|
|
|
|
const sellY = yOfValue(-row.sellTotalWan)
|
2026-04-18 22:16:03 +08:00
|
|
|
|
return {
|
|
|
|
|
|
x: x + 2,
|
|
|
|
|
|
y,
|
|
|
|
|
|
height: Math.max(2, sellY - zeroY),
|
|
|
|
|
|
title: `${row.trade_date}\n买入 ${formatWanAmount(row.buyTotalWan)}\n卖出 ${formatWanAmount(row.sellTotalWan)}\n净额 ${formatSignedWanAmount(row.netTotalWan)}`,
|
|
|
|
|
|
}
|
|
|
|
|
|
}),
|
|
|
|
|
|
netPoints: rows.map((row, index) => ({
|
|
|
|
|
|
x: left + (rows.length === 1 ? innerWidth / 2 : index * stepX),
|
2026-04-20 21:41:31 +08:00
|
|
|
|
y: yOfValue(row.netTotalWan),
|
2026-04-18 22:16:03 +08:00
|
|
|
|
})),
|
|
|
|
|
|
cumulativePoints: rows.map((row, index) => ({
|
|
|
|
|
|
x: left + (rows.length === 1 ? innerWidth / 2 : index * stepX),
|
2026-04-20 21:41:31 +08:00
|
|
|
|
y: yOfValue(row.cumulativeNetWan),
|
2026-04-18 22:16:03 +08:00
|
|
|
|
})),
|
|
|
|
|
|
labels: rows.map((row, index) => ({
|
|
|
|
|
|
x: left + (rows.length === 1 ? innerWidth / 2 : index * stepX),
|
|
|
|
|
|
label: row.trade_date.slice(5),
|
|
|
|
|
|
visible: index % labelStep === 0 || index === rows.length - 1,
|
|
|
|
|
|
})),
|
|
|
|
|
|
leftAxis: Array.from({ length: 5 }, (_, index) => {
|
2026-04-20 21:41:31 +08:00
|
|
|
|
const value = valueMax - (valueMax * 2 * index) / 4
|
|
|
|
|
|
return { y: yOfValue(value), label: formatSignedWanAmount(value) }
|
2026-04-18 22:16:03 +08:00
|
|
|
|
}),
|
|
|
|
|
|
hoverColumns: rows.map((_row, index) => ({
|
|
|
|
|
|
x: left + (rows.length === 1 ? innerWidth / 2 : index * stepX) - Math.max(stepX, 20) / 2,
|
|
|
|
|
|
width: Math.max(stepX, 20),
|
|
|
|
|
|
})),
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const netLinePoints = computed(() => chartModel.value.netPoints.map((point) => `${point.x},${point.y}`).join(' '))
|
|
|
|
|
|
const cumulativeLinePoints = computed(() =>
|
|
|
|
|
|
chartModel.value.cumulativePoints.map((point) => `${point.x},${point.y}`).join(' '),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const activeRow = computed(() => {
|
|
|
|
|
|
if (!aggregatedRows.value.length) return null
|
|
|
|
|
|
if (hoveredIndex.value === null) return null
|
|
|
|
|
|
return aggregatedRows.value[hoveredIndex.value] ?? null
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const activeTooltip = computed(() => {
|
|
|
|
|
|
if (hoveredIndex.value === null || !activeRow.value) return null
|
|
|
|
|
|
const point = chartModel.value.netPoints[hoveredIndex.value]
|
|
|
|
|
|
if (!point) return null
|
|
|
|
|
|
|
|
|
|
|
|
const tooltipWidth = 168
|
|
|
|
|
|
const tooltipHeight = 90
|
|
|
|
|
|
const x = Math.min(
|
|
|
|
|
|
Math.max(point.x - tooltipWidth / 2, chartModel.value.left + 6),
|
|
|
|
|
|
chartModel.value.width - chartModel.value.right - tooltipWidth - 6,
|
|
|
|
|
|
)
|
|
|
|
|
|
const prefersBelow = point.y < chartModel.value.top + tooltipHeight + 24
|
|
|
|
|
|
const y = prefersBelow
|
|
|
|
|
|
? Math.min(point.y + 12, chartModel.value.height - chartModel.value.bottom - tooltipHeight - 6)
|
|
|
|
|
|
: Math.max(point.y - tooltipHeight - 12, chartModel.value.top + 6)
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
x,
|
|
|
|
|
|
y,
|
|
|
|
|
|
width: tooltipWidth,
|
|
|
|
|
|
height: tooltipHeight,
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
function setHoveredIndex(index: number) {
|
|
|
|
|
|
hoveredIndex.value = index
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clearHoveredIndex() {
|
|
|
|
|
|
hoveredIndex.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let resizeObserver: ResizeObserver | null = null
|
|
|
|
|
|
|
|
|
|
|
|
function syncContainerWidth() {
|
|
|
|
|
|
containerWidth.value = chartContainerRef.value?.clientWidth ?? 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
syncContainerWidth()
|
|
|
|
|
|
if (chartContainerRef.value) {
|
|
|
|
|
|
resizeObserver = new ResizeObserver(() => {
|
|
|
|
|
|
syncContainerWidth()
|
|
|
|
|
|
})
|
|
|
|
|
|
resizeObserver.observe(chartContainerRef.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
resizeObserver?.disconnect()
|
|
|
|
|
|
resizeObserver = null
|
|
|
|
|
|
})
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="action-chart-panel">
|
|
|
|
|
|
<div class="action-chart-head">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h4 class="action-chart-title">买卖力度趋势</h4>
|
|
|
|
|
|
<p class="action-chart-note">柱形图显示买卖,折线显示单日净额和累计净额。</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="action-chart-legend">
|
|
|
|
|
|
<span class="legend-chip">
|
|
|
|
|
|
<span class="legend-swatch bar buy" />
|
|
|
|
|
|
买入
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="legend-chip">
|
|
|
|
|
|
<span class="legend-swatch bar sell" />
|
|
|
|
|
|
卖出
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="legend-chip">
|
|
|
|
|
|
<span class="legend-swatch line net" />
|
|
|
|
|
|
当日净额
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="legend-chip">
|
|
|
|
|
|
<span class="legend-swatch line cumulative" />
|
|
|
|
|
|
累计净额
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="aggregatedRows.length" class="action-chart-body">
|
|
|
|
|
|
<div ref="chartContainerRef" class="action-chart-scroll">
|
|
|
|
|
|
<svg
|
|
|
|
|
|
class="action-chart-svg"
|
|
|
|
|
|
:viewBox="`0 0 ${chartModel.width} ${chartModel.height}`"
|
|
|
|
|
|
:style="{ width: `${chartModel.width}px`, height: `${chartModel.height}px` }"
|
|
|
|
|
|
preserveAspectRatio="none"
|
|
|
|
|
|
@mouseleave="clearHoveredIndex"
|
|
|
|
|
|
>
|
|
|
|
|
|
<g opacity="0.08" stroke="#ffffff">
|
|
|
|
|
|
<line
|
|
|
|
|
|
v-for="tick in chartModel.leftAxis"
|
|
|
|
|
|
:key="`left-grid-${tick.label}`"
|
|
|
|
|
|
:x1="chartModel.left"
|
|
|
|
|
|
:y1="tick.y"
|
|
|
|
|
|
:x2="chartModel.width - chartModel.right"
|
|
|
|
|
|
:y2="tick.y"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</g>
|
|
|
|
|
|
|
|
|
|
|
|
<line
|
|
|
|
|
|
:x1="chartModel.left"
|
|
|
|
|
|
:y1="chartModel.zeroY"
|
|
|
|
|
|
:x2="chartModel.width - chartModel.right"
|
|
|
|
|
|
:y2="chartModel.zeroY"
|
|
|
|
|
|
stroke="rgba(255,255,255,0.22)"
|
|
|
|
|
|
stroke-width="1.2"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<g v-for="(tick, index) in chartModel.leftAxis" :key="`left-axis-${index}`">
|
|
|
|
|
|
<text x="0" :y="tick.y + 4" fill="#93a2b5" font-size="10">{{ tick.label }}</text>
|
|
|
|
|
|
</g>
|
|
|
|
|
|
|
|
|
|
|
|
<g v-for="(column, index) in chartModel.hoverColumns" :key="`hover-${index}`">
|
|
|
|
|
|
<rect
|
|
|
|
|
|
:x="column.x"
|
|
|
|
|
|
y="0"
|
|
|
|
|
|
:width="column.width"
|
|
|
|
|
|
:height="chartModel.height"
|
|
|
|
|
|
:fill="hoveredIndex === index ? 'rgba(255,255,255,0.04)' : 'transparent'"
|
|
|
|
|
|
@mouseenter="setHoveredIndex(index)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</g>
|
|
|
|
|
|
|
|
|
|
|
|
<g v-for="(bar, index) in chartModel.buyBars" :key="`buy-${index}`">
|
|
|
|
|
|
<rect
|
|
|
|
|
|
:x="bar.x"
|
|
|
|
|
|
:y="bar.y"
|
|
|
|
|
|
width="10"
|
|
|
|
|
|
:height="bar.height"
|
|
|
|
|
|
rx="2"
|
|
|
|
|
|
fill="#ff7b7b"
|
|
|
|
|
|
>
|
|
|
|
|
|
<title>{{ bar.title }}</title>
|
|
|
|
|
|
</rect>
|
|
|
|
|
|
</g>
|
|
|
|
|
|
|
|
|
|
|
|
<g v-for="(bar, index) in chartModel.sellBars" :key="`sell-${index}`">
|
|
|
|
|
|
<rect
|
|
|
|
|
|
:x="bar.x"
|
|
|
|
|
|
:y="bar.y"
|
|
|
|
|
|
width="10"
|
|
|
|
|
|
:height="bar.height"
|
|
|
|
|
|
rx="2"
|
|
|
|
|
|
fill="#4ca8ff"
|
|
|
|
|
|
>
|
|
|
|
|
|
<title>{{ bar.title }}</title>
|
|
|
|
|
|
</rect>
|
|
|
|
|
|
</g>
|
|
|
|
|
|
|
|
|
|
|
|
<polyline
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
stroke="#f0c96a"
|
|
|
|
|
|
stroke-width="2.4"
|
|
|
|
|
|
:points="cumulativeLinePoints"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<polyline
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
stroke="#7de1b2"
|
|
|
|
|
|
stroke-width="2.2"
|
|
|
|
|
|
:points="netLinePoints"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<g v-for="(point, index) in chartModel.netPoints" :key="`net-point-${index}`">
|
|
|
|
|
|
<circle :cx="point.x" :cy="point.y" r="3.4" fill="#7de1b2" />
|
|
|
|
|
|
</g>
|
|
|
|
|
|
<g v-for="(point, index) in chartModel.cumulativePoints" :key="`cumulative-point-${index}`">
|
|
|
|
|
|
<circle :cx="point.x" :cy="point.y" r="3.2" fill="#f0c96a" />
|
|
|
|
|
|
</g>
|
|
|
|
|
|
|
|
|
|
|
|
<g v-if="activeTooltip && activeRow">
|
|
|
|
|
|
<rect
|
|
|
|
|
|
:x="activeTooltip.x"
|
|
|
|
|
|
:y="activeTooltip.y"
|
|
|
|
|
|
:width="activeTooltip.width"
|
|
|
|
|
|
:height="activeTooltip.height"
|
|
|
|
|
|
rx="10"
|
|
|
|
|
|
fill="rgba(8, 12, 18, 0.96)"
|
|
|
|
|
|
stroke="rgba(255,255,255,0.08)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<text :x="activeTooltip.x + 12" :y="activeTooltip.y + 18" fill="#ffffff" font-size="11" font-weight="700">
|
|
|
|
|
|
{{ activeRow.trade_date }}
|
|
|
|
|
|
</text>
|
|
|
|
|
|
<text :x="activeTooltip.x + 12" :y="activeTooltip.y + 36" fill="#ff7b7b" font-size="11">
|
|
|
|
|
|
买入 {{ formatWanAmount(activeRow.buyTotalWan) }}
|
|
|
|
|
|
</text>
|
|
|
|
|
|
<text :x="activeTooltip.x + 12" :y="activeTooltip.y + 52" fill="#4ca8ff" font-size="11">
|
|
|
|
|
|
卖出 {{ formatWanAmount(activeRow.sellTotalWan) }}
|
|
|
|
|
|
</text>
|
|
|
|
|
|
<text :x="activeTooltip.x + 12" :y="activeTooltip.y + 68" fill="#7de1b2" font-size="11">
|
|
|
|
|
|
当日净额 {{ formatSignedWanAmount(activeRow.netTotalWan) }}
|
|
|
|
|
|
</text>
|
|
|
|
|
|
<text :x="activeTooltip.x + 12" :y="activeTooltip.y + 84" fill="#f0c96a" font-size="11">
|
|
|
|
|
|
累计净额 {{ formatSignedWanAmount(activeRow.cumulativeNetWan) }}
|
|
|
|
|
|
</text>
|
|
|
|
|
|
</g>
|
|
|
|
|
|
|
|
|
|
|
|
<g v-for="(label, index) in chartModel.labels" :key="`label-${index}`">
|
|
|
|
|
|
<text
|
|
|
|
|
|
v-if="label.visible"
|
|
|
|
|
|
:x="label.x"
|
|
|
|
|
|
:y="chartModel.height - 10"
|
|
|
|
|
|
text-anchor="middle"
|
|
|
|
|
|
fill="#93a2b5"
|
|
|
|
|
|
font-size="10"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ label.label }}
|
|
|
|
|
|
</text>
|
|
|
|
|
|
</g>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<p v-else class="action-chart-empty">暂无可展示的买卖明细。</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.action-chart-panel {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-chart-head {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-chart-title {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-chart-note {
|
|
|
|
|
|
margin: 6px 0 0;
|
|
|
|
|
|
color: var(--color-muted);
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
line-height: 1.55;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-chart-legend {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.legend-chip {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: 6px 10px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
|
|
|
|
color: var(--color-muted);
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.03);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.legend-swatch {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 18px;
|
|
|
|
|
|
height: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.legend-swatch.bar {
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.legend-swatch.buy {
|
|
|
|
|
|
background: #ff7b7b;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.legend-swatch.sell {
|
|
|
|
|
|
background: #4ca8ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.legend-swatch.line::before {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
width: 18px;
|
|
|
|
|
|
height: 2px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.legend-swatch.line.net::before {
|
|
|
|
|
|
background: #7de1b2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.legend-swatch.line.cumulative::before {
|
|
|
|
|
|
background: #f0c96a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-chart-body {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-chart-scroll {
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
overflow-y: hidden;
|
|
|
|
|
|
padding-bottom: 2px;
|
|
|
|
|
|
min-height: 228px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-chart-svg {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
min-height: 228px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-chart-empty {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
color: var(--color-muted);
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
line-height: 1.65;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|