21 changed files with 1451 additions and 0 deletions
@ -0,0 +1,24 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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