feat: 增强用户界面配置选项

This commit is contained in:
tiantian guo
2025-09-05 14:48:09 +08:00
parent 920c89998a
commit 04d965c832
12 changed files with 1283 additions and 33 deletions
+756
View File
@@ -0,0 +1,756 @@
<template>
<div class="resource-management">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-left">
<h2>资源管理</h2>
<el-breadcrumb separator="/">
<el-breadcrumb-item
v-for="(item, index) in breadcrumbList"
:key="index"
@click="handleBreadcrumbClick(item, index)"
:class="{ 'is-link': index < breadcrumbList.length - 1 }"
>
{{ item.name }}
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="header-right">
<el-button type="primary" @click="handleUpload">
<el-icon><Upload /></el-icon>
上传文件
</el-button>
<el-button @click="handleCreateDir">
<el-icon><FolderAdd /></el-icon>
新建文件夹
</el-button>
<el-button @click="handleRefresh">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
<!-- 搜索和筛选 -->
<div class="search-section">
<el-form :model="searchForm" inline>
<el-form-item label="关键词">
<el-input
v-model="searchForm.keyword"
placeholder="请输入文件名或关键词"
clearable
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item label="文件类型">
<el-select v-model="searchForm.file_type" placeholder="请选择" clearable>
<el-option label="图片" value="image" />
<el-option label="文档" value="document" />
<el-option label="视频" value="video" />
<el-option label="音频" value="audio" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
<el-form-item label="文件大小">
<el-input-number
v-model="searchForm.min_size"
placeholder="最小"
:min="0"
controls-position="right"
/>
<span style="margin: 0 8px">-</span>
<el-input-number
v-model="searchForm.max_size"
placeholder="最大"
:min="0"
controls-position="right"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleResetSearch">
<el-icon><RefreshLeft /></el-icon>
重置
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 工具栏 -->
<div class="toolbar">
<div class="toolbar-left">
<el-checkbox
v-model="showHiddenFiles"
@change="handleShowHiddenChange"
>
显示隐藏文件
</el-checkbox>
<el-checkbox
v-model="recursiveMode"
@change="handleRecursiveChange"
>
递归显示
</el-checkbox>
</div>
<div class="toolbar-right">
<el-button-group>
<el-button
:type="viewMode === 'list' ? 'primary' : ''"
@click="viewMode = 'list'"
>
<el-icon><List /></el-icon>
</el-button>
<el-button
:type="viewMode === 'grid' ? 'primary' : ''"
@click="viewMode = 'grid'"
>
<el-icon><Grid /></el-icon>
</el-button>
</el-button-group>
</div>
</div>
<!-- 文件列表 -->
<div class="file-list">
<el-table
v-if="viewMode === 'list'"
:data="fileList"
v-loading="loading"
@selection-change="handleSelectionChange"
@row-dblclick="handleRowDoubleClick"
row-key="path"
>
<el-table-column type="selection" width="55" />
<el-table-column label="名称" min-width="200">
<template #default="{ row }">
<div class="file-name">
<el-icon class="file-icon">
<Folder v-if="row.is_directory" />
<Document v-else />
</el-icon>
<span>{{ row.name }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="大小" width="120">
<template #default="{ row }">
<span v-if="!row.is_directory">{{ formatFileSize(row.size) }}</span>
</template>
</el-table-column>
<el-table-column label="类型" width="100">
<template #default="{ row }">
<el-tag v-if="row.file_type" size="small">{{ row.file_type }}</el-tag>
</template>
</el-table-column>
<el-table-column label="修改时间" width="180">
<template #default="{ row }">
{{ formatDate(row.modified_time) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button
v-if="!row.is_directory"
type="primary"
link
@click="handleDownload(row)"
>
下载
</el-button>
<el-button type="primary" link @click="handleRename(row)">
重命名
</el-button>
<el-button type="primary" link @click="handleMove(row)">
移动
</el-button>
<el-button type="primary" link @click="handleCopy(row)">
复制
</el-button>
<el-button type="danger" link @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 网格视图 -->
<div v-else class="grid-view">
<div
v-for="item in fileList"
:key="item.path"
class="grid-item"
@dblclick="handleItemDoubleClick(item)"
>
<div class="item-icon">
<el-icon v-if="item.is_directory" size="48">
<Folder />
</el-icon>
<el-icon v-else size="48">
<Document />
</el-icon>
</div>
<div class="item-name">{{ item.name }}</div>
<div class="item-size" v-if="!item.is_directory">
{{ formatFileSize(item.size) }}
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div class="pagination">
<el-pagination
v-model:current-page="pagination.page_no"
v-model:page-size="pagination.page_size"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 上传对话框 -->
<el-dialog
v-model="uploadDialogVisible"
title="上传文件"
width="500px"
:before-close="handleUploadClose"
>
<el-upload
ref="uploadRef"
:auto-upload="false"
:multiple="true"
:file-list="uploadFileList"
@change="handleUploadChange"
drag
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处,或<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
支持多文件上传,单个文件不超过100MB
</div>
</template>
</el-upload>
<template #footer>
<el-button @click="uploadDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleUploadConfirm" :loading="uploading">
确定上传
</el-button>
</template>
</el-dialog>
<!-- 新建文件夹对话框 -->
<el-dialog
v-model="createDirDialogVisible"
title="新建文件夹"
width="400px"
>
<el-form :model="createDirForm" label-width="80px">
<el-form-item label="文件夹名" required>
<el-input
v-model="createDirForm.dir_name"
placeholder="请输入文件夹名称"
@keyup.enter="handleCreateDirConfirm"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="createDirDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleCreateDirConfirm">确定</el-button>
</template>
</el-dialog>
<!-- 重命名对话框 -->
<el-dialog
v-model="renameDialogVisible"
title="重命名"
width="400px"
>
<el-form :model="renameForm" label-width="80px">
<el-form-item label="新名称" required>
<el-input
v-model="renameForm.new_name"
placeholder="请输入新名称"
@keyup.enter="handleRenameConfirm"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="renameDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleRenameConfirm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Upload,
FolderAdd,
Refresh,
Search,
RefreshLeft,
List,
Grid,
Folder,
Document,
UploadFilled
} from '@element-plus/icons-vue'
import { ResourceAPI, type ResourceItem, type ResourceListQuery, type ResourceSearchQuery } from '@/api/resource/resource'
// 响应式数据
const loading = ref(false)
const fileList = ref<ResourceItem[]>([])
const selectedItems = ref<ResourceItem[]>([])
const currentPath = ref('/')
const breadcrumbList = ref([{ name: '根目录', path: '/' }])
const showHiddenFiles = ref(false)
const recursiveMode = ref(false)
const viewMode = ref<'list' | 'grid'>('list')
const total = ref(0)
// 分页
const pagination = reactive({
page_no: 1,
page_size: 20
})
// 搜索表单
const searchForm = reactive<ResourceSearchQuery>({
keyword: '',
file_type: '',
min_size: undefined,
max_size: undefined
})
// 对话框状态
const uploadDialogVisible = ref(false)
const createDirDialogVisible = ref(false)
const renameDialogVisible = ref(false)
const uploading = ref(false)
// 上传相关
const uploadRef = ref()
const uploadFileList = ref<any[]>([])
// 表单数据
const createDirForm = reactive({
dir_name: ''
})
const renameForm = reactive({
new_name: '',
old_path: ''
})
// 计算属性
const currentQuery = computed(() => ({
path: currentPath.value,
recursive: recursiveMode.value,
include_hidden: showHiddenFiles.value
}))
// 方法
const loadFileList = async () => {
try {
loading.value = true
const response = await ResourceAPI.getResourceList(currentQuery.value)
fileList.value = response.data.data || []
total.value = fileList.value.length
} catch (error) {
ElMessage.error('加载文件列表失败')
console.error('Load file list error:', error)
} finally {
loading.value = false
}
}
const handleBreadcrumbClick = (item: any, index: number) => {
if (index < breadcrumbList.value.length - 1) {
currentPath.value = item.path
updateBreadcrumb()
loadFileList()
}
}
const updateBreadcrumb = () => {
const pathParts = currentPath.value.split('/').filter(Boolean)
breadcrumbList.value = [
{ name: '根目录', path: '/' },
...pathParts.map((part, index) => ({
name: part,
path: '/' + pathParts.slice(0, index + 1).join('/')
}))
]
}
const handleRowDoubleClick = (row: ResourceItem) => {
if (row.is_directory) {
currentPath.value = row.path
updateBreadcrumb()
loadFileList()
}
}
const handleItemDoubleClick = (item: ResourceItem) => {
if (item.is_directory) {
currentPath.value = item.path
updateBreadcrumb()
loadFileList()
}
}
const handleSelectionChange = (selection: ResourceItem[]) => {
selectedItems.value = selection
}
const handleUpload = () => {
uploadDialogVisible.value = true
uploadFileList.value = []
}
const handleUploadChange = (file: any, fileList: any[]) => {
uploadFileList.value = fileList
}
const handleUploadConfirm = async () => {
if (uploadFileList.value.length === 0) {
ElMessage.warning('请选择要上传的文件')
return
}
try {
uploading.value = true
const formData = new FormData()
uploadFileList.value.forEach((file: any) => {
formData.append('file', file.raw)
})
formData.append('target_path', currentPath.value)
await ResourceAPI.uploadFile(formData)
ElMessage.success('上传成功')
uploadDialogVisible.value = false
loadFileList()
} catch (error) {
ElMessage.error('上传失败')
console.error('Upload error:', error)
} finally {
uploading.value = false
}
}
const handleUploadClose = () => {
uploadDialogVisible.value = false
uploadFileList.value = []
}
const handleCreateDir = () => {
createDirForm.dir_name = ''
createDirDialogVisible.value = true
}
const handleCreateDirConfirm = async () => {
if (!createDirForm.dir_name.trim()) {
ElMessage.warning('请输入文件夹名称')
return
}
try {
await ResourceAPI.createDirectory({
parent_path: currentPath.value,
dir_name: createDirForm.dir_name.trim()
})
ElMessage.success('创建成功')
createDirDialogVisible.value = false
loadFileList()
} catch (error) {
ElMessage.error('创建失败')
console.error('Create directory error:', error)
}
}
const handleRefresh = () => {
loadFileList()
}
const handleSearch = async () => {
try {
loading.value = true
const response = await ResourceAPI.searchResource(searchForm)
fileList.value = response.data.data || []
total.value = fileList.value.length
} catch (error) {
ElMessage.error('搜索失败')
console.error('Search error:', error)
} finally {
loading.value = false
}
}
const handleResetSearch = () => {
Object.assign(searchForm, {
keyword: '',
file_type: '',
min_size: undefined,
max_size: undefined
})
loadFileList()
}
const handleShowHiddenChange = () => {
loadFileList()
}
const handleRecursiveChange = () => {
loadFileList()
}
const handleDownload = async (item: ResourceItem) => {
try {
const response = await ResourceAPI.downloadFile(item.path)
const blob = response.data
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = item.name
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
} catch (error) {
ElMessage.error('下载失败')
console.error('Download error:', error)
}
}
const handleRename = (item: ResourceItem) => {
renameForm.old_path = item.path
renameForm.new_name = item.name
renameDialogVisible.value = true
}
const handleRenameConfirm = async () => {
if (!renameForm.new_name.trim()) {
ElMessage.warning('请输入新名称')
return
}
try {
await ResourceAPI.renameResource({
old_path: renameForm.old_path,
new_name: renameForm.new_name.trim()
})
ElMessage.success('重命名成功')
renameDialogVisible.value = false
loadFileList()
} catch (error) {
ElMessage.error('重命名失败')
console.error('Rename error:', error)
}
}
const handleMove = (item: ResourceItem) => {
// TODO: 实现移动功能
ElMessage.info('移动功能待实现')
}
const handleCopy = (item: ResourceItem) => {
// TODO: 实现复制功能
ElMessage.info('复制功能待实现')
}
const handleDelete = async (item: ResourceItem) => {
try {
await ElMessageBox.confirm(
`确定要删除 ${item.name} 吗?`,
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
await ResourceAPI.deleteResource([item.path])
ElMessage.success('删除成功')
loadFileList()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
console.error('Delete error:', error)
}
}
}
const handleSizeChange = (size: number) => {
pagination.page_size = size
loadFileList()
}
const handleCurrentChange = (page: number) => {
pagination.page_no = page
loadFileList()
}
// 工具函数
const formatFileSize = (size?: number) => {
if (!size) return '-'
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let unitIndex = 0
let fileSize = size
while (fileSize >= 1024 && unitIndex < units.length - 1) {
fileSize /= 1024
unitIndex++
}
return `${fileSize.toFixed(1)} ${units[unitIndex]}`
}
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString()
}
// 生命周期
onMounted(() => {
loadFileList()
})
</script>
<style lang="scss" scoped>
.resource-management {
padding: 20px;
background: #f5f5f5;
min-height: 100vh;
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
.header-left {
h2 {
margin: 0 0 10px 0;
color: #303133;
}
}
.header-right {
display: flex;
gap: 10px;
}
}
.search-section {
margin-bottom: 20px;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 15px 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
.toolbar-left {
display: flex;
gap: 20px;
}
}
.file-list {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
.file-name {
display: flex;
align-items: center;
gap: 8px;
.file-icon {
color: #409eff;
}
}
.grid-view {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 20px;
padding: 20px;
.grid-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 15px;
border: 1px solid #e4e7ed;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #409eff;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
}
.item-icon {
margin-bottom: 10px;
color: #409eff;
}
.item-name {
font-size: 14px;
text-align: center;
word-break: break-all;
margin-bottom: 5px;
}
.item-size {
font-size: 12px;
color: #909399;
}
}
}
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: center;
}
}
:deep(.el-breadcrumb__item) {
&.is-link {
cursor: pointer;
color: #409eff;
&:hover {
color: #66b1ff;
}
}
}
</style>
@@ -1,13 +0,0 @@
<template>
<div>
<h1>资源管理开发中,暂未提供</h1>
</div>
</template>
<script setup lang="ts">
</script>
<style lang="scss" scoped>
</style>