2026-01-17 14:43:42 +08:00
|
|
|
<template>
|
2026-02-11 00:39:16 +08:00
|
|
|
<FullWidthLayout :show-padding="false" class="material-list-layout">
|
|
|
|
|
<div class="material-list-container">
|
2026-01-17 14:43:42 +08:00
|
|
|
<!-- 左侧边栏 -->
|
|
|
|
|
<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" />
|
2026-01-18 02:35:41 +08:00
|
|
|
<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>
|
2026-01-17 14:43:42 +08:00
|
|
|
</div>
|
|
|
|
|
</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">
|
2026-01-18 00:34:04 +08:00
|
|
|
<!-- 搜索栏 -->
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
<!-- 操作按钮 -->
|
2026-01-17 14:43:42 +08:00
|
|
|
<div class="material-content__actions">
|
2026-01-18 00:34:04 +08:00
|
|
|
<!-- 全选/取消全选 -->
|
|
|
|
|
<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>
|
|
|
|
|
|
2026-01-17 14:43:42 +08:00
|
|
|
<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__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"
|
2026-01-18 00:34:04 +08:00
|
|
|
:data-file-id="file.id"
|
2026-01-17 14:43:42 +08:00
|
|
|
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"
|
2026-01-18 00:34:04 +08:00
|
|
|
loading="lazy"
|
2026-01-17 14:43:42 +08:00
|
|
|
/>
|
|
|
|
|
<div v-else class="material-item__preview-placeholder">
|
|
|
|
|
<FileOutlined />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 文件信息 -->
|
|
|
|
|
<div class="material-item__info">
|
2026-01-18 00:34:04 +08:00
|
|
|
<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
|
2026-01-18 02:15:08 +08:00
|
|
|
ref="nameInputRef"
|
2026-01-18 00:34:04 +08:00
|
|
|
v-model:value="editingDisplayName"
|
|
|
|
|
size="small"
|
|
|
|
|
@blur="handleSaveName(file)"
|
|
|
|
|
@press-enter="handleSaveName(file)"
|
|
|
|
|
@click.stop
|
|
|
|
|
autofocus
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
2026-01-17 14:43:42 +08:00
|
|
|
</div>
|
|
|
|
|
<div class="material-item__meta">
|
2026-01-18 00:34:04 +08:00
|
|
|
<span class="material-item__type">{{ getFileTypeText(file.fileName) }}</span>
|
2026-01-17 14:43:42 +08:00
|
|
|
<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>
|
2026-01-18 00:34:04 +08:00
|
|
|
<a-empty
|
|
|
|
|
v-else
|
|
|
|
|
description="暂无素材"
|
|
|
|
|
style="padding: 48px 0;"
|
|
|
|
|
>
|
|
|
|
|
|
|
|
|
|
</a-empty>
|
2026-01-17 14:43:42 +08:00
|
|
|
</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"
|
2026-01-18 02:15:08 +08:00
|
|
|
:uploading="uploadLoading"
|
2026-01-17 14:43:42 +08:00
|
|
|
@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-textarea v-model:value="createGroupForm.description" placeholder="请输入分组描述(可选)" />
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-form>
|
|
|
|
|
</a-modal>
|
|
|
|
|
</div>
|
2026-02-11 00:39:16 +08:00
|
|
|
</FullWidthLayout>
|
2026-01-17 14:43:42 +08:00
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-01-18 02:15:08 +08:00
|
|
|
import { ref, reactive, onMounted, watch, nextTick } from 'vue';
|
2026-01-17 14:43:42 +08:00
|
|
|
import {
|
|
|
|
|
UploadOutlined,
|
|
|
|
|
SearchOutlined,
|
|
|
|
|
FileOutlined,
|
|
|
|
|
FolderOutlined,
|
2026-01-18 02:35:41 +08:00
|
|
|
PlusOutlined,
|
|
|
|
|
EditOutlined,
|
|
|
|
|
DeleteOutlined
|
2026-01-17 14:43:42 +08:00
|
|
|
} from '@ant-design/icons-vue';
|
2026-01-18 00:34:04 +08:00
|
|
|
import { message, Modal } from 'ant-design-vue';
|
2026-01-17 14:43:42 +08:00
|
|
|
import MaterialUploadModal from '@/components/material/MaterialUploadModal.vue';
|
|
|
|
|
import MaterialService, { MaterialGroupService } from '@/api/material';
|
|
|
|
|
import { useUpload } from '@/composables/useUpload';
|
2026-02-11 00:39:16 +08:00
|
|
|
import FullWidthLayout from '@/layouts/components/FullWidthLayout.vue';
|
2026-01-17 14:43:42 +08:00
|
|
|
|
2026-01-18 02:35:41 +08:00
|
|
|
// 状态管理
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
const activeCategory = ref('MIX')
|
|
|
|
|
const selectedGroupId = ref(null)
|
|
|
|
|
const groupList = ref([])
|
|
|
|
|
const fileList = ref([])
|
|
|
|
|
const totalFileCount = ref(0)
|
|
|
|
|
const searchKeyword = ref('')
|
|
|
|
|
|
|
|
|
|
// 模态框状态
|
|
|
|
|
const uploadModalVisible = ref(false)
|
|
|
|
|
const uploadLoading = ref(false)
|
|
|
|
|
const createGroupModalVisible = ref(false)
|
|
|
|
|
|
|
|
|
|
// 表单数据
|
2026-01-17 14:43:42 +08:00
|
|
|
const createGroupForm = reactive({
|
|
|
|
|
name: '',
|
|
|
|
|
description: '',
|
|
|
|
|
category: 'MIX'
|
2026-01-18 02:35:41 +08:00
|
|
|
})
|
2026-01-17 14:43:42 +08:00
|
|
|
|
2026-01-18 02:35:41 +08:00
|
|
|
// 文件选择
|
|
|
|
|
const selectedFileIds = ref([])
|
2026-01-17 14:43:42 +08:00
|
|
|
|
2026-01-18 00:34:04 +08:00
|
|
|
// 编辑状态
|
2026-01-18 02:35:41 +08:00
|
|
|
const editingFileId = ref(null)
|
|
|
|
|
const editingDisplayName = ref('')
|
|
|
|
|
const nameInputRef = ref(null)
|
|
|
|
|
const editingGroupId = ref(null)
|
|
|
|
|
const editingGroupName = ref('')
|
2026-01-18 00:34:04 +08:00
|
|
|
|
2026-01-18 02:35:41 +08:00
|
|
|
// 分页
|
2026-01-17 14:43:42 +08:00
|
|
|
const pagination = reactive({
|
|
|
|
|
current: 1,
|
|
|
|
|
pageSize: 20,
|
|
|
|
|
total: 0
|
2026-01-18 02:35:41 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Hooks
|
|
|
|
|
const { upload } = useUpload()
|
2026-01-17 14:43:42 +08:00
|
|
|
|
|
|
|
|
// 方法
|
|
|
|
|
const handleCategoryChange = async (category) => {
|
2026-01-18 02:35:41 +08:00
|
|
|
activeCategory.value = category
|
|
|
|
|
selectedGroupId.value = null
|
|
|
|
|
createGroupForm.category = category
|
|
|
|
|
await loadGroupList()
|
|
|
|
|
await loadFileList()
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
|
|
|
|
|
const handleSelectGroup = (group) => {
|
2026-01-18 02:35:41 +08:00
|
|
|
// 编辑中则先保存
|
|
|
|
|
if (editingGroupId.value) {
|
|
|
|
|
const currentGroup = groupList.value.find(g => g.id === editingGroupId.value)
|
|
|
|
|
if (currentGroup) {
|
|
|
|
|
handleSaveGroupName(currentGroup)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 已是选中状态则不处理
|
2026-01-18 00:34:04 +08:00
|
|
|
if (selectedGroupId.value === group.id) {
|
2026-01-18 02:35:41 +08:00
|
|
|
return
|
2026-01-18 00:34:04 +08:00
|
|
|
}
|
2026-01-18 02:35:41 +08:00
|
|
|
selectedGroupId.value = group.id
|
|
|
|
|
loadFileList()
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
|
2026-01-18 02:35:41 +08:00
|
|
|
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
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
|
|
|
|
|
try {
|
2026-01-18 02:35:41 +08:00
|
|
|
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('分组重命名成功')
|
2026-01-17 14:43:42 +08:00
|
|
|
} catch (error) {
|
2026-01-18 02:35:41 +08:00
|
|
|
console.error('分组重命名失败:', error)
|
|
|
|
|
message.error('分组重命名失败,请重试')
|
|
|
|
|
} finally {
|
|
|
|
|
editingGroupId.value = null
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
2026-01-18 02:35:41 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2026-01-17 14:43:42 +08:00
|
|
|
|
|
|
|
|
try {
|
2026-01-18 02:35:41 +08:00
|
|
|
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)
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
|
2026-01-18 02:35:41 +08:00
|
|
|
// 重新选择分组
|
|
|
|
|
if (selectedGroupId.value === group.id) {
|
|
|
|
|
selectedGroupId.value = groupList.value.length > 0 ? groupList.value[0].id : null
|
|
|
|
|
await loadFileList()
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:35:41 +08:00
|
|
|
message.success('分组删除成功')
|
2026-01-17 14:43:42 +08:00
|
|
|
} catch (error) {
|
2026-01-18 02:35:41 +08:00
|
|
|
console.error('分组删除失败:', error)
|
|
|
|
|
message.error('分组删除失败,请重试')
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
|
2026-01-18 02:35:41 +08:00
|
|
|
const handleOpenCreateGroupModal = () => {
|
|
|
|
|
createGroupForm.name = ''
|
|
|
|
|
createGroupForm.description = ''
|
|
|
|
|
createGroupForm.category = activeCategory.value
|
|
|
|
|
createGroupModalVisible.value = true
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
|
2026-01-18 02:35:41 +08:00
|
|
|
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 || []
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('加载分组列表失败:', error)
|
|
|
|
|
try {
|
|
|
|
|
const allGroups = await MaterialGroupService.getGroupList()
|
|
|
|
|
groupList.value = allGroups.data || allGroups || []
|
2026-01-17 14:43:42 +08:00
|
|
|
} catch (fallbackError) {
|
2026-01-18 02:35:41 +08:00
|
|
|
console.error('加载所有分组也失败:', fallbackError)
|
|
|
|
|
return
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-18 02:35:41 +08:00
|
|
|
|
|
|
|
|
// 默认选中第一个分组
|
|
|
|
|
if (groupList.value.length > 0 && selectedGroupId.value === null) {
|
|
|
|
|
selectedGroupId.value = groupList.value[0].id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await loadFileList()
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
|
|
|
|
|
const loadFileList = async () => {
|
2026-01-18 02:35:41 +08:00
|
|
|
loading.value = true
|
2026-01-17 14:43:42 +08:00
|
|
|
try {
|
|
|
|
|
const params = {
|
|
|
|
|
pageNo: pagination.current,
|
|
|
|
|
pageSize: pagination.pageSize,
|
2026-01-18 02:35:41 +08:00
|
|
|
fileCategory: 'video',
|
2026-01-17 14:43:42 +08:00
|
|
|
fileName: searchKeyword.value || undefined,
|
|
|
|
|
groupId: selectedGroupId.value || undefined
|
2026-01-18 02:35:41 +08:00
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
|
2026-01-18 02:35:41 +08:00
|
|
|
const result = await MaterialService.getFilePage(params)
|
|
|
|
|
fileList.value = result.data?.list || result.list || []
|
|
|
|
|
const total = result.data?.total || result.total || 0
|
|
|
|
|
pagination.total = total
|
|
|
|
|
totalFileCount.value = total
|
2026-01-17 14:43:42 +08:00
|
|
|
} catch (error) {
|
2026-01-18 02:35:41 +08:00
|
|
|
console.error('加载文件列表失败:', error)
|
|
|
|
|
message.error('加载文件列表失败')
|
2026-01-17 14:43:42 +08:00
|
|
|
} finally {
|
2026-01-18 02:35:41 +08:00
|
|
|
loading.value = false
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
2026-01-18 02:35:41 +08:00
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
|
|
|
|
|
const handleSearch = () => {
|
2026-01-18 02:35:41 +08:00
|
|
|
pagination.current = 1
|
|
|
|
|
loadFileList()
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
|
|
|
|
|
const handlePageChange = (page, pageSize) => {
|
2026-01-18 02:35:41 +08:00
|
|
|
pagination.current = page
|
|
|
|
|
pagination.pageSize = pageSize
|
|
|
|
|
loadFileList()
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
|
|
|
|
|
const handlePageSizeChange = (current, size) => {
|
2026-01-18 02:35:41 +08:00
|
|
|
pagination.current = 1
|
|
|
|
|
pagination.pageSize = size
|
|
|
|
|
loadFileList()
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
|
|
|
|
|
const handleFileClick = (file) => {
|
2026-01-18 02:35:41 +08:00
|
|
|
const index = selectedFileIds.value.indexOf(file.id)
|
2026-01-18 00:34:04 +08:00
|
|
|
if (index > -1) {
|
2026-01-18 02:35:41 +08:00
|
|
|
selectedFileIds.value.splice(index, 1)
|
2026-01-17 14:43:42 +08:00
|
|
|
} else {
|
2026-01-18 02:35:41 +08:00
|
|
|
selectedFileIds.value.push(file.id)
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
2026-01-18 02:35:41 +08:00
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
|
|
|
|
|
const handleFileSelectChange = (fileId, checked) => {
|
2026-01-18 02:35:41 +08:00
|
|
|
const index = selectedFileIds.value.indexOf(fileId)
|
2026-01-18 00:34:04 +08:00
|
|
|
if (checked && index === -1) {
|
2026-01-18 02:35:41 +08:00
|
|
|
selectedFileIds.value.push(fileId)
|
2026-01-18 00:34:04 +08:00
|
|
|
} else if (!checked && index > -1) {
|
2026-01-18 02:35:41 +08:00
|
|
|
selectedFileIds.value.splice(index, 1)
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
2026-01-18 02:35:41 +08:00
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
|
|
|
|
|
const handleOpenUploadModal = () => {
|
2026-01-18 02:35:41 +08:00
|
|
|
uploadModalVisible.value = true
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
|
|
|
|
|
const handleFileUpload = async (filesWithCover, category, groupId) => {
|
|
|
|
|
try {
|
2026-01-18 02:35:41 +08:00
|
|
|
uploadLoading.value = true
|
|
|
|
|
|
2026-01-17 14:43:42 +08:00
|
|
|
for (const fileWithCover of filesWithCover) {
|
|
|
|
|
await upload(fileWithCover.file, {
|
|
|
|
|
fileCategory: category,
|
2026-01-18 02:35:41 +08:00
|
|
|
groupId,
|
2026-01-17 14:43:42 +08:00
|
|
|
coverBase64: fileWithCover.coverBase64,
|
|
|
|
|
onStart: () => {},
|
|
|
|
|
onProgress: () => {},
|
|
|
|
|
onSuccess: (id) => {
|
|
|
|
|
console.log('文件上传成功:', id)
|
|
|
|
|
},
|
|
|
|
|
onError: (error) => {
|
|
|
|
|
message.error(error.message || '上传失败')
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:35:41 +08:00
|
|
|
message.success(`成功上传 ${filesWithCover.length} 个文件`)
|
|
|
|
|
uploadModalVisible.value = false
|
|
|
|
|
await loadFileList()
|
|
|
|
|
await loadGroupList()
|
2026-01-17 14:43:42 +08:00
|
|
|
} catch (error) {
|
2026-01-18 02:35:41 +08:00
|
|
|
console.error("文件上传失败:", error)
|
|
|
|
|
message.error("文件上传失败: " + (error.message || "未知错误"))
|
2026-01-18 02:15:08 +08:00
|
|
|
} finally {
|
2026-01-18 02:35:41 +08:00
|
|
|
uploadLoading.value = false
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
2026-01-18 02:35:41 +08:00
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
|
|
|
|
|
|
2026-01-18 00:34:04 +08:00
|
|
|
const handleSelectAll = (event) => {
|
2026-01-18 02:35:41 +08:00
|
|
|
const checked = event.target.checked
|
2026-01-18 00:34:04 +08:00
|
|
|
if (checked) {
|
2026-01-18 02:35:41 +08:00
|
|
|
selectedFileIds.value = fileList.value.map(file => file.id)
|
2026-01-18 00:34:04 +08:00
|
|
|
} else {
|
2026-01-18 02:35:41 +08:00
|
|
|
selectedFileIds.value = []
|
2026-01-18 00:34:04 +08:00
|
|
|
}
|
2026-01-18 02:35:41 +08:00
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
|
2026-01-18 00:34:04 +08:00
|
|
|
const handleBatchDelete = async () => {
|
|
|
|
|
if (selectedFileIds.value.length === 0) {
|
2026-01-18 02:35:41 +08:00
|
|
|
message.warning('请先选择要删除的文件')
|
|
|
|
|
return
|
2026-01-18 00:34:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:35:41 +08:00
|
|
|
const count = selectedFileIds.value.length
|
|
|
|
|
const fileIdsToDelete = [...selectedFileIds.value]
|
2026-01-18 00:34:04 +08:00
|
|
|
const confirmed = await new Promise((resolve) => {
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
title: '确认删除',
|
|
|
|
|
content: `确定要删除选中的 ${count} 个文件吗?此操作不可恢复。`,
|
|
|
|
|
okText: '确认删除',
|
|
|
|
|
cancelText: '取消',
|
|
|
|
|
okType: 'danger',
|
|
|
|
|
centered: true,
|
|
|
|
|
class: 'batch-delete-modal',
|
|
|
|
|
onOk() {
|
2026-01-18 02:35:41 +08:00
|
|
|
resolve(true)
|
2026-01-18 00:34:04 +08:00
|
|
|
},
|
|
|
|
|
onCancel() {
|
2026-01-18 02:35:41 +08:00
|
|
|
resolve(false)
|
2026-01-18 00:34:04 +08:00
|
|
|
},
|
2026-01-18 02:35:41 +08:00
|
|
|
})
|
|
|
|
|
})
|
2026-01-18 00:34:04 +08:00
|
|
|
|
2026-01-18 02:35:41 +08:00
|
|
|
if (!confirmed) return
|
2026-01-18 00:34:04 +08:00
|
|
|
|
|
|
|
|
try {
|
2026-01-18 02:35:41 +08:00
|
|
|
loading.value = true
|
|
|
|
|
await MaterialService.deleteFiles(fileIdsToDelete)
|
2026-01-18 00:34:04 +08:00
|
|
|
|
2026-01-18 02:35:41 +08:00
|
|
|
// 移除已删除的文件
|
|
|
|
|
fileList.value = fileList.value.filter(file => !fileIdsToDelete.includes(file.id))
|
2026-01-18 00:34:04 +08:00
|
|
|
|
2026-01-18 02:35:41 +08:00
|
|
|
// 更新状态
|
|
|
|
|
totalFileCount.value = Math.max(0, totalFileCount.value - count)
|
|
|
|
|
selectedFileIds.value = []
|
2026-01-18 00:34:04 +08:00
|
|
|
|
2026-01-18 02:35:41 +08:00
|
|
|
// 如果删除后当前页没有数据了,则加载上一页
|
2026-01-18 00:34:04 +08:00
|
|
|
if (fileList.value.length === 0 && pagination.current > 1) {
|
2026-01-18 02:35:41 +08:00
|
|
|
pagination.current = pagination.current - 1
|
|
|
|
|
await loadFileList()
|
2026-01-18 00:34:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:35:41 +08:00
|
|
|
message.success(`成功删除 ${count} 个文件`)
|
2026-01-18 00:34:04 +08:00
|
|
|
} catch (error) {
|
2026-01-18 02:35:41 +08:00
|
|
|
console.error('批量删除失败:', error)
|
|
|
|
|
message.error('删除失败,请重试')
|
2026-01-18 00:34:04 +08:00
|
|
|
} finally {
|
2026-01-18 02:35:41 +08:00
|
|
|
loading.value = false
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
2026-01-18 02:35:41 +08:00
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
|
|
|
|
|
const formatFileSize = (size) => {
|
2026-01-18 02:35:41 +08:00
|
|
|
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'
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
|
|
|
|
|
const formatDate = (date) => {
|
2026-01-18 02:35:41 +08:00
|
|
|
return new Date(date).toLocaleDateString()
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
|
2026-01-18 00:34:04 +08:00
|
|
|
const getFileTypeText = (fileName) => {
|
2026-01-18 02:35:41 +08:00
|
|
|
if (!fileName) return ''
|
|
|
|
|
const ext = fileName.split('.').pop()
|
|
|
|
|
return ext ? `${ext.toLowerCase()}` : ''
|
|
|
|
|
}
|
2026-01-18 00:34:04 +08:00
|
|
|
|
2026-01-18 02:15:08 +08:00
|
|
|
const handleEditName = async (file) => {
|
2026-01-18 02:35:41 +08:00
|
|
|
editingFileId.value = file.id
|
|
|
|
|
editingDisplayName.value = file.displayName || file.fileName
|
|
|
|
|
await nextTick()
|
2026-01-18 02:15:08 +08:00
|
|
|
|
|
|
|
|
// 查找当前编辑文件的输入框
|
2026-01-18 02:35:41 +08:00
|
|
|
const nameElement = document.querySelector(`[data-file-id="${file.id}"] .material-item__name`)
|
2026-01-18 02:15:08 +08:00
|
|
|
if (nameElement) {
|
2026-01-18 02:35:41 +08:00
|
|
|
const input = nameElement.querySelector('input')
|
2026-01-18 00:34:04 +08:00
|
|
|
if (input) {
|
2026-01-18 02:35:41 +08:00
|
|
|
input.focus()
|
|
|
|
|
input.select()
|
2026-01-18 00:34:04 +08:00
|
|
|
}
|
2026-01-18 02:15:08 +08:00
|
|
|
}
|
2026-01-18 02:35:41 +08:00
|
|
|
}
|
2026-01-18 00:34:04 +08:00
|
|
|
|
|
|
|
|
const handleSaveName = async (file) => {
|
2026-01-18 02:35:41 +08:00
|
|
|
const newName = editingDisplayName.value.trim()
|
|
|
|
|
|
|
|
|
|
if (!newName) {
|
|
|
|
|
message.warning('名称不能为空')
|
|
|
|
|
editingFileId.value = null
|
|
|
|
|
return
|
2026-01-18 00:34:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:35:41 +08:00
|
|
|
if (newName === file.displayName) {
|
|
|
|
|
editingFileId.value = null
|
|
|
|
|
return
|
2026-01-18 00:34:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-18 02:35:41 +08:00
|
|
|
await MaterialService.updateDisplayName(file.id, newName)
|
|
|
|
|
file.displayName = newName
|
|
|
|
|
message.success('重命名成功')
|
2026-01-18 00:34:04 +08:00
|
|
|
|
2026-01-18 02:35:41 +08:00
|
|
|
// 成功动画效果
|
|
|
|
|
const nameElement = document.querySelector(`[data-file-id="${file.id}"] .material-item__name`)
|
2026-01-18 00:34:04 +08:00
|
|
|
if (nameElement) {
|
2026-01-18 02:35:41 +08:00
|
|
|
nameElement.style.transition = 'all 0.3s ease'
|
|
|
|
|
nameElement.style.transform = 'scale(1.05)'
|
2026-01-18 00:34:04 +08:00
|
|
|
setTimeout(() => {
|
2026-01-18 02:35:41 +08:00
|
|
|
nameElement.style.transform = 'scale(1)'
|
|
|
|
|
}, 300)
|
2026-01-18 00:34:04 +08:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2026-01-18 02:35:41 +08:00
|
|
|
console.error('重命名失败:', error)
|
|
|
|
|
message.error('重命名失败,请重试')
|
2026-01-18 00:34:04 +08:00
|
|
|
} finally {
|
2026-01-18 02:35:41 +08:00
|
|
|
editingFileId.value = null
|
2026-01-18 00:34:04 +08:00
|
|
|
}
|
2026-01-18 02:35:41 +08:00
|
|
|
}
|
2026-01-18 00:34:04 +08:00
|
|
|
|
2026-01-17 14:43:42 +08:00
|
|
|
const handleImageError = (e) => {
|
2026-01-18 02:35:41 +08:00
|
|
|
e.target.style.display = 'none'
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
|
|
|
|
|
// 监听分类变化
|
|
|
|
|
watch(activeCategory, () => {
|
2026-01-18 02:35:41 +08:00
|
|
|
selectedFileIds.value = []
|
|
|
|
|
})
|
2026-01-17 14:43:42 +08:00
|
|
|
|
|
|
|
|
// 初始化
|
|
|
|
|
onMounted(() => {
|
2026-01-18 02:35:41 +08:00
|
|
|
loadGroupList()
|
|
|
|
|
})
|
2026-01-17 14:43:42 +08:00
|
|
|
</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;
|
2026-01-17 19:33:59 +08:00
|
|
|
background: transparent;
|
|
|
|
|
border-bottom: 1px solid #e5e7eb;
|
|
|
|
|
padding-bottom: 0;
|
2026-01-17 14:43:42 +08:00
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.category-tab {
|
|
|
|
|
flex: 1;
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
background: transparent;
|
|
|
|
|
font-weight: 400;
|
2026-01-17 19:33:59 +08:00
|
|
|
font-size: 14px;
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
border-bottom: 2px solid transparent;
|
|
|
|
|
transition: all 0.15s ease;
|
|
|
|
|
margin-bottom: -1px;
|
|
|
|
|
|
2026-01-17 14:43:42 +08:00
|
|
|
&:hover {
|
2026-01-17 19:33:59 +08:00
|
|
|
color: #374151;
|
|
|
|
|
background: rgba(243, 244, 246, 0.5);
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&--active {
|
2026-01-17 19:33:59 +08:00
|
|
|
color: #2563eb;
|
|
|
|
|
border-bottom-color: #2563eb;
|
|
|
|
|
font-weight: 500;
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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 {
|
|
|
|
|
&__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;
|
2026-01-18 02:35:41 +08:00
|
|
|
|
|
|
|
|
.sidebar-group-item__action-btn {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
transform: translateX(0);
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&--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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:35:41 +08:00
|
|
|
&__actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-17 14:43:42 +08:00
|
|
|
&__count {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #999;
|
2026-01-18 02:35:41 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sidebar-actions {
|
|
|
|
|
margin-top: auto;
|
|
|
|
|
padding-top: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.material-content {
|
|
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
2026-01-18 00:34:04 +08:00
|
|
|
background: #F8FAFC;
|
|
|
|
|
margin: 0px 16px;
|
|
|
|
|
border-radius: 12px;
|
2026-01-17 14:43:42 +08:00
|
|
|
overflow: hidden;
|
2026-01-18 00:34:04 +08:00
|
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
2026-01-17 14:43:42 +08:00
|
|
|
|
2026-01-18 00:34:04 +08:00
|
|
|
&__search {
|
|
|
|
|
padding: 20px 32px;
|
|
|
|
|
border-bottom: 1px solid #E2E8F0;
|
|
|
|
|
background: #fff;
|
2026-01-17 14:43:42 +08:00
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
2026-01-18 00:34:04 +08:00
|
|
|
gap: 16px;
|
|
|
|
|
transition: all 0.2s ease;
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__actions {
|
|
|
|
|
display: flex;
|
2026-01-18 00:34:04 +08:00
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
|
|
|
|
|
.ant-checkbox {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
background: #F1F5F9;
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
|
2026-01-18 00:34:04 +08:00
|
|
|
.ant-checkbox__inner {
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ant-checkbox-checked {
|
|
|
|
|
.ant-checkbox__inner {
|
|
|
|
|
background-color: #3B82F6;
|
|
|
|
|
border-color: #3B82F6;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ant-btn {
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
border-radius: 6px;
|
2026-01-18 02:15:08 +08:00
|
|
|
transition: all 0.2s ease;
|
2026-01-18 00:34:04 +08:00
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
box-shadow: 0 4px 8px rgba(59, 130, 246, 0.2);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__list {
|
|
|
|
|
flex: 1;
|
2026-01-18 00:34:04 +08:00
|
|
|
padding: 32px;
|
2026-01-17 14:43:42 +08:00
|
|
|
overflow-y: auto;
|
2026-01-18 00:34:04 +08:00
|
|
|
background: #F8FAFC;
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__pagination {
|
2026-01-18 00:34:04 +08:00
|
|
|
padding: 20px 32px;
|
|
|
|
|
border-top: 1px solid #E2E8F0;
|
|
|
|
|
background: #fff;
|
2026-01-17 14:43:42 +08:00
|
|
|
text-align: right;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.material-grid {
|
|
|
|
|
display: grid;
|
2026-01-18 00:34:04 +08:00
|
|
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
|
|
|
gap: 24px;
|
|
|
|
|
padding: 4px;
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.material-item {
|
|
|
|
|
position: relative;
|
2026-01-18 00:34:04 +08:00
|
|
|
background: #fff;
|
|
|
|
|
border: 1px solid #E2E8F0;
|
|
|
|
|
border-radius: 12px;
|
2026-01-17 14:43:42 +08:00
|
|
|
overflow: hidden;
|
|
|
|
|
cursor: pointer;
|
2026-01-18 00:34:04 +08:00
|
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
2026-01-17 14:43:42 +08:00
|
|
|
|
|
|
|
|
&:hover {
|
2026-01-18 00:34:04 +08:00
|
|
|
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);
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&--selected {
|
2026-01-18 00:34:04 +08:00
|
|
|
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);
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__content {
|
2026-01-18 00:34:04 +08:00
|
|
|
padding: 16px;
|
|
|
|
|
transition: all 0.2s ease;
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__preview {
|
|
|
|
|
width: 100%;
|
2026-01-18 00:34:04 +08:00
|
|
|
height: 160px;
|
|
|
|
|
background: linear-gradient(135deg, #F1F5F9 0%, #E2E8F0 100%);
|
|
|
|
|
border-radius: 8px;
|
2026-01-17 14:43:42 +08:00
|
|
|
overflow: hidden;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
2026-01-18 00:34:04 +08:00
|
|
|
margin-bottom: 12px;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
position: relative;
|
2026-01-17 14:43:42 +08:00
|
|
|
|
|
|
|
|
img {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
object-fit: cover;
|
2026-01-18 00:34:04 +08:00
|
|
|
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);
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__preview-placeholder {
|
|
|
|
|
font-size: 48px;
|
2026-01-18 00:34:04 +08:00
|
|
|
color: #94A3B8;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
color: #3B82F6;
|
|
|
|
|
transform: scale(1.1);
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__info {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
2026-01-18 00:34:04 +08:00
|
|
|
gap: 8px;
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__name {
|
2026-01-18 00:34:04 +08:00
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #1E293B;
|
2026-01-17 14:43:42 +08:00
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
2026-01-18 00:34:04 +08:00
|
|
|
line-height: 1.4;
|
|
|
|
|
transition: color 0.2s ease;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
color: #3B82F6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ant-input {
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 600;
|
2026-01-18 02:15:08 +08:00
|
|
|
height: 20px; /* 与文字高度一致 */
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
border-radius: 4px;
|
2026-01-18 00:34:04 +08:00
|
|
|
border-color: #3B82F6;
|
|
|
|
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
2026-01-18 02:15:08 +08:00
|
|
|
padding: 0 4px; /* 减少内边距 */
|
|
|
|
|
margin: 0; /* 移除margin避免布局问题 */
|
|
|
|
|
display: block; /* 确保输入框正常显示 */
|
2026-01-18 00:34:04 +08:00
|
|
|
|
|
|
|
|
&:focus {
|
|
|
|
|
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15);
|
|
|
|
|
}
|
2026-01-18 02:15:08 +08:00
|
|
|
|
|
|
|
|
/* 移除默认的内边距和高度 */
|
|
|
|
|
input {
|
|
|
|
|
height: 100%;
|
|
|
|
|
padding: 0;
|
|
|
|
|
margin: 0;
|
|
|
|
|
line-height: inherit;
|
|
|
|
|
font-size: inherit;
|
|
|
|
|
font-weight: inherit;
|
|
|
|
|
color: inherit;
|
|
|
|
|
}
|
2026-01-18 00:34:04 +08:00
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__meta {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
2026-01-18 00:34:04 +08:00
|
|
|
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;
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__select {
|
|
|
|
|
position: absolute;
|
2026-01-18 00:34:04 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-01 19:16:31 +08:00
|
|
|
|
|
|
|
|
|
2026-01-18 00:34:04 +08:00
|
|
|
// 批量删除确认弹窗样式
|
|
|
|
|
: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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-18 02:35:41 +08:00
|
|
|
|
|
|
|
|
// 删除分组确认弹窗样式
|
|
|
|
|
: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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-17 14:43:42 +08:00
|
|
|
</style>
|