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
+276 -1
View File
@@ -1 +1,276 @@
// 待开发
import request from "@/utils/request";
export const ResourceAPI = {
/**
* 获取目录列表
* @param query 查询参数
*/
getResourceList(query: ResourceListQuery) {
return request<ApiResponse<ResourceItem[]>>({
url: `/resource/resource/list`,
method: "get",
params: query,
});
},
/**
* 搜索资源
* @param body 搜索条件
*/
searchResource(body: ResourceSearchQuery) {
return request<ApiResponse<ResourceItem[]>>({
url: `/resource/resource/search`,
method: "post",
data: body,
});
},
/**
* 上传文件
* @param formData 文件数据
*/
uploadFile(formData: FormData) {
return request<ApiResponse<UploadFilePath>>({
url: `/resource/resource/upload`,
method: "post",
data: formData,
headers: { "Content-Type": "multipart/form-data" },
});
},
/**
* 下载文件
* @param path 文件路径
*/
downloadFile(path: string) {
return request<Blob>({
url: `/resource/resource/download`,
method: "get",
params: { path },
responseType: "blob",
});
},
/**
* 删除文件或目录
* @param body 文件路径数组
*/
deleteResource(body: string[]) {
return request<ApiResponse>({
url: `/resource/resource/delete`,
method: "delete",
data: body,
});
},
/**
* 移动文件或目录
* @param body 移动参数
*/
moveResource(body: ResourceMoveQuery) {
return request<ApiResponse>({
url: `/resource/resource/move`,
method: "post",
data: body,
});
},
/**
* 复制文件或目录
* @param body 复制参数
*/
copyResource(body: ResourceCopyQuery) {
return request<ApiResponse>({
url: `/resource/resource/copy`,
method: "post",
data: body,
});
},
/**
* 重命名文件或目录
* @param body 重命名参数
*/
renameResource(body: ResourceRenameQuery) {
return request<ApiResponse>({
url: `/resource/resource/rename`,
method: "post",
data: body,
});
},
/**
* 创建目录
* @param body 创建目录参数
*/
createDirectory(body: ResourceCreateDirQuery) {
return request<ApiResponse>({
url: `/resource/resource/create-dir`,
method: "post",
data: body,
});
},
/**
* 获取资源统计信息
*/
getResourceStats() {
return request<ApiResponse<ResourceStats>>({
url: `/resource/resource/stats`,
method: "get",
});
},
/**
* 导出资源列表
* @param body 导出条件
*/
exportResource(body: ResourceSearchQuery) {
return request<Blob>({
url: `/resource/resource/export`,
method: "post",
data: body,
responseType: "blob",
});
},
};
export default ResourceAPI;
/**
* 资源列表查询参数
*/
export interface ResourceListQuery {
/** 目录路径 */
path: string;
/** 递归获取 */
recursive?: boolean;
/** 包含隐藏文件 */
include_hidden?: boolean;
}
/**
* 资源搜索查询参数
*/
export interface ResourceSearchQuery {
/** 关键词 */
keyword?: string;
/** 文件类型 */
file_type?: string;
/** 资源类型 */
resource_type?: string;
/** 最小文件大小 */
min_size?: number;
/** 最大文件大小 */
max_size?: number;
/** 开始时间 */
start_date?: string;
/** 结束时间 */
end_date?: string;
/** 文件扩展名 */
extensions?: string[];
/** 包含隐藏文件 */
include_hidden?: boolean;
/** 最大深度 */
max_depth?: number;
}
/**
* 资源项信息
*/
export interface ResourceItem {
/** 文件/目录名称 */
name: string;
/** 完整路径 */
path: string;
/** 是否为目录 */
is_directory: boolean;
/** 文件大小(字节) */
size?: number;
/** 文件扩展名 */
extension?: string;
/** 修改时间 */
modified_time: string;
/** 创建时间 */
created_time: string;
/** 是否为隐藏文件 */
is_hidden: boolean;
/** 文件类型 */
file_type?: string;
/** 资源类型 */
resource_type?: string;
/** 文件URL(如果是图片等可预览文件) */
file_url?: string;
/** 缩略图URL */
thumbnail_url?: string;
}
/**
* 资源移动参数
*/
export interface ResourceMoveQuery {
/** 源路径 */
source_path: string;
/** 目标路径 */
target_path: string;
/** 是否覆盖 */
overwrite?: boolean;
}
/**
* 资源复制参数
*/
export interface ResourceCopyQuery {
/** 源路径 */
source_path: string;
/** 目标路径 */
target_path: string;
/** 是否覆盖 */
overwrite?: boolean;
}
/**
* 资源重命名参数
*/
export interface ResourceRenameQuery {
/** 原路径 */
old_path: string;
/** 新名称 */
new_name: string;
}
/**
* 创建目录参数
*/
export interface ResourceCreateDirQuery {
/** 父目录路径 */
parent_path: string;
/** 目录名称 */
dir_name: string;
}
/**
* 资源统计信息
*/
export interface ResourceStats {
/** 总文件数 */
total_files: number;
/** 总目录数 */
total_directories: number;
/** 总大小(字节) */
total_size: number;
/** 按文件类型统计 */
file_type_stats: Array<{
file_type: string;
count: number;
size: number;
}>;
/** 按资源类型统计 */
resource_type_stats: Array<{
resource_type: string;
count: number;
size: number;
}>;
/** 最近修改的文件 */
recent_files: ResourceItem[];
}
+4
View File
@@ -15,6 +15,8 @@ export const DICT_CACHE_KEY = "dict_cache";
export const SHOW_TAGS_VIEW_KEY = "showTagsView";
export const SHOW_APP_LOGO_KEY = "showAppLogo";
export const SHOW_WATERMARK_KEY = "showWatermark";
export const SHOW_SETTINGS_KEY = "showSettings";
export const SHOW_DESKTOP_TOOLS_KEY = "showDesktopTools";
export const LAYOUT_KEY = "layout";
export const SIDEBAR_COLOR_SCHEME_KEY = "sidebarColorScheme";
export const THEME_KEY = "theme";
@@ -39,6 +41,8 @@ export const SETTINGS_KEYS = {
SHOW_TAGS_VIEW: SHOW_TAGS_VIEW_KEY,
SHOW_APP_LOGO: SHOW_APP_LOGO_KEY,
SHOW_WATERMARK: SHOW_WATERMARK_KEY,
SHOW_SETTINGS: SHOW_SETTINGS_KEY,
SHOW_DESKTOP_TOOLS: SHOW_DESKTOP_TOOLS_KEY,
SIDEBAR_COLOR_SCHEME: SIDEBAR_COLOR_SCHEME_KEY,
LAYOUT: LAYOUT_KEY,
THEME_COLOR: THEME_COLOR_KEY,
+8
View File
@@ -136,6 +136,14 @@ export default {
profile: "User Profile",
config: "Config Center",
tour: "Project Tour",
showSettings: "Show Project Settings",
hideSettings: "Hide Project Settings",
showDesktopTools: "Show Desktop Tools",
hideDesktopTools: "Hide Desktop Tools",
settingsEnabled: "Project settings button enabled",
settingsDisabled: "Project settings button disabled",
desktopToolsEnabled: "Desktop tools enabled",
desktopToolsDisabled: "Desktop tools disabled",
refresh: "Refresh",
close: "Close",
closeLeft: "Close Left",
+8
View File
@@ -136,6 +136,14 @@ export default {
profile: "个人中心",
config: "配置中心",
tour: "项目引导",
showSettings: "打开项目配置",
hideSettings: "隐藏项目配置",
showDesktopTools: "打开顶部工具",
hideDesktopTools: "隐藏顶部工具",
settingsEnabled: "已打开项目配置按钮",
settingsDisabled: "已隐藏项目配置按钮",
desktopToolsEnabled: "已打开顶部工具",
desktopToolsDisabled: "已隐藏顶部工具",
refresh: "刷新",
close: "关闭",
closeLeft: "关闭左侧",
@@ -1,7 +1,7 @@
<template>
<div ref="navbar-actions" :class="['navbar-actions', navbarActionsClass]">
<!-- 桌面端工具项 -->
<template v-if="isDesktop">
<template v-if="isDesktop && settingStore.showDesktopTools">
<!-- 搜索 -->
<div class="navbar-actions__item">
<MenuSearch />
@@ -67,6 +67,14 @@
<el-icon><Position /></el-icon>
{{ t("navbar.tour") }}
</el-dropdown-item>
<el-dropdown-item divided @click="handleToggleSettings">
<el-icon><Tools /></el-icon>
{{ settingStore.showSettings ? t('navbar.hideSettings') : t('navbar.showSettings') }}
</el-dropdown-item>
<el-dropdown-item @click="handleToggleDesktopTools">
<el-icon><Operation /></el-icon>
{{ settingStore.showDesktopTools ? t('navbar.hideDesktopTools') : t('navbar.showDesktopTools') }}
</el-dropdown-item>
<el-dropdown-item divided @click="handlelockScreen">
<el-icon><Lock /></el-icon>
{{ t("navbar.lock") }}
@@ -179,6 +187,26 @@ function handleTourClick() {
}
}
/**
* 切换项目配置显示状态
*/
function handleToggleSettings() {
settingStore.updateSetting('showSettings', !settingStore.showSettings);
ElMessage.success(
settingStore.showSettings ? t('navbar.settingsEnabled') : t('navbar.settingsDisabled')
);
}
/**
* 切换桌面端工具项打开状态
*/
function handleToggleDesktopTools() {
settingStore.updateSetting('showDesktopTools', !settingStore.showDesktopTools);
ElMessage.success(
settingStore.showDesktopTools ? t('navbar.desktopToolsEnabled') : t('navbar.desktopToolsDisabled')
);
}
/**
* 锁屏
*/
@@ -68,16 +68,44 @@
<!-- 系统主题 -->
<section class="config-section">
<el-divider>{{ t("settings.systemTheme") }}</el-divider>
<div class="config-item flex-x-between">
<span class="text-xs">{{ t("settings.themeColor") }}</span>
<el-color-picker
v-model="selectedThemeColor"
:predefine="colorPresets"
popper-class="theme-picker-dropdown"
/>
<div class="config-item">
<div class="flex-x-between mb-3">
<span class="text-xs">{{ t("settings.themeColor") }}</span>
</div>
<!-- 自定义主题颜色选择器 -->
<div class="theme-color-selector">
<div class="color-options">
<!-- 预设颜色选项 -->
<div
v-for="(color, index) in displayColorPresets"
:key="color"
:class="[
'color-option',
{ 'is-active': selectedThemeColor === color }
]"
:style="{ backgroundColor: color }"
@click="handleColorSelect(color)"
>
<div v-if="selectedThemeColor === color" class="color-check">
<el-icon><Check /></el-icon>
</div>
</div>
<!-- 自定义颜色选择器 -->
<div class="color-picker-wrapper">
<el-color-picker
v-model="selectedThemeColor"
:predefine="allColorPresets"
show-alpha
size="small"
class="custom-color-picker"
/>
</div>
</div>
</div>
</div>
</section>
<!-- 导航主题 -->
<section v-if="!isDark" class="config-section ">
<el-divider>{{ t("settings.navigation") }}</el-divider>
@@ -157,7 +185,7 @@
</template>
<script setup lang="ts">
import { DocumentCopy, RefreshLeft, Check } from "@element-plus/icons-vue";
import { DocumentCopy, RefreshLeft, Check, Plus } from "@element-plus/icons-vue";
const { t } = useI18n();
import { LayoutMode, SidebarColor, ThemeMode } from "@/enums";
@@ -192,9 +220,17 @@ const layoutOptions: LayoutOption[] = [
// 使用统一的颜色预设配置
const colorPresets = themeColorPresets;
const settingsStore = useSettingsStore();
// 主题颜色选择器相关
const displayColorPresets = computed(() => themeColorPresets.slice(0, 9)); // 只显示前9个预设颜色
const allColorPresets = themeColorPresets; // 所有颜色预设,用于自定义颜色选择器
// 判断当前颜色是否为自定义颜色(不在前7个预设中)
const isCustomColor = computed(() => {
return !displayColorPresets.value.includes(selectedThemeColor.value);
});
const isDark = ref<boolean>(settingsStore.theme === ThemeMode.DARK);
const sidebarColor = ref(settingsStore.sidebarColorScheme);
@@ -237,6 +273,16 @@ const handleLayoutChange = (layout: LayoutMode) => {
settingsStore.updateLayout(layout);
};
/**
* 处理颜色选择
*
* @param color - 选中的颜色
*/
const handleColorSelect = (color: string) => {
selectedThemeColor.value = color;
};
/**
* 复制当前配置
*/
@@ -618,6 +664,118 @@ const handleCloseDrawer = () => {
}
}
/* 主题颜色选择器样式 */
.theme-color-selector {
.color-options {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.color-option {
position: relative;
width: 22px;
height: 22px;
cursor: pointer;
border: 2px solid var(--el-border-color-light);
border-radius: 6px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
&:hover {
transform: translateY(-2px) scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border-color: var(--el-color-primary-light-3);
}
&.is-active {
border-color: var(--el-color-primary);
transform: translateY(-1px) scale(1.08);
box-shadow: 0 4px 16px rgba(64, 128, 255, 0.3);
}
.color-check {
position: absolute;
top: 50%;
left: 50%;
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
color: white;
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
transform: translate(-50%, -50%);
font-size: 10px;
backdrop-filter: blur(2px);
}
}
.color-picker-wrapper {
.custom-color-picker {
:deep(.el-color-picker__trigger) {
width: 22px !important;
height: 22px !important;
border: 2px solid var(--el-border-color-light) !important;
border-radius: 6px !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
&:hover {
transform: translateY(-2px) scale(1.05) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
border-color: var(--el-color-primary-light-3) !important;
}
}
:deep(.el-color-picker__color) {
border: none !important;
border-radius: 3px !important;
}
:deep(.el-color-picker__color-inner) {
border-radius: 3px !important;
}
:deep(.el-color-picker__icon) {
font-size: 10px !important;
}
}
}
}
/* 深色模式适配 */
.dark {
.theme-color-selector {
.color-option {
border-color: var(--el-border-color);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
&:hover {
border-color: var(--el-color-primary-light-3);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
}
.color-picker-wrapper {
.custom-color-picker {
:deep(.el-color-picker__trigger) {
border-color: var(--el-border-color) !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3) !important;
&:hover {
border-color: var(--el-color-primary-light-3) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important;
}
}
}
}
}
}
/* 复制配置对话框样式 */
:deep(.copy-config-dialog) {
.el-message-box__content {
@@ -626,3 +784,4 @@ const handleCloseDrawer = () => {
}
}
</style>
+1 -1
View File
@@ -13,7 +13,7 @@
<!-- 新增悬浮的系统设置按钮 -->
<el-button
v-if="defaultSettings.showSettings"
v-if="settingStore.showSettings"
class="floating-settings-button"
type="primary"
@click="handleSettingsClick"
+13 -8
View File
@@ -12,6 +12,8 @@ export const defaultSettings: AppSettings = {
version: pkg.version,
// 是否显示设置
showSettings: true,
// 是否显示桌面端工具项
showDesktopTools: true,
// 是否显示标签视图
showTagsView: true,
// 是否显示应用Logo
@@ -40,47 +42,50 @@ export const defaultSettings: AppSettings = {
// 主题色预设 - 现代化配色方案
// 注意:修改默认主题色时,需要同步修改 src/styles/variables.scss 中的 primary.base 值
export const themeColorPresets = [
// === 蓝色系 - 科技与专业 ===
// === 精选常用颜色 - 多样化色系 ===
"#4080FF", // Arco Design 蓝 - 现代感强
"#1890FF", // Ant Design 蓝 - 经典商务
"#52C41A", // 成功绿 - 活力清新
"#722ED1", // 优雅紫 - 高端大气
"#FA8C16", // 活力橙 - 温暖友好
"#13C2C2", // 青色 - 科技感
"#F5222D", // 警示红 - 醒目强烈
"#EB2F96", // 品红 - 时尚个性
"#EC4899", // 玫瑰粉 - 浪漫温馨
"#10B981", // 翠绿色 - 清新自然
// === 蓝色系 - 科技与专业 ===
"#409EFF", // Element Plus 蓝 - 清新自然
"#2F54EB", // 深蓝 - 稳重专业
"#1E40AF", // 深蓝色 - 商务精英
"#1D4ED8", // 皇家蓝 - 高端商务
// === 绿色系 - 自然与活力 ===
"#52C41A", // 成功绿 - 活力清新
"#10B981", // 翠绿色 - 清新自然
"#059669", // 森林绿 - 生态环保
"#16A34A", // 草绿色 - 健康活力
"#15803D", // 深绿色 - 稳重大气
// === 紫色系 - 创意与优雅 ===
"#722ED1", // 优雅紫 - 高端大气
"#7C3AED", // 紫罗兰 - 创意无限
"#8B5CF6", // 浅紫色 - 时尚现代
"#6D28D9", // 深紫色 - 神秘高端
"#5B21B6", // 皇家紫 - 王者风范
// === 橙色系 - 温暖与活力 ===
"#FA8C16", // 活力橙 - 温暖友好
"#F97316", // 火橙色 - 热情奔放
"#EA580C", // 深橙色 - 阳光活力
"#DC2626", // 珊瑚红 - 温暖亲切
// === 青色系 - 科技与清新 ===
"#13C2C2", // 青色 - 科技感
"#0891B2", // 天蓝色 - 清新自然
"#0E7490", // 深青色 - 专业科技
"#06B6D4", // 青蓝色 - 海洋清新
// === 红色系 - 激情与警示 ===
"#F5222D", // 警示红 - 醒目强烈
"#DC2626", // 猩红色 - 激情四射
"#B91C1C", // 深红色 - 庄重严肃
// === 粉色系 - 温柔与时尚 ===
"#EB2F96", // 品红 - 时尚个性
"#EC4899", // 玫瑰粉 - 浪漫温馨
"#F472B6", // 浅粉色 - 柔美可爱
@@ -11,6 +11,8 @@ interface SettingsState {
showTagsView: boolean;
showAppLogo: boolean;
showWatermark: boolean;
showSettings: boolean;
showDesktopTools: boolean;
// 布局设置
layout: LayoutMode;
@@ -42,6 +44,16 @@ export const useSettingsStore = defineStore("setting", () => {
defaultSettings.showWatermark
);
const showSettings = useStorage<boolean>(
SETTINGS_KEYS.SHOW_SETTINGS,
defaultSettings.showSettings
);
const showDesktopTools = useStorage<boolean>(
SETTINGS_KEYS.SHOW_DESKTOP_TOOLS,
defaultSettings.showDesktopTools
);
const sidebarColorScheme = useStorage<string>(
SETTINGS_KEYS.SIDEBAR_COLOR_SCHEME,
defaultSettings.sidebarColorScheme
@@ -58,6 +70,8 @@ export const useSettingsStore = defineStore("setting", () => {
showTagsView,
showAppLogo,
showWatermark,
showSettings,
showDesktopTools,
sidebarColorScheme,
layout,
} as const;
@@ -125,6 +139,8 @@ export const useSettingsStore = defineStore("setting", () => {
showTagsView.value = defaultSettings.showTagsView;
showAppLogo.value = defaultSettings.showAppLogo;
showWatermark.value = defaultSettings.showWatermark;
showSettings.value = defaultSettings.showSettings;
showDesktopTools.value = defaultSettings.showDesktopTools;
sidebarColorScheme.value = defaultSettings.sidebarColorScheme;
layout.value = defaultSettings.layout as LayoutMode;
themeColor.value = defaultSettings.themeColor;
@@ -137,6 +153,8 @@ export const useSettingsStore = defineStore("setting", () => {
showTagsView,
showAppLogo,
showWatermark,
showSettings,
showDesktopTools,
sidebarColorScheme,
layout,
themeColor,
+2
View File
@@ -64,6 +64,8 @@ declare global {
version: string;
/** 是否显示设置 */
showSettings: boolean;
/** 是否显示桌面端工具项 */
showDesktopTools: boolean;
/** 是否显示多标签导航 */
showTagsView: boolean;
/** 是否显示应用Logo */
+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>