35 changed files with 7004 additions and 9844 deletions
@ -1,3 +1,3 @@ |
|||||||
VITE_BASE_API = '/remoteServer' |
VITE_BASE_API = '/remoteServer/admin-api/' |
||||||
VITE_SHOW_ONLINE_DEVICE = true |
VITE_SHOW_ONLINE_DEVICE = true |
||||||
|
VITE_BASE_URL = 'http://43.140.245.32:48081' |
||||||
|
|||||||
@ -1,17 +1,29 @@ |
|||||||
import { globalServer } from '../index' |
import { globalServer } from '../index' |
||||||
import type { IDeviceCategory } from './index.d' |
import type { IDeviceCategoryOV, IDeviceBase, IDeviceCategoryFileOV, IDeviceCategoryList } from './index.d' |
||||||
|
|
||||||
export const getDeviceTypeList = () => { |
export const getDeviceTypeList = (params: IDeviceBase) => { |
||||||
return globalServer<IDeviceCategory[]>({ |
return globalServer<IDeviceCategoryList>({ |
||||||
url: '/device/type/list', |
url: '/project/point/list-all-with-status', |
||||||
method: 'get', |
method: 'get', |
||||||
|
params |
||||||
}) |
}) |
||||||
} |
} |
||||||
|
|
||||||
export const createDeviceType = (params: IDeviceCategory) => { |
export const createDeviceType = (params: IDeviceCategoryOV & IDeviceBase) => { |
||||||
return globalServer({ |
return globalServer({ |
||||||
url: '/device/type/create', |
url: '/project/point/generate-file', |
||||||
method: 'post', |
method: 'post', |
||||||
data: params, |
data: params, |
||||||
}) |
}) |
||||||
|
} |
||||||
|
|
||||||
|
export const uploadDeviceTypeFile = (params: { file: File, projectName: string, fileName: string }, abort: AbortController) => { |
||||||
|
return globalServer({ |
||||||
|
url: '/project/point/import-file', |
||||||
|
method: 'post', |
||||||
|
data: params, |
||||||
|
signal: abort.signal, |
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }, |
||||||
|
timeout: 0, |
||||||
|
}) |
||||||
} |
} |
||||||
@ -1,14 +1,23 @@ |
|||||||
import { globalServer } from '../index' |
import { globalServer } from '../index' |
||||||
|
|
||||||
import { getEngineeringListMock, createEngineeringMock } from '@/api/modkJs/engineering' |
import type { IEngineeringOV } from './index.d' |
||||||
import type { IEngineeringOV, IEngineering } from './index.d' |
|
||||||
|
|
||||||
export const getEngineeringList = () => { |
export const createEngineering = (params: IEngineeringOV) => { |
||||||
// Use mock data
|
return globalServer({ |
||||||
return getEngineeringListMock() |
url: '/project/generate', |
||||||
|
method: 'POST', |
||||||
|
data: params, |
||||||
|
}) |
||||||
} |
} |
||||||
|
|
||||||
export const createEngineering = (params: IEngineeringOV) => { |
export const getEngineeringList = ( |
||||||
// Use mock data
|
params?: Pick<IEngineeringOV, 'name' | 'isCreate'>, |
||||||
return createEngineeringMock(params) |
) => { |
||||||
|
return globalServer<{ |
||||||
|
list: IEngineeringOV[] |
||||||
|
}>({ |
||||||
|
url: '/project/page', |
||||||
|
method: 'get', |
||||||
|
params, |
||||||
|
}) |
||||||
} |
} |
||||||
|
|||||||
@ -0,0 +1,177 @@ |
|||||||
|
<template> |
||||||
|
<div class="edfs-number-input"> |
||||||
|
<div class="label" v-if="labelName"> |
||||||
|
<span v-if="props.require" class="require">*</span> |
||||||
|
{{ labelName }} |
||||||
|
</div> |
||||||
|
<el-tooltip |
||||||
|
popper-class="number-tips" |
||||||
|
:content="tipText" |
||||||
|
placement="top" |
||||||
|
:visible="isShowTip" |
||||||
|
:disabled="!isUseTip" |
||||||
|
> |
||||||
|
<el-input |
||||||
|
class="number-input" |
||||||
|
v-model="inputValue" |
||||||
|
type="text" |
||||||
|
@input="handleInput" |
||||||
|
@wheel="onWheel" |
||||||
|
:placeholder="placeholder" |
||||||
|
@focus="onFocus" |
||||||
|
@blur="onBlur" |
||||||
|
/> |
||||||
|
</el-tooltip> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script setup lang="ts"> |
||||||
|
import { ref, watch } from 'vue' |
||||||
|
|
||||||
|
interface Props { |
||||||
|
tipText?: string |
||||||
|
isUseTip?: boolean |
||||||
|
labelName?: string |
||||||
|
require?: boolean |
||||||
|
placeholder?: string |
||||||
|
modelValue: number | string |
||||||
|
numMax?: number | undefined |
||||||
|
numMin?: number | undefined |
||||||
|
useWheel?: boolean |
||||||
|
isShowLabel?: boolean |
||||||
|
} |
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), { |
||||||
|
labelName: '', |
||||||
|
modelValue: '', |
||||||
|
require: false, |
||||||
|
placeholder: '请输入', |
||||||
|
numMax: undefined, |
||||||
|
numMin: undefined, |
||||||
|
isUseTip: false, |
||||||
|
tipText: '', |
||||||
|
useWheel: false, |
||||||
|
}) |
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'change']) |
||||||
|
const inputValue = ref(props.modelValue.toString()) |
||||||
|
|
||||||
|
watch( |
||||||
|
() => props.modelValue, |
||||||
|
val => { |
||||||
|
inputValue.value = val !== undefined ? val.toString() : '' |
||||||
|
}, |
||||||
|
) |
||||||
|
|
||||||
|
const handleInput = (val: string) => { |
||||||
|
let newValue = val.replace(/[^0-9.-]/g, '').replace(/(\..*)\./g, '$1') |
||||||
|
|
||||||
|
if (newValue.indexOf('-') > 0) { |
||||||
|
newValue = newValue.replace(/-/g, '') |
||||||
|
} |
||||||
|
|
||||||
|
isShowTip.value && (isShowTip.value = false) |
||||||
|
if (newValue.length === 0) isShowTip.value = true |
||||||
|
|
||||||
|
inputValue.value = newValue |
||||||
|
} |
||||||
|
|
||||||
|
const onWheel = (event: WheelEvent) => { |
||||||
|
// event.preventDefault() // el-input root div might not support preventDefault on wheel the same way for input value, but let's see. |
||||||
|
// Actually, preventing default on the wrapper div prevents scrolling the page, which is desired if we are changing value. |
||||||
|
// But we need to check if we are focused or hovering. |
||||||
|
if (!props.useWheel) return |
||||||
|
event.preventDefault() |
||||||
|
let currentValue = Number(inputValue.value) || 0 |
||||||
|
if (event.deltaY < 0) { |
||||||
|
currentValue++ |
||||||
|
} else { |
||||||
|
currentValue-- |
||||||
|
} |
||||||
|
inputValue.value = currentValue.toString() |
||||||
|
// Trigger update |
||||||
|
emit('update:modelValue', currentValue) |
||||||
|
} |
||||||
|
|
||||||
|
watch(inputValue, newValue => { |
||||||
|
if (!newValue) { |
||||||
|
emit('update:modelValue', undefined) |
||||||
|
return |
||||||
|
} |
||||||
|
let numericValue = Number(newValue) |
||||||
|
emit('update:modelValue', numericValue) |
||||||
|
}) |
||||||
|
|
||||||
|
const isShowTip = ref(false) |
||||||
|
|
||||||
|
function onBlur() { |
||||||
|
onChangeTip(false) |
||||||
|
} |
||||||
|
|
||||||
|
function onFocus() { |
||||||
|
onChangeTip(true) |
||||||
|
} |
||||||
|
|
||||||
|
function onChangeTip(visible: boolean) { |
||||||
|
isShowTip.value = visible |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss"> |
||||||
|
.number-tips { |
||||||
|
font-size: 12px; |
||||||
|
padding: 5px 11px; |
||||||
|
} |
||||||
|
</style> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.edfs-number-input { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
box-sizing: border-box; |
||||||
|
|
||||||
|
.label { |
||||||
|
color: #4d4d4d; |
||||||
|
white-space: nowrap; |
||||||
|
text-align: right; |
||||||
|
width: 110px; |
||||||
|
|
||||||
|
.require { |
||||||
|
color: red; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.number-input { |
||||||
|
flex: 1; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
// padding: 6px 12px; // el-input handles padding |
||||||
|
// font-size: 12px; |
||||||
|
box-sizing: border-box; |
||||||
|
// border: 1px solid #899ed0; // el-input handles border |
||||||
|
// border-radius: 3px; |
||||||
|
// outline: none; |
||||||
|
// transition: all 0.3s; |
||||||
|
// box-shadow: 0 0 0 0.009rem var(--el-border-color) inset; |
||||||
|
|
||||||
|
// &:focus { |
||||||
|
// border-color: #409eff; |
||||||
|
// } |
||||||
|
|
||||||
|
// &:hover { |
||||||
|
// border-color: #c0c4cc; |
||||||
|
// } |
||||||
|
|
||||||
|
// &::placeholder { |
||||||
|
// font-size: 14px; |
||||||
|
// color: #a8abb2; |
||||||
|
// } |
||||||
|
|
||||||
|
:deep(.el-input__inner) { |
||||||
|
text-align: left; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,158 @@ |
|||||||
|
<template> |
||||||
|
<div class="additem-item-input-root"> |
||||||
|
<div class="label" :class="labelWidth" v-if="label"> |
||||||
|
<span v-if="props.require" class="require">*</span> |
||||||
|
{{ label }} |
||||||
|
</div> |
||||||
|
<el-input |
||||||
|
v-model="localValue" |
||||||
|
:disabled="!canEdit" |
||||||
|
:show-password="showPassword" |
||||||
|
class="input" |
||||||
|
:placeholder="placeholder" |
||||||
|
@input="inputChange" |
||||||
|
resize="none" |
||||||
|
:rows="rows" |
||||||
|
@blur="handleBlur" |
||||||
|
:formatter="handleFormatter" |
||||||
|
></el-input> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script setup lang="ts"> |
||||||
|
export interface Props { |
||||||
|
type?: string |
||||||
|
require?: boolean |
||||||
|
label?: string |
||||||
|
modelValue?: string | number |
||||||
|
canEdit?: boolean |
||||||
|
widthType?: number |
||||||
|
stringNum?: boolean //获取string类型的number,开启时,输入框只能输入数字,且value值为string,适用于id类型的编辑 |
||||||
|
placeholder?: string |
||||||
|
showPassword?: boolean |
||||||
|
rows?: number |
||||||
|
} |
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), { |
||||||
|
type: 'text', |
||||||
|
require: false, |
||||||
|
label: '', |
||||||
|
modelValue: '', |
||||||
|
canEdit: true, |
||||||
|
widthType: 0, |
||||||
|
stringNum: false, |
||||||
|
cmd: false, |
||||||
|
placeholder: '', |
||||||
|
showPassword: false, |
||||||
|
rows: 5, |
||||||
|
}) |
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'input']) |
||||||
|
|
||||||
|
const localValue = ref<string | number>('') |
||||||
|
|
||||||
|
const labelWidth = computed(() => { |
||||||
|
switch (props.widthType) { |
||||||
|
case 0: |
||||||
|
return 'width-normal' |
||||||
|
case 1: |
||||||
|
return '' |
||||||
|
default: |
||||||
|
return 'width-normal' |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const inputChange = (val: string | number) => { |
||||||
|
const value = props.type === 'number' ? Number(val) : val |
||||||
|
emit('update:modelValue', value) |
||||||
|
emit('input', value) |
||||||
|
} |
||||||
|
|
||||||
|
const handleBlur = () => { |
||||||
|
if (props.type === 'number') { |
||||||
|
if ( |
||||||
|
String(localValue.value).length === 0 || |
||||||
|
String(localValue.value).match(/^[0]+$/) !== null |
||||||
|
) { |
||||||
|
localValue.value = 0 |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const handleFormatter = (value: string | number) => { |
||||||
|
if (props.stringNum && typeof value === 'string') { |
||||||
|
return value.replace(/([^\d])+/g, '') |
||||||
|
} else if (props.type === 'url' && typeof value === 'string') { |
||||||
|
return value.replace(/([^\d., ])+/g, '') |
||||||
|
} |
||||||
|
return value |
||||||
|
} |
||||||
|
|
||||||
|
watch( |
||||||
|
() => props.modelValue, |
||||||
|
modelValue => { |
||||||
|
localValue.value = modelValue |
||||||
|
}, |
||||||
|
{ immediate: true }, |
||||||
|
) |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.additem-item-input-root { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
box-sizing: border-box; |
||||||
|
// font-size: 0.18rem; |
||||||
|
// padding-right: 42px; |
||||||
|
|
||||||
|
.label { |
||||||
|
// font-size: 14px; |
||||||
|
color: #4d4d4d; |
||||||
|
line-height: 33px; |
||||||
|
text-align: right; |
||||||
|
// margin-right: 24px; |
||||||
|
|
||||||
|
.require { |
||||||
|
color: red; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.input { |
||||||
|
flex-grow: 1; |
||||||
|
width: 0; |
||||||
|
height: 100%; |
||||||
|
line-height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.width-normal { |
||||||
|
width: 110px; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(input::-webkit-outer-spin-button), |
||||||
|
:deep(input::-webkit-inner-spin-button) { |
||||||
|
-webkit-appearance: none; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(input[type='number']) { |
||||||
|
-moz-appearance: textfield; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.el-input) { |
||||||
|
font-size: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.el-input__inner) { |
||||||
|
color: #4d4d4d; |
||||||
|
background-color: transparent; |
||||||
|
height: 100%; |
||||||
|
line-height: 100%; |
||||||
|
// @include border_color("ws_dialog_input"); |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.el-input__inner::-webkit-input-placeholder) { |
||||||
|
// color: #4d4d4d; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
@ -1,175 +0,0 @@ |
|||||||
import type { |
|
||||||
ManualAction, |
|
||||||
PublishMsg, |
|
||||||
PubMsgData, |
|
||||||
SubMsgData, |
|
||||||
TimeoutMsg, |
|
||||||
ZmqMessage |
|
||||||
} from '@/utils/zmq' |
|
||||||
import { WorkerCMD, ZmqCMD, } from '@/utils/zmq' |
|
||||||
import webWorker from '@/utils/zmqJsonWorker?worker' |
|
||||||
|
|
||||||
let defaultHost = env.VITE_ZMQ_BASE_URL |
|
||||||
|
|
||||||
|
|
||||||
const SUBDEFAULTKEY = 'default' |
|
||||||
|
|
||||||
type Handler = (msg: SubMsgData | PubMsgData) => void |
|
||||||
|
|
||||||
class ZMQJsonWorker { |
|
||||||
private static instance: ZMQJsonWorker | null = null; // ➤ 单例实例
|
|
||||||
private worker: Worker; |
|
||||||
private scribeHandlers: Map<string, Map<string, Handler>> = new Map(); |
|
||||||
private pubTimeoutHandlers: Map<string, (msg: TimeoutMsg) => void> = new Map(); |
|
||||||
private readonly host: string; |
|
||||||
private statusCallback: ((status: string) => void) | null = null; |
|
||||||
private isAlwaysListenMsgMap: Map<string, PublishMsg<any>> = new Map(); |
|
||||||
|
|
||||||
private constructor(host: string = defaultHost) { |
|
||||||
this.host = host; |
|
||||||
this.worker = new webWorker(); |
|
||||||
this.worker.onmessage = this.handleMessage.bind(this); |
|
||||||
} |
|
||||||
|
|
||||||
public static getInstance(host: string = defaultHost): ZMQJsonWorker { |
|
||||||
if (!ZMQJsonWorker.instance) { |
|
||||||
ZMQJsonWorker.instance = new ZMQJsonWorker(host); |
|
||||||
} |
|
||||||
return ZMQJsonWorker.instance; |
|
||||||
} |
|
||||||
|
|
||||||
start() { |
|
||||||
this.worker.postMessage({ cmd: WorkerCMD.START, msg: this.host }); |
|
||||||
} |
|
||||||
|
|
||||||
stop() { |
|
||||||
this.worker.postMessage({ cmd: WorkerCMD.STOP }); |
|
||||||
this.worker.terminate(); |
|
||||||
ZMQJsonWorker.instance = null; // ➤ 释放实例,允许重新创建
|
|
||||||
} |
|
||||||
|
|
||||||
subscribe(topic: string, handler: (msg: any) => void, id?: string) { |
|
||||||
const key = id ?? SUBDEFAULTKEY; |
|
||||||
let topicMap = this.scribeHandlers.get(topic); |
|
||||||
if (!topicMap) { |
|
||||||
topicMap = new Map<string, Handler>(); |
|
||||||
this.scribeHandlers.set(topic, topicMap); |
|
||||||
} |
|
||||||
|
|
||||||
// 添加 handler,不会覆盖其他 id 的 handler
|
|
||||||
topicMap.set(key, handler); |
|
||||||
this.worker.postMessage({ cmd: WorkerCMD.SUBSCRIBE, topic }); |
|
||||||
} |
|
||||||
|
|
||||||
unsubscribe(topic: string, id?: string) { |
|
||||||
const topicMap = this.scribeHandlers.get(topic); |
|
||||||
if (!topicMap) return; |
|
||||||
|
|
||||||
if (id) { |
|
||||||
topicMap.delete(id); |
|
||||||
} else { |
|
||||||
topicMap.delete(SUBDEFAULTKEY); |
|
||||||
} |
|
||||||
|
|
||||||
if (topicMap.size === 0) { |
|
||||||
this.scribeHandlers.delete(topic); |
|
||||||
this.worker.postMessage({ cmd: WorkerCMD.UNSUBSCRIBE, topic }); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
publish<T extends ManualAction>(topic: string, msg: PublishMsg<T>, isTimeout: boolean = false, handler?: (msg: TimeoutMsg) => void, isAlwaysListen: boolean = false) { |
|
||||||
if (isTimeout) { |
|
||||||
const timeoutId = msg.id |
|
||||||
if (typeof handler !== 'function') { |
|
||||||
console.warn(`发布主题${topic}失败, 回调函数handler为空`) |
|
||||||
return |
|
||||||
} |
|
||||||
this.pubTimeoutHandlers.set(timeoutId, handler) |
|
||||||
} |
|
||||||
if (isAlwaysListen) { |
|
||||||
this.isAlwaysListenMsgMap.set(`${msg.id}`, msg) |
|
||||||
} |
|
||||||
this.worker.postMessage({ |
|
||||||
cmd: WorkerCMD.PUBLISH, |
|
||||||
topic, |
|
||||||
msg: JSON.stringify(msg), |
|
||||||
isTimeout, |
|
||||||
isAlwaysListen |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
setStatusCallback(callback: (status: string) => void) { |
|
||||||
this.statusCallback = callback; |
|
||||||
} |
|
||||||
|
|
||||||
private handleSubscribeMessage(topic: string, json: PubMsgData & SubMsgData) { |
|
||||||
const topicMap = this.scribeHandlers.get(topic); |
|
||||||
if (!topicMap) return; |
|
||||||
|
|
||||||
topicMap.forEach((handler, id) => { |
|
||||||
try { |
|
||||||
handler(json); |
|
||||||
} catch (error) { |
|
||||||
console.error(`主题: ${topic} 的 handler ${id} 执行失败:`, error); |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
private handleTimeoutMessage(timeoutTopic: string, timeoutId: string) { |
|
||||||
const handler = this.pubTimeoutHandlers.get(timeoutId); |
|
||||||
if (handler) { |
|
||||||
handler({ |
|
||||||
timeoutId, |
|
||||||
timeoutTopic |
|
||||||
}); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// 回收发布后订阅的回调
|
|
||||||
private GC_pubReleaseSub(key: string) { |
|
||||||
this.scribeHandlers.delete(key); |
|
||||||
} |
|
||||||
|
|
||||||
// 回收超时消息的回调
|
|
||||||
private GC_pubReleaseTimeout(key: string) { |
|
||||||
this.pubTimeoutHandlers.delete(key); |
|
||||||
} |
|
||||||
|
|
||||||
private handleMessage(e: MessageEvent<ZmqMessage>) { |
|
||||||
const { cmd, msg, topic, community } = e.data; |
|
||||||
// const now = dayjs().format('YYYY-MM-DD HH:mm:ss')
|
|
||||||
// console.log(now, e.data)
|
|
||||||
if (cmd === ZmqCMD.STATUS) { |
|
||||||
const status = community ? 'disconnected' : 'connected'; |
|
||||||
if (this.statusCallback) { |
|
||||||
this.statusCallback(status); |
|
||||||
} |
|
||||||
} else if (cmd === ZmqCMD.JSON_MSG) { |
|
||||||
const json = JSON.parse(msg) as PubMsgData & SubMsgData; |
|
||||||
|
|
||||||
if (json.action) { |
|
||||||
// 处理订阅消息
|
|
||||||
this.handleSubscribeMessage(topic, json); |
|
||||||
} else { |
|
||||||
// 处理需要发布消息并有返回的订阅消息
|
|
||||||
this.handleSubscribeMessage(`${topic}-${json.id}`, json); |
|
||||||
// 删除发布消息的回调
|
|
||||||
if (Object.keys(json).includes('result') && json.result !== 'progress') { |
|
||||||
if (!this.isAlwaysListenMsgMap.has(`${json.id}`)) { |
|
||||||
this.GC_pubReleaseSub(`${topic}-${json.id}`) |
|
||||||
this.GC_pubReleaseTimeout(`${json.id}`) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
} else if (cmd === ZmqCMD.TIMEOUT) { |
|
||||||
this.handleTimeoutMessage(topic, msg); |
|
||||||
this.GC_pubReleaseTimeout(msg) |
|
||||||
// console.log('pubTimeoutHandlers=>', this.pubTimeoutHandlers)
|
|
||||||
|
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export default ZMQJsonWorker; |
|
||||||
|
|
||||||
@ -1,14 +0,0 @@ |
|||||||
import * as zmq from "jszmq"; |
|
||||||
export default class ZmqClient { |
|
||||||
sock: zmq.Dealer | zmq.Router | zmq.XSub | zmq.XPub | zmq.Pull | zmq.Push | zmq.Pair; |
|
||||||
constructor(type: string); |
|
||||||
zmqReq(host: string, request_type: string, dataStr: string, timeout: number): Promise<string>; |
|
||||||
zmqSub(host: string, callback: (...args: any[]) => void): void; |
|
||||||
zmqPub(host: string): void; |
|
||||||
subscribe(topic: string): void; |
|
||||||
unsubscribe(topic: string): void; |
|
||||||
publishHex(topic: string, dataStr: string): Promise<void>; |
|
||||||
publishStr(topic: string, dataStr: string): Promise<void>; |
|
||||||
close(host: string, callback?: (...args: any[]) => void): void; |
|
||||||
} |
|
||||||
//# sourceMappingURL=zmqClient.d.ts.map
|
|
||||||
@ -1,10 +0,0 @@ |
|||||||
{ |
|
||||||
"version": 3, |
|
||||||
"file": "zmqClient.d.ts", |
|
||||||
"sourceRoot": "", |
|
||||||
"sources": [ |
|
||||||
"../src/zmqClient.ts" |
|
||||||
], |
|
||||||
"names": [], |
|
||||||
"mappings": "AAAA,OAAO,KAAK,GAAG,MAAM,OAAO,CAAC;AAG7B,MAAM,CAAC,OAAO,OAAO,SAAS;IAC1B,IAAI,EACE,GAAG,CAAC,MAAM,GACV,GAAG,CAAC,MAAM,GACV,GAAG,CAAC,IAAI,GACR,GAAG,CAAC,IAAI,GACR,GAAG,CAAC,IAAI,GACR,GAAG,CAAC,IAAI,GACR,GAAG,CAAC,IAAI,CAAC;gBACH,IAAI,EAAE,MAAM;IAgBlB,MAAM,CACR,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC;IA6BlB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI;IAKvD,MAAM,CAAC,IAAI,EAAE,MAAM;IAInB,SAAS,CAAC,KAAK,EAAE,MAAM;IAIvB,WAAW,CAAC,KAAK,EAAE,MAAM;IAInB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;IAOzC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;IAO/C,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI;CAO1D" |
|
||||||
} |
|
||||||
@ -1,118 +0,0 @@ |
|||||||
var __awaiter = |
|
||||||
(this && this.__awaiter) || |
|
||||||
function (thisArg, _arguments, P, generator) { |
|
||||||
function adopt(value) { |
|
||||||
return value instanceof P |
|
||||||
? value |
|
||||||
: new P(function (resolve) { |
|
||||||
resolve(value) |
|
||||||
}) |
|
||||||
} |
|
||||||
return new (P || (P = Promise))(function (resolve, reject) { |
|
||||||
function fulfilled(value) { |
|
||||||
try { |
|
||||||
step(generator.next(value)) |
|
||||||
} catch (e) { |
|
||||||
reject(e) |
|
||||||
} |
|
||||||
} |
|
||||||
function rejected(value) { |
|
||||||
try { |
|
||||||
step(generator['throw'](value)) |
|
||||||
} catch (e) { |
|
||||||
reject(e) |
|
||||||
} |
|
||||||
} |
|
||||||
function step(result) { |
|
||||||
result.done |
|
||||||
? resolve(result.value) |
|
||||||
: adopt(result.value).then(fulfilled, rejected) |
|
||||||
} |
|
||||||
step((generator = generator.apply(thisArg, _arguments || [])).next()) |
|
||||||
}) |
|
||||||
} |
|
||||||
import * as zmq from 'jszmq' |
|
||||||
import { Buffer } from 'buffer' |
|
||||||
export default class ZmqClient { |
|
||||||
constructor(type) { |
|
||||||
switch (type) { |
|
||||||
case 'req': |
|
||||||
this.sock = zmq.socket('req') |
|
||||||
break |
|
||||||
case 'sub': |
|
||||||
this.sock = zmq.socket('sub') |
|
||||||
break |
|
||||||
case 'pub': |
|
||||||
this.sock = zmq.socket('pub') |
|
||||||
break |
|
||||||
default: |
|
||||||
throw new Error('unsupported client type') |
|
||||||
} |
|
||||||
} |
|
||||||
zmqReq(host, request_type, dataStr, timeout) { |
|
||||||
return __awaiter(this, void 0, void 0, function* () { |
|
||||||
this.sock.connect(host) |
|
||||||
var request = new Array() |
|
||||||
request[0] = request_type |
|
||||||
request[1] = dataStr |
|
||||||
this.sock.send(request) |
|
||||||
return Promise.race([ |
|
||||||
new Promise((resolve, reject) => { |
|
||||||
this.sock.on('message', function (message) { |
|
||||||
resolve(message.toString()) |
|
||||||
}) |
|
||||||
}), |
|
||||||
new Promise((resolve, reject) => { |
|
||||||
setTimeout(() => { |
|
||||||
reject('timeout error') |
|
||||||
}, timeout) |
|
||||||
}), |
|
||||||
]) |
|
||||||
// var isMessageArrived = false;
|
|
||||||
// const result: string = "";
|
|
||||||
// var msgReceiveTimer = setTimeout(() => {
|
|
||||||
// if (!isMessageArrived) {
|
|
||||||
// console.log("msg receive timed out");
|
|
||||||
// }
|
|
||||||
// }, timeout); // 设置超时时间为5秒
|
|
||||||
}) |
|
||||||
} |
|
||||||
zmqSub(host, callback) { |
|
||||||
this.sock.connect(host) |
|
||||||
this.sock.on('message', callback) |
|
||||||
} |
|
||||||
zmqPub(host) { |
|
||||||
this.sock.connect(host) |
|
||||||
} |
|
||||||
subscribe(topic) { |
|
||||||
this.sock.subscribe(topic) |
|
||||||
} |
|
||||||
unsubscribe(topic) { |
|
||||||
this.sock.unsubscribe(topic) |
|
||||||
} |
|
||||||
publishHex(topic, dataStr) { |
|
||||||
return __awaiter(this, void 0, void 0, function* () { |
|
||||||
var request = new Array() |
|
||||||
request[0] = topic |
|
||||||
const buf = Buffer.from(dataStr, 'hex') |
|
||||||
request[1] = buf |
|
||||||
this.sock.send(request) |
|
||||||
}) |
|
||||||
} |
|
||||||
publishStr(topic, dataStr) { |
|
||||||
return __awaiter(this, void 0, void 0, function* () { |
|
||||||
var request = new Array() |
|
||||||
request[0] = topic |
|
||||||
request[1] = dataStr |
|
||||||
this.sock.send(request) |
|
||||||
}) |
|
||||||
} |
|
||||||
close(host, callback) { |
|
||||||
if (callback) { |
|
||||||
this.sock.removeListener('message', callback) |
|
||||||
} |
|
||||||
this.sock.disconnect(host) |
|
||||||
this.sock.close() |
|
||||||
} |
|
||||||
} |
|
||||||
//# sourceMappingURL=zmqClient.js.map
|
|
||||||
@ -1,10 +0,0 @@ |
|||||||
{ |
|
||||||
"version": 3, |
|
||||||
"file": "zmqClient.js", |
|
||||||
"sourceRoot": "", |
|
||||||
"sources": [ |
|
||||||
"../src/zmqClient.ts" |
|
||||||
], |
|
||||||
"names": [], |
|
||||||
"mappings": ";;;;;;;;;AAAA,OAAO,KAAK,GAAG,MAAM,OAAO,CAAC;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,MAAM,CAAC,OAAO,OAAO,SAAS;IAS1B,YAAY,IAAY;QACpB,QAAQ,IAAI,EAAE;YACV,KAAK,KAAK;gBACN,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC9B,MAAM;YACV,KAAK,KAAK;gBACN,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC9B,MAAM;YACV,KAAK,KAAK;gBACN,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC9B,MAAM;YACV;gBACI,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;SAClD;IACL,CAAC;IAEK,MAAM,CACR,IAAY,EACZ,YAAoB,EACpB,OAAe,EACf,OAAe;;YAEf,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YACxB,IAAI,OAAO,GAAG,IAAI,KAAK,EAAE,CAAC;YAC1B,OAAO,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC;YAC1B,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC;YACrB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAExB,OAAO,OAAO,CAAC,IAAI,CAAC;gBAChB,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;oBACpC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,UAAU,OAAO;wBACrC,OAAO,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;oBAChC,CAAC,CAAC,CAAC;gBACP,CAAC,CAAC;gBACF,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;oBACpC,UAAU,CAAC,GAAG,EAAE;wBACZ,MAAM,CAAC,eAAe,CAAC,CAAC;oBAC5B,CAAC,EAAE,OAAO,CAAC,CAAC;gBAChB,CAAC,CAAC;aACL,CAAC,CAAC;YAEH,gCAAgC;YAChC,6BAA6B;YAC7B,2CAA2C;YAC3C,+BAA+B;YAC/B,gDAAgD;YAChD,QAAQ;YACR,4BAA4B;QAChC,CAAC;KAAA;IAED,MAAM,CAAC,IAAY,EAAE,QAAkC;QACnD,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IACtC,CAAC;IAED,MAAM,CAAC,IAAY;QACf,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAED,SAAS,CAAC,KAAa;QACnB,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IAED,WAAW,CAAC,KAAa;QACrB,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACjC,CAAC;IAEK,UAAU,CAAC,KAAa,EAAE,OAAe;;YAC3C,IAAI,OAAO,GAAG,IAAI,KAAK,EAAE,CAAC;YAC1B,OAAO,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;YACnB,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YACxC,OAAO,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC;YACjB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC5B,CAAC;KAAA;IACK,UAAU,CAAC,KAAa,EAAE,OAAe;;YAC3C,IAAI,OAAO,GAAG,IAAI,KAAK,EAAE,CAAC;YAC1B,OAAO,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;YACnB,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC;YACrB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC5B,CAAC;KAAA;IAED,KAAK,CAAC,IAAY,EAAE,QAAmC;QACnD,IAAI,QAAQ,EAAE;YACV,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;SACjD;QACD,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC;CACJ" |
|
||||||
} |
|
||||||
@ -0,0 +1,32 @@ |
|||||||
|
import { ref } from 'vue' |
||||||
|
import { defineStore } from 'pinia' |
||||||
|
import { getEngineeringList } from '@/api/module/engineering' |
||||||
|
import type { IEngineeringOV } from '@/api/module/engineering/index.d' |
||||||
|
|
||||||
|
export const useEngineeringStore = defineStore('engineering', () => { |
||||||
|
// State
|
||||||
|
const engineeringList = ref<IEngineeringOV[]>([]) |
||||||
|
const loading = ref(false) |
||||||
|
|
||||||
|
// Actions
|
||||||
|
const fetchEngineeringList = async () => { |
||||||
|
loading.value = true |
||||||
|
try { |
||||||
|
const res = await getEngineeringList() |
||||||
|
engineeringList.value = Array.isArray(res.data?.list) ? res.data.list : [] |
||||||
|
} catch (error) { |
||||||
|
console.error(error) |
||||||
|
engineeringList.value = [] |
||||||
|
} finally { |
||||||
|
loading.value = false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
// State
|
||||||
|
engineeringList, |
||||||
|
loading, |
||||||
|
// Actions
|
||||||
|
fetchEngineeringList, |
||||||
|
} |
||||||
|
}) |
||||||
@ -1,270 +0,0 @@ |
|||||||
import { ref, computed, reactive } from 'vue' |
|
||||||
import { defineStore } from 'pinia' |
|
||||||
import type { IOnlineDevice, IUpFirmwareStatus } from '@/views/stationData/type' |
|
||||||
import ZMQWorker from '@/composables/useZMQJsonWorker' |
|
||||||
import { getSubTopic, type SubMsgData } from '@/utils/zmq' |
|
||||||
import { getDeviceTopic } from '@/views/stationData/utils' |
|
||||||
|
|
||||||
export interface SiteInfo { |
|
||||||
id: string |
|
||||||
onlineCount: number |
|
||||||
offlineCount: number |
|
||||||
devices: Map<string, IOnlineDevice> |
|
||||||
} |
|
||||||
|
|
||||||
export const useTransferDataStore = defineStore('transfer', () => { |
|
||||||
const subDevices = getSubTopic('client', 'status', 'transfer') |
|
||||||
const worker = ZMQWorker.getInstance() |
|
||||||
const isConnected = ref(false) |
|
||||||
|
|
||||||
const connectSite = ref<any>(null) |
|
||||||
|
|
||||||
const checkDeviceStatusInterval = ref<NodeJS.Timeout>() |
|
||||||
|
|
||||||
const siteMap = reactive(new Map<string, SiteInfo>()) |
|
||||||
|
|
||||||
const devicesMap = reactive(new Map<string, IOnlineDevice>()) |
|
||||||
|
|
||||||
// =========== mock =================
|
|
||||||
// let i = 0
|
|
||||||
//
|
|
||||||
// function mockSubDeviceMsg() {
|
|
||||||
// // 随机生成 SN
|
|
||||||
// const sn = `SN-${Math.floor(Math.random() * 1000)}`
|
|
||||||
// // const siteId = `${i % 2 === 0 ? 'site1' : 'site2'}`
|
|
||||||
// const siteId = `siteId-${Math.floor(Math.random() * 1000)}`
|
|
||||||
// const clientIp = `192.168.0.${Math.floor(Math.random() * 255)}`
|
|
||||||
// const version = `v${(Math.random() * 2 + 1).toFixed(2)}`
|
|
||||||
// const footprint = (Math.random() * 50000).toFixed(2) // KB
|
|
||||||
//
|
|
||||||
// // const sn = 123
|
|
||||||
// // const siteId = 123123
|
|
||||||
// // const clientIp = `192.168.0.${Math.floor(Math.random() * 255)}`
|
|
||||||
// // const version = `v${(Math.random() * 2 + 1).toFixed(2)}`
|
|
||||||
// // const footprint = (Math.random() * 50000).toFixed(2) // KB
|
|
||||||
//
|
|
||||||
// const msg: any = {
|
|
||||||
// feedback: [clientIp, sn, siteId, version, footprint],
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// getSubDevicesCb(msg)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// let a = setInterval(() => {
|
|
||||||
// i++
|
|
||||||
// mockSubDeviceMsg()
|
|
||||||
// }, 1000)
|
|
||||||
|
|
||||||
// setTimeout(() => {
|
|
||||||
// clearInterval(a)
|
|
||||||
// }, 8000)
|
|
||||||
|
|
||||||
// =========== mock end =================
|
|
||||||
|
|
||||||
function startCheckStatus() { |
|
||||||
clearInterval(checkDeviceStatusInterval.value) |
|
||||||
checkDeviceStatusInterval.value = setInterval(checkDeviceStatusFn, 1000); |
|
||||||
} |
|
||||||
|
|
||||||
const checkDeviceStatusFn = () => { |
|
||||||
const now = Date.now(); |
|
||||||
devicesMap.forEach((device: IOnlineDevice, sn) => { |
|
||||||
// console.log(device, dayjs(now).format('HH:mm:ss'), dayjs(device.lastUpdated).format('HH:mm:ss'), now - device.lastUpdated)
|
|
||||||
if (now - device.lastUpdated > 5500) { |
|
||||||
device.status = '离线'; |
|
||||||
} |
|
||||||
}); |
|
||||||
reassignDevicesToSites() |
|
||||||
} |
|
||||||
|
|
||||||
function formatSizeFromKB(num: number): string { |
|
||||||
const sizeKB = Number(num) |
|
||||||
const units = ['KB', 'MB', 'GB', 'TB', 'PB'] |
|
||||||
let size = sizeKB |
|
||||||
let unitIndex = 0 |
|
||||||
|
|
||||||
while (size >= 1024 && unitIndex < units.length - 1) { |
|
||||||
size = size / 1024 |
|
||||||
unitIndex++ |
|
||||||
} |
|
||||||
|
|
||||||
return `${size.toFixed(2)} ${units[unitIndex]}` |
|
||||||
} |
|
||||||
|
|
||||||
function getSubDevicesCb(msg: SubMsgData) { |
|
||||||
const { feedback } = msg |
|
||||||
const sn = feedback[1] |
|
||||||
const hasDevice = devicesMap.get(sn) |
|
||||||
if (hasDevice) { |
|
||||||
hasDevice.lastUpdated = Date.now() |
|
||||||
hasDevice.status = '在线' |
|
||||||
hasDevice.footprint = formatSizeFromKB(Number(feedback[4] || 0)) |
|
||||||
hasDevice.clientIp = feedback[0] |
|
||||||
hasDevice.versions = feedback[3] ?? '--' |
|
||||||
} else { |
|
||||||
const num = feedback[4] || 0 |
|
||||||
const device: IOnlineDevice = { |
|
||||||
clientIp: feedback[0], |
|
||||||
sn: sn, |
|
||||||
site_id: feedback[2], |
|
||||||
versions: feedback[3] ?? '--', |
|
||||||
footprint: formatSizeFromKB(Number(num)), |
|
||||||
lastUpdated: Date.now(), |
|
||||||
status: '在线', // 初始状态为在线
|
|
||||||
isChecked: false, |
|
||||||
} |
|
||||||
devicesMap.set(sn, device) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function reassignDevicesToSites() { |
|
||||||
devicesMap.forEach((device) => { |
|
||||||
const siteId = device.site_id |
|
||||||
if (!siteId) return |
|
||||||
|
|
||||||
// 若该站点还未创建,则初始化
|
|
||||||
if (!siteMap.has(siteId)) { |
|
||||||
siteMap.set(siteId, { |
|
||||||
id: siteId, |
|
||||||
onlineCount: 0, |
|
||||||
offlineCount: 0, |
|
||||||
devices: reactive(new Map<string, IOnlineDevice>()), |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// 获取站点信息并更新
|
|
||||||
const siteInfo = siteMap.get(siteId)! |
|
||||||
siteInfo.devices.set(device.sn, device) |
|
||||||
}) |
|
||||||
|
|
||||||
// 更新site 在线数量和离线数量
|
|
||||||
siteMap.forEach((siteInfo) => { |
|
||||||
const devices = Array.from(siteInfo.devices.values()) |
|
||||||
siteInfo.onlineCount = devices.filter(item => item.status === '在线').length |
|
||||||
siteInfo.offlineCount = devices.filter(item => item.status === '离线').length |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
const onlineCount = computed(() => { |
|
||||||
return Array.from(devicesMap.values()).filter(item => item.status === '在线') |
|
||||||
.length |
|
||||||
}) |
|
||||||
|
|
||||||
const offlineCount = computed(() => { |
|
||||||
return Array.from(devicesMap.values()).filter(item => item.status === '离线') |
|
||||||
.length |
|
||||||
}) |
|
||||||
|
|
||||||
onMounted(() => { |
|
||||||
worker.subscribe(getDeviceTopic, getSubDevicesCb) |
|
||||||
startCheckStatus() |
|
||||||
document.addEventListener('visibilitychange', handleVisibilityChange) |
|
||||||
}) |
|
||||||
|
|
||||||
const route = useRoute() |
|
||||||
|
|
||||||
watch(() => route.path, (val) => { |
|
||||||
if (!['/station/data-transfer', '/station'].includes(val)) { |
|
||||||
clearInterval(checkDeviceStatusInterval.value) |
|
||||||
document.removeEventListener('visibilitychange', handleVisibilityChange) |
|
||||||
worker.unsubscribe(subDevices) |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
function handleVisibilityChange() { |
|
||||||
if (document.hidden) { |
|
||||||
clearInterval(checkDeviceStatusInterval.value) |
|
||||||
} else { |
|
||||||
startCheckStatus() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function upFirmwareStatus(sn: string, feedback: any[]) { |
|
||||||
const device = devicesMap.get(sn) |
|
||||||
if (device) { |
|
||||||
device.upFirmware = 'updating' |
|
||||||
const step = feedback[1] |
|
||||||
const progress = feedback[2] || undefined |
|
||||||
const errMsg = feedback[3] || undefined |
|
||||||
if (step < (device.upFirmwareStatus?.step ?? -100)) return |
|
||||||
device.upFirmwareStatus = { |
|
||||||
step, |
|
||||||
progress: progress === -1 ? 100 : progress, |
|
||||||
errMsg, |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function upFirmwareStatusReject(sn: string, feedback: any[]) { |
|
||||||
const device = devicesMap.get(sn) |
|
||||||
if (device) { |
|
||||||
device.upFirmware = 'rejected' |
|
||||||
const step = feedback[1] |
|
||||||
const progress = feedback[2] || undefined |
|
||||||
const errMsg = feedback[3] || undefined |
|
||||||
if (step < (device.upFirmwareStatus?.step ?? -100)) return |
|
||||||
device.upFirmwareStatus = { |
|
||||||
step, |
|
||||||
progress: progress === -1 ? 100 : progress, |
|
||||||
errMsg, |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function upFirmwarePending(snList: string[]) { |
|
||||||
for (const sn of snList) { |
|
||||||
const device = devicesMap.get(sn) |
|
||||||
if (device) { |
|
||||||
device.upFirmware = 'pending' |
|
||||||
device.upFirmwareStatus = { |
|
||||||
step: 0, |
|
||||||
progress: undefined, |
|
||||||
errMsg: undefined, |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function upFirmwareReset(snList: string[]) { |
|
||||||
for (const sn of snList) { |
|
||||||
const device = devicesMap.get(sn) |
|
||||||
if (device) { |
|
||||||
upFirmwareSucceed(sn) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function upFirmwareSucceed(sn: string) { |
|
||||||
const device = devicesMap.get(sn) |
|
||||||
if (device) { |
|
||||||
device.upFirmware = undefined |
|
||||||
device.upFirmwareStatus = undefined |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function upFirmwareTimeout(sn: string) { |
|
||||||
const device = devicesMap.get(sn) |
|
||||||
if (device) { |
|
||||||
device.upFirmware = 'timeout' |
|
||||||
device.upFirmwareStatus = undefined |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
return { |
|
||||||
siteMap, |
|
||||||
isConnected, |
|
||||||
devicesMap, |
|
||||||
connectSite, |
|
||||||
checkDeviceStatusInterval, |
|
||||||
onlineCount, |
|
||||||
offlineCount, |
|
||||||
upFirmwarePending, |
|
||||||
upFirmwareReset, |
|
||||||
upFirmwareStatus, |
|
||||||
upFirmwareSucceed, |
|
||||||
upFirmwareStatusReject, |
|
||||||
upFirmwareTimeout |
|
||||||
} |
|
||||||
}) |
|
||||||
@ -0,0 +1,18 @@ |
|||||||
|
|
||||||
|
export function validateName(name: string): string | null { |
||||||
|
if (!name) return '名称不能为空' |
||||||
|
|
||||||
|
// Check for Chinese and special characters (allow alphanumeric, _, -)
|
||||||
|
const regex = /^[a-zA-Z0-9_-]+$/ |
||||||
|
if (!regex.test(name)) { |
||||||
|
return '名称不能包含中文或特殊符号' |
||||||
|
} |
||||||
|
|
||||||
|
// Check byte length (max 32)
|
||||||
|
const byteLength = new Blob([name]).size |
||||||
|
if (byteLength > 32) { |
||||||
|
return '名称长度不能超过32字节' |
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
} |
||||||
@ -1,174 +0,0 @@ |
|||||||
import { v4 as uuidv4 } from 'uuid'; |
|
||||||
export type ManualAction = |
|
||||||
'init' | 'release' | 'write' | 'report' | 'lock' | 'unlock' | |
|
||||||
'export' | 'cancel' | 'import' | 'upgrade' |
|
||||||
|
|
||||||
export type ZmqStatus = 'disconnected' | 'connected' |
|
||||||
|
|
||||||
export enum WorkerCMD { |
|
||||||
START, |
|
||||||
SUBSCRIBE, |
|
||||||
UNSUBSCRIBE, |
|
||||||
PUBLISH, |
|
||||||
STOP, |
|
||||||
SET_TIMEOUT |
|
||||||
} |
|
||||||
|
|
||||||
export enum ZmqMsgResultType { |
|
||||||
SUCCESS = 200, |
|
||||||
PROGRESS = 1002, |
|
||||||
ERROR = 1003, |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
export enum ZmqCMD { |
|
||||||
MSG, |
|
||||||
JSON_MSG, |
|
||||||
STATUS, |
|
||||||
TIMEOUT |
|
||||||
} |
|
||||||
|
|
||||||
export interface TimeoutMsg { |
|
||||||
timeoutId: string |
|
||||||
timeoutTopic: string |
|
||||||
} |
|
||||||
export interface ZmqMessage { |
|
||||||
cmd: ZmqCMD |
|
||||||
msg: string |
|
||||||
community: boolean |
|
||||||
topic: string |
|
||||||
} |
|
||||||
type TopicType = 'event' | 'status' |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// web端发布消息类型
|
|
||||||
export interface PublishMsg<A> { |
|
||||||
id: string // 每条消息的唯一标识
|
|
||||||
action: A // 消息的动作
|
|
||||||
reply: 'yes' | 'no' // 是否需要回复
|
|
||||||
params: any // 消息的参数
|
|
||||||
} |
|
||||||
// web端发布消息服务端返回的数据类型
|
|
||||||
export interface PubMsgData { |
|
||||||
code: number |
|
||||||
feedback: any, |
|
||||||
id: string |
|
||||||
result: 'success' | 'progress' | 'failure' | 'refuse' | 'error' |
|
||||||
} |
|
||||||
|
|
||||||
// 订阅消息服务端返回的数据类型
|
|
||||||
export interface SubMsgData { |
|
||||||
id: string |
|
||||||
action: 'report' |
|
||||||
feedback: any, |
|
||||||
reply: 'yes' | 'no' // 是否需要回复
|
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* |
|
||||||
* @Description: 获取发布的主题 |
|
||||||
* type: 主题类型 |
|
||||||
* module: 模块名称 |
|
||||||
* userid: 用户唯一标识 |
|
||||||
*/ |
|
||||||
|
|
||||||
export function getPubTopic(type: TopicType, module: string,) { |
|
||||||
return `web/${type}/${module}/` |
|
||||||
} |
|
||||||
|
|
||||||
export function getSubTopic(server: string, type: TopicType, module: string,) { |
|
||||||
return `${server}/${type}/${module}/` |
|
||||||
} |
|
||||||
|
|
||||||
// 获取随机id
|
|
||||||
|
|
||||||
export function getRandomId() { |
|
||||||
const uniqueId = `${Date.now()}-${uuidv4()}`; |
|
||||||
return uniqueId |
|
||||||
} |
|
||||||
|
|
||||||
export function generatePubMsg<A>(massage: Omit<PublishMsg<A>, 'id'>): PublishMsg<A> { |
|
||||||
return { |
|
||||||
id: getRandomId(), |
|
||||||
action: massage.action, |
|
||||||
reply: massage.reply, |
|
||||||
params: massage.params |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export function getLockPubMsg(action: 'lock' | 'unlock'): PublishMsg<'lock' | 'unlock'> { |
|
||||||
return { |
|
||||||
id: getRandomId(), |
|
||||||
action: action, |
|
||||||
params: [], |
|
||||||
reply: 'yes' |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export function pollingWithCallback(intervalInSeconds: number, callback: Function) { |
|
||||||
let counter = 0 |
|
||||||
const fps = 60 |
|
||||||
|
|
||||||
function poll() { |
|
||||||
counter++ |
|
||||||
if (counter >= intervalInSeconds * fps) { |
|
||||||
callback() |
|
||||||
counter = 0 |
|
||||||
} |
|
||||||
requestAnimationFrame(poll) |
|
||||||
} |
|
||||||
|
|
||||||
poll() |
|
||||||
} |
|
||||||
|
|
||||||
export function getPubInitData<T extends ManualAction>(action: T, params: any, reply: 'yes' | 'no' = 'yes') { |
|
||||||
return generatePubMsg<T>({ |
|
||||||
action, |
|
||||||
reply, |
|
||||||
params |
|
||||||
}) |
|
||||||
} |
|
||||||
export function isHexadecimal(text: string) { |
|
||||||
const hexRegex = /^[0-9A-Fa-f]+$/ |
|
||||||
return hexRegex.test(text) |
|
||||||
} |
|
||||||
|
|
||||||
export function stringToHex(str: string) { |
|
||||||
return Array.from(str) |
|
||||||
.map(char => char.charCodeAt(0).toString(16).toUpperCase()) |
|
||||||
.join('') |
|
||||||
} |
|
||||||
|
|
||||||
export function stringToDecimalismNumbers(str: string): number[] { |
|
||||||
return Array.from(str).map(char => char.charCodeAt(0)) |
|
||||||
} |
|
||||||
|
|
||||||
export function hexToArray(num: string) { |
|
||||||
const numStr = num.toString() |
|
||||||
let paddedNumStr = numStr |
|
||||||
if (numStr.length % 2 !== 0) { |
|
||||||
paddedNumStr = numStr.slice(0, -1) + '0' + numStr.slice(-1) |
|
||||||
} |
|
||||||
|
|
||||||
const result = [] |
|
||||||
|
|
||||||
for (let i = 0; i < paddedNumStr.length; i += 2) { |
|
||||||
result.push(paddedNumStr.slice(i, i + 2)) |
|
||||||
} |
|
||||||
|
|
||||||
return result |
|
||||||
} |
|
||||||
|
|
||||||
export function hexToDecimal(hex: string[]) { |
|
||||||
return hex.map(item => parseInt(item, 16)) |
|
||||||
} |
|
||||||
|
|
||||||
export function decimalToHexArray(decimalArray: number[]) { |
|
||||||
return decimalArray.map(num => num.toString(16).toUpperCase()).join('') |
|
||||||
} |
|
||||||
|
|
||||||
export function decimalToString(arr: number[]) { |
|
||||||
return arr.map(num => String.fromCharCode(num)).join('') |
|
||||||
} |
|
||||||
@ -1,166 +0,0 @@ |
|||||||
import ZmqClient from '@/lib/zmq/zmqClient' |
|
||||||
import { type PublishMsg, type PubMsgData, WorkerCMD, ZmqCMD, } from './zmq' |
|
||||||
|
|
||||||
|
|
||||||
const HEARTBEAT_TOPIC = 'HEARTBEAT' |
|
||||||
const HEARTBEAT_INTERVAL = 3000 |
|
||||||
const STATUS_CHECK_INTERVAL = 1000 |
|
||||||
let messageTimeout = 20000 |
|
||||||
|
|
||||||
let heartClient: ZmqClient | null, subClient: ZmqClient | null, pubClient: ZmqClient | null |
|
||||||
let subHost = '', pubHost = '' |
|
||||||
let lastHeartbeatTime = 0 |
|
||||||
let statusTimerId: ReturnType<typeof setInterval> | null |
|
||||||
let isConnectionError = false |
|
||||||
|
|
||||||
function decodeMessage(data: Uint8Array) { |
|
||||||
return new TextDecoder().decode(data) |
|
||||||
} |
|
||||||
|
|
||||||
function updateHeartbeat() { |
|
||||||
lastHeartbeatTime = Date.now() |
|
||||||
} |
|
||||||
|
|
||||||
function changeConnectionStatus(hasError: boolean) { |
|
||||||
if (isConnectionError !== hasError) { |
|
||||||
isConnectionError = hasError |
|
||||||
console.log('ZMQ连接状态更新:', hasError ? '断开' : '连接') |
|
||||||
postMessage({ cmd: ZmqCMD.STATUS, community: hasError }) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function monitorConnection() { |
|
||||||
if (statusTimerId) return |
|
||||||
|
|
||||||
lastHeartbeatTime = Date.now() |
|
||||||
statusTimerId = setInterval(() => { |
|
||||||
const currentTime = Date.now() |
|
||||||
const hasError = currentTime - lastHeartbeatTime > HEARTBEAT_INTERVAL |
|
||||||
changeConnectionStatus(hasError) |
|
||||||
}, STATUS_CHECK_INTERVAL) |
|
||||||
} |
|
||||||
|
|
||||||
function stopMonitoringConnection() { |
|
||||||
if (statusTimerId) { |
|
||||||
clearInterval(statusTimerId) |
|
||||||
statusTimerId = null |
|
||||||
} |
|
||||||
changeConnectionStatus(false) |
|
||||||
} |
|
||||||
|
|
||||||
function disconnect() { |
|
||||||
if (subClient) { |
|
||||||
subClient.close(subHost, handleZmqMessage) |
|
||||||
subClient = null |
|
||||||
} |
|
||||||
|
|
||||||
if (heartClient) { |
|
||||||
heartClient.unsubscribe(HEARTBEAT_TOPIC) |
|
||||||
heartClient.close(subHost, updateHeartbeat) |
|
||||||
heartClient = null |
|
||||||
} |
|
||||||
|
|
||||||
if (pubClient) { |
|
||||||
pubClient.close(pubHost) |
|
||||||
pubClient = null |
|
||||||
} |
|
||||||
|
|
||||||
stopMonitoringConnection() |
|
||||||
} |
|
||||||
|
|
||||||
function handleZmqMessage(topic: Uint8Array, msg: Uint8Array) { |
|
||||||
try { |
|
||||||
if (msg instanceof Uint8Array) { |
|
||||||
const jsonMessage = decodeMessage(msg) as string |
|
||||||
postMessage({ |
|
||||||
topic: decodeMessage(topic), |
|
||||||
cmd: ZmqCMD.JSON_MSG, |
|
||||||
msg: jsonMessage |
|
||||||
}) |
|
||||||
const parsedMessage = JSON.parse(jsonMessage) as PubMsgData |
|
||||||
// traceMessages.get(parsedMessage.id)
|
|
||||||
const curTraceMessages = traceMessages.get(parsedMessage.id) |
|
||||||
if (parsedMessage.id && !!curTraceMessages) { |
|
||||||
if (curTraceMessages.isAlwaysListen || parsedMessage.result === 'progress') { |
|
||||||
// 重置消息超时时间
|
|
||||||
const val = traceMessages.get(parsedMessage.id) |
|
||||||
if (val) { |
|
||||||
val.timestamp = Date.now() |
|
||||||
} |
|
||||||
traceMessages.set(parsedMessage.id, val) |
|
||||||
} else { |
|
||||||
traceMessages.delete(parsedMessage.id) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} catch (e) { |
|
||||||
console.error('handleZmqMessage error:', e) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function connect(host: string) { |
|
||||||
disconnect() |
|
||||||
|
|
||||||
subHost = `ws://${host}:15555` |
|
||||||
subClient = new ZmqClient('sub') |
|
||||||
subClient.zmqSub(subHost, handleZmqMessage) |
|
||||||
|
|
||||||
heartClient = new ZmqClient('sub') |
|
||||||
heartClient.zmqSub(subHost, updateHeartbeat) |
|
||||||
heartClient.subscribe(HEARTBEAT_TOPIC) |
|
||||||
monitorConnection() |
|
||||||
|
|
||||||
pubHost = `ws://${host}:15556` |
|
||||||
pubClient = new ZmqClient('pub') |
|
||||||
pubClient.zmqPub(pubHost) |
|
||||||
setInterval(() => { |
|
||||||
const now = Date.now() |
|
||||||
traceMessages.forEach((val, id) => { |
|
||||||
if (now - val.timestamp > messageTimeout) { |
|
||||||
console.warn(`Message ${id} timed out.`) |
|
||||||
postMessage({ |
|
||||||
cmd: ZmqCMD.TIMEOUT, |
|
||||||
topic: val.topic.replace(/^web\//, 'server/'), |
|
||||||
msg: id |
|
||||||
}) |
|
||||||
traceMessages.delete(id) |
|
||||||
} |
|
||||||
}) |
|
||||||
}, 1000) |
|
||||||
} |
|
||||||
|
|
||||||
const traceMessages = new Map<string, any>() |
|
||||||
|
|
||||||
self.onmessage = function (event) { |
|
||||||
const { cmd, topic, msg, isTimeout = false, isAlwaysListen } = event.data |
|
||||||
|
|
||||||
switch (cmd) { |
|
||||||
case WorkerCMD.START: |
|
||||||
connect(msg) |
|
||||||
break |
|
||||||
case WorkerCMD.SET_TIMEOUT: |
|
||||||
messageTimeout = msg |
|
||||||
break |
|
||||||
case WorkerCMD.SUBSCRIBE: |
|
||||||
subClient?.subscribe(topic) |
|
||||||
break |
|
||||||
case WorkerCMD.UNSUBSCRIBE: |
|
||||||
subClient?.unsubscribe(topic) |
|
||||||
break |
|
||||||
case WorkerCMD.PUBLISH: |
|
||||||
if (!msg) throw new Error('msg is required') |
|
||||||
if (isTimeout) { |
|
||||||
const parseMsg = JSON.parse(msg) as PublishMsg<string> |
|
||||||
traceMessages.set(parseMsg.id, { |
|
||||||
timestamp: Date.now(), |
|
||||||
topic: topic, |
|
||||||
isAlwaysListen, |
|
||||||
}) |
|
||||||
} |
|
||||||
pubClient?.publishStr(topic, msg) |
|
||||||
break |
|
||||||
case WorkerCMD.STOP: |
|
||||||
disconnect() |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
@ -0,0 +1,216 @@ |
|||||||
|
<template> |
||||||
|
<EdfsDialog |
||||||
|
:title="'新增通道'" |
||||||
|
:is-show="visible" |
||||||
|
width="580px" |
||||||
|
@on-close="close" |
||||||
|
@on-save="onSave" |
||||||
|
> |
||||||
|
<div class="flex-col gap-10 w-80% m-x-30px"> |
||||||
|
<el-row> |
||||||
|
<div class="label"><span class="require">*</span>通道名称:</div> |
||||||
|
<el-input class="flex-1" v-model="form.name" placeholder="请输入通道名称" /> |
||||||
|
</el-row> |
||||||
|
<el-row> |
||||||
|
<div class="label">通道类型:</div> |
||||||
|
<el-select v-model="form.channel" placeholder="请选择通道类型" class="flex-1"> |
||||||
|
<el-option |
||||||
|
v-for="[key, val] in Object.entries(ChannelEnum)" |
||||||
|
:key="key" |
||||||
|
:label="val" |
||||||
|
:value="val" |
||||||
|
/> |
||||||
|
</el-select> |
||||||
|
</el-row> |
||||||
|
<template v-for="item in commTypeInputs[form.channel]"> |
||||||
|
<el-row> |
||||||
|
<NumberItemInput |
||||||
|
v-if="item.type === 'input'" |
||||||
|
v-model="form[item.key as keyof IFormData]" |
||||||
|
:labelName="item.label + ':'" |
||||||
|
:numMax="item.max" |
||||||
|
:numMin="item.min" |
||||||
|
:require="item.require" |
||||||
|
:placeholder="`范围${item.min}~${item.max}`" |
||||||
|
class="item-input" |
||||||
|
/> |
||||||
|
<FormItemInput |
||||||
|
v-if="item.type === 'string-input'" |
||||||
|
v-model="form[item.key as keyof IFormData]" |
||||||
|
:require="item.require" |
||||||
|
:label="item.label + ':'" |
||||||
|
class="item-input" |
||||||
|
/> |
||||||
|
</el-row> |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
</EdfsDialog> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script setup lang="ts"> |
||||||
|
import { cloneDeep } from 'lodash-es' |
||||||
|
import { ChannelEnum } from '@/api/module/channel/index' |
||||||
|
import FormItemInput from '@/components/FormItemInput.vue' |
||||||
|
import NumberItemInput from '@/components/Edfs-number-item-input.vue' |
||||||
|
import { useMessage } from '@/composables/useMessage' |
||||||
|
import EdfsDialog from '@/components/Edfs-dialog.vue' |
||||||
|
import type { ICustomFormData, IFormData } from './types' |
||||||
|
import { validateName } from '@/utils/validate' |
||||||
|
const message = useMessage() |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
existingNames?: string[] |
||||||
|
}>() |
||||||
|
|
||||||
|
const emit = defineEmits<{ |
||||||
|
'on-save': [form: ICustomFormData] |
||||||
|
}>() |
||||||
|
|
||||||
|
const baseFormData = { |
||||||
|
name: '', |
||||||
|
channel: ChannelEnum.MODBUS_TCP, |
||||||
|
} |
||||||
|
|
||||||
|
const customFormData: ICustomFormData = { |
||||||
|
...baseFormData, |
||||||
|
ip: '', |
||||||
|
port: 502, |
||||||
|
broker: '', |
||||||
|
} |
||||||
|
|
||||||
|
const MODBUS_TCPInput = [ |
||||||
|
{ |
||||||
|
label: 'IP地址', |
||||||
|
max: 255, |
||||||
|
min: 0, |
||||||
|
type: 'string-input', |
||||||
|
require: true, |
||||||
|
key: 'ip', |
||||||
|
}, |
||||||
|
{ |
||||||
|
label: '端口', |
||||||
|
max: 65535, |
||||||
|
min: 0, |
||||||
|
type: 'input', |
||||||
|
require: true, |
||||||
|
key: 'port', |
||||||
|
}, |
||||||
|
] |
||||||
|
|
||||||
|
const MQTTInput = [ |
||||||
|
{ |
||||||
|
label: 'Broker', |
||||||
|
max: 7, |
||||||
|
min: 0, |
||||||
|
key: 'broker', |
||||||
|
require: true, |
||||||
|
type: 'input', |
||||||
|
}, |
||||||
|
] |
||||||
|
|
||||||
|
const commTypeInputs = { |
||||||
|
[ChannelEnum.MODBUS_TCP]: MODBUS_TCPInput, |
||||||
|
[ChannelEnum.MQTT]: MQTTInput, |
||||||
|
} |
||||||
|
|
||||||
|
const form = ref<ICustomFormData>(cloneDeep(customFormData)) |
||||||
|
const visible = ref(false) |
||||||
|
const originalName = ref<string>('') |
||||||
|
|
||||||
|
function open(item?: ICustomFormData) { |
||||||
|
visible.value = true |
||||||
|
if (item) { |
||||||
|
form.value = cloneDeep(item) |
||||||
|
originalName.value = item.name |
||||||
|
} else { |
||||||
|
form.value = cloneDeep(customFormData) |
||||||
|
originalName.value = '' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function onSave() { |
||||||
|
if (verifyData()) return |
||||||
|
emit('on-save', form.value) |
||||||
|
message.success('保存成功') |
||||||
|
close() |
||||||
|
} |
||||||
|
|
||||||
|
function close() { |
||||||
|
form.value = cloneDeep(customFormData) |
||||||
|
console.log(form.value) |
||||||
|
visible.value = false |
||||||
|
} |
||||||
|
|
||||||
|
const ipPattern = |
||||||
|
/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ |
||||||
|
|
||||||
|
function verifyData() { |
||||||
|
if (!form.value.name) { |
||||||
|
message.warning('请输通道名称') |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
const nameError = validateName(form.value.name) |
||||||
|
if (nameError) { |
||||||
|
message.warning(nameError) |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
if (props.existingNames && props.existingNames.length > 0) { |
||||||
|
const isDuplicate = props.existingNames.some( |
||||||
|
name => name === form.value.name && name !== originalName.value, |
||||||
|
) |
||||||
|
if (isDuplicate) { |
||||||
|
message.warning('名称已存在') |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for (const key of commTypeInputs[form.value.channel]) { |
||||||
|
if (!form.value[key.key as keyof IFormData]) { |
||||||
|
message.warning(`请输入${key.label}`) |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (form.value.channel === ChannelEnum.MODBUS_TCP) { |
||||||
|
if (!ipPattern.test(form.value.ip as string)) { |
||||||
|
message.warning('请输入正确的IP地址') |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
defineExpose({ |
||||||
|
open, |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.el-row { |
||||||
|
column-gap: 8px; |
||||||
|
|
||||||
|
.label { |
||||||
|
color: var(--label-color); |
||||||
|
line-height: 33px; |
||||||
|
text-align: right; |
||||||
|
width: 110px; |
||||||
|
|
||||||
|
.require { |
||||||
|
color: red; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.item-input { |
||||||
|
height: 32px; |
||||||
|
margin-bottom: 0px; |
||||||
|
:deep(.label) { |
||||||
|
margin-right: 10px; |
||||||
|
} |
||||||
|
:deep(.label) { |
||||||
|
margin-right: 10px; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,218 @@ |
|||||||
|
<template> |
||||||
|
<EdfsDialog |
||||||
|
:title="'新增通道'" |
||||||
|
:is-show="visible" |
||||||
|
width="580px" |
||||||
|
@on-close="close" |
||||||
|
@on-save="onSave" |
||||||
|
> |
||||||
|
<div class="flex-col gap-10 w-80% m-x-30px"> |
||||||
|
<el-row> |
||||||
|
<div class="label"><span class="require">*</span>设备名称:</div> |
||||||
|
<el-input v-model="form.name" placeholder="请输入设备名称" class="flex-1" /> |
||||||
|
</el-row> |
||||||
|
<el-row> |
||||||
|
<div class="label"><span class="require">*</span>通道类型:</div> |
||||||
|
<el-select |
||||||
|
v-model="form.channel" |
||||||
|
placeholder="请选择通道类型" |
||||||
|
class="flex-1" |
||||||
|
@change="handleChannelChange" |
||||||
|
> |
||||||
|
<el-option |
||||||
|
v-for="[key, val] in Object.entries(ChannelEnum)" |
||||||
|
:key="key" |
||||||
|
:label="val" |
||||||
|
:value="val" |
||||||
|
/> |
||||||
|
</el-select> |
||||||
|
</el-row> |
||||||
|
<el-row label="" prop="ch"> |
||||||
|
<div class="label"><span class="require">*</span>所属通道:</div> |
||||||
|
<el-select v-model="form.ch" placeholder="请选择通道" class="flex-1"> |
||||||
|
<el-option |
||||||
|
v-for="item in computedChannels" |
||||||
|
:key="item.name" |
||||||
|
:label="item.name" |
||||||
|
:value="item.name" |
||||||
|
/> |
||||||
|
</el-select> |
||||||
|
</el-row> |
||||||
|
<el-row label="设备类别" prop="point"> |
||||||
|
<div class="label"><span class="require">*</span>设备类别:</div> |
||||||
|
<el-select v-model="form.point" placeholder="请选择类别" class="flex-1"> |
||||||
|
<el-option |
||||||
|
v-for="item in computedCategories" |
||||||
|
:key="item.fileName" |
||||||
|
:label="item.name" |
||||||
|
:value="item.name" |
||||||
|
/> |
||||||
|
</el-select> |
||||||
|
</el-row> |
||||||
|
<el-row label="地址" prop="addr"> |
||||||
|
<NumberItemInput |
||||||
|
v-model="form.addr" |
||||||
|
:labelName="'地址' + ':'" |
||||||
|
:numMax="255" |
||||||
|
:numMin="0" |
||||||
|
:require="true" |
||||||
|
:placeholder="`范围0~255`" |
||||||
|
class="item-input" |
||||||
|
/> |
||||||
|
</el-row> |
||||||
|
</div> |
||||||
|
</EdfsDialog> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script setup lang="ts"> |
||||||
|
import { ref, computed } from 'vue' |
||||||
|
import { cloneDeep } from 'lodash-es' |
||||||
|
import { ChannelEnum } from '@/api/module/channel/index' |
||||||
|
import { useMessage } from '@/composables/useMessage' |
||||||
|
import EdfsDialog from '@/components/Edfs-dialog.vue' |
||||||
|
import type { |
||||||
|
IDevice, |
||||||
|
IDeviceCategoryList, |
||||||
|
IDeviceCategoryOV, |
||||||
|
} from '@/api/module/device/index.d' |
||||||
|
import type { Channel, IChannelOV } from '@/api/module/channel/index.d' |
||||||
|
import NumberItemInput from '@/components/Edfs-number-item-input.vue' |
||||||
|
import { validateName } from '@/utils/validate' |
||||||
|
const message = useMessage() |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
channels: IChannelOV |
||||||
|
categories: IDeviceCategoryList |
||||||
|
existingNames?: string[] |
||||||
|
}>() |
||||||
|
|
||||||
|
const emit = defineEmits<{ |
||||||
|
'on-save': [form: IDeviceFormData] |
||||||
|
}>() |
||||||
|
|
||||||
|
type IDeviceFormData = IDevice & { channel: Channel } |
||||||
|
|
||||||
|
const FormData: IDeviceFormData = { |
||||||
|
name: '', |
||||||
|
channel: ChannelEnum.MODBUS_TCP, |
||||||
|
ch: '', |
||||||
|
point: '', |
||||||
|
addr: 0, |
||||||
|
} |
||||||
|
|
||||||
|
function handleChannelChange() { |
||||||
|
form.value.ch = '' |
||||||
|
form.value.point = '' |
||||||
|
} |
||||||
|
|
||||||
|
const computedChannels = computed(() => { |
||||||
|
return props.channels[form.value.channel] |
||||||
|
}) |
||||||
|
|
||||||
|
const computedCategories = computed(() => { |
||||||
|
return props.categories[form.value.channel] |
||||||
|
}) |
||||||
|
|
||||||
|
const form = ref<IDeviceFormData>(cloneDeep(FormData)) |
||||||
|
const visible = ref(false) |
||||||
|
const originalName = ref<string>('') |
||||||
|
|
||||||
|
function open(item?: IDeviceFormData) { |
||||||
|
visible.value = true |
||||||
|
if (item) { |
||||||
|
form.value = cloneDeep(item) |
||||||
|
originalName.value = item.name |
||||||
|
} else { |
||||||
|
form.value = cloneDeep(FormData) |
||||||
|
originalName.value = '' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function onSave() { |
||||||
|
if (verifyData()) return |
||||||
|
emit('on-save', form.value) |
||||||
|
close() |
||||||
|
} |
||||||
|
|
||||||
|
function close() { |
||||||
|
form.value = cloneDeep(FormData) |
||||||
|
visible.value = false |
||||||
|
} |
||||||
|
|
||||||
|
function verifyData() { |
||||||
|
if (!form.value.name) { |
||||||
|
message.warning('请输通道名称') |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
const nameError = validateName(form.value.name) |
||||||
|
if (nameError) { |
||||||
|
message.warning(nameError) |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
if (props.existingNames && props.existingNames.length > 0) { |
||||||
|
const isDuplicate = props.existingNames.some( |
||||||
|
name => name === form.value.name && name !== originalName.value, |
||||||
|
) |
||||||
|
if (isDuplicate) { |
||||||
|
message.warning('名称已存在') |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!form.value.channel) { |
||||||
|
message.warning('请选择通道类型') |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
if (!form.value.ch) { |
||||||
|
message.warning('请选择所属通道') |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
if (!form.value.point) { |
||||||
|
message.warning('请选择设备类别') |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
if (form.value.addr < 0 || form.value.addr > 255) { |
||||||
|
message.warning('地址范围0~255') |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
defineExpose({ |
||||||
|
open, |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.el-row { |
||||||
|
column-gap: 8px; |
||||||
|
|
||||||
|
.label { |
||||||
|
color: var(--label-color); |
||||||
|
line-height: 33px; |
||||||
|
text-align: right; |
||||||
|
width: 110px; |
||||||
|
|
||||||
|
.require { |
||||||
|
color: red; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.item-input { |
||||||
|
height: 32px; |
||||||
|
margin-bottom: 0px; |
||||||
|
:deep(.label) { |
||||||
|
margin-right: 10px; |
||||||
|
} |
||||||
|
:deep(.label) { |
||||||
|
margin-right: 10px; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
import type { Channel, IMqttChannelOV } from "@/api/module/channel/index.d" |
||||||
|
import type { IModbusTcpChannelOV } from "@/api/module/channel/index.d" |
||||||
|
|
||||||
|
export type IFormData = IModbusTcpChannelOV & IMqttChannelOV |
||||||
|
|
||||||
|
export type ICustomFormData = IFormData & { channel: Channel } |
||||||
@ -1,223 +1,233 @@ |
|||||||
<template> |
<template> |
||||||
<div class="step-channel h-full flex flex-col"> |
<div class="step-channel h-full flex flex-col"> |
||||||
<div class="mb-4 flex justify-end"> |
|
||||||
<el-button type="primary" @click="handleAdd">新增通道</el-button> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto"> |
<div class="flex-1 overflow-y-auto"> |
||||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-4"> |
<el-tabs |
||||||
<div |
v-if="Object.keys(modelValue).length > 0" |
||||||
v-for="(item, index) in modelValue" |
v-model="activeTab" |
||||||
:key="index" |
type="card" |
||||||
class="bg-white rounded-lg shadow-sm p-12 border border-gray-200 hover:shadow-md transition-all duration-300 mb-4" |
class="h-full flex flex-col" |
||||||
|
> |
||||||
|
<el-tab-pane |
||||||
|
v-for="(channels, channelType) in modelValue" |
||||||
|
:key="channelType" |
||||||
|
:label=" |
||||||
|
Object.entries(ChannelEnum).find(([key, val]) => val === channelType)?.[1] |
||||||
|
" |
||||||
|
:name="channelType" |
||||||
> |
> |
||||||
<div class="flex justify-between items-start"> |
<el-scrollbar> |
||||||
<span class="font-bold text-lg text-gray-800">{{ item.name }}</span> |
<div class="grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-4 p-4"> |
||||||
<el-tag size="small" type="primary"> |
<!-- Add Channel Card --> |
||||||
{{ item.channel === 'modbusTcp' ? 'Modbus TCP' : 'MQTT' }} |
<div |
||||||
</el-tag> |
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]" |
||||||
</div> |
@click="handleAdd" |
||||||
|
> |
||||||
<div class="space-y-2 mb-4"> |
<div class="flex flex-col items-center gap-3"> |
||||||
<!-- Modbus TCP Section --> |
<el-icon :size="48" class="text-gray-400"> |
||||||
<div v-if="item.channel === 'modbusTcp'" class="text-sm"> |
<Plus /> |
||||||
<div class="flex items-center text-gray-600 mb-2"> |
</el-icon> |
||||||
<span class="font-medium">IP地址:</span> |
<span class="text-base font-medium text-gray-600">新增通道</span> |
||||||
<span class="text-gray-800 ml-2">{{ item.ip }}</span> |
</div> |
||||||
</div> |
</div> |
||||||
<div class="flex items-center text-gray-600"> |
<div |
||||||
<span class="font-medium">端口:</span> |
v-for="(item, index) in channels" |
||||||
<span class="text-gray-800 ml-2">{{ item.port }}</span> |
:key="index" |
||||||
</div> |
class="bg-white rounded-xl shadow-sm p-12 border border-gray-100 hover:shadow-lg transition-all duration-300 flex flex-col" |
||||||
</div> |
> |
||||||
|
<div class="flex justify-between items-start mb-3"> |
||||||
<!-- MQTT Section --> |
<span class="font-bold text-lg text-gray-900" |
||||||
<div v-else-if="item.channel === 'mqtt'" class="text-sm"> |
>通道名称:{{ item.name }}</span |
||||||
<div class="flex items-center text-gray-600"> |
> |
||||||
<span class="font-medium">Broker:</span> |
</div> |
||||||
<span class="text-gray-800">{{ item.broker }}</span> |
|
||||||
|
<div class="space-y-2 mb-3 flex-1"> |
||||||
|
<div v-if="channelType === ChannelEnum.MODBUS_TCP" class="text-sm"> |
||||||
|
<div class="flex items-center justify-between mb-1.5"> |
||||||
|
<span class="text-gray-500">IP地址</span> |
||||||
|
<span class="text-gray-900 font-medium font-mono">{{ |
||||||
|
(item as any).ip |
||||||
|
}}</span> |
||||||
|
</div> |
||||||
|
<div class="flex items-center justify-between"> |
||||||
|
<span class="text-gray-500">端口</span> |
||||||
|
<span class="text-gray-900 font-medium font-mono">{{ |
||||||
|
(item as any).port |
||||||
|
}}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div v-else-if="channelType === ChannelEnum.MQTT" class="text-sm"> |
||||||
|
<div class="flex items-center justify-between"> |
||||||
|
<span class="text-gray-500">Broker</span> |
||||||
|
<span class="text-gray-900 font-medium">{{ |
||||||
|
(item as any).broker |
||||||
|
}}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="flex justify-end pt-3 border-t border-gray-50"> |
||||||
|
<el-button |
||||||
|
type="primary" |
||||||
|
text |
||||||
|
@click=" |
||||||
|
handleEdit( |
||||||
|
Object.assign({ channel: channelType }, item) as ICustomFormData, |
||||||
|
index, |
||||||
|
) |
||||||
|
" |
||||||
|
> |
||||||
|
编辑 |
||||||
|
</el-button> |
||||||
|
<el-button type="danger" text @click="handleDelete(channelType, index)"> |
||||||
|
删除 |
||||||
|
</el-button> |
||||||
|
</div> |
||||||
</div> |
</div> |
||||||
</div> |
</div> |
||||||
</div> |
</el-scrollbar> |
||||||
|
</el-tab-pane> |
||||||
<div class="flex justify-end pt-3 border-t border-gray-100"> |
</el-tabs> |
||||||
<el-button |
<el-empty v-else description="暂无通道,请点击上方按钮添加" /> |
||||||
type="primary" |
|
||||||
text |
|
||||||
@click="handleEdit(index)" |
|
||||||
class="hover:bg-blue-500 hover:text-white transition-colors duration-200" |
|
||||||
> |
|
||||||
编辑 |
|
||||||
</el-button> |
|
||||||
<el-button |
|
||||||
type="danger" |
|
||||||
text |
|
||||||
@click="handleDelete(index)" |
|
||||||
class="hover:bg-red-500 hover:text-white transition-colors duration-200" |
|
||||||
> |
|
||||||
删除 |
|
||||||
</el-button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
<el-empty |
|
||||||
v-if="modelValue.length === 0" |
|
||||||
description="暂无通道,请点击上方按钮添加" |
|
||||||
/> |
|
||||||
</div> |
</div> |
||||||
|
|
||||||
<el-dialog |
<ChannelDlg |
||||||
v-model="dialogVisible" |
ref="ChannelDlgRef" |
||||||
:title="editingIndex !== null ? '编辑通道' : '新增通道'" |
:existingNames="existingChannelNames" |
||||||
width="500px" |
@on-save="handleSave" |
||||||
append-to-body |
/> |
||||||
destroy-on-close |
|
||||||
> |
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px"> |
|
||||||
<el-form-item label="通道名称" prop="name"> |
|
||||||
<el-input v-model="form.name" placeholder="请输入通道名称" /> |
|
||||||
</el-form-item> |
|
||||||
<el-form-item label="通道类型" prop="channel"> |
|
||||||
<el-select v-model="form.channel" placeholder="请选择通道类型" class="w-full"> |
|
||||||
<el-option label="Modbus TCP" value="modbusTcp" /> |
|
||||||
<!-- <el-option label="MQTT" value="mqtt" /> --> |
|
||||||
</el-select> |
|
||||||
</el-form-item> |
|
||||||
<template v-if="form.channel === 'modbusTcp'"> |
|
||||||
<el-form-item label="IP地址" prop="ip"> |
|
||||||
<el-input v-model="form.ip" placeholder="请输入IP地址" /> |
|
||||||
</el-form-item> |
|
||||||
<el-form-item label="端口" prop="port"> |
|
||||||
<el-input-number |
|
||||||
v-model="form.port" |
|
||||||
:min="1" |
|
||||||
:max="65535" |
|
||||||
placeholder="请输入端口" |
|
||||||
class="w-full" |
|
||||||
/> |
|
||||||
</el-form-item> |
|
||||||
</template> |
|
||||||
<!-- <template v-else-if="form.channel === 'mqtt'"> |
|
||||||
<el-form-item label="Broker" prop="broker"> |
|
||||||
<el-input v-model="form.broker" placeholder="请输入Broker地址" /> |
|
||||||
</el-form-item> |
|
||||||
</template> --> |
|
||||||
</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> |
</div> |
||||||
</template> |
</template> |
||||||
|
|
||||||
<script setup lang="ts"> |
<script setup lang="ts"> |
||||||
import { ref, reactive } from 'vue' |
import type { IChannelOV, Channel } from '@/api/module/channel/index.d' |
||||||
import type { |
import { ChannelEnum } from '@/api/module/channel/index' |
||||||
IChannel, |
import ChannelDlg from '../../components/channel-dlg.vue' |
||||||
IModbusTcpChannelOV, |
import type { ICustomFormData } from '../../components/types' |
||||||
IMqttChannelOV, |
import { Plus } from '@element-plus/icons-vue' |
||||||
} from '@/api/module/channel/index.d' |
|
||||||
import type { FormInstance, FormRules } from 'element-plus' |
|
||||||
|
|
||||||
const props = defineProps<{ |
const props = defineProps<{ |
||||||
modelValue: IChannel[] |
modelValue: IChannelOV |
||||||
}>() |
}>() |
||||||
|
|
||||||
const emit = defineEmits<{ |
const emit = defineEmits<{ |
||||||
(e: 'update:modelValue', value: IChannel[]): void |
(e: 'update:modelValue', value: IChannelOV): void |
||||||
}>() |
}>() |
||||||
|
|
||||||
const dialogVisible = ref(false) |
const ChannelDlgRef = ref<typeof ChannelDlg | null>(null) |
||||||
const formRef = ref<FormInstance>() |
|
||||||
const editingIndex = ref<number | null>(null) |
|
||||||
|
|
||||||
type ChannelForm = Partial< |
|
||||||
IModbusTcpChannelOV & IMqttChannelOV & { channel: 'modbusTcp' | 'mqtt' } |
|
||||||
> |
|
||||||
|
|
||||||
const form = reactive<ChannelForm>({ |
const activeTab = ref<string>('modbus') |
||||||
name: '', |
const editingIndex = ref<number | null>(null) |
||||||
channel: 'modbusTcp', |
const editingType = ref<Channel | null>(null) |
||||||
ip: '', |
|
||||||
port: 502, |
|
||||||
broker: '', |
|
||||||
}) |
|
||||||
|
|
||||||
const rules = reactive<FormRules>({ |
const existingChannelNames = computed(() => { |
||||||
name: [{ required: true, message: '请输入通道名称', trigger: 'blur' }], |
const currentChannels = props.modelValue[activeTab.value as Channel] |
||||||
channel: [{ required: true, message: '请选择通道类型', trigger: 'change' }], |
return currentChannels ? currentChannels.map(ch => ch.name) : [] |
||||||
ip: [{ required: true, message: '请输入IP地址', trigger: 'blur' }], |
|
||||||
port: [{ required: true, message: '请输入端口', trigger: 'blur' }], |
|
||||||
broker: [{ required: true, message: '请输入Broker地址', trigger: 'blur' }], |
|
||||||
}) |
}) |
||||||
|
|
||||||
const handleAdd = () => { |
watch( |
||||||
editingIndex.value = null |
() => props.modelValue, |
||||||
form.name = '' |
val => { |
||||||
form.channel = 'modbusTcp' |
if ( |
||||||
form.ip = '' |
val && |
||||||
form.port = 502 |
Object.keys(val).length > 0 && |
||||||
form.broker = '' |
!Object.keys(val).includes(activeTab.value) |
||||||
dialogVisible.value = true |
) { |
||||||
} |
activeTab.value = Object.keys(val)[0] |
||||||
|
} |
||||||
|
}, |
||||||
|
{ immediate: true, deep: true }, |
||||||
|
) |
||||||
|
|
||||||
|
function handleSave(form: ICustomFormData) { |
||||||
|
const newModelValue = { ...props.modelValue } |
||||||
|
const channelType = form.channel as Channel |
||||||
|
|
||||||
|
const channelData = |
||||||
|
channelType === ChannelEnum.MODBUS_TCP |
||||||
|
? { |
||||||
|
name: form.name!, |
||||||
|
ip: form.ip!, |
||||||
|
port: form.port!, |
||||||
|
} |
||||||
|
: { |
||||||
|
name: form.name!, |
||||||
|
broker: form.broker!, |
||||||
|
} |
||||||
|
|
||||||
|
if (editingIndex.value !== null && editingType.value) { |
||||||
|
if (editingType.value === channelType) { |
||||||
|
const list = newModelValue[editingType.value] |
||||||
|
if (list) { |
||||||
|
list[editingIndex.value] = channelData |
||||||
|
} |
||||||
|
} else { |
||||||
|
const oldList = newModelValue[editingType.value] |
||||||
|
if (oldList) { |
||||||
|
oldList.splice(editingIndex.value, 1) |
||||||
|
if (oldList.length === 0) { |
||||||
|
delete newModelValue[editingType.value] |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
const handleEdit = (index: number) => { |
if (!newModelValue[channelType]) { |
||||||
editingIndex.value = index |
newModelValue[channelType] = [] |
||||||
const item = props.modelValue[index] |
} |
||||||
form.name = item.name |
newModelValue[channelType]!.push(channelData) |
||||||
form.channel = item.channel |
} |
||||||
if (item.channel === 'modbusTcp') { |
|
||||||
form.ip = item.ip |
|
||||||
form.port = item.port |
|
||||||
} else { |
} else { |
||||||
form.broker = item.broker |
if (!newModelValue[channelType]) { |
||||||
|
newModelValue[channelType] = [] |
||||||
|
} |
||||||
|
newModelValue[channelType]!.push(channelData) |
||||||
} |
} |
||||||
dialogVisible.value = true |
activeTab.value = channelType |
||||||
|
emit('update:modelValue', newModelValue) |
||||||
} |
} |
||||||
|
|
||||||
const handleDelete = (index: number) => { |
const handleDelete = (type: Channel, index: number) => { |
||||||
const newList = [...props.modelValue] |
const newModelValue = { ...props.modelValue } |
||||||
newList.splice(index, 1) |
const list = newModelValue[type] |
||||||
emit('update:modelValue', newList) |
if (list) { |
||||||
|
list.splice(index, 1) |
||||||
|
if (list.length === 0) { |
||||||
|
delete newModelValue[type] |
||||||
|
if (activeTab.value === type) { |
||||||
|
const remainingKeys = Object.keys(newModelValue) |
||||||
|
if (remainingKeys.length > 0) { |
||||||
|
activeTab.value = remainingKeys[0] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
emit('update:modelValue', newModelValue) |
||||||
|
} |
||||||
} |
} |
||||||
|
|
||||||
const handleSave = async () => { |
function handleAdd() { |
||||||
if (!formRef.value) return |
editingIndex.value = null |
||||||
await formRef.value.validate(valid => { |
editingType.value = null |
||||||
if (valid) { |
ChannelDlgRef.value?.open() |
||||||
const newList = [...props.modelValue] |
} |
||||||
const channelData = |
|
||||||
form.channel === 'modbusTcp' |
|
||||||
? { |
|
||||||
name: form.name!, |
|
||||||
channel: 'modbusTcp' as const, |
|
||||||
ip: form.ip!, |
|
||||||
port: form.port!, |
|
||||||
} |
|
||||||
: { |
|
||||||
name: form.name!, |
|
||||||
channel: 'mqtt' as const, |
|
||||||
broker: form.broker!, |
|
||||||
} |
|
||||||
|
|
||||||
if (editingIndex.value !== null) { |
|
||||||
// 编辑模式 |
|
||||||
newList[editingIndex.value] = channelData |
|
||||||
} else { |
|
||||||
// 新增模式 |
|
||||||
newList.push(channelData) |
|
||||||
} |
|
||||||
|
|
||||||
emit('update:modelValue', newList) |
function handleEdit(item: ICustomFormData, index: number) { |
||||||
dialogVisible.value = false |
editingIndex.value = index |
||||||
} |
editingType.value = item.channel |
||||||
}) |
ChannelDlgRef.value?.open(item) |
||||||
} |
} |
||||||
</script> |
</script> |
||||||
|
|
||||||
<style scoped> |
<style scoped> |
||||||
/* UnoCSS handled classes */ |
:deep(.el-tabs__content) { |
||||||
|
flex: 1; |
||||||
|
overflow: hidden; |
||||||
|
height: 0; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.el-tab-pane) { |
||||||
|
height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.el-scrollbar__view) { |
||||||
|
height: 100%; |
||||||
|
} |
||||||
</style> |
</style> |
||||||
|
|||||||
@ -1,187 +1,234 @@ |
|||||||
<template> |
<template> |
||||||
<div class="step-device h-full flex flex-col"> |
<div class="step-device h-full flex flex-col"> |
||||||
<div class="mb-4 flex justify-end"> |
|
||||||
<el-button type="primary" @click="handleAdd">新增设备</el-button> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto"> |
<div class="flex-1 overflow-y-auto"> |
||||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-4"> |
<el-tabs |
||||||
<div |
v-if="Object.keys(modelValue).length > 0" |
||||||
v-for="(item, index) in modelValue" |
v-model="activeTab" |
||||||
:key="index" |
class="h-full flex flex-col" |
||||||
class="bg-white rounded-lg shadow-sm p-12 border border-gray-200 hover:shadow-md transition-all duration-300 mb-4" |
type="card" |
||||||
|
> |
||||||
|
<el-tab-pane |
||||||
|
v-for="(devices, channelType) in modelValue" |
||||||
|
:key="channelType" |
||||||
|
:label=" |
||||||
|
Object.entries(ChannelEnum).find(([key, val]) => val === channelType)?.[1] |
||||||
|
" |
||||||
|
:name="channelType" |
||||||
> |
> |
||||||
<div class="flex justify-between items-start mb-2"> |
<el-scrollbar> |
||||||
<span class="font-bold text-lg truncate pr-8" :title="item.name">{{ |
<div class="grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-4 p-4"> |
||||||
item.name |
<!-- Add Device Card --> |
||||||
}}</span> |
<div |
||||||
</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="space-y-2 mb-4"> |
> |
||||||
<div class="flex items-center text-gray-600"> |
<div class="flex flex-col items-center gap-3"> |
||||||
<span class="font-medium">通道:</span> |
<el-icon :size="48" class="text-gray-400"> |
||||||
<span class="text-gray-800 ml-2">{{ item.ch }}</span> |
<Plus /> |
||||||
</div> |
</el-icon> |
||||||
<div class="flex items-center text-gray-600"> |
<span class="text-base font-medium text-gray-600">新增设备</span> |
||||||
<span class="font-medium">类别:</span> |
</div> |
||||||
<span class="text-gray-800 ml-2">{{ item.point }}</span> |
</div> |
||||||
</div> |
<div |
||||||
<div class="flex items-center text-gray-600"> |
v-for="(item, index) in devices" |
||||||
<span class="font-medium">地址:</span> |
:key="index" |
||||||
<span class="text-gray-800 ml-2">{{ item.addr }}</span> |
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"> |
||||||
|
<span |
||||||
|
:title="item.name" |
||||||
|
class="font-bold text-lg text-gray-900 truncate pr-8" |
||||||
|
>设备名称:{{ item.name }}</span |
||||||
|
> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="space-y-2 mb-3"> |
||||||
|
<div class="flex items-center justify-between text-sm"> |
||||||
|
<span class="text-gray-500">通道</span> |
||||||
|
<span class="text-gray-900 font-medium">{{ item.ch }}</span> |
||||||
|
</div> |
||||||
|
<div class="flex items-center justify-between text-sm"> |
||||||
|
<span class="text-gray-500">类别</span> |
||||||
|
<span class="text-gray-900 font-medium">{{ item.point }}</span> |
||||||
|
</div> |
||||||
|
<div class="flex items-center justify-between text-sm"> |
||||||
|
<span class="text-gray-500">地址</span> |
||||||
|
<span class="text-gray-900 font-medium font-mono">{{ |
||||||
|
item.addr |
||||||
|
}}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="flex justify-end pt-3 border-t border-gray-50"> |
||||||
|
<el-button |
||||||
|
class="hover:bg-blue-50 transition-colors duration-200" |
||||||
|
text |
||||||
|
type="primary" |
||||||
|
@click=" |
||||||
|
handleEdit( |
||||||
|
Object.assign({ channel: channelType }, item) as IDeviceFormData, |
||||||
|
index, |
||||||
|
) |
||||||
|
" |
||||||
|
> |
||||||
|
编辑 |
||||||
|
</el-button> |
||||||
|
<el-button |
||||||
|
class="hover:bg-red-50 transition-colors duration-200" |
||||||
|
text |
||||||
|
type="danger" |
||||||
|
@click="handleDelete(channelType, index)" |
||||||
|
> |
||||||
|
删除 |
||||||
|
</el-button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
</div> |
</div> |
||||||
</div> |
</el-scrollbar> |
||||||
|
</el-tab-pane> |
||||||
<div class="flex justify-end pt-3 border-t border-gray-100"> |
</el-tabs> |
||||||
<el-button |
<el-empty v-else description="暂无设备,请点击上方按钮添加" /> |
||||||
type="primary" |
|
||||||
text |
|
||||||
@click="handleEdit(index)" |
|
||||||
class="hover:bg-blue-500 hover:text-white transition-colors duration-200" |
|
||||||
> |
|
||||||
编辑 |
|
||||||
</el-button> |
|
||||||
<el-button |
|
||||||
type="danger" |
|
||||||
text |
|
||||||
@click="handleDelete(index)" |
|
||||||
class="hover:bg-red-500 hover:text-white transition-colors duration-200" |
|
||||||
> |
|
||||||
删除 |
|
||||||
</el-button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
<el-empty |
|
||||||
v-if="modelValue.length === 0" |
|
||||||
description="暂无设备,请点击上方按钮添加" |
|
||||||
/> |
|
||||||
</div> |
</div> |
||||||
|
|
||||||
<el-dialog |
<DeviceDlg |
||||||
v-model="dialogVisible" |
ref="DeviceDlgRef" |
||||||
title="新增设备" |
:categories="categories" |
||||||
width="500px" |
:channels="channels" |
||||||
append-to-body |
:existing-names="existingDeviceNames" |
||||||
destroy-on-close |
@on-save="handleSave" |
||||||
> |
/> |
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px"> |
|
||||||
<el-form-item label="设备名称" prop="name"> |
|
||||||
<el-input v-model="form.name" placeholder="请输入设备名称" /> |
|
||||||
</el-form-item> |
|
||||||
<el-form-item label="所属通道" prop="ch"> |
|
||||||
<el-select v-model="form.ch" placeholder="请选择通道" class="w-full"> |
|
||||||
<el-option |
|
||||||
v-for="item in channels" |
|
||||||
:key="item.name" |
|
||||||
:label="item.name" |
|
||||||
:value="item.name" |
|
||||||
/> |
|
||||||
</el-select> |
|
||||||
</el-form-item> |
|
||||||
<el-form-item label="设备类别" prop="point"> |
|
||||||
<el-select v-model="form.point" placeholder="请选择类别" class="w-full"> |
|
||||||
<el-option |
|
||||||
v-for="item in categories" |
|
||||||
:key="item.name" |
|
||||||
:label="item.name" |
|
||||||
:value="item.name" |
|
||||||
/> |
|
||||||
</el-select> |
|
||||||
</el-form-item> |
|
||||||
<el-form-item label="地址" prop="addr"> |
|
||||||
<el-input-number |
|
||||||
v-model="form.addr" |
|
||||||
:min="1" |
|
||||||
placeholder="请输入地址" |
|
||||||
class="w-full" |
|
||||||
/> |
|
||||||
</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> |
</div> |
||||||
</template> |
</template> |
||||||
|
|
||||||
<script setup lang="ts"> |
<script lang="ts" setup> |
||||||
import { ref, reactive } from 'vue' |
import type { IDevice, IDeviceCategoryList, IDeviceOV } from '@/api/module/device/index.d' |
||||||
import { Delete } from '@element-plus/icons-vue' |
import type { Channel, IChannelOV } from '@/api/module/channel/index.d' |
||||||
import type { IDevice } from '@/api/module/device/index.d' |
import { ChannelEnum } from '@/api/module/channel/index' |
||||||
import type { IChannel } from '@/api/module/channel/index.d' |
import DeviceDlg from '../../components/device-dlg.vue' |
||||||
import type { IDeviceCategory } from '@/api/module/device/index.d' |
import { ref, watch, computed } from 'vue' |
||||||
import type { FormInstance, FormRules } from 'element-plus' |
import { Plus } from '@element-plus/icons-vue' |
||||||
|
|
||||||
const props = defineProps<{ |
const props = defineProps<{ |
||||||
modelValue: IDevice[] |
modelValue: IDeviceOV |
||||||
channels: IChannel[] |
channels: IChannelOV |
||||||
categories: IDeviceCategory[] |
categories: IDeviceCategoryList |
||||||
}>() |
}>() |
||||||
|
|
||||||
const emit = defineEmits<{ |
const emit = defineEmits<{ |
||||||
(e: 'update:modelValue', value: IDevice[]): void |
(e: 'update:modelValue', value: IDeviceOV): void |
||||||
}>() |
}>() |
||||||
|
|
||||||
const dialogVisible = ref(false) |
type IDeviceFormData = IDevice & { channel: Channel } |
||||||
const formRef = ref<FormInstance>() |
|
||||||
const editingIndex = ref<number | null>(null) |
|
||||||
|
|
||||||
const form = reactive<IDevice>({ |
const DeviceDlgRef = ref<InstanceType<typeof DeviceDlg> | null>(null) |
||||||
name: '', |
|
||||||
ch: '', |
const activeTab = ref<string>('modbus') |
||||||
point: '', |
|
||||||
addr: 1, |
|
||||||
}) |
|
||||||
|
|
||||||
const rules = reactive<FormRules>({ |
const existingDeviceNames = computed(() => { |
||||||
name: [{ required: true, message: '请输入设备名称', trigger: 'blur' }], |
const currentDevices = props.modelValue[activeTab.value as Channel] |
||||||
ch: [{ required: true, message: '请选择通道', trigger: 'change' }], |
return currentDevices ? currentDevices.map(device => device.name) : [] |
||||||
point: [{ required: true, message: '请选择类别', trigger: 'change' }], |
|
||||||
addr: [{ required: true, message: '请输入地址', trigger: 'blur' }], |
|
||||||
}) |
}) |
||||||
|
const editingIndex = ref<number | null>(null) |
||||||
|
const editingType = ref<Channel | null>(null) |
||||||
|
|
||||||
const handleAdd = () => { |
watch( |
||||||
editingIndex.value = null |
() => props.modelValue, |
||||||
form.name = '' |
val => { |
||||||
form.ch = '' |
if ( |
||||||
form.point = '' |
val && |
||||||
form.addr = 1 |
Object.keys(val).length > 0 && |
||||||
dialogVisible.value = true |
!Object.keys(val).includes(activeTab.value) |
||||||
|
) { |
||||||
|
activeTab.value = Object.keys(val)[0] |
||||||
|
} |
||||||
|
}, |
||||||
|
{ immediate: true, deep: true }, |
||||||
|
) |
||||||
|
|
||||||
|
function handleSave(form: IDeviceFormData) { |
||||||
|
const newModelValue = { ...props.modelValue } |
||||||
|
const channelType = form.channel |
||||||
|
|
||||||
|
const deviceData: IDevice = { |
||||||
|
name: form.name, |
||||||
|
ch: form.ch, |
||||||
|
point: form.point, |
||||||
|
addr: form.addr, |
||||||
|
} |
||||||
|
|
||||||
|
if (editingIndex.value !== null && editingType.value) { |
||||||
|
if (editingType.value === channelType) { |
||||||
|
const list = newModelValue[editingType.value] |
||||||
|
if (list) { |
||||||
|
list[editingIndex.value] = deviceData |
||||||
|
} |
||||||
|
} else { |
||||||
|
const oldList = newModelValue[editingType.value] |
||||||
|
if (oldList) { |
||||||
|
oldList.splice(editingIndex.value, 1) |
||||||
|
if (oldList.length === 0) { |
||||||
|
delete newModelValue[editingType.value] |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!newModelValue[channelType]) { |
||||||
|
newModelValue[channelType] = [] |
||||||
|
} |
||||||
|
newModelValue[channelType]!.push(deviceData) |
||||||
|
} |
||||||
|
} else { |
||||||
|
if (!newModelValue[channelType]) { |
||||||
|
newModelValue[channelType] = [] |
||||||
|
} |
||||||
|
newModelValue[channelType]!.push(deviceData) |
||||||
|
} |
||||||
|
activeTab.value = channelType |
||||||
|
emit('update:modelValue', newModelValue) |
||||||
} |
} |
||||||
|
|
||||||
const handleDelete = (index: number) => { |
const handleDelete = (type: Channel, index: number) => { |
||||||
const newList = [...props.modelValue] |
const newModelValue = { ...props.modelValue } |
||||||
newList.splice(index, 1) |
const list = newModelValue[type] |
||||||
emit('update:modelValue', newList) |
if (list) { |
||||||
|
list.splice(index, 1) |
||||||
|
if (list.length === 0) { |
||||||
|
delete newModelValue[type] |
||||||
|
if (activeTab.value === type) { |
||||||
|
const remainingKeys = Object.keys(newModelValue) |
||||||
|
if (remainingKeys.length > 0) { |
||||||
|
activeTab.value = remainingKeys[0] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
emit('update:modelValue', newModelValue) |
||||||
|
} |
||||||
} |
} |
||||||
|
|
||||||
const handleEdit = (index: number) => { |
function handleAdd() { |
||||||
editingIndex.value = index |
editingIndex.value = null |
||||||
const item = props.modelValue[index] |
editingType.value = null |
||||||
form.name = item.name |
DeviceDlgRef.value?.open() |
||||||
form.ch = item.ch |
|
||||||
form.point = item.point |
|
||||||
form.addr = item.addr |
|
||||||
dialogVisible.value = true |
|
||||||
} |
} |
||||||
|
|
||||||
const handleSave = async () => { |
function handleEdit(item: IDeviceFormData, index: number) { |
||||||
if (!formRef.value) return |
editingIndex.value = index |
||||||
await formRef.value.validate(valid => { |
editingType.value = item.channel |
||||||
if (valid) { |
DeviceDlgRef.value?.open(item) |
||||||
const newList = [...props.modelValue] |
|
||||||
newList.push({ ...form }) |
|
||||||
emit('update:modelValue', newList) |
|
||||||
dialogVisible.value = false |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
} |
||||||
</script> |
</script> |
||||||
|
|
||||||
<style scoped> |
<style scoped> |
||||||
/* UnoCSS handled classes */ |
:deep(.el-tabs__content) { |
||||||
|
flex: 1; |
||||||
|
overflow: hidden; |
||||||
|
height: 0; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.el-tab-pane) { |
||||||
|
height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.el-scrollbar__view) { |
||||||
|
height: 100%; |
||||||
|
} |
||||||
</style> |
</style> |
||||||
|
|||||||
Loading…
Reference in new issue