feat: 功能优化

This commit is contained in:
2026-01-17 14:43:42 +08:00
parent 5ed0cfff07
commit fecd47e25d
60 changed files with 3529 additions and 827 deletions

View File

@@ -0,0 +1,736 @@
<template>
<div class="material-list-container">
<!-- 左侧边栏 -->
<div class="material-sidebar">
<!-- 分类标签 -->
<div class="category-tabs">
<div
class="category-tab"
:class="{ 'category-tab--active': activeCategory === 'MIX' }"
@click="handleCategoryChange('MIX')"
>
混剪素材
</div>
<div
class="category-tab"
:class="{ 'category-tab--active': activeCategory === 'DIGITAL_HUMAN' }"
@click="handleCategoryChange('DIGITAL_HUMAN')"
>
数字人素材
</div>
</div>
<!-- 分组列表 -->
<div class="sidebar-section">
<div class="sidebar-section__header">
<span>分组列表</span>
</div>
<div class="sidebar-group-list">
<div
v-for="group in groupList"
:key="group.id"
class="sidebar-group-item"
:class="{ 'sidebar-group-item--active': selectedGroupId === group.id }"
@click="handleSelectGroup(group)"
>
<div class="sidebar-group-item__content">
<FolderOutlined class="sidebar-group-item__icon" />
<span class="sidebar-group-item__name">{{ group.name }}</span>
</div>
<span class="sidebar-group-item__count">{{ group.fileCount }}</span>
</div>
</div>
</div>
<!-- 新建分组按钮 -->
<div class="sidebar-actions">
<a-button
type="primary"
block
@click="handleOpenCreateGroupModal"
>
<template #icon>
<PlusOutlined />
</template>
新建分组
</a-button>
</div>
</div>
<!-- 右侧内容区域 -->
<div class="material-content">
<!-- 头部操作栏 -->
<div class="material-content__header">
<h2 class="material-content__title">
{{ getContentTitle() }}
</h2>
<div class="material-content__actions">
<a-button type="primary" @click="handleOpenUploadModal">
<template #icon>
<UploadOutlined />
</template>
上传素材
</a-button>
<a-button
v-if="selectedFileIds.length > 0"
type="primary"
status="danger"
@click="handleBatchDelete"
>
批量删除 ({{ selectedFileIds.length }})
</a-button>
<a-button
type="primary"
ghost
@click="$router.push('/material/mix')"
>
素材混剪
</a-button>
</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;">
<template v-if="fileList.length > 0">
<div class="material-grid">
<div
v-for="file in fileList"
:key="file.id"
class="material-item"
:class="{ 'material-item--selected': selectedFileIds.includes(file.id) }"
@click="handleFileClick(file)"
>
<div class="material-item__content">
<!-- 预览图 -->
<div class="material-item__preview">
<img
v-if="file.coverBase64"
:src="file.coverBase64"
:alt="file.fileName"
@error="handleImageError"
/>
<div v-else class="material-item__preview-placeholder">
<FileOutlined />
</div>
</div>
<!-- 文件信息 -->
<div class="material-item__info">
<div class="material-item__name" :title="file.fileName">
{{ file.fileName }}
</div>
<div class="material-item__meta">
<span class="material-item__size">{{ formatFileSize(file.fileSize) }}</span>
<span class="material-item__time">{{ formatDate(file.createTime) }}</span>
</div>
</div>
</div>
<!-- 选择框 -->
<div class="material-item__select">
<a-checkbox
:checked="selectedFileIds.includes(file.id)"
@change="(checked) => handleFileSelectChange(file.id, checked)"
@click.stop
/>
</div>
</div>
</div>
</template>
<a-empty v-else description="暂无素材" />
</a-spin>
</div>
<!-- 分页 -->
<div class="material-content__pagination">
<a-pagination
v-model:current="pagination.current"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:show-size-changer="true"
:show-quick-jumper="true"
:show-total="(total, range) => `${range[0]}-${range[1]} 条,共 ${total}`"
@change="handlePageChange"
@show-size-change="handlePageSizeChange"
/>
</div>
</div>
<!-- 上传素材弹窗 -->
<MaterialUploadModal
v-model:visible="uploadModalVisible"
:group-id="selectedGroupId"
@confirm="handleFileUpload"
/>
<!-- 新建分组弹窗 -->
<a-modal
v-model:visible="createGroupModalVisible"
title="新建分组"
@ok="handleCreateGroup"
@cancel="createGroupModalVisible = false"
>
<a-form :model="createGroupForm" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<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>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, watch } from 'vue';
import {
UploadOutlined,
SearchOutlined,
FileOutlined,
FolderOutlined,
PlusOutlined
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import MaterialUploadModal from '@/components/material/MaterialUploadModal.vue';
import MaterialService, { MaterialGroupService } from '@/api/material';
import { useUpload } from '@/composables/useUpload';
// 响应式数据
const loading = ref(false);
const activeCategory = ref('MIX'); // MIX 或 DIGITAL_HUMAN
const selectedGroupId = ref(null);
const groupList = ref([]);
const fileList = ref([]);
const totalFileCount = ref(0);
const searchKeyword = ref('');
const uploadModalVisible = ref(false);
const createGroupModalVisible = ref(false);
// Upload Hook
const { upload } = useUpload()
const createGroupForm = reactive({
name: '',
description: '',
category: 'MIX'
});
// 选中的文件ID列表
const selectedFileIds = ref([]);
// 分页信息
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0
});
// 方法
const handleCategoryChange = async (category) => {
activeCategory.value = category;
selectedGroupId.value = null;
createGroupForm.category = category;
await loadGroupList();
await loadFileList();
};
const handleSelectGroup = (group) => {
selectedGroupId.value = group.id;
loadFileList();
};
const handleOpenCreateGroupModal = () => {
createGroupForm.name = '';
createGroupForm.description = '';
createGroupForm.category = activeCategory.value;
createGroupModalVisible.value = true;
};
const handleCreateGroup = async () => {
try {
await MaterialGroupService.createGroup(createGroupForm);
message.success('分组创建成功');
createGroupModalVisible.value = false;
await loadGroupList();
} catch (error) {
message.error('分组创建失败');
}
};
const loadGroupList = async () => {
try {
const result = await MaterialGroupService.getGroupListByCategory(activeCategory.value);
groupList.value = result.data || result || [];
// 默认选中第一个分组
if (groupList.value.length > 0 && selectedGroupId.value === null) {
selectedGroupId.value = groupList.value[0].id;
}
await loadFileList();
} catch (error) {
console.error('加载分组列表失败:', error);
// 如果按分类查询失败,尝试加载所有分组
try {
const allGroups = await MaterialGroupService.getGroupList();
groupList.value = allGroups.data || allGroups || [];
// 默认选中第一个分组
if (groupList.value.length > 0 && selectedGroupId.value === null) {
selectedGroupId.value = groupList.value[0].id;
}
await loadFileList();
} catch (fallbackError) {
console.error('加载所有分组也失败:', fallbackError);
}
}
};
const loadFileList = async () => {
loading.value = true;
try {
const params = {
pageNo: pagination.current,
pageSize: pagination.pageSize,
fileCategory: 'video', // 只查询视频文件
fileName: searchKeyword.value || undefined,
groupId: selectedGroupId.value || undefined
};
const result = await MaterialService.getFilePage(params);
fileList.value = result.data?.list || result.list || [];
pagination.total = result.data?.total || result.total || 0;
totalFileCount.value = result.data?.total || result.total || 0;
} catch (error) {
console.error('加载文件列表失败:', error);
message.error('加载文件列表失败');
} finally {
loading.value = false;
}
};
const handleSearch = () => {
pagination.current = 1;
loadFileList();
};
const handlePageChange = (page, pageSize) => {
pagination.current = page;
pagination.pageSize = pageSize;
loadFileList();
};
const handlePageSizeChange = (current, size) => {
pagination.current = 1;
pagination.pageSize = size;
loadFileList();
};
const handleFileClick = (file) => {
// 切换文件选中状态
const isSelected = selectedFileIds.value.includes(file.id);
if (isSelected) {
// 如果已选中,则取消选中
selectedFileIds.value = selectedFileIds.value.filter(id => id !== file.id);
} else {
// 如果未选中,则添加到选中列表
if (!selectedFileIds.value.includes(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 handleOpenUploadModal = () => {
uploadModalVisible.value = true;
};
const handleFileUpload = async (filesWithCover, category, groupId) => {
try {
// 上传每个文件
for (const fileWithCover of filesWithCover) {
await upload(fileWithCover.file, {
fileCategory: category,
groupId, // 素材库模块使用groupId
coverBase64: fileWithCover.coverBase64,
onStart: () => {},
onProgress: () => {},
onSuccess: (id) => {
console.log('文件上传成功:', id)
},
onError: (error) => {
message.error(error.message || '上传失败')
}
})
}
message.success(`成功上传 ${filesWithCover.length} 个文件`);
uploadModalVisible.value = false;
await loadFileList();
await loadGroupList();
} catch (error) {
console.error("文件上传失败:", error);
message.error("文件上传失败: " + (error.message || "未知错误"));
}
};
const handleBatchDelete = async () => {
// TODO: 实现批量删除
message.info('批量删除功能待实现');
};
const getContentTitle = () => {
if (selectedGroupId.value === null) {
return activeCategory.value === 'MIX' ? '混剪素材' : '数字人素材';
}
const group = groupList.value.find(g => g.id === selectedGroupId.value);
return group ? group.name : '素材列表';
};
const formatFileSize = (size) => {
if (size < 1024) return size + ' B';
if (size < 1024 * 1024) return (size / 1024).toFixed(2) + ' KB';
if (size < 1024 * 1024 * 1024) return (size / (1024 * 1024)).toFixed(2) + ' MB';
return (size / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
};
const formatDate = (date) => {
return new Date(date).toLocaleDateString();
};
const handleImageError = (e) => {
e.target.style.display = 'none';
};
// 监听分类变化
watch(activeCategory, () => {
selectedFileIds.value = [];
});
// 初始化
onMounted(() => {
loadGroupList();
});
</script>
<style scoped lang="less">
.material-list-container {
display: flex;
height: 100%;
}
.material-sidebar {
width: 256px;
background: #fff;
border-right: 1px solid #e8e8e8;
display: flex;
flex-direction: column;
padding: 16px;
overflow-y: auto;
}
.category-tabs {
display: flex;
background: #f5f5f5;
border-radius: 8px;
padding: 3px;
margin-bottom: 16px;
border: 1px solid #e8e8e8;
}
.category-tab {
flex: 1;
padding: 8px 16px;
text-align: center;
cursor: pointer;
background: transparent;
border-radius: 6px;
font-weight: 400;
font-size: 13px;
color: #666;
border: 1px solid transparent;
&:hover {
color: #1890ff;
background: rgba(24, 144, 255, 0.05);
}
&--active {
background: #fff;
color: #1890ff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid #d9d9d9;
}
}
.sidebar-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
cursor: pointer;
border-radius: 6px;
margin-bottom: 4px;
transition: all 0.3s;
&:hover {
background: #f5f5f5;
}
&--active {
background: #e6f7ff;
color: #1890ff;
}
&__content {
display: flex;
align-items: center;
}
&__icon {
margin-right: 8px;
font-size: 16px;
}
&__name {
font-size: 14px;
}
&__count {
font-size: 12px;
color: #999;
}
}
.sidebar-section {
margin-top: 16px;
&__header {
padding: 8px 12px;
font-weight: 500;
color: #666;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
}
.sidebar-group-list {
max-height: 300px;
overflow-y: auto;
}
.sidebar-group-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
cursor: pointer;
border-radius: 6px;
margin-bottom: 2px;
transition: all 0.3s;
&:hover {
background: #f5f5f5;
}
&--active {
background: #e6f7ff;
color: #1890ff;
}
&__content {
display: flex;
align-items: center;
min-width: 0;
flex: 1;
}
&__icon {
margin-right: 8px;
font-size: 14px;
flex-shrink: 0;
}
&__name {
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__count {
font-size: 12px;
color: #999;
margin-left: 8px;
}
}
.sidebar-actions {
margin-top: auto;
padding-top: 16px;
}
.material-content {
flex: 1;
display: flex;
flex-direction: column;
background: #fff;
margin: 16px;
border-radius: 8px;
overflow: hidden;
&__header {
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;
}
&__actions {
display: flex;
gap: 8px;
}
&__search {
padding: 16px 24px;
border-bottom: 1px solid #e8e8e8;
display: flex;
gap: 8px;
}
&__list {
flex: 1;
padding: 24px;
overflow-y: auto;
}
&__pagination {
padding: 16px 24px;
border-top: 1px solid #e8e8e8;
text-align: right;
}
}
.material-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
.material-item {
position: relative;
border: 1px solid #e8e8e8;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #1890ff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
&--selected {
border-color: #1890ff;
background: #e6f7ff;
}
&__content {
padding: 12px;
}
&__preview {
width: 100%;
height: 120px;
background: #f5f5f5;
border-radius: 4px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
&__preview-placeholder {
font-size: 48px;
color: #d9d9d9;
}
&__info {
display: flex;
flex-direction: column;
gap: 4px;
}
&__name {
font-size: 14px;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__meta {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #999;
}
&__select {
position: absolute;
top: 8px;
right: 8px;
background: rgba(255, 255, 255, 0.9);
border-radius: 4px;
padding: 4px;
}
}
</style>