Add entry plan page with CRUD, archive flow, and win-rate stats.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,693 @@
|
||||
/**
|
||||
* 开仓计划:新建 / 进行中 / 历史 / 胜率统计
|
||||
*/
|
||||
(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);
|
||||
fillSelect($("plan-create-scheme"), meta.entry_schemes, "value", "label");
|
||||
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 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) +
|
||||
" · " +
|
||||
esc(p.entry_scheme_label || p.entry_scheme) +
|
||||
"</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-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 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) || "",
|
||||
entry_scheme: ($("plan-create-scheme") && $("plan-create-scheme").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 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 result = resultEl && resultEl.value;
|
||||
if (!result) {
|
||||
toast("请先选择结果(盈/亏)", true);
|
||||
return;
|
||||
}
|
||||
const payload = { result: result };
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 refreshAll();
|
||||
return;
|
||||
}
|
||||
inited = true;
|
||||
bindEvents();
|
||||
try {
|
||||
await loadMeta();
|
||||
await refreshAll();
|
||||
} catch (e) {
|
||||
toast(e.message || "加载失败", true);
|
||||
}
|
||||
}
|
||||
|
||||
function destroy() {}
|
||||
|
||||
window.hubPlanPage = { init: init, destroy: destroy };
|
||||
})();
|
||||
Reference in New Issue
Block a user