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(() => {