feat: 新增项目资产库与形态参考素材

This commit is contained in:
GouJ
2026-09-21 19:54:51 +08:00
parent e6d02a20a7
commit b834d1d259
18 changed files with 737 additions and 41 deletions
+53 -23
View File
@@ -40,27 +40,6 @@ async function submit() {
}
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 () => {
const fetcher = vi
.fn<typeof fetch>()
@@ -71,7 +50,6 @@ describe('统一的 Naive UI 表单校验', () => {
await flushPromises()
expect(document.querySelector('form')!.noValidate).toBe(true)
expect(document.querySelector('[required]')).toBeNull()
await input('#title', '测试剧本')
await input('#topic', ' ')
const focus = vi.spyOn(HTMLTextAreaElement.prototype, 'focus')
await submit()
@@ -131,7 +109,10 @@ describe('统一的 Naive UI 表单校验', () => {
})
it('登记参考图拦截非法地址和小数排序,允许有效存储地址与负整数排序', async () => {
wrapper = mount(StyleImages, { attachTo: document.body, props: { images: [], disabled: false } })
wrapper = mount(StyleImages, {
attachTo: document.body,
props: { projectId: 'form-test-project', images: [], disabled: false }
})
await input('[aria-label="风格图片地址"]', 'javascript:alert(1)')
await input('[aria-label="风格图片排序"]', '1.5')
await submit()
@@ -144,6 +125,55 @@ describe('统一的 Naive UI 表单校验', () => {
expect(wrapper.emitted('add')?.[0]?.[0]).toMatchObject({ imageUrl: '/storage/style.png', sortOrder: -1 })
})
it('按需读取项目资产并用正式素材 ID 登记风格参考图', async () => {
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(
new Response(
JSON.stringify({
data: [
{
id: 'asset-db-1',
projectId: 'form-test-project',
name: '人物质感参考',
type: 'image',
category: 'character',
mimeType: 'image/png',
extension: 'png',
size: 1024,
publicUrl: '/storage/reference.png',
metadata: null,
createdAt: '2026-09-21T00:00:00Z',
updatedAt: '2026-09-21T00:00:00Z'
}
]
})
)
)
vi.stubGlobal('fetch', fetcher)
wrapper = mount(StyleImages, {
attachTo: document.body,
props: { projectId: 'form-test-project', images: [], disabled: false }
})
expect(fetcher).not.toHaveBeenCalled()
button('从资产库选择').click()
await flushPromises()
expect(fetcher).toHaveBeenCalledWith(
'/api/projects/form-test-project/assets',
expect.objectContaining({ method: 'GET' })
)
const option = document.querySelector<HTMLButtonElement>('.style-asset-option')!
expect(option.textContent).toContain('人物质感参考')
option.click()
await flushPromises()
button('登记为整体参考图').click()
await flushPromises()
expect(wrapper.emitted('add')?.[0]?.[0]).toMatchObject({
projectAssetId: 'asset-db-1',
category: 'overall',
source: 'upload'
})
})
it('异步验证期间切换对象或重复提交不会执行旧动作', async () => {
let finish: (() => void) | undefined
const model = reactive({ text: '有效输入' })
+4 -3
View File
@@ -125,7 +125,7 @@ describe('管理后台组件边界', () => {
await router.push('/projects/test/production')
wrapper = mount(App, { attachTo: document.body, global: { plugins: [router] } })
await flushPromises()
expect(wrapper.findAll('.n-menu .n-menu-item')).toHaveLength(8)
expect(wrapper.findAll('.n-menu .n-menu-item')).toHaveLength(9)
expect(wrapper.get('#main-content .workspace-page').text()).toBe('镜头详情')
expect(wrapper.get('.theme-toggle').text()).toBe('')
expect(wrapper.get('.theme-toggle').attributes('aria-haspopup')).toBe('menu')
@@ -194,6 +194,7 @@ describe('管理后台组件边界', () => {
'视觉风格',
'主体身份',
'形态图片',
'资产库',
'分镜设计',
'镜头生产'
])
@@ -202,7 +203,7 @@ describe('管理后台组件边界', () => {
await settings.trigger('click')
await flushPromises()
expect(document.querySelector('[role="dialog"][aria-label="后端连接"]')).not.toBeNull()
expect(wrapper.findAll('.admin-nav-scroll .n-menu .n-menu-item')).toHaveLength(8)
expect(wrapper.findAll('.admin-nav-scroll .n-menu .n-menu-item')).toHaveLength(9)
})
it('侧栏菜单独立滚动,窄屏改为不占正文宽度的全屏固定层', () => {
@@ -304,7 +305,7 @@ describe('管理后台组件边界', () => {
expect(wrapper.get('.admin-sider').attributes('style')).toContain('width: 100%')
expect(wrapper.get('.admin-sider').attributes('aria-hidden')).toBeUndefined()
expect(wrapper.find('.admin-sider-mask').exists()).toBe(false)
expect(wrapper.get('.admin-sider').findAll('.n-menu .n-menu-item')).toHaveLength(8)
expect(wrapper.get('.admin-sider').findAll('.n-menu .n-menu-item')).toHaveLength(9)
expect(wrapper.get('.admin-topbar [aria-label="关闭主菜单"]').attributes('aria-expanded')).toBe('true')
const expandedBrand = wrapper.get('.admin-brand')
expect(expandedBrand.classes()).not.toContain('is-collapsed')