Explorar o código

修改文件上传

HuRongxin hai 4 meses
pai
achega
eac2769664

+ 104 - 0
src/api/file/category/index.ts

@@ -0,0 +1,104 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { FileCategoryVO, FileCategoryForm, FileCategoryQuery, TableDataInfo } from './types';
+
+/**
+ * 查询文件分类列表
+ * @param query 查询参数
+ * @returns Promise<TableDataInfo<FileCategoryVO>>
+ */
+export const listFileCategory = (query?: FileCategoryQuery): AxiosPromise<TableDataInfo<FileCategoryVO>> => {
+  // 移除空值
+  const cleanQuery: Record<string, any> = {};
+  if (query) {
+    Object.keys(query).forEach((key) => {
+      if (
+        query[key as keyof FileCategoryQuery] !== undefined &&
+        query[key as keyof FileCategoryQuery] !== null &&
+        query[key as keyof FileCategoryQuery] !== ''
+      ) {
+        cleanQuery[key] = query[key as keyof FileCategoryQuery];
+      }
+    });
+  }
+
+  return request({
+    url: '/resource/file/category/list',
+    method: 'get',
+    params: cleanQuery
+  });
+};
+
+/**
+ * 查询文件分类详细
+ * @param id 分类ID
+ * @returns Promise<FileCategoryVO>
+ */
+export const getFileCategory = (id: number): AxiosPromise<FileCategoryVO> => {
+  return request({
+    url: `/resource/file/category/${id}`,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增文件分类
+ * @param data 分类数据
+ * @returns Promise<any>
+ */
+export const addFileCategory = (data: FileCategoryForm): AxiosPromise<any> => {
+  return request({
+    url: '/resource/file/category',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改文件分类
+ * @param data 分类数据
+ * @returns Promise<any>
+ */
+export const updateFileCategory = (data: FileCategoryForm): AxiosPromise<any> => {
+  return request({
+    url: '/resource/file/category',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除文件分类
+ * @param id 分类ID
+ * @returns Promise<any>
+ */
+export const delFileCategory = (id: number | number[]): AxiosPromise<any> => {
+  const ids = Array.isArray(id) ? id.join(',') : id;
+  return request({
+    url: `/resource/file/category/${ids}`,
+    method: 'delete'
+  });
+};
+
+/**
+ * 查询文件分类树结构
+ * @returns Promise<FileCategoryVO[]>
+ */
+export const listFileCategoryTree = (): AxiosPromise<FileCategoryVO[]> => {
+  return request({
+    url: '/resource/file/category/tree',
+    method: 'get'
+  });
+};
+
+/**
+ * 根据类型查询分类列表
+ * @param type 分类类型
+ * @returns Promise<FileCategoryVO[]>
+ */
+export const listFileCategoryByType = (type: number): AxiosPromise<FileCategoryVO[]> => {
+  return request({
+    url: `/resource/file/category/type/${type}`,
+    method: 'get'
+  });
+};

+ 199 - 0
src/api/file/category/types.ts

@@ -0,0 +1,199 @@
+/**
+ * 文件分类VO
+ */
+export interface FileCategoryVO {
+  /**
+   * 分类ID
+   */
+  id: number;
+
+  /**
+   * 分类名称
+   */
+  name: string;
+
+  /**
+   * 分类编码
+   */
+  code: string;
+
+  /**
+   * 父分类ID(0表示顶级分类)
+   */
+  parentId: number;
+
+  /**
+   * 分类类型(1图片 2视频 3音频 4文档 5其他)
+   */
+  type: number;
+
+  /**
+   * 排序号
+   */
+  sort: number;
+
+  /**
+   * 分类图标
+   */
+  icon?: string;
+
+  /**
+   * 分类描述
+   */
+  description?: string;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status: number;
+
+  /**
+   * 删除标志(0代表存在 2代表删除)
+   */
+  delFlag: number;
+
+  /**
+   * 创建时间
+   */
+  createTime: string;
+
+  /**
+   * 更新时间
+   */
+  updateTime: string;
+
+  /**
+   * 子分类列表
+   */
+  children?: FileCategoryVO[];
+}
+
+/**
+ * 文件分类表单
+ */
+export interface FileCategoryForm {
+  /**
+   * 分类ID
+   */
+  id?: number;
+
+  /**
+   * 分类名称
+   */
+  name: string;
+
+  /**
+   * 分类编码
+   */
+  code: string;
+
+  /**
+   * 父分类ID(0表示顶级分类)
+   */
+  parentId: number;
+
+  /**
+   * 分类类型(1图片 2视频 3音频 4文档 5其他)
+   */
+  type: number;
+
+  /**
+   * 排序号
+   */
+  sort?: number;
+
+  /**
+   * 分类图标
+   */
+  icon?: string;
+
+  /**
+   * 分类描述
+   */
+  description?: string;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status?: number;
+}
+
+/**
+ * 文件分类查询参数
+ */
+export interface FileCategoryQuery {
+  /**
+   * 分类名称
+   */
+  name?: string;
+
+  /**
+   * 分类编码
+   */
+  code?: string;
+
+  /**
+   * 父分类ID
+   */
+  parentId?: number;
+
+  /**
+   * 分类类型
+   */
+  type?: number;
+
+  /**
+   * 状态
+   */
+  status?: number;
+
+  /**
+   * 页码
+   */
+  pageNum?: number;
+
+  /**
+   * 每页大小
+   */
+  pageSize?: number;
+}
+
+/**
+ * 分页查询参数
+ */
+export interface PageQuery {
+  /**
+   * 页码
+   */
+  pageNum?: number;
+
+  /**
+   * 每页大小
+   */
+  pageSize?: number;
+}
+
+/**
+ * 分页响应结果
+ */
+export interface TableDataInfo<T> {
+  /**
+   * 总记录数
+   */
+  total: number;
+
+  /**
+   * 数据列表
+   */
+  rows: T[];
+
+  /**
+   * 页码
+   */
+  pageNum: number;
+
+  /**
+   * 每页大小
+   */
+  pageSize: number;
+} 

+ 125 - 0
src/api/file/info/index.ts

@@ -0,0 +1,125 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { FileInfoVO, FileInfoForm, FileInfoQuery, TableDataInfo } from './types';
+
+/**
+ * 查询文件信息列表
+ * @param query 查询参数
+ * @returns Promise<TableDataInfo<FileInfoVO>>
+ */
+export const listFileInfo = (query?: FileInfoQuery): AxiosPromise<TableDataInfo<FileInfoVO>> => {
+  // 移除空值
+  const cleanQuery: Record<string, any> = {};
+  if (query) {
+    Object.keys(query).forEach((key) => {
+      if (query[key as keyof FileInfoQuery] !== undefined && query[key as keyof FileInfoQuery] !== null && query[key as keyof FileInfoQuery] !== '') {
+        cleanQuery[key] = query[key as keyof FileInfoQuery];
+      }
+    });
+  }
+
+  return request({
+    url: '/resource/file/info/list',
+    method: 'get',
+    params: cleanQuery
+  });
+};
+
+/**
+ * 查询文件信息详细
+ * @param id 文件ID
+ * @returns Promise<FileInfoVO>
+ */
+export const getFileInfo = (id: number): AxiosPromise<FileInfoVO> => {
+  return request({
+    url: `/resource/file/info/${id}`,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增文件信息
+ * @param data 文件数据
+ * @returns Promise<any>
+ */
+export const addFileInfo = (data: FileInfoForm): AxiosPromise<any> => {
+  return request({
+    url: '/resource/file/info',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改文件信息
+ * @param data 文件数据
+ * @returns Promise<any>
+ */
+export const updateFileInfo = (data: FileInfoForm): AxiosPromise<any> => {
+  return request({
+    url: '/resource/file/info',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除文件信息
+ * @param id 文件ID
+ * @returns Promise<any>
+ */
+export const delFileInfo = (id: number | number[]): AxiosPromise<any> => {
+  const ids = Array.isArray(id) ? id.join(',') : id;
+  return request({
+    url: `/resource/file/info/${ids}`,
+    method: 'delete'
+  });
+};
+
+/**
+ * 根据分类ID查询文件列表
+ * @param categoryId 分类ID
+ * @returns Promise<FileInfoVO[]>
+ */
+export const listFileInfoByCategory = (categoryId: number): AxiosPromise<FileInfoVO[]> => {
+  return request({
+    url: `/resource/file/info/category/${categoryId}`,
+    method: 'get'
+  });
+};
+
+/**
+ * 根据文件类型查询文件列表
+ * @param type 文件类型
+ * @returns Promise<FileInfoVO[]>
+ */
+export const listFileInfoByType = (type: string): AxiosPromise<FileInfoVO[]> => {
+  return request({
+    url: `/resource/file/info/type/${type}`,
+    method: 'get'
+  });
+};
+
+/**
+ * 更新文件下载次数
+ * @param id 文件ID
+ * @returns Promise<any>
+ */
+export const updateDownloadCount = (id: number): AxiosPromise<any> => {
+  return request({
+    url: `/resource/file/info/download/${id}`,
+    method: 'put'
+  });
+};
+
+/**
+ * 更新文件查看次数
+ * @param id 文件ID
+ * @returns Promise<any>
+ */
+export const updateViewCount = (id: number): AxiosPromise<any> => {
+  return request({
+    url: `/resource/file/info/view/${id}`,
+    method: 'put'
+  });
+};

+ 319 - 0
src/api/file/info/types.ts

@@ -0,0 +1,319 @@
+/**
+ * 文件信息VO
+ */
+export interface FileInfoVO {
+  /**
+   * 文件ID
+   */
+  id: number;
+
+  /**
+   * 文件名称
+   */
+  name: string;
+
+  /**
+   * 原始文件名
+   */
+  originalName: string;
+
+  /**
+   * 文件路径
+   */
+  path: string;
+
+  /**
+   * 文件URL
+   */
+  url?: string;
+
+  /**
+   * 文件大小(字节)
+   */
+  size: number;
+
+  /**
+   * 文件类型
+   */
+  type: string;
+
+  /**
+   * 文件扩展名
+   */
+  extension?: string;
+
+  /**
+   * 分类ID
+   */
+  categoryId?: number;
+
+  /**
+   * 分类名称
+   */
+  categoryName?: string;
+
+  /**
+   * 文件哈希值
+   */
+  hash?: string;
+
+  /**
+   * 图片宽度
+   */
+  width?: number;
+
+  /**
+   * 图片高度
+   */
+  height?: number;
+
+  /**
+   * 视频时长(秒)
+   */
+  duration?: number;
+
+  /**
+   * OSS存储桶
+   */
+  ossBucket?: string;
+
+  /**
+   * OSS对象键
+   */
+  ossKey?: string;
+
+  /**
+   * 上传状态(0上传中 1上传完成 2上传失败)
+   */
+  uploadStatus: number;
+
+  /**
+   * 下载次数
+   */
+  downloadCount: number;
+
+  /**
+   * 查看次数
+   */
+  viewCount: number;
+
+  /**
+   * 是否公开(0私有 1公开)
+   */
+  isPublic: number;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status: number;
+
+  /**
+   * 文件描述
+   */
+  description?: string;
+
+  /**
+   * 创建时间
+   */
+  createTime: string;
+
+  /**
+   * 更新时间
+   */
+  updateTime: string;
+}
+
+/**
+ * 文件信息表单
+ */
+export interface FileInfoForm {
+  /**
+   * 文件ID
+   */
+  id?: number;
+
+  /**
+   * 文件名称
+   */
+  name: string;
+
+  /**
+   * 原始文件名
+   */
+  originalName: string;
+
+  /**
+   * 文件路径
+   */
+  path: string;
+
+  /**
+   * 文件URL
+   */
+  url?: string;
+
+  /**
+   * 文件大小(字节)
+   */
+  size?: number;
+
+  /**
+   * 文件类型
+   */
+  type?: string;
+
+  /**
+   * 文件扩展名
+   */
+  extension?: string;
+
+  /**
+   * 分类ID
+   */
+  categoryId?: number;
+
+  /**
+   * 文件主分类
+   */
+  categoryType?: number;
+
+  /**
+   * 文件哈希值
+   */
+  hash?: string;
+
+  /**
+   * 图片宽度
+   */
+  width?: number;
+
+  /**
+   * 图片高度
+   */
+  height?: number;
+
+  /**
+   * 视频时长(秒)
+   */
+  duration?: number;
+
+  /**
+   * OSS存储桶
+   */
+  ossBucket?: string;
+
+  /**
+   * OSS对象键
+   */
+  ossKey?: string;
+
+  /**
+   * 上传状态(0上传中 1上传完成 2上传失败)
+   */
+  uploadStatus?: number;
+
+  /**
+   * 是否公开(0私有 1公开)
+   */
+  isPublic?: number;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status?: number;
+
+  /**
+   * 文件描述
+   */
+  description?: string;
+}
+
+/**
+ * 文件信息查询参数
+ */
+export interface FileInfoQuery {
+  /**
+   * 文件名称
+   */
+  name?: string;
+
+  /**
+   * 文件主分类
+   */
+  categoryType?: number;
+
+  /**
+   * 原始文件名
+   */
+  originalName?: string;
+
+  /**
+   * 分类ID
+   */
+  categoryId?: number;
+
+  /**
+   * 文件类型
+   */
+  type?: string;
+
+  /**
+   * 上传状态
+   */
+  uploadStatus?: number;
+
+  /**
+   * 是否公开
+   */
+  isPublic?: number;
+
+  /**
+   * 页码
+   */
+  pageNum?: number;
+
+  /**
+   * 每页大小
+   */
+  pageSize?: number;
+}
+
+/**
+ * 分页查询参数
+ */
+export interface PageQuery {
+  /**
+   * 页码
+   */
+  pageNum?: number;
+
+  /**
+   * 每页大小
+   */
+  pageSize?: number;
+}
+
+/**
+ * 分页响应结果
+ */
+export interface TableDataInfo<T> {
+  /**
+   * 总记录数
+   */
+  total: number;
+
+  /**
+   * 数据列表
+   */
+  rows: T[];
+
+  /**
+   * 页码
+   */
+  pageNum: number;
+
+  /**
+   * 每页大小
+   */
+  pageSize: number;
+} 

+ 187 - 0
src/components/FileSelector/README.md

@@ -0,0 +1,187 @@
+# FileSelector 文件选择器组件
+
+基于现有文件管理界面功能开发的公共文件选择器组件,支持从已有文件中选择或上传新文件。
+
+## 功能特性
+
+- 🗂️ **多文件类型支持**: 图片、视频、音频、文档等多种文件类型
+- 📁 **分类管理**: 支持文件分类和子分类选择
+- 🔍 **搜索功能**: 支持文件名搜索
+- 📱 **双视图模式**: 网格视图和列表视图切换
+- ✅ **多选/单选**: 可配置单选或多选模式
+- ⬆️ **上传功能**: 支持直接上传新文件
+- 🎨 **响应式设计**: 适配不同屏幕尺寸
+
+## 使用方法
+
+### 基础用法
+
+```vue
+<template>
+  <div>
+    <el-button @click="showSelector = true">选择文件</el-button>
+    
+    <FileSelector
+      v-model="showSelector"
+      :allowed-types="[1]"
+      :multiple="false"
+      @confirm="handleFileSelected"
+    />
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import FileSelector from '@/components/FileSelector/index.vue';
+
+const showSelector = ref(false);
+
+const handleFileSelected = (files) => {
+  console.log('选择的文件:', files);
+};
+</script>
+```
+
+### 高级用法
+
+```vue
+<template>
+  <FileSelector
+    v-model="visible"
+    :allowed-types="[1, 2]"
+    :multiple="true"
+    :allow-upload="true"
+    title="选择课程资源"
+    @confirm="onConfirm"
+  />
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import FileSelector from '@/components/FileSelector/index.vue';
+
+const visible = ref(false);
+
+const onConfirm = (selectedFiles) => {
+  // 处理选择的文件
+  selectedFiles.forEach(file => {
+    console.log('文件信息:', {
+      id: file.id,
+      name: file.name,
+      url: file.url,
+      size: file.size,
+      type: file.type
+    });
+  });
+};
+</script>
+```
+
+## Props 属性
+
+| 属性名 | 类型 | 默认值 | 说明 |
+|--------|------|-------|------|
+| modelValue | boolean | false | 控制对话框显示/隐藏 |
+| allowedTypes | number[] | [1] | 允许的文件类型数组 |
+| multiple | boolean | false | 是否允许多选 |
+| allowUpload | boolean | true | 是否允许上传新文件 |
+| title | string | '选择文件' | 对话框标题 |
+
+### 文件类型说明
+
+| 类型值 | 文件类型 | 支持的扩展名 |
+|--------|----------|-------------|
+| 1 | 图片文件 | jpg, jpeg, png, gif, bmp, webp |
+| 2 | 视频文件 | mp4, avi, mov, wmv, flv, mkv |
+| 3 | 音频文件 | mp3, wav, flac, aac, ogg |
+| 4 | 文档文件 | pdf, doc, docx, xls, xlsx, ppt, pptx, txt |
+
+## Events 事件
+
+| 事件名 | 参数 | 说明 |
+|--------|------|------|
+| update:modelValue | boolean | 对话框显示状态改变时触发 |
+| confirm | File[] | 确认选择文件时触发,返回选中的文件数组 |
+
+## 使用场景示例
+
+### 1. 课程封面选择(单选图片)
+
+```vue
+<FileSelector
+  v-model="showCoverSelector"
+  :allowed-types="[1]"
+  :multiple="false"
+  title="选择课程封面"
+  @confirm="handleCoverSelected"
+/>
+```
+
+### 2. 课程视频选择(多选视频)
+
+```vue
+<FileSelector
+  v-model="showVideoSelector"
+  :allowed-types="[2]"
+  :multiple="true"
+  title="选择课程视频"
+  @confirm="handleVideosSelected"
+/>
+```
+
+### 3. 教学资料选择(多选文档)
+
+```vue
+<FileSelector
+  v-model="showMaterialSelector"
+  :allowed-types="[4]"
+  :multiple="true"
+  title="选择教学资料"
+  @confirm="handleMaterialsSelected"
+/>
+```
+
+### 4. 混合媒体选择(图片+视频)
+
+```vue
+<FileSelector
+  v-model="showMediaSelector"
+  :allowed-types="[1, 2]"
+  :multiple="true"
+  title="选择媒体文件"
+  @confirm="handleMediaSelected"
+/>
+```
+
+## 完整示例
+
+参考 `example.vue` 文件,展示了在课程管理表单中的完整使用示例,包括:
+
+- 课程封面选择(单选图片)
+- 课程视频选择(多选视频)
+- 教学资料选择(多选文档)
+- 文件删除和重新选择功能
+
+## 注意事项
+
+1. **文件类型限制**: 组件会根据 `allowedTypes` 自动过滤和验证文件类型
+2. **上传限制**: 单个文件大小限制为 50MB
+3. **分类依赖**: 组件依赖文件分类API,确保后端分类数据正确
+4. **权限控制**: 确保用户有访问文件管理的权限
+5. **样式覆盖**: 如需自定义样式,可以通过CSS覆盖组件样式
+
+## 依赖要求
+
+- Vue 3.x
+- Element Plus
+- 文件管理相关API (`@/api/file/info`, `@/api/file/category`)
+- 认证工具 (`@/utils/request`, `@/utils/auth`)
+
+## 更新日志
+
+### v1.0.0
+- 初始版本发布
+- 支持基础文件选择功能
+- 支持文件上传功能
+- 支持多种文件类型
+- 支持单选/多选模式

+ 8 - 0
src/components/FileSelector/index.js

@@ -0,0 +1,8 @@
+import FileSelector from './index.vue';
+
+// 为组件提供 install 方法,支持 use 方式注册
+FileSelector.install = function (app) {
+  app.component('FileSelector', FileSelector);
+};
+
+export default FileSelector;

+ 222 - 0
src/components/FileSelector/index.vue

@@ -0,0 +1,222 @@
+<template>
+  <div class="file-selector">
+    <!-- 文件选择对话框 -->
+    <el-dialog
+      v-model="dialogVisible"
+      :title="dialogTitle"
+      width="90%"
+      :close-on-click-modal="false"
+      class="file-selector-dialog"
+      top="5vh"
+      :before-close="handleClose"
+      append-to-body
+      :z-index="9999"
+    >
+      <!-- 直接嵌入文件管理页面组件 -->
+      <div class="file-manager-container">
+        <FileManager
+          ref="fileManagerRef"
+          :select-mode="true"
+          :file-type="getFileTypeString()"
+          :multiple="multiple"
+          @file-selected="handleFileSelected"
+          @files-selected="handleFilesSelected"
+        />
+      </div>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="handleClose">取消</el-button>
+          <el-button type="primary" @click="confirmSelection" :disabled="multiple ? selectedFiles.length === 0 : !selectedFile">
+            确认选择{{ multiple ? `(${selectedFiles.length})` : '' }}
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, nextTick } from 'vue';
+import { ElMessage } from 'element-plus';
+import FileManager from '@/views/file/info/index.vue';
+
+// Props
+interface Props {
+  modelValue: boolean;
+  // 允许的文件类型 [1: 图片, 2: 视频, 3: 音频, 4: 文档, 5: 其他]
+  // 例如:[1] 仅显示图片类型,[1,2] 显示图片和视频类型
+  allowedTypes?: number[];
+  // 是否允许多选(单选/多选文件)
+  multiple?: boolean;
+  // 是否允许上传新文件
+  allowUpload?: boolean;
+  // 对话框标题
+  title?: string;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  allowedTypes: () => [1], // 默认只允许图片
+  multiple: false,
+  allowUpload: true,
+  title: '选择文件'
+});
+
+// Emits
+const emit = defineEmits<{
+  'update:modelValue': [value: boolean];
+  'confirm': [files: any[]];
+}>();
+
+// 响应式数据
+const dialogVisible = computed({
+  get: () => props.modelValue,
+  set: (value) => emit('update:modelValue', value)
+});
+
+const dialogTitle = computed(() => {
+  const typeText = getTypeText();
+  return props.title || `选择${typeText}${props.multiple ? '(多选)' : ''}`;
+});
+
+const selectedFile = ref<any>(null);
+const selectedFiles = ref<any[]>([]);
+const fileManagerRef = ref();
+
+// 方法
+const getFileTypeString = () => {
+  // 如果只有一种类型,返回对应的类型字符串
+  if (props.allowedTypes.length === 1) {
+    switch (props.allowedTypes[0]) {
+      case 1:
+        return 'image';
+      case 2:
+        return 'video';
+      case 3:
+        return 'audio';
+      case 4:
+        return 'document';
+      default:
+        return '';
+    }
+  }
+  // 多种类型时返回空字符串,表示不限制
+  return '';
+};
+
+const getTypeText = () => {
+  if (props.allowedTypes.length === 1) {
+    switch (props.allowedTypes[0]) {
+      case 1:
+        return '图片';
+      case 2:
+        return '视频';
+      case 3:
+        return '音频';
+      case 4:
+        return '文档';
+      case 5:
+        return '文件';
+      default:
+        return '文件';
+    }
+  }
+  return '文件';
+};
+
+const handleClose = () => {
+  selectedFile.value = null;
+  selectedFiles.value = [];
+
+  // 重置FileManager组件的选择状态
+  nextTick(() => {
+    if (fileManagerRef.value && fileManagerRef.value.clearSelection) {
+      fileManagerRef.value.clearSelection();
+    }
+  });
+
+  emit('update:modelValue', false);
+};
+
+// 处理文件选择(单选模式)
+const handleFileSelected = (file: any) => {
+  if (props.multiple) return;
+
+  selectedFile.value = file;
+  console.log('选择的文件:', file);
+};
+
+// 处理多文件选择
+const handleFilesSelected = (files: any[]) => {
+  if (!props.multiple) return;
+
+  selectedFiles.value = files;
+  console.log('选择的文件列表:', files);
+};
+
+// 确认选择
+const confirmSelection = () => {
+  if (props.multiple) {
+    if (selectedFiles.value.length === 0) {
+      ElMessage.warning(`请选择至少一个${getTypeText()}`);
+      return;
+    }
+
+    emit('confirm', selectedFiles.value);
+    emit('update:modelValue', false);
+    ElMessage.success(`成功选择${selectedFiles.value.length}个${getTypeText()}`);
+  } else {
+    if (!selectedFile.value) {
+      ElMessage.warning(`请选择一个${getTypeText()}`);
+      return;
+    }
+
+    emit('confirm', [selectedFile.value]);
+    emit('update:modelValue', false);
+    ElMessage.success(`${getTypeText()}选择成功`);
+  }
+
+  // 清空选择
+  selectedFile.value = null;
+  selectedFiles.value = [];
+};
+</script>
+
+<style scoped>
+.file-selector {
+  display: inline-block;
+}
+
+/* 文件选择器对话框样式 */
+.file-selector-dialog :deep(.el-dialog__body) {
+  padding: 0;
+  height: 80vh;
+  overflow: hidden;
+}
+
+.file-manager-container {
+  height: 100%;
+  overflow: hidden;
+}
+
+.dialog-footer {
+  text-align: right;
+}
+
+/* 确保对话框样式正常 */
+.file-selector-dialog :deep(.el-dialog) {
+  display: flex;
+  flex-direction: column;
+  max-height: 90vh;
+}
+
+.file-selector-dialog :deep(.el-dialog__header) {
+  flex-shrink: 0;
+}
+
+.file-selector-dialog :deep(.el-dialog__footer) {
+  border-top: 1px solid #ebeef5;
+  padding: 15px 20px;
+  flex-shrink: 0;
+}
+</style>

+ 707 - 0
src/components/ImageCropper/index.vue

@@ -0,0 +1,707 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    title="图片裁剪"
+    width="90%"
+    :close-on-click-modal="false"
+    custom-class="image-crop-dialog"
+    @close="handleClose"
+  >
+    <div class="image-editor-container">
+      <!-- 主编辑区域 -->
+      <div class="editor-main">
+        <!-- 图片预览区域 -->
+        <div class="image-preview-area">
+          <div class="preview-container" style="position: relative; display: inline-block;">
+            <img
+              ref="cropImageRef"
+              :src="imageUrl"
+              class="crop-image"
+              @load="onImageLoad"
+              @error="onImageError"
+              style="max-width: 100%; max-height: 500px; display: block;"
+            />
+            <!-- 裁剪框 -->
+            <div
+              v-if="showCropBox"
+              class="crop-box"
+              :style="{
+                left: cropForm.x + 'px',
+                top: cropForm.y + 'px',
+                width: cropForm.width + 'px',
+                height: cropForm.height + 'px'
+              }"
+              @mousedown="startDrag"
+            >
+              <!-- 裁剪句柄 -->
+              <div class="crop-handle crop-handle-nw" @mousedown="startResize('nw', $event)"></div>
+              <div class="crop-handle crop-handle-ne" @mousedown="startResize('ne', $event)"></div>
+              <div class="crop-handle crop-handle-sw" @mousedown="startResize('sw', $event)"></div>
+              <div class="crop-handle crop-handle-se" @mousedown="startResize('se', $event)"></div>
+            </div>
+          </div>
+        </div>
+
+        <!-- 工具栏 -->
+        <div class="editor-toolbar">
+          <el-button-group>
+            <el-button @click="resetCrop">
+              <el-icon><Refresh /></el-icon>
+              重置
+            </el-button>
+            <el-button @click="rotateLeft">
+              <el-icon><RefreshLeft /></el-icon>
+              左转
+            </el-button>
+            <el-button @click="rotateRight">
+              <el-icon><RefreshRight /></el-icon>
+              右转
+            </el-button>
+          </el-button-group>
+        </div>
+      </div>
+
+      <!-- 右侧设置面板 -->
+      <div class="editor-sidebar">
+        <div class="size-panel">
+          <h4>裁剪设置</h4>
+          
+          <!-- 预设尺寸 -->
+          <div class="preset-sizes">
+            <h5>预设尺寸</h5>
+            <div class="preset-buttons">
+              <el-button size="small" @click="applyPresetSize(300, 300)">正方形</el-button>
+              <el-button size="small" @click="applyPresetSize(400, 300)">横版</el-button>
+              <el-button size="small" @click="applyPresetSize(300, 400)">竖版</el-button>
+              <el-button size="small" @click="applyPresetSize(800, 400)">轮播图</el-button>
+              <el-button size="small" @click="applyPresetSize(200, 200)">头像</el-button>
+            </div>
+          </div>
+
+          <!-- 自定义尺寸 -->
+          <div class="custom-size">
+            <h5>自定义尺寸</h5>
+            <el-form :model="cropSizeForm" label-width="60px" size="small">
+              <el-form-item label="宽度">
+                <el-input-number
+                  v-model="cropSizeForm.width"
+                  :min="50"
+                  :max="2000"
+                  @change="applyCropSize"
+                />
+                <span class="unit">px</span>
+              </el-form-item>
+              <el-form-item label="高度">
+                <el-input-number
+                  v-model="cropSizeForm.height"
+                  :min="50"
+                  :max="2000"
+                  @change="applyCropSize"
+                />
+                <span class="unit">px</span>
+              </el-form-item>
+              <el-form-item>
+                <el-checkbox v-model="cropSizeForm.maintainAspectRatio">
+                  保持宽高比
+                </el-checkbox>
+              </el-form-item>
+            </el-form>
+          </div>
+
+          <!-- 裁剪信息 -->
+          <div class="crop-settings">
+            <h5>裁剪信息</h5>
+            <el-form :model="cropForm" label-width="60px" size="small">
+              <el-form-item label="X">
+                <el-input-number v-model="cropForm.x" :min="0" @change="updateCropBox" />
+              </el-form-item>
+              <el-form-item label="Y">
+                <el-input-number v-model="cropForm.y" :min="0" @change="updateCropBox" />
+              </el-form-item>
+              <el-form-item label="宽">
+                <el-input-number v-model="cropForm.width" :min="1" @change="updateCropBox" />
+              </el-form-item>
+              <el-form-item label="高">
+                <el-input-number v-model="cropForm.height" :min="1" @change="updateCropBox" />
+              </el-form-item>
+            </el-form>
+          </div>
+
+          <!-- 原始图片信息 -->
+          <div class="image-info-panel">
+            <h5>原始图片</h5>
+            <div class="info-item">
+              <span class="label">尺寸:</span>
+              <span class="value">{{ originalSize.width }} × {{ originalSize.height }}</span>
+            </div>
+            <div class="info-item" v-if="file">
+              <span class="label">文件名:</span>
+              <span class="value">{{ file.name }}</span>
+            </div>
+            <div class="info-item" v-if="file">
+              <span class="label">文件大小:</span>
+              <span class="value">{{ formatFileSize(file.size) }}</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="handleCancel">取消</el-button>
+        <el-button type="primary" @click="handleConfirm" :loading="loading">
+          <el-icon><Check /></el-icon>
+          确认裁剪
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch } from 'vue';
+import { ElMessage } from 'element-plus';
+import { Check, Refresh, RefreshLeft, RefreshRight } from '@element-plus/icons-vue';
+
+// Props
+interface Props {
+  modelValue: boolean;
+  file?: File | null;
+  imageUrl?: string;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  modelValue: false,
+  file: null,
+  imageUrl: ''
+});
+
+// Emits
+interface Emits {
+  (e: 'update:modelValue', value: boolean): void;
+  (e: 'confirm', croppedFile: File): void;
+  (e: 'cancel'): void;
+}
+
+const emit = defineEmits<Emits>();
+
+// 响应式数据
+const loading = ref(false);
+const cropImageRef = ref();
+const originalSize = ref({ width: 0, height: 0 });
+const showCropBox = ref(false);
+const isDragging = ref(false);
+const isResizing = ref(false);
+const resizeDirection = ref('');
+
+// 尺寸表单
+const cropSizeForm = ref({
+  width: 300,
+  height: 300,
+  maintainAspectRatio: true
+});
+
+// 剪切表单
+const cropForm = ref({
+  x: 0,
+  y: 0,
+  width: 0,
+  height: 0
+});
+
+// 计算属性
+const visible = computed({
+  get: () => props.modelValue,
+  set: (value) => emit('update:modelValue', value)
+});
+
+// 格式化文件大小
+const formatFileSize = (size: number) => {
+  if (!size) return '0 B';
+  const units = ['B', 'KB', 'MB', 'GB'];
+  let index = 0;
+  let fileSize = size;
+  while (fileSize >= 1024 && index < units.length - 1) {
+    fileSize /= 1024;
+    index++;
+  }
+  return fileSize.toFixed(2) + ' ' + units[index];
+};
+
+// 图片加载完成
+const onImageLoad = () => {
+  const img = cropImageRef.value;
+  if (img) {
+    originalSize.value = {
+      width: img.naturalWidth,
+      height: img.naturalHeight
+    };
+    
+    // 初始化裁剪框
+    const containerWidth = img.clientWidth;
+    const containerHeight = img.clientHeight;
+    const initialSize = Math.min(containerWidth, containerHeight) * 0.6;
+    
+    cropForm.value = {
+      x: (containerWidth - initialSize) / 2,
+      y: (containerHeight - initialSize) / 2,
+      width: initialSize,
+      height: initialSize
+    };
+    
+    showCropBox.value = true;
+    console.log('图片加载完成,原始尺寸:', originalSize.value, '裁剪框:', cropForm.value);
+  }
+};
+
+// 图片加载错误
+const onImageError = () => {
+  ElMessage.error('图片加载失败');
+};
+
+// 应用预设尺寸
+const applyPresetSize = (width: number, height: number) => {
+  cropSizeForm.value.width = width;
+  cropSizeForm.value.height = height;
+  applyCropSize();
+};
+
+// 应用裁剪尺寸
+const applyCropSize = () => {
+  if (!showCropBox.value) return;
+  
+  const img = cropImageRef.value;
+  if (!img) return;
+  
+  const scaleX = img.clientWidth / originalSize.value.width;
+  const scaleY = img.clientHeight / originalSize.value.height;
+  
+  const newWidth = cropSizeForm.value.width * scaleX;
+  const newHeight = cropSizeForm.value.height * scaleY;
+  
+  // 确保裁剪框不超出图片边界
+  const maxX = img.clientWidth - newWidth;
+  const maxY = img.clientHeight - newHeight;
+  
+  cropForm.value.width = Math.min(newWidth, img.clientWidth);
+  cropForm.value.height = Math.min(newHeight, img.clientHeight);
+  cropForm.value.x = Math.max(0, Math.min(cropForm.value.x, maxX));
+  cropForm.value.y = Math.max(0, Math.min(cropForm.value.y, maxY));
+};
+
+// 更新裁剪框
+const updateCropBox = () => {
+  // 裁剪框位置和尺寸已通过v-model绑定,这里可以添加额外的验证逻辑
+};
+
+// 开始拖拽
+const startDrag = (e: MouseEvent) => {
+  e.preventDefault();
+  isDragging.value = true;
+  
+  const startX = e.clientX - cropForm.value.x;
+  const startY = e.clientY - cropForm.value.y;
+  
+  const onMouseMove = (e: MouseEvent) => {
+    if (!isDragging.value) return;
+    
+    const img = cropImageRef.value;
+    if (!img) return;
+    
+    const newX = e.clientX - startX;
+    const newY = e.clientY - startY;
+    
+    // 限制在图片范围内
+    const maxX = img.clientWidth - cropForm.value.width;
+    const maxY = img.clientHeight - cropForm.value.height;
+    
+    cropForm.value.x = Math.max(0, Math.min(newX, maxX));
+    cropForm.value.y = Math.max(0, Math.min(newY, maxY));
+  };
+  
+  const onMouseUp = () => {
+    isDragging.value = false;
+    document.removeEventListener('mousemove', onMouseMove);
+    document.removeEventListener('mouseup', onMouseUp);
+  };
+  
+  document.addEventListener('mousemove', onMouseMove);
+  document.addEventListener('mouseup', onMouseUp);
+};
+
+// 开始调整大小
+const startResize = (direction: string, e: MouseEvent) => {
+  e.preventDefault();
+  e.stopPropagation();
+  
+  isResizing.value = true;
+  resizeDirection.value = direction;
+  
+  const startX = e.clientX;
+  const startY = e.clientY;
+  const startCrop = { ...cropForm.value };
+  
+  const onMouseMove = (e: MouseEvent) => {
+    if (!isResizing.value) return;
+    
+    const img = cropImageRef.value;
+    if (!img) return;
+    
+    const deltaX = e.clientX - startX;
+    const deltaY = e.clientY - startY;
+    
+    let newX = startCrop.x;
+    let newY = startCrop.y;
+    let newWidth = startCrop.width;
+    let newHeight = startCrop.height;
+    
+    switch (direction) {
+      case 'nw':
+        newX = startCrop.x + deltaX;
+        newY = startCrop.y + deltaY;
+        newWidth = startCrop.width - deltaX;
+        newHeight = startCrop.height - deltaY;
+        break;
+      case 'ne':
+        newY = startCrop.y + deltaY;
+        newWidth = startCrop.width + deltaX;
+        newHeight = startCrop.height - deltaY;
+        break;
+      case 'sw':
+        newX = startCrop.x + deltaX;
+        newWidth = startCrop.width - deltaX;
+        newHeight = startCrop.height + deltaY;
+        break;
+      case 'se':
+        newWidth = startCrop.width + deltaX;
+        newHeight = startCrop.height + deltaY;
+        break;
+    }
+    
+    // 保持宽高比
+    if (cropSizeForm.value.maintainAspectRatio) {
+      const aspectRatio = cropSizeForm.value.width / cropSizeForm.value.height;
+      if (Math.abs(deltaX) > Math.abs(deltaY)) {
+        newHeight = newWidth / aspectRatio;
+      } else {
+        newWidth = newHeight * aspectRatio;
+      }
+    }
+    
+    // 限制最小尺寸
+    newWidth = Math.max(20, newWidth);
+    newHeight = Math.max(20, newHeight);
+    
+    // 限制在图片范围内
+    newX = Math.max(0, Math.min(newX, img.clientWidth - newWidth));
+    newY = Math.max(0, Math.min(newY, img.clientHeight - newHeight));
+    newWidth = Math.min(newWidth, img.clientWidth - newX);
+    newHeight = Math.min(newHeight, img.clientHeight - newY);
+    
+    cropForm.value.x = newX;
+    cropForm.value.y = newY;
+    cropForm.value.width = newWidth;
+    cropForm.value.height = newHeight;
+  };
+  
+  const onMouseUp = () => {
+    isResizing.value = false;
+    resizeDirection.value = '';
+    document.removeEventListener('mousemove', onMouseMove);
+    document.removeEventListener('mouseup', onMouseUp);
+  };
+  
+  document.addEventListener('mousemove', onMouseMove);
+  document.addEventListener('mouseup', onMouseUp);
+};
+
+// 重置裁剪
+const resetCrop = () => {
+  const img = cropImageRef.value;
+  if (img) {
+    const containerWidth = img.clientWidth;
+    const containerHeight = img.clientHeight;
+    const initialSize = Math.min(containerWidth, containerHeight) * 0.6;
+    
+    cropForm.value = {
+      x: (containerWidth - initialSize) / 2,
+      y: (containerHeight - initialSize) / 2,
+      width: initialSize,
+      height: initialSize
+    };
+  }
+};
+
+// 左旋转
+const rotateLeft = () => {
+  ElMessage.info('旋转功能开发中...');
+};
+
+// 右旋转
+const rotateRight = () => {
+  ElMessage.info('旋转功能开发中...');
+};
+
+// 处理取消
+const handleCancel = () => {
+  visible.value = false;
+  emit('cancel');
+};
+
+// 处理关闭
+const handleClose = () => {
+  showCropBox.value = false;
+  emit('cancel');
+};
+
+// 处理确认
+const handleConfirm = async () => {
+  if (!props.file || !showCropBox.value) {
+    ElMessage.error('请先选择裁剪区域');
+    return;
+  }
+  
+  try {
+    loading.value = true;
+    
+    // 创建canvas进行裁剪
+    const canvas = document.createElement('canvas');
+    const ctx = canvas.getContext('2d');
+    const img = cropImageRef.value;
+    
+    if (!img || !ctx) {
+      throw new Error('无法创建画布或获取图片');
+    }
+    
+    // 计算实际裁剪区域(基于原始图片尺寸)
+    const scaleX = originalSize.value.width / img.clientWidth;
+    const scaleY = originalSize.value.height / img.clientHeight;
+    
+    const cropX = cropForm.value.x * scaleX;
+    const cropY = cropForm.value.y * scaleY;
+    const cropWidth = cropForm.value.width * scaleX;
+    const cropHeight = cropForm.value.height * scaleY;
+    
+    // 设置canvas尺寸
+    canvas.width = cropWidth;
+    canvas.height = cropHeight;
+    
+    // 创建新的Image对象来绘制
+    const image = new Image();
+    image.onload = () => {
+      // 绘制裁剪后的图片
+      ctx.drawImage(
+        image,
+        cropX, cropY, cropWidth, cropHeight,
+        0, 0, cropWidth, cropHeight
+      );
+      
+      // 转换为Blob
+      canvas.toBlob((blob) => {
+        if (blob) {
+          // 创建新的File对象
+          const croppedFile = new File(
+            [blob],
+            `cropped_${props.file!.name}`,
+            { type: props.file!.type }
+          );
+          
+          console.log('裁剪完成,新文件大小:', (croppedFile.size / 1024 / 1024).toFixed(2), 'MB');
+          
+          // 发射确认事件
+          emit('confirm', croppedFile);
+          visible.value = false;
+        } else {
+          throw new Error('裁剪失败');
+        }
+      }, props.file!.type, 0.9);
+    };
+    
+    image.onerror = () => {
+      throw new Error('图片加载失败');
+    };
+    
+    image.src = props.imageUrl;
+    
+  } catch (error) {
+    console.error('裁剪失败:', error);
+    const errorMessage = error instanceof Error ? error.message : String(error);
+    ElMessage.error('裁剪失败: ' + errorMessage);
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 监听文件变化,重置状态
+watch(() => props.file, () => {
+  showCropBox.value = false;
+  cropSizeForm.value = {
+    width: 300,
+    height: 300,
+    maintainAspectRatio: true
+  };
+});
+</script>
+
+<style scoped>
+/* 图片裁剪对话框样式 */
+.image-crop-dialog :deep(.el-dialog__body) {
+  padding: 20px;
+  height: 80vh;
+  overflow: visible;
+}
+
+.image-editor-container {
+  display: flex;
+  height: 100%;
+  gap: 20px;
+}
+
+.editor-main {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.image-preview-area {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #f5f7fa;
+  border-radius: 6px;
+  margin-bottom: 15px;
+}
+
+.preview-container {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: white;
+}
+
+.crop-image {
+  max-width: 100%;
+  max-height: 100%;
+  object-fit: contain;
+}
+
+.crop-box {
+  position: absolute;
+  border: 2px solid #409eff;
+  background: rgba(64, 158, 255, 0.1);
+  cursor: move;
+}
+
+.crop-handle {
+  position: absolute;
+  width: 8px;
+  height: 8px;
+  background: #409eff;
+  border: 1px solid white;
+  border-radius: 50%;
+}
+
+.crop-handle-nw {
+  top: -4px;
+  left: -4px;
+  cursor: nw-resize;
+}
+
+.crop-handle-ne {
+  top: -4px;
+  right: -4px;
+  cursor: ne-resize;
+}
+
+.crop-handle-sw {
+  bottom: -4px;
+  left: -4px;
+  cursor: sw-resize;
+}
+
+.crop-handle-se {
+  bottom: -4px;
+  right: -4px;
+  cursor: se-resize;
+}
+
+.editor-toolbar {
+  display: flex;
+  justify-content: center;
+  padding: 10px 0;
+}
+
+.editor-sidebar {
+  width: 300px;
+  background: #fafafa;
+  border-radius: 6px;
+  padding: 15px;
+  overflow-y: auto;
+}
+
+.size-panel h4 {
+  margin: 0 0 15px 0;
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.size-panel h5 {
+  margin: 15px 0 10px 0;
+  font-size: 14px;
+  font-weight: 500;
+  color: #606266;
+}
+
+.preset-sizes {
+  margin-bottom: 20px;
+}
+
+.preset-buttons {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+
+.custom-size {
+  margin-bottom: 20px;
+}
+
+.unit {
+  margin-left: 8px;
+  color: #909399;
+}
+
+.crop-settings {
+  margin-bottom: 20px;
+  padding-top: 15px;
+  border-top: 1px solid #ebeef5;
+}
+
+.image-info-panel {
+  padding-top: 15px;
+  border-top: 1px solid #ebeef5;
+}
+
+.info-item {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 8px;
+  font-size: 12px;
+}
+
+.info-item .label {
+  color: #909399;
+}
+
+.info-item .value {
+  color: #303133;
+  font-weight: 500;
+}
+
+.dialog-footer {
+  text-align: right;
+}
+</style>

+ 3046 - 0
src/views/file/info/index.vue

@@ -0,0 +1,3046 @@
+<template>
+  <div class="file-manager" :class="{ 'select-mode': selectMode }">
+    <!-- 顶部分类导航 - 选择模式下根据 fileType 自动隐藏 -->
+    <div class="top-categories" v-if="!selectMode || filteredTopCategories.length > 1">
+      <div class="category-tabs">
+        <!-- 全部文件标签 - 选择模式下如果指定了文件类型则隐藏 -->
+        <div v-if="!selectMode || !fileType" class="category-tab" :class="{ active: !currentTopCategory }" @click="switchTopCategory(null)">
+          <el-icon>
+            <Folder />
+          </el-icon>
+          <span>全部文件</span>
+        </div>
+        <!-- 具体分类标签 -->
+        <div
+          v-for="topCategory in filteredTopCategories"
+          :key="topCategory.id"
+          class="category-tab"
+          :class="{ active: currentTopCategory?.id === topCategory.id }"
+          @click="switchTopCategory(topCategory)"
+        >
+          <el-icon v-if="topCategory.icon">
+            <component :is="topCategory.icon" />
+          </el-icon>
+          <el-icon v-else>
+            <Folder />
+          </el-icon>
+          <span>{{ topCategory.name }}</span>
+        </div>
+      </div>
+    </div>
+
+    <!-- 主要内容区域 -->
+    <div class="main-container">
+      <!-- 工具栏 -->
+      <div class="toolbar">
+        <div class="toolbar-left">
+          <el-button type="primary" @click="openUploadDialog">
+            <el-icon><Plus /></el-icon>
+            {{ getUploadButtonText() }}
+          </el-button>
+          <el-button @click="handleBatchDelete" :disabled="selectedFiles.length === 0"> 删除{{ getFileTypeText() }} </el-button>
+          <el-dropdown @command="handleMoveToCategory" :disabled="selectedFiles.length === 0">
+            <el-button>
+              {{ getFileTypeText() }}移至<el-icon class="el-icon--right"><ArrowDown /></el-icon>
+            </el-button>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item v-for="category in categoryList" :key="category.id" :command="category.id">
+                  {{ category.name }}
+                </el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+          <span v-if="selectedFiles.length > 0" class="selected-info"> 已选择 {{ selectedFiles.length }} 个{{ getFileTypeText() }} </span>
+        </div>
+        <div class="toolbar-right">
+          <el-input v-model="searchKeyword" :placeholder="`请输入${getFileTypeText()}名`" style="width: 200px" clearable @input="handleSearch">
+            <template #prefix>
+              <el-icon><Search /></el-icon>
+            </template>
+          </el-input>
+          <div class="view-toggle">
+            <el-button :type="viewMode === 'grid' ? 'primary' : 'default'" size="small" @click="viewMode = 'grid'">
+              <el-icon><Grid /></el-icon>
+            </el-button>
+            <el-button :type="viewMode === 'list' ? 'primary' : 'default'" size="small" @click="viewMode = 'list'">
+              <el-icon><List /></el-icon>
+            </el-button>
+          </div>
+        </div>
+      </div>
+
+      <!-- 内容区域 -->
+      <div class="content-wrapper">
+        <!-- 左侧分类导航 -->
+        <div class="sidebar">
+          <div class="sidebar-header">
+            <h3>添加分类</h3>
+            <el-button link icon="Plus" @click="handleAddCategory">+</el-button>
+          </div>
+          <div class="category-list">
+            <!-- 全部文件 -->
+            <div class="category-item all-files" :class="{ active: currentCategory?.id === null }" @click="handleShowAllFiles">
+              <el-icon class="category-icon"><Folder /></el-icon>
+              <span>全部{{ getFileTypeText() }}</span>
+            </div>
+
+            <!-- 树形控件 -->
+            <el-tree
+              ref="categoryTreeRef"
+              :data="filteredCategoryTree"
+              :props="treeProps"
+              :expand-on-click-node="false"
+              :highlight-current="true"
+              node-key="id"
+              @node-click="handleTreeNodeClick"
+              class="category-tree"
+            >
+              <template #default="{ node, data }">
+                <div class="tree-node-content">
+                  <!-- 根据是否有子分类显示不同图标:有子分类显示文件夹,无子分类显示文件 -->
+                  <el-icon class="category-icon">
+                    <Folder v-if="data.children && data.children.length > 0" />
+                    <Document v-else />
+                  </el-icon>
+                  <span class="node-label">{{ data.name }}</span>
+                  <el-dropdown @command="handleCategoryAction" trigger="click" @click.stop class="node-actions">
+                    <el-icon class="more-icon"><MoreFilled /></el-icon>
+                    <template #dropdown>
+                      <el-dropdown-menu>
+                        <el-dropdown-item :command="{ action: 'addChild', data: data }">添加子分类</el-dropdown-item>
+                        <el-dropdown-item :command="{ action: 'edit', data: data }">编辑</el-dropdown-item>
+                        <el-dropdown-item :command="{ action: 'delete', data: data }">删除</el-dropdown-item>
+                      </el-dropdown-menu>
+                    </template>
+                  </el-dropdown>
+                </div>
+              </template>
+            </el-tree>
+          </div>
+        </div>
+
+        <!-- 右侧文件展示区 -->
+        <div class="content-area">
+          <!-- 网格视图 -->
+          <div v-if="viewMode === 'grid'" class="file-grid">
+            <div
+              v-for="file in fileList"
+              :key="file.id"
+              class="file-item"
+              :class="{ selected: selectedFiles.includes(file.id) }"
+              @click="toggleFileSelection(file.id)"
+            >
+              <div class="file-wrapper">
+                <el-image v-if="isImage(file)" :src="getImageUrl(file)" fit="cover" class="file-thumbnail" :preview-disabled="true" lazy>
+                  <template #error>
+                    <div class="file-error">
+                      <el-icon size="24" color="#c0c4cc">
+                        <Picture />
+                      </el-icon>
+                    </div>
+                  </template>
+                  <template #placeholder>
+                    <div class="file-loading">
+                      <el-icon size="24" color="#409eff">
+                        <Loading />
+                      </el-icon>
+                    </div>
+                  </template>
+                </el-image>
+                <div v-else-if="isVideo(file)" class="video-thumbnail-wrapper full">
+                  <video
+                    :src="getVideoUrl(file)"
+                    class="file-thumbnail video-thumbnail"
+                    muted
+                    preload="metadata"
+                    @loadedmetadata="onVideoLoadedMetadata"
+                    @error="onVideoError"
+                  ></video>
+                  <div class="video-play-overlay">
+                    <el-icon size="32" color="#fff">
+                      <VideoPlay />
+                    </el-icon>
+                  </div>
+                </div>
+                <div v-else class="file-icon-wrapper">
+                  <el-icon class="file-icon" :color="getFileIconColor(file)">
+                    <component :is="getFileIcon(file)" />
+                  </el-icon>
+                </div>
+                <div class="file-checkbox">
+                  <el-checkbox :model-value="selectedFiles.includes(file.id)" @change="(val) => toggleFileSelection(file.id, val)" @click.stop />
+                </div>
+              </div>
+              <div class="file-info">
+                <div class="file-name">{{ file.name || file.originalName }}</div>
+                <div class="file-actions">
+                  <el-button link size="small" @click="handlePreview(file)">预览</el-button>
+                  <el-button link size="small" @click="handleRename(file)">重命名</el-button>
+                  <el-button link size="small" class="delete-btn" @click="handleDelete(file)"> 删除 </el-button>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <!-- 列表视图 -->
+          <div v-else class="file-list">
+            <el-table v-loading="loading" :data="fileList" style="width: 100%" @selection-change="handleSelectionChange">
+              <el-table-column type="selection" width="55" />
+              <el-table-column label="预览" width="80">
+                <template #default="{ row }">
+                  <el-image
+                    v-if="isImage(row)"
+                    :src="getImageUrl(row)"
+                    style="width: 50px; height: 50px; border-radius: 4px"
+                    fit="cover"
+                    :preview-disabled="true"
+                    lazy
+                  >
+                    <template #error>
+                      <div class="list-image-error">
+                        <el-icon size="20" color="#c0c4cc">
+                          <Picture />
+                        </el-icon>
+                      </div>
+                    </template>
+                    <template #placeholder>
+                      <div class="list-image-loading">
+                        <el-icon size="20" color="#409eff">
+                          <Loading />
+                        </el-icon>
+                      </div>
+                    </template>
+                  </el-image>
+                  <div v-else-if="isVideo(row)" class="video-thumbnail-wrapper">
+                    <video
+                      :src="getVideoUrl(row)"
+                      style="width: 50px; height: 50px; border-radius: 4px; object-fit: cover"
+                      muted
+                      preload="metadata"
+                      @loadedmetadata="onVideoLoadedMetadata"
+                      @error="onVideoError"
+                      class="video-thumbnail"
+                    ></video>
+                    <div class="video-play-overlay">
+                      <el-icon size="16" color="#fff">
+                        <VideoPlay />
+                      </el-icon>
+                    </div>
+                  </div>
+                  <el-icon v-else size="30" :color="getFileIconColor(row)">
+                    <component :is="getFileIcon(row)" />
+                  </el-icon>
+                </template>
+              </el-table-column>
+              <el-table-column label="文件名" prop="name" min-width="200">
+                <template #default="{ row }">
+                  {{ row.name || row.originalName }}
+                </template>
+              </el-table-column>
+              <el-table-column label="大小" width="100">
+                <template #default="{ row }">
+                  {{ formatFileSize(row.size) }}
+                </template>
+              </el-table-column>
+              <el-table-column label="类型" prop="type" width="120" />
+              <el-table-column label="上传时间" width="180">
+                <template #default="{ row }">
+                  {{ formatTime(row.createTime) }}
+                </template>
+              </el-table-column>
+              <el-table-column label="操作" width="200" fixed="right">
+                <template #default="{ row }">
+                  <el-button link @click="handlePreview(row)">预览</el-button>
+                  <el-button link @click="handleRename(row)">重命名</el-button>
+                  <el-button link class="delete-btn" @click="handleDelete(row)"> 删除 </el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+
+          <!-- 分页 -->
+          <div class="pagination">
+            <el-pagination
+              v-model:current-page="queryParams.pageNum"
+              v-model:page-size="queryParams.pageSize"
+              :total="total"
+              :page-sizes="[12, 24, 48, 96]"
+              layout="total, sizes, prev, pager, next, jumper"
+              @size-change="getList"
+              @current-change="getList"
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 上传对话框 -->
+    <el-dialog v-model="showUploadDialog" :title="`上传${getFileTypeText()}`" width="600px">
+      <el-form ref="uploadFormRef" :model="uploadForm" label-width="100px">
+        <!-- 文件分类选择 -->
+        <el-form-item label="文件分类" prop="categoryId">
+          <el-tree-select
+            v-model="uploadForm.categoryId"
+            :data="uploadCategoryTree"
+            :props="{ value: 'id', label: 'name', children: 'children' }"
+            placeholder="请选择文件分类(可选)"
+            style="width: 100%"
+            clearable
+            check-strictly
+            @change="onCategoryChange"
+          />
+          <div class="form-tip" v-if="selectedCategoryType">当前分类只能上传:{{ getFileTypeByCategory(selectedCategoryType) }}</div>
+        </el-form-item>
+
+        <!-- 文件上传 -->
+        <el-form-item label="文件上传" required>
+          <el-upload
+            ref="uploadRef"
+            :action="uploadUrl"
+            :headers="uploadHeaders"
+            :before-upload="beforeUpload"
+            :on-success="onUploadSuccess"
+            :on-error="onUploadError"
+            :file-list="uploadFileList"
+            multiple
+            drag
+            :accept="getUploadFileAccept()"
+          >
+            <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
+            <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+            <template #tip>
+              <div class="el-upload__tip">
+                <span v-if="uploadForm.categoryId"> 支持 {{ getUploadFileTypeText() }} 格式,单个文件不超过 50MB </span>
+                <span v-else> 支持多文件上传,单个文件不超过 50MB </span>
+              </div>
+            </template>
+          </el-upload>
+        </el-form-item>
+
+        <!-- 文件描述 -->
+        <el-form-item label="文件描述" prop="description">
+          <el-input v-model="uploadForm.description" type="textarea" :rows="3" placeholder="请输入文件描述(可选)" />
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <el-button @click="showUploadDialog = false">取消</el-button>
+        <el-button type="primary" @click="submitUpload" :loading="uploadLoading">确定</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 分类管理对话框 -->
+    <el-dialog v-model="categoryDialogVisible" :title="categoryDialogTitle" width="500px">
+      <el-form ref="categoryFormRef" :model="categoryForm" :rules="categoryRules" label-width="100px">
+        <!-- 分类名称 -->
+        <el-form-item label="分类名称" prop="name" required>
+          <el-input v-model="categoryForm.name" placeholder="请输入分类名称" />
+        </el-form-item>
+
+        <!-- 分类编码 -->
+        <el-form-item label="分类编码" prop="code" required>
+          <el-input v-model="categoryForm.code" placeholder="请输入分类编码(英文大写)" />
+        </el-form-item>
+
+        <!-- 父级分类 -->
+        <el-form-item label="父级分类" prop="parentId">
+          <el-tree-select
+            v-model="categoryForm.parentId"
+            :data="categoryTree"
+            :props="{ value: 'id', label: 'name', children: 'children' }"
+            placeholder="请选择父级分类(可选,不选则为顶级分类)"
+            clearable
+            check-strictly
+            :disabled="(categoryForm.id && isEditing) || isAddingChildCategory"
+          />
+          <div class="form-tip">
+            <span v-if="categoryForm.id && isEditing">编辑模式下不能修改父级分类</span>
+            <span v-else-if="isAddingChildCategory">正在为选定的分类添加子分类</span>
+            <span v-else>选择父级分类可以创建子分类,不选则创建顶级分类</span>
+          </div>
+        </el-form-item>
+
+        <!-- 分类类型 -->
+        <el-form-item label="分类类型" prop="type" required>
+          <el-select v-model="categoryForm.type" placeholder="请选择分类类型" style="width: 100%">
+            <el-option label="图片文件" :value="1" />
+            <el-option label="视频文件" :value="2" />
+            <el-option label="音频文件" :value="3" />
+            <el-option label="文档文件" :value="4" />
+            <el-option label="其他文件" :value="5" />
+          </el-select>
+        </el-form-item>
+
+        <!-- 排序 -->
+        <el-form-item label="排序" prop="sort">
+          <el-input-number v-model="categoryForm.sort" :min="0" :max="999" placeholder="排序值" />
+        </el-form-item>
+
+        <!-- 描述 -->
+        <el-form-item label="描述" prop="description">
+          <el-input v-model="categoryForm.description" type="textarea" :rows="3" placeholder="请输入分类描述(可选)" />
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="categoryDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="submitCategory" :loading="categoryLoading">确定</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 重命名对话框 -->
+    <el-dialog v-model="renameDialogVisible" title="重命名文件" width="400px">
+      <el-form ref="renameFormRef" :model="renameForm" label-width="80px">
+        <el-form-item label="原文件名">
+          <el-input v-model="renameForm.originalName" readonly />
+        </el-form-item>
+        <el-form-item
+          label="新文件名"
+          prop="name"
+          :rules="[
+            { required: true, message: '请输入新文件名', trigger: 'blur' },
+            { min: 1, max: 100, message: '文件名长度在 1 到 100 个字符', trigger: 'blur' }
+          ]"
+        >
+          <el-input v-model="renameForm.name" placeholder="请输入新文件名" @keyup.enter="submitRename" />
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="renameDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="submitRename">确定</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 图片裁剪组件 -->
+    <ImageCropper
+      v-model="cropDialogVisible"
+      :file="currentCropFile"
+      :image-url="cropImageUrl"
+      @confirm="handleCropConfirm"
+      @cancel="handleCropCancel"
+    />
+  </div>
+</template>
+
+<script setup name="FileInfo" lang="ts">
+import { ref, onMounted, getCurrentInstance, watch, h, computed, nextTick } from 'vue';
+import { listFileInfo, delFileInfo, addFileInfo, updateDownloadCount, updateFileInfo } from '@/api/file/info';
+import { listFileCategoryTree, addFileCategory, updateFileCategory, delFileCategory } from '@/api/file/category';
+import {
+  Plus,
+  Search,
+  UploadFilled,
+  Document,
+  VideoPlay,
+  Microphone,
+  Picture,
+  FolderOpened,
+  Edit,
+  MoreFilled,
+  Back,
+  ArrowDown,
+  Grid,
+  List,
+  Folder,
+  Loading
+} from '@element-plus/icons-vue';
+import ImageCropper from '@/components/ImageCropper/index.vue';
+import { globalHeaders } from '@/utils/request';
+import { getToken } from '@/utils/auth';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import type { FormRules } from 'element-plus';
+
+const { proxy } = getCurrentInstance();
+
+// 定义 Props - 支持选择模式
+interface Props {
+  selectMode?: boolean; // 是否为选择模式
+  fileType?: string; // 文件类型过滤('image', 'video', 'audio', 'document')
+  multiple?: boolean; // 是否支持多选
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  selectMode: false,
+  fileType: '',
+  multiple: false
+});
+
+// 定义 Emits
+const emit = defineEmits<{
+  'file-selected': [file: any];
+  'files-selected': [files: any[]];
+}>();
+
+// 计算属性 - 用于模板访问 props
+const selectMode = computed(() => props.selectMode);
+const multiple = computed(() => props.multiple);
+const fileType = computed(() => props.fileType);
+
+// 响应式数据
+const loading = ref(false);
+const fileList = ref([]);
+const total = ref(0);
+const searchKeyword = ref('');
+const selectedFiles = ref([]);
+const showUploadDialog = ref(false);
+const uploadFileList = ref([]);
+const uploadLoading = ref(false);
+const currentTopCategory = ref(null);
+const viewMode = ref('grid');
+
+// 分类相关数据
+const categoryTree = ref([]);
+const categoryList = ref([]);
+const topCategories = ref([]);
+const currentCategory = ref(null);
+const expandedKeys = ref([]);
+const categoryTreeRef = ref();
+
+// 分类管理对话框
+const categoryDialogVisible = ref(false);
+const categoryDialogTitle = ref('添加分类');
+const categoryLoading = ref(false);
+const categoryFormRef = ref();
+const isEditing = ref(false);
+const isAddingChildCategory = ref(false);
+
+// 上传表单
+const uploadFormRef = ref();
+const selectedCategoryType = ref(null);
+
+// 重命名相关数据
+const renameDialogVisible = ref(false);
+const renameForm = ref({
+  id: null,
+  name: '',
+  originalName: '',
+  currentFile: null
+});
+const renameFormRef = ref();
+
+// 图片裁剪相关数据
+const cropDialogVisible = ref(false);
+const currentCropFile = ref(null);
+const cropImageUrl = ref('');
+
+// 上传配置
+const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload');
+const uploadHeaders = ref(globalHeaders());
+
+// 查询参数
+const queryParams = ref({
+  pageNum: 1,
+  pageSize: 20,
+  name: null,
+  categoryId: null,
+  categoryType: null
+});
+
+// 上传表单数据
+const uploadForm = ref({
+  categoryId: null,
+  categoryType: null,
+  categoryName: null,
+  description: ''
+});
+
+// 分类表单数据
+const categoryForm = ref({
+  id: null,
+  name: '',
+  code: '',
+  parentId: null,
+  type: 1,
+  sort: 0,
+  description: '',
+  status: 0
+});
+
+// 分类表单验证规则
+const categoryRules = ref<FormRules>({
+  name: [
+    { required: true, message: '请输入分类名称', trigger: 'blur' },
+    { min: 2, max: 50, message: '分类名称长度在 2 到 50 个字符', trigger: 'blur' }
+  ],
+  code: [
+    { required: true, message: '请输入分类编码', trigger: 'blur' },
+    { pattern: /^[A-Z_]+$/, message: '分类编码只能包含大写字母和下划线', trigger: 'blur' }
+  ],
+  type: [{ required: true, message: '请选择分类类型', trigger: 'change' }]
+});
+
+// 树形控件配置
+const treeProps = {
+  label: 'name',
+  children: 'children'
+};
+
+// 获取文件列表
+const getList = async () => {
+  try {
+    loading.value = true;
+    const response = (await listFileInfo(queryParams.value)) as any;
+    const data = response?.data ?? response;
+    if (data && data.rows) {
+      fileList.value = data.rows as any[];
+      total.value = data.total || 0;
+    } else {
+      fileList.value = [];
+      total.value = 0;
+    }
+  } catch (error) {
+    console.error('获取文件列表失败:', error);
+    ElMessage.error('获取文件列表失败');
+    fileList.value = [];
+    total.value = 0;
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 搜索文件
+const handleSearch = () => {
+  queryParams.value.name = searchKeyword.value || null;
+  queryParams.value.pageNum = 1;
+  getList();
+};
+
+// 多选处理
+const handleSelectionChange = (selection) => {
+  selectedFiles.value = selection.map((item) => item.id);
+};
+
+// 获取分类树
+const getCategoryTree = async () => {
+  try {
+    const response = await listFileCategoryTree();
+    categoryTree.value = response.data || [];
+    categoryList.value = flattenCategories(categoryTree.value);
+
+    console.log('getCategoryTree - response.data:', response.data);
+    console.log('getCategoryTree - categoryTree.value:', categoryTree.value);
+
+    // 获取顶级分类(parent_id为0的分类)
+    topCategories.value = categoryTree.value.filter((category) => category.parentId === 0 || category.parentId === null);
+    console.log('getCategoryTree - topCategories.value:', topCategories.value);
+
+    // 默认不选择任何顶级分类,显示所有文件
+    if (!currentTopCategory.value) {
+      currentTopCategory.value = null;
+      queryParams.value.categoryType = null;
+      console.log('getCategoryTree - set currentTopCategory to null (show all files)');
+    }
+
+    return response;
+  } catch (error) {
+    console.error('获取分类树失败:', error);
+    ElMessage.error('获取分类树失败');
+    throw error;
+  }
+};
+
+// 扁平化分类列表
+const flattenCategories = (categories, result = []) => {
+  categories.forEach((category) => {
+    result.push(category);
+    if (category.children && category.children.length > 0) {
+      flattenCategories(category.children, result);
+    }
+  });
+  return result;
+};
+
+// 分类点击事件
+const handleCategoryClick = (data) => {
+  currentCategory.value = data;
+  queryParams.value.categoryId = data.id;
+  queryParams.value.pageNum = 1;
+  getList();
+};
+
+// 树节点点击事件
+const handleTreeNodeClick = (data, node, treeComponent) => {
+  handleCategoryClick(data);
+};
+
+// 显示全部文件
+const handleShowAllFiles = () => {
+  currentCategory.value = null;
+  queryParams.value.categoryId = null;
+  queryParams.value.pageNum = 1;
+  getList();
+};
+
+// 切换分类展开状态
+const toggleExpand = (categoryId) => {
+  const index = expandedKeys.value.indexOf(categoryId);
+  if (index > -1) {
+    // 如果已展开,则收起
+    expandedKeys.value.splice(index, 1);
+  } else {
+    // 如果未展开,则展开
+    expandedKeys.value.push(categoryId);
+  }
+  console.log('toggleExpand - categoryId:', categoryId, 'expandedKeys:', expandedKeys.value);
+};
+
+// 文件类型判断
+const isImage = (file) => {
+  // 首先检查MIME类型
+  if (file.type?.includes('image')) {
+    return true;
+  }
+
+  // 检查文件扩展名
+  const extension = file.extension?.toLowerCase();
+  if (extension && ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'].includes(extension)) {
+    return true;
+  }
+
+  // 从文件名中提取扩展名作为备选方案
+  const fileName = file.name || file.originalName || '';
+  const fileExt = fileName.split('.').pop()?.toLowerCase();
+  if (fileExt && ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'].includes(fileExt)) {
+    return true;
+  }
+
+  return false;
+};
+
+// 视频文件类型判断
+const isVideo = (file) => {
+  // 首先检查MIME类型
+  if (file.type?.includes('video')) {
+    return true;
+  }
+
+  // 检查文件扩展名
+  const extension = file.extension?.toLowerCase();
+  if (extension && ['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm', 'm4v', '3gp'].includes(extension)) {
+    return true;
+  }
+
+  // 从文件名中提取扩展名作为备选方案
+  const fileName = file.name || file.originalName || '';
+  const fileExt = fileName.split('.').pop()?.toLowerCase();
+  if (fileExt && ['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm', 'm4v', '3gp'].includes(fileExt)) {
+    return true;
+  }
+
+  return false;
+};
+
+// 获取图片URL
+const getImageUrl = (file) => {
+  // 优先使用url字段
+  if (file.url) {
+    // 如果是完整的URL,直接返回
+    if (file.url.startsWith('http://') || file.url.startsWith('https://')) {
+      return file.url;
+    }
+    // 如果是相对路径,添加基础URL
+    if (file.url.startsWith('/')) {
+      return import.meta.env.VITE_APP_BASE_API + file.url;
+    }
+    return file.url;
+  }
+
+  // 备选方案:使用path字段
+  if (file.path) {
+    if (file.path.startsWith('http://') || file.path.startsWith('https://')) {
+      return file.path;
+    }
+    if (file.path.startsWith('/')) {
+      return import.meta.env.VITE_APP_BASE_API + file.path;
+    }
+    return file.path;
+  }
+
+  // 如果都没有,返回空字符串,触发错误处理
+  return '';
+};
+
+// 获取视频URL
+const getVideoUrl = (file) => {
+  // 优先使用url字段
+  if (file.url) {
+    // 如果是完整的URL,直接返回
+    if (file.url.startsWith('http://') || file.url.startsWith('https://')) {
+      return file.url;
+    }
+    // 如果是相对路径,添加基础URL
+    if (file.url.startsWith('/')) {
+      return import.meta.env.VITE_APP_BASE_API + file.url;
+    }
+    return file.url;
+  }
+
+  // 备选方案:使用path字段
+  if (file.path) {
+    if (file.path.startsWith('http://') || file.path.startsWith('https://')) {
+      return file.path;
+    }
+    if (file.path.startsWith('/')) {
+      return import.meta.env.VITE_APP_BASE_API + file.path;
+    }
+    return file.path;
+  }
+
+  // 如果都没有,返回空字符串,触发错误处理
+  return '';
+};
+
+const getFileIcon = (file) => {
+  if (file.type?.includes('video')) return VideoPlay;
+  if (file.type?.includes('audio')) return Microphone;
+  if (file.type?.includes('pdf')) return Document;
+  if (file.type?.includes('word') || file.extension === 'doc' || file.extension === 'docx') return Edit;
+  if (file.type?.includes('zip') || file.extension === 'zip') return FolderOpened;
+  return Document;
+};
+
+const getFileIconColor = (file) => {
+  if (file.type?.includes('video')) return '#409eff';
+  if (file.type?.includes('audio')) return '#67c23a';
+  if (file.type?.includes('pdf')) return '#f56c6c';
+  return '#909399';
+};
+
+// 视频缩略图相关事件处理
+const onVideoLoadedMetadata = (event) => {
+  const video = event.target;
+  // 跳转到视频的第一秒或10%位置来获取有意义的缩略图
+  video.currentTime = Math.min(1, video.duration * 0.1);
+};
+
+const onVideoError = (event) => {
+  console.warn('视频加载失败:', event.target.src);
+  // 可以在这里添加错误处理逻辑,比如显示默认图标
+};
+
+// 预览文件
+const handlePreview = (file) => {
+  if (file.type?.includes('image')) {
+    // 使用弹窗预览图片
+    handlePreviewImage(file);
+  } else if (file.type?.includes('video')) {
+    handlePlayVideo(file);
+  } else if (file.type?.includes('audio')) {
+    handlePlayAudio(file);
+  } else if (file.type?.includes('pdf')) {
+    handlePreviewPdf(file);
+  } else if (file.type?.includes('text')) {
+    handlePreviewText(file);
+  } else {
+    // 其他文件类型使用弹窗预览
+    handlePreviewOther(file);
+  }
+};
+
+// 下载文件
+const handleDownload = async (file) => {
+  console.log('下载文件:', file);
+
+  if (!file.url && !file.path) {
+    ElMessage.error('文件URL不存在');
+    return;
+  }
+
+  // 获取文件URL
+  let fileUrl = file.url || file.path;
+  let fileName = file.name || file.originalName || 'download';
+
+  // 确保URL是完整的
+  if (fileUrl && !fileUrl.startsWith('http')) {
+    // 如果是相对路径,添加基础URL
+    fileUrl = import.meta.env.VITE_APP_BASE_API + fileUrl;
+  }
+
+  // 确保文件名有正确的扩展名
+  const extension = file.extension || fileName.split('.').pop()?.toLowerCase();
+  if (extension && !fileName.toLowerCase().endsWith('.' + extension)) {
+    fileName = fileName + '.' + extension;
+  }
+
+  console.log('文件URL:', fileUrl);
+  console.log('文件名:', fileName);
+  console.log('文件类型:', file.type);
+  console.log('文件扩展名:', extension);
+
+  try {
+    // 如果URL是OSS ID(纯数字),需要先获取文件信息
+    if (/^\d+$/.test(fileUrl)) {
+      console.log('检测到OSS ID,获取文件信息');
+      try {
+        const { listByIds } = await import('@/api/system/oss');
+        const res = await listByIds(fileUrl);
+        if (res.data && res.data.length > 0) {
+          fileUrl = res.data[0].url;
+          fileName = res.data[0].originalName || fileName;
+          console.log('获取到OSS文件信息:', fileUrl, fileName);
+        } else {
+          ElMessage.error('未找到对应的文件信息');
+          return;
+        }
+      } catch (error) {
+        console.error('获取OSS文件信息失败:', error);
+        ElMessage.error('获取文件信息失败');
+        return;
+      }
+    }
+
+    // 使用fetch下载文件
+    const response = await fetch(fileUrl, {
+      method: 'GET',
+      headers: {
+        'Authorization': 'Bearer ' + getToken()
+      }
+    });
+
+    if (!response.ok) {
+      throw new Error(`HTTP error! status: ${response.status}`);
+    }
+
+    const blob = await response.blob();
+
+    // 根据文件类型设置正确的MIME类型
+    let mimeType = file.type || 'application/octet-stream';
+
+    // 如果文件类型不正确,根据扩展名推断
+    if (!mimeType || mimeType === 'application/octet-stream') {
+      switch (extension) {
+        case 'jpg':
+        case 'jpeg':
+          mimeType = 'image/jpeg';
+          break;
+        case 'png':
+          mimeType = 'image/png';
+          break;
+        case 'gif':
+          mimeType = 'image/gif';
+          break;
+        case 'bmp':
+          mimeType = 'image/bmp';
+          break;
+        case 'webp':
+          mimeType = 'image/webp';
+          break;
+        case 'mp4':
+          mimeType = 'video/mp4';
+          break;
+        case 'avi':
+          mimeType = 'video/x-msvideo';
+          break;
+        case 'mov':
+          mimeType = 'video/quicktime';
+          break;
+        case 'wmv':
+          mimeType = 'video/x-ms-wmv';
+          break;
+        case 'flv':
+          mimeType = 'video/x-flv';
+          break;
+        case 'mkv':
+          mimeType = 'video/x-matroska';
+          break;
+        case 'mp3':
+          mimeType = 'audio/mpeg';
+          break;
+        case 'wav':
+          mimeType = 'audio/wav';
+          break;
+        case 'flac':
+          mimeType = 'audio/flac';
+          break;
+        case 'aac':
+          mimeType = 'audio/aac';
+          break;
+        case 'ogg':
+          mimeType = 'audio/ogg';
+          break;
+        case 'pdf':
+          mimeType = 'application/pdf';
+          break;
+        case 'doc':
+          mimeType = 'application/msword';
+          break;
+        case 'docx':
+          mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
+          break;
+        case 'xls':
+          mimeType = 'application/vnd.ms-excel';
+          break;
+        case 'xlsx':
+          mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
+          break;
+        case 'ppt':
+          mimeType = 'application/vnd.ms-powerpoint';
+          break;
+        case 'pptx':
+          mimeType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
+          break;
+        case 'txt':
+          mimeType = 'text/plain';
+          break;
+        case 'zip':
+          mimeType = 'application/zip';
+          break;
+        case 'rar':
+          mimeType = 'application/vnd.rar';
+          break;
+        case '7z':
+          mimeType = 'application/x-7z-compressed';
+          break;
+        default:
+          mimeType = 'application/octet-stream';
+      }
+    }
+
+    console.log('使用的MIME类型:', mimeType);
+    console.log('最终文件名:', fileName);
+
+    // 创建带有正确MIME类型的blob
+    const correctBlob = new Blob([blob], { type: mimeType });
+
+    // 创建下载链接
+    const url = window.URL.createObjectURL(correctBlob);
+    const link = document.createElement('a');
+    link.href = url;
+    link.download = fileName;
+    link.style.display = 'none';
+
+    // 添加到DOM并触发下载
+    document.body.appendChild(link);
+    link.click();
+
+    // 清理
+    document.body.removeChild(link);
+    window.URL.revokeObjectURL(url);
+
+    ElMessage.success('文件下载已开始');
+
+    // 更新下载次数
+    updateDownloadCount(file.id)
+      .then(() => {
+        console.log('下载次数已更新');
+      })
+      .catch((error) => {
+        console.error('更新下载次数失败:', error);
+      });
+  } catch (error) {
+    console.error('下载文件失败:', error);
+    const msg = error instanceof Error ? error.message : String(error);
+    ElMessage.error('下载文件失败: ' + msg);
+  }
+};
+
+// 重命名文件
+const handleRename = (file) => {
+  console.log('重命名文件:', file);
+  renameForm.value = {
+    id: file.id,
+    name: file.name || file.originalName || '',
+    originalName: file.name || file.originalName || '',
+    currentFile: file // 保存完整的文件信息
+  };
+  renameDialogVisible.value = true;
+};
+
+// 提交重命名
+const submitRename = async () => {
+  try {
+    await renameFormRef.value?.validate();
+
+    if (!renameForm.value.name.trim()) {
+      ElMessage.error('请输入新文件名');
+      return;
+    }
+
+    if (renameForm.value.name === renameForm.value.originalName) {
+      ElMessage.warning('新文件名与原文件名相同');
+      return;
+    }
+
+    // 调用重命名API
+    const file = renameForm.value.currentFile;
+    await updateFileInfo({
+      id: renameForm.value.id,
+      name: renameForm.value.name.trim(),
+      originalName: file.originalName,
+      path: file.path,
+      url: file.url,
+      size: file.size,
+      type: file.type,
+      extension: file.extension,
+      categoryId: file.categoryId,
+      description: file.description
+    });
+
+    ElMessage.success('重命名成功');
+    renameDialogVisible.value = false;
+
+    // 刷新文件列表
+    getList();
+
+    console.log('重命名文件:', {
+      id: renameForm.value.id,
+      oldName: renameForm.value.originalName,
+      newName: renameForm.value.name.trim()
+    });
+  } catch (error) {
+    console.error('重命名失败:', error);
+    ElMessage.error('重命名失败');
+  }
+};
+
+// 删除文件
+const handleDelete = async (file) => {
+  try {
+    await ElMessageBox.confirm(`确定要删除文件"${file.name || file.originalName}"吗?`, '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    });
+
+    await delFileInfo(file.id);
+    ElMessage.success('删除成功');
+    getList();
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error('删除失败:', error);
+      ElMessage.error('删除失败');
+    }
+  }
+};
+
+// 批量删除
+const handleBatchDelete = async () => {
+  if (selectedFiles.value.length === 0) {
+    ElMessage.warning('请选择要删除的文件');
+    return;
+  }
+
+  try {
+    await ElMessageBox.confirm(`确定要删除选中的 ${selectedFiles.value.length} 个文件吗?`, '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    });
+
+    const promises = selectedFiles.value.map((id) => delFileInfo(id));
+    await Promise.all(promises);
+    ElMessage.success('批量删除成功');
+    selectedFiles.value = [];
+    getList();
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error('批量删除失败:', error);
+      ElMessage.error('批量删除失败');
+    }
+  }
+};
+
+// 移动到分类
+const handleMoveToCategory = async (categoryId) => {
+  if (selectedFiles.value.length === 0) {
+    ElMessage.warning('请选择要移动的文件');
+    return;
+  }
+
+  try {
+    await ElMessageBox.confirm(
+      `确定要将选中的 ${selectedFiles.value.length} 个文件移动到分类"${categoryList.value.find((cat) => cat.id === categoryId)?.name}"吗?`,
+      '提示',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }
+    );
+
+    const promises = selectedFiles.value.map((id) => updateFileInfoCategory(id, categoryId));
+    await Promise.all(promises);
+    ElMessage.success('批量移动成功');
+    selectedFiles.value = [];
+    getList();
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error('批量移动失败:', error);
+      ElMessage.error('批量移动失败');
+    }
+  }
+};
+
+// 更新文件分类
+const updateFileInfoCategory = async (fileId, categoryId) => {
+  const fileInfo = {
+    id: fileId,
+    categoryId: categoryId
+  };
+  // 这里需要调用更新文件信息的API,暂时注释掉
+  // await updateFileInfo(fileInfo);
+  console.log('更新文件分类:', fileId, categoryId);
+};
+
+// 添加分类
+const handleAddCategory = () => {
+  categoryDialogVisible.value = true;
+  categoryDialogTitle.value = '添加分类';
+  categoryForm.value = {
+    id: null,
+    name: '',
+    code: '',
+    parentId: null,
+    type: 1,
+    sort: 0,
+    description: '',
+    status: 0
+  };
+  isEditing.value = false;
+  isAddingChildCategory.value = false;
+};
+
+// 添加子分类
+const handleAddChildCategory = (parentCategory) => {
+  console.log('添加子分类,父分类:', parentCategory);
+  categoryDialogVisible.value = true;
+  categoryDialogTitle.value = `添加子分类 - ${parentCategory.name}`;
+  categoryForm.value = {
+    id: null,
+    name: '',
+    code: '',
+    parentId: parentCategory.id, // 设置父分类ID
+    type: parentCategory.type, // 继承父分类的类型
+    sort: 0,
+    description: '',
+    status: 0
+  };
+  isEditing.value = false;
+  isAddingChildCategory.value = true;
+};
+
+// 打开上传对话框
+const openUploadDialog = () => {
+  showUploadDialog.value = true;
+  // 自动填充当前主分类的类型和名称
+  const defaultCategoryType = currentTopCategory.value ? currentTopCategory.value.type : null;
+  const defaultCategoryName = currentTopCategory.value ? currentTopCategory.value.name : null;
+
+  uploadForm.value = {
+    categoryId: null,
+    categoryType: defaultCategoryType,
+    categoryName: defaultCategoryName,
+    description: ''
+  };
+  uploadFileList.value = [];
+  selectedCategoryType.value = defaultCategoryType;
+};
+
+// 编辑分类
+const handleEditCategory = (data) => {
+  categoryDialogVisible.value = true;
+  categoryDialogTitle.value = '编辑分类';
+  categoryForm.value = { ...data };
+  isEditing.value = true;
+  isAddingChildCategory.value = false;
+};
+
+// 删除分类
+const handleDeleteCategory = async (data) => {
+  try {
+    await ElMessageBox.confirm(`确定要删除分类"${data.name}"吗?`, '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    });
+
+    await delFileCategory(data.id);
+    ElMessage.success('删除成功');
+    getCategoryTree();
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error('删除分类失败:', error);
+      ElMessage.error('删除分类失败');
+    }
+  }
+};
+
+// 分类操作处理
+const handleCategoryAction = (command) => {
+  if (command.action === 'addChild') {
+    handleAddChildCategory(command.data);
+  } else if (command.action === 'edit') {
+    handleEditCategory(command.data);
+  } else if (command.action === 'delete') {
+    // 检查是否有子分类
+    if (command.data.children && command.data.children.length > 0) {
+      ElMessage.warning('该分类下有子分类,请先删除子分类');
+      return;
+    }
+    handleDeleteCategory(command.data);
+  }
+};
+
+// 提交分类
+const submitCategory = async () => {
+  try {
+    await categoryFormRef.value?.validate();
+    categoryLoading.value = true;
+
+    const categoryData = {
+      ...categoryForm.value,
+      tenantId: '000000'
+    };
+
+    if (categoryForm.value.id) {
+      await updateFileCategory(categoryData);
+    } else {
+      await addFileCategory(categoryData);
+    }
+
+    ElMessage.success(categoryForm.value.id ? '更新成功' : '添加成功');
+    categoryDialogVisible.value = false;
+    getCategoryTree();
+  } catch (error) {
+    console.error('保存分类失败:', error);
+    ElMessage.error('保存分类失败');
+  } finally {
+    categoryLoading.value = false;
+  }
+};
+
+// 上传前检查
+const beforeUpload = (file) => {
+  console.log('开始上传文件:', file.name, '原始MIME类型:', file.type);
+
+  // 获取准确的MIME类型
+  const actualMimeType = getFileMimeType(file);
+  const fileName = file.name || '';
+  const extension = fileName.split('.').pop()?.toLowerCase();
+
+  console.log('检测到的MIME类型:', actualMimeType, '文件扩展名:', extension);
+
+  // 如果是图片文件,且当前分类是图片分类,则打开裁剪对话框
+  const currentCategoryType = uploadForm.value.categoryType || getCategoryType(uploadForm.value.categoryId);
+  const isImageFile = actualMimeType.startsWith('image/') || ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(extension);
+
+  if (isImageFile && (currentCategoryType === 1 || !currentCategoryType)) {
+    // 延迟打开裁剪对话框,让上传组件先完成初始化
+    setTimeout(() => {
+      openCropDialog(file);
+    }, 100);
+
+    // 返回false阻止自动上传,等待裁剪完成后手动上传
+    return false;
+  }
+
+  // 如果选择了分类或有分类类型,检查文件类型
+  const selectedCategoryType = uploadForm.value.categoryType || getCategoryType(uploadForm.value.categoryId);
+
+  if (selectedCategoryType) {
+    let isValidType = false;
+    let fileTypeText = '';
+
+    if (selectedCategoryType === 1) {
+      // 图片分类
+      isValidType = actualMimeType.startsWith('image/') || ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(extension);
+      fileTypeText = '图片';
+    } else if (selectedCategoryType === 2) {
+      // 视频分类
+      isValidType = actualMimeType.startsWith('video/') || ['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm'].includes(extension);
+      fileTypeText = '视频';
+    } else if (selectedCategoryType === 3) {
+      // 音频分类
+      isValidType = actualMimeType.startsWith('audio/') || ['mp3', 'wav', 'flac', 'aac', 'ogg', 'm4a'].includes(extension);
+      fileTypeText = '音频';
+    } else if (selectedCategoryType === 4) {
+      // 文档分类
+      isValidType =
+        actualMimeType.includes('pdf') ||
+        actualMimeType.includes('document') ||
+        actualMimeType.includes('word') ||
+        actualMimeType.includes('excel') ||
+        actualMimeType.includes('powerpoint') ||
+        actualMimeType.includes('text') ||
+        actualMimeType === 'application/msword' ||
+        actualMimeType === 'application/vnd.ms-excel' ||
+        actualMimeType === 'application/vnd.ms-powerpoint' ||
+        ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'].includes(extension);
+      fileTypeText = '文档';
+    } else {
+      isValidType = true;
+      fileTypeText = '文件';
+    }
+
+    if (!isValidType) {
+      ElMessage.error(`当前分类只能上传${fileTypeText}文件! 检测到的文件类型: ${actualMimeType}`);
+      return false;
+    }
+
+    console.log('文件类型验证通过:', fileTypeText, '类型');
+  }
+
+  // 检查文件大小 (50MB)
+  const isLtSize = file.size / 1024 / 1024 < 50;
+  if (!isLtSize) {
+    ElMessage.error('上传文件大小不能超过 50MB!');
+    return false;
+  }
+
+  console.log('文件大小验证通过:', (file.size / 1024 / 1024).toFixed(2), 'MB');
+
+  // 非图片文件或非图片分类,直接上传
+  return true;
+};
+
+// 获取文件MIME类型
+const getFileMimeType = (file) => {
+  // 优先使用浏览器检测的MIME类型
+  if (file.type) {
+    return file.type;
+  }
+
+  // 如果浏览器无法检测,根据文件扩展名推断
+  const fileName = file.name || '';
+  const extension = fileName.split('.').pop()?.toLowerCase();
+
+  const mimeTypeMap = {
+    // 图片类型
+    'jpg': 'image/jpeg',
+    'jpeg': 'image/jpeg',
+    'png': 'image/png',
+    'gif': 'image/gif',
+    'bmp': 'image/bmp',
+    'webp': 'image/webp',
+    'svg': 'image/svg+xml',
+
+    // 视频类型
+    'mp4': 'video/mp4',
+    'avi': 'video/x-msvideo',
+    'mov': 'video/quicktime',
+    'wmv': 'video/x-ms-wmv',
+    'flv': 'video/x-flv',
+    'mkv': 'video/x-matroska',
+    'webm': 'video/webm',
+
+    // 音频类型
+    'mp3': 'audio/mpeg',
+    'wav': 'audio/wav',
+    'flac': 'audio/flac',
+    'aac': 'audio/aac',
+    'ogg': 'audio/ogg',
+    'm4a': 'audio/mp4',
+
+    // 文档类型
+    'pdf': 'application/pdf',
+    'doc': 'application/msword',
+    'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+    'xls': 'application/vnd.ms-excel',
+    'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+    'ppt': 'application/vnd.ms-powerpoint',
+    'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+    'txt': 'text/plain',
+
+    // 压缩文件
+    'zip': 'application/zip',
+    'rar': 'application/vnd.rar',
+    '7z': 'application/x-7z-compressed',
+    'tar': 'application/x-tar',
+    'gz': 'application/gzip',
+
+    // 其他常见类型
+    'json': 'application/json',
+    'xml': 'application/xml',
+    'csv': 'text/csv',
+    'html': 'text/html',
+    'css': 'text/css',
+    'js': 'application/javascript'
+  };
+
+  return mimeTypeMap[extension] || 'application/octet-stream';
+};
+
+// 上传成功
+const onUploadSuccess = (response, file) => {
+  if (response.code === 200) {
+    ElMessage.success('文件上传成功');
+
+    // 获取文件MIME类型和扩展名
+    const mimeType = getFileMimeType(file);
+    const fileName = file.name || '';
+    const extension = fileName.split('.').pop()?.toLowerCase() || '';
+
+    uploadFileList.value.push({
+      name: file.name,
+      url: response.data?.url || '',
+      size: file.size,
+      type: mimeType,
+      extension: extension,
+      originalName: file.name,
+      ossId: response.data?.ossId || ''
+    });
+
+    console.log('文件上传成功,MIME类型:', mimeType, '扩展名:', extension);
+  } else {
+    ElMessage.error('上传失败:' + response.msg);
+  }
+};
+
+// 上传失败
+const onUploadError = (error) => {
+  console.error('上传失败:', error);
+  ElMessage.error('上传失败');
+};
+
+// 提交上传
+const submitUpload = async () => {
+  if (uploadFileList.value.length === 0) {
+    ElMessage.error('请先上传文件');
+    return;
+  }
+
+  // 验证分类信息,如果没有分类信息,提示用户选择分类
+  if (!uploadForm.value.categoryType && !uploadForm.value.categoryId) {
+    ElMessage.error('请先选择文件分类(点击顶部分类标签)或在上传对话框中选择具体分类');
+    return;
+  }
+
+  try {
+    uploadLoading.value = true;
+
+    // 批量保存文件信息
+    const promises = uploadFileList.value.map((file) => {
+      const fileInfo = {
+        name: file.name,
+        originalName: file.originalName,
+        path: file.url,
+        url: file.url,
+        size: file.size,
+        type: file.type,
+        extension: file.extension,
+        categoryId: uploadForm.value.categoryId,
+        categoryType: uploadForm.value.categoryType,
+        description: uploadForm.value.description || '',
+        uploadStatus: 1,
+        isPublic: 1,
+        status: 0,
+        downloadCount: 0,
+        viewCount: 0
+      };
+
+      console.log('保存文件信息到后端:', {
+        文件名: fileInfo.name,
+        原始文件名: fileInfo.originalName,
+        MIME类型: fileInfo.type,
+        文件扩展名: fileInfo.extension,
+        文件大小: (fileInfo.size / 1024 / 1024).toFixed(2) + 'MB',
+        文件主分类: fileInfo.categoryType,
+        分类ID: fileInfo.categoryId,
+        文件URL: fileInfo.url
+      });
+
+      return addFileInfo(fileInfo);
+    });
+
+    await Promise.all(promises);
+
+    ElMessage.success('文件上传成功');
+    showUploadDialog.value = false;
+    uploadFileList.value = [];
+    uploadForm.value = {
+      categoryId: null,
+      categoryType: null,
+      categoryName: null,
+      description: ''
+    };
+    getList();
+  } catch (error) {
+    console.error('保存文件信息失败:', error);
+    ElMessage.error('保存文件信息失败');
+  } finally {
+    uploadLoading.value = false;
+  }
+};
+
+// 获取分类类型
+const getCategoryType = (categoryId) => {
+  const category = categoryList.value.find((cat) => cat.id === categoryId);
+  return category ? category.type : null;
+};
+
+// 获取分类文件类型文本
+const getFileTypeByCategory = (categoryType) => {
+  if (categoryType === 1) return 'jpg/png/gif/bmp/webp';
+  if (categoryType === 2) return 'mp4/avi/mov/wmv/flv/mkv';
+  if (categoryType === 3) return 'mp3/wav/flac/aac/ogg';
+  if (categoryType === 4) return 'pdf/doc/docx/xls/xlsx/ppt/pptx/txt';
+  if (categoryType === 5) return '所有文件';
+  return '所有文件';
+};
+
+// 获取上传文件接受类型
+const getUploadFileAccept = () => {
+  if (uploadForm.value.categoryId) {
+    const categoryType = getCategoryType(uploadForm.value.categoryId);
+    if (categoryType === 1) return '.jpg,.jpeg,.png,.gif,.bmp,.webp';
+    if (categoryType === 2) return '.mp4,.avi,.mov,.wmv,.flv,.mkv';
+    if (categoryType === 3) return '.mp3,.wav,.flac,.aac,.ogg';
+    if (categoryType === 4) return '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt';
+    if (categoryType === 5) return ''; // 其他文件类型允许所有文件格式
+  }
+  return '';
+};
+
+// 获取上传文件类型文本
+const getUploadFileTypeText = () => {
+  if (uploadForm.value.categoryId) {
+    const categoryType = getCategoryType(uploadForm.value.categoryId);
+    return getFileTypeByCategory(categoryType);
+  }
+  return '所有文件';
+};
+
+// 类别变化时更新上传表单
+const onCategoryChange = () => {
+  selectedCategoryType.value = getCategoryType(uploadForm.value.categoryId);
+};
+
+// 打开裁剪对话框
+const openCropDialog = (file) => {
+  console.log('准备打开裁剪对话框,文件:', file.name, '大小:', (file.size / 1024 / 1024).toFixed(2), 'MB');
+  currentCropFile.value = file;
+
+  // 清除可能的表单验证状态
+  nextTick(() => {
+    // 清除上传表单的验证状态
+    uploadFormRef.value?.clearValidate();
+    // 清除分类表单的验证状态
+    categoryFormRef.value?.clearValidate();
+    // 清除重命名表单的验证状态
+    renameFormRef.value?.clearValidate();
+  });
+
+  // 读取文件为base64
+  const reader = new FileReader();
+  reader.onload = (e) => {
+    const result = e.target?.result as string;
+    if (result) {
+      console.log('图片已转换为base64,长度:', result.length);
+      cropImageUrl.value = result;
+
+      // 延迟打开对话框,确保图片已加载
+      setTimeout(() => {
+        cropDialogVisible.value = true;
+      }, 100);
+    }
+  };
+
+  reader.onerror = () => {
+    ElMessage.error('读取图片文件失败');
+  };
+
+  reader.readAsDataURL(file);
+};
+
+// 处理裁剪确认
+const handleCropConfirm = async (croppedFile) => {
+  try {
+    console.log('裁剪完成,新文件大小:', (croppedFile.size / 1024 / 1024).toFixed(2), 'MB');
+
+    // 上传裁剪后的文件
+    await uploadCroppedFile(croppedFile);
+  } catch (error) {
+    console.error('处理裁剪文件失败:', error);
+    const errorMessage = error instanceof Error ? error.message : String(error);
+    ElMessage.error('处理失败: ' + errorMessage);
+  }
+};
+
+// 处理裁剪取消
+const handleCropCancel = () => {
+  currentCropFile.value = null;
+  cropImageUrl.value = '';
+
+  // 关闭对话框后清除表单验证状态
+  nextTick(() => {
+    uploadFormRef.value?.clearValidate();
+    categoryFormRef.value?.clearValidate();
+    renameFormRef.value?.clearValidate();
+  });
+};
+
+// 上传裁剪后的文件
+const uploadCroppedFile = async (file) => {
+  try {
+    // 创建FormData
+    const formData = new FormData();
+    formData.append('file', file);
+
+    // 上传文件
+    const response = await fetch(uploadUrl.value, {
+      method: 'POST',
+      headers: uploadHeaders.value,
+      body: formData
+    });
+
+    const result = await response.json();
+
+    if (result.code === 200) {
+      ElMessage.success('裁剪并上传成功');
+
+      // 添加到上传文件列表
+      const mimeType = getFileMimeType(file);
+      const fileName = file.name || '';
+      const extension = fileName.split('.').pop()?.toLowerCase() || '';
+
+      uploadFileList.value.push({
+        name: file.name,
+        url: result.data?.url || '',
+        size: file.size,
+        type: mimeType,
+        extension: extension,
+        originalName: file.name,
+        ossId: result.data?.ossId || ''
+      });
+
+      // 清理裁剪状态
+      handleCropCancel();
+    } else {
+      throw new Error(result.msg || '上传失败');
+    }
+  } catch (error) {
+    console.error('上传裁剪文件失败:', error);
+    const errorMessage = error instanceof Error ? error.message : String(error);
+    ElMessage.error('上传失败: ' + errorMessage);
+  }
+};
+
+// 格式化文件大小
+const formatFileSize = (size) => {
+  if (!size) return '0 B';
+  const units = ['B', 'KB', 'MB', 'GB'];
+  let index = 0;
+  let fileSize = size;
+  while (fileSize >= 1024 && index < units.length - 1) {
+    fileSize /= 1024;
+    index++;
+  }
+  return fileSize.toFixed(2) + ' ' + units[index];
+};
+
+// 格式化时间
+const formatTime = (time) => {
+  if (!time) return '';
+  return new Date(time).toLocaleString();
+};
+
+// 预览图片
+const handlePreviewImage = (file) => {
+  if (!file.url && !file.path) {
+    ElMessage.error('图片文件地址不存在');
+    return;
+  }
+
+  // 使用ElMessageBox创建图片预览对话框
+  const imageUrl = file.url || file.path;
+  ElMessageBox({
+    title: `预览图片 - ${file.name || file.originalName}`,
+    message: h('div', { style: 'text-align: center;' }, [
+      h('img', {
+        src: imageUrl,
+        style: 'max-width: 100%; max-height: 500px; height: auto; border-radius: 4px;',
+        onError: () => {
+          ElMessage.error('图片加载失败');
+        }
+      })
+    ]),
+    showCancelButton: false,
+    confirmButtonText: '关闭',
+    customClass: 'image-preview-dialog'
+  });
+};
+
+// 播放视频
+const handlePlayVideo = (file) => {
+  if (!file.url && !file.path) {
+    ElMessage.error('视频文件地址不存在');
+    return;
+  }
+
+  // 创建视频播放对话框
+  const videoUrl = file.url || file.path;
+  ElMessageBox({
+    title: `播放视频 - ${file.name || file.originalName}`,
+    message: h('div', { style: 'text-align: center;' }, [
+      h('video', {
+        src: videoUrl,
+        controls: true,
+        autoplay: true,
+        style: 'width: 100%; max-width: 800px; height: auto;'
+      })
+    ]),
+    showCancelButton: false,
+    confirmButtonText: '关闭',
+    customClass: 'video-player-dialog'
+  });
+};
+
+// 播放音频
+const handlePlayAudio = (file) => {
+  if (!file.url && !file.path) {
+    ElMessage.error('音频文件地址不存在');
+    return;
+  }
+
+  console.log('开始播放音频文件:', file);
+
+  // 获取完整的音频URL
+  let audioUrl = file.url || file.path;
+
+  // 处理URL,确保是完整路径
+  if (audioUrl) {
+    // 如果是OSS ID(纯数字),需要特殊处理
+    if (/^\d+$/.test(audioUrl)) {
+      // 直接打开新窗口播放
+      window.open(import.meta.env.VITE_APP_BASE_API + '/resource/oss/download/' + audioUrl, '_blank');
+      return;
+    }
+
+    // 处理相对路径
+    if (!audioUrl.startsWith('http')) {
+      // 如果是相对路径,添加基础URL
+      audioUrl = import.meta.env.VITE_APP_BASE_API + (audioUrl.startsWith('/') ? audioUrl : '/' + audioUrl);
+    }
+  }
+
+  console.log('处理后的音频URL:', audioUrl);
+
+  // 创建一个临时的audio元素测试URL是否可访问
+  const testAudio = new Audio();
+  testAudio.src = audioUrl;
+  testAudio.oncanplay = () => {
+    console.log('音频可以播放,URL有效');
+    // URL有效,创建音频播放对话框
+    showAudioDialog(file, audioUrl);
+  };
+
+  testAudio.onerror = (e) => {
+    console.error('音频URL测试失败:', e);
+    // 尝试直接在新窗口打开
+    const confirmed = window.confirm(`音频预览加载失败,是否在新窗口打开?`);
+    if (confirmed) {
+      window.open(audioUrl, '_blank');
+    }
+  };
+};
+
+// 显示音频播放对话框
+const showAudioDialog = (file, audioUrl) => {
+  // 创建音频播放对话框
+  ElMessageBox({
+    title: `播放音频 - ${file.name || file.originalName}`,
+    message: h('div', { style: 'text-align: center;' }, [
+      h('div', { class: 'audio-container' }, [
+        h('audio', {
+          src: audioUrl,
+          controls: true,
+          autoplay: false,
+          preload: 'auto',
+          style: 'width: 100%; max-width: 500px;',
+          ref: 'audioPlayer',
+          id: `audio-player-${file.id || Date.now()}`,
+          onError: (e) => {
+            console.error('音频加载失败:', e);
+            ElMessage.error('音频加载失败,请检查文件格式或网络连接');
+          }
+        }),
+        h('div', { class: 'audio-info' }, [
+          h('p', { style: 'margin-top: 15px; color: #606266;' }, `文件名: ${file.name || file.originalName}`),
+          h('p', { style: 'color: #909399; font-size: 12px;' }, `格式: ${file.type || '音频文件'}`),
+          h('p', { style: 'color: #909399; font-size: 12px;' }, `大小: ${formatFileSize(file.size)}`)
+        ])
+      ])
+    ]),
+    showCancelButton: true,
+    confirmButtonText: '在新窗口打开',
+    cancelButtonText: '关闭',
+    customClass: 'audio-player-dialog',
+    beforeClose: (action, instance, done) => {
+      // 关闭对话框前停止音频播放
+      try {
+        const audioId = document.getElementById(`audio-player-${file.id || Date.now()}`);
+        if (audioId && audioId instanceof HTMLAudioElement) {
+          audioId.pause();
+        }
+
+        const audioElements = document.querySelectorAll('.audio-player-dialog audio');
+        if (audioElements && audioElements.length > 0) {
+          audioElements.forEach((audio) => {
+            if (audio instanceof HTMLAudioElement) {
+              audio.pause();
+            }
+          });
+        }
+      } catch (err) {
+        console.warn('停止音频播放失败:', err);
+      }
+      done();
+    }
+  })
+    .then(() => {
+      // 用户点击"在新窗口打开"按钮
+      window.open(audioUrl, '_blank');
+    })
+    .catch(() => {
+      // 用户点击"关闭"按钮,不做任何操作
+    });
+};
+
+// 预览PDF
+const handlePreviewPdf = (file) => {
+  if (!file.url && !file.path) {
+    ElMessage.error('PDF文件地址不存在');
+    return;
+  }
+
+  const pdfUrl = file.url || file.path;
+  // 使用弹窗预览PDF
+  ElMessageBox({
+    title: `预览PDF - ${file.name || file.originalName}`,
+    message: h('div', { style: 'text-align: center;' }, [
+      h('iframe', {
+        src: pdfUrl,
+        style: 'width: 100%; height: 600px; border: none; border-radius: 4px;',
+        onError: () => {
+          ElMessage.error('PDF加载失败');
+        }
+      })
+    ]),
+    showCancelButton: false,
+    confirmButtonText: '关闭',
+    customClass: 'pdf-preview-dialog'
+  });
+};
+
+// 预览其他文件类型
+const handlePreviewOther = (file) => {
+  if (!file.url && !file.path) {
+    ElMessage.error('文件地址不存在');
+    return;
+  }
+
+  const fileUrl = file.url || file.path;
+  const fileExtension = file.extension?.toLowerCase() || '';
+
+  // 判断文件类型并使用相应的预览方式
+  if (['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(fileExtension)) {
+    // Office文档使用iframe预览
+    ElMessageBox({
+      title: `预览文档 - ${file.name || file.originalName}`,
+      message: h('div', { style: 'text-align: center;' }, [
+        h('iframe', {
+          src: `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(fileUrl)}`,
+          style: 'width: 100%; height: 600px; border: none; border-radius: 4px;',
+          onError: () => {
+            ElMessage.error('文档预览失败,请尝试下载查看');
+          }
+        })
+      ]),
+      showCancelButton: false,
+      confirmButtonText: '关闭',
+      customClass: 'document-preview-dialog'
+    });
+  } else {
+    // 其他文件类型显示文件信息和下载提示
+    ElMessageBox({
+      title: `文件信息 - ${file.name || file.originalName}`,
+      message: h('div', { style: 'text-align: center; padding: 20px;' }, [
+        h('div', { style: 'margin-bottom: 15px;' }, [h('i', { class: 'el-icon-document', style: 'font-size: 48px; color: #909399;' })]),
+        h('p', { style: 'margin: 10px 0; font-size: 16px; font-weight: 500;' }, file.name || file.originalName),
+        h('p', { style: 'margin: 5px 0; color: #666;' }, `文件类型: ${file.type || '未知'}`),
+        h('p', { style: 'margin: 5px 0; color: #666;' }, `文件大小: ${formatFileSize(file.size)}`),
+        h('p', { style: 'margin: 15px 0; color: #999;' }, '此文件类型无法直接预览,您可以下载后查看')
+      ]),
+      showCancelButton: true,
+      confirmButtonText: '下载文件',
+      cancelButtonText: '关闭',
+      customClass: 'file-info-dialog'
+    })
+      .then(() => {
+        // 点击下载按钮时执行下载
+        handleDownload(file);
+      })
+      .catch(() => {
+        // 用户取消,不做任何操作
+      });
+  }
+};
+
+// 预览文本文件
+const handlePreviewText = (file) => {
+  if (!file.url && !file.path) {
+    ElMessage.error('文本文件地址不存在');
+    return;
+  }
+
+  // 下载并读取文本内容
+  fetch(file.url || file.path)
+    .then((response) => response.text())
+    .then((text) => {
+      ElMessageBox({
+        title: `预览文本 - ${file.name || file.originalName}`,
+        message: h('div', { style: 'max-height: 400px; overflow-y: auto;' }, [
+          h('pre', { style: 'white-space: pre-wrap; word-wrap: break-word;' }, text)
+        ]),
+        showCancelButton: false,
+        confirmButtonText: '关闭',
+        customClass: 'text-preview-dialog'
+      });
+    })
+    .catch((error) => {
+      console.error('读取文本文件失败:', error);
+      ElMessage.error('读取文本文件失败');
+    });
+};
+
+// 切换顶级分类
+const switchTopCategory = (topCategory) => {
+  console.log('switchTopCategory - switching to:', topCategory);
+  currentTopCategory.value = topCategory;
+  currentCategory.value = null;
+  selectedFiles.value = [];
+  queryParams.value.pageNum = 1;
+  queryParams.value.categoryId = null;
+
+  // 设置categoryType为顶部分类的ID值
+  if (topCategory && topCategory.type) {
+    // 按类型过滤(1=图片,2=视频,3=音频,4=文档)
+    queryParams.value.categoryType = topCategory.type;
+    console.log('switchTopCategory - set categoryType to type:', topCategory.type);
+  } else {
+    queryParams.value.categoryType = null;
+    console.log('switchTopCategory - cleared categoryType (show all files)');
+  }
+
+  getList();
+};
+
+// 获取上传按钮文本
+const getUploadButtonText = () => {
+  if (!currentTopCategory.value) return '上传文件';
+
+  switch (currentTopCategory.value.type) {
+    case 1:
+      return '上传图片';
+    case 2:
+      return '上传视频';
+    case 3:
+      return '上传音频';
+    case 4:
+      return '上传文档';
+    default:
+      return '上传文件';
+  }
+};
+
+// 获取文件类型文本
+const getFileTypeText = () => {
+  if (!currentTopCategory.value) return '文件';
+
+  switch (currentTopCategory.value.type) {
+    case 1:
+      return '图片';
+    case 2:
+      return '视频';
+    case 3:
+      return '音频';
+    case 4:
+      return '文档';
+    default:
+      return '文件';
+  }
+};
+
+// 切换文件选择状态
+const toggleFileSelection = (id, checked = null) => {
+  // 选择模式下的特殊处理
+  if (props.selectMode) {
+    const file = fileList.value.find((f) => f.id === id);
+    if (!file) return;
+
+    if (props.multiple) {
+      // 多选模式
+      if (checked !== null) {
+        if (checked) {
+          if (!selectedFiles.value.includes(id)) {
+            selectedFiles.value.push(id);
+          }
+        } else {
+          selectedFiles.value = selectedFiles.value.filter((fileId) => fileId !== id);
+        }
+      } else {
+        const index = selectedFiles.value.indexOf(id);
+        if (index > -1) {
+          selectedFiles.value.splice(index, 1);
+        } else {
+          selectedFiles.value.push(id);
+        }
+      }
+
+      // 发射多选事件
+      const selectedFileObjects = fileList.value.filter((f) => selectedFiles.value.includes(f.id));
+      emit('files-selected', selectedFileObjects);
+    } else {
+      // 单选模式 - 只保留当前选中的文件
+      selectedFiles.value = [id];
+      emit('file-selected', file);
+    }
+    return;
+  }
+
+  // 非选择模式下的原有逻辑
+  if (checked !== null) {
+    // 如果传入了checked参数,使用它
+    if (checked) {
+      selectedFiles.value.push(id);
+    } else {
+      selectedFiles.value = selectedFiles.value.filter((fileId) => fileId !== id);
+    }
+  } else {
+    // 如果没有传入checked参数,切换状态
+    const index = selectedFiles.value.indexOf(id);
+    if (index > -1) {
+      selectedFiles.value.splice(index, 1);
+    } else {
+      selectedFiles.value.push(id);
+    }
+  }
+};
+
+// 根据 fileType 过滤顶级分类(选择模式下使用)
+const filteredTopCategories = computed(() => {
+  // 非选择模式或没有指定 fileType,返回所有分类
+  if (!props.selectMode || !props.fileType) {
+    return topCategories.value;
+  }
+
+  // 根据 fileType 映射到对应的分类类型
+  let targetType = null;
+  switch (props.fileType.toLowerCase()) {
+    case 'image':
+      targetType = 1;
+      break;
+    case 'video':
+      targetType = 2;
+      break;
+    case 'audio':
+      targetType = 3;
+      break;
+    case 'document':
+      targetType = 4;
+      break;
+  }
+
+  // 如果没有匹配的类型,返回所有分类
+  if (!targetType) {
+    return topCategories.value;
+  }
+
+  // 只返回匹配的分类
+  return topCategories.value.filter((cat) => cat.type === targetType);
+});
+
+// 根据当前顶级分类过滤子分类
+const filteredCategoryTree = computed(() => {
+  console.log('filteredCategoryTree computed - currentTopCategory:', currentTopCategory.value);
+  console.log('filteredCategoryTree computed - categoryTree:', categoryTree.value);
+
+  if (!currentTopCategory.value || !categoryTree.value || categoryTree.value.length === 0) {
+    console.log('filteredCategoryTree computed - returning empty array');
+    return [];
+  }
+
+  // 从树形结构中获取当前顶级分类的子分类
+  const getSubCategories = (parentId) => {
+    // 在顶级分类中查找对应的分类
+    const parentCategory = categoryTree.value.find((category) => category.id === parentId);
+    if (parentCategory && parentCategory.children) {
+      console.log('getSubCategories - parentId:', parentId, 'subCategories:', parentCategory.children);
+      return parentCategory.children;
+    }
+    console.log('getSubCategories - parentId:', parentId, 'subCategories: not found');
+    return [];
+  };
+
+  const result = getSubCategories(currentTopCategory.value.id);
+  console.log('filteredCategoryTree computed - result:', result);
+  return result;
+});
+
+// 上传对话框中的分类选项
+const uploadCategoryOptions = computed(() => {
+  if (!currentTopCategory.value || !categoryTree.value || categoryTree.value.length === 0) {
+    return [];
+  }
+
+  // 获取当前顶级分类的子分类
+  const parentCategory = categoryTree.value.find((category) => category.id === currentTopCategory.value.id);
+  if (parentCategory && parentCategory.children) {
+    return parentCategory.children;
+  }
+
+  return [];
+});
+
+// 上传对话框中的树形分类数据(仅允许选择叶子节点:非叶子设置为 disabled)
+const uploadCategoryTree = computed(() => {
+  if (!currentTopCategory.value || !categoryTree.value || categoryTree.value.length === 0) {
+    return [];
+  }
+  const mapWithDisabled = (nodes) => {
+    if (!Array.isArray(nodes)) return [];
+    return nodes.map((node) => {
+      const hasChildren = Array.isArray(node.children) && node.children.length > 0;
+      const mappedChildren = hasChildren ? mapWithDisabled(node.children) : undefined;
+      return {
+        ...node,
+        children: mappedChildren,
+        disabled: hasChildren
+      };
+    });
+  };
+  const parentCategory = categoryTree.value.find((category) => category.id === currentTopCategory.value.id);
+  const children = parentCategory && Array.isArray(parentCategory.children) ? parentCategory.children : [];
+  return mapWithDisabled(children);
+});
+
+// 监听分类变化
+watch(currentCategory, () => {
+  queryParams.value.pageNum = 1;
+  selectedFiles.value = [];
+  getList();
+});
+
+// 监听 fileType 变化,自动切换到对应分类(选择模式下)
+watch(
+  () => props.fileType,
+  async (newType) => {
+    if (newType && props.selectMode) {
+      // 等待分类树加载完成
+      if (topCategories.value.length === 0) {
+        await getCategoryTree();
+      }
+
+      let categoryType = null;
+      switch (newType.toLowerCase()) {
+        case 'image':
+          categoryType = 1;
+          break;
+        case 'video':
+          categoryType = 2;
+          break;
+        case 'audio':
+          categoryType = 3;
+          break;
+        case 'document':
+          categoryType = 4;
+          break;
+      }
+
+      if (categoryType) {
+        queryParams.value.categoryType = categoryType;
+        // 找到对应的顶级分类并自动选中
+        const topCategory = topCategories.value.find((cat) => cat.type === categoryType);
+        if (topCategory) {
+          currentTopCategory.value = topCategory;
+        }
+        getList();
+      }
+    }
+  },
+  { immediate: true }
+);
+
+// 清除选择状态(供外部调用)
+const clearSelection = () => {
+  selectedFiles.value = [];
+};
+
+// 暴露方法给父组件
+defineExpose({
+  clearSelection
+});
+
+// 页面初始化
+onMounted(async () => {
+  await getCategoryTree();
+  getList();
+});
+</script>
+
+<style scoped>
+.file-manager {
+  padding: 20px;
+  background: #f5f7fa;
+  min-height: calc(100vh - 120px);
+}
+
+/* 选择模式下的样式调整 */
+.file-manager.select-mode {
+  padding: 0;
+  background: transparent;
+  min-height: auto;
+}
+
+.file-manager.select-mode .top-categories {
+  border-radius: 0;
+  margin-bottom: 0;
+}
+
+.file-manager.select-mode .main-container {
+  margin-left: 0;
+  padding: 15px;
+  box-shadow: none;
+  border-radius: 0;
+}
+
+.file-manager.select-mode .toolbar {
+  margin-bottom: 15px;
+}
+
+/* 选择信息样式 */
+.selected-info {
+  color: #409eff;
+  font-size: 14px;
+  font-weight: 500;
+  padding-left: 12px;
+}
+
+/* 顶部分类导航 */
+.top-categories {
+  background: white;
+  border-radius: 8px 8px 0 0;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  margin-bottom: 0;
+  padding: 0 20px;
+}
+
+.category-tabs {
+  display: flex;
+  gap: 0;
+}
+
+.category-tab {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 16px 24px;
+  cursor: pointer;
+  border-bottom: 3px solid transparent;
+  transition: all 0.3s ease;
+  color: #606266;
+  font-size: 14px;
+  font-weight: 500;
+}
+
+.category-tab:hover {
+  color: #409eff;
+  background: #f5f7fa;
+}
+
+.category-tab.active {
+  color: #409eff;
+  border-bottom-color: #409eff;
+  background: #f0f9ff;
+}
+
+.category-tab .el-icon {
+  font-size: 18px;
+}
+
+/* 主容器 */
+.main-container {
+  background: #f5f7fa;
+  padding: 20px;
+  border-radius: 0 0 8px 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  margin-left: 0px !important;
+}
+
+.content-wrapper {
+  display: flex;
+  gap: 20px;
+  margin-top: 20px;
+  height: 100%;
+}
+
+.toolbar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+  padding: 16px 20px;
+  background: white;
+}
+
+.toolbar-left {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.toolbar-right {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.view-toggle {
+  display: flex;
+  gap: 0;
+}
+
+.toolbar-left h2 {
+  margin: 0;
+  color: #303133;
+  font-size: 18px;
+  font-weight: 600;
+}
+
+.toolbar-right {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.main-content {
+  display: flex;
+  gap: 20px;
+  height: calc(100vh - 280px);
+}
+
+.sidebar {
+  width: 280px;
+  background: white;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.sidebar-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px 20px;
+  border-bottom: 1px solid #ebeef5;
+  background: #fafafa;
+}
+
+.sidebar-header h3 {
+  margin: 0;
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.sidebar-header h3 {
+  margin: 0;
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.category-list {
+  padding: 10px;
+  overflow-y: auto;
+  flex: 1;
+  min-height: 0;
+}
+
+.category-item {
+  display: flex;
+  align-items: center;
+  padding: 10px 15px;
+  border-radius: 6px;
+  cursor: pointer;
+  margin-bottom: 8px;
+  background: #f5f7fa;
+  transition: background-color 0.3s;
+}
+
+.category-item:hover {
+  background: #ebeef5;
+}
+
+.category-item.active {
+  background: #409eff;
+  color: white;
+}
+
+.category-item.active:hover {
+  background: #66b1ff;
+}
+
+.category-icon {
+  margin-right: 10px;
+  font-size: 18px;
+}
+
+.category-actions {
+  margin-left: auto;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.expand-icon {
+  font-size: 16px;
+  color: #909399;
+  cursor: pointer;
+  transition: transform 0.3s;
+  margin-right: 5px;
+}
+
+.expand-icon:hover {
+  color: #409eff;
+}
+
+.expand-icon.expanded {
+  transform: rotate(90deg);
+}
+
+.indent {
+  width: 20px;
+  flex-shrink: 0;
+}
+
+.child-category {
+  margin-left: 0;
+}
+
+.grandchild-category {
+  margin-left: 0;
+}
+
+.more-icon {
+  font-size: 16px;
+  color: #909399;
+  cursor: pointer;
+  transition: color 0.3s;
+}
+
+.more-icon:hover {
+  color: #409eff;
+}
+
+/* 树形控件样式 */
+.category-tree {
+  margin-top: 10px;
+  background: transparent;
+}
+
+.category-tree :deep(.el-tree-node__content) {
+  height: 40px;
+  border-radius: 6px;
+  margin-bottom: 4px;
+  background: #f5f7fa;
+  transition: background-color 0.3s;
+}
+
+.category-tree :deep(.el-tree-node__content:hover) {
+  background: #ebeef5;
+}
+
+.category-tree :deep(.el-tree-node.is-current > .el-tree-node__content) {
+  background: #409eff;
+  color: white;
+}
+
+.category-tree :deep(.el-tree-node.is-current > .el-tree-node__content:hover) {
+  background: #66b1ff;
+}
+
+.tree-node-content {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  padding: 0 5px;
+}
+
+.tree-node-content .category-icon {
+  margin-right: 8px;
+  font-size: 16px;
+}
+
+.tree-node-content .node-label {
+  flex: 1;
+  font-size: 14px;
+}
+
+.tree-node-content .node-actions {
+  margin-left: auto;
+}
+
+.tree-node-content .more-icon {
+  font-size: 16px;
+  color: #909399;
+  cursor: pointer;
+  transition: color 0.3s;
+  padding: 2px 5px;
+  border-radius: 2px;
+}
+
+.tree-node-content .more-icon:hover {
+  color: #409eff;
+  background: rgba(64, 158, 255, 0.1);
+}
+
+.content-area {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  overflow: hidden;
+}
+
+.content-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px 20px;
+  background: white;
+  border-radius: 8px 8px 0 0;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  margin-bottom: 0;
+}
+
+.breadcrumb .clickable {
+  cursor: pointer;
+  color: #409eff;
+}
+
+.breadcrumb .clickable:hover {
+  text-decoration: underline;
+}
+
+.header-actions {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.file-count {
+  color: #909399;
+  font-size: 14px;
+}
+
+.content {
+  flex: 1;
+  background: white;
+  border-radius: 0 0 8px 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+}
+
+.pagination {
+  display: flex;
+  justify-content: center;
+  padding: 20px;
+  background: white;
+  flex-shrink: 0;
+}
+
+.form-tip {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 4px;
+}
+
+.el-table {
+  border: none;
+}
+
+.el-table th {
+  background: #fafafa;
+  color: #606266;
+  font-weight: 500;
+}
+
+.el-table td {
+  border-bottom: 1px solid #ebeef5;
+}
+
+.el-table tr:hover td {
+  background: #f5f7fa;
+}
+
+.el-upload-dragger {
+  width: 100%;
+  height: 180px;
+  background: #fafafa;
+  border: 2px dashed #d9d9d9;
+  border-radius: 6px;
+  box-sizing: border-box;
+  text-align: center;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+}
+
+.el-upload-dragger:hover {
+  border-color: #409eff;
+}
+
+.el-icon--upload {
+  font-size: 67px;
+  color: #c0c4cc;
+  margin: 40px 0 16px;
+  line-height: 50px;
+}
+
+.el-upload__text {
+  color: #606266;
+  font-size: 14px;
+  text-align: center;
+}
+
+.el-upload__text em {
+  color: #409eff;
+  font-style: normal;
+}
+
+.el-upload__tip {
+  font-size: 12px;
+  color: #606266;
+  margin-top: 7px;
+}
+
+.file-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+  gap: 15px;
+  padding: 15px;
+  flex: 1;
+  overflow-y: auto;
+  min-height: 0;
+}
+
+.file-item {
+  position: relative;
+  cursor: pointer;
+  border: 1px solid #ebeef5;
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  transition:
+    transform 0.3s ease,
+    box-shadow 0.3s ease;
+}
+
+.file-item:hover {
+  transform: translateY(-5px);
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
+}
+
+.file-item.selected {
+  border: 2px solid #409eff;
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
+}
+
+.file-wrapper {
+  position: relative;
+  width: 100%;
+  height: 150px; /* Fixed height for grid view */
+  overflow: hidden;
+}
+
+.file-thumbnail {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.file-icon-wrapper {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background: #f5f7fa;
+}
+
+.file-icon {
+  font-size: 48px;
+}
+
+.file-checkbox {
+  position: absolute;
+  top: 8px;
+  right: 8px;
+  z-index: 10;
+}
+
+.file-checkbox :deep(.el-checkbox__inner) {
+  border: 2px solid #fff;
+  background: transparent;
+  border-radius: 0;
+}
+
+.file-checkbox :deep(.el-checkbox.is-checked .el-checkbox__inner) {
+  background: #409eff;
+  border-color: #409eff;
+}
+
+.file-info {
+  padding: 10px;
+  background: #f5f7fa;
+  border-top: 1px solid #ebeef5;
+  border-radius: 0 0 8px 8px;
+}
+
+.file-name {
+  font-size: 14px;
+  color: #303133;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  margin-bottom: 5px;
+}
+
+.file-actions {
+  display: flex;
+  justify-content: space-around;
+  gap: 8px;
+}
+
+.file-error {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 100%;
+  height: 100%;
+  background: #f5f7fa;
+  color: #c0c4cc;
+}
+
+.file-loading {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 100%;
+  height: 100%;
+  background: #f0f9ff;
+  color: #409eff;
+}
+
+/* 列表视图中的图片预览样式 */
+.list-image-error {
+  width: 50px;
+  height: 50px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background: #f5f7fa;
+  border-radius: 4px;
+  border: 1px solid #ebeef5;
+}
+
+.list-image-loading {
+  width: 50px;
+  height: 50px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background: #f0f9ff;
+  border-radius: 4px;
+  border: 1px solid #b3d8ff;
+}
+
+/* 视频缩略图样式 */
+.video-thumbnail-wrapper {
+  position: relative;
+  display: inline-block;
+  overflow: hidden;
+  border-radius: 4px;
+}
+
+.video-thumbnail-wrapper.full {
+  width: 100%;
+  height: 100%;
+}
+
+.video-thumbnail {
+  display: block;
+  object-fit: cover;
+  background: #000;
+}
+
+.video-play-overlay {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  background: rgba(0, 0, 0, 0.6);
+  border-radius: 50%;
+  width: 40px;
+  height: 40px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  pointer-events: none;
+  transition: all 0.3s ease;
+}
+
+.video-thumbnail-wrapper:hover .video-play-overlay {
+  background: rgba(0, 0, 0, 0.8);
+  transform: translate(-50%, -50%) scale(1.1);
+}
+
+/* 列表视图中的视频缩略图样式 */
+.video-thumbnail-wrapper:not(.full) {
+  width: 50px;
+  height: 50px;
+}
+
+.video-thumbnail-wrapper:not(.full) .video-play-overlay {
+  width: 30px;
+  height: 30px;
+}
+
+.file-list {
+  padding: 15px;
+  flex: 1;
+  overflow-y: auto;
+  min-height: 0;
+}
+
+/* 删除按钮样式 - 浅灰色 */
+.delete-btn {
+  color: #909399 !important;
+}
+
+.delete-btn:hover {
+  color: #606266 !important;
+}
+
+/* 图片预览对话框样式 */
+.image-preview-dialog {
+  max-width: 80vw;
+}
+
+.image-preview-dialog .el-message-box__content {
+  padding: 20px !important;
+}
+
+.image-preview-dialog .el-message-box__message {
+  margin: 0 !important;
+}
+
+/* 音频播放器对话框样式 */
+.audio-player-dialog {
+  max-width: 600px;
+  min-width: 400px;
+}
+
+.audio-player-dialog .el-message-box__content {
+  padding: 30px 20px !important;
+}
+
+.audio-player-dialog .el-message-box__message {
+  margin: 0 !important;
+}
+
+.audio-player-dialog .audio-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 15px;
+  background: #f8f9fa;
+  border-radius: 10px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+}
+
+.audio-player-dialog audio {
+  width: 100%;
+  border-radius: 8px;
+  background: #f0f9ff;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+  padding: 10px;
+  margin-bottom: 10px;
+}
+
+.audio-player-dialog .audio-info {
+  width: 100%;
+  text-align: left;
+  margin-top: 10px;
+  padding: 10px;
+  background: white;
+  border-radius: 8px;
+  border: 1px solid #ebeef5;
+}
+
+.audio-player-dialog .el-message-box__btns {
+  padding: 15px 20px !important;
+}
+
+/* 选中指示器 */
+.select-indicator {
+  position: absolute;
+  top: 8px;
+  right: 8px;
+  z-index: 10;
+  background: rgba(255, 255, 255, 0.9);
+  border-radius: 50%;
+  padding: 4px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.dialog-footer {
+  text-align: right;
+}
+
+/* 防止对话框影响主页面元素样式 */
+.file-selector :deep(.el-input.is-error),
+.file-selector :deep(.el-input.is-invalid) {
+  border-color: #dcdfe6 !important;
+}
+
+.file-selector :deep(.el-input.is-error:hover),
+.file-selector :deep(.el-input.is-invalid:hover) {
+  border-color: #c0c4cc !important;
+}
+
+.file-selector :deep(.el-input.is-error:focus),
+.file-selector :deep(.el-input.is-invalid:focus) {
+  border-color: #409eff !important;
+}
+
+/* 确保分页组件样式正常 */
+.file-selector :deep(.el-pagination) {
+  border: none !important;
+}
+
+.file-selector :deep(.el-pagination .el-input) {
+  border-color: #dcdfe6 !important;
+}
+
+.file-selector :deep(.el-pagination .el-input:hover) {
+  border-color: #c0c4cc !important;
+}
+
+.file-selector :deep(.el-pagination .el-input:focus) {
+  border-color: #409eff !important;
+}
+</style>