ui: 复盘表单紧凑布局、即时上传截图并移除占用时新开仓

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-05 22:46:05 +08:00
parent 7bb6ee1154
commit f688a504e1
17 changed files with 544 additions and 263 deletions
+105 -16
View File
@@ -3,21 +3,119 @@ from __future__ import annotations
import json
import os
import re
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"})
_JOURNAL_DRAFT_ID_RE = re.compile(r"^[a-f0-9]{32}$")
_JOURNAL_SLOT_FILE_RE = re.compile(
r"^journal_([a-f0-9]{32})_(5m|15m|1h|4h)\.(png|jpg|jpeg|webp|gif|bmp)$",
re.I,
)
def journal_upload_field_name(tf: str) -> str:
return f"screenshot_{tf}"
def uploaded_screenshot_field_name(tf: str) -> str:
return f"uploaded_screenshot_{tf}"
def normalize_journal_draft_id(raw: Any) -> Optional[str]:
s = str(raw or "").strip().lower()
if _JOURNAL_DRAFT_ID_RE.match(s):
return s
return None
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 build_journal_slot_filename(
entry_id: str,
tf: str,
ext: str,
*,
secure_filename_fn: Callable[[str], str],
) -> str:
ext = ext if ext.startswith(".") else f".{ext}"
ext = _safe_ext(f"x{ext}")
fname = secure_filename_fn(f"journal_{entry_id}_{tf}{ext}")
return fname or ""
def is_valid_preuploaded_journal_file(filename: str, entry_id: str, tf: str) -> bool:
fn = os.path.basename(str(filename or "").strip())
if not fn or fn != str(filename or "").strip():
return False
m = _JOURNAL_SLOT_FILE_RE.match(fn)
if not m:
return False
return m.group(1) == entry_id.lower() and m.group(2) == tf
def save_journal_slot_file(
file,
entry_id: str,
tf: str,
upload_folder: str,
*,
secure_filename_fn: Callable[[str], str],
) -> Optional[Dict[str, str]]:
if tf not in JOURNAL_UPLOAD_TFS or not entry_id or not upload_folder:
return None
if not file or not getattr(file, "filename", None):
return None
ext = _safe_ext(file.filename)
fname = build_journal_slot_filename(
entry_id, tf, ext, secure_filename_fn=secure_filename_fn
)
if not fname:
return None
os.makedirs(upload_folder, exist_ok=True)
path = os.path.join(upload_folder, fname)
file.save(path)
return {"tf": tf, "file": fname}
def collect_journal_slot_images(
form,
files,
entry_id: str,
upload_folder: str,
*,
secure_filename_fn: Callable[[str], str],
) -> List[Dict[str, str]]:
"""优先使用即时上传 hidden 字段;否则回退到表单 multipart。"""
saved: List[Dict[str, str]] = []
if not entry_id or not upload_folder:
return saved
for tf in JOURNAL_UPLOAD_TFS:
pre = ""
if form is not None:
pre = str(form.get(uploaded_screenshot_field_name(tf)) or "").strip()
if pre and is_valid_preuploaded_journal_file(pre, entry_id, tf):
path = os.path.join(upload_folder, os.path.basename(pre))
if os.path.isfile(path):
saved.append({"tf": tf, "file": os.path.basename(pre)})
continue
f = files.get(journal_upload_field_name(tf)) if files else None
item = save_journal_slot_file(
f,
entry_id,
tf,
upload_folder,
secure_filename_fn=secure_filename_fn,
)
if item:
saved.append(item)
return saved
def save_journal_slot_uploads(
files,
entry_id: str,
@@ -26,22 +124,13 @@ def save_journal_slot_uploads(
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
return collect_journal_slot_images(
None,
files,
entry_id,
upload_folder,
secure_filename_fn=secure_filename_fn,
)
def images_json_dumps(items: Sequence[Mapping[str, str]]) -> Optional[str]: