Make options review detail a modal and fix screenshot display.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 · 期期情景测算:盈亏配色 + 盈亏比(亏=全额保费)
|
||||
|
||||
### 修改原因
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user