feat: 从书籍生成教案(粘贴目录 + 上传 md 压缩包)
新增「从书籍生成」流程:用户粘贴教材目录(多行)并上传内含各章
.md 的 ZIP(文件名=目录标题),前端用 JSZip 解压匹配,AI 按章节
体量划分课时,预览/编辑后批量生成,每篇以对应章节正文作为上下文。
服务端
- generate.ts: 新增 POST /lessons-from-book(目录条目→AI 划分课时
JSON,剥围栏/校验/丢非法行);扩展 POST / 支持可选 content 上下文,
无 content 时请求体不变(向后兼容)。
前端
- bookImport.ts: normalizeTitle / parseZip / matchToc / assembleContent。
- booksApi.ts: generateLesson 兼容签名 + divideLessonsFromBook。
- useTeachingBook.ts: 抽出 runGenerationPool,新增 generateLessonsFromBook。
- BookImportDialog.vue 多阶段对话框;UploadDropzone 加 accept/multiple;
菜单串接「从书籍生成」。
顺带修复 5 个遗留失败测试:import 功能已于 fb0b8d1 删除但测试漏删,
清理孤儿测试;批量生成对齐为单参 generateLesson(topic)。
docs/book-import-spec.md 记录规格。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useTeachingBook } from '../composables/useTeachingBook'
|
||||
import { useTeachingBook, type BookLessonInput } from '../composables/useTeachingBook'
|
||||
import type { TeachingDesign } from '../../shared/domain/teachingDesign'
|
||||
import { createBookZip, downloadBlob } from '../services/zipExporter'
|
||||
import A4Workspace from './A4Workspace.vue'
|
||||
import BatchGenerateDialog from './BatchGenerateDialog.vue'
|
||||
import BookImportDialog from './BookImportDialog.vue'
|
||||
import FixBrokenDialog from './FixBrokenDialog.vue'
|
||||
import GenerateLessonDialog from './GenerateLessonDialog.vue'
|
||||
import LessonSidebar from './LessonSidebar.vue'
|
||||
@@ -35,6 +36,7 @@ const {
|
||||
clearBook,
|
||||
generateLesson,
|
||||
generateLessons,
|
||||
generateLessonsFromBook,
|
||||
regenerateLesson,
|
||||
} = useTeachingBook(props.bookId)
|
||||
|
||||
@@ -52,6 +54,14 @@ const batchCurrentTopic = ref('')
|
||||
const batchError = ref<string | null>(null)
|
||||
const batchCancelled = ref(false)
|
||||
|
||||
const showBookImportDialog = ref(false)
|
||||
const bookImportRunning = ref(false)
|
||||
const bookImportDone = ref(0)
|
||||
const bookImportTotal = ref(0)
|
||||
const bookImportCurrentTopic = ref('')
|
||||
const bookImportError = ref<string | null>(null)
|
||||
const bookImportCancelled = ref(false)
|
||||
|
||||
const showFixDialog = ref(false)
|
||||
const fixRunning = ref(false)
|
||||
const fixDone = ref(0)
|
||||
@@ -153,6 +163,42 @@ function closeBatchDialog(): void {
|
||||
batchError.value = null
|
||||
}
|
||||
|
||||
async function handleBookImportStart(lessons: BookLessonInput[]): Promise<void> {
|
||||
bookImportRunning.value = true
|
||||
bookImportCancelled.value = false
|
||||
bookImportDone.value = 0
|
||||
bookImportTotal.value = lessons.length
|
||||
bookImportError.value = null
|
||||
|
||||
const result = await generateLessonsFromBook(lessons, {
|
||||
concurrency: BATCH_GENERATE_CONCURRENCY,
|
||||
isCancelled: () => bookImportCancelled.value,
|
||||
onTopicStart: (topic) => {
|
||||
bookImportCurrentTopic.value = topic
|
||||
},
|
||||
onLessonComplete: (count) => {
|
||||
bookImportDone.value += count
|
||||
},
|
||||
})
|
||||
|
||||
if (!result.ok) {
|
||||
bookImportError.value = result.message
|
||||
}
|
||||
|
||||
bookImportRunning.value = false
|
||||
}
|
||||
|
||||
function handleBookImportCancel(): void {
|
||||
bookImportCancelled.value = true
|
||||
}
|
||||
|
||||
function closeBookImportDialog(): void {
|
||||
showBookImportDialog.value = false
|
||||
bookImportDone.value = 0
|
||||
bookImportTotal.value = 0
|
||||
bookImportError.value = null
|
||||
}
|
||||
|
||||
function openFixDialog(): void {
|
||||
fixTotal.value = book.value.designs.filter((d) => d.warnings.length > 0).length
|
||||
fixDone.value = 0
|
||||
@@ -220,6 +266,17 @@ function closeFixDialog(): void {
|
||||
@cancel="handleBatchCancel"
|
||||
@close="closeBatchDialog"
|
||||
/>
|
||||
<BookImportDialog
|
||||
v-if="showBookImportDialog"
|
||||
:running="bookImportRunning"
|
||||
:done="bookImportDone"
|
||||
:total="bookImportTotal"
|
||||
:current-topic="bookImportCurrentTopic"
|
||||
:error="bookImportError"
|
||||
@start="handleBookImportStart"
|
||||
@cancel="handleBookImportCancel"
|
||||
@close="closeBookImportDialog"
|
||||
/>
|
||||
<FixBrokenDialog
|
||||
v-if="showFixDialog"
|
||||
:running="fixRunning"
|
||||
@@ -247,6 +304,7 @@ function closeFixDialog(): void {
|
||||
@back="$emit('back')"
|
||||
@generate="openGenerateDialog"
|
||||
@batch-generate="showBatchDialog = true"
|
||||
@book-import="showBookImportDialog = true"
|
||||
@fix-broken="openFixDialog"
|
||||
@print="handlePrint"
|
||||
@export="handleExport"
|
||||
|
||||
Reference in New Issue
Block a user