35 changed files with 7004 additions and 9844 deletions
@ -1,3 +1,3 @@
@@ -1,3 +1,3 @@
|
||||
VITE_BASE_API = '/remoteServer' |
||||
VITE_BASE_API = '/remoteServer/admin-api/' |
||||
VITE_SHOW_ONLINE_DEVICE = true |
||||
|
||||
VITE_BASE_URL = 'http://43.140.245.32:48081' |
||||
|
||||
@ -1,17 +1,29 @@
@@ -1,17 +1,29 @@
|
||||
import { globalServer } from '../index' |
||||
import type { IDeviceCategory } from './index.d' |
||||
import type { IDeviceCategoryOV, IDeviceBase, IDeviceCategoryFileOV, IDeviceCategoryList } from './index.d' |
||||
|
||||
export const getDeviceTypeList = () => { |
||||
return globalServer<IDeviceCategory[]>({ |
||||
url: '/device/type/list', |
||||
export const getDeviceTypeList = (params: IDeviceBase) => { |
||||
return globalServer<IDeviceCategoryList>({ |
||||
url: '/project/point/list-all-with-status', |
||||
method: 'get', |
||||
params |
||||
}) |
||||
} |
||||
|
||||
export const createDeviceType = (params: IDeviceCategory) => { |
||||
export const createDeviceType = (params: IDeviceCategoryOV & IDeviceBase) => { |
||||
return globalServer({ |
||||
url: '/device/type/create', |
||||
url: '/project/point/generate-file', |
||||
method: 'post', |
||||
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 @@
@@ -1,14 +1,23 @@
|
||||
import { globalServer } from '../index' |
||||
|
||||
import { getEngineeringListMock, createEngineeringMock } from '@/api/modkJs/engineering' |
||||
import type { IEngineeringOV, IEngineering } from './index.d' |
||||
import type { IEngineeringOV } from './index.d' |
||||
|
||||
export const getEngineeringList = () => { |
||||
// Use mock data
|
||||
return getEngineeringListMock() |
||||
export const createEngineering = (params: IEngineeringOV) => { |
||||
return globalServer({ |
||||
url: '/project/generate', |
||||
method: 'POST', |
||||
data: params, |
||||
}) |
||||
} |
||||
|
||||
export const createEngineering = (params: IEngineeringOV) => { |
||||
// Use mock data
|
||||
return createEngineeringMock(params) |
||||
export const getEngineeringList = ( |
||||
params?: Pick<IEngineeringOV, 'name' | 'isCreate'>, |
||||
) => { |
||||
return globalServer<{ |
||||
list: IEngineeringOV[] |
||||
}>({ |
||||
url: '/project/page', |
||||
method: 'get', |
||||
params, |
||||
}) |
||||
} |
||||
|
||||
@ -0,0 +1,177 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -1,223 +1,233 @@
|
||||
<template> |
||||
<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="grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-4"> |
||||
<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="(channels, 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"> |
||||
<!-- Add Channel Card --> |
||||
<div |
||||
v-for="(item, index) in modelValue" |
||||
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="(item, index) in channels" |
||||
:key="index" |
||||
class="bg-white rounded-lg shadow-sm p-12 border border-gray-200 hover:shadow-md transition-all duration-300 mb-4" |
||||
class="bg-white rounded-xl shadow-sm p-12 border border-gray-100 hover:shadow-lg transition-all duration-300 flex flex-col" |
||||
> |
||||
<div class="flex justify-between items-start mb-3"> |
||||
<span class="font-bold text-lg text-gray-900" |
||||
>通道名称:{{ item.name }}</span |
||||
> |
||||
<div class="flex justify-between items-start"> |
||||
<span class="font-bold text-lg text-gray-800">{{ item.name }}</span> |
||||
<el-tag size="small" type="primary"> |
||||
{{ item.channel === 'modbusTcp' ? 'Modbus TCP' : 'MQTT' }} |
||||
</el-tag> |
||||
</div> |
||||
|
||||
<div class="space-y-2 mb-4"> |
||||
<!-- Modbus TCP Section --> |
||||
<div v-if="item.channel === 'modbusTcp'" class="text-sm"> |
||||
<div class="flex items-center text-gray-600 mb-2"> |
||||
<span class="font-medium">IP地址:</span> |
||||
<span class="text-gray-800 ml-2">{{ item.ip }}</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 text-gray-600"> |
||||
<span class="font-medium">端口:</span> |
||||
<span class="text-gray-800 ml-2">{{ item.port }}</span> |
||||
<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> |
||||
|
||||
<!-- MQTT Section --> |
||||
<div v-else-if="item.channel === 'mqtt'" class="text-sm"> |
||||
<div class="flex items-center text-gray-600"> |
||||
<span class="font-medium">Broker:</span> |
||||
<span class="text-gray-800">{{ item.broker }}</span> |
||||
<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-100"> |
||||
<div class="flex justify-end pt-3 border-t border-gray-50"> |
||||
<el-button |
||||
type="primary" |
||||
text |
||||
@click="handleEdit(index)" |
||||
class="hover:bg-blue-500 hover:text-white transition-colors duration-200" |
||||
@click=" |
||||
handleEdit( |
||||
Object.assign({ channel: channelType }, item) as ICustomFormData, |
||||
index, |
||||
) |
||||
" |
||||
> |
||||
编辑 |
||||
</el-button> |
||||
<el-button |
||||
type="danger" |
||||
text |
||||
@click="handleDelete(index)" |
||||
class="hover:bg-red-500 hover:text-white transition-colors duration-200" |
||||
> |
||||
<el-button type="danger" text @click="handleDelete(channelType, index)"> |
||||
删除 |
||||
</el-button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<el-empty |
||||
v-if="modelValue.length === 0" |
||||
description="暂无通道,请点击上方按钮添加" |
||||
/> |
||||
</el-scrollbar> |
||||
</el-tab-pane> |
||||
</el-tabs> |
||||
<el-empty v-else description="暂无通道,请点击上方按钮添加" /> |
||||
</div> |
||||
|
||||
<el-dialog |
||||
v-model="dialogVisible" |
||||
:title="editingIndex !== null ? '编辑通道' : '新增通道'" |
||||
width="500px" |
||||
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" |
||||
<ChannelDlg |
||||
ref="ChannelDlgRef" |
||||
:existingNames="existingChannelNames" |
||||
@on-save="handleSave" |
||||
/> |
||||
</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> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { ref, reactive } from 'vue' |
||||
import type { |
||||
IChannel, |
||||
IModbusTcpChannelOV, |
||||
IMqttChannelOV, |
||||
} from '@/api/module/channel/index.d' |
||||
import type { FormInstance, FormRules } from 'element-plus' |
||||
import type { IChannelOV, Channel } from '@/api/module/channel/index.d' |
||||
import { ChannelEnum } from '@/api/module/channel/index' |
||||
import ChannelDlg from '../../components/channel-dlg.vue' |
||||
import type { ICustomFormData } from '../../components/types' |
||||
import { Plus } from '@element-plus/icons-vue' |
||||
|
||||
const props = defineProps<{ |
||||
modelValue: IChannel[] |
||||
modelValue: IChannelOV |
||||
}>() |
||||
|
||||
const emit = defineEmits<{ |
||||
(e: 'update:modelValue', value: IChannel[]): void |
||||
(e: 'update:modelValue', value: IChannelOV): void |
||||
}>() |
||||
|
||||
const dialogVisible = ref(false) |
||||
const formRef = ref<FormInstance>() |
||||
const editingIndex = ref<number | null>(null) |
||||
const ChannelDlgRef = ref<typeof ChannelDlg | null>(null) |
||||
|
||||
type ChannelForm = Partial< |
||||
IModbusTcpChannelOV & IMqttChannelOV & { channel: 'modbusTcp' | 'mqtt' } |
||||
> |
||||
|
||||
const form = reactive<ChannelForm>({ |
||||
name: '', |
||||
channel: 'modbusTcp', |
||||
ip: '', |
||||
port: 502, |
||||
broker: '', |
||||
}) |
||||
const activeTab = ref<string>('modbus') |
||||
const editingIndex = ref<number | null>(null) |
||||
const editingType = ref<Channel | null>(null) |
||||
|
||||
const rules = reactive<FormRules>({ |
||||
name: [{ required: true, message: '请输入通道名称', trigger: 'blur' }], |
||||
channel: [{ required: true, message: '请选择通道类型', trigger: 'change' }], |
||||
ip: [{ required: true, message: '请输入IP地址', trigger: 'blur' }], |
||||
port: [{ required: true, message: '请输入端口', trigger: 'blur' }], |
||||
broker: [{ required: true, message: '请输入Broker地址', trigger: 'blur' }], |
||||
const existingChannelNames = computed(() => { |
||||
const currentChannels = props.modelValue[activeTab.value as Channel] |
||||
return currentChannels ? currentChannels.map(ch => ch.name) : [] |
||||
}) |
||||
|
||||
const handleAdd = () => { |
||||
editingIndex.value = null |
||||
form.name = '' |
||||
form.channel = 'modbusTcp' |
||||
form.ip = '' |
||||
form.port = 502 |
||||
form.broker = '' |
||||
dialogVisible.value = true |
||||
} |
||||
|
||||
const handleEdit = (index: number) => { |
||||
editingIndex.value = index |
||||
const item = props.modelValue[index] |
||||
form.name = item.name |
||||
form.channel = item.channel |
||||
if (item.channel === 'modbusTcp') { |
||||
form.ip = item.ip |
||||
form.port = item.port |
||||
} else { |
||||
form.broker = item.broker |
||||
} |
||||
dialogVisible.value = true |
||||
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 handleDelete = (index: number) => { |
||||
const newList = [...props.modelValue] |
||||
newList.splice(index, 1) |
||||
emit('update:modelValue', newList) |
||||
} |
||||
function handleSave(form: ICustomFormData) { |
||||
const newModelValue = { ...props.modelValue } |
||||
const channelType = form.channel as Channel |
||||
|
||||
const handleSave = async () => { |
||||
if (!formRef.value) return |
||||
await formRef.value.validate(valid => { |
||||
if (valid) { |
||||
const newList = [...props.modelValue] |
||||
const channelData = |
||||
form.channel === 'modbusTcp' |
||||
channelType === ChannelEnum.MODBUS_TCP |
||||
? { |
||||
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 |
||||
if (editingIndex.value !== null && editingType.value) { |
||||
if (editingType.value === channelType) { |
||||
const list = newModelValue[editingType.value] |
||||
if (list) { |
||||
list[editingIndex.value] = channelData |
||||
} |
||||
} else { |
||||
// 新增模式 |
||||
newList.push(channelData) |
||||
const oldList = newModelValue[editingType.value] |
||||
if (oldList) { |
||||
oldList.splice(editingIndex.value, 1) |
||||
if (oldList.length === 0) { |
||||
delete newModelValue[editingType.value] |
||||
} |
||||
} |
||||
|
||||
emit('update:modelValue', newList) |
||||
dialogVisible.value = false |
||||
if (!newModelValue[channelType]) { |
||||
newModelValue[channelType] = [] |
||||
} |
||||
newModelValue[channelType]!.push(channelData) |
||||
} |
||||
} else { |
||||
if (!newModelValue[channelType]) { |
||||
newModelValue[channelType] = [] |
||||
} |
||||
newModelValue[channelType]!.push(channelData) |
||||
} |
||||
activeTab.value = channelType |
||||
emit('update:modelValue', newModelValue) |
||||
} |
||||
|
||||
const handleDelete = (type: Channel, index: number) => { |
||||
const newModelValue = { ...props.modelValue } |
||||
const list = newModelValue[type] |
||||
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) |
||||
} |
||||
} |
||||
|
||||
function handleAdd() { |
||||
editingIndex.value = null |
||||
editingType.value = null |
||||
ChannelDlgRef.value?.open() |
||||
} |
||||
|
||||
function handleEdit(item: ICustomFormData, index: number) { |
||||
editingIndex.value = index |
||||
editingType.value = item.channel |
||||
ChannelDlgRef.value?.open(item) |
||||
} |
||||
</script> |
||||
|
||||
<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> |
||||
|
||||
@ -1,187 +1,234 @@
@@ -1,187 +1,234 @@
|
||||
<template> |
||||
<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="grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-4"> |
||||
<el-tabs |
||||
v-if="Object.keys(modelValue).length > 0" |
||||
v-model="activeTab" |
||||
class="h-full flex flex-col" |
||||
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" |
||||
> |
||||
<el-scrollbar> |
||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-4 p-4"> |
||||
<!-- Add Device Card --> |
||||
<div |
||||
v-for="(item, index) in modelValue" |
||||
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="(item, index) in devices" |
||||
:key="index" |
||||
class="bg-white rounded-lg shadow-sm p-12 border border-gray-200 hover:shadow-md transition-all duration-300 mb-4" |
||||
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 class="flex justify-between items-start mb-2"> |
||||
<span class="font-bold text-lg truncate pr-8" :title="item.name">{{ |
||||
item.name |
||||
}}</span> |
||||
</div> |
||||
|
||||
<div class="space-y-2 mb-4"> |
||||
<div class="flex items-center text-gray-600"> |
||||
<span class="font-medium">通道:</span> |
||||
<span class="text-gray-800 ml-2">{{ item.ch }}</span> |
||||
<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 text-gray-600"> |
||||
<span class="font-medium">类别:</span> |
||||
<span class="text-gray-800 ml-2">{{ item.point }}</span> |
||||
<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 text-gray-600"> |
||||
<span class="font-medium">地址:</span> |
||||
<span class="text-gray-800 ml-2">{{ item.addr }}</span> |
||||
<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-100"> |
||||
<div class="flex justify-end pt-3 border-t border-gray-50"> |
||||
<el-button |
||||
type="primary" |
||||
class="hover:bg-blue-50 transition-colors duration-200" |
||||
text |
||||
@click="handleEdit(index)" |
||||
class="hover:bg-blue-500 hover:text-white transition-colors duration-200" |
||||
type="primary" |
||||
@click=" |
||||
handleEdit( |
||||
Object.assign({ channel: channelType }, item) as IDeviceFormData, |
||||
index, |
||||
) |
||||
" |
||||
> |
||||
编辑 |
||||
</el-button> |
||||
<el-button |
||||
type="danger" |
||||
class="hover:bg-red-50 transition-colors duration-200" |
||||
text |
||||
@click="handleDelete(index)" |
||||
class="hover:bg-red-500 hover:text-white transition-colors duration-200" |
||||
type="danger" |
||||
@click="handleDelete(channelType, index)" |
||||
> |
||||
删除 |
||||
</el-button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<el-empty |
||||
v-if="modelValue.length === 0" |
||||
description="暂无设备,请点击上方按钮添加" |
||||
/> |
||||
</el-scrollbar> |
||||
</el-tab-pane> |
||||
</el-tabs> |
||||
<el-empty v-else description="暂无设备,请点击上方按钮添加" /> |
||||
</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="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" |
||||
<DeviceDlg |
||||
ref="DeviceDlgRef" |
||||
:categories="categories" |
||||
:channels="channels" |
||||
:existing-names="existingDeviceNames" |
||||
@on-save="handleSave" |
||||
/> |
||||
</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> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { ref, reactive } from 'vue' |
||||
import { Delete } from '@element-plus/icons-vue' |
||||
import type { IDevice } from '@/api/module/device/index.d' |
||||
import type { IChannel } from '@/api/module/channel/index.d' |
||||
import type { IDeviceCategory } from '@/api/module/device/index.d' |
||||
import type { FormInstance, FormRules } from 'element-plus' |
||||
<script lang="ts" setup> |
||||
import type { IDevice, IDeviceCategoryList, IDeviceOV } from '@/api/module/device/index.d' |
||||
import type { Channel, IChannelOV } from '@/api/module/channel/index.d' |
||||
import { ChannelEnum } from '@/api/module/channel/index' |
||||
import DeviceDlg from '../../components/device-dlg.vue' |
||||
import { ref, watch, computed } from 'vue' |
||||
import { Plus } from '@element-plus/icons-vue' |
||||
|
||||
const props = defineProps<{ |
||||
modelValue: IDevice[] |
||||
channels: IChannel[] |
||||
categories: IDeviceCategory[] |
||||
modelValue: IDeviceOV |
||||
channels: IChannelOV |
||||
categories: IDeviceCategoryList |
||||
}>() |
||||
|
||||
const emit = defineEmits<{ |
||||
(e: 'update:modelValue', value: IDevice[]): void |
||||
(e: 'update:modelValue', value: IDeviceOV): void |
||||
}>() |
||||
|
||||
const dialogVisible = ref(false) |
||||
const formRef = ref<FormInstance>() |
||||
const editingIndex = ref<number | null>(null) |
||||
type IDeviceFormData = IDevice & { channel: Channel } |
||||
|
||||
const form = reactive<IDevice>({ |
||||
name: '', |
||||
ch: '', |
||||
point: '', |
||||
addr: 1, |
||||
}) |
||||
const DeviceDlgRef = ref<InstanceType<typeof DeviceDlg> | null>(null) |
||||
|
||||
const activeTab = ref<string>('modbus') |
||||
|
||||
const rules = reactive<FormRules>({ |
||||
name: [{ required: true, message: '请输入设备名称', trigger: 'blur' }], |
||||
ch: [{ required: true, message: '请选择通道', trigger: 'change' }], |
||||
point: [{ required: true, message: '请选择类别', trigger: 'change' }], |
||||
addr: [{ required: true, message: '请输入地址', trigger: 'blur' }], |
||||
const existingDeviceNames = computed(() => { |
||||
const currentDevices = props.modelValue[activeTab.value as Channel] |
||||
return currentDevices ? currentDevices.map(device => device.name) : [] |
||||
}) |
||||
const editingIndex = ref<number | null>(null) |
||||
const editingType = ref<Channel | null>(null) |
||||
|
||||
const handleAdd = () => { |
||||
editingIndex.value = null |
||||
form.name = '' |
||||
form.ch = '' |
||||
form.point = '' |
||||
form.addr = 1 |
||||
dialogVisible.value = true |
||||
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 }, |
||||
) |
||||
|
||||
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, |
||||
} |
||||
|
||||
const handleDelete = (index: number) => { |
||||
const newList = [...props.modelValue] |
||||
newList.splice(index, 1) |
||||
emit('update:modelValue', newList) |
||||
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] |
||||
} |
||||
} |
||||
|
||||
const handleEdit = (index: number) => { |
||||
editingIndex.value = index |
||||
const item = props.modelValue[index] |
||||
form.name = item.name |
||||
form.ch = item.ch |
||||
form.point = item.point |
||||
form.addr = item.addr |
||||
dialogVisible.value = true |
||||
} |
||||
|
||||
const handleSave = async () => { |
||||
if (!formRef.value) return |
||||
await formRef.value.validate(valid => { |
||||
if (valid) { |
||||
const newList = [...props.modelValue] |
||||
newList.push({ ...form }) |
||||
emit('update:modelValue', newList) |
||||
dialogVisible.value = false |
||||
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 = (type: Channel, index: number) => { |
||||
const newModelValue = { ...props.modelValue } |
||||
const list = newModelValue[type] |
||||
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) |
||||
} |
||||
} |
||||
|
||||
function handleAdd() { |
||||
editingIndex.value = null |
||||
editingType.value = null |
||||
DeviceDlgRef.value?.open() |
||||
} |
||||
|
||||
function handleEdit(item: IDeviceFormData, index: number) { |
||||
editingIndex.value = index |
||||
editingType.value = item.channel |
||||
DeviceDlgRef.value?.open(item) |
||||
} |
||||
</script> |
||||
|
||||
<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> |
||||
|
||||
Loading…
Reference in new issue