From c79bb2ea4becc24e2978056530f6a6ee3d1da0ab Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 26 Jun 2026 20:47:22 +0800 Subject: [PATCH] Speed up top nav with turbo routing and external base CSS. Remove view-transition lag, swap main content without full reload, prefetch pages, and tear down SSE timers on leave. Co-authored-by: Cursor --- static/css/base.css | 468 ++++++++++++++++++++++++++++++++++++++++ static/css/tech.css | 20 +- static/js/keys.js | 16 +- static/js/market.js | 36 ++-- static/js/nav.js | 4 + static/js/page.js | 17 ++ static/js/plans.js | 13 +- static/js/positions.js | 16 +- static/js/review.js | 7 +- static/js/stats.js | 10 +- static/js/symbol.js | 9 +- static/js/trade.js | 46 ++-- static/js/turbonav.js | 230 ++++++++++++++++++++ static/sw.js | 5 +- templates/base.html | 472 +---------------------------------------- templates/records.html | 44 ++-- 16 files changed, 865 insertions(+), 548 deletions(-) create mode 100644 static/css/base.css create mode 100644 static/js/page.js create mode 100644 static/js/turbonav.js diff --git a/static/css/base.css b/static/css/base.css new file mode 100644 index 0000000..419c829 --- /dev/null +++ b/static/css/base.css @@ -0,0 +1,468 @@ +/* Copyright (c) 2025-2026 马建军. All rights reserved. */ +html{background:#050508;color-scheme:dark} +html[data-theme="light"]{background:#e8eef8;color-scheme:light} +html:not([data-theme="light"]){ +--bg-page:#050508; +--bg-grid:rgba(76,194,255,.045); +--border-header:rgba(76,194,255,.12); +--header-bg:transparent; +--text-primary:#e8eaf6; +--text-title:#ffffff; +--text-muted:#7a82a0; +--text-label:#a8b4ff; +--accent:#4cc2ff; +--accent-2:#9d6bff; +--ambient-glow:rgba(76,194,255,.14); +--ambient-glow-2:rgba(123,66,255,.1); +--scanline:rgba(76,194,255,.03); +--title-glow:rgba(76,194,255,.35); +--card-bg:rgba(10,12,22,.82); +--card-border:rgba(76,194,255,.22); +--card-border-hover:rgba(76,194,255,.45); +--card-glow:rgba(76,194,255,.12); +--card-inner:rgba(16,20,36,.9); +--input-bg:rgba(12,14,26,.95); +--input-border:rgba(76,194,255,.18); +--nav-bg:rgba(14,16,28,.9); +--nav-border:rgba(76,194,255,.15); +--nav-hover:rgba(30,40,68,.85); +--nav-hover-glow:rgba(76,194,255,.15); +--nav-active:#2563eb; +--nav-active-border:transparent; +--nav-active-glow:rgba(76,194,255,.45); +--focus-ring:rgba(76,194,255,.25); +--focus-glow:rgba(76,194,255,.2); +--btn-glow:rgba(76,194,255,.35); +--btn-glow-strong:rgba(123,66,255,.4); +--row-hover:rgba(76,194,255,.06); +--list-item-bg:rgba(14,18,32,.8); +--table-border:rgba(76,194,255,.1); +--profit:#4cd97f; +--profit-bg:rgba(76,217,127,.12); +--loss:#ff6b7a; +--loss-bg:rgba(255,107,122,.12); +--dir-bg:rgba(30,45,80,.6); +--planned-bg:rgba(234,193,71,.12); +--planned-text:#eac147; +--expired-bg:rgba(40,44,60,.8); +--expired-text:#8a8a9e; +--flash-bg:rgba(30,45,80,.7); +--flash-text:#4cc2ff; +--modal-mask:rgba(2,4,12,.82); +--danger:#ff6b7a; +--shadow-card:0 8px 32px rgba(0,0,0,.45),0 0 1px rgba(76,194,255,.3),inset 0 1px 0 rgba(255,255,255,.04); +--shadow-card-hover:0 16px 48px rgba(0,0,0,.5),0 0 24px var(--card-glow); +--calc-bg:rgba(20,24,42,.9); +--toggle-bg:rgba(14,16,28,.9); +--toggle-border:rgba(76,194,255,.2); +} +[data-theme="light"]{ +--bg-page:#e8eef8; +--bg-grid:rgba(37,99,235,.07); +--border-header:rgba(37,99,235,.12); +--header-bg:transparent; +--text-primary:#1a2233; +--text-title:#0a1628; +--text-muted:#5c6578; +--text-label:#1d4ed8; +--accent:#2563eb; +--accent-2:#7c3aed; +--ambient-glow:rgba(37,99,235,.12); +--ambient-glow-2:rgba(124,58,237,.08); +--scanline:rgba(37,99,235,.04); +--title-glow:rgba(37,99,235,.2); +--card-bg:rgba(255,255,255,.92); +--card-border:rgba(37,99,235,.2); +--card-border-hover:rgba(37,99,235,.4); +--card-glow:rgba(37,99,235,.1); +--card-inner:#f4f7fc; +--input-bg:#ffffff; +--input-border:#b8c5d6; +--nav-bg:rgba(255,255,255,.95); +--nav-border:rgba(37,99,235,.18); +--nav-hover:#eef4ff; +--nav-hover-glow:rgba(37,99,235,.12); +--nav-active:#2563eb; +--nav-active-border:transparent; +--nav-active-glow:rgba(37,99,235,.35); +--focus-ring:rgba(37,99,235,.2); +--focus-glow:rgba(37,99,235,.15); +--btn-glow:rgba(37,99,235,.25); +--btn-glow-strong:rgba(37,99,235,.35); +--row-hover:rgba(37,99,235,.05); +--list-item-bg:#f1f5f9; +--table-border:#e2e8f0; +--profit:#15803d; +--profit-bg:#dcfce7; +--loss:#dc2626; +--loss-bg:#fee2e2; +--dir-bg:#dbeafe; +--planned-bg:#fef9c3; +--planned-text:#a16207; +--expired-bg:#f1f5f9; +--expired-text:#64748b; +--flash-bg:#dbeafe; +--flash-text:#1d4ed8; +--modal-mask:rgba(15,23,42,.45); +--danger:#dc2626; +--shadow-card:0 8px 28px rgba(15,23,42,.08),0 0 1px rgba(37,99,235,.15),inset 0 1px 0 rgba(255,255,255,.95); +--shadow-card-hover:0 16px 40px rgba(15,23,42,.12),0 0 20px var(--card-glow); +--calc-bg:#eef2ff; +--toggle-bg:#ffffff; +--toggle-border:#c5d0dc; +} +*{margin:0;padding:0;box-sizing:border-box} +body{ +font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif; +background:var(--bg-page); +color:var(--text-primary); +min-height:100vh; +} +.page-wrap{max-width:1800px;margin:0 auto;min-height:100vh} +.site-header{text-align:center;padding:1.5rem 1rem 1.25rem;border-bottom:1px solid var(--border-header);position:relative} +.site-title{font-size:1.75rem;font-weight:700;color:var(--text-title);margin-bottom:1.65rem;line-height:1.3} +.header-tools{position:absolute;top:1rem;left:1.5rem;display:flex;gap:.5rem;align-items:center;z-index:20} +.theme-switch{ +display:inline-flex;align-items:center; +border-radius:999px;border:1px solid var(--toggle-border); +background:var(--toggle-bg);padding:3px;gap:2px; +} +.theme-switch-btn{ +padding:.38rem .75rem;border:none;border-radius:999px; +background:transparent;color:var(--text-muted); +font-size:.75rem;cursor:pointer;transition:.2s; +white-space:nowrap;width:auto;flex-shrink:0; +} +.theme-switch-btn:hover{color:var(--text-primary)} +.theme-switch-btn.active{ +background:linear-gradient(135deg,var(--accent),var(--accent-2)); +color:#fff;box-shadow:0 0 12px var(--btn-glow); +} +.site-nav{display:flex;justify-content:center;gap:.45rem;flex-wrap:wrap} +.site-nav a{ +padding:.55rem 1.15rem;border-radius:8px; +border:1px solid transparent; +background:transparent; +color:var(--text-primary); +text-decoration:none;font-size:.88rem; +transition:.2s;white-space:nowrap; +} +.site-nav a:hover{background:var(--nav-hover);border-color:var(--nav-border);color:var(--text-title)} +.site-nav a.active{background:var(--nav-active);border-color:var(--nav-active-border);color:#fff} +.user-bar{position:absolute;top:1rem;right:1.5rem;font-size:.8rem;color:var(--text-muted);white-space:nowrap} +.user-bar a{color:var(--danger);text-decoration:none;margin-left:.5rem} +.main{padding:1.5rem} +.text-muted{color:var(--text-muted)} +.text-label{color:var(--text-label)} +.text-accent{color:var(--accent)} +.text-profit{color:var(--profit)} +.text-loss{color:var(--loss)} +.section-label{font-size:.9rem;color:var(--text-label);margin:.75rem 0 .5rem} +.empty-hint{color:var(--text-muted);padding:.75rem;font-size:.85rem} +.flash{padding:1rem;background:var(--flash-bg);color:var(--flash-text);border-radius:10px;margin-bottom:1.5rem;text-align:center;border:1px solid var(--card-border)} +.card{ +background:var(--card-bg); +border-radius:16px;padding:1.5rem; +border:1px solid var(--card-border); +margin-bottom:1.5rem; +position:relative;overflow:hidden; +box-shadow:var(--shadow-card); +backdrop-filter:blur(10px); +transition:background .25s,border-color .25s,box-shadow .25s; +} +.card::before{ +content:"";position:absolute;inset:0; +background:linear-gradient(135deg,var(--card-glow) 0%,transparent 55%); +pointer-events:none; +} +.card::after{ +content:"";position:absolute;top:0;left:12%;right:12%;height:1px; +background:linear-gradient(90deg,transparent,var(--accent),var(--accent-2),transparent); +opacity:.55;pointer-events:none; +} +.card > *{position:relative;z-index:1} +.card h2{ +font-size:1.15rem;margin-bottom:1rem;color:var(--text-label); +display:flex;align-items:center;gap:.5rem; +} +.card h2:before{ +content:"";width:4px;height:16px; +background:linear-gradient(180deg,var(--accent),var(--accent-2)); +border-radius:2px;box-shadow:0 0 8px var(--card-glow); +} +.form-row{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem;align-items:center} +.form-compact{display:flex;flex-direction:column;gap:.5rem;margin-bottom:1rem} +.form-compact .form-line{display:grid;gap:.5rem;align-items:center} +.form-compact .line-2{grid-template-columns:repeat(2,1fr)} +.form-compact .line-3{grid-template-columns:repeat(3,1fr)} +.form-compact .line-4{grid-template-columns:repeat(4,1fr)} +.form-compact .line-5{grid-template-columns:repeat(5,1fr)} +.form-compact.line-tight{gap:.35rem;margin-bottom:.5rem} +.form-compact .line-btn{display:flex;gap:.5rem;align-items:center} +.form-compact input,.form-compact select{padding:.55rem .7rem;font-size:.85rem;border-radius:8px} +.form-compact .symbol-selected{font-size:.7rem;margin-top:2px} +.form-compact button.btn-primary{padding:.55rem 1.5rem;font-size:.85rem;white-space:nowrap} +.form-compact-review input.calc-readonly{color:var(--accent);background:var(--calc-bg);font-size:.82rem} +.form-compact-review textarea{min-height:44px;font-size:.85rem;padding:.45rem .65rem} +.form-compact-review .section-hint{font-size:.72rem;color:var(--text-muted);margin:.25rem 0 .15rem} +.form-compact-review .tag-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:.15rem .5rem;font-size:.75rem} +.form-compact-review .tag-grid label{display:flex;align-items:center;gap:.3rem;cursor:pointer;color:var(--text-muted);white-space:nowrap} +.form-compact-review .tag-grid input{width:auto;flex-shrink:0} +.form-compact-review input[type="file"]{font-size:.72rem;padding:.4rem .5rem} +.form-compact-review .mini-field span{font-size:.65rem;color:var(--text-muted);display:block;line-height:1;margin-bottom:2px} +.form-compact-review .kline-row label{display:flex;align-items:center;gap:.35rem;color:var(--text-muted);white-space:nowrap} +.form-compact-review .kline-row{display:grid;grid-template-columns:auto 1fr 1fr 1fr auto;gap:.5rem;align-items:end;font-size:.78rem} +.split-grid.records-split .card{min-height:auto} +.split-grid.records-split .card h2{font-size:1rem;margin-bottom:.5rem} +.split-grid.records-split .card{padding:1rem 1.25rem} +.page-title-sm{font-size:1.25rem;margin-bottom:.75rem} +.form-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:.75rem;margin-bottom:1rem} +.form-grid .full{grid-column:1/-1} +.field label{display:block;font-size:.8rem;color:var(--text-label);margin-bottom:.35rem} +input,select,textarea,button{ +padding:.7rem 1rem;border-radius:10px; +border:1px solid var(--input-border); +background:var(--input-bg);color:var(--text-primary); +font-size:.9rem;outline:none;width:100%; +transition:border-color .2s,background .2s; +} +textarea{min-height:80px;resize:vertical} +input:focus,select:focus,textarea:focus{border-color:var(--accent)} +button.btn-primary{background:linear-gradient(90deg,var(--accent),var(--accent-2));border:none;cursor:pointer;color:#fff;width:auto} +button.btn-primary:hover{opacity:.9} +.list{display:flex;flex-direction:column;gap:.75rem} +.list-item{ +display:flex;justify-content:space-between;align-items:center; +padding:1rem;background:var(--list-item-bg); +border-radius:10px;gap:1rem;flex-wrap:wrap; +border:1px solid var(--card-border); +} +.btn-del{padding:.4rem .8rem;background:var(--loss-bg);color:var(--loss);border-radius:8px;text-decoration:none;font-size:.85rem} +table{width:100%;border-collapse:collapse} +th,td{padding:.85rem;text-align:left;border-bottom:1px solid var(--table-border);font-size:.9rem;color:var(--text-primary)} +th{color:var(--text-label);white-space:nowrap} +.badge{padding:.25rem .5rem;border-radius:6px;font-size:.75rem} +.badge.profit{background:var(--profit-bg);color:var(--profit)} +.badge.loss{background:var(--loss-bg);color:var(--loss)} +.badge.dir{background:var(--dir-bg);color:var(--accent)} +.badge.planned{background:var(--planned-bg);color:var(--planned-text)} +.badge.active{background:var(--profit-bg);color:var(--profit)} +.badge.expired{background:var(--expired-bg);color:var(--expired-text)} +.stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:1rem;margin-bottom:1.5rem} +.stat-item{ +background:var(--card-inner);padding:1rem;border-radius:12px;text-align:center; +border:1px solid var(--card-border);position:relative;overflow:hidden; +} +.stat-item::before{ +content:"";position:absolute;top:0;left:0;right:0;height:2px; +background:linear-gradient(90deg,var(--accent),var(--accent-2));opacity:.5; +} +.stat-item .label{font-size:.8rem;color:var(--text-muted)} +.stat-item .value{font-size:1.4rem;font-weight:600;color:var(--text-title);margin-top:.25rem} +.symbol-wrap{position:relative;z-index:1} +.symbol-wrap:has(.symbol-dropdown.show){z-index:50} +.symbol-dropdown{ +position:absolute;top:100%;left:0;right:0; +background:var(--input-bg);border:1px solid var(--input-border); +border-radius:10px;margin-top:4px;z-index:200; +max-height:240px;overflow-y:auto;display:none; +box-shadow:var(--shadow-card-hover); +} +.card:has(.symbol-dropdown.show){overflow:visible} +.symbol-dropdown.show{display:block} +.symbol-option{padding:.65rem 1rem;cursor:pointer;font-size:.85rem;border-bottom:1px solid var(--table-border)} +.symbol-option:hover{background:var(--list-item-bg)} +.symbol-option .sub{font-size:.75rem;color:var(--text-muted);margin-top:2px} +.symbol-option.near-expiry{color:#ff6b7a} +html[data-theme="light"] .symbol-option.near-expiry{color:#dc2626} +.symbol-option.near-expiry .sub{color:inherit;opacity:.85} +.near-expiry-tag{ +font-size:.68rem;padding:.1rem .35rem;border-radius:4px; +background:rgba(255,107,122,.15);color:inherit;font-weight:600; +} +html[data-theme="light"] .near-expiry-tag{background:rgba(220,38,38,.12)} +.symbol-group-head{ +padding:.4rem .85rem;font-size:.72rem;font-weight:600; +color:var(--text-muted);background:var(--card-inner); +border-bottom:1px solid var(--table-border);position:sticky;top:0; +} +.symbol-selected{font-size:.75rem;color:var(--accent);margin-top:4px} +.check-row{display:flex;flex-wrap:wrap;gap:1rem;margin:.75rem 0} +.check-row label{display:flex;align-items:center;gap:.4rem;font-size:.85rem;color:var(--text-muted);cursor:pointer} +.check-row input{width:auto} +.hint{font-size:.78rem;color:var(--text-muted);line-height:1.5;margin-top:.5rem} +.filter-row{display:flex;gap:.75rem;flex-wrap:wrap;align-items:flex-end;margin-bottom:1rem} +.filter-row .field{width:auto;min-width:140px} +.filter-row button{width:auto} +.split-grid{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;align-items:stretch;margin-bottom:1.5rem} +.split-grid .card{margin-bottom:0;height:100%;min-height:480px;display:flex;flex-direction:column} +.split-grid .card-body{flex:1;overflow:auto} +.card-scroll{max-height:420px;overflow-y:auto} +.preset-tabs{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem} +.preset-tabs a{ +padding:.45rem .85rem;border-radius:8px; +border:1px solid var(--input-border); +color:var(--text-muted);text-decoration:none;font-size:.85rem; +} +.preset-tabs a.active,.preset-tabs a:hover{background:var(--dir-bg);color:var(--accent);border-color:var(--accent)} +.btn-link{color:var(--accent);cursor:pointer;font-size:.85rem;background:none;border:none;padding:0} +.btn-link:hover{text-decoration:underline} +.modal-mask{position:fixed;inset:0;background:var(--modal-mask);z-index:1000;display:none;align-items:center;justify-content:center;padding:1rem} +.modal-mask.show{display:flex} +.modal-box{ +background:var(--card-bg);border:1px solid var(--card-border); +border-radius:16px;max-width:900px;width:100%; +max-height:90vh;overflow:auto;padding:1.5rem; +box-shadow:var(--shadow-card); +} +.modal-box h3{margin-bottom:1rem;color:var(--text-label)} +.modal-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:.75rem;font-size:.9rem} +.modal-grid .item label{color:var(--text-muted);font-size:.75rem;display:block} +.modal-grid .item div{margin-top:.2rem;color:var(--text-primary)} +.form-compact .line-plan-1{grid-template-columns:minmax(0,1fr) minmax(72px,0.45fr) minmax(0,1.6fr)} +.form-compact .line-plan-2{grid-template-columns:repeat(4,minmax(0,1fr)) auto;align-items:center} +.form-compact .field-short select{padding:.55rem .5rem} +.badge.emotion{background:var(--loss-bg);color:var(--loss);font-weight:600;border:1px solid var(--loss)} +tr.row-emotion td{color:var(--loss)} +tr.row-emotion td .badge.emotion{background:var(--loss);color:#fff;border-color:var(--loss)} +.modal-box.review-modal-wide{max-width:960px} +.modal-box.review-modal-fullscreen{ +width:calc(100vw - 1.5rem);max-width:none; +height:calc(100vh - 1.5rem);max-height:none; +border-radius:12px;display:flex;flex-direction:column; +} +.modal-box.review-modal-fullscreen h3{flex-shrink:0} +#review-modal-body.review-modal-body{flex:1;overflow:auto;min-height:0} +.review-detail-table{margin-bottom:1rem;overflow-x:auto} +.review-detail-headers,.review-detail-values{ +display:grid; +grid-template-columns:repeat(16,minmax(52px,1fr)); +gap:.35rem .4rem;align-items:start; +} +.review-detail-headers span{ +font-size:.68rem;color:var(--text-muted); +white-space:nowrap;line-height:1.3; +} +.review-detail-values span{ +font-size:.82rem;color:var(--text-primary); +line-height:1.35;word-break:break-all; +} +.review-detail-values span.emotion-val{color:var(--loss);font-weight:600} +.review-detail-fields{padding:.25rem 0} +.review-detail-section{margin-bottom:.85rem} +.review-detail-section h4{font-size:.78rem;color:var(--text-label);margin-bottom:.45rem;font-weight:600} +.review-detail-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:.45rem .65rem} +.review-detail-item label{display:block;font-size:.7rem;color:var(--text-muted);margin-bottom:.15rem} +.review-detail-item div{font-size:.85rem;color:var(--text-primary);line-height:1.35} +.review-detail-item.wide{grid-column:span 2} +.review-detail-item.full{grid-column:1/-1} +.review-detail-item.emotion div{color:var(--loss);font-weight:600} +.review-detail-image{flex-shrink:0;padding-top:.75rem;border-top:1px solid var(--table-border)} +.review-detail-image img{width:100%;border-radius:10px;border:1px solid var(--card-border)} +.review-detail-image .no-img{color:var(--text-muted);font-size:.85rem;padding:2rem;text-align:center;background:var(--card-inner);border-radius:10px} +.key-live{display:flex;align-items:center;justify-content:space-between;gap:.75rem;flex:1;min-width:160px} +.key-live .live-price-line{font-size:.85rem;font-weight:600;color:var(--accent);white-space:nowrap} +.key-live .live-dist{font-size:.72rem;color:var(--text-muted);white-space:nowrap} +.key-live .live-dist span{color:var(--text-primary)} +.list-item.key-item{gap:.65rem} +.pos-card{background:var(--card-inner);border:1px solid var(--card-border);border-radius:12px;padding:1rem;margin-bottom:.75rem} +.pos-card-head{display:flex;justify-content:space-between;align-items:flex-start;gap:.5rem;margin-bottom:.65rem} +.pos-card-head .title{font-size:1rem;font-weight:600;color:var(--text-title)} +.pos-card-meta{font-size:.75rem;color:var(--text-muted);margin-bottom:.65rem} +.pos-card-meta strong{color:var(--text-primary)} +.pos-metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:.5rem .65rem;margin-bottom:.65rem} +.pos-metrics .cell label{display:block;font-size:.68rem;color:var(--text-muted);margin-bottom:.15rem} +.pos-metrics .cell div{font-size:.88rem;color:var(--text-primary)} +.pos-metrics .cell.pnl-pos div{color:var(--profit)} +.pos-metrics .cell.pnl-neg div{color:var(--loss)} +.pos-footer{font-size:.72rem;color:var(--text-muted);display:flex;flex-wrap:wrap;gap:.35rem 1rem;padding-top:.65rem;border-top:1px solid var(--table-border)} +.pos-footer span{color:var(--text-primary)} +.pos-del{font-size:.75rem;padding:.35rem .65rem} +.trade-toolbar{display:flex;align-items:center;gap:1rem;margin-bottom:1rem;flex-wrap:wrap} +.trade-switch-label{ +display:flex;align-items:center;gap:.4rem; +font-size:.78rem;color:var(--text-muted); +white-space:normal;margin-bottom:.65rem;cursor:pointer; +line-height:1.45;max-width:100%; +} +.trade-switch-label span{line-height:1.45;color:var(--text-muted)} +.trade-switch-label input{flex-shrink:0;width:auto} +.trade-table-wrap{ +overflow:auto; +max-height:420px; +width:100%; +-webkit-overflow-scrolling:touch; +border-radius:10px; +border:1px solid var(--table-border); +background:var(--card-inner); +} +.trade-table{font-size:.8rem;width:max-content;min-width:100%;table-layout:auto} +.trade-table th{font-size:.75rem;padding:.55rem .45rem;white-space:nowrap;background:var(--card-inner)} +.trade-table td{padding:.45rem .4rem;vertical-align:middle;white-space:nowrap;background:var(--card-inner)} +.trade-table th:last-child, +.trade-table td:last-child{ +position:sticky;right:0;z-index:3; +box-shadow:-6px 0 10px rgba(0,0,0,.08); +} +.trade-table thead th:last-child{z-index:4} +.trade-table input,.trade-table select{ +padding:.35rem .45rem;font-size:.78rem;border-radius:6px;width:100%;min-width:0; +} +.trade-table .cell-readonly{color:var(--text-primary)} +.records-equity-card .card-body{padding-top:0;background:transparent} +.records-equity-card #equity-curve-chart{background:transparent;border-radius:0} +.records-trade-card{overflow:visible} +.records-trade-card .card-body{overflow:visible;background:transparent} +.records-trade-card .trade-table-wrap{ +--rec-row-h:2.35rem; +--rec-head-h:2.1rem; +overflow:auto; +height:calc(var(--rec-row-h) * 10 + var(--rec-head-h)); +max-height:calc(var(--rec-row-h) * 10 + var(--rec-head-h)); +width:100%; +-webkit-overflow-scrolling:touch; +border:none; +border-radius:0; +background:transparent; +} +.records-trade-card .trade-table th, +.records-trade-card .trade-table td{ +background:transparent; +} +.records-trade-card .trade-table thead th{ +position:sticky; +top:0; +z-index:2; +background:transparent; +backdrop-filter:blur(12px); +-webkit-backdrop-filter:blur(12px); +box-shadow:0 1px 0 var(--table-border); +} +.records-trade-card .trade-table th:last-child, +.records-trade-card .trade-table td:last-child{ +box-shadow:none; +background:transparent; +} +.records-trade-card .trade-table thead th:last-child{ +z-index:5; +} +.trade-actions{display:flex;gap:.35rem;flex-wrap:nowrap;align-items:center;min-width:230px;white-space:nowrap} +.trade-actions a,.trade-actions button{flex-shrink:0;white-space:nowrap;font-size:.72rem;padding:.3rem .55rem;border-radius:6px;text-decoration:none;border:none;cursor:pointer;width:auto;min-width:0} +.btn-fill{background:var(--dir-bg);color:var(--accent)} +.btn-verify{background:var(--nav-active);color:#fff} +.btn-verify:disabled{opacity:.45;cursor:not-allowed} +.badge.result-manual{background:var(--dir-bg);color:var(--accent)} +.badge.result-external{background:var(--expired-bg);color:var(--expired-text)} +.profile-page .profile-head{display:flex;align-items:center;gap:.65rem;flex-wrap:wrap;margin:1rem 0 .75rem;font-size:.9rem} +.profile-page .profile-source{font-size:.72rem;color:var(--text-muted)} +.profile-spec{max-width:820px;border:1px solid var(--card-border);border-radius:10px;background:var(--card-inner);padding:.25rem .85rem} +.profile-row{display:grid;grid-template-columns:minmax(120px,28%) 1fr;gap:.5rem 1rem;padding:.6rem 0;border-bottom:1px solid var(--table-border);align-items:start} +.profile-row:last-child{border-bottom:none} +.profile-label{color:var(--text-muted);font-size:.84rem;line-height:1.4} +.profile-value{color:var(--text-primary);font-size:.86rem;line-height:1.5;word-break:break-word} +.profile-hint{color:var(--planned-text);font-size:.74rem;margin-top:.25rem;line-height:1.35} +.calc-readonly{background:var(--calc-bg);color:var(--accent)} +@media(max-width:1100px){ +.split-grid{grid-template-columns:1fr} +.split-grid .card{min-height:auto} +} diff --git a/static/css/tech.css b/static/css/tech.css index 7f32a7d..945902f 100644 --- a/static/css/tech.css +++ b/static/css/tech.css @@ -1,25 +1,8 @@ /* Copyright (c) 2025-2026 马建军. All rights reserved. 专有软件,详见 LICENSE.zh-CN.txt */ /* 科技感增强层 — 与 base.html 变量配合 */ -@view-transition { - navigation: auto; -} - -::view-transition-old(app-main), -::view-transition-new(app-main) { - animation-duration: .18s; -} - -::view-transition-old(app-tech-bg), -::view-transition-new(app-tech-bg), -::view-transition-old(site-header), -::view-transition-new(site-header) { - animation: none; -} - .tech-bg{ position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden; - view-transition-name:app-tech-bg; } .tech-grid{ position:absolute;inset:0; @@ -70,13 +53,14 @@ .tech-glow,.tech-glow-2,.tech-scanline,.card::after,.site-header::after{animation:none} } +.main.nav-loading{opacity:.75;pointer-events:none;transition:opacity .12s} + .page-wrap{position:relative;z-index:1} .site-header{ border-bottom:1px solid var(--border-header); background:transparent; backdrop-filter:none; - view-transition-name:site-header; } .site-header::after{ content:"";display:block;height:1px;margin-top:-1px; diff --git a/static/js/keys.js b/static/js/keys.js index c99bf87..0db6a41 100644 --- a/static/js/keys.js +++ b/static/js/keys.js @@ -31,8 +31,20 @@ .catch(function () { /* ignore */ }); } - document.addEventListener('DOMContentLoaded', function () { + function stopPolling() { + if (keyTimer) { + clearInterval(keyTimer); + keyTimer = null; + } + } + + function startPolling() { + stopPolling(); pollKeyPrices(); keyTimer = setInterval(pollKeyPrices, 1000); - }); + } + + if (window.qihuoOnPageLoad) window.qihuoOnPageLoad(startPolling); + else document.addEventListener('DOMContentLoaded', startPolling); + if (window.qihuoOnPageLeave) window.qihuoOnPageLeave(stopPolling); })(); diff --git a/static/js/market.js b/static/js/market.js index 653bd7d..f8c4870 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -689,7 +689,12 @@ } } - document.addEventListener('DOMContentLoaded', function () { + function cleanupMarketPage() { + stopKlineStream(); + destroyChart(); + } + + function bootMarketPage() { if (!window.LightweightCharts) { if (emptyEl) emptyEl.textContent = '图表库加载失败,请刷新页面'; return; @@ -699,15 +704,6 @@ bindAutoButton(); bindChartOptions(); - document.addEventListener('click', function (e) { - if (e.target.closest('[data-theme-pick]') && lastData) { - setTimeout(function () { - destroyChart(); - renderChart(lastData, { forceFull: true }); - }, 80); - } - }); - var active = document.querySelector('.period-tab.active'); if (active) currentPeriod = active.getAttribute('data-period') || '15m'; @@ -732,10 +728,22 @@ } else { updateRefreshHint(false); } + } - window.addEventListener('beforeunload', function () { - stopKlineStream(); - destroyChart(); + if (!window.__QIHUO_MARKET_THEME__) { + window.__QIHUO_MARKET_THEME__ = true; + document.addEventListener('click', function (e) { + if (e.target.closest('[data-theme-pick]') && lastData) { + setTimeout(function () { + destroyChart(); + renderChart(lastData, { forceFull: true }); + }, 80); + } }); - }); + } + + if (window.qihuoOnPageLoad) window.qihuoOnPageLoad(bootMarketPage); + else document.addEventListener('DOMContentLoaded', bootMarketPage); + if (window.qihuoOnPageLeave) window.qihuoOnPageLeave(cleanupMarketPage); + window.addEventListener('pagehide', cleanupMarketPage); })(); diff --git a/static/js/nav.js b/static/js/nav.js index d643240..e9ebf4c 100644 --- a/static/js/nav.js +++ b/static/js/nav.js @@ -42,6 +42,10 @@ } function prefetchNav(href) { + if (window.qihuoPrefetchPage) { + window.qihuoPrefetchPage(href); + return; + } if (!href || href.indexOf(window.location.origin) !== 0) return; if (href === window.location.href) return; var links = document.head.querySelectorAll('link[rel="prefetch"]'); diff --git a/static/js/page.js b/static/js/page.js new file mode 100644 index 0000000..187e27d --- /dev/null +++ b/static/js/page.js @@ -0,0 +1,17 @@ +/* Copyright (c) 2025-2026 马建军. All rights reserved. + * 专有软件 — 未经授权禁止复制、传播、转售。 + * 详见 LICENSE.zh-CN.txt + */ +(function (global) { + global.qihuoOnPageLoad = function (fn) { + global.addEventListener('qihuo:page-load', fn); + }; + + global.qihuoOnPageLeave = function (fn) { + global.addEventListener('qihuo:page-leave', fn); + }; + + global.qihuoEmitPageLoad = function () { + global.dispatchEvent(new Event('qihuo:page-load')); + }; +})(); diff --git a/static/js/plans.js b/static/js/plans.js index cd7d2be..7767e29 100644 --- a/static/js/plans.js +++ b/static/js/plans.js @@ -39,11 +39,20 @@ .catch(function () { /* ignore */ }); } + function stopPolling() { + if (timer) { + clearInterval(timer); + timer = null; + } + } + function startPolling() { - if (timer) clearInterval(timer); + stopPolling(); pollPrices(); timer = setInterval(pollPrices, 1000); } - document.addEventListener('DOMContentLoaded', startPolling); + if (window.qihuoOnPageLoad) window.qihuoOnPageLoad(startPolling); + else document.addEventListener('DOMContentLoaded', startPolling); + if (window.qihuoOnPageLeave) window.qihuoOnPageLeave(stopPolling); })(); diff --git a/static/js/positions.js b/static/js/positions.js index 1c7acfb..724f1eb 100644 --- a/static/js/positions.js +++ b/static/js/positions.js @@ -72,8 +72,20 @@ .catch(function () { /* ignore */ }); } - document.addEventListener('DOMContentLoaded', function () { + function stopPolling() { + if (posTimer) { + clearInterval(posTimer); + posTimer = null; + } + } + + function startPolling() { + stopPolling(); pollPositions(); posTimer = setInterval(pollPositions, 1000); - }); + } + + if (window.qihuoOnPageLoad) window.qihuoOnPageLoad(startPolling); + else document.addEventListener('DOMContentLoaded', startPolling); + if (window.qihuoOnPageLeave) window.qihuoOnPageLeave(stopPolling); })(); diff --git a/static/js/review.js b/static/js/review.js index 1ef08ed..495d308 100644 --- a/static/js/review.js +++ b/static/js/review.js @@ -163,11 +163,14 @@ }); } - document.addEventListener('DOMContentLoaded', function () { + function bootReviewPage() { bindForm(); bindModal(); recalc(); - }); + } + + if (window.qihuoOnPageLoad) window.qihuoOnPageLoad(bootReviewPage); + else document.addEventListener('DOMContentLoaded', bootReviewPage); window.recalc = recalc; })(); diff --git a/static/js/stats.js b/static/js/stats.js index 132f3b8..0c89767 100644 --- a/static/js/stats.js +++ b/static/js/stats.js @@ -147,13 +147,17 @@ }); } - document.addEventListener('DOMContentLoaded', function () { + function bootStatsPage() { var viewSel = document.getElementById('stats-view-select'); - if (viewSel) { + if (viewSel && !viewSel.dataset.statsBound) { + viewSel.dataset.statsBound = '1'; viewSel.addEventListener('change', function () { renderBreakdown(this.value); }); } loadStats(); - }); + } + + if (window.qihuoOnPageLoad) window.qihuoOnPageLoad(bootStatsPage); + else document.addEventListener('DOMContentLoaded', bootStatsPage); })(); diff --git a/static/js/symbol.js b/static/js/symbol.js index 78242d5..6a123b9 100644 --- a/static/js/symbol.js +++ b/static/js/symbol.js @@ -261,12 +261,14 @@ }); } - document.addEventListener('DOMContentLoaded', function () { + function initSymbolForms() { document.querySelectorAll('.symbol-wrap').forEach(initSymbolInput); document.querySelectorAll('form').forEach(function (form) { if (!form.querySelector('.symbol-wrap')) return; if (form.id === 'market-form') return; + if (form.dataset.symbolGuard) return; + form.dataset.symbolGuard = '1'; form.addEventListener('submit', function (e) { const ths = form.querySelector('input[name="symbol"]') || form.querySelector('.symbol-ths-code'); @@ -282,5 +284,8 @@ } }); }); - }); + } + + if (window.qihuoOnPageLoad) window.qihuoOnPageLoad(initSymbolForms); + else document.addEventListener('DOMContentLoaded', initSymbolForms); })(); diff --git a/static/js/trade.js b/static/js/trade.js index e861b95..26cb005 100644 --- a/static/js/trade.js +++ b/static/js/trade.js @@ -49,14 +49,6 @@ var productCategories = window.PRODUCT_CATEGORIES || []; var POS_CACHE_KEY = 'qihuo_trading_live_v5'; - function runWhenReady(fn) { - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', fn); - } else { - fn(); - } - } - function fmtNum(v, digits) { if (v === null || v === undefined) return '--'; return Number(v).toFixed(digits === undefined ? 2 : digits); @@ -1595,7 +1587,27 @@ .catch(function () {}); } - runWhenReady(function () { + function cleanupTradePage() { + if (positionSource) { + positionSource.close(); + positionSource = null; + } + if (recommendSource) { + recommendSource.close(); + recommendSource = null; + } + if (quoteTimer) { + clearTimeout(quoteTimer); + quoteTimer = null; + } + if (calcTimer) { + clearTimeout(calcTimer); + calcTimer = null; + } + } + + function bootTradePage() { + if (!list && !orderList) return; updateCtpConnectButtonState(); setPriceType('limit'); if (isFixedMode() && lotsCalc) { @@ -1624,14 +1636,20 @@ .then(function (r) { return r.json(); }) .then(function (data) { if (data.ok) renderRecommendations(data); }) .catch(function () {}); - document.addEventListener('visibilitychange', function () { - if (document.visibilityState === 'visible' && !positionSource) { - connectPositionStream(); - } - }); updateSessionUi(); updateRRDisplay(); scheduleQuote(); scheduleAutoCalc(); + } + + document.addEventListener('visibilitychange', function () { + if (document.visibilityState === 'visible' && list && !positionSource) { + connectPositionStream(); + } }); + + if (window.qihuoOnPageLoad) window.qihuoOnPageLoad(bootTradePage); + else bootTradePage(); + if (window.qihuoOnPageLeave) window.qihuoOnPageLeave(cleanupTradePage); + window.addEventListener('pagehide', cleanupTradePage); })(); diff --git a/static/js/turbonav.js b/static/js/turbonav.js new file mode 100644 index 0000000..d0e49ea --- /dev/null +++ b/static/js/turbonav.js @@ -0,0 +1,230 @@ +/* Copyright (c) 2025-2026 马建军. All rights reserved. + * 专有软件 — 未经授权禁止复制、传播、转售。 + * 详见 LICENSE.zh-CN.txt + */ +(function () { + var PERMANENT_CSS = ['/static/css/base.css', '/static/css/tech.css', '/static/css/responsive.css']; + var CORE_SCRIPT_MARKERS = ['theme.js', 'symbol.js', 'page.js', 'nav.js', 'pwa.js', 'turbonav.js']; + var pageCache = new Map(); + var MAX_CACHE = 10; + var inflight = null; + var currentUrl = normalizeUrl(window.location.href); + + function normalizeUrl(href) { + var u = new URL(href, window.location.origin); + u.hash = ''; + return u.href; + } + + function isPermanentStylesheet(el) { + var href = el.getAttribute('href') || ''; + return PERMANENT_CSS.some(function (p) { return href.indexOf(p) !== -1; }); + } + + function isCoreScript(el) { + var src = el.getAttribute('src') || ''; + if (!src) return false; + return CORE_SCRIPT_MARKERS.some(function (m) { return src.indexOf(m) !== -1; }); + } + + function shouldTurboClick(e, link) { + if (!link || !link.href) return false; + if (e.defaultPrevented) return false; + if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return false; + if (link.target && link.target !== '_self') return false; + if (link.hasAttribute('download')) return false; + if (link.origin !== window.location.origin) return false; + if (normalizeUrl(link.href) === currentUrl) return false; + return true; + } + + function parseHtml(html) { + return new DOMParser().parseFromString(html, 'text/html'); + } + + function fetchPage(url, signal) { + var key = normalizeUrl(url); + if (pageCache.has(key)) { + return Promise.resolve(pageCache.get(key)); + } + return fetch(key, { + credentials: 'same-origin', + headers: { Accept: 'text/html' }, + signal: signal + }).then(function (res) { + if (!res.ok) throw new Error('HTTP ' + res.status); + return res.text(); + }).then(function (html) { + var doc = parseHtml(html); + pageCache.set(key, doc); + if (pageCache.size > MAX_CACHE) { + var first = pageCache.keys().next().value; + pageCache.delete(first); + } + return doc; + }); + } + + function collectPageCss(doc) { + var out = []; + doc.querySelectorAll('head link[rel="stylesheet"], head style').forEach(function (el) { + if (el.tagName === 'LINK' && isPermanentStylesheet(el)) return; + out.push(el); + }); + return out; + } + + function collectPageScripts(doc) { + var out = []; + var pastCore = false; + doc.body.querySelectorAll(':scope > script').forEach(function (el) { + if (isCoreScript(el)) { + if ((el.getAttribute('src') || '').indexOf('pwa.js') !== -1) pastCore = true; + return; + } + if (!pastCore) return; + out.push(el); + }); + return out; + } + + function removePageAssets() { + document.querySelectorAll('head link[rel="stylesheet"]').forEach(function (el) { + if (!isPermanentStylesheet(el)) el.remove(); + }); + document.querySelectorAll('head style').forEach(function (el) { el.remove(); }); + document.querySelectorAll('body script[data-page-js]').forEach(function (el) { el.remove(); }); + } + + function applyPageCss(items) { + items.forEach(function (srcEl) { + var el = srcEl.cloneNode(true); + if (el.tagName === 'LINK') { + el.setAttribute('data-page-css', ''); + } else { + el.setAttribute('data-page-css', ''); + } + document.head.appendChild(el); + }); + } + + function runPageScripts(items) { + return items.reduce(function (chain, srcEl) { + return chain.then(function () { + return new Promise(function (resolve) { + var s = document.createElement('script'); + s.setAttribute('data-page-js', ''); + var src = srcEl.getAttribute('src'); + if (src) { + var bust = (src.indexOf('?') >= 0 ? '&' : '?') + '_turbo=' + Date.now(); + s.src = src + bust; + s.async = false; + s.onload = function () { resolve(); }; + s.onerror = function () { resolve(); }; + document.body.appendChild(s); + } else { + s.textContent = srcEl.textContent; + document.body.appendChild(s); + resolve(); + } + }); + }); + }, Promise.resolve()); + } + + function syncNavActive(doc, url) { + var nav = document.getElementById('site-nav'); + var fetchedNav = doc.getElementById('site-nav'); + if (!nav || !fetchedNav) return; + nav.querySelectorAll('a.active').forEach(function (a) { a.classList.remove('active'); }); + fetchedNav.querySelectorAll('a[href].active').forEach(function (fa) { + var href = fa.getAttribute('href'); + if (!href) return; + var local = nav.querySelector('a[href="' + href + '"]') + || nav.querySelector('a[href="' + new URL(href, window.location.origin).pathname + '"]'); + if (local) local.classList.add('active'); + }); + } + + function setLoading(on) { + var main = document.querySelector('.main'); + if (main) main.classList.toggle('nav-loading', on); + } + + function applyDocument(doc, url, fromPopstate) { + var main = document.querySelector('.main'); + var fetchedMain = doc.querySelector('.main'); + if (!main || !fetchedMain) { + window.location.href = url; + return Promise.resolve(); + } + + window.dispatchEvent(new Event('qihuo:page-leave')); + removePageAssets(); + applyPageCss(collectPageCss(doc)); + main.innerHTML = fetchedMain.innerHTML; + document.title = doc.title || document.title; + syncNavActive(doc, url); + + return runPageScripts(collectPageScripts(doc)).then(function () { + currentUrl = normalizeUrl(url); + if (!fromPopstate) { + history.pushState({ turbo: true }, '', currentUrl); + } + setLoading(false); + window.scrollTo(0, 0); + if (window.qihuoEmitPageLoad) window.qihuoEmitPageLoad(); + }); + } + + function navigateTo(url, opts) { + opts = opts || {}; + var target = normalizeUrl(url); + if (target === currentUrl && !opts.force) return Promise.resolve(); + + if (inflight) inflight.abort(); + var ctrl = new AbortController(); + inflight = ctrl; + setLoading(true); + + return fetchPage(target, ctrl.signal).then(function (doc) { + if (ctrl.signal.aborted) return; + inflight = null; + applyDocument(doc, target, !!opts.fromPopstate); + }).catch(function (err) { + if (ctrl.signal.aborted) return; + inflight = null; + setLoading(false); + window.location.href = target; + }); + } + + function prefetchPage(href) { + var target = normalizeUrl(href); + if (target === currentUrl || pageCache.has(target)) return; + fetchPage(target).catch(function () { /* ignore */ }); + } + + window.qihuoNavigate = navigateTo; + window.qihuoPrefetchPage = prefetchPage; + + document.addEventListener('click', function (e) { + var link = e.target.closest('#site-nav a[href]'); + if (!link || !shouldTurboClick(e, link)) return; + e.preventDefault(); + var navEl = document.getElementById('site-nav'); + if (navEl) { + navEl.querySelectorAll('a.active').forEach(function (a) { a.classList.remove('active'); }); + link.classList.add('active'); + } + navigateTo(link.href); + }, true); + + window.addEventListener('popstate', function () { + var target = normalizeUrl(window.location.href); + if (target === currentUrl) return; + navigateTo(target, { fromPopstate: true, force: true }); + }); + + if (window.qihuoEmitPageLoad) window.qihuoEmitPageLoad(); +})(); diff --git a/static/sw.js b/static/sw.js index 8d35917..4bdf277 100644 --- a/static/sw.js +++ b/static/sw.js @@ -2,14 +2,17 @@ * 专有软件 — 未经授权禁止复制、传播、转售。 * 详见 LICENSE.zh-CN.txt */ -var CACHE_VERSION = 'qihuo-v4'; +var CACHE_VERSION = 'qihuo-v6'; var STATIC_CACHE = CACHE_VERSION + '-static'; var STATIC_ASSETS = [ + '/static/css/base.css', '/static/css/tech.css', '/static/css/responsive.css', '/static/css/trade.css', '/static/js/theme.js', + '/static/js/page.js', '/static/js/nav.js', + '/static/js/turbonav.js', '/static/js/pwa.js', '/static/js/symbol.js', '/static/js/trade.js', diff --git a/templates/base.html b/templates/base.html index c7d70c6..03b0ffd 100644 --- a/templates/base.html +++ b/templates/base.html @@ -25,475 +25,7 @@ } catch (e) { /* ignore */ } - + {% block extra_css %}{% endblock %} @@ -541,8 +73,10 @@ + {% block extra_js %}{% endblock %} + diff --git a/templates/records.html b/templates/records.html index 51aedfc..b542680 100644 --- a/templates/records.html +++ b/templates/records.html @@ -336,28 +336,34 @@ {% if prefill %} {% endif %} {% endblock %}