From db0a1198773c95a2680887d23d0fbaba7f8475de Mon Sep 17 00:00:00 2001
From: dengzedong <dengzedong@email>
Date: 星期三, 18 九月 2024 09:25:00 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/master'

---
 src/views/data/channel/opcua/tag/TagForm.vue  |  143 ++++++
 src/views/data/channel/opcua/tag/index.vue    |  221 +++++++++
 src/utils/constants.ts                        |    5 
 src/views/data/channel/opcda/tag/TagForm.vue  |  141 ++++++
 src/views/data/channel/kio/tag/TagForm.vue    |  162 +++++++
 src/views/data/channel/opcua/index.vue        |   58 +
 src/api/data/channel/opcua/tag.ts             |   41 +
 src/api/data/channel/opcda/index.ts           |    2 
 src/api/data/channel/opcua/index.ts           |    4 
 src/api/data/channel/opcda/tag.ts             |   40 +
 src/views/data/channel/modbus/tag/TagForm.vue |    1 
 src/views/data/channel/opcda/index.vue        |   24 
 src/views/data/channel/opcda/tag/index.vue    |  198 ++++++++
 src/views/data/channel/kio/index.vue          |   46 +
 src/views/data/channel/kio/tag/index.vue      |  214 +++++++++
 src/api/data/channel/kio/tag.ts               |   42 +
 16 files changed, 1,301 insertions(+), 41 deletions(-)

diff --git a/src/api/data/channel/kio/tag.ts b/src/api/data/channel/kio/tag.ts
new file mode 100644
index 0000000..32a714e
--- /dev/null
+++ b/src/api/data/channel/kio/tag.ts
@@ -0,0 +1,42 @@
+import request from '@/config/axios'
+
+export interface KioTagVO {
+  id: string
+  tagName: string
+  dataType: string
+  tagId: number
+  tagDesc: string
+  enabled: boolean
+  device: string
+  samplingRate: number
+}
+
+export interface KioTagPageReqVO extends PageParam {
+  tagName?: string
+  tagDesc?: string
+}
+
+// 查询KioTag列表
+export const getKioTagPage = (params: KioTagPageReqVO) => {
+  return request.get({ url: '/data/channel/kio/tag/page', params })
+}
+
+// 查询KioTag详情
+export const getKioTag = (id: number) => {
+  return request.get({ url: `/data/channel/kio/tag/info/${id}`})
+}
+
+// 新增KioTag
+export const createKioTag = (data: KioTagVO) => {
+  return request.post({ url: '/data/channel/kio/tag/create', data })
+}
+
+// 修改KioTag
+export const updateKioTag = (data: KioTagVO) => {
+  return request.put({ url: '/data/channel/kio/tag/update', data })
+}
+
+// 删除KioTag
+export const deleteKioTag = (id: number) => {
+  return request.delete({ url: '/data/channel/kio/tag/delete?id=' + id })
+}
diff --git a/src/api/data/channel/opcda/index.ts b/src/api/data/channel/opcda/index.ts
index 2347d4a..9dccd36 100644
--- a/src/api/data/channel/opcda/index.ts
+++ b/src/api/data/channel/opcda/index.ts
@@ -26,7 +26,7 @@
 
 // 新增OpcDaDevice
 export const createOpcDaDevice = (data: OpcDaDeviceVO) => {
-  return request.post({ url: '/data/channel/opcda/device/add', data })
+  return request.post({ url: '/data/channel/opcda/device/create', data })
 }
 
 // 修改OpcDaDevice
diff --git a/src/api/data/channel/opcda/tag.ts b/src/api/data/channel/opcda/tag.ts
new file mode 100644
index 0000000..4f90eb1
--- /dev/null
+++ b/src/api/data/channel/opcda/tag.ts
@@ -0,0 +1,40 @@
+import request from '@/config/axios'
+
+export interface OpcdaTagVO {
+  id: string
+  serverId: string
+  tagName: string
+  dataType: string
+  enabled: boolean
+  itemId: string
+}
+
+export interface OpcdaTagPageReqVO extends PageParam {
+  serverId?: string
+  tagName?: string
+}
+
+// 查询OpcdaTag列表
+export const getOpcdaTagPage = (params: OpcdaTagPageReqVO) => {
+  return request.get({ url: '/data/channel/opcda/tag/page', params })
+}
+
+// 查询OpcdaTag详情
+export const getOpcdaTag = (id: number) => {
+  return request.get({ url: `/data/channel/opcda/tag/info/${id}`})
+}
+
+// 新增OpcdaTag
+export const createOpcdaTag = (data: OpcdaTagVO) => {
+  return request.post({ url: '/data/channel/opcda/tag/create', data })
+}
+
+// 修改OpcdaTag
+export const updateOpcdaTag = (data: OpcdaTagVO) => {
+  return request.put({ url: '/data/channel/opcda/tag/update', data })
+}
+
+// 删除OpcdaTag
+export const deleteOpcdaTag = (id: number) => {
+  return request.delete({ url: '/data/channel/opcda/tag/delete?id=' + id })
+}
diff --git a/src/api/data/channel/opcua/index.ts b/src/api/data/channel/opcua/index.ts
index f553a41..c9edeef 100644
--- a/src/api/data/channel/opcua/index.ts
+++ b/src/api/data/channel/opcua/index.ts
@@ -30,7 +30,7 @@
 
 // 新增OpcUaDevice
 export const createOpcUaDevice = (data: OpcUaDeviceVO) => {
-  return request.post({ url: '/data/channel/opcua/device/add', data })
+  return request.post({ url: '/data/channel/opcua/device/create', data })
 }
 
 // 修改OpcUaDevice
@@ -39,6 +39,6 @@
 }
 
 // 删除OpcUaDevice
-export const deleteOpcUaDevice = (id: number) => {
+export const deleteOpcUaDevice = (id: string) => {
   return request.delete({ url: '/data/channel/opcua/device/delete?id=' + id })
 }
diff --git a/src/api/data/channel/opcua/tag.ts b/src/api/data/channel/opcua/tag.ts
new file mode 100644
index 0000000..e1373fb
--- /dev/null
+++ b/src/api/data/channel/opcua/tag.ts
@@ -0,0 +1,41 @@
+import request from '@/config/axios'
+
+export interface OpcuaTagVO {
+  id: string
+  device: string
+  tagName: string
+  dataType: string
+  enabled: boolean
+  address: string
+  samplingRate: number
+}
+
+export interface OpcuaTagPageReqVO extends PageParam {
+  device?: string
+  tagName?: string
+}
+
+// 查询OpcuaTag列表
+export const getOpcuaTagPage = (params: OpcuaTagPageReqVO) => {
+  return request.get({ url: '/data/channel/opcua/tag/page', params })
+}
+
+// 查询OpcuaTag详情
+export const getOpcuaTag = (id: number) => {
+  return request.get({ url: `/data/channel/opcua/tag/info/${id}`})
+}
+
+// 新增OpcuaTag
+export const createOpcuaTag = (data: OpcuaTagVO) => {
+  return request.post({ url: '/data/channel/opcua/tag/create', data })
+}
+
+// 修改OpcuaTag
+export const updateOpcuaTag = (data: OpcuaTagVO) => {
+  return request.put({ url: '/data/channel/opcua/tag/update', data })
+}
+
+// 删除OpcuaTag
+export const deleteOpcuaTag = (id: number) => {
+  return request.delete({ url: '/data/channel/opcua/tag/delete?id=' + id })
+}
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 360cf05..d0b4888 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -451,3 +451,8 @@
   ENABLE: 1, // 启用
   DISABLE: 0 // 禁用
 }
+
+export const CommonEnabledBool = {
+  ENABLE: true, // 启用
+  DISABLE: false // 禁用
+}
diff --git a/src/views/data/channel/kio/index.vue b/src/views/data/channel/kio/index.vue
index 36cea40..eeeb0fc 100644
--- a/src/views/data/channel/kio/index.vue
+++ b/src/views/data/channel/kio/index.vue
@@ -19,20 +19,20 @@
       </el-form-item>
       <el-form-item>
         <el-button @click="handleQuery">
-          <Icon icon="ep:search" class="mr-5px" />
+          <Icon icon="ep:search" class="mr-5px"/>
           搜索
         </el-button>
         <el-button @click="resetQuery">
-          <Icon icon="ep:refresh" class="mr-5px" />
+          <Icon icon="ep:refresh" class="mr-5px"/>
           重置
         </el-button>
         <el-button
           type="primary"
           plain
           @click="openForm('create')"
-          v-hasPermi="['system:tenant:create']"
+          v-hasPermi="['data:channel-kio:create']"
         >
-          <Icon icon="ep:plus" class="mr-5px" />
+          <Icon icon="ep:plus" class="mr-5px"/>
           新增
         </el-button>
       </el-form-item>
@@ -42,25 +42,33 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
-      <el-table-column label="实例名称" align="center" prop="instanceName" />
-      <el-table-column label="IP地址" align="center" prop="address" />
-      <el-table-column label="端口" align="center" prop="port" />
-      <el-table-column label="用户名" align="center" prop="username" />
+      <el-table-column label="实例名称" align="center" prop="instanceName"/>
+      <el-table-column label="IP地址" align="center" prop="address"/>
+      <el-table-column label="端口" align="center" prop="port"/>
+      <el-table-column label="用户名" align="center" prop="username"/>
       <el-table-column label="操作" align="center" min-width="110" fixed="right">
         <template #default="scope">
           <el-button
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
-            v-hasPermi="['system:tenant:update']"
+            v-hasPermi="['data:channel-kio:update']"
           >
             编辑
           </el-button>
           <el-button
             link
+            type="primary"
+            @click="openTagList(scope.row.name)"
+            v-hasPermi="['data:channel-kio:update']"
+          >
+            TAG
+          </el-button>
+          <el-button
+            link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['system:tenant:delete']"
+            v-hasPermi="['data:channel-kio:delete']"
           >
             删除
           </el-button>
@@ -77,14 +85,18 @@
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->
-  <KioDeviceForm ref="formRef" @success="getList" />
+  <KioDeviceForm ref="formRef" @success="getList"/>
+
+  <!-- TAG弹窗:添加/修改 -->
+  <TagList ref="tagRef" @success="getList" />
 
 </template>
 <script lang="ts" setup>
-import * as KioApi from '@/api/data/channel/kio'
-import KioDeviceForm from './KioDeviceForm.vue'
+  import * as KioApi from '@/api/data/channel/kio'
+  import KioDeviceForm from './KioDeviceForm.vue'
+  import TagList from './tag/index.vue'
 
-defineOptions({name: 'DataKio'})
+  defineOptions({name: 'DataKio'})
 
   const message = useMessage() // 消息弹窗
   const {t} = useI18n() // 国际化
@@ -131,6 +143,12 @@
     formRef.value.open(type, id)
   }
 
+  /** TAG操作 */
+  const tagRef = ref()
+  const openTagList = (name?: string) => {
+    tagRef.value.open(name)
+  }
+
   /** 删除按钮操作 */
   const handleDelete = async (id: number) => {
     try {
diff --git a/src/views/data/channel/kio/tag/TagForm.vue b/src/views/data/channel/kio/tag/TagForm.vue
new file mode 100644
index 0000000..1f3604e
--- /dev/null
+++ b/src/views/data/channel/kio/tag/TagForm.vue
@@ -0,0 +1,162 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="50%">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+    >
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="Tag名称" prop="tagName">
+            <el-input v-model="formData.tagName" placeholder="请输Tag名称"/>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="数据类型" prop="dataType">
+            <el-select v-model="formData.dataType" placeholder="请选择">
+              <el-option
+                v-for="dict in getStrDictOptions(DICT_TYPE.TAG_DATA_TYPE)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="采集频率" prop="samplingRate">
+            <el-input v-model="formData.samplingRate" placeholder="请输入采集频率"/>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="是否启用" prop="enabled">
+            <el-select v-model="formData.enabled" placeholder="请选择">
+              <el-option
+                v-for="dict in getBoolDictOptions(DICT_TYPE.IS_ENABLED)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="24">
+          <el-form-item label="描述" prop="tagDesc">
+            <el-input v-model="formData.tagDesc" placeholder="描述"/>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+  import * as KioTagApi from '@/api/data/channel/kio/tag'
+  import { CommonEnabled } from '@/utils/constants'
+  import {isPositiveInteger} from '@/utils/validate'
+  import { DICT_TYPE, getStrDictOptions, getBoolDictOptions } from '@/utils/dict'
+
+  defineOptions({name: 'KioTagForm'})
+
+  const {t} = useI18n() // 国际化
+  const message = useMessage() // 消息弹窗
+  const dialogVisible = ref(false) // 弹窗的是否展示
+  const dialogTitle = ref('') // 弹窗的标题
+  const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+  const formType = ref('') // 表单的类型:create - 新增;update - 修改
+  const formData = ref({
+    id: undefined,
+    tagName: undefined,
+    dataType: undefined,
+    tagId: undefined,
+    tagDesc: '',
+    enabled: CommonEnabled.ENABLE,
+    device: undefined,
+    samplingRate: undefined
+
+  })
+  const validateNum = (rule, value, callback) => {
+    if (!isPositiveInteger(value)) {
+      callback(new Error('格式不正确'))
+    } else {
+      callback()
+    }
+  }
+  const formRules = reactive({
+    tagName: [{required: true, message: 'Tag名称不能为空', trigger: 'blur'}],
+    dataType: [{required: true, message: '数据类型不能为空', trigger: 'blur'}]
+  })
+  const formRef = ref() // 表单 Ref
+
+  /** 打开弹窗 */
+  const open = async (type: string, id?: number, device?: string) => {
+    dialogVisible.value = true
+    dialogTitle.value = t('action.' + type)
+    formType.value = type
+    resetForm()
+    if (device) {
+      formData.value.device = device
+    }
+    // 修改时,设置数据
+    if (id) {
+      formLoading.value = true
+      try {
+        formData.value = await KioTagApi.getKioTag(id)
+      } finally {
+        formLoading.value = false
+      }
+    }
+  }
+  defineExpose({open}) // 提供 open 方法,用于打开弹窗
+
+  /** 提交表单 */
+  const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+  const submitForm = async () => {
+    // 校验表单
+    if (!formRef) return
+    const valid = await formRef.value.validate()
+    if (!valid) return
+    // 提交请求
+    formLoading.value = true
+    try {
+      const data = formData.value as unknown as KioTagApi.KioTagVO
+      if (formType.value === 'create') {
+        await KioTagApi.createKioTag(data)
+        message.success(t('common.createSuccess'))
+      } else {
+        await KioTagApi.updateKioTag(data)
+        message.success(t('common.updateSuccess'))
+      }
+      dialogVisible.value = false
+      // 发送操作成功的事件
+      emit('success')
+    } finally {
+      formLoading.value = false
+    }
+  }
+
+  /** 重置表单 */
+  const resetForm = () => {
+    formData.value = {
+      id: undefined,
+      tagName: undefined,
+      dataType: undefined,
+      tagId: undefined,
+      tagDesc: '',
+      enabled: CommonEnabled.ENABLE,
+      device: undefined,
+      samplingRate: undefined
+    }
+    formRef.value?.resetFields()
+  }
+</script>
+ss
diff --git a/src/views/data/channel/kio/tag/index.vue b/src/views/data/channel/kio/tag/index.vue
new file mode 100644
index 0000000..32d489a
--- /dev/null
+++ b/src/views/data/channel/kio/tag/index.vue
@@ -0,0 +1,214 @@
+<template>
+  <el-drawer
+    v-model="drawer"
+    size="50%"
+    title="Kio Tag"
+    :direction="direction"
+    :before-close="handleClose"
+  >
+    <!-- 搜索 -->
+    <ContentWrap>
+      <el-form
+        class="-mb-15px"
+        :model="queryParams"
+        ref="queryFormRef"
+        :inline="true"
+        label-width="68px"
+      >
+        <el-form-item label="Tag名称" prop="tagName">
+          <el-input
+            v-model="queryParams.tagName"
+            placeholder="请输入Tag名称"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-240px"
+          />
+        </el-form-item>
+        <el-form-item label="地址" prop="address">
+          <el-input
+            v-model="queryParams.address"
+            placeholder="请输入Modbus地址"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-240px"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="handleQuery">
+            <Icon icon="ep:search" class="mr-5px" />
+            搜索
+          </el-button>
+          <el-button @click="resetQuery">
+            <Icon icon="ep:refresh" class="mr-5px" />
+            重置
+          </el-button>
+          <el-button
+            type="primary"
+            plain
+            @click="openForm('create')"
+            v-hasPermi="['data:channel-kio:create']"
+          >
+            <Icon icon="ep:plus" class="mr-5px" />
+            新增
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </ContentWrap>
+    <!-- 列表 -->
+    <ContentWrap>
+      <el-table v-loading="loading" :data="list">
+        <el-table-column
+          prop="tagName"
+          label="Tag名称"
+          header-align="center"
+          align="left"
+          min-width="150"
+        />
+        <el-table-column
+          prop="tagDesc"
+          label="Tag描述"
+          header-align="center"
+          align="left"
+          min-width="150"
+        />
+        <el-table-column
+          prop="dataType"
+          label="数据类型"
+          header-align="center"
+          align="center"
+        />
+        <el-table-column
+          prop="enabled"
+          label="是否启用"
+          header-align="center"
+          align="center"
+        >
+          <template #default="scope">
+            <el-tag v-if="scope.row.enabled === true" size="small">是</el-tag>
+            <el-tag v-else size="small" type="danger">否</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" min-width="110" fixed="right">
+          <template #default="scope">
+            <el-button
+              link
+              type="primary"
+              @click="openForm('update', scope.row.id)"
+              v-hasPermi="['data:channel-kio:update']"
+            >
+              编辑
+            </el-button>
+            <el-button
+              link
+              type="danger"
+              @click="handleDelete(scope.row.id)"
+              v-hasPermi="['data:channel-kio:delete']"
+            >
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <!-- 分页 -->
+      <Pagination
+        :total="total"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
+      />
+    </ContentWrap>
+    <!-- 表单弹窗:添加/修改 -->
+    <TagForm ref="formRef" @success="getList" />
+  </el-drawer>
+</template>
+<script lang="ts" setup>
+  import type { DrawerProps } from 'element-plus'
+  import * as KioTagApi from "@/api/data/channel/kio/tag";
+  import TagForm from './TagForm.vue'
+
+  defineOptions({name: 'KioTag'})
+
+  const message = useMessage() // 消息弹窗
+  const {t} = useI18n() // 国际化
+
+  const drawer = ref(false)
+  const direction = ref<DrawerProps['direction']>('rtl')
+  const loading = ref(true) // 列表的加载中
+  const total = ref(0) // 列表的总页数
+  const list = ref([]) // 列表的数据
+  const queryParams = reactive({
+    pageNo: 1,
+    pageSize: 10,
+    device: undefined,
+    tagName: undefined
+  })
+  const queryFormRef = ref() // 搜索的表单
+  const exportLoading = ref(false) // 导出的加载中
+
+  /** 查询列表 */
+  const getList = async () => {
+    loading.value = true
+    try {
+      const page = await KioTagApi.getKioTagPage(queryParams)
+      list.value = page.list
+      total.value = page.total
+    } finally {
+      loading.value = false
+    }
+  }
+
+  /** 搜索按钮操作 */
+  const handleQuery = () => {
+    queryParams.pageNo = 1
+    getList()
+  }
+
+  /** 重置按钮操作 */
+  const resetQuery = () => {
+    queryFormRef.value.resetFields()
+    handleQuery()
+  }
+
+  /** 添加/修改操作 */
+  const formRef = ref()
+  const openForm = (type: string, id?: number) => {
+    formRef.value.open(type, id, queryParams.device)
+  }
+
+  /** 删除按钮操作 */
+  const handleDelete = async (id: number) => {
+    try {
+      // 删除的二次确认
+      await message.delConfirm()
+      // 发起删除
+      await KioTagApi.deleteKioTag(id)
+      message.success(t('common.delSuccess'))
+      // 刷新列表
+      await getList()
+    } catch {
+    }
+  }
+
+  /** 打开弹窗 */
+  const open = async (device?: string) => {
+    resetForm()
+    drawer.value = true
+    queryParams.device = device
+    if (device) {
+      getList()
+    }
+  }
+  defineExpose({open}) // 提供 open 方法,用于打开弹窗
+
+  /** 重置表单 */
+  const resetForm = () => {
+    queryParams.pageNo = 1
+    queryParams.pageSize = 10
+    queryParams.device = ''
+    queryParams.tagName = ''
+  }
+
+  const handleClose = (done: () => void) => {
+    drawer.value = false
+  }
+</script>
diff --git a/src/views/data/channel/modbus/tag/TagForm.vue b/src/views/data/channel/modbus/tag/TagForm.vue
index fc70722..a8ec15d 100644
--- a/src/views/data/channel/modbus/tag/TagForm.vue
+++ b/src/views/data/channel/modbus/tag/TagForm.vue
@@ -133,7 +133,6 @@
       formLoading.value = true
       try {
         formData.value = await ModBusTagApi.getModBusTag(id)
-        formData.device = device
       } finally {
         formLoading.value = false
       }
diff --git a/src/views/data/channel/opcda/index.vue b/src/views/data/channel/opcda/index.vue
index d9b62fb..b538779 100644
--- a/src/views/data/channel/opcda/index.vue
+++ b/src/views/data/channel/opcda/index.vue
@@ -30,7 +30,7 @@
           type="primary"
           plain
           @click="openForm('create')"
-          v-hasPermi="['system:tenant:create']"
+          v-hasPermi="['data:channel-opcda:create']"
         >
           <Icon icon="ep:plus" class="mr-5px" />
           新增
@@ -53,15 +53,23 @@
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
-            v-hasPermi="['system:tenant:update']"
+            v-hasPermi="['data:channel-opcda:update']"
           >
             编辑
           </el-button>
           <el-button
             link
+            type="primary"
+            @click="openTagList(scope.row.id)"
+            v-hasPermi="['data:channel-opcda:update']"
+          >
+            TAG
+          </el-button>
+          <el-button
+            link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['system:tenant:delete']"
+            v-hasPermi="['data:channel-opcda:delete']"
           >
             删除
           </el-button>
@@ -80,10 +88,14 @@
   <!-- 表单弹窗:添加/修改 -->
   <OpcDaDeviceForm ref="formRef" @success="getList" />
 
+  <!-- TAG弹窗:添加/修改 -->
+  <TagList ref="tagRef" @success="getList" />
+
 </template>
 <script lang="ts" setup>
 import * as OpcDaApi from '@/api/data/channel/opcda'
 import OpcDaDeviceForm from './OpcDaDeviceForm.vue'
+import TagList from './tag/index.vue'
 
 defineOptions({name: 'DataOpcDa'})
 
@@ -131,6 +143,12 @@
     formRef.value.open(type, id)
   }
 
+  /** TAG操作 */
+  const tagRef = ref()
+  const openTagList = (id?: string) => {
+    tagRef.value.open(id)
+  }
+
   /** 删除按钮操作 */
   const handleDelete = async (id: number) => {
     try {
diff --git a/src/views/data/channel/opcda/tag/TagForm.vue b/src/views/data/channel/opcda/tag/TagForm.vue
new file mode 100644
index 0000000..9f4b6ab
--- /dev/null
+++ b/src/views/data/channel/opcda/tag/TagForm.vue
@@ -0,0 +1,141 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="50%">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+    >
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="Tag名称" prop="tagName">
+            <el-input v-model="formData.tagName" placeholder="请输Tag名称"/>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="数据类型" prop="dataType">
+            <el-select v-model="formData.dataType" placeholder="请选择">
+              <el-option
+                v-for="dict in getStrDictOptions(DICT_TYPE.TAG_DATA_TYPE)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="itemId" prop="itemId">
+            <el-input v-model="formData.itemId" placeholder="请输入ItemId"/>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="是否启用" prop="enabled">
+            <el-select v-model="formData.enabled" placeholder="请选择">
+              <el-option
+                v-for="dict in getBoolDictOptions(DICT_TYPE.IS_ENABLED)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+  import * as OpcdaTagApi from '@/api/data/channel/opcda/tag'
+  import { CommonEnabledBool } from '@/utils/constants'
+  import { DICT_TYPE, getStrDictOptions, getBoolDictOptions } from '@/utils/dict'
+
+  defineOptions({name: 'OpcdaTagForm'})
+
+  const {t} = useI18n() // 国际化
+  const message = useMessage() // 消息弹窗
+  const dialogVisible = ref(false) // 弹窗的是否展示
+  const dialogTitle = ref('') // 弹窗的标题
+  const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+  const formType = ref('') // 表单的类型:create - 新增;update - 修改
+  const formData = ref({
+    id: undefined,
+    serverId: undefined,
+    tagName: undefined,
+    dataType: undefined,
+    enabled: CommonEnabledBool.ENABLE,
+    itemId: undefined
+  })
+  const formRules = reactive({
+    tagName: [{required: true, message: '标签名不能为空', trigger: 'blur'}],
+    dataType: [{required: true, message: '数据类型不能为空', trigger: 'blur'}]
+  })
+  const formRef = ref() // 表单 Ref
+
+  /** 打开弹窗 */
+  const open = async (type: string, id?: number, serverId?: string) => {
+    dialogVisible.value = true
+    dialogTitle.value = t('action.' + type)
+    formType.value = type
+    resetForm()
+    if (serverId) {
+      formData.value.serverId = serverId
+    }
+    // 修改时,设置数据
+    if (id) {
+      formLoading.value = true
+      try {
+        formData.value = await OpcdaTagApi.getOpcdaTag(id)
+      } finally {
+        formLoading.value = false
+      }
+    }
+  }
+  defineExpose({open}) // 提供 open 方法,用于打开弹窗
+
+  /** 提交表单 */
+  const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+  const submitForm = async () => {
+    // 校验表单
+    if (!formRef) return
+    const valid = await formRef.value.validate()
+    if (!valid) return
+    // 提交请求
+    formLoading.value = true
+    try {
+      const data = formData.value as unknown as OpcdaTagApi.OpcdaTagVO
+      if (formType.value === 'create') {
+        await OpcdaTagApi.createOpcdaTag(data)
+        message.success(t('common.createSuccess'))
+      } else {
+        await OpcdaTagApi.updateOpcdaTag(data)
+        message.success(t('common.updateSuccess'))
+      }
+      dialogVisible.value = false
+      // 发送操作成功的事件
+      emit('success')
+    } finally {
+      formLoading.value = false
+    }
+  }
+
+  /** 重置表单 */
+  const resetForm = () => {
+    formData.value = {
+      id: undefined,
+      serverId: undefined,
+      tagName: undefined,
+      dataType: undefined,
+      enabled: CommonEnabledBool.ENABLE,
+      itemId: undefined
+    }
+    formRef.value?.resetFields()
+  }
+</script>
diff --git a/src/views/data/channel/opcda/tag/index.vue b/src/views/data/channel/opcda/tag/index.vue
new file mode 100644
index 0000000..98f57da
--- /dev/null
+++ b/src/views/data/channel/opcda/tag/index.vue
@@ -0,0 +1,198 @@
+<template>
+  <el-drawer
+    v-model="drawer"
+    size="50%"
+    title="ModBus Tag"
+    :direction="direction"
+    :before-close="handleClose"
+  >
+    <!-- 搜索 -->
+    <ContentWrap>
+      <el-form
+        class="-mb-15px"
+        :model="queryParams"
+        ref="queryFormRef"
+        :inline="true"
+        label-width="68px"
+      >
+        <el-form-item label="Tag名称" prop="tagName">
+          <el-input
+            v-model="queryParams.tagName"
+            placeholder="请输入Tag名称"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-240px"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="handleQuery">
+            <Icon icon="ep:search" class="mr-5px" />
+            搜索
+          </el-button>
+          <el-button @click="resetQuery">
+            <Icon icon="ep:refresh" class="mr-5px" />
+            重置
+          </el-button>
+          <el-button
+            type="primary"
+            plain
+            @click="openForm('create')"
+            v-hasPermi="['data:channel-modbus:create']"
+          >
+            <Icon icon="ep:plus" class="mr-5px" />
+            新增
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </ContentWrap>
+    <!-- 列表 -->
+    <ContentWrap>
+      <el-table v-loading="loading" :data="list">
+        <el-table-column
+          prop="tagName"
+          label="Tag名称"
+          header-align="center"
+          align="left"
+          min-width="150"
+        />
+        <el-table-column
+          prop="dataType"
+          label="数据类型"
+          header-align="center"
+          align="center"
+        />
+        <el-table-column
+          prop="enabled"
+          label="是否启用"
+          header-align="center"
+          align="center"
+        >
+          <template #default="scope">
+            <el-tag v-if="scope.row.enabled === true" size="small">是</el-tag>
+            <el-tag v-else size="small" type="danger">否</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" min-width="110" fixed="right">
+          <template #default="scope">
+            <el-button
+              link
+              type="primary"
+              @click="openForm('update', scope.row.id)"
+              v-hasPermi="['data:channel-modbus:update']"
+            >
+              编辑
+            </el-button>
+            <el-button
+              link
+              type="danger"
+              @click="handleDelete(scope.row.id)"
+              v-hasPermi="['data:channel-modbus:delete']"
+            >
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <!-- 分页 -->
+      <Pagination
+        :total="total"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
+      />
+    </ContentWrap>
+    <!-- 表单弹窗:添加/修改 -->
+    <TagForm ref="formRef" @success="getList" />
+  </el-drawer>
+</template>
+<script lang="ts" setup>
+  import type { DrawerProps } from 'element-plus'
+  import * as OpcdaTagApi from "@/api/data/channel/opcda/tag";
+  import TagForm from './TagForm.vue'
+
+  defineOptions({name: 'ModBusTag'})
+
+  const message = useMessage() // 消息弹窗
+  const {t} = useI18n() // 国际化
+
+  const drawer = ref(false)
+  const direction = ref<DrawerProps['direction']>('rtl')
+  const loading = ref(true) // 列表的加载中
+  const total = ref(0) // 列表的总页数
+  const list = ref([]) // 列表的数据
+  const queryParams = reactive({
+    pageNo: 1,
+    pageSize: 10,
+    serverId: undefined,
+    tagName: undefined
+  })
+  const queryFormRef = ref() // 搜索的表单
+  const exportLoading = ref(false) // 导出的加载中
+
+  /** 查询列表 */
+  const getList = async () => {
+    loading.value = true
+    try {
+      const page = await OpcdaTagApi.getOpcdaTagPage(queryParams)
+      list.value = page.list
+      total.value = page.total
+    } finally {
+      loading.value = false
+    }
+  }
+
+  /** 搜索按钮操作 */
+  const handleQuery = () => {
+    queryParams.pageNo = 1
+    getList()
+  }
+
+  /** 重置按钮操作 */
+  const resetQuery = () => {
+    queryFormRef.value.resetFields()
+    handleQuery()
+  }
+
+  /** 添加/修改操作 */
+  const formRef = ref()
+  const openForm = (type: string, id?: number) => {
+    formRef.value.open(type, id, queryParams.serverId)
+  }
+
+  /** 删除按钮操作 */
+  const handleDelete = async (id: number) => {
+    try {
+      // 删除的二次确认
+      await message.delConfirm()
+      // 发起删除
+      await OpcdaTagApi.deleteOpcdaTag(id)
+      message.success(t('common.delSuccess'))
+      // 刷新列表
+      await getList()
+    } catch {
+    }
+  }
+
+  /** 打开弹窗 */
+  const open = async (serverId?: string) => {
+    resetForm()
+    drawer.value = true
+    queryParams.serverId = serverId
+    if (serverId) {
+      getList()
+    }
+  }
+  defineExpose({open}) // 提供 open 方法,用于打开弹窗
+
+  /** 重置表单 */
+  const resetForm = () => {
+    queryParams.pageNo = 1
+    queryParams.pageSize = 10
+    queryParams.serverId = ''
+    queryParams.tagName = ''
+  }
+
+  const handleClose = (done: () => void) => {
+    drawer.value = false
+  }
+</script>
diff --git a/src/views/data/channel/opcua/index.vue b/src/views/data/channel/opcua/index.vue
index a30307a..25a160f 100644
--- a/src/views/data/channel/opcua/index.vue
+++ b/src/views/data/channel/opcua/index.vue
@@ -19,20 +19,20 @@
       </el-form-item>
       <el-form-item>
         <el-button @click="handleQuery">
-          <Icon icon="ep:search" class="mr-5px" />
+          <Icon icon="ep:search" class="mr-5px"/>
           搜索
         </el-button>
         <el-button @click="resetQuery">
-          <Icon icon="ep:refresh" class="mr-5px" />
+          <Icon icon="ep:refresh" class="mr-5px"/>
           重置
         </el-button>
         <el-button
           type="primary"
           plain
           @click="openForm('create')"
-          v-hasPermi="['system:tenant:create']"
+          v-hasPermi="['data:channel-opcua:create']"
         >
-          <Icon icon="ep:plus" class="mr-5px" />
+          <Icon icon="ep:plus" class="mr-5px"/>
           新增
         </el-button>
       </el-form-item>
@@ -42,31 +42,39 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
-      <el-table-column label="服务名" align="center" prop="serverName" />
-      <el-table-column label="端点URL" align="center" prop="endpointUrl" />
-      <el-table-column label="安全策略" align="center" prop="securityPolicy" />
-      <el-table-column label="安全模式" align="center" prop="securityMode" />
-      <el-table-column label="连接方式" align="center" prop="connectionType" />
-      <el-table-column label="用户名" align="center" prop="userName" />
-      <el-table-column label="密码" align="center" prop="password" />
-      <el-table-column label="安全证书路径" align="center" prop="certificatePath" />
-      <el-table-column label="设备不活动超时时间" align="center" prop="connectInactivityTimeout" />
-      <el-table-column label="重连超时" align="center" prop="reconnectInterval" />
+      <el-table-column label="服务名" align="center" prop="serverName"/>
+      <el-table-column label="端点URL" align="center" prop="endpointUrl"/>
+      <el-table-column label="安全策略" align="center" prop="securityPolicy"/>
+      <el-table-column label="安全模式" align="center" prop="securityMode"/>
+      <el-table-column label="连接方式" align="center" prop="connectionType"/>
+      <el-table-column label="用户名" align="center" prop="userName"/>
+      <el-table-column label="密码" align="center" prop="password"/>
+      <el-table-column label="安全证书路径" align="center" prop="certificatePath"/>
+      <el-table-column label="设备不活动超时时间" align="center" prop="connectInactivityTimeout"/>
+      <el-table-column label="重连超时" align="center" prop="reconnectInterval"/>
       <el-table-column label="操作" align="center" min-width="110" fixed="right">
         <template #default="scope">
           <el-button
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
-            v-hasPermi="['system:tenant:update']"
+            v-hasPermi="['data:channel-opcua:update']"
           >
             编辑
           </el-button>
           <el-button
             link
+            type="primary"
+            @click="openTagList(scope.row.serverName)"
+            v-hasPermi="['data:channel-modbus:update']"
+          >
+            TAG
+          </el-button>
+          <el-button
+            link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['system:tenant:delete']"
+            v-hasPermi="['data:channel-opcua:delete']"
           >
             删除
           </el-button>
@@ -83,14 +91,18 @@
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->
-  <OpcUaDeviceForm ref="formRef" @success="getList" />
+  <OpcUaDeviceForm ref="formRef" @success="getList"/>
+
+  <!-- TAG弹窗:添加/修改 -->
+  <TagList ref="tagRef" @success="getList"/>
 
 </template>
 <script lang="ts" setup>
-import * as OpcUaApi from '@/api/data/channel/opcua'
-import OpcUaDeviceForm from './OpcUaDeviceForm.vue'
+  import * as OpcUaApi from '@/api/data/channel/opcua'
+  import OpcUaDeviceForm from './OpcUaDeviceForm.vue'
+  import TagList from './tag/index.vue'
 
-defineOptions({name: 'DataOpcUa'})
+  defineOptions({name: 'DataOpcUa'})
 
   const message = useMessage() // 消息弹窗
   const {t} = useI18n() // 国际化
@@ -136,6 +148,12 @@
     formRef.value.open(type, id)
   }
 
+  /** TAG操作 */
+  const tagRef = ref()
+  const openTagList = (serverName?: string) => {
+    tagRef.value.open(serverName)
+  }
+
   /** 删除按钮操作 */
   const handleDelete = async (id: number) => {
     try {
diff --git a/src/views/data/channel/opcua/tag/TagForm.vue b/src/views/data/channel/opcua/tag/TagForm.vue
new file mode 100644
index 0000000..810fbff
--- /dev/null
+++ b/src/views/data/channel/opcua/tag/TagForm.vue
@@ -0,0 +1,143 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="50%">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+    >
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="Tag名称" prop="tagName">
+            <el-input v-model="formData.tagName" placeholder="请输Tag名称"/>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="数据类型" prop="dataType">
+            <el-select v-model="formData.dataType" placeholder="请选择">
+              <el-option
+                v-for="dict in getStrDictOptions(DICT_TYPE.TAG_DATA_TYPE)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="address" prop="address">
+            <el-input v-model="formData.address" placeholder="请输入地址"/>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="是否启用" prop="enabled">
+            <el-select v-model="formData.enabled" placeholder="请选择">
+              <el-option
+                v-for="dict in getBoolDictOptions(DICT_TYPE.IS_ENABLED)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+  import * as OpcuaTagApi from '@/api/data/channel/opcua/tag'
+  import { CommonEnabledBool } from '@/utils/constants'
+  import { DICT_TYPE, getStrDictOptions, getBoolDictOptions } from '@/utils/dict'
+
+  defineOptions({name: 'OpcuaTagForm'})
+
+  const {t} = useI18n() // 国际化
+  const message = useMessage() // 消息弹窗
+  const dialogVisible = ref(false) // 弹窗的是否展示
+  const dialogTitle = ref('') // 弹窗的标题
+  const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+  const formType = ref('') // 表单的类型:create - 新增;update - 修改
+  const formData = ref({
+    id: undefined,
+    device: undefined,
+    tagName: undefined,
+    dataType: undefined,
+    enabled: CommonEnabledBool.ENABLE,
+    address: undefined,
+    samplingRate: undefined
+  })
+  const formRules = reactive({
+    tagName: [{required: true, message: '标签名不能为空', trigger: 'blur'}],
+    dataType: [{required: true, message: '数据类型不能为空', trigger: 'blur'}]
+  })
+  const formRef = ref() // 表单 Ref
+
+  /** 打开弹窗 */
+  const open = async (type: string, id?: number, device?: string) => {
+    dialogVisible.value = true
+    dialogTitle.value = t('action.' + type)
+    formType.value = type
+    resetForm()
+    if (device) {
+      formData.value.device = device
+    }
+    // 修改时,设置数据
+    if (id) {
+      formLoading.value = true
+      try {
+        formData.value = await OpcuaTagApi.getOpcuaTag(id)
+      } finally {
+        formLoading.value = false
+      }
+    }
+  }
+  defineExpose({open}) // 提供 open 方法,用于打开弹窗
+
+  /** 提交表单 */
+  const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+  const submitForm = async () => {
+    // 校验表单
+    if (!formRef) return
+    const valid = await formRef.value.validate()
+    if (!valid) return
+    // 提交请求
+    formLoading.value = true
+    try {
+      const data = formData.value as unknown as OpcuaTagApi.OpcuaTagVO
+      if (formType.value === 'create') {
+        await OpcuaTagApi.createOpcuaTag(data)
+        message.success(t('common.createSuccess'))
+      } else {
+        await OpcuaTagApi.updateOpcuaTag(data)
+        message.success(t('common.updateSuccess'))
+      }
+      dialogVisible.value = false
+      // 发送操作成功的事件
+      emit('success')
+    } finally {
+      formLoading.value = false
+    }
+  }
+
+  /** 重置表单 */
+  const resetForm = () => {
+    formData.value = {
+      id: undefined,
+      device: undefined,
+      tagName: undefined,
+      dataType: undefined,
+      enabled: CommonEnabledBool.ENABLE,
+      address: undefined,
+      samplingRate: undefined
+    }
+    formRef.value?.resetFields()
+  }
+</script>
diff --git a/src/views/data/channel/opcua/tag/index.vue b/src/views/data/channel/opcua/tag/index.vue
new file mode 100644
index 0000000..a535147
--- /dev/null
+++ b/src/views/data/channel/opcua/tag/index.vue
@@ -0,0 +1,221 @@
+<template>
+  <el-drawer
+    v-model="drawer"
+    size="50%"
+    title="Opcua Tag"
+    :direction="direction"
+    :before-close="handleClose"
+  >
+    <!-- 搜索 -->
+    <ContentWrap>
+      <el-form
+        class="-mb-15px"
+        :model="queryParams"
+        ref="queryFormRef"
+        :inline="true"
+        label-width="68px"
+      >
+        <el-form-item label="Tag名称" prop="tagName">
+          <el-input
+            v-model="queryParams.tagName"
+            placeholder="请输入Tag名称"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-240px"
+          />
+        </el-form-item>
+        <el-form-item label="地址" prop="address">
+          <el-input
+            v-model="queryParams.address"
+            placeholder="请输入地址"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-240px"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="handleQuery">
+            <Icon icon="ep:search" class="mr-5px" />
+            搜索
+          </el-button>
+          <el-button @click="resetQuery">
+            <Icon icon="ep:refresh" class="mr-5px" />
+            重置
+          </el-button>
+          <el-button
+            type="primary"
+            plain
+            @click="openForm('create')"
+            v-hasPermi="['data:channel-opcua:create']"
+          >
+            <Icon icon="ep:plus" class="mr-5px" />
+            新增
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </ContentWrap>
+    <!-- 列表 -->
+    <ContentWrap>
+      <el-table v-loading="loading" :data="list">
+        <el-table-column
+          prop="tagName"
+          label="Tag名称"
+          header-align="center"
+          align="left"
+          min-width="150"
+        />
+        <el-table-column
+          prop="dataType"
+          label="数据类型"
+          header-align="center"
+          align="center"
+        />
+        <el-table-column
+          prop="address"
+          label="地址"
+          header-align="center"
+          align="center"
+        />
+        <el-table-column
+          prop="samplingRate"
+          label="采集频率"
+          header-align="center"
+          align="center"
+        />
+        <el-table-column
+          prop="enabled"
+          label="是否启用"
+          header-align="center"
+          align="center"
+        >
+          <template #default="scope">
+            <el-tag v-if="scope.row.enabled === true" size="small">是</el-tag>
+            <el-tag v-else size="small" type="danger">否</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" min-width="110" fixed="right">
+          <template #default="scope">
+            <el-button
+              link
+              type="primary"
+              @click="openForm('update', scope.row.id)"
+              v-hasPermi="['data:channel-opcua:update']"
+            >
+              编辑
+            </el-button>
+            <el-button
+              link
+              type="danger"
+              @click="handleDelete(scope.row.id)"
+              v-hasPermi="['data:channel-opcua:delete']"
+            >
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <!-- 分页 -->
+      <Pagination
+        :total="total"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
+      />
+    </ContentWrap>
+    <!-- 表单弹窗:添加/修改 -->
+    <TagForm ref="formRef" @success="getList" />
+  </el-drawer>
+</template>
+<script lang="ts" setup>
+  import type { DrawerProps } from 'element-plus'
+  import * as OpcuaTagApi from "@/api/data/channel/opcua/tag";
+  import TagForm from './TagForm.vue'
+
+  defineOptions({name: 'OpcuaTag'})
+
+  const message = useMessage() // 消息弹窗
+  const {t} = useI18n() // 国际化
+
+  const drawer = ref(false)
+  const direction = ref<DrawerProps['direction']>('rtl')
+  const loading = ref(true) // 列表的加载中
+  const total = ref(0) // 列表的总页数
+  const list = ref([]) // 列表的数据
+  const queryParams = reactive({
+    pageNo: 1,
+    pageSize: 10,
+    device: undefined,
+    tagName: undefined,
+    address: undefined
+  })
+  const queryFormRef = ref() // 搜索的表单
+  const exportLoading = ref(false) // 导出的加载中
+
+  /** 查询列表 */
+  const getList = async () => {
+    loading.value = true
+    try {
+      const page = await OpcuaTagApi.getOpcuaTagPage(queryParams)
+      list.value = page.list
+      total.value = page.total
+    } finally {
+      loading.value = false
+    }
+  }
+
+  /** 搜索按钮操作 */
+  const handleQuery = () => {
+    queryParams.pageNo = 1
+    getList()
+  }
+
+  /** 重置按钮操作 */
+  const resetQuery = () => {
+    queryFormRef.value.resetFields()
+    handleQuery()
+  }
+
+  /** 添加/修改操作 */
+  const formRef = ref()
+  const openForm = (type: string, id?: number) => {
+    formRef.value.open(type, id, queryParams.device)
+  }
+
+  /** 删除按钮操作 */
+  const handleDelete = async (id: number) => {
+    try {
+      // 删除的二次确认
+      await message.delConfirm()
+      // 发起删除
+      await OpcuaTagApi.deleteOpcuaTag(id)
+      message.success(t('common.delSuccess'))
+      // 刷新列表
+      await getList()
+    } catch {
+    }
+  }
+
+  /** 打开弹窗 */
+  const open = async (device?: string) => {
+    resetForm()
+    drawer.value = true
+    queryParams.device = device
+    if (device) {
+      getList()
+    }
+  }
+  defineExpose({open}) // 提供 open 方法,用于打开弹窗
+
+  /** 重置表单 */
+  const resetForm = () => {
+    queryParams.pageNo = 1
+    queryParams.pageSize = 10
+    queryParams.device = ''
+    queryParams.tagName = ''
+    queryParams.address = ''
+  }
+
+  const handleClose = (done: () => void) => {
+    drawer.value = false
+  }
+</script>

--
Gitblit v1.9.3