feat(short-drama-agent-front): 新增项目标题输入
This commit is contained in:
@@ -11,12 +11,16 @@ import { errorMessage } from '../../../lib/http'
|
|||||||
|
|
||||||
/** 新建剧本表单,202 成功后交由父级跳转,不在前端模拟生成。 */
|
/** 新建剧本表单,202 成功后交由父级跳转,不在前端模拟生成。 */
|
||||||
const emit = defineEmits<{ created: [projectId: string] }>()
|
const emit = defineEmits<{ created: [projectId: string] }>()
|
||||||
|
/** 新建剧本弹窗是否打开。 */
|
||||||
const open = ref(false)
|
const open = ref(false)
|
||||||
|
/** 创建请求是否正在提交。 */
|
||||||
const busy = ref(false)
|
const busy = ref(false)
|
||||||
|
/** 创建请求的错误提示。 */
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
const form = reactive({ topic: '', style: '', episodeCount: 1 })
|
/** 与后端 POST /projects 入参一致的创建表单。 */
|
||||||
|
const form = reactive({ title: '', topic: '', style: '', episodeCount: 1 })
|
||||||
|
|
||||||
/** 校验正整数集数和主题,禁止双击产生重复项目。 */
|
/** 校验项目标题、主题与正整数集数,禁止双击产生重复项目。 */
|
||||||
async function submit() {
|
async function submit() {
|
||||||
if (busy.value) return
|
if (busy.value) return
|
||||||
error.value = ''
|
error.value = ''
|
||||||
@@ -24,10 +28,12 @@ async function submit() {
|
|||||||
try {
|
try {
|
||||||
const result = await projectsApi.create({
|
const result = await projectsApi.create({
|
||||||
...form,
|
...form,
|
||||||
|
title: form.title.trim(),
|
||||||
topic: form.topic.trim(),
|
topic: form.topic.trim(),
|
||||||
style: form.style.trim()
|
style: form.style.trim()
|
||||||
})
|
})
|
||||||
open.value = false
|
open.value = false
|
||||||
|
form.title = ''
|
||||||
form.topic = ''
|
form.topic = ''
|
||||||
emit('created', result.projectId)
|
emit('created', result.projectId)
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
@@ -36,7 +42,12 @@ async function submit() {
|
|||||||
busy.value = false
|
busy.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const rules = { topic: requiredTextRule('故事主题'), episodeCount: integerRule('计划集数') }
|
/** 创建项目表单校验规则。 */
|
||||||
|
const rules = {
|
||||||
|
title: requiredTextRule('项目标题'),
|
||||||
|
topic: requiredTextRule('故事主题'),
|
||||||
|
episodeCount: integerRule('计划集数')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -57,6 +68,15 @@ const rules = { topic: requiredTextRule('故事主题'), episodeCount: integerRu
|
|||||||
class="mt-7 space-y-4"
|
class="mt-7 space-y-4"
|
||||||
@submit="submit"
|
@submit="submit"
|
||||||
>
|
>
|
||||||
|
<!-- 以下是项目基本信息模块 -->
|
||||||
|
<NFormItem path="title" label="项目标题" :label-props="{ for: 'title' }"
|
||||||
|
><NInput
|
||||||
|
v-model:value="form.title"
|
||||||
|
placeholder="例如:记忆当铺"
|
||||||
|
:disabled="busy"
|
||||||
|
:input-props="{ id: 'title', autocomplete: 'off' }"
|
||||||
|
></NInput
|
||||||
|
></NFormItem>
|
||||||
<NFormItem path="topic" label="故事主题" :label-props="{ for: 'topic' }"
|
<NFormItem path="topic" label="故事主题" :label-props="{ for: 'topic' }"
|
||||||
><NInput
|
><NInput
|
||||||
placeholder="描述主角、故事背景,以及你希望展开的核心冲突……"
|
placeholder="描述主角、故事背景,以及你希望展开的核心冲突……"
|
||||||
@@ -68,6 +88,7 @@ const rules = { topic: requiredTextRule('故事主题'), episodeCount: integerRu
|
|||||||
:autosize="{ minRows: 3, maxRows: 12 }"
|
:autosize="{ minRows: 3, maxRows: 12 }"
|
||||||
></NInput
|
></NInput
|
||||||
></NFormItem>
|
></NFormItem>
|
||||||
|
<!-- 以下是生成参数模块 -->
|
||||||
<div class="grid grid-cols-1 gap-4">
|
<div class="grid grid-cols-1 gap-4">
|
||||||
<NFormItem path="style" label="剧本风格" :label-props="{ for: 'style' }"
|
<NFormItem path="style" label="剧本风格" :label-props="{ for: 'style' }"
|
||||||
><NInput
|
><NInput
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
/** 后端 DramaProject.status 的原始取值。 */
|
/** 后端 DramaProject.status 的原始取值。 */
|
||||||
export type ProjectStatus = 'draft' | 'generating' | 'completed' | 'need_review' | 'failed'
|
export type ProjectStatus = 'draft' | 'generating' | 'completed' | 'need_review' | 'failed'
|
||||||
|
|
||||||
/** 创建请求不包含标题,标题由后端在工作流收尾时更新。 */
|
/** 创建请求参数,与后端 POST /projects 入参保持一致。 */
|
||||||
export interface CreateProjectInput {
|
export interface CreateProjectInput {
|
||||||
|
/** 项目标题 */
|
||||||
|
title: string
|
||||||
|
/** 故事主题 */
|
||||||
topic: string
|
topic: string
|
||||||
|
/** 剧本风格 */
|
||||||
style: string
|
style: string
|
||||||
|
/** 计划生成集数 */
|
||||||
episodeCount: number
|
episodeCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,27 @@ async function submit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('统一的 Naive UI 表单校验', () => {
|
describe('统一的 Naive UI 表单校验', () => {
|
||||||
|
it('新建剧本要求填写项目标题,并将清理后的标题提交给后端', async () => {
|
||||||
|
const fetcher = vi
|
||||||
|
.fn<typeof fetch>()
|
||||||
|
.mockResolvedValue(new Response('{"projectId":"created","status":"generating"}', { status: 202 }))
|
||||||
|
vi.stubGlobal('fetch', fetcher)
|
||||||
|
wrapper = mount(CreateProjectDialog, { attachTo: document.body })
|
||||||
|
button('新建剧本').click()
|
||||||
|
await flushPromises()
|
||||||
|
expect(document.querySelector('#title')).toBeInstanceOf(HTMLInputElement)
|
||||||
|
await input('#topic', '雨夜来信')
|
||||||
|
await submit()
|
||||||
|
expect(feedback()).toContain('请填写项目标题')
|
||||||
|
expect(fetcher).not.toHaveBeenCalled()
|
||||||
|
await input('#title', ' 记忆当铺 ')
|
||||||
|
await submit()
|
||||||
|
expect(JSON.parse(fetcher.mock.calls[0]![1]!.body as string)).toMatchObject({
|
||||||
|
title: '记忆当铺',
|
||||||
|
topic: '雨夜来信'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('新建剧本使用字段反馈拦截空白主题和小数集数,修正后只发送一次请求', async () => {
|
it('新建剧本使用字段反馈拦截空白主题和小数集数,修正后只发送一次请求', async () => {
|
||||||
const fetcher = vi
|
const fetcher = vi
|
||||||
.fn<typeof fetch>()
|
.fn<typeof fetch>()
|
||||||
@@ -50,6 +71,7 @@ describe('统一的 Naive UI 表单校验', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(document.querySelector('form')!.noValidate).toBe(true)
|
expect(document.querySelector('form')!.noValidate).toBe(true)
|
||||||
expect(document.querySelector('[required]')).toBeNull()
|
expect(document.querySelector('[required]')).toBeNull()
|
||||||
|
await input('#title', '测试剧本')
|
||||||
await input('#topic', ' ')
|
await input('#topic', ' ')
|
||||||
const focus = vi.spyOn(HTMLTextAreaElement.prototype, 'focus')
|
const focus = vi.spyOn(HTMLTextAreaElement.prototype, 'focus')
|
||||||
await submit()
|
await submit()
|
||||||
|
|||||||
@@ -1452,6 +1452,9 @@ describe('工作台页面交互', () => {
|
|||||||
wrapper = mount(CreateProjectDialog, { attachTo: document.body })
|
wrapper = mount(CreateProjectDialog, { attachTo: document.body })
|
||||||
button('新建剧本').click()
|
button('新建剧本').click()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
const title = document.querySelector<HTMLInputElement>('#title')!
|
||||||
|
title.value = ' 记忆当铺 '
|
||||||
|
title.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
const topic = document.querySelector<HTMLTextAreaElement>('#topic')!
|
const topic = document.querySelector<HTMLTextAreaElement>('#topic')!
|
||||||
topic.value = ' 雨夜来信 '
|
topic.value = ' 雨夜来信 '
|
||||||
topic.dispatchEvent(new Event('input', { bubbles: true }))
|
topic.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
@@ -1462,6 +1465,7 @@ describe('工作台页面交互', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.emitted('created')).toEqual([['created']])
|
expect(wrapper.emitted('created')).toEqual([['created']])
|
||||||
expect(JSON.parse(fetcher.mock.calls[0]![1]!.body as string)).toEqual({
|
expect(JSON.parse(fetcher.mock.calls[0]![1]!.body as string)).toEqual({
|
||||||
|
title: '记忆当铺',
|
||||||
topic: '雨夜来信',
|
topic: '雨夜来信',
|
||||||
style: '',
|
style: '',
|
||||||
episodeCount: 6
|
episodeCount: 6
|
||||||
|
|||||||
@@ -110,11 +110,12 @@ describe('后端 API 契约', () => {
|
|||||||
)
|
)
|
||||||
vi.stubGlobal('fetch', fetcher)
|
vi.stubGlobal('fetch', fetcher)
|
||||||
expect(await projectsApi.list()).toEqual([{ id: 'p1' }])
|
expect(await projectsApi.list()).toEqual([{ id: 'p1' }])
|
||||||
expect(await projectsApi.create({ topic: '故事', style: '悬疑', episodeCount: 3 })).toEqual({
|
expect(await projectsApi.create({ title: '测试剧本', topic: '故事', style: '悬疑', episodeCount: 3 })).toEqual({
|
||||||
projectId: 'p2',
|
projectId: 'p2',
|
||||||
status: 'generating'
|
status: 'generating'
|
||||||
})
|
})
|
||||||
expect(JSON.parse(fetcher.mock.calls[1]![1].body as string)).toEqual({
|
expect(JSON.parse(fetcher.mock.calls[1]![1].body as string)).toEqual({
|
||||||
|
title: '测试剧本',
|
||||||
topic: '故事',
|
topic: '故事',
|
||||||
style: '悬疑',
|
style: '悬疑',
|
||||||
episodeCount: 3
|
episodeCount: 3
|
||||||
|
|||||||
Reference in New Issue
Block a user