refactor: 使用 Naive UI 重构后台布局与黑白双主题

This commit is contained in:
GouJ
2026-09-01 16:31:21 +08:00
parent 55516537bf
commit db8c176b55
51 changed files with 3277 additions and 2442 deletions
+55 -15
View File
@@ -27,6 +27,9 @@ import SubjectIdentityPage from '../subject-identity/SubjectIdentityPage.vue'
import { styleFixture, styleImageFixture } from '../visual-style/testing/fixtures'
import { identityFixture, identityImageFixture } from '../subject-identity/testing/fixtures'
import { getIdentitySession } from '../subject-identity/model'
import { expandSections, selectControl } from '../../testing/naive'
import ProductionPage from '../production/ProductionPage.vue'
import { getProductionSession } from '../production/model'
/** 新资产页面复用已有项目上下文和内存路由,不接入真实模型。 */
async function mountAssets(page: 'style' | 'identity', query = '') {
@@ -39,6 +42,7 @@ async function mountAssets(page: 'style' | 'identity', query = '') {
global: { plugins: [router], provide: { [projectContextKey as symbol]: provided } }
})
await flushPromises()
if (page === 'identity') await expandSections()
return provided
}
@@ -78,7 +82,7 @@ function context(): ReturnType<typeof useProjectData> {
}
}
/** 找到挂载到 body 的 Reka 弹窗按钮。 */
/** 找到挂载到 body 的 Naive 弹窗按钮。 */
function button(label: string): HTMLButtonElement {
const element = [...document.querySelectorAll('button')].find(item => item.textContent?.trim() === label)
if (!element) throw new Error('找不到按钮:' + label)
@@ -89,7 +93,7 @@ function button(label: string): HTMLButtonElement {
async function confirmGeneration(label: string) {
button(label).click()
await flushPromises()
const acknowledgement = document.querySelector<HTMLInputElement>('[role="dialog"] input[type="checkbox"]')
const acknowledgement = document.querySelector<HTMLElement>('[role="dialog"] [role="checkbox"]')
expect(button('确认' + label).disabled).toBe(true)
acknowledgement!.click()
await flushPromises()
@@ -98,13 +102,15 @@ async function confirmGeneration(label: string) {
}
/** 挂载分镜页面并复用项目上下文 fixture。 */
function mountStoryboard() {
async function mountStoryboard() {
const provided = context()
provided.data.value!.checkpoints = [storyboardCheckpoint()]
wrapper = mount(StoryboardPage, {
attachTo: document.body,
global: { provide: { [projectContextKey as symbol]: provided }, stubs: { RouterLink: true } }
})
await flushPromises()
await expandSections()
return provided
}
@@ -120,9 +126,11 @@ async function mountSubjectImages() {
global: { plugins: [router], provide: { [projectContextKey as symbol]: context() } }
})
await flushPromises()
await expandSections()
}
afterEach(() => {
getProductionSession(fixture.id).receipt = null
getIdentitySession(fixture.id).receipt = null
wrapper?.unmount()
wrapper = undefined
@@ -133,6 +141,38 @@ afterEach(() => {
Object.assign(getOperation(fixture.id), { pending: false, label: '', error: '', notice: '' })
})
describe('内容级滚动下的生产反馈', () => {
it('默认折叠总览时查询错误仍显示在内容区外层', async () => {
vi.stubGlobal('fetch', vi.fn<typeof fetch>().mockRejectedValue(new Error('生产连接中断')))
wrapper = mount(ProductionPage, {
attachTo: document.body,
global: { provide: { [projectContextKey as symbol]: context() }, stubs: { RouterLink: true } }
})
await flushPromises()
expect(wrapper.find('.workspace-overview .n-collapse-item--active').exists()).toBe(false)
const alert = wrapper.get('.workspace-split [role="alert"]')
expect(alert.element.closest('.workspace-overview')).toBeNull()
expect(alert.text()).toContain('无法连接后端')
})
it('回执返回时自动展开总览,部分失败诊断不会留在折叠区内', async () => {
vi.stubGlobal('fetch', vi.fn<typeof fetch>().mockRejectedValue(new Error('离线测试')))
wrapper = mount(ProductionPage, {
attachTo: document.body,
global: { provide: { [projectContextKey as symbol]: context() }, stubs: { RouterLink: true } }
})
await flushPromises()
getProductionSession(fixture.id).receipt = {
kind: 'retry',
title: '重试失败视频任务',
result: { totalFailed: 1, retried: 0, failed: 1, failures: [{ shotId: 'shot-1', error: '供应商不可用' }] }
}
await flushPromises()
expect(wrapper.find('.workspace-overview .n-collapse-item--active').exists()).toBe(true)
expect(wrapper.text()).toContain('供应商不可用')
})
})
describe('视觉风格与主体身份工作区', () => {
it('从形态深链接进入后,手动切换主体不会被目录刷新切回', async () => {
const form = formFixture()
@@ -180,7 +220,7 @@ describe('视觉风格与主体身份工作区', () => {
vi.stubGlobal('fetch', fetcher)
await mountAssets('identity')
await wrapper!.get('#identity-description').setValue('人工稳定身份')
await wrapper!.get('#identity-lock').setValue(true)
await wrapper!.get('#identity-lock').trigger('click')
button('保存主体身份').click()
await flushPromises()
const put = fetcher.mock.calls.find(([, init]) => init?.method === 'PUT')!
@@ -226,7 +266,7 @@ describe('视觉风格与主体身份工作区', () => {
await mountAssets('style')
button('AI 重新生成风格').click()
await flushPromises()
document.querySelector<HTMLInputElement>('[role="dialog"] input[type="checkbox"]')!.click()
document.querySelector<HTMLElement>('[role="dialog"] [role="checkbox"]')!.click()
await flushPromises()
expect(button('确认AI 重新生成风格').disabled).toBe(false)
button('取消').click()
@@ -250,7 +290,7 @@ describe('视觉风格与主体身份工作区', () => {
await wrapper!.get('#style-constraints').setValue('{"不应静默丢弃":true}')
expect(button('保存视觉风格').disabled).toBe(true)
await wrapper!.get('#style-constraints').setValue('["真人写实"]')
await wrapper!.get('#style-lock').setValue(true)
await wrapper!.get('#style-lock').trigger('click')
await wrapper!.get('#style-name').setValue('雨夜风格')
button('保存视觉风格').click()
await flushPromises()
@@ -625,7 +665,7 @@ describe('工作台页面交互', () => {
expect(wrapper!.text()).toContain('服务暂不可用')
const post = fetcher.mock.calls.find(([, init]) => init?.method === 'POST')!
expect(JSON.parse(post[1]!.body as string)).toEqual({ provider: 'seedream', concurrency: 2, force: false })
await wrapper!.get('.panel details input[type="checkbox"]').setValue(true)
await wrapper!.get('.n-collapse [role="checkbox"]').trigger('click')
await confirmGeneration('为全项目新增候选图')
const lastPost = fetcher.mock.calls.filter(([, init]) => init?.method === 'POST').at(-1)!
expect(JSON.parse(lastPost[1]!.body as string).force).toBe(true)
@@ -740,11 +780,11 @@ describe('工作台页面交互', () => {
)
})
vi.stubGlobal('fetch', fetcher)
mountStoryboard()
await mountStoryboard()
await flushPromises()
await confirmGeneration('生成本集导演设计')
expect(button('补齐项目导演设计').disabled).toBe(true)
await wrapper!.get('select').setValue('2')
selectControl(wrapper!, 'aria-label', '选择分镜剧集').vm.$emit('update:value', 2)
await flushPromises()
wrapper!.unmount()
wrapper = undefined
@@ -791,7 +831,7 @@ describe('工作台页面交互', () => {
return new Response(JSON.stringify({ data: result }))
})
vi.stubGlobal('fetch', fetcher)
mountStoryboard()
await mountStoryboard()
await flushPromises()
expect(button('生成本集导演设计').disabled).toBe(false)
expect(button('生成本集即时状态').disabled).toBe(true)
@@ -844,12 +884,12 @@ describe('工作台页面交互', () => {
)
})
vi.stubGlobal('fetch', fetcher)
mountStoryboard()
await mountStoryboard()
await flushPromises()
await confirmGeneration('补齐项目导演设计')
expect(wrapper!.get('[aria-label="生成回执"] [role="alert"]').text()).toContain('存在失败')
expect(wrapper!.text()).toContain('模型校验失败')
await wrapper!.get('input[type="checkbox"]').setValue(true)
await wrapper!.get('[role="checkbox"]').trigger('click')
await confirmGeneration('重生成全部导演设计')
const posts = fetcher.mock.calls.filter(([, init]) => init?.method === 'POST')
expect(posts.map(([, init]) => JSON.parse(init!.body as string))).toEqual([
@@ -877,9 +917,9 @@ describe('工作台页面交互', () => {
)
})
vi.stubGlobal('fetch', fetcher)
mountStoryboard()
await mountStoryboard()
await flushPromises()
await wrapper!.get('select[aria-label="选择分镜剧集"]').setValue('2')
selectControl(wrapper!, 'aria-label', '选择分镜剧集').vm.$emit('update:value', 2)
await flushPromises()
expect(oldRequests.every(request => request.signal.aborted)).toBe(true)
for (const request of oldRequests)
@@ -911,7 +951,7 @@ describe('工作台页面交互', () => {
)
})
vi.stubGlobal('fetch', fetcher)
mountStoryboard()
await mountStoryboard()
await flushPromises()
fail = true
button('刷新分镜').click()