remove import teaching design feature
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,6 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
upload: []
|
|
||||||
print: []
|
print: []
|
||||||
export: []
|
export: []
|
||||||
clear: []
|
clear: []
|
||||||
@@ -29,7 +28,6 @@ const saveStatusLabel: Record<SaveStatus, string> = {
|
|||||||
<template>
|
<template>
|
||||||
<header class="workspace-toolbar">
|
<header class="workspace-toolbar">
|
||||||
<button type="button" data-testid="back" @click="$emit('back')">返回列表</button>
|
<button type="button" data-testid="back" @click="$emit('back')">返回列表</button>
|
||||||
<button type="button" data-testid="upload" @click="$emit('upload')">导入教案</button>
|
|
||||||
<button type="button" data-testid="generate" @click="$emit('generate')">生成一篇</button>
|
<button type="button" data-testid="generate" @click="$emit('generate')">生成一篇</button>
|
||||||
<button type="button" data-testid="batch-generate" @click="$emit('batchGenerate')">批量生成</button>
|
<button type="button" data-testid="batch-generate" @click="$emit('batchGenerate')">批量生成</button>
|
||||||
<button type="button" data-testid="print" :disabled="lessonCount === 0" @click="$emit('print')">打印整册</button>
|
<button type="button" data-testid="print" :disabled="lessonCount === 0" @click="$emit('print')">打印整册</button>
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { type DuplicateStrategy, useTeachingBook } from '../composables/useTeachingBook'
|
import { useTeachingBook } from '../composables/useTeachingBook'
|
||||||
import type { TeachingDesign } from '../../shared/domain/teachingDesign'
|
import type { TeachingDesign } from '../../shared/domain/teachingDesign'
|
||||||
import { createBookZip, downloadBlob } from '../services/zipExporter'
|
import { createBookZip, downloadBlob } from '../services/zipExporter'
|
||||||
import A4Workspace from './A4Workspace.vue'
|
import A4Workspace from './A4Workspace.vue'
|
||||||
import BatchGenerateDialog from './BatchGenerateDialog.vue'
|
import BatchGenerateDialog from './BatchGenerateDialog.vue'
|
||||||
import FixBrokenDialog from './FixBrokenDialog.vue'
|
import FixBrokenDialog from './FixBrokenDialog.vue'
|
||||||
import GenerateLessonDialog from './GenerateLessonDialog.vue'
|
import GenerateLessonDialog from './GenerateLessonDialog.vue'
|
||||||
import ImportConflictDialog from './ImportConflictDialog.vue'
|
|
||||||
import LessonSidebar from './LessonSidebar.vue'
|
import LessonSidebar from './LessonSidebar.vue'
|
||||||
import PrintBook from './PrintBook.vue'
|
import PrintBook from './PrintBook.vue'
|
||||||
import UploadDropzone from './UploadDropzone.vue'
|
|
||||||
import WorkspaceToolbar from './WorkspaceToolbar.vue'
|
import WorkspaceToolbar from './WorkspaceToolbar.vue'
|
||||||
|
|
||||||
const BATCH_GENERATE_CONCURRENCY = 3
|
const BATCH_GENERATE_CONCURRENCY = 3
|
||||||
@@ -30,8 +28,6 @@ const {
|
|||||||
selectedDesign,
|
selectedDesign,
|
||||||
hasDesigns,
|
hasDesigns,
|
||||||
warningCount,
|
warningCount,
|
||||||
importFiles,
|
|
||||||
detectDuplicates,
|
|
||||||
selectPage,
|
selectPage,
|
||||||
moveDesign,
|
moveDesign,
|
||||||
removeDesign,
|
removeDesign,
|
||||||
@@ -42,10 +38,7 @@ const {
|
|||||||
regenerateLesson,
|
regenerateLesson,
|
||||||
} = useTeachingBook(props.bookId)
|
} = useTeachingBook(props.bookId)
|
||||||
|
|
||||||
const pendingFiles = ref<File[]>([])
|
|
||||||
const duplicateNames = ref<string[]>([])
|
|
||||||
const errorMessage = ref<string | null>(null)
|
const errorMessage = ref<string | null>(null)
|
||||||
const uploadRef = ref<InstanceType<typeof UploadDropzone> | null>(null)
|
|
||||||
|
|
||||||
const showGenerateDialog = ref(false)
|
const showGenerateDialog = ref(false)
|
||||||
const generateLoading = ref(false)
|
const generateLoading = ref(false)
|
||||||
@@ -67,37 +60,6 @@ const fixCurrentTopic = ref('')
|
|||||||
const fixError = ref<string | null>(null)
|
const fixError = ref<string | null>(null)
|
||||||
const fixCancelled = ref(false)
|
const fixCancelled = ref(false)
|
||||||
|
|
||||||
async function runImport(files: File[], strategy: DuplicateStrategy): Promise<void> {
|
|
||||||
const result = await importFiles(files, strategy)
|
|
||||||
if (result.failed.length > 0) {
|
|
||||||
errorMessage.value = `${result.failed.length} 个文件导入失败:${result.failed
|
|
||||||
.map((entry) => `${entry.filename}(${entry.message})`)
|
|
||||||
.join('、')}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleFiles(files: File[]): Promise<void> {
|
|
||||||
const duplicates = detectDuplicates(files)
|
|
||||||
if (duplicates.length > 0) {
|
|
||||||
pendingFiles.value = files
|
|
||||||
duplicateNames.value = duplicates
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await runImport(files, 'keep')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveConflict(strategy: DuplicateStrategy | 'cancel'): Promise<void> {
|
|
||||||
const files = pendingFiles.value
|
|
||||||
pendingFiles.value = []
|
|
||||||
duplicateNames.value = []
|
|
||||||
if (strategy === 'cancel') return
|
|
||||||
await runImport(files, strategy)
|
|
||||||
}
|
|
||||||
|
|
||||||
function triggerUpload(): void {
|
|
||||||
uploadRef.value?.openPicker()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePrint(): void {
|
function handlePrint(): void {
|
||||||
const prev = document.title
|
const prev = document.title
|
||||||
document.title = bookName.value || prev
|
document.title = bookName.value || prev
|
||||||
@@ -239,13 +201,6 @@ function closeFixDialog(): void {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<ImportConflictDialog
|
|
||||||
v-if="duplicateNames.length > 0"
|
|
||||||
:duplicates="duplicateNames"
|
|
||||||
@replace="resolveConflict('replace')"
|
|
||||||
@keep="resolveConflict('keep')"
|
|
||||||
@cancel="resolveConflict('cancel')"
|
|
||||||
/>
|
|
||||||
<GenerateLessonDialog
|
<GenerateLessonDialog
|
||||||
v-if="showGenerateDialog"
|
v-if="showGenerateDialog"
|
||||||
:loading="generateLoading"
|
:loading="generateLoading"
|
||||||
@@ -290,7 +245,6 @@ function closeFixDialog(): void {
|
|||||||
:warning-count="warningCount"
|
:warning-count="warningCount"
|
||||||
:save-status="saveStatus"
|
:save-status="saveStatus"
|
||||||
@back="$emit('back')"
|
@back="$emit('back')"
|
||||||
@upload="triggerUpload"
|
|
||||||
@generate="openGenerateDialog"
|
@generate="openGenerateDialog"
|
||||||
@batch-generate="showBatchDialog = true"
|
@batch-generate="showBatchDialog = true"
|
||||||
@fix-broken="openFixDialog"
|
@fix-broken="openFixDialog"
|
||||||
@@ -299,24 +253,19 @@ function closeFixDialog(): void {
|
|||||||
@clear="handleClear"
|
@clear="handleClear"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UploadDropzone v-if="!hasDesigns" @files="handleFiles" />
|
<div v-if="hasDesigns" class="workspace-layout">
|
||||||
|
<LessonSidebar
|
||||||
<template v-else>
|
:designs="book.designs"
|
||||||
<div class="workspace-layout">
|
:selected-id="book.selectedId"
|
||||||
<LessonSidebar
|
@select="selectPage"
|
||||||
:designs="book.designs"
|
@remove="removeDesign"
|
||||||
:selected-id="book.selectedId"
|
@move="moveDesign"
|
||||||
@select="selectPage"
|
/>
|
||||||
@remove="removeDesign"
|
<A4Workspace
|
||||||
@move="moveDesign"
|
:selected-design="selectedDesign"
|
||||||
/>
|
@update:design="handleDesignUpdate"
|
||||||
<A4Workspace
|
/>
|
||||||
:selected-design="selectedDesign"
|
</div>
|
||||||
@update:design="handleDesignUpdate"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<UploadDropzone ref="uploadRef" compact class="visually-hidden" @files="handleFiles" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<PrintBook :designs="book.designs" />
|
<PrintBook :designs="book.designs" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,12 +7,9 @@ import {
|
|||||||
} from '../../shared/domain/teachingDesign'
|
} from '../../shared/domain/teachingDesign'
|
||||||
import * as booksApi from '../services/booksApi'
|
import * as booksApi from '../services/booksApi'
|
||||||
import { parseTeachingDesign } from '../services/markdownParser'
|
import { parseTeachingDesign } from '../services/markdownParser'
|
||||||
import { sortFilesNaturally } from '../services/naturalSort'
|
|
||||||
|
|
||||||
const AUTOSAVE_DELAY_MS = 300
|
const AUTOSAVE_DELAY_MS = 300
|
||||||
|
|
||||||
export type DuplicateStrategy = 'replace' | 'keep'
|
|
||||||
|
|
||||||
export type SaveStatus = 'idle' | 'saving' | 'saved' | 'error'
|
export type SaveStatus = 'idle' | 'saving' | 'saved' | 'error'
|
||||||
|
|
||||||
export type LoadStatus = 'loading' | 'loaded' | 'error'
|
export type LoadStatus = 'loading' | 'loaded' | 'error'
|
||||||
@@ -30,12 +27,6 @@ export interface BatchGenerateLessonOptions {
|
|||||||
onLessonComplete?: (count: number) => void
|
onLessonComplete?: (count: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImportResult {
|
|
||||||
imported: number
|
|
||||||
failed: Array<{ filename: string; message: string }>
|
|
||||||
duplicates: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TeachingBookStore {
|
export interface TeachingBookStore {
|
||||||
book: Ref<TeachingBook>
|
book: Ref<TeachingBook>
|
||||||
bookName: Ref<string>
|
bookName: Ref<string>
|
||||||
@@ -46,8 +37,6 @@ export interface TeachingBookStore {
|
|||||||
selectedDesign: Ref<TeachingDesign | null>
|
selectedDesign: Ref<TeachingDesign | null>
|
||||||
hasDesigns: Ref<boolean>
|
hasDesigns: Ref<boolean>
|
||||||
warningCount: Ref<number>
|
warningCount: Ref<number>
|
||||||
importFiles: (files: readonly File[], strategy: DuplicateStrategy) => Promise<ImportResult>
|
|
||||||
detectDuplicates: (files: readonly File[]) => string[]
|
|
||||||
selectPage: (id: DesignId) => void
|
selectPage: (id: DesignId) => void
|
||||||
moveDesign: (from: number, to: number) => void
|
moveDesign: (from: number, to: number) => void
|
||||||
removeDesign: (id: DesignId) => void
|
removeDesign: (id: DesignId) => void
|
||||||
@@ -142,64 +131,6 @@ export function useTeachingBook(bookId: string): TeachingBookStore {
|
|||||||
|
|
||||||
void load()
|
void load()
|
||||||
|
|
||||||
function detectDuplicates(files: readonly File[]): string[] {
|
|
||||||
const existingNames = new Set(book.value.designs.map((design) => design.originalFilename))
|
|
||||||
return files.map((file) => file.name).filter((name) => existingNames.has(name))
|
|
||||||
}
|
|
||||||
|
|
||||||
async function importFiles(
|
|
||||||
files: readonly File[],
|
|
||||||
strategy: DuplicateStrategy,
|
|
||||||
): Promise<ImportResult> {
|
|
||||||
const markdownFiles = files.filter((file) => /\.md$/i.test(file.name))
|
|
||||||
const failed: ImportResult['failed'] = files
|
|
||||||
.filter((file) => !/\.md$/i.test(file.name))
|
|
||||||
.map((file) => ({ filename: file.name, message: '仅支持 .md 文件。' }))
|
|
||||||
|
|
||||||
const sortedFiles = sortFilesNaturally([...markdownFiles])
|
|
||||||
const duplicates: string[] = []
|
|
||||||
let imported = 0
|
|
||||||
|
|
||||||
for (const file of sortedFiles) {
|
|
||||||
try {
|
|
||||||
const text = await file.text()
|
|
||||||
const design = parseTeachingDesign(file.name, text)
|
|
||||||
|
|
||||||
const existingIndex = book.value.designs.findIndex(
|
|
||||||
(existing) => existing.originalFilename === file.name,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (existingIndex !== -1) {
|
|
||||||
duplicates.push(file.name)
|
|
||||||
if (strategy === 'replace') {
|
|
||||||
book.value.designs.splice(existingIndex, 1, design)
|
|
||||||
} else {
|
|
||||||
book.value.designs.push(design)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
book.value.designs.push(design)
|
|
||||||
}
|
|
||||||
|
|
||||||
imported++
|
|
||||||
} catch (error) {
|
|
||||||
failed.push({
|
|
||||||
filename: file.name,
|
|
||||||
message: error instanceof Error ? error.message : '解析失败。',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imported > 0 && book.value.selectedId === null && book.value.designs.length > 0) {
|
|
||||||
book.value.selectedId = book.value.designs[0]!.id
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imported > 0) {
|
|
||||||
touch()
|
|
||||||
}
|
|
||||||
|
|
||||||
return { imported, failed, duplicates }
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectPage(id: DesignId): void {
|
function selectPage(id: DesignId): void {
|
||||||
book.value.selectedId = id
|
book.value.selectedId = id
|
||||||
}
|
}
|
||||||
@@ -359,8 +290,6 @@ export function useTeachingBook(bookId: string): TeachingBookStore {
|
|||||||
selectedDesign,
|
selectedDesign,
|
||||||
hasDesigns,
|
hasDesigns,
|
||||||
warningCount,
|
warningCount,
|
||||||
importFiles,
|
|
||||||
detectDuplicates,
|
|
||||||
selectPage,
|
selectPage,
|
||||||
moveDesign,
|
moveDesign,
|
||||||
removeDesign,
|
removeDesign,
|
||||||
|
|||||||
Reference in New Issue
Block a user