index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. <template>
  2. <ContentWrap>
  3. <div style="display: flex; justify-content: space-between; align-items: center;">
  4. <el-breadcrumb separator=">" class="breadcrumb-container">
  5. <el-breadcrumb-item
  6. v-for="(item, index) in breadcrumbs"
  7. :key="index"
  8. @click="handleBreadcrumbClick(index)"
  9. :class="{ 'current-crumb': index === breadcrumbs.length - 1 }"
  10. class="custom-breadcrumb-item"
  11. >
  12. {{ item.name }}
  13. </el-breadcrumb-item>
  14. </el-breadcrumb>
  15. <el-input :placeholder="'在'+breadcrumbs[breadcrumbs.length - 1].name+'下搜索'" style="width: 250px;height: 30px"/>
  16. </div>
  17. </ContentWrap>
  18. <div class="container-tree" ref="container">
  19. <el-row >
  20. <div class="left-tree" :style="{ width: leftWidth + 'px' }">
  21. <ContentWrapNoBottom >
  22. <PmsTree @node-click="handleFileNodeClick" @success="successList" :deviceId="id" />
  23. </ContentWrapNoBottom>
  24. </div>
  25. <!-- </el-col>-->
  26. <div
  27. class="divider-tree"
  28. @mousedown="startDrag"
  29. ></div>
  30. <div class="right-tree" :style="{ width: rightWidth + 'px' }">
  31. <ContentWrap>
  32. <el-form
  33. class="-mb-15px"
  34. :model="queryParams"
  35. ref="queryFormRef"
  36. :inline="true"
  37. label-width="68px"
  38. >
  39. <el-form-item :label="t('file.name') " prop="filename">
  40. <el-input
  41. v-model="queryParams.filename"
  42. :placeholder="t('file.nameHolder')"
  43. clearable
  44. @keyup.enter="handleQuery"
  45. class="!w-240px"
  46. />
  47. </el-form-item>
  48. <el-form-item>
  49. <el-button @click="handleQuery"><Icon icon="ep:search" />
  50. {{ t('file.search')}}</el-button>
  51. <el-button @click="resetQuery"><Icon icon="ep:refresh" />{{ t('file.reset')}}</el-button>
  52. <el-button type="primary" plain @click="openForm('create')">
  53. <Icon icon="ep:plus" /> {{ t('file.upload')}}
  54. </el-button>
  55. </el-form-item>
  56. </el-form>
  57. </ContentWrap>
  58. <ContentWrap>
  59. <el-table v-loading="formLoading" :data="list" :stripe="true" :show-overflow-tooltip="true" @row-dblclick="inContent" class="custom-table">
  60. <el-table-column :label="t('file.name') " align="left" prop="filename" min-width="300">
  61. <template #default="scope">
  62. <div style="display: flex; align-items: center; gap: 5px;">
  63. <Icon v-if="scope.row.fileType==='content'" icon="fa:folder-open" color="orange"/>
  64. <Icon v-else-if="scope.row.fileType==='pic'||scope.row.fileClassify==='jpg'||scope.row.fileClassify==='png'" icon="ep:picture-filled" color="#2183D1"/>
  65. <Icon v-else-if="scope.row.fileType==='file'&&scope.row.fileClassify==='pdf'" icon="fa-solid:file-pdf" color="#E20012"/>
  66. <Icon v-else-if="scope.row.fileType==='file'&&(scope.row.fileClassify==='doc'||scope.row.fileClassify==='docx')" icon="fa:file-word-o" color="blue"/>
  67. <Icon v-else-if="scope.row.fileType==='file'&&(scope.row.fileClassify==='xls'||scope.row.fileClassify==='xlsx')" icon="fa-solid:file-excel" color="#107C41"/>
  68. <Icon v-else-if="scope.row.fileType==='file'&&(scope.row.fileClassify==='txt')" icon="fa:file-text-o" />
  69. {{scope.row.filename}}
  70. </div>
  71. </template>
  72. </el-table-column>
  73. <el-table-column :label="t('file.fileType') " align="center" prop="fileType" >
  74. <template #default="scope">
  75. <dict-tag :type="DICT_TYPE.PMS_FILE_TYPE" :value="scope.row.fileType" />
  76. </template>
  77. </el-table-column>
  78. <el-table-column :label="t('file.fileSize') " align="center" prop="fileSize" />
  79. <!-- <el-table-column :label="t('file.preview') " align="center" prop="filePath" >-->
  80. <!-- <template #default="scope">-->
  81. <!-- <el-button v-if="scope.row.fileType!=='content'" link type="primary" @click="openWeb(scope.row.filePath)"> <Icon size="19" icon="ep:view" /> </el-button>-->
  82. <!-- </template>-->
  83. <!-- </el-table-column>-->
  84. <el-table-column :label="t('file.dept') " align="center" prop="deptName" />
  85. <el-table-column :label="t('file.device') " align="center" prop="deviceName" min-width="220"/> />
  86. <el-table-column :label="t('file.operation') " align="center" width="160">
  87. <template #default="scope">
  88. <div class="flex items-center justify-center">
  89. <el-button type="primary" v-if="scope.row.fileType!=='content'" link @click="handleDownload( scope.row.filePath)" v-hasPermi="['rq:iot-info:download']">
  90. <Icon icon="ep:download" />{{t('file.dow')}}
  91. </el-button>
  92. <!-- <el-button type="primary" link @click="handleView( scope.row)">-->
  93. <!-- <Icon icon="ep:view" />{{t('file.preview')}}-->
  94. <!-- </el-button>-->
  95. </div>
  96. </template>
  97. </el-table-column>
  98. </el-table>
  99. </ContentWrap>
  100. </div>
  101. </el-row>
  102. </div>
  103. <IotInfoFormTree
  104. ref="formRef"
  105. @success="getList"
  106. :deviceId="deviceId"
  107. :nodeId = "nodeId"
  108. :classId="clickNodeId"
  109. />
  110. </template>
  111. <script lang="ts" setup>
  112. import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
  113. import { dateFormatter } from '@/utils/formatTime'
  114. import IotInfoFormTree from '@/views/pms/iotinfo/IotInfoFormTree.vue'
  115. import * as IotInfoApi from '@/api/pms/iotinfo'
  116. import { IotInfoVO } from '@/api/pms/iotinfo'
  117. import { ref, onMounted, onUnmounted } from 'vue'
  118. import PmsTree from '@/views/system/tree/PmsTree.vue'
  119. import {CACHE_KEY, useCache} from "@/hooks/web/useCache";
  120. import {DICT_TYPE} from "@/utils/dict";
  121. import {IotInfoClassifyApi} from "@/api/pms/info";
  122. import {IotTreeApi} from "@/api/system/tree";
  123. defineOptions({ name: 'IotTree' })
  124. const container = ref(null)
  125. const leftWidth = ref(350) // 初始左侧宽度
  126. const rightWidth = ref(window.innerWidth * 0.8)
  127. let isDragging = false
  128. const openWeb = (url) => {
  129. window.open('http://1.94.244.160:8012/onlinePreview?url='+encodeURIComponent(Base64.encode(url)));
  130. }
  131. const handleView = (row) => {
  132. openForm('detail', row.id)
  133. }
  134. const startDrag = (e) => {
  135. isDragging = true
  136. document.addEventListener('mousemove', onDrag)
  137. document.addEventListener('mouseup', stopDrag)
  138. }
  139. const topNodeId = ref('')
  140. const successList = async (id) => {
  141. queryParams.classId = id
  142. topNodeId.value = id
  143. const rootItem = breadcrumbs.value.find(item => item.type === 'root');
  144. if (rootItem) {
  145. rootItem.id = id;
  146. }
  147. await getList()
  148. // queryParams.classId = ''
  149. }
  150. const onDrag = (e) => {
  151. if (!isDragging) return
  152. const containerRect = container.value.getBoundingClientRect()
  153. const newWidth = e.clientX - containerRect.left
  154. // 设置最小和最大宽度限制
  155. if (newWidth > 300 && newWidth < containerRect.width - 100) {
  156. leftWidth.value = newWidth
  157. }
  158. }
  159. const stopDrag = () => {
  160. isDragging = false
  161. document.removeEventListener('mousemove', onDrag)
  162. document.removeEventListener('mouseup', stopDrag)
  163. }
  164. const queryFormRef = ref() // 搜索的表单
  165. const { t } = useI18n() // 国际化
  166. const message = useMessage() // 消息弹窗
  167. const loading = ref(true) // 列表的加载中
  168. const { params } = useRoute() // 查询参数
  169. const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
  170. const list = ref<IotDeviceVO[]>([]) // 列表的数据
  171. // const total = ref(0) // 列表的总页数
  172. const id = params.id as unknown as number
  173. const queryParams = reactive({
  174. pageNo: 1,
  175. pageSize: 10,
  176. filename: null,
  177. createTime: [],
  178. deviceId: null,
  179. classId: null,
  180. deptId: undefined
  181. })
  182. // SPU 表单数据
  183. const formData = ref({
  184. id: undefined,
  185. deviceId: undefined,
  186. deptId: undefined,
  187. filename: undefined,
  188. fileType: undefined,
  189. filePath: undefined,
  190. remark: undefined,
  191. classId: undefined
  192. })
  193. const handleDownload = async (url) => {
  194. try {
  195. const response = await fetch(url)
  196. const blob = await response.blob()
  197. const downloadUrl = window.URL.createObjectURL(blob)
  198. const link = document.createElement('a')
  199. link.href = downloadUrl
  200. link.download = url.split('/').pop() // 自动获取文件名‌:ml-citation{ref="3" data="citationList"}
  201. link.click()
  202. URL.revokeObjectURL(downloadUrl)
  203. } catch (error) {
  204. console.error('下载失败:', error)
  205. }
  206. }
  207. const handleFileView = (url: string) => {
  208. window.open(
  209. 'http://1.94.244.160:8012/onlinePreview?url=' + encodeURIComponent(Base64.encode(url))
  210. )
  211. }
  212. const handleDelete = async (id: number) => {
  213. try {
  214. // 删除的二次确认
  215. await message.delConfirm()
  216. // 发起删除
  217. await IotInfoApi.IotInfoApi.deleteIotInfo(id)
  218. message.success(t('common.delSuccess'))
  219. // 刷新列表
  220. await getList()
  221. } catch {}
  222. }
  223. const formRef = ref()
  224. const openForm = (type: string, id?: number) => {
  225. if (classType.value==='dept'){
  226. message.error(t('common.deptChoose'))
  227. return;
  228. }
  229. if (!queryParams.classId) {
  230. message.error(t('common.leftNode'))
  231. return
  232. }
  233. formRef.value.open(type, id)
  234. }
  235. const deviceId = ref('')
  236. const clickNodeId = ref('')
  237. const nodeId = ref('')
  238. const classType = ref('')
  239. const breadcrumbs = ref([
  240. { id: null, name: '科瑞石油技术',type:'root' } // 根节点
  241. ])
  242. // 共享的面包屑更新逻辑
  243. const updateBreadcrumbs = async (node) => {
  244. // 查找当前节点是否已在面包屑中
  245. const currentIndex = breadcrumbs.value.findIndex(item => item.id === node.id)
  246. if (currentIndex > -1) {
  247. // 如果已存在则截断后面的节点
  248. breadcrumbs.value = breadcrumbs.value.slice(0, currentIndex + 1)
  249. } else {
  250. // 新增节点到面包屑
  251. breadcrumbs.value.push({ id: node.id, name: node.filename || node.name })
  252. }
  253. // 更新表格数据
  254. queryParams.classId = node.id;
  255. const data = await IotInfoApi.IotInfoApi.getChildContentFile(queryParams)
  256. list.value = data
  257. }
  258. // 表格行点击事件
  259. const inContent = async (row) => {
  260. debugger
  261. if (row.fileType!='content') {
  262. window.open('http://1.94.244.160:8012/onlinePreview?url='+encodeURIComponent(Base64.encode(row.filePath)));
  263. return
  264. }
  265. queryParams.filename = ''
  266. formLoading.value = true
  267. // 调用共享方法更新面包屑和表格
  268. await updateBreadcrumbs(row)
  269. // 可以添加其他表格行点击需要的逻辑
  270. queryParams.classId = row.id;
  271. const data = await IotInfoApi.IotInfoApi.getChildContentFile(queryParams)
  272. formLoading.value = false
  273. list.value = data
  274. }
  275. // 文件树节点点击事件
  276. const handleFileNodeClick = async (row) => {
  277. queryParams.filename = ''
  278. const parentItems = await IotTreeApi.getParentIds(row.id);
  279. breadcrumbs.value = [];
  280. parentItems.forEach(item => {
  281. breadcrumbs.value.push({ id: item.id, name: item.name,type:'root' } );
  282. })
  283. queryParams.classId = row.id
  284. classType.value = row.type
  285. if (row.type==='device') {
  286. deviceId.value = row.originId
  287. const queryParam = {
  288. deviceId: row.originId,
  289. pageNo: 1,
  290. pagesize: 10,
  291. }
  292. const data = await IotInfoClassifyApi.getIotInfoClassifyPage(queryParam)
  293. if (data){
  294. const target = data.filter((item)=> item.parentId===0)
  295. clickNodeId.value = target[0].id
  296. }
  297. } else if (row.type === 'file'){
  298. clickNodeId.value = row.originId
  299. } else if (row.type==='dept') {
  300. // message.error("请选择设备及文件节点")
  301. // return
  302. }
  303. nodeId.value = row.id
  304. await getList()
  305. }
  306. // 面包屑点击事件
  307. const handleBreadcrumbClick = async (index) => {
  308. queryParams.filename = ''
  309. formLoading.value = true
  310. // 忽略当前节点的点击
  311. if (index === breadcrumbs.value.length - 1) return
  312. // 截断面包屑到点击的节点
  313. const targetBreadcrumbs = breadcrumbs.value.slice(0, index + 1)
  314. breadcrumbs.value = targetBreadcrumbs
  315. // 获取对应节点的数据
  316. let targetId = targetBreadcrumbs[index].id
  317. if (!targetId){
  318. targetId = topNodeId.value
  319. }
  320. queryParams.classId = targetId
  321. const data = await IotInfoApi.IotInfoApi.getChildContentFile(queryParams)
  322. list.value = data
  323. formLoading.value = false
  324. }
  325. // const handleFileNodeClick = async (row) => {
  326. // queryParams.classId = row.id
  327. // classType.value = row.type
  328. // if (row.type==='device') {
  329. // deviceId.value = row.originId
  330. // const queryParam = {
  331. // deviceId: row.originId,
  332. // pageNo: 1,
  333. // pagesize: 10,
  334. // }
  335. // const data = await IotInfoClassifyApi.getIotInfoClassifyPage(queryParam)
  336. // debugger
  337. // if (data){
  338. // const target = data.filter((item)=> item.parentId===0)
  339. // clickNodeId.value = target[0].id
  340. // }
  341. // } else if (row.type === 'file'){
  342. // clickNodeId.value = row.originId
  343. // } else if (row.type==='dept') {
  344. // // message.error("请选择设备及文件节点")
  345. // // return
  346. // }
  347. // nodeId.value = row.id
  348. // await getList()
  349. // }
  350. /** 获得详情 */
  351. // const getDetail = async () => {
  352. // if (id) {
  353. // formLoading.value = true
  354. // try {
  355. // formData.value = (await IotDeviceApi.getIotDevice(id)) as IotDeviceVO
  356. // } finally {
  357. // formLoading.value = false
  358. // }
  359. // }
  360. // }
  361. /** 查询列表 */
  362. const getList = async () => {
  363. formLoading.value = true
  364. try {
  365. const data = await IotInfoApi.IotInfoApi.getChildContentFile(queryParams)
  366. list.value = data
  367. } finally {
  368. formLoading.value = false
  369. }
  370. }
  371. /** 搜索按钮操作 */
  372. const handleQuery = () => {
  373. queryParams.pageNo = 1
  374. getList()
  375. }
  376. /** 重置按钮操作 */
  377. const resetQuery = () => {
  378. queryFormRef.value?.resetFields()
  379. handleQuery()
  380. }
  381. const { wsCache } = useCache()
  382. /** 初始化 */
  383. onMounted(async () => {
  384. // await getDetail()
  385. // queryParams.deptId = wsCache.get(CACHE_KEY.USER).user.deptId;
  386. // await getList()
  387. // deviceId.value = params.id as unknown as number
  388. })
  389. </script>
  390. <style scoped>
  391. ::v-deep .breadcrumb-container {
  392. padding: 12px 16px;
  393. background-color: #f5f7fa;
  394. border-radius: 6px;
  395. box-shadow: 0 1px 1px rgba(0, 0, 0, 0.06);
  396. }
  397. ::v-deep .custom-breadcrumb-item {
  398. font-size: 14px;
  399. font-weight: bold;
  400. }
  401. /* 面包屑文本样式 */
  402. ::v-deep .el-breadcrumb__item .el-breadcrumb__inner {
  403. color: #101010;
  404. text-decoration: none;
  405. padding: 2px 4px;
  406. border-radius: 2px;
  407. transition: all 0.2s ease;
  408. }
  409. /* 可点击项悬停效果 */
  410. ::v-deep .el-breadcrumb__item:not(:last-child) .el-breadcrumb__inner:hover {
  411. color: #409eff;
  412. background-color: rgba(64, 158, 255, 0.1);
  413. cursor: pointer;
  414. }
  415. /* 当前项样式 */
  416. ::v-deep .el-breadcrumb__item:last-child .el-breadcrumb__inner {
  417. color: #1890ff;
  418. font-weight: 500;
  419. cursor: default;
  420. }
  421. /* 分隔符样式优化 */
  422. ::v-deep .el-breadcrumb__separator {
  423. margin: 0 8px;
  424. color: #0954f6;
  425. font-size: 12px;
  426. }
  427. .custom-table {
  428. cursor: pointer;
  429. --el-table-row-hover-bg-color: #f5f7fa; /* 优化悬停背景色 */
  430. }
  431. .container-tree {
  432. display: flex;
  433. height: 100%;
  434. user-select: none; /* 防止拖动时选中文本 */
  435. }
  436. .left-tree {
  437. background: #f0f0f0;
  438. height: 100%;
  439. overflow: auto;
  440. }
  441. .right-tree {
  442. flex: 1;
  443. height: 100%;
  444. overflow: auto;
  445. margin-left: 15px;
  446. }
  447. .divider-tree {
  448. width: 2px;
  449. background: #ccc;
  450. cursor: col-resize;
  451. position: relative;
  452. }
  453. .divider-tree:hover {
  454. background: #666;
  455. }
  456. </style>