diff --git a/frontend/src/components/HomeControlScreen.vue b/frontend/src/components/HomeControlScreen.vue index 63212de..d786793 100644 --- a/frontend/src/components/HomeControlScreen.vue +++ b/frontend/src/components/HomeControlScreen.vue @@ -286,7 +286,7 @@ function updateDateRange(field: 'from' | 'to', value: string) {

待加入关注

-

净额已放大显示,同时补上板块、股价、市值和所属板块信息,方便你快速筛候选。

+

默认展示当天游资操作过的全部股票,买入、卖出和净额方向可通过上方筛选快速切换。

{{ candidateActions.length }} 个候选
@@ -301,7 +301,7 @@ function updateDateRange(field: 'from' | 'to', value: string) { /> -
当前筛选条件下没有新的候选股,调整日期或游资条件后再看。
+
当前筛选条件下没有新的候选股,调整日期、游资或方向条件后再看。
@@ -488,9 +488,11 @@ function updateDateRange(field: 'from' | 'to', value: string) { } .home-grid { - grid-template-columns: minmax(0, 1.56fr) minmax(660px, 1.16fr); + grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); + gap: 12px; align-items: stretch; min-height: 0; + overflow: hidden; } .card-panel { @@ -608,6 +610,7 @@ function updateDateRange(field: 'from' | 'to', value: string) { .candidate-panel { min-height: 0; + min-width: 0; } .candidate-list { @@ -616,7 +619,7 @@ function updateDateRange(field: 'from' | 'to', value: string) { align-content: start; overflow: auto; padding-right: 4px; - grid-auto-rows: 124px; + grid-auto-rows: minmax(124px, auto); } .buy { diff --git a/frontend/src/components/StockActionTimelineChart.vue b/frontend/src/components/StockActionTimelineChart.vue index 915a05a..5f4bf59 100644 --- a/frontend/src/components/StockActionTimelineChart.vue +++ b/frontend/src/components/StockActionTimelineChart.vue @@ -97,7 +97,6 @@ const chartModel = computed(() => { cumulativePoints: [] as Array<{ x: number; y: number }>, labels: [] as Array<{ x: number; label: string; visible: boolean }>, leftAxis: [] as Array<{ y: number; label: string }>, - rightAxis: [] as Array<{ y: number; label: string }>, hoverColumns: [] as Array<{ x: number; width: number }>, } @@ -105,18 +104,17 @@ const chartModel = computed(() => { return emptyModel } - const leftMax = Math.max( - ...rows.flatMap((row) => [Math.abs(row.buyTotalWan), Math.abs(row.sellTotalWan), Math.abs(row.netTotalWan)]), + const valueMax = Math.max( + ...rows.flatMap((row) => [ + Math.abs(row.buyTotalWan), + Math.abs(row.sellTotalWan), + Math.abs(row.netTotalWan), + Math.abs(row.cumulativeNetWan), + ]), 1, ) const zeroY = top + innerHeight / 2 - const yOfLeft = (value: number) => zeroY - (value / leftMax) * (innerHeight / 2) - - const cumulativeValues = rows.map((row) => row.cumulativeNetWan) - const rightMin = Math.min(0, ...cumulativeValues) - const rightMax = Math.max(0, ...cumulativeValues) - const rightRange = rightMax - rightMin || 1 - const yOfRight = (value: number) => top + ((rightMax - value) / rightRange) * innerHeight + const yOfValue = (value: number) => zeroY - (value / valueMax) * (innerHeight / 2) const stepX = rows.length === 1 ? innerWidth / 2 : innerWidth / (rows.length - 1) const groupWidth = Math.max(20, Math.min(28, stepX * 0.58)) @@ -134,7 +132,7 @@ const chartModel = computed(() => { stepX, buyBars: rows.map((row, index) => { const x = left + (rows.length === 1 ? innerWidth / 2 : index * stepX) - const y = yOfLeft(row.buyTotalWan) + const y = yOfValue(row.buyTotalWan) return { x: x - barWidth - 2, y, @@ -145,7 +143,7 @@ const chartModel = computed(() => { sellBars: rows.map((row, index) => { const x = left + (rows.length === 1 ? innerWidth / 2 : index * stepX) const y = zeroY - const sellY = yOfLeft(-row.sellTotalWan) + const sellY = yOfValue(-row.sellTotalWan) return { x: x + 2, y, @@ -155,11 +153,11 @@ const chartModel = computed(() => { }), netPoints: rows.map((row, index) => ({ x: left + (rows.length === 1 ? innerWidth / 2 : index * stepX), - y: yOfLeft(row.netTotalWan), + y: yOfValue(row.netTotalWan), })), cumulativePoints: rows.map((row, index) => ({ x: left + (rows.length === 1 ? innerWidth / 2 : index * stepX), - y: yOfRight(row.cumulativeNetWan), + y: yOfValue(row.cumulativeNetWan), })), labels: rows.map((row, index) => ({ x: left + (rows.length === 1 ? innerWidth / 2 : index * stepX), @@ -167,12 +165,8 @@ const chartModel = computed(() => { visible: index % labelStep === 0 || index === rows.length - 1, })), leftAxis: Array.from({ length: 5 }, (_, index) => { - const value = leftMax - (leftMax * 2 * index) / 4 - return { y: yOfLeft(value), label: formatSignedWanAmount(value) } - }), - rightAxis: Array.from({ length: 5 }, (_, index) => { - const value = rightMax - (rightRange * index) / 4 - return { y: yOfRight(value), label: formatSignedWanAmount(value) } + const value = valueMax - (valueMax * 2 * index) / 4 + return { y: yOfValue(value), label: formatSignedWanAmount(value) } }), hoverColumns: rows.map((_row, index) => ({ x: left + (rows.length === 1 ? innerWidth / 2 : index * stepX) - Math.max(stepX, 20) / 2, @@ -306,12 +300,6 @@ onUnmounted(() => { {{ tick.label }} - - - {{ tick.label }} - - - { class="hover-action" > {{ action.matched_trader_name }} + 营业部 {{ action.seat_name || '-' }} 买入 {{ formatWanAmount(action.buy_amount_wan) }} 卖出 {{ formatWanAmount(action.sell_amount_wan) }} 净额 {{ formatSignedWanAmount(action.net_amount_wan) }} @@ -931,6 +932,13 @@ onUnmounted(() => { font-size: 12px; } +.seat-name { + color: var(--color-muted); + line-height: 1.45; + overflow-wrap: anywhere; + word-break: break-word; +} + .hover-empty { margin: 0; color: var(--color-muted); diff --git a/frontend/src/composables/useDashboardData.ts b/frontend/src/composables/useDashboardData.ts index bb700bb..8bac5bf 100644 --- a/frontend/src/composables/useDashboardData.ts +++ b/frontend/src/composables/useDashboardData.ts @@ -59,16 +59,59 @@ export function useDashboardData() { .filter((item): item is string => Boolean(item)) }) + function inferActionSide(buyAmount: number, sellAmount: number, netAmount: number): ActionItem['action_side'] { + if (buyAmount > 0 && sellAmount <= 0) return 'buy' + if (sellAmount > 0 && buyAmount <= 0) return 'sell' + if (netAmount >= 0) return 'net_buy' + return 'net_sell' + } + + function matchesActionFilter(item: ActionItem): boolean { + const buyAmount = numberFromText(item.buy_amount_wan) ?? 0 + const sellAmount = numberFromText(item.sell_amount_wan) ?? 0 + const netAmount = numberFromText(item.net_amount_wan) ?? buyAmount - sellAmount + + if (selectedActionFilter.value === 'buy') return buyAmount > 0 + if (selectedActionFilter.value === 'sell') return sellAmount > 0 + if (selectedActionFilter.value === 'net_buy') return netAmount > 0 + if (selectedActionFilter.value === 'net_sell') return netAmount < 0 + return true + } + + function aggregateCandidateActions(rows: ActionItem[]): ActionItem[] { + const groups = new Map() + + for (const item of rows) { + const existing = groups.get(item.stock_code) + if (!existing) { + groups.set(item.stock_code, { ...item }) + continue + } + + const nextBuy = (numberFromText(existing.buy_amount_wan) ?? 0) + (numberFromText(item.buy_amount_wan) ?? 0) + const nextSell = (numberFromText(existing.sell_amount_wan) ?? 0) + (numberFromText(item.sell_amount_wan) ?? 0) + const nextNet = nextBuy - nextSell + const traderNames = new Set([existing.trader_name, item.trader_name].filter(Boolean)) + const tableTitles = new Set([existing.table_title, item.table_title].filter(Boolean)) + const seatNames = new Set([existing.seat_name, item.seat_name].filter(Boolean)) + + groups.set(item.stock_code, { + ...existing, + trader_name: [...traderNames].join(' / '), + table_title: [...tableTitles].join(' / '), + seat_name: seatNames.size > 1 ? `${seatNames.size}个席位` : existing.seat_name, + buy_amount_wan: nextBuy.toFixed(2), + sell_amount_wan: nextSell.toFixed(2), + net_amount_wan: nextNet.toFixed(2), + action_side: inferActionSide(nextBuy, nextSell, nextNet), + }) + } + + return [...groups.values()] + } + const filteredActions = computed(() => { - return actions.value.filter((item) => { - if (selectedTraderFilter.value !== 'all' && item.trader_name !== selectedTraderFilter.value) { - return false - } - if (selectedActionFilter.value !== 'all' && item.action_side !== selectedActionFilter.value) { - return false - } - return true - }) + return actions.value.filter(matchesActionFilter) }) const watchlistMap = computed(() => { @@ -80,14 +123,8 @@ export function useDashboardData() { }) const candidateActionRows = computed(() => { - const unique = new Map() - for (const item of filteredActions.value) { - if (watchlistMap.value.has(item.stock_code)) continue - if (!unique.has(item.stock_code)) { - unique.set(item.stock_code, item) - } - } - return [...unique.values()] + const rows = actions.value.filter((item) => !watchlistMap.value.has(item.stock_code)) + return aggregateCandidateActions(rows).filter(matchesActionFilter) }) const watchlistMetrics = computed(() => {