3e8ecbf712
Co-authored-by: Cursor <cursoragent@cursor.com>
773 lines
26 KiB
JavaScript
773 lines
26 KiB
JavaScript
/**
|
|
* 开仓计划:新建 / 进行中 / 历史 / 胜率统计
|
|
*/
|
|
(function () {
|
|
const page = document.getElementById("page-plan");
|
|
if (!page) return;
|
|
|
|
let meta = null;
|
|
let activePlans = [];
|
|
let archivedPlans = [];
|
|
let statsPeriod = "all";
|
|
let statsDim = "symbol";
|
|
let statsDateFrom = "";
|
|
let statsDateTo = "";
|
|
let editingPlanId = null;
|
|
let inited = false;
|
|
|
|
function $(id) {
|
|
return document.getElementById(id);
|
|
}
|
|
|
|
function esc(s) {
|
|
return String(s == null ? "" : s)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
function toast(msg, isErr) {
|
|
const el = $("toast");
|
|
if (!el) return;
|
|
el.textContent = msg;
|
|
el.className = isErr ? "err" : "ok";
|
|
clearTimeout(el._t);
|
|
el._t = setTimeout(function () {
|
|
el.className = "";
|
|
el.textContent = "";
|
|
}, 3200);
|
|
}
|
|
|
|
async function api(path, opts) {
|
|
const r = await fetch(path, Object.assign({ credentials: "same-origin" }, opts || {}));
|
|
let data = {};
|
|
try {
|
|
data = await r.json();
|
|
} catch (_e) {
|
|
data = {};
|
|
}
|
|
if (!r.ok) {
|
|
const detail = (data && data.detail) || r.statusText || "请求失败";
|
|
throw new Error(typeof detail === "string" ? detail : JSON.stringify(detail));
|
|
}
|
|
return data;
|
|
}
|
|
|
|
function todayIso() {
|
|
const d = new Date();
|
|
const y = d.getFullYear();
|
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
const day = String(d.getDate()).padStart(2, "0");
|
|
return y + "-" + m + "-" + day;
|
|
}
|
|
|
|
function exchangeLabel(key) {
|
|
const ex = (meta && meta.exchanges) || [];
|
|
const row = ex.find(function (e) {
|
|
return String(e.key) === String(key);
|
|
});
|
|
return (row && row.name) || key || "—";
|
|
}
|
|
|
|
function fmtPnl(v) {
|
|
if (v == null || v === "") return "";
|
|
const n = Number(v);
|
|
if (!Number.isFinite(n)) return String(v);
|
|
return (n >= 0 ? "+" : "") + n.toFixed(2) + "U";
|
|
}
|
|
|
|
function fillSelect(el, options, valueKey, labelKey) {
|
|
if (!el) return;
|
|
el.innerHTML = "";
|
|
(options || []).forEach(function (opt) {
|
|
const o = document.createElement("option");
|
|
if (typeof opt === "string") {
|
|
o.value = opt;
|
|
o.textContent = opt;
|
|
} else {
|
|
o.value = opt[valueKey];
|
|
o.textContent = opt[labelKey];
|
|
}
|
|
el.appendChild(o);
|
|
});
|
|
}
|
|
|
|
function renderDirectionRadios(container, name, selected) {
|
|
if (!container || !meta) return;
|
|
container.innerHTML = "";
|
|
(meta.directions || []).forEach(function (d) {
|
|
const label = document.createElement("label");
|
|
label.className = "plan-radio-label";
|
|
const input = document.createElement("input");
|
|
input.type = "radio";
|
|
input.name = name;
|
|
input.value = d.value;
|
|
if (d.value === selected) input.checked = true;
|
|
label.appendChild(input);
|
|
label.appendChild(document.createTextNode(" " + d.label));
|
|
container.appendChild(label);
|
|
});
|
|
}
|
|
|
|
function bindMetaToCreateForm() {
|
|
fillSelect($("plan-create-exchange"), meta.exchanges, "key", "name");
|
|
fillSelect($("plan-create-type"), meta.plan_types, "value", "label");
|
|
fillSelect($("plan-create-trend-tf"), meta.trend_timeframes);
|
|
fillSelect($("plan-create-entry-tf"), meta.entry_timeframes);
|
|
renderDirectionRadios($("plan-create-direction"), "plan-direction", "long");
|
|
const dateEl = $("plan-create-date");
|
|
if (dateEl && !dateEl.value) dateEl.value = todayIso();
|
|
}
|
|
|
|
function planSummaryLine(p) {
|
|
return (
|
|
esc(p.symbol) +
|
|
" · " +
|
|
esc(exchangeLabel(p.exchange_key)) +
|
|
" · " +
|
|
esc(p.direction_label || p.direction) +
|
|
" · " +
|
|
esc(p.plan_type_label || p.plan_type)
|
|
);
|
|
}
|
|
|
|
function schemeOptionsHtml(selected) {
|
|
let html = '<option value="">请选择</option>';
|
|
(meta.entry_schemes || []).forEach(function (s) {
|
|
html +=
|
|
'<option value="' +
|
|
esc(s.value) +
|
|
'"' +
|
|
(selected === s.value ? " selected" : "") +
|
|
">" +
|
|
esc(s.label) +
|
|
"</option>";
|
|
});
|
|
return html;
|
|
}
|
|
|
|
function renderActiveList() {
|
|
const host = $("plan-active-list");
|
|
const cnt = $("plan-active-count");
|
|
if (!host) return;
|
|
if (cnt) cnt.textContent = activePlans.length ? activePlans.length + " 条" : "";
|
|
if (!activePlans.length) {
|
|
host.innerHTML = '<p class="plan-empty">暂无进行中的计划</p>';
|
|
return;
|
|
}
|
|
host.innerHTML = activePlans
|
|
.map(function (p) {
|
|
return (
|
|
'<article class="plan-active-card" data-id="' +
|
|
esc(p.id) +
|
|
'">' +
|
|
'<div class="plan-active-head">' +
|
|
'<div class="plan-active-title">' +
|
|
planSummaryLine(p) +
|
|
"</div>" +
|
|
'<div class="plan-active-actions">' +
|
|
'<button type="button" class="ghost plan-btn-edit" data-id="' +
|
|
esc(p.id) +
|
|
'">修改</button>' +
|
|
'<button type="button" class="ghost plan-btn-del" data-id="' +
|
|
esc(p.id) +
|
|
'">删除</button>' +
|
|
"</div></div>" +
|
|
'<div class="plan-active-meta">' +
|
|
esc(p.plan_date) +
|
|
" · 趋势 " +
|
|
esc(p.trend_timeframe) +
|
|
" / 入场 " +
|
|
esc(p.entry_timeframe) +
|
|
"</div>" +
|
|
'<div class="plan-active-levels">目标 ' +
|
|
esc(p.target_level || "—") +
|
|
" · 区间 " +
|
|
esc(p.current_range || "—") +
|
|
"</div>" +
|
|
(p.note ? '<div class="plan-active-note">' + esc(p.note) + "</div>" : "") +
|
|
'<div class="plan-scheme-row">' +
|
|
'<label class="plan-field plan-field-inline plan-field-scheme"><span>入场方案</span>' +
|
|
'<select class="plan-active-scheme" data-id="' +
|
|
esc(p.id) +
|
|
'">' +
|
|
schemeOptionsHtml(p.entry_scheme || "") +
|
|
"</select></label>" +
|
|
"</div>" +
|
|
'<div class="plan-close-row">' +
|
|
'<label class="plan-field plan-field-inline"><span>结果</span>' +
|
|
'<select class="plan-close-result" data-id="' +
|
|
esc(p.id) +
|
|
'"><option value="">—</option>' +
|
|
(meta.results || [])
|
|
.map(function (r) {
|
|
return (
|
|
'<option value="' +
|
|
esc(r.value) +
|
|
'"' +
|
|
(p.result === r.value ? " selected" : "") +
|
|
">" +
|
|
esc(r.label) +
|
|
"</option>"
|
|
);
|
|
})
|
|
.join("") +
|
|
"</select></label>" +
|
|
'<label class="plan-field plan-field-inline"><span>盈亏</span>' +
|
|
'<input class="plan-close-pnl" data-id="' +
|
|
esc(p.id) +
|
|
'" type="number" step="any" placeholder="U(可选)" value="' +
|
|
(p.pnl_amount != null ? esc(p.pnl_amount) : "") +
|
|
'" /></label>' +
|
|
'<button type="button" class="primary plan-btn-archive" data-id="' +
|
|
esc(p.id) +
|
|
'">填写结果并归档</button>' +
|
|
"</div></article>"
|
|
);
|
|
})
|
|
.join("");
|
|
}
|
|
|
|
function renderHistoryList() {
|
|
const host = $("plan-history-list");
|
|
const cnt = $("plan-history-count");
|
|
if (!host) return;
|
|
if (cnt) cnt.textContent = archivedPlans.length ? archivedPlans.length + " 条" : "";
|
|
if (!archivedPlans.length) {
|
|
host.innerHTML = '<p class="plan-empty">暂无历史计划</p>';
|
|
return;
|
|
}
|
|
host.innerHTML = archivedPlans
|
|
.map(function (p) {
|
|
const pnlTxt = fmtPnl(p.pnl_amount);
|
|
const resCls = p.result === "win" ? "plan-res-win" : "plan-res-loss";
|
|
return (
|
|
'<button type="button" class="plan-history-row" data-id="' +
|
|
esc(p.id) +
|
|
'">' +
|
|
'<span class="plan-history-date">' +
|
|
esc(p.plan_date) +
|
|
"</span>" +
|
|
'<span class="plan-history-main">' +
|
|
esc(p.symbol) +
|
|
" · " +
|
|
esc(exchangeLabel(p.exchange_key)) +
|
|
"</span>" +
|
|
'<span class="plan-history-scheme">' +
|
|
esc(p.entry_scheme_label || p.entry_scheme) +
|
|
"</span>" +
|
|
'<span class="plan-history-result ' +
|
|
resCls +
|
|
'">' +
|
|
esc(p.result_label || p.result) +
|
|
(pnlTxt ? " " + esc(pnlTxt) : "") +
|
|
"</span></button>"
|
|
);
|
|
})
|
|
.join("");
|
|
}
|
|
|
|
function renderStatsTable(stats) {
|
|
const host = $("plan-stats-table");
|
|
const labelEl = $("plan-stats-label");
|
|
if (labelEl) labelEl.textContent = (stats && stats.period_label) || "";
|
|
if (!host) return;
|
|
const items = (stats && stats.items) || [];
|
|
if (!items.length) {
|
|
host.innerHTML = '<p class="plan-empty">该范围内暂无已归档且有结果的计划</p>';
|
|
return;
|
|
}
|
|
const dimLabel =
|
|
stats.dimension === "trend_tf"
|
|
? "趋势周期"
|
|
: stats.dimension === "entry_scheme"
|
|
? "入场方案"
|
|
: "币种";
|
|
let rows = items
|
|
.map(function (it) {
|
|
return (
|
|
"<tr><td>" +
|
|
esc(it.label || it.key) +
|
|
"</td><td>" +
|
|
(it.total || 0) +
|
|
"</td><td>" +
|
|
(it.win_count || 0) +
|
|
"</td><td>" +
|
|
(it.loss_count || 0) +
|
|
"</td><td>" +
|
|
(it.win_rate != null ? it.win_rate + "%" : "—") +
|
|
"</td></tr>"
|
|
);
|
|
})
|
|
.join("");
|
|
host.innerHTML =
|
|
'<table class="plan-stats-table"><thead><tr>' +
|
|
"<th>" +
|
|
esc(dimLabel) +
|
|
"</th><th>计划数</th><th>盈利</th><th>亏损</th><th>胜率</th>" +
|
|
"</tr></thead><tbody>" +
|
|
rows +
|
|
"</tbody></table>";
|
|
}
|
|
|
|
function statsQuery() {
|
|
const q = new URLSearchParams();
|
|
q.set("dimension", statsDim);
|
|
q.set("period", statsPeriod);
|
|
if (statsPeriod === "range") {
|
|
if (statsDateFrom) q.set("date_from", statsDateFrom);
|
|
if (statsDateTo) q.set("date_to", statsDateTo);
|
|
}
|
|
return q.toString();
|
|
}
|
|
|
|
async function loadMeta() {
|
|
const data = await api("/api/entry-plans/meta");
|
|
meta = data;
|
|
bindMetaToCreateForm();
|
|
}
|
|
|
|
async function loadActive() {
|
|
const data = await api("/api/entry-plans?status=active");
|
|
activePlans = data.plans || [];
|
|
renderActiveList();
|
|
}
|
|
|
|
async function loadHistory() {
|
|
const data = await api("/api/entry-plans?status=archived");
|
|
archivedPlans = data.plans || [];
|
|
renderHistoryList();
|
|
}
|
|
|
|
async function loadStats() {
|
|
const data = await api("/api/entry-plans/stats?" + statsQuery());
|
|
renderStatsTable(data.stats || {});
|
|
}
|
|
|
|
async function refreshAll() {
|
|
await Promise.all([loadActive(), loadHistory(), loadStats()]);
|
|
}
|
|
|
|
function fmtRefreshTime() {
|
|
const d = new Date();
|
|
const h = String(d.getHours()).padStart(2, "0");
|
|
const m = String(d.getMinutes()).padStart(2, "0");
|
|
const s = String(d.getSeconds()).padStart(2, "0");
|
|
return h + ":" + m + ":" + s;
|
|
}
|
|
|
|
async function refreshPage() {
|
|
const btn = $("plan-btn-refresh");
|
|
const status = $("plan-refresh-status");
|
|
if (btn) btn.disabled = true;
|
|
if (status) status.textContent = "刷新中…";
|
|
try {
|
|
await loadMeta();
|
|
await refreshAll();
|
|
if (status) status.textContent = "已刷新 " + fmtRefreshTime();
|
|
} catch (e) {
|
|
toast(e.message || "刷新失败", true);
|
|
if (status) status.textContent = "刷新失败";
|
|
} finally {
|
|
if (btn) btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
function readCreateForm() {
|
|
const dir = document.querySelector('input[name="plan-direction"]:checked');
|
|
return {
|
|
plan_date: ($("plan-create-date") && $("plan-create-date").value) || "",
|
|
exchange_key: ($("plan-create-exchange") && $("plan-create-exchange").value) || "",
|
|
symbol: ($("plan-create-symbol") && $("plan-create-symbol").value) || "",
|
|
plan_type: ($("plan-create-type") && $("plan-create-type").value) || "",
|
|
trend_timeframe: ($("plan-create-trend-tf") && $("plan-create-trend-tf").value) || "",
|
|
entry_timeframe: ($("plan-create-entry-tf") && $("plan-create-entry-tf").value) || "",
|
|
direction: (dir && dir.value) || "",
|
|
target_level: ($("plan-create-target") && $("plan-create-target").value) || "",
|
|
current_range: ($("plan-create-range") && $("plan-create-range").value) || "",
|
|
note: ($("plan-create-note") && $("plan-create-note").value) || "",
|
|
};
|
|
}
|
|
|
|
function resetCreateForm() {
|
|
const form = $("plan-create-form");
|
|
if (form) form.reset();
|
|
bindMetaToCreateForm();
|
|
if ($("plan-create-date")) $("plan-create-date").value = todayIso();
|
|
}
|
|
|
|
function openDetailModal(plan) {
|
|
const modal = $("plan-detail-modal");
|
|
const body = $("plan-detail-body");
|
|
const title = $("plan-detail-title");
|
|
if (!modal || !body || !plan) return;
|
|
if (title) title.textContent = plan.symbol + " · " + (plan.result_label || "计划");
|
|
const rows = [
|
|
["日期", plan.plan_date],
|
|
["交易所", exchangeLabel(plan.exchange_key)],
|
|
["币种", plan.symbol],
|
|
["类型", plan.plan_type_label],
|
|
["趋势周期", plan.trend_timeframe],
|
|
["入场周期", plan.entry_timeframe],
|
|
["方向", plan.direction_label],
|
|
["目标位", plan.target_level || "—"],
|
|
["当前区间", plan.current_range || "—"],
|
|
["入场方案", plan.entry_scheme_label],
|
|
["结果", plan.result_label || "—"],
|
|
["盈亏", fmtPnl(plan.pnl_amount) || "—"],
|
|
["备注", plan.note || "—"],
|
|
];
|
|
body.innerHTML = rows
|
|
.map(function (pair) {
|
|
return (
|
|
'<div class="plan-detail-row"><span class="plan-detail-k">' +
|
|
esc(pair[0]) +
|
|
'</span><span class="plan-detail-v">' +
|
|
esc(pair[1]) +
|
|
"</span></div>"
|
|
);
|
|
})
|
|
.join("");
|
|
modal.classList.remove("hidden");
|
|
modal.setAttribute("aria-hidden", "false");
|
|
}
|
|
|
|
function closeDetailModal() {
|
|
const modal = $("plan-detail-modal");
|
|
if (!modal) return;
|
|
modal.classList.add("hidden");
|
|
modal.setAttribute("aria-hidden", "true");
|
|
}
|
|
|
|
function buildEditFormHtml(p) {
|
|
const dirs = (meta.directions || [])
|
|
.map(function (d) {
|
|
return (
|
|
'<label class="plan-radio-label"><input type="radio" name="edit-direction" value="' +
|
|
esc(d.value) +
|
|
'"' +
|
|
(p.direction === d.value ? " checked" : "") +
|
|
" /> " +
|
|
esc(d.label) +
|
|
"</label>"
|
|
);
|
|
})
|
|
.join("");
|
|
function opts(list, key, valKey, labelKey) {
|
|
return (list || [])
|
|
.map(function (o) {
|
|
const v = typeof o === "string" ? o : o[valKey];
|
|
const lbl = typeof o === "string" ? o : o[labelKey];
|
|
return (
|
|
'<option value="' +
|
|
esc(v) +
|
|
'"' +
|
|
(String(p[key]) === String(v) ? " selected" : "") +
|
|
">" +
|
|
esc(lbl) +
|
|
"</option>"
|
|
);
|
|
})
|
|
.join("");
|
|
}
|
|
return (
|
|
'<div class="plan-form-grid">' +
|
|
'<label class="plan-field"><span>日期</span><input name="plan_date" type="date" value="' +
|
|
esc(p.plan_date) +
|
|
'" required /></label>' +
|
|
'<label class="plan-field"><span>交易所</span><select name="exchange_key" required>' +
|
|
opts(meta.exchanges, "exchange_key", "key", "name") +
|
|
"</select></label>" +
|
|
'<label class="plan-field"><span>币种</span><input name="symbol" type="text" value="' +
|
|
esc(p.symbol) +
|
|
'" required /></label>' +
|
|
'<label class="plan-field"><span>类型</span><select name="plan_type" required>' +
|
|
opts(meta.plan_types, "plan_type", "value", "label") +
|
|
"</select></label>" +
|
|
'<label class="plan-field"><span>趋势周期</span><select name="trend_timeframe" required>' +
|
|
opts(meta.trend_timeframes, "trend_timeframe") +
|
|
"</select></label>" +
|
|
'<label class="plan-field"><span>入场周期</span><select name="entry_timeframe" required>' +
|
|
opts(meta.entry_timeframes, "entry_timeframe") +
|
|
"</select></label>" +
|
|
'<label class="plan-field plan-field-full"><span>方向</span><span class="plan-radio-row">' +
|
|
dirs +
|
|
"</span></label>" +
|
|
'<label class="plan-field"><span>目标位</span><input name="target_level" type="text" value="' +
|
|
esc(p.target_level || "") +
|
|
'" /></label>' +
|
|
'<label class="plan-field"><span>当前区间</span><input name="current_range" type="text" value="' +
|
|
esc(p.current_range || "") +
|
|
'" /></label>' +
|
|
'<label class="plan-field plan-field-full"><span>入场方案</span><select name="entry_scheme" required>' +
|
|
opts(meta.entry_schemes, "entry_scheme", "value", "label") +
|
|
"</select></label>" +
|
|
'<label class="plan-field plan-field-full"><span>备注</span><textarea name="note" rows="2">' +
|
|
esc(p.note || "") +
|
|
"</textarea></label>" +
|
|
"</div>" +
|
|
'<div class="modal-actions"><button type="button" class="ghost" data-plan-edit-close>取消</button>' +
|
|
'<button type="submit" class="primary">保存修改</button></div>'
|
|
);
|
|
}
|
|
|
|
function openEditModal(plan) {
|
|
const modal = $("plan-edit-modal");
|
|
const form = $("plan-edit-form");
|
|
if (!modal || !form || !plan) return;
|
|
editingPlanId = plan.id;
|
|
form.innerHTML = buildEditFormHtml(plan);
|
|
modal.classList.remove("hidden");
|
|
modal.setAttribute("aria-hidden", "false");
|
|
}
|
|
|
|
function closeEditModal() {
|
|
const modal = $("plan-edit-modal");
|
|
if (!modal) return;
|
|
editingPlanId = null;
|
|
modal.classList.add("hidden");
|
|
modal.setAttribute("aria-hidden", "true");
|
|
}
|
|
|
|
function readEditForm(form) {
|
|
const fd = new FormData(form);
|
|
const dir = form.querySelector('input[name="edit-direction"]:checked');
|
|
return {
|
|
plan_date: fd.get("plan_date") || "",
|
|
exchange_key: fd.get("exchange_key") || "",
|
|
symbol: fd.get("symbol") || "",
|
|
plan_type: fd.get("plan_type") || "",
|
|
trend_timeframe: fd.get("trend_timeframe") || "",
|
|
entry_timeframe: fd.get("entry_timeframe") || "",
|
|
direction: (dir && dir.value) || "",
|
|
target_level: fd.get("target_level") || "",
|
|
current_range: fd.get("current_range") || "",
|
|
entry_scheme: fd.get("entry_scheme") || "",
|
|
note: fd.get("note") || "",
|
|
};
|
|
}
|
|
|
|
function bindEvents() {
|
|
const refreshBtn = $("plan-btn-refresh");
|
|
if (refreshBtn) {
|
|
refreshBtn.addEventListener("click", function () {
|
|
void refreshPage();
|
|
});
|
|
}
|
|
|
|
const createForm = $("plan-create-form");
|
|
if (createForm) {
|
|
createForm.addEventListener("submit", function (ev) {
|
|
ev.preventDefault();
|
|
api("/api/entry-plans", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(readCreateForm()),
|
|
})
|
|
.then(function () {
|
|
toast("计划已加入进行中");
|
|
resetCreateForm();
|
|
return refreshAll();
|
|
})
|
|
.catch(function (e) {
|
|
toast(e.message || "保存失败", true);
|
|
});
|
|
});
|
|
}
|
|
|
|
const activeList = $("plan-active-list");
|
|
if (activeList) {
|
|
activeList.addEventListener("click", function (ev) {
|
|
const t = ev.target;
|
|
if (!(t instanceof HTMLElement)) return;
|
|
const id = t.getAttribute("data-id");
|
|
if (!id) return;
|
|
if (t.classList.contains("plan-btn-del")) {
|
|
if (!window.confirm("确定删除该进行中的计划?")) return;
|
|
api("/api/entry-plans/" + id, { method: "DELETE" })
|
|
.then(function () {
|
|
toast("已删除");
|
|
return refreshAll();
|
|
})
|
|
.catch(function (e) {
|
|
toast(e.message || "删除失败", true);
|
|
});
|
|
return;
|
|
}
|
|
if (t.classList.contains("plan-btn-edit")) {
|
|
const plan = activePlans.find(function (p) {
|
|
return String(p.id) === String(id);
|
|
});
|
|
if (plan) openEditModal(plan);
|
|
return;
|
|
}
|
|
if (t.classList.contains("plan-btn-archive")) {
|
|
const card = t.closest(".plan-active-card");
|
|
const resultEl = card && card.querySelector('.plan-close-result[data-id="' + id + '"]');
|
|
const pnlEl = card && card.querySelector('.plan-close-pnl[data-id="' + id + '"]');
|
|
const schemeEl = card && card.querySelector('.plan-active-scheme[data-id="' + id + '"]');
|
|
const result = resultEl && resultEl.value;
|
|
if (!result) {
|
|
toast("请先选择结果(盈/亏)", true);
|
|
return;
|
|
}
|
|
const scheme = schemeEl && schemeEl.value;
|
|
if (!scheme) {
|
|
toast("请先选择入场方案(根据实际进场填写)", true);
|
|
return;
|
|
}
|
|
const payload = { result: result, entry_scheme: scheme };
|
|
const pnlRaw = pnlEl && pnlEl.value;
|
|
if (pnlRaw !== "" && pnlRaw != null) payload.pnl_amount = Number(pnlRaw);
|
|
api("/api/entry-plans/" + id, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload),
|
|
})
|
|
.then(function () {
|
|
toast("已归档");
|
|
return refreshAll();
|
|
})
|
|
.catch(function (e) {
|
|
toast(e.message || "归档失败", true);
|
|
});
|
|
}
|
|
});
|
|
|
|
activeList.addEventListener("change", function (ev) {
|
|
const t = ev.target;
|
|
if (!(t instanceof HTMLElement) || !t.classList.contains("plan-active-scheme")) return;
|
|
const id = t.getAttribute("data-id");
|
|
const scheme = t.value;
|
|
if (!id || !scheme) return;
|
|
api("/api/entry-plans/" + id, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ entry_scheme: scheme }),
|
|
})
|
|
.then(function () {
|
|
toast("入场方案已保存");
|
|
return loadActive();
|
|
})
|
|
.catch(function (e) {
|
|
toast(e.message || "保存失败", true);
|
|
});
|
|
});
|
|
}
|
|
|
|
const historyList = $("plan-history-list");
|
|
if (historyList) {
|
|
historyList.addEventListener("click", function (ev) {
|
|
const row = ev.target.closest(".plan-history-row");
|
|
if (!row) return;
|
|
const id = row.getAttribute("data-id");
|
|
const plan = archivedPlans.find(function (p) {
|
|
return String(p.id) === String(id);
|
|
});
|
|
if (plan) openDetailModal(plan);
|
|
else {
|
|
api("/api/entry-plans/" + id).then(function (data) {
|
|
openDetailModal(data.plan);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll("[data-plan-modal-close]").forEach(function (el) {
|
|
el.addEventListener("click", closeDetailModal);
|
|
});
|
|
document.querySelectorAll("[data-plan-edit-close]").forEach(function (el) {
|
|
el.addEventListener("click", closeEditModal);
|
|
});
|
|
|
|
const editForm = $("plan-edit-form");
|
|
if (editForm) {
|
|
editForm.addEventListener("submit", function (ev) {
|
|
ev.preventDefault();
|
|
if (!editingPlanId) return;
|
|
api("/api/entry-plans/" + editingPlanId, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(readEditForm(editForm)),
|
|
})
|
|
.then(function () {
|
|
toast("已保存");
|
|
closeEditModal();
|
|
return refreshAll();
|
|
})
|
|
.catch(function (e) {
|
|
toast(e.message || "保存失败", true);
|
|
});
|
|
});
|
|
}
|
|
|
|
const periodTabs = $("plan-stats-period-tabs");
|
|
if (periodTabs) {
|
|
periodTabs.addEventListener("click", function (ev) {
|
|
const btn = ev.target.closest(".plan-period-btn");
|
|
if (!btn) return;
|
|
statsPeriod = btn.getAttribute("data-period") || "all";
|
|
periodTabs.querySelectorAll(".plan-period-btn").forEach(function (b) {
|
|
b.classList.toggle("is-active", b === btn);
|
|
});
|
|
const rangeWrap = $("plan-stats-range-wrap");
|
|
if (rangeWrap) rangeWrap.classList.toggle("hidden", statsPeriod !== "range");
|
|
loadStats().catch(function (e) {
|
|
toast(e.message || "统计加载失败", true);
|
|
});
|
|
});
|
|
}
|
|
|
|
const dimTabs = $("plan-stats-dim-tabs");
|
|
if (dimTabs) {
|
|
dimTabs.addEventListener("click", function (ev) {
|
|
const btn = ev.target.closest(".plan-dim-btn");
|
|
if (!btn) return;
|
|
statsDim = btn.getAttribute("data-dim") || "symbol";
|
|
dimTabs.querySelectorAll(".plan-dim-btn").forEach(function (b) {
|
|
b.classList.toggle("is-active", b === btn);
|
|
});
|
|
loadStats().catch(function (e) {
|
|
toast(e.message || "统计加载失败", true);
|
|
});
|
|
});
|
|
}
|
|
|
|
["plan-stats-date-from", "plan-stats-date-to"].forEach(function (id) {
|
|
const el = $(id);
|
|
if (!el) return;
|
|
el.addEventListener("change", function () {
|
|
statsDateFrom = ($("plan-stats-date-from") && $("plan-stats-date-from").value) || "";
|
|
statsDateTo = ($("plan-stats-date-to") && $("plan-stats-date-to").value) || "";
|
|
if (statsPeriod === "range") {
|
|
loadStats().catch(function (e) {
|
|
toast(e.message || "统计加载失败", true);
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async function init() {
|
|
if (inited) {
|
|
await refreshPage();
|
|
return;
|
|
}
|
|
inited = true;
|
|
bindEvents();
|
|
try {
|
|
await loadMeta();
|
|
await refreshAll();
|
|
const status = $("plan-refresh-status");
|
|
if (status) status.textContent = "已刷新 " + fmtRefreshTime();
|
|
} catch (e) {
|
|
toast(e.message || "加载失败", true);
|
|
}
|
|
}
|
|
|
|
function destroy() {}
|
|
|
|
window.hubPlanPage = { init: init, refresh: refreshPage, destroy: destroy };
|
|
})();
|