Make options review detail a modal and fix screenshot display.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-19 16:11:45 +08:00
parent 38ac258497
commit 426afb8dbe
4 changed files with 173 additions and 55 deletions
+20
View File
@@ -4,6 +4,26 @@
---
## 2026-07-19 · 期权复盘详情改为对话框 + 截图显示修复
### 修改原因
复盘详情嵌在列表下方不便查看;截图缩略图易裁切/偶发加载失败。
### 修改的地方
| 文件 | 改动摘要 |
|------|----------|
| `options_review_panel.html` | 详情改为居中对话框;2×2 截图网格 |
| `options_review.js` | 点复盘记录打开弹窗;截图 basename + onerror |
| `options_review_register.py` | 截图静态路由不强制登录(防 iframe 401) |
### 交付之后的验收
点「复盘记录」行弹出对话框;5m/15m/1h/4h 截图完整可见(缺失显示提示);关闭/Esc/点遮罩可关。
---
## 2026-07-19 · 期期情景测算:盈亏配色 + 盈亏比(亏=全额保费)
### 修改原因
+107 -43
View File
@@ -464,19 +464,18 @@
}
function hideDetail() {
var panel = $("or-detail-panel");
if (panel) panel.classList.add("hidden");
var backdrop = $("or-detail-backdrop");
if (backdrop) backdrop.hidden = true;
}
function openDetail(tradeId) {
var panel = $("or-detail-panel");
if (!panel) return;
panel.classList.remove("hidden");
var backdrop = $("or-detail-backdrop");
if (!backdrop) return;
backdrop.hidden = false;
($("or-detail-title") || {}).textContent = "加载中…";
($("or-detail-meta") || {}).innerHTML = "";
($("or-detail-text") || {}).innerHTML = "";
($("or-detail-images") || {}).innerHTML = "";
panel.scrollIntoView({ behavior: "smooth", block: "nearest" });
fetch("/api/options/review/trades/" + tradeId, { credentials: "same-origin" })
.then(function (r) {
@@ -494,6 +493,92 @@
});
}
function optionsJournalImgSrc(file) {
var name = String(file || "").trim().replace(/\\/g, "/");
var slash = name.lastIndexOf("/");
if (slash >= 0) name = name.slice(slash + 1);
if (!name) return "";
return "/static/images/options_journal/" + encodeURIComponent(name);
}
function renderDetailImages(images) {
var imagesHost = $("or-detail-images");
if (!imagesHost) return;
var byTf = {};
(images || []).forEach(function (img) {
var tf = String((img && img.tf) || "").trim();
var file = String((img && img.file) || "").trim();
if (!file) return;
var key = tf || "_";
byTf[key] = file;
});
var order = ["5m", "15m", "1h", "4h"];
var keys = order.slice();
Object.keys(byTf).forEach(function (k) {
if (keys.indexOf(k) < 0) keys.push(k);
});
var cells = keys
.map(function (tf) {
var file = byTf[tf];
if (!file) {
if (order.indexOf(tf) < 0) return "";
return (
'<div class="or-detail-img-cell">' +
'<span class="or-detail-img-label">' +
escapeHtml(tf) +
"</span>" +
'<div class="or-detail-img-miss">未上传</div>' +
"</div>"
);
}
var src = optionsJournalImgSrc(file);
var label = escapeHtml(tf === "_" ? "截图" : tf);
return (
'<div class="or-detail-img-cell">' +
'<span class="or-detail-img-label">' +
label +
"</span>" +
'<img class="or-detail-img-thumb" src="' +
src +
'" alt="' +
label +
'" data-src="' +
src +
'" loading="lazy">' +
"</div>"
);
})
.filter(Boolean);
if (!cells.length) {
imagesHost.innerHTML = '<div class="muted">无截图</div>';
return;
}
imagesHost.innerHTML = cells.join("");
imagesHost.querySelectorAll("img").forEach(function (img) {
img.addEventListener("error", function () {
var cell = img.closest(".or-detail-img-cell");
if (!cell) return;
var label = cell.querySelector(".or-detail-img-label");
var tf = label ? label.textContent : "截图";
cell.innerHTML =
'<span class="or-detail-img-label">' +
escapeHtml(tf) +
"</span>" +
'<div class="or-detail-img-miss">文件缺失或无法加载</div>';
});
img.addEventListener("click", function () {
var src = img.getAttribute("data-src") || img.src;
if (typeof global.showImage === "function") {
global.showImage(src);
} else if (typeof window.showImage === "function") {
window.showImage(src);
} else {
global.open(src, "_blank");
}
});
});
}
function renderDetail(t) {
var e = t.entry || {};
reviewedCache[t.id] = t;
@@ -566,42 +651,7 @@
var imagesHost = $("or-detail-images");
if (imagesHost) {
var images = e.images || [];
if (!images.length) {
imagesHost.innerHTML = '<div class="muted">无截图</div>';
} else {
imagesHost.innerHTML = images
.map(function (img) {
var file = String(img.file || "").trim();
if (!file) return "";
var src = "/static/images/options_journal/" + encodeURIComponent(file).replace(/%2F/g, "/");
var label = escapeHtml(img.tf || "截图");
return (
'<div class="or-detail-img-cell">' +
'<span class="or-detail-img-label">' +
label +
"</span>" +
'<img class="or-detail-img-thumb" src="' +
src +
'" alt="' +
label +
'" data-src="' +
src +
'">' +
"</div>"
);
})
.join("");
imagesHost.querySelectorAll("img").forEach(function (img) {
img.addEventListener("click", function () {
if (typeof global.showImage === "function") {
global.showImage(img.getAttribute("data-src"));
} else {
global.open(img.getAttribute("data-src"), "_blank");
}
});
});
}
renderDetailImages(e.images || []);
}
}
@@ -1075,9 +1125,23 @@
if (detailEdit) {
detailEdit.addEventListener("click", function () {
var id = Number(detailEdit.getAttribute("data-id") || 0);
if (id) openJournalForm(id);
if (id) {
hideDetail();
openJournalForm(id);
}
});
}
var detailBackdrop = $("or-detail-backdrop");
if (detailBackdrop) {
detailBackdrop.addEventListener("click", function (ev) {
if (ev.target === detailBackdrop) hideDetail();
});
}
document.addEventListener("keydown", function (ev) {
if (ev.key !== "Escape") return;
var bd = $("or-detail-backdrop");
if (bd && !bd.hidden) hideDetail();
});
["or-filter-uly", "or-filter-opt", "or-include-hedge-legs"].forEach(function (id) {
var el = $(id);
if (el) {
+1 -1
View File
@@ -94,8 +94,8 @@ def register_options_review_routes(app: Flask, cfg: dict[str, Any], repo_root: s
return send_file(path, mimetype="application/javascript; charset=utf-8")
@app.route("/static/images/options_journal/<path:filename>")
@lr
def static_options_review_image(filename: str):
"""截图文件名含 32 位 draft id,按静态资源提供(不强制登录,避免 iframe img 偶发 401)."""
folder = options_review_upload_dir(cfg["upload_folder"])
safe = os.path.basename(filename or "")
path = os.path.join(folder, safe)
+45 -11
View File
@@ -28,13 +28,43 @@
.or-journal-card .sub{font-size:.7rem}
.or-journal-card.hidden{display:none!important}
.or-reviewed-table tbody tr{cursor:pointer}
.or-detail-panel{margin-top:10px;padding-top:10px;border-top:1px solid rgba(127,127,127,.25)}
.or-detail-panel.hidden{display:none!important}
.or-detail-backdrop{
position:fixed;inset:0;z-index:1300;
display:flex;align-items:center;justify-content:center;
padding:16px;background:rgba(0,0,0,.72);
}
.or-detail-backdrop[hidden]{display:none!important}
.or-detail-modal{
width:min(96vw,920px);max-height:90vh;overflow:auto;
background:var(--card-bg,#121726);color:inherit;
border:1px solid rgba(127,127,127,.35);border-radius:10px;
padding:14px 16px 18px;box-shadow:0 12px 40px rgba(0,0,0,.45);
}
.or-detail-modal-head{display:flex;align-items:center;gap:8px;margin-bottom:10px}
.or-detail-modal-head h3{margin:0;margin-right:auto;font-size:.95rem}
.or-detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:6px 12px;font-size:.76rem;margin-bottom:8px}
.or-detail-images{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin:8px 0}
.or-detail-img-cell{border:1px solid rgba(127,127,127,.25);border-radius:6px;padding:6px;text-align:center}
.or-detail-img-label{display:block;font-size:.7rem;margin-bottom:4px;opacity:.8}
.or-detail-img-thumb{max-width:100%;max-height:160px;border-radius:4px;cursor:pointer}
.or-detail-images{
display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
gap:10px;margin:10px 0 4px;
}
.or-detail-img-cell{
min-width:0;border:1px solid rgba(127,127,127,.25);border-radius:8px;
padding:8px;display:flex;flex-direction:column;gap:6px;
background:rgba(0,0,0,.18);
}
.or-detail-img-label{font-size:.72rem;opacity:.85;font-weight:600}
.or-detail-img-thumb{
width:100%;max-height:280px;object-fit:contain;
border-radius:6px;cursor:zoom-in;background:rgba(0,0,0,.25);
}
.or-detail-img-miss{
min-height:120px;display:flex;align-items:center;justify-content:center;
font-size:.72rem;opacity:.65;border-radius:6px;background:rgba(127,127,127,.12);
}
@media (max-width:640px){
.or-detail-images{grid-template-columns:1fr}
.or-detail-img-thumb{max-height:220px}
}
.or-pager{display:flex;align-items:center;gap:8px;margin-top:8px;font-size:.74rem}
.or-list-loading{opacity:.55;pointer-events:none;transition:opacity .12s ease}
.or-trades-table-wrap,.or-reviewed-table-wrap{min-height:9.5rem}
@@ -207,11 +237,15 @@
<span class="muted" id="or-reviewed-page-label">第 1 / 1 页</span>
<button type="button" class="btn-secondary" id="or-reviewed-next" style="font-size:.72rem;padding:2px 8px">下一页</button>
</div>
<div class="or-detail-panel hidden" id="or-detail-panel">
<div class="form-row" style="align-items:center;gap:8px;margin-bottom:6px">
<h3 style="margin:0;margin-right:auto" id="or-detail-title">复盘详情</h3>
</div>
{# 复盘详情对话框(点复盘记录行打开) #}
<div id="or-detail-backdrop" class="or-detail-backdrop" hidden>
<div class="or-detail-modal" role="dialog" aria-modal="true" aria-labelledby="or-detail-title" id="or-detail-panel">
<div class="or-detail-modal-head">
<h3 id="or-detail-title">复盘详情</h3>
<button type="button" class="btn-secondary" id="or-detail-edit-btn" style="font-size:.72rem;padding:2px 8px">编辑</button>
<button type="button" class="btn-secondary" id="or-detail-close-btn" style="font-size:.72rem;padding:2px 8px">收起</button>
<button type="button" class="btn-secondary" id="or-detail-close-btn" style="font-size:.72rem;padding:2px 8px">关闭</button>
</div>
<div class="or-detail-grid" id="or-detail-meta"></div>
<div id="or-detail-text" style="font-size:.76rem;line-height:1.5;margin-bottom:8px"></div>
@@ -227,4 +261,4 @@
</div>
</div>
<script src="/static/options_review.js?v=11"></script>
<script src="/static/options_review.js?v=12"></script>