feat: add generate lesson dialog
This commit is contained in:
50
src/components/GenerateLessonDialog.test.ts
Normal file
50
src/components/GenerateLessonDialog.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import GenerateLessonDialog from './GenerateLessonDialog.vue'
|
||||
|
||||
describe('GenerateLessonDialog', () => {
|
||||
it('disables submit until a topic is entered', async () => {
|
||||
const wrapper = mount(GenerateLessonDialog, { props: { loading: false, error: null } })
|
||||
|
||||
const submit = wrapper.findAll('button')[0]!
|
||||
expect(submit.attributes('disabled')).toBeDefined()
|
||||
|
||||
await wrapper.get('input').setValue('CSS 弹性布局')
|
||||
expect(submit.attributes('disabled')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('emits submit with the trimmed topic', async () => {
|
||||
const wrapper = mount(GenerateLessonDialog, { props: { loading: false, error: null } })
|
||||
|
||||
await wrapper.get('input').setValue(' CSS 弹性布局 ')
|
||||
await wrapper.findAll('button')[0]!.trigger('click')
|
||||
|
||||
expect(wrapper.emitted('submit')).toEqual([['CSS 弹性布局']])
|
||||
})
|
||||
|
||||
it('shows a loading state and disables interaction', () => {
|
||||
const wrapper = mount(GenerateLessonDialog, { props: { loading: true, error: null } })
|
||||
|
||||
expect(wrapper.get('input').attributes('disabled')).toBeDefined()
|
||||
expect(wrapper.findAll('button')[0]!.text()).toContain('生成中')
|
||||
expect(wrapper.findAll('button')[0]!.attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('shows an error message and allows retry without closing', async () => {
|
||||
const wrapper = mount(GenerateLessonDialog, { props: { loading: false, error: 'Deepseek 请求失败。' } })
|
||||
|
||||
expect(wrapper.text()).toContain('Deepseek 请求失败。')
|
||||
expect(wrapper.findAll('button')[0]!.attributes('disabled')).toBeDefined()
|
||||
|
||||
await wrapper.get('input').setValue('CSS 弹性布局')
|
||||
expect(wrapper.findAll('button')[0]!.attributes('disabled')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('emits cancel', async () => {
|
||||
const wrapper = mount(GenerateLessonDialog, { props: { loading: false, error: null } })
|
||||
|
||||
await wrapper.findAll('button')[1]!.trigger('click')
|
||||
|
||||
expect(wrapper.emitted('cancel')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
44
src/components/GenerateLessonDialog.vue
Normal file
44
src/components/GenerateLessonDialog.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [topic: string]
|
||||
cancel: []
|
||||
}>()
|
||||
|
||||
const topic = ref('')
|
||||
|
||||
function submit(): void {
|
||||
const value = topic.value.trim()
|
||||
if (!value || props.loading) return
|
||||
emit('submit', value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dialog-overlay" role="dialog" aria-modal="true" aria-labelledby="generate-lesson-title">
|
||||
<div class="dialog">
|
||||
<h2 id="generate-lesson-title">生成教案</h2>
|
||||
<p>输入主题,AI 将生成一份符合模板结构的教案,加入当前整本末尾。</p>
|
||||
<input
|
||||
v-model="topic"
|
||||
type="text"
|
||||
placeholder="例如:CSS 弹性布局入门"
|
||||
:disabled="loading"
|
||||
@keydown.enter="submit"
|
||||
/>
|
||||
<p v-if="error" class="app-notice app-notice--error" role="alert">{{ error }}</p>
|
||||
<div class="dialog-actions">
|
||||
<button type="button" :disabled="loading || !topic.trim()" @click="submit">
|
||||
{{ loading ? '生成中…' : '生成' }}
|
||||
</button>
|
||||
<button type="button" :disabled="loading" @click="emit('cancel')">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user