This commit is contained in:
2026-06-16 07:51:38 -06:00
parent 8b0e4df43d
commit dcec78d4b7
7 changed files with 87 additions and 10 deletions

View File

@@ -98,7 +98,7 @@ function handleClose(): void {
<!-- 第二步确认/编辑大纲 -->
<template v-else-if="phase === 'outline'">
<p>AI 已生成以下大纲可直接编辑后开始生成</p>
<textarea v-model="outlineText" class="batch-topics-input" rows="12" />
<textarea v-model="outlineText" class="batch-topics-input" rows="24" />
<p class="batch-topics-count"> {{ parsedTopics.length }} 个课题</p>
<div class="dialog-actions">
<button type="button" :disabled="parsedTopics.length === 0" @click="handleStart">开始生成</button>

View File

@@ -4,6 +4,16 @@ import { createEmptyTeachingDesign, type TeachingDesign } from '../domain/teachi
import TeachingDesignPage from './TeachingDesignPage.vue'
describe('TeachingDesignPage', () => {
it('does not show an empty additional content section while editing', () => {
const design = createEmptyTeachingDesign('1.md')
const wrapper = mount(TeachingDesignPage, {
props: { design, editable: true },
})
expect(wrapper.text()).not.toContain('附加内容')
})
it('adds and removes teaching process rows', async () => {
const design = createEmptyTeachingDesign('1.md')
const wrapper = mount(TeachingDesignPage, {

View File

@@ -269,7 +269,7 @@ function removeStep(index: number): void {
</tbody>
</table>
<template v-if="design.additionalContent || editable">
<template v-if="design.additionalContent.trim()">
<h2 class="section-heading">附加内容</h2>
<EditableMarkdown
:model-value="design.additionalContent"

View File

@@ -30,10 +30,10 @@ const saveStatusLabel: Record<SaveStatus, string> = {
<header class="workspace-toolbar">
<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="print" :disabled="lessonCount === 0" @click="$emit('print')">打印整册</button>
<button type="button" data-testid="export" :disabled="lessonCount === 0" @click="$emit('export')">导出 Markdown</button>
<button type="button" data-testid="export" :disabled="lessonCount === 0" @click="$emit('export')">导出 MD</button>
<button type="button" data-testid="clear" :disabled="lessonCount === 0" @click="$emit('clear')">清空</button>
<span class="workspace-toolbar-count"> {{ lessonCount }} </span>

View File

@@ -18,6 +18,42 @@ function createBookWithDesign(filename = '1.md'): { data: TeachingBook; design:
return { data, design }
}
function generatedMarkdownWithAdditionalSection(topic: string): string {
return [
`# ${topic} 教学设计`,
'| | |',
'|:---|:---|',
`| **课题** | **${topic}** |`,
'| **课时** | 1课时40分钟 |',
'| **教学目标** | **知识目标**:理解概念。<br>**技能目标**:完成任务。<br>**素养目标**:规范表达。 |',
'| **教学重难点** | **重点**:任务流程。<br>**难点**:问题定位。 |',
'| **教学资源准备** | 机房、示例文件。 |',
'',
'## 教学过程',
'',
'| 教学环节 | 教学内容 | 教师活动 | 学生活动 | 设计意图 |',
'|:---|:---|:---|:---|:---|',
'| **1. 导入**<br>5分钟 | 引出任务。 | **情境导入**<br>展示案例。 | **观察思考**<br>回答问题。 | 明确目标。 |',
'',
'## 板书设计',
'',
'```text',
`${topic}`,
'```',
'',
'## 教学成效与反思',
'',
'| | |',
'|:---|:---|',
'| **教学成效** | 学生完成任务。 |',
'| **教学反思** | 后续加强练习。 |',
'',
'## 附加说明',
'',
'这是模型额外生成的内容。',
].join('\n')
}
describe('useTeachingBook', () => {
beforeEach(() => {
vi.clearAllMocks()
@@ -140,6 +176,25 @@ describe('useTeachingBook', () => {
expect(store.book.value.selectedId).toBe(store.book.value.designs[0]?.id)
})
it('generateLesson discards unclassified additional content from AI output', async () => {
mockGetBook(createEmptyBook())
vi.mocked(booksApi.generateLesson).mockResolvedValue({
filename: 'css-flex.md',
markdown: generatedMarkdownWithAdditionalSection('CSS 弹性布局'),
})
const store = useTeachingBook('b1')
await flushPromises()
const result = await store.generateLesson('CSS 弹性布局')
expect(result).toEqual({ ok: true })
expect(store.book.value.designs[0]?.additionalContent).toBe('')
expect(store.book.value.designs[0]?.warnings).not.toContainEqual(
expect.objectContaining({ code: 'unclassified-content' }),
)
})
it('generateLesson returns an error when the API call fails', async () => {
mockGetBook(createEmptyBook())
vi.mocked(booksApi.generateLesson).mockRejectedValue(new Error('Deepseek 请求失败。'))

View File

@@ -244,10 +244,18 @@ export function useTeachingBook(bookId: string): TeachingBookStore {
touch()
}
function removeGeneratedAdditionalContent(design: TeachingDesign): TeachingDesign {
design.additionalContent = ''
design.warnings = design.warnings.filter((warning) => warning.code !== 'unclassified-content')
return design
}
async function generateLesson(topic: string): Promise<GenerateLessonResult> {
try {
const result = await booksApi.generateLesson(topic)
const design = parseTeachingDesign(result.filename, result.markdown)
const design = removeGeneratedAdditionalContent(
parseTeachingDesign(result.filename, result.markdown),
)
book.value.designs.push(design)
book.value.selectedId = design.id
touch()
@@ -299,7 +307,9 @@ export function useTeachingBook(bookId: string): TeachingBookStore {
try {
const result = await booksApi.generateLesson(topic)
results[index] = parseTeachingDesign(result.filename, result.markdown)
results[index] = removeGeneratedAdditionalContent(
parseTeachingDesign(result.filename, result.markdown),
)
appendReadyLessons()
} catch (error) {
firstError = error instanceof Error ? error.message : '生成失败。'
@@ -322,7 +332,9 @@ export function useTeachingBook(bookId: string): TeachingBookStore {
const topic = existing.originalFilename.replace(/\.md$/i, '')
try {
const result = await booksApi.generateLesson(topic)
const newDesign = parseTeachingDesign(result.filename, result.markdown)
const newDesign = removeGeneratedAdditionalContent(
parseTeachingDesign(result.filename, result.markdown),
)
const index = book.value.designs.findIndex((d) => d.id === id)
if (index !== -1) {
book.value.designs.splice(index, 1, newDesign)

View File

@@ -229,8 +229,8 @@ input {
/* Sidebar */
.lesson-sidebar {
width: 260px;
flex: 0 0 260px;
width: 360px;
flex: 0 0 360px;
background: #fff;
border-right: 1px solid var(--line);
overflow-y: auto;
@@ -300,7 +300,7 @@ input {
color: var(--muted);
cursor: pointer;
padding: 0 12px;
font-size: 16px;
font-size: 24px;
}
.lesson-sidebar-remove:hover {