Răsfoiți Sursa

refactor(FileList): 修改列表形式为文件夹形式

fxs 1 lună în urmă
părinte
comite
a4ad10ab5c

+ 2 - 0
.env

@@ -1,8 +1,10 @@
 # 开发
 VITE_API_URL_DEV="http://192.168.1.139:8000"
 #VITE_API_URL_DEV='http://server.ichunhao.cn'
+#VITE_API_URL_DEV="http://service.ichunhao.cn"
 # 测试服和本地开发
 #VITE_API_URL_TEST='http://server.ichunhao.cn'
 VITE_API_URL_TEST="http://192.168.1.139:8000"
+#VITE_API_URL_TEST="http://service.ichunhao.cn"
 # 线上
 VITE_API_URL_PRODUCT='http://service.ichunhao.cn'

+ 5 - 0
components.d.ts

@@ -8,11 +8,15 @@ export {}
 declare module 'vue' {
   export interface GlobalComponents {
     CommonFileUpload: typeof import('./src/components/form/CommonFileUpload.vue')['default']
+    CustomCheckBox: typeof import('./src/components/common/CustomCheckBox.vue')['default']
     CustomDialog: typeof import('./src/components/common/CustomDialog.vue')['default']
     CustomFilter: typeof import('./src/components/form/CustomFilter.vue')['default']
     CustomForm: typeof import('./src/components/form/CustomForm.vue')['default']
     CustomTable: typeof import('./src/components/table/CustomTable.vue')['default']
+    CustormCheckBox: typeof import('./src/components/common/CustormCheckBox.vue')['default']
     DropDownSelection: typeof import('./src/components/dataAnalysis/DropDownSelection.vue')['default']
+    ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
+    ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
@@ -48,6 +52,7 @@ declare module 'vue' {
     ElText: typeof import('element-plus/es')['ElText']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
     ElUpload: typeof import('element-plus/es')['ElUpload']
+    FileItem: typeof import('./src/components/file/FileItem.vue')['default']
     FileUpload: typeof import('./src/components/form/FileUpload.vue')['default']
     FilterInput: typeof import('./src/components/table/TableFilterForm/FilterInput.vue')['default']
     FilterPopover: typeof import('./src/components/toolsBtn/FilterPopover.vue')['default']

+ 10 - 2
src/assets/base.css

@@ -31,8 +31,8 @@ img {
 }
 
 body {
-  font-family: 'Helvetica Neue', 'Hiragino Sans GB', 'Segoe UI', 'Microsoft Yahei', '微软雅黑',
-    Tahoma, Arial, STHeiti, sans-serif;
+  font-family: PingFang SC, Microsoft YaHei UI, Microsoft YaHei, Helvetica Neue, Helvetica, Hiragino Sans GB, Arial, sans-serif;
+
 }
 
 
@@ -40,3 +40,11 @@ body {
 .noneExpandIcon .el-table__expand-column .cell {
   display: none !important;
 }
+
+
+.disableHandle {
+  pointer-events: none;     /* 禁用鼠标交互 */
+  opacity: 0.5;             /* 降低不透明度显示为禁用 */
+  cursor: not-allowed;      /* 鼠标悬停变成禁止图标 */
+  user-select: none;        /* 禁止选中文本 */
+}

BIN
src/assets/fileIcon/checkbox-active.png


BIN
src/assets/fileIcon/checkbox-default.png


BIN
src/assets/fileIcon/folder.png


BIN
src/assets/fileIcon/img-file.png


BIN
src/assets/fileIcon/json-file.png


BIN
src/assets/fileIcon/mp3-file.png


BIN
src/assets/fileIcon/txt-file.png


BIN
src/assets/fileIcon/unknow-file.png


+ 46 - 0
src/components/common/CustomCheckBox.vue

@@ -0,0 +1,46 @@
+<template>
+  <i class="custom-checkbox" :class="{ checked: modelValue }" @click.stop="toggle" />
+</template>
+
+<script setup lang="ts">
+const modelValue = defineModel<boolean>({ default: false })
+
+const emit = defineEmits<{
+  (e: 'click', value: boolean): void
+}>()
+
+const toggle = () => {
+  modelValue.value = !modelValue.value
+  // 这里已经被改了,所以在抛出去的时候要改回去
+  emit('click', !modelValue.value)
+}
+</script>
+
+<style scoped>
+.custom-checkbox {
+  cursor: pointer;
+  position: relative;
+  display: inline-block;
+  width: 16px;
+  height: 16px;
+  margin-right: 5px;
+  vertical-align: middle;
+}
+
+/* 点击区域填满 */
+.custom-checkbox::before {
+  content: '';
+  position: absolute;
+  inset: 0; /* 等同于 top: 0; right: 0; bottom: 0; left: 0 */
+  background-image: url('@/assets/fileIcon/checkbox-default.png');
+  background-size: 100% 100%;
+  background-repeat: no-repeat;
+  background-position: center;
+  transition: all 0.2s ease-in-out;
+}
+
+/* 选中态,切换另一张图 */
+.custom-checkbox.checked::before {
+  background-image: url('@/assets/fileIcon/checkbox-active.png'); /* 自己替换成选中的图 */
+}
+</style>

+ 122 - 0
src/components/file/FileItem.vue

@@ -0,0 +1,122 @@
+<template>
+  <div
+    class="file-item"
+    :class="{ selected: modelValue, 'is-folder': type === 'folder' }"
+    @click="handleClick"
+    @dblclick="$emit('dblclick')"
+  >
+    <!-- 自定义勾选框 -->
+    <div v-if="selectedAble" class="checkbox" @click.stop="toggleSelect">
+      <CustomCheckBox v-model="modelValue" />
+    </div>
+
+    <img :src="icon" alt="icon" class="item-icon" />
+    <span class="item-name">{{ name }}</span>
+  </div>
+</template>
+
+<script setup lang="ts">
+import CustomCheckBox from '@/components/common/CustomCheckBox.vue'
+import { getFileIconBySuffix } from '@/utils/common'
+import { loadResource } from '@/utils/resource'
+import { computed, onMounted, ref } from 'vue'
+
+interface FileItemProps {
+  /** 项目类型 - 文件夹或文件 */
+  type: 'folder' | 'file'
+  /** 显示名称 */
+  name: string
+  /** 是否可选中 */
+  selectedAble?: boolean
+}
+
+const props = withDefaults(defineProps<FileItemProps>(), {
+  selectedAble: false
+})
+
+// 是否被选中(使用 v-model 双向绑定)
+const modelValue = defineModel<boolean>({ default: false })
+
+// 图标地址
+const iconUrl = ref('')
+
+const emit = defineEmits<{
+  (e: 'dblclick'): void
+  (e: 'click', event: MouseEvent): void
+}>()
+
+// 返回图标路径
+const icon = computed(() => {
+  return iconUrl.value
+})
+
+// 切换选中状态
+const toggleSelect = () => {
+  modelValue.value = !modelValue.value
+}
+
+// 点击事件处理函数
+const handleClick = (event: MouseEvent) => {
+  emit('click', event)
+  if (props.selectedAble) {
+    modelValue.value = !modelValue.value
+  }
+}
+
+// 根据文件类型加载图标
+onMounted(async () => {
+  console.log(props.type)
+  const path = getFileIconBySuffix(props.name, props.type)
+  console.log(path)
+  iconUrl.value = await loadResource(path)
+})
+</script>
+
+<style scoped>
+.file-item {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 100px;
+  padding: 8px;
+  margin: 4px;
+  border-radius: 4px;
+  cursor: pointer;
+  user-select: none;
+  transition: background-color 0.2s ease-in-out;
+}
+
+.file-item:hover,
+.file-item.selected {
+  background-color: #e6f3ff;
+}
+
+.checkbox {
+  position: absolute;
+  top: 4px;
+  left: 4px;
+  z-index: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.item-icon {
+  width: 64px;
+  height: 64px;
+  margin-bottom: 4px;
+  object-fit: contain;
+}
+
+.item-name {
+  text-align: center;
+  font-size: 12px;
+  word-break: break-word;
+  max-width: 100%;
+}
+
+.file-item.is-folder .item-icon {
+  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
+}
+</style>

+ 19 - 2
src/components/form/CommonFileUpload.vue

@@ -1,5 +1,11 @@
 <template>
-  <el-dialog v-model="dialogVisible" title="文件上传" width="500px" :close-on-click-modal="false" :before-close="handleClose">
+  <el-dialog
+    v-model="dialogVisible"
+    title="文件上传"
+    width="500px"
+    :close-on-click-modal="false"
+    :before-close="handleClose"
+  >
     <!-- 文件上传组件 -->
     <el-upload
       class="upload-demo"
@@ -25,7 +31,12 @@
     <template #footer>
       <span class="dialog-footer">
         <el-button @click="dialogVisible = false" class="cancelUpload">取消</el-button>
-        <el-button type="primary" @click="handleSubmit" :disabled="!fileList.length">
+        <el-button
+          :loading="uploadLoading"
+          type="primary"
+          @click="handleSubmit"
+          :disabled="!fileList.length"
+        >
           上传文件
         </el-button>
       </span>
@@ -39,6 +50,12 @@ import { UploadFilled } from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
 import type { UploadFile } from 'element-plus'
 
+defineProps<{
+  // 父组件传入的配置参数
+  // ...
+  uploadLoading: boolean
+}>()
+
 // 组件状态
 const dialogVisible = ref(false) // 控制对话框显示
 const fileList = ref<UploadFile[]>([]) // 文件列表

+ 3 - 3
src/components/form/FileUpload.vue

@@ -224,8 +224,8 @@ defineExpose({
             {
                 "id": 1,  // 更新需要传入id
                 "actionId": 123,
-                "optionId": "test",
-                "optionName": "test",
+                "optionId": "openUploadFile",
+                "optionName": "openUploadFile",
                 "optionType": "string",
                 "status": 0,
             }
@@ -236,7 +236,7 @@ defineExpose({
             {
               "actionId": 456,
                 "optionId": "ba",
-                "optionName": "test",
+                "optionName": "openUploadFile",
                 "optionType": "int",
                 "status": 0
             }

+ 2 - 1
src/hooks/useRequest.ts

@@ -78,7 +78,8 @@ export function useRequest() {
     fileList: `/file/fileList`, // 文件列表
     fileUploadToServer: `/upload`, // 上传到服务器
     fileUploadToTencent: `/file/localFileToService`, // 上传到腾讯云
-    fileManageDeleteFile: `/file/fileDelete` // 删除文件
+    fileManageDeleteFile: `/file/fileDelete`, // 删除文件
+    makeDir: `/file/createDir` // 新建文件夹
   }
 
   /**

+ 22 - 0
src/utils/common/index.ts

@@ -269,3 +269,25 @@ export function generateUniqueColors(count: number) {
   }
   return colors
 }
+
+export const getFileIconBySuffix = (filename: string, fileType: 'folder' | 'file'): string => {
+  const iconBasePath = '/src/assets/fileIcon/'
+  if (fileType === 'folder') return iconBasePath + 'folder.png'
+  const suffix = filename.split('.').pop()?.toLowerCase() || ''
+  switch (suffix) {
+    case 'json':
+      return iconBasePath + 'json-file.png'
+    case 'png':
+    case 'jpg':
+    case 'jpeg':
+    case 'gif':
+      return iconBasePath + 'img-file.png'
+    case 'txt':
+      return iconBasePath + 'txt-file.png'
+    case 'mp3':
+    case 'wav':
+      return iconBasePath + 'mp3-file.png'
+    default:
+      return iconBasePath + 'unknow-file.png'
+  }
+}

+ 5 - 4
src/views/AppManage/EventDetailsView.vue

@@ -260,15 +260,16 @@ const dialogRules = reactive<FormRules<typeof dialogRuleForm>>({
 // 表单字段信息
 const dialogFormFields: Array<FormField> = [
   {
-    name: 'optionName',
-    cnName: '选项名称',
+    name: 'optionId',
+    cnName: '选项ID',
     type: FormFieldType.INPUT
   },
   {
-    name: 'optionId',
-    cnName: '选项ID',
+    name: 'optionName',
+    cnName: '选项名称',
     type: FormFieldType.INPUT
   },
+
   {
     name: 'status',
     cnName: '选项状态',

+ 506 - 195
src/views/FileManage/FileList.vue

@@ -1,121 +1,68 @@
 <script setup lang="ts">
 import CommonFileUpload from '@/components/form/CommonFileUpload.vue'
-import type CustomTable from '@/components/table/CustomTable.vue'
+
 import { useRequest } from '@/hooks/useRequest.ts'
 
-import {
-  FilterType,
-  type QueryInfo,
-  type TableFieldInfo,
-  type TablePaginationSetting,
-  type TableToolsConfig
-} from '@/types/table.ts'
+import { FilterType, type QueryInfo } from '@/types/table.ts'
 import axiosInstance from '@/utils/axios/axiosInstance.ts'
-import { ElMessageBox } from 'element-plus'
-import { ref, reactive } from 'vue'
-import HeaderCard from '@/components/dataAnalysis/HeaderCard.vue'
-import type { ReqConfig } from '@/types/dataAnalysis'
+import { ElMessageBox, type FormInstance } from 'element-plus'
+import { ref, onMounted } from 'vue'
 
-const { AllApi } = useRequest()
+interface PaginationConfig {
+  curPage: number
+  curSize: number
+  sizeList: Array<number>
+  total: number
+}
 
-// 表格请求配置
-const requestConfig = reactive<ReqConfig>({
-  url: AllApi.fileList,
-  otherOptions: {}
-})
+interface FileInfo {
+  id: number
+  file_name: string
+  type: number
+  dir: string
+  local_path: string
+  cos_path: string
+  createdAt: string // 或者使用 Date 类型
+  updatedAt: string // 或者使用 Date 类型
+}
 
-// --- Refs ---
-const headerCard = ref<InstanceType<typeof HeaderCard> | null>(null)
+interface FileQueryField {
+  search: string
+}
+
+const { AllApi } = useRequest()
+
+// 上传组件引用
 const uploadRef = ref<InstanceType<typeof CommonFileUpload> | null>(null)
-const fileListTable = ref<InstanceType<typeof CustomTable> | null>(null)
-
-// 表格分页设置
-const pagingConfig = reactive<TablePaginationSetting>({
-  limit: 20,
-  currentPage: 1,
-  total: 0,
-  pageSizeList: [20, 30]
-})
 
-// 表格字段信息
-const tableFieldsInfo = reactive<Array<TableFieldInfo>>([
-  {
-    name: 'id',
-    cnName: 'ID',
-    isShow: false,
-    needSort: false
-  },
-  {
-    name: 'file_name',
-    cnName: '文件名',
-    isShow: true,
-    needSort: false
-  },
-  {
-    name: 'local_path',
-    cnName: '本地路径',
-    isShow: true,
-    needSort: false
-  },
-  {
-    name: 'cos_path',
-    cnName: 'COS路径',
-    isShow: true,
-    needSort: false
-  },
-  {
-    name: 'createdAt',
-    cnName: '创建时间',
-    isShow: true,
-    needSort: false
-  },
-  {
-    name: 'updatedAt',
-    cnName: '更新时间',
-    isShow: true,
-    needSort: false
-  }
-])
+// 当前路径面包屑列表
+const breadcrumbList = ref<string[]>([])
 
-/**
- * 触发新增
- */
-const addNewEvent = () => {
-  // eventDialog.value.addForm()
-  uploadRef.value?.openDialog()
-}
+// 当前目录下的文件/文件夹列表
+const fileListData = ref<FileInfo[]>([])
 
-// 工具栏配置
-const tableToolsConfig: TableToolsConfig = {
-  add: true,
-  filterFields: true,
-  refresh: true,
-  download: false
-}
+// 选中的文件记录,key 为文件 id
+const selectedRecord = ref<Record<number, boolean>>({})
 
-const deleteFile = async (row: any) => {
-  try {
-    const confirmRes = await ElMessageBox.confirm('确认删除该配置吗', '警告', {
-      confirmButtonText: '确定',
-      cancelButtonText: '取消',
-      type: 'warning',
-      closeOnClickModal: false
-    })
-    if (!confirmRes) return
-    const delRes = (await axiosInstance.post(AllApi.fileManageDeleteFile, {
-      fileId: row.id
-    })) as { code: number }
-    if (delRes.code !== 0) {
-      ElMessage.error('删除失败')
-      return
-    }
-    ElMessage.success('删除成功')
-  } catch (err) {
-    console.error(err)
-  } finally {
-    fileListTable.value?.updateTableData()
-  }
-}
+// 是否处于文件管理(多选)模式
+const selectedAble = ref<boolean>(false)
+
+// 是否全选所有文件
+const selectedAll = ref<boolean>(false)
+
+// 是否正在加载文件列表
+const isLoadData = ref<boolean>(false)
+
+// 创建文件夹表单引用
+const makeDirForm = ref<FormInstance | null>(null)
+
+// 分页配置
+const paginationConfig = ref<PaginationConfig>({
+  curPage: 1,
+  sizeList: [10, 20, 30],
+  curSize: 10,
+  total: 0
+})
 
 // 查询字段设置
 const queryInfo: Array<QueryInfo> = [
@@ -128,16 +75,53 @@ const queryInfo: Array<QueryInfo> = [
   }
 ]
 
+// 查询表单数据
+const queryFormData = ref<FileQueryField>({
+  search: ''
+})
+
+// 文件是否正在上传
+const fileUploading = ref<boolean>(false)
+
+// 创建文件夹弹窗配置
+const makeDirDialogConfig = ref({
+  visible: false,
+  formData: {
+    name: ''
+  },
+  rules: {
+    name: [
+      { required: true, message: '请输入文件夹名称', trigger: 'blur' },
+      { min: 1, max: 50, message: '长度应在 1 到 50 个字符之间', trigger: 'blur' }
+    ]
+  }
+})
+
+/**
+ * 获取当前文件路径的完整路径
+ * @returns 返回完整路径字符串,如 /a/b/
+ */
+const getFullPath = () => {
+  let fullPath = '/' + breadcrumbList.value.join('/')
+  if (fullPath !== '/') fullPath += '/'
+  return fullPath
+}
+
+/**
+ * 提交上传的文件
+ * @param file 需要上传的文件对象
+ */
 const handleSubmit = async (file: File) => {
-  const tableData = fileListTable.value?.outGetTableData()
-  if (tableData) {
-    const hasSameFileName = tableData.some((item) => item.file_name === file.name)
-    if (hasSameFileName) {
-      ElMessage.error('文件名重复')
-      return
-    }
+  const fileList = fileListData.value
+
+  const hasSameFileName = fileList.some((item) => item.file_name === file.name)
+  if (hasSameFileName) {
+    ElMessage.error('文件名重复')
+    return
   }
+
   try {
+    fileUploading.value = true
     const uploadUrl = AllApi.fileUploadToServer
     const tencentUrl = AllApi.fileUploadToTencent
     const formData = new FormData()
@@ -150,8 +134,11 @@ const handleSubmit = async (file: File) => {
       ElMessage.error('上传失败')
       return
     }
+
+    const fullPath = getFullPath()
     const uploadToTencent = (await axiosInstance.post(tencentUrl, {
-      filePath: uploadRes.path
+      filePath: uploadRes.path,
+      dir: fullPath
     })) as {
       code: number
       path: string
@@ -161,68 +148,363 @@ const handleSubmit = async (file: File) => {
       return
     }
     ElMessage.success('上传成功')
-    fileListTable.value?.updateTableData()
+    await getFileList()
   } catch (err) {
     console.error(err)
   } finally {
     uploadRef.value?.closeDialog()
+    fileUploading.value = false
+  }
+}
+
+/**
+ * 对文件列表进行排序:文件夹排在文件前
+ * @param data 原始文件数据列表
+ * @returns 排序后的文件列表
+ */
+const sortTableData = (data: FileInfo[]) => {
+  return data.sort((a, b) => {
+    // 文件夹 (type 2) 放前面,文件 (type 1) 放后面
+    return a.type === b.type ? 0 : a.type === 2 ? -1 : 1
+  })
+}
+
+/**
+ * 获取当前目录下的文件列表
+ */
+const getFileList = async () => {
+  try {
+    isLoadData.value = true
+    const dir = getFullPath()
+    const { curSize, curPage } = paginationConfig.value
+    // http://localhost:8113/fileList
+    const res = (await axiosInstance.post(AllApi.fileList, {
+      dir,
+      limit: curSize,
+      offset: (curPage - 1) * curSize,
+      search: queryFormData.value.search
+    })) as {
+      code: number
+      count: number
+      data: Array<FileInfo>
+    }
+    if (res.code !== 0) {
+      return
+    }
+    let finalData = res.data ?? []
+    sortTableData(finalData)
+    fileListData.value.splice(0, fileListData.value.length, ...finalData)
+    paginationConfig.value.total = res.count
+  } catch (err) {
+    console.error(err)
+    ElMessage.error('获取文件列表失败')
+  } finally {
+    isLoadData.value = false
+  }
+}
+
+/**
+ * 打开上传文件弹窗
+ */
+const openUploadFile = () => {
+  uploadRef.value?.openDialog()
+  // console.log(selectedRecord.value)
+  // console.log(Object.values(selectedRecord).filter((item) => item).length === 1)
+}
+
+/**
+ * 切换管理文件操作状态
+ */
+const manageFile = () => {
+  selectedAble.value = !selectedAble.value
+  if (!selectedAble.value) {
+    selectedRecord.value = {}
+  }
+}
+
+/**
+ * 文件或文件夹双击事件处理
+ * @param id 被双击项的 id
+ */
+const handleDbClick = (id: number) => {
+  // 处于管理模式下,不触发双击事件
+  if (selectedAble.value) return
+  console.log(id)
+  const selectedItem = fileListData.value.find((item) => item.id === id)
+
+  if (!selectedItem) {
+    ElMessage.warning('未选中文件,请重试')
+    return
+  }
+  // 是文件就返回
+  if (selectedItem.type === 1) return
+  breadcrumbList.value.push(selectedItem.file_name)
+
+  getFileList()
+}
+
+/**
+ * 选择或取消选择全部文件
+ * @param isSelected 是否选中
+ */
+const handleSelectAll = (isSelected: boolean) => {
+  fileListData.value.forEach((item) => {
+    selectedRecord.value[item.id] = isSelected
+  })
+}
+
+/**
+ * 鼠标移出取消选中状态
+ */
+const handleOver = () => {
+  selectedAble.value = false
+  selectedRecord.value = {}
+}
+
+/**
+ * 每页显示条数变更时触发
+ * @param newSize 新的每页条数
+ */
+const handleSizeChange = (newSize: number) => {
+  paginationConfig.value.curSize = newSize
+  paginationConfig.value.curPage = 1
+  getFileList()
+}
+
+/**
+ * 当前页码变更时触发
+ * @param newPage 新的页码
+ */
+const handleCurPageChange = (newPage: number) => {
+  paginationConfig.value.curPage = newPage
+  getFileList()
+}
+
+/**
+ * 面包屑导航点击跳转目录
+ * @param index 点击的面包屑索引
+ */
+const handleBreadcrumbNav = (index: number) => {
+  if (index === breadcrumbList.value.length) return
+  breadcrumbList.value.splice(index, breadcrumbList.value.length)
+  getFileList()
+}
+
+/**
+ * 打开新建文件夹弹窗
+ */
+const openCreateFolder = () => {
+  makeDirDialogConfig.value.visible = true
+}
+
+/**
+ * 创建新文件夹
+ */
+const handleCreate = async () => {
+  const isValid = await makeDirForm.value?.validate()
+  if (!isValid) return
+  const res = (await axiosInstance.post(AllApi.makeDir, {
+    dir: getFullPath(),
+    dirName: makeDirDialogConfig.value.formData.name
+  })) as { code: number }
+  if (res.code !== 0) {
+    ElMessage.error('创建文件夹失败')
+    return
+  }
+  ElMessage.success('创建文件夹成功')
+  handleClose()
+  await getFileList()
+}
+
+/**
+ * 关闭弹窗并重置表单
+ */
+const handleClose = () => {
+  makeDirDialogConfig.value.visible = false
+  makeDirForm.value?.resetFields()
+}
+
+/**
+ * 删除选中的文件或文件夹
+ */
+const handleDel = async () => {
+  let fileId = -1
+  for (let [k, v] of Object.entries(selectedRecord.value)) {
+    if (v && fileId === -1) {
+      fileId = parseInt(k) ?? -1
+    }
   }
+
+  if (isNaN(fileId)) {
+    ElMessage.warning('删除错误,请重试')
+    return
+  }
+  const fileItem = fileListData.value.find((item) => item.id === fileId)
+  if (!fileItem) {
+    console.log(fileItem)
+    ElMessage.warning('删除错误,请重试')
+    return
+  }
+  const type = fileItem.type
+  const tipText =
+    type === 1
+      ? '确定删除该文件吗'
+      : '确定删除该文件夹吗<br/><strong>删除文件夹会导致该文件夹下所有文件都被删除</strong>'
+
+  const confirm = await ElMessageBox.confirm(tipText, '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning',
+    dangerouslyUseHTMLString: true
+  })
+  if (!confirm) return
+  const res = (await axiosInstance.post(AllApi.fileManageDeleteFile, {
+    fileId
+  })) as { code: number }
+  if (res.code !== 0) {
+    ElMessage.error('删除失败')
+    return
+  }
+  selectedRecord.value[fileId] = false
+  ElMessage.success('删除成功')
+
+  await getFileList()
 }
+
+/**
+ * 页面加载完成后,获取文件列表
+ */
+onMounted(async () => {
+  await getFileList()
+})
 </script>
 
 <template>
-  <div class="fileList">
-    <div class="breadcrumbBox">
-      <HeaderCard
-        ref="headerCard"
-        :need-breadcrumb="false"
-        :title="'文件列表'"
-        :need-pf-select="false"
-      ></HeaderCard>
-    </div>
-
-    <div class="fileListBox">
-      <CustomTable
-        ref="fileListTable"
-        :request-config="requestConfig"
-        :open-page-query="true"
-        :pagination-config="pagingConfig"
-        :table-fields-info="tableFieldsInfo"
-        :open-filter-query="true"
-        :open-remote-query="true"
-        :tools="tableToolsConfig"
+  <div class="fileListContainer">
+    <div :class="['queryContainer', isLoadData ? 'disableHandle' : '']">
+      <table-filter-form
+        :queryFormData="queryFormData"
         :query-info="queryInfo"
-        @add-new-item="addNewEvent"
+        @query="getFileList"
+      ></table-filter-form>
+    </div>
+    <div :class="['toolContainer', isLoadData ? 'disableHandle' : '']">
+      <el-button class="toolBtn" color="#197afb" @click="openUploadFile">上传文件</el-button>
+      <el-button class="toolBtn" color="#197afb" plain @click="openCreateFolder"
+        >新建文件夹</el-button
       >
-        <template #tableOperation>
-          <el-table-column label="操作" align="center">
-            <template #default="scope">
-              <el-button
-                size="small"
-                class="operationBtn"
-                type="danger"
-                @click="deleteFile(scope.row)"
-                >删除</el-button
-              >
-            </template>
-          </el-table-column>
-        </template>
-      </CustomTable>
+      <el-button class="toolBtn" color="#197afb" plain @click="manageFile">管理</el-button>
+    </div>
+
+    <div class="folderContainer" v-loading="isLoadData">
+      <div class="operationContainer">
+        <!-- 批量操作  -->
+        <div class="batchOperationContainer" v-if="selectedAble">
+          <div class="selectedAll">
+            <CustomCheckBox v-model="selectedAll" @click="handleSelectAll"></CustomCheckBox>
+            <span class="commonTxt">全选</span>
+          </div>
+          <div class="operationGroup">
+            <el-button
+              class="toolBtn delBtn"
+              color="#F56C6C"
+              :disabled="Object.values(selectedRecord).filter((item) => item).length !== 1"
+              @click="handleDel"
+              >删除
+            </el-button>
+            <el-button class="toolBtn" color="#197afb" plain @click="handleOver">完成</el-button>
+          </div>
+        </div>
+        <!--    面包屑导航    -->
+        <div class="breadcrumbContainer" v-else>
+          <el-breadcrumb separator="/">
+            <el-breadcrumb-item @click="handleBreadcrumbNav(0)">
+              <span class="el-breadcrumb__separator">/</span>
+              <span class="breadcrumbSeparatorItem">全部</span>
+            </el-breadcrumb-item>
+            <el-breadcrumb-item
+              v-for="(item, index) in breadcrumbList"
+              :key="item"
+              @click="handleBreadcrumbNav(index + 1)"
+            >
+              <span class="breadcrumbSeparatorItem">{{ item }}</span>
+            </el-breadcrumb-item>
+          </el-breadcrumb>
+        </div>
+      </div>
+      <div class="fileContainer">
+        <FileItem
+          v-for="item in fileListData"
+          :key="item.file_name"
+          :name="item.file_name"
+          :type="item.type === 1 ? 'file' : 'folder'"
+          :selected-able="selectedAble"
+          v-model="selectedRecord[item.id]"
+          @dblclick="handleDbClick(item.id)"
+        ></FileItem>
+      </div>
+      <div class="paginationContainer">
+        <el-pagination
+          v-model:current-page="paginationConfig.curPage"
+          v-model:page-size="paginationConfig.curSize"
+          :page-sizes="paginationConfig.sizeList"
+          :total="paginationConfig.total"
+          layout="total, prev,  pager, next,sizes, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurPageChange"
+        />
+      </div>
     </div>
 
     <div class="uploadFileBox">
-      <CommonFileUpload ref="uploadRef" @submit="handleSubmit">
+      <CommonFileUpload :upload-loading="fileUploading" ref="uploadRef" @submit="handleSubmit">
         <template #tip>
           <div class="el-upload__tip">每次只能上传一个文件,且文件名不能重复</div>
         </template>
       </CommonFileUpload>
+
+      <div class="makeDir">
+        <el-dialog
+          v-model="makeDirDialogConfig.visible"
+          title="新建文件夹"
+          @close="handleClose"
+          class="make-dir-dialog"
+          :close-on-click-modal="false"
+        >
+          <el-form
+            :model="makeDirDialogConfig.formData"
+            :rules="makeDirDialogConfig.rules"
+            label-width="120px"
+            label-position="left"
+            class="make-dir-form-row"
+            ref="makeDirForm"
+          >
+            <el-form-item label="文件夹名称" prop="name" class="make-dir-form-item">
+              <el-input
+                v-model="makeDirDialogConfig.formData.name"
+                placeholder="请输入文件夹名称"
+                class="make-dir-input"
+              />
+            </el-form-item>
+          </el-form>
+
+          <template #footer>
+            <div class="make-dir-footer">
+              <el-button class="make-dir-btn confirm" type="primary" @click="handleCreate"
+                >确定</el-button
+              >
+              <el-button class="make-dir-btn cancel" @click="handleClose">取消</el-button>
+            </div>
+          </template>
+        </el-dialog>
+      </div>
     </div>
   </div>
 </template>
 
 <style scoped>
 /* Styles adapted with renamed classes */
-.fileList {
+.fileListContainer {
   /* Renamed from eventManage */
   width: 98%;
   margin: 1% auto;
@@ -231,69 +513,98 @@ const handleSubmit = async (file: File) => {
   position: relative; /* Keep relative positioning for the absolute positioned handleFileActions */
 }
 
-.breadcrumbBox {
-  background-color: white;
+.folderContainer {
+  /* Renamed from eventTableBox */
   box-sizing: border-box;
-  height: 64px;
-  font-size: 16px;
-  color: #17233d;
-  font-weight: 600;
-  /* padding: 0 24px; */ /* Kept original style */
-  line-height: 64px;
+  /* Add padding-bottom if needed to avoid overlap with potential footers */
+  padding: 0 24px 24px;
 }
 
-.fileListBox {
-  /* Renamed from eventTableBox */
+.queryContainer {
+  width: 100%;
+}
+
+.toolContainer {
+  width: 100%;
   box-sizing: border-box;
-  /* Add padding-bottom if needed to avoid overlap with potential footers */
   padding: 0 24px 24px;
 }
 
-.handleFileActions {
-  /* Renamed from handleEvent */
-  position: absolute;
-  /* width: 12%; */ /* Kept original style */
-  background-color: white;
+.toolBtn {
+  min-width: 80px;
+  height: 28px;
+  margin-right: 16px;
+  /*border: 1px solid #197afb;*/
+  border-radius: 3px;
+  font-size: 12px;
+  font-weight: 500;
+  font-family:
+    PingFang SC,
+    Microsoft YaHei UI,
+    Microsoft YaHei,
+    Helvetica Neue,
+    Helvetica,
+    Hiragino Sans GB,
+    Arial,
+    sans-serif;
+}
+
+.delBtn {
+  color: white;
+}
+
+.operationContainer,
+.fileContainer {
+  width: 100%;
+}
+
+.fileContainer {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+.batchOperationContainer,
+.breadcrumbContainer {
+  width: 100%;
   box-sizing: border-box;
-  /* height: 48px; */ /* Kept original style */
-  font-size: 16px;
-  font-weight: 600;
-  top: 20px; /* Kept original position */
-  right: 24px; /* Kept original position */
-  /* position: relative; */ /* Kept original style (commented) */
-  /* justify-content: flex-end; */ /* Kept original style (commented) */
-  z-index: 10; /* Ensure buttons are above the table box if overlap occurs */
+  padding-bottom: 22px;
 }
 
-.fileGroup {
-  width: 100%; /* Kept original style */
-  display: flex; /* Added to align buttons nicely */
-  justify-content: flex-end; /* Align buttons to the right */
+.batchOperationContainer {
+  display: flex;
+  justify-content: space-between;
 }
 
-.fileBtn {
-  margin-left: 10px; /* Changed from margin-right for flex-end alignment */
+.selectedAll,
+.operationGroup {
+  display: flex;
+  align-items: center;
 }
-.fileBtn:first-child {
-  margin-left: 0; /* Remove margin for the first button */
+
+.commonTxt {
+  display: inline-block;
+  height: 16px;
+  font-size: 12px;
+  line-height: 16px;
+  vertical-align: middle;
 }
 
-/* Basic styling for the new table */
-.file-list-table {
+.paginationContainer {
   width: 100%;
-  border-collapse: collapse;
-  margin-top: 20px; /* Add some space below the header/action buttons area */
+  display: flex;
+  justify-content: center;
+}
+
+.breadcrumbSeparatorItem {
+  cursor: pointer;
 }
 
-.file-list-table th,
-.file-list-table td {
-  border: 1px solid #dfe6ec; /* Example border */
-  padding: 8px 12px;
-  text-align: left;
+.breadcrumbSeparatorItem:hover {
+  color: #409eff;
 }
 
-.file-list-table th {
-  background-color: #f8f8f9; /* Example header background */
-  font-weight: 600;
+/* 创建文件夹表单 */
+.make-dir-btn {
+  margin-right: 10px;
 }
 </style>

+ 1 - 1
vite.config.ts

@@ -85,7 +85,7 @@ const DEFAULT_OPTIONS = {
   cache: false,
   cacheLocation: undefined
 }
-// drop: mode === 'production' || mode === 'test' ? ['console', 'debugger'] : []
+// drop: mode === 'production' || mode === 'openUploadFile' ? ['console', 'debugger'] : []
 export default defineConfig(({ mode }) => {
   return {
     build: {