Files
sionrui/frontend/app/web-gold/src/views/material/MaterialListNew.vue
2026-02-11 00:39:16 +08:00

1241 lines
29 KiB
Vue
Raw Blame History

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