Files
sionrui/frontend/app/web-gold/src/views/material/MaterialListNew.vue

1400 lines
36 KiB
Vue
Raw Normal View History

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-02-24 23:58:17 +08:00
<!-- 左侧分组面板 - 仅混剪素材显示 -->
<transition name="sidebar-slide">
<div class="material-sidebar" v-show="activeCategory === 'MIX'">
<!-- 分组列表 -->
<div class="sidebar-section">
<div class="sidebar-section__header">
<span>分组</span>
<a-button type="link" size="small" @click="handleOpenCreateGroupModal">
<PlusOutlined />
</a-button>
</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" />
<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, $event)"
/>
<DeleteOutlined
class="sidebar-group-item__action-btn sidebar-group-item__action-btn--danger"
@click.stop="handleDeleteGroup(group, $event)"
/>
</template>
</div>
2026-01-17 14:43:42 +08:00
</div>
</div>
</div>
</div>
2026-02-24 23:58:17 +08:00
</transition>
2026-01-17 14:43:42 +08:00
<!-- 右侧内容区域 -->
<div class="material-content">
2026-02-24 23:58:17 +08:00
<!-- 顶部工具栏 -->
<div class="material-toolbar">
<!-- 分类切换器 -->
<div class="category-switcher">
<div
class="category-switcher__item"
:class="{ 'category-switcher__item--active': activeCategory === 'MIX' }"
@click="handleCategoryChange('MIX')"
2026-01-18 00:34:04 +08:00
>
2026-02-24 23:58:17 +08:00
<VideoCameraOutlined class="category-switcher__icon" />
<span>混剪素材</span>
</div>
<div
class="category-switcher__item"
:class="{ 'category-switcher__item--active': activeCategory === 'DIGITAL_HUMAN' }"
@click="handleCategoryChange('DIGITAL_HUMAN')"
2026-01-17 14:43:42 +08:00
>
2026-02-24 23:58:17 +08:00
<UserOutlined class="category-switcher__icon" />
<span>数字人素材</span>
</div>
</div>
<!-- 搜索和操作区 -->
<div class="toolbar-actions">
2026-02-25 21:30:24 +08:00
<!-- 存储配额显示 -->
<div class="storage-quota">
<span class="storage-quota__label">存储空间</span>
<span class="storage-quota__value">{{ userStore.usedStorage.toFixed(2) }} / {{ userStore.totalStorage }} GB</span>
<div class="storage-quota__bar">
<div class="storage-quota__used" :style="{ width: `${Math.min((userStore.usedStorage / userStore.totalStorage) * 100, 100)}%` }"></div>
</div>
</div>
2026-02-24 23:58:17 +08:00
<a-input
v-model="searchKeyword"
placeholder="搜索文件名..."
class="search-input"
allow-clear
@press-enter="handleSearch"
2026-01-17 14:43:42 +08:00
>
2026-02-24 23:58:17 +08:00
<template #prefix>
<SearchOutlined />
</template>
</a-input>
<a-button type="primary" class="upload-btn" @click="handleOpenUploadModal">
<UploadOutlined />
上传
2026-01-17 14:43:42 +08:00
</a-button>
</div>
</div>
2026-02-24 23:58:17 +08:00
<!-- 批量操作栏 -->
<transition name="fade-slide">
<div class="batch-actions" v-if="selectedFileIds.length > 0">
<div class="batch-actions__info">
<CheckSquareOutlined />
<span>已选择 <strong>{{ selectedFileIds.length }}</strong> 个文件</span>
</div>
<div class="batch-actions__btns">
<a-button size="small" @click="selectedFileIds = []">取消选择</a-button>
<a-button type="primary" danger size="small" @click="handleBatchDelete">
<DeleteOutlined /> 删除
</a-button>
</div>
</div>
</transition>
2026-01-17 14:43:42 +08:00
<!-- 文件列表 -->
<div class="material-content__list">
2026-02-24 23:58:17 +08:00
<a-spin :spinning="loading" tip="加载中..." class="loading-spinner">
2026-01-17 14:43:42 +08:00
<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)"
>
2026-02-24 23:58:17 +08:00
<!-- 选择指示器 -->
<div class="material-item__check">
<div class="check-circle" :class="{ 'check-circle--checked': selectedFileIds.includes(file.id) }">
<CheckOutlined v-if="selectedFileIds.includes(file.id)" />
2026-01-17 14:43:42 +08:00
</div>
2026-02-24 23:58:17 +08:00
</div>
2026-01-17 14:43:42 +08:00
2026-02-24 23:58:17 +08:00
<!-- 预览图 -->
<div class="material-item__preview">
<img
v-if="file.coverBase64"
:src="file.coverBase64"
:alt="file.fileName"
@error="handleImageError"
loading="lazy"
/>
<div v-else class="material-item__preview-placeholder">
<FileOutlined />
</div>
<div class="material-item__overlay">
<span class="material-item__type-tag">{{ getFileTypeText(file.fileName) }}</span>
2026-01-17 14:43:42 +08:00
</div>
</div>
2026-02-24 23:58:17 +08:00
<!-- 文件信息 -->
<div class="material-item__info">
<div
class="material-item__name"
:title="file.displayName"
@click.stop="handleEditName(file)"
>
<template v-if="editingFileId !== file.id">
{{ file.displayName }}
</template>
<template v-else>
<a-input
ref="nameInputRef"
v-model:value="editingDisplayName"
size="small"
@blur="handleSaveName(file)"
@press-enter="handleSaveName(file)"
@click.stop
autofocus
/>
</template>
</div>
<div class="material-item__meta">
<span>{{ formatFileSize(file.fileSize) }}</span>
<span class="meta-dot">·</span>
<span>{{ formatDate(file.createTime) }}</span>
</div>
2026-01-17 14:43:42 +08:00
</div>
</div>
</div>
</template>
2026-02-24 23:58:17 +08:00
<div v-else class="empty-state">
<div class="empty-state__icon">
<FolderOpenOutlined />
</div>
<p class="empty-state__text">暂无素材</p>
<a-button type="primary" @click="handleOpenUploadModal">
<UploadOutlined /> 上传素材
</a-button>
</div>
2026-01-17 14:43:42 +08:00
</a-spin>
</div>
2026-02-24 23:58:17 +08:00
<!-- 底部操作栏 -->
<div class="material-footer">
<div class="footer-left">
<a-checkbox
:checked="selectedFileIds.length === fileList.length && fileList.length > 0"
:indeterminate="selectedFileIds.length > 0 && selectedFileIds.length < fileList.length"
@change="handleSelectAll"
>
全选
</a-checkbox>
<span class="footer-count" v-if="pagination.total > 0">
{{ pagination.total }} 个文件
</span>
</div>
2026-01-17 14:43:42 +08:00
<a-pagination
v-model:current="pagination.current"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:show-size-changer="true"
:show-quick-jumper="true"
2026-02-24 23:58:17 +08:00
:show-total="false"
size="small"
2026-01-17 14:43:42 +08:00
@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-02-24 23:58:17 +08:00
FolderOpenOutlined,
2026-01-18 02:35:41 +08:00
PlusOutlined,
EditOutlined,
2026-02-24 23:58:17 +08:00
DeleteOutlined,
VideoCameraOutlined,
UserOutlined,
CheckOutlined,
CheckSquareOutlined
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-02-25 21:30:24 +08:00
import { useUserStore } from '@/stores/user';
// 用户状态(获取存储配额)
const userStore = useUserStore()
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
2026-02-24 23:58:17 +08:00
// 混剪素材需要加载分组,数字人素材不需要
if (category === 'MIX') {
await loadGroupList()
} else {
await loadFileList()
}
2026-01-18 02:35:41 +08:00
}
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-02-24 23:58:17 +08:00
if (selectedGroupId.value === group.id) return
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 {
2026-02-24 23:58:17 +08:00
// 根据分类选择不同的 fileCategory
// 混剪素材用 video数字人素材用 digital_human
const fileCategory = activeCategory.value === 'DIGITAL_HUMAN' ? 'digital_human' : 'video'
2026-01-17 14:43:42 +08:00
const params = {
pageNo: pagination.current,
pageSize: pagination.pageSize,
2026-02-24 23:58:17 +08:00
fileCategory,
2026-01-17 14:43:42 +08:00
fileName: searchKeyword.value || undefined,
2026-02-24 23:58:17 +08:00
// 数字人素材不传 groupId
groupId: activeCategory.value === 'DIGITAL_HUMAN' ? undefined : (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 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-02-24 23:58:17 +08:00
// 根据当前分类确定 fileCategory
// 混剪素材用 video数字人素材用 digital_human
const fileCategory = activeCategory.value === 'DIGITAL_HUMAN' ? 'digital_human' : 'video'
// 数字人素材不关联分组
const uploadGroupId = activeCategory.value === 'DIGITAL_HUMAN' ? null : groupId
2026-01-17 14:43:42 +08:00
for (const fileWithCover of filesWithCover) {
await upload(fileWithCover.file, {
2026-02-24 23:58:17 +08:00
fileCategory,
groupId: uploadGroupId,
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()
2026-02-25 21:30:24 +08:00
// 刷新存储配额
await userStore.fetchUserProfile()
2026-02-24 23:58:17 +08:00
// 混剪素材才刷新分组列表
if (activeCategory.value === 'MIX') {
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-02-25 21:30:24 +08:00
// 刷新存储配额
await userStore.fetchUserProfile()
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
// 初始化
2026-02-25 21:30:24 +08:00
onMounted(async () => {
// 刷新用户档案获取最新存储配额
await userStore.fetchUserProfile()
await loadGroupList()
2026-01-18 02:35:41 +08:00
})
2026-01-17 14:43:42 +08:00
</script>
<style scoped lang="less">
2026-02-25 23:44:01 +08:00
// 使用设计系统变量
// 主色: var(--color-primary-500) = #3B82F6
2026-02-24 23:58:17 +08:00
// ========================================
// 主容器
// ========================================
2026-01-17 14:43:42 +08:00
.material-list-container {
display: flex;
height: 100%;
2026-02-25 23:44:01 +08:00
background: var(--color-gray-50);
2026-01-17 14:43:42 +08:00
}
2026-02-24 23:58:17 +08:00
// ========================================
// 侧边栏 - 分组面板
// ========================================
2026-01-17 14:43:42 +08:00
.material-sidebar {
2026-02-24 23:58:17 +08:00
width: 220px;
2026-02-25 23:44:01 +08:00
background: var(--color-bg-card);
border-right: 1px solid var(--color-gray-200);
2026-01-17 14:43:42 +08:00
display: flex;
flex-direction: column;
2026-02-25 23:44:01 +08:00
padding: var(--space-5) var(--space-3);
2026-02-24 23:58:17 +08:00
flex-shrink: 0;
2026-01-17 14:43:42 +08:00
}
2026-02-24 23:58:17 +08:00
.sidebar-slide-enter-active,
.sidebar-slide-leave-active {
2026-02-26 21:04:33 +08:00
transition: all var(--duration-slow) cubic-bezier(0.4, 0, 0.2, 1);
2026-01-17 14:43:42 +08:00
}
2026-02-24 23:58:17 +08:00
.sidebar-slide-enter-from,
.sidebar-slide-leave-to {
width: 0;
opacity: 0;
padding-left: 0;
padding-right: 0;
2026-01-17 14:43:42 +08:00
}
2026-02-24 23:58:17 +08:00
.sidebar-section {
&__header {
2026-01-17 14:43:42 +08:00
display: flex;
align-items: center;
2026-02-24 23:58:17 +08:00
justify-content: space-between;
2026-02-25 23:44:01 +08:00
padding: 0 var(--space-2);
margin-bottom: var(--space-3);
2026-01-17 14:43:42 +08:00
2026-02-24 23:58:17 +08:00
span {
2026-02-26 21:04:33 +08:00
font-size: var(--font-size-xs);
2026-02-24 23:58:17 +08:00
font-weight: 600;
2026-02-25 23:44:01 +08:00
color: var(--color-gray-500);
2026-02-24 23:58:17 +08:00
text-transform: uppercase;
letter-spacing: 0.8px;
}
2026-01-17 14:43:42 +08:00
2026-02-24 23:58:17 +08:00
.ant-btn {
padding: 2px 6px;
height: auto;
2026-02-25 23:44:01 +08:00
color: var(--color-gray-500);
2026-01-17 14:43:42 +08:00
2026-02-24 23:58:17 +08:00
&:hover {
2026-02-25 23:44:01 +08:00
color: var(--color-primary-500);
2026-02-24 23:58:17 +08:00
}
}
2026-01-17 14:43:42 +08:00
}
}
.sidebar-group-list {
2026-02-24 23:58:17 +08:00
flex: 1;
2026-01-17 14:43:42 +08:00
overflow-y: auto;
2026-02-25 23:44:01 +08:00
margin: 0 calc(var(--space-1) * -1);
padding: 0 var(--space-1);
2026-02-24 23:58:17 +08:00
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
2026-02-25 23:44:01 +08:00
background: var(--color-gray-200);
2026-02-24 23:58:17 +08:00
border-radius: 2px;
}
2026-01-17 14:43:42 +08:00
}
.sidebar-group-item {
display: flex;
align-items: center;
justify-content: space-between;
2026-02-26 21:04:33 +08:00
padding: var(--space-2) var(--space-3);
2026-01-17 14:43:42 +08:00
cursor: pointer;
2026-02-25 23:44:01 +08:00
border-radius: var(--radius-md);
2026-01-17 14:43:42 +08:00
margin-bottom: 2px;
2026-02-25 23:44:01 +08:00
transition: all var(--duration-fast);
2026-01-17 14:43:42 +08:00
&:hover {
2026-02-25 23:44:01 +08:00
background: var(--color-gray-100);
2026-01-18 02:35:41 +08:00
.sidebar-group-item__action-btn {
opacity: 1;
}
2026-01-17 14:43:42 +08:00
}
&--active {
2026-02-25 23:44:01 +08:00
background: var(--color-primary-50);
color: var(--color-primary-500);
2026-02-24 23:58:17 +08:00
.sidebar-group-item__icon {
2026-02-25 23:44:01 +08:00
color: var(--color-primary-500);
2026-02-24 23:58:17 +08:00
}
2026-01-17 14:43:42 +08:00
}
&__content {
display: flex;
align-items: center;
min-width: 0;
flex: 1;
}
&__icon {
2026-02-26 21:04:33 +08:00
margin-right: var(--space-2);
font-size: var(--font-size-base);
2026-02-25 23:44:01 +08:00
color: var(--color-gray-500);
2026-01-17 14:43:42 +08:00
flex-shrink: 0;
}
&__name {
2026-02-25 23:44:01 +08:00
font-size: var(--font-size-sm);
2026-02-24 23:58:17 +08:00
font-weight: 500;
2026-01-17 14:43:42 +08:00
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
2026-01-18 02:35:41 +08:00
&__actions {
display: flex;
align-items: center;
2026-02-24 23:58:17 +08:00
gap: 2px;
2026-01-18 02:35:41 +08:00
}
2026-01-17 14:43:42 +08:00
&__count {
2026-02-26 21:04:33 +08:00
font-size: var(--font-size-xs);
2026-02-25 23:44:01 +08:00
color: var(--color-gray-500);
margin-right: var(--space-2);
2026-01-18 02:35:41 +08:00
}
&__action-btn {
opacity: 0;
2026-02-25 23:44:01 +08:00
padding: var(--space-1);
2026-01-18 02:35:41 +08:00
cursor: pointer;
2026-02-25 23:44:01 +08:00
color: var(--color-gray-500);
font-size: var(--font-size-xs);
transition: all var(--duration-fast);
2026-01-18 02:35:41 +08:00
&:hover {
2026-02-25 23:44:01 +08:00
color: var(--color-primary-500);
2026-01-18 02:35:41 +08:00
}
&--danger:hover {
2026-02-25 23:44:01 +08:00
color: var(--color-error-500);
2026-01-18 02:35:41 +08:00
}
}
&__edit-input {
2026-02-24 23:58:17 +08:00
:deep(.ant-input) {
2026-01-18 02:35:41 +08:00
height: 24px;
2026-02-25 23:44:01 +08:00
font-size: var(--font-size-sm);
border-color: var(--color-primary-500);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
border-radius: var(--radius-sm);
2026-01-18 02:35:41 +08:00
}
2026-01-17 14:43:42 +08:00
}
}
2026-02-24 23:58:17 +08:00
// ========================================
// 内容区域
// ========================================
2026-01-17 14:43:42 +08:00
.material-content {
flex: 1;
display: flex;
flex-direction: column;
2026-02-24 23:58:17 +08:00
min-width: 0;
2026-02-26 21:04:33 +08:00
transition: all var(--duration-slow) cubic-bezier(0.4, 0, 0.2, 1);
2026-02-24 23:58:17 +08:00
}
2026-01-17 14:43:42 +08:00
2026-02-24 23:58:17 +08:00
// ========================================
// 顶部工具栏
// ========================================
.material-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
2026-02-25 23:44:01 +08:00
padding: var(--space-4) var(--space-6);
background: var(--color-bg-card);
border-bottom: 1px solid var(--color-gray-200);
gap: var(--space-6);
2026-02-24 23:58:17 +08:00
}
2026-01-17 14:43:42 +08:00
2026-02-24 23:58:17 +08:00
// 分类切换器
.category-switcher {
display: flex;
2026-02-25 23:44:01 +08:00
background: var(--color-gray-50);
2026-02-26 21:04:33 +08:00
border-radius: var(--radius-md);
2026-02-25 23:44:01 +08:00
padding: var(--space-1);
gap: var(--space-1);
2026-02-24 23:58:17 +08:00
&__item {
2026-01-17 14:43:42 +08:00
display: flex;
2026-01-18 00:34:04 +08:00
align-items: center;
2026-02-25 23:44:01 +08:00
gap: var(--space-2);
2026-02-26 21:04:33 +08:00
padding: var(--space-2) var(--space-5);
2026-02-25 23:44:01 +08:00
border-radius: var(--radius-md);
2026-02-24 23:58:17 +08:00
cursor: pointer;
2026-02-25 23:44:01 +08:00
font-size: var(--font-size-base);
2026-02-24 23:58:17 +08:00
font-weight: 500;
2026-02-25 23:44:01 +08:00
color: var(--color-gray-600);
2026-02-26 21:04:33 +08:00
transition: all var(--duration-base) ease;
2026-01-18 00:34:04 +08:00
2026-02-24 23:58:17 +08:00
&:hover {
2026-02-25 23:44:01 +08:00
color: var(--color-gray-900);
2026-02-24 23:58:17 +08:00
}
2026-01-18 00:34:04 +08:00
2026-02-24 23:58:17 +08:00
&--active {
2026-02-25 23:44:01 +08:00
background: var(--color-bg-card);
color: var(--color-primary-500);
box-shadow: var(--shadow-sm);
2026-01-17 14:43:42 +08:00
2026-02-24 23:58:17 +08:00
.category-switcher__icon {
2026-02-25 23:44:01 +08:00
color: var(--color-primary-500);
2026-01-18 00:34:04 +08:00
}
2026-02-24 23:58:17 +08:00
}
}
2026-01-18 00:34:04 +08:00
2026-02-24 23:58:17 +08:00
&__icon {
2026-02-26 21:04:33 +08:00
font-size: var(--font-size-md);
2026-02-25 23:44:01 +08:00
color: var(--color-gray-500);
2026-02-26 21:04:33 +08:00
transition: color var(--duration-base) ease;
2026-02-24 23:58:17 +08:00
}
}
2026-02-25 21:30:24 +08:00
// 存储配额显示
.storage-quota {
display: flex;
align-items: center;
2026-02-25 23:44:01 +08:00
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
background: var(--color-gray-50);
border-radius: var(--radius-md);
border: 1px solid var(--color-gray-200);
2026-02-25 21:30:24 +08:00
&__label {
2026-02-25 23:44:01 +08:00
font-size: var(--font-size-xs);
color: var(--color-gray-500);
2026-02-25 21:30:24 +08:00
}
&__value {
2026-02-25 23:44:01 +08:00
font-size: var(--font-size-xs);
2026-02-25 21:30:24 +08:00
font-weight: 500;
2026-02-25 23:44:01 +08:00
color: var(--color-gray-900);
2026-02-25 21:30:24 +08:00
}
&__bar {
width: 80px;
height: 4px;
2026-02-25 23:44:01 +08:00
background: var(--color-gray-200);
2026-02-25 21:30:24 +08:00
border-radius: 2px;
overflow: hidden;
}
&__used {
height: 100%;
2026-02-25 23:44:01 +08:00
background: linear-gradient(90deg, var(--color-primary-500), var(--color-primary-400));
2026-02-25 21:30:24 +08:00
border-radius: 2px;
2026-02-26 21:04:33 +08:00
transition: width var(--duration-slow) ease;
2026-02-25 21:30:24 +08:00
}
}
2026-02-24 23:58:17 +08:00
// 工具栏操作区
.toolbar-actions {
display: flex;
align-items: center;
2026-02-25 23:44:01 +08:00
gap: var(--space-3);
2026-02-24 23:58:17 +08:00
.search-input {
width: 280px;
:deep(.ant-input) {
2026-02-25 23:44:01 +08:00
border-radius: var(--radius-md);
border-color: var(--color-gray-200);
background: var(--color-gray-50);
2026-02-24 23:58:17 +08:00
&:focus {
2026-02-25 23:44:01 +08:00
background: var(--color-bg-card);
border-color: var(--color-primary-500);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
2026-01-18 00:34:04 +08:00
}
}
2026-02-24 23:58:17 +08:00
:deep(.ant-input-prefix) {
2026-02-25 23:44:01 +08:00
color: var(--color-gray-500);
2026-02-24 23:58:17 +08:00
}
}
2026-01-18 00:34:04 +08:00
2026-02-24 23:58:17 +08:00
.upload-btn {
2026-02-25 23:44:01 +08:00
border-radius: var(--radius-md);
2026-02-24 23:58:17 +08:00
font-weight: 500;
display: flex;
align-items: center;
2026-02-25 23:44:01 +08:00
gap: var(--space-2);
2026-02-24 23:58:17 +08:00
}
}
// ========================================
// 批量操作栏
// ========================================
.batch-actions {
display: flex;
align-items: center;
justify-content: space-between;
2026-02-25 23:44:01 +08:00
padding: var(--space-3) var(--space-6);
background: var(--color-primary-50);
border-bottom: 1px solid rgba(59, 130, 246, 0.1);
2026-02-24 23:58:17 +08:00
&__info {
display: flex;
align-items: center;
2026-02-26 21:04:33 +08:00
gap: var(--space-2);
2026-02-25 23:44:01 +08:00
font-size: var(--font-size-base);
color: var(--color-gray-900);
2026-02-24 23:58:17 +08:00
.anticon {
2026-02-25 23:44:01 +08:00
color: var(--color-primary-500);
2026-02-26 21:04:33 +08:00
font-size: var(--font-size-md);
2026-02-24 23:58:17 +08:00
}
strong {
2026-02-25 23:44:01 +08:00
color: var(--color-primary-500);
2026-02-24 23:58:17 +08:00
font-weight: 600;
2026-01-18 00:34:04 +08:00
}
2026-01-17 14:43:42 +08:00
}
2026-02-24 23:58:17 +08:00
&__btns {
display: flex;
2026-02-25 23:44:01 +08:00
gap: var(--space-2);
2026-02-24 23:58:17 +08:00
}
}
.fade-slide-enter-active,
.fade-slide-leave-active {
2026-02-26 21:04:33 +08:00
transition: all var(--duration-slow) ease;
2026-02-24 23:58:17 +08:00
}
.fade-slide-enter-from,
.fade-slide-leave-to {
opacity: 0;
transform: translateY(-10px);
}
// ========================================
// 文件列表区域
// ========================================
.material-content__list {
flex: 1;
2026-02-25 23:44:01 +08:00
padding: var(--space-6);
2026-02-24 23:58:17 +08:00
overflow-y: auto;
.loading-spinner {
width: 100%;
min-height: 400px;
}
}
// 空状态
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
2026-02-25 23:44:01 +08:00
padding: 80px var(--space-6);
2026-02-24 23:58:17 +08:00
&__icon {
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
2026-02-25 23:44:01 +08:00
background: var(--color-gray-100);
2026-02-24 23:58:17 +08:00
border-radius: 50%;
2026-02-25 23:44:01 +08:00
margin-bottom: var(--space-5);
2026-02-24 23:58:17 +08:00
.anticon {
font-size: 36px;
2026-02-25 23:44:01 +08:00
color: var(--color-gray-500);
2026-02-24 23:58:17 +08:00
}
2026-01-17 14:43:42 +08:00
}
2026-02-24 23:58:17 +08:00
&__text {
2026-02-25 23:44:01 +08:00
font-size: var(--font-size-md);
color: var(--color-gray-600);
margin-bottom: var(--space-5);
2026-01-17 14:43:42 +08:00
}
}
2026-02-24 23:58:17 +08:00
// ========================================
// 素材网格
// ========================================
2026-01-17 14:43:42 +08:00
.material-grid {
display: grid;
2026-02-24 23:58:17 +08:00
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
2026-02-25 23:44:01 +08:00
gap: var(--space-5);
2026-01-17 14:43:42 +08:00
}
.material-item {
position: relative;
2026-02-25 23:44:01 +08:00
background: var(--color-bg-card);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-lg);
2026-01-17 14:43:42 +08:00
overflow: hidden;
cursor: pointer;
2026-02-26 21:04:33 +08:00
transition: all var(--duration-slow) cubic-bezier(0.4, 0, 0.2, 1);
2026-01-17 14:43:42 +08:00
&:hover {
2026-02-25 23:44:01 +08:00
border-color: var(--color-primary-500);
box-shadow: var(--shadow-md);
2026-02-24 23:58:17 +08:00
transform: translateY(-2px);
2026-01-18 00:34:04 +08:00
2026-02-24 23:58:17 +08:00
.material-item__check {
opacity: 1;
}
.material-item__overlay {
opacity: 1;
}
2026-01-17 14:43:42 +08:00
}
&--selected {
2026-02-25 23:44:01 +08:00
border-color: var(--color-primary-500);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2), var(--shadow-md);
2026-01-18 00:34:04 +08:00
2026-02-24 23:58:17 +08:00
.material-item__check .check-circle {
2026-02-25 23:44:01 +08:00
background: var(--color-primary-500);
border-color: var(--color-primary-500);
2026-01-18 00:34:04 +08:00
opacity: 1;
2026-02-24 23:58:17 +08:00
.anticon {
opacity: 1;
}
2026-01-18 00:34:04 +08:00
}
2026-01-17 14:43:42 +08:00
}
2026-02-24 23:58:17 +08:00
// 选择指示器
&__check {
position: absolute;
2026-02-26 21:04:33 +08:00
top: var(--space-2);
left: var(--space-2);
2026-02-24 23:58:17 +08:00
z-index: 10;
opacity: 0;
2026-02-25 23:44:01 +08:00
transition: opacity var(--duration-fast);
2026-02-24 23:58:17 +08:00
.check-circle {
width: 22px;
height: 22px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
2026-02-25 23:44:01 +08:00
border: 2px solid var(--color-gray-200);
2026-02-24 23:58:17 +08:00
display: flex;
align-items: center;
justify-content: center;
2026-02-25 23:44:01 +08:00
transition: all var(--duration-fast);
box-shadow: var(--shadow-sm);
2026-02-24 23:58:17 +08:00
.anticon {
2026-02-25 23:44:01 +08:00
font-size: var(--font-size-xs);
2026-02-24 23:58:17 +08:00
color: white;
opacity: 0;
2026-02-25 23:44:01 +08:00
transition: opacity var(--duration-fast);
2026-02-24 23:58:17 +08:00
}
&--checked {
2026-02-25 23:44:01 +08:00
background: var(--color-primary-500);
border-color: var(--color-primary-500);
2026-02-24 23:58:17 +08:00
.anticon {
opacity: 1;
}
}
}
2026-01-17 14:43:42 +08:00
}
2026-02-24 23:58:17 +08:00
// 预览区
2026-01-17 14:43:42 +08:00
&__preview {
2026-02-24 23:58:17 +08:00
position: relative;
2026-01-17 14:43:42 +08:00
width: 100%;
2026-02-24 23:58:17 +08:00
aspect-ratio: 16 / 10;
2026-02-26 21:04:33 +08:00
background: linear-gradient(135deg, var(--color-gray-50) 0%, var(--color-gray-100) 100%);
2026-01-17 14:43:42 +08:00
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
2026-02-26 21:04:33 +08:00
transition: transform var(--duration-slower) ease;
2026-01-18 00:34:04 +08:00
}
&:hover img {
transform: scale(1.05);
}
2026-02-24 23:58:17 +08:00
}
2026-01-18 00:34:04 +08:00
2026-02-24 23:58:17 +08:00
&__preview-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
2026-01-18 00:34:04 +08:00
2026-02-24 23:58:17 +08:00
.anticon {
font-size: 40px;
2026-02-25 23:44:01 +08:00
color: var(--color-gray-500);
2026-01-17 14:43:42 +08:00
}
}
2026-02-24 23:58:17 +08:00
&__overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
2026-02-25 23:44:01 +08:00
padding: var(--space-2) var(--space-3);
2026-02-24 23:58:17 +08:00
background: linear-gradient(transparent, rgba(0, 0, 0, 0.6));
opacity: 0;
2026-02-26 21:04:33 +08:00
transition: opacity var(--duration-slow) ease;
2026-02-24 23:58:17 +08:00
}
2026-01-18 00:34:04 +08:00
2026-02-24 23:58:17 +08:00
&__type-tag {
2026-02-26 21:04:33 +08:00
font-size: var(--font-size-xs);
2026-02-24 23:58:17 +08:00
font-weight: 600;
color: white;
text-transform: uppercase;
letter-spacing: 0.5px;
2026-01-17 14:43:42 +08:00
}
2026-02-24 23:58:17 +08:00
// 信息区
2026-01-17 14:43:42 +08:00
&__info {
2026-02-26 21:04:33 +08:00
padding: var(--space-3) var(--space-4);
2026-01-17 14:43:42 +08:00
}
&__name {
2026-02-25 23:44:01 +08:00
font-size: var(--font-size-base);
2026-01-18 00:34:04 +08:00
font-weight: 600;
2026-02-25 23:44:01 +08:00
color: var(--color-gray-900);
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;
2026-02-25 23:44:01 +08:00
margin-bottom: var(--space-2);
transition: color var(--duration-fast);
2026-01-18 00:34:04 +08:00
&:hover {
2026-02-25 23:44:01 +08:00
color: var(--color-primary-500);
2026-01-18 00:34:04 +08:00
}
2026-02-24 23:58:17 +08:00
:deep(.ant-input) {
2026-02-25 23:44:01 +08:00
font-size: var(--font-size-base);
2026-01-18 00:34:04 +08:00
font-weight: 600;
2026-02-24 23:58:17 +08:00
height: 22px;
2026-02-25 23:44:01 +08:00
border-radius: var(--radius-sm);
border-color: var(--color-primary-500);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
2026-01-18 00:34:04 +08:00
}
2026-01-17 14:43:42 +08:00
}
&__meta {
display: flex;
2026-01-18 00:34:04 +08:00
align-items: center;
2026-02-25 23:44:01 +08:00
font-size: var(--font-size-xs);
color: var(--color-gray-500);
2026-01-18 00:34:04 +08:00
2026-02-24 23:58:17 +08:00
.meta-dot {
2026-02-25 23:44:01 +08:00
margin: 0 var(--space-2);
2026-01-18 00:34:04 +08:00
}
2026-01-17 14:43:42 +08:00
}
2026-02-24 23:58:17 +08:00
}
2026-01-17 14:43:42 +08:00
2026-02-24 23:58:17 +08:00
// ========================================
// 底部操作栏
// ========================================
.material-footer {
display: flex;
align-items: center;
justify-content: space-between;
2026-02-26 21:04:33 +08:00
padding: var(--space-3) var(--space-6);
2026-02-25 23:44:01 +08:00
background: var(--color-bg-card);
border-top: 1px solid var(--color-gray-200);
2026-01-18 00:34:04 +08:00
2026-02-24 23:58:17 +08:00
.footer-left {
display: flex;
align-items: center;
2026-02-25 23:44:01 +08:00
gap: var(--space-4);
2026-01-18 00:34:04 +08:00
}
2026-02-24 23:58:17 +08:00
.footer-count {
2026-02-25 23:44:01 +08:00
font-size: var(--font-size-sm);
color: var(--color-gray-500);
2026-02-24 23:58:17 +08:00
}
2026-01-18 00:34:04 +08:00
2026-02-24 23:58:17 +08:00
:deep(.ant-pagination) {
.ant-pagination-item {
2026-02-25 23:44:01 +08:00
border-radius: var(--radius-base);
border-color: var(--color-gray-200);
2026-02-01 19:16:31 +08:00
2026-02-24 23:58:17 +08:00
&:hover {
2026-02-25 23:44:01 +08:00
border-color: var(--color-primary-500);
2026-02-01 19:16:31 +08:00
2026-02-24 23:58:17 +08:00
a {
2026-02-25 23:44:01 +08:00
color: var(--color-primary-500);
2026-02-24 23:58:17 +08:00
}
}
}
2026-01-18 00:34:04 +08:00
2026-02-24 23:58:17 +08:00
.ant-pagination-item-active {
2026-02-25 23:44:01 +08:00
border-color: var(--color-primary-500);
background: var(--color-primary-500);
2026-01-18 00:34:04 +08:00
2026-02-24 23:58:17 +08:00
a {
color: white;
}
}
2026-01-18 00:34:04 +08:00
2026-02-24 23:58:17 +08:00
.ant-pagination-options {
.ant-select-selector {
2026-02-25 23:44:01 +08:00
border-radius: var(--radius-base);
border-color: var(--color-gray-200);
2026-01-18 00:34:04 +08:00
}
}
2026-01-17 14:43:42 +08:00
}
}
2026-01-18 02:35:41 +08:00
2026-02-24 23:58:17 +08:00
// ========================================
// 弹窗样式
// ========================================
:deep(.batch-delete-modal),
2026-01-18 02:35:41 +08:00
:deep(.delete-group-modal) {
2026-02-24 23:58:17 +08:00
.ant-modal-content {
2026-02-25 23:44:01 +08:00
border-radius: var(--radius-lg);
2026-02-24 23:58:17 +08:00
overflow: hidden;
}
2026-01-18 02:35:41 +08:00
.ant-modal-body {
2026-02-25 23:44:01 +08:00
padding: var(--space-6);
2026-01-18 02:35:41 +08:00
}
.ant-modal-footer {
2026-02-25 23:44:01 +08:00
padding: var(--space-4) var(--space-6);
border-top: 1px solid var(--color-gray-200);
2026-01-18 02:35:41 +08:00
}
}
2026-01-17 14:43:42 +08:00
</style>