diff --git a/src/features/project-assets/AssetLibraryPage.vue b/src/features/project-assets/AssetLibraryPage.vue index d55d31b..a3c9fb1 100644 --- a/src/features/project-assets/AssetLibraryPage.vue +++ b/src/features/project-assets/AssetLibraryPage.vue @@ -1,26 +1,74 @@ diff --git a/src/features/project-assets/api.ts b/src/features/project-assets/api.ts index 6aafee2..efeecbf 100644 --- a/src/features/project-assets/api.ts +++ b/src/features/project-assets/api.ts @@ -1,5 +1,5 @@ import { request } from '../../lib/http' -import type { ProjectAsset, UpdateProjectAssetInput, UploadProjectAssetInput } from './types' +import type { GlobalProjectAsset, ProjectAsset, UpdateProjectAssetInput, UploadProjectAssetInput } from './types' /** 固定项目资产路径并编码正式数据库 ID。 */ function assetPath(projectId: string, assetId?: string) { @@ -9,6 +9,7 @@ function assetPath(projectId: string, assetId?: string) { /** 项目素材 API;上传使用 multipart,浏览和编辑不调用生成模型。 */ export const projectAssetsApi = { + listAll: (signal?: AbortSignal) => request('/assets', { signal }), list: (projectId: string, signal?: AbortSignal) => request(assetPath(projectId), { signal }), get: (projectId: string, assetId: string, signal?: AbortSignal) => request(assetPath(projectId, assetId), { signal }), diff --git a/src/features/project-assets/types.ts b/src/features/project-assets/types.ts index 5f6970c..4ad861b 100644 --- a/src/features/project-assets/types.ts +++ b/src/features/project-assets/types.ts @@ -14,6 +14,19 @@ export interface ProjectAsset { updatedAt: string } +/** 全局资产列表附带最小剧本信息,供筛选和来源标识使用。 */ +export interface ProjectAssetProject { + id: string + title: string | null + topic: string + status: string +} + +/** 全局资产接口记录。 */ +export interface GlobalProjectAsset extends ProjectAsset { + project: ProjectAssetProject +} + /** 素材编辑只修改展示信息,不替换物理文件。 */ export interface UpdateProjectAssetInput { name?: string diff --git a/tests/features/project-assets/api.test.ts b/tests/features/project-assets/api.test.ts index b2f03b7..654fcdd 100644 --- a/tests/features/project-assets/api.test.ts +++ b/tests/features/project-assets/api.test.ts @@ -20,6 +20,19 @@ const asset: ProjectAsset = { afterEach(() => vi.unstubAllGlobals()) describe('项目资产 API', () => { + it('通过单个全局接口读取全部项目素材', async () => { + const globalAsset = { + ...asset, + project: { id: 'project-db-1', title: '记忆当铺', topic: '记忆交易', status: 'completed' } + } + const fetcher = vi.fn().mockResolvedValue(new Response(JSON.stringify({ data: [globalAsset] }))) + vi.stubGlobal('fetch', fetcher) + + await expect(projectAssetsApi.listAll()).resolves.toEqual([globalAsset]) + expect(fetcher).toHaveBeenCalledOnce() + expect(fetcher).toHaveBeenCalledWith('/api/assets', expect.objectContaining({ method: 'GET' })) + }) + it('使用 multipart 上传文件,不手工设置 Content-Type', async () => { const fetcher = vi .fn() diff --git a/tests/features/project-assets/library.test.ts b/tests/features/project-assets/library.test.ts new file mode 100644 index 0000000..6e489a4 --- /dev/null +++ b/tests/features/project-assets/library.test.ts @@ -0,0 +1,78 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { flushPromises, mount, type VueWrapper } from '@vue/test-utils' +import { createMemoryHistory, createRouter } from 'vue-router' +import AssetLibraryPage from '@/features/project-assets/AssetLibraryPage.vue' + +let wrapper: VueWrapper | undefined +afterEach(() => { + wrapper?.unmount() + wrapper = undefined + document.body.innerHTML = '' + vi.unstubAllGlobals() +}) + +describe('全局资产库', () => { + it('单次读取并直接显示全部剧本图片,支持按图片与剧本搜索', async () => { + const fetcher = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + data: [ + { + id: 'asset-scene', + projectId: 'project-one', + name: '雨夜街道', + type: 'image', + category: 'scene', + mimeType: 'image/png', + extension: 'png', + size: 4096, + publicUrl: '/storage/scene.png', + metadata: null, + createdAt: '2026-09-22T00:00:00Z', + updatedAt: '2026-09-22T00:00:00Z', + project: { id: 'project-one', title: '记忆当铺', topic: '记忆交易', status: 'completed' } + }, + { + id: 'asset-character', + projectId: 'project-two', + name: '林默正面照', + type: 'image', + category: 'character', + mimeType: 'image/jpeg', + extension: 'jpg', + size: 8192, + publicUrl: '/storage/character.jpg', + metadata: null, + createdAt: '2026-09-22T01:00:00Z', + updatedAt: '2026-09-22T01:00:00Z', + project: { id: 'project-two', title: '天降甘霖', topic: '乡村故事', status: 'completed' } + } + ] + }) + ) + ) + vi.stubGlobal('fetch', fetcher) + const router = createRouter({ + history: createMemoryHistory(), + routes: [ + { path: '/assets', component: AssetLibraryPage }, + { path: '/projects/:projectId/assets', component: { template: '
' } } + ] + }) + await router.push('/assets') + wrapper = mount(AssetLibraryPage, { attachTo: document.body, global: { plugins: [router] } }) + await flushPromises() + + expect(fetcher).toHaveBeenCalledOnce() + expect(fetcher).toHaveBeenCalledWith('/api/assets', expect.objectContaining({ method: 'GET' })) + expect(wrapper.findAll('.asset-library-card')).toHaveLength(2) + expect(wrapper.text()).toContain('雨夜街道') + expect(wrapper.text()).toContain('林默正面照') + expect(wrapper.text()).toContain('记忆当铺') + + await wrapper.get('input[aria-label="搜索资产图片"]').setValue('天降甘霖') + expect(wrapper.findAll('.asset-library-card')).toHaveLength(1) + expect(wrapper.text()).toContain('林默正面照') + expect(wrapper.text()).not.toContain('雨夜街道') + }) +})