mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2026-08-12 09:20:31 +00:00
feat.改进UAF样式
This commit is contained in:
parent
d4024ec14a
commit
08fcc5cb4f
1
.gitignore
vendored
1
.gitignore
vendored
@ -179,3 +179,4 @@ typed-router.d.ts
|
|||||||
|
|
||||||
# Package lock files (using pnpm)
|
# Package lock files (using pnpm)
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
/.trae
|
||||||
|
|||||||
0
docs/auto_commit_md/20260711_d4024ec.md
Normal file
0
docs/auto_commit_md/20260711_d4024ec.md
Normal file
@ -1,17 +1,30 @@
|
|||||||
import assert from "node:assert/strict";
|
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 = [
|
const items = [
|
||||||
{ type: "exam", name: "考试安排", content: "ignored" },
|
{ type: "exam", name: "考试安排", content: "ignored" },
|
||||||
{ type: "homework", name: "数学", content: "完成第 1、2 题" },
|
{ type: "homework", name: "数学", content: "完成第 1、2 题", tags: ["必做"] },
|
||||||
{ type: "time", name: "时间" },
|
{ type: "time", name: "时间" },
|
||||||
{ type: "custom", name: "班级任务", content: "整理讲台" },
|
{ type: "custom", name: "班级任务", content: "整理讲台" },
|
||||||
];
|
];
|
||||||
|
|
||||||
assert.equal(normalizeUafDate("20260711"), "2026-07-11");
|
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.equal(hasExportableHomework(items), true);
|
||||||
assert.deepEqual(createUafDocument(items, "20260711"), [
|
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: [] },
|
{ subject: "班级任务", date: "2026-07-11", content: "整理讲台", tags: [] },
|
||||||
]);
|
]);
|
||||||
assert.throws(() => createUafDocument([], "20260711"), UafExportValidationError);
|
assert.throws(() => createUafDocument([], "20260711"), UafExportValidationError);
|
||||||
@ -19,4 +32,62 @@ assert.throws(
|
|||||||
() => createUafDocument([{ type: "homework", name: "数学", content: "x".repeat(2001) }], "20260711"),
|
() => createUafDocument([{ type: "homework", name: "数学", content: "x".repeat(2001) }], "20260711"),
|
||||||
/2000/,
|
/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.");
|
||||||
|
|||||||
@ -83,18 +83,35 @@
|
|||||||
>
|
>
|
||||||
添加测试卡片
|
添加测试卡片
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
<v-menu location="bottom end">
|
||||||
|
<template #activator="{ props: menuProps }">
|
||||||
<v-btn
|
<v-btn
|
||||||
:disabled="uafExportDisabled"
|
v-bind="menuProps"
|
||||||
:loading="uafExportLoading"
|
:disabled="uafTransferLoading"
|
||||||
|
:loading="uafTransferLoading"
|
||||||
|
append-icon="mdi-menu-down"
|
||||||
class="ml-2"
|
class="ml-2"
|
||||||
color="indigo"
|
color="indigo"
|
||||||
prepend-icon="mdi-file-pdf-box"
|
prepend-icon="mdi-swap-vertical-bold"
|
||||||
size="large"
|
size="large"
|
||||||
rounded="xl"
|
rounded="xl"
|
||||||
@click="$emit('export-uaf')"
|
|
||||||
>
|
>
|
||||||
导出 UAF
|
作业导出导入
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list density="comfortable">
|
||||||
|
<v-list-item
|
||||||
|
prepend-icon="mdi-file-export-outline"
|
||||||
|
title="导出 UAF"
|
||||||
|
@click="$emit('open-uaf-export')"
|
||||||
|
/>
|
||||||
|
<v-list-item
|
||||||
|
prepend-icon="mdi-file-import-outline"
|
||||||
|
title="导入 UAF"
|
||||||
|
@click="$emit('open-uaf-import')"
|
||||||
|
/>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-card
|
<v-card
|
||||||
@ -139,8 +156,7 @@ export default {
|
|||||||
isFullscreen: Boolean,
|
isFullscreen: Boolean,
|
||||||
showAntiScreenBurnCard: Boolean,
|
showAntiScreenBurnCard: Boolean,
|
||||||
showTestCardButton: Boolean,
|
showTestCardButton: Boolean,
|
||||||
uafExportDisabled: Boolean,
|
uafTransferLoading: Boolean,
|
||||||
uafExportLoading: Boolean,
|
|
||||||
},
|
},
|
||||||
emits: [
|
emits: [
|
||||||
"upload",
|
"upload",
|
||||||
@ -149,7 +165,8 @@ export default {
|
|||||||
"toggle-fullscreen",
|
"toggle-fullscreen",
|
||||||
"add-test-card",
|
"add-test-card",
|
||||||
"add-exam-card",
|
"add-exam-card",
|
||||||
"export-uaf",
|
"open-uaf-export",
|
||||||
|
"open-uaf-import",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
495
src/components/home/UafTransferDialog.vue
Normal file
495
src/components/home/UafTransferDialog.vue
Normal file
@ -0,0 +1,495 @@
|
|||||||
|
<template>
|
||||||
|
<v-dialog
|
||||||
|
v-model="dialog"
|
||||||
|
:fullscreen="mobile"
|
||||||
|
max-width="920"
|
||||||
|
scrollable
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="d-flex align-center">
|
||||||
|
<v-icon
|
||||||
|
:icon="mode === 'export' ? 'mdi-file-export-outline' : 'mdi-file-import-outline'"
|
||||||
|
class="mr-2"
|
||||||
|
/>
|
||||||
|
{{ mode === "export" ? "导出 UAF" : "导入 UAF" }}
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-close"
|
||||||
|
variant="text"
|
||||||
|
@click="dialog = false"
|
||||||
|
/>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-text class="transfer-content">
|
||||||
|
<template v-if="mode === 'export'">
|
||||||
|
<v-text-field
|
||||||
|
v-model="exportDate"
|
||||||
|
class="mb-3"
|
||||||
|
label="导出日期"
|
||||||
|
prepend-inner-icon="mdi-calendar"
|
||||||
|
type="date"
|
||||||
|
hide-details
|
||||||
|
:disabled="busy"
|
||||||
|
@update:model-value="loadExportPreview"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-progress-linear
|
||||||
|
v-if="loadingPreview"
|
||||||
|
class="mb-3"
|
||||||
|
indeterminate
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-alert
|
||||||
|
v-if="exportError"
|
||||||
|
class="mb-3"
|
||||||
|
type="warning"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
{{ exportError }}
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="previewRows.length"
|
||||||
|
class="d-flex align-center mb-2"
|
||||||
|
>
|
||||||
|
<v-checkbox-btn
|
||||||
|
:model-value="allValidSelected"
|
||||||
|
:indeterminate="someValidSelected && !allValidSelected"
|
||||||
|
@click="toggleAll"
|
||||||
|
/>
|
||||||
|
<span class="text-body-2">选择全部有效作业</span>
|
||||||
|
<v-spacer />
|
||||||
|
<span class="text-caption text-medium-emphasis">
|
||||||
|
已选择 {{ selectedAssignments.length }} / {{ previewRows.length }} 项
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-list
|
||||||
|
v-if="previewRows.length"
|
||||||
|
border
|
||||||
|
lines="three"
|
||||||
|
>
|
||||||
|
<v-list-item
|
||||||
|
v-for="row in previewRows"
|
||||||
|
:key="row.id"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-checkbox-btn
|
||||||
|
v-model="row.selected"
|
||||||
|
:disabled="row.issues.length > 0"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>{{ row.assignment.subject }}</v-list-item-title>
|
||||||
|
<v-list-item-subtitle class="content-preview">
|
||||||
|
{{ row.assignment.content }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
<div class="d-flex flex-wrap align-center mt-1 ga-1">
|
||||||
|
<v-chip
|
||||||
|
size="x-small"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
{{ row.assignment.date }}
|
||||||
|
</v-chip>
|
||||||
|
<v-chip
|
||||||
|
v-for="tag in row.assignment.tags"
|
||||||
|
:key="tag"
|
||||||
|
size="x-small"
|
||||||
|
color="primary"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</v-chip>
|
||||||
|
<span
|
||||||
|
v-if="row.issues.length"
|
||||||
|
class="text-caption text-error"
|
||||||
|
>
|
||||||
|
{{ row.issues.join(";") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<v-empty-state
|
||||||
|
v-else-if="!loadingPreview && !exportError"
|
||||||
|
icon="mdi-book-open-blank-variant-outline"
|
||||||
|
text="该日期没有可导出的作业"
|
||||||
|
title="暂无作业"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<v-file-input
|
||||||
|
v-model="importFile"
|
||||||
|
accept="application/pdf,.pdf"
|
||||||
|
clearable
|
||||||
|
label="选择 UAF PDF"
|
||||||
|
prepend-icon="mdi-file-pdf-box"
|
||||||
|
:disabled="busy"
|
||||||
|
@update:model-value="prepareImport"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-progress-linear
|
||||||
|
v-if="loadingImport"
|
||||||
|
class="mb-3"
|
||||||
|
indeterminate
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-alert
|
||||||
|
v-if="importError"
|
||||||
|
class="mb-3"
|
||||||
|
type="error"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
{{ importError }}
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<v-alert
|
||||||
|
v-if="planIssues.length"
|
||||||
|
class="mb-3"
|
||||||
|
type="warning"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
{{ planIssues.join(";") }}
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<template v-if="importPlan">
|
||||||
|
<div class="text-body-2 mb-3">
|
||||||
|
共 {{ importPlan.rows.length }} 项作业,分布在 {{ groupedRows.length }} 个日期。
|
||||||
|
冲突项默认保留现有内容。
|
||||||
|
</div>
|
||||||
|
<v-expansion-panels
|
||||||
|
multiple
|
||||||
|
variant="accordion"
|
||||||
|
>
|
||||||
|
<v-expansion-panel
|
||||||
|
v-for="group in groupedRows"
|
||||||
|
:key="group.date"
|
||||||
|
>
|
||||||
|
<v-expansion-panel-title>
|
||||||
|
{{ displayDate(group.date) }}
|
||||||
|
<v-chip
|
||||||
|
class="ml-2"
|
||||||
|
size="small"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
{{ group.rows.length }} 项
|
||||||
|
</v-chip>
|
||||||
|
</v-expansion-panel-title>
|
||||||
|
<v-expansion-panel-text>
|
||||||
|
<v-list lines="three">
|
||||||
|
<v-list-item
|
||||||
|
v-for="row in group.rows"
|
||||||
|
:key="row.id"
|
||||||
|
>
|
||||||
|
<v-list-item-title class="d-flex align-center">
|
||||||
|
{{ row.assignment.subject }}
|
||||||
|
<v-chip
|
||||||
|
v-if="row.conflict"
|
||||||
|
class="ml-2"
|
||||||
|
color="warning"
|
||||||
|
size="x-small"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
已有同名卡片
|
||||||
|
</v-chip>
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list-item-subtitle class="content-preview">
|
||||||
|
{{ row.assignment.content }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
<template #append>
|
||||||
|
<v-select
|
||||||
|
v-model="row.action"
|
||||||
|
class="action-select"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
:items="actionOptions(row)"
|
||||||
|
item-title="title"
|
||||||
|
item-value="value"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-expansion-panel-text>
|
||||||
|
</v-expansion-panel>
|
||||||
|
</v-expansion-panels>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
<v-card-actions>
|
||||||
|
<span class="text-caption text-medium-emphasis ml-2">
|
||||||
|
{{ footerText }}
|
||||||
|
</span>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
:disabled="busy"
|
||||||
|
@click="dialog = false"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
:disabled="primaryDisabled"
|
||||||
|
:loading="busy"
|
||||||
|
:prepend-icon="mode === 'export' ? 'mdi-download' : 'mdi-database-import-outline'"
|
||||||
|
@click="mode === 'export' ? exportSelected() : importSelected()"
|
||||||
|
>
|
||||||
|
{{ mode === "export" ? "导出所选作业" : "确认导入" }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { useDisplay } from "vuetify";
|
||||||
|
import dataProvider from "@/utils/dataProvider";
|
||||||
|
import {
|
||||||
|
createExportPreview,
|
||||||
|
createImportPlan,
|
||||||
|
downloadUafAssignments,
|
||||||
|
executeImportPlan,
|
||||||
|
findImportPlanIssues,
|
||||||
|
itemsFromBoardData,
|
||||||
|
parseUafPdf,
|
||||||
|
UafExportValidationError,
|
||||||
|
} from "@/utils/uafExport";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: Boolean,
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: "export",
|
||||||
|
},
|
||||||
|
currentDate: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
currentItems: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
currentBoardData: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
subjects: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue", "success", "error", "imported"]);
|
||||||
|
const { mobile } = useDisplay();
|
||||||
|
const dialog = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value) => emit("update:modelValue", value),
|
||||||
|
});
|
||||||
|
const exportDate = ref("");
|
||||||
|
const previewRows = ref([]);
|
||||||
|
const exportError = ref("");
|
||||||
|
const loadingPreview = ref(false);
|
||||||
|
const exporting = ref(false);
|
||||||
|
const importFile = ref(null);
|
||||||
|
const importPlan = ref(null);
|
||||||
|
const importError = ref("");
|
||||||
|
const loadingImport = ref(false);
|
||||||
|
const importing = ref(false);
|
||||||
|
const busy = computed(() => exporting.value || importing.value || loadingImport.value);
|
||||||
|
|
||||||
|
const selectedAssignments = computed(() =>
|
||||||
|
previewRows.value.filter((row) => row.selected).map((row) => row.assignment),
|
||||||
|
);
|
||||||
|
const validRows = computed(() => previewRows.value.filter((row) => row.issues.length === 0));
|
||||||
|
const allValidSelected = computed(
|
||||||
|
() => validRows.value.length > 0 && validRows.value.every((row) => row.selected),
|
||||||
|
);
|
||||||
|
const someValidSelected = computed(() => validRows.value.some((row) => row.selected));
|
||||||
|
const groupedRows = computed(() => {
|
||||||
|
if (!importPlan.value) return [];
|
||||||
|
const groups = new Map();
|
||||||
|
for (const row of importPlan.value.rows) {
|
||||||
|
if (!groups.has(row.date)) groups.set(row.date, []);
|
||||||
|
groups.get(row.date).push(row);
|
||||||
|
}
|
||||||
|
return [...groups].map(([date, rows]) => ({ date, rows }));
|
||||||
|
});
|
||||||
|
const planIssues = computed(() => findImportPlanIssues(importPlan.value?.rows || []));
|
||||||
|
const primaryDisabled = computed(() =>
|
||||||
|
props.mode === "export"
|
||||||
|
? selectedAssignments.value.length === 0 || loadingPreview.value
|
||||||
|
: !importPlan.value || planIssues.value.length > 0 || importing.value,
|
||||||
|
);
|
||||||
|
const footerText = computed(() => {
|
||||||
|
if (props.mode === "export") return `文件日期:${exportDate.value || "未选择"}`;
|
||||||
|
if (!importPlan.value) return "仅支持内嵌 uaf_payload.csv 的 UAF PDF";
|
||||||
|
const active = importPlan.value.rows.filter((row) => row.action !== "keep").length;
|
||||||
|
return `将导入 ${active} 项,保留 ${importPlan.value.rows.length - active} 项`;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(open) => {
|
||||||
|
if (!open) return;
|
||||||
|
if (props.mode === "export") {
|
||||||
|
exportDate.value = displayDate(props.currentDate);
|
||||||
|
loadExportPreview();
|
||||||
|
} else {
|
||||||
|
importFile.value = null;
|
||||||
|
importPlan.value = null;
|
||||||
|
importError.value = "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function cloneBoard(board) {
|
||||||
|
return JSON.parse(JSON.stringify(board || { homework: {}, attendance: {} }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBoard(date) {
|
||||||
|
if (date === props.currentDate) return cloneBoard(props.currentBoardData);
|
||||||
|
const result = await dataProvider.loadData(`classworks-data-${date}`);
|
||||||
|
if (result?.success === false) {
|
||||||
|
if (result.error?.code === "NOT_FOUND") {
|
||||||
|
return { homework: {}, attendance: { absent: [], late: [], exclude: [] } };
|
||||||
|
}
|
||||||
|
throw new Error(result.error?.message || `无法读取 ${date} 的作业`);
|
||||||
|
}
|
||||||
|
return cloneBoard(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadExportPreview() {
|
||||||
|
if (!exportDate.value) return;
|
||||||
|
loadingPreview.value = true;
|
||||||
|
exportError.value = "";
|
||||||
|
try {
|
||||||
|
const date = exportDate.value.replaceAll("-", "");
|
||||||
|
const items = date === props.currentDate
|
||||||
|
? props.currentItems
|
||||||
|
: itemsFromBoardData(await loadBoard(date), props.subjects);
|
||||||
|
previewRows.value = createExportPreview(items, exportDate.value);
|
||||||
|
} catch (error) {
|
||||||
|
previewRows.value = [];
|
||||||
|
exportError.value = error.message || "无法加载该日期的作业";
|
||||||
|
} finally {
|
||||||
|
loadingPreview.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAll() {
|
||||||
|
const selected = !allValidSelected.value;
|
||||||
|
for (const row of validRows.value) row.selected = selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportSelected() {
|
||||||
|
exporting.value = true;
|
||||||
|
try {
|
||||||
|
const filename = await downloadUafAssignments(selectedAssignments.value, exportDate.value);
|
||||||
|
emit("success", "导出成功", filename);
|
||||||
|
dialog.value = false;
|
||||||
|
} catch (error) {
|
||||||
|
emitError("导出失败", error);
|
||||||
|
} finally {
|
||||||
|
exporting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareImport(value) {
|
||||||
|
const file = Array.isArray(value) ? value[0] : value;
|
||||||
|
importPlan.value = null;
|
||||||
|
importError.value = "";
|
||||||
|
if (!file) return;
|
||||||
|
loadingImport.value = true;
|
||||||
|
try {
|
||||||
|
const document = await parseUafPdf(file);
|
||||||
|
importPlan.value = await createImportPlan(document, props.subjects, loadBoard);
|
||||||
|
} catch (error) {
|
||||||
|
importError.value = formatError(error);
|
||||||
|
} finally {
|
||||||
|
loadingImport.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function actionOptions(row) {
|
||||||
|
return row.conflict
|
||||||
|
? [
|
||||||
|
{ title: "保留现有", value: "keep" },
|
||||||
|
{ title: "覆盖现有", value: "overwrite" },
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ title: "导入", value: "import" },
|
||||||
|
{ title: "跳过", value: "keep" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importSelected() {
|
||||||
|
importing.value = true;
|
||||||
|
try {
|
||||||
|
const result = await executeImportPlan(importPlan.value, async (date, board) => {
|
||||||
|
const response = await dataProvider.saveData(`classworks-data-${date}`, board);
|
||||||
|
if (response?.success === false) throw new Error(response.error?.message || "保存失败");
|
||||||
|
});
|
||||||
|
emit("imported", result);
|
||||||
|
if (result.failedDates.length) {
|
||||||
|
emit(
|
||||||
|
"error",
|
||||||
|
"部分日期导入失败",
|
||||||
|
`已保存:${result.savedDates.join("、") || "无"}\n失败:${result.failedDates.map((item) => item.date).join("、")}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
emit("success", "导入完成", `已导入 ${result.imported} 项,保留 ${result.skipped} 项`);
|
||||||
|
dialog.value = false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
emitError("导入失败", error);
|
||||||
|
} finally {
|
||||||
|
importing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitError(title, error) {
|
||||||
|
emit("error", title, formatError(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatError(error) {
|
||||||
|
if (error instanceof UafExportValidationError) return error.issues.join("\n");
|
||||||
|
return error?.message || "发生未知错误";
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayDate(date) {
|
||||||
|
if (/^\d{8}$/.test(date)) return `${date.slice(0, 4)}-${date.slice(4, 6)}-${date.slice(6, 8)}`;
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.transfer-content {
|
||||||
|
min-height: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-preview {
|
||||||
|
display: -webkit-box;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: normal;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-select {
|
||||||
|
min-width: 132px;
|
||||||
|
width: 132px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.action-select {
|
||||||
|
min-width: 112px;
|
||||||
|
width: 112px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -200,15 +200,27 @@
|
|||||||
:is-fullscreen="state.isFullscreen"
|
:is-fullscreen="state.isFullscreen"
|
||||||
:show-anti-screen-burn-card="showAntiScreenBurnCard"
|
:show-anti-screen-burn-card="showAntiScreenBurnCard"
|
||||||
:show-test-card-button="showTestCardButton"
|
:show-test-card-button="showTestCardButton"
|
||||||
:uaf-export-disabled="!hasExportableHomework"
|
:uaf-transfer-loading="loading.exportUaf"
|
||||||
:uaf-export-loading="loading.exportUaf"
|
|
||||||
@upload="manualUpload"
|
@upload="manualUpload"
|
||||||
@show-sync-message="showSyncMessage"
|
@show-sync-message="showSyncMessage"
|
||||||
@open-random-picker="openRandomPicker"
|
@open-random-picker="openRandomPicker"
|
||||||
@toggle-fullscreen="toggleFullscreen"
|
@toggle-fullscreen="toggleFullscreen"
|
||||||
@add-test-card="addTestCard"
|
@add-test-card="addTestCard"
|
||||||
@add-exam-card="showAddExamDialog = true"
|
@add-exam-card="showAddExamDialog = true"
|
||||||
@export-uaf="exportUaf"
|
@open-uaf-export="openUafTransfer('export')"
|
||||||
|
@open-uaf-import="openUafTransfer('import')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<uaf-transfer-dialog
|
||||||
|
v-model="uafTransfer.show"
|
||||||
|
:mode="uafTransfer.mode"
|
||||||
|
:current-date="state.dateString"
|
||||||
|
:current-items="sortedItems"
|
||||||
|
:current-board-data="state.boardData"
|
||||||
|
:subjects="state.availableSubjects"
|
||||||
|
@success="handleUafSuccess"
|
||||||
|
@error="handleUafError"
|
||||||
|
@imported="handleUafImported"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<pwa-install-card />
|
<pwa-install-card />
|
||||||
@ -564,11 +576,6 @@ import HomeActions from "@/components/home/HomeActions.vue";
|
|||||||
import FloatingICP from "@/components/FloatingICP.vue";
|
import FloatingICP from "@/components/FloatingICP.vue";
|
||||||
import HitokotoCard from "@/components/HitokotoCard.vue";
|
import HitokotoCard from "@/components/HitokotoCard.vue";
|
||||||
import HomeSkeleton from "@/components/common/HomeSkeleton.vue";
|
import HomeSkeleton from "@/components/common/HomeSkeleton.vue";
|
||||||
import {
|
|
||||||
downloadUafDocument,
|
|
||||||
hasExportableHomework as containsExportableHomework,
|
|
||||||
UafExportValidationError,
|
|
||||||
} from "@/utils/uafExport.js";
|
|
||||||
|
|
||||||
// ===== 非首屏 / 条件渲染组件(异步懒加载)=====
|
// ===== 非首屏 / 条件渲染组件(异步懒加载)=====
|
||||||
const MessageLog = defineAsyncComponent({
|
const MessageLog = defineAsyncComponent({
|
||||||
@ -592,6 +599,10 @@ const HomeworkEditDialog = defineAsyncComponent({
|
|||||||
loader: () => import("@/components/HomeworkEditDialog.vue"),
|
loader: () => import("@/components/HomeworkEditDialog.vue"),
|
||||||
delay: 0,
|
delay: 0,
|
||||||
});
|
});
|
||||||
|
const UafTransferDialog = defineAsyncComponent({
|
||||||
|
loader: () => import("@/components/home/UafTransferDialog.vue"),
|
||||||
|
delay: 0,
|
||||||
|
});
|
||||||
const InitServiceChooser = defineAsyncComponent({
|
const InitServiceChooser = defineAsyncComponent({
|
||||||
loader: () => import("@/components/InitServiceChooser.vue"),
|
loader: () => import("@/components/InitServiceChooser.vue"),
|
||||||
loadingComponent: AsyncLoadingPlaceholder,
|
loadingComponent: AsyncLoadingPlaceholder,
|
||||||
@ -669,6 +680,7 @@ export default {
|
|||||||
ExamScheduleCard,
|
ExamScheduleCard,
|
||||||
ExamConfigEditor,
|
ExamConfigEditor,
|
||||||
HomeSkeleton,
|
HomeSkeleton,
|
||||||
|
UafTransferDialog,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const { mobile } = useDisplay();
|
const { mobile } = useDisplay();
|
||||||
@ -742,6 +754,10 @@ export default {
|
|||||||
copyToToday: false,
|
copyToToday: false,
|
||||||
exportUaf: false,
|
exportUaf: false,
|
||||||
},
|
},
|
||||||
|
uafTransfer: {
|
||||||
|
show: false,
|
||||||
|
mode: "export",
|
||||||
|
},
|
||||||
dataReady: false,
|
dataReady: false,
|
||||||
debouncedUpload: null,
|
debouncedUpload: null,
|
||||||
debouncedAttendanceSave: null,
|
debouncedAttendanceSave: null,
|
||||||
@ -897,6 +913,7 @@ export default {
|
|||||||
name: subjectKey,
|
name: subjectKey,
|
||||||
type: 'homework',
|
type: 'homework',
|
||||||
content: subjectData.content,
|
content: subjectData.content,
|
||||||
|
tags: Array.isArray(subjectData.tags) ? subjectData.tags : [],
|
||||||
order: subject.order,
|
order: subject.order,
|
||||||
rowSpan: estimatedHeight, // Used for sorting only
|
rowSpan: estimatedHeight, // Used for sorting only
|
||||||
});
|
});
|
||||||
@ -937,6 +954,7 @@ export default {
|
|||||||
name: card.name,
|
name: card.name,
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
content: card.content,
|
content: card.content,
|
||||||
|
tags: Array.isArray(card.tags) ? card.tags : [],
|
||||||
order: 9999, // Put at the end
|
order: 9999, // Put at the end
|
||||||
rowSpan: estimatedHeight, // Used for sorting only
|
rowSpan: estimatedHeight, // Used for sorting only
|
||||||
});
|
});
|
||||||
@ -1094,9 +1112,6 @@ export default {
|
|||||||
.sort((a, b) => a.order - b.order)
|
.sort((a, b) => a.order - b.order)
|
||||||
.map((subject) => subject.name);
|
.map((subject) => subject.name);
|
||||||
},
|
},
|
||||||
hasExportableHomework() {
|
|
||||||
return containsExportableHomework(this.sortedItems);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@ -1664,6 +1679,7 @@ export default {
|
|||||||
this.state.boardData.homework[this.currentEditSubject].content = content;
|
this.state.boardData.homework[this.currentEditSubject].content = content;
|
||||||
} else {
|
} else {
|
||||||
this.state.boardData.homework[this.currentEditSubject] = {
|
this.state.boardData.homework[this.currentEditSubject] = {
|
||||||
|
...this.state.boardData.homework[this.currentEditSubject],
|
||||||
content: content,
|
content: content,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -1797,6 +1813,7 @@ export default {
|
|||||||
this.state.boardData.homework[this.currentEditSubject].content = content;
|
this.state.boardData.homework[this.currentEditSubject].content = content;
|
||||||
} else {
|
} else {
|
||||||
this.state.boardData.homework[this.currentEditSubject] = {
|
this.state.boardData.homework[this.currentEditSubject] = {
|
||||||
|
...this.state.boardData.homework[this.currentEditSubject],
|
||||||
content: content,
|
content: content,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -2064,21 +2081,22 @@ export default {
|
|||||||
this.state.synced = false;
|
this.state.synced = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
async exportUaf() {
|
openUafTransfer(mode) {
|
||||||
if (this.loading.exportUaf) return;
|
this.uafTransfer.mode = mode;
|
||||||
this.loading.exportUaf = true;
|
this.uafTransfer.show = true;
|
||||||
try {
|
},
|
||||||
const filename = await downloadUafDocument(this.sortedItems, this.state.dateString);
|
|
||||||
this.$message.success("导出成功", filename);
|
handleUafSuccess(title, content) {
|
||||||
} catch (error) {
|
this.$message.success(title, content);
|
||||||
console.error("UAF export failed:", error);
|
},
|
||||||
if (error instanceof UafExportValidationError) {
|
|
||||||
this.$message.error("无法导出 UAF", error.issues.join("\n"));
|
handleUafError(title, content) {
|
||||||
} else {
|
this.$message.error(title, content);
|
||||||
this.$message.error("导出失败", error?.message || "无法生成 UAF PDF");
|
},
|
||||||
}
|
|
||||||
} finally {
|
async handleUafImported(result) {
|
||||||
this.loading.exportUaf = false;
|
if (result.savedDates.includes(this.state.dateString)) {
|
||||||
|
await this.downloadData(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -2484,7 +2502,10 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
// 普通作业,只复制内容
|
// 普通作业,只复制内容
|
||||||
newHomework[key] = {
|
newHomework[key] = {
|
||||||
content: sourceHomework[key].content
|
content: sourceHomework[key].content,
|
||||||
|
tags: Array.isArray(sourceHomework[key].tags)
|
||||||
|
? [...sourceHomework[key].tags]
|
||||||
|
: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,47 @@ export function normalizeUafDate(value) {
|
|||||||
return `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`;
|
return `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`;
|
||||||
}
|
}
|
||||||
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return value;
|
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) {
|
export function createUafDocument(items, dateValue) {
|
||||||
@ -30,38 +70,68 @@ export function createUafDocument(items, dateValue) {
|
|||||||
subject: String(item.name || "").trim(),
|
subject: String(item.name || "").trim(),
|
||||||
date,
|
date,
|
||||||
content: item.content,
|
content: item.content,
|
||||||
tags: [],
|
tags: normalizeTags(item.tags),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (assignments.length === 0) {
|
if (assignments.length === 0) {
|
||||||
throw new UafExportValidationError(["当前日期没有可导出的作业"]);
|
throw new UafExportValidationError(["所选日期没有可导出的作业"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const issues = [];
|
const issues = assignments.flatMap((assignment, index) =>
|
||||||
assignments.forEach((assignment, index) => {
|
validateUafAssignment(assignment, assignment.subject || `第 ${index + 1} 张卡片`),
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (issues.length > 0) throw new UafExportValidationError(issues);
|
if (issues.length > 0) throw new UafExportValidationError(issues);
|
||||||
return assignments;
|
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) {
|
export function hasExportableHomework(items) {
|
||||||
return items.some(
|
return items.some(
|
||||||
(item) =>
|
(item) =>
|
||||||
@ -72,13 +142,137 @@ export function hasExportableHomework(items) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadUafDocument(items, dateValue) {
|
async function loadBrowserUaf() {
|
||||||
const assignments = createUafDocument(items, dateValue);
|
return import("../vendor/uaf/browser.js");
|
||||||
const { createUafPdf } = await 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 base = import.meta.env.BASE_URL || "/";
|
||||||
const fontUrl = new URL(`${base}uaf/NotoSansSC-Regular.otf`, window.location.origin);
|
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 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 blob = new window.Blob([pdfBytes], { type: "application/pdf" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
@ -91,3 +285,7 @@ export async function downloadUafDocument(items, dateValue) {
|
|||||||
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||||
return link.download;
|
return link.download;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function downloadUafDocument(items, dateValue) {
|
||||||
|
return downloadUafAssignments(createUafDocument(items, dateValue), dateValue);
|
||||||
|
}
|
||||||
|
|||||||
104
src/vendor/uaf/browser.js
vendored
104
src/vendor/uaf/browser.js
vendored
@ -56374,13 +56374,14 @@ function drawPill(page, x, y, w, h, fill2, border) {
|
|||||||
// src/renderCard.ts
|
// src/renderCard.ts
|
||||||
var PAGE_WIDTH = 595.28;
|
var PAGE_WIDTH = 595.28;
|
||||||
var PAGE_HEIGHT = 841.89;
|
var PAGE_HEIGHT = 841.89;
|
||||||
var PAGE_MARGIN = 36;
|
var PAGE_MARGIN = 40;
|
||||||
var WATERMARK_SPACE = 24;
|
var WATERMARK_SPACE = 24;
|
||||||
var COLUMN_GAP = 14;
|
var COLUMN_GAP = 14;
|
||||||
var ROW_GAP = 14;
|
var ROW_GAP = 14;
|
||||||
var CARD_WIDTH = (PAGE_WIDTH - PAGE_MARGIN * 2 - COLUMN_GAP) / 2;
|
var CARD_WIDTH = (PAGE_WIDTH - PAGE_MARGIN * 2 - COLUMN_GAP) / 2;
|
||||||
var CARD_PAD = 14;
|
var CARD_PAD = 16;
|
||||||
var HEADER_HEIGHT = 48;
|
var HEADER_HEIGHT = 56;
|
||||||
|
var CARD_RADIUS = 16;
|
||||||
var CONTENT_FONT = 13.5;
|
var CONTENT_FONT = 13.5;
|
||||||
var CONTENT_LINE_HEIGHT = 19;
|
var CONTENT_LINE_HEIGHT = 19;
|
||||||
var MAX_LINES_PER_FRAGMENT = 12;
|
var MAX_LINES_PER_FRAGMENT = 12;
|
||||||
@ -56389,20 +56390,34 @@ var MIN_CARD_HEIGHT = 140;
|
|||||||
var SUBJECT_FONT = 17;
|
var SUBJECT_FONT = 17;
|
||||||
var DATE_FONT = 9.5;
|
var DATE_FONT = 9.5;
|
||||||
var TAG_FONT = 9.5;
|
var TAG_FONT = 9.5;
|
||||||
var WATERMARK_FONT = 9;
|
var WATERMARK_FONT = 10;
|
||||||
var COLORS = {
|
var DEFAULT_COLORS = {
|
||||||
pageBg: rgb(248 / 255, 250 / 255, 252 / 255),
|
pageBg: rgb(255 / 255, 251 / 255, 254 / 255),
|
||||||
shadow: rgb(203 / 255, 213 / 255, 225 / 255),
|
shadow: rgb(0 / 255, 0 / 255, 0 / 255),
|
||||||
border: rgb(226 / 255, 232 / 255, 240 / 255),
|
border: rgb(121 / 255, 116 / 255, 126 / 255),
|
||||||
card: rgb(1, 1, 1),
|
card: rgb(255 / 255, 251 / 255, 254 / 255),
|
||||||
header: rgb(37 / 255, 99 / 255, 235 / 255),
|
header: rgb(24 / 255, 103 / 255, 192 / 255),
|
||||||
headerText: rgb(1, 1, 1),
|
headerText: rgb(29 / 255, 27 / 255, 32 / 255),
|
||||||
dateText: rgb(219 / 255, 234 / 255, 254 / 255),
|
dateText: rgb(73 / 255, 69 / 255, 79 / 255),
|
||||||
content: rgb(15 / 255, 23 / 255, 42 / 255),
|
content: rgb(29 / 255, 27 / 255, 32 / 255),
|
||||||
chip: rgb(224 / 255, 231 / 255, 255 / 255),
|
chip: rgb(232 / 255, 222 / 255, 248 / 255),
|
||||||
chipText: rgb(55 / 255, 48 / 255, 163 / 255),
|
chipText: rgb(29 / 255, 25 / 255, 43 / 255),
|
||||||
muted: rgb(100 / 255, 116 / 255, 139 / 255),
|
muted: rgb(73 / 255, 69 / 255, 79 / 255),
|
||||||
watermark: rgb(148 / 255, 163 / 255, 184 / 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) {
|
function widthOf(font, text, size) {
|
||||||
return font.widthOfTextAtSize(text, size);
|
return font.widthOfTextAtSize(text, size);
|
||||||
@ -56467,19 +56482,18 @@ function formatDate(date, mode) {
|
|||||||
const parsed = new Date(date);
|
const parsed = new Date(date);
|
||||||
return Number.isNaN(parsed.getTime()) ? date : `${parsed.getFullYear()}\u5E74${parsed.getMonth() + 1}\u6708${parsed.getDate()}\u65E5`;
|
return Number.isNaN(parsed.getTime()) ? date : `${parsed.getFullYear()}\u5E74${parsed.getMonth() + 1}\u6708${parsed.getDate()}\u65E5`;
|
||||||
}
|
}
|
||||||
function drawPageBackground(page, font, canRenderCjk) {
|
function drawPageBackground(page, font, canRenderCjk, colors) {
|
||||||
page.drawRectangle({ x: 0, y: 0, width: PAGE_WIDTH, height: PAGE_HEIGHT, color: COLORS.pageBg });
|
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";
|
const watermark = canRenderCjk ? "\u4F7F\u7528 UAF v1.0 \u5BFC\u51FA" : "Exported with UAF v1.0";
|
||||||
page.drawText(watermark, {
|
page.drawText(watermark, {
|
||||||
x: PAGE_WIDTH - PAGE_MARGIN - widthOf(font, watermark, WATERMARK_FONT),
|
x: PAGE_WIDTH - PAGE_MARGIN - widthOf(font, watermark, WATERMARK_FONT),
|
||||||
y: PAGE_MARGIN - 4,
|
y: PAGE_MARGIN - 4,
|
||||||
size: WATERMARK_FONT,
|
size: WATERMARK_FONT,
|
||||||
font,
|
font,
|
||||||
color: COLORS.watermark,
|
color: colors.watermark
|
||||||
opacity: 0.65
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function drawTags(page, tags, x, y, font) {
|
function drawTags(page, tags, x, y, font, colors) {
|
||||||
if (tags.length === 0) {
|
if (tags.length === 0) {
|
||||||
page.drawText("", { x, y, font, size: TAG_FONT });
|
page.drawText("", { x, y, font, size: TAG_FONT });
|
||||||
return;
|
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 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);
|
const width = Math.min(widthOf(font, label, TAG_FONT) + 16, CARD_WIDTH - CARD_PAD * 2);
|
||||||
if (cursor + width > maxX) break;
|
if (cursor + width > maxX) break;
|
||||||
drawPill(page, cursor, y, width, 19, COLORS.chip);
|
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 });
|
page.drawText(label, { x: cursor + 8, y: y + 5.2, size: TAG_FONT, font, color: colors.chipText });
|
||||||
cursor += width + 6;
|
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;
|
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, CARD_RADIUS, colors.card, {
|
||||||
drawRoundedRect(page, x, y, CARD_WIDTH, fragment.height, 12, COLORS.card, {
|
color: colors.border,
|
||||||
color: COLORS.border,
|
|
||||||
width: 1
|
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 continuation = fragment.continuation ? canRenderCjk ? "\uFF08\u7EED\uFF09" : " (cont.)" : "";
|
||||||
const subject = ellipsize(
|
const subject = ellipsize(
|
||||||
`${fragment.assignment.subject}${continuation}`,
|
`${fragment.assignment.subject}${continuation}`,
|
||||||
@ -56513,39 +56524,39 @@ function drawFragment(page, fragment, x, top, font, fontBold, dateDisplay, canRe
|
|||||||
);
|
);
|
||||||
page.drawText(subject, {
|
page.drawText(subject, {
|
||||||
x: x + CARD_PAD,
|
x: x + CARD_PAD,
|
||||||
y: top - 23,
|
y: top - CARD_PAD - 20,
|
||||||
size: SUBJECT_FONT,
|
size: SUBJECT_FONT,
|
||||||
font: fontBold,
|
font: fontBold,
|
||||||
color: COLORS.headerText
|
color: colors.headerText
|
||||||
});
|
});
|
||||||
page.drawText(formatDate(fragment.assignment.date, dateDisplay), {
|
page.drawText(formatDate(fragment.assignment.date, dateDisplay), {
|
||||||
x: x + CARD_PAD,
|
x: x + CARD_PAD,
|
||||||
y: top - 39,
|
y: top - CARD_PAD - 40,
|
||||||
size: DATE_FONT,
|
size: DATE_FONT,
|
||||||
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) {
|
for (const line of fragment.lines) {
|
||||||
page.drawText(line || " ", {
|
page.drawText(line || " ", {
|
||||||
x: x + CARD_PAD,
|
x: x + CARD_PAD,
|
||||||
y: lineY,
|
y: lineY,
|
||||||
size: CONTENT_FONT,
|
size: CONTENT_FONT,
|
||||||
font,
|
font,
|
||||||
color: COLORS.content
|
color: colors.content
|
||||||
});
|
});
|
||||||
lineY -= CONTENT_LINE_HEIGHT;
|
lineY -= CONTENT_LINE_HEIGHT;
|
||||||
}
|
}
|
||||||
if (fragment.showTags) {
|
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 {
|
} else {
|
||||||
const continued = canRenderCjk ? "\u6B63\u6587\u4E0B\u9875\u7EE7\u7EED" : "Continued on next card";
|
const continued = canRenderCjk ? "\u6B63\u6587\u4E0B\u9875\u7EE7\u7EED" : "Continued on next card";
|
||||||
page.drawText(continued, {
|
page.drawText(continued, {
|
||||||
x: x + CARD_PAD,
|
x: x + CARD_PAD,
|
||||||
y: y + 15,
|
y: y + CARD_PAD + 8,
|
||||||
size: TAG_FONT,
|
size: TAG_FONT,
|
||||||
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 fragments = createFragments(document, font);
|
||||||
const pages = [];
|
const pages = [];
|
||||||
const dateDisplay = options.dateDisplay ?? "zh";
|
const dateDisplay = options.dateDisplay ?? "zh";
|
||||||
|
const colors = options.theme === "classworks-dark" ? CLASSWORKS_DARK_COLORS : DEFAULT_COLORS;
|
||||||
const pageBottom = PAGE_MARGIN + WATERMARK_SPACE;
|
const pageBottom = PAGE_MARGIN + WATERMARK_SPACE;
|
||||||
let page;
|
let page;
|
||||||
let cursorTop = PAGE_HEIGHT - PAGE_MARGIN;
|
let cursorTop = PAGE_HEIGHT - PAGE_MARGIN;
|
||||||
@ -56562,7 +56574,7 @@ function renderAssignmentDocument(pdfDoc, document, font, fontBold, options = {}
|
|||||||
if (!page || cursorTop - rowHeight < pageBottom) {
|
if (!page || cursorTop - rowHeight < pageBottom) {
|
||||||
page = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
|
page = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
|
||||||
pages.push(page);
|
pages.push(page);
|
||||||
drawPageBackground(page, font, options.canRenderCjk !== false);
|
drawPageBackground(page, font, options.canRenderCjk !== false, colors);
|
||||||
cursorTop = PAGE_HEIGHT - PAGE_MARGIN;
|
cursorTop = PAGE_HEIGHT - PAGE_MARGIN;
|
||||||
}
|
}
|
||||||
pair.forEach((fragment, column) => {
|
pair.forEach((fragment, column) => {
|
||||||
@ -56574,7 +56586,8 @@ function renderAssignmentDocument(pdfDoc, document, font, fontBold, options = {}
|
|||||||
font,
|
font,
|
||||||
fontBold,
|
fontBold,
|
||||||
dateDisplay,
|
dateDisplay,
|
||||||
options.canRenderCjk !== false
|
options.canRenderCjk !== false,
|
||||||
|
colors
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
cursorTop -= rowHeight + ROW_GAP;
|
cursorTop -= rowHeight + ROW_GAP;
|
||||||
@ -56597,7 +56610,8 @@ async function createUafPdfWithFont(document, options = {}) {
|
|||||||
}
|
}
|
||||||
renderAssignmentDocument(pdfDoc, validated, font, font, {
|
renderAssignmentDocument(pdfDoc, validated, font, font, {
|
||||||
dateDisplay: options.useStandardFont ? "iso" : "zh",
|
dateDisplay: options.useStandardFont ? "iso" : "zh",
|
||||||
canRenderCjk: !options.useStandardFont
|
canRenderCjk: !options.useStandardFont,
|
||||||
|
theme: options.theme
|
||||||
});
|
});
|
||||||
await pdfDoc.attach(csvBytes, UAF_PAYLOAD_FILENAME, {
|
await pdfDoc.attach(csvBytes, UAF_PAYLOAD_FILENAME, {
|
||||||
mimeType: "text/csv",
|
mimeType: "text/csv",
|
||||||
@ -56833,7 +56847,11 @@ async function createUafPdf(document, options = {}) {
|
|||||||
const fontBytes = await loadBrowserFont(options);
|
const fontBytes = await loadBrowserFont(options);
|
||||||
const wasmUrl = options.wasmUrl ?? new URL("../assets/hb-subset.wasm", import.meta.url);
|
const wasmUrl = options.wasmUrl ?? new URL("../assets/hb-subset.wasm", import.meta.url);
|
||||||
const subset = await subsetFontInBrowser(fontBytes, collectDocumentText(document), wasmUrl);
|
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 = {}) {
|
async function createUafPdfFromCsv(csv, options = {}) {
|
||||||
return createUafPdf(parsePayload(csv), options);
|
return createUafPdf(parsePayload(csv), options);
|
||||||
|
|||||||
2
src/vendor/uaf/manifest.json
vendored
2
src/vendor/uaf/manifest.json
vendored
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"uafVersion": "1.0",
|
"uafVersion": "1.0",
|
||||||
"source": "../UnifiedAssignmentFormat/implementations/typescript/packages/pdf",
|
"source": "../UnifiedAssignmentFormat/implementations/typescript/packages/pdf",
|
||||||
"bundleSha256": "6086d88815b14b0daeb5c27782f0eeb5b4a9dd0fdc51ff5371166bf79dd6e2b2",
|
"bundleSha256": "1e059777c77c22569ed628bbf3f4b20f59abab46337ff6a6d814c2497d17e4e1",
|
||||||
"fontSha256": "a3041811a78c361b1de50f953c805e0244951c21c5bd412f7232ef0d899af0da",
|
"fontSha256": "a3041811a78c361b1de50f953c805e0244951c21c5bd412f7232ef0d899af0da",
|
||||||
"wasmSha256": "1bf32603c1dfe17e1b9d54acaec6adfd0fc5c517e088648ba7e44a24213ae93e"
|
"wasmSha256": "1bf32603c1dfe17e1b9d54acaec6adfd0fc5c517e088648ba7e44a24213ae93e"
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user