Fix options review screenshots missing after journal upload hijack.
Stop journal_upload_slots from binding options slots; resolve journal_* files from static/images root so existing reviews display again. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -93,8 +93,16 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isOptionsReviewSlot(input) {
|
||||||
|
if (!input) return false;
|
||||||
|
if (input.classList && input.classList.contains("or-upload-input")) return true;
|
||||||
|
return !!(input.closest && input.closest("#or-upload-slots, #options-review-root"));
|
||||||
|
}
|
||||||
|
|
||||||
function bindInput(input) {
|
function bindInput(input) {
|
||||||
if (!input || input.dataset.journalSlotBound === "1") return;
|
if (!input || input.dataset.journalSlotBound === "1") return;
|
||||||
|
// 期权复盘槽位由 options_review.js 处理,勿被合约复盘上传抢走
|
||||||
|
if (isOptionsReviewSlot(input)) return;
|
||||||
input.dataset.journalSlotBound = "1";
|
input.dataset.journalSlotBound = "1";
|
||||||
input.addEventListener("change", function () {
|
input.addEventListener("change", function () {
|
||||||
var file = input.files && input.files[0];
|
var file = input.files && input.files[0];
|
||||||
|
|||||||
@@ -498,7 +498,12 @@
|
|||||||
var slash = name.lastIndexOf("/");
|
var slash = name.lastIndexOf("/");
|
||||||
if (slash >= 0) name = name.slice(slash + 1);
|
if (slash >= 0) name = name.slice(slash + 1);
|
||||||
if (!name) return "";
|
if (!name) return "";
|
||||||
return "/static/images/options_journal/" + encodeURIComponent(name);
|
// options_journal_* 在子目录;误走合约上传的 journal_* 在 static/images 根目录
|
||||||
|
var base =
|
||||||
|
name.toLowerCase().indexOf("options_journal_") === 0
|
||||||
|
? "/static/images/options_journal/"
|
||||||
|
: "/static/images/";
|
||||||
|
return base + encodeURIComponent(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDetailImages(images) {
|
function renderDetailImages(images) {
|
||||||
@@ -940,7 +945,21 @@
|
|||||||
);
|
);
|
||||||
if (hidden && img.file) {
|
if (hidden && img.file) {
|
||||||
hidden.value = img.file;
|
hidden.value = img.file;
|
||||||
if (status) status.textContent = "已有 " + img.file;
|
if (status) {
|
||||||
|
var src = optionsJournalImgSrc(img.file);
|
||||||
|
status.innerHTML =
|
||||||
|
'已有 <a href="' +
|
||||||
|
src +
|
||||||
|
'" target="_blank" rel="noopener">' +
|
||||||
|
escapeHtml(img.file) +
|
||||||
|
'</a><br><img class="or-slot-thumb" src="' +
|
||||||
|
src +
|
||||||
|
'" alt="' +
|
||||||
|
escapeHtml(img.tf || "") +
|
||||||
|
'" loading="lazy">';
|
||||||
|
status.className =
|
||||||
|
"journal-upload-status or-upload-status journal-upload-status--ok";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/instance_ui.js?v=10"></script>
|
<script src="/static/instance_ui.js?v=10"></script>
|
||||||
<script src="/static/journal_upload_slots.js?v=3"></script>
|
<script src="/static/journal_upload_slots.js?v=4"></script>
|
||||||
<script src="/static/instance_records_mobile.js?v=2"></script>
|
<script src="/static/instance_records_mobile.js?v=2"></script>
|
||||||
<script src="/static/time_close_ui.js?v=3"></script>
|
<script src="/static/time_close_ui.js?v=3"></script>
|
||||||
<script src="/static/ai_review_render.js?v=2"></script>
|
<script src="/static/ai_review_render.js?v=2"></script>
|
||||||
|
|||||||
@@ -456,7 +456,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/instance_ui.js?v=10"></script>
|
<script src="/static/instance_ui.js?v=10"></script>
|
||||||
<script src="/static/journal_upload_slots.js?v=3"></script>
|
<script src="/static/journal_upload_slots.js?v=4"></script>
|
||||||
<script src="/static/instance_records_mobile.js?v=2"></script>
|
<script src="/static/instance_records_mobile.js?v=2"></script>
|
||||||
<script src="/static/time_close_ui.js?v=3"></script>
|
<script src="/static/time_close_ui.js?v=3"></script>
|
||||||
<script src="/static/ai_review_render.js?v=2"></script>
|
<script src="/static/ai_review_render.js?v=2"></script>
|
||||||
|
|||||||
@@ -112,17 +112,23 @@ def images_json_dumps(items: Sequence[Mapping[str, str]]) -> Optional[str]:
|
|||||||
|
|
||||||
|
|
||||||
def options_review_image_paths(row: Any, upload_folder: str) -> List[str]:
|
def options_review_image_paths(row: Any, upload_folder: str) -> List[str]:
|
||||||
upload_folder = os.path.abspath(upload_folder or "")
|
upload_root = os.path.abspath(upload_folder or "")
|
||||||
|
options_dir = options_review_upload_dir(upload_root)
|
||||||
paths: List[str] = []
|
paths: List[str] = []
|
||||||
seen: set[str] = set()
|
seen: set[str] = set()
|
||||||
|
|
||||||
def _add(name: Optional[str]) -> None:
|
def _add(name: Optional[str]) -> None:
|
||||||
if not name:
|
if not name:
|
||||||
return
|
return
|
||||||
p = os.path.abspath(os.path.join(upload_folder, str(name).strip()))
|
base = os.path.basename(str(name).strip())
|
||||||
|
if not base:
|
||||||
|
return
|
||||||
|
for folder in (options_dir, upload_root):
|
||||||
|
p = os.path.abspath(os.path.join(folder, base))
|
||||||
if os.path.isfile(p) and p not in seen:
|
if os.path.isfile(p) and p not in seen:
|
||||||
seen.add(p)
|
seen.add(p)
|
||||||
paths.append(p)
|
paths.append(p)
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
keys = row.keys() if hasattr(row, "keys") else ()
|
keys = row.keys() if hasattr(row, "keys") else ()
|
||||||
|
|||||||
@@ -100,6 +100,12 @@ def register_options_review_routes(app: Flask, cfg: dict[str, Any], repo_root: s
|
|||||||
safe = os.path.basename(filename or "")
|
safe = os.path.basename(filename or "")
|
||||||
path = os.path.join(folder, safe)
|
path = os.path.join(folder, safe)
|
||||||
if not os.path.isfile(path):
|
if not os.path.isfile(path):
|
||||||
|
# 兼容误走合约 journal 上传、落在 UPLOAD_FOLDER 根目录的文件
|
||||||
|
root = os.path.abspath(cfg["upload_folder"] or "")
|
||||||
|
alt = os.path.join(root, safe)
|
||||||
|
if os.path.isfile(alt):
|
||||||
|
path = alt
|
||||||
|
else:
|
||||||
return ("not found", 404)
|
return ("not found", 404)
|
||||||
return send_file(path)
|
return send_file(path)
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,11 @@
|
|||||||
.or-pager{display:flex;align-items:center;gap:8px;margin-top:8px;font-size:.74rem}
|
.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-list-loading{opacity:.55;pointer-events:none;transition:opacity .12s ease}
|
||||||
.or-trades-table-wrap,.or-reviewed-table-wrap{min-height:9.5rem}
|
.or-trades-table-wrap,.or-reviewed-table-wrap{min-height:9.5rem}
|
||||||
|
.or-slot-thumb{
|
||||||
|
display:block;margin-top:6px;max-width:160px;max-height:90px;
|
||||||
|
object-fit:contain;border-radius:4px;border:1px solid rgba(127,127,127,.3);
|
||||||
|
background:rgba(0,0,0,.2);cursor:zoom-in;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
{# 1. 交易记录(含 Tab/筛选,固定约5行) #}
|
{# 1. 交易记录(含 Tab/筛选,固定约5行) #}
|
||||||
@@ -176,13 +181,12 @@
|
|||||||
<option value="">入场逻辑</option>
|
<option value="">入场逻辑</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<input type="hidden" id="journal-draft-id" value="">
|
|
||||||
<div class="journal-upload-slots" id="or-upload-slots">
|
<div class="journal-upload-slots" id="or-upload-slots">
|
||||||
{% for tf in ['5m', '15m', '1h', '4h'] %}
|
{% for tf in ['5m', '15m', '1h', '4h'] %}
|
||||||
<div class="journal-upload-row" data-tf="{{ tf }}">
|
<div class="journal-upload-row" data-tf="{{ tf }}">
|
||||||
<span class="journal-upload-slot-label">{{ tf }}</span>
|
<span class="journal-upload-slot-label">{{ tf }}</span>
|
||||||
<input type="file" accept="image/*" class="journal-upload-slot-input or-upload-input" data-tf="{{ tf }}">
|
<input type="file" accept="image/*" class="or-upload-input" data-tf="{{ tf }}">
|
||||||
<input type="hidden" class="journal-upload-hidden-file or-upload-hidden" data-tf="{{ tf }}" value="">
|
<input type="hidden" class="or-upload-hidden" data-tf="{{ tf }}" value="">
|
||||||
<span class="journal-upload-status or-upload-status" data-tf="{{ tf }}" aria-live="polite"></span>
|
<span class="journal-upload-status or-upload-status" data-tf="{{ tf }}" aria-live="polite"></span>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -261,4 +265,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/options_review.js?v=13"></script>
|
<script src="/static/options_review.js?v=14"></script>
|
||||||
|
|||||||
@@ -284,6 +284,31 @@ class OptionsReviewTests(unittest.TestCase):
|
|||||||
self.assertEqual(float(row["realized_pnl_total"]), 3.2)
|
self.assertEqual(float(row["realized_pnl_total"]), 3.2)
|
||||||
self.assertEqual(row["source_type"], SOURCE_OPTION)
|
self.assertEqual(row["source_type"], SOURCE_OPTION)
|
||||||
|
|
||||||
|
def test_image_paths_resolve_legacy_journal_root(self):
|
||||||
|
with tempfile.TemporaryDirectory() as td:
|
||||||
|
root = Path(td)
|
||||||
|
# 误存到 UPLOAD 根目录的 journal_*
|
||||||
|
legacy = root / "journal_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_5m.png"
|
||||||
|
legacy.write_bytes(b"img")
|
||||||
|
# 正常 options_journal 子目录
|
||||||
|
sub = root / "options_journal"
|
||||||
|
sub.mkdir()
|
||||||
|
modern = sub / "options_journal_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_5m.png"
|
||||||
|
modern.write_bytes(b"img2")
|
||||||
|
from lib.options.options_review_images_lib import options_review_image_paths
|
||||||
|
|
||||||
|
class Row:
|
||||||
|
images_json = (
|
||||||
|
'[{"tf":"5m","file":"journal_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_5m.png"},'
|
||||||
|
'{"tf":"5m","file":"options_journal_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_5m.png"}]'
|
||||||
|
)
|
||||||
|
image = None
|
||||||
|
|
||||||
|
paths = options_review_image_paths(Row(), str(root))
|
||||||
|
self.assertEqual(len(paths), 2)
|
||||||
|
self.assertTrue(any(p.endswith(legacy.name) for p in paths))
|
||||||
|
self.assertTrue(any(p.endswith(modern.name) for p in paths))
|
||||||
|
|
||||||
def test_image_namespace(self):
|
def test_image_namespace(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
folder = options_review_upload_dir(tmp)
|
folder = options_review_upload_dir(tmp)
|
||||||
|
|||||||
Reference in New Issue
Block a user