统一汇总时间展示格式

This commit is contained in:
Codex
2026-06-16 13:56:30 +08:00
parent 6c68eb7152
commit 4875a0ee77
7 changed files with 76 additions and 25 deletions
+39 -9
View File
@@ -1233,6 +1233,16 @@ def duration_text_from_minutes(minutes: int) -> str:
return f"{minutes // 60}小时{minutes % 60}" return f"{minutes // 60}小时{minutes % 60}"
def duration_text_from_hours(hours: float) -> str:
total_minutes = int(round(float(hours or 0) * 60))
sign = "-" if total_minutes < 0 else ""
return f"{sign}{duration_text_from_minutes(abs(total_minutes))}"
def duration_text_map(values: dict[str, float]) -> dict[str, str]:
return {key: duration_text_from_hours(value) for key, value in values.items()}
def duration_minutes_from_time_range(time_range: str) -> int | None: def duration_minutes_from_time_range(time_range: str) -> int | None:
if not time_range: if not time_range:
return None return None
@@ -2718,12 +2728,20 @@ def summarize_records(records: list[ClassRecord]) -> dict:
by_student[record.student] += record.duration_hours by_student[record.student] += record.duration_hours
by_teacher[record.teacher] += record.duration_hours by_teacher[record.teacher] += record.duration_hours
by_subject[record.subject] += record.duration_hours by_subject[record.subject] += record.duration_hours
students = dict(sorted((key, round(value, 2)) for key, value in by_student.items()))
teachers = dict(sorted((key, round(value, 2)) for key, value in by_teacher.items()))
subjects = dict(sorted((key, round(value, 2)) for key, value in by_subject.items()))
total_hours = round(sum(record.duration_hours for record in records), 2)
return { return {
"count": len(records), "count": len(records),
"total_hours": round(sum(record.duration_hours for record in records), 2), "total_hours": total_hours,
"students": dict(sorted((key, round(value, 2)) for key, value in by_student.items())), "total_duration": duration_text_from_hours(total_hours),
"teachers": dict(sorted((key, round(value, 2)) for key, value in by_teacher.items())), "students": students,
"subjects": dict(sorted((key, round(value, 2)) for key, value in by_subject.items())), "students_duration": duration_text_map(students),
"teachers": teachers,
"teachers_duration": duration_text_map(teachers),
"subjects": subjects,
"subjects_duration": duration_text_map(subjects),
} }
@@ -2736,12 +2754,20 @@ def summarize_public_records(records: list[ClassRecord], teachers: list[Teacher]
by_student[record.student] += record.duration_hours by_student[record.student] += record.duration_hours
by_teacher[display_names.get(record.teacher, record.teacher)] += record.duration_hours by_teacher[display_names.get(record.teacher, record.teacher)] += record.duration_hours
by_subject[record.subject] += record.duration_hours by_subject[record.subject] += record.duration_hours
students = dict(sorted((key, round(value, 2)) for key, value in by_student.items()))
teachers_summary = dict(sorted((key, round(value, 2)) for key, value in by_teacher.items()))
subjects = dict(sorted((key, round(value, 2)) for key, value in by_subject.items()))
total_hours = round(sum(record.duration_hours for record in records), 2)
return { return {
"count": len(records), "count": len(records),
"total_hours": round(sum(record.duration_hours for record in records), 2), "total_hours": total_hours,
"students": dict(sorted((key, round(value, 2)) for key, value in by_student.items())), "total_duration": duration_text_from_hours(total_hours),
"teachers": dict(sorted((key, round(value, 2)) for key, value in by_teacher.items())), "students": students,
"subjects": dict(sorted((key, round(value, 2)) for key, value in by_subject.items())), "students_duration": duration_text_map(students),
"teachers": teachers_summary,
"teachers_duration": duration_text_map(teachers_summary),
"subjects": subjects,
"subjects_duration": duration_text_map(subjects),
} }
@@ -2798,9 +2824,13 @@ def account_to_dict(account: Account) -> dict:
return { return {
"student_id": account.student_id, "student_id": account.student_id,
"student": account.student, "student": account.student,
"payments": [{"date": payment.date, "hours": payment.hours} for payment in account.payments], "payments": [
{"date": payment.date, "hours": payment.hours, "duration": duration_text_from_hours(payment.hours)}
for payment in account.payments
],
"payments_count": len(account.payments), "payments_count": len(account.payments),
"remaining": account.remaining, "remaining": account.remaining,
"remaining_duration": duration_text_from_hours(account.remaining),
"account_status": account.account_status, "account_status": account.account_status,
"note": account.note, "note": account.note,
} }
+2 -2
View File
@@ -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=20260611-accounts-split" /> <link rel="stylesheet" href="/static/styles.css?v=20260616-duration-text" />
</head> </head>
<body> <body>
<header class="topbar"> <header class="topbar">
@@ -55,6 +55,6 @@
</section> </section>
</main> </main>
<script src="/static/accounts.js?v=20260611-accounts-split"></script> <script src="/static/accounts.js?v=20260616-duration-text"></script>
</body> </body>
</html> </html>
+6 -2
View File
@@ -13,6 +13,10 @@ function fmtHours(value) {
return `${hours}小时${minutes}`; return `${hours}小时${minutes}`;
} }
function displayHours(value, fallback) {
return fallback || fmtHours(value);
}
function fmtTime(seconds) { function fmtTime(seconds) {
if (!seconds) return "未知"; if (!seconds) return "未知";
return new Date(seconds * 1000).toLocaleString("zh-CN", { hour12: false }); return new Date(seconds * 1000).toLocaleString("zh-CN", { hour12: false });
@@ -40,7 +44,7 @@ function statusClass(status) {
function renderPayments(payments) { function renderPayments(payments) {
if (!payments.length) return "暂无"; if (!payments.length) return "暂无";
return payments.map((item) => `${escapeHtml(item.date)}${fmtHours(item.hours)}`).join("<br>"); return payments.map((item) => `${escapeHtml(item.date)}${escapeHtml(displayHours(item.hours, item.duration))}`).join("<br>");
} }
async function fetchJson(url) { async function fetchJson(url) {
@@ -89,7 +93,7 @@ async function loadAccounts() {
(row) => `<tr> (row) => `<tr>
<td>${escapeHtml(row.student)}<br><small>${escapeHtml(row.student_id)}</small></td> <td>${escapeHtml(row.student)}<br><small>${escapeHtml(row.student_id)}</small></td>
<td><span class="status ${statusClass(row.account_status)}">${escapeHtml(row.account_status)}</span></td> <td><span class="status ${statusClass(row.account_status)}">${escapeHtml(row.account_status)}</span></td>
<td class="num">${fmtHours(row.remaining)}</td> <td class="num">${escapeHtml(displayHours(row.remaining, row.remaining_duration))}</td>
<td>${renderPayments(row.payments)}</td> <td>${renderPayments(row.payments)}</td>
<td class="note-cell">${escapeHtml(row.note || "")}</td> <td class="note-cell">${escapeHtml(row.note || "")}</td>
</tr>`, </tr>`,
+2 -2
View File
@@ -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=20260615-summary-time-review" /> <link rel="stylesheet" href="/static/styles.css?v=20260616-duration-text" />
</head> </head>
<body> <body>
<header class="topbar"> <header class="topbar">
@@ -417,6 +417,6 @@
</section> </section>
</main> </main>
<script src="/static/admin.js?v=20260616-summary-single-input"></script> <script src="/static/admin.js?v=20260616-duration-text"></script>
</body> </body>
</html> </html>
+17 -4
View File
@@ -112,6 +112,10 @@ function fmtHours(value) {
return `${hours}小时${minutes}`; return `${hours}小时${minutes}`;
} }
function displayHours(value, fallback) {
return fallback || fmtHours(value);
}
function fmtTime(seconds) { function fmtTime(seconds) {
if (!seconds) return "未知"; if (!seconds) return "未知";
return new Date(seconds * 1000).toLocaleString("zh-CN", { hour12: false }); return new Date(seconds * 1000).toLocaleString("zh-CN", { hour12: false });
@@ -146,7 +150,7 @@ function taskStatusClass(status) {
function renderPayments(payments) { function renderPayments(payments) {
if (!payments.length) return "暂无"; if (!payments.length) return "暂无";
return payments.map((item) => `${escapeHtml(item.date)}${fmtHours(item.hours)}`).join("<br>"); return payments.map((item) => `${escapeHtml(item.date)}${escapeHtml(displayHours(item.hours, item.duration))}`).join("<br>");
} }
async function fetchJson(url, options = {}) { async function fetchJson(url, options = {}) {
@@ -212,7 +216,7 @@ async function loadAccounts() {
(row) => `<tr> (row) => `<tr>
<td>${escapeHtml(row.student)}<br><small>${escapeHtml(row.student_id)}</small></td> <td>${escapeHtml(row.student)}<br><small>${escapeHtml(row.student_id)}</small></td>
<td><span class="status ${statusClass(row.account_status)}">${escapeHtml(row.account_status)}</span></td> <td><span class="status ${statusClass(row.account_status)}">${escapeHtml(row.account_status)}</span></td>
<td class="num">${fmtHours(row.remaining)}</td> <td class="num">${escapeHtml(displayHours(row.remaining, row.remaining_duration))}</td>
<td>${renderPayments(row.payments)}</td> <td>${renderPayments(row.payments)}</td>
<td class="note-cell">${escapeHtml(row.note || "")}</td> <td class="note-cell">${escapeHtml(row.note || "")}</td>
<td><button class="small-button account-edit" type="button" data-student-id="${escapeHtml(row.student_id)}">编辑</button></td> <td><button class="small-button account-edit" type="button" data-student-id="${escapeHtml(row.student_id)}">编辑</button></td>
@@ -228,7 +232,16 @@ async function loadAccounts() {
} }
function paymentsToText(payments) { function paymentsToText(payments) {
return payments.map((item) => `${item.date}:${item.hours}`).join("\n"); return payments.map((item) => `${item.date}:${displayHours(item.hours, item.duration)}`).join("\n");
}
function parseHoursInput(value) {
const text = String(value || "").trim();
const match = text.match(/^(\d+)小时(?:(\d+)分)?$/);
if (match) return Number(match[1]) + Number(match[2] || 0) / 60;
const minuteMatch = text.match(/^(\d+)分$/);
if (minuteMatch) return Number(minuteMatch[1]) / 60;
return Number(text);
} }
function parsePaymentsInput(value) { function parsePaymentsInput(value) {
@@ -241,7 +254,7 @@ function parsePaymentsInput(value) {
if (!date || hoursText === undefined) { if (!date || hoursText === undefined) {
throw new Error(`缴费记录格式错误:${item}`); throw new Error(`缴费记录格式错误:${item}`);
} }
const hours = Number(hoursText); const hours = parseHoursInput(hoursText);
if (!Number.isFinite(hours)) { if (!Number.isFinite(hours)) {
throw new Error(`缴费课时格式错误:${item}`); throw new Error(`缴费课时格式错误:${item}`);
} }
+8 -4
View File
@@ -48,6 +48,10 @@ function fmtHours(value) {
return `${hours}小时${minutes}`; return `${hours}小时${minutes}`;
} }
function displayHours(value, fallback) {
return fallback || fmtHours(value);
}
function fmtTime(seconds) { function fmtTime(seconds) {
if (!seconds) return "未知"; if (!seconds) return "未知";
return new Date(seconds * 1000).toLocaleString("zh-CN", { hour12: false }); return new Date(seconds * 1000).toLocaleString("zh-CN", { hour12: false });
@@ -227,7 +231,7 @@ function renderGroupedRecords(records) {
<td colspan="7"> <td colspan="7">
<div class="teacher-group-title"> <div class="teacher-group-title">
<strong>${escapeHtml(group.teacher)}</strong> <strong>${escapeHtml(group.teacher)}</strong>
<span>${group.count} 条记录 · ${fmtHours(group.totalHours)}</span> <span>${group.count} 条记录 · ${displayHours(group.totalHours)}</span>
</div> </div>
</td> </td>
</tr>${sortRecordsForDisplay(group.records) </tr>${sortRecordsForDisplay(group.records)
@@ -265,7 +269,7 @@ function statusClass(status) {
function renderPayments(payments) { function renderPayments(payments) {
if (!payments.length) return "暂无缴费记录"; if (!payments.length) return "暂无缴费记录";
return payments return payments
.map((item) => `<span class="payment-line">${escapeHtml(item.date)}${fmtHours(item.hours)}</span>`) .map((item) => `<span class="payment-line">${escapeHtml(item.date)}${escapeHtml(displayHours(item.hours, item.duration))}</span>`)
.join(""); .join("");
} }
@@ -279,7 +283,7 @@ function renderInlineAccount(account) {
<span class="status ${statusClass(account.account_status)}">${escapeHtml(account.account_status)}</span> <span class="status ${statusClass(account.account_status)}">${escapeHtml(account.account_status)}</span>
</div> </div>
<div class="inline-account-grid"> <div class="inline-account-grid">
${metric("剩余课时", fmtHours(account.remaining))} ${metric("剩余课时", displayHours(account.remaining, account.remaining_duration))}
${metric("缴费次数", account.payments_count)} ${metric("缴费次数", account.payments_count)}
${metricHtml("缴费记录", renderPayments(account.payments))} ${metricHtml("缴费记录", renderPayments(account.payments))}
${metric("备注", account.note || "无")} ${metric("备注", account.note || "无")}
@@ -593,7 +597,7 @@ async function queryRecords(query) {
recordMeta.innerHTML = [ recordMeta.innerHTML = [
metric("识别日期", data.query.date_range), metric("识别日期", data.query.date_range),
metric("命中记录", `${summary.count}`), metric("命中记录", `${summary.count}`),
metric("总课时", fmtHours(summary.total_hours)), metric("总课时", displayHours(summary.total_hours, summary.total_duration)),
metric("授课老师", `${Object.keys(summary.teachers || {}).length} 位`), metric("授课老师", `${Object.keys(summary.teachers || {}).length} 位`),
].join(""); ].join("");
+2 -2
View File
@@ -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=20260615-record-summary-toggle" /> <link rel="stylesheet" href="/static/styles.css?v=20260616-duration-text" />
</head> </head>
<body> <body>
<header class="topbar"> <header class="topbar">
@@ -140,6 +140,6 @@
</section> </section>
</div> </div>
<script src="/static/app.js?v=20260615-record-summary-toggle"></script> <script src="/static/app.js?v=20260616-duration-text"></script>
</body> </body>
</html> </html>