feat: 复盘四周期手动上传,移除 AI 识别预填
固定 5m/15m/1h/4h 四槽截图上传与详情四宫格展示;自动 K 线默认关闭且与手动上传互斥。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+16
-16
@@ -12,6 +12,7 @@ from lib.instance.journal_chart_lib import (
|
||||
JOURNAL_CHART_DEFAULT_TF2,
|
||||
normalize_chart_timeframe,
|
||||
)
|
||||
from lib.instance.journal_images_lib import journal_image_paths
|
||||
|
||||
|
||||
def _journal_nz(v: Any, default: str = "无") -> str:
|
||||
@@ -92,32 +93,31 @@ def collect_images_for_ai_review(
|
||||
) -> List[str]:
|
||||
"""
|
||||
收集传给视觉模型的本地图片路径。
|
||||
- 优先 journal_entries.image 已存附图;
|
||||
- 优先 journal_entries.images_json / image 已存附图(含多周期手动上传);
|
||||
- 若无附图且提供 build_chart_if_missing,则临时生成 K 线图。
|
||||
"""
|
||||
paths: List[str] = []
|
||||
seen = set()
|
||||
upload_folder = os.path.abspath(upload_folder or "")
|
||||
for row in rows or []:
|
||||
candidate = None
|
||||
try:
|
||||
keys = row.keys() if hasattr(row, "keys") else []
|
||||
except Exception:
|
||||
keys = []
|
||||
img = row["image"] if "image" in keys else None
|
||||
if img:
|
||||
candidate = os.path.join(upload_folder, str(img).strip())
|
||||
elif build_chart_if_missing:
|
||||
row_paths = journal_image_paths(row, upload_folder)
|
||||
if row_paths:
|
||||
for candidate in row_paths:
|
||||
if candidate not in seen:
|
||||
seen.add(candidate)
|
||||
paths.append(candidate)
|
||||
continue
|
||||
if build_chart_if_missing:
|
||||
try:
|
||||
candidate = build_chart_if_missing(row)
|
||||
except Exception:
|
||||
candidate = None
|
||||
if not candidate:
|
||||
continue
|
||||
candidate = os.path.abspath(candidate)
|
||||
if os.path.isfile(candidate) and candidate not in seen:
|
||||
seen.add(candidate)
|
||||
paths.append(candidate)
|
||||
if not candidate:
|
||||
continue
|
||||
candidate = os.path.abspath(candidate)
|
||||
if os.path.isfile(candidate) and candidate not in seen:
|
||||
seen.add(candidate)
|
||||
paths.append(candidate)
|
||||
return paths
|
||||
|
||||
|
||||
|
||||
@@ -89,6 +89,10 @@
|
||||
const root = document.getElementById("embed-page-root") || document;
|
||||
global.SymbolLivePrice.init(root);
|
||||
}
|
||||
if (global.JournalUploadSlots && typeof global.JournalUploadSlots.init === "function") {
|
||||
const root = document.getElementById("embed-page-root") || document;
|
||||
global.JournalUploadSlots.init(root);
|
||||
}
|
||||
}
|
||||
|
||||
function injectFragment(html) {
|
||||
|
||||
@@ -1659,3 +1659,108 @@ html[data-theme="light"] .symbol-live-price--ok {
|
||||
border-color: #9ed4b8;
|
||||
}
|
||||
|
||||
/* ── 复盘:四周期截图上传 / 详情四宫格 ── */
|
||||
.journal-upload-slots {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.journal-upload-slot {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 8px 10px;
|
||||
border: 1px dashed rgba(130, 145, 190, 0.45);
|
||||
border-radius: 8px;
|
||||
background: rgba(12, 16, 32, 0.35);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.journal-upload-slot-label {
|
||||
color: #9aa3c7;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.journal-upload-slot-input {
|
||||
font-size: 0.78rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.journal-upload-slot-preview {
|
||||
min-height: 72px;
|
||||
border-radius: 6px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.journal-upload-slot-preview--filled {
|
||||
min-height: 96px;
|
||||
}
|
||||
|
||||
.journal-upload-slot-thumb {
|
||||
width: 100%;
|
||||
max-height: 140px;
|
||||
object-fit: contain;
|
||||
cursor: zoom-in;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.journal-upload-hint {
|
||||
margin-top: 6px;
|
||||
font-size: 0.72rem;
|
||||
color: #8892b0;
|
||||
}
|
||||
|
||||
.journal-detail-images {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
padding: 10px 14px 14px;
|
||||
border-top: 1px solid rgba(130, 145, 190, 0.25);
|
||||
}
|
||||
|
||||
.journal-detail-img-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.journal-detail-img-label {
|
||||
font-size: 0.75rem;
|
||||
color: #9aa3c7;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.journal-detail-img-thumb {
|
||||
width: 100%;
|
||||
max-height: 220px;
|
||||
object-fit: contain;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border-radius: 6px;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .journal-upload-slot {
|
||||
background: #f6f8fb;
|
||||
border-color: #c5d0de;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .journal-upload-slot-preview {
|
||||
background: #eef2f7;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .journal-detail-images {
|
||||
border-top-color: #d0dae4;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .journal-detail-img-thumb {
|
||||
background: #eef2f7;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,76 @@
|
||||
return lines.join("<br>");
|
||||
}
|
||||
|
||||
function resolveJournalImages(o) {
|
||||
if (Array.isArray(o.images) && o.images.length) return o.images;
|
||||
if (o.image) return [{ tf: "", file: o.image }];
|
||||
return [];
|
||||
}
|
||||
|
||||
function setJournalDetailImages(o) {
|
||||
const grid = document.getElementById("detailImages");
|
||||
const legacyImg = document.getElementById("detailImage");
|
||||
const images = resolveJournalImages(o || {});
|
||||
|
||||
if (grid) {
|
||||
if (!images.length) {
|
||||
grid.innerHTML = "";
|
||||
grid.style.display = "none";
|
||||
} else {
|
||||
grid.innerHTML = images
|
||||
.map(function (img) {
|
||||
const tf = String(img.tf || "").trim();
|
||||
const file = String(img.file || "").trim();
|
||||
if (!file) return "";
|
||||
const label = tf ? escapeHtml(tf) : "截图";
|
||||
const src = "/static/images/" + encodeURIComponent(file).replace(/%2F/g, "/");
|
||||
return (
|
||||
'<div class="journal-detail-img-cell">' +
|
||||
'<span class="journal-detail-img-label">' +
|
||||
label +
|
||||
"</span>" +
|
||||
'<img class="journal-detail-img-thumb" src="' +
|
||||
src +
|
||||
'" alt="' +
|
||||
label +
|
||||
'" onclick="showImage(this.src)">' +
|
||||
"</div>"
|
||||
);
|
||||
})
|
||||
.join("");
|
||||
grid.style.display = "grid";
|
||||
}
|
||||
if (legacyImg) {
|
||||
legacyImg.src = "";
|
||||
legacyImg.style.display = "none";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (legacyImg) {
|
||||
if (images.length === 1) {
|
||||
legacyImg.src = "/static/images/" + images[0].file;
|
||||
legacyImg.style.display = "block";
|
||||
} else {
|
||||
legacyImg.src = "";
|
||||
legacyImg.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearJournalDetailImages() {
|
||||
const grid = document.getElementById("detailImages");
|
||||
if (grid) {
|
||||
grid.innerHTML = "";
|
||||
grid.style.display = "none";
|
||||
}
|
||||
const legacyImg = document.getElementById("detailImage");
|
||||
if (legacyImg) {
|
||||
legacyImg.src = "";
|
||||
legacyImg.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function setJournalDetailBody(o, formatExitLine) {
|
||||
const body = document.getElementById("detailBody");
|
||||
if (!body) return;
|
||||
@@ -67,16 +137,7 @@
|
||||
}
|
||||
setJournalDetailBody(o, formatExitLine);
|
||||
clearDetailActions();
|
||||
const imgEl = document.getElementById("detailImage");
|
||||
if (imgEl) {
|
||||
if (o.image) {
|
||||
imgEl.src = `/static/images/${o.image}`;
|
||||
imgEl.style.display = "block";
|
||||
} else {
|
||||
imgEl.src = "";
|
||||
imgEl.style.display = "none";
|
||||
}
|
||||
}
|
||||
setJournalDetailImages(o);
|
||||
if (typeof setDetailModalFullscreen === "function") {
|
||||
setDetailModalFullscreen(false);
|
||||
}
|
||||
@@ -265,5 +326,7 @@
|
||||
buildTradeRecordDetailHtml: buildTradeRecordDetailHtml,
|
||||
openTradeRecordDetailModal: openTradeRecordDetailModal,
|
||||
clearDetailActions: clearDetailActions,
|
||||
clearJournalDetailImages: clearJournalDetailImages,
|
||||
setJournalDetailImages: setJournalDetailImages,
|
||||
};
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 复盘表单:四周期截图槽位本地预览。
|
||||
*/
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
const previewUrls = new WeakMap();
|
||||
|
||||
function clearPreview(cell) {
|
||||
if (!cell) return;
|
||||
const prev = previewUrls.get(cell);
|
||||
if (prev) {
|
||||
URL.revokeObjectURL(prev);
|
||||
previewUrls.delete(cell);
|
||||
}
|
||||
cell.innerHTML = "";
|
||||
cell.classList.remove("journal-upload-slot-preview--filled");
|
||||
}
|
||||
|
||||
function renderPreview(cell, file, tf) {
|
||||
clearPreview(cell);
|
||||
if (!file || !cell) return;
|
||||
const url = URL.createObjectURL(file);
|
||||
previewUrls.set(cell, url);
|
||||
const img = document.createElement("img");
|
||||
img.src = url;
|
||||
img.alt = tf + " 预览";
|
||||
img.className = "journal-upload-slot-thumb";
|
||||
img.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
if (typeof global.showImage === "function") {
|
||||
global.showImage(url);
|
||||
}
|
||||
});
|
||||
cell.appendChild(img);
|
||||
cell.classList.add("journal-upload-slot-preview--filled");
|
||||
}
|
||||
|
||||
function bindInput(input) {
|
||||
if (!input || input.dataset.journalSlotBound === "1") return;
|
||||
input.dataset.journalSlotBound = "1";
|
||||
const tf = input.getAttribute("data-tf") || "";
|
||||
const cell = input.closest(".journal-upload-slot");
|
||||
const preview = cell ? cell.querySelector(".journal-upload-slot-preview") : null;
|
||||
input.addEventListener("change", function () {
|
||||
const file = input.files && input.files[0];
|
||||
if (!file) {
|
||||
clearPreview(preview);
|
||||
return;
|
||||
}
|
||||
renderPreview(preview, file, tf);
|
||||
});
|
||||
}
|
||||
|
||||
function init(root) {
|
||||
const scope = root || document;
|
||||
scope.querySelectorAll(".journal-upload-slot-input").forEach(bindInput);
|
||||
}
|
||||
|
||||
global.JournalUploadSlots = { init: init };
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
init(document);
|
||||
});
|
||||
} else {
|
||||
init(document);
|
||||
}
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
@@ -64,6 +64,7 @@ def install_instance_theme_static(app) -> None:
|
||||
"time_close_ui.js": "application/javascript; charset=utf-8",
|
||||
"manual_order_rr_preview.js": "application/javascript; charset=utf-8",
|
||||
"symbol_live_price.js": "application/javascript; charset=utf-8",
|
||||
"journal_upload_slots.js": "application/javascript; charset=utf-8",
|
||||
"strategy_roll.js": "application/javascript; charset=utf-8",
|
||||
"instance_page.css": "text/css; charset=utf-8",
|
||||
"instance_embed.js": "application/javascript; charset=utf-8",
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
"""复盘记录:多周期截图上传、存储与读取(三所共用)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence
|
||||
|
||||
JOURNAL_UPLOAD_TFS: tuple[str, ...] = ("5m", "15m", "1h", "4h")
|
||||
JOURNAL_UPLOAD_ALLOWED_EXT = frozenset({".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"})
|
||||
|
||||
|
||||
def journal_upload_field_name(tf: str) -> str:
|
||||
return f"screenshot_{tf}"
|
||||
|
||||
|
||||
def _safe_ext(filename: str) -> str:
|
||||
ext = os.path.splitext(str(filename or ""))[1].lower()
|
||||
return ext if ext in JOURNAL_UPLOAD_ALLOWED_EXT else ".png"
|
||||
|
||||
|
||||
def save_journal_slot_uploads(
|
||||
files,
|
||||
entry_id: str,
|
||||
upload_folder: str,
|
||||
*,
|
||||
secure_filename_fn: Callable[[str], str],
|
||||
) -> List[Dict[str, str]]:
|
||||
"""保存四槽位手动截图,返回 [{"tf":"5m","file":"journal_xxx_5m.png"}, ...]。"""
|
||||
saved: List[Dict[str, str]] = []
|
||||
if not entry_id or not upload_folder:
|
||||
return saved
|
||||
os.makedirs(upload_folder, exist_ok=True)
|
||||
for tf in JOURNAL_UPLOAD_TFS:
|
||||
f = files.get(journal_upload_field_name(tf)) if files else None
|
||||
if not f or not getattr(f, "filename", None):
|
||||
continue
|
||||
ext = _safe_ext(f.filename)
|
||||
fname = secure_filename_fn(f"journal_{entry_id}_{tf}{ext}")
|
||||
if not fname:
|
||||
continue
|
||||
path = os.path.join(upload_folder, fname)
|
||||
f.save(path)
|
||||
saved.append({"tf": tf, "file": fname})
|
||||
return saved
|
||||
|
||||
|
||||
def images_json_dumps(items: Sequence[Mapping[str, str]]) -> Optional[str]:
|
||||
if not items:
|
||||
return None
|
||||
return json.dumps(list(items), ensure_ascii=False, separators=(",", ":"))
|
||||
|
||||
|
||||
def parse_images_json(raw: Any) -> List[Dict[str, str]]:
|
||||
if not raw:
|
||||
return []
|
||||
if isinstance(raw, list):
|
||||
data = raw
|
||||
else:
|
||||
try:
|
||||
data = json.loads(str(raw))
|
||||
except (TypeError, ValueError, json.JSONDecodeError):
|
||||
return []
|
||||
if not isinstance(data, list):
|
||||
return []
|
||||
out: List[Dict[str, str]] = []
|
||||
for item in data:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
tf = str(item.get("tf") or "").strip()
|
||||
file = str(item.get("file") or "").strip()
|
||||
if file:
|
||||
out.append({"tf": tf, "file": file})
|
||||
return out
|
||||
|
||||
|
||||
def primary_journal_image(
|
||||
manual_images: Sequence[Mapping[str, str]],
|
||||
*,
|
||||
fallback: Optional[str] = None,
|
||||
) -> Optional[str]:
|
||||
if manual_images:
|
||||
return str(manual_images[0].get("file") or "").strip() or None
|
||||
return fallback
|
||||
|
||||
|
||||
def enrich_journal_api_item(item: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""API 输出:解析 images_json,兼容旧单图 image 字段。"""
|
||||
images = parse_images_json(item.get("images_json"))
|
||||
if not images and item.get("image"):
|
||||
images = [{"tf": "", "file": str(item["image"]).strip()}]
|
||||
item["images"] = images
|
||||
return item
|
||||
|
||||
|
||||
def journal_image_paths(row: Any, upload_folder: str) -> List[str]:
|
||||
"""删除 / AI 附图:收集本条复盘所有本地图片路径(去重)。"""
|
||||
upload_folder = os.path.abspath(upload_folder or "")
|
||||
paths: List[str] = []
|
||||
seen = set()
|
||||
|
||||
def _add(name: Optional[str]) -> None:
|
||||
if not name:
|
||||
return
|
||||
p = os.path.abspath(os.path.join(upload_folder, str(name).strip()))
|
||||
if os.path.isfile(p) and p not in seen:
|
||||
seen.add(p)
|
||||
paths.append(p)
|
||||
|
||||
try:
|
||||
keys = row.keys() if hasattr(row, "keys") else ()
|
||||
except Exception:
|
||||
keys = ()
|
||||
|
||||
if "images_json" in keys and row["images_json"]:
|
||||
for img in parse_images_json(row["images_json"]):
|
||||
_add(img.get("file"))
|
||||
if "image" in keys:
|
||||
_add(row["image"])
|
||||
return paths
|
||||
@@ -583,61 +583,6 @@ function fillJournalFromTrade(t){
|
||||
alert("已填入下方复盘表单,请手动补充主观原因。");
|
||||
}
|
||||
|
||||
function prefillJournalByImage(){
|
||||
const fileInput = document.getElementById("journal-screenshot");
|
||||
if(!fileInput || !fileInput.files || !fileInput.files.length){
|
||||
alert("请先选择截图");
|
||||
return;
|
||||
}
|
||||
const fd = new FormData();
|
||||
fd.append("screenshot", fileInput.files[0]);
|
||||
fetch("/api/journal_prefill", { method: "POST", body: fd })
|
||||
.then(r=>r.json())
|
||||
.then(res=>{
|
||||
if(!res.ok){
|
||||
alert(res.msg || "AI识别失败");
|
||||
return;
|
||||
}
|
||||
const d = res.data || {};
|
||||
setJournalField("open_datetime", normalizeDatetimeLocal(d.open_datetime));
|
||||
setJournalField("close_datetime", normalizeDatetimeLocal(d.close_datetime));
|
||||
setJournalField("coin", d.coin || "");
|
||||
setJournalField("tf", d.tf || "");
|
||||
setJournalField("pnl", d.pnl || "");
|
||||
setJournalField("expect_rr", d.expect_rr || "");
|
||||
setJournalField("real_rr", d.real_rr || "");
|
||||
let entryReason = String(d.entry_reason || "").trim();
|
||||
let customEr = "";
|
||||
if(JOURNAL_ENTRY_REASON_OPTIONS && JOURNAL_ENTRY_REASON_OPTIONS.includes(entryReason)){
|
||||
// keep
|
||||
} else if(entryReason){
|
||||
customEr = entryReason;
|
||||
entryReason = JOURNAL_ENTRY_REASON_OTHER;
|
||||
} else {
|
||||
entryReason = "";
|
||||
}
|
||||
setJournalField("entry_reason", entryReason);
|
||||
setJournalField("entry_reason_custom", customEr);
|
||||
syncJournalEntryReasonOtherUi();
|
||||
const trig = (d.early_exit_trigger || "").trim();
|
||||
let noteEx = (d.early_exit_note || "").trim();
|
||||
const legacy = (d.early_exit_reason || "").trim();
|
||||
if(!noteEx && legacy && !trig){
|
||||
const sp = splitLegacyEarlyExitReason(legacy);
|
||||
setJournalField("early_exit_trigger", sp.trigger);
|
||||
setJournalField("early_exit_note", sp.note);
|
||||
} else {
|
||||
setJournalField("early_exit_trigger", trig);
|
||||
setJournalField("early_exit_note", noteEx);
|
||||
}
|
||||
setJournalField("note", d.note || "");
|
||||
if(typeof syncEarlyExitNoteRequired === "function") syncEarlyExitNoteRequired();
|
||||
recomputeJournalRealRr();
|
||||
alert("已完成预填,请手动检查并补充原因");
|
||||
})
|
||||
.catch(()=>alert("AI识别请求失败"));
|
||||
}
|
||||
|
||||
function recomputeJournalRealRr(){
|
||||
const form = document.getElementById("journal-form");
|
||||
if(!form) return;
|
||||
|
||||
@@ -345,11 +345,12 @@
|
||||
<input name="early_exit_note" id="early-exit-note" placeholder="离场补充(仅手工平仓必填)">
|
||||
<select name="post_breakeven_stare"><option value="否">保本后盯盘:否</option><option value="是">保本后盯盘:是</option></select>
|
||||
<select name="new_trade_while_occupied"><option value="否">占用时新开仓:否</option><option value="是">占用时新开仓:是</option></select>
|
||||
<input id="journal-screenshot" type="file" name="screenshot" accept="image/*">
|
||||
</div>
|
||||
{% from 'journal_upload_slots.html' import journal_upload_slots %}
|
||||
{{ journal_upload_slots() }}
|
||||
<div class="form-row" style="margin-top:8px;flex-wrap:wrap;gap:10px;align-items:center">
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
|
||||
<input type="checkbox" name="journal_exchange_chart" value="true" checked>
|
||||
<input type="checkbox" name="journal_exchange_chart" value="true">
|
||||
保存时自动生成 K 线图并作为截图
|
||||
</label>
|
||||
<label style="font-size:.82rem;color:#9aa">周期1</label>
|
||||
@@ -377,9 +378,6 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="sub" id="journal-chart-anchor-hint" style="font-size:.72rem;color:#8892b0;margin-top:4px">双周期上下排列;截止=平仓时间:开仓前背景至平仓;截止=当前时间:最近 N 根至此刻(可看平仓后走势);标注开仓、平仓与止损位</div>
|
||||
<div class="form-row" style="margin-top:8px">
|
||||
<button type="button" style="background:#1f3a5a" onclick="prefillJournalByImage()">AI识别预填(你再手动改原因)</button>
|
||||
</div>
|
||||
<div class="mood-grid" style="margin-top:8px">
|
||||
<label><input type="checkbox" name="mood_issues" value="怕踏空">怕踏空</label>
|
||||
<label><input type="checkbox" name="mood_issues" value="报复开仓">报复开仓</label>
|
||||
|
||||
@@ -111,11 +111,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body" id="detailBody"></div>
|
||||
<div id="detailImages" class="journal-detail-images" style="display:none"></div>
|
||||
<img id="detailImage" class="panel-image" src="" alt="detail-image" style="display:none" onclick="showImage(this.src)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/instance_ui.js?v=4"></script>
|
||||
<script src="/static/instance_ui.js?v=5"></script>
|
||||
<script src="/static/journal_upload_slots.js?v=1"></script>
|
||||
<script src="/static/instance_records_mobile.js?v=2"></script>
|
||||
<script src="/static/time_close_ui.js?v=2"></script>
|
||||
<script src="/static/ai_review_render.js?v=2"></script>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{# 复盘四周期截图槽位(须加载 journal_upload_slots.js) #}
|
||||
{% macro journal_upload_slots() -%}
|
||||
<div class="journal-upload-slots" id="journal-upload-slots">
|
||||
{% for tf in ['5m', '15m', '1h', '4h'] %}
|
||||
<label class="journal-upload-slot">
|
||||
<span class="journal-upload-slot-label">{{ tf }}</span>
|
||||
<input
|
||||
type="file"
|
||||
name="screenshot_{{ tf }}"
|
||||
accept="image/*"
|
||||
class="journal-upload-slot-input"
|
||||
data-tf="{{ tf }}"
|
||||
>
|
||||
<div class="journal-upload-slot-preview" data-tf="{{ tf }}" aria-hidden="true"></div>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="sub journal-upload-hint">可只传部分周期;保存后按 5m / 15m / 1h / 4h 命名,详情页四宫格查看</p>
|
||||
{%- endmacro %}
|
||||
Reference in New Issue
Block a user