Browse Source

feat: 基础功能添加

main
betaqi 2 months ago
parent
commit
a41b19ffbb
  1. 24
      public/commun_config_modbustcp/commun_channel_moebustcp.json
  2. 34
      public/commun_config_modbustcp/commun_dev_modbustcp.json
  3. 22
      public/commun_config_modbustcp/index.json
  4. 61
      public/commun_config_modbustcp/point_table_modbus_01.json
  5. 61
      public/commun_config_modbustcp/point_table_modbus_02.json
  6. 61
      public/commun_config_modbustcp/point_table_modbus_03.json
  7. 53
      public/commun_config_modbustcp/工程说明.md
  8. 18
      src/api/module/channel/index.d.ts
  9. 17
      src/api/module/channel/index.ts
  10. 17
      src/api/module/device/category.ts
  11. 11
      src/api/module/device/index.d.ts
  12. 18
      src/api/module/device/index.ts
  13. 1
      src/assets/images/exception/no-permission.svg
  14. 1
      src/assets/images/exception/not-found.svg
  15. 82
      src/components/Edfs-exception.vue
  16. 21
      src/views/Error/404.vue
  17. 115
      src/views/engineering/components/create-engineering-dlg.vue
  18. 223
      src/views/engineering/config/components/StepChannel.vue
  19. 187
      src/views/engineering/config/components/StepDevice.vue
  20. 230
      src/views/engineering/config/components/StepDeviceCategory.vue
  21. 194
      src/views/engineering/config/index.vue

24
public/commun_config_modbustcp/commun_channel_moebustcp.json

@ -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
}
]
}

34
public/commun_config_modbustcp/commun_dev_modbustcp.json

@ -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"
}
]
}

22
public/commun_config_modbustcp/index.json

@ -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"
}
}
}

61
public/commun_config_modbustcp/point_table_modbus_01.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,
"点位段名称"
]
]
}

61
public/commun_config_modbustcp/point_table_modbus_02.json

@ -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,
"点位段名称"
]
]
}

61
public/commun_config_modbustcp/point_table_modbus_03.json

@ -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,
"点位段名称"
]
]
}

53
public/commun_config_modbustcp/工程说明.md

@ -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
```

18
src/api/module/channel/index.d.ts vendored

@ -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
}

17
src/api/module/channel/index.ts

@ -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,
})
}

17
src/api/module/device/category.ts

@ -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,
})
}

11
src/api/module/device/index.d.ts vendored

@ -0,0 +1,11 @@
export interface IDevice {
name: string
ch: string
point: string
addr: number
}
export interface IDeviceCategory {
name: string,
fileName?: string,
}

18
src/api/module/device/index.ts

@ -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,
})
}

1
src/assets/images/exception/no-permission.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 50 KiB

1
src/assets/images/exception/not-found.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 33 KiB

82
src/components/Edfs-exception.vue

@ -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>

21
src/views/Error/404.vue

@ -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>

115
src/views/engineering/components/create-engineering-dlg.vue

@ -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>

223
src/views/engineering/config/components/StepChannel.vue

@ -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>

187
src/views/engineering/config/components/StepDevice.vue

@ -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>

230
src/views/engineering/config/components/StepDeviceCategory.vue

@ -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>

194
src/views/engineering/config/index.vue

@ -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…
Cancel
Save