diff --git a/app/frontend/m/css/features.css b/app/frontend/m/css/features.css
index 9d2c30a..455d1a2 100644
--- a/app/frontend/m/css/features.css
+++ b/app/frontend/m/css/features.css
@@ -2751,7 +2751,380 @@
.m-mentor-pill:active,
.m-chat-send:active,
.m-screener-drawer-row:active,
- .m-mentor-row:active {
+ .m-mentor-row:active,
+ .m-seg-opt:active,
+ .m-note-card:active,
+ .m-alert-row:active,
+ .m-daily-history-row:active,
+ .m-stock-picker-row:active {
transform: none;
}
}
+
+/* ============== 我的复盘(P3b) ============== */
+.m-num-strong { font-weight: 600; }
+.m-cell-text--empty { color: var(--text-tertiary); }
+
+/* 自选股:颜色圆点标记 */
+.m-stock--watch { flex-direction: row; align-items: center; gap: 8px; }
+.m-stock-copy { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; min-width: 0; }
+.m-stock-copy .m-stock-name { max-width: 100%; }
+.m-watch-dot { flex: 0 0 auto; width: 4px; height: 14px; border-radius: 999px; }
+.m-watch-dot--red { background: var(--market-up); }
+.m-watch-dot--blue { background: var(--action); }
+.m-watch-dot--green { background: var(--market-down); }
+.m-watch-dot--amber { background: var(--warning); }
+
+/* 交易动作徽标 */
+.m-trade-badge {
+ display: inline-flex;
+ align-items: center;
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 500;
+ white-space: nowrap;
+}
+.m-trade-badge--up { background: var(--market-up-soft); color: var(--market-up); }
+.m-trade-badge--down { background: var(--market-down-soft); color: var(--market-down); }
+.m-trade-badge--flat { background: var(--surface-muted); color: var(--text-secondary); }
+
+/* 详情 Sheet 内的通用正文块 */
+.m-detail-text {
+ padding: 10px 12px;
+ background: var(--surface);
+ border-radius: 8px;
+ font-size: 14px;
+ line-height: 1.6;
+ color: var(--text-primary);
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+/* 交易详情 */
+.m-trade-detail { padding: 0 12px; }
+.m-trade-detail-row {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ padding: 9px 12px;
+ border-bottom: 1px solid var(--border-subtle);
+ font-size: 13.5px;
+ gap: 12px;
+}
+.m-trade-detail-label { color: var(--text-secondary); flex: 0 0 auto; }
+.m-trade-detail-value { color: var(--text-primary); text-align: right; font-variant-numeric: tabular-nums; word-break: break-word; }
+.m-trade-detail-value.up { color: var(--market-up); }
+.m-trade-detail-value.down { color: var(--market-down); }
+
+/* 每日复盘 */
+.m-daily-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 4px 2px 8px;
+}
+.m-daily-date { font-size: 13px; color: var(--text-secondary); }
+.m-daily-edit {
+ appearance: none;
+ border: none;
+ background: transparent;
+ color: var(--action);
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+ padding: 6px;
+}
+.m-daily-edit:active { opacity: 0.7; }
+.m-daily-card {
+ margin-bottom: 12px;
+ padding: 12px;
+ border-radius: 8px;
+ background: var(--surface);
+}
+.m-daily-card-title { font-size: 15px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; }
+.m-daily-card-body { margin: 0; font-size: 14px; line-height: 1.6; color: var(--text-primary); white-space: pre-wrap; word-break: break-word; }
+.m-daily-delete {
+ margin: 8px 2px 16px;
+ background: transparent;
+ border: none;
+ color: var(--market-up);
+ font-size: 13px;
+ cursor: pointer;
+ padding: 8px 0;
+ height: auto;
+}
+.m-daily-delete:active { opacity: 0.7; }
+.m-daily-history { margin: 4px 0 16px; }
+.m-daily-history-list { display: flex; flex-direction: column; }
+.m-daily-history-row {
+ appearance: none;
+ border: none;
+ background: transparent;
+ border-bottom: 1px solid var(--border);
+ padding: 0 2px;
+ min-height: 52px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ text-align: left;
+ cursor: pointer;
+ -webkit-tap-highlight-color: transparent;
+}
+.m-daily-history-row:active { background: var(--surface-hover); }
+.m-daily-history-row.is-active { background: var(--surface-selected); }
+.m-daily-history-date { flex: 0 0 auto; font-size: 14px; font-weight: 500; color: var(--text-primary); font-variant-numeric: tabular-nums; }
+.m-daily-history-line { flex: 1 1 auto; min-width: 0; font-size: 12px; color: var(--text-tertiary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+
+/* 个股笔记 */
+.m-note-group { margin-bottom: 16px; }
+.m-note-group-head {
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+ height: 40px;
+ padding: 0 2px;
+}
+.m-note-group-name { font-size: 14px; font-weight: 600; color: var(--text-primary); }
+.m-note-group-code { font-size: 11.5px; color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
+.m-note-group-count { margin-left: auto; font-size: 12px; color: var(--text-tertiary); }
+.m-note-group-list { display: flex; flex-direction: column; gap: 8px; }
+.m-note-card {
+ appearance: none;
+ border: none;
+ text-align: left;
+ cursor: pointer;
+ -webkit-tap-highlight-color: transparent;
+ padding: 12px;
+ border-radius: 8px;
+ background: var(--surface);
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+.m-note-card:active { background: var(--surface-hover); transform: scale(0.99); }
+.m-note-card-date { font-size: 12px; color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
+.m-note-card-body {
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--text-primary);
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+.m-note-card-plan {
+ font-size: 12px;
+ color: var(--text-secondary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.m-note-detail-block { margin-bottom: 16px; }
+
+/* 提醒中心 */
+.m-alert-tabs { margin-bottom: 12px; }
+.m-alert-dot {
+ display: inline-block;
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--market-up);
+ margin-left: 4px;
+ vertical-align: middle;
+}
+.m-alert-list { display: flex; flex-direction: column; }
+.m-alert-row {
+ appearance: none;
+ border: none;
+ background: transparent;
+ border-bottom: 1px solid var(--border);
+ padding: 12px 4px;
+ min-height: 64px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ text-align: left;
+ cursor: pointer;
+ -webkit-tap-highlight-color: transparent;
+ width: 100%;
+}
+.m-alert-row:active { background: var(--surface-hover); }
+.m-alert-row-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
+.m-alert-row-top { display: flex; align-items: center; gap: 8px; }
+.m-alert-row-date { font-size: 12px; color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
+.m-alert-row-title { font-size: 14px; color: var(--text-primary); display: flex; align-items: center; gap: 6px; }
+.m-alert-row.is-unread .m-alert-row-title { font-weight: 600; }
+.m-alert-row.is-read .m-alert-row-title { color: var(--text-secondary); font-weight: 400; }
+.m-alert-unread-dot { flex: 0 0 auto; width: 6px; height: 6px; border-radius: 50%; background: var(--action); }
+.m-alert-row-summary {
+ font-size: 12px;
+ color: var(--text-tertiary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.m-alert-row-chevron { flex: 0 0 auto; color: var(--text-tertiary); display: inline-flex; }
+.m-alert-kind {
+ display: inline-flex;
+ align-items: center;
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 11.5px;
+ font-weight: 500;
+ white-space: nowrap;
+}
+.m-alert-kind--action { background: var(--action-soft); color: var(--action); }
+.m-alert-kind--warn { background: var(--warning-soft); color: var(--warning); }
+.m-alert-kind--down { background: var(--market-down-soft); color: var(--market-down); }
+.m-alert-detail { padding: 0 4px; }
+.m-alert-detail-title { margin: 12px 0 4px; font-size: 16px; font-weight: 600; color: var(--text-primary); }
+.m-alert-detail-meta { font-size: 12px; color: var(--text-tertiary); margin-bottom: 12px; }
+.m-alert-stock {
+ appearance: none;
+ border: 1px solid var(--border-strong);
+ background: var(--surface);
+ color: var(--action);
+ border-radius: 999px;
+ padding: 6px 14px;
+ font-size: 13px;
+ cursor: pointer;
+ margin: 8px 0;
+ -webkit-tap-highlight-color: transparent;
+}
+.m-alert-stock:active { background: var(--action-soft); }
+
+/* 表单控件族 */
+.m-form-field textarea {
+ min-height: 88px;
+ padding: 10px 12px;
+ border: 1px solid var(--border-strong);
+ border-radius: 8px;
+ background: var(--surface);
+ color: var(--text-primary);
+ font-size: 14px;
+ line-height: 1.5;
+ font-family: inherit;
+ resize: none;
+}
+.m-form-field textarea:focus { outline: none; border-color: var(--action); }
+.m-form-field input[type="date"] { padding-right: 36px; }
+.m-form-field .m-field-req {
+ font-style: normal;
+ color: var(--market-up);
+ margin-left: 2px;
+}
+.m-field-textarea { display: flex; flex-direction: column; gap: 2px; }
+.m-form-field .m-field-counter { font-size: 11.5px; color: var(--text-tertiary); text-align: right; }
+.m-form-field .m-field-error { font-size: 12px; color: var(--market-up); }
+.m-form-field.is-invalid input,
+.m-form-field.is-invalid textarea { border-color: var(--market-up); }
+.m-input-unit { position: relative; display: flex; align-items: center; }
+.m-input-unit input { width: 100%; padding-right: 40px; }
+.m-form-field .m-input-unit-suffix {
+ position: absolute;
+ right: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ font-size: 12px;
+ color: var(--text-tertiary);
+ pointer-events: none;
+}
+.m-input-date { position: relative; display: flex; align-items: center; }
+.m-input-date input { width: 100%; }
+.m-input-date input::-webkit-calendar-picker-indicator { opacity: 0; position: absolute; right: 0; width: 100%; height: 100%; cursor: pointer; }
+.m-form-field .m-input-date-icon {
+ position: absolute;
+ right: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ color: var(--text-tertiary);
+ display: inline-flex;
+ pointer-events: none;
+}
+.m-seg { display: flex; flex-wrap: wrap; gap: 8px; }
+.m-seg-opt {
+ height: 36px;
+ padding: 0 14px;
+ border-radius: 999px;
+ border: 1px solid var(--border-strong);
+ background: transparent;
+ color: var(--text-secondary);
+ font-size: 13px;
+ cursor: pointer;
+ -webkit-tap-highlight-color: transparent;
+}
+.m-seg-opt:active { transform: scale(0.97); }
+.m-seg-opt.active {
+ background: var(--action-soft);
+ border-color: var(--action);
+ color: var(--action);
+ font-weight: 600;
+}
+.m-stock-picker { position: relative; }
+.m-stock-picker-results {
+ position: absolute;
+ top: calc(100% + 4px);
+ left: 0;
+ right: 0;
+ background: var(--surface);
+ border-radius: 8px;
+ box-shadow: var(--elevation-float);
+ max-height: 240px;
+ overflow-y: auto;
+ z-index: 10;
+}
+.m-stock-picker-row {
+ appearance: none;
+ border: none;
+ background: transparent;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 0 12px;
+ height: 44px;
+ cursor: pointer;
+ width: 100%;
+ text-align: left;
+ -webkit-tap-highlight-color: transparent;
+}
+.m-stock-picker-row:active { background: var(--surface-hover); }
+.m-stock-picker-row-name { font-size: 14px; font-weight: 600; color: var(--text-primary); }
+.m-stock-picker-row-sub { font-size: 11.5px; color: var(--text-tertiary); flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.m-stock-picker-row-code { font-size: 12px; color: var(--text-secondary); font-variant-numeric: tabular-nums; }
+.m-stock-picker-empty { padding: 12px; font-size: 13px; color: var(--text-tertiary); text-align: center; }
+.m-stock-chip {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ height: 44px;
+ padding: 0 12px;
+ border: 1px solid var(--border-strong);
+ border-radius: 8px;
+ background: var(--surface);
+}
+.m-stock-chip-name { font-size: 14px; font-weight: 600; color: var(--text-primary); }
+.m-stock-chip-code { font-size: 12px; color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
+.m-stock-chip-clear {
+ margin-left: auto;
+ color: var(--text-tertiary);
+ background: none;
+ border: none;
+ cursor: pointer;
+ display: inline-flex;
+ padding: 4px;
+ -webkit-tap-highlight-color: transparent;
+}
+.m-stock-chip-clear:active { color: var(--text-primary); }
+
+/* 全屏表单页 */
+.m-form-body { padding: 12px 12px 16px; }
+.m-form-bar {
+ flex: 0 0 auto;
+ display: flex;
+ gap: 8px;
+ padding: 8px 12px calc(8px + var(--mobile-safe-bottom));
+ background: var(--surface);
+ border-top: 1px solid var(--border);
+ transform: translateY(calc(-1 * var(--m-keyboard-inset, 0px)));
+}
diff --git a/app/frontend/m/js/pages.js b/app/frontend/m/js/pages.js
index ece9f5e..251a665 100644
--- a/app/frontend/m/js/pages.js
+++ b/app/frontend/m/js/pages.js
@@ -83,6 +83,13 @@
send: '',
square: '',
"alert-triangle": '',
+ check: '',
+ "check-check": '',
+ star: '',
+ "scroll-text": '',
+ "calendar-check": '',
+ "sticky-note": '',
+ bell: '',
};
function icon(name, size) {
@@ -168,6 +175,17 @@
loading: false,
refreshing: false,
},
+ review: {
+ subKey: "",
+ watchlist: null,
+ trades: null,
+ notes: null,
+ dailyItems: null,
+ alerts: null,
+ alertStatus: "all",
+ form: null,
+ dirty: false,
+ },
};
let sheetToken = 0;
@@ -3044,6 +3062,11 @@
"tools/tracking": setupTrackingPage,
"tools/mentor": setupChatPage,
"assistant/chat": setupChatPage,
+ "review/watchlist": setupReviewPage,
+ "review/trades": setupReviewPage,
+ "review/daily": setupReviewPage,
+ "review/notes": setupReviewPage,
+ "review/alerts": setupReviewPage,
};
const COMPLEX_LOADERS = {
@@ -3057,6 +3080,11 @@
"tools/tracking": loadTracking,
"tools/mentor": loadChat,
"assistant/chat": loadChat,
+ "review/watchlist": loadReview,
+ "review/trades": loadReview,
+ "review/daily": loadReview,
+ "review/notes": loadReview,
+ "review/alerts": loadReview,
};
function isComplexPage(key) {
@@ -3351,6 +3379,13 @@
detail.watchlist = adding;
updateStarButton();
showToast(adding ? "已加入自选" : "已移出自选");
+ // 自选股列表页在背后:同步移除该行,避免关 Sheet 后仍显示已移除标的
+ if (!adding && state.review && state.review.watchlist) {
+ state.review.watchlist.items = (state.review.watchlist.items || []).filter(function (it) {
+ return String(it.code) !== String(code);
+ });
+ renderReviewWatchlist();
+ }
}).catch(function () {
updateStarButton();
showToast("操作失败,请重试");
@@ -3571,11 +3606,1192 @@
"";
}
+ /* ============================================================== 我的复盘(P3b) ============================================================== */
+
+ const TRADE_ACTION_OPTIONS = [
+ { key: "buy", label: "买入", tone: "up" },
+ { key: "add", label: "加仓", tone: "up" },
+ { key: "sell", label: "卖出", tone: "down" },
+ { key: "trim", label: "减仓", tone: "down" },
+ { key: "watch", label: "观察", tone: "flat" }
+ ];
+ const TRADE_EMOTION_OPTIONS = [
+ { key: "calm", label: "平静" },
+ { key: "confident", label: "笃定" },
+ { key: "hesitant", label: "犹豫" },
+ { key: "anxious", label: "焦虑" },
+ { key: "impulsive", label: "冲动" }
+ ];
+
+ function reviewSubKey(key) {
+ const parts = String(key || "").split("/");
+ return parts[1] || "";
+ }
+
+ function reviewTitle(key) {
+ const map = {
+ watchlist: "自选股",
+ trades: "交易日志",
+ daily: "每日复盘",
+ notes: "个股笔记",
+ alerts: "提醒中心"
+ };
+ return map[reviewSubKey(key)] || "我的复盘";
+ }
+
+ function headerActionButton(dataAttr, iconName, ariaLabel) {
+ return '";
+ }
+
+ function setupReviewPage(key) {
+ const subKey = reviewSubKey(key);
+ state.key = key;
+ state.requestedDate = todayString();
+ state.sort = { key: "", dir: null };
+ state.sortTable = { cols: null, reapply: null };
+ state.detail = null;
+ state.review.subKey = subKey;
+ state.review.form = null;
+ state.review.dirty = false;
+ state.review.watchlist = null;
+ state.review.trades = null;
+ state.review.notes = null;
+ state.review.dailyItems = null;
+ state.review.alerts = null;
+ state.review.alertStatus = "all";
+
+ document.getElementById("m-view").classList.add("m-view-feature");
+ const title = reviewTitle(key);
+ let actions = "";
+ if (subKey === "trades") {
+ actions = headerActionButton("review-add-trade", "plus", "新增交易");
+ } else if (subKey === "notes") {
+ actions = headerActionButton("review-add-note", "plus", "新增笔记");
+ } else if (subKey === "alerts") {
+ actions =
+ headerActionButton("review-add-alert", "plus", "新增提醒") +
+ '";
+ }
+ if (subKey === "daily") {
+ updateHeader(title, state.requestedDate);
+ } else {
+ global.MobileRouter.updateHeader({ title: title, back: true, actions: actions });
+ }
+ document.getElementById("m-view").innerHTML = complexFrame(key, complexScroll(skeletonHtml(6)));
+ }
+
+ function loadReview() {
+ const subKey = state.review.subKey;
+ if (subKey === "watchlist") loadReviewWatchlist();
+ else if (subKey === "trades") loadReviewTrades();
+ else if (subKey === "daily") loadReviewDaily();
+ else if (subKey === "notes") loadReviewNotes();
+ else if (subKey === "alerts") loadReviewAlerts();
+ }
+
+ function reviewState(kind) {
+ return '
' + escapeHtml(kind) + "加载失败
" +
+ '
';
+ }
+
+ /* ----- 自选股 ----- */
+
+ function loadReviewWatchlist() {
+ const seq = nextSeq();
+ global.MobileAPI.request("/api/watchlist?trade_date=" + encodeURIComponent(todayString())).then(function (payload) {
+ if (seq !== state.seq) return;
+ state.review.watchlist = payload || { items: [] };
+ renderReviewWatchlist();
+ }).catch(function (error) {
+ if (seq !== state.seq) return;
+ const scroll = document.getElementById("m-scroll");
+ if (scroll) scroll.innerHTML = reviewState("自选股");
+ });
+ }
+
+ function watchDotColor(color) {
+ return { red: true, blue: true, green: true, amber: true }[color] ? color : "red";
+ }
+
+ function watchStockCell(row) {
+ const color = watchDotColor(row.color);
+ return '' +
+ '' +
+ '' +
+ '' + escapeHtml(row.name || "--") + "" +
+ '' + escapeHtml(row.code || "") + "" +
+ "";
+ }
+
+ function renderReviewWatchlist() {
+ const scroll = document.getElementById("m-scroll");
+ if (!scroll) return;
+ const data = state.review.watchlist || { items: [] };
+ const rows = data.items || [];
+ if (!rows.length) {
+ scroll.innerHTML = '' +
+ '
' + icon("star", 26) + "" +
+ "
还没有自选股
" +
+ "
在任意股票详情点星形加入" +
+ "
";
+ return;
+ }
+ const cols = {
+ frozenColumns: [{ key: "stock", label: "股票", type: "stock", width: 112 }],
+ primaryColumns: [
+ { key: "change", label: "当日涨幅", type: "change" },
+ { key: "attention_score", label: "关注分", type: "score" }
+ ],
+ scrollColumns: [
+ { key: "sector", label: "板块", type: "text" },
+ { key: "return_5d", label: "5日涨幅", type: "change" },
+ { key: "remark", label: "跟踪备注", type: "text" }
+ ]
+ };
+ state.sortTable = { cols: cols, reapply: renderReviewWatchlist };
+ const sorted = sortedRows(rows, cols);
+ scroll.innerHTML = '' + buildReviewTable(cols, sorted, {
+ noSort: true,
+ cellRenderer: function (col, row) {
+ if (col.key === "stock") return watchStockCell(row);
+ if (col.key === "attention_score") {
+ if (row.attention_score == null || row.attention_score === "") return '';
+ return '' + formatNumber(number(row.attention_score), 1) + "";
+ }
+ if (col.key === "remark") {
+ const text = String(row.remark == null ? "" : row.remark);
+ if (!text) return '--';
+ return '' + escapeHtml(text) + "";
+ }
+ return null;
+ }
+ }) + "
";
+ }
+
+ /* ----- 交易日志 ----- */
+
+ function loadReviewTrades() {
+ const seq = nextSeq();
+ global.MobileAPI.request("/api/trades").then(function (payload) {
+ if (seq !== state.seq) return;
+ state.review.trades = payload || { items: [], summary: {} };
+ renderReviewTrades();
+ }).catch(function (error) {
+ if (seq !== state.seq) return;
+ const scroll = document.getElementById("m-scroll");
+ if (scroll) scroll.innerHTML = reviewState("交易日志");
+ });
+ }
+
+ function tradeActionBadge(action) {
+ const opt = TRADE_ACTION_OPTIONS.find(function (o) { return o.key === action; });
+ const label = opt ? opt.label : (action || "--");
+ const tone = opt ? opt.tone : "flat";
+ return '' + escapeHtml(label) + "";
+ }
+
+ function renderReviewTrades() {
+ const scroll = document.getElementById("m-scroll");
+ if (!scroll) return;
+ const data = state.review.trades || { items: [], summary: {} };
+ const items = data.items || [];
+ const summary = data.summary || {};
+ if (!items.length) {
+ scroll.innerHTML = '' +
+ '
' + icon("scroll-text", 26) + "" +
+ "
还没有交易记录
" +
+ "
记下每一次买卖,回头复盘更有据" +
+ '
' +
+ "
";
+ return;
+ }
+ const pnlAmount = summary.pnl_amount;
+ const avgPos = summary.average_position;
+ const strip = renderStrip5([
+ { label: "记录", value: String(summary.total != null ? summary.total : items.length) },
+ { label: "已实现", value: String(summary.realized != null ? summary.realized : 0) },
+ { label: "胜率", value: summary.win_rate != null ? summary.win_rate + "%" : "--" },
+ { label: "累计盈亏", value: pnlAmount == null ? "--" : (pnlAmount > 0 ? "+" : "") + formatNumber(pnlAmount, 2), tone: pnlAmount == null ? "" : (pnlAmount > 0 ? "up" : (pnlAmount < 0 ? "down" : "")) },
+ { label: "平均仓位", value: avgPos == null ? "--" : avgPos + "%" }
+ ]);
+
+ const cols = {
+ frozenColumns: [{ key: "stock", label: "股票", type: "stock", width: 96 }],
+ primaryColumns: [
+ { key: "action_label", label: "动作", type: "text" },
+ { key: "pnl_pct", label: "盈亏%", type: "change" }
+ ],
+ scrollColumns: [
+ { key: "trade_date", label: "日期", type: "text" },
+ { key: "position_pct", label: "仓位%", type: "rate" },
+ { key: "pnl_amount", label: "盈亏额", type: "change" },
+ { key: "emotion_label", label: "情绪", type: "text" }
+ ]
+ };
+ const rows = items.map(function (it) {
+ return {
+ code: it.code,
+ name: it.name,
+ action: it.action,
+ action_label: it.action_label,
+ pnl_pct: it.pnl_pct,
+ trade_date: displayCompactDate(it.trade_date),
+ position_pct: it.position_pct,
+ pnl_amount: it.pnl_amount,
+ emotion_label: it.emotion_label,
+ id: it.id,
+ _raw: it
+ };
+ });
+ state.sortTable = { cols: cols, reapply: renderReviewTrades };
+ const sorted = sortedRows(rows, cols);
+ const table = buildReviewTable(cols, sorted, {
+ cellRenderer: function (col, row) {
+ if (col.key === "action_label") return tradeActionBadge(row.action);
+ if (col.key === "pnl_pct") {
+ if (row.pnl_pct == null || row.pnl_pct === "") return '--';
+ return '' + (number(row.pnl_pct) > 0 ? "+" : "") + formatNumber(number(row.pnl_pct), 2) + "";
+ }
+ if (col.key === "pnl_amount") {
+ if (row.pnl_amount == null || row.pnl_amount === "") return '--';
+ return '' + (number(row.pnl_amount) > 0 ? "+" : "") + formatNumber(number(row.pnl_amount), 2) + "";
+ }
+ if (col.key === "position_pct") {
+ if (row.position_pct == null || row.position_pct === "") return '--';
+ return '' + formatNumber(number(row.position_pct), 1) + "";
+ }
+ return null;
+ },
+ rowAttrs: function (row) {
+ return ' data-trade-id="' + escapeHtml(String(row.id || "")) + '"';
+ }
+ });
+ scroll.innerHTML = strip + '' + table + "
";
+ }
+
+ function openTradeDetail(tradeId) {
+ const data = state.review.trades || { items: [] };
+ const item = (data.items || []).find(function (it) { return String(it.id) === String(tradeId); });
+ if (!item) return;
+ openSheet(
+ '交易详情
' +
+ '" +
+ '' + tradeDetailBody(item) + "
",
+ { detail: false }
+ );
+ }
+
+ function tradeDetailBody(item) {
+ const rows = [
+ ["股票", (item.name || "--") + " " + (item.code || "")],
+ ["日期", displayCompactDate(item.trade_date)],
+ ["动作", item.action_label || "--"],
+ ["价格", item.price != null ? formatNumber(number(item.price), 2) : "--"],
+ ["数量", item.quantity != null ? intOrDash(item.quantity) : "--"],
+ ["仓位", item.position_pct != null ? formatNumber(number(item.position_pct), 1) + "%" : "--"],
+ ["盈亏额", item.pnl_amount == null ? "--" : (number(item.pnl_amount) > 0 ? "+" : "") + formatNumber(number(item.pnl_amount), 2)],
+ ["盈亏%", item.pnl_pct == null ? "--" : (number(item.pnl_pct) > 0 ? "+" : "") + formatNumber(number(item.pnl_pct), 2) + "%"],
+ ["情绪", item.emotion_label || "--"],
+ ["标签", (item.tags && item.tags.length) ? item.tags.join("、") : "--"]
+ ];
+ const toneRow = item.pnl_amount == null ? "" : (number(item.pnl_amount) > 0 ? "up" : number(item.pnl_amount) < 0 ? "down" : "");
+ const grid = rows.map(function (pair) {
+ const isPnl = pair[0] === "盈亏额";
+ return '' + escapeHtml(pair[0]) + "" +
+ '' + escapeHtml(pair[1]) + "
";
+ }).join("");
+ const block = function (title, text) {
+ return '' + escapeHtml(title) + "
" +
+ '
' + escapeHtml(text || "尚未填写") + "
";
+ };
+ return '' + grid + "
" +
+ '";
+ }
+
+ function deleteTrade(tradeId) {
+ openConfirmSheet("删除交易记录", "删除后无法恢复,确定删除这条交易记录吗?", {
+ danger: true,
+ confirmLabel: "删除",
+ onConfirm: function () {
+ global.MobileAPI.request("/api/trades/" + encodeURIComponent(tradeId), "DELETE").then(function (payload) {
+ state.review.trades = { items: payload.items || [], summary: payload.summary || {} };
+ closeSheet();
+ renderReviewTrades();
+ showToast("交易记录已删除");
+ }).catch(function (error) {
+ showToast(error && error.message ? error.message : "删除失败");
+ });
+ }
+ });
+ }
+
+ /* ----- 每日复盘 ----- */
+
+ function loadReviewDaily() {
+ const seq = nextSeq();
+ global.MobileAPI.request("/api/notes?scope=daily").then(function (payload) {
+ if (seq !== state.seq) return;
+ state.review.dailyItems = (payload && payload.items) || [];
+ updateHeader("每日复盘", state.requestedDate);
+ renderReviewDaily();
+ }).catch(function (error) {
+ if (seq !== state.seq) return;
+ const scroll = document.getElementById("m-scroll");
+ if (scroll) scroll.innerHTML = reviewState("每日复盘");
+ });
+ }
+
+ function dailyNoteFor(dateStr) {
+ const items = state.review.dailyItems || [];
+ const compact = String(dateStr || "").replaceAll("-", "");
+ return items.find(function (it) { return String(it.trade_date || "").replaceAll("-", "") === compact; }) || null;
+ }
+
+ function renderReviewDaily() {
+ const scroll = document.getElementById("m-scroll");
+ if (!scroll) return;
+ const dateStr = state.requestedDate || todayString();
+ const note = dailyNoteFor(dateStr);
+ const items = state.review.dailyItems || [];
+
+ const dateLine = '' +
+ '' + escapeHtml(displayCompactDate(dateStr)) + "" +
+ '" +
+ "
";
+
+ if (!note) {
+ const history = dailyHistoryBlock(items, dateStr);
+ scroll.innerHTML = '' +
+ '
' + icon("calendar-check", 26) + "" +
+ "
今日还未复盘
" +
+ '
花几分钟回顾今天的盘面与操作' +
+ '
' +
+ "
" + history;
+ return;
+ }
+
+ const card = function (title, text) {
+ return '' +
+ '
' + escapeHtml(title) + "
" +
+ '
' + escapeHtml(text || "尚未填写") + "
" +
+ "
";
+ };
+
+ const history = dailyHistoryBlock(items, dateStr);
+ scroll.innerHTML = dateLine +
+ card("今日盘面", note.summary) +
+ card("做对做错", note.content) +
+ card("明日计划", note.plan) +
+ '' +
+ history;
+ }
+
+ function dailyHistoryBlock(items, currentDate) {
+ const sorted = (items || []).slice().sort(function (a, b) {
+ return String(b.trade_date || "").localeCompare(String(a.trade_date || ""));
+ });
+ if (!sorted.length) return "";
+ const rows = sorted.map(function (it) {
+ const active = String(it.trade_date || "").replaceAll("-", "") === String(currentDate || "").replaceAll("-", "");
+ const oneLine = String(it.summary || "").trim() || "(无盘面摘要)";
+ return '";
+ }).join("");
+ return '' +
+ '
历史复盘
' +
+ '
' + rows + "
" +
+ "
";
+ }
+
+ function deleteDailyNote(noteId) {
+ openConfirmSheet("删除此日复盘", "删除后无法恢复,确定删除这一天的复盘吗?", {
+ danger: true,
+ confirmLabel: "删除",
+ onConfirm: function () {
+ global.MobileAPI.request("/api/notes/" + encodeURIComponent(noteId), "DELETE").then(function () {
+ state.review.dailyItems = (state.review.dailyItems || []).filter(function (it) { return String(it.id) !== String(noteId); });
+ closeSheet();
+ renderReviewDaily();
+ showToast("复盘已删除");
+ }).catch(function (error) {
+ showToast(error && error.message ? error.message : "删除失败");
+ });
+ }
+ });
+ }
+
+ /* ----- 个股笔记 ----- */
+
+ function loadReviewNotes() {
+ const seq = nextSeq();
+ global.MobileAPI.request("/api/notes?scope=stock").then(function (payload) {
+ if (seq !== state.seq) return;
+ state.review.notes = (payload && payload.items) || [];
+ renderReviewNotes();
+ }).catch(function (error) {
+ if (seq !== state.seq) return;
+ const scroll = document.getElementById("m-scroll");
+ if (scroll) scroll.innerHTML = reviewState("个股笔记");
+ });
+ }
+
+ function groupNotesByStock(items) {
+ const groups = {};
+ (items || []).forEach(function (it) {
+ const key = it.code || "_";
+ if (!groups[key]) groups[key] = { code: it.code, name: it.stock_name || it.code || "--", notes: [] };
+ groups[key].notes.push(it);
+ });
+ return Object.keys(groups).map(function (k) { return groups[k]; });
+ }
+
+ function renderReviewNotes() {
+ const scroll = document.getElementById("m-scroll");
+ if (!scroll) return;
+ const items = state.review.notes || [];
+ if (!items.length) {
+ scroll.innerHTML = '' +
+ '
' + icon("sticky-note", 26) + "" +
+ "
还没有个股笔记
" +
+ "
把对个股的观察与计划记下来" +
+ '
' +
+ "
";
+ return;
+ }
+ const groups = groupNotesByStock(items);
+ scroll.innerHTML = groups.map(function (g) {
+ const cards = g.notes.map(function (it) {
+ const plan = it.plan ? '计划:' + escapeHtml(it.plan) + "" : "";
+ return '";
+ }).join("");
+ return '' +
+ '
' +
+ '' + escapeHtml(g.name) + "" +
+ '' + escapeHtml(g.code || "") + "" +
+ '' + g.notes.length + " 条" +
+ "
" +
+ '
' + cards + "
" +
+ "
";
+ }).join("");
+ }
+
+ function openNoteDetail(noteId) {
+ const item = (state.review.notes || []).find(function (it) { return String(it.id) === String(noteId); });
+ if (!item) return;
+ openSheet(
+ '' + escapeHtml((item.stock_name || item.code || "笔记") + " · " + displayCompactDate(item.trade_date)) + "
" +
+ '" +
+ '' +
+ '
笔记
' + escapeHtml(item.content || "尚未填写") + "
" +
+ '
计划
' + escapeHtml(item.plan || "尚未填写") + "
" +
+ '
' +
+ '' +
+ "
",
+ { detail: false }
+ );
+ }
+
+ function deleteNote(noteId) {
+ openConfirmSheet("删除笔记", "删除后无法恢复,确定删除这条笔记吗?", {
+ danger: true,
+ confirmLabel: "删除",
+ onConfirm: function () {
+ global.MobileAPI.request("/api/notes/" + encodeURIComponent(noteId), "DELETE").then(function () {
+ state.review.notes = (state.review.notes || []).filter(function (it) { return String(it.id) !== String(noteId); });
+ closeSheet();
+ renderReviewNotes();
+ showToast("笔记已删除");
+ }).catch(function (error) {
+ showToast(error && error.message ? error.message : "删除失败");
+ });
+ }
+ });
+ }
+
+ /* ----- 提醒中心 ----- */
+
+ function loadReviewAlerts() {
+ const seq = nextSeq();
+ const status = state.review.alertStatus;
+ global.MobileAPI.request("/api/alerts?status=" + status + "&as_of=" + encodeURIComponent(todayString())).then(function (payload) {
+ if (seq !== state.seq) return;
+ state.review.alerts = payload || { items: [], unread_count: 0 };
+ renderReviewAlerts();
+ }).catch(function (error) {
+ if (seq !== state.seq) return;
+ const scroll = document.getElementById("m-scroll");
+ if (scroll) scroll.innerHTML = reviewState("提醒中心");
+ });
+ }
+
+ function alertKindLabel(kind) {
+ if (kind === "manual") return "自定提醒";
+ if (kind === "strategy_t5") return "跟踪完成";
+ return "策略反馈";
+ }
+
+ function alertKindBadge(kind) {
+ const tone = kind === "manual" ? "action" : kind === "strategy_t5" ? "down" : "warn";
+ return '' + escapeHtml(alertKindLabel(kind)) + "";
+ }
+
+ function renderReviewAlerts() {
+ const scroll = document.getElementById("m-scroll");
+ if (!scroll) return;
+ const data = state.review.alerts || { items: [], unread_count: 0 };
+ const items = data.items || [];
+ const unreadCount = number(data.unread_count);
+
+ const readAllBtn = document.querySelector("[data-review-read-all]");
+ if (readAllBtn) readAllBtn.hidden = unreadCount <= 0;
+
+ const filterTabs = ["all", "unread"].map(function (f) {
+ const active = state.review.alertStatus === f;
+ const label = f === "all" ? "全部" : "未读";
+ const dot = f === "unread" && unreadCount > 0 ? '' : "";
+ return '";
+ }).join("");
+ const tabs = '' + filterTabs + "
";
+
+ if (!items.length) {
+ const emptyText = state.review.alertStatus === "unread" ? "没有未读提醒" : "暂无提醒";
+ scroll.innerHTML = tabs + '' +
+ '
' + icon("bell", 26) + "" +
+ "
" + escapeHtml(emptyText) + "
" +
+ (state.review.alertStatus === "all" ? '
重要节点给自己留个备忘' : "") +
+ "
";
+ return;
+ }
+
+ const rows = items.map(function (it) {
+ const upcoming = !it.due;
+ const unread = !it.is_read;
+ const dot = unread && it.due ? '' : "";
+ const dateText = displayCompactDate(it.available_date);
+ return '";
+ }).join("");
+
+ scroll.innerHTML = tabs + '' + rows + "
";
+ }
+
+ function openAlertDetail(alertId) {
+ const item = (state.review.alerts && state.review.alerts.items || []).find(function (it) { return String(it.id) === String(alertId); });
+ if (!item) return;
+ const upcoming = !item.due;
+ const canRead = !item.is_read && !upcoming;
+ openSheet(
+ '提醒详情
' +
+ '" +
+ '' +
+ '
' +
+ alertKindBadge(item.kind) +
+ '
' + escapeHtml(item.title) + "
" +
+ '
' + escapeHtml(displayCompactDate(item.available_date)) + (upcoming ? "(未到期)" : "") + "
" +
+ (item.content ? '
' + escapeHtml(item.content) + "
" : "") +
+ (item.code ? '
" : "") +
+ "
" +
+ '
' +
+ (canRead ? '' : "") +
+ '' +
+ "
",
+ { detail: false }
+ );
+ }
+
+ function markAlertRead(alertId) {
+ global.MobileAPI.request("/api/alerts/" + encodeURIComponent(alertId) + "/read", "POST", {}).then(function (payload) {
+ state.review.alerts = payload || state.review.alerts;
+ closeSheet();
+ renderReviewAlerts();
+ showToast("已标为已读");
+ }).catch(function (error) {
+ showToast(error && error.message ? error.message : "操作失败");
+ });
+ }
+
+ function deleteAlert(alertId) {
+ openConfirmSheet("删除提醒", "删除后无法恢复,确定删除这条提醒吗?", {
+ danger: true,
+ confirmLabel: "删除",
+ onConfirm: function () {
+ global.MobileAPI.request("/api/alerts/" + encodeURIComponent(alertId), "DELETE").then(function (payload) {
+ state.review.alerts = payload || state.review.alerts;
+ closeSheet();
+ renderReviewAlerts();
+ showToast("提醒已删除");
+ }).catch(function (error) {
+ showToast(error && error.message ? error.message : "删除失败");
+ });
+ }
+ });
+ }
+
+ function markAllAlertsRead() {
+ global.MobileAPI.request("/api/alerts/read-all", "POST", { as_of: todayString() }).then(function (payload) {
+ state.review.alerts = payload || state.review.alerts;
+ renderReviewAlerts();
+ showToast("全部已读");
+ }).catch(function (error) {
+ showToast(error && error.message ? error.message : "操作失败");
+ });
+ }
+
+ /* ----- 全屏表单页 ----- */
+
+ function openForm(kind, opts) {
+ state.review.form = { kind: kind, opts: opts || {}, action: "buy", emotion: "calm", stock: null };
+ state.review.dirty = false;
+ const title = kind === "trade" ? ((opts && opts.trade) ? "编辑交易" : "新增交易") : kind === "daily" ? "写复盘" : "新增笔记";
+ global.MobileRouter.updateHeader({ title: title, back: true, actions: "" });
+ document.getElementById("m-view").classList.add("m-view-feature");
+ document.getElementById("m-view").innerHTML = renderForm();
+ bindForm();
+ }
+
+ function closeForm() {
+ if (state.review.dirty) {
+ openConfirmSheet("放弃本次编辑?", "返回后已填写的内容将不会保存。", {
+ confirmLabel: "放弃",
+ onConfirm: function () { exitForm(); }
+ });
+ return;
+ }
+ exitForm();
+ }
+
+ function exitForm() {
+ const key = state.key;
+ const wasDaily = state.review.subKey === "daily";
+ const keepDate = state.requestedDate;
+ state.review.form = null;
+ state.review.dirty = false;
+ setupReviewPage(key);
+ if (wasDaily) state.requestedDate = keepDate;
+ loadReview();
+ }
+
+ function formFieldHtml(label, controlHtml, required, hintHtml) {
+ return '";
+ }
+
+ function textInputHtml(id, value, attrs) {
+ return '";
+ }
+
+ function numberInputHtml(id, value, attrs) {
+ return '";
+ }
+
+ function textareaHtml(id, value, maxlength, rows) {
+ return '' +
+ '" +
+ '0/' + maxlength + "" +
+ "
";
+ }
+
+ function dateInputHtml(id, value) {
+ return '' +
+ '' +
+ '' + icon("calendar", 16) + "" +
+ "
";
+ }
+
+ function segHtml(group, options, selected) {
+ return '' +
+ options.map(function (opt) {
+ const active = opt.key === selected;
+ return '";
+ }).join("") +
+ "
";
+ }
+
+ function stockPickerHtml(id, stock) {
+ const chip = stock
+ ? '' +
+ '' + escapeHtml(stock.name) + "" +
+ '' + escapeHtml(stock.code) + "" +
+ '" +
+ ""
+ : "";
+ return '' +
+ (chip || '
') +
+ '
' +
+ "
";
+ }
+
+ function renderForm() {
+ const form = state.review.form;
+ if (form.kind === "trade") {
+ return renderTradeForm(form);
+ }
+ if (form.kind === "daily") {
+ return renderDailyForm(form);
+ }
+ return renderNoteForm(form);
+ }
+
+ function renderTradeForm(form) {
+ const t = form.opts.trade || {};
+ const tags = (t.tags || []).join(", ");
+ const fields = [
+ formFieldHtml("日期", dateInputHtml("m-trade-date", displayCompactDate(t.trade_date)), true),
+ formFieldHtml("代码", textInputHtml("m-trade-code", t.code, 'inputmode="numeric" maxlength="6" placeholder="6 位代码"'), true),
+ formFieldHtml("名称", textInputHtml("m-trade-name", t.name, 'placeholder="股票名称"'), true),
+ formFieldHtml("动作", segHtml("action", TRADE_ACTION_OPTIONS, form.action || t.action || "buy"), true),
+ formFieldHtml("价格", numberInputHtml("m-trade-price", t.price, 'placeholder="成交价格"'), true),
+ formFieldHtml("数量", numberInputHtml("m-trade-quantity", t.quantity, 'inputmode="numeric" placeholder="成交数量"'), false),
+ formFieldHtml("仓位%", '' + numberInputHtml("m-trade-position", t.position_pct, 'placeholder="0"') + '%
', false),
+ formFieldHtml("盈亏额", numberInputHtml("m-trade-pnl-amount", t.pnl_amount, 'placeholder="可留空"'), false),
+ formFieldHtml("盈亏%", '' + numberInputHtml("m-trade-pnl-pct", t.pnl_pct, 'placeholder="可留空"') + '%
', false),
+ formFieldHtml("情绪", segHtml("emotion", TRADE_EMOTION_OPTIONS, form.emotion || t.emotion || "calm"), true),
+ formFieldHtml("标签", textInputHtml("m-trade-tags", tags, 'placeholder="逗号分隔,如 打板、龙头"'), false),
+ formFieldHtml("交易逻辑", textareaHtml("m-trade-thesis", t.thesis, 2000, 4), false),
+ formFieldHtml("执行复核", textareaHtml("m-trade-execution", t.execution, 2000, 4), false)
+ ];
+ return '";
+ }
+
+ function renderDailyForm() {
+ const note = state.review.form.opts.note || {};
+ const fields = [
+ formFieldHtml("日期", dateInputHtml("m-daily-date", state.requestedDate || todayString()), true),
+ formFieldHtml("今日盘面", textareaHtml("m-daily-summary", note.summary, 500, 3), false),
+ formFieldHtml("做对做错", textareaHtml("m-daily-content", note.content, 5000, 8), false),
+ formFieldHtml("明日计划", textareaHtml("m-daily-plan", note.plan, 2000, 5), false)
+ ];
+ return '";
+ }
+
+ function renderNoteForm() {
+ const fields = [
+ formFieldHtml("股票", stockPickerHtml("m-note-stock", state.review.form.stock), true),
+ formFieldHtml("笔记内容", textareaHtml("m-note-content", "", 5000, 6), false),
+ formFieldHtml("计划", textareaHtml("m-note-plan", "", 2000, 4), false)
+ ];
+ return '";
+ }
+
+ function markDirty() {
+ if (state.review.form) state.review.dirty = true;
+ }
+
+ function bindForm() {
+ const scroll = document.getElementById("m-scroll");
+ if (!scroll) return;
+
+ scroll.addEventListener("input", function (event) {
+ if (event.target.matches("input, textarea")) markDirty();
+ if (event.target.dataset && event.target.dataset.maxlength) {
+ const counter = document.querySelector('[data-counter-for="' + event.target.id + '"]');
+ if (counter) counter.textContent = event.target.value.length + "/" + event.target.dataset.maxlength;
+ }
+ });
+
+ scroll.querySelectorAll("textarea[data-maxlength]").forEach(function (ta) {
+ const counter = document.querySelector('[data-counter-for="' + ta.id + '"]');
+ if (counter) counter.textContent = ta.value.length + "/" + ta.dataset.maxlength;
+ });
+
+ scroll.addEventListener("click", function (event) {
+ const seg = event.target.closest("[data-seg-key]");
+ if (seg) {
+ const group = seg.parentElement && seg.parentElement.dataset.segGroup;
+ seg.parentElement.querySelectorAll("[data-seg-key]").forEach(function (b) {
+ b.classList.toggle("active", b === seg);
+ b.setAttribute("aria-checked", String(b === seg));
+ });
+ state.review.form[group] = seg.dataset.segKey;
+ markDirty();
+ return;
+ }
+ });
+
+ const picker = scroll.querySelector("[data-stock-picker]");
+ if (picker) bindStockPicker(picker, state.review.form);
+ }
+
+ function bindStockPicker(picker, target) {
+ const t = target || { stock: null };
+ let timer = null;
+ let requestSeq = 0;
+
+ function hideResults() {
+ const results = picker.querySelector("[data-stock-results]");
+ if (results) { results.hidden = true; results.innerHTML = ""; }
+ }
+
+ function renderPickerContent() {
+ picker.innerHTML = stockPickerHtml("", t.stock);
+ }
+
+ function runStockSearch(q) {
+ const seq = ++requestSeq;
+ global.MobileAPI.request("/api/search?q=" + encodeURIComponent(q) + "&trade_date=" + encodeURIComponent(todayString())).then(function (payload) {
+ if (seq !== requestSeq) return;
+ const stocks = (payload && payload.groups && payload.groups.stocks) || [];
+ const results = picker.querySelector("[data-stock-results]");
+ if (!results) return;
+ results.hidden = false;
+ results.innerHTML = stocks.length
+ ? stocks.map(function (s) {
+ return '";
+ }).join("")
+ : '没有找到匹配的股票
';
+ }).catch(function () {
+ if (seq !== requestSeq) return;
+ const results = picker.querySelector("[data-stock-results]");
+ if (!results) return;
+ results.hidden = false;
+ results.innerHTML = '搜索失败
';
+ });
+ }
+
+ picker.addEventListener("input", function (event) {
+ if (event.target.tagName !== "INPUT") return;
+ markDirty();
+ clearTimeout(timer);
+ const q = event.target.value.trim();
+ if (!q) { hideResults(); return; }
+ timer = setTimeout(function () { runStockSearch(q); }, 180);
+ });
+
+ picker.addEventListener("focusin", function (event) {
+ if (event.target.tagName !== "INPUT") return;
+ const q = event.target.value.trim();
+ if (q) runStockSearch(q);
+ });
+
+ picker.addEventListener("click", function (event) {
+ const row = event.target.closest("[data-stock-pick]");
+ if (row) {
+ t.stock = { code: row.dataset.stockPick, name: row.dataset.stockName, sector: row.dataset.stockSector };
+ hideResults();
+ renderPickerContent();
+ markDirty();
+ return;
+ }
+ const clear = event.target.closest("[data-stock-clear]");
+ if (clear) {
+ t.stock = null;
+ renderPickerContent();
+ markDirty();
+ return;
+ }
+ });
+
+ document.addEventListener("click", function onDoc(event) {
+ if (!picker.contains(event.target)) hideResults();
+ });
+ }
+
+ function fieldError(id, message) {
+ const input = document.getElementById(id);
+ if (!input) return;
+ const label = input.closest(".m-form-field");
+ if (!label) return;
+ label.classList.add("is-invalid");
+ let err = label.querySelector(".m-field-error");
+ if (!err) {
+ err = document.createElement("span");
+ err.className = "m-field-error";
+ label.appendChild(err);
+ }
+ err.textContent = message;
+ }
+
+ function clearFieldError(id) {
+ const input = document.getElementById(id);
+ if (!input) return;
+ const label = input.closest(".m-form-field");
+ if (!label) return;
+ label.classList.remove("is-invalid");
+ const err = label.querySelector(".m-field-error");
+ if (err) err.remove();
+ }
+
+ function numberValue(id) {
+ const el = document.getElementById(id);
+ const raw = el ? el.value.trim() : "";
+ return raw === "" ? "" : number(raw);
+ }
+
+ function submitForm() {
+ const form = state.review.form;
+ if (!form) return;
+ if (form.kind === "trade") submitTradeForm();
+ else if (form.kind === "daily") submitDailyForm();
+ else submitNoteForm();
+ }
+
+ function setSubmitting(btn, submitting) {
+ if (!btn) return;
+ btn.disabled = submitting;
+ if (submitting) {
+ btn.innerHTML = '保存中…';
+ } else {
+ btn.innerHTML = "保存";
+ }
+ }
+
+ function submitTradeForm() {
+ const form = state.review.form;
+ const trade = form.opts.trade || {};
+ const tradeDate = document.getElementById("m-trade-date").value;
+ const code = document.getElementById("m-trade-code").value.trim();
+ const name = document.getElementById("m-trade-name").value.trim();
+ const price = numberValue("m-trade-price");
+ const position = numberValue("m-trade-position");
+ const pnlAmount = numberValue("m-trade-pnl-amount");
+ const pnlPct = numberValue("m-trade-pnl-pct");
+ const tags = document.getElementById("m-trade-tags").value.trim();
+
+ clearFieldError("m-trade-code");
+ clearFieldError("m-trade-name");
+ clearFieldError("m-trade-price");
+ let invalid = false;
+ if (!code) { fieldError("m-trade-code", "请输入股票代码"); invalid = true; }
+ if (!name) { fieldError("m-trade-name", "请输入股票名称"); invalid = true; }
+ if (price === "") { fieldError("m-trade-price", "请输入成交价格"); invalid = true; }
+ if (invalid) return;
+
+ const payload = {
+ trade_date: tradeDate || todayString(),
+ code: code,
+ name: name,
+ action: form.action,
+ price: price,
+ quantity: number(document.getElementById("m-trade-quantity").value || 0),
+ position_pct: position === "" ? 0 : position,
+ pnl_amount: pnlAmount === "" ? null : pnlAmount,
+ pnl_pct: pnlPct === "" ? null : pnlPct,
+ emotion: form.emotion,
+ tags: tags ? tags.split(/[,,]/).map(function (s) { return s.trim(); }).filter(Boolean) : [],
+ thesis: document.getElementById("m-trade-thesis").value.trim(),
+ execution: document.getElementById("m-trade-execution").value.trim()
+ };
+ if (trade.id) payload.id = trade.id;
+
+ const btn = document.querySelector("[data-review-submit]");
+ setSubmitting(btn, true);
+ global.MobileAPI.request("/api/trades", "POST", payload).then(function () {
+ state.review.dirty = false;
+ exitForm();
+ showToast("交易已保存");
+ }).catch(function (error) {
+ setSubmitting(btn, false);
+ showToast(error && error.message ? error.message : "保存失败");
+ });
+ }
+
+ function submitDailyForm() {
+ const form = state.review.form;
+ const note = form.opts.note || {};
+ const summary = document.getElementById("m-daily-summary").value.trim();
+ const content = document.getElementById("m-daily-content").value.trim();
+ const plan = document.getElementById("m-daily-plan").value.trim();
+ const tradeDate = document.getElementById("m-daily-date").value || todayString();
+ if (!summary && !content && !plan) {
+ showToast("复盘内容不能全部为空");
+ return;
+ }
+ const payload = {
+ trade_date: tradeDate,
+ summary: summary,
+ content: content,
+ plan: plan
+ };
+ if (note.id) payload.id = note.id;
+
+ const btn = document.querySelector("[data-review-submit]");
+ setSubmitting(btn, true);
+ global.MobileAPI.request("/api/notes", "POST", payload).then(function () {
+ state.review.dirty = false;
+ state.requestedDate = tradeDate;
+ exitForm();
+ showToast("已保存");
+ }).catch(function (error) {
+ setSubmitting(btn, false);
+ showToast(error && error.message ? error.message : "保存失败");
+ });
+ }
+
+ function submitNoteForm() {
+ const form = state.review.form;
+ const stock = form.stock;
+ if (!stock || !stock.code) {
+ showToast("请先选择股票");
+ return;
+ }
+ const payload = {
+ code: stock.code,
+ stock_name: stock.name,
+ trade_date: todayString(),
+ content: document.getElementById("m-note-content").value.trim(),
+ plan: document.getElementById("m-note-plan").value.trim()
+ };
+ if (!payload.content && !payload.plan) {
+ showToast("笔记内容不能为空");
+ return;
+ }
+ const btn = document.querySelector("[data-review-submit]");
+ setSubmitting(btn, true);
+ global.MobileAPI.request("/api/notes", "POST", payload).then(function () {
+ state.review.dirty = false;
+ exitForm();
+ showToast("笔记已保存");
+ }).catch(function (error) {
+ setSubmitting(btn, false);
+ showToast(error && error.message ? error.message : "保存失败");
+ });
+ }
+
+ /* ----- 提醒新增抽屉 ----- */
+
+ function openAlertAddDrawer() {
+ state.review.alertDraft = { stock: null };
+ openSheet(
+ '新增提醒
' +
+ '" +
+ '' + alertAddDrawerBody() + "
",
+ { detail: false }
+ );
+ const body = document.getElementById("m-alert-add-body");
+ const picker = body.querySelector("[data-stock-picker]");
+ if (picker) bindStockPicker(picker, state.review.alertDraft);
+ }
+
+ function alertAddDrawerBody() {
+ return '' +
+ formFieldHtml("提醒日期", dateInputHtml("m-alert-date", todayString()), true) +
+ formFieldHtml("关联股票", stockPickerHtml("m-alert-stock", state.review.alertDraft ? state.review.alertDraft.stock : null), false) +
+ formFieldHtml("内容", textareaHtml("m-alert-content", "", 500, 3), false) +
+ '';
+ }
+
+ function submitAlertAdd() {
+ const title = document.getElementById("m-alert-title").value.trim();
+ if (!title) { showToast("请输入提醒标题"); return; }
+ const stock = state.review.alertDraft && state.review.alertDraft.stock;
+ const payload = {
+ title: title,
+ remind_date: document.getElementById("m-alert-date").value || todayString(),
+ code: stock ? stock.code : "",
+ content: document.getElementById("m-alert-content").value.trim()
+ };
+ global.MobileAPI.request("/api/alerts", "POST", payload).then(function (res) {
+ state.review.alerts = res || state.review.alerts;
+ closeSheet();
+ renderReviewAlerts();
+ showToast("已添加提醒");
+ }).catch(function (error) {
+ showToast(error && error.message ? error.message : "保存失败");
+ });
+ }
+
+ function buildReviewTable(cols, rows, opts) {
+ const o = opts || {};
+ const frozen = cols.frozenColumns || [];
+ const primary = cols.primaryColumns || [];
+ const scroll = cols.scrollColumns || [];
+ const ordered = frozen.concat(primary, scroll);
+ let left = 0;
+ const frozenLeft = frozen.map(function (col) { const off = left; left += columnWidth(col); return off; });
+
+ function cellOpen(col, index, isHead, extraCls, extraAttrs) {
+ const isFrozen = index < frozen.length;
+ const width = columnWidth(col);
+ const clamp = (col.type === "text" || col.type === "concepts") ? ";max-width:" + width + "px" : "";
+ const style = "min-width:" + width + "px" + clamp + (isFrozen ? ";width:" + width + "px;left:" + frozenLeft[index] + "px" : "");
+ const cls = (isHead ? "m-th" : "m-td") + " " + colAlign(col) + (isFrozen ? " m-frozen" : "") + (extraCls ? " " + extraCls : "");
+ return '<' + (isHead ? "th" : "td") + ' class="' + cls + '" style="' + style + '"' + (extraAttrs || "") + '>';
+ }
+
+ const head = ordered.map(function (col, i) {
+ const sortable = !o.noSort && sortableColumn(col);
+ let extraCls = "";
+ let extraAttrs = "";
+ let indicator = "";
+ if (sortable) {
+ const active = state.sort.key === col.key && Boolean(state.sort.dir);
+ extraCls = " m-sortable" + (active ? " is-sorted" : "");
+ extraAttrs = ' data-sort-key="' + escapeHtml(col.key) + '" aria-sort="' + (active ? (state.sort.dir === "asc" ? "ascending" : "descending") : "none") + '"';
+ indicator = sortIndicatorHtml(active);
+ }
+ return cellOpen(col, i, true, extraCls, extraAttrs) + '' + escapeHtml(col.label) + indicator + "";
+ }).join("");
+
+ const body = rows.map(function (row, rIndex) {
+ const cells = ordered.map(function (col, i) {
+ const custom = o.cellRenderer ? o.cellRenderer(col, row, rIndex) : null;
+ const html = custom != null ? custom : cellHtml(col, row, rIndex);
+ return cellOpen(col, i, false) + html + "";
+ }).join("");
+ const linkable = !o.noLink && /^\d{6}$/.test(String(row.code || ""));
+ const code = linkable ? ' data-code="' + escapeHtml(row.code) + '"' : "";
+ const extra = o.rowAttrs ? o.rowAttrs(row) : "";
+ return "" + cells + "
";
+ }).join("");
+
+ return '";
+ }
+
/* ---------------------------------------------------------------- events */
function bindEvents() {
window.addEventListener("hashchange", closeSheet);
+ // 全屏表单页的返回:优先回到列表/查看态,而非离开路由(capture 阶段先于路由层返回)
+ document.getElementById("m-back").addEventListener("click", function (event) {
+ if (state.review && state.review.form) {
+ event.preventDefault();
+ event.stopPropagation();
+ closeForm();
+ }
+ }, true);
+
document.addEventListener("click", function (event) {
const dateBtn = event.target.closest("[data-action=date]");
if (dateBtn) { openDateSheet(); return; }
@@ -3691,11 +4907,71 @@
return;
}
+ // 我的复盘:新增/编辑/删除/筛选/提交等操作
+ const reviewAddTrade = event.target.closest("[data-review-add-trade]");
+ if (reviewAddTrade) { openForm("trade"); return; }
+ const reviewAddNote = event.target.closest("[data-review-add-note]");
+ if (reviewAddNote) { openForm("note"); return; }
+ const reviewAddAlert = event.target.closest("[data-review-add-alert]");
+ if (reviewAddAlert) { openAlertAddDrawer(); return; }
+ const reviewReadAll = event.target.closest("[data-review-read-all]");
+ if (reviewReadAll) { markAllAlertsRead(); return; }
+ const reviewEditTrade = event.target.closest("[data-review-edit-trade]");
+ if (reviewEditTrade) {
+ const id = reviewEditTrade.dataset.tradeId;
+ const data = state.review.trades || { items: [] };
+ const item = (data.items || []).find(function (it) { return String(it.id) === String(id); });
+ closeSheet();
+ if (item) openForm("trade", { trade: item });
+ return;
+ }
+ const reviewDeleteTrade = event.target.closest("[data-review-delete-trade]");
+ if (reviewDeleteTrade) { deleteTrade(reviewDeleteTrade.dataset.tradeId); return; }
+ const reviewEditDaily = event.target.closest("[data-review-edit-daily]");
+ if (reviewEditDaily) {
+ const note = dailyNoteFor(state.requestedDate || todayString());
+ openForm("daily", { note: note });
+ return;
+ }
+ const reviewDeleteDaily = event.target.closest("[data-review-delete-daily]");
+ if (reviewDeleteDaily) { deleteDailyNote(reviewDeleteDaily.dataset.noteId); return; }
+ const dailyDateRow = event.target.closest("[data-daily-date]");
+ if (dailyDateRow) {
+ state.requestedDate = dailyDateRow.dataset.dailyDate;
+ updateHeader("每日复盘", state.requestedDate);
+ renderReviewDaily();
+ return;
+ }
+ const reviewDeleteNote = event.target.closest("[data-review-delete-note]");
+ if (reviewDeleteNote) { deleteNote(reviewDeleteNote.dataset.noteId); return; }
+ const noteCard = event.target.closest("[data-note-id]");
+ if (noteCard) { openNoteDetail(noteCard.dataset.noteId); return; }
+ const alertRow = event.target.closest("[data-alert-id]");
+ if (alertRow) { openAlertDetail(alertRow.dataset.alertId); return; }
+ const alertRead = event.target.closest("[data-review-alert-read]");
+ if (alertRead) { markAlertRead(alertRead.dataset.alertId); return; }
+ const alertDelete = event.target.closest("[data-review-alert-delete]");
+ if (alertDelete) { deleteAlert(alertDelete.dataset.alertId); return; }
+ const alertFilter = event.target.closest("[data-review-alert-filter]");
+ if (alertFilter) {
+ state.review.alertStatus = alertFilter.dataset.reviewAlertFilter === "unread" ? "unread" : "all";
+ loadReviewAlerts();
+ return;
+ }
+ const reviewSubmit = event.target.closest("[data-review-submit]");
+ if (reviewSubmit) { submitForm(); return; }
+ const alertSave = event.target.closest("[data-alert-save]");
+ if (alertSave) { submitAlertAdd(); return; }
+ const alertStock = event.target.closest("[data-alert-stock]");
+ if (alertStock) { openDetailSheet(alertStock.dataset.code); return; }
+
const row = event.target.closest("[data-code]");
if (row) {
const interactive = event.target.closest("button, a, input, select, textarea");
if (!interactive) {
- if (state.key === "tools/tracking" && row.dataset.trackId) {
+ if (state.key === "review/trades" && row.dataset.tradeId) {
+ openTradeDetail(row.dataset.tradeId);
+ } else if (state.key === "tools/tracking" && row.dataset.trackId) {
openTrackingDetailSheet(row.dataset.code, row.dataset.trackId);
} else if (state.key === "tools/screener") {
openScreenerDetailSheet(row.dataset.code);