|
|
|
|
<template>
|
|
|
|
|
<div class="step-device-category h-full flex flex-col">
|
|
|
|
|
<div class="flex-1 overflow-y-auto">
|
|
|
|
|
<el-tabs
|
|
|
|
|
v-if="Object.keys(modelValue).length > 0"
|
|
|
|
|
v-model="activeTab"
|
|
|
|
|
type="card"
|
|
|
|
|
class="h-full flex flex-col"
|
|
|
|
|
>
|
|
|
|
|
<el-tab-pane
|
|
|
|
|
v-for="(categoriesMap, channelType) in modelValue"
|
|
|
|
|
:key="channelType"
|
|
|
|
|
:label="
|
|
|
|
|
Object.entries(ChannelEnum).find(([key, val]) => val === channelType)?.[1]
|
|
|
|
|
"
|
|
|
|
|
:name="channelType"
|
|
|
|
|
>
|
|
|
|
|
<el-scrollbar>
|
|
|
|
|
<div class="grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-4 p-4">
|
|
|
|
|
<div
|
|
|
|
|
class="bg-white rounded-xl shadow-sm p-12 border-2 border-dashed border-gray-200 hover:border-blue-400 transition-all duration-300 cursor-pointer flex items-center justify-center min-h-[200px]"
|
|
|
|
|
@click="handleAdd"
|
|
|
|
|
>
|
|
|
|
|
<div class="flex flex-col items-center gap-3">
|
|
|
|
|
<el-icon :size="48" class="text-gray-400">
|
|
|
|
|
<Plus />
|
|
|
|
|
</el-icon>
|
|
|
|
|
<span class="text-base font-medium text-gray-600">新增设备类别</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
v-for="{ fileName, name: categoryName, status } in categoriesMap"
|
|
|
|
|
:key="fileName"
|
|
|
|
|
class="bg-white rounded-xl shadow-sm p-12 border border-gray-100 hover:shadow-lg transition-all duration-300"
|
|
|
|
|
>
|
|
|
|
|
<div class="flex justify-between items-start mb-3">
|
|
|
|
|
<div class="flex-1 min-w-0 flex items-center justify-between">
|
|
|
|
|
<h3
|
|
|
|
|
class="font-bold text-lg text-gray-900 truncate"
|
|
|
|
|
:title="categoryName as string"
|
|
|
|
|
>
|
|
|
|
|
类别名称:{{ categoryName }}
|
|
|
|
|
</h3>
|
|
|
|
|
<el-tag
|
|
|
|
|
v-if="!!status"
|
|
|
|
|
size="small"
|
|
|
|
|
type="success"
|
|
|
|
|
effect="light"
|
|
|
|
|
class="rounded-md"
|
|
|
|
|
>
|
|
|
|
|
已上传
|
|
|
|
|
</el-tag>
|
|
|
|
|
<el-tag
|
|
|
|
|
v-else
|
|
|
|
|
size="small"
|
|
|
|
|
type="info"
|
|
|
|
|
effect="light"
|
|
|
|
|
class="rounded-md"
|
|
|
|
|
>
|
|
|
|
|
未上传
|
|
|
|
|
</el-tag>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div class="space-y-2 mb-3">
|
|
|
|
|
<div class="text-sm">
|
|
|
|
|
<div class="flex items-center justify-between">
|
|
|
|
|
<span class="text-gray-500 whitespace-nowrap">点表文件:</span>
|
|
|
|
|
<el-tooltip
|
|
|
|
|
effect="dark"
|
|
|
|
|
:content="fileName"
|
|
|
|
|
placement="top"
|
|
|
|
|
:disabled="!fileName"
|
|
|
|
|
>
|
|
|
|
|
<span class="text-gray-900 font-medium truncate max-w-[170px]">
|
|
|
|
|
{{ fileName || '-' }}
|
|
|
|
|
</span>
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
<div
|
|
|
|
|
class="ml-2"
|
|
|
|
|
v-if="!!status"
|
|
|
|
|
@click.stop="handleDownload(categoryName as string)"
|
|
|
|
|
>
|
|
|
|
|
<el-button type="primary" :icon="Download" link>下载</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="upload-area relative">
|
|
|
|
|
<div
|
|
|
|
|
v-if="uploadingStates[fileName]"
|
|
|
|
|
class="absolute inset-0 z-10 bg-white/90 flex items-center justify-center rounded-lg"
|
|
|
|
|
>
|
|
|
|
|
<el-button
|
|
|
|
|
type="danger"
|
|
|
|
|
size="small"
|
|
|
|
|
@click="handleCancelUpload(fileName as string)"
|
|
|
|
|
>
|
|
|
|
|
取消上传
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<el-upload
|
|
|
|
|
:ref="(el) => setUploadRef(el, fileName as string)"
|
|
|
|
|
class="upload-demo"
|
|
|
|
|
drag
|
|
|
|
|
:auto-upload="false"
|
|
|
|
|
:limit="1"
|
|
|
|
|
:show-file-list="false"
|
|
|
|
|
:disabled="uploadingStates[fileName]"
|
|
|
|
|
:on-change="
|
|
|
|
|
(file: any) => handleFileChange(file, fileName as string, categoryName)
|
|
|
|
|
"
|
|
|
|
|
>
|
|
|
|
|
<el-icon class="el-icon--upload !text-3xl !mb-2 !text-gray-400"
|
|
|
|
|
><upload-filled
|
|
|
|
|
/></el-icon>
|
|
|
|
|
<div class="el-upload__text text-xs text-gray-500">
|
|
|
|
|
拖拽文件或<em class="text-blue-500 not-italic cursor-pointer"
|
|
|
|
|
>点击/{{ !!status ? '重新' : '' }}上传</em
|
|
|
|
|
>
|
|
|
|
|
</div>
|
|
|
|
|
</el-upload>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-scrollbar>
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
</el-tabs>
|
|
|
|
|
<div v-if="Object.keys(modelValue).length === 0">
|
|
|
|
|
<div
|
|
|
|
|
class="bg-white rounded-xl w-340 shadow-sm p-12 border-2 border-dashed border-gray-200 hover:border-blue-400 transition-all duration-300 cursor-pointer flex items-center justify-center min-h-[200px]"
|
|
|
|
|
@click="handleAdd"
|
|
|
|
|
>
|
|
|
|
|
<div class="flex flex-col items-center gap-3">
|
|
|
|
|
<el-icon :size="48" class="text-gray-400">
|
|
|
|
|
<Plus />
|
|
|
|
|
</el-icon>
|
|
|
|
|
<span class="text-base font-medium text-gray-600">新增设备类别</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<el-empty description="暂无设备类别,请点击上方按钮添加" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<el-dialog
|
|
|
|
|
v-model="dialogVisible"
|
|
|
|
|
:title="'新增设备类别'"
|
|
|
|
|
width="500px"
|
|
|
|
|
append-to-body
|
|
|
|
|
destroy-on-close
|
|
|
|
|
>
|
|
|
|
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
|
|
|
|
<el-form-item label="类别名称:" prop="pointName">
|
|
|
|
|
<el-input v-model="form.pointName" placeholder="请输入类别名称" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
|
|
|
|
<el-form-item label="通道类型:" prop="groupName">
|
|
|
|
|
<el-select v-model="form.groupName" placeholder="请选择通道类型">
|
|
|
|
|
<el-option
|
|
|
|
|
v-for="[key, val] in Object.entries(ChannelEnum)"
|
|
|
|
|
:key="key"
|
|
|
|
|
:label="val"
|
|
|
|
|
:value="val"
|
|
|
|
|
/>
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
<template #footer>
|
|
|
|
|
<span class="dialog-footer">
|
|
|
|
|
<el-button @click="dialogVisible = false">取消</el-button>
|
|
|
|
|
<el-button type="primary" @click="handleSave">确定</el-button>
|
|
|
|
|
</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ref, reactive, computed, watch } from 'vue'
|
|
|
|
|
import { useRoute } from 'vue-router'
|
|
|
|
|
import { UploadFilled, Plus, Download } from '@element-plus/icons-vue'
|
|
|
|
|
import type { IDeviceCategoryList, IDeviceCategoryOV } from '@/api/module/device/index.d'
|
|
|
|
|
import type { FormInstance, FormRules, UploadFile } from 'element-plus'
|
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
|
import {
|
|
|
|
|
createDeviceType,
|
|
|
|
|
uploadDeviceTypeFile,
|
|
|
|
|
downloadDeviceTypeFile,
|
|
|
|
|
} from '@/api/module/device/category'
|
|
|
|
|
import { ChannelEnum } from '@/api/module/channel/index'
|
|
|
|
|
import type { IChannelOV } from '@/api/module/channel/index.d'
|
|
|
|
|
import { validateName } from '@/utils/validate'
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
modelValue: IDeviceCategoryList
|
|
|
|
|
channels: IChannelOV
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
(e: 'update:modelValue', value: IDeviceCategoryList): void
|
|
|
|
|
(e: 'on-load-device-categoty'): void
|
|
|
|
|
}>()
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
const projectName = computed(() => route.query.name as string)
|
|
|
|
|
|
|
|
|
|
const dialogVisible = ref(false)
|
|
|
|
|
const formRef = ref<FormInstance>()
|
|
|
|
|
|
|
|
|
|
const uploadControllers = reactive<{ [key: string]: AbortController }>({})
|
|
|
|
|
const uploadingStates = reactive<{ [key: string]: boolean }>({})
|
|
|
|
|
|
|
|
|
|
const activeTab = ref<string>('modbus')
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => props.modelValue,
|
|
|
|
|
val => {
|
|
|
|
|
if (
|
|
|
|
|
val &&
|
|
|
|
|
Object.keys(val).length > 0 &&
|
|
|
|
|
!Object.keys(val).includes(activeTab.value)
|
|
|
|
|
) {
|
|
|
|
|
activeTab.value = Object.keys(val)[0]
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{ immediate: true, deep: true },
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const form = reactive<IDeviceCategoryOV>({
|
|
|
|
|
pointName: '',
|
|
|
|
|
fileName: '',
|
|
|
|
|
groupName: '',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const rules = reactive<FormRules>({
|
|
|
|
|
pointName: [
|
|
|
|
|
{ required: true, message: '请输入类别名称', trigger: 'blur' },
|
|
|
|
|
{
|
|
|
|
|
validator: (rule: any, value: any, callback: any) => {
|
|
|
|
|
const error = validateName(value)
|
|
|
|
|
if (error) {
|
|
|
|
|
callback(new Error(error))
|
|
|
|
|
} else {
|
|
|
|
|
callback()
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
trigger: 'blur',
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
groupName: [{ required: true, message: '请输入通道类型', trigger: 'blur' }],
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const handleAdd = () => {
|
|
|
|
|
form.pointName = ''
|
|
|
|
|
form.fileName = ''
|
|
|
|
|
form.groupName = ''
|
|
|
|
|
dialogVisible.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleSave = async () => {
|
|
|
|
|
if (!formRef.value) return
|
|
|
|
|
await formRef.value.validate(async valid => {
|
|
|
|
|
if (valid) {
|
|
|
|
|
await addDeviceType()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function addDeviceType() {
|
|
|
|
|
const res = await createDeviceType({
|
|
|
|
|
pointName: form.pointName,
|
|
|
|
|
projectName: projectName.value,
|
|
|
|
|
groupName: form.groupName,
|
|
|
|
|
})
|
|
|
|
|
if (res.code !== 0) {
|
|
|
|
|
ElMessage.error(res?.msg || '添加失败')
|
|
|
|
|
dialogVisible.value = false
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
emit('on-load-device-categoty')
|
|
|
|
|
dialogVisible.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const uploadRefs = ref<Record<string, any>>({})
|
|
|
|
|
const setUploadRef = (el: any, key: string) => {
|
|
|
|
|
if (el) {
|
|
|
|
|
uploadRefs.value[key] = el
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleFileChange = async (
|
|
|
|
|
file: UploadFile,
|
|
|
|
|
fileName: string,
|
|
|
|
|
categoryName: string,
|
|
|
|
|
) => {
|
|
|
|
|
if (!file.raw) return
|
|
|
|
|
const controller = new AbortController()
|
|
|
|
|
uploadControllers[fileName] = controller
|
|
|
|
|
uploadingStates[fileName] = true
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await uploadDeviceTypeFile(
|
|
|
|
|
{
|
|
|
|
|
file: file.raw,
|
|
|
|
|
projectName: projectName.value,
|
|
|
|
|
fileName: fileName,
|
|
|
|
|
pointName: categoryName,
|
|
|
|
|
},
|
|
|
|
|
controller,
|
|
|
|
|
)
|
|
|
|
|
if (res) {
|
|
|
|
|
if (res.code === 0) {
|
|
|
|
|
ElMessage.success('上传成功')
|
|
|
|
|
emit('on-load-device-categoty')
|
|
|
|
|
} else if (res.code === 10001) {
|
|
|
|
|
ElMessage.success('取消成功')
|
|
|
|
|
} else {
|
|
|
|
|
ElMessage.error(res.msg || '上传失败')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
delete uploadControllers[fileName]
|
|
|
|
|
uploadingStates[fileName] = false
|
|
|
|
|
uploadRefs.value[fileName]?.clearFiles()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleCancelUpload = (fileName: string) => {
|
|
|
|
|
const controller = uploadControllers[fileName]
|
|
|
|
|
if (controller) {
|
|
|
|
|
controller.abort()
|
|
|
|
|
delete uploadControllers[fileName]
|
|
|
|
|
uploadingStates[fileName] = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if there are any uploading files
|
|
|
|
|
const hasUploadingFiles = () => {
|
|
|
|
|
return Object.values(uploadingStates).some(state => state === true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cancel all uploads
|
|
|
|
|
const cancelAllUploads = () => {
|
|
|
|
|
Object.keys(uploadControllers).forEach(fileName => {
|
|
|
|
|
handleCancelUpload(fileName)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleDownload(pointName: string) {
|
|
|
|
|
try {
|
|
|
|
|
const res = await downloadDeviceTypeFile({
|
|
|
|
|
projectName: projectName.value,
|
|
|
|
|
pointName,
|
|
|
|
|
})
|
|
|
|
|
if (res) {
|
|
|
|
|
const blob = res.data
|
|
|
|
|
const link = document.createElement('a')
|
|
|
|
|
link.style.display = 'none'
|
|
|
|
|
link.href = URL.createObjectURL(blob)
|
|
|
|
|
link.download = `${pointName}.xlsx`
|
|
|
|
|
document.body.appendChild(link)
|
|
|
|
|
link.click()
|
|
|
|
|
document.body.removeChild(link)
|
|
|
|
|
URL.revokeObjectURL(link.href)
|
|
|
|
|
} else {
|
|
|
|
|
ElMessage.error('下载失败')
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
ElMessage.error('下载出现错误')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check before leaving step
|
|
|
|
|
const checkBeforeLeave = async (): Promise<boolean> => {
|
|
|
|
|
if (!hasUploadingFiles()) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new Promise(resolve => {
|
|
|
|
|
ElMessageBox.confirm(
|
|
|
|
|
'当前有文件正在上传,离开将取消所有上传任务,是否确定离开?',
|
|
|
|
|
'提示',
|
|
|
|
|
{
|
|
|
|
|
confirmButtonText: '确定离开',
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
type: 'warning',
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.then(() => {
|
|
|
|
|
cancelAllUploads()
|
|
|
|
|
resolve(true)
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {
|
|
|
|
|
resolve(false)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defineExpose({
|
|
|
|
|
checkBeforeLeave,
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
:deep(.el-tabs__content) {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
height: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.el-tab-pane) {
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.el-scrollbar__view) {
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.upload-demo {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.el-upload-dragger) {
|
|
|
|
|
padding: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.el-icon--upload) {
|
|
|
|
|
font-size: 40px;
|
|
|
|
|
color: #409eff;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.el-upload__text) {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
}
|
|
|
|
|
</style>
|