mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2026-08-12 09:20:31 +00:00
Compare commits
No commits in common. "cf9352fc1f8ca4b3a410d77618111e6eac864e81" and "5bfb5c9abb089ecb1856e7041e240540aa40e1c8" have entirely different histories.
cf9352fc1f
...
5bfb5c9abb
@ -1,7 +1,3 @@
|
||||
allowBuilds:
|
||||
'@parcel/watcher': false
|
||||
esbuild: false
|
||||
sharp: false
|
||||
onlyBuiltDependencies:
|
||||
- '@parcel/watcher'
|
||||
- esbuild
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/uaf/NotoSansSC-Regular.otf
Normal file
BIN
public/uaf/NotoSansSC-Regular.otf
Normal file
Binary file not shown.
@ -33,9 +33,7 @@
|
||||
|
||||
size="large"
|
||||
rounded="xl"
|
||||
>
|
||||
<v-icon icon="mdi-swap-vertical-bold" />
|
||||
</v-btn>
|
||||
><v-icon icon="mdi-swap-vertical-bold"></v-icon></v-btn>
|
||||
</template>
|
||||
<v-list density="comfortable">
|
||||
<v-list-item
|
||||
@ -55,7 +53,7 @@
|
||||
:prepend-icon="
|
||||
isFullscreen ? 'mdi-fullscreen-exit' : 'mdi-fullscreen'
|
||||
"
|
||||
rounded="xl"
|
||||
rounded="xl"
|
||||
class="ml-2"
|
||||
size="large"
|
||||
@click="$emit('toggle-fullscreen')"
|
||||
@ -74,16 +72,16 @@
|
||||
随机点名
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
<v-btn
|
||||
v-if="showExamScheduleButton"
|
||||
prepend-icon="mdi-calendar-check"
|
||||
size="large"
|
||||
class="ml-2"
|
||||
color="green"
|
||||
@click="$emit('add-exam-card')"
|
||||
>
|
||||
考试看板
|
||||
</v-btn>
|
||||
prepend-icon="mdi-calendar-check"
|
||||
size="large"
|
||||
@click="$emit('add-exam-card')"
|
||||
class="ml-2"
|
||||
color="green"
|
||||
>
|
||||
考试看板
|
||||
</v-btn>
|
||||
|
||||
|
||||
<v-btn
|
||||
@ -107,6 +105,7 @@
|
||||
>
|
||||
添加测试卡片
|
||||
</v-btn>
|
||||
|
||||
</div>
|
||||
|
||||
<v-card
|
||||
|
||||
@ -24,210 +24,99 @@
|
||||
|
||||
<v-card-text class="transfer-content">
|
||||
<template v-if="mode === 'export'">
|
||||
<v-tabs
|
||||
v-if="mobile"
|
||||
v-model="mobileTab"
|
||||
fixed-tabs
|
||||
<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"
|
||||
>
|
||||
<v-tab value="edit">
|
||||
编辑
|
||||
</v-tab>
|
||||
<v-tab value="preview">
|
||||
预览
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
{{ exportError }}
|
||||
</v-alert>
|
||||
|
||||
<div
|
||||
class="export-layout"
|
||||
:class="{ mobile: mobile, 'show-preview': mobile && mobileTab === 'preview' }"
|
||||
v-if="previewRows.length"
|
||||
class="d-flex align-center mb-2"
|
||||
>
|
||||
<div class="editor-section">
|
||||
<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-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-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-list
|
||||
v-if="previewRows.length"
|
||||
border
|
||||
lines="three"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="row in previewRows"
|
||||
:key="row.id"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-checkbox-btn
|
||||
:model-value="allValidSelected"
|
||||
:indeterminate="someValidSelected && !allValidSelected"
|
||||
@click="toggleAll"
|
||||
v-model="row.selected"
|
||||
:disabled="row.issues.length > 0"
|
||||
/>
|
||||
<span class="text-body-2">选择全部有效作业</span>
|
||||
<v-spacer />
|
||||
<span class="text-caption text-medium-emphasis">
|
||||
已选择 {{ selectedAssignments.length }} / {{ previewRows.length }} 项
|
||||
</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-list
|
||||
v-if="previewRows.length"
|
||||
border
|
||||
lines="three"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="row in previewRows"
|
||||
:key="row.id"
|
||||
class="edit-list-item"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-checkbox-btn
|
||||
v-model="row.selected"
|
||||
:disabled="row.issues.length > 0"
|
||||
/>
|
||||
</template>
|
||||
<div class="d-flex flex-column w-100">
|
||||
<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>
|
||||
</div>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<v-empty-state
|
||||
v-else-if="!loadingPreview && !exportError"
|
||||
icon="mdi-book-open-blank-variant-outline"
|
||||
text="该日期没有可导出的作业"
|
||||
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>
|
||||
<v-empty-state
|
||||
v-else-if="!loadingPreview && !exportError"
|
||||
icon="mdi-book-open-blank-variant-outline"
|
||||
text="该日期没有可导出的作业"
|
||||
title="暂无作业"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
@ -358,12 +247,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onUnmounted, ref, watch } from "vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useDisplay } from "vuetify";
|
||||
import dataProvider from "@/utils/dataProvider";
|
||||
import { debounce } from "@/utils/debounce";
|
||||
import {
|
||||
buildUafPdfBytes,
|
||||
createExportPreview,
|
||||
createImportPlan,
|
||||
downloadUafAssignments,
|
||||
@ -400,22 +287,6 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "success", "error", "imported"]);
|
||||
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({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit("update:modelValue", value),
|
||||
@ -430,21 +301,11 @@ const importPlan = ref(null);
|
||||
const importError = ref("");
|
||||
const loadingImport = 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 selectedAssignments = computed(() =>
|
||||
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 allValidSelected = computed(
|
||||
() => validRows.value.length > 0 && validRows.value.every((row) => row.selected),
|
||||
@ -474,11 +335,7 @@ const footerText = computed(() => {
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(open, oldOpen) => {
|
||||
if (!open && oldOpen) {
|
||||
releasePreviewUrl();
|
||||
mobileTab.value = "edit";
|
||||
}
|
||||
(open) => {
|
||||
if (!open) return;
|
||||
if (props.mode === "export") {
|
||||
exportDate.value = displayDate(props.currentDate);
|
||||
@ -491,20 +348,6 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
selectedAssignments,
|
||||
() => {
|
||||
generatePreview();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(globalStyle, () => generatePreview(), { deep: true });
|
||||
|
||||
onUnmounted(() => {
|
||||
releasePreviewUrl();
|
||||
});
|
||||
|
||||
function cloneBoard(board) {
|
||||
return JSON.parse(JSON.stringify(board || { homework: {}, attendance: {} }));
|
||||
}
|
||||
@ -544,47 +387,10 @@ function toggleAll() {
|
||||
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() {
|
||||
exporting.value = true;
|
||||
try {
|
||||
const filename = await downloadUafAssignments(styledSelectedAssignments.value, exportDate.value);
|
||||
const filename = await downloadUafAssignments(selectedAssignments.value, exportDate.value);
|
||||
emit("success", "导出成功", filename);
|
||||
dialog.value = false;
|
||||
} catch (error) {
|
||||
@ -675,97 +481,6 @@ function displayDate(date) {
|
||||
-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 {
|
||||
min-width: 132px;
|
||||
width: 132px;
|
||||
|
||||
@ -490,16 +490,7 @@
|
||||
考试看板
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<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-list-item active color="green" @click="$router.push('/examschedule')" append-icon="mdi-arrow-right">打开考试看板</v-list-item></v-list>
|
||||
<v-list v-if="examStore.examList.length > 0">
|
||||
<v-list-item
|
||||
v-for="exam in examStore.examList"
|
||||
|
||||
@ -71,7 +71,6 @@ export function createUafDocument(items, dateValue) {
|
||||
date,
|
||||
content: item.content,
|
||||
tags: normalizeTags(item.tags),
|
||||
style: item.style ? JSON.parse(JSON.stringify(item.style)) : JSON.parse(JSON.stringify(DEFAULT_ASSIGNMENT_STYLE)),
|
||||
}));
|
||||
|
||||
if (assignments.length === 0) {
|
||||
@ -85,12 +84,6 @@ export function createUafDocument(items, dateValue) {
|
||||
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) {
|
||||
const date = normalizeUafDate(dateValue);
|
||||
return items
|
||||
@ -102,10 +95,9 @@ export function createExportPreview(items, dateValue) {
|
||||
date,
|
||||
content: item.content,
|
||||
tags: normalizeTags(item.tags),
|
||||
style: JSON.parse(JSON.stringify(DEFAULT_ASSIGNMENT_STYLE)),
|
||||
};
|
||||
const issues = validateUafAssignment(assignment, assignment.subject || `第 ${index + 1} 张卡片`);
|
||||
return { id: `${item.key || index}-${index}`, assignment, selected: issues.length === 0, issues, expanded: false };
|
||||
return { id: `${item.key || index}-${index}`, assignment, selected: issues.length === 0, issues };
|
||||
});
|
||||
}
|
||||
|
||||
@ -268,35 +260,19 @@ export async function executeImportPlan(plan, saveBoardData) {
|
||||
return { imported, skipped, savedDates, failedDates };
|
||||
}
|
||||
|
||||
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) {
|
||||
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 [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 fontUrl = new URL(`${base}uaf/NotoSansSC-Regular.otf`, window.location.origin);
|
||||
const wasmUrl = new URL(`${base}uaf/hb-subset.wasm`, window.location.origin);
|
||||
return createUafPdf(assignments, {
|
||||
fontBytes,
|
||||
fontLightBytes,
|
||||
fontBoldBytes,
|
||||
const pdfBytes = await createUafPdf(assignments, {
|
||||
fontUrl,
|
||||
wasmUrl,
|
||||
theme: "classworks-dark",
|
||||
});
|
||||
}
|
||||
|
||||
export async function downloadUafAssignments(assignments, dateValue) {
|
||||
const pdfBytes = await buildUafPdfBytes(assignments);
|
||||
const blob = new window.Blob([pdfBytes], { type: "application/pdf" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
|
||||
151
src/vendor/uaf/browser.js
vendored
151
src/vendor/uaf/browser.js
vendored
@ -8323,22 +8323,13 @@ function isValidIso8601(value) {
|
||||
const parsed = Date.parse(value);
|
||||
return !Number.isNaN(parsed);
|
||||
}
|
||||
var stylePartSchema = external_exports.object({
|
||||
fontSize: external_exports.number().optional(),
|
||||
fontWeight: external_exports.string().optional()
|
||||
});
|
||||
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`),
|
||||
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`),
|
||||
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 ";"')
|
||||
).max(LIMITS.tagCountMax, `at most ${LIMITS.tagCountMax} tags allowed`),
|
||||
style: external_exports.object({
|
||||
subject: stylePartSchema.optional(),
|
||||
content: stylePartSchema.optional(),
|
||||
tags: stylePartSchema.optional()
|
||||
}).optional()
|
||||
).max(LIMITS.tagCountMax, `at most ${LIMITS.tagCountMax} tags allowed`)
|
||||
});
|
||||
var uafDocumentSchema = external_exports.array(uafAssignmentSchema).min(1, "document must contain at least one assignment");
|
||||
function escapeField(value) {
|
||||
@ -25497,7 +25488,7 @@ var INTRINSICS = {
|
||||
"%Error%": Error,
|
||||
"%ErrorPrototype%": Error.prototype,
|
||||
"%eval%": eval,
|
||||
|
||||
// eslint-disable-line no-eval
|
||||
"%EvalError%": EvalError,
|
||||
"%EvalErrorPrototype%": EvalError.prototype,
|
||||
"%Float32Array%": typeof Float32Array === "undefined" ? undefined$1 : Float32Array,
|
||||
@ -48411,7 +48402,7 @@ var decodePDFRawStream = function(_a) {
|
||||
for (var idx = 0, len2 = Filter.size(); idx < len2; idx++) {
|
||||
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);
|
||||
}
|
||||
return stream2;
|
||||
@ -50839,7 +50830,7 @@ var PDFParser = (
|
||||
this.context.header = this.parseHeader();
|
||||
_a.label = 1;
|
||||
case 1:
|
||||
if (this.bytes.done()) return [3, 3];
|
||||
if (!!this.bytes.done()) return [3, 3];
|
||||
return [4, this.parseDocumentSection()];
|
||||
case 2:
|
||||
_a.sent();
|
||||
@ -53028,7 +53019,7 @@ var PDFEmbeddedPage = (
|
||||
return __generator(this, function(_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
if (this.alreadyEmbedded) return [3, 2];
|
||||
if (!!this.alreadyEmbedded) return [3, 2];
|
||||
return [4, this.embedder.embedIntoContext(this.doc.context, this.ref)];
|
||||
case 1:
|
||||
_a.sent();
|
||||
@ -54754,7 +54745,7 @@ var PDFEmbeddedFile = (
|
||||
return __generator(this, function(_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
if (this.alreadyEmbedded) return [3, 2];
|
||||
if (!!this.alreadyEmbedded) return [3, 2];
|
||||
return [4, this.embedder.embedIntoContext(this.doc.context, this.ref)];
|
||||
case 1:
|
||||
ref = _a.sent();
|
||||
@ -54812,7 +54803,7 @@ var PDFJavaScript = (
|
||||
return __generator(this, function(_b) {
|
||||
switch (_b.label) {
|
||||
case 0:
|
||||
if (this.alreadyEmbedded) return [3, 2];
|
||||
if (!!this.alreadyEmbedded) return [3, 2];
|
||||
_a = this.doc, catalog = _a.catalog, context2 = _a.context;
|
||||
return [4, this.embedder.embedIntoContext(this.doc.context, this.ref)];
|
||||
case 1:
|
||||
@ -56464,28 +56455,21 @@ function createFragments(document, font) {
|
||||
const maxWidth = CARD_WIDTH - CARD_PAD * 2;
|
||||
const fragments = [];
|
||||
for (const assignment of document) {
|
||||
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);
|
||||
const lines = wrapText(assignment.content, font, CONTENT_FONT, maxWidth);
|
||||
for (let index = 0; index < lines.length; 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 contentHeight = fragmentLines.length * contentLineHeight;
|
||||
const contentHeight = fragmentLines.length * CONTENT_LINE_HEIGHT;
|
||||
const height = Math.max(
|
||||
MIN_CARD_HEIGHT,
|
||||
CARD_PAD + HEADER_HEIGHT + 12 + contentHeight + (showTags ? tagAreaHeight : 12) + CARD_PAD
|
||||
CARD_PAD + HEADER_HEIGHT + 12 + contentHeight + (showTags ? TAG_AREA_HEIGHT : 12) + CARD_PAD
|
||||
);
|
||||
fragments.push({
|
||||
assignment,
|
||||
continuation: index > 0,
|
||||
lines: fragmentLines,
|
||||
showTags,
|
||||
height,
|
||||
contentLineHeight
|
||||
height
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -56509,67 +56493,40 @@ function drawPageBackground(page, font, canRenderCjk, colors) {
|
||||
color: colors.watermark
|
||||
});
|
||||
}
|
||||
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) {
|
||||
function drawTags(page, tags, x, y, font, colors) {
|
||||
if (tags.length === 0) {
|
||||
page.drawText("", { x, y, font, size: fontSize });
|
||||
page.drawText("", { x, y, font, size: TAG_FONT });
|
||||
return;
|
||||
}
|
||||
let cursor = x;
|
||||
const maxX = x + CARD_WIDTH - CARD_PAD * 2;
|
||||
const pillHeight = Math.max(19, fontSize + 8);
|
||||
for (const tag2 of tags) {
|
||||
const label = ellipsize(tag2, font, fontSize, CARD_WIDTH - CARD_PAD * 2 - 16);
|
||||
const width = Math.min(widthOf(font, label, fontSize) + 16, CARD_WIDTH - CARD_PAD * 2);
|
||||
const label = ellipsize(tag2, font, TAG_FONT, CARD_WIDTH - CARD_PAD * 2 - 16);
|
||||
const width = Math.min(widthOf(font, label, TAG_FONT) + 16, CARD_WIDTH - CARD_PAD * 2);
|
||||
if (cursor + width > maxX) break;
|
||||
drawPill(page, cursor, y, width, pillHeight, colors.chip);
|
||||
drawTextWithWeight(page, label, {
|
||||
x: cursor + 8,
|
||||
y: y + (pillHeight - fontSize) / 2 - 1,
|
||||
size: fontSize,
|
||||
font,
|
||||
fontLight,
|
||||
fontBold,
|
||||
fontWeight,
|
||||
color: colors.chipText
|
||||
});
|
||||
drawPill(page, cursor, y, width, 19, colors.chip);
|
||||
page.drawText(label, { x: cursor + 8, y: y + 5.2, size: TAG_FONT, font, color: colors.chipText });
|
||||
cursor += width + 6;
|
||||
}
|
||||
}
|
||||
function drawFragment(page, fragment, x, top, font, fontLight, fontBold, dateDisplay, canRenderCjk, colors) {
|
||||
function drawFragment(page, fragment, x, top, font, fontBold, dateDisplay, canRenderCjk, colors) {
|
||||
const y = top - fragment.height;
|
||||
drawRoundedRect(page, x, y, CARD_WIDTH, fragment.height, CARD_RADIUS, colors.card, {
|
||||
color: colors.border,
|
||||
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 subject = ellipsize(
|
||||
`${fragment.assignment.subject}${continuation}`,
|
||||
font,
|
||||
subjectFontSize,
|
||||
fontBold,
|
||||
SUBJECT_FONT,
|
||||
CARD_WIDTH - CARD_PAD * 2
|
||||
);
|
||||
drawTextWithWeight(page, subject, {
|
||||
page.drawText(subject, {
|
||||
x: x + CARD_PAD,
|
||||
y: top - CARD_PAD - 20,
|
||||
size: subjectFontSize,
|
||||
font,
|
||||
fontLight,
|
||||
fontBold,
|
||||
fontWeight: subjectFontWeight,
|
||||
size: SUBJECT_FONT,
|
||||
font: fontBold,
|
||||
color: colors.headerText
|
||||
});
|
||||
page.drawText(formatDate(fragment.assignment.date, dateDisplay), {
|
||||
@ -56579,44 +56536,31 @@ function drawFragment(page, fragment, x, top, font, fontLight, fontBold, dateDis
|
||||
font,
|
||||
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;
|
||||
for (const line of fragment.lines) {
|
||||
drawTextWithWeight(page, line || " ", {
|
||||
page.drawText(line || " ", {
|
||||
x: x + CARD_PAD,
|
||||
y: lineY,
|
||||
size: contentFontSize,
|
||||
size: CONTENT_FONT,
|
||||
font,
|
||||
fontLight,
|
||||
fontBold,
|
||||
fontWeight: contentFontWeight,
|
||||
color: colors.content
|
||||
});
|
||||
lineY -= contentLineHeight;
|
||||
lineY -= CONTENT_LINE_HEIGHT;
|
||||
}
|
||||
if (fragment.showTags) {
|
||||
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);
|
||||
drawTags(page, fragment.assignment.tags, x + CARD_PAD, y + CARD_PAD + 5, font, colors);
|
||||
} else {
|
||||
const continued = canRenderCjk ? "\u6B63\u6587\u4E0B\u9875\u7EE7\u7EED" : "Continued on next card";
|
||||
drawTextWithWeight(page, continued, {
|
||||
page.drawText(continued, {
|
||||
x: x + CARD_PAD,
|
||||
y: y + CARD_PAD + 8,
|
||||
size: TAG_FONT,
|
||||
font,
|
||||
fontLight,
|
||||
fontBold,
|
||||
fontWeight: "regular",
|
||||
color: colors.muted
|
||||
});
|
||||
}
|
||||
}
|
||||
function renderAssignmentDocument(pdfDoc, document, font, fontLight, fontBold, options = {}) {
|
||||
function renderAssignmentDocument(pdfDoc, document, font, fontBold, options = {}) {
|
||||
const fragments = createFragments(document, font);
|
||||
const pages = [];
|
||||
const dateDisplay = options.dateDisplay ?? "zh";
|
||||
@ -56640,7 +56584,6 @@ function renderAssignmentDocument(pdfDoc, document, font, fontLight, fontBold, o
|
||||
PAGE_MARGIN + column * (CARD_WIDTH + COLUMN_GAP),
|
||||
cursorTop,
|
||||
font,
|
||||
fontLight,
|
||||
fontBold,
|
||||
dateDisplay,
|
||||
options.canRenderCjk !== false,
|
||||
@ -56657,21 +56600,15 @@ async function createUafPdfWithFont(document, options = {}) {
|
||||
const validated = validatePayload(document);
|
||||
const csvBytes = new TextEncoder().encode(serializePayload(validated));
|
||||
const pdfDoc = await PDFDocument_default.create();
|
||||
let fontNormal, fontLight, fontBold;
|
||||
let font;
|
||||
if (options.useStandardFont) {
|
||||
fontNormal = fontLight = fontBold = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
font = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
} else {
|
||||
if (!options.fontBytes) throw new Error("fontBytes are required for CJK PDF rendering");
|
||||
pdfDoc.registerFontkit(fontkit_es_default);
|
||||
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;
|
||||
font = await pdfDoc.embedFont(options.fontBytes, { subset: options.subsetFont ?? true });
|
||||
}
|
||||
renderAssignmentDocument(pdfDoc, validated, fontNormal, fontLight, fontBold, {
|
||||
renderAssignmentDocument(pdfDoc, validated, font, font, {
|
||||
dateDisplay: options.useStandardFont ? "iso" : "zh",
|
||||
canRenderCjk: !options.useStandardFont,
|
||||
theme: options.theme
|
||||
@ -56695,9 +56632,6 @@ function collectDocumentText(document) {
|
||||
return [...new Set(parts.join(""))].join("");
|
||||
}
|
||||
async function subsetFontInBrowser(fontBytes, text, wasmUrl) {
|
||||
if (fontBytes instanceof ArrayBuffer) {
|
||||
fontBytes = new Uint8Array(fontBytes);
|
||||
}
|
||||
const response = await fetch(wasmUrl);
|
||||
if (!response.ok) throw new Error(`Failed to load HarfBuzz subset engine: ${response.status}`);
|
||||
const result = await WebAssembly.instantiate(await response.arrayBuffer());
|
||||
@ -56910,24 +56844,13 @@ async function loadBrowserFont(options) {
|
||||
return cachedFontBytes;
|
||||
}
|
||||
async function createUafPdf(document, options = {}) {
|
||||
const fontBytes = await loadBrowserFont(options);
|
||||
const wasmUrl = options.wasmUrl ?? new URL("../assets/hb-subset.wasm", import.meta.url);
|
||||
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),
|
||||
]);
|
||||
const subset = await subsetFontInBrowser(fontBytes, collectDocumentText(document), wasmUrl);
|
||||
return createUafPdfWithFont(document, {
|
||||
fontBytes: subsetNormal,
|
||||
fontLightBytes: subsetLight,
|
||||
fontBoldBytes: subsetBold,
|
||||
fontBytes: subset,
|
||||
subsetFont: false,
|
||||
theme: options.theme,
|
||||
theme: options.theme
|
||||
});
|
||||
}
|
||||
async function createUafPdfFromCsv(csv, options = {}) {
|
||||
|
||||
@ -38,7 +38,6 @@ export default defineConfig({
|
||||
|
||||
|
||||
workbox: {
|
||||
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
|
||||
globPatterns: [
|
||||
'**/*.{js,css,html,ico,png,svg,webmanifest,txt,json,woff2,ttf,mp3}',
|
||||
],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user