初步重构智能商务模块代码,但还存在一些bug
This commit is contained in:
parent
5f676536dd
commit
b2f09d3474
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,105 @@
|
|||
<template>
|
||||
<div class="file-header">
|
||||
<div class="breadcrumbs">
|
||||
<a-breadcrumb>
|
||||
<a-breadcrumb-item
|
||||
v-for="(item, index) in breadcrumbPath"
|
||||
:key="index"
|
||||
:class="{ 'clickable': index < breadcrumbPath.length - 1 }"
|
||||
@click="handleBreadcrumbClick(index)"
|
||||
>
|
||||
{{ item }}
|
||||
</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
<a-button
|
||||
type="text"
|
||||
shape="circle"
|
||||
@click="handleRefresh"
|
||||
:loading="refreshing"
|
||||
tooltip="刷新数据"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-refresh :spin="refreshing" />
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-button type="outline" @click="handleUpload">
|
||||
<template #icon><icon-upload /></template>
|
||||
上传文件
|
||||
</a-button>
|
||||
<a-button type="primary" @click="handleCreateFolder">
|
||||
<template #icon><icon-plus /></template>
|
||||
新建文件夹
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { IconRefresh, IconUpload, IconPlus } from '@arco-design/web-vue/es/icon';
|
||||
|
||||
// 定义props
|
||||
const props = defineProps({
|
||||
breadcrumbPath: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
refreshing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
// 定义emit事件
|
||||
const emit = defineEmits(['breadcrumb-click', 'refresh', 'upload', 'create-folder']);
|
||||
|
||||
// 处理面包屑点击
|
||||
const handleBreadcrumbClick = (index) => {
|
||||
emit('breadcrumb-click', index);
|
||||
};
|
||||
|
||||
// 处理刷新
|
||||
const handleRefresh = () => {
|
||||
emit('refresh');
|
||||
};
|
||||
|
||||
// 处理上传文件
|
||||
const handleUpload = () => {
|
||||
emit('upload');
|
||||
};
|
||||
|
||||
// 处理新建文件夹
|
||||
const handleCreateFolder = () => {
|
||||
emit('create-folder');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.file-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
height: 64px;
|
||||
background: var(--color-bg-1);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
color: var(--color-primary);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.clickable:hover {
|
||||
color: var(--color-primary-light-1);
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,669 @@
|
|||
<template>
|
||||
<div class="file-list-container">
|
||||
<!-- 文件列表标题和搜索框在同一行 -->
|
||||
<div v-if="currentFolderId" class="file-header-container">
|
||||
<div class="file-title">
|
||||
<span class="file-list-title">文件列表 ({{ files.length }})</span>
|
||||
</div>
|
||||
<div class="file-search-container">
|
||||
<a-input-search
|
||||
v-model="fileSearchKeyword"
|
||||
placeholder="搜索文件名..."
|
||||
class="file-search-input"
|
||||
@search="handleFileSearch"
|
||||
@input="handleFileSearchInput"
|
||||
@clear="handleFileSearchClear"
|
||||
allow-clear
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-divider size="small" v-if="currentFolderId" />
|
||||
|
||||
<template v-if="!currentFolderId">
|
||||
<div class="initial-state">
|
||||
<icon-folder-add class="initial-icon" />
|
||||
<div class="initial-text">请从左侧选择一个文件夹</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 文件列表加载状态 -->
|
||||
<a-skeleton
|
||||
:loading="loading && currentFolderId"
|
||||
:rows="8"
|
||||
v-if="loading && currentFolderId"
|
||||
animation="pulse"
|
||||
>
|
||||
<template #skeleton>
|
||||
<a-row class="table-data-row" v-for="i in 8" :key="i">
|
||||
<a-col :span="10" class="table-column name-column">
|
||||
<div class="file-main">
|
||||
<div class="w-8 h-8 rounded bg-gray-200 mr-3"></div>
|
||||
<div class="file-name-wrap">
|
||||
<div class="h-5 bg-gray-200 rounded w-1/2 mb-1"></div>
|
||||
<div class="h-4 bg-gray-200 rounded w-1/3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="4" class="table-column type-column">
|
||||
<div class="h-4 bg-gray-200 rounded w-1/3"></div>
|
||||
</a-col>
|
||||
<a-col :span="3" class="table-column size-column">
|
||||
<div class="h-4 bg-gray-200 rounded w-1/2"></div>
|
||||
</a-col>
|
||||
<a-col :span="5" class="table-column time-column">
|
||||
<div class="h-4 bg-gray-200 rounded w-2/3"></div>
|
||||
</a-col>
|
||||
<a-col :span="2" class="table-column action-column">
|
||||
<div class="flex gap-2">
|
||||
<div class="w-6 h-6 rounded bg-gray-200"></div>
|
||||
<div class="w-6 h-6 rounded bg-gray-200"></div>
|
||||
<div class="w-6 h-6 rounded bg-gray-200"></div>
|
||||
<div class="w-6 h-6 rounded bg-gray-200"></div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
</a-skeleton>
|
||||
|
||||
<!-- 文件表格 -->
|
||||
<div class="file-grid-container" v-if="currentFolderId && !loading">
|
||||
<!-- 表头行 -->
|
||||
<a-row class="table-header-row">
|
||||
<a-col :span="10" class="table-column name-column">
|
||||
<div class="sortable-header" @click="handleSortChange('fileName')">
|
||||
<span>文件名</span>
|
||||
<div class="sort-indicator">
|
||||
<div class="sort-arrow up" :class="{ active: props.sortField === 'file_name' && props.sortOrder === 'asc' }"></div>
|
||||
<div class="sort-arrow down" :class="{ active: props.sortField === 'file_name' && props.sortOrder === 'desc' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="4" class="table-column type-column">
|
||||
<div class="sortable-header" @click="handleSortChange('fileType')">
|
||||
<span>类型</span>
|
||||
<div class="sort-indicator">
|
||||
<div class="sort-arrow up" :class="{ active: props.sortField === 'file_type' && props.sortOrder === 'asc' }"></div>
|
||||
<div class="sort-arrow down" :class="{ active: props.sortField === 'file_type' && props.sortOrder === 'desc' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="3" class="table-column size-column">
|
||||
<div class="sortable-header" @click="handleSortChange('fileSize')">
|
||||
<span>大小</span>
|
||||
<div class="sort-indicator">
|
||||
<div class="sort-arrow up" :class="{ active: props.sortField === 'file_size' && props.sortOrder === 'asc' }"></div>
|
||||
<div class="sort-arrow down" :class="{ active: props.sortField === 'file_size' && props.sortOrder === 'desc' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="5" class="table-column time-column">
|
||||
<div class="sortable-header" @click="handleSortChange('uploadTime')">
|
||||
<span>修改时间</span>
|
||||
<div class="sort-indicator">
|
||||
<div class="sort-arrow up" :class="{ active: props.sortField === 'upload_time' && props.sortOrder === 'asc' }"></div>
|
||||
<div class="sort-arrow down" :class="{ active: props.sortField === 'upload_time' && props.sortOrder === 'desc' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="2" class="table-column action-column">操作</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 数据行 -->
|
||||
<a-row
|
||||
v-for="file in files"
|
||||
:key="file.fileId"
|
||||
class="table-data-row"
|
||||
>
|
||||
<!-- 文件名列 -->
|
||||
<a-col :span="10" class="table-column name-column">
|
||||
<div class="file-main">
|
||||
<icon-file :style="{ color: fileColor(getFileExtension(file.fileName || file.name)) }" class="file-icon-large" />
|
||||
<div class="file-name-wrap">
|
||||
<a-typography-title :heading="6" class="file-name">{{ file.fileName || file.name }}</a-typography-title>
|
||||
<div class="file-name-small">{{ file.fileName || file.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 类型列 -->
|
||||
<a-col :span="4" class="table-column type-column">
|
||||
<div class="cell-content">{{ fileTypeText(getFileExtension(file.fileName || file.name)) }}</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 大小列 -->
|
||||
<a-col :span="3" class="table-column size-column">
|
||||
<div class="cell-content">{{ formatFileListSize(file.fileSize || file.size) }}</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 时间列 -->
|
||||
<a-col :span="5" class="table-column time-column">
|
||||
<div class="cell-content">{{ formatUploadTime(file.uploadTime || file.uploadTime) }}</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<a-col :span="2" class="table-column action-column">
|
||||
<div class="file-actions">
|
||||
<a-button
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
tooltip="预览"
|
||||
@click="handlePreview(file)"
|
||||
>
|
||||
<icon-eye />
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
tooltip="下载"
|
||||
@click="handleDownload(file)"
|
||||
>
|
||||
<icon-download />
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
tooltip="重命名"
|
||||
@click="handleEditFile(file)"
|
||||
>
|
||||
<icon-edit />
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
tooltip="删除"
|
||||
@click="handleDelete(file)"
|
||||
class="action-btn delete-btn"
|
||||
>
|
||||
<icon-delete />
|
||||
</a-button>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<a-empty
|
||||
v-if="!loading && currentFolderId && files.length === 0"
|
||||
description="暂无文件"
|
||||
class="empty-state"
|
||||
>
|
||||
<template #image><icon-file /></template>
|
||||
<template #actions>
|
||||
<a-button type="primary" @click="handleUpload">
|
||||
<template #icon><icon-upload /></template>
|
||||
上传文件
|
||||
</a-button>
|
||||
</template>
|
||||
</a-empty>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 导入依赖
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import {
|
||||
IconFolder,
|
||||
IconFile,
|
||||
IconMore,
|
||||
IconDownload,
|
||||
IconDelete,
|
||||
IconEdit,
|
||||
IconEye,
|
||||
IconCopy,
|
||||
IconFolderAdd,
|
||||
IconUpload
|
||||
} from '@arco-design/web-vue/es/icon';
|
||||
|
||||
// 定义props
|
||||
const props = defineProps({
|
||||
files: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
currentFolderId: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
},
|
||||
sortField: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
sortOrder: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
// 定义emit事件
|
||||
const emit = defineEmits([
|
||||
'file-click',
|
||||
'file-download',
|
||||
'file-delete',
|
||||
'file-edit',
|
||||
'file-preview',
|
||||
'file-copy',
|
||||
'file-more',
|
||||
'file-search',
|
||||
'file-search-input',
|
||||
'file-search-clear',
|
||||
'sort-change',
|
||||
'upload'
|
||||
]);
|
||||
|
||||
// 本地状态
|
||||
const fileSearchKeyword = ref('');
|
||||
|
||||
// 监听排序状态变化
|
||||
watch(() => props.sortField, (newVal, oldVal) => {
|
||||
console.log('👀 FileList组件 - sortField变化:', oldVal, '->', newVal);
|
||||
});
|
||||
|
||||
watch(() => props.sortOrder, (newVal, oldVal) => {
|
||||
console.log('👀 FileList组件 - sortOrder变化:', oldVal, '->', newVal);
|
||||
});
|
||||
|
||||
// 处理文件搜索
|
||||
const handleFileSearch = (value) => {
|
||||
emit('file-search', value);
|
||||
};
|
||||
|
||||
// 处理文件搜索输入
|
||||
const handleFileSearchInput = (value) => {
|
||||
emit('file-search-input', value);
|
||||
};
|
||||
|
||||
// 处理文件搜索清除
|
||||
const handleFileSearchClear = () => {
|
||||
emit('file-search-clear');
|
||||
};
|
||||
|
||||
// 处理排序变化
|
||||
const handleSortChange = (field) => {
|
||||
console.log('🎯 FileList组件 - 排序点击:', field);
|
||||
console.log('🎯 FileList组件 - 当前sortField:', props.sortField);
|
||||
console.log('🎯 FileList组件 - 当前sortOrder:', props.sortOrder);
|
||||
emit('sort-change', field);
|
||||
};
|
||||
|
||||
// 处理预览
|
||||
const handlePreview = (file) => {
|
||||
emit('file-preview', file);
|
||||
};
|
||||
|
||||
// 处理下载
|
||||
const handleDownload = (file) => {
|
||||
emit('file-download', file);
|
||||
};
|
||||
|
||||
// 处理编辑文件
|
||||
const handleEditFile = (file) => {
|
||||
emit('file-edit', file);
|
||||
};
|
||||
|
||||
// 处理删除
|
||||
const handleDelete = (file) => {
|
||||
emit('file-delete', file);
|
||||
};
|
||||
|
||||
// 处理上传
|
||||
const handleUpload = () => {
|
||||
emit('upload');
|
||||
};
|
||||
|
||||
// 工具函数 - 获取文件扩展名
|
||||
const getFileExtension = (filename) => {
|
||||
if (!filename) return '';
|
||||
return filename.split('.').pop().toLowerCase();
|
||||
};
|
||||
|
||||
// 工具函数 - 文件颜色
|
||||
const fileColor = (extension) => {
|
||||
const colorMap = {
|
||||
pdf: '#ff4d4f',
|
||||
doc: '#1890ff',
|
||||
docx: '#1890ff',
|
||||
xls: '#52c41a',
|
||||
xlsx: '#52c41a',
|
||||
ppt: '#fa8c16',
|
||||
pptx: '#fa8c16',
|
||||
zip: '#722ed1',
|
||||
rar: '#722ed1',
|
||||
txt: '#8c8c8c',
|
||||
jpg: '#fadb14',
|
||||
jpeg: '#fadb14',
|
||||
png: '#fadb14',
|
||||
gif: '#fadb14',
|
||||
bmp: '#fadb14',
|
||||
webp: '#fadb14'
|
||||
};
|
||||
return colorMap[extension] || '#8c8c8c';
|
||||
};
|
||||
|
||||
// 工具函数 - 文件类型文本
|
||||
const fileTypeText = (extension) => {
|
||||
const typeMap = {
|
||||
pdf: 'PDF文档',
|
||||
doc: 'Word文档',
|
||||
docx: 'Word文档',
|
||||
xls: 'Excel表格',
|
||||
xlsx: 'Excel表格',
|
||||
ppt: 'PPT演示',
|
||||
pptx: 'PPT演示',
|
||||
zip: '压缩文件',
|
||||
rar: '压缩文件',
|
||||
txt: '文本文件',
|
||||
jpg: '图片文件',
|
||||
jpeg: '图片文件',
|
||||
png: '图片文件',
|
||||
gif: '图片文件',
|
||||
bmp: '图片文件',
|
||||
webp: '图片文件'
|
||||
};
|
||||
return typeMap[extension] || '未知文件';
|
||||
};
|
||||
|
||||
// 工具函数 - 格式化文件大小
|
||||
const formatFileListSize = (bytes) => {
|
||||
if (!bytes || bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// 工具函数 - 格式化上传时间
|
||||
const formatUploadTime = (time) => {
|
||||
if (!time) return '';
|
||||
const date = new Date(time);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.file-list-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.file-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-list-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.file-search-container {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-search-input {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.initial-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 64px 0;
|
||||
color: var(--color-text-3);
|
||||
background-color: var(--color-fill-1);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.initial-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
color: var(--color-text-4);
|
||||
}
|
||||
|
||||
.file-grid-container {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
overflow-y: auto;
|
||||
background-color: var(--color-bg-1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 0;
|
||||
min-height: 300px;
|
||||
max-height: calc(100vh - 380px);
|
||||
}
|
||||
|
||||
.table-header-row {
|
||||
padding: 0 16px;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
background-color: var(--color-fill-1);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
font-size: 13px;
|
||||
color: var(--color-text-3);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.table-data-row {
|
||||
display: flex;
|
||||
padding: 0 16px;
|
||||
height: 64px;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
transition: all 0.25s ease;
|
||||
cursor: pointer;
|
||||
background-color: var(--color-bg-1);
|
||||
}
|
||||
|
||||
.table-data-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table-data-row:hover {
|
||||
background-color: rgba(22, 93, 255, 0.1);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.table-column {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.name-column {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.type-column {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.size-column {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.time-column {
|
||||
flex: 1.5;
|
||||
}
|
||||
|
||||
.action-column {
|
||||
flex: 0.5;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sortable-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.sortable-header:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.sort-indicator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 4px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.sort-arrow {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 3px solid transparent;
|
||||
border-right: 3px solid transparent;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.sort-arrow.up {
|
||||
border-bottom: 3px solid var(--color-text-4);
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.sort-arrow.down {
|
||||
border-top: 3px solid var(--color-text-4);
|
||||
}
|
||||
|
||||
.sort-arrow.active {
|
||||
border-bottom-color: var(--color-primary);
|
||||
border-top-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.file-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-icon-large {
|
||||
font-size: 24px;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-name-wrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-name-small {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cell-content {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-2);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.table-data-row:hover .file-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.file-actions .action-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
color: var(--color-text-3);
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.file-actions .action-btn:hover {
|
||||
color: var(--color-primary);
|
||||
background: var(--color-fill-3);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.file-actions .delete-btn:hover {
|
||||
color: var(--color-danger);
|
||||
background: var(--color-danger-light-1);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 64px 0;
|
||||
color: var(--color-text-3);
|
||||
background-color: var(--color-fill-1);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep(.empty-state .arco-btn) {
|
||||
margin-top: 16px;
|
||||
padding: 8px 16px;
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.empty-state .arco-btn:hover) {
|
||||
background-color: var(--color-primary-dark-1);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:deep(.empty-state .arco-btn:active) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,116 @@
|
|||
<template>
|
||||
<div v-if="visible" class="pagination-container">
|
||||
<a-pagination
|
||||
:total="total"
|
||||
:current="current"
|
||||
:page-size="pageSize"
|
||||
:show-total="true"
|
||||
:show-page-size="true"
|
||||
:page-size-options="[10, 20, 50, 100]"
|
||||
:show-jumper="true"
|
||||
:hide-on-single-page="false"
|
||||
size="default"
|
||||
@change="handlePageChange"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 定义props
|
||||
const props = defineProps({
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
current: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
});
|
||||
|
||||
// 定义emit事件
|
||||
const emit = defineEmits(['page-change', 'page-size-change']);
|
||||
|
||||
// 处理页码变化
|
||||
const handlePageChange = (page) => {
|
||||
emit('page-change', page);
|
||||
};
|
||||
|
||||
// 处理每页条数变化
|
||||
const handlePageSizeChange = (pageSize) => {
|
||||
emit('page-size-change', pageSize);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pagination-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--color-bg-1);
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.pagination-container :deep(.arco-pagination) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pagination-container :deep(.arco-pagination-item) {
|
||||
border-radius: 6px;
|
||||
margin: 0 4px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pagination-container :deep(.arco-pagination-item:hover) {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.pagination-container :deep(.arco-pagination-item-active) {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination-container :deep(.arco-pagination-prev),
|
||||
.pagination-container :deep(.arco-pagination-next) {
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pagination-container :deep(.arco-pagination-prev:hover),
|
||||
.pagination-container :deep(.arco-pagination-next:hover) {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.pagination-container :deep(.arco-pagination-size-changer) {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.pagination-container :deep(.arco-pagination-jumper) {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.pagination-container :deep(.arco-pagination-total) {
|
||||
color: var(--color-text-2);
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,557 @@
|
|||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="上传文件"
|
||||
width="620px"
|
||||
:mask-closable="false"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
:confirm-loading="uploading"
|
||||
:ok-disabled="!canUpload"
|
||||
@update:visible="(val) => emit('update:visible', val)"
|
||||
>
|
||||
<a-form :model="uploadForm" ref="uploadFormRef" layout="vertical">
|
||||
<!-- 选择文件 -->
|
||||
<a-form-item
|
||||
label="选择文件"
|
||||
:validate-status="!hasFiles ? 'error' : ''"
|
||||
:help="!hasFiles ? '请选择需要上传的文件' : ''"
|
||||
>
|
||||
<div class="upload-container">
|
||||
<!-- 上传按钮 -->
|
||||
<a-upload
|
||||
ref="uploadRef"
|
||||
:key="visible ? 'upload-open' : 'upload-closed'"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
@change="handleFileChange"
|
||||
:accept="allowedFileTypes"
|
||||
multiple
|
||||
>
|
||||
<a-button type="primary" class="upload-btn">
|
||||
<icon-upload />
|
||||
点击选择文件
|
||||
</a-button>
|
||||
</a-upload>
|
||||
|
||||
<!-- 文件类型提示 -->
|
||||
<div class="upload-hint">
|
||||
支持 {{ allowedFileTypesText }} 等格式,单个文件不超过 {{ maxFileSizeText }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件列表 -->
|
||||
<div class="upload-file-list" v-if="fileListTemp.length > 0">
|
||||
<div
|
||||
class="upload-file-item"
|
||||
v-for="file in fileListTemp"
|
||||
:key="file.uid"
|
||||
:class="{ 'file-error': file.error }"
|
||||
>
|
||||
<div class="file-info">
|
||||
<icon-file
|
||||
:style="{ color: fileColor(getFileExtension(file.name)) }"
|
||||
class="file-icon"
|
||||
/>
|
||||
<div class="file-details">
|
||||
<div class="file-name">{{ file.name }}</div>
|
||||
<div class="file-meta">
|
||||
{{ formatFileSize(file.size) }}
|
||||
<span v-if="file.error" class="error-text">{{ file.error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="file-progress" v-if="file.status === 'uploading'">
|
||||
<a-progress
|
||||
:percent="file.percent || 0"
|
||||
size="small"
|
||||
:status="file.percent === 100 ? 'success' : 'processing'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="file-actions">
|
||||
<a-button
|
||||
v-if="file.status !== 'uploading'"
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
@click="removeFile(file)"
|
||||
class="remove-btn"
|
||||
>
|
||||
<icon-delete />
|
||||
</a-button>
|
||||
<a-button
|
||||
v-else
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
@click="cancelUpload(file)"
|
||||
class="cancel-btn"
|
||||
>
|
||||
<icon-stop />
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 目标文件夹选择 -->
|
||||
<a-form-item
|
||||
label="上传至目录"
|
||||
field="folderId"
|
||||
:rules="[{ required: true, message: '请选择目标文件夹' }]"
|
||||
>
|
||||
<a-select
|
||||
v-model="uploadForm.folderId"
|
||||
placeholder="请选择目标文件夹"
|
||||
allow-clear
|
||||
>
|
||||
<a-option value="0">根目录</a-option>
|
||||
<a-option
|
||||
v-for="folder in folderList"
|
||||
:key="folder.id"
|
||||
:value="folder.id"
|
||||
>
|
||||
{{ folder.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { IconUpload, IconFile, IconDelete, IconStop } from '@arco-design/web-vue/es/icon'
|
||||
import { uploadFileApi } from '@/apis/bussiness'
|
||||
import axios from 'axios'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
folderList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
currentFolderId: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits([
|
||||
'update:visible',
|
||||
'upload-success'
|
||||
])
|
||||
|
||||
// 响应式数据
|
||||
const uploadForm = reactive({
|
||||
folderId: ''
|
||||
})
|
||||
|
||||
const fileListTemp = ref([])
|
||||
const uploadFormRef = ref(null)
|
||||
const uploadRef = ref(null)
|
||||
const uploading = ref(false)
|
||||
const cancelTokens = ref({})
|
||||
|
||||
// 常量
|
||||
const allowedFileTypes = '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.zip,.txt,.jpg,.jpeg,.png,.gif,.bmp,.webp'
|
||||
const allowedFileTypesText = 'PDF, Word, Excel, PPT, 压缩文件, 文本文件, 图片文件'
|
||||
const maxFileSize = 1000 * 1024 * 1024 // 1000MB
|
||||
const maxFileSizeText = '1000MB'
|
||||
|
||||
// 计算属性
|
||||
const hasFiles = computed(() => {
|
||||
const validFiles = fileListTemp.value.filter(file => {
|
||||
return !file.error && file.status !== 'removed' && file.status !== 'canceled'
|
||||
})
|
||||
return validFiles.length > 0
|
||||
})
|
||||
|
||||
const canUpload = computed(() => {
|
||||
return hasFiles.value && !uploading.value && uploadForm.folderId
|
||||
})
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(() => props.visible, (visible) => {
|
||||
if (visible) {
|
||||
// 重置表单
|
||||
uploadForm.folderId = props.currentFolderId || ''
|
||||
fileListTemp.value = []
|
||||
|
||||
// 重置上传组件
|
||||
if (uploadRef.value) {
|
||||
try {
|
||||
uploadRef.value.reset()
|
||||
} catch (error) {
|
||||
console.log('重置上传组件时出错:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 文件变化处理
|
||||
const handleFileChange = (info) => {
|
||||
if (!info || !Array.isArray(info) || !props.visible) {
|
||||
return
|
||||
}
|
||||
|
||||
const fileList = info
|
||||
if (fileList.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前已存在的文件UID列表,用于去重
|
||||
const existingUids = fileListTemp.value.map(f => f.uid)
|
||||
|
||||
// 处理新选择的文件
|
||||
fileList.forEach((file) => {
|
||||
// 检查文件是否已存在(去重)
|
||||
if (existingUids.includes(file.uid)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 确保文件对象有正确的属性
|
||||
const fileObj = {
|
||||
uid: file.uid,
|
||||
name: file.name,
|
||||
size: file.size || file.file?.size || 0,
|
||||
type: file.type || file.file?.type || '',
|
||||
status: 'ready',
|
||||
error: '',
|
||||
originFileObj: file.file || file
|
||||
}
|
||||
|
||||
// 验证文件
|
||||
const isValid = validateFile(fileObj)
|
||||
|
||||
if (isValid) {
|
||||
fileListTemp.value.push(fileObj)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 文件验证
|
||||
const validateFile = (file) => {
|
||||
file.error = ''
|
||||
|
||||
// 验证文件类型
|
||||
const ext = getFileExtension(file.name).toLowerCase()
|
||||
const allowedExts = allowedFileTypes
|
||||
.split(',')
|
||||
.map(type => type.toLowerCase().replace(/^\./, ''))
|
||||
|
||||
if (!allowedExts.includes(ext)) {
|
||||
file.error = `不支持的文件类型,支持: ${allowedFileTypesText}`
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证文件大小
|
||||
if (file.size > maxFileSize) {
|
||||
file.error = `文件过大,最大支持 ${maxFileSizeText}`
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 获取文件扩展名
|
||||
const getFileExtension = (fileName) => {
|
||||
const lastDotIndex = fileName.lastIndexOf('.')
|
||||
return lastDotIndex > 0 ? fileName.slice(lastDotIndex + 1) : ''
|
||||
}
|
||||
|
||||
// 获取文件图标颜色
|
||||
const fileColor = (extension) => {
|
||||
const colorMap = {
|
||||
pdf: '#ff4d4f',
|
||||
doc: '#1890ff',
|
||||
docx: '#1890ff',
|
||||
xls: '#52c41a',
|
||||
xlsx: '#52c41a',
|
||||
ppt: '#faad14',
|
||||
pptx: '#faad14',
|
||||
zip: '#722ed1',
|
||||
txt: '#8c8c8c',
|
||||
jpg: '#52c41a',
|
||||
jpeg: '#52c41a',
|
||||
png: '#1890ff',
|
||||
gif: '#faad14',
|
||||
bmp: '#722ed1',
|
||||
webp: '#13c2c2'
|
||||
}
|
||||
return colorMap[extension.toLowerCase()] || 'var(--color-text-3)'
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (fileSize) => {
|
||||
if (fileSize === 0) return '0 B'
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let size = fileSize
|
||||
let unitIndex = 0
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024
|
||||
unitIndex++
|
||||
}
|
||||
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
// 移除文件
|
||||
const removeFile = (file) => {
|
||||
fileListTemp.value = fileListTemp.value.filter(f => f.uid !== file.uid)
|
||||
|
||||
// 如果是正在上传的文件,取消请求
|
||||
if (file.status === 'uploading' && cancelTokens.value[file.uid]) {
|
||||
cancelTokens.value[file.uid].cancel('上传已取消')
|
||||
delete cancelTokens.value[file.uid]
|
||||
}
|
||||
}
|
||||
|
||||
// 取消上传
|
||||
const cancelUpload = (file) => {
|
||||
if (cancelTokens.value[file.uid]) {
|
||||
cancelTokens.value[file.uid].cancel('上传已取消')
|
||||
file.status = 'canceled'
|
||||
}
|
||||
}
|
||||
|
||||
// 提交上传
|
||||
const handleSubmit = async () => {
|
||||
// 过滤有效文件
|
||||
const validFiles = fileListTemp.value.filter(file =>
|
||||
!file.error && file.status !== 'removed' && file.status !== 'canceled'
|
||||
)
|
||||
|
||||
if (validFiles.length === 0) {
|
||||
Message.warning('请选择有效的文件')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件夹ID
|
||||
if (!uploadForm.folderId) {
|
||||
Message.warning('请选择目标文件夹')
|
||||
return
|
||||
}
|
||||
|
||||
uploading.value = true
|
||||
let hasError = false
|
||||
let hasFileExists = false
|
||||
|
||||
for (const fileItem of validFiles) {
|
||||
// 获取原始File对象
|
||||
const realFile = fileItem.originFileObj || fileItem
|
||||
|
||||
if (!realFile) {
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
|
||||
fileItem.status = 'uploading'
|
||||
fileItem.percent = 0
|
||||
|
||||
// 创建取消令牌
|
||||
const source = axios.CancelToken.source()
|
||||
cancelTokens.value[fileItem.uid] = source
|
||||
|
||||
// 调用API
|
||||
const result = await uploadFileApi(
|
||||
realFile,
|
||||
Number(uploadForm.folderId),
|
||||
(progressEvent) => {
|
||||
if (progressEvent.lengthComputable) {
|
||||
fileItem.percent = Math.round((progressEvent.loaded / progressEvent.total) * 100)
|
||||
}
|
||||
},
|
||||
source.token
|
||||
)
|
||||
|
||||
// 检查上传结果
|
||||
if (result.code === 200) {
|
||||
fileItem.status = 'success'
|
||||
fileItem.percent = 100
|
||||
} else if (result.code === 400 && result.msg && result.msg.includes('已存在')) {
|
||||
// 文件已存在的情况
|
||||
fileItem.status = 'error'
|
||||
fileItem.error = '文件已存在'
|
||||
hasFileExists = true
|
||||
} else {
|
||||
fileItem.status = 'error'
|
||||
fileItem.error = result.msg || '上传失败'
|
||||
hasError = true
|
||||
}
|
||||
}
|
||||
|
||||
// 根据结果显示相应的消息
|
||||
if (hasFileExists && !hasError) {
|
||||
Message.warning('文件已存在')
|
||||
} else if (hasError) {
|
||||
Message.error('上传失败')
|
||||
} else {
|
||||
Message.success('上传成功')
|
||||
emit('upload-success')
|
||||
}
|
||||
|
||||
handleCancel()
|
||||
}
|
||||
|
||||
// 取消上传
|
||||
const handleCancel = () => {
|
||||
// 取消所有正在进行的上传
|
||||
Object.values(cancelTokens.value).forEach(source => {
|
||||
source.cancel('上传已取消')
|
||||
})
|
||||
|
||||
// 重置所有状态
|
||||
emit('update:visible', false)
|
||||
uploadForm.folderId = props.currentFolderId || ''
|
||||
fileListTemp.value = []
|
||||
cancelTokens.value = {}
|
||||
uploading.value = false
|
||||
|
||||
// 清空上传组件
|
||||
if (uploadRef.value) {
|
||||
uploadRef.value.reset()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 上传文件相关样式 */
|
||||
.upload-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
align-self: flex-start;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
color: var(--color-text-3);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
padding: 8px 12px;
|
||||
background: var(--color-fill-2);
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid var(--color-primary-light-3);
|
||||
}
|
||||
|
||||
.upload-file-list {
|
||||
margin-top: 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
background: var(--color-bg-1);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.upload-file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-fill-1);
|
||||
}
|
||||
|
||||
&.file-error {
|
||||
background: rgba(255, 77, 79, 0.05);
|
||||
border-left: 3px solid #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-details {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #ff4d4f;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.file-progress {
|
||||
margin: 0 16px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
color: var(--color-text-3);
|
||||
|
||||
&:hover {
|
||||
color: #ff4d4f;
|
||||
background: rgba(255, 77, 79, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
color: var(--color-text-3);
|
||||
|
||||
&:hover {
|
||||
color: #faad14;
|
||||
background: rgba(250, 173, 20, 0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue