You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
449 lines
14 KiB
449 lines
14 KiB
<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-button @click.stop="handleOpenPointDrawer(fileName)"> |
|
点位详情 |
|
</el-button> |
|
<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> |
|
<CategoryPointDrawer ref="categoryPointDrawerRef" /> |
|
</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' |
|
import CategoryPointDrawer from '../../components/category-point-drawer.vue' |
|
|
|
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 categoryPointDrawerRef = ref<InstanceType<typeof CategoryPointDrawer>>() |
|
|
|
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('下载出现错误') |
|
} |
|
} |
|
|
|
const handleOpenPointDrawer = (fileName: string) => { |
|
categoryPointDrawerRef.value?.open(projectName.value, fileName) |
|
} |
|
|
|
// 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>
|
|
|