feat: 功能优化

This commit is contained in:
2026-01-18 00:34:04 +08:00
parent fe91226727
commit e0cf6092a9
18 changed files with 1826 additions and 1187 deletions

View File

@@ -59,12 +59,33 @@
<!-- 右侧内容区域 -->
<div class="material-content">
<!-- 头部操作 -->
<div class="material-content__header">
<h2 class="material-content__title">
{{ getContentTitle() }}
</h2>
<!-- 搜索 -->
<div class="material-content__search">
<a-input
v-model="searchKeyword"
placeholder="搜索文件名"
style="width: 300px"
allow-clear
@press-enter="handleSearch"
>
<template #prefix>
<SearchOutlined />
</template>
</a-input>
<a-button type="primary" @click="handleSearch">搜索</a-button>
<!-- 操作按钮 -->
<div class="material-content__actions">
<!-- 全选/取消全选 -->
<a-checkbox
:checked="selectedFileIds.length === fileList.length && fileList.length > 0"
:indeterminate="selectedFileIds.length > 0 && selectedFileIds.length < fileList.length"
@change="handleSelectAll"
>
<span v-if="selectedFileIds.length === 0">全选</span>
<span v-else>已选 {{ selectedFileIds.length }}</span>
</a-checkbox>
<a-button type="primary" @click="handleOpenUploadModal">
<template #icon>
<UploadOutlined />
@@ -89,22 +110,6 @@
</div>
</div>
<!-- 搜索栏 -->
<div class="material-content__search">
<a-input
v-model="searchKeyword"
placeholder="搜索文件名"
style="width: 300px"
allow-clear
@press-enter="handleSearch"
>
<template #prefix>
<SearchOutlined />
</template>
</a-input>
<a-button type="primary" @click="handleSearch">搜索</a-button>
</div>
<!-- 文件列表 -->
<div class="material-content__list">
<a-spin :spinning="loading" tip="加载中..." style="width: 100%; min-height: 400px;">
@@ -113,6 +118,7 @@
<div
v-for="file in fileList"
:key="file.id"
:data-file-id="file.id"
class="material-item"
:class="{ 'material-item--selected': selectedFileIds.includes(file.id) }"
@click="handleFileClick(file)"
@@ -125,6 +131,7 @@
:src="file.coverBase64"
:alt="file.fileName"
@error="handleImageError"
loading="lazy"
/>
<div v-else class="material-item__preview-placeholder">
<FileOutlined />
@@ -133,10 +140,27 @@
<!-- 文件信息 -->
<div class="material-item__info">
<div class="material-item__name" :title="file.fileName">
{{ file.fileName }}
<div
class="material-item__name"
:title="file.displayName"
@click="handleEditName(file)"
>
<template v-if="editingFileId !== file.id">
{{ file.displayName }}
</template>
<template v-else>
<a-input
v-model:value="editingDisplayName"
size="small"
@blur="handleSaveName(file)"
@press-enter="handleSaveName(file)"
@click.stop
autofocus
/>
</template>
</div>
<div class="material-item__meta">
<span class="material-item__type">{{ getFileTypeText(file.fileName) }}</span>
<span class="material-item__size">{{ formatFileSize(file.fileSize) }}</span>
<span class="material-item__time">{{ formatDate(file.createTime) }}</span>
</div>
@@ -154,7 +178,13 @@
</div>
</div>
</template>
<a-empty v-else description="暂无素材" />
<a-empty
v-else
description="暂无素材"
style="padding: 48px 0;"
>
</a-empty>
</a-spin>
</div>
@@ -191,9 +221,6 @@
<a-form-item label="分组名称" name="name" :rules="[{ required: true, message: '请输入分组名称' }]">
<a-input v-model:value="createGroupForm.name" placeholder="请输入分组名称" />
</a-form-item>
<a-form-item label="分组分类">
<a-input v-model:value="createGroupForm.category" disabled />
</a-form-item>
<a-form-item label="分组描述">
<a-textarea v-model:value="createGroupForm.description" placeholder="请输入分组描述可选" />
</a-form-item>
@@ -211,7 +238,7 @@ import {
FolderOutlined,
PlusOutlined
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import { message, Modal } from 'ant-design-vue';
import MaterialUploadModal from '@/components/material/MaterialUploadModal.vue';
import MaterialService, { MaterialGroupService } from '@/api/material';
import { useUpload } from '@/composables/useUpload';
@@ -240,6 +267,10 @@ const createGroupForm = reactive({
// 选中的文件ID列表
const selectedFileIds = ref([]);
// 编辑状态
const editingFileId = ref(null);
const editingDisplayName = ref('');
// 分页信息
const pagination = reactive({
current: 1,
@@ -258,6 +289,10 @@ const handleCategoryChange = async (category) => {
const handleSelectGroup = (group) => {
// 如果点击的是当前已选中的分组,则不执行任何操作
if (selectedGroupId.value === group.id) {
return;
}
selectedGroupId.value = group.id;
loadFileList();
};
@@ -351,26 +386,20 @@ const handlePageSizeChange = (current, size) => {
};
const handleFileClick = (file) => {
// 切换文件选中状态
const isSelected = selectedFileIds.value.includes(file.id);
if (isSelected) {
// 如果已选中,则取消选中
selectedFileIds.value = selectedFileIds.value.filter(id => id !== file.id);
const index = selectedFileIds.value.indexOf(file.id);
if (index > -1) {
selectedFileIds.value.splice(index, 1);
} else {
// 如果未选中,则添加到选中列表
if (!selectedFileIds.value.includes(file.id)) {
selectedFileIds.value.push(file.id);
}
selectedFileIds.value.push(file.id);
}
};
const handleFileSelectChange = (fileId, checked) => {
if (checked) {
if (!selectedFileIds.value.includes(fileId)) {
selectedFileIds.value.push(fileId);
}
} else {
selectedFileIds.value = selectedFileIds.value.filter(id => id !== fileId);
const index = selectedFileIds.value.indexOf(fileId);
if (checked && index === -1) {
selectedFileIds.value.push(fileId);
} else if (!checked && index > -1) {
selectedFileIds.value.splice(index, 1);
}
};
@@ -408,17 +437,74 @@ const handleFileUpload = async (filesWithCover, category, groupId) => {
};
const handleBatchDelete = async () => {
// TODO: 实现批量删除
message.info('批量删除功能待实现');
const handleSelectAll = (event) => {
const checked = event.target.checked;
if (checked) {
// 全选:选中当前页所有文件
selectedFileIds.value = fileList.value.map(file => file.id);
} else {
// 取消全选:清空选中状态
selectedFileIds.value = [];
}
};
const getContentTitle = () => {
if (selectedGroupId.value === null) {
return activeCategory.value === 'MIX' ? '混剪素材' : '数字人素材';
const handleBatchDelete = async () => {
if (selectedFileIds.value.length === 0) {
message.warning('请先选择要删除的文件');
return;
}
// 确认对话框
const count = selectedFileIds.value.length;
const fileIdsToDelete = [...selectedFileIds.value]; // 保存要删除的文件ID
const confirmed = await new Promise((resolve) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除选中的 ${count} 个文件吗?此操作不可恢复。`,
okText: '确认删除',
cancelText: '取消',
okType: 'danger',
centered: true,
class: 'batch-delete-modal',
onOk() {
resolve(true);
},
onCancel() {
resolve(false);
},
});
});
if (!confirmed) {
return;
}
try {
loading.value = true;
await MaterialService.deleteFiles(fileIdsToDelete);
// 从列表中移除已删除的文件
fileList.value = fileList.value.filter(file => !fileIdsToDelete.includes(file.id));
// 更新总数
totalFileCount.value = Math.max(0, totalFileCount.value - count);
// 清空选中状态
selectedFileIds.value = [];
// 如果删除后当前页没有数据了,且不是第一页,则加载上一页
if (fileList.value.length === 0 && pagination.current > 1) {
pagination.current = pagination.current - 1;
await loadFileList();
}
message.success(`成功删除 ${count} 个文件`);
} catch (error) {
console.error('批量删除失败:', error);
message.error('删除失败,请重试');
} finally {
loading.value = false;
}
const group = groupList.value.find(g => g.id === selectedGroupId.value);
return group ? group.name : '素材列表';
};
const formatFileSize = (size) => {
@@ -432,6 +518,60 @@ const formatDate = (date) => {
return new Date(date).toLocaleDateString();
};
const getFileTypeText = (fileName) => {
if (!fileName) return '';
// 提取文件后缀,如 .mp3、.mp4、.avi 等
const ext = fileName.split('.').pop();
return ext ? `${ext.toLowerCase()}` : '';
};
const handleEditName = (file) => {
editingFileId.value = file.id;
editingDisplayName.value = file.displayName || file.fileName;
// 延迟聚焦,确保输入框已渲染
setTimeout(() => {
const input = document.querySelector('.ant-input:focus');
if (input) {
input.focus();
input.select();
}
}, 50);
};
const handleSaveName = async (file) => {
if (!editingDisplayName.value.trim()) {
message.warning('名称不能为空');
editingFileId.value = null;
return;
}
if (editingDisplayName.value === file.displayName) {
editingFileId.value = null;
return;
}
try {
await MaterialService.updateDisplayName(file.id, editingDisplayName.value.trim());
file.displayName = editingDisplayName.value.trim();
message.success('重命名成功');
// 添加成功动画效果
const nameElement = document.querySelector(`[data-file-id="${file.id}"] .material-item__name`);
if (nameElement) {
nameElement.style.transition = 'all 0.3s ease';
nameElement.style.transform = 'scale(1.05)';
setTimeout(() => {
nameElement.style.transform = 'scale(1)';
}, 300);
}
} catch (error) {
console.error('重命名失败:', error);
message.error('重命名失败,请重试');
} finally {
editingFileId.value = null;
}
};
const handleImageError = (e) => {
e.target.style.display = 'none';
};
@@ -568,6 +708,11 @@ onMounted(() => {
&--active {
background: #e6f7ff;
color: #1890ff;
// 移除active状态下的hover效果
&:hover {
background: #e6f7ff;
}
}
&__content {
@@ -606,129 +751,349 @@ onMounted(() => {
flex: 1;
display: flex;
flex-direction: column;
background: #fff;
margin: 16px;
border-radius: 8px;
background: #F8FAFC;
margin: 0px 16px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
&__header {
&__search {
padding: 20px 32px;
border-bottom: 1px solid #E2E8F0;
background: #fff;
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
border-bottom: 1px solid #e8e8e8;
}
&__title {
margin: 0;
font-size: 18px;
font-weight: 500;
gap: 16px;
transition: all 0.2s ease;
}
&__actions {
display: flex;
gap: 8px;
}
align-items: center;
gap: 12px;
margin-left: auto;
&__search {
padding: 16px 24px;
border-bottom: 1px solid #e8e8e8;
display: flex;
gap: 8px;
.ant-checkbox {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border-radius: 6px;
transition: all 0.2s ease;
&:hover {
background: #F1F5F9;
}
.ant-checkbox__inner {
border-radius: 4px;
}
.ant-checkbox-checked {
.ant-checkbox__inner {
background-color: #3B82F6;
border-color: #3B82F6;
}
}
}
.ant-btn {
transition: all 0.2s ease;
font-weight: 500;
border-radius: 6px;
&:hover {
box-shadow: 0 4px 8px rgba(59, 130, 246, 0.2);
}
}
}
&__list {
flex: 1;
padding: 24px;
padding: 32px;
overflow-y: auto;
background: #F8FAFC;
}
&__pagination {
padding: 16px 24px;
border-top: 1px solid #e8e8e8;
padding: 20px 32px;
border-top: 1px solid #E2E8F0;
background: #fff;
text-align: right;
}
}
.material-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 24px;
padding: 4px;
}
.material-item {
position: relative;
border: 1px solid #e8e8e8;
border-radius: 8px;
background: #fff;
border: 1px solid #E2E8F0;
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
&:hover {
border-color: #1890ff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-color: #3B82F6;
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.15);
}
&:active {
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
}
&--selected {
border-color: #1890ff;
background: #e6f7ff;
border-color: #3B82F6;
background: linear-gradient(to bottom, #EFF6FF, #fff);
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.2);
.material-item__select {
opacity: 1;
transform: scale(1);
}
}
&__content {
padding: 12px;
padding: 16px;
transition: all 0.2s ease;
}
&__preview {
width: 100%;
height: 120px;
background: #f5f5f5;
border-radius: 4px;
height: 160px;
background: linear-gradient(135deg, #F1F5F9 0%, #E2E8F0 100%);
border-radius: 8px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
margin-bottom: 12px;
transition: all 0.3s ease;
position: relative;
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
&:hover img {
transform: scale(1.05);
}
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(59, 130, 246, 0);
transition: all 0.3s ease;
pointer-events: none;
}
&:hover::after {
background: rgba(59, 130, 246, 0.05);
}
}
&__preview-placeholder {
font-size: 48px;
color: #d9d9d9;
color: #94A3B8;
transition: all 0.3s ease;
&:hover {
color: #3B82F6;
transform: scale(1.1);
}
}
&__info {
display: flex;
flex-direction: column;
gap: 4px;
gap: 8px;
}
&__name {
font-size: 14px;
font-weight: 500;
font-size: 15px;
font-weight: 600;
color: #1E293B;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.4;
transition: color 0.2s ease;
&:hover {
color: #3B82F6;
}
.ant-input {
font-size: 15px;
font-weight: 600;
height: 32px;
line-height: 30px;
border-radius: 6px;
border-color: #3B82F6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
&:focus {
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15);
}
}
}
&__meta {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #999;
align-items: center;
font-size: 13px;
color: #64748B;
padding-top: 8px;
border-top: 1px solid #F1F5F9;
.material-item__type {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
color: #3B82F6;
background: #EFF6FF;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
.material-item__size {
flex-shrink: 0;
margin-left: 12px;
font-weight: 500;
}
.material-item__time {
flex-shrink: 0;
margin-left: 12px;
color: #94A3B8;
font-size: 12px;
}
}
&__select {
position: absolute;
top: 8px;
right: 8px;
background: rgba(255, 255, 255, 0.9);
border-radius: 4px;
padding: 4px;
top: 12px;
right: 12px;
background: rgba(255, 255, 255, 0.95);
border-radius: 8px;
padding: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
opacity: 0;
transform: scale(0.9);
transition: all 0.2s ease;
backdrop-filter: blur(8px);
.ant-checkbox {
&:hover .ant-checkbox__inner {
border-color: #3B82F6;
}
&-checked .ant-checkbox__inner {
background-color: #3B82F6;
border-color: #3B82F6;
}
}
}
}
// 键盘导航支持
.material-item:focus-within {
outline: 2px solid #3B82F6;
outline-offset: 2px;
}
// 响应式设计
@media (max-width: 768px) {
.material-grid {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 12px;
}
.material-content__search {
flex-direction: column;
align-items: stretch;
gap: 12px;
.material-content__actions {
margin-left: 0;
flex-wrap: wrap;
}
}
.material-item {
&__preview {
height: 120px;
}
}
}
@media (max-width: 480px) {
.material-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 8px;
}
.material-content {
margin: 8px;
border-radius: 8px;
&__search,
&__list,
&__pagination {
padding: 16px;
}
}
}
// 无障碍支持
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
// 批量删除确认弹窗样式
:deep(.batch-delete-modal) {
.ant-modal-body {
padding: 24px;
}
.ant-modal-footer {
text-align: right;
padding: 16px 24px;
border-top: 1px solid #e8e8e8;
.ant-btn {
margin-left: 8px;
&:first-child {
margin-left: 0;
}
}
}
}
</style>