feat: 档案统计独立卡片、共用交易日历与四所统计页日历

内照明心统计表移至顶部卡片,右侧为日历/图表/交易记录;日历样式适配浅深主题,四所统计分析页同步展示按月盈亏日历。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-30 08:17:53 +08:00
parent 6b872b1f43
commit 14dbf25798
16 changed files with 681 additions and 245 deletions
+22
View File
@@ -564,6 +564,8 @@ app = FastAPI(title="复盘系统中控", docs_url=None, redoc_url=None, lifespa
STATIC_DIR = DIR / "static"
_REPO_STATIC = _REPO_ROOT / "static"
_AI_REVIEW_RENDER_JS = _REPO_STATIC / "ai_review_render.js"
_TRADE_STATS_CALENDAR_CSS = _REPO_STATIC / "trade_stats_calendar.css"
_TRADE_STATS_CALENDAR_JS = _REPO_STATIC / "trade_stats_calendar.js"
_ACCOUNT_RISK_BADGE_CSS = _REPO_STATIC / "account_risk_badge.css"
_ACCOUNT_RISK_BADGE_JS = _REPO_STATIC / "account_risk_badge.js"
@@ -601,6 +603,26 @@ def hub_ai_review_render_js():
)
@app.get("/assets/trade_stats_calendar.css")
def hub_trade_stats_calendar_css():
if not _TRADE_STATS_CALENDAR_CSS.is_file():
raise HTTPException(status_code=404, detail="trade_stats_calendar.css not found")
return FileResponse(
str(_TRADE_STATS_CALENDAR_CSS),
media_type="text/css; charset=utf-8",
)
@app.get("/assets/trade_stats_calendar.js")
def hub_trade_stats_calendar_js():
if not _TRADE_STATS_CALENDAR_JS.is_file():
raise HTTPException(status_code=404, detail="trade_stats_calendar.js not found")
return FileResponse(
str(_TRADE_STATS_CALENDAR_JS),
media_type="application/javascript; charset=utf-8",
)
if STATIC_DIR.is_dir():
app.mount("/assets", StaticFiles(directory=str(STATIC_DIR)), name="assets")
+19 -107
View File
@@ -6460,6 +6460,22 @@ body.funds-fullscreen-open {
color: var(--muted);
font-size: 0.82rem;
}
.archive-stats-card {
margin-bottom: 14px;
padding: 12px;
background: var(--panel);
border: 1px solid var(--border-soft);
border-radius: var(--radius);
}
.archive-stats-card-head h2 {
margin: 0 0 10px;
font-size: 0.95rem;
}
.archive-stats-card .archive-stats-bar {
border: none;
background: transparent;
overflow: auto;
}
.archive-overview-panel {
display: flex;
flex-direction: column;
@@ -6703,107 +6719,6 @@ body.funds-fullscreen-open {
.archive-trades-table td.neg {
color: #ef4444;
}
.archive-calendar-wrap {
margin-top: 4px;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid var(--border-soft);
background: rgba(15, 23, 42, 0.35);
}
.archive-calendar-head {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-bottom: 8px;
}
.archive-cal-title {
font-size: 0.95rem;
font-weight: 600;
min-width: 120px;
text-align: center;
}
.archive-cal-weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
margin-bottom: 4px;
}
.archive-cal-wd {
text-align: center;
font-size: 0.72rem;
color: var(--text-muted, #9aa);
}
.archive-cal-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
}
.archive-cal-cell {
min-height: 62px;
padding: 4px 3px;
border-radius: 8px;
border: 1px solid transparent;
background: rgba(30, 41, 59, 0.45);
color: inherit;
font: inherit;
cursor: default;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: 2px;
}
.archive-cal-cell.has-trade {
cursor: pointer;
}
.archive-cal-cell.has-trade:hover {
border-color: rgba(99, 102, 241, 0.45);
background: rgba(49, 46, 129, 0.25);
}
.archive-cal-cell.is-selected {
border-color: rgba(99, 102, 241, 0.75);
box-shadow: 0 0 0 1px rgba(99, 102, 241, 0.35);
}
.archive-cal-cell.is-sick-day {
border-color: rgba(239, 68, 68, 0.55);
background: rgba(127, 29, 29, 0.22);
}
.archive-cal-cell.is-sick-day.is-selected {
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.45);
}
.archive-cal-cell.pnl-pos .archive-cal-pnl {
color: #22c55e;
}
.archive-cal-cell.pnl-neg .archive-cal-pnl {
color: #ef4444;
}
.archive-cal-day-num {
font-size: 0.78rem;
font-weight: 600;
}
.archive-cal-pnl {
font-size: 0.72rem;
font-weight: 600;
line-height: 1.1;
}
.archive-cal-cnt {
font-size: 0.65rem;
color: var(--text-muted, #9aa);
}
.archive-cal-sick-tag {
font-size: 0.62rem;
padding: 1px 4px;
border-radius: 4px;
background: rgba(239, 68, 68, 0.25);
color: #fca5a5;
font-weight: 600;
}
.archive-cal-pad {
background: transparent;
border: none;
min-height: 0;
}
.archive-del-btn {
padding: 3px 8px;
font-size: 0.72rem;
@@ -6875,13 +6790,10 @@ body.funds-fullscreen-open {
order: 2;
flex: 0 0 auto;
min-height: 0;
gap: 0;
gap: 10px;
}
#page-archive .archive-overview-panel {
width: 100%;
}
#page-archive .archive-overview-panel > .archive-panel-head {
display: flex;
#page-archive .archive-stats-card {
margin-bottom: 10px;
}
#page-archive .archive-quotes-list {
min-height: 120px;
+40 -120
View File
@@ -77,9 +77,7 @@
let chartExchangeSymbol = "";
let chartMarketType = "swap";
let searchTimer = null;
let calendarYear = 0;
let calendarMonth = 0;
let calendarDays = {};
let calendarWidget = null;
let selectedCalendarDay = "";
function esc(s) {
@@ -508,133 +506,54 @@
);
}
function calendarMonthLabel(y, m) {
return y + " 年 " + m + " 月";
}
function ensureCalendarMonthFromUI() {
if (calendarYear > 0 && calendarMonth > 0) return;
function calendarRefDate() {
let ref = tradingDay || (elTradingDay && elTradingDay.value) || "";
if (!ref && dateFrom) ref = dateFrom;
if (!ref) {
const now = new Date();
calendarYear = now.getFullYear();
calendarMonth = now.getMonth() + 1;
return;
}
const p = String(ref).slice(0, 10).split("-");
calendarYear = parseInt(p[0], 10) || new Date().getFullYear();
calendarMonth = parseInt(p[1], 10) || new Date().getMonth() + 1;
return ref || new Date();
}
function renderCalendar() {
if (!elCalendar || !elCalTitle) return;
ensureCalendarMonthFromUI();
elCalTitle.textContent = calendarMonthLabel(calendarYear, calendarMonth);
const first = new Date(calendarYear, calendarMonth - 1, 1);
const lastDay = new Date(calendarYear, calendarMonth, 0).getDate();
const startWd = first.getDay();
const weekdays = ["日", "一", "二", "三", "四", "五", "六"];
let html =
'<div class="archive-cal-weekdays">' +
weekdays.map(function (w) {
return '<span class="archive-cal-wd">' + w + "</span>";
}).join("") +
"</div><div class=\"archive-cal-grid\">";
for (let i = 0; i < startWd; i++) {
html += '<span class="archive-cal-cell archive-cal-pad"></span>';
}
for (let d = 1; d <= lastDay; d++) {
const dayStr =
calendarYear +
"-" +
String(calendarMonth).padStart(2, "0") +
"-" +
String(d).padStart(2, "0");
const info = calendarDays[dayStr];
const hasTrade = info && info.open_count > 0;
const sick = info && info.has_sick;
const pnl = hasTrade ? Number(info.pnl_total) : null;
const cnt = hasTrade ? info.open_count : 0;
const cls =
"archive-cal-cell" +
(hasTrade ? " has-trade" : "") +
(sick ? " is-sick-day" : "") +
(selectedCalendarDay === dayStr ? " is-selected" : "") +
(pnl != null && pnl > 0.0001 ? " pnl-pos" : pnl != null && pnl < -0.0001 ? " pnl-neg" : "");
let body = '<span class="archive-cal-day-num">' + d + "</span>";
if (hasTrade) {
const pnlTxt = (pnl >= 0 ? "+" : "") + pnl.toFixed(1);
body +=
'<span class="archive-cal-pnl">' +
esc(pnlTxt) +
"</span>" +
'<span class="archive-cal-cnt">' +
cnt +
"笔</span>";
if (sick) body += '<span class="archive-cal-sick-tag">犯病</span>';
}
html +=
'<button type="button" class="' +
cls +
'" data-day="' +
dayStr +
'" data-sick="' +
(sick ? "1" : "0") +
'"' +
(hasTrade ? "" : " disabled") +
">" +
body +
"</button>";
}
html += "</div>";
elCalendar.innerHTML = html;
elCalendar.querySelectorAll(".archive-cal-cell[data-day]").forEach(function (btn) {
btn.addEventListener("click", function () {
const day = btn.getAttribute("data-day");
if (!day) return;
function ensureCalendarWidget() {
if (calendarWidget || !window.TradeStatsCalendar || !elCalendar) return calendarWidget;
calendarWidget = new TradeStatsCalendar({
gridEl: elCalendar,
titleEl: elCalTitle,
prevBtn: elCalPrev,
nextBtn: elCalNext,
showSick: true,
buildQuery: function (year, month) {
const q = new URLSearchParams();
q.set("year", String(year));
q.set("month", String(month));
const ex = (elExchange && elExchange.value) || "";
if (ex) q.set("exchange_key", ex);
return q;
},
fetchFn: async function (q) {
const r = await apiFetch("/api/archive/calendar?" + q.toString());
return r.json();
},
parseResponse: function (data) {
if (!data || !data.ok) return {};
return data.days || {};
},
onDayClick: function (day, sick) {
selectedCalendarDay = day;
setPeriodMode("today");
if (elTradingDay) elTradingDay.value = day;
if (elFilterSick) {
elFilterSick.checked = btn.getAttribute("data-sick") === "1";
}
if (elFilterSick) elFilterSick.checked = sick;
syncPeriodUI();
void loadDailyTrades();
renderCalendar();
});
},
});
calendarWidget.ensureMonth(calendarRefDate());
return calendarWidget;
}
async function loadCalendar() {
ensureCalendarMonthFromUI();
const q = new URLSearchParams();
q.set("year", String(calendarYear));
q.set("month", String(calendarMonth));
const ex = (elExchange && elExchange.value) || "";
if (ex) q.set("exchange_key", ex);
try {
const r = await apiFetch("/api/archive/calendar?" + q.toString());
const data = await r.json();
if (!data.ok) return;
calendarDays = data.days || {};
renderCalendar();
} catch (e) {
console.warn("[archive calendar]", e);
}
}
function shiftCalendarMonth(delta) {
ensureCalendarMonthFromUI();
calendarMonth += delta;
if (calendarMonth > 12) {
calendarMonth = 1;
calendarYear += 1;
} else if (calendarMonth < 1) {
calendarMonth = 12;
calendarYear -= 1;
}
void loadCalendar();
const cal = ensureCalendarWidget();
if (!cal) return;
cal.selectedDay = selectedCalendarDay;
await cal.load();
}
function renderStats() {
@@ -1527,7 +1446,10 @@
syncPeriodUI();
dailyTrades = j.trades || [];
dailyStats = j.stats || { open_count: 0, by_exchange: {} };
if (periodMode === "today" && tradingDay) selectedCalendarDay = tradingDay;
if (periodMode === "today" && tradingDay) {
selectedCalendarDay = tradingDay;
if (calendarWidget) calendarWidget.selectedDay = tradingDay;
}
renderStats();
renderTrades();
void loadCalendar();
@@ -1600,8 +1522,6 @@
void loadCalendar();
});
}
if (elCalPrev) elCalPrev.addEventListener("click", function () { shiftCalendarMonth(-1); });
if (elCalNext) elCalNext.addEventListener("click", function () { shiftCalendarMonth(1); });
if (elPeriodTabs) {
elPeriodTabs.addEventListener("click", function (ev) {
const btn = ev.target.closest(".archive-period-btn");
+16 -14
View File
@@ -16,6 +16,7 @@
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
<link rel="stylesheet" href="/assets/app.css?v=20260614-plan-detail" />
<link rel="stylesheet" href="/assets/trade_stats_calendar.css?v=1" />
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=4" />
<script src="/assets/account_risk_badge.js?v=4"></script>
<link rel="stylesheet" href="/assets/dashboard.css?v=20260612-dash-monitor-count" />
@@ -469,6 +470,12 @@
<button type="button" id="archive-btn-sync" class="ghost">同步</button>
<span id="archive-status" class="toolbar-meta"></span>
</div>
<section class="archive-stats-card card">
<div class="archive-stats-card-head">
<h2>统计分析</h2>
</div>
<div id="archive-stats" class="archive-stats-bar"></div>
</section>
<div class="archive-layout">
<aside class="archive-quotes-panel">
<div class="archive-panel-head">
@@ -483,20 +490,14 @@
<div id="archive-quotes-list" class="archive-quotes-list"></div>
</aside>
<main class="archive-main-panel">
<section class="archive-overview-panel">
<div class="archive-panel-head">
<h2>数据总览</h2>
<div id="archive-calendar-wrap" class="trade-cal-wrap">
<div class="trade-cal-head">
<button type="button" id="archive-cal-prev" class="ghost" title="上一月"></button>
<span id="archive-cal-title" class="trade-cal-title"></span>
<button type="button" id="archive-cal-next" class="ghost" title="下一月"></button>
</div>
<div id="archive-stats" class="archive-stats-bar"></div>
<div id="archive-calendar-wrap" class="archive-calendar-wrap">
<div class="archive-calendar-head">
<button type="button" id="archive-cal-prev" class="ghost" title="上一月"></button>
<span id="archive-cal-title" class="archive-cal-title"></span>
<button type="button" id="archive-cal-next" class="ghost" title="下一月"></button>
</div>
<div id="archive-calendar" class="archive-calendar" role="grid" aria-label="交易日历"></div>
</div>
</section>
<div id="archive-calendar" class="trade-cal-grid-host" role="grid" aria-label="交易日历"></div>
</div>
<details id="archive-chart-section" class="archive-acc-section archive-chart-section archive-panel-desktop">
<summary class="archive-acc-summary">K 线图表 <span id="archive-chart-title" class="archive-acc-sub"></span></summary>
<div class="archive-chart-toolbar toolbar">
@@ -1057,7 +1058,8 @@
<script src="/assets/chart.js?v=20260626-market-tail-patch"></script>
<script src="/assets/plan.js?v=20260614-plan-refresh"></script>
<script src="/assets/calculator.js?v=3"></script>
<script src="/assets/archive.js?v=20260612-archive-ai-chat"></script>
<script src="/assets/trade_stats_calendar.js?v=1"></script>
<script src="/assets/archive.js?v=20260626-archive-layout"></script>
<script src="/assets/funds.js?v=20260609-hub-funds-fold"></script>
<script src="/assets/dashboard.js?v=20260612-dash-monitor-count"></script>
<script src="/assets/ai_review_render.js?v=3"></script>