feat: 功能优化

This commit is contained in:
2026-01-18 02:35:41 +08:00
parent 961e4bcc77
commit 85073b7960
3 changed files with 377 additions and 199 deletions

View File

@@ -5,7 +5,7 @@ import { theme } from 'ant-design-vue'
import SvgSprite from '@/components/icons/SvgSprite.vue' import SvgSprite from '@/components/icons/SvgSprite.vue'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import tokenManager from '@gold/utils/token-manager' import tokenManager from '@gold/utils/token-manager'
import zhCN from 'ant-design-vue/es/locale/zh_CN';
const themeToken = ref({ const themeToken = ref({
token: { token: {
colorPrimary: '#3B82F6', colorPrimary: '#3B82F6',
@@ -70,7 +70,7 @@ onMounted(async () => {})
</script> </script>
<template> <template>
<a-config-provider :theme="themeToken"> <a-config-provider :theme="themeToken" :locale="zhCN">
<SvgSprite /> <SvgSprite />
<keep-alive> <keep-alive>
<RouterView /> <RouterView />

View File

@@ -4,6 +4,8 @@ import Antd from 'ant-design-vue'
import 'normalize.css' import 'normalize.css'
import 'ant-design-vue/dist/reset.css' import 'ant-design-vue/dist/reset.css'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import 'dayjs/locale/zh-cn';
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'

View File

@@ -35,9 +35,34 @@
> >
<div class="sidebar-group-item__content"> <div class="sidebar-group-item__content">
<FolderOutlined class="sidebar-group-item__icon" /> <FolderOutlined class="sidebar-group-item__icon" />
<span class="sidebar-group-item__name">{{ group.name }}</span> <template v-if="editingGroupId !== group.id">
<span class="sidebar-group-item__name">{{ group.name }}</span>
</template>
<template v-else>
<a-input
v-model:value="editingGroupName"
size="small"
class="sidebar-group-item__edit-input"
@blur="handleSaveGroupName(group)"
@press-enter="handleSaveGroupName(group)"
@click.stop
autofocus
/>
</template>
</div>
<div class="sidebar-group-item__actions">
<template v-if="editingGroupId !== group.id">
<span class="sidebar-group-item__count">{{ group.fileCount }}</span>
<EditOutlined
class="sidebar-group-item__action-btn"
@click.stop="handleEditGroup(group)"
/>
<DeleteOutlined
class="sidebar-group-item__action-btn sidebar-group-item__action-btn--danger"
@click.stop="handleDeleteGroup(group)"
/>
</template>
</div> </div>
<span class="sidebar-group-item__count">{{ group.fileCount }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -238,187 +263,283 @@ import {
SearchOutlined, SearchOutlined,
FileOutlined, FileOutlined,
FolderOutlined, FolderOutlined,
PlusOutlined PlusOutlined,
EditOutlined,
DeleteOutlined
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { message, Modal } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
import MaterialUploadModal from '@/components/material/MaterialUploadModal.vue'; import MaterialUploadModal from '@/components/material/MaterialUploadModal.vue';
import MaterialService, { MaterialGroupService } from '@/api/material'; import MaterialService, { MaterialGroupService } from '@/api/material';
import { useUpload } from '@/composables/useUpload'; import { useUpload } from '@/composables/useUpload';
// 响应式数据 // 状态管理
const loading = ref(false); const loading = ref(false)
const activeCategory = ref('MIX'); // MIX 或 DIGITAL_HUMAN const activeCategory = ref('MIX')
const selectedGroupId = ref(null); const selectedGroupId = ref(null)
const groupList = ref([]); const groupList = ref([])
const fileList = ref([]); const fileList = ref([])
const totalFileCount = ref(0); const totalFileCount = ref(0)
const searchKeyword = ref(''); const searchKeyword = ref('')
const uploadModalVisible = ref(false); // 模态框状态
const uploadLoading = ref(false); const uploadModalVisible = ref(false)
const createGroupModalVisible = ref(false); const uploadLoading = ref(false)
const createGroupModalVisible = ref(false)
// Upload Hook
const { upload } = useUpload()
// 表单数据
const createGroupForm = reactive({ const createGroupForm = reactive({
name: '', name: '',
description: '', description: '',
category: 'MIX' category: 'MIX'
}); })
// 选中的文件ID列表 // 文件选择
const selectedFileIds = ref([]); const selectedFileIds = ref([])
// 编辑状态 // 编辑状态
const editingFileId = ref(null); const editingFileId = ref(null)
const editingDisplayName = ref(''); const editingDisplayName = ref('')
const nameInputRef = ref(null); const nameInputRef = ref(null)
const editingGroupId = ref(null)
const editingGroupName = ref('')
// 分页信息 // 分页
const pagination = reactive({ const pagination = reactive({
current: 1, current: 1,
pageSize: 20, pageSize: 20,
total: 0 total: 0
}); })
// Hooks
const { upload } = useUpload()
// 方法 // 方法
const handleCategoryChange = async (category) => { const handleCategoryChange = async (category) => {
activeCategory.value = category; activeCategory.value = category
selectedGroupId.value = null; selectedGroupId.value = null
createGroupForm.category = category; createGroupForm.category = category
await loadGroupList(); await loadGroupList()
await loadFileList(); await loadFileList()
}; }
const handleSelectGroup = (group) => { const handleSelectGroup = (group) => {
// 如果点击的是当前已选中的分组,则不执行任何操作 // 编辑中则先保存
if (selectedGroupId.value === group.id) { if (editingGroupId.value) {
return; const currentGroup = groupList.value.find(g => g.id === editingGroupId.value)
if (currentGroup) {
handleSaveGroupName(currentGroup)
}
} }
selectedGroupId.value = group.id;
loadFileList(); // 已是选中状态则不处理
}; if (selectedGroupId.value === group.id) {
return
}
selectedGroupId.value = group.id
loadFileList()
}
const handleEditGroup = async (group, event) => {
event?.stopPropagation?.()
editingGroupId.value = group.id
editingGroupName.value = group.name
await nextTick()
}
const handleSaveGroupName = async (group) => {
const newName = editingGroupName.value.trim()
if (!newName) {
message.warning('分组名称不能为空')
editingGroupId.value = null
return
}
if (newName === group.name) {
editingGroupId.value = null
return
}
try {
await MaterialGroupService.updateGroup({
id: group.id,
name: newName,
description: group.description || '',
sort: group.sort || 0,
icon: group.icon || 'folder',
parentId: group.parentId || 0
})
group.name = newName
message.success('分组重命名成功')
} catch (error) {
console.error('分组重命名失败:', error)
message.error('分组重命名失败,请重试')
} finally {
editingGroupId.value = null
}
}
const handleDeleteGroup = async (group, event) => {
event?.stopPropagation?.()
const confirmed = await new Promise((resolve) => {
Modal.confirm({
title: '删除分组',
content: `确定要删除分组「${group.name}」吗?删除后该分组下的所有文件将被移动到默认分组。此操作不可恢复。`,
okText: '确认删除',
cancelText: '取消',
okType: 'danger',
centered: true,
class: 'delete-group-modal',
onOk() {
resolve(true)
},
onCancel() {
resolve(false)
},
})
})
if (!confirmed) return
try {
loading.value = true
await MaterialGroupService.deleteGroup(group.id)
// 移除已删除的分组
const index = groupList.value.findIndex(g => g.id === group.id)
if (index > -1) {
groupList.value.splice(index, 1)
}
// 重新选择分组
if (selectedGroupId.value === group.id) {
selectedGroupId.value = groupList.value.length > 0 ? groupList.value[0].id : null
await loadFileList()
}
message.success('分组删除成功')
} catch (error) {
console.error('分组删除失败:', error)
message.error('分组删除失败,请重试')
} finally {
loading.value = false
}
}
const handleOpenCreateGroupModal = () => { const handleOpenCreateGroupModal = () => {
createGroupForm.name = ''; createGroupForm.name = ''
createGroupForm.description = ''; createGroupForm.description = ''
createGroupForm.category = activeCategory.value; createGroupForm.category = activeCategory.value
createGroupModalVisible.value = true; createGroupModalVisible.value = true
}; }
const handleCreateGroup = async () => { const handleCreateGroup = async () => {
try { try {
await MaterialGroupService.createGroup(createGroupForm); await MaterialGroupService.createGroup(createGroupForm)
message.success('分组创建成功'); message.success('分组创建成功')
createGroupModalVisible.value = false; createGroupModalVisible.value = false
await loadGroupList(); await loadGroupList()
} catch (error) { } catch (error) {
message.error('分组创建失败'); message.error('分组创建失败')
} }
}; }
const loadGroupList = async () => { const loadGroupList = async () => {
try { try {
const result = await MaterialGroupService.getGroupListByCategory(activeCategory.value); const result = await MaterialGroupService.getGroupListByCategory(activeCategory.value)
groupList.value = result.data || result || []; groupList.value = result.data || result || []
// 默认选中第一个分组
if (groupList.value.length > 0 && selectedGroupId.value === null) {
selectedGroupId.value = groupList.value[0].id;
}
await loadFileList();
} catch (error) { } catch (error) {
console.error('加载分组列表失败:', error); console.error('加载分组列表失败:', error)
// 如果按分类查询失败,尝试加载所有分组
try { try {
const allGroups = await MaterialGroupService.getGroupList(); const allGroups = await MaterialGroupService.getGroupList()
groupList.value = allGroups.data || allGroups || []; groupList.value = allGroups.data || allGroups || []
// 默认选中第一个分组
if (groupList.value.length > 0 && selectedGroupId.value === null) {
selectedGroupId.value = groupList.value[0].id;
}
await loadFileList();
} catch (fallbackError) { } catch (fallbackError) {
console.error('加载所有分组也失败:', fallbackError); console.error('加载所有分组也失败:', fallbackError)
return
} }
} }
};
// 默认选中第一个分组
if (groupList.value.length > 0 && selectedGroupId.value === null) {
selectedGroupId.value = groupList.value[0].id
}
await loadFileList()
}
const loadFileList = async () => { const loadFileList = async () => {
loading.value = true; loading.value = true
try { try {
const params = { const params = {
pageNo: pagination.current, pageNo: pagination.current,
pageSize: pagination.pageSize, pageSize: pagination.pageSize,
fileCategory: 'video', // 只查询视频文件 fileCategory: 'video',
fileName: searchKeyword.value || undefined, fileName: searchKeyword.value || undefined,
groupId: selectedGroupId.value || undefined groupId: selectedGroupId.value || undefined
}; }
const result = await MaterialService.getFilePage(params); const result = await MaterialService.getFilePage(params)
fileList.value = result.data?.list || result.list || []; fileList.value = result.data?.list || result.list || []
pagination.total = result.data?.total || result.total || 0; const total = result.data?.total || result.total || 0
totalFileCount.value = result.data?.total || result.total || 0; pagination.total = total
totalFileCount.value = total
} catch (error) { } catch (error) {
console.error('加载文件列表失败:', error); console.error('加载文件列表失败:', error)
message.error('加载文件列表失败'); message.error('加载文件列表失败')
} finally { } finally {
loading.value = false; loading.value = false
} }
}; }
const handleSearch = () => { const handleSearch = () => {
pagination.current = 1; pagination.current = 1
loadFileList(); loadFileList()
}; }
const handlePageChange = (page, pageSize) => { const handlePageChange = (page, pageSize) => {
pagination.current = page; pagination.current = page
pagination.pageSize = pageSize; pagination.pageSize = pageSize
loadFileList(); loadFileList()
}; }
const handlePageSizeChange = (current, size) => { const handlePageSizeChange = (current, size) => {
pagination.current = 1; pagination.current = 1
pagination.pageSize = size; pagination.pageSize = size
loadFileList(); loadFileList()
}; }
const handleFileClick = (file) => { const handleFileClick = (file) => {
const index = selectedFileIds.value.indexOf(file.id); const index = selectedFileIds.value.indexOf(file.id)
if (index > -1) { if (index > -1) {
selectedFileIds.value.splice(index, 1); selectedFileIds.value.splice(index, 1)
} else { } else {
selectedFileIds.value.push(file.id); selectedFileIds.value.push(file.id)
} }
}; }
const handleFileSelectChange = (fileId, checked) => { const handleFileSelectChange = (fileId, checked) => {
const index = selectedFileIds.value.indexOf(fileId); const index = selectedFileIds.value.indexOf(fileId)
if (checked && index === -1) { if (checked && index === -1) {
selectedFileIds.value.push(fileId); selectedFileIds.value.push(fileId)
} else if (!checked && index > -1) { } else if (!checked && index > -1) {
selectedFileIds.value.splice(index, 1); selectedFileIds.value.splice(index, 1)
} }
}; }
const handleOpenUploadModal = () => { const handleOpenUploadModal = () => {
uploadModalVisible.value = true; uploadModalVisible.value = true
}; }
const handleFileUpload = async (filesWithCover, category, groupId) => { const handleFileUpload = async (filesWithCover, category, groupId) => {
try { try {
uploadLoading.value = true; uploadLoading.value = true
// 上传每个文件
for (const fileWithCover of filesWithCover) { for (const fileWithCover of filesWithCover) {
await upload(fileWithCover.file, { await upload(fileWithCover.file, {
fileCategory: category, fileCategory: category,
groupId, // 素材库模块使用groupId groupId,
coverBase64: fileWithCover.coverBase64, coverBase64: fileWithCover.coverBase64,
onStart: () => {}, onStart: () => {},
onProgress: () => {}, onProgress: () => {},
@@ -431,39 +552,36 @@ const handleFileUpload = async (filesWithCover, category, groupId) => {
}) })
} }
message.success(`成功上传 ${filesWithCover.length} 个文件`); message.success(`成功上传 ${filesWithCover.length} 个文件`)
uploadModalVisible.value = false; uploadModalVisible.value = false
await loadFileList(); await loadFileList()
await loadGroupList(); await loadGroupList()
} catch (error) { } catch (error) {
console.error("文件上传失败:", error); console.error("文件上传失败:", error)
message.error("文件上传失败: " + (error.message || "未知错误")); message.error("文件上传失败: " + (error.message || "未知错误"))
} finally { } finally {
uploadLoading.value = false; uploadLoading.value = false
} }
}; }
const handleSelectAll = (event) => { const handleSelectAll = (event) => {
const checked = event.target.checked; const checked = event.target.checked
if (checked) { if (checked) {
// 全选:选中当前页所有文件 selectedFileIds.value = fileList.value.map(file => file.id)
selectedFileIds.value = fileList.value.map(file => file.id);
} else { } else {
// 取消全选:清空选中状态 selectedFileIds.value = []
selectedFileIds.value = [];
} }
}; }
const handleBatchDelete = async () => { const handleBatchDelete = async () => {
if (selectedFileIds.value.length === 0) { if (selectedFileIds.value.length === 0) {
message.warning('请先选择要删除的文件'); message.warning('请先选择要删除的文件')
return; return
} }
// 确认对话框 const count = selectedFileIds.value.length
const count = selectedFileIds.value.length; const fileIdsToDelete = [...selectedFileIds.value]
const fileIdsToDelete = [...selectedFileIds.value]; // 保存要删除的文件ID
const confirmed = await new Promise((resolve) => { const confirmed = await new Promise((resolve) => {
Modal.confirm({ Modal.confirm({
title: '确认删除', title: '确认删除',
@@ -474,128 +592,124 @@ const handleBatchDelete = async () => {
centered: true, centered: true,
class: 'batch-delete-modal', class: 'batch-delete-modal',
onOk() { onOk() {
resolve(true); resolve(true)
}, },
onCancel() { onCancel() {
resolve(false); resolve(false)
}, },
}); })
}); })
if (!confirmed) { if (!confirmed) return
return;
}
try { try {
loading.value = true; loading.value = true
await MaterialService.deleteFiles(fileIdsToDelete); await MaterialService.deleteFiles(fileIdsToDelete)
// 从列表中移除已删除的文件 // 移除已删除的文件
fileList.value = fileList.value.filter(file => !fileIdsToDelete.includes(file.id)); fileList.value = fileList.value.filter(file => !fileIdsToDelete.includes(file.id))
// 更新总数 // 更新状态
totalFileCount.value = Math.max(0, totalFileCount.value - count); totalFileCount.value = Math.max(0, totalFileCount.value - count)
selectedFileIds.value = []
// 清空选中状态 // 如果删除后当前页没有数据了,则加载上一页
selectedFileIds.value = [];
// 如果删除后当前页没有数据了,且不是第一页,则加载上一页
if (fileList.value.length === 0 && pagination.current > 1) { if (fileList.value.length === 0 && pagination.current > 1) {
pagination.current = pagination.current - 1; pagination.current = pagination.current - 1
await loadFileList(); await loadFileList()
} }
message.success(`成功删除 ${count} 个文件`); message.success(`成功删除 ${count} 个文件`)
} catch (error) { } catch (error) {
console.error('批量删除失败:', error); console.error('批量删除失败:', error)
message.error('删除失败,请重试'); message.error('删除失败,请重试')
} finally { } finally {
loading.value = false; loading.value = false
} }
}; }
const formatFileSize = (size) => { const formatFileSize = (size) => {
if (size < 1024) return size + ' B'; if (size < 1024) return size + ' B'
if (size < 1024 * 1024) return (size / 1024).toFixed(2) + ' KB'; if (size < 1024 * 1024) return (size / 1024).toFixed(2) + ' KB'
if (size < 1024 * 1024 * 1024) return (size / (1024 * 1024)).toFixed(2) + ' MB'; if (size < 1024 * 1024 * 1024) return (size / (1024 * 1024)).toFixed(2) + ' MB'
return (size / (1024 * 1024 * 1024)).toFixed(2) + ' GB'; return (size / (1024 * 1024 * 1024)).toFixed(2) + ' GB'
}; }
const formatDate = (date) => { const formatDate = (date) => {
return new Date(date).toLocaleDateString(); return new Date(date).toLocaleDateString()
}; }
const getFileTypeText = (fileName) => { const getFileTypeText = (fileName) => {
if (!fileName) return ''; if (!fileName) return ''
// 提取文件后缀,如 .mp3、.mp4、.avi 等 const ext = fileName.split('.').pop()
const ext = fileName.split('.').pop(); return ext ? `${ext.toLowerCase()}` : ''
return ext ? `${ext.toLowerCase()}` : ''; }
};
const handleEditName = async (file) => { const handleEditName = async (file) => {
editingFileId.value = file.id; editingFileId.value = file.id
editingDisplayName.value = file.displayName || file.fileName; editingDisplayName.value = file.displayName || file.fileName
// 使用 nextTick 确保 DOM 更新后再聚焦 await nextTick()
await nextTick();
// 查找当前编辑文件的输入框 // 查找当前编辑文件的输入框
const nameElement = document.querySelector(`[data-file-id="${file.id}"] .material-item__name`); const nameElement = document.querySelector(`[data-file-id="${file.id}"] .material-item__name`)
if (nameElement) { if (nameElement) {
const input = nameElement.querySelector('input'); const input = nameElement.querySelector('input')
if (input) { if (input) {
input.focus(); input.focus()
input.select(); input.select()
} }
} }
}; }
const handleSaveName = async (file) => { const handleSaveName = async (file) => {
if (!editingDisplayName.value.trim()) { const newName = editingDisplayName.value.trim()
message.warning('名称不能为空');
editingFileId.value = null; if (!newName) {
return; message.warning('名称不能为空')
editingFileId.value = null
return
} }
if (editingDisplayName.value === file.displayName) { if (newName === file.displayName) {
editingFileId.value = null; editingFileId.value = null
return; return
} }
try { try {
await MaterialService.updateDisplayName(file.id, editingDisplayName.value.trim()); await MaterialService.updateDisplayName(file.id, newName)
file.displayName = editingDisplayName.value.trim(); file.displayName = newName
message.success('重命名成功'); message.success('重命名成功')
// 添加成功动画效果 // 成功动画效果
const nameElement = document.querySelector(`[data-file-id="${file.id}"] .material-item__name`); const nameElement = document.querySelector(`[data-file-id="${file.id}"] .material-item__name`)
if (nameElement) { if (nameElement) {
nameElement.style.transition = 'all 0.3s ease'; nameElement.style.transition = 'all 0.3s ease'
nameElement.style.transform = 'scale(1.05)'; nameElement.style.transform = 'scale(1.05)'
setTimeout(() => { setTimeout(() => {
nameElement.style.transform = 'scale(1)'; nameElement.style.transform = 'scale(1)'
}, 300); }, 300)
} }
} catch (error) { } catch (error) {
console.error('重命名失败:', error); console.error('重命名失败:', error)
message.error('重命名失败,请重试'); message.error('重命名失败,请重试')
} finally { } finally {
editingFileId.value = null; editingFileId.value = null
} }
}; }
const handleImageError = (e) => { const handleImageError = (e) => {
e.target.style.display = 'none'; e.target.style.display = 'none'
}; }
// 监听分类变化 // 监听分类变化
watch(activeCategory, () => { watch(activeCategory, () => {
selectedFileIds.value = []; selectedFileIds.value = []
}); })
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
loadGroupList(); loadGroupList()
}); })
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
@@ -714,6 +828,11 @@ onMounted(() => {
&:hover { &:hover {
background: #f5f5f5; background: #f5f5f5;
.sidebar-group-item__action-btn {
opacity: 1;
transform: translateX(0);
}
} }
&--active { &--active {
@@ -741,10 +860,46 @@ onMounted(() => {
white-space: nowrap; white-space: nowrap;
} }
&__actions {
display: flex;
align-items: center;
gap: 4px;
}
&__count { &__count {
font-size: 12px; font-size: 12px;
color: #999; color: #999;
margin-left: 8px; }
&__action-btn {
opacity: 0;
transform: translateX(4px);
transition: all 0.2s ease;
padding: 4px;
cursor: pointer;
color: #94a3b8;
font-size: 12px;
&:hover {
color: #1890ff;
}
&--danger:hover {
color: #ff4d4f;
}
}
&__edit-input {
.ant-input {
height: 24px;
font-size: 13px;
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
&:focus {
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
} }
} }
@@ -1056,4 +1211,25 @@ onMounted(() => {
} }
} }
} }
// 删除分组确认弹窗样式
:deep(.delete-group-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> </style>