style: 优化资产卡片信息与瀑布流布局

This commit is contained in:
GouJ
2026-09-22 11:12:40 +08:00
parent 2d0d7e22d3
commit 2240a90424
2 changed files with 71 additions and 19 deletions
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, reactive, ref } from 'vue' import { computed, reactive, ref } from 'vue'
import { NAlert, NButton, NFormItem, NInput, NPopconfirm, NSelect, NTag } from 'naive-ui' import { NAlert, NButton, NFormItem, NInput, NPopconfirm, NSelect, NTag, NTooltip } from 'naive-ui'
import { Pencil, RefreshCw, Search, Trash2, Upload } from '@lucide/vue' import { Pencil, RefreshCw, Search, Trash2, Upload } from '@lucide/vue'
import { AppDialog, AssetImage, EmptyState } from '../../components/ui' import { AppDialog, AssetImage, EmptyState } from '../../components/ui'
import AppForm from '../../components/ui/AppForm.vue' import AppForm from '../../components/ui/AppForm.vue'
@@ -93,6 +93,17 @@ function formatBytes(value: number) {
return `${(value / 1024 / 1024).toFixed(1)} MB` return `${(value / 1024 / 1024).toFixed(1)} MB`
} }
function formatDate(value: string) {
const date = new Date(value)
return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN', { hour12: false })
}
function metadataOriginalName(value: unknown) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return '未记录'
const originalName = (value as Record<string, unknown>).originalName
return typeof originalName === 'string' && originalName.trim() ? originalName : '未记录'
}
function clearFilters() { function clearFilters() {
search.value = '' search.value = ''
projectId.value = null projectId.value = null
@@ -272,19 +283,19 @@ async function removeAsset() {
<article v-for="asset in filteredAssets" :key="asset.id" class="asset-library-card"> <article v-for="asset in filteredAssets" :key="asset.id" class="asset-library-card">
<AssetImage :src="asset.publicUrl" :alt="asset.name" preview class="asset-library-image" /> <AssetImage :src="asset.publicUrl" :alt="asset.name" preview class="asset-library-image" />
<div class="asset-library-card-content"> <div class="asset-library-card-content">
<div class="flex min-w-0 items-start justify-between gap-2"> <NTooltip placement="top-start" trigger="hover">
<div class="min-w-0"> <template #trigger>
<h2 class="truncate text-sm font-semibold" :title="asset.name">{{ asset.name }}</h2> <h2 class="asset-library-title">{{ asset.name }}</h2>
<p class="mt-1 truncate text-xs text-muted" :title="projectName(asset.project)"> </template>
{{ projectName(asset.project) }} {{ asset.name }}
</p> </NTooltip>
</div> <div class="mt-2 flex min-w-0 items-center justify-between gap-2">
<p class="truncate text-xs text-muted" :title="projectName(asset.project)">
{{ projectName(asset.project) }}
</p>
<NTag size="small" :bordered="false">{{ categoryLabel(asset.category) }}</NTag> <NTag size="small" :bordered="false">{{ categoryLabel(asset.category) }}</NTag>
</div> </div>
<div class="mt-3 flex items-center justify-between gap-3"> <div class="mt-3 flex justify-end">
<span class="text-[11px] text-muted">
{{ asset.extension.toUpperCase() }} · {{ formatBytes(asset.size) }}
</span>
<NButton text size="small" type="primary" :disabled="busy" @click="openEdit(asset)"> <NButton text size="small" type="primary" :disabled="busy" @click="openEdit(asset)">
<template #icon><Pencil :size="14" /></template>管理 <template #icon><Pencil :size="14" /></template>管理
</NButton> </NButton>
@@ -330,8 +341,8 @@ async function removeAsset() {
<AppDialog <AppDialog
v-model:open="editOpen" v-model:open="editOpen"
title="管理素材信息" title="素材详情与管理"
description="可在当前页面修改名称和分类或删除素材文件。" description="查看文件详情,也可在当前页面修改名称和分类或删除素材。"
:busy="busy" :busy="busy"
> >
<NAlert v-if="actionError" type="error" :show-icon="false" class="mt-5">{{ actionError }}</NAlert> <NAlert v-if="actionError" type="error" :show-icon="false" class="mt-5">{{ actionError }}</NAlert>
@@ -341,6 +352,18 @@ async function removeAsset() {
</NFormItem> </NFormItem>
<NFormItem path="name" label="素材名称"><NInput v-model:value="editForm.name" /></NFormItem> <NFormItem path="name" label="素材名称"><NInput v-model:value="editForm.name" /></NFormItem>
<NFormItem path="category" label="分类标识"><NInput v-model:value="editForm.category" /></NFormItem> <NFormItem path="category" label="分类标识"><NInput v-model:value="editForm.category" /></NFormItem>
<dl v-if="selectedAsset" class="asset-detail-grid">
<dt>原始文件名</dt>
<dd>{{ metadataOriginalName(selectedAsset.metadata) }}</dd>
<dt>文件类型</dt>
<dd>{{ selectedAsset.mimeType }} · {{ selectedAsset.extension.toUpperCase() }}</dd>
<dt>文件大小</dt>
<dd>{{ formatBytes(selectedAsset.size) }}</dd>
<dt>创建时间</dt>
<dd>{{ formatDate(selectedAsset.createdAt) }}</dd>
<dt>更新时间</dt>
<dd>{{ formatDate(selectedAsset.updatedAt) }}</dd>
</dl>
<div class="mt-4 flex items-center justify-between gap-3"> <div class="mt-4 flex items-center justify-between gap-3">
<NPopconfirm <NPopconfirm
:positive-button-props="{ disabled: busy }" :positive-button-props="{ disabled: busy }"
@@ -377,17 +400,37 @@ async function removeAsset() {
@apply grid grid-cols-[minmax(220px,_420px)_220px_180px_auto] items-center gap-3 mb-4 p-3 bg-(--app-subtle); @apply grid grid-cols-[minmax(220px,_420px)_220px_180px_auto] items-center gap-3 mb-4 p-3 bg-(--app-subtle);
} }
.asset-library-grid { .asset-library-grid {
@apply grid grid-cols-[repeat(auto-fill,_minmax(min(240px,_100%),_1fr))] gap-4; column-width: 240px;
column-gap: 16px;
} }
.asset-library-card { .asset-library-card {
@apply min-w-0 overflow-hidden bg-(--app-surface); @apply inline-block w-full min-w-0 overflow-hidden bg-(--app-surface) align-top;
break-inside: avoid;
margin-bottom: 16px;
} }
.asset-library-image { .asset-library-image {
@apply aspect-[4/3]; @apply block aspect-auto;
}
.asset-library-image :deep(.reveal-image),
.asset-library-image :deep(.n-image),
.asset-library-image :deep(.n-image img) {
@apply h-auto;
} }
.asset-library-card-content { .asset-library-card-content {
@apply p-4; @apply p-4;
} }
.asset-library-title {
@apply block w-full truncate text-sm font-semibold;
}
.asset-detail-grid {
@apply grid grid-cols-[96px_minmax(0,_1fr)] gap-x-4 gap-y-2 mt-2 p-3 bg-(--app-subtle) text-xs;
}
.asset-detail-grid dt {
@apply text-muted;
}
.asset-detail-grid dd {
@apply min-w-0 break-all text-ink;
}
@container asset-library (max-width: 840px) { @container asset-library (max-width: 840px) {
.asset-library-filters { .asset-library-filters {
@apply grid-cols-2; @apply grid-cols-2;
+11 -2
View File
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest' import { afterEach, describe, expect, it, vi } from 'vitest'
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils' import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
import { createMemoryHistory, createRouter } from 'vue-router' import { createMemoryHistory, createRouter } from 'vue-router'
import { NTooltip } from 'naive-ui'
import AssetLibraryPage from '@/features/project-assets/AssetLibraryPage.vue' import AssetLibraryPage from '@/features/project-assets/AssetLibraryPage.vue'
let wrapper: VueWrapper | undefined let wrapper: VueWrapper | undefined
@@ -24,7 +25,7 @@ describe('全局资产库', () => {
extension: 'png', extension: 'png',
size: 4096, size: 4096,
publicUrl: '/storage/scene.png', publicUrl: '/storage/scene.png',
metadata: null, metadata: { originalName: 'rain-street-original.png' },
createdAt: '2026-09-22T00:00:00Z', createdAt: '2026-09-22T00:00:00Z',
updatedAt: '2026-09-22T00:00:00Z', updatedAt: '2026-09-22T00:00:00Z',
project: { id: 'project-one', title: '记忆当铺', topic: '记忆交易', status: 'completed' } project: { id: 'project-one', title: '记忆当铺', topic: '记忆交易', status: 'completed' }
@@ -73,13 +74,21 @@ describe('全局资产库', () => {
expect(wrapper.text()).toContain('雨夜街道') expect(wrapper.text()).toContain('雨夜街道')
expect(wrapper.text()).toContain('林默正面照') expect(wrapper.text()).toContain('林默正面照')
expect(wrapper.text()).toContain('记忆当铺') expect(wrapper.text()).toContain('记忆当铺')
expect(wrapper.findAllComponents(NTooltip)).toHaveLength(2)
expect(wrapper.get('.asset-library-title').classes()).toContain('asset-library-title')
expect(wrapper.get('.asset-library-card').text()).not.toContain('rain-street-original.png')
expect(wrapper.get('.asset-library-card').text()).not.toContain('4.0 KB')
await wrapper await wrapper
.findAll('button') .findAll('button')
.find(button => button.text() === '管理')! .find(button => button.text() === '管理')!
.trigger('click') .trigger('click')
await flushPromises() await flushPromises()
expect(document.querySelector('[role="dialog"][aria-label="管理素材信息"]')).not.toBeNull() const dialog = document.querySelector('[role="dialog"][aria-label="素材详情与管理"]')!
expect(dialog).not.toBeNull()
expect(dialog.textContent).toContain('rain-street-original.png')
expect(dialog.textContent).toContain('image/png')
expect(dialog.textContent).toContain('4.0 KB')
expect(router.currentRoute.value.path).toBe('/assets') expect(router.currentRoute.value.path).toBe('/assets')
await wrapper.get('input[aria-label="搜索资产图片"]').setValue('天降甘霖') await wrapper.get('input[aria-label="搜索资产图片"]').setValue('天降甘霖')