Files
admin-govern/src/views/govern/device/fileService/index.vue
2026-06-04 19:06:36 +08:00

962 lines
34 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- 文件管理 -->
<template>
<div class="default-main main" :style="{ height: pageHeight.height }">
<div class="main_left">
<DeviceTree @node-click="nodeClick" @deviceTypeChange="deviceTypeChange"></DeviceTree>
</div>
<div class="main_right" v-loading="loading">
<div class="right_nav">
<div class="menu" v-if="activePathList.length != 0">
<el-breadcrumb separator="/">
<el-breadcrumb-item
v-for="(item, index) in activePathList"
style="cursor: pointer"
:key="index"
@click="handleIntoByPath(item)"
>
<span>{{ outPutPath(item, index) }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<!-- <el-button :icon="Refresh" @click="handleRestartDevice" type="primary" :loading="deviceRestartLoading">
设备重启
</el-button> -->
</div>
<div class="filter" v-if="activePathList.length != 0">
<el-input
maxlength="32"
show-word-limit
style="width: 240px; height: 32px"
placeholder="请输入文件或文件夹名称"
clearable
v-model.trim="filterText"
type="text"
></el-input>
<el-button type="primary" @click="handleSearch" :icon="Search">搜索</el-button>
<!-- <el-button @click="handleRefresh" :icon="Refresh">重置</el-button> -->
<el-upload
v-if="activePath != '/'"
action=""
:auto-upload="false"
:show-file-list="false"
:on-change="(file: any, fileList: any) => {
handleUpload(file, fileList, activePath)
}
"
>
<el-button>
文件上传
<el-icon class="el-icon--right">
<Upload />
</el-icon>
</el-button>
</el-upload>
<el-button @click="handleAddNewDir" v-if="activePath != '/'" type="primary" :icon="Plus">
新建文件夹
</el-button>
<div class="upload_progress" v-if="status != 0 && status != 100 && changeType == 'upload'">
正在上传:{{ fileName }}
<el-progress :percentage="status" />
</div>
</div>
<!-- 以列表形式展示 -->
<div :style="tableHeight">
<vxe-table
style="margin-top: 10px"
border
auto-resize
height="auto"
:data="dirList"
v-bind="defaultAttribute"
>
<vxe-column type="seq" title="序号" width="80"></vxe-column>
<vxe-column field="prjDataPath" align="center" title="名称" minWidth="180" #default="{ row }">
<span
style="cursor: pointer; color: #551a8b"
:style="{
'text-decoration': row.type == 'dir' ? 'underline' : 'none',
color: row.type == 'dir' ? '#551a8b' : '#000'
}"
@click="handleIntoDir(row)"
>
{{
row &&
row?.prjDataPath &&
row?.prjDataPath.includes(activePath) &&
row?.prjDataPath.length > activePath.length
? row?.prjDataPath.replace(activePath, ' ').replace('/', ' ')
: row?.prjDataPath.replace('/', ' ')
}}
</span>
</vxe-column>
<vxe-column field="startTime" align="center" title="文件时间" minWidth="140" #default="{ row }">
{{ row.startTime ? row.startTime : '/' }}
</vxe-column>
<vxe-column field="type" align="center" title="类型" minWidth="100" #default="{ row }">
<span>
{{ row.type == 'dir' ? '文件夹' : row.type == 'file' ? '文件' : '/' }}
</span>
</vxe-column>
<vxe-column field="size" align="center" minWidth="100" title="大小" #default="{ row }">
<span>
{{ row.size && row.type == 'file' ? row.size + 'KB' : '/' }}
</span>
</vxe-column>
<!--<vxe-column field="fileCheck" align="center" title="文件校验码" width="100" #default="{ row }">
{{ row.fileCheck ? row.fileCheck : '/' }}
</vxe-column> -->
<vxe-column title="操作" width="120px" fixed="right">
<template #default="{ row }">
<el-button link size="small" type="danger" @click="handleDelDirOrFile(row)">删除</el-button>
<el-button
v-if="row.type == 'file'"
link
size="small"
type="primary"
@click="handleDownLoad(row)"
>
下载
</el-button>
</template>
</vxe-column>
</vxe-table>
</div>
<div class="list" v-if="dirList.length != 0 && !loading" style="display: none">
<div class="list_item" v-for="(item, index) in dirList" :key="index">
<div class="item_download">
<el-button
v-if="activePath && activePath != '/'"
type="danger"
size="small"
@click="handleDelDirOrFile(item)"
circle
>
<el-icon>
<Delete />
</el-icon>
</el-button>
<el-button v-if="item?.type == 'file'" size="small" @click="handleDownLoad(item)" circle>
<el-icon>
<Download />
</el-icon>
</el-button>
</div>
<img v-if="item?.type == 'dir'" @click="handleIntoDir(item)" src="@/assets/img/wenjianjia.svg" />
<img
class="img_file"
@click="handleIntoDir(item)"
v-if="item?.type == 'file'"
src="@/assets/img/wenjian.svg"
/>
<!-- <span v-if="!item.type">暂无数据</span> -->
<p>
{{
item &&
item?.prjDataPath &&
item?.prjDataPath.includes(activePath) &&
item?.prjDataPath.length > activePath.length
? item?.prjDataPath.replace(activePath, ' ').replace('/', ' ')
: item?.prjDataPath.replace('/', ' ')
}}
</p>
</div>
</div>
<!-- <el-empty v-if="dirList.length === 0" /> -->
</div>
<popup ref="fileRef"></popup>
<el-dialog
v-model.trim="addDeviceDirOpen"
:destroy-on-close="true"
title="新建文件夹目录"
width="500"
@closed="close"
>
<el-form
ref="formRef"
:model="form"
:rules="{ path: [{ required: true, message: '请输入文件夹名称', trigger: 'blur' }] }"
>
<el-form-item label="文件夹名称" prop="path">
<el-input maxlength="32" show-word-limit v-model.trim="form.path" placeholder="请输入文件夹名称" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="close">取消</el-button>
<el-button type="primary" @click="submitDeviceDir">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import DeviceTree from '@/components/tree/govern/deviceTree.vue'
import { mainHeight } from '@/utils/layout'
import { ref, watch, onMounted, onBeforeUnmount, h, inject } from 'vue'
import { ElMessage, ElMessageBox, ElInput, ElSegmented } from 'element-plus'
import {
getFileServiceFileOrDir,
uploadDeviceFile,
reStartDevice,
addDeviceDir,
delDeviceDir,
listDir,
downloadFileFromFrontr,
deleteCld,
uploadFileToFront,
mkdir
} from '@/api/cs-device-boot/fileService'
import { defaultAttribute } from '@/components/table/defaultAttribute'
import { Delete, Download, Upload, Plus, Refresh, Search } from '@element-plus/icons-vue'
import popup from './popup.vue'
import mqtt from 'mqtt'
import { useAdminInfo } from '@/stores/adminInfo'
import { passwordConfirm } from '@/api/user-boot/user'
import { downLoadFile } from '@/utils/downloadFile.ts'
defineOptions({
name: 'govern/device/fileService/index'
})
const pageHeight = mainHeight(20)
const tableHeight = mainHeight(130)
const adminInfo = useAdminInfo()
const loading = ref(false)
//nDid
const nDid = ref<string>('')
const devId = ref<string>('')
//当前目录
const activePath = ref<string>('')
//判断是否是根目录
const isRoot = ref<boolean>(true)
//储存所有点击过的目录
const activePathList: any = ref([])
const devConType = ref<string>('')
const deviceTypeChange = (val: any, obj: any) => {
nodeClick(obj)
}
const nodeClick = (e: any) => {
if (e && (e.level == 2 || e.level == 3 || e.type == 'device')) {
loading.value = true
nDid.value = e.ndid
devId.value = e.id
dirList.value = []
activePathList.value = []
activePath.value = '/'
devConType.value = e.devConType
if (devConType.value == 'CLD') {
listDir({ devId: devId.value, filePath: activePath.value })
.then((resp: any) => {
if (resp.code == 'A0000') {
dirList.value = resp.data
currentDirList.value = resp.data
activePathList.value = [{ path: activePath.value }]
loading.value = false
}
})
.catch(e => {
loading.value = false
})
} else {
getFileServiceFileOrDir({ nDid: nDid.value, name: activePath.value, type: 'dir' })
.then((resp: any) => {
if (resp.code == 'A0000') {
dirList.value = resp.data
currentDirList.value = resp.data
activePathList.value = [{ path: activePath.value }]
loading.value = false
}
})
.catch(e => {
loading.value = false
})
}
}
}
//搜索文件或文件夹
const filterText = ref('')
const handleSearch = () => {
let filterList: any = []
dirList.value = currentDirList.value
dirList.value.map(item => {
if (filterText.value && item.prjDataPath.includes(filterText.value)) {
filterList.push(item)
}
})
if (filterList.length != 0) {
dirList.value = filterList
}
if (filterList.length == 0) {
dirList.value = []
}
if (!filterText.value) {
dirList.value = currentDirList.value
}
}
//重置搜索
const handleRefresh = () => {
loading.value = true
filterText.value = ''
dirList.value = currentDirList.value
reloadCurrentMenu('')
}
const reboot: any = ref('')
const vNode = () => {
return h('div', {}, [
h(ElInput, {
modelValue: reboot.value,
'onUpdate:modelValue': ($event: any) => {
reboot.value = $event
},
placeholder: '请输入姓名',
type: 'password',
autocomplete: 'off',
class: 'displayPass'
})
])
}
//设备重启
const deviceRestartLoading = ref<boolean>(false)
const handleRestartDevice = () => {
deviceRestartLoading.value = true
ElMessageBox.prompt('二次校验密码确认', '设备重启', {
confirmButtonText: '确认',
cancelButtonText: '取消',
customClass: 'customInput',
inputType: 'text',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
if (instance.inputValue == null) {
return ElMessage.warning('请输入密码')
} else if (instance.inputValue?.length > 32) {
return ElMessage.warning('密码长度不能超过32位,当前密码长度为' + instance.inputValue.length)
} else {
done()
}
} else {
done()
}
}
})
.then(({ value }) => {
if (!value) {
ElMessage.warning('请输入密码')
loading.value = false
deviceRestartLoading.value = false
} else {
passwordConfirm(value)
.then((resp: any) => {
if (resp.code == 'A0000') {
reStartDevice({ nDid: nDid.value }).then((res: any) => {
if (res.code == 'A0000') {
deviceRestartLoading.value = false
ElMessage({ message: res.message, type: 'success', duration: 5000 })
} else {
deviceRestartLoading.value = false
}
})
}
})
.catch(e => {
loading.value = false
deviceRestartLoading.value = false
})
}
})
.catch(() => {
deviceRestartLoading.value = false
})
}
// 进入文件夹
const dirList = ref([])
// 当前目录数据
const currentDirList = ref([])
const handleIntoDir = (row: any) => {
if (!row.type || row.type == 'file') return
loading.value = true
const obj = {
nDid: nDid.value,
name: row.prjDataPath,
type: row.type
}
//当前点击的目录
activePath.value = row.prjDataPath
if (activePathList.value.indexOf(obj.name) == -1) {
activePathList.value.push({ path: obj.name })
}
if (devConType.value == 'CLD') {
listDir({ devId: devId.value, filePath: row.prjDataPath })
.then((resp: any) => {
if (resp.code == 'A0000') {
dirList.value = resp.data
currentDirList.value = resp.data
activePathList.value.map((item: any, index: any) => {
if (item.path.includes(activePath.value) && item.path.length > activePath.value.length) {
activePathList.value.splice(index, 1)
}
})
loading.value = false
}
})
.catch(e => {
loading.value = false
})
} else {
getFileServiceFileOrDir(obj)
.then(res => {
dirList.value = res.data
loading.value = false
currentDirList.value = res.data
activePathList.value.map((item: any, index: any) => {
if (item.path.includes(activePath.value) && item.path.length > activePath.value.length) {
activePathList.value.splice(index, 1)
}
})
isRoot.value = false
})
.catch(e => {
loading.value = false
})
}
}
//处理导航栏路径
const outPutPath = (row: any, key: any) => {
let path = ''
if (key == 0) {
path = '/根目录'
}
if (key == 1) {
path = row.path
}
if (key > 1) {
if (row.path.includes(activePathList.value[1].path)) {
path = row.path.replace(activePathList.value[1].path, ' ')
}
if (row.path.split('/').length !== 0) {
path = '/' + row.path.split('/')[row.path.split('/').length - 1]
}
}
return path.split('/')[1]
}
//根据面包屑导航切换
const handleIntoByPath = async (val: any) => {
const obj = {
nDid: nDid.value,
name: val.path,
type: 'dir'
}
activePath.value = val.path
loading.value = true
if (devConType.value == 'CLD') {
listDir({ devId: devId.value, filePath: val.path })
.then((resp: any) => {
if (resp.code == 'A0000') {
dirList.value = resp.data
activePathList.value.map((item: any, index: any) => {
if (item.path.includes(activePath.value) && item.path.length > activePath.value.length) {
activePathList.value.splice(index, 1)
}
})
loading.value = false
}
})
.catch(e => {
loading.value = false
})
} else {
getFileServiceFileOrDir(obj)
.then(res => {
dirList.value = res.data
activePathList.value.map((item: any, index: any) => {
if (item.path.includes(activePath.value) && item.path.length > activePath.value.length) {
activePathList.value.splice(index, 1)
}
})
loading.value = false
})
.catch(e => {
loading.value = false
})
}
}
const form = ref({
path: ''
})
//新建文件夹弹框flag
const addDeviceDirOpen = ref<boolean>(false)
const close = () => {
addDeviceDirOpen.value = false
}
//打开新建文件夹弹框
const handleAddNewDir = () => {
form.value.path = ''
addDeviceDirOpen.value = true
}
const formRef = ref()
//重新加载当前页面菜单
const reloadCurrentMenu = (msg: string) => {
loading.value = true
if (devConType.value == 'CLD') {
listDir({ devId: devId.value, filePath: activePath.value })
.then((resp: any) => {
if (resp.code == 'A0000') {
dirList.value = resp.data
currentDirList.value = resp.data
activePathList.value.map((item: any, index: any) => {
if (item.path.includes(activePath.value) && item.path.length > activePath.value.length) {
activePathList.value.splice(index, 1)
}
})
loading.value = false
if (!msg) return
ElMessage({ message: msg, type: 'success', duration: 5000 })
}
})
.catch(e => {
loading.value = false
})
} else {
getFileServiceFileOrDir({ nDid: nDid.value, name: activePath.value, type: 'dir' })
.then((resp: any) => {
if (resp.code == 'A0000') {
loading.value = false
dirList.value = resp.data
currentDirList.value = resp.data
activePathList.value.map((item: any, index: any) => {
if (item.path.includes(activePath.value) && item.path.length > activePath.value.length) {
activePathList.value.splice(index, 1)
}
})
loading.value = false
if (!msg) return
ElMessage({ message: msg, type: 'success', duration: 5000 })
}
})
.catch(e => {
loading.value = false
})
}
}
//新建文件夹
const submitDeviceDir = () => {
formRef.value.validate((valid: any) => {
if (valid) {
if (devConType.value == 'CLD') {
let obj = {
devId: devId.value,
filePath:
activePath.value == '/'
? activePath.value + form.value.path
: activePath.value + '/' + form.value.path
}
loading.value = true
mkdir(obj).then((res: any) => {
if (res.code == 'A0000') {
reloadCurrentMenu(res.message)
addDeviceDirOpen.value = false
}
})
} else {
let obj = {
nDid: nDid.value,
path:
activePath.value == '/'
? activePath.value + form.value.path
: activePath.value + '/' + form.value.path
}
loading.value = true
addDeviceDir(obj).then((res: any) => {
if (res.code == 'A0000') {
reloadCurrentMenu(res.message)
addDeviceDirOpen.value = false
}
})
}
}
})
}
//删除文件夹或文件
const handleDelDirOrFile = (row: any) => {
ElMessageBox.prompt('二次校验密码确认', '', {
confirmButtonText: '确认',
cancelButtonText: '取消',
customClass: 'customInput',
inputType: 'text',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
if (instance.inputValue == null) {
return ElMessage.warning('请输入密码')
} else if (instance.inputValue?.length > 32) {
return ElMessage.warning('密码长度不能超过32位,当前密码长度为' + instance.inputValue.length)
} else {
done()
}
} else {
done()
}
}
}).then(({ value }) => {
if (!value) {
ElMessage.warning('请输入密码')
} else {
loading.value = true
passwordConfirm(value)
.then((resp: any) => {
if (resp.code == 'A0000') {
if (devConType.value == 'CLD') {
deleteCld({
devId: devId.value,
filePath: row.prjDataPath
})
.then((res: any) => {
if (res.code == 'A0000') {
reloadCurrentMenu(res.message)
}
})
.catch(e => {
loading.value = false
})
} else {
delDeviceDir({ nDid: nDid.value, path: row.prjDataPath })
.then((res: any) => {
if (res.code == 'A0000') {
reloadCurrentMenu(res.message)
}
})
.catch(e => {
loading.value = false
})
}
}
})
.catch(e => {
loading.value = false
})
}
})
}
const changeType = ref<any>('')
//下载文件
const fileRef = ref()
const handleDownLoad = async (row: any) => {
if (devConType.value == 'CLD') {
ElMessage.info('下载中,请稍等...')
downloadFileFromFrontr({
devId: devId.value,
filePath: row.prjDataPath
}).then(res => {
downLoadFile(row.name, row.name, res)
})
} else {
;(await nDid.value) && fileRef.value && fileRef.value.open(row, nDid.value)
// fileName.value = row?.prjDataPath.split('/')[row?.prjDataPath.split('/').length - 1]
// localStorage.setItem('fileName', fileName.value)
changeType.value = 'download'
localStorage.setItem('changeType', changeType.value)
}
}
//上传文件
const fileName = ref<any>('')
const handleUpload = (e: any, fileList: any, row: any) => {
// loading.value=true
fileName.value = e.name
localStorage.setItem('fileName', fileName.value)
changeType.value = 'upload'
localStorage.setItem('changeType', changeType.value)
if (devConType.value == 'CLD') {
const obj = {
devId: devId.value,
file: e.raw,
dirPath: row || row.prjDataPath
}
uploadFileToFront(obj).then((res: any) => {
if (res.code == 'A0000') {
reloadCurrentMenu(res.message)
status.value = 100
}
})
} else {
const obj = {
id: nDid.value,
file: e.raw,
filePath: row || row.prjDataPath
}
uploadDeviceFile(obj).then((res: any) => {
if (res.code == 'A0000') {
reloadCurrentMenu(res.message)
status.value = 100
}
})
}
}
watch(
() => activePathList.value,
async (val, oldVal) => {
if (val) {
val.map((item: any, index: any) => {
if (item.path.includes(activePath.value) && item.path.length > activePath.value.length) {
val.splice(index, 1)
}
})
}
},
{
immediate: true,
deep: true
}
)
const mqttRef = ref()
const url: any = window.localStorage.getItem('MQTTURL')
const connectMqtt = () => {
if (mqttRef.value) {
if (mqttRef.value.connected) {
return
}
}
const options = {
protocolId: 'MQTT',
qos: 2,
clean: true,
connectTimeout: 30 * 1000,
clientId: 'mqttjs' + Math.random(),
username: 't_user',
password: 'njcnpqs'
}
mqttRef.value = mqtt.connect(url, options)
}
connectMqtt()
mqttRef.value.on('connect', (e: any) => {
// ElMessage.success('连接mqtt服务器成功!')
console.log('mqtt客户端已连接....')
// mqttRef.value.subscribe('/Web/Progress')
mqttRef.value.subscribe('/Web/Progress/+')
})
const mqttMessage = ref<any>({})
const status: any = ref()
function parseStringToObject(str: string) {
const content = str.replace(/^{|}$/g, '')
const result: any = {}
// 正则匹配key:value 格式,支持 value 里带 : / 等字符
const regex = /([^,:]+):([^,]+)(?=,|$)/g
let match
while ((match = regex.exec(content)) !== null) {
const key = match[1].trim()
const value = match[2].trim()
// 数字自动转 Number
result[key] = isNaN(Number(value)) ? value : Number(value)
}
return result
}
mqttRef.value.on('message', (topic: any, message: any) => {
// console.log('mqtt接收到消息', JSON.parse(JSON.stringify(JSON.parse(new TextDecoder().decode(message)))))
let str = JSON.parse(JSON.stringify(JSON.parse(new TextDecoder().decode(message))))
mqttMessage.value = parseStringToObject(str)
if (adminInfo.id != mqttMessage.value.userId) return
// console.log("🚀 ~ str.match(regex3)[1]:", str.match(regex3)[1])
status.value = parseInt(Number((mqttMessage.value.nowStep / mqttMessage.value.allStep) * 100))
fileRef.value.setStatus(mqttMessage.value)
fileName.value = mqttMessage.value.fileName
localStorage.setItem('fileName', fileName.value)
if (status.value == 100) {
status.value = 99
}
})
mqttRef.value.on('error', (error: any) => {
console.log('mqtt连接失败...', error)
mqttRef.value.end()
})
mqttRef.value.on('close', function () {
console.log('mqtt客户端已断开连接.....')
})
onMounted(() => {
status.value = 0
fileName.value = localStorage.getItem('fileName') ? localStorage.getItem('fileName') : ''
changeType.value = localStorage.getItem('changeType') ? localStorage.getItem('changeType') : ''
})
onBeforeUnmount(() => {
if (mqttRef.value) {
mqttRef.value.end()
}
})
</script>
<style lang="scss" scoped>
.main {
display: flex;
justify-content: space-between;
padding-bottom: 10px;
.main_left {
// width: 280px;
}
.main_right {
overflow: hidden;
flex: 1;
padding: 10px 10px 10px 10px;
//margin-left: 10px;
border: 1px solid #eee;
.el-input__wrapper {
-webkit-text-security: disc !important;
}
.right_nav {
width: 100%;
height: 32px;
display: flex;
justify-content: space-between;
align-items: center;
.menu {
width: 100%;
height: 100%;
display: flex;
justify-content: flex-start;
overflow-x: auto;
align-items: center;
background-color: var(--el-color-primary);
border-radius: 4px;
span {
font-size: 14px;
font-weight: 800;
padding: 0 5px;
color: #fff;
cursor: pointer;
}
}
.el-button {
margin: 0 10px;
}
}
.filter {
width: 100%;
height: 30px;
display: flex;
margin-top: 10px;
justify-content: flex-start;
.el-button {
margin-left: 10px;
}
.upload_progress {
flex: 1;
height: 30px;
display: flex;
align-items: center;
justify-content: flex-start;
margin-left: 10px;
.el-progress {
width: 300px;
margin-left: 10px;
}
}
}
.list {
// display: flex;
// flex-wrap: wrap;
// align-items: flex-start;
// justify-content: space-between;
overflow-y: auto;
margin-top: 10px;
max-height: 100%;
padding-bottom: 200px;
z-index: 100;
position: relative;
.list_item {
flex: none;
width: 23.3%;
height: 100px;
border: 1px solid var(--el-color-primary);
margin: 10px 0.5%;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
// border-radius: 6px;
cursor: pointer;
position: relative;
z-index: 1001 !important;
width: 100%;
height: 40px;
margin: 0;
border: 1px solid #eee;
.item_download,
.item_upload {
position: absolute;
top: 10px;
right: 10px;
z-index: 2001;
}
.img_file {
// width: 60px;
// height: 60px;
width: 30px;
height: 30px;
}
img {
// width: 50px;
// height: 50px;
width: 30px;
height: 30px;
cursor: pointer !important;
}
p {
margin-top: 10px;
}
}
.list_item:nth-child(4n + 2),
.list_item:nth-child(4n + 3) {
// margin: 10px 0.8%;
}
}
}
}
.el-form {
padding: 20px 10px;
box-sizing: border-box;
}
:deep(.el-breadcrumb__separator) {
margin: 0px -10px 0px 0px;
}
</style>
<style lang="scss">
.customInput {
.el-input__inner {
-webkit-text-security: disc !important;
}
}
</style>