统一汇总时间展示格式
This commit is contained in:
+39
-9
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>`,
|
||||||
|
|||||||
@@ -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
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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("");
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user