Переглянути джерело

pms 瑞鹰日报 列表表头分组

(cherry picked from commit f34c15de8c6003976b75b144fa00fbb0104b9c03)
zhangcl 1 тиждень тому
батько
коміт
3d99dc87f8

+ 3 - 0
src/api/pms/iotinfo/index.ts

@@ -22,6 +22,9 @@ export const IotInfoApi = {
   getChildContentFile: async (params: any) => {
     return await request.post({ url: `/rq/iot-info/child/content-file`, params })
   },
+  getAllChildContentFile: async (params: any) => {
+    return await request.post({ url: `/rq/iot-info/all/content-file`, params })
+  },
   getIotInfoFilePage: async (params: any) => {
     return await request.post({ url: `/rq/iot-info/file/page`, params })
   },

+ 3 - 0
src/api/system/tree/index.ts

@@ -15,6 +15,9 @@ export interface IotTreeVO {
 
 // pms树 API
 export const IotTreeApi = {
+  getDeviceTree: async (params): Promise<any> => {
+    return await request.get({ url: '/rq/iot-tree/device/'+ params})
+  },
   getSimpleTreeList: async (): Promise<IotTreeVO[]> => {
     return await request.get({ url: '/rq/iot-tree/simple-list' })
   },

+ 486 - 0
src/components/UploadFile/src/FileUpload.vue

@@ -0,0 +1,486 @@
+<template>
+  <div class="file-upload-container">
+    <!-- 上传按钮区域 - 区分文件和文件夹按钮 -->
+    <div class="upload-btn-area">
+      <el-button
+        type="primary"
+        :loading="uploading"
+        @click="handleFileUploadClick"
+        class="upload-btn"
+        style="margin-right: 10px"
+      >
+        <el-icon><Document /></el-icon>
+        {{ uploading ? '文件上传中...' : '选择文件' }}
+      </el-button>
+      <el-button
+        type="success"
+        :loading="uploading"
+        @click="handleFolderUploadClick"
+        class="upload-btn"
+      >
+        <el-icon><Folder /></el-icon>
+        {{ uploading ? '文件夹上传中...' : '选择文件夹' }}
+      </el-button>
+      <input
+        ref="fileInput"
+        type="file"
+        class="file-input"
+        :multiple="true"
+        accept="*"
+        :webkitdirectory="isFolderMode"
+        :directory="isFolderMode"
+        @change="handleFileChange"
+      />
+      <p class="upload-hint">单个文件夹总大小不能超过300M</p>
+    </div>
+
+    <!-- 上传列表与进度 - 保持不变 -->
+    <el-card v-if="uploadList.length > 0" class="upload-list-card">
+      <el-table
+        :data="uploadList"
+        border
+        style="width: 100%; margin-bottom: 0px;"
+      >
+        <!-- 表格内容保持不变 -->
+        <el-table-column
+          prop="name"
+          label="文件名/文件夹名"
+          width="300"
+        >
+          <template #default="scope">
+            <div class="file-name">
+              <el-icon v-if="scope.row.type === 'folder'"><Folder /></el-icon>
+              <el-icon v-else><Document /></el-icon>
+              <span>{{ scope.row.name }}</span>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="size"
+          label="大小"
+          width="120"
+          :formatter="formatSize"
+        />
+        <el-table-column
+          prop="type"
+          label="类型"
+          width="120"
+        >
+          <template #default="scope">
+            <el-tag size="small" :type="scope.row.type === 'folder' ? 'info' : 'primary'">
+              {{ scope.row.type || '文件夹' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="progress"
+          label="进度"
+          min-width="200"
+        >
+          <template #default="scope">
+            <el-progress
+              :percentage="scope.row.progress"
+              :status="getProgressStatus(scope.row.status)"
+              stroke-width="6"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="操作"
+          width="120"
+        >
+          <template #default="scope">
+            <el-button
+              v-if="scope.row.status === 'uploading'"
+              size="small"
+              type="text"
+              @click.stop="handleCancelUpload(scope.row)"
+              text
+            >
+              取消
+            </el-button>
+            <el-icon v-if="scope.row.status === 'success'" color="#10b981">
+<!--              <CheckCircle />-->
+            </el-icon>
+            <el-icon v-if="scope.row.status === 'error'" color="#ef4444">
+<!--              <CloseCircle />-->
+            </el-icon>
+            <span v-if="scope.row.status === 'cancelled'">已取消</span>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+
+    <!-- 错误提示 - 保持不变 -->
+    <el-dialog
+      v-model="showErrorModal"
+      title="上传错误"
+      @close="showErrorModal = false"
+    >
+      <p>{{ errorMessage }}</p>
+      <template #footer>
+        <el-button @click="showErrorModal = false">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue';
+import {
+  Upload,
+  Folder,
+  Document,
+} from '@element-plus/icons-vue';
+
+// 组件参数 - 保持不变
+const props = defineProps({
+  allowFolderUpload: {
+    type: Boolean,
+    default: true
+  },
+  maxFolderSize: {
+    type: Number,
+    default: 300
+  },
+  uploadUrl: {
+    type: String,
+    default: import.meta.env.VITE_BASE_URL+'/admin-api/rq/file/upload'
+  }
+});
+
+// 组件事件 - 保持不变
+const emit = defineEmits(['uploadSuccess', 'uploadError', 'uploadComplete']);
+
+// 新增:用于区分当前是文件还是文件夹上传模式
+const isFolderMode = ref();
+
+// 状态管理 - 保持不变
+const fileInput = ref(null);
+const uploading = ref(false);
+const uploadList = ref([]);
+const showErrorModal = ref(false);
+const errorMessage = ref('');
+const uploadTasks = ref(new Map());
+
+// 获取进度条状态 - 保持不变
+const getProgressStatus = (status) => {
+  switch(status) {
+    case 'success':
+      return 'success';
+    case 'error':
+      return 'exception';
+    case 'uploading':
+      return 'processing';
+    default:
+      return '';
+  }
+};
+
+// 格式化文件大小 - 保持不变
+const formatSize = (row, column, bytes) => {
+  if (bytes === 0) return '0 Bytes';
+  const k = 1024;
+  const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+  const i = Math.floor(Math.log(bytes) / Math.log(k));
+  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+};
+
+// 计算文件夹总大小 - 保持不变
+const calculateFolderSize = (files) => {
+  let totalSize = 0;
+  files.forEach(file => {
+    totalSize += file.size;
+  });
+  return totalSize;
+};
+
+// 新增:文件上传按钮点击事件
+const handleFileUploadClick = () => {
+  if (uploading.value) return;
+  if (fileInput.value) {
+    fileInput.value.value = '';
+  }
+  isFolderMode.value = false; // 切换到文件模式
+  // 强制DOM更新后再触发点击
+  nextTick(() => {
+    fileInput.value.click();
+  });
+};
+
+// 新增:文件夹上传按钮点击事件
+const handleFolderUploadClick = () => {
+  if (uploading.value) return;
+  if (fileInput.value) {
+    fileInput.value.value = '';
+  }
+  isFolderMode.value = true; // 切换到文件夹模式
+  if (!props.allowFolderUpload) {
+    errorMessage.value = '不允许上传文件夹';
+    showErrorModal.value = true;
+    return;
+  }
+  // 强制DOM更新后再触发点击
+  nextTick(() => {
+    fileInput.value.click();
+  });
+};
+
+// 处理文件选择 - 保持不变
+const handleFileChange = async (e) => {
+  const files = Array.from(e.target.files);
+  if (files.length === 0) return;
+
+  // 清空input值,允许重复选择同一文件
+  e.target.value = '';
+
+  // 检查是否包含文件夹(根据当前模式判断)
+  const hasFolders = isFolderMode.value && files.some(file => file.webkitRelativePath.includes('/'));
+
+  if (hasFolders) {
+    // 按文件夹分组
+    const folderMap = new Map();
+
+    files.forEach(file => {
+      // 提取文件夹路径
+      const relativePath = file.webkitRelativePath;
+      const folderPath = relativePath.substring(0, relativePath.lastIndexOf('/') + 1);
+
+      if (!folderMap.has(folderPath)) {
+        folderMap.set(folderPath, []);
+      }
+      folderMap.get(folderPath).push(file);
+    });
+
+    // 检查每个文件夹大小
+    for (const [folderPath, folderFiles] of folderMap) {
+      const folderSize = calculateFolderSize(folderFiles);
+      const folderSizeMB = folderSize / (1024 * 1024);
+
+      if (folderSizeMB > props.maxFolderSize) {
+        errorMessage.value = `文件夹 "${folderPath}" 大小超过${props.maxFolderSize}M限制,无法上传`;
+        showErrorModal.value = true;
+        return;
+      }
+    }
+
+    // 添加文件夹到上传列表
+    folderMap.forEach((folderFiles, folderPath) => {
+      const folderSize = calculateFolderSize(folderFiles);
+      const uid = Date.now() + Math.random().toString(36).substr(2, 9);
+
+      uploadList.value.push({
+        uid,
+        name: folderPath,
+        size: folderSize,
+        type: 'folder',
+        progress: 0,
+        status: 'wait',
+        files: folderFiles
+      });
+    });
+  } else {
+    // 单个或多个文件
+    files.forEach(file => {
+      uploadList.value.push({
+        uid: Date.now() + Math.random().toString(36).substr(2, 9),
+        name: file.name,
+        size: file.size,
+        type: file.type || 'file',
+        progress: 0,
+        status: 'wait',
+        files: [file]
+      });
+    });
+  }
+
+  // 开始上传
+  await startUpload();
+};
+
+// 开始上传 - 保持不变
+const startUpload = async () => {
+  uploading.value = true;
+
+  // 过滤出等待上传的文件/文件夹
+  const pendingFiles = uploadList.value.filter(item => item.status === 'wait');
+
+  for (const fileItem of pendingFiles) {
+    try {
+      // 创建FormData
+      const formData = new FormData();
+
+      // 添加文件
+      fileItem.files.forEach(file => {
+        formData.append('files', file, file.webkitRelativePath || file.name);
+      });
+
+      // 添加文件夹标识和路径
+      if (fileItem.type === 'folder') {
+        formData.append('isFolder', 'true');
+        formData.append('folderPath', fileItem.name);
+      }
+
+      // 创建上传任务
+      const xhr = new XMLHttpRequest();
+      uploadTasks.value.set(fileItem.uid, xhr);
+
+      // 监听进度
+      xhr.upload.addEventListener('progress', (e) => {
+        if (e.lengthComputable) {
+          const percent = Math.round((e.loaded / e.total) * 100);
+          const index = uploadList.value.findIndex(item => item.uid === fileItem.uid);
+          if (index !== -1) {
+            uploadList.value[index].progress = percent;
+            uploadList.value[index].status = 'uploading';
+          }
+        }
+      });
+
+      // 监听完成
+      xhr.addEventListener('load', () => {
+        const index = uploadList.value.findIndex(item => item.uid === fileItem.uid);
+        if (index !== -1) {
+          if (xhr.status >= 200 && xhr.status < 300) {
+            uploadList.value[index].status = 'success';
+            uploadList.value[index].progress = 100;
+            emit('uploadSuccess', {
+              uid: fileItem.uid,
+              name: fileItem.name,
+              response: JSON.parse(xhr.responseText)
+            });
+          } else {
+            uploadList.value[index].status = 'error';
+            emit('uploadError', {
+              uid: fileItem.uid,
+              name: fileItem.name,
+              error: new Error(`上传失败: ${xhr.statusText}`)
+            });
+          }
+        }
+        uploadTasks.value.delete(fileItem.uid);
+      });
+
+      // 监听错误
+      xhr.addEventListener('error', () => {
+        const index = uploadList.value.findIndex(item => item.uid === fileItem.uid);
+        if (index !== -1) {
+          uploadList.value[index].status = 'error';
+          emit('uploadError', {
+            uid: fileItem.uid,
+            name: fileItem.name,
+            error: new Error('网络错误,上传失败')
+          });
+        }
+        uploadTasks.value.delete(fileItem.uid);
+      });
+
+      // 发送请求
+      xhr.open('POST', props.uploadUrl, true);
+      xhr.setRequestHeader('tenant-id', 1);
+      xhr.send(formData);
+
+    } catch (error) {
+      console.error('上传失败', error);
+      const index = uploadList.value.findIndex(item => item.uid === fileItem.uid);
+      if (index !== -1) {
+        uploadList.value[index].status = 'error';
+      }
+      emit('uploadError', {
+        uid: fileItem.uid,
+        name: fileItem.name,
+        error
+      });
+    }
+  }
+
+  // 检查是否所有上传都已完成
+  const checkUploadComplete = setInterval(() => {
+    const allCompleted = uploadList.value.every(item =>
+      item.status === 'success' || item.status === 'error' || item.status === 'cancelled'
+    );
+
+    if (allCompleted) {
+      clearInterval(checkUploadComplete);
+      uploading.value = false;
+      emit('uploadComplete');
+    }
+  }, 1000);
+};
+
+// 取消上传 - 保持不变
+const handleCancelUpload = (fileItem) => {
+  const xhr = uploadTasks.value.get(fileItem.uid);
+  if (xhr) {
+    xhr.abort();
+    uploadTasks.value.delete(fileItem.uid);
+  }
+
+  const index = uploadList.value.findIndex(item => item.uid === fileItem.uid);
+  if (index !== -1) {
+    uploadList.value[index].status = 'cancelled';
+  }
+
+  // 检查是否还有正在上传的任务
+  const hasUploading = uploadList.value.some(item => item.status === 'uploading');
+  if (!hasUploading) {
+    uploading.value = false;
+  }
+};
+
+// 组件挂载时设置样式 - 保持不变
+onMounted(() => {
+  const container = document.querySelector('.file-upload-container');
+  if (container) {
+    container.style.height = '8vh';
+    container.style.display = 'flex';
+    container.style.flexDirection = 'column';
+    container.style.padding = '20px';
+    container.style.boxSizing = 'border-box';
+  }
+});
+</script>
+
+<style scoped>
+/* 样式保持不变 */
+.file-upload-container {
+  width: 100%;
+  max-width: 1200px;
+  margin: 0 auto;
+}
+
+.upload-btn-area {
+  display: flex;
+  flex-direction: row;
+  margin-bottom: 20px;
+  text-align: left;
+  align-items: center; /* 新增:对齐按钮和提示文字 */
+}
+
+.file-input {
+  display: none;
+}
+
+.upload-btn {
+  margin-bottom: 10px;
+}
+
+.upload-hint {
+  color: #666;
+  font-size: 10px;
+  margin: 0 0 0 10px; /* 调整左边距 */
+  color: orangered;
+  align-self: flex-end; /* 对齐按钮底部 */
+}
+
+.upload-list-card {
+  flex: 1;
+  overflow-y: auto;
+}
+
+.file-name {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+</style>

+ 282 - 0
src/views/pms/device/DeviceFile.vue

@@ -0,0 +1,282 @@
+<template>
+  <ContentWrap>
+    <el-breadcrumb separator=">" class="breadcrumb-container">
+      <el-breadcrumb-item
+        v-for="(item, index) in breadcrumbs"
+        :key="index"
+        @click="handleBreadcrumbClick(index)"
+        :class="{ 'current-crumb': index === breadcrumbs.length - 1 }"
+        class="custom-breadcrumb-item"
+      >
+        {{ item.name }}
+      </el-breadcrumb-item>
+    </el-breadcrumb>
+  </ContentWrap>
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item :label="t('file.name') " prop="filename">
+        <el-input
+          v-model="queryParams.filename"
+          :placeholder="t('file.nameHolder')"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item v-show="false" :label="t('file.fileType') " prop="fileType">
+        <el-select
+          v-model="queryParams.fileType"
+          :placeholder="t('file.fileTypeHolder')"
+          clearable
+          class="!w-200px"
+        >
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.PMS_FILE_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" />
+          {{  t('file.search')}}</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" />{{  t('file.reset')}}</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+  <ContentWrap>
+    <el-table v-loading="formLoading" :data="list" :stripe="true" :show-overflow-tooltip="true" @row-dblclick="inContent" class="custom-table">
+      <el-table-column :label="t('file.name') " align="left" prop="filename" min-width="300">
+        <template #default="scope">
+          <div style="display: flex; align-items: center; gap: 5px;">
+            <Icon v-if="scope.row.fileType==='content'" icon="fa:folder-open"  color="orange"/>
+            <Icon v-else-if="scope.row.fileType==='pic'||scope.row.fileClassify==='jpg'||scope.row.fileClassify==='png'" icon="ep:picture-filled"  color="#2183D1"/>
+            <Icon v-else-if="scope.row.fileType==='file'&&scope.row.fileClassify==='pdf'" icon="fa-solid:file-pdf"  color="#E20012"/>
+            <Icon v-else-if="scope.row.fileType==='file'&&(scope.row.fileClassify==='doc'||scope.row.fileClassify==='docx')" icon="fa:file-word-o"  color="blue"/>
+            <Icon v-else-if="scope.row.fileType==='file'&&(scope.row.fileClassify==='xls'||scope.row.fileClassify==='xlsx')" icon="fa-solid:file-excel"  color="#107C41"/>
+            <Icon v-else-if="scope.row.fileType==='file'&&(scope.row.fileClassify==='txt')" icon="fa:file-text-o" />
+            <Icon v-else-if="scope.row.fileType==='file'&&(scope.row.fileClassify==='ppt'||scope.row.fileClassify==='pptx')" icon="fa-solid:file-powerpoint" color="#C43E1C" />
+            <Icon v-else icon="fa-solid:file-alt" />
+            {{scope.row.filename}}
+          </div>
+        </template>
+
+      </el-table-column>
+      <el-table-column :label="t('file.fileType') " align="center" prop="fileType" >
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PMS_FILE_TYPE" :value="scope.row.fileType" />
+        </template>
+      </el-table-column>
+      <el-table-column :label="t('file.fileSize') " align="center" prop="fileSize" />
+      <!--            <el-table-column :label="t('file.preview') " align="center" prop="filePath" >-->
+      <!--              <template #default="scope">-->
+      <!--                <el-button v-if="scope.row.fileType!=='content'" link type="primary" @click="openWeb(scope.row.filePath)"> <Icon size="19" icon="ep:view"  /> </el-button>-->
+      <!--              </template>-->
+      <!--            </el-table-column>-->
+      <el-table-column :label="t('file.dept') " align="center" prop="deptName" />
+      <el-table-column :label="t('file.device') " align="center" prop="deviceCode" min-width="220"/> />
+      <el-table-column :label="t('file.operation') " align="center" width="160">
+        <template #default="scope">
+          <div class="flex items-center justify-center">
+            <el-button type="primary" v-if="scope.row.fileType!=='content'" link @click="handleDownload( scope.row.filePath)" v-hasPermi="['rq:iot-info:download']">
+              <Icon icon="ep:download" />{{t('file.dow')}}
+            </el-button>
+            <!--                  <el-button type="primary" link @click="handleView( scope.row)">-->
+            <!--                    <Icon icon="ep:view" />{{t('file.preview')}}-->
+            <!--                  </el-button>-->
+          </div>
+        </template>
+      </el-table-column>
+    </el-table>
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import {DICT_TYPE, getStrDictOptions} from "@/utils/dict";
+import {ref} from "vue";
+import {IotDeviceVO} from "@/api/pms/device";
+import {IotInfoApi} from "@/api/pms/iotinfo";
+import {IotTreeApi} from "@/api/system/tree";
+
+const { t } = useI18n() // 国际化
+const queryFormRef = ref()
+const formLoading = ref(false)
+const list = ref<IotDeviceVO[]>([])
+const props = defineProps<{ deviceId?: number,deviceName?:string , deviceCategoryName?:string }>()
+const topNodeId = ref('')
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  filename: null,
+  fileType: null,
+  createTime: [],
+  deviceId: null,
+  classId: null,
+  deptId: undefined,
+  allName: null,
+})
+
+const breadcrumbs = ref([
+  { id: null, name: '',type:'root' } // 根节点
+])
+
+// 面包屑点击事件
+const handleBreadcrumbClick = async (index) => {
+  queryParams.filename = ''
+  formLoading.value = true
+  // 忽略当前节点的点击
+  if (index === breadcrumbs.value.length - 1) return
+
+  // 截断面包屑到点击的节点
+  const targetBreadcrumbs = breadcrumbs.value.slice(0, index + 1)
+  breadcrumbs.value = targetBreadcrumbs
+
+  // 获取对应节点的数据
+  let targetId = targetBreadcrumbs[index].id
+  if (!targetId){
+    targetId = topNodeId.value
+  }
+  queryParams.classId = targetId
+  const data = await IotInfoApi.getChildContentFile(queryParams)
+  list.value = data
+  formLoading.value = false
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+const getList = async () => {
+  formLoading.value = true
+  try {
+    const data = await IotInfoApi.getChildContentFile(queryParams)
+    list.value = data
+  } finally {
+    formLoading.value = false
+  }
+}
+
+const inContent = async (row) => {
+  debugger
+  if (row.fileType!='content') {
+    window.open('http://1.94.244.160:8012/onlinePreview?url='+encodeURIComponent(Base64.encode(row.filePath)));
+    return
+  }
+  queryParams.filename = ''
+  formLoading.value = true
+  // 调用共享方法更新面包屑和表格
+  await updateBreadcrumbs(row)
+  // 可以添加其他表格行点击需要的逻辑
+  queryParams.classId = row.id;
+  const data = await IotInfoApi.getChildContentFile(queryParams)
+  formLoading.value = false
+  list.value = data
+}
+const updateBreadcrumbs = async (node) => {
+  // 查找当前节点是否已在面包屑中
+  const currentIndex = breadcrumbs.value.findIndex(item => item.id === node.id)
+
+  if (currentIndex > -1) {
+    // 如果已存在则截断后面的节点
+    breadcrumbs.value = breadcrumbs.value.slice(0, currentIndex + 1)
+  } else {
+    // 新增节点到面包屑
+    breadcrumbs.value.push({ id: node.id, name: node.filename || node.name })
+  }
+
+  // 更新表格数据
+  queryParams.classId = node.id;
+  const data = await IotInfoApi.getChildContentFile(queryParams)
+  list.value = data
+}
+const handleDownload = async (url) => {
+  try {
+    const response = await fetch(url)
+    const blob = await response.blob()
+    const downloadUrl = window.URL.createObjectURL(blob)
+
+    const link = document.createElement('a')
+    link.href = downloadUrl
+    link.download = url.split('/').pop() // 自动获取文件名‌:ml-citation{ref="3" data="citationList"}
+    link.click()
+
+    URL.revokeObjectURL(downloadUrl)
+  } catch (error) {
+    console.error('下载失败:', error)
+  }
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+onMounted(async ()=>{
+  const deviceid = props.deviceId
+  debugger
+  const data = await IotTreeApi.getDeviceTree(deviceid)
+  queryParams.classId = data
+  const listdata = await IotInfoApi.getChildContentFile(queryParams)
+  list.value = listdata
+  breadcrumbs.value[0].name = props.deviceName
+  breadcrumbs.value[0].id = data;
+})
+</script>
+<style scoped lang="scss">
+.custom-table {
+  cursor: pointer;
+  --el-table-row-hover-bg-color: #f5f7fa; /* 优化悬停背景色 */
+}
+
+::v-deep .breadcrumb-container {
+  padding: 12px 16px;
+  background-color: #f5f7fa;
+  border-radius: 6px;
+  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.06);
+}
+::v-deep .custom-breadcrumb-item {
+  font-size: 14px;
+  font-weight: bold;
+}
+
+/* 面包屑文本样式 */
+::v-deep .el-breadcrumb__item .el-breadcrumb__inner {
+  color: #101010;
+  text-decoration: none;
+  padding: 2px 4px;
+  border-radius: 2px;
+  transition: all 0.2s ease;
+}
+
+/* 可点击项悬停效果 */
+::v-deep .el-breadcrumb__item:not(:last-child) .el-breadcrumb__inner:hover {
+  color: #409eff;
+  background-color: rgba(64, 158, 255, 0.1);
+  cursor: pointer;
+}
+
+/* 当前项样式 */
+::v-deep .el-breadcrumb__item:last-child .el-breadcrumb__inner {
+  color: #1890ff;
+  font-weight: 500;
+  cursor: default;
+}
+
+/* 分隔符样式优化 */
+::v-deep .el-breadcrumb__separator {
+  margin: 0 8px;
+  color: #0954f6;
+  font-size: 12px;
+}
+
+</style>

+ 3 - 1
src/views/pms/device/DeviceInfo.vue

@@ -145,7 +145,8 @@
         </el-form>
       </el-tab-pane>
       <el-tab-pane :label="t('deviceInfo.fileLibrary')" name="file">
-        <DeviceUpload ref="fileRef" v-if="loadedTabs.includes('1')" />
+<!--        <DeviceUpload ref="fileRef" v-if="loadedTabs.includes('1')" />-->
+        <DeviceFile ref="fileRef" :deviceId="id" :deviceName="formData.deviceName"  v-if="loadedTabs.includes('1')" />
       </el-tab-pane>
       <el-tab-pane :label="t('deviceInfo.deviceBOM')" name="bom">
         <BomList ref="bomRef" v-model:activeName="activeName" :deviceId="id" :deviceCategoryName="formData.assetClassName" v-if="loadedTabs.includes('2')" />
@@ -208,6 +209,7 @@ import { DICT_TYPE, getDictLabel } from '@/utils/dict'
 import { formatDate } from '../../../utils/formatTime'
 import DeviceUpload from '@/views/pms/device/DeviceUpload.vue'
 import BomInfo from '@/views/pms/device/bom/BomInfo.vue'
+import DeviceFile from '@/views/pms/device/DeviceFile.vue'
 import BomList from '@/views/pms/device/bom/BomList.vue'
 import FailureList from '@/views/pms/device/FailureList.vue'
 import MaintainList from '@/views/pms/device/MaintainList.vue'

+ 3 - 3
src/views/pms/device/index.vue

@@ -175,9 +175,9 @@
               >
                 {{t('iotDevice.delete')}}
               </el-button>
-              <el-button link type="warning" @click="handleUpload(scope.row.id)">
-                {{t('iotDevice.upload')}}
-              </el-button>
+<!--              <el-button link type="warning" @click="handleUpload(scope.row.id)">-->
+<!--                {{t('iotDevice.upload')}}-->
+<!--              </el-button>-->
             </template>
           </el-table-column>
         </el-table>

+ 42 - 12
src/views/pms/iotinfo/IotInfoFormTree.vue

@@ -1,5 +1,5 @@
 <template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
+  <Dialog :title="dialogTitle" v-model="dialogVisible" style="width: 50vw">
     <el-form
       ref="formRef"
       :model="formData"
@@ -22,12 +22,18 @@
         />
       </el-form-item>
       <el-form-item :label="t('fileInfo.appendix')" prop="filePath" style="">
-        <UploadFileAll style="vertical-align: middle"
-          :is-show-tip="false"
-          :disabled="formType==='detail'"
-          v-model="formData.filePath"
-          :multiple="formType === 'create'"
-          :limit="formType === 'create' ? 5 : 1"
+<!--        <UploadFileAll style="vertical-align: middle"-->
+<!--          :is-show-tip="false"-->
+<!--          :disabled="formType==='detail'"-->
+<!--          v-model="formData.filePath"-->
+<!--          :multiple="formType === 'create'"-->
+<!--          :limit="formType === 'create' ? 5 : 1"-->
+<!--        />-->
+        <file-upload
+          style="height: 300px"
+          @uploadSuccess="handleUploadSuccess"
+          @uploadError="handleUploadError"
+          @uploadComplete="handleUploadComplete"
         />
       </el-form-item>
       <el-form-item :label="t('fileInfo.fileType')" prop="fileType">
@@ -56,6 +62,7 @@ import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import {defaultProps, handleTree} from "@/utils/tree";
 import * as FileClassifyApi from "@/api/pms/info";
 import {IotTreeApi} from "@/api/system/tree";
+import FileUpload from "@/components/UploadFile/src/FileUpload.vue";
 
 /** 资料 表单 */
 defineOptions({ name: 'IotInfoFormTree' })
@@ -77,11 +84,13 @@ const formData = ref({
   fileType: undefined,
   filePath: undefined,
   remark: undefined,
-  fileList: []
+  fileList: [],
+  folderTree: [],
+  folderJson: undefined,
 })
 const formRules = reactive({
   // classId: [{ required: true, message: '文件不能为空', trigger: 'blur' }],
-  filePath: [{ required: true, message: '文件不能为空', trigger: 'blur' }]
+  // filePath: [{ required: true, message: '文件不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
 const props = defineProps<{
@@ -138,12 +147,13 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    formData.value.fileList = formData.value.filePath
-    formData.value.filePath = ''
+    // formData.value.fileList = formData.value.filePath
+    // formData.value.filePath = ''
     const data = formData.value as unknown as IotInfoVO
     if (formType.value === 'create') {
       data.deviceId = props.deviceId
       // data.filePath = data.filePath.join(',')
+      formData.value.folderJson = JSON.stringify(formData.value.folderTree);
       await IotInfoApi.createIotInfo(data)
       message.success(t('common.createSuccess'))
     } else {
@@ -167,8 +177,28 @@ const resetForm = () => {
     filename: undefined,
     fileType: undefined,
     filePath: undefined,
-    remark: undefined
+    remark: undefined,
+    fileList: [],
+    folderTree: []
   }
   formRef.value?.resetFields()
 }
+
+const handleUploadSuccess = (data) => {
+  if (!Array.isArray(formData.value.folderTree)){
+    formData.value.folderTree = []
+  }
+  formData.value.folderTree.push(data.response.data);
+  console.log('777777777777777'+JSON.stringify(formData.value.folderTree))
+  message.success(`"${data.name}" 上传成功`);
+};
+
+const handleUploadError = (error) => {
+  console.error('上传失败', error);
+  message.error(`"${error.name}" 上传失败: ${error.error.message}`);
+};
+
+const handleUploadComplete = () => {
+  message.info('所有上传任务已处理完成');
+};
 </script>

+ 26 - 3
src/views/pms/iotopeationfill/index1.vue

@@ -70,13 +70,30 @@
                 />
               </div>
                 <el-form-item :label='item.name' prop="deviceId"  label-position="top">
+                  <div v-if="fillStatus === '1'">
+
+                    <el-select
+                      disabled
+                      v-model="item.fillContent"
+                      v-if="item.type === 'enum' && item.description !== null"
+                      style="width: 200px">
+                      <el-option
+                        v-for="dict in getIntDictOptions(item.description)"
+                        :key="dict.label"
+                        :label="dict.label"
+                        :value="dict.value"
+                      />
+                    </el-select>
                     <el-input
-                      v-if="fillStatus === '1'"
+                      v-else
                       v-model="item.fillContent"
                       clearable
-                      style="width: 200px"
+                      style="width: 200px; margin-right: 10px"
                       disabled
                     />
+                  </div>
+
+
                     <el-input
                       v-else-if="item.type === 'textarea'"
                       v-model="item.fillContent"
@@ -84,7 +101,11 @@
                       clearable
                       style="width: 200px"
                     />
-                    <el-select  v-model="item.fillContent" clearable v-else-if="item.type === 'enum' && item.description !== null" style="width: 200px" filterable>
+                    <el-select  v-model="item.fillContent"
+                                clearable
+                                v-else-if="item.type === 'enum' && item.description !== null"
+                                style="width: 200px"
+                                filterable>
                       <el-option
                         v-for="dict in getIntDictOptions(item.description)"
                         :key="dict.label"
@@ -346,6 +367,7 @@ const getAttrList = async () => {
       item.deviceId = queryParams.deviceId;
       item.deviceCategoryId = queryParams.deviceCategoryId;
       item.modelId = item.id;
+      console.log(item.fillContent)
     })
     attrList1.value.forEach(function (item,index){
       item.deviceCode = queryParams.deviceCode;
@@ -585,4 +607,5 @@ onMounted(() => {
   padding: 2px 4px;
   border-radius: 4px;
 }
+
 </style>

+ 219 - 108
src/views/system/tree/PmsTree.vue

@@ -13,97 +13,125 @@
       :expand-on-click-node="false"
       :filter-node-method="filterNode"
       :props="defaultProps"
-      :default-expanded-keys="firstLevelKeys"
-      highlight-current
-      node-key="id"
-      @node-click="handleNodeClick"
-      @node-contextmenu="handleRightClick"
-      style="height: 52em"
+      :default-expanded-keys="expandedKeys"
+    highlight-current
+    node-key="id"
+    @node-click="handleNodeClick"
+    @node-contextmenu="handleRightClick"
+    style="height: 52em"
     >
-      <template #default="{ node }">
-        <div
-          style="display: flex; justify-content: space-between; align-items: center; width: 100%"
-        >
-          <div>
-            <Icon
-              style="vertical-align: middle;fill: currentColor;color: orange"
-              v-if="node.data.type === 'dept'"
-              icon="fa:folder-open"
-            />
-            <Icon
-              style="vertical-align: middle;fill: currentColor;color:orange"
-              v-if="node.data.type === 'device'"
-              icon="fa:folder-open"
-            />
-            <Icon icon="fa:folder-open" v-if="node.data.type === 'file'" style="vertical-align: middle;color: orange;fill: currentColor;"/>
-            <!-- 文件夹图标 -->
-            <span style="vertical-align: middle; margin-left: 3px">{{ node.data.name }}</span>
-          </div>
-          <!--          <div>-->
-          <!--            <icon style="vertical-align: middle" @click="handleRightClick" icon="ep:edit" />-->
-          <!--          </div>-->
+    <template #default="{ node }">
+      <div
+        style="display: flex; justify-content: space-between; align-items: center; width: 100%"
+      >
+        <div>
+          <Icon
+            style="vertical-align: middle;fill: currentColor;color: orange"
+            v-if="node.data.type === 'dept'"
+            icon="fa:folder-open"
+          />
+          <Icon
+            style="vertical-align: middle;fill: currentColor;color:orange"
+            v-if="node.data.type === 'device'"
+            icon="fa:folder-open"
+          />
+          <Icon icon="fa:folder-open" v-if="node.data.type === 'file'" style="vertical-align: middle;color: orange;fill: currentColor;"/>
+          <span style="vertical-align: middle; margin-left: 3px">{{ node.data.name }}</span>
         </div>
-      </template>
+      </div>
+    </template>
     </el-tree>
   </div>
   <div v-show="deviceVisible" ref="contextMenuRef" class="custom-menu" :style="{ left: menuX + 'px', top: menuY + 'px' }">
     <ul>
       <li style="border-bottom: 1px solid #ccc;" @click="handleDeviceClick('add')">设备详情</li>
+      <li style="border-bottom: 0px solid #ccc;" @click="handleMenuClick('add')">新建目录</li>
     </ul>
   </div>
-    <div v-show="menuVisible" ref="contextMenuRef" class="custom-menu" :style="{ left: menuX + 'px', top: menuY + 'px' }">
-      <ul>
-        <li style="border-bottom: 1px solid #ccc;" @click="handleMenuClick('add')">新增子节点</li>
-        <li style="border-bottom: 1px solid #ccc;" @click="handleMenuClick('edit')">编辑节点</li>
-        <li @click="handleMenuClick('delete')">删除节点</li>
-      </ul>
-    </div>
-
-    <Dialog v-model="dialogVisible" :title="dialogTitle" style="width: 40em">
-      <el-form
-        ref="formRef"
-        v-loading="formLoading"
-        :model="formData"
-        :rules="formRules"
-        label-width="80px"
-      >
-        <el-form-item label="资料分类名称" prop="name" label-width="110px">
-          <el-input v-model="formData.name" placeholder="请输入资料分类名称" />
-        </el-form-item>
-        <el-form-item label="显示排序" prop="sort" label-width="110px">
-          <el-input-number v-model="formData.sort" :min="0" controls-position="right" />
-        </el-form-item>
-        <el-form-item label="备注" prop="remark" label-width="110px">
-          <el-input
-            v-model="formData.remark"
-            maxlength="11"
-            placeholder="请输入备注"
-            type="textarea"
-          />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="dialogVisible = false">取 消</el-button>
-      </template>
-    </Dialog>
+  <div v-show="menuVisible" ref="contextMenuRef" class="custom-menu" :style="{ left: menuX + 'px', top: menuY + 'px' }">
+    <ul>
+      <li style="border-bottom: 1px solid #ccc;" @click="handleMenuClick('add')">新建目录</li>
+      <li style="border-bottom: 1px solid #ccc;" @click="handleMenuClick('edit')">编辑</li>
+      <li @click="handleMenuClick('delete')">删除目录</li>
+    </ul>
+  </div>
+
+  <Dialog v-model="dialogVisible" :title="dialogTitle" style="width: 40em">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="80px"
+    >
+      <el-form-item label="目录名称" prop="name" label-width="110px">
+        <el-input v-model="formData.name" placeholder="请输入目录名称" />
+      </el-form-item>
+      <el-form-item label="显示排序" prop="sort" label-width="110px">
+        <el-input-number v-model="formData.sort" :min="0" controls-position="right" />
+      </el-form-item>
+<!--      <el-form-item label="备注" prop="remark" label-width="110px">-->
+<!--        <el-input-->
+<!--          v-model="formData.remark"-->
+<!--          maxlength="11"-->
+<!--          placeholder="请输入备注"-->
+<!--          type="textarea"-->
+<!--        />-->
+<!--      </el-form-item>-->
+    </el-form>
+    <template #footer>
+      <el-button type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
 </template>
 
 <script lang="ts" setup>
 import { ElTree } from 'element-plus'
 import { defaultProps, handleTree } from '@/utils/tree'
 import { CommonStatusEnum } from '@/utils/constants'
-// import {IotInfoClassifyApi} from "@/api/pms/info";
 import { IotTreeApi } from '@/api/system/tree'
-import { Folder } from '@element-plus/icons-vue'
-import {flatten} from "min-dash";
+import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
+import { useI18n } from 'vue-i18n'
+import { useRouter } from 'vue-router'
+import {IotInfoClassifyApi} from "@/api/pms/info";
+
+// 类型定义
+interface Tree {
+  id: number | string
+  name: string
+  type: string
+  children?: Tree[]
+  [key: string]: any
+}
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formRef = ref() // 搜索的表单
-const firstLevelKeys = ref([])
+const firstLevelKeys = ref<(number | string)[]>([])
+const expandedKeys = ref<(number | string)[]>([]) // 用于展开节点的路径
+
+// 表单相关
+const formLoading = ref(false)
+const formType = ref<'create' | 'update'>('create')
+const formData = ref({
+  id: undefined,
+  title: '',
+  parentId: undefined,
+  name: undefined,
+  sort: undefined,
+  leaderUserId: undefined,
+  phone: undefined,
+  email: undefined,
+  status: CommonStatusEnum.ENABLE
+})
+const formRules = ref({
+  name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
+  sort: [{ required: true, message: '请输入排序', trigger: 'blur' }]
+})
+
 const openForm = (type: string, id?: number) => {
   formRef.value.open(type, id)
 }
@@ -123,10 +151,15 @@ const resetForm = () => {
   }
   formRef.value?.resetFields()
 }
+
 defineOptions({ name: 'IotTree' })
+
+// 接收外部参数
 const props = defineProps({
-  deviceId: { type: Number, required: true }
+  deviceId: { type: Number, required: true },
+  currentId: { type: [Number, String], default: null } // 新增:接收需要定位的节点ID
 })
+
 const deptName = ref('')
 const nodeInfo = ref({})
 const treeList = ref<Tree[]>([]) // 树形结构
@@ -137,9 +170,10 @@ const menuX = ref(0)
 const menuY = ref(0)
 const contextMenuRef = ref(null) // 弹窗DOM引用
 let selectedNode = null
-
+const parentId = ref()
 
 const { push } = useRouter() // 路由跳转
+
 // 动态高度计算
 const treeContainer = ref(null)
 const setHeight = () => {
@@ -149,6 +183,48 @@ const setHeight = () => {
   treeContainer.value.style.height = `${windowHeight * 0.78}px` // 60px 底部预留
 }
 
+// 新增:查找节点路径的递归方法
+const findNodePath = (nodes: Tree[], targetId: number | string, path: (number | string)[] = []): (number | string)[] | null => {
+  for (const node of nodes) {
+    path.push(node.id)
+    if (node.id === targetId) {
+      return [...path]
+    }
+    if (node.children && node.children.length) {
+      const result = findNodePath(node.children, targetId, path)
+      if (result) {
+        return result
+      }
+    }
+    path.pop()
+  }
+  return null
+}
+
+// 新增:定位节点并高亮
+const locateNode = (targetId: number | string) => {
+  if (!targetId || !treeList.value.length) return
+
+  const pathIds = findNodePath(treeList.value, targetId)
+  if (pathIds) {
+    // 展开所有父节点
+    expandedKeys.value = pathIds.slice(0, -1)
+
+    // 等待DOM更新后设置当前节点
+    nextTick(() => {
+      if (treeRef.value) {
+        treeRef.value.setCurrentKey(targetId)
+
+        // 滚动到节点位置
+        const node = treeRef.value.getNode(targetId)
+        if (node && node.$el) {
+          node.$el.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
+        }
+      }
+    })
+  }
+}
+
 onMounted(async () => {
   await getTreeInfo()
   setHeight()
@@ -161,11 +237,12 @@ onUnmounted(() => {
 
 const handleRightClick = (event, node, data) => {
   nodeInfo.value = node
-  console.log(JSON.stringify(nodeInfo.value))
   event.preventDefault()
   menuX.value = event.clientX
   menuY.value = event.clientY
   selectedNode = data
+  debugger
+  parentId.value = data.data.id
   if (nodeInfo.value.type === 'device') {
     deviceVisible.value = true
   } else if (nodeInfo.value.type === 'file') {
@@ -177,44 +254,52 @@ const handleDeviceClick = async () => {
   const id = nodeInfo.value.originId
   push({ name: 'DeviceDetailInfo', params: { id } })
   deviceVisible.value = false
+  menuVisible.value = false
 }
 
-
 const handleMenuClick = async (action) => {
   switch (action) {
     case 'add':
       dialogVisible.value = true
-      dialogTitle.value = '新增资料分类'
+      dialogTitle.value = '新增目录'
       formType.value = 'create'
       resetForm()
       break
     case 'edit':
       resetForm()
       dialogVisible.value = true
-      dialogTitle.value = '编辑资料分类'
+      dialogTitle.value = '编辑目录'
       formType.value = 'update'
-      formData.value = nodeInfo.value
-      console.log(JSON.stringify(formData.value))
+      formData.value = { ...nodeInfo.value }
       break
     case 'delete':
       // 删除的二次确认
       await message.delConfirm()
-      // 发起删除
-      await IotInfoClassifyApi.deleteIotInfoClassify(nodeInfo.value.id)
+      // 假设存在删除接口
+      await IotTreeApi.deleteIotTree(nodeInfo.value.id)
       message.success(t('common.delSuccess'))
       // 刷新列表
-      await getTree()
+      await getTreeInfo()
       break
   }
+  deviceVisible.value = false
   menuVisible.value = false
 }
+
 /** 获得部门树 */
 const getTreeInfo = async () => {
   const res = await IotTreeApi.getSimpleTreeList()
   treeList.value = []
   treeList.value.push(...handleTree(res))
-  firstLevelKeys.value = treeList.value.map(node => node.id);
-  emits('success', treeList.value[0].id)
+
+  // 处理展开逻辑:有currentId则展开对应路径,否则展开一级节点
+  if (props.currentId) {
+    locateNode(props.currentId)
+  } else {
+    firstLevelKeys.value = treeList.value.map(node => node.id)
+    expandedKeys.value = [...firstLevelKeys.value]
+  }
+  emits('success', treeList.value[0]?.id)
 }
 
 /** 基于名字过滤 */
@@ -223,42 +308,61 @@ const filterNode = (name: string, data: Tree) => {
   return data.name.includes(name)
 }
 
-/** 处理部门被点击 */
+/** 处理节点被点击 */
 const handleNodeClick = async (row: { [key: string]: any }) => {
   deviceVisible.value = false
   menuVisible.value = false
   emits('node-click', row)
 }
+
+/** 提交表单 */
+const submitForm = async () => {
+  // 表单验证逻辑
+  formLoading.value = true
+  try {
+    // 假设存在提交接口
+    formData.value.deviceId = props.deviceId
+    debugger
+    formData.value.parentId = parentId.value
+    if (formData.value.parentId===undefined||formData.value.parentId===null) {
+      formData.value.parentId = props.currentId
+    }
+    formData.value.type = 'file'
+    if (formType.value === 'create') {
+      debugger
+      await IotTreeApi.createIotTree(formData.value)
+    } else {
+      await IotTreeApi.updateIotTree(formData.value)
+    }
+    message.success(t(formType.value === 'create' ? 'common.addSuccess' : 'common.updateSuccess'))
+    dialogVisible.value = false
+    await getTreeInfo()
+  } catch (error) {
+    console.error(error)
+  } finally {
+    formLoading.value = false
+  }
+}
+
 const emits = defineEmits(['node-click', 'success'])
 
-/** 监听deptName */
+/** 监听搜索框变化 */
 watch(deptName, (val) => {
   treeRef.value!.filter(val)
 })
-// const handleClickOutside = (event) => {
-//   if (
-//     menuVisible.value &&
-//     contextMenuRef.value &&
-//     !contextMenuRef.value.contains(event.target)
-//   ) {
-//     menuVisible.value = false
-//   }
-// }
-// watch(menuVisible, (visible) => {
-//   if (visible) {
-//     document.addEventListener('click', handleClickOutside)
-//   } else {
-//     document.removeEventListener('click', handleClickOutside)
-//   }
-// })
-// onBeforeUnmount(() => {
-//   document.removeEventListener('click', handleClickOutside)
-// })
-/** 初始化 */
-// onMounted(async () => {
-//   await getTreeInfo()
-// })
+
+/** 监听currentId变化,重新定位 */
+watch(
+  () => props.currentId,
+  (newVal) => {
+    if (newVal && treeList.value.length) {
+      locateNode(newVal)
+    }
+  },
+  { immediate: true }
+)
 </script>
+
 <style lang="scss" scoped>
 .custom-menu {
   position: fixed;
@@ -287,7 +391,7 @@ watch(deptName, (val) => {
   border: 1px solid #e4e7ed;
   border-radius: 4px;
 }
-/* 自定义滚动条(可选) */
+/* 自定义滚动条 */
 .tree-container::-webkit-scrollbar {
   width: 6px;
 }
@@ -295,4 +399,11 @@ watch(deptName, (val) => {
   background: #c0c4cc;
   border-radius: 3px;
 }
+
+/* 自定义高亮样式 */
+::v-deep .el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
+  background-color: #eaf5ff !important;
+  color: #1890ff !important;
+  font-weight: bold;
+}
 </style>

+ 55 - 13
src/views/system/tree/index.vue

@@ -12,14 +12,14 @@
           {{ item.name }}
         </el-breadcrumb-item>
       </el-breadcrumb>
-      <el-input :placeholder="'在'+breadcrumbs[breadcrumbs.length - 1].name+'下搜索'" style="width: 250px;height: 30px"/>
+      <el-input v-model="queryParams.allName" :placeholder="'在'+breadcrumbs[breadcrumbs.length - 1].name+'下搜索'" style="width: 250px;height: 30px" @input="searchFolderAndFile"/>
     </div>
   </ContentWrap>
   <div class="container-tree" ref="container">
   <el-row >
     <div class="left-tree" :style="{ width: leftWidth + 'px' }">
       <ContentWrapNoBottom >
-        <PmsTree @node-click="handleFileNodeClick" @success="successList" :deviceId="id" />
+        <PmsTree @node-click="handleFileNodeClick" @success="successList" :currentId="queryParams.classId" :deviceId="id" />
       </ContentWrapNoBottom>
     </div>
 <!--    </el-col>-->
@@ -45,11 +45,26 @@
                 class="!w-240px"
               />
             </el-form-item>
+            <el-form-item v-show="false" :label="t('file.fileType') " prop="fileType">
+              <el-select
+                v-model="queryParams.fileType"
+                :placeholder="t('file.fileTypeHolder')"
+                clearable
+                class="!w-200px"
+              >
+                <el-option
+                  v-for="dict in getStrDictOptions(DICT_TYPE.PMS_FILE_TYPE)"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+              </el-select>
+            </el-form-item>
             <el-form-item>
               <el-button @click="handleQuery"><Icon icon="ep:search" />
                 {{  t('file.search')}}</el-button>
               <el-button @click="resetQuery"><Icon icon="ep:refresh" />{{  t('file.reset')}}</el-button>
-              <el-button type="primary" plain @click="openForm('create')">
+              <el-button type="primary" :loading="uploadLoading"  @click="openForm('create')">
                 <Icon icon="ep:plus" /> {{  t('file.upload')}}
               </el-button>
             </el-form-item>
@@ -66,6 +81,8 @@
                 <Icon v-else-if="scope.row.fileType==='file'&&(scope.row.fileClassify==='doc'||scope.row.fileClassify==='docx')" icon="fa:file-word-o"  color="blue"/>
                 <Icon v-else-if="scope.row.fileType==='file'&&(scope.row.fileClassify==='xls'||scope.row.fileClassify==='xlsx')" icon="fa-solid:file-excel"  color="#107C41"/>
                 <Icon v-else-if="scope.row.fileType==='file'&&(scope.row.fileClassify==='txt')" icon="fa:file-text-o" />
+                <Icon v-else-if="scope.row.fileType==='file'&&(scope.row.fileClassify==='ppt'||scope.row.fileClassify==='pptx')" icon="fa-solid:file-powerpoint" color="#C43E1C" />
+                <Icon v-else icon="fa-solid:file-alt" />
                   {{scope.row.filename}}
                 </div>
               </template>
@@ -83,7 +100,7 @@
 <!--              </template>-->
 <!--            </el-table-column>-->
             <el-table-column :label="t('file.dept') " align="center" prop="deptName" />
-            <el-table-column :label="t('file.device') " align="center" prop="deviceName" min-width="220"/> />
+            <el-table-column :label="t('file.device') " align="center" prop="deviceCode" min-width="220"/> />
             <el-table-column :label="t('file.operation') " align="center" width="160">
               <template #default="scope">
                 <div class="flex items-center justify-center">
@@ -119,7 +136,7 @@ import { ref, onMounted, onUnmounted } from 'vue'
 
 import PmsTree from '@/views/system/tree/PmsTree.vue'
 import {CACHE_KEY, useCache} from "@/hooks/web/useCache";
-import {DICT_TYPE} from "@/utils/dict";
+import {DICT_TYPE, getStrDictOptions} from "@/utils/dict";
 import {IotInfoClassifyApi} from "@/api/pms/info";
 import {IotTreeApi} from "@/api/system/tree";
 defineOptions({ name: 'IotTree' })
@@ -128,7 +145,18 @@ const container = ref(null)
 const leftWidth = ref(350) // 初始左侧宽度
 const rightWidth = ref(window.innerWidth * 0.8)
 let isDragging = false
+const uploadLoading = ref(false);
 
+
+const searchFolderAndFile = async () =>{
+  debugger
+  formLoading.value = true
+  const data = await IotInfoApi.IotInfoApi.getAllChildContentFile(queryParams)
+  debugger
+  list.value = data
+  formLoading.value = false
+  queryParams.filename = "";
+}
 const openWeb = (url) => {
   window.open('http://1.94.244.160:8012/onlinePreview?url='+encodeURIComponent(Base64.encode(url)));
 }
@@ -183,15 +211,17 @@ const { params } = useRoute() // 查询参数
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const list = ref<IotDeviceVO[]>([]) // 列表的数据
 // const total = ref(0) // 列表的总页数
-const id = params.id as unknown as number
+const id = ref()
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   filename: null,
+  fileType: null,
   createTime: [],
   deviceId: null,
   classId: null,
-  deptId: undefined
+  deptId: undefined,
+  allName: null,
 })
 // SPU 表单数据
 const formData = ref({
@@ -237,16 +267,23 @@ const handleDelete = async (id: number) => {
   } catch {}
 }
 const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  if (classType.value==='dept'){
-    message.error(t('common.deptChoose'))
-    return;
-  }
+const openForm = async (type: string, id?: number) => {
+  // if (classType.value==='dept'){
+  //   message.error(t('common.deptChoose'))
+  //   return;
+  // }
+  uploadLoading.value = true
   if (!queryParams.classId) {
     message.error(t('common.leftNode'))
     return
   }
   formRef.value.open(type, id)
+  await new Promise((resolve) => {
+    setTimeout(() => {
+      resolve(); // 操作完成后resolve
+    }, 2000); // 模拟2秒耗时
+  });
+  uploadLoading.value = false
 }
 const deviceId = ref('')
 const clickNodeId = ref('')
@@ -278,7 +315,6 @@ const updateBreadcrumbs = async (node) => {
 
 // 表格行点击事件
 const inContent = async (row) => {
-  debugger
   if (row.fileType!='content') {
     window.open('http://1.94.244.160:8012/onlinePreview?url='+encodeURIComponent(Base64.encode(row.filePath)));
     return
@@ -289,6 +325,7 @@ const inContent = async (row) => {
   await updateBreadcrumbs(row)
   // 可以添加其他表格行点击需要的逻辑
   queryParams.classId = row.id;
+  nodeId.value = row.id
   const data = await IotInfoApi.IotInfoApi.getChildContentFile(queryParams)
   formLoading.value = false
   list.value = data
@@ -304,6 +341,10 @@ const handleFileNodeClick = async (row) => {
   })
 
   queryParams.classId = row.id
+  if (row.type=='device') {
+    id.value = row.originId
+  }
+
   classType.value = row.type
   if (row.type==='device') {
     deviceId.value = row.originId
@@ -414,6 +455,7 @@ onMounted(async () => {
   // queryParams.deptId = wsCache.get(CACHE_KEY.USER).user.deptId;
   // await getList()
   // deviceId.value = params.id as unknown as number
+  id.value = params.id as unknown as number
 })
 </script>
 <style scoped>