21 changed files with 1451 additions and 0 deletions
@ -0,0 +1,24 @@ |
|||||||
|
{ |
||||||
|
"channel": [ |
||||||
|
{ |
||||||
|
"name": "名称,唯一标识不可重复,不能为中文或特殊符号,长度32字节", |
||||||
|
"ip": "设备IP地址,字符串", |
||||||
|
"port": "端口号,整形数字,0~65535,一般为502" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "modbustcp_ch1", |
||||||
|
"ip": "192.168.10.123", |
||||||
|
"port": 502 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "xxxx", |
||||||
|
"ip": "192.168.10.133", |
||||||
|
"port": 1502 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "xxxxxx", |
||||||
|
"ip": "192.168.10.143", |
||||||
|
"port": 2502 |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
@ -0,0 +1,34 @@ |
|||||||
|
{ |
||||||
|
"dev": [ |
||||||
|
{ |
||||||
|
"name": "名称,唯一标识不可重复,不能为中文或特殊符号,长度32字节", |
||||||
|
"ch": "所在通道名称,特指ModbusRtu通道的名称,字符串", |
||||||
|
"point": "使用点表名称,特指Modbus点表的名称,字符串", |
||||||
|
"addr": "站地址,字符串,0 ~ 255" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "xxxx1", |
||||||
|
"ch": "modbustcp_ch1", |
||||||
|
"point": "pcs", |
||||||
|
"addr": "1" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "xxxx2", |
||||||
|
"ch": "modbustcp_ch1", |
||||||
|
"point": "pcs", |
||||||
|
"addr": "2" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "xxxx3", |
||||||
|
"ch": "xxxx", |
||||||
|
"point": "bms", |
||||||
|
"addr": "1" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "xxxx4", |
||||||
|
"ch": "xxxxxx", |
||||||
|
"point": "tms", |
||||||
|
"addr": "1" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
{ |
||||||
|
"name": "工程名xxxx", |
||||||
|
"commun_channel": { |
||||||
|
"通讯通道类型,固定如下": "通讯通道配置文件路径", |
||||||
|
"modbustcp": "commun_channel_moebustcp.json" |
||||||
|
}, |
||||||
|
"commun_dev": { |
||||||
|
"通讯设备类型,固定如下": "通讯设备配置文件路径", |
||||||
|
"modbustcp": "commun_dev_moebustcp.json" |
||||||
|
}, |
||||||
|
"point_table": { |
||||||
|
"通讯点表类型,固定如下": { |
||||||
|
"名称": "路径" |
||||||
|
}, |
||||||
|
"modbus": { |
||||||
|
"modbus点表名称,自定义,非中文、非特殊字符,32字节": "modbus点表文件路径", |
||||||
|
"pcs2": "point_table_modbus_01.json", |
||||||
|
"bms": "point_table_modbus_02.json", |
||||||
|
"tms": "point_table_modbus_03.json" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,61 @@ |
|||||||
|
{ |
||||||
|
"name": "p12312cs", |
||||||
|
"point": [ |
||||||
|
[ |
||||||
|
"点起始地址,进制字符串, 范围 0x0000~0xFFFF 或 0~65535", |
||||||
|
"点数量,字符串, 范围 0x0001~0x10000 或 1~65536", |
||||||
|
"功能码,字符串,可选项: '0x01' '0x02' '0x03' '0x04' 或 '1' '2' '3' '4'", |
||||||
|
"数据类型,字符串,可选 'U16' 'S16' 'U32' 'S32'", |
||||||
|
"点读取周期ms,整形数字,-1时标志该点位未启用", |
||||||
|
"点位段名称,字符串,可设为空字符串" |
||||||
|
], |
||||||
|
[ |
||||||
|
"0x0001", |
||||||
|
"0x0010", |
||||||
|
"0x03", |
||||||
|
"U16", |
||||||
|
1000, |
||||||
|
"点位段名称1" |
||||||
|
], |
||||||
|
[ |
||||||
|
"0x0100", |
||||||
|
"0x0020", |
||||||
|
"0x03", |
||||||
|
"U16", |
||||||
|
1000, |
||||||
|
"点位段名称2" |
||||||
|
], |
||||||
|
[ |
||||||
|
"0x0120", |
||||||
|
"0x0020", |
||||||
|
"0x03", |
||||||
|
"U32", |
||||||
|
1000, |
||||||
|
"" |
||||||
|
], |
||||||
|
[ |
||||||
|
"0x0000", |
||||||
|
"0x0020", |
||||||
|
"0x04", |
||||||
|
"U16", |
||||||
|
1000, |
||||||
|
"点位段名称" |
||||||
|
], |
||||||
|
[ |
||||||
|
"0x0020", |
||||||
|
"0x0010", |
||||||
|
"0x04", |
||||||
|
"U32", |
||||||
|
1000, |
||||||
|
"-" |
||||||
|
], |
||||||
|
[ |
||||||
|
"0x0040", |
||||||
|
"0x0020", |
||||||
|
"0x04", |
||||||
|
"U16", |
||||||
|
1000, |
||||||
|
"点位段名称" |
||||||
|
] |
||||||
|
] |
||||||
|
} |
||||||
@ -0,0 +1,61 @@ |
|||||||
|
{ |
||||||
|
"name": "xxxx", |
||||||
|
"point": [ |
||||||
|
[ |
||||||
|
"点起始地址,进制字符串, 范围 0x0000~0xFFFF 或 0~65535", |
||||||
|
"点数量,字符串, 范围 0x0001~0x10000 或 1~65536", |
||||||
|
"功能码,字符串,可选项: '0x01' '0x02' '0x03' '0x04' 或 '1' '2' '3' '4'", |
||||||
|
"数据类型,字符串,可选 'U16' 'S16' 'U32' 'S32'", |
||||||
|
"点读取周期ms,整形数字,-1时标志该点位未启用", |
||||||
|
"点位段名称,字符串,可设为空字符串" |
||||||
|
], |
||||||
|
[ |
||||||
|
"0x0001", |
||||||
|
"0x0010", |
||||||
|
"0x03", |
||||||
|
"U16", |
||||||
|
1000, |
||||||
|
"点位段名称1" |
||||||
|
], |
||||||
|
[ |
||||||
|
"0x0100", |
||||||
|
"0x0020", |
||||||
|
"0x03", |
||||||
|
"U16", |
||||||
|
1000, |
||||||
|
"点位段名称2" |
||||||
|
], |
||||||
|
[ |
||||||
|
"0x0120", |
||||||
|
"0x0020", |
||||||
|
"0x03", |
||||||
|
"U32", |
||||||
|
1000, |
||||||
|
"" |
||||||
|
], |
||||||
|
[ |
||||||
|
"0x0000", |
||||||
|
"0x0020", |
||||||
|
"0x04", |
||||||
|
"U16", |
||||||
|
1000, |
||||||
|
"点位段名称" |
||||||
|
], |
||||||
|
[ |
||||||
|
"0x0020", |
||||||
|
"0x0010", |
||||||
|
"0x04", |
||||||
|
"U32", |
||||||
|
1000, |
||||||
|
"-" |
||||||
|
], |
||||||
|
[ |
||||||
|
"0x0040", |
||||||
|
"0x0020", |
||||||
|
"0x04", |
||||||
|
"U16", |
||||||
|
1000, |
||||||
|
"点位段名称" |
||||||
|
] |
||||||
|
] |
||||||
|
} |
||||||
@ -0,0 +1,61 @@ |
|||||||
|
{ |
||||||
|
"name": "xxxx", |
||||||
|
"point": [ |
||||||
|
[ |
||||||
|
"点起始地址,进制字符串, 范围 0x0000~0xFFFF 或 0~65535", |
||||||
|
"点数量,字符串, 范围 0x0001~0x10000 或 1~65536", |
||||||
|
"功能码,字符串,可选项: '0x01' '0x02' '0x03' '0x04' 或 '1' '2' '3' '4'", |
||||||
|
"数据类型,字符串,可选 'U16' 'S16' 'U32' 'S32'", |
||||||
|
"点读取周期ms,整形数字,-1时标志该点位未启用", |
||||||
|
"点位段名称,字符串,可设为空字符串" |
||||||
|
], |
||||||
|
[ |
||||||
|
"0x0001", |
||||||
|
"0x0010", |
||||||
|
"0x03", |
||||||
|
"U16", |
||||||
|
1000, |
||||||
|
"点位段名称1" |
||||||
|
], |
||||||
|
[ |
||||||
|
"0x0100", |
||||||
|
"0x0020", |
||||||
|
"0x03", |
||||||
|
"U16", |
||||||
|
1000, |
||||||
|
"点位段名称2" |
||||||
|
], |
||||||
|
[ |
||||||
|
"0x0120", |
||||||
|
"0x0020", |
||||||
|
"0x03", |
||||||
|
"U32", |
||||||
|
1000, |
||||||
|
"" |
||||||
|
], |
||||||
|
[ |
||||||
|
"0x0000", |
||||||
|
"0x0020", |
||||||
|
"0x04", |
||||||
|
"U16", |
||||||
|
1000, |
||||||
|
"点位段名称" |
||||||
|
], |
||||||
|
[ |
||||||
|
"0x0020", |
||||||
|
"0x0010", |
||||||
|
"0x04", |
||||||
|
"U32", |
||||||
|
1000, |
||||||
|
"-" |
||||||
|
], |
||||||
|
[ |
||||||
|
"0x0040", |
||||||
|
"0x0020", |
||||||
|
"0x04", |
||||||
|
"U16", |
||||||
|
1000, |
||||||
|
"点位段名称" |
||||||
|
] |
||||||
|
] |
||||||
|
} |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
|
||||||
|
|
||||||
|
## 创建工程 |
||||||
|
|
||||||
|
填写工程名,用户自定义输入 |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 通讯点表 |
||||||
|
|
||||||
|
1. 展示目前工程中已有的通讯点表 |
||||||
|
|
||||||
|
2. 导入新的通讯点表 |
||||||
|
|
||||||
|
``` |
||||||
|
自定义名称:设置点表自定义名字,用户自定义输入,需检查唯一性,禁止中文、特殊字符 |
||||||
|
通讯点表类型:通过下拉菜单选择,目前只支持ModbusTcp(预留支持其他类型,不同类型的点表需要分类保存) |
||||||
|
``` |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 通讯通道 |
||||||
|
|
||||||
|
1. 展示目前工程中已有的通讯通道 |
||||||
|
|
||||||
|
2. 创建新的通讯通道 |
||||||
|
|
||||||
|
``` |
||||||
|
自定义名称:设置通讯通道自定义名字,用户自定义输入,需检查唯一性,禁止中文、特殊字符 |
||||||
|
通讯通道类型:通过下拉菜单选择,目前只支持ModbusTcp(预留支持其他类型,不同类型的通讯通道分类保存) |
||||||
|
设备IP地址:用户自定义输入,须符合IP地址的格式,如192.168.10.123 |
||||||
|
设备端口:用户自定义输入,范围0~65535,一般为502 |
||||||
|
``` |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 通讯设备 |
||||||
|
|
||||||
|
1. 展示目前工程中已有的通讯设备 |
||||||
|
|
||||||
|
2. 创建新的通讯设备 |
||||||
|
|
||||||
|
``` |
||||||
|
自定义名称:设置通讯设备自定义名字,用户自定义输入,需检查唯一性,禁止中文、特殊字符 |
||||||
|
通讯设备类型:通过下拉菜单选择,目前只支持ModbusTcp(预留支持其他类型,不同类型的通讯设备分类保存) |
||||||
|
通讯通道选择:通过下拉菜单选择,选项为已选择的类型(目前只有ModbusTcp)的所有通讯通道(是否支持在此处新建通道) |
||||||
|
通讯点表选择:通过下拉菜单选择,选项为已选择的类型(目前只有ModbusTcp)的所有通讯点表(是否支持在此处新建点表) |
||||||
|
通讯地址:用户自定义输入通讯站地址,范围0~255,一般为1 |
||||||
|
``` |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,18 @@ |
|||||||
|
export type Channel = 'modbusTcp' | 'mqtt' |
||||||
|
|
||||||
|
export type IChannel = |
||||||
|
| (IModbusTcpChannelOV & { channel: 'modbusTcp' }) |
||||||
|
| (IMqttChannelOV & { channel: 'mqtt' }) |
||||||
|
|
||||||
|
interface IChannelBaseOV { |
||||||
|
name: string |
||||||
|
} |
||||||
|
|
||||||
|
export interface IModbusTcpChannelOV extends IChannelBaseOV { |
||||||
|
ip: string |
||||||
|
port: number |
||||||
|
} |
||||||
|
|
||||||
|
export interface IMqttChannelOV extends IChannelBaseOV { |
||||||
|
broker: string |
||||||
|
} |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
import { globalServer } from '../index' |
||||||
|
import type { IChannel } from './index.d' |
||||||
|
|
||||||
|
export const getChannelList = () => { |
||||||
|
return globalServer<IChannel[]>({ |
||||||
|
url: '/channel/list', |
||||||
|
method: 'get', |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export const saveChannel = (params: IChannel[]) => { |
||||||
|
return globalServer({ |
||||||
|
url: '/channel/save', |
||||||
|
method: 'post', |
||||||
|
data: params, |
||||||
|
}) |
||||||
|
} |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
import { globalServer } from '../index' |
||||||
|
import type { IDeviceCategory } from './index.d' |
||||||
|
|
||||||
|
export const getDeviceTypeList = () => { |
||||||
|
return globalServer<IDeviceCategory[]>({ |
||||||
|
url: '/device/type/list', |
||||||
|
method: 'get', |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export const createDeviceType = (params: IDeviceCategory) => { |
||||||
|
return globalServer({ |
||||||
|
url: '/device/type/create', |
||||||
|
method: 'post', |
||||||
|
data: params, |
||||||
|
}) |
||||||
|
} |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
export interface IDevice { |
||||||
|
name: string |
||||||
|
ch: string |
||||||
|
point: string |
||||||
|
addr: number |
||||||
|
} |
||||||
|
|
||||||
|
export interface IDeviceCategory { |
||||||
|
name: string, |
||||||
|
fileName?: string, |
||||||
|
} |
||||||
@ -0,0 +1,18 @@ |
|||||||
|
import { globalServer } from '../index' |
||||||
|
import type { IDevice } from './index.d' |
||||||
|
|
||||||
|
export const getDeviceList = () => { |
||||||
|
return globalServer<IDevice[]>({ |
||||||
|
url: '/device/list', |
||||||
|
method: 'get', |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export const saveDevice = (params: IDevice[]) => { |
||||||
|
return globalServer({ |
||||||
|
url: '/device/update', |
||||||
|
method: 'put', |
||||||
|
data: params, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 33 KiB |
@ -0,0 +1,82 @@ |
|||||||
|
<template> |
||||||
|
<div class="exception-page"> |
||||||
|
<div class="exception-image"> |
||||||
|
<img :src="type === '403' ? permission : notFound" /> |
||||||
|
<slot name="description"></slot> |
||||||
|
</div> |
||||||
|
<div class="exception-btns"> |
||||||
|
<el-button |
||||||
|
class="exception-button" |
||||||
|
type="primary" |
||||||
|
@click=" |
||||||
|
() => { |
||||||
|
router.push('/') |
||||||
|
} |
||||||
|
" |
||||||
|
>返回首页</el-button |
||||||
|
> |
||||||
|
<el-button |
||||||
|
class="exception-button" |
||||||
|
type="primary" |
||||||
|
@click=" |
||||||
|
() => { |
||||||
|
router.back() |
||||||
|
} |
||||||
|
" |
||||||
|
>返回上一页</el-button |
||||||
|
> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<script lang="ts" setup> |
||||||
|
import { useRouter } from 'vue-router' |
||||||
|
|
||||||
|
import permission from '@/assets/images/exception/no-permission.svg' |
||||||
|
import notFound from '@/assets/images/exception/not-found.svg' |
||||||
|
|
||||||
|
defineOptions({ name: 'Exception' }) |
||||||
|
const router = useRouter() |
||||||
|
|
||||||
|
interface Props { |
||||||
|
type: '403' | '404' |
||||||
|
} |
||||||
|
defineProps<Props>() |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped lang="scss"> |
||||||
|
.exception-page { |
||||||
|
position: relative; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
|
||||||
|
.exception-image { |
||||||
|
width: 100%; |
||||||
|
height: calc(100vh - 300px); |
||||||
|
|
||||||
|
img { |
||||||
|
width: 100%; |
||||||
|
height: 92%; |
||||||
|
} |
||||||
|
} |
||||||
|
.exception-btns { |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
margin-top: 20px; |
||||||
|
|
||||||
|
.exception-button { |
||||||
|
width: 100px; |
||||||
|
margin: 0 10px; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// .exception-button { |
||||||
|
// width: 100px; |
||||||
|
// position: absolute; |
||||||
|
// top: 2%; |
||||||
|
// left: 2%; |
||||||
|
// } |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,21 @@ |
|||||||
|
<template> |
||||||
|
<div> |
||||||
|
<EdfsException type="404"> |
||||||
|
<template #description> |
||||||
|
<div class="description">抱歉,你访问的页面不存在</div> |
||||||
|
</template> |
||||||
|
</EdfsException> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script setup> |
||||||
|
import EdfsException from '@/components/Edfs-exception.vue' |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.description { |
||||||
|
text-align: center; |
||||||
|
color: #848484; |
||||||
|
height: 100px; |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,115 @@ |
|||||||
|
<template> |
||||||
|
<EdfsDialog |
||||||
|
:title="'新增工程'" |
||||||
|
:is-show="visible" |
||||||
|
width="580px" |
||||||
|
:btnLoading="btnLoading" |
||||||
|
@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" |
||||||
|
class="flex-1" |
||||||
|
placeholder="请输入工程名称" |
||||||
|
clearable |
||||||
|
/> |
||||||
|
</el-row> |
||||||
|
<el-row> |
||||||
|
<div class="label">工程描述:</div> |
||||||
|
<el-input |
||||||
|
v-model="form.description" |
||||||
|
class="flex-1" |
||||||
|
type="textarea" |
||||||
|
:rows="3" |
||||||
|
placeholder="请输入工程描述" |
||||||
|
clearable |
||||||
|
/> |
||||||
|
</el-row> |
||||||
|
</div> |
||||||
|
</EdfsDialog> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script setup lang="ts"> |
||||||
|
import { ref } from 'vue' |
||||||
|
import { cloneDeep } from 'lodash-es' |
||||||
|
import { useRouter } from 'vue-router' |
||||||
|
|
||||||
|
import { useMessage } from '@/composables/useMessage' |
||||||
|
import EdfsDialog from '@/components/Edfs-dialog.vue' |
||||||
|
import type { IEngineeringOV } from '@/api/module/engineering/index.d' |
||||||
|
import { createEngineering } from '@/api/module/engineering' |
||||||
|
|
||||||
|
const message = useMessage() |
||||||
|
const router = useRouter() |
||||||
|
|
||||||
|
const emit = defineEmits<{ |
||||||
|
'on-save': [] |
||||||
|
}>() |
||||||
|
|
||||||
|
const fromData: IEngineeringOV = { |
||||||
|
name: '', |
||||||
|
description: '', |
||||||
|
} |
||||||
|
|
||||||
|
const form = ref(cloneDeep(fromData)) |
||||||
|
const visible = ref(false) |
||||||
|
const btnLoading = ref(false) |
||||||
|
|
||||||
|
function open() { |
||||||
|
visible.value = true |
||||||
|
} |
||||||
|
|
||||||
|
async function onSave() { |
||||||
|
if (verifyData()) return |
||||||
|
|
||||||
|
btnLoading.value = true |
||||||
|
try { |
||||||
|
await createEngineering(form.value) |
||||||
|
message.success('创建成功') |
||||||
|
emit('on-save') |
||||||
|
close() |
||||||
|
router.push({ path: '/engineering-config/created' }) |
||||||
|
} catch (error) { |
||||||
|
console.error(error) |
||||||
|
} finally { |
||||||
|
btnLoading.value = false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function close() { |
||||||
|
visible.value = false |
||||||
|
form.value = cloneDeep(fromData) |
||||||
|
} |
||||||
|
|
||||||
|
function verifyData() { |
||||||
|
if (!form.value.name) { |
||||||
|
message.warning('请输入工程名称') |
||||||
|
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; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,223 @@ |
|||||||
|
<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"> |
||||||
|
<div |
||||||
|
v-for="(item, index) in modelValue" |
||||||
|
:key="index" |
||||||
|
class="bg-white rounded-lg shadow-sm p-12 border border-gray-200 hover:shadow-md transition-all duration-300 mb-4" |
||||||
|
> |
||||||
|
<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> |
||||||
|
<div class="flex items-center text-gray-600"> |
||||||
|
<span class="font-medium">端口:</span> |
||||||
|
<span class="text-gray-800 ml-2">{{ item.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> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="flex justify-end pt-3 border-t border-gray-100"> |
||||||
|
<el-button |
||||||
|
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> |
||||||
|
|
||||||
|
<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" |
||||||
|
/> |
||||||
|
</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' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
modelValue: IChannel[] |
||||||
|
}>() |
||||||
|
|
||||||
|
const emit = defineEmits<{ |
||||||
|
(e: 'update:modelValue', value: IChannel[]): void |
||||||
|
}>() |
||||||
|
|
||||||
|
const dialogVisible = ref(false) |
||||||
|
const formRef = ref<FormInstance>() |
||||||
|
const editingIndex = ref<number | null>(null) |
||||||
|
|
||||||
|
type ChannelForm = Partial< |
||||||
|
IModbusTcpChannelOV & IMqttChannelOV & { channel: 'modbusTcp' | 'mqtt' } |
||||||
|
> |
||||||
|
|
||||||
|
const form = reactive<ChannelForm>({ |
||||||
|
name: '', |
||||||
|
channel: 'modbusTcp', |
||||||
|
ip: '', |
||||||
|
port: 502, |
||||||
|
broker: '', |
||||||
|
}) |
||||||
|
|
||||||
|
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 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 |
||||||
|
} |
||||||
|
|
||||||
|
const handleDelete = (index: number) => { |
||||||
|
const newList = [...props.modelValue] |
||||||
|
newList.splice(index, 1) |
||||||
|
emit('update:modelValue', newList) |
||||||
|
} |
||||||
|
|
||||||
|
const handleSave = async () => { |
||||||
|
if (!formRef.value) return |
||||||
|
await formRef.value.validate(valid => { |
||||||
|
if (valid) { |
||||||
|
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) |
||||||
|
dialogVisible.value = false |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
/* UnoCSS handled classes */ |
||||||
|
</style> |
||||||
@ -0,0 +1,187 @@ |
|||||||
|
<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"> |
||||||
|
<div |
||||||
|
v-for="(item, index) in modelValue" |
||||||
|
:key="index" |
||||||
|
class="bg-white rounded-lg shadow-sm p-12 border border-gray-200 hover:shadow-md transition-all duration-300 mb-4" |
||||||
|
> |
||||||
|
<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> |
||||||
|
<div class="flex items-center text-gray-600"> |
||||||
|
<span class="font-medium">类别:</span> |
||||||
|
<span class="text-gray-800 ml-2">{{ 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> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="flex justify-end pt-3 border-t border-gray-100"> |
||||||
|
<el-button |
||||||
|
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> |
||||||
|
|
||||||
|
<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" |
||||||
|
/> |
||||||
|
</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' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
modelValue: IDevice[] |
||||||
|
channels: IChannel[] |
||||||
|
categories: IDeviceCategory[] |
||||||
|
}>() |
||||||
|
|
||||||
|
const emit = defineEmits<{ |
||||||
|
(e: 'update:modelValue', value: IDevice[]): void |
||||||
|
}>() |
||||||
|
|
||||||
|
const dialogVisible = ref(false) |
||||||
|
const formRef = ref<FormInstance>() |
||||||
|
const editingIndex = ref<number | null>(null) |
||||||
|
|
||||||
|
const form = reactive<IDevice>({ |
||||||
|
name: '', |
||||||
|
ch: '', |
||||||
|
point: '', |
||||||
|
addr: 1, |
||||||
|
}) |
||||||
|
|
||||||
|
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 handleAdd = () => { |
||||||
|
editingIndex.value = null |
||||||
|
form.name = '' |
||||||
|
form.ch = '' |
||||||
|
form.point = '' |
||||||
|
form.addr = 1 |
||||||
|
dialogVisible.value = true |
||||||
|
} |
||||||
|
|
||||||
|
const handleDelete = (index: number) => { |
||||||
|
const newList = [...props.modelValue] |
||||||
|
newList.splice(index, 1) |
||||||
|
emit('update:modelValue', newList) |
||||||
|
} |
||||||
|
|
||||||
|
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 |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
/* UnoCSS handled classes */ |
||||||
|
</style> |
||||||
@ -0,0 +1,230 @@ |
|||||||
|
<template> |
||||||
|
<div class="step-device-category 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"> |
||||||
|
<div |
||||||
|
v-for="(item, index) in modelValue" |
||||||
|
:key="index" |
||||||
|
class="bg-white rounded-lg shadow-sm p-4 border border-gray-200 hover:shadow-md transition-all duration-300" |
||||||
|
> |
||||||
|
<div class="flex justify-between items-start mb-3"> |
||||||
|
<div class="flex-1 min-w-0 flex items-center justify-between"> |
||||||
|
<h3 |
||||||
|
class="font-bold text-lg text-gray-900 truncate mb-1" |
||||||
|
:title="item.name" |
||||||
|
> |
||||||
|
{{ item.name }} |
||||||
|
</h3> |
||||||
|
<el-tag v-if="item.fileName" size="small" type="success" effect="light"> |
||||||
|
已上传 |
||||||
|
</el-tag> |
||||||
|
<el-tag v-else size="small" type="info" effect="light"> 未上传 </el-tag> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- Upload Area or File Display --> |
||||||
|
<div> |
||||||
|
<div class="space-y-2"> |
||||||
|
<div class="text-sm"> |
||||||
|
<div class="flex items-center"> |
||||||
|
<div class="flex-1 flex gap-6"> |
||||||
|
<p class="text-gray-500 mb-0.5">点表文件:</p> |
||||||
|
<p |
||||||
|
class="text-sm font-medium text-gray-900 truncate" |
||||||
|
:title="item.fileName" |
||||||
|
> |
||||||
|
{{ item.fileName }} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="upload-area"> |
||||||
|
<el-upload |
||||||
|
class="upload-demo" |
||||||
|
drag |
||||||
|
:auto-upload="false" |
||||||
|
:limit="1" |
||||||
|
:show-file-list="false" |
||||||
|
:on-change="file => handleFileChange(file, index)" |
||||||
|
> |
||||||
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon> |
||||||
|
<div class="el-upload__text"> |
||||||
|
拖拽文件到此处<br />或<em>点击{{ item.fileName ? '重新' : '' }}上传</em> |
||||||
|
</div> |
||||||
|
</el-upload> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="flex justify-end pt-3 border-t border-gray-100"> |
||||||
|
<div class="flex gap-2"> |
||||||
|
<el-button type="primary" size="small" text @click="handleEdit(index)"> |
||||||
|
编辑 |
||||||
|
</el-button> |
||||||
|
<el-button type="danger" size="small" text @click="handleDelete(index)"> |
||||||
|
删除 |
||||||
|
</el-button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<el-empty |
||||||
|
v-if="modelValue.length === 0" |
||||||
|
description="暂无设备类别,请点击上方按钮添加" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- Create/Edit Category Dialog --> |
||||||
|
<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> |
||||||
|
<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 { UploadFilled } from '@element-plus/icons-vue' |
||||||
|
import type { IDeviceCategory } from '@/api/module/device/index.d' |
||||||
|
import type { FormInstance, FormRules, UploadFile } from 'element-plus' |
||||||
|
import { ElMessage } from 'element-plus' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
modelValue: IDeviceCategory[] |
||||||
|
}>() |
||||||
|
|
||||||
|
const emit = defineEmits<{ |
||||||
|
(e: 'update:modelValue', value: IDeviceCategory[]): void |
||||||
|
}>() |
||||||
|
|
||||||
|
const dialogVisible = ref(false) |
||||||
|
const formRef = ref<FormInstance>() |
||||||
|
const editingIndex = ref<number | null>(null) |
||||||
|
|
||||||
|
const form = reactive<IDeviceCategory>({ |
||||||
|
name: '', |
||||||
|
fileName: '', |
||||||
|
}) |
||||||
|
|
||||||
|
const rules = reactive<FormRules>({ |
||||||
|
name: [{ required: true, message: '请输入类别名称', trigger: 'blur' }], |
||||||
|
}) |
||||||
|
|
||||||
|
const handleAdd = () => { |
||||||
|
editingIndex.value = null |
||||||
|
form.name = '' |
||||||
|
form.fileName = '' |
||||||
|
dialogVisible.value = true |
||||||
|
} |
||||||
|
|
||||||
|
const handleEdit = (index: number) => { |
||||||
|
editingIndex.value = index |
||||||
|
const item = props.modelValue[index] |
||||||
|
form.name = item.name |
||||||
|
form.fileName = item.fileName || '' |
||||||
|
dialogVisible.value = true |
||||||
|
} |
||||||
|
|
||||||
|
const handleDelete = (index: number) => { |
||||||
|
const newList = [...props.modelValue] |
||||||
|
newList.splice(index, 1) |
||||||
|
emit('update:modelValue', newList) |
||||||
|
} |
||||||
|
|
||||||
|
const handleSave = async () => { |
||||||
|
if (!formRef.value) return |
||||||
|
await formRef.value.validate(valid => { |
||||||
|
if (valid) { |
||||||
|
const newList = [...props.modelValue] |
||||||
|
if (editingIndex.value !== null) { |
||||||
|
// Edit mode - keep existing fileName |
||||||
|
newList[editingIndex.value] = { |
||||||
|
name: form.name, |
||||||
|
fileName: newList[editingIndex.value].fileName || '', |
||||||
|
} |
||||||
|
} else { |
||||||
|
// Add mode - no fileName yet |
||||||
|
newList.push({ |
||||||
|
name: form.name, |
||||||
|
fileName: '', |
||||||
|
}) |
||||||
|
} |
||||||
|
emit('update:modelValue', newList) |
||||||
|
dialogVisible.value = false |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const handleFileChange = async (file: UploadFile, index: number) => { |
||||||
|
if (!file.raw) return |
||||||
|
|
||||||
|
try { |
||||||
|
// Mock: extract filename from the uploaded file |
||||||
|
const fileName = file.name |
||||||
|
|
||||||
|
// Simulate upload delay |
||||||
|
await new Promise(resolve => setTimeout(resolve, 500)) |
||||||
|
|
||||||
|
// Update the category with the filename |
||||||
|
const newList = [...props.modelValue] |
||||||
|
newList[index] = { |
||||||
|
...newList[index], |
||||||
|
fileName: fileName, |
||||||
|
} |
||||||
|
emit('update:modelValue', newList) |
||||||
|
ElMessage.success('文件上传成功') |
||||||
|
} catch (error) { |
||||||
|
ElMessage.error('文件上传失败') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const handleReupload = (index: number) => { |
||||||
|
// Clear the filename to show upload area again |
||||||
|
const newList = [...props.modelValue] |
||||||
|
newList[index] = { |
||||||
|
...newList[index], |
||||||
|
fileName: '', |
||||||
|
} |
||||||
|
emit('update:modelValue', newList) |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
.upload-demo { |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.el-upload-dragger) { |
||||||
|
padding: 20px; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.el-icon--upload) { |
||||||
|
font-size: 40px; |
||||||
|
color: #409eff; |
||||||
|
margin-bottom: 8px; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.el-upload__text) { |
||||||
|
font-size: 14px; |
||||||
|
line-height: 1.5; |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,194 @@ |
|||||||
|
<template> |
||||||
|
<div class="engineering-config h-full flex flex-col bg-white"> |
||||||
|
<div class="border-b border-gray-200"> |
||||||
|
<div class="px-6 py-4"> |
||||||
|
<el-page-header @back="goBack" title="返回"> |
||||||
|
<template #content> |
||||||
|
<div class="flex flex-col gap-1"> |
||||||
|
<div class="flex items-center gap-3"> |
||||||
|
<span class="text-xl font-bold text-gray-900">{{ |
||||||
|
projectName || '未命名工程' |
||||||
|
}}</span> |
||||||
|
<el-tag type="success" v-if="projectVersion">{{ projectVersion }}</el-tag> |
||||||
|
</div> |
||||||
|
<span class="text-xs text-gray-500">工程配置</span> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</el-page-header> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="px-6 pb-4"> |
||||||
|
<div class="w-full"> |
||||||
|
<el-steps :active="activeStep" finish-status="success" simple align-center> |
||||||
|
<el-step title="通道管理" :icon="Connection" /> |
||||||
|
<el-step title="设备类别" :icon="Collection" /> |
||||||
|
<el-step title="设备管理" :icon="Monitor" /> |
||||||
|
</el-steps> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="flex-1 overflow-hidden flex flex-col"> |
||||||
|
<div class="flex-1 overflow-hidden p-6"> |
||||||
|
<transition name="fade" mode="out-in"> |
||||||
|
<keep-alive> |
||||||
|
<component |
||||||
|
:is="currentStepComponent" |
||||||
|
v-model="currentModel" |
||||||
|
v-bind="currentProps" |
||||||
|
/> |
||||||
|
</keep-alive> |
||||||
|
</transition> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="p-4 border-t border-gray-100 flex justify-center gap-4 bg-white"> |
||||||
|
<el-button v-if="activeStep > 0" @click="prevStep" :icon="ArrowLeft" |
||||||
|
>上一步</el-button |
||||||
|
> |
||||||
|
<el-button v-if="activeStep < 2" type="primary" @click="nextStep"> |
||||||
|
下一步<el-icon class="el-icon--right"><ArrowRight /></el-icon> |
||||||
|
</el-button> |
||||||
|
<el-button |
||||||
|
v-if="activeStep === 2" |
||||||
|
type="success" |
||||||
|
@click="handleFinish" |
||||||
|
:icon="Check" |
||||||
|
>完成</el-button |
||||||
|
> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script setup lang="ts"> |
||||||
|
import { ref, computed } from 'vue' |
||||||
|
import { useRouter, useRoute } from 'vue-router' |
||||||
|
import { ElMessage } from 'element-plus' |
||||||
|
import { |
||||||
|
Connection, |
||||||
|
Collection, |
||||||
|
Monitor, |
||||||
|
ArrowLeft, |
||||||
|
ArrowRight, |
||||||
|
Check, |
||||||
|
} from '@element-plus/icons-vue' |
||||||
|
import StepChannel from './components/StepChannel.vue' |
||||||
|
import StepDeviceCategory from './components/StepDeviceCategory.vue' |
||||||
|
import StepDevice from './components/StepDevice.vue' |
||||||
|
|
||||||
|
import type { IChannel } from '@/api/module/channel/index.d' |
||||||
|
import type { IDevice, IDeviceCategory } from '@/api/module/device/index.d' |
||||||
|
|
||||||
|
const router = useRouter() |
||||||
|
const route = useRoute() |
||||||
|
|
||||||
|
const projectName = computed(() => route.query.name as string) |
||||||
|
const projectVersion = computed(() => route.query.version as string) |
||||||
|
|
||||||
|
const activeStep = ref(0) |
||||||
|
const channels = ref<IChannel[]>([]) |
||||||
|
const categories = ref<IDeviceCategory[]>([]) |
||||||
|
const devices = ref<IDevice[]>([]) |
||||||
|
|
||||||
|
const currentStepComponent = computed(() => { |
||||||
|
switch (activeStep.value) { |
||||||
|
case 0: |
||||||
|
return StepChannel |
||||||
|
case 1: |
||||||
|
return StepDeviceCategory |
||||||
|
case 2: |
||||||
|
return StepDevice |
||||||
|
default: |
||||||
|
return null |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const currentModel = computed({ |
||||||
|
get: () => { |
||||||
|
switch (activeStep.value) { |
||||||
|
case 0: |
||||||
|
return channels.value |
||||||
|
case 1: |
||||||
|
return categories.value |
||||||
|
case 2: |
||||||
|
return devices.value |
||||||
|
default: |
||||||
|
return [] |
||||||
|
} |
||||||
|
}, |
||||||
|
set: (val: any) => { |
||||||
|
switch (activeStep.value) { |
||||||
|
case 0: |
||||||
|
channels.value = val |
||||||
|
break |
||||||
|
case 1: |
||||||
|
categories.value = val |
||||||
|
break |
||||||
|
case 2: |
||||||
|
devices.value = val |
||||||
|
break |
||||||
|
} |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
const currentProps = computed(() => { |
||||||
|
if (activeStep.value === 2) { |
||||||
|
return { |
||||||
|
channels: channels.value, |
||||||
|
categories: categories.value, |
||||||
|
} |
||||||
|
} |
||||||
|
return {} |
||||||
|
}) |
||||||
|
|
||||||
|
const goBack = () => { |
||||||
|
router.push('/engineering') |
||||||
|
} |
||||||
|
|
||||||
|
const prevStep = () => { |
||||||
|
if (activeStep.value > 0) { |
||||||
|
activeStep.value-- |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const nextStep = () => { |
||||||
|
activeStep.value++ |
||||||
|
} |
||||||
|
|
||||||
|
const handleFinish = () => { |
||||||
|
ElMessage.success('配置完成') |
||||||
|
goBack() |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
.fade-enter-active, |
||||||
|
.fade-leave-active { |
||||||
|
transition: opacity 0.3s ease; |
||||||
|
} |
||||||
|
|
||||||
|
.fade-enter-from, |
||||||
|
.fade-leave-to { |
||||||
|
opacity: 0; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.el-page-header__back) { |
||||||
|
cursor: pointer; |
||||||
|
padding: 4px 8px; |
||||||
|
border-radius: 4px; |
||||||
|
transition: all 0.2s; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.el-page-header__back:hover) { |
||||||
|
background-color: rgba(64, 158, 255, 0.1); |
||||||
|
color: var(--el-color-primary); |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.el-page-header__back:active) { |
||||||
|
background-color: rgba(64, 158, 255, 0.2); |
||||||
|
transform: scale(0.95); |
||||||
|
} |
||||||
|
:deep(.el-steps--simple) { |
||||||
|
padding-left: 16%; |
||||||
|
} |
||||||
|
</style> |
||||||
Loading…
Reference in new issue