修复课程小结补充流程
This commit is contained in:
+104
-2
@@ -1,17 +1,33 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
|
||||||
from ..api_utils import load_accounts, load_records, load_teachers
|
from ..api_utils import load_accounts, load_records, load_teachers
|
||||||
from ..auth import verify_records_auth
|
from ..auth import verify_records_auth
|
||||||
from ..config import ADMIN_TASKS_PATH, COURSE_SUMMARIES_ROOT
|
from ..config import (
|
||||||
|
ADMIN_TASKS_PATH,
|
||||||
|
COURSE_SUMMARIES_ROOT,
|
||||||
|
COURSE_SUMMARY_STATE_PATH,
|
||||||
|
OPERATION_LOGS_PATH,
|
||||||
|
write_lock,
|
||||||
|
)
|
||||||
from ..data import (
|
from ..data import (
|
||||||
account_to_dict,
|
account_to_dict,
|
||||||
|
append_operation_log,
|
||||||
|
course_summary_semantic_key,
|
||||||
|
duration_minutes_from_time_range,
|
||||||
|
find_record_by_identity,
|
||||||
query_public_records,
|
query_public_records,
|
||||||
|
read_course_summary_state,
|
||||||
|
save_course_summary_markdown,
|
||||||
|
sha1_text,
|
||||||
submit_public_correction_tasks,
|
submit_public_correction_tasks,
|
||||||
submit_public_deletion_tasks,
|
submit_public_deletion_tasks,
|
||||||
|
write_course_summary_state,
|
||||||
)
|
)
|
||||||
from ..schemas import CorrectionSubmitPayload, DeletionSubmitPayload
|
from ..schemas import CorrectionSubmitPayload, CourseSummarySupplementPayload, DeletionSubmitPayload
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -59,3 +75,89 @@ def submit_deletions(payload: DeletionSubmitPayload, _user: str = Depends(verify
|
|||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
return {"ok": True, **result}
|
return {"ok": True, **result}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/course-summaries/supplement")
|
||||||
|
def supplement_course_summary(payload: CourseSummarySupplementPayload, _user: str = Depends(verify_records_auth)):
|
||||||
|
body = payload.body.strip()
|
||||||
|
if not body:
|
||||||
|
raise HTTPException(status_code=400, detail="课程小结正文不能为空")
|
||||||
|
try:
|
||||||
|
records = load_records()
|
||||||
|
record = find_record_by_identity(records, payload.record_id)
|
||||||
|
date_iso = record.date.replace(".", "-")
|
||||||
|
source_id = f"supplement:{payload.record_id}:{sha1_text(body, 12)}"
|
||||||
|
summary = {
|
||||||
|
"source_id": source_id,
|
||||||
|
"student": record.student,
|
||||||
|
"date_iso": date_iso,
|
||||||
|
"time_range": record.time,
|
||||||
|
"duration_minutes": duration_minutes_from_time_range(record.time),
|
||||||
|
"teacher": record.teacher,
|
||||||
|
"subject": record.subject,
|
||||||
|
"group": "",
|
||||||
|
"sender": "课程记录页补充",
|
||||||
|
"message_time": "",
|
||||||
|
"body": body,
|
||||||
|
}
|
||||||
|
semantic_key = course_summary_semantic_key(summary)
|
||||||
|
with write_lock:
|
||||||
|
state = read_course_summary_state(COURSE_SUMMARY_STATE_PATH)
|
||||||
|
seen_source_ids = set(str(item) for item in state.get("seen_source_ids", []))
|
||||||
|
seen_semantic_keys = set(str(item) for item in state.get("seen_semantic_keys", []))
|
||||||
|
if source_id in seen_source_ids or semantic_key in seen_semantic_keys:
|
||||||
|
log_id = append_operation_log(
|
||||||
|
OPERATION_LOGS_PATH,
|
||||||
|
"课程小结补充",
|
||||||
|
"重复",
|
||||||
|
record_id=payload.record_id,
|
||||||
|
student=record.student,
|
||||||
|
teacher=record.teacher,
|
||||||
|
subject=record.subject,
|
||||||
|
source_id=source_id,
|
||||||
|
)
|
||||||
|
return {"ok": True, "log_id": log_id, "submitted": 0, "status": "duplicate"}
|
||||||
|
saved = save_course_summary_markdown(COURSE_SUMMARIES_ROOT, summary)
|
||||||
|
status_value = "完成" if saved.get("added") else "重复"
|
||||||
|
seen_source_ids.add(source_id)
|
||||||
|
seen_semantic_keys.add(semantic_key)
|
||||||
|
state["seen_source_ids"] = sorted(seen_source_ids)
|
||||||
|
state["seen_semantic_keys"] = sorted(seen_semantic_keys)
|
||||||
|
state.setdefault("batches", []).append(
|
||||||
|
{
|
||||||
|
"batch_id": f"supplement-{payload.record_id}-{sha1_text(body, 8)}",
|
||||||
|
"received_at": datetime.now().isoformat(timespec="seconds"),
|
||||||
|
"window": {"source": "records_page_supplement"},
|
||||||
|
"students": [record.student],
|
||||||
|
"result": {
|
||||||
|
"received": 1,
|
||||||
|
"saved": 1 if saved.get("added") else 0,
|
||||||
|
"auto_registered": 0,
|
||||||
|
"review_pending": 0,
|
||||||
|
"duplicates": 0 if saved.get("added") else 1,
|
||||||
|
"rejected": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
state["batches"] = state["batches"][-200:]
|
||||||
|
write_course_summary_state(COURSE_SUMMARY_STATE_PATH, state)
|
||||||
|
log_id = append_operation_log(
|
||||||
|
OPERATION_LOGS_PATH,
|
||||||
|
"课程小结补充",
|
||||||
|
status_value,
|
||||||
|
record_id=payload.record_id,
|
||||||
|
student=record.student,
|
||||||
|
teacher=record.teacher,
|
||||||
|
subject=record.subject,
|
||||||
|
source_id=source_id,
|
||||||
|
saved_path=str(saved.get("path") or ""),
|
||||||
|
heading=str(saved.get("heading") or ""),
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"log_id": log_id,
|
||||||
|
"submitted": 1 if status_value == "完成" else 0,
|
||||||
|
"status": "saved" if status_value == "完成" else "duplicate",
|
||||||
|
}
|
||||||
|
|||||||
@@ -60,6 +60,11 @@ class DeletionSubmitPayload(BaseModel):
|
|||||||
items: list[DeletionItemPayload]
|
items: list[DeletionItemPayload]
|
||||||
|
|
||||||
|
|
||||||
|
class CourseSummarySupplementPayload(BaseModel):
|
||||||
|
record_id: str
|
||||||
|
body: str
|
||||||
|
|
||||||
|
|
||||||
class CourseSummaryPayload(BaseModel):
|
class CourseSummaryPayload(BaseModel):
|
||||||
source_id: str = ""
|
source_id: str = ""
|
||||||
student: str = ""
|
student: str = ""
|
||||||
|
|||||||
+142
-8
@@ -29,6 +29,15 @@ const deleteError = document.querySelector("#deleteError");
|
|||||||
const closeDeleteBtn = document.querySelector("#closeDeleteBtn");
|
const closeDeleteBtn = document.querySelector("#closeDeleteBtn");
|
||||||
const cancelDeleteBtn = document.querySelector("#cancelDeleteBtn");
|
const cancelDeleteBtn = document.querySelector("#cancelDeleteBtn");
|
||||||
const confirmDeleteBtn = document.querySelector("#confirmDeleteBtn");
|
const confirmDeleteBtn = document.querySelector("#confirmDeleteBtn");
|
||||||
|
const summarySupplementDialog = document.querySelector("#summarySupplementDialog");
|
||||||
|
const summarySupplementForm = document.querySelector("#summarySupplementForm");
|
||||||
|
const summarySupplementOriginal = document.querySelector("#summarySupplementOriginal");
|
||||||
|
const summarySupplementBody = document.querySelector("#summarySupplementBody");
|
||||||
|
const summarySupplementError = document.querySelector("#summarySupplementError");
|
||||||
|
const summarySupplementPreview = document.querySelector("#summarySupplementPreview");
|
||||||
|
const closeSummarySupplementBtn = document.querySelector("#closeSummarySupplementBtn");
|
||||||
|
const cancelSummarySupplementBtn = document.querySelector("#cancelSummarySupplementBtn");
|
||||||
|
const submitSummarySupplementBtn = document.querySelector("#submitSummarySupplementBtn");
|
||||||
|
|
||||||
const WEEKDAYS = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"];
|
const WEEKDAYS = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"];
|
||||||
const COPY_LINE_BREAK = "\r\n";
|
const COPY_LINE_BREAK = "\r\n";
|
||||||
@@ -40,6 +49,7 @@ let correctedRecords = new Map();
|
|||||||
let expandedSummaryRecords = new Set();
|
let expandedSummaryRecords = new Set();
|
||||||
let activeCorrectionKey = "";
|
let activeCorrectionKey = "";
|
||||||
let activeDeleteKey = "";
|
let activeDeleteKey = "";
|
||||||
|
let activeSummarySupplementKey = "";
|
||||||
|
|
||||||
function fmtHours(value) {
|
function fmtHours(value) {
|
||||||
const totalMinutes = Math.round(Number(value || 0) * 60);
|
const totalMinutes = Math.round(Number(value || 0) * 60);
|
||||||
@@ -194,9 +204,8 @@ function renderRecordRow(row) {
|
|||||||
const correctedClass = corrected ? " corrected-row" : "";
|
const correctedClass = corrected ? " corrected-row" : "";
|
||||||
const actionLabel = corrected ? "编辑" : "纠错";
|
const actionLabel = corrected ? "编辑" : "纠错";
|
||||||
const badge = corrected ? '<span class="correction-badge">已修改</span>' : "";
|
const badge = corrected ? '<span class="correction-badge">已修改</span>' : "";
|
||||||
const summaryCount = Number(displayRow.summary_count || 0);
|
|
||||||
const summaryExpanded = expandedSummaryRecords.has(key);
|
const summaryExpanded = expandedSummaryRecords.has(key);
|
||||||
return `<tr class="record-row${correctedClass}">
|
return `<tr class="record-row${correctedClass}" data-record-key="${escapeHtml(key)}" tabindex="0" role="button" aria-expanded="${summaryExpanded ? "true" : "false"}">
|
||||||
<td>${escapeHtml(displayRow.date)} ${escapeHtml(displayRow.weekday)}</td>
|
<td>${escapeHtml(displayRow.date)} ${escapeHtml(displayRow.weekday)}</td>
|
||||||
<td>${escapeHtml(displayRow.time)}</td>
|
<td>${escapeHtml(displayRow.time)}</td>
|
||||||
<td>${escapeHtml(displayRow.student)}</td>
|
<td>${escapeHtml(displayRow.student)}</td>
|
||||||
@@ -205,7 +214,6 @@ function renderRecordRow(row) {
|
|||||||
<td class="num">${escapeHtml(displayRow.duration)}</td>
|
<td class="num">${escapeHtml(displayRow.duration)}</td>
|
||||||
<td class="record-action-cell">
|
<td class="record-action-cell">
|
||||||
<div class="record-actions">
|
<div class="record-actions">
|
||||||
<button class="small-button summary-toggle" type="button" data-record-key="${escapeHtml(key)}" ${summaryCount > 0 ? "" : "disabled"}>${summaryExpanded ? "收起小结" : "课程小结"}</button>
|
|
||||||
<button class="small-button correction-edit" type="button" data-record-key="${escapeHtml(key)}">${actionLabel}</button>
|
<button class="small-button correction-edit" type="button" data-record-key="${escapeHtml(key)}">${actionLabel}</button>
|
||||||
<button class="small-button record-delete" type="button" data-record-key="${escapeHtml(key)}">删除</button>
|
<button class="small-button record-delete" type="button" data-record-key="${escapeHtml(key)}">删除</button>
|
||||||
${badge}
|
${badge}
|
||||||
@@ -223,6 +231,17 @@ function renderSummaryEntry(summary) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderSummaryMissingRow(key, expanded) {
|
||||||
|
return `<tr class="summary-collapse-row summary-missing-row" data-summary-row="${escapeHtml(key)}" ${expanded ? "" : "hidden"}>
|
||||||
|
<td colspan="7">
|
||||||
|
<div class="summary-missing">
|
||||||
|
<span>无课程小结</span>
|
||||||
|
<button class="small-button summary-supplement" type="button" data-record-key="${escapeHtml(key)}">补充课程小结</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderGroupedRecords(records) {
|
function renderGroupedRecords(records) {
|
||||||
currentRecordOrder = [];
|
currentRecordOrder = [];
|
||||||
return groupRecordsByTeacher(records)
|
return groupRecordsByTeacher(records)
|
||||||
@@ -242,7 +261,7 @@ function renderGroupedRecords(records) {
|
|||||||
? `<tr class="summary-collapse-row" data-summary-row="${escapeHtml(row._recordKey)}" ${expandedSummaryRecords.has(row._recordKey) ? "" : "hidden"}>
|
? `<tr class="summary-collapse-row" data-summary-row="${escapeHtml(row._recordKey)}" ${expandedSummaryRecords.has(row._recordKey) ? "" : "hidden"}>
|
||||||
<td colspan="7">${(row.summaries || []).map(renderSummaryEntry).join("")}</td>
|
<td colspan="7">${(row.summaries || []).map(renderSummaryEntry).join("")}</td>
|
||||||
</tr>`
|
</tr>`
|
||||||
: "";
|
: renderSummaryMissingRow(row._recordKey, expandedSummaryRecords.has(row._recordKey));
|
||||||
return `${renderRecordRow(row)}${summaryRow}`;
|
return `${renderRecordRow(row)}${summaryRow}`;
|
||||||
})
|
})
|
||||||
.join("")}`,
|
.join("")}`,
|
||||||
@@ -259,6 +278,10 @@ function toggleSummaryRow(key) {
|
|||||||
renderCurrentRecords();
|
renderCurrentRecords();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleRecordSummary(key) {
|
||||||
|
toggleSummaryRow(key);
|
||||||
|
}
|
||||||
|
|
||||||
function statusClass(status) {
|
function statusClass(status) {
|
||||||
if (status === "欠费") return "debt";
|
if (status === "欠费") return "debt";
|
||||||
if (status === "预警") return "warning";
|
if (status === "预警") return "warning";
|
||||||
@@ -455,6 +478,92 @@ function closeDeleteDialog() {
|
|||||||
setDeleteError("");
|
setDeleteError("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setSummarySupplementError(message) {
|
||||||
|
summarySupplementError.textContent = message;
|
||||||
|
summarySupplementError.hidden = !message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSummarySupplementText(record, body) {
|
||||||
|
return [
|
||||||
|
`学生:${record.student}`,
|
||||||
|
`日期:${record.date}`,
|
||||||
|
`时间:${record.time}`,
|
||||||
|
`老师:${record.teacher}`,
|
||||||
|
`科目:${record.subject}`,
|
||||||
|
"",
|
||||||
|
body.trim(),
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSummarySupplementPreview() {
|
||||||
|
const original = findCurrentRecord(activeSummarySupplementKey);
|
||||||
|
if (!original) return;
|
||||||
|
const body = summarySupplementBody.value.trim();
|
||||||
|
if (!body) {
|
||||||
|
summarySupplementPreview.textContent = "提交后会直接保存为这节课的课程小结。";
|
||||||
|
setSummarySupplementError("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
summarySupplementPreview.textContent = buildSummarySupplementText(original, body);
|
||||||
|
setSummarySupplementError("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSummarySupplementDialog(key) {
|
||||||
|
const original = findCurrentRecord(key);
|
||||||
|
if (!original) return;
|
||||||
|
activeSummarySupplementKey = key;
|
||||||
|
summarySupplementOriginal.textContent = `将补充课程小结:${buildRecordLine(original)}`;
|
||||||
|
summarySupplementBody.value = "";
|
||||||
|
summarySupplementPreview.textContent = "提交后会直接保存为这节课的课程小结。";
|
||||||
|
setSummarySupplementError("");
|
||||||
|
summarySupplementDialog.hidden = false;
|
||||||
|
summarySupplementBody.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSummarySupplementDialog() {
|
||||||
|
summarySupplementDialog.hidden = true;
|
||||||
|
activeSummarySupplementKey = "";
|
||||||
|
setSummarySupplementError("");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitSummarySupplement(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const original = findCurrentRecord(activeSummarySupplementKey);
|
||||||
|
if (!original) return;
|
||||||
|
const body = summarySupplementBody.value.trim();
|
||||||
|
if (!body) {
|
||||||
|
setSummarySupplementError("课程小结正文不能为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
submitSummarySupplementBtn.disabled = true;
|
||||||
|
summarySupplementPreview.textContent = "正在提交";
|
||||||
|
try {
|
||||||
|
const key = activeSummarySupplementKey;
|
||||||
|
const data = await fetchJson("/api/course-summaries/supplement", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
record_id: original.record_id,
|
||||||
|
body,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
closeSummarySupplementDialog();
|
||||||
|
await loadHealth();
|
||||||
|
if (recordQuery.value.trim()) {
|
||||||
|
await queryRecords(recordQuery.value);
|
||||||
|
if (key) {
|
||||||
|
expandedSummaryRecords.add(key);
|
||||||
|
renderCurrentRecords();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setSummarySupplementError(`提交失败:${error.message}`);
|
||||||
|
summarySupplementPreview.textContent = "请修正后重新提交";
|
||||||
|
} finally {
|
||||||
|
submitSummarySupplementBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function submitDeleteRecord() {
|
async function submitDeleteRecord() {
|
||||||
const original = findCurrentRecord(activeDeleteKey);
|
const original = findCurrentRecord(activeDeleteKey);
|
||||||
if (!original) return;
|
if (!original) return;
|
||||||
@@ -642,18 +751,33 @@ document.querySelectorAll("[data-query]").forEach((button) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
recordRows.addEventListener("click", (event) => {
|
recordRows.addEventListener("click", (event) => {
|
||||||
const summaryButton = event.target.closest(".summary-toggle");
|
const summarySupplementButton = event.target.closest(".summary-supplement");
|
||||||
const editButton = event.target.closest(".correction-edit");
|
const editButton = event.target.closest(".correction-edit");
|
||||||
const deleteButton = event.target.closest(".record-delete");
|
const deleteButton = event.target.closest(".record-delete");
|
||||||
if (summaryButton) {
|
if (summarySupplementButton) {
|
||||||
toggleSummaryRow(summaryButton.dataset.recordKey);
|
openSummarySupplementDialog(summarySupplementButton.dataset.recordKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (editButton) {
|
if (editButton) {
|
||||||
openCorrectionDialog(editButton.dataset.recordKey);
|
openCorrectionDialog(editButton.dataset.recordKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (deleteButton) openDeleteDialog(deleteButton.dataset.recordKey);
|
if (deleteButton) {
|
||||||
|
openDeleteDialog(deleteButton.dataset.recordKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const row = event.target.closest(".record-row");
|
||||||
|
if (row && !event.target.closest(".record-action-cell")) toggleRecordSummary(row.dataset.recordKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
recordRows.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key !== "Enter" && event.key !== " ") return;
|
||||||
|
const row = event.target.closest(".record-row");
|
||||||
|
if (!row) return;
|
||||||
|
const target = event.target.closest(".summary-supplement, .correction-edit, .record-delete");
|
||||||
|
if (target) return;
|
||||||
|
event.preventDefault();
|
||||||
|
toggleRecordSummary(row.dataset.recordKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
[correctionDate, correctionTime, correctionStudent, correctionTeacher, correctionSubject].forEach((input) => {
|
[correctionDate, correctionTime, correctionStudent, correctionTeacher, correctionSubject].forEach((input) => {
|
||||||
@@ -681,6 +805,13 @@ confirmDeleteBtn.addEventListener("click", submitDeleteRecord);
|
|||||||
deleteDialog.addEventListener("click", (event) => {
|
deleteDialog.addEventListener("click", (event) => {
|
||||||
if (event.target === deleteDialog) closeDeleteDialog();
|
if (event.target === deleteDialog) closeDeleteDialog();
|
||||||
});
|
});
|
||||||
|
closeSummarySupplementBtn.addEventListener("click", closeSummarySupplementDialog);
|
||||||
|
cancelSummarySupplementBtn.addEventListener("click", closeSummarySupplementDialog);
|
||||||
|
summarySupplementDialog.addEventListener("click", (event) => {
|
||||||
|
if (event.target === summarySupplementDialog) closeSummarySupplementDialog();
|
||||||
|
});
|
||||||
|
summarySupplementBody.addEventListener("input", updateSummarySupplementPreview);
|
||||||
|
summarySupplementForm.addEventListener("submit", submitSummarySupplement);
|
||||||
document.addEventListener("keydown", (event) => {
|
document.addEventListener("keydown", (event) => {
|
||||||
if (event.key === "Escape" && !correctionDialog.hidden) {
|
if (event.key === "Escape" && !correctionDialog.hidden) {
|
||||||
closeCorrectionDialog();
|
closeCorrectionDialog();
|
||||||
@@ -688,6 +819,9 @@ document.addEventListener("keydown", (event) => {
|
|||||||
if (event.key === "Escape" && !deleteDialog.hidden) {
|
if (event.key === "Escape" && !deleteDialog.hidden) {
|
||||||
closeDeleteDialog();
|
closeDeleteDialog();
|
||||||
}
|
}
|
||||||
|
if (event.key === "Escape" && !summarySupplementDialog.hidden) {
|
||||||
|
closeSummarySupplementDialog();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
copyCorrectedBtn.addEventListener("click", copyCorrectedRecords);
|
copyCorrectedBtn.addEventListener("click", copyCorrectedRecords);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>新时空教务管理系统</title>
|
<title>新时空教务管理系统</title>
|
||||||
<link rel="stylesheet" href="/static/styles.css?v=20260616-duration-text" />
|
<link rel="stylesheet" href="/static/styles.css?v=20260618-record-summary-row" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
@@ -140,6 +140,31 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/app.js?v=20260616-duration-text"></script>
|
<div id="summarySupplementDialog" class="modal-backdrop" hidden>
|
||||||
|
<section class="correction-modal summary-supplement-modal" role="dialog" aria-modal="true" aria-labelledby="summarySupplementTitle">
|
||||||
|
<div class="modal-head">
|
||||||
|
<h2 id="summarySupplementTitle">补充课程小结</h2>
|
||||||
|
<button id="closeSummarySupplementBtn" class="modal-close" type="button" aria-label="关闭">关闭</button>
|
||||||
|
</div>
|
||||||
|
<form id="summarySupplementForm" class="correction-form">
|
||||||
|
<p id="summarySupplementOriginal" class="correction-original"></p>
|
||||||
|
<label class="summary-supplement-body">
|
||||||
|
小结正文
|
||||||
|
<textarea id="summarySupplementBody" rows="9" autocomplete="off" placeholder="填写本节课课堂内容、作业、问题和下节安排"></textarea>
|
||||||
|
</label>
|
||||||
|
<p id="summarySupplementError" class="correction-error" hidden></p>
|
||||||
|
<div class="correction-preview">
|
||||||
|
<span>提交说明</span>
|
||||||
|
<code id="summarySupplementPreview">提交后会直接保存为这节课的课程小结。</code>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button id="cancelSummarySupplementBtn" class="secondary-button" type="button">取消</button>
|
||||||
|
<button id="submitSummarySupplementBtn" type="submit">提交课程小结</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/static/app.js?v=20260618-record-summary-row"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -756,6 +756,18 @@ td {
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.record-row {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-row:hover td {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-row .record-action-cell {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
.corrected-row td {
|
.corrected-row td {
|
||||||
background: #fffaf0;
|
background: #fffaf0;
|
||||||
}
|
}
|
||||||
@@ -801,6 +813,10 @@ td {
|
|||||||
background: #fbfcfd;
|
background: #fbfcfd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.summary-missing-row td {
|
||||||
|
background: #fcfcfd;
|
||||||
|
}
|
||||||
|
|
||||||
.record-summary-item {
|
.record-summary-item {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-top: 1px solid var(--line);
|
border-top: 1px solid var(--line);
|
||||||
@@ -827,6 +843,45 @@ td {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.summary-missing {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-supplement-modal {
|
||||||
|
width: min(100%, 700px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-supplement-body {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
color: #344054;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-supplement-body textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 180px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-supplement-body textarea:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
.correction-badge {
|
.correction-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1132,6 +1187,10 @@ td {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.summary-supplement-modal .correction-preview code {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-actions {
|
.modal-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|||||||
Reference in New Issue
Block a user