diff --git a/app/app/data.py b/app/app/data.py index a8a7bcf..ad80098 100644 --- a/app/app/data.py +++ b/app/app/data.py @@ -1628,6 +1628,7 @@ OPERATION_LABELS = { "admin-approve-correction": "审核批准上课记录纠错", "admin-approve-deletion": "审核批准上课记录删除", "admin-update-course-summary-time": "课程小结补齐时间", + "admin-update-course-summary-body": "课程小结修改正文", "admin-delete-course-summary": "课程小结删除", "rollback-operation": "撤回操作", } @@ -2481,6 +2482,37 @@ def replace_course_summary_block(root: Path, path: Path, summary_id: str, new_ti raise ValueError("未找到课程小结") +def replace_course_summary_body(root: Path, path: Path, summary_id: str, new_body: str) -> dict: + text = path.read_text(encoding="utf-8") + matches = list(COURSE_SUMMARY_HEADING_RE.finditer(text)) + identity = parse_course_summary_file_identity(root, path) + for index, match in enumerate(matches): + title = match.group("title").strip() + body_start = match.end() + end = matches[index + 1].start() if index + 1 < len(matches) else len(text) + body = text[body_start:end].strip() + body_without_meta = re.sub(r"^(?:>\s+.*\n)+\s*", "", body).strip() + body_text = body_without_meta or body + item_id = sha1_text(f"{identity['relative_path']}|{title}|{index}|{body_text[:200]}", 20) + if item_id != summary_id: + continue + updated_body = new_body.strip() + meta_match = re.match(r"(?P(?:>\s+.*(?:\n|$))+)\s*", body) + metadata = meta_match.group("meta").rstrip() if meta_match else "" + updated_block = f"{metadata}\n\n{updated_body}" if metadata else updated_body + new_text = f"{text[:body_start].rstrip()}\n\n{updated_block}\n\n{text[end:].lstrip()}" + atomic_write_text(path, new_text.rstrip() + "\n") + new_id = sha1_text(f"{identity['relative_path']}|{title}|{index}|{updated_body[:200]}", 20) + return { + "id": summary_id, + "new_id": new_id, + "title": title, + "path": str(path), + "body": updated_body, + } + raise ValueError("未找到课程小结") + + def find_course_summary_item(root: Path, summary_id: str) -> dict: for item in iter_course_summary_markdown(root): if str(item.get("id") or "") == summary_id: @@ -2516,6 +2548,23 @@ def update_course_summary_time(root: Path, summary_id: str, time_range: str) -> return result +def update_course_summary_body(root: Path, summary_id: str, body: str) -> dict: + new_body = body.strip() + if not new_body: + raise ValueError("课程小结正文不能为空") + item = find_course_summary_item(root, summary_id) + path = Path(str(item.get("source_path") or "")) + original = path.read_text(encoding="utf-8") + backup_dir = create_data_backup("admin-update-course-summary-body", {path: original}, [summary_id]) + result = replace_course_summary_body(root, path, summary_id, new_body) + result["backup_id"] = backup_dir.name + try: + prune_data_backups(backup_dir.parent) + except OSError: + pass + return result + + def delete_course_summary(root: Path, summary_id: str) -> dict: item = find_course_summary_item(root, summary_id) path = Path(str(item.get("source_path") or "")) diff --git a/app/app/routers/admin.py b/app/app/routers/admin.py index 491564b..e800110 100644 --- a/app/app/routers/admin.py +++ b/app/app/routers/admin.py @@ -27,6 +27,7 @@ from ..data import ( reject_admin_task, resolve_duplicate_course_summary_task, rollback_operation_log, + update_course_summary_body, update_course_summary_review_task, update_course_summary_time, ) @@ -279,6 +280,23 @@ def admin_update_course_summary_time(summary_id: str, payload: dict, _user: str return {"ok": True, **result} +@router.post("/api/admin/course-summaries/{summary_id}/body") +def admin_update_course_summary_body(summary_id: str, payload: dict, _user: str = Depends(verify_admin_auth)): + try: + with write_lock: + result = update_course_summary_body(COURSE_SUMMARIES_ROOT, summary_id, str(payload.get("body") or "")) + append_operation_log( + OPERATION_LOGS_PATH, + "课程小结修改正文", + "已更新", + summary_id=summary_id, + backup_id=str(result.get("backup_id") or ""), + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"ok": True, **result} + + @router.delete("/api/admin/course-summaries/{summary_id}") def admin_delete_course_summary(summary_id: str, _user: str = Depends(verify_admin_auth)): try: diff --git a/app/app/static/admin.js b/app/app/static/admin.js index eafe8fa..038e557 100644 --- a/app/app/static/admin.js +++ b/app/app/static/admin.js @@ -112,6 +112,7 @@ let currentSummaryReviews = []; let activeSummaryReview = null; let currentSummarySearchItems = []; let expandedSummarySearchId = ""; +let editingSummarySearchId = ""; let currentOperationLogs = []; let expandedOperationLogId = ""; const registerPreviewState = { @@ -795,6 +796,23 @@ function renderSummaryActions(item) { `; } +function renderSummaryBodyEditor(item) { + const body = item.body || item.body_preview || ""; + if (String(item.id) !== editingSummarySearchId) { + return `