mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2026-08-12 09:20:31 +00:00
Compare commits
4 Commits
5bfb5c9abb
...
cf9352fc1f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf9352fc1f | ||
|
|
7dcdb57bfc | ||
|
|
69ff28e39d | ||
|
|
64b15d058a |
@ -1,3 +1,7 @@
|
|||||||
|
allowBuilds:
|
||||||
|
'@parcel/watcher': false
|
||||||
|
esbuild: false
|
||||||
|
sharp: false
|
||||||
onlyBuiltDependencies:
|
onlyBuiltDependencies:
|
||||||
- '@parcel/watcher'
|
- '@parcel/watcher'
|
||||||
- esbuild
|
- esbuild
|
||||||
|
|||||||
BIN
public/uaf/HarmonyOS_Sans_SC_Bold.ttf
Normal file
BIN
public/uaf/HarmonyOS_Sans_SC_Bold.ttf
Normal file
Binary file not shown.
BIN
public/uaf/HarmonyOS_Sans_SC_Light.ttf
Normal file
BIN
public/uaf/HarmonyOS_Sans_SC_Light.ttf
Normal file
Binary file not shown.
BIN
public/uaf/HarmonyOS_Sans_SC_Regular.ttf
Normal file
BIN
public/uaf/HarmonyOS_Sans_SC_Regular.ttf
Normal file
Binary file not shown.
Binary file not shown.
@ -33,7 +33,9 @@
|
|||||||
|
|
||||||
size="large"
|
size="large"
|
||||||
rounded="xl"
|
rounded="xl"
|
||||||
><v-icon icon="mdi-swap-vertical-bold"></v-icon></v-btn>
|
>
|
||||||
|
<v-icon icon="mdi-swap-vertical-bold" />
|
||||||
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<v-list density="comfortable">
|
<v-list density="comfortable">
|
||||||
<v-list-item
|
<v-list-item
|
||||||
@ -76,9 +78,9 @@
|
|||||||
v-if="showExamScheduleButton"
|
v-if="showExamScheduleButton"
|
||||||
prepend-icon="mdi-calendar-check"
|
prepend-icon="mdi-calendar-check"
|
||||||
size="large"
|
size="large"
|
||||||
@click="$emit('add-exam-card')"
|
|
||||||
class="ml-2"
|
class="ml-2"
|
||||||
color="green"
|
color="green"
|
||||||
|
@click="$emit('add-exam-card')"
|
||||||
>
|
>
|
||||||
考试看板
|
考试看板
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@ -105,7 +107,6 @@
|
|||||||
>
|
>
|
||||||
添加测试卡片
|
添加测试卡片
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-card
|
<v-card
|
||||||
|
|||||||
@ -24,6 +24,25 @@
|
|||||||
|
|
||||||
<v-card-text class="transfer-content">
|
<v-card-text class="transfer-content">
|
||||||
<template v-if="mode === 'export'">
|
<template v-if="mode === 'export'">
|
||||||
|
<v-tabs
|
||||||
|
v-if="mobile"
|
||||||
|
v-model="mobileTab"
|
||||||
|
fixed-tabs
|
||||||
|
class="mb-3"
|
||||||
|
>
|
||||||
|
<v-tab value="edit">
|
||||||
|
编辑
|
||||||
|
</v-tab>
|
||||||
|
<v-tab value="preview">
|
||||||
|
预览
|
||||||
|
</v-tab>
|
||||||
|
</v-tabs>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="export-layout"
|
||||||
|
:class="{ mobile: mobile, 'show-preview': mobile && mobileTab === 'preview' }"
|
||||||
|
>
|
||||||
|
<div class="editor-section">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="exportDate"
|
v-model="exportDate"
|
||||||
class="mb-3"
|
class="mb-3"
|
||||||
@ -74,6 +93,7 @@
|
|||||||
<v-list-item
|
<v-list-item
|
||||||
v-for="row in previewRows"
|
v-for="row in previewRows"
|
||||||
:key="row.id"
|
:key="row.id"
|
||||||
|
class="edit-list-item"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<v-checkbox-btn
|
<v-checkbox-btn
|
||||||
@ -81,6 +101,7 @@
|
|||||||
:disabled="row.issues.length > 0"
|
:disabled="row.issues.length > 0"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
<div class="d-flex flex-column w-100">
|
||||||
<v-list-item-title>{{ row.assignment.subject }}</v-list-item-title>
|
<v-list-item-title>{{ row.assignment.subject }}</v-list-item-title>
|
||||||
<v-list-item-subtitle class="content-preview">
|
<v-list-item-subtitle class="content-preview">
|
||||||
{{ row.assignment.content }}
|
{{ row.assignment.content }}
|
||||||
@ -108,6 +129,7 @@
|
|||||||
{{ row.issues.join(";") }}
|
{{ row.issues.join(";") }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
|
|
||||||
@ -117,6 +139,95 @@
|
|||||||
text="该日期没有可导出的作业"
|
text="该日期没有可导出的作业"
|
||||||
title="暂无作业"
|
title="暂无作业"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="preview-section">
|
||||||
|
<div class="preview-placeholder">
|
||||||
|
<div class="preview-scroll-container">
|
||||||
|
<v-progress-linear
|
||||||
|
v-if="previewLoading"
|
||||||
|
indeterminate
|
||||||
|
/>
|
||||||
|
<v-alert
|
||||||
|
v-else-if="previewError"
|
||||||
|
type="error"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
{{ previewError }}
|
||||||
|
</v-alert>
|
||||||
|
<v-alert
|
||||||
|
v-else-if="!previewUrl"
|
||||||
|
type="info"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
请选择有效作业以预览
|
||||||
|
</v-alert>
|
||||||
|
<iframe
|
||||||
|
v-else
|
||||||
|
:src="previewUrl"
|
||||||
|
class="pdf-preview-frame"
|
||||||
|
title="UAF 导出预览"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-expansion-panels
|
||||||
|
v-model="fontSettingsPanels"
|
||||||
|
variant="accordion"
|
||||||
|
class="mt-3 font-settings-panels"
|
||||||
|
>
|
||||||
|
<v-expansion-panel value="settings">
|
||||||
|
<v-expansion-panel-title>
|
||||||
|
导出字体设置
|
||||||
|
</v-expansion-panel-title>
|
||||||
|
<v-expansion-panel-text>
|
||||||
|
<div
|
||||||
|
v-for="field in styleFields"
|
||||||
|
:key="field.key"
|
||||||
|
class="mb-3"
|
||||||
|
>
|
||||||
|
<div class="d-flex align-center ga-3">
|
||||||
|
<span class="text-caption label-min-width">{{ field.label }}</span>
|
||||||
|
<v-slider
|
||||||
|
v-model="globalStyle[field.key].fontSize"
|
||||||
|
:min="field.min"
|
||||||
|
:max="field.max"
|
||||||
|
:step="0.5"
|
||||||
|
hide-details
|
||||||
|
class="flex-grow-1"
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
:model-value="globalStyle[field.key].fontSize"
|
||||||
|
type="number"
|
||||||
|
:min="field.min"
|
||||||
|
:max="field.max"
|
||||||
|
step="0.5"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
variant="outlined"
|
||||||
|
class="style-number-field"
|
||||||
|
@update:model-value="setGlobalFontSize(field.key, $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-center ga-3 mt-2">
|
||||||
|
<span class="text-caption label-min-width">字重</span>
|
||||||
|
<v-select
|
||||||
|
v-model="globalStyle[field.key].fontWeight"
|
||||||
|
:items="fontWeightOptions"
|
||||||
|
item-title="title"
|
||||||
|
item-value="value"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
variant="outlined"
|
||||||
|
class="style-weight-select"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-expansion-panel-text>
|
||||||
|
</v-expansion-panel>
|
||||||
|
</v-expansion-panels>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@ -247,10 +358,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from "vue";
|
import { computed, onUnmounted, ref, watch } from "vue";
|
||||||
import { useDisplay } from "vuetify";
|
import { useDisplay } from "vuetify";
|
||||||
import dataProvider from "@/utils/dataProvider";
|
import dataProvider from "@/utils/dataProvider";
|
||||||
|
import { debounce } from "@/utils/debounce";
|
||||||
import {
|
import {
|
||||||
|
buildUafPdfBytes,
|
||||||
createExportPreview,
|
createExportPreview,
|
||||||
createImportPlan,
|
createImportPlan,
|
||||||
downloadUafAssignments,
|
downloadUafAssignments,
|
||||||
@ -287,6 +400,22 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(["update:modelValue", "success", "error", "imported"]);
|
const emit = defineEmits(["update:modelValue", "success", "error", "imported"]);
|
||||||
const { mobile } = useDisplay();
|
const { mobile } = useDisplay();
|
||||||
|
const fontWeightOptions = [
|
||||||
|
{ title: "Light", value: "light" },
|
||||||
|
{ title: "Regular", value: "regular" },
|
||||||
|
{ title: "Bold", value: "bold" },
|
||||||
|
];
|
||||||
|
const styleFields = [
|
||||||
|
{ key: "subject", label: "标题", min: 10, max: 32 },
|
||||||
|
{ key: "content", label: "正文", min: 8, max: 24 },
|
||||||
|
{ key: "tags", label: "标签", min: 6, max: 16 },
|
||||||
|
];
|
||||||
|
const globalStyle = ref({
|
||||||
|
subject: { fontSize: 17, fontWeight: "regular" },
|
||||||
|
content: { fontSize: 13.5, fontWeight: "regular" },
|
||||||
|
tags: { fontSize: 9.5, fontWeight: "regular" },
|
||||||
|
});
|
||||||
|
const fontSettingsPanels = ref([]);
|
||||||
const dialog = computed({
|
const dialog = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
set: (value) => emit("update:modelValue", value),
|
set: (value) => emit("update:modelValue", value),
|
||||||
@ -301,11 +430,21 @@ const importPlan = ref(null);
|
|||||||
const importError = ref("");
|
const importError = ref("");
|
||||||
const loadingImport = ref(false);
|
const loadingImport = ref(false);
|
||||||
const importing = ref(false);
|
const importing = ref(false);
|
||||||
|
const mobileTab = ref("edit");
|
||||||
|
const previewUrl = ref("");
|
||||||
|
const previewLoading = ref(false);
|
||||||
|
const previewError = ref("");
|
||||||
const busy = computed(() => exporting.value || importing.value || loadingImport.value);
|
const busy = computed(() => exporting.value || importing.value || loadingImport.value);
|
||||||
|
|
||||||
const selectedAssignments = computed(() =>
|
const selectedAssignments = computed(() =>
|
||||||
previewRows.value.filter((row) => row.selected).map((row) => row.assignment),
|
previewRows.value.filter((row) => row.selected).map((row) => row.assignment),
|
||||||
);
|
);
|
||||||
|
const styledSelectedAssignments = computed(() =>
|
||||||
|
selectedAssignments.value.map((assignment) => ({
|
||||||
|
...assignment,
|
||||||
|
style: JSON.parse(JSON.stringify(globalStyle.value)),
|
||||||
|
})),
|
||||||
|
);
|
||||||
const validRows = computed(() => previewRows.value.filter((row) => row.issues.length === 0));
|
const validRows = computed(() => previewRows.value.filter((row) => row.issues.length === 0));
|
||||||
const allValidSelected = computed(
|
const allValidSelected = computed(
|
||||||
() => validRows.value.length > 0 && validRows.value.every((row) => row.selected),
|
() => validRows.value.length > 0 && validRows.value.every((row) => row.selected),
|
||||||
@ -335,7 +474,11 @@ const footerText = computed(() => {
|
|||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(open) => {
|
(open, oldOpen) => {
|
||||||
|
if (!open && oldOpen) {
|
||||||
|
releasePreviewUrl();
|
||||||
|
mobileTab.value = "edit";
|
||||||
|
}
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
if (props.mode === "export") {
|
if (props.mode === "export") {
|
||||||
exportDate.value = displayDate(props.currentDate);
|
exportDate.value = displayDate(props.currentDate);
|
||||||
@ -348,6 +491,20 @@ watch(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
selectedAssignments,
|
||||||
|
() => {
|
||||||
|
generatePreview();
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(globalStyle, () => generatePreview(), { deep: true });
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
releasePreviewUrl();
|
||||||
|
});
|
||||||
|
|
||||||
function cloneBoard(board) {
|
function cloneBoard(board) {
|
||||||
return JSON.parse(JSON.stringify(board || { homework: {}, attendance: {} }));
|
return JSON.parse(JSON.stringify(board || { homework: {}, attendance: {} }));
|
||||||
}
|
}
|
||||||
@ -387,10 +544,47 @@ function toggleAll() {
|
|||||||
for (const row of validRows.value) row.selected = selected;
|
for (const row of validRows.value) row.selected = selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setGlobalFontSize(fieldKey, value) {
|
||||||
|
const field = styleFields.find((f) => f.key === fieldKey);
|
||||||
|
const parsed = Number(value);
|
||||||
|
const size = Number.isFinite(parsed) ? parsed : field.min;
|
||||||
|
globalStyle.value[fieldKey].fontSize =
|
||||||
|
Math.round(Math.min(field.max, Math.max(field.min, size)) * 2) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function releasePreviewUrl() {
|
||||||
|
if (previewUrl.value) {
|
||||||
|
URL.revokeObjectURL(previewUrl.value);
|
||||||
|
previewUrl.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const generatePreview = debounce(async () => {
|
||||||
|
if (!styledSelectedAssignments.value.length) {
|
||||||
|
releasePreviewUrl();
|
||||||
|
previewError.value = "";
|
||||||
|
previewLoading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
previewLoading.value = true;
|
||||||
|
previewError.value = "";
|
||||||
|
try {
|
||||||
|
const pdfBytes = await buildUafPdfBytes(styledSelectedAssignments.value);
|
||||||
|
const blob = new window.Blob([pdfBytes], { type: "application/pdf" });
|
||||||
|
releasePreviewUrl();
|
||||||
|
previewUrl.value = URL.createObjectURL(blob);
|
||||||
|
} catch (error) {
|
||||||
|
releasePreviewUrl();
|
||||||
|
previewError.value = formatError(error);
|
||||||
|
} finally {
|
||||||
|
previewLoading.value = false;
|
||||||
|
}
|
||||||
|
}, 800);
|
||||||
|
|
||||||
async function exportSelected() {
|
async function exportSelected() {
|
||||||
exporting.value = true;
|
exporting.value = true;
|
||||||
try {
|
try {
|
||||||
const filename = await downloadUafAssignments(selectedAssignments.value, exportDate.value);
|
const filename = await downloadUafAssignments(styledSelectedAssignments.value, exportDate.value);
|
||||||
emit("success", "导出成功", filename);
|
emit("success", "导出成功", filename);
|
||||||
dialog.value = false;
|
dialog.value = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -481,6 +675,97 @@ function displayDate(date) {
|
|||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.export-layout {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-section {
|
||||||
|
flex: 7;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-section {
|
||||||
|
flex: 5;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-placeholder {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 360px;
|
||||||
|
max-height: 480px;
|
||||||
|
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-scroll-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-preview-frame {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-list-item {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-weight-select {
|
||||||
|
max-width: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-min-width {
|
||||||
|
min-width: 40px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-number-field {
|
||||||
|
max-width: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-select-field {
|
||||||
|
max-width: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-layout.mobile {
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-layout.mobile .editor-section,
|
||||||
|
.export-layout.mobile .preview-section {
|
||||||
|
display: none;
|
||||||
|
overflow-y: visible;
|
||||||
|
min-height: 60vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-layout.mobile.show-preview .preview-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-layout.mobile.show-preview .preview-placeholder {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-layout.mobile:not(.show-preview) .editor-section {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.action-select {
|
.action-select {
|
||||||
min-width: 132px;
|
min-width: 132px;
|
||||||
width: 132px;
|
width: 132px;
|
||||||
|
|||||||
@ -490,7 +490,16 @@
|
|||||||
考试看板
|
考试看板
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-list><v-list-item active color="green" @click="$router.push('/examschedule')" append-icon="mdi-arrow-right">打开考试看板</v-list-item></v-list>
|
<v-list>
|
||||||
|
<v-list-item
|
||||||
|
active
|
||||||
|
color="green"
|
||||||
|
append-icon="mdi-arrow-right"
|
||||||
|
@click="$router.push('/examschedule')"
|
||||||
|
>
|
||||||
|
打开考试看板
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
<v-list v-if="examStore.examList.length > 0">
|
<v-list v-if="examStore.examList.length > 0">
|
||||||
<v-list-item
|
<v-list-item
|
||||||
v-for="exam in examStore.examList"
|
v-for="exam in examStore.examList"
|
||||||
|
|||||||
@ -71,6 +71,7 @@ export function createUafDocument(items, dateValue) {
|
|||||||
date,
|
date,
|
||||||
content: item.content,
|
content: item.content,
|
||||||
tags: normalizeTags(item.tags),
|
tags: normalizeTags(item.tags),
|
||||||
|
style: item.style ? JSON.parse(JSON.stringify(item.style)) : JSON.parse(JSON.stringify(DEFAULT_ASSIGNMENT_STYLE)),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (assignments.length === 0) {
|
if (assignments.length === 0) {
|
||||||
@ -84,6 +85,12 @@ export function createUafDocument(items, dateValue) {
|
|||||||
return assignments;
|
return assignments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_ASSIGNMENT_STYLE = {
|
||||||
|
subject: { fontSize: 17, fontWeight: "regular" },
|
||||||
|
content: { fontSize: 13.5, fontWeight: "regular" },
|
||||||
|
tags: { fontSize: 9.5, fontWeight: "regular" },
|
||||||
|
};
|
||||||
|
|
||||||
export function createExportPreview(items, dateValue) {
|
export function createExportPreview(items, dateValue) {
|
||||||
const date = normalizeUafDate(dateValue);
|
const date = normalizeUafDate(dateValue);
|
||||||
return items
|
return items
|
||||||
@ -95,9 +102,10 @@ export function createExportPreview(items, dateValue) {
|
|||||||
date,
|
date,
|
||||||
content: item.content,
|
content: item.content,
|
||||||
tags: normalizeTags(item.tags),
|
tags: normalizeTags(item.tags),
|
||||||
|
style: JSON.parse(JSON.stringify(DEFAULT_ASSIGNMENT_STYLE)),
|
||||||
};
|
};
|
||||||
const issues = validateUafAssignment(assignment, assignment.subject || `第 ${index + 1} 张卡片`);
|
const issues = validateUafAssignment(assignment, assignment.subject || `第 ${index + 1} 张卡片`);
|
||||||
return { id: `${item.key || index}-${index}`, assignment, selected: issues.length === 0, issues };
|
return { id: `${item.key || index}-${index}`, assignment, selected: issues.length === 0, issues, expanded: false };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,19 +268,35 @@ export async function executeImportPlan(plan, saveBoardData) {
|
|||||||
return { imported, skipped, savedDates, failedDates };
|
return { imported, skipped, savedDates, failedDates };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadUafAssignments(assignments, dateValue) {
|
async function fetchFont(base, filename) {
|
||||||
|
const response = await fetch(new URL(`${base}uaf/${filename}`, window.location.origin));
|
||||||
|
if (!response.ok) throw new Error(`加载字体 ${filename} 失败:${response.status}`);
|
||||||
|
return response.arrayBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildUafPdfBytes(assignments) {
|
||||||
if (!assignments.length) throw new UafExportValidationError(["请至少选择一项作业"]);
|
if (!assignments.length) throw new UafExportValidationError(["请至少选择一项作业"]);
|
||||||
const issues = assignments.flatMap((assignment) => validateUafAssignment(assignment));
|
const issues = assignments.flatMap((assignment) => validateUafAssignment(assignment));
|
||||||
if (issues.length) throw new UafExportValidationError(issues);
|
if (issues.length) throw new UafExportValidationError(issues);
|
||||||
const { createUafPdf } = await loadBrowserUaf();
|
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 [fontBytes, fontLightBytes, fontBoldBytes] = await Promise.all([
|
||||||
|
fetchFont(base, "HarmonyOS_Sans_SC_Regular.ttf"),
|
||||||
|
fetchFont(base, "HarmonyOS_Sans_SC_Light.ttf"),
|
||||||
|
fetchFont(base, "HarmonyOS_Sans_SC_Bold.ttf"),
|
||||||
|
]);
|
||||||
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, {
|
return createUafPdf(assignments, {
|
||||||
fontUrl,
|
fontBytes,
|
||||||
|
fontLightBytes,
|
||||||
|
fontBoldBytes,
|
||||||
wasmUrl,
|
wasmUrl,
|
||||||
theme: "classworks-dark",
|
theme: "classworks-dark",
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadUafAssignments(assignments, dateValue) {
|
||||||
|
const pdfBytes = await buildUafPdfBytes(assignments);
|
||||||
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");
|
||||||
|
|||||||
151
src/vendor/uaf/browser.js
vendored
151
src/vendor/uaf/browser.js
vendored
@ -8323,13 +8323,22 @@ function isValidIso8601(value) {
|
|||||||
const parsed = Date.parse(value);
|
const parsed = Date.parse(value);
|
||||||
return !Number.isNaN(parsed);
|
return !Number.isNaN(parsed);
|
||||||
}
|
}
|
||||||
|
var stylePartSchema = external_exports.object({
|
||||||
|
fontSize: external_exports.number().optional(),
|
||||||
|
fontWeight: external_exports.string().optional()
|
||||||
|
});
|
||||||
var uafAssignmentSchema = external_exports.object({
|
var uafAssignmentSchema = external_exports.object({
|
||||||
subject: external_exports.string().min(1, "subject must not be empty").max(LIMITS.subjectMax, `subject must be at most ${LIMITS.subjectMax} characters`),
|
subject: external_exports.string().min(1, "subject must not be empty").max(LIMITS.subjectMax, `subject must be at most ${LIMITS.subjectMax} characters`),
|
||||||
date: external_exports.string().min(1, "date must not be empty").refine(isValidIso8601, "date must be valid ISO 8601"),
|
date: external_exports.string().min(1, "date must not be empty").refine(isValidIso8601, "date must be valid ISO 8601"),
|
||||||
content: external_exports.string().min(1, "content must not be empty").max(LIMITS.contentMax, `content must be at most ${LIMITS.contentMax} characters`),
|
content: external_exports.string().min(1, "content must not be empty").max(LIMITS.contentMax, `content must be at most ${LIMITS.contentMax} characters`),
|
||||||
tags: external_exports.array(
|
tags: external_exports.array(
|
||||||
external_exports.string().min(1, "tag must not be empty").max(LIMITS.tagMax, `each tag must be at most ${LIMITS.tagMax} characters`).refine((t) => !t.includes(";"), 'tag must not contain ";"')
|
external_exports.string().min(1, "tag must not be empty").max(LIMITS.tagMax, `each tag must be at most ${LIMITS.tagMax} characters`).refine((t) => !t.includes(";"), 'tag must not contain ";"')
|
||||||
).max(LIMITS.tagCountMax, `at most ${LIMITS.tagCountMax} tags allowed`)
|
).max(LIMITS.tagCountMax, `at most ${LIMITS.tagCountMax} tags allowed`),
|
||||||
|
style: external_exports.object({
|
||||||
|
subject: stylePartSchema.optional(),
|
||||||
|
content: stylePartSchema.optional(),
|
||||||
|
tags: stylePartSchema.optional()
|
||||||
|
}).optional()
|
||||||
});
|
});
|
||||||
var uafDocumentSchema = external_exports.array(uafAssignmentSchema).min(1, "document must contain at least one assignment");
|
var uafDocumentSchema = external_exports.array(uafAssignmentSchema).min(1, "document must contain at least one assignment");
|
||||||
function escapeField(value) {
|
function escapeField(value) {
|
||||||
@ -25488,7 +25497,7 @@ var INTRINSICS = {
|
|||||||
"%Error%": Error,
|
"%Error%": Error,
|
||||||
"%ErrorPrototype%": Error.prototype,
|
"%ErrorPrototype%": Error.prototype,
|
||||||
"%eval%": eval,
|
"%eval%": eval,
|
||||||
// eslint-disable-line no-eval
|
|
||||||
"%EvalError%": EvalError,
|
"%EvalError%": EvalError,
|
||||||
"%EvalErrorPrototype%": EvalError.prototype,
|
"%EvalErrorPrototype%": EvalError.prototype,
|
||||||
"%Float32Array%": typeof Float32Array === "undefined" ? undefined$1 : Float32Array,
|
"%Float32Array%": typeof Float32Array === "undefined" ? undefined$1 : Float32Array,
|
||||||
@ -48402,7 +48411,7 @@ var decodePDFRawStream = function(_a) {
|
|||||||
for (var idx = 0, len2 = Filter.size(); idx < len2; idx++) {
|
for (var idx = 0, len2 = Filter.size(); idx < len2; idx++) {
|
||||||
stream2 = decodeStream(stream2, Filter.lookup(idx, PDFName_default), DecodeParms && DecodeParms.lookupMaybe(idx, PDFDict_default));
|
stream2 = decodeStream(stream2, Filter.lookup(idx, PDFName_default), DecodeParms && DecodeParms.lookupMaybe(idx, PDFDict_default));
|
||||||
}
|
}
|
||||||
} else if (!!Filter) {
|
} else if (Filter) {
|
||||||
throw new UnexpectedObjectTypeError([PDFName_default, PDFArray_default], Filter);
|
throw new UnexpectedObjectTypeError([PDFName_default, PDFArray_default], Filter);
|
||||||
}
|
}
|
||||||
return stream2;
|
return stream2;
|
||||||
@ -50830,7 +50839,7 @@ var PDFParser = (
|
|||||||
this.context.header = this.parseHeader();
|
this.context.header = this.parseHeader();
|
||||||
_a.label = 1;
|
_a.label = 1;
|
||||||
case 1:
|
case 1:
|
||||||
if (!!this.bytes.done()) return [3, 3];
|
if (this.bytes.done()) return [3, 3];
|
||||||
return [4, this.parseDocumentSection()];
|
return [4, this.parseDocumentSection()];
|
||||||
case 2:
|
case 2:
|
||||||
_a.sent();
|
_a.sent();
|
||||||
@ -53019,7 +53028,7 @@ var PDFEmbeddedPage = (
|
|||||||
return __generator(this, function(_a) {
|
return __generator(this, function(_a) {
|
||||||
switch (_a.label) {
|
switch (_a.label) {
|
||||||
case 0:
|
case 0:
|
||||||
if (!!this.alreadyEmbedded) return [3, 2];
|
if (this.alreadyEmbedded) return [3, 2];
|
||||||
return [4, this.embedder.embedIntoContext(this.doc.context, this.ref)];
|
return [4, this.embedder.embedIntoContext(this.doc.context, this.ref)];
|
||||||
case 1:
|
case 1:
|
||||||
_a.sent();
|
_a.sent();
|
||||||
@ -54745,7 +54754,7 @@ var PDFEmbeddedFile = (
|
|||||||
return __generator(this, function(_a) {
|
return __generator(this, function(_a) {
|
||||||
switch (_a.label) {
|
switch (_a.label) {
|
||||||
case 0:
|
case 0:
|
||||||
if (!!this.alreadyEmbedded) return [3, 2];
|
if (this.alreadyEmbedded) return [3, 2];
|
||||||
return [4, this.embedder.embedIntoContext(this.doc.context, this.ref)];
|
return [4, this.embedder.embedIntoContext(this.doc.context, this.ref)];
|
||||||
case 1:
|
case 1:
|
||||||
ref = _a.sent();
|
ref = _a.sent();
|
||||||
@ -54803,7 +54812,7 @@ var PDFJavaScript = (
|
|||||||
return __generator(this, function(_b) {
|
return __generator(this, function(_b) {
|
||||||
switch (_b.label) {
|
switch (_b.label) {
|
||||||
case 0:
|
case 0:
|
||||||
if (!!this.alreadyEmbedded) return [3, 2];
|
if (this.alreadyEmbedded) return [3, 2];
|
||||||
_a = this.doc, catalog = _a.catalog, context2 = _a.context;
|
_a = this.doc, catalog = _a.catalog, context2 = _a.context;
|
||||||
return [4, this.embedder.embedIntoContext(this.doc.context, this.ref)];
|
return [4, this.embedder.embedIntoContext(this.doc.context, this.ref)];
|
||||||
case 1:
|
case 1:
|
||||||
@ -56455,21 +56464,28 @@ function createFragments(document, font) {
|
|||||||
const maxWidth = CARD_WIDTH - CARD_PAD * 2;
|
const maxWidth = CARD_WIDTH - CARD_PAD * 2;
|
||||||
const fragments = [];
|
const fragments = [];
|
||||||
for (const assignment of document) {
|
for (const assignment of document) {
|
||||||
const lines = wrapText(assignment.content, font, CONTENT_FONT, maxWidth);
|
const contentStyle = assignment.style?.content || {};
|
||||||
|
const contentFontSize = typeof contentStyle.fontSize === "number" ? contentStyle.fontSize : CONTENT_FONT;
|
||||||
|
const contentLineHeight = contentFontSize * (CONTENT_LINE_HEIGHT / CONTENT_FONT);
|
||||||
|
const tagStyle = assignment.style?.tags || {};
|
||||||
|
const tagFontSize = typeof tagStyle.fontSize === "number" ? tagStyle.fontSize : TAG_FONT;
|
||||||
|
const tagAreaHeight = Math.max(TAG_AREA_HEIGHT, tagFontSize * 3 + 12);
|
||||||
|
const lines = wrapText(assignment.content, font, contentFontSize, maxWidth);
|
||||||
for (let index = 0; index < lines.length; index += MAX_LINES_PER_FRAGMENT) {
|
for (let index = 0; index < lines.length; index += MAX_LINES_PER_FRAGMENT) {
|
||||||
const fragmentLines = lines.slice(index, index + MAX_LINES_PER_FRAGMENT);
|
const fragmentLines = lines.slice(index, index + MAX_LINES_PER_FRAGMENT);
|
||||||
const showTags = index + MAX_LINES_PER_FRAGMENT >= lines.length;
|
const showTags = index + MAX_LINES_PER_FRAGMENT >= lines.length;
|
||||||
const contentHeight = fragmentLines.length * CONTENT_LINE_HEIGHT;
|
const contentHeight = fragmentLines.length * contentLineHeight;
|
||||||
const height = Math.max(
|
const height = Math.max(
|
||||||
MIN_CARD_HEIGHT,
|
MIN_CARD_HEIGHT,
|
||||||
CARD_PAD + HEADER_HEIGHT + 12 + contentHeight + (showTags ? TAG_AREA_HEIGHT : 12) + CARD_PAD
|
CARD_PAD + HEADER_HEIGHT + 12 + contentHeight + (showTags ? tagAreaHeight : 12) + CARD_PAD
|
||||||
);
|
);
|
||||||
fragments.push({
|
fragments.push({
|
||||||
assignment,
|
assignment,
|
||||||
continuation: index > 0,
|
continuation: index > 0,
|
||||||
lines: fragmentLines,
|
lines: fragmentLines,
|
||||||
showTags,
|
showTags,
|
||||||
height
|
height,
|
||||||
|
contentLineHeight
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -56493,40 +56509,67 @@ function drawPageBackground(page, font, canRenderCjk, colors) {
|
|||||||
color: colors.watermark
|
color: colors.watermark
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function drawTags(page, tags, x, y, font, colors) {
|
function resolveFont(fontWeight, fontNormal, fontLight, fontBold) {
|
||||||
|
const weight = String(fontWeight || "regular").toLowerCase();
|
||||||
|
if (weight === "light") return fontLight;
|
||||||
|
if (weight === "bold") return fontBold;
|
||||||
|
return fontNormal;
|
||||||
|
}
|
||||||
|
function drawTextWithWeight(page, text, options) {
|
||||||
|
const { font, fontLight, fontBold, fontWeight, opacity, ...rest } = options;
|
||||||
|
const fontToUse = resolveFont(fontWeight, font, fontLight, fontBold);
|
||||||
|
page.drawText(text, { ...rest, font: fontToUse, opacity: opacity ?? 1 });
|
||||||
|
}
|
||||||
|
function drawTags(page, tags, x, y, font, fontLight, fontBold, fontSize, fontWeight, colors) {
|
||||||
if (tags.length === 0) {
|
if (tags.length === 0) {
|
||||||
page.drawText("", { x, y, font, size: TAG_FONT });
|
page.drawText("", { x, y, font, size: fontSize });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let cursor = x;
|
let cursor = x;
|
||||||
const maxX = x + CARD_WIDTH - CARD_PAD * 2;
|
const maxX = x + CARD_WIDTH - CARD_PAD * 2;
|
||||||
|
const pillHeight = Math.max(19, fontSize + 8);
|
||||||
for (const tag2 of tags) {
|
for (const tag2 of tags) {
|
||||||
const label = ellipsize(tag2, font, TAG_FONT, CARD_WIDTH - CARD_PAD * 2 - 16);
|
const label = ellipsize(tag2, font, fontSize, 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, fontSize) + 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, pillHeight, colors.chip);
|
||||||
page.drawText(label, { x: cursor + 8, y: y + 5.2, size: TAG_FONT, font, color: colors.chipText });
|
drawTextWithWeight(page, label, {
|
||||||
|
x: cursor + 8,
|
||||||
|
y: y + (pillHeight - fontSize) / 2 - 1,
|
||||||
|
size: fontSize,
|
||||||
|
font,
|
||||||
|
fontLight,
|
||||||
|
fontBold,
|
||||||
|
fontWeight,
|
||||||
|
color: colors.chipText
|
||||||
|
});
|
||||||
cursor += width + 6;
|
cursor += width + 6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function drawFragment(page, fragment, x, top, font, fontBold, dateDisplay, canRenderCjk, colors) {
|
function drawFragment(page, fragment, x, top, font, fontLight, fontBold, dateDisplay, canRenderCjk, colors) {
|
||||||
const y = top - fragment.height;
|
const y = top - fragment.height;
|
||||||
drawRoundedRect(page, x, y, CARD_WIDTH, fragment.height, CARD_RADIUS, colors.card, {
|
drawRoundedRect(page, x, y, CARD_WIDTH, fragment.height, CARD_RADIUS, colors.card, {
|
||||||
color: colors.border,
|
color: colors.border,
|
||||||
width: 1
|
width: 1
|
||||||
});
|
});
|
||||||
|
const subjectStyle = fragment.assignment.style?.subject || {};
|
||||||
|
const subjectFontSize = typeof subjectStyle.fontSize === "number" ? subjectStyle.fontSize : SUBJECT_FONT;
|
||||||
|
const subjectFontWeight = subjectStyle.fontWeight || "regular";
|
||||||
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}`,
|
||||||
fontBold,
|
font,
|
||||||
SUBJECT_FONT,
|
subjectFontSize,
|
||||||
CARD_WIDTH - CARD_PAD * 2
|
CARD_WIDTH - CARD_PAD * 2
|
||||||
);
|
);
|
||||||
page.drawText(subject, {
|
drawTextWithWeight(page, subject, {
|
||||||
x: x + CARD_PAD,
|
x: x + CARD_PAD,
|
||||||
y: top - CARD_PAD - 20,
|
y: top - CARD_PAD - 20,
|
||||||
size: SUBJECT_FONT,
|
size: subjectFontSize,
|
||||||
font: fontBold,
|
font,
|
||||||
|
fontLight,
|
||||||
|
fontBold,
|
||||||
|
fontWeight: subjectFontWeight,
|
||||||
color: colors.headerText
|
color: colors.headerText
|
||||||
});
|
});
|
||||||
page.drawText(formatDate(fragment.assignment.date, dateDisplay), {
|
page.drawText(formatDate(fragment.assignment.date, dateDisplay), {
|
||||||
@ -56536,31 +56579,44 @@ function drawFragment(page, fragment, x, top, font, fontBold, dateDisplay, canRe
|
|||||||
font,
|
font,
|
||||||
color: colors.dateText
|
color: colors.dateText
|
||||||
});
|
});
|
||||||
|
const contentStyle = fragment.assignment.style?.content || {};
|
||||||
|
const contentFontSize = typeof contentStyle.fontSize === "number" ? contentStyle.fontSize : CONTENT_FONT;
|
||||||
|
const contentFontWeight = contentStyle.fontWeight || "regular";
|
||||||
|
const contentLineHeight = fragment.contentLineHeight || contentFontSize * (CONTENT_LINE_HEIGHT / CONTENT_FONT);
|
||||||
let lineY = top - CARD_PAD - HEADER_HEIGHT - 12;
|
let lineY = top - CARD_PAD - HEADER_HEIGHT - 12;
|
||||||
for (const line of fragment.lines) {
|
for (const line of fragment.lines) {
|
||||||
page.drawText(line || " ", {
|
drawTextWithWeight(page, line || " ", {
|
||||||
x: x + CARD_PAD,
|
x: x + CARD_PAD,
|
||||||
y: lineY,
|
y: lineY,
|
||||||
size: CONTENT_FONT,
|
size: contentFontSize,
|
||||||
font,
|
font,
|
||||||
|
fontLight,
|
||||||
|
fontBold,
|
||||||
|
fontWeight: contentFontWeight,
|
||||||
color: colors.content
|
color: colors.content
|
||||||
});
|
});
|
||||||
lineY -= CONTENT_LINE_HEIGHT;
|
lineY -= contentLineHeight;
|
||||||
}
|
}
|
||||||
if (fragment.showTags) {
|
if (fragment.showTags) {
|
||||||
drawTags(page, fragment.assignment.tags, x + CARD_PAD, y + CARD_PAD + 5, font, colors);
|
const tagStyle = fragment.assignment.style?.tags || {};
|
||||||
|
const tagFontSize = typeof tagStyle.fontSize === "number" ? tagStyle.fontSize : TAG_FONT;
|
||||||
|
const tagFontWeight = tagStyle.fontWeight || "regular";
|
||||||
|
drawTags(page, fragment.assignment.tags, x + CARD_PAD, y + CARD_PAD + 5, font, fontLight, fontBold, tagFontSize, tagFontWeight, 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, {
|
drawTextWithWeight(page, continued, {
|
||||||
x: x + CARD_PAD,
|
x: x + CARD_PAD,
|
||||||
y: y + CARD_PAD + 8,
|
y: y + CARD_PAD + 8,
|
||||||
size: TAG_FONT,
|
size: TAG_FONT,
|
||||||
font,
|
font,
|
||||||
|
fontLight,
|
||||||
|
fontBold,
|
||||||
|
fontWeight: "regular",
|
||||||
color: colors.muted
|
color: colors.muted
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function renderAssignmentDocument(pdfDoc, document, font, fontBold, options = {}) {
|
function renderAssignmentDocument(pdfDoc, document, font, fontLight, 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";
|
||||||
@ -56584,6 +56640,7 @@ function renderAssignmentDocument(pdfDoc, document, font, fontBold, options = {}
|
|||||||
PAGE_MARGIN + column * (CARD_WIDTH + COLUMN_GAP),
|
PAGE_MARGIN + column * (CARD_WIDTH + COLUMN_GAP),
|
||||||
cursorTop,
|
cursorTop,
|
||||||
font,
|
font,
|
||||||
|
fontLight,
|
||||||
fontBold,
|
fontBold,
|
||||||
dateDisplay,
|
dateDisplay,
|
||||||
options.canRenderCjk !== false,
|
options.canRenderCjk !== false,
|
||||||
@ -56600,15 +56657,21 @@ async function createUafPdfWithFont(document, options = {}) {
|
|||||||
const validated = validatePayload(document);
|
const validated = validatePayload(document);
|
||||||
const csvBytes = new TextEncoder().encode(serializePayload(validated));
|
const csvBytes = new TextEncoder().encode(serializePayload(validated));
|
||||||
const pdfDoc = await PDFDocument_default.create();
|
const pdfDoc = await PDFDocument_default.create();
|
||||||
let font;
|
let fontNormal, fontLight, fontBold;
|
||||||
if (options.useStandardFont) {
|
if (options.useStandardFont) {
|
||||||
font = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
fontNormal = fontLight = fontBold = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||||
} else {
|
} else {
|
||||||
if (!options.fontBytes) throw new Error("fontBytes are required for CJK PDF rendering");
|
if (!options.fontBytes) throw new Error("fontBytes are required for CJK PDF rendering");
|
||||||
pdfDoc.registerFontkit(fontkit_es_default);
|
pdfDoc.registerFontkit(fontkit_es_default);
|
||||||
font = await pdfDoc.embedFont(options.fontBytes, { subset: options.subsetFont ?? true });
|
fontNormal = await pdfDoc.embedFont(options.fontBytes, { subset: options.subsetFont ?? true });
|
||||||
|
fontLight = options.fontLightBytes
|
||||||
|
? await pdfDoc.embedFont(options.fontLightBytes, { subset: options.subsetFont ?? true })
|
||||||
|
: fontNormal;
|
||||||
|
fontBold = options.fontBoldBytes
|
||||||
|
? await pdfDoc.embedFont(options.fontBoldBytes, { subset: options.subsetFont ?? true })
|
||||||
|
: fontNormal;
|
||||||
}
|
}
|
||||||
renderAssignmentDocument(pdfDoc, validated, font, font, {
|
renderAssignmentDocument(pdfDoc, validated, fontNormal, fontLight, fontBold, {
|
||||||
dateDisplay: options.useStandardFont ? "iso" : "zh",
|
dateDisplay: options.useStandardFont ? "iso" : "zh",
|
||||||
canRenderCjk: !options.useStandardFont,
|
canRenderCjk: !options.useStandardFont,
|
||||||
theme: options.theme
|
theme: options.theme
|
||||||
@ -56632,6 +56695,9 @@ function collectDocumentText(document) {
|
|||||||
return [...new Set(parts.join(""))].join("");
|
return [...new Set(parts.join(""))].join("");
|
||||||
}
|
}
|
||||||
async function subsetFontInBrowser(fontBytes, text, wasmUrl) {
|
async function subsetFontInBrowser(fontBytes, text, wasmUrl) {
|
||||||
|
if (fontBytes instanceof ArrayBuffer) {
|
||||||
|
fontBytes = new Uint8Array(fontBytes);
|
||||||
|
}
|
||||||
const response = await fetch(wasmUrl);
|
const response = await fetch(wasmUrl);
|
||||||
if (!response.ok) throw new Error(`Failed to load HarfBuzz subset engine: ${response.status}`);
|
if (!response.ok) throw new Error(`Failed to load HarfBuzz subset engine: ${response.status}`);
|
||||||
const result = await WebAssembly.instantiate(await response.arrayBuffer());
|
const result = await WebAssembly.instantiate(await response.arrayBuffer());
|
||||||
@ -56844,13 +56910,24 @@ async function loadBrowserFont(options) {
|
|||||||
return cachedFontBytes;
|
return cachedFontBytes;
|
||||||
}
|
}
|
||||||
async function createUafPdf(document, options = {}) {
|
async function createUafPdf(document, 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 text = collectDocumentText(document);
|
||||||
|
const [fontBytes, fontLightBytes, fontBoldBytes] = await Promise.all([
|
||||||
|
loadBrowserFont(options),
|
||||||
|
options.fontLightBytes ? Promise.resolve(options.fontLightBytes) : Promise.resolve(null),
|
||||||
|
options.fontBoldBytes ? Promise.resolve(options.fontBoldBytes) : Promise.resolve(null),
|
||||||
|
]);
|
||||||
|
const [subsetNormal, subsetLight, subsetBold] = await Promise.all([
|
||||||
|
subsetFontInBrowser(fontBytes, text, wasmUrl),
|
||||||
|
fontLightBytes ? subsetFontInBrowser(fontLightBytes, text, wasmUrl) : Promise.resolve(null),
|
||||||
|
fontBoldBytes ? subsetFontInBrowser(fontBoldBytes, text, wasmUrl) : Promise.resolve(null),
|
||||||
|
]);
|
||||||
return createUafPdfWithFont(document, {
|
return createUafPdfWithFont(document, {
|
||||||
fontBytes: subset,
|
fontBytes: subsetNormal,
|
||||||
|
fontLightBytes: subsetLight,
|
||||||
|
fontBoldBytes: subsetBold,
|
||||||
subsetFont: false,
|
subsetFont: false,
|
||||||
theme: options.theme
|
theme: options.theme,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function createUafPdfFromCsv(csv, options = {}) {
|
async function createUafPdfFromCsv(csv, options = {}) {
|
||||||
|
|||||||
@ -38,6 +38,7 @@ export default defineConfig({
|
|||||||
|
|
||||||
|
|
||||||
workbox: {
|
workbox: {
|
||||||
|
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
|
||||||
globPatterns: [
|
globPatterns: [
|
||||||
'**/*.{js,css,html,ico,png,svg,webmanifest,txt,json,woff2,ttf,mp3}',
|
'**/*.{js,css,html,ico,png,svg,webmanifest,txt,json,woff2,ttf,mp3}',
|
||||||
],
|
],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user