fix: refine home filters and stock action chart
This commit is contained in:
@ -286,7 +286,7 @@ function updateDateRange(field: 'from' | 'to', value: string) {
|
|||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="section-title">待加入关注</h3>
|
<h3 class="section-title">待加入关注</h3>
|
||||||
<p class="section-caption">净额已放大显示,同时补上板块、股价、市值和所属板块信息,方便你快速筛候选。</p>
|
<p class="section-caption">默认展示当天游资操作过的全部股票,买入、卖出和净额方向可通过上方筛选快速切换。</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="pill">{{ candidateActions.length }} 个候选</span>
|
<span class="pill">{{ candidateActions.length }} 个候选</span>
|
||||||
</div>
|
</div>
|
||||||
@ -301,7 +301,7 @@ function updateDateRange(field: 'from' | 'to', value: string) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="empty-state">当前筛选条件下没有新的候选股,调整日期或游资条件后再看。</div>
|
<div v-else class="empty-state">当前筛选条件下没有新的候选股,调整日期、游资或方向条件后再看。</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -488,9 +488,11 @@ function updateDateRange(field: 'from' | 'to', value: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.home-grid {
|
.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;
|
align-items: stretch;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-panel {
|
.card-panel {
|
||||||
@ -608,6 +610,7 @@ function updateDateRange(field: 'from' | 'to', value: string) {
|
|||||||
|
|
||||||
.candidate-panel {
|
.candidate-panel {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.candidate-list {
|
.candidate-list {
|
||||||
@ -616,7 +619,7 @@ function updateDateRange(field: 'from' | 'to', value: string) {
|
|||||||
align-content: start;
|
align-content: start;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
grid-auto-rows: 124px;
|
grid-auto-rows: minmax(124px, auto);
|
||||||
}
|
}
|
||||||
|
|
||||||
.buy {
|
.buy {
|
||||||
|
|||||||
@ -97,7 +97,6 @@ const chartModel = computed(() => {
|
|||||||
cumulativePoints: [] as Array<{ x: number; y: number }>,
|
cumulativePoints: [] as Array<{ x: number; y: number }>,
|
||||||
labels: [] as Array<{ x: number; label: string; visible: boolean }>,
|
labels: [] as Array<{ x: number; label: string; visible: boolean }>,
|
||||||
leftAxis: [] as Array<{ y: number; label: string }>,
|
leftAxis: [] as Array<{ y: number; label: string }>,
|
||||||
rightAxis: [] as Array<{ y: number; label: string }>,
|
|
||||||
hoverColumns: [] as Array<{ x: number; width: number }>,
|
hoverColumns: [] as Array<{ x: number; width: number }>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,18 +104,17 @@ const chartModel = computed(() => {
|
|||||||
return emptyModel
|
return emptyModel
|
||||||
}
|
}
|
||||||
|
|
||||||
const leftMax = Math.max(
|
const valueMax = Math.max(
|
||||||
...rows.flatMap((row) => [Math.abs(row.buyTotalWan), Math.abs(row.sellTotalWan), Math.abs(row.netTotalWan)]),
|
...rows.flatMap((row) => [
|
||||||
|
Math.abs(row.buyTotalWan),
|
||||||
|
Math.abs(row.sellTotalWan),
|
||||||
|
Math.abs(row.netTotalWan),
|
||||||
|
Math.abs(row.cumulativeNetWan),
|
||||||
|
]),
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
const zeroY = top + innerHeight / 2
|
const zeroY = top + innerHeight / 2
|
||||||
const yOfLeft = (value: number) => zeroY - (value / leftMax) * (innerHeight / 2)
|
const yOfValue = (value: number) => zeroY - (value / valueMax) * (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 stepX = rows.length === 1 ? innerWidth / 2 : innerWidth / (rows.length - 1)
|
const stepX = rows.length === 1 ? innerWidth / 2 : innerWidth / (rows.length - 1)
|
||||||
const groupWidth = Math.max(20, Math.min(28, stepX * 0.58))
|
const groupWidth = Math.max(20, Math.min(28, stepX * 0.58))
|
||||||
@ -134,7 +132,7 @@ const chartModel = computed(() => {
|
|||||||
stepX,
|
stepX,
|
||||||
buyBars: rows.map((row, index) => {
|
buyBars: rows.map((row, index) => {
|
||||||
const x = left + (rows.length === 1 ? innerWidth / 2 : index * stepX)
|
const x = left + (rows.length === 1 ? innerWidth / 2 : index * stepX)
|
||||||
const y = yOfLeft(row.buyTotalWan)
|
const y = yOfValue(row.buyTotalWan)
|
||||||
return {
|
return {
|
||||||
x: x - barWidth - 2,
|
x: x - barWidth - 2,
|
||||||
y,
|
y,
|
||||||
@ -145,7 +143,7 @@ const chartModel = computed(() => {
|
|||||||
sellBars: rows.map((row, index) => {
|
sellBars: rows.map((row, index) => {
|
||||||
const x = left + (rows.length === 1 ? innerWidth / 2 : index * stepX)
|
const x = left + (rows.length === 1 ? innerWidth / 2 : index * stepX)
|
||||||
const y = zeroY
|
const y = zeroY
|
||||||
const sellY = yOfLeft(-row.sellTotalWan)
|
const sellY = yOfValue(-row.sellTotalWan)
|
||||||
return {
|
return {
|
||||||
x: x + 2,
|
x: x + 2,
|
||||||
y,
|
y,
|
||||||
@ -155,11 +153,11 @@ const chartModel = computed(() => {
|
|||||||
}),
|
}),
|
||||||
netPoints: rows.map((row, index) => ({
|
netPoints: rows.map((row, index) => ({
|
||||||
x: left + (rows.length === 1 ? innerWidth / 2 : index * stepX),
|
x: left + (rows.length === 1 ? innerWidth / 2 : index * stepX),
|
||||||
y: yOfLeft(row.netTotalWan),
|
y: yOfValue(row.netTotalWan),
|
||||||
})),
|
})),
|
||||||
cumulativePoints: rows.map((row, index) => ({
|
cumulativePoints: rows.map((row, index) => ({
|
||||||
x: left + (rows.length === 1 ? innerWidth / 2 : index * stepX),
|
x: left + (rows.length === 1 ? innerWidth / 2 : index * stepX),
|
||||||
y: yOfRight(row.cumulativeNetWan),
|
y: yOfValue(row.cumulativeNetWan),
|
||||||
})),
|
})),
|
||||||
labels: rows.map((row, index) => ({
|
labels: rows.map((row, index) => ({
|
||||||
x: left + (rows.length === 1 ? innerWidth / 2 : index * stepX),
|
x: left + (rows.length === 1 ? innerWidth / 2 : index * stepX),
|
||||||
@ -167,12 +165,8 @@ const chartModel = computed(() => {
|
|||||||
visible: index % labelStep === 0 || index === rows.length - 1,
|
visible: index % labelStep === 0 || index === rows.length - 1,
|
||||||
})),
|
})),
|
||||||
leftAxis: Array.from({ length: 5 }, (_, index) => {
|
leftAxis: Array.from({ length: 5 }, (_, index) => {
|
||||||
const value = leftMax - (leftMax * 2 * index) / 4
|
const value = valueMax - (valueMax * 2 * index) / 4
|
||||||
return { y: yOfLeft(value), label: formatSignedWanAmount(value) }
|
return { y: yOfValue(value), label: formatSignedWanAmount(value) }
|
||||||
}),
|
|
||||||
rightAxis: Array.from({ length: 5 }, (_, index) => {
|
|
||||||
const value = rightMax - (rightRange * index) / 4
|
|
||||||
return { y: yOfRight(value), label: formatSignedWanAmount(value) }
|
|
||||||
}),
|
}),
|
||||||
hoverColumns: rows.map((_row, index) => ({
|
hoverColumns: rows.map((_row, index) => ({
|
||||||
x: left + (rows.length === 1 ? innerWidth / 2 : index * stepX) - Math.max(stepX, 20) / 2,
|
x: left + (rows.length === 1 ? innerWidth / 2 : index * stepX) - Math.max(stepX, 20) / 2,
|
||||||
@ -306,12 +300,6 @@ onUnmounted(() => {
|
|||||||
<text x="0" :y="tick.y + 4" fill="#93a2b5" font-size="10">{{ tick.label }}</text>
|
<text x="0" :y="tick.y + 4" fill="#93a2b5" font-size="10">{{ tick.label }}</text>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<g v-for="(tick, index) in chartModel.rightAxis" :key="`right-axis-${index}`">
|
|
||||||
<text :x="chartModel.width - chartModel.right + 8" :y="tick.y + 4" fill="#c9ad68" font-size="10">
|
|
||||||
{{ tick.label }}
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<g v-for="(column, index) in chartModel.hoverColumns" :key="`hover-${index}`">
|
<g v-for="(column, index) in chartModel.hoverColumns" :key="`hover-${index}`">
|
||||||
<rect
|
<rect
|
||||||
:x="column.x"
|
:x="column.x"
|
||||||
|
|||||||
@ -534,6 +534,7 @@ onUnmounted(() => {
|
|||||||
class="hover-action"
|
class="hover-action"
|
||||||
>
|
>
|
||||||
<strong>{{ action.matched_trader_name }}</strong>
|
<strong>{{ action.matched_trader_name }}</strong>
|
||||||
|
<span class="seat-name">营业部 {{ action.seat_name || '-' }}</span>
|
||||||
<span class="buy">买入 {{ formatWanAmount(action.buy_amount_wan) }}</span>
|
<span class="buy">买入 {{ formatWanAmount(action.buy_amount_wan) }}</span>
|
||||||
<span class="sell">卖出 {{ formatWanAmount(action.sell_amount_wan) }}</span>
|
<span class="sell">卖出 {{ formatWanAmount(action.sell_amount_wan) }}</span>
|
||||||
<span class="net" :class="priceTone(String(action.net_amount_wan))">净额 {{ formatSignedWanAmount(action.net_amount_wan) }}</span>
|
<span class="net" :class="priceTone(String(action.net_amount_wan))">净额 {{ formatSignedWanAmount(action.net_amount_wan) }}</span>
|
||||||
@ -931,6 +932,13 @@ onUnmounted(() => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.seat-name {
|
||||||
|
color: var(--color-muted);
|
||||||
|
line-height: 1.45;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
.hover-empty {
|
.hover-empty {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--color-muted);
|
color: var(--color-muted);
|
||||||
|
|||||||
@ -59,16 +59,59 @@ export function useDashboardData() {
|
|||||||
.filter((item): item is string => Boolean(item))
|
.filter((item): item is string => Boolean(item))
|
||||||
})
|
})
|
||||||
|
|
||||||
const filteredActions = computed(() => {
|
function inferActionSide(buyAmount: number, sellAmount: number, netAmount: number): ActionItem['action_side'] {
|
||||||
return actions.value.filter((item) => {
|
if (buyAmount > 0 && sellAmount <= 0) return 'buy'
|
||||||
if (selectedTraderFilter.value !== 'all' && item.trader_name !== selectedTraderFilter.value) {
|
if (sellAmount > 0 && buyAmount <= 0) return 'sell'
|
||||||
return false
|
if (netAmount >= 0) return 'net_buy'
|
||||||
}
|
return 'net_sell'
|
||||||
if (selectedActionFilter.value !== 'all' && item.action_side !== selectedActionFilter.value) {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function aggregateCandidateActions(rows: ActionItem[]): ActionItem[] {
|
||||||
|
const groups = new Map<string, ActionItem>()
|
||||||
|
|
||||||
|
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(matchesActionFilter)
|
||||||
})
|
})
|
||||||
|
|
||||||
const watchlistMap = computed(() => {
|
const watchlistMap = computed(() => {
|
||||||
@ -80,14 +123,8 @@ export function useDashboardData() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const candidateActionRows = computed(() => {
|
const candidateActionRows = computed(() => {
|
||||||
const unique = new Map<string, ActionItem>()
|
const rows = actions.value.filter((item) => !watchlistMap.value.has(item.stock_code))
|
||||||
for (const item of filteredActions.value) {
|
return aggregateCandidateActions(rows).filter(matchesActionFilter)
|
||||||
if (watchlistMap.value.has(item.stock_code)) continue
|
|
||||||
if (!unique.has(item.stock_code)) {
|
|
||||||
unique.set(item.stock_code, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [...unique.values()]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const watchlistMetrics = computed(() => {
|
const watchlistMetrics = computed(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user