From ec9ea366108e6633a60bfb15cc8d7091a786cd8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Wed, 3 Jun 2026 17:33:23 +0800 Subject: [PATCH] fix doc --- app.css | 658 +++++++++++++++++++++++++++++++-------------------- app.js | 245 ++++++++++++++++--- index.html | 69 +++--- lib/graph.js | 115 +++++++++ server.js | 11 + 5 files changed, 774 insertions(+), 324 deletions(-) create mode 100644 lib/graph.js diff --git a/app.css b/app.css index ba94cf3..3021eca 100644 --- a/app.css +++ b/app.css @@ -1,319 +1,455 @@ /* ═══════════════════════════════════════════════════════════ - Emmy 投資台 — 學習教材 / 財報健檢 / 交易復盤 的樣式 - 沿用 index.html 既有的 CSS 變數(--bg/--surface/--card…) + Emmy 投資台 — Apple 風格 UI(簡潔、色塊、幾何互動) ═══════════════════════════════════════════════════════════ */ -/* ── 主視圖切換 tabs ── */ -.view-tabs{display:flex;gap:4px;flex-wrap:wrap} -.view-tabs a{ - padding:7px 16px;border-radius:8px;font-size:.9rem;font-weight:600;color:var(--text2);cursor:pointer; - transition:background .15s,color .15s; +/* ── 主視圖切換(膠囊分段)── */ +.view-tabs{ + display:flex;gap:2px;flex-wrap:wrap; + background:rgba(0,0,0,.04);border-radius:12px;padding:3px; +} +.view-tabs a{ + padding:8px 18px;border-radius:10px;font-size:.88rem;font-weight:600;color:var(--text2); + cursor:pointer;transition:background .2s,color .2s,box-shadow .2s; +} +.view-tabs a:hover{color:var(--text)} +.view-tabs a.active{ + background:var(--surface);color:var(--text); + box-shadow:0 1px 4px rgba(0,0,0,.08); } -.view-tabs a:hover{color:var(--text);background:rgba(77,166,255,.08)} -.view-tabs a.active{background:rgba(77,166,255,.16);color:var(--blue)} .view[hidden]{display:none} - -/* 非總經視圖時,隱藏總經的群組子導覽 */ body[data-view="macro"] #navLinks{display:flex} body:not([data-view="macro"]) #navLinks{display:none} -/* ── 共用:頁面區塊標題 ── */ -.page{margin:24px 32px 0;animation:fadeInUp .4s ease both} -.page-head{margin-bottom:18px} -.page-title{font-size:1.35rem;font-weight:700;letter-spacing:-.01em;display:flex;align-items:center;gap:10px} -.page-sub{font-size:.85rem;color:var(--text2);margin-top:6px;line-height:1.6;max-width:880px} -.disclaimer{font-size:.72rem;color:var(--text2);background:rgba(255,138,77,.08);border:1px solid rgba(255,138,77,.2); - border-radius:8px;padding:8px 14px;margin-top:14px;line-height:1.6} +/* ── 頁面 ── */ +.page{margin:28px 32px 0;animation:fadeInUp .35s ease both} +.page-head{margin-bottom:22px} +.page-title{font-size:1.5rem;font-weight:700;letter-spacing:-.02em} +.page-sub{font-size:.9rem;color:var(--text2);margin-top:8px;line-height:1.65;max-width:720px} +.disclaimer{ + font-size:.78rem;color:var(--text2); + background:rgba(255,149,0,.08);border:1px solid rgba(255,149,0,.18); + border-radius:var(--radius);padding:12px 16px;margin-top:16px;line-height:1.6; +} -@media(max-width:900px){ .page{margin:18px 16px 0} } +@keyframes fadeInUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}} +@keyframes spin{to{transform:rotate(360deg)}} + +@media(max-width:900px){ .page{margin:20px 16px 0} } + +/* ── 色塊 / 分段控制(取代下拉)── */ +.chip-row,.chip-group{display:flex;flex-wrap:wrap;gap:8px} +.chip,.chip-item{ + display:inline-flex;align-items:center;gap:6px; + padding:10px 18px;border-radius:12px;font-size:.86rem;font-weight:600; + border:1.5px solid var(--border);background:var(--surface);color:var(--text2); + cursor:pointer;transition:all .18s;font-family:inherit;user-select:none; +} +.chip:hover,.chip-item:hover{border-color:rgba(0,113,227,.35);color:var(--text)} +.chip.on,.chip-item.on{ + background:var(--blue);border-color:var(--blue);color:#fff; + box-shadow:0 4px 14px rgba(0,113,227,.28); +} +.chip.sm,.chip-item.sm{padding:6px 14px;font-size:.8rem;border-radius:10px} +.chip.tint-green.on{background:var(--green);border-color:var(--green);box-shadow:0 4px 14px rgba(52,199,89,.25)} +.chip.tint-red.on{background:var(--red);border-color:var(--red);box-shadow:0 4px 14px rgba(255,59,48,.25)} +.chip.tint-purple.on{background:var(--purple);border-color:var(--purple);box-shadow:0 4px 14px rgba(175,82,222,.25)} + +.seg-pill{ + display:inline-flex;background:rgba(0,0,0,.05);border-radius:12px;padding:3px;gap:2px; +} +.seg-pill button{ + padding:8px 16px;border:none;border-radius:10px;font-size:.84rem;font-weight:600; + background:transparent;color:var(--text2);cursor:pointer;transition:.18s;font-family:inherit; +} +.seg-pill button.on{background:var(--surface);color:var(--text);box-shadow:0 1px 4px rgba(0,0,0,.08)} + +/* 大色塊選項(交易方向等) */ +.tile-row{display:flex;gap:10px;flex-wrap:wrap} +.tile{ + flex:1;min-width:120px;padding:16px 18px;border-radius:var(--radius); + border:2px solid var(--border);background:var(--surface);cursor:pointer; + text-align:center;transition:all .18s; +} +.tile .tile-label{font-size:.95rem;font-weight:700} +.tile .tile-sub{font-size:.72rem;color:var(--text2);margin-top:4px} +.tile.on{border-color:var(--blue);background:rgba(0,113,227,.06);box-shadow:0 4px 16px rgba(0,113,227,.12)} +.tile.on.tint-green{border-color:var(--green);background:rgba(52,199,89,.08)} +.tile.on.tint-red{border-color:var(--red);background:rgba(255,59,48,.06)} /* ═══════════ 學習教材 ═══════════ */ -.learn-layout{display:grid;grid-template-columns:230px 1fr;gap:22px;align-items:start} -.learn-side{position:sticky;top:78px;display:flex;flex-direction:column;gap:4px} -.learn-side .side-group{font-size:.7rem;color:var(--text2);letter-spacing:.06em;margin:12px 4px 4px;text-transform:uppercase} -.learn-side a{ - padding:7px 12px;border-radius:7px;font-size:.85rem;color:var(--text);cursor:pointer;transition:.15s; - display:flex;justify-content:space-between;align-items:center;gap:8px; +.learn-layout{display:grid;grid-template-columns:240px 1fr;gap:24px;align-items:start} +.learn-side{ + position:sticky;top:88px;display:flex;flex-direction:column;gap:6px; } -.learn-side a:hover{background:rgba(77,166,255,.08)} -.learn-side a.active{background:rgba(77,166,255,.15);color:var(--blue)} -.learn-side a .count{font-size:.68rem;color:var(--text2)} +.learn-side .side-group{ + font-size:.68rem;color:var(--text2);letter-spacing:.08em;margin:14px 4px 6px; + text-transform:uppercase;font-weight:600; +} +.learn-side a,.side-tile{ + padding:10px 14px;border-radius:12px;font-size:.86rem;color:var(--text); + cursor:pointer;transition:.15s;display:flex;justify-content:space-between;align-items:center;gap:8px; + border:1px solid transparent;background:transparent;text-decoration:none; +} +.learn-side a:hover,.side-tile:hover{background:rgba(0,0,0,.04)} +.learn-side a.active,.side-tile.active{ + background:var(--surface);border-color:var(--border); + box-shadow:var(--shadow);font-weight:600; +} +.learn-side a .count{font-size:.68rem;color:var(--text2);background:rgba(0,0,0,.05); + padding:2px 8px;border-radius:20px} .learn-content{min-width:0} @media(max-width:780px){ .learn-layout{grid-template-columns:1fr} - .learn-side{position:static;flex-direction:row;flex-wrap:wrap;gap:6px;margin-bottom:14px} - .learn-side .side-group{width:100%;margin:6px 0 0} + .learn-side{position:static;flex-direction:row;flex-wrap:wrap} + .learn-side .side-group{width:100%} } -/* 三階段課綱卡片 */ -.stage{margin-bottom:24px} -.stage-title{font-size:1.05rem;font-weight:700;margin-bottom:4px;display:flex;align-items:center;gap:8px} -.stage-badge{font-size:.66rem;font-weight:700;padding:2px 9px;border-radius:20px} -.stage-desc{font-size:.82rem;color:var(--text2);margin-bottom:12px;line-height:1.6} -.module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:12px} +.stage{margin-bottom:28px} +.stage-title{font-size:1.1rem;font-weight:700;margin-bottom:6px} +.stage-badge{font-size:.66rem;font-weight:700;padding:3px 10px;border-radius:20px} +.stage-desc{font-size:.84rem;color:var(--text2);margin-bottom:14px;line-height:1.6} +.module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px} .module-card{ - background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:16px 18px;cursor:pointer; - transition:border-color .2s,box-shadow .2s;display:flex;flex-direction:column;gap:8px; + background:var(--card);border:1px solid var(--border);border-radius:var(--radius); + padding:18px 20px;cursor:pointer;transition:transform .15s,box-shadow .2s; + display:flex;flex-direction:column;gap:8px;box-shadow:var(--shadow); } -.module-card:hover{border-color:rgba(77,166,255,.35);box-shadow:0 0 18px rgba(77,166,255,.06)} +.module-card:hover{transform:translateY(-2px);box-shadow:0 8px 28px rgba(0,0,0,.1)} .module-card .mod-name{font-size:.98rem;font-weight:700} -.module-card .mod-meta{font-size:.74rem;color:var(--text2);line-height:1.55} -.module-card .mod-tags{display:flex;flex-wrap:wrap;gap:5px;margin-top:2px} -.chip{font-size:.68rem;color:var(--text2);background:var(--surface);border:1px solid var(--border); - border-radius:20px;padding:2px 9px;cursor:pointer;transition:.15s;white-space:nowrap} -.chip:hover{border-color:var(--blue);color:var(--blue)} +.module-card .mod-meta{font-size:.76rem;color:var(--text2);line-height:1.55} +.module-card .mod-tags{display:flex;flex-wrap:wrap;gap:6px;margin-top:4px} +.chip-tag{ + font-size:.68rem;color:var(--text2);background:rgba(0,0,0,.04); + border-radius:20px;padding:3px 10px; +} -/* 速查(名詞 / 公司 / 單集)搜尋 */ -.search-box{display:flex;gap:8px;margin-bottom:14px} +.search-box{display:flex;gap:10px;margin-bottom:16px} .search-box input{ - flex:1;background:var(--surface);border:1px solid var(--border);border-radius:8px;color:var(--text); - padding:10px 14px;font-size:.9rem;outline:none;transition:.15s;font-family:inherit; + flex:1;background:var(--surface);border:1px solid var(--border);border-radius:12px; + color:var(--text);padding:12px 16px;font-size:.92rem;outline:none; + box-shadow:var(--shadow);font-family:inherit; } -.search-box input:focus{border-color:var(--blue)} -.glossary-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px} +.search-box input:focus{border-color:var(--blue);box-shadow:0 0 0 4px rgba(0,113,227,.15)} +.glossary-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px} .gloss-item{ - background:var(--card);border:1px solid var(--border);border-radius:8px;padding:9px 12px;cursor:pointer;transition:.15s; + background:var(--card);border:1px solid var(--border);border-radius:12px; + padding:12px 14px;cursor:pointer;transition:.15s;box-shadow:var(--shadow); } -.gloss-item:hover{border-color:var(--blue);background:rgba(77,166,255,.06)} -.gloss-item .gi-title{font-size:.85rem;font-weight:600;color:var(--text)} -.gloss-item .gi-sub{font-size:.7rem;color:var(--text2);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -.list-meta{font-size:.76rem;color:var(--text2);margin-bottom:10px} +.gloss-item:hover{border-color:rgba(0,113,227,.3);transform:translateY(-1px)} +.gloss-item .gi-title{font-size:.86rem;font-weight:600} +.gloss-item .gi-sub{font-size:.72rem;color:var(--text2);margin-top:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.list-meta{font-size:.78rem;color:var(--text2);margin-bottom:12px} -/* Markdown 內文渲染 */ -.md{font-size:.9rem;line-height:1.75;color:var(--text)} -.md h1{font-size:1.5rem;font-weight:700;margin:.2em 0 .5em} -.md h2{font-size:1.18rem;font-weight:700;margin:1.3em 0 .5em;padding-bottom:.3em;border-bottom:1px solid var(--border)} -.md h3{font-size:1.02rem;font-weight:700;margin:1.1em 0 .4em;color:var(--text)} -.md h4{font-size:.92rem;font-weight:700;margin:1em 0 .3em;color:var(--text2)} -.md p{margin:.6em 0} +/* Markdown */ +.md{font-size:.94rem;line-height:1.75;color:var(--text)} +.md h1{font-size:1.55rem;font-weight:700;margin:.2em 0 .5em;letter-spacing:-.02em} +.md h2{font-size:1.22rem;font-weight:700;margin:1.4em 0 .5em;padding-bottom:.35em;border-bottom:1px solid var(--border)} +.md h3{font-size:1.05rem;font-weight:700;margin:1.2em 0 .4em} +.md h4{font-size:.94rem;font-weight:600;margin:1em 0 .3em;color:var(--text2)} +.md p{margin:.65em 0} .md ul,.md ol{margin:.5em 0 .5em 1.3em} -.md li{margin:.25em 0} -.md blockquote{border-left:3px solid var(--blue);background:var(--surface);margin:.8em 0;padding:.6em 1em; - color:var(--text2);border-radius:0 8px 8px 0} -.md blockquote p{margin:.2em 0} -.md code{background:var(--surface);padding:2px 6px;border-radius:4px;color:var(--yellow);font-size:.86em; - font-family:ui-monospace,SFMono-Regular,Menlo,monospace} -.md pre{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px 14px;overflow:auto;margin:.8em 0} -.md pre code{background:none;padding:0;color:var(--text2)} -.md hr{border:none;border-top:1px solid var(--border);margin:1.2em 0} -.md table{border-collapse:collapse;width:100%;margin:.9em 0;font-size:.84rem;display:block;overflow-x:auto} -.md th,.md td{border:1px solid var(--border);padding:7px 11px;text-align:left;vertical-align:top} -.md th{background:var(--surface);font-weight:600;color:var(--text);white-space:nowrap} -.md td{color:var(--text2)} -.md a{color:var(--blue)} -.md .wlink{color:var(--purple);border-bottom:1px dashed rgba(179,136,255,.4);cursor:pointer} -.md .wlink:hover{border-bottom-style:solid} -.md .wlink.dead{color:var(--text2);border-bottom-color:transparent;cursor:default} - -.back-link{display:inline-flex;align-items:center;gap:6px;font-size:.82rem;color:var(--text2);cursor:pointer;margin-bottom:14px} -.back-link:hover{color:var(--blue)} -.note-frontmatter{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px} -.fm-tag{font-size:.7rem;color:var(--text2);background:var(--surface);border:1px solid var(--border);border-radius:20px;padding:2px 10px} - -/* 練習題庫 */ -.quiz-q{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:14px 16px;margin-bottom:10px} -.quiz-q .q-text{font-size:.9rem;line-height:1.6} -.quiz-q .q-src{font-size:.72rem;color:var(--text2);margin-top:8px;cursor:pointer} -.quiz-q .q-src:hover{color:var(--blue)} - -/* ═══════════ 財報健檢 ═══════════ */ -.finbox-search{display:flex;gap:8px;margin-bottom:6px;max-width:520px} -.finbox-search input{ - flex:1;background:var(--surface);border:1px solid var(--border);border-radius:8px;color:var(--text); - padding:11px 15px;font-size:1rem;outline:none;font-family:inherit;letter-spacing:.04em;text-transform:uppercase; +.md li{margin:.3em 0} +.md blockquote{ + border-left:4px solid var(--blue);background:rgba(0,113,227,.05); + margin:.9em 0;padding:.7em 1.1em;color:var(--text2);border-radius:0 12px 12px 0; } -.finbox-search input:focus{border-color:var(--blue)} +.md code{background:rgba(0,0,0,.05);padding:2px 8px;border-radius:6px;color:var(--purple);font-size:.88em} +.md pre{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px;overflow:auto;margin:.9em 0;box-shadow:var(--shadow)} +.md pre code{background:none;padding:0;color:var(--text2)} +.md table{border-collapse:collapse;width:100%;margin:1em 0;font-size:.86rem;display:block;overflow-x:auto;border-radius:12px} +.md th,.md td{border:1px solid var(--border);padding:10px 14px;text-align:left} +.md th{background:rgba(0,0,0,.03);font-weight:600} +.md hr{border:none;border-top:1px solid var(--border);margin:1.4em 0} +.md a{color:var(--blue)} +.md .wlink{color:var(--purple);border-bottom:1px dashed rgba(175,82,222,.4);cursor:pointer;font-weight:500} +.md .wlink:hover{border-bottom-style:solid} +.md .wlink.dead{color:var(--text2);border-bottom:none;cursor:default} + +/* Mermaid 圖表 */ +.mermaid-wrap{ + background:var(--surface);border:1px solid var(--border);border-radius:var(--radius); + padding:20px;margin:1.2em 0;overflow-x:auto;box-shadow:var(--shadow); +} +.mermaid-wrap .mermaid{display:flex;justify-content:center} +.mermaid-wrap svg{max-width:100%;height:auto} + +.note-toolbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:14px} +.back-link{ + display:inline-flex;align-items:center;gap:6px;font-size:.84rem;color:var(--text2); + cursor:pointer;margin-bottom:16px;padding:6px 12px;border-radius:10px; + background:rgba(0,0,0,.04);transition:.15s; +} +.back-link:hover{color:var(--blue);background:rgba(0,113,227,.08)} +.note-frontmatter{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px} +.fm-tag{ + font-size:.72rem;color:var(--text2);background:var(--surface); + border:1px solid var(--border);border-radius:20px;padding:4px 12px;box-shadow:var(--shadow); +} + +/* 知識圖譜 */ +.graph-panel{ + background:var(--surface);border:1px solid var(--border);border-radius:var(--radius); + box-shadow:var(--shadow);overflow:hidden; +} +.graph-toolbar{ + display:flex;flex-wrap:wrap;gap:10px;align-items:center;padding:14px 16px; + border-bottom:1px solid var(--border); +} +.graph-canvas{height:min(62vh,520px);background:linear-gradient(180deg,#fafafa 0%,#f5f5f7 100%)} +.graph-foot{font-size:.74rem;color:var(--text2);padding:10px 16px;border-top:1px solid var(--border)} +.graph-legend{display:flex;flex-wrap:wrap;gap:12px;font-size:.72rem} +.graph-legend span{display:inline-flex;align-items:center;gap:5px} +.graph-legend i{width:10px;height:10px;border-radius:50%;display:inline-block} + +.quiz-q{ + background:var(--card);border:1px solid var(--border);border-radius:var(--radius); + padding:16px 18px;margin-bottom:12px;box-shadow:var(--shadow); +} +.quiz-q .q-text{font-size:.92rem;line-height:1.65} +.quiz-q .q-src{font-size:.74rem;color:var(--text2);margin-top:10px;cursor:pointer} + +/* ═══════════ 個股工具 ═══════════ */ +.finbox-search{display:flex;gap:10px;margin-bottom:8px;max-width:480px} +.finbox-search input{ + flex:1;background:var(--surface);border:1px solid var(--border);border-radius:12px; + color:var(--text);padding:14px 18px;font-size:1.05rem;outline:none; + box-shadow:var(--shadow);font-family:inherit;letter-spacing:.03em;text-transform:uppercase; +} +.finbox-search input:focus{border-color:var(--blue);box-shadow:0 0 0 4px rgba(0,113,227,.12)} .finbox-search button{ - background:var(--blue);color:#08111d;border:none;padding:0 22px;border-radius:8px;font-weight:700;font-size:.92rem;cursor:pointer; + background:var(--blue);color:#fff;border:none;padding:0 24px;border-radius:12px; + font-weight:600;font-size:.92rem;cursor:pointer;box-shadow:0 4px 14px rgba(0,113,227,.25); } .finbox-search button:disabled{opacity:.5;cursor:wait} -.finbox-examples{font-size:.76rem;color:var(--text2);margin-bottom:18px} -.finbox-examples b{cursor:pointer;color:var(--blue);font-weight:600;margin:0 4px} +.finbox-examples{font-size:.78rem;color:var(--text2);margin-bottom:20px} +.finbox-examples b{cursor:pointer;color:var(--blue);font-weight:600;margin:0 6px} -.fin-summary{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:20px 24px;margin-bottom:18px; - display:grid;grid-template-columns:auto 1fr;gap:24px;align-items:center} -.fin-verdict{text-align:center} -.fin-verdict .v-big{font-size:2.4rem;font-weight:800;line-height:1} +.sub-tabs{ + display:flex;gap:4px;background:rgba(0,0,0,.04);border-radius:12px; + padding:4px;margin:8px 0 20px;flex-wrap:wrap;width:fit-content; +} +.sub-tabs a{ + padding:10px 20px;border-radius:10px;font-size:.86rem;font-weight:600; + color:var(--text2);cursor:pointer;transition:.15s; +} +.sub-tabs a:hover{color:var(--text)} +.sub-tabs a.active{background:var(--surface);color:var(--blue);box-shadow:0 1px 4px rgba(0,0,0,.08)} +.stk-pane[hidden]{display:none} + +.chart-wrap{ + position:relative;width:100%;background:var(--surface); + border:1px solid var(--border);border-radius:var(--radius);padding:12px;box-shadow:var(--shadow); +} +.chart-wrap svg{display:block;width:100%;height:auto} +.chart-empty{padding:48px 0;text-align:center;color:var(--text2)} +.chart-legend{display:flex;gap:16px;font-size:.78rem;color:var(--text2);margin-bottom:8px} +.chart-legend i{display:inline-block;width:12px;height:12px;border-radius:4px;margin-right:6px} +.chart-hover{font-size:.8rem;color:var(--text2);margin-top:8px;min-height:1.2em} +.range-btns{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px} +.range-btns button{ + background:var(--surface);border:1px solid var(--border);color:var(--text2); + border-radius:10px;padding:8px 16px;font-size:.82rem;font-weight:600; + cursor:pointer;font-family:inherit;box-shadow:var(--shadow);transition:.15s; +} +.range-btns button:hover{border-color:var(--blue)} +.range-btns button.active{background:var(--blue);border-color:var(--blue);color:#fff} + +.fin-summary{ + background:var(--card);border:1px solid var(--border);border-radius:var(--radius); + padding:22px 26px;margin-bottom:20px;display:grid;grid-template-columns:auto 1fr; + gap:24px;align-items:center;box-shadow:var(--shadow); +} +.fin-verdict .v-big{font-size:2.5rem;font-weight:800;line-height:1} .fin-verdict .v-sub{font-size:.78rem;color:var(--text2);margin-top:4px} -.fin-lights{display:flex;gap:18px} +.fin-lights{display:flex;gap:20px} .fin-light{text-align:center} -.fin-light .fl-num{font-size:1.6rem;font-weight:700;line-height:1} -.fin-light .fl-lab{font-size:.72rem;color:var(--text2);margin-top:3px} -.fin-co{font-size:.82rem;color:var(--text2);margin-bottom:8px} -.fin-co b{color:var(--text);font-size:1.05rem} -.fin-fresh{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap; - font-size:.74rem;color:var(--text2);margin-bottom:16px} +.fin-light .fl-num{font-size:1.55rem;font-weight:700} +.fin-light .fl-lab{font-size:.72rem;color:var(--text2);margin-top:4px} +.fin-co{font-size:.84rem;color:var(--text2);margin-bottom:10px} +.fin-co b{color:var(--text);font-size:1.08rem} +.fin-fresh{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;font-size:.76rem;color:var(--text2);margin-bottom:18px} -.fin-step{margin-bottom:18px} -.fin-step-head{display:flex;align-items:center;gap:10px;margin-bottom:8px} -.fin-step-num{width:24px;height:24px;border-radius:7px;background:rgba(77,166,255,.15);color:var(--blue); - font-size:.78rem;font-weight:700;display:flex;align-items:center;justify-content:center} -.fin-step-title{font-size:1rem;font-weight:700} +.fin-step{margin-bottom:20px} +.fin-step-head{display:flex;align-items:center;gap:10px;margin-bottom:10px} +.fin-step-num{ + width:28px;height:28px;border-radius:10px;background:rgba(0,113,227,.1); + color:var(--blue);font-size:.8rem;font-weight:700; + display:flex;align-items:center;justify-content:center; +} +.fin-step-title{font-size:1.02rem;font-weight:700} .check-row{ - background:var(--card);border:1px solid var(--border);border-radius:9px;padding:11px 14px;margin-bottom:8px; - display:grid;grid-template-columns:8px 1fr auto;gap:12px;align-items:center;border-left:3px solid var(--border); + background:var(--card);border:1px solid var(--border);border-radius:12px; + padding:14px 16px;margin-bottom:8px;display:grid;grid-template-columns:10px 1fr auto; + gap:14px;align-items:center;border-left:4px solid var(--border);box-shadow:var(--shadow); } .check-row.good{border-left-color:var(--green)} .check-row.warn{border-left-color:var(--yellow)} .check-row.bad{border-left-color:var(--red)} -.check-row.na{border-left-color:var(--text2);opacity:.7} -.check-dot{width:9px;height:9px;border-radius:50%} +.check-row.na{border-left-color:var(--text2);opacity:.75} +.check-dot{width:10px;height:10px;border-radius:50%} .check-row.good .check-dot{background:var(--green)} .check-row.warn .check-dot{background:var(--yellow)} .check-row.bad .check-dot{background:var(--red)} -.check-row.na .check-dot{background:var(--text2)} -.check-main .ck-label{font-size:.88rem;font-weight:600} -.check-main .ck-note{font-size:.78rem;color:var(--text2);line-height:1.55;margin-top:3px} -.check-main .ck-links{margin-top:5px;display:flex;flex-wrap:wrap;gap:6px} -.check-main .ck-links .wlink{font-size:.72rem} -.check-val{font-size:1.05rem;font-weight:700;text-align:right;white-space:nowrap} -.check-val.good{color:var(--green)}.check-val.warn{color:var(--yellow)}.check-val.bad{color:var(--red)}.check-val.na{color:var(--text2)} - -/* ═══════════ 交易復盤 ═══════════ */ -.stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-bottom:22px} -.stat-card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:16px 18px} -.stat-card .st-lab{font-size:.74rem;color:var(--text2);margin-bottom:6px} -.stat-card .st-val{font-size:1.7rem;font-weight:700;line-height:1} -.stat-card .st-sub{font-size:.72rem;color:var(--text2);margin-top:4px} - -.btn{background:var(--blue);color:#08111d;border:none;padding:8px 16px;border-radius:7px;font-weight:600;font-size:.85rem;cursor:pointer;transition:.15s} -.btn:hover{filter:brightness(1.08)} -.btn.ghost{background:var(--surface);border:1px solid var(--border);color:var(--text2)} -.btn.ghost:hover{border-color:var(--blue);color:var(--blue)} -.btn.danger{background:var(--surface);border:1px solid var(--border);color:var(--red)} -.btn.danger:hover{border-color:var(--red)} -.btn.sm{padding:4px 10px;font-size:.76rem} - -.journal-bar{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:14px;flex-wrap:wrap} -.seg{display:flex;gap:4px;background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:3px} -.seg a{padding:6px 14px;border-radius:6px;font-size:.82rem;color:var(--text2);cursor:pointer;transition:.15s} -.seg a.active{background:var(--card);color:var(--text)} - -.trade-table{width:100%;border-collapse:collapse;font-size:.82rem} -.trade-table th{text-align:left;padding:9px 10px;color:var(--text2);font-weight:600;font-size:.74rem; - border-bottom:1px solid var(--border);white-space:nowrap} -.trade-table td{padding:10px;border-bottom:1px solid var(--border);vertical-align:middle} -.trade-table tr:hover td{background:rgba(77,166,255,.04)} -.t-sym{font-weight:700;color:var(--text)} -.t-sym .t-name{font-weight:400;color:var(--text2);font-size:.76rem;margin-left:5px} -.pill{font-size:.68rem;font-weight:600;padding:2px 8px;border-radius:20px;white-space:nowrap} -.pill.long{background:rgba(0,212,170,.12);color:var(--green)} -.pill.short{background:rgba(255,77,106,.12);color:var(--red)} -.pill.invest{background:rgba(77,166,255,.12);color:var(--blue)} -.pill.trade{background:rgba(179,136,255,.12);color:var(--purple)} -.pill.open{background:rgba(255,193,77,.12);color:var(--yellow)} -.pill.mistake{background:rgba(255,77,106,.14);color:var(--red)} -.pnl-pos{color:var(--green);font-weight:700} -.pnl-neg{color:var(--red);font-weight:700} -.t-actions{display:flex;gap:6px;justify-content:flex-end} -.empty-state{text-align:center;color:var(--text2);padding:50px 20px;font-size:.9rem} - -.group-stat{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:14px 16px;margin-bottom:10px} -.group-stat h4{font-size:.86rem;margin-bottom:10px;color:var(--text)} -.gs-row{display:grid;grid-template-columns:1fr auto auto auto;gap:10px;font-size:.8rem;padding:5px 0;border-top:1px solid var(--border)} -.gs-row:first-of-type{border-top:none} -.gs-row .gs-name{color:var(--text)} -.gs-row .gs-cell{color:var(--text2);text-align:right;min-width:64px} - -/* Modal 表單(沿用 index.html 的 #modalOverlay 樣式,這裡補表單元素) */ -.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px 14px;margin-top:6px} -.form-grid .full{grid-column:1/-1} -.field label{display:block;font-size:.74rem;color:var(--text2);margin-bottom:4px} -.field input,.field select,.field textarea{ - width:100%;background:var(--surface);border:1px solid var(--border);border-radius:7px;color:var(--text); - padding:8px 11px;font-size:.86rem;outline:none;font-family:inherit; -} -.field input:focus,.field select:focus,.field textarea:focus{border-color:var(--blue)} -.field textarea{resize:vertical;min-height:60px} -.form-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:18px} -.check-inline{display:flex;align-items:center;gap:8px;font-size:.84rem;color:var(--text)} -.check-inline input{width:auto} -@media(max-width:600px){ .form-grid{grid-template-columns:1fr} } - -/* ═══════════ 個股工具(子分頁 / 圖表 / 投資地圖 / 回測)═══════════ */ -.sub-tabs{display:flex;gap:4px;background:var(--surface);border:1px solid var(--border);border-radius:9px;padding:3px;margin:4px 0 18px;flex-wrap:wrap;width:fit-content} -.sub-tabs a{padding:7px 16px;border-radius:7px;font-size:.85rem;font-weight:600;color:var(--text2);cursor:pointer;transition:.15s} -.sub-tabs a:hover{color:var(--text)} -.sub-tabs a.active{background:var(--card);color:var(--blue)} -.stk-pane[hidden]{display:none} - -/* 共用折線圖 */ -.chart-wrap{position:relative;width:100%;background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:8px} -.chart-wrap svg{display:block;width:100%;height:auto} -.chart-empty{padding:40px 0;text-align:center;color:var(--text2);font-size:.85rem} -.chart-legend{display:flex;gap:16px;font-size:.78rem;color:var(--text2);margin-bottom:6px} -.chart-legend i{display:inline-block;width:11px;height:11px;border-radius:3px;margin-right:5px;vertical-align:middle} -.chart-hover{font-size:.78rem;color:var(--text2);margin-top:6px;min-height:1.2em} -.range-btns{display:flex;gap:5px;flex-wrap:wrap;margin-bottom:12px} -.range-btns button{background:var(--surface);border:1px solid var(--border);color:var(--text2);border-radius:7px; - padding:5px 13px;font-size:.8rem;cursor:pointer;font-family:inherit;transition:.15s} -.range-btns button:hover{border-color:var(--blue);color:var(--text)} -.range-btns button.active{background:rgba(77,166,255,.16);border-color:var(--blue);color:var(--blue);font-weight:600} +.check-main .ck-label{font-size:.9rem;font-weight:600} +.check-main .ck-note{font-size:.8rem;color:var(--text2);line-height:1.55;margin-top:4px} +.check-main .ck-links{margin-top:6px;display:flex;flex-wrap:wrap;gap:6px} +.check-main .ck-links .wlink{font-size:.74rem;color:var(--purple);cursor:pointer} +.check-val{font-size:1.05rem;font-weight:700;text-align:right} +.check-val.good{color:var(--green)}.check-val.warn{color:var(--yellow)}.check-val.bad{color:var(--red)} /* 投資地圖 */ -.map-core{background:rgba(179,136,255,.08);border:1px solid rgba(179,136,255,.25);border-radius:10px; - padding:13px 16px;font-size:.85rem;font-weight:700;color:var(--purple);margin-bottom:14px;line-height:1.5} -.map-core span{display:block;font-weight:400;color:var(--text2);font-size:.8rem;margin-top:5px;line-height:1.6} -.map-verdict{background:var(--card);border:1px solid var(--border);border-radius:11px;padding:15px 18px;margin-bottom:16px; - border-left:4px solid var(--text2)} +.map-core{ + background:linear-gradient(135deg,rgba(175,82,222,.08),rgba(0,113,227,.06)); + border:1px solid rgba(175,82,222,.2);border-radius:var(--radius); + padding:16px 20px;font-weight:700;margin-bottom:16px;box-shadow:var(--shadow); +} +.map-core span{display:block;font-weight:400;color:var(--text2);font-size:.84rem;margin-top:6px;line-height:1.6} +.map-verdict{ + background:var(--card);border:1px solid var(--border);border-radius:var(--radius); + padding:18px 22px;margin-bottom:18px;border-left:5px solid var(--text2);box-shadow:var(--shadow); +} .map-verdict.good{border-left-color:var(--green)} .map-verdict.warn{border-left-color:var(--yellow)} .map-verdict.bad{border-left-color:var(--red)} -.map-verdict .mv-lab{font-size:.74rem;color:var(--text2);margin-bottom:4px} -.map-verdict .mv-text{font-size:.96rem;font-weight:700;line-height:1.5} -.map-verdict .mv-actions{display:flex;gap:8px;margin-top:12px} -.map-layer{background:var(--card);border:1px solid var(--border);border-radius:11px;padding:15px 18px;margin-bottom:12px; - border-left:3px solid var(--border)} +.map-verdict .mv-lab{font-size:.76rem;color:var(--text2)} +.map-verdict .mv-text{font-size:1rem;font-weight:700;line-height:1.5;margin-top:4px} +.map-verdict .mv-actions{display:flex;gap:10px;margin-top:14px;flex-wrap:wrap} +.map-layer{ + background:var(--card);border:1px solid var(--border);border-radius:var(--radius); + padding:18px 22px;margin-bottom:14px;border-left:4px solid var(--border);box-shadow:var(--shadow); +} .map-layer.pass{border-left-color:var(--green)} .map-layer.watch{border-left-color:var(--yellow)} .map-layer.out{border-left-color:var(--red)} -.ml-head{display:flex;align-items:center;gap:10px;margin-bottom:6px} -.ml-num{width:24px;height:24px;border-radius:7px;background:rgba(77,166,255,.15);color:var(--blue); - font-size:.78rem;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0} -.ml-title{font-size:1rem;font-weight:700;flex:1} -.ml-badge{font-size:.68rem;font-weight:700;padding:2px 10px;border-radius:20px} -.ml-badge.good{background:rgba(0,212,170,.14);color:var(--green)} -.ml-badge.warn{background:rgba(255,193,77,.14);color:var(--yellow)} -.ml-badge.bad{background:rgba(255,77,106,.14);color:var(--red)} -.ml-badge.na{background:var(--surface);color:var(--text2)} -.ml-ask{font-size:.82rem;color:var(--text);line-height:1.6} -.ml-pillar{font-size:.74rem;color:var(--text2);margin:3px 0 10px} -.map-q{border-top:1px solid var(--border);padding:10px 0} -.map-q:last-of-type{border-bottom:1px solid var(--border)} -.mq-text{font-size:.85rem;line-height:1.55;margin-bottom:7px} -.mq-text .gate{font-size:.64rem;font-weight:700;background:rgba(255,138,77,.16);color:var(--orange); - border-radius:4px;padding:1px 6px;margin-right:7px;vertical-align:middle} -.mq-ans{display:flex;gap:7px;flex-wrap:wrap} -.ans{font-size:.78rem;padding:4px 13px;border-radius:7px;border:1px solid var(--border);background:var(--surface); - color:var(--text2);cursor:pointer;transition:.15s;user-select:none} +.ml-head{display:flex;align-items:center;gap:12px;margin-bottom:8px} +.ml-num{ + width:32px;height:32px;border-radius:10px;background:rgba(0,113,227,.1); + color:var(--blue);font-weight:700;display:flex;align-items:center;justify-content:center; +} +.ml-title{font-size:1.05rem;font-weight:700;flex:1} +.ml-badge{font-size:.68rem;font-weight:700;padding:4px 12px;border-radius:20px} +.ml-badge.good{background:rgba(52,199,89,.12);color:var(--green)} +.ml-badge.warn{background:rgba(255,149,0,.12);color:var(--orange)} +.ml-badge.bad{background:rgba(255,59,48,.12);color:var(--red)} +.ml-badge.na{background:rgba(0,0,0,.05);color:var(--text2)} +.ml-ask,.ml-pillar{font-size:.82rem;line-height:1.6} +.ml-pillar{color:var(--text2);margin:4px 0 12px} +.map-q{border-top:1px solid var(--border);padding:12px 0} +.mq-text{font-size:.88rem;line-height:1.55;margin-bottom:8px} +.mq-text .gate{ + font-size:.64rem;font-weight:700;background:rgba(255,149,0,.15);color:var(--orange); + border-radius:6px;padding:2px 8px;margin-right:8px; +} +.mq-ans{display:flex;gap:8px;flex-wrap:wrap} +.ans{ + font-size:.8rem;padding:8px 16px;border-radius:10px;border:1.5px solid var(--border); + background:var(--surface);color:var(--text2);cursor:pointer;transition:.15s; +} .ans input{display:none} -.ans:hover{border-color:var(--blue)} -.ans.yes.on{background:rgba(0,212,170,.16);border-color:var(--green);color:var(--green)} -.ans.unsure.on{background:rgba(255,193,77,.16);border-color:var(--yellow);color:var(--yellow)} -.ans.no.on{background:rgba(255,77,106,.16);border-color:var(--red);color:var(--red)} -.ml-out{font-size:.72rem;color:var(--text2);margin-top:9px;font-style:italic} -.map-q .ck-links{margin-top:6px;display:flex;flex-wrap:wrap;gap:6px} -.map-q .ck-links .wlink{font-size:.72rem} +.ans.yes.on{background:rgba(52,199,89,.12);border-color:var(--green);color:var(--green);font-weight:600} +.ans.unsure.on{background:rgba(255,149,0,.1);border-color:var(--orange);color:var(--orange);font-weight:600} +.ans.no.on{background:rgba(255,59,48,.1);border-color:var(--red);color:var(--red);font-weight:600} +.ml-out{font-size:.74rem;color:var(--text2);margin-top:10px;font-style:italic} /* 回測 */ -.bt-controls{display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;background:var(--card);border:1px solid var(--border); - border-radius:10px;padding:14px 16px;margin-bottom:16px} -.bt-params{display:flex;gap:12px;flex-wrap:wrap} -.bt-field{display:flex;flex-direction:column;gap:4px} -.bt-field label{font-size:.72rem;color:var(--text2)} -.bt-field select,.bt-field input{background:var(--surface);border:1px solid var(--border);border-radius:7px;color:var(--text); - padding:8px 11px;font-size:.85rem;outline:none;font-family:inherit;min-width:120px} -.bt-field input{width:100px;min-width:0} -.bt-field select:focus,.bt-field input:focus{border-color:var(--blue)} -.bt-controls .btn{align-self:flex-end} -.bt-stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:14px} -.bt-stat{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:14px 16px} -.bt-stat .bts-title{font-size:.84rem;font-weight:700;margin-bottom:10px} -.bts-grid{display:grid;grid-template-columns:1fr 1fr;gap:9px 14px} -.bts-grid div{display:flex;flex-direction:column;gap:2px} +.bt-controls{ + display:flex;gap:14px;align-items:flex-end;flex-wrap:wrap; + background:var(--card);border:1px solid var(--border);border-radius:var(--radius); + padding:18px 20px;margin-bottom:18px;box-shadow:var(--shadow); +} +.bt-field label{font-size:.72rem;color:var(--text2);font-weight:600;margin-bottom:6px;display:block} +.bt-params{display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end} +.bt-stats{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:16px} +.bt-stat{ + background:var(--card);border:1px solid var(--border);border-radius:var(--radius); + padding:16px 18px;box-shadow:var(--shadow); +} +.bt-stat .bts-title{font-size:.86rem;font-weight:700;margin-bottom:12px} +.bts-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px 16px} .bts-grid span{font-size:.7rem;color:var(--text2)} -.bts-grid b{font-size:1.02rem;font-weight:700} -.bt-note{font-size:.76rem;color:var(--text2);margin-top:12px;line-height:1.6} +.bts-grid b{font-size:1.02rem} +.bt-note{font-size:.78rem;color:var(--text2);margin-top:14px;line-height:1.6} @media(max-width:680px){ .bt-stats{grid-template-columns:1fr} } + +/* ═══════════ 交易復盤 ═══════════ */ +.stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:14px;margin-bottom:24px} +.stat-card{ + background:var(--card);border:1px solid var(--border);border-radius:var(--radius); + padding:18px 20px;box-shadow:var(--shadow); +} +.stat-card .st-lab{font-size:.76rem;color:var(--text2);margin-bottom:8px} +.stat-card .st-val{font-size:1.75rem;font-weight:700;line-height:1} +.stat-card .st-sub{font-size:.74rem;color:var(--text2);margin-top:6px} + +.btn{ + background:var(--blue);color:#fff;border:none;padding:10px 20px;border-radius:12px; + font-weight:600;font-size:.88rem;cursor:pointer;transition:.15s; + box-shadow:0 4px 14px rgba(0,113,227,.2);font-family:inherit; +} +.btn:hover{filter:brightness(1.05);transform:translateY(-1px)} +.btn.ghost{background:var(--surface);border:1px solid var(--border);color:var(--text2);box-shadow:var(--shadow)} +.btn.ghost:hover{border-color:var(--blue);color:var(--blue)} +.btn.danger{background:var(--surface);border:1px solid var(--border);color:var(--red);box-shadow:var(--shadow)} +.btn.danger:hover{border-color:var(--red)} +.btn.sm{padding:6px 14px;font-size:.8rem;border-radius:10px} + +.journal-bar{display:flex;justify-content:space-between;align-items:center;gap:14px;margin-bottom:18px;flex-wrap:wrap} +.seg{display:flex;gap:3px;background:rgba(0,0,0,.04);border-radius:12px;padding:4px} +.seg a{ + padding:8px 16px;border-radius:10px;font-size:.84rem;font-weight:600; + color:var(--text2);cursor:pointer;transition:.15s; +} +.seg a.active{background:var(--surface);color:var(--text);box-shadow:0 1px 4px rgba(0,0,0,.08)} + +.trade-table{width:100%;border-collapse:separate;border-spacing:0;font-size:.84rem} +.trade-table th{ + text-align:left;padding:12px 14px;color:var(--text2);font-weight:600;font-size:.74rem; + border-bottom:1px solid var(--border); +} +.trade-table td{padding:12px 14px;border-bottom:1px solid var(--border);vertical-align:middle} +.trade-table tr:hover td{background:rgba(0,113,227,.03)} +.t-sym{font-weight:700} +.t-sym .t-name{font-weight:400;color:var(--text2);font-size:.76rem;margin-left:6px} +.pill{font-size:.68rem;font-weight:600;padding:3px 10px;border-radius:20px} +.pill.long{background:rgba(52,199,89,.12);color:var(--green)} +.pill.short{background:rgba(255,59,48,.12);color:var(--red)} +.pill.invest{background:rgba(0,113,227,.1);color:var(--blue)} +.pill.trade{background:rgba(175,82,222,.1);color:var(--purple)} +.pill.open{background:rgba(255,149,0,.12);color:var(--orange)} +.pill.mistake{background:rgba(255,59,48,.12);color:var(--red)} +.pnl-pos{color:var(--green);font-weight:700} +.pnl-neg{color:var(--red);font-weight:700} +.t-actions{display:flex;gap:8px;justify-content:flex-end} +.empty-state{text-align:center;color:var(--text2);padding:56px 24px;font-size:.92rem} + +.group-stat{ + background:var(--card);border:1px solid var(--border);border-radius:var(--radius); + padding:16px 18px;margin-bottom:12px;box-shadow:var(--shadow); +} +.group-stat h4{font-size:.88rem;margin-bottom:12px} +.gs-row{display:grid;grid-template-columns:1fr auto auto auto;gap:12px;font-size:.82rem;padding:6px 0;border-top:1px solid var(--border)} +.gs-row:first-of-type{border-top:none} +.gs-row .gs-cell{text-align:right;min-width:64px;color:var(--text2)} + +/* Modal */ +#tradeModal .modal-panel{background:var(--surface);border-radius:18px;box-shadow:0 24px 80px rgba(0,0,0,.18)} +.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px 16px;margin-top:8px} +.form-grid .full{grid-column:1/-1} +.field label{display:block;font-size:.74rem;color:var(--text2);font-weight:600;margin-bottom:6px} +.field input,.field textarea{ + width:100%;background:var(--bg);border:1px solid var(--border);border-radius:12px; + color:var(--text);padding:10px 14px;font-size:.88rem;outline:none;font-family:inherit; +} +.field input:focus,.field textarea:focus{border-color:var(--blue);box-shadow:0 0 0 4px rgba(0,113,227,.12)} +.field textarea{resize:vertical;min-height:72px} +.field select{display:none} +.principle-chips{ + display:flex;flex-wrap:wrap;gap:8px;max-height:160px;overflow-y:auto; + padding:12px;background:var(--bg);border-radius:12px;border:1px solid var(--border); +} +.form-actions{display:flex;justify-content:flex-end;gap:12px;margin-top:20px} +.check-inline{display:flex;align-items:center;gap:10px;font-size:.88rem;cursor:pointer} +.check-inline input{width:18px;height:18px;accent-color:var(--blue)} +@media(max-width:600px){ .form-grid{grid-template-columns:1fr} } diff --git a/app.js b/app.js index b14d376..a230ce6 100644 --- a/app.js +++ b/app.js @@ -33,6 +33,52 @@ function fmtMoney(v) { return s + '$' + a.toFixed(2); } +// ═══════════════════════════════════════════════════════════ +// UI 元件:色塊分段(取代傳統下拉) +// ═══════════════════════════════════════════════════════════ +function mountChips(container, items, value, onChange, opts = {}) { + const cls = opts.sm ? 'chip sm' : 'chip'; + container.innerHTML = items.map(it => { + const tint = it.tint ? ` tint-${it.tint}` : ''; + const on = it.id === value ? ' on' : ''; + return ``; + }).join(''); + $$('button', container).forEach(btn => btn.addEventListener('click', () => { + const v = btn.dataset.v; + onChange(v); + $$('button', container).forEach(b => b.classList.toggle('on', b.dataset.v === v)); + })); +} +function mountTiles(container, items, value, onChange) { + container.innerHTML = items.map(it => { + const on = it.id === value ? ' on' : ''; + const tint = it.tint ? ` tint-${it.tint}` : ''; + return `
+
${escapeHtml(it.label)}
${it.sub ? `
${escapeHtml(it.sub)}
` : ''}
`; + }).join(''); + $$('.tile', container).forEach(el => { + const pick = () => { onChange(el.dataset.v); $$('.tile', container).forEach(t => t.classList.toggle('on', t.dataset.v === el.dataset.v)); }; + el.addEventListener('click', pick); + el.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pick(); } }); + }); +} + +// Mermaid 初始化(Apple 中性淺色主題) +function initMermaid() { + if (!window.mermaid || window._mermaidReady) return; + window._mermaidReady = true; + mermaid.initialize({ + startOnLoad: false, theme: 'neutral', securityLevel: 'loose', + fontFamily: '-apple-system, BlinkMacSystemFont, "PingFang TC", sans-serif', + }); +} +async function renderMermaid(container) { + initMermaid(); + const els = $$('.mermaid', container); + if (!els.length || !window.mermaid) return; + try { await mermaid.run({ nodes: els, suppressErrors: true }); } catch (_) {} +} + // ═══════════════════════════════════════════════════════════ // 輕量 Markdown 渲染(支援標題/清單/表格/引用/粗體/行內碼/[[wikilink]]) // ═══════════════════════════════════════════════════════════ @@ -85,7 +131,13 @@ function renderListBlock(lines) { function renderMarkdown(md) { md = String(md || '').replace(/\r\n/g, '\n'); const fences = []; - md = md.replace(/```[\s\S]*?```/g, (m) => { fences.push(m); return '\u0000F' + (fences.length - 1) + '\u0000'; }); + const fenceLangs = []; + md = md.replace(/```[\s\S]*?```/g, (m) => { + const lang = (m.match(/^```(\w+)/) || [])[1] || ''; + fenceLangs.push(lang.toLowerCase()); + fences.push(m); + return '\u0000F' + (fences.length - 1) + '\u0000'; + }); const lines = md.split('\n'); const blank = s => !s.trim(); let html = '', i = 0; @@ -93,7 +145,15 @@ function renderMarkdown(md) { const line = lines[i]; if (blank(line)) { i++; continue; } const fm = line.match(/^\u0000F(\d+)\u0000$/); - if (fm) { const code = fences[+fm[1]].replace(/^```[^\n]*\n?/, '').replace(/```\s*$/, ''); html += '
' + escapeHtml(code) + '
'; i++; continue; } + if (fm) { + const idx = +fm[1]; + const raw = fences[idx]; + const lang = fenceLangs[idx]; + const code = raw.replace(/^```[^\n]*\n?/, '').replace(/```\s*$/, ''); + if (lang === 'mermaid') html += `
${escapeHtml(code)}
`; + else html += '
' + escapeHtml(code) + '
'; + i++; continue; + } const h = line.match(/^(#{1,6})\s+(.*)$/); if (h) { const l = h[1].length; html += `${mdInline(h[2])}`; i++; continue; } if (/^(-{3,}|\*{3,}|_{3,})$/.test(line.trim())) { html += '
'; i++; continue; } @@ -159,6 +219,7 @@ async function openNote(kind, id) { let note = findLocalNote(kind, id); if (!note) { try { note = await api(`/api/note/${encodeURIComponent(kind)}/${encodeURIComponent(id)}`); } catch (e) { note = null; } } const finalNote = note || { body: `# 找不到這篇筆記\n(${kind} / ${id})` }; + finalNote.kind = kind; if (!inited.learn) { // 學習教材還沒初始化:暫存,切到 learn 後由 initLearn 渲染(避免被課綱總覽蓋掉) pendingNote = finalNote; @@ -180,25 +241,39 @@ function findLocalNote(kind, id) { function renderNote(note) { const content = $('#learnContent'); const fm = note.frontmatter || {}; + LEARN.currentNote = note; let tags = ''; if (fm.ticker) tags += `代號 ${escapeHtml([].concat(fm.ticker).join(' / '))}`; if (fm.sector) tags += `${escapeHtml(fm.sector)}`; if (fm.category) tags += `${escapeHtml(fm.category)}`; if (fm.date) tags += `${escapeHtml(fm.date)}`; if (Array.isArray(fm.aliases) && fm.aliases.length) tags += `別名 ${escapeHtml(fm.aliases.join(' · '))}`; + const kind = note.kind || LEARN.noteKind; + const center = (kind && note.id) ? `${kind}:${note.id}` : ''; content.innerHTML = - `← 返回` + + `
+ ← 返回 + ${center ? '' : ''} +
` + (tags ? `
${tags}
` : '') + `
${renderMarkdown(note.body || '')}
`; bindWlinks(content); + renderMermaid(content); $('#noteBack').addEventListener('click', () => showSection(LEARN.lastSection || 'overview')); + const gb = $('#noteGraphBtn'); + if (gb) gb.addEventListener('click', () => showGraph({ center, depth: 2 })); window.scrollTo({ top: 0 }); } // ═══════════════════════════════════════════════════════════ // 學習教材視圖 // ═══════════════════════════════════════════════════════════ -const LEARN = { lastSection: 'overview' }; +const LEARN = { lastSection: 'overview', graphFilter: 'curriculum', currentNote: null, noteKind: null }; +const GRAPH_KINDS = [ + { id: 'curriculum', label: '課程骨架', kinds: 'overview,principleMap,category,case,principle' }, + { id: 'terms', label: '名詞', kinds: 'term', includeIndex: '1' }, + { id: 'companies', label: '公司', kinds: 'company', includeIndex: '1' }, +]; function setLearnActive(section) { $$('#learnSide a').forEach(a => a.classList.toggle('active', a.dataset.section === section)); } @@ -226,6 +301,8 @@ async function initLearn() { 學習分類 ${(KB.categories || []).length} 案例講解 ${(KB.cases || []).length} 投資心法 ${(KB.principles || []).length} +
視覺化
+ 🔗 知識圖譜
速查
名詞 ${c.terms || 0} 公司 ${c.companies || 0} @@ -243,9 +320,10 @@ function showSection(section) { setLearnActive(section); const content = $('#learnContent'); if (!content) return; - if (section === 'overview') return renderNote(KB.overview || { body: '# 課綱總覽\n(尚無內容)' }); - if (section === 'principleMap') return renderNote(KB.principleMap || { body: '# 心法地圖\n(尚無內容)' }); + if (section === 'overview') return renderNote(Object.assign({ kind: 'overview' }, KB.overview || { body: '# 課綱總覽\n(尚無內容)' })); + if (section === 'principleMap') return renderNote(Object.assign({ kind: 'principleMap' }, KB.principleMap || { body: '# 心法地圖\n(尚無內容)' })); if (section === 'quiz') return renderQuiz(); + if (section === 'graph') return showGraph(); if (section === 'categories') return renderCardList('學習分類', KB.categories, 'category'); if (section === 'cases') return renderCardList('案例講解', KB.cases, 'case'); if (section === 'principles') return renderPrincipleList(); @@ -305,7 +383,82 @@ function renderGlossary(section) { window.scrollTo({ top: 0 }); } function renderQuiz() { - renderNote(KB.quiz || { body: '# 練習題庫\n(尚無內容)' }); + renderNote(Object.assign({ kind: 'quiz' }, KB.quiz || { body: '# 練習題庫\n(尚無內容)' })); +} + +// ── 知識圖譜(vis-network)── +let graphNetwork = null; +const GRAPH_LEGEND = [ + ['category', '分類', '#0071e3'], ['case', '案例', '#34c759'], ['principle', '心法', '#af52de'], + ['term', '名詞', '#ff9500'], ['company', '公司', '#5ac8fa'], ['episode', '單集', '#8e8e93'], +]; +async function showGraph(opts = {}) { + LEARN.lastSection = 'graph'; + setLearnActive('graph'); + const content = $('#learnContent'); + const filter = opts.filter || LEARN.graphFilter || 'curriculum'; + const center = opts.center || ''; + const depth = opts.depth || 2; + LEARN.graphFilter = filter; + content.innerHTML = ` +
知識圖譜
+
節點是筆記與概念,連線來自文內 [[連結]]。點一下節點可開啟該篇;拖曳平移、雙指或滾輪縮放。
+
+
+
載入圖譜中…
+
+
`; + mountChips($('#graphFilterChips'), GRAPH_KINDS.map(g => ({ id: g.id, label: g.label })), filter, v => showGraph({ filter: v })); + $('#graphLegend').innerHTML = GRAPH_LEGEND.map(([, lab, col]) => + `${lab}`).join(''); + const cfg = GRAPH_KINDS.find(g => g.id === filter) || GRAPH_KINDS[0]; + const qs = new URLSearchParams({ kinds: cfg.kinds, limit: 500 }); + if (cfg.includeIndex) qs.set('includeIndex', '1'); + if (center) { qs.set('center', center); qs.set('depth', String(depth)); } + try { + const data = await api('/api/graph?' + qs); + const el = $('#graphCanvas'); + el.innerHTML = ''; + if (!data.nodes || !data.nodes.length) { + el.innerHTML = '
此範圍沒有足夠的連結可繪製。
'; + return; + } + if (!window.vis) { el.innerHTML = '
圖譜元件載入失敗,請重新整理。
'; return; } + const nodes = new vis.DataSet(data.nodes.map(n => ({ + id: n.id, label: n.label, title: n.title, + color: { background: n.color, border: n.color, highlight: { background: n.color, border: '#1d1d1f' } }, + shape: n.shape === 'box' ? 'box' : 'dot', + font: { face: '-apple-system, BlinkMacSystemFont, sans-serif', size: 13, color: '#1d1d1f' }, + margin: 10, + }))); + const edges = new vis.DataSet(data.edges.map(e => ({ + from: e.from, to: e.to, arrows: { to: { scaleFactor: 0.45 } }, + color: { color: 'rgba(0,0,0,.12)', highlight: 'rgba(0,113,227,.45)' }, + smooth: { type: 'continuous', roundness: 0.2 }, + }))); + if (graphNetwork) { graphNetwork.destroy(); graphNetwork = null; } + graphNetwork = new vis.Network(el, { nodes, edges }, { + physics: { stabilization: { iterations: 100 }, barnesHut: { gravitationalConstant: -12000, springLength: 120 } }, + interaction: { hover: true, tooltipDelay: 80, navigationButtons: false }, + nodes: { borderWidth: 0, shadow: { enabled: true, size: 6, x: 0, y: 2, color: 'rgba(0,0,0,.08)' } }, + }); + graphNetwork.on('click', p => { + if (!p.nodes.length) return; + const nid = p.nodes[0]; + const node = data.nodes.find(n => n.id === nid); + if (!node) return; + const colon = nid.indexOf(':'); + if (colon < 0) return; + openNote(nid.slice(0, colon), nid.slice(colon + 1)); + }); + if (center && data.nodes.some(n => n.id === center)) { + graphNetwork.focus(center, { scale: 1.2, animation: { duration: 500, easingFunction: 'easeInOutQuad' } }); + } + $('#graphStat').textContent = `${data.nodes.length} 個節點 · ${data.edges.length} 條連線${center ? '(聚焦模式)' : ''}`; + } catch (e) { + $('#graphCanvas').innerHTML = `
圖譜載入失敗:${escapeHtml(e.message || '')}
`; + } + window.scrollTo({ top: 0 }); } // ═══════════════════════════════════════════════════════════ @@ -330,9 +483,9 @@ function drawLineChart(el, series, opts = {}) { const toX = i => padL + (i / (n - 1)) * plotW; const toY = v => padT + (1 - (v - yMin) / yRange) * plotH; let grid = ''; - for (let k = 0; k <= 5; k++) { const v = yMin + yRange * k / 5; const y = toY(v); grid += `${fmt(v)}`; } + for (let k = 0; k <= 5; k++) { const v = yMin + yRange * k / 5; const y = toY(v); grid += `${fmt(v)}`; } let xlab = ''; const xt = Math.min(5, n); - for (let k = 0; k < xt; k++) { const idx = Math.round(k * (n - 1) / (xt - 1)); xlab += `${(dates[idx] || '').slice(2, 7).replace('-', '/')}`; } + for (let k = 0; k < xt; k++) { const idx = Math.round(k * (n - 1) / (xt - 1)); xlab += `${(dates[idx] || '').slice(2, 7).replace('-', '/')}`; } let paths = '', dots = ''; series.forEach(s => { const d = s.points.slice(0, n).map((p, i) => `${i === 0 ? 'M' : 'L'}${toX(i).toFixed(1)},${toY(p.val).toFixed(1)}`).join(' '); @@ -342,7 +495,7 @@ function drawLineChart(el, series, opts = {}) { const legend = series.length > 1 ? `
${series.map(s => `${escapeHtml(s.name)}`).join('')}
` : ''; el.innerHTML = `${legend}
${grid}${xlab}${paths} - + ${dots}
`; @@ -643,23 +796,35 @@ function renderBacktestPane() { if (!STOCK.bt) STOCK.bt = { strategy: 'sma', range: '5y', params: {} }; pane.innerHTML = `
-
-
+
+ +
+
+
+ +
+
選好策略與期間,按「跑回測」。以還原股價、初始資金 $10,000 模擬。
`; - const drawParams = () => { - const s = BT_STRATS[$('#btStrat').value]; - $('#btParams').innerHTML = s.params.map(p => `
`).join(''); + mountChips($('#btStratChips'), Object.entries(BT_STRATS).map(([k, v]) => ({ id: k, label: v.label })), STOCK.bt.strategy, v => { + STOCK.bt.strategy = v; drawBtParams(); + }); + mountChips($('#btRangeChips'), BT_RANGES.map(r => ({ id: r[0], label: r[1] })), STOCK.bt.range, v => { STOCK.bt.range = v; }); + const drawBtParams = () => { + const s = BT_STRATS[STOCK.bt.strategy]; + const box = $('#btParams'); + if (!s.params.length) { box.innerHTML = ''; return; } + box.innerHTML = s.params.map(p => ` +
+
`).join(''); }; - $('#btStrat').addEventListener('change', drawParams); - drawParams(); + drawBtParams(); $('#btRun').addEventListener('click', runBacktestUI); } async function runBacktestUI() { - STOCK.bt.strategy = $('#btStrat').value; - STOCK.bt.range = $('#btRange').value; const params = {}; $$('#btParams input').forEach(i => params[i.dataset.pk] = i.value); STOCK.bt.params = params; const out = $('#btResult'); out.innerHTML = `
回測中…
`; @@ -812,25 +977,25 @@ function ensureTradeModal() { const div = document.createElement('div'); div.id = 'tradeModal'; div.className = 'view'; // reuse nothing; styled inline below - div.style.cssText = 'position:fixed;inset:0;z-index:600;background:rgba(4,8,14,.72);backdrop-filter:blur(3px);display:none;align-items:center;justify-content:center;padding:20px'; + div.style.cssText = 'position:fixed;inset:0;z-index:600;background:rgba(0,0,0,.35);backdrop-filter:blur(8px);display:none;align-items:center;justify-content:center;padding:20px'; div.innerHTML = ``; @@ -841,10 +1006,11 @@ function ensureTradeModal() { $('#tradeForm [name=mistake]').addEventListener('change', e => { $('#mistakeNoteWrap').style.display = e.target.checked ? '' : 'none'; }); $('#tradeForm').addEventListener('submit', submitTradeForm); } -function principleOptions(selected) { - const ps = (KB.principles || []); - return '' + ps.map(p => - ``).join(''); +function mountPrincipleChips(container, hiddenInput, selected) { + const items = [{ id: '', label: '不指定' }].concat((KB.principles || []).map(p => ({ + id: 'Emmy 投資心法#' + p.id, label: p.title.replace(/^原則[^:]+:/, '').slice(0, 24), + }))); + mountChips(container, items, selected || '', v => { hiddenInput.value = v; }, { sm: true }); } async function openTradeForm(trade) { ensureTradeModal(); @@ -854,14 +1020,30 @@ async function openTradeForm(trade) { const isEdit = !!(trade && trade.id); $('#tradeFormTitle').textContent = isEdit ? '編輯交易' : '新增交易'; f.dataset.id = isEdit ? trade.id : ''; - f.principle.innerHTML = principleOptions(trade ? trade.principle : ''); - f.mistake_note.innerHTML = principleOptions(trade ? trade.mistake_note : ''); + const dir = trade ? trade.direction : 'long'; + const kind = trade ? trade.kind : '投資'; + mountTiles($('#dirTiles'), [ + { id: 'long', label: '做多', sub: 'Long', tint: 'green' }, + { id: 'short', label: '做空', sub: 'Short', tint: 'red' }, + ], dir, v => { f.direction.value = v; }); + mountTiles($('#kindTiles'), [ + { id: '投資', label: '投資', sub: '基本面 · 趨勢' }, + { id: '交易', label: '交易', sub: '情緒 · 資金' }, + ], kind, v => { f.kind.value = v; }); + mountPrincipleChips($('#principleChips'), f.principle, trade ? trade.principle : ''); + mountPrincipleChips($('#mistakeChips'), f.mistake_note, trade ? trade.mistake_note : ''); if (trade) { - ['symbol', 'name', 'direction', 'kind', 'entry_date', 'entry_price', 'shares', 'entry_reason', 'exit_date', 'exit_price', 'exit_reason', 'note'].forEach(k => { if (f[k] != null && trade[k] != null) f[k].value = trade[k]; }); + ['symbol', 'name', 'entry_date', 'entry_price', 'shares', 'entry_reason', 'exit_date', 'exit_price', 'exit_reason', 'note'].forEach(k => { if (f[k] != null && trade[k] != null) f[k].value = trade[k]; }); + f.direction.value = trade.direction || 'long'; + f.kind.value = trade.kind || '投資'; f.mistake.checked = !!trade.mistake; f.principle.value = trade.principle || ''; f.mistake_note.value = trade.mistake_note || ''; $('#mistakeNoteWrap').style.display = trade.mistake ? '' : 'none'; + mountTiles($('#dirTiles'), [{ id: 'long', label: '做多', sub: 'Long', tint: 'green' }, { id: 'short', label: '做空', sub: 'Short', tint: 'red' }], f.direction.value, v => { f.direction.value = v; }); + mountTiles($('#kindTiles'), [{ id: '投資', label: '投資', sub: '基本面 · 趨勢' }, { id: '交易', label: '交易', sub: '情緒 · 資金' }], f.kind.value, v => { f.kind.value = v; }); + mountPrincipleChips($('#principleChips'), f.principle, f.principle.value); + mountPrincipleChips($('#mistakeChips'), f.mistake_note, f.mistake_note.value); } $('#tradeModal').style.display = 'flex'; } @@ -895,4 +1077,5 @@ async function submitTradeForm(e) { } // 啟動:依目前 hash 顯示視圖(macro 由 index.html 內聯負責載入) +initMermaid(); setView(parseHash()); diff --git a/index.html b/index.html index 96d4223..1d18d16 100644 --- a/index.html +++ b/index.html @@ -3,35 +3,38 @@ -Emmy 投資台 — 學習 · 財報健檢 · 交易復盤 +Emmy 投資台 — 學習 · 個股工具 · 交易復盤 +