新增首页详情合计页

This commit is contained in:
2026-05-14 11:18:55 +08:00
parent 4e4975ab67
commit 87cc632c03
+122 -51
View File
@@ -77,11 +77,21 @@
</div>
<div class="metric-row break">
<span>炸板</span>
<strong>{{ formatRatio(day.stat.break_next_profit_count, day.stat.break_count) }}</strong>
<strong>{{
formatRatio(
day.stat.break_next_profit_count,
getProfitDataCount(day.stat, "break"),
)
}}</strong>
</div>
<div class="metric-row limit">
<span>涨停板</span>
<strong>{{ formatRatio(day.stat.limit_next_profit_count, day.stat.limit_count) }}</strong>
<strong>{{
formatRatio(
day.stat.limit_next_profit_count,
getProfitDataCount(day.stat, "limit"),
)
}}</strong>
</div>
</div>
@@ -123,7 +133,7 @@
<strong>{{
formatRatio(
selectedDayStat?.limit_next_profit_count,
selectedDayStat?.limit_count,
getProfitDataCount(selectedDayStat, "limit"),
)
}}</strong>
</div>
@@ -132,58 +142,35 @@
<strong>{{
formatRatio(
selectedDayStat?.break_next_profit_count,
selectedDayStat?.break_count,
getProfitDataCount(selectedDayStat, "break"),
)
}}</strong>
</div>
</div>
<el-tabs v-model="activeDetailTab" class="detail-tabs">
<el-tab-pane :label="`涨停板(${limitStocks.length})`" name="limit">
<el-table :data="limitStocks" border stripe max-height="520" :default-sort="defaultDetailSort">
<el-table-column prop="name" label="股票简称" min-width="120" sortable />
<el-table-column prop="ts_code" label="股票代码" min-width="120" sortable />
<el-table-column prop="industry" label="行业" min-width="120" sortable show-overflow-tooltip />
<el-table-column
prop="first_limit_time"
label="首次涨停时间"
width="130"
align="center"
sortable
:sort-method="sortLimitTimeColumn"
>
<el-tab-pane
v-for="tab in detailTabs"
:key="tab.name"
:label="`${tab.label}(${tab.stocks.length})`"
:name="tab.name"
>
<el-table
:data="tab.stocks"
border
stripe
max-height="520"
class="detail-stock-table"
:default-sort="defaultDetailSort"
:row-class-name="getStockRowClassName"
>
<el-table-column v-if="tab.showStatus" prop="status" label="状态" width="86" align="center" sortable>
<template #default="{ row }">
{{ formatLimitTime(row.first_limit_time) }}
<span :class="['stock-status', row.status === 'break' ? 'break' : 'limit']">
{{ formatStatus(row.status) }}
</span>
</template>
</el-table-column>
<el-table-column prop="open_times" label="炸板次数" width="100" align="center" sortable>
<template #default="{ row }">
{{ formatOpenTimes(row.open_times) }}
</template>
</el-table-column>
<el-table-column prop="t_high" label="T日最高" width="100" align="right" sortable>
<template #default="{ row }">{{ formatPrice(row.t_high) }}</template>
</el-table-column>
<el-table-column prop="t_close" label="T日收盘" width="100" align="right" sortable>
<template #default="{ row }">{{ formatPrice(row.t_close) }}</template>
</el-table-column>
<el-table-column prop="next_high" label="T+1最高" width="100" align="right" sortable>
<template #default="{ row }">{{ formatPrice(row.next_high) }}</template>
</el-table-column>
<el-table-column prop="next_intraday_profitable" label="T+1盈利" width="100" align="center" sortable>
<template #default="{ row }">
{{ formatProfitFlag(row.next_intraday_profitable) }}
</template>
</el-table-column>
<el-table-column prop="next_high_profit_pct" label="最高收益" width="110" align="right" sortable>
<template #default="{ row }">
{{ formatSignedRate(row.next_high_profit_pct) }}
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane :label="`炸板(${breakStocks.length})`" name="break">
<el-table :data="breakStocks" border stripe max-height="520" :default-sort="defaultDetailSort">
<el-table-column prop="name" label="股票简称" min-width="120" sortable />
<el-table-column prop="ts_code" label="股票代码" min-width="120" sortable />
<el-table-column prop="industry" label="行业" min-width="120" sortable show-overflow-tooltip />
@@ -281,6 +268,13 @@ const detailDialogTitle = computed(() => {
const detailStocks = computed(() =>
Array.isArray(selectedDayStat.value?.stocks) ? selectedDayStat.value.stocks : [],
);
const totalStocks = computed(() => {
const stocks = Array.isArray(selectedDayStat.value?.total_stocks)
? selectedDayStat.value.total_stocks
: detailStocks.value;
return sortStocksByLimitTime(stocks);
});
const limitStocks = computed(() =>
sortStocksByLimitTime(
detailStocks.value.filter((stock) => stock?.status === "limit"),
@@ -291,6 +285,26 @@ const breakStocks = computed(() =>
detailStocks.value.filter((stock) => stock?.status === "break"),
),
);
const detailTabs = computed(() => [
{
name: "total",
label: "合计",
stocks: totalStocks.value,
showStatus: true,
},
{
name: "limit",
label: "涨停板",
stocks: limitStocks.value,
showStatus: false,
},
{
name: "break",
label: "炸板",
stocks: breakStocks.value,
showStatus: false,
},
]);
const statMap = computed(() => {
const map = new Map();
@@ -371,7 +385,11 @@ const openDayDetail = (day) => {
if (!day?.stat) return;
selectedDay.value = day;
activeDetailTab.value = limitStocks.value.length ? "limit" : "break";
activeDetailTab.value = totalStocks.value.length
? "total"
: limitStocks.value.length
? "limit"
: "break";
detailVisible.value = true;
};
@@ -399,11 +417,14 @@ const fetchMonthData = async (withLoading = true) => {
};
const sortStocksByLimitTime = (stocks) =>
[...stocks].sort(
(left, right) =>
[...stocks].sort((left, right) => {
const timeDiff =
getLimitTimeSortValue(left?.first_limit_time) -
getLimitTimeSortValue(right?.first_limit_time),
);
getLimitTimeSortValue(right?.first_limit_time);
if (timeDiff !== 0) return timeDiff;
return String(left?.ts_code || "").localeCompare(String(right?.ts_code || ""));
});
const sortLimitTimeColumn = (left, right) =>
getLimitTimeSortValue(left?.first_limit_time) -
@@ -445,6 +466,14 @@ const formatCount = (value) => {
const formatRatio = (numerator, denominator) =>
`${formatCount(numerator)}/${formatCount(denominator)}`;
const getProfitDataCount = (stat, type) => {
if (!stat) return 0;
const nextDataKey = type === "break" ? "break_next_data_count" : "limit_next_data_count";
const fallbackKey = type === "break" ? "break_count" : "limit_count";
return stat[nextDataKey] ?? stat[fallbackKey];
};
const formatPrice = (value) => {
if (value === null || value === undefined || value === "") return "--";
@@ -466,6 +495,12 @@ const formatProfitFlag = (value) => {
return "--";
};
const formatStatus = (value) => {
if (value === "limit") return "封板";
if (value === "break") return "炸板";
return "--";
};
const formatLimitTime = (value) => {
if (value === null || value === undefined || value === "") return "--";
return String(value);
@@ -477,6 +512,9 @@ const formatOpenTimes = (value) => {
const number = Number(value);
return Number.isFinite(number) ? number : "--";
};
const getStockRowClassName = ({ row }) =>
row?.status === "break" ? "break-stock-row" : "";
</script>
<style scoped>
@@ -787,6 +825,39 @@ const formatOpenTimes = (value) => {
min-width: 0;
}
.stock-status {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 44px;
height: 24px;
padding: 0 8px;
box-sizing: border-box;
border-radius: 999px;
font-size: 12px;
line-height: 1;
font-weight: 700;
}
.stock-status.limit {
background: #eaf2ff;
color: #2563a8;
}
.stock-status.break {
background: #fee2e2;
color: #b4232c;
}
:deep(.detail-stock-table .break-stock-row td.el-table__cell) {
background: #fff1f2 !important;
color: #b4232c;
}
:deep(.detail-stock-table .break-stock-row:hover > td.el-table__cell) {
background: #ffe4e6 !important;
}
:deep(.second-calendar-detail-dialog .el-dialog__body) {
padding-top: 8px;
}