From 08fcc5cb4f6eae0c647268ad7a9405a70968b912 Mon Sep 17 00:00:00 2001 From: lincube Date: Sat, 11 Jul 2026 17:02:22 +0900 Subject: [PATCH] =?UTF-8?q?feat.=E6=94=B9=E8=BF=9BUAF=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + docs/auto_commit_md/20260711_d4024ec.md | 0 scripts/test-uaf-export.js | 79 +++- src/components/home/HomeActions.vue | 47 +- src/components/home/UafTransferDialog.vue | 495 ++++++++++++++++++++++ src/pages/index.vue | 75 ++-- src/utils/uafExport.js | 254 +++++++++-- src/vendor/uaf/browser.js | 104 +++-- src/vendor/uaf/manifest.json | 2 +- 9 files changed, 939 insertions(+), 118 deletions(-) create mode 100644 docs/auto_commit_md/20260711_d4024ec.md create mode 100644 src/components/home/UafTransferDialog.vue diff --git a/.gitignore b/.gitignore index bcf2bca..73c5b7d 100644 --- a/.gitignore +++ b/.gitignore @@ -179,3 +179,4 @@ typed-router.d.ts # Package lock files (using pnpm) package-lock.json +/.trae diff --git a/docs/auto_commit_md/20260711_d4024ec.md b/docs/auto_commit_md/20260711_d4024ec.md new file mode 100644 index 0000000..e69de29 diff --git a/scripts/test-uaf-export.js b/scripts/test-uaf-export.js index 67fa842..899cd9d 100644 --- a/scripts/test-uaf-export.js +++ b/scripts/test-uaf-export.js @@ -1,17 +1,30 @@ import assert from "node:assert/strict"; -import { createUafDocument, hasExportableHomework, normalizeUafDate, UafExportValidationError } from "../src/utils/uafExport.js"; +import { + createExportPreview, + createImportPlan, + createUafDocument, + executeImportPlan, + findImportPlanIssues, + hasExportableHomework, + itemsFromBoardData, + normalizeUafDate, + UafExportValidationError, +} from "../src/utils/uafExport.js"; + +const clone = (value) => JSON.parse(JSON.stringify(value)); const items = [ { type: "exam", name: "考试安排", content: "ignored" }, - { type: "homework", name: "数学", content: "完成第 1、2 题" }, + { type: "homework", name: "数学", content: "完成第 1、2 题", tags: ["必做"] }, { type: "time", name: "时间" }, { type: "custom", name: "班级任务", content: "整理讲台" }, ]; assert.equal(normalizeUafDate("20260711"), "2026-07-11"); +assert.equal(normalizeUafDate("2026-07-11T08:30:00+08:00"), "2026-07-11"); assert.equal(hasExportableHomework(items), true); assert.deepEqual(createUafDocument(items, "20260711"), [ - { subject: "数学", date: "2026-07-11", content: "完成第 1、2 题", tags: [] }, + { subject: "数学", date: "2026-07-11", content: "完成第 1、2 题", tags: ["必做"] }, { subject: "班级任务", date: "2026-07-11", content: "整理讲台", tags: [] }, ]); assert.throws(() => createUafDocument([], "20260711"), UafExportValidationError); @@ -19,4 +32,62 @@ assert.throws( () => createUafDocument([{ type: "homework", name: "数学", content: "x".repeat(2001) }], "20260711"), /2000/, ); -console.log("Classworks UAF export mapping tests passed."); + +const board = { + homework: { + 数学: { content: "旧数学作业", tags: ["旧标签"] }, + "custom-existing": { type: "custom", name: "班级任务", content: "旧任务" }, + "exam-1": { type: "exam", examId: "1", content: "" }, + }, + attendance: { absent: ["张三"], late: [], exclude: [] }, +}; +assert.deepEqual(itemsFromBoardData(board, [{ name: "数学", order: 0 }]), [ + { key: "数学", name: "数学", type: "homework", content: "旧数学作业", tags: ["旧标签"], order: 0 }, + { + key: "custom-existing", + name: "班级任务", + type: "custom", + content: "旧任务", + tags: [], + order: 9999, + }, +]); +assert.equal(createExportPreview(items, "20260711")[0].selected, true); + +const importedDocument = [ + { subject: "数学", date: "2026-07-11", content: "新数学作业", tags: ["导入"] }, + { subject: "班级任务", date: "2026-07-11", content: "新任务", tags: [] }, + { subject: "物理", date: "2026-07-12", content: "新日期作业", tags: ["实验"] }, +]; +const boardsByDate = { + 20260711: board, + 20260712: { homework: {}, attendance: { absent: [], late: ["李四"], exclude: [] } }, +}; +const plan = await createImportPlan( + importedDocument, + [{ name: "数学", order: 0 }], + async (date) => clone(boardsByDate[date]), +); +assert.deepEqual(plan.rows.map((row) => [row.targetType, row.conflict, row.action]), [ + ["homework", true, "keep"], + ["custom", true, "keep"], + ["custom", false, "import"], +]); +plan.rows[0].action = "overwrite"; +const saved = new Map(); +const result = await executeImportPlan(plan, async (date, value) => saved.set(date, clone(value))); +assert.deepEqual(result, { imported: 2, skipped: 1, savedDates: ["20260711", "20260712"], failedDates: [] }); +assert.deepEqual(saved.get("20260711").homework.数学, { content: "新数学作业", tags: ["导入"] }); +assert.deepEqual(saved.get("20260711").attendance.absent, ["张三"]); +assert.equal(saved.get("20260711").homework["exam-1"].type, "exam"); +assert.equal(Object.values(saved.get("20260712").homework)[0].name, "物理"); + +const duplicatePlan = await createImportPlan( + [importedDocument[0], { ...importedDocument[0], content: "重复作业" }], + [{ name: "数学", order: 0 }], + async () => clone(board), +); +duplicatePlan.rows.forEach((row) => { row.action = "overwrite"; }); +assert.equal(findImportPlanIssues(duplicatePlan.rows).length, 1); + +console.log("Classworks UAF import/export tests passed."); diff --git a/src/components/home/HomeActions.vue b/src/components/home/HomeActions.vue index 244bfad..4f5271b 100644 --- a/src/components/home/HomeActions.vue +++ b/src/components/home/HomeActions.vue @@ -83,18 +83,35 @@ > 添加测试卡片 - - 导出 UAF - + + + + + + + diff --git a/src/components/home/UafTransferDialog.vue b/src/components/home/UafTransferDialog.vue new file mode 100644 index 0000000..30cc149 --- /dev/null +++ b/src/components/home/UafTransferDialog.vue @@ -0,0 +1,495 @@ + + + + + diff --git a/src/pages/index.vue b/src/pages/index.vue index 434dca7..ce12c2d 100644 --- a/src/pages/index.vue +++ b/src/pages/index.vue @@ -200,15 +200,27 @@ :is-fullscreen="state.isFullscreen" :show-anti-screen-burn-card="showAntiScreenBurnCard" :show-test-card-button="showTestCardButton" - :uaf-export-disabled="!hasExportableHomework" - :uaf-export-loading="loading.exportUaf" + :uaf-transfer-loading="loading.exportUaf" @upload="manualUpload" @show-sync-message="showSyncMessage" @open-random-picker="openRandomPicker" @toggle-fullscreen="toggleFullscreen" @add-test-card="addTestCard" @add-exam-card="showAddExamDialog = true" - @export-uaf="exportUaf" + @open-uaf-export="openUafTransfer('export')" + @open-uaf-import="openUafTransfer('import')" + /> + + @@ -564,11 +576,6 @@ import HomeActions from "@/components/home/HomeActions.vue"; import FloatingICP from "@/components/FloatingICP.vue"; import HitokotoCard from "@/components/HitokotoCard.vue"; import HomeSkeleton from "@/components/common/HomeSkeleton.vue"; -import { - downloadUafDocument, - hasExportableHomework as containsExportableHomework, - UafExportValidationError, -} from "@/utils/uafExport.js"; // ===== 非首屏 / 条件渲染组件(异步懒加载)===== const MessageLog = defineAsyncComponent({ @@ -592,6 +599,10 @@ const HomeworkEditDialog = defineAsyncComponent({ loader: () => import("@/components/HomeworkEditDialog.vue"), delay: 0, }); +const UafTransferDialog = defineAsyncComponent({ + loader: () => import("@/components/home/UafTransferDialog.vue"), + delay: 0, +}); const InitServiceChooser = defineAsyncComponent({ loader: () => import("@/components/InitServiceChooser.vue"), loadingComponent: AsyncLoadingPlaceholder, @@ -669,6 +680,7 @@ export default { ExamScheduleCard, ExamConfigEditor, HomeSkeleton, + UafTransferDialog, }, setup() { const { mobile } = useDisplay(); @@ -742,6 +754,10 @@ export default { copyToToday: false, exportUaf: false, }, + uafTransfer: { + show: false, + mode: "export", + }, dataReady: false, debouncedUpload: null, debouncedAttendanceSave: null, @@ -897,6 +913,7 @@ export default { name: subjectKey, type: 'homework', content: subjectData.content, + tags: Array.isArray(subjectData.tags) ? subjectData.tags : [], order: subject.order, rowSpan: estimatedHeight, // Used for sorting only }); @@ -937,6 +954,7 @@ export default { name: card.name, type: 'custom', content: card.content, + tags: Array.isArray(card.tags) ? card.tags : [], order: 9999, // Put at the end rowSpan: estimatedHeight, // Used for sorting only }); @@ -1094,9 +1112,6 @@ export default { .sort((a, b) => a.order - b.order) .map((subject) => subject.name); }, - hasExportableHomework() { - return containsExportableHomework(this.sortedItems); - }, }, watch: { @@ -1664,6 +1679,7 @@ export default { this.state.boardData.homework[this.currentEditSubject].content = content; } else { this.state.boardData.homework[this.currentEditSubject] = { + ...this.state.boardData.homework[this.currentEditSubject], content: content, }; } @@ -1797,6 +1813,7 @@ export default { this.state.boardData.homework[this.currentEditSubject].content = content; } else { this.state.boardData.homework[this.currentEditSubject] = { + ...this.state.boardData.homework[this.currentEditSubject], content: content, }; } @@ -2064,21 +2081,22 @@ export default { this.state.synced = false; }, - async exportUaf() { - if (this.loading.exportUaf) return; - this.loading.exportUaf = true; - try { - const filename = await downloadUafDocument(this.sortedItems, this.state.dateString); - this.$message.success("导出成功", filename); - } catch (error) { - console.error("UAF export failed:", error); - if (error instanceof UafExportValidationError) { - this.$message.error("无法导出 UAF", error.issues.join("\n")); - } else { - this.$message.error("导出失败", error?.message || "无法生成 UAF PDF"); - } - } finally { - this.loading.exportUaf = false; + openUafTransfer(mode) { + this.uafTransfer.mode = mode; + this.uafTransfer.show = true; + }, + + handleUafSuccess(title, content) { + this.$message.success(title, content); + }, + + handleUafError(title, content) { + this.$message.error(title, content); + }, + + async handleUafImported(result) { + if (result.savedDates.includes(this.state.dateString)) { + await this.downloadData(true); } }, @@ -2484,7 +2502,10 @@ export default { } else { // 普通作业,只复制内容 newHomework[key] = { - content: sourceHomework[key].content + content: sourceHomework[key].content, + tags: Array.isArray(sourceHomework[key].tags) + ? [...sourceHomework[key].tags] + : [], }; } } diff --git a/src/utils/uafExport.js b/src/utils/uafExport.js index 7e24584..b4de9e6 100644 --- a/src/utils/uafExport.js +++ b/src/utils/uafExport.js @@ -18,7 +18,47 @@ export function normalizeUafDate(value) { return `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`; } if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return value; - throw new UafExportValidationError([`日期 ${value || "(空)"} 不是有效的 YYYYMMDD 或 YYYY-MM-DD 格式`]); + if ( + /^\d{4}-\d{2}-\d{2}T/.test(value) && + !Number.isNaN(Date.parse(value)) + ) { + return value.slice(0, 10); + } + throw new UafExportValidationError([ + `日期 ${value || "(空)"} 不是有效的 YYYYMMDD 或 YYYY-MM-DD 格式`, + ]); +} + +export function toClassworksDate(value) { + return normalizeUafDate(value).replaceAll("-", ""); +} + +function normalizeTags(tags) { + return Array.isArray(tags) ? tags.map((tag) => String(tag)) : []; +} + +export function validateUafAssignment(assignment, label = assignment.subject || "作业") { + const issues = []; + if (!assignment.subject) issues.push(`${label}:科目名称不能为空`); + if (assignment.subject.length > LIMITS.subjectMax) { + issues.push(`${label}:科目名称超过 ${LIMITS.subjectMax} 个字符`); + } + try { + normalizeUafDate(assignment.date); + } catch (error) { + issues.push(...error.issues.map((issue) => `${label}:${issue}`)); + } + if (!assignment.content) issues.push(`${label}:正文不能为空`); + if (assignment.content.length > LIMITS.contentMax) { + issues.push(`${label}:正文超过 ${LIMITS.contentMax} 个字符`); + } + if (assignment.tags.length > LIMITS.tagCountMax) { + issues.push(`${label}:标签超过 ${LIMITS.tagCountMax} 个`); + } + if (assignment.tags.some((tag) => !tag || tag.length > LIMITS.tagMax || tag.includes(";"))) { + issues.push(`${label}:包含无效标签`); + } + return issues; } export function createUafDocument(items, dateValue) { @@ -30,38 +70,68 @@ export function createUafDocument(items, dateValue) { subject: String(item.name || "").trim(), date, content: item.content, - tags: [], + tags: normalizeTags(item.tags), })); if (assignments.length === 0) { - throw new UafExportValidationError(["当前日期没有可导出的作业"]); + throw new UafExportValidationError(["所选日期没有可导出的作业"]); } - const issues = []; - assignments.forEach((assignment, index) => { - const label = assignment.subject || `第 ${index + 1} 张卡片`; - if (!assignment.subject) issues.push(`${label}:科目名称不能为空`); - if (assignment.subject.length > LIMITS.subjectMax) { - issues.push(`${label}:科目名称超过 ${LIMITS.subjectMax} 个字符`); - } - if (!assignment.content) issues.push(`${label}:正文不能为空`); - if (assignment.content.length > LIMITS.contentMax) { - issues.push(`${label}:正文超过 ${LIMITS.contentMax} 个字符`); - } - if (assignment.tags.length > LIMITS.tagCountMax) { - issues.push(`${label}:标签超过 ${LIMITS.tagCountMax} 个`); - } - for (const tag of assignment.tags) { - if (!tag || tag.length > LIMITS.tagMax || tag.includes(";")) { - issues.push(`${label}:包含无效标签`); - break; - } - } - }); + const issues = assignments.flatMap((assignment, index) => + validateUafAssignment(assignment, assignment.subject || `第 ${index + 1} 张卡片`), + ); if (issues.length > 0) throw new UafExportValidationError(issues); return assignments; } +export function createExportPreview(items, dateValue) { + const date = normalizeUafDate(dateValue); + return items + .filter((item) => item && (item.type === "homework" || item.type === "custom")) + .filter((item) => typeof item.content === "string" && item.content.trim().length > 0) + .map((item, index) => { + const assignment = { + subject: String(item.name || "").trim(), + date, + content: item.content, + tags: normalizeTags(item.tags), + }; + const issues = validateUafAssignment(assignment, assignment.subject || `第 ${index + 1} 张卡片`); + return { id: `${item.key || index}-${index}`, assignment, selected: issues.length === 0, issues }; + }); +} + +export function itemsFromBoardData(boardData, subjects = []) { + const homework = boardData?.homework || {}; + const items = []; + for (const subject of subjects) { + const card = homework[subject.name]; + if (card?.content?.trim()) { + items.push({ + key: subject.name, + name: subject.name, + type: "homework", + content: card.content, + tags: normalizeTags(card.tags), + order: subject.order ?? 0, + }); + } + } + for (const [key, card] of Object.entries(homework)) { + if (key.startsWith("custom-") && card?.content?.trim()) { + items.push({ + key, + name: card.name, + type: "custom", + content: card.content, + tags: normalizeTags(card.tags), + order: 9999, + }); + } + } + return items.sort((a, b) => a.order - b.order); +} + export function hasExportableHomework(items) { return items.some( (item) => @@ -72,13 +142,137 @@ export function hasExportableHomework(items) { ); } -export async function downloadUafDocument(items, dateValue) { - const assignments = createUafDocument(items, dateValue); - const { createUafPdf } = await import("../vendor/uaf/browser.js"); +async function loadBrowserUaf() { + return import("../vendor/uaf/browser.js"); +} + +export async function parseUafPdf(file) { + if (!file || !file.name?.toLowerCase().endsWith(".pdf")) { + throw new UafExportValidationError(["请选择 UAF PDF 文件"]); + } + const bytes = new Uint8Array(await file.arrayBuffer()); + const { validateUafPdf, extractUafPayload } = await loadBrowserUaf(); + const validation = await validateUafPdf(bytes); + if (!validation.valid) { + throw new UafExportValidationError( + validation.errors.length ? validation.errors : ["文件不是有效的 UAF PDF"], + ); + } + return validation.payload || extractUafPayload(bytes); +} + +export async function createImportPlan(document, subjects, loadBoardData) { + const subjectNames = new Set(subjects.map((subject) => subject.name)); + const dates = [...new Set(document.map((assignment) => toClassworksDate(assignment.date)))]; + const boards = new Map(); + for (const date of dates) boards.set(date, await loadBoardData(date)); + + const rows = document.map((assignment, index) => { + const date = toClassworksDate(assignment.date); + const board = boards.get(date); + const isSubject = subjectNames.has(assignment.subject); + let targetKey = isSubject ? assignment.subject : null; + if (!isSubject) { + targetKey = Object.keys(board.homework || {}).find( + (key) => key.startsWith("custom-") && board.homework[key]?.name === assignment.subject, + ); + } + const conflict = Boolean(targetKey && board.homework?.[targetKey]?.content?.trim()); + return { + id: `uaf-import-${index}`, + index, + date, + assignment: { ...assignment, tags: normalizeTags(assignment.tags) }, + targetType: isSubject ? "homework" : "custom", + targetKey, + conflict, + action: conflict ? "keep" : "import", + }; + }); + return { rows, boards }; +} + +export function findImportPlanIssues(rows) { + const issues = []; + const activeTargets = new Map(); + for (const row of rows) { + if (row.action === "keep") continue; + const key = row.targetKey ? `${row.date}:${row.targetKey}` : null; + if (key && activeTargets.has(key)) { + issues.push(`${row.date} 的“${row.assignment.subject}”有多条记录指向同一卡片`); + } + if (key) activeTargets.set(key, row.id); + } + return issues; +} + +export async function executeImportPlan(plan, saveBoardData) { + const issues = findImportPlanIssues(plan.rows); + if (issues.length) throw new UafExportValidationError(issues); + const changedDates = new Set(); + let imported = 0; + let skipped = 0; + + for (const row of plan.rows) { + if (row.action === "keep") { + skipped += 1; + continue; + } + const board = plan.boards.get(row.date); + board.homework ||= {}; + if (row.targetType === "homework") { + board.homework[row.assignment.subject] = { + ...(board.homework[row.assignment.subject] || {}), + content: row.assignment.content, + tags: row.assignment.tags, + }; + } else if (row.targetKey) { + board.homework[row.targetKey] = { + ...board.homework[row.targetKey], + name: row.assignment.subject, + type: "custom", + content: row.assignment.content, + tags: row.assignment.tags, + }; + } else { + const key = `custom-uaf-${Date.now()}-${row.index}`; + board.homework[key] = { + name: row.assignment.subject, + type: "custom", + content: row.assignment.content, + tags: row.assignment.tags, + }; + } + imported += 1; + changedDates.add(row.date); + } + + const savedDates = []; + const failedDates = []; + for (const date of changedDates) { + try { + await saveBoardData(date, plan.boards.get(date)); + savedDates.push(date); + } catch (error) { + failedDates.push({ date, error }); + } + } + return { imported, skipped, savedDates, failedDates }; +} + +export async function downloadUafAssignments(assignments, dateValue) { + if (!assignments.length) throw new UafExportValidationError(["请至少选择一项作业"]); + const issues = assignments.flatMap((assignment) => validateUafAssignment(assignment)); + if (issues.length) throw new UafExportValidationError(issues); + const { createUafPdf } = await loadBrowserUaf(); const base = import.meta.env.BASE_URL || "/"; const fontUrl = new URL(`${base}uaf/NotoSansSC-Regular.otf`, window.location.origin); const wasmUrl = new URL(`${base}uaf/hb-subset.wasm`, window.location.origin); - const pdfBytes = await createUafPdf(assignments, { fontUrl, wasmUrl }); + const pdfBytes = await createUafPdf(assignments, { + fontUrl, + wasmUrl, + theme: "classworks-dark", + }); const blob = new window.Blob([pdfBytes], { type: "application/pdf" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); @@ -91,3 +285,7 @@ export async function downloadUafDocument(items, dateValue) { window.setTimeout(() => URL.revokeObjectURL(url), 1000); return link.download; } + +export async function downloadUafDocument(items, dateValue) { + return downloadUafAssignments(createUafDocument(items, dateValue), dateValue); +} diff --git a/src/vendor/uaf/browser.js b/src/vendor/uaf/browser.js index d9fd866..d6edcaa 100644 --- a/src/vendor/uaf/browser.js +++ b/src/vendor/uaf/browser.js @@ -56374,13 +56374,14 @@ function drawPill(page, x, y, w, h, fill2, border) { // src/renderCard.ts var PAGE_WIDTH = 595.28; var PAGE_HEIGHT = 841.89; -var PAGE_MARGIN = 36; +var PAGE_MARGIN = 40; var WATERMARK_SPACE = 24; var COLUMN_GAP = 14; var ROW_GAP = 14; var CARD_WIDTH = (PAGE_WIDTH - PAGE_MARGIN * 2 - COLUMN_GAP) / 2; -var CARD_PAD = 14; -var HEADER_HEIGHT = 48; +var CARD_PAD = 16; +var HEADER_HEIGHT = 56; +var CARD_RADIUS = 16; var CONTENT_FONT = 13.5; var CONTENT_LINE_HEIGHT = 19; var MAX_LINES_PER_FRAGMENT = 12; @@ -56389,20 +56390,34 @@ var MIN_CARD_HEIGHT = 140; var SUBJECT_FONT = 17; var DATE_FONT = 9.5; var TAG_FONT = 9.5; -var WATERMARK_FONT = 9; -var COLORS = { - pageBg: rgb(248 / 255, 250 / 255, 252 / 255), - shadow: rgb(203 / 255, 213 / 255, 225 / 255), - border: rgb(226 / 255, 232 / 255, 240 / 255), - card: rgb(1, 1, 1), - header: rgb(37 / 255, 99 / 255, 235 / 255), - headerText: rgb(1, 1, 1), - dateText: rgb(219 / 255, 234 / 255, 254 / 255), - content: rgb(15 / 255, 23 / 255, 42 / 255), - chip: rgb(224 / 255, 231 / 255, 255 / 255), - chipText: rgb(55 / 255, 48 / 255, 163 / 255), - muted: rgb(100 / 255, 116 / 255, 139 / 255), - watermark: rgb(148 / 255, 163 / 255, 184 / 255) +var WATERMARK_FONT = 10; +var DEFAULT_COLORS = { + pageBg: rgb(255 / 255, 251 / 255, 254 / 255), + shadow: rgb(0 / 255, 0 / 255, 0 / 255), + border: rgb(121 / 255, 116 / 255, 126 / 255), + card: rgb(255 / 255, 251 / 255, 254 / 255), + header: rgb(24 / 255, 103 / 255, 192 / 255), + headerText: rgb(29 / 255, 27 / 255, 32 / 255), + dateText: rgb(73 / 255, 69 / 255, 79 / 255), + content: rgb(29 / 255, 27 / 255, 32 / 255), + chip: rgb(232 / 255, 222 / 255, 248 / 255), + chipText: rgb(29 / 255, 25 / 255, 43 / 255), + muted: rgb(73 / 255, 69 / 255, 79 / 255), + watermark: rgb(73 / 255, 69 / 255, 79 / 255) +}; +var CLASSWORKS_DARK_COLORS = { + pageBg: rgb(20 / 255, 18 / 255, 24 / 255), + shadow: rgb(0 / 255, 0 / 255, 0 / 255), + border: rgb(73 / 255, 69 / 255, 79 / 255), + card: rgb(29 / 255, 27 / 255, 32 / 255), + header: rgb(24 / 255, 103 / 255, 192 / 255), + headerText: rgb(230 / 255, 225 / 255, 229 / 255), + dateText: rgb(202 / 255, 196 / 255, 208 / 255), + content: rgb(230 / 255, 225 / 255, 229 / 255), + chip: rgb(74 / 255, 68 / 255, 88 / 255), + chipText: rgb(232 / 255, 222 / 255, 248 / 255), + muted: rgb(202 / 255, 196 / 255, 208 / 255), + watermark: rgb(255 / 255, 255 / 255, 255 / 255) }; function widthOf(font, text, size) { return font.widthOfTextAtSize(text, size); @@ -56467,19 +56482,18 @@ function formatDate(date, mode) { const parsed = new Date(date); return Number.isNaN(parsed.getTime()) ? date : `${parsed.getFullYear()}\u5E74${parsed.getMonth() + 1}\u6708${parsed.getDate()}\u65E5`; } -function drawPageBackground(page, font, canRenderCjk) { - page.drawRectangle({ x: 0, y: 0, width: PAGE_WIDTH, height: PAGE_HEIGHT, color: COLORS.pageBg }); +function drawPageBackground(page, font, canRenderCjk, colors) { + page.drawRectangle({ x: 0, y: 0, width: PAGE_WIDTH, height: PAGE_HEIGHT, color: colors.pageBg }); const watermark = canRenderCjk ? "\u4F7F\u7528 UAF v1.0 \u5BFC\u51FA" : "Exported with UAF v1.0"; page.drawText(watermark, { x: PAGE_WIDTH - PAGE_MARGIN - widthOf(font, watermark, WATERMARK_FONT), y: PAGE_MARGIN - 4, size: WATERMARK_FONT, font, - color: COLORS.watermark, - opacity: 0.65 + color: colors.watermark }); } -function drawTags(page, tags, x, y, font) { +function drawTags(page, tags, x, y, font, colors) { if (tags.length === 0) { page.drawText("", { x, y, font, size: TAG_FONT }); return; @@ -56490,20 +56504,17 @@ function drawTags(page, tags, x, y, font) { const label = ellipsize(tag2, font, TAG_FONT, CARD_WIDTH - CARD_PAD * 2 - 16); const width = Math.min(widthOf(font, label, TAG_FONT) + 16, CARD_WIDTH - CARD_PAD * 2); if (cursor + width > maxX) break; - drawPill(page, cursor, y, width, 19, COLORS.chip); - page.drawText(label, { x: cursor + 8, y: y + 5.2, size: TAG_FONT, font, color: COLORS.chipText }); + drawPill(page, cursor, y, width, 19, colors.chip); + page.drawText(label, { x: cursor + 8, y: y + 5.2, size: TAG_FONT, font, color: colors.chipText }); cursor += width + 6; } } -function drawFragment(page, fragment, x, top, font, fontBold, dateDisplay, canRenderCjk) { +function drawFragment(page, fragment, x, top, font, fontBold, dateDisplay, canRenderCjk, colors) { const y = top - fragment.height; - drawRoundedRect(page, x + 3, y - 3, CARD_WIDTH, fragment.height, 12, COLORS.shadow); - drawRoundedRect(page, x, y, CARD_WIDTH, fragment.height, 12, COLORS.card, { - color: COLORS.border, + drawRoundedRect(page, x, y, CARD_WIDTH, fragment.height, CARD_RADIUS, colors.card, { + color: colors.border, width: 1 }); - drawRoundedRect(page, x, top - HEADER_HEIGHT, CARD_WIDTH, HEADER_HEIGHT, 12, COLORS.header); - page.drawRectangle({ x, y: top - HEADER_HEIGHT, width: CARD_WIDTH, height: 12, color: COLORS.header }); const continuation = fragment.continuation ? canRenderCjk ? "\uFF08\u7EED\uFF09" : " (cont.)" : ""; const subject = ellipsize( `${fragment.assignment.subject}${continuation}`, @@ -56513,39 +56524,39 @@ function drawFragment(page, fragment, x, top, font, fontBold, dateDisplay, canRe ); page.drawText(subject, { x: x + CARD_PAD, - y: top - 23, + y: top - CARD_PAD - 20, size: SUBJECT_FONT, font: fontBold, - color: COLORS.headerText + color: colors.headerText }); page.drawText(formatDate(fragment.assignment.date, dateDisplay), { x: x + CARD_PAD, - y: top - 39, + y: top - CARD_PAD - 40, size: DATE_FONT, font, - color: COLORS.dateText + color: colors.dateText }); - let lineY = top - HEADER_HEIGHT - 24; + let lineY = top - CARD_PAD - HEADER_HEIGHT - 12; for (const line of fragment.lines) { page.drawText(line || " ", { x: x + CARD_PAD, y: lineY, size: CONTENT_FONT, font, - color: COLORS.content + color: colors.content }); lineY -= CONTENT_LINE_HEIGHT; } if (fragment.showTags) { - drawTags(page, fragment.assignment.tags, x + CARD_PAD, y + 13, font); + drawTags(page, fragment.assignment.tags, x + CARD_PAD, y + CARD_PAD + 5, font, colors); } else { const continued = canRenderCjk ? "\u6B63\u6587\u4E0B\u9875\u7EE7\u7EED" : "Continued on next card"; page.drawText(continued, { x: x + CARD_PAD, - y: y + 15, + y: y + CARD_PAD + 8, size: TAG_FONT, font, - color: COLORS.muted + color: colors.muted }); } } @@ -56553,6 +56564,7 @@ function renderAssignmentDocument(pdfDoc, document, font, fontBold, options = {} const fragments = createFragments(document, font); const pages = []; const dateDisplay = options.dateDisplay ?? "zh"; + const colors = options.theme === "classworks-dark" ? CLASSWORKS_DARK_COLORS : DEFAULT_COLORS; const pageBottom = PAGE_MARGIN + WATERMARK_SPACE; let page; let cursorTop = PAGE_HEIGHT - PAGE_MARGIN; @@ -56562,7 +56574,7 @@ function renderAssignmentDocument(pdfDoc, document, font, fontBold, options = {} if (!page || cursorTop - rowHeight < pageBottom) { page = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]); pages.push(page); - drawPageBackground(page, font, options.canRenderCjk !== false); + drawPageBackground(page, font, options.canRenderCjk !== false, colors); cursorTop = PAGE_HEIGHT - PAGE_MARGIN; } pair.forEach((fragment, column) => { @@ -56574,7 +56586,8 @@ function renderAssignmentDocument(pdfDoc, document, font, fontBold, options = {} font, fontBold, dateDisplay, - options.canRenderCjk !== false + options.canRenderCjk !== false, + colors ); }); cursorTop -= rowHeight + ROW_GAP; @@ -56597,7 +56610,8 @@ async function createUafPdfWithFont(document, options = {}) { } renderAssignmentDocument(pdfDoc, validated, font, font, { dateDisplay: options.useStandardFont ? "iso" : "zh", - canRenderCjk: !options.useStandardFont + canRenderCjk: !options.useStandardFont, + theme: options.theme }); await pdfDoc.attach(csvBytes, UAF_PAYLOAD_FILENAME, { mimeType: "text/csv", @@ -56833,7 +56847,11 @@ async function createUafPdf(document, options = {}) { const fontBytes = await loadBrowserFont(options); const wasmUrl = options.wasmUrl ?? new URL("../assets/hb-subset.wasm", import.meta.url); const subset = await subsetFontInBrowser(fontBytes, collectDocumentText(document), wasmUrl); - return createUafPdfWithFont(document, { fontBytes: subset, subsetFont: false }); + return createUafPdfWithFont(document, { + fontBytes: subset, + subsetFont: false, + theme: options.theme + }); } async function createUafPdfFromCsv(csv, options = {}) { return createUafPdf(parsePayload(csv), options); diff --git a/src/vendor/uaf/manifest.json b/src/vendor/uaf/manifest.json index e2dcfb0..4c3eced 100644 --- a/src/vendor/uaf/manifest.json +++ b/src/vendor/uaf/manifest.json @@ -1,7 +1,7 @@ { "uafVersion": "1.0", "source": "../UnifiedAssignmentFormat/implementations/typescript/packages/pdf", - "bundleSha256": "6086d88815b14b0daeb5c27782f0eeb5b4a9dd0fdc51ff5371166bf79dd6e2b2", + "bundleSha256": "1e059777c77c22569ed628bbf3f4b20f59abab46337ff6a6d814c2497d17e4e1", "fontSha256": "a3041811a78c361b1de50f953c805e0244951c21c5bd412f7232ef0d899af0da", "wasmSha256": "1bf32603c1dfe17e1b9d54acaec6adfd0fc5c517e088648ba7e44a24213ae93e" }