From df2dfebf9f5525fdf6b162cb0ed2f0a5c6fa6146 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期二, 20 十月 2026 14:00:55 +0800
Subject: [PATCH] feat(mes): 添加数采监控功能并完善设备管理

---
 src/api/mes/dv/machinery/index.ts       |    2 
 src/api/mes/collect/index.ts            |  220 ++++++++++++
 src/views/mes/dv/machinery/index.vue    |    7 
 src/views/mes/dv/machinery/data.ts      |   48 ++
 src/views/mes/collect/monitor/index.vue |  719 ++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 995 insertions(+), 1 deletions(-)

diff --git a/src/api/mes/collect/index.ts b/src/api/mes/collect/index.ts
new file mode 100644
index 0000000..f058c69
--- /dev/null
+++ b/src/api/mes/collect/index.ts
@@ -0,0 +1,220 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesCollectApi {
+  /** 鏁伴噰璁惧鐘舵�� */
+  export interface Device {
+    machineryId?: number; // 璁惧鍙拌处缂栧彿
+    machineryCode?: string; // 璁惧缂栫爜
+    machineryName?: string; // 璁惧鍚嶇О
+    workstationId?: number; // 宸ヤ綔绔欑紪鍙�
+    workstationName?: string; // 宸ヤ綔绔欏悕绉�
+    collectDeviceCode?: string; // 鏁伴噰璁惧缂栧彿
+    collectEnabled?: boolean; // 鏄惁鍚敤鏁伴噰
+    lastCollectTime?: string; // 鏈�杩戦噰闆嗘椂闂�
+    online?: boolean; // 鏄惁鍦ㄧ嚎
+    sts?: number; // 杩愯鐘舵��
+    stsName?: string; // 杩愯鐘舵�佸悕绉�
+    opm?: number; // 杩愯妯″紡
+    opmName?: string; // 杩愯妯″紡鍚嶇О
+    ot?: number; // 娌规俯
+    temps?: string; // 鍚勬娓╁害 JSON
+    taskId?: number; // 鐢熶骇浠诲姟缂栧彿
+    taskCode?: string; // 鐢熶骇浠诲姟缂栫爜
+    taskName?: string; // 鐢熶骇浠诲姟鍚嶇О
+    workOrderId?: number; // 鐢熶骇宸ュ崟缂栧彿
+    workOrderCode?: string; // 鐢熶骇宸ュ崟缂栫爜
+    activeAlarmCount?: number; // 鏈秷闄ゆ姤璀︽暟閲�
+  }
+
+  /** 瀹炴椂鐘舵�� */
+  export interface Realtime {
+    id?: number;
+    machineryId?: number;
+    deviceCode?: string;
+    taskId?: number;
+    workOrderId?: number;
+    collectTime?: string;
+    sts?: number;
+    stsName?: string;
+    opm?: number;
+    opmName?: string;
+    asts?: number;
+    ot?: number;
+    temps?: string;
+    dataJson?: string;
+  }
+
+  /** 鍛ㄦ湡杩囩▼鏁版嵁 */
+  export interface Cycle {
+    id?: number;
+    machineryId?: number;
+    deviceCode?: string;
+    taskId?: number;
+    workOrderId?: number;
+    cycleNo?: number;
+    collectTime?: string;
+    ecyct?: number;
+    eiss?: number;
+    eivm?: number;
+    eipm?: number;
+    esipt?: number;
+    esipp?: number;
+    esips?: number;
+    eipt?: number;
+    eipse?: number;
+    eplst?: number;
+    eplsse?: number;
+    eplspm?: number;
+    etTemps?: string;
+    dataJson?: string;
+  }
+
+  /** 宸ヨ壓鍙傛暟蹇収 */
+  export interface Tech {
+    id?: number;
+    machineryId?: number;
+    deviceCode?: string;
+    taskId?: number;
+    workOrderId?: number;
+    collectTime?: string;
+    dataJson?: string;
+  }
+
+  /** 鍙傛暟鍙樻洿鏃ュ織 */
+  export interface OperateLog {
+    id?: number;
+    machineryId?: number;
+    deviceCode?: string;
+    taskId?: number;
+    workOrderId?: number;
+    varId?: string;
+    oldValue?: string;
+    newValue?: string;
+    modifyTime?: string;
+    collectTime?: string;
+    dataJson?: string;
+  }
+
+  /** 鎶ヨ璁板綍 */
+  export interface Alarm {
+    id?: number;
+    machineryId?: number;
+    deviceCode?: string;
+    taskId?: number;
+    workOrderId?: number;
+    wmId?: number;
+    wmMsg?: string;
+    wmTime?: string;
+    status?: number;
+    statusName?: string;
+    clearTime?: string;
+    collectTime?: string;
+    dataJson?: string;
+  }
+
+  /** 瀹炴椂鍘嗗彶鏌ヨ鍙傛暟 */
+  export interface RealtimeHistoryPageParam extends PageParam {
+    deviceCode?: string;
+    machineryId?: number;
+    taskId?: number;
+    workOrderId?: number;
+    collectTime?: string[];
+  }
+
+  /** 鍛ㄦ湡杩囩▼鏌ヨ鍙傛暟 */
+  export interface CyclePageParam extends PageParam {
+    deviceCode?: string;
+    machineryId?: number;
+    taskId?: number;
+    workOrderId?: number;
+    collectTime?: string[];
+  }
+
+  /** 鍙傛暟鍙樻洿鏌ヨ鍙傛暟 */
+  export interface OperateLogPageParam extends PageParam {
+    deviceCode?: string;
+    machineryId?: number;
+    workOrderId?: number;
+    varId?: string;
+    modifyTime?: string[];
+  }
+
+  /** 鎶ヨ鏌ヨ鍙傛暟 */
+  export interface AlarmPageParam extends PageParam {
+    deviceCode?: string;
+    machineryId?: number;
+    workOrderId?: number;
+    status?: number;
+    wmTime?: string[];
+  }
+}
+
+/** 鏌ヨ鏁伴噰璁惧鐘舵�佸垪琛� */
+export function getCollectDeviceList() {
+  return requestClient.get<MesCollectApi.Device[]>('/mes/collect/device/list');
+}
+
+/** 鏌ヨ璁惧鏈�鏂板疄鏃剁姸鎬� */
+export function getCollectRealtime(deviceCode: string) {
+  return requestClient.get<MesCollectApi.Realtime | null>(
+    '/mes/collect/realtime/get',
+    { params: { deviceCode } },
+  );
+}
+
+/** 鏌ヨ瀹炴椂鐘舵�佸巻鍙插垎椤� */
+export function getCollectRealtimeHistoryPage(
+  params: MesCollectApi.RealtimeHistoryPageParam,
+) {
+  return requestClient.get<PageResult<MesCollectApi.Realtime>>(
+    '/mes/collect/realtime/history-page',
+    { params },
+  );
+}
+
+/** 鏌ヨ鍛ㄦ湡杩囩▼鏁版嵁鍒嗛〉 */
+export function getCollectCyclePage(params: MesCollectApi.CyclePageParam) {
+  return requestClient.get<PageResult<MesCollectApi.Cycle>>(
+    '/mes/collect/cycle/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ鍛ㄦ湡杩囩▼鏁版嵁鏄庣粏 */
+export function getCollectCycle(id: number) {
+  return requestClient.get<MesCollectApi.Cycle>('/mes/collect/cycle/get', {
+    params: { id },
+  });
+}
+
+/** 鏌ヨ璁惧鏈�鏂板伐鑹哄弬鏁板揩鐓� */
+export function getCollectTech(deviceCode: string) {
+  return requestClient.get<MesCollectApi.Tech | null>('/mes/collect/tech/get', {
+    params: { deviceCode },
+  });
+}
+
+/** 鏌ヨ鍙傛暟鍙樻洿鏃ュ織鍒嗛〉 */
+export function getCollectOperateLogPage(
+  params: MesCollectApi.OperateLogPageParam,
+) {
+  return requestClient.get<PageResult<MesCollectApi.OperateLog>>(
+    '/mes/collect/operate-log/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ鎶ヨ璁板綍鍒嗛〉 */
+export function getCollectAlarmPage(params: MesCollectApi.AlarmPageParam) {
+  return requestClient.get<PageResult<MesCollectApi.Alarm>>(
+    '/mes/collect/alarm/page',
+    { params },
+  );
+}
+
+/** 浜哄伐纭娑堥櫎鎶ヨ */
+export function ackCollectAlarm(id: number) {
+  return requestClient.put<boolean>(`/mes/collect/alarm/ack?id=${id}`);
+}
diff --git a/src/api/mes/dv/machinery/index.ts b/src/api/mes/dv/machinery/index.ts
index c46b39f..9d42c8a 100644
--- a/src/api/mes/dv/machinery/index.ts
+++ b/src/api/mes/dv/machinery/index.ts
@@ -21,6 +21,8 @@
     conveyorBelt?: number; // 浼犻�佸饱甯︽暟閲�
     workstationId?: number; // 宸ヤ綔绔欑紪鍙�
     workstationName?: string; // 宸ヤ綔绔欏悕绉�
+    collectDeviceCode?: string; // 鏁伴噰璁惧缂栧彿
+    collectEnabled?: boolean; // 鏄惁鍚敤鏁伴噰
     remark?: string; // 澶囨敞
     createTime?: Date; // 鍒涘缓鏃堕棿
   }
diff --git a/src/views/mes/collect/monitor/index.vue b/src/views/mes/collect/monitor/index.vue
new file mode 100644
index 0000000..226cb25
--- /dev/null
+++ b/src/views/mes/collect/monitor/index.vue
@@ -0,0 +1,719 @@
+<script lang="ts" setup>
+import type { Ref } from 'vue';
+
+import type { MesCollectApi } from '#/api/mes/collect';
+
+import { computed, onMounted, onUnmounted, ref } from 'vue';
+
+import { Page } from '@vben/common-ui';
+import { formatDateTime } from '@vben/utils';
+
+import {
+  Button,
+  Descriptions,
+  Drawer,
+  Empty,
+  message,
+  Modal,
+  Popconfirm,
+  Space,
+  Table,
+  Tabs,
+  Tag,
+} from 'ant-design-vue';
+
+import {
+  ackCollectAlarm,
+  getCollectAlarmPage,
+  getCollectCyclePage,
+  getCollectDeviceList,
+  getCollectOperateLogPage,
+  getCollectRealtime,
+  getCollectRealtimeHistoryPage,
+  getCollectTech,
+} from '#/api/mes/collect';
+
+defineOptions({ name: 'MesCollectMonitor' });
+
+interface PageData<T> {
+  list: T[];
+  total: number;
+}
+
+interface PageQuery {
+  current?: number;
+  pageSize?: number;
+}
+
+const deviceLoading = ref(false);
+const devices = ref<MesCollectApi.Device[]>([]);
+const lastRefreshAt = ref('');
+let refreshTimer: ReturnType<typeof setInterval> | undefined;
+
+const drawerOpen = ref(false);
+const currentDevice = ref<MesCollectApi.Device>();
+const activeTab = ref('realtime');
+
+const realtime = ref<MesCollectApi.Realtime | null>(null);
+const realtimeLoading = ref(false);
+const tech = ref<MesCollectApi.Tech | null>(null);
+const techLoading = ref(false);
+
+const rawJsonOpen = ref(false);
+const rawJsonTitle = ref('');
+const rawJsonContent = ref('');
+
+const drawerTitle = computed(() => {
+  const device = currentDevice.value;
+  if (!device) {
+    return '鏁伴噰璁惧璇︽儏';
+  }
+  return `${device.machineryName ?? ''}锛�${device.collectDeviceCode ?? ''}锛塦;
+});
+
+const onlineCount = computed(
+  () => devices.value.filter((item) => item.online).length,
+);
+
+const alarmCount = computed(() =>
+  devices.value.reduce(
+    (total, item) => total + Number(item.activeAlarmCount ?? 0),
+    0,
+  ),
+);
+
+function usePagedTable<T>(
+  label: string,
+  loader: (pageNo: number, pageSize: number) => Promise<PageData<T>>,
+) {
+  const loading = ref(false);
+  const rows = ref([]) as Ref<T[]>;
+  const total = ref(0);
+  const pageNo = ref(1);
+  const pageSize = ref(10);
+
+  async function load() {
+    loading.value = true;
+    try {
+      const data = await loader(pageNo.value, pageSize.value);
+      rows.value = data.list ?? [];
+      total.value = data.total ?? 0;
+    } catch {
+      rows.value = [];
+      total.value = 0;
+      message.error(`${label}鍔犺浇澶辫触锛岃绋嶅悗閲嶈瘯`);
+    } finally {
+      loading.value = false;
+    }
+  }
+
+  function changePage(page: PageQuery) {
+    pageNo.value = page.current ?? 1;
+    pageSize.value = page.pageSize ?? 10;
+    void load();
+  }
+
+  function reset() {
+    pageNo.value = 1;
+    pageSize.value = 10;
+    rows.value = [];
+    total.value = 0;
+  }
+
+  return { changePage, load, loading, pageNo, pageSize, reset, rows, total };
+}
+
+const realtimeHistory = usePagedTable<MesCollectApi.Realtime>(
+  '瀹炴椂鐘舵�佸巻鍙�',
+  async (pageNo, pageSize) =>
+    await getCollectRealtimeHistoryPage({
+      pageNo,
+      pageSize,
+      deviceCode: currentDevice.value?.collectDeviceCode,
+    }),
+);
+const cyclePage = usePagedTable<MesCollectApi.Cycle>(
+  '鍛ㄦ湡杩囩▼鏁版嵁',
+  async (pageNo, pageSize) =>
+    await getCollectCyclePage({
+      pageNo,
+      pageSize,
+      deviceCode: currentDevice.value?.collectDeviceCode,
+    }),
+);
+const operateLogPage = usePagedTable<MesCollectApi.OperateLog>(
+  '鍙傛暟鍙樻洿鏃ュ織',
+  async (pageNo, pageSize) =>
+    await getCollectOperateLogPage({
+      pageNo,
+      pageSize,
+      deviceCode: currentDevice.value?.collectDeviceCode,
+    }),
+);
+const alarmPage = usePagedTable<MesCollectApi.Alarm>(
+  '鎶ヨ璁板綍',
+  async (pageNo, pageSize) =>
+    await getCollectAlarmPage({
+      pageNo,
+      pageSize,
+      deviceCode: currentDevice.value?.collectDeviceCode,
+    }),
+);
+
+const deviceColumns = [
+  { title: '璁惧', key: 'device', width: 220, fixed: 'left' as const },
+  { title: '鏁伴噰缂栫爜', dataIndex: 'collectDeviceCode', width: 110 },
+  { title: '鍦ㄧ嚎', key: 'online', width: 90 },
+  { title: '杩愯鐘舵��', key: 'sts', width: 100 },
+  { title: '杩愯妯″紡', key: 'opm', width: 110 },
+  { title: '娌规俯(鈩�)', dataIndex: 'ot', width: 100 },
+  { title: '褰撳墠浠诲姟', key: 'task', width: 220 },
+  { title: '鐢熶骇宸ュ崟', dataIndex: 'workOrderCode', width: 160 },
+  { title: '鏈秷闄ゆ姤璀�', key: 'alarm', width: 110 },
+  {
+    title: '鏈�杩戦噰闆嗘椂闂�',
+    dataIndex: 'lastCollectTime',
+    key: 'lastCollectTime',
+    width: 180,
+  },
+  { title: '鎿嶄綔', key: 'action', width: 110, fixed: 'right' as const },
+];
+
+const realtimeHistoryColumns = [
+  {
+    title: '閲囬泦鏃堕棿',
+    dataIndex: 'collectTime',
+    key: 'collectTime',
+    width: 180,
+  },
+  { title: '杩愯鐘舵��', dataIndex: 'stsName', width: 100 },
+  { title: '杩愯妯″紡', dataIndex: 'opmName', width: 110 },
+  { title: '娌规俯(鈩�)', dataIndex: 'ot', width: 100 },
+  { title: '闄勫姞鐘舵��', dataIndex: 'asts', width: 100 },
+  { title: '娓╁害', key: 'temps', minWidth: 220 },
+  { title: '鍘熷鏁版嵁', key: 'raw', width: 100 },
+];
+
+const cycleColumns = [
+  { title: '鍛ㄦ湡搴忓彿', dataIndex: 'cycleNo', width: 110 },
+  {
+    title: '閲囬泦鏃堕棿',
+    dataIndex: 'collectTime',
+    key: 'collectTime',
+    width: 180,
+  },
+  { title: '鍛ㄦ湡鏃堕棿', dataIndex: 'ecyct', width: 100 },
+  { title: '灏勫嚭鏃堕棿', dataIndex: 'eipt', width: 100 },
+  { title: '鏈�澶у皠閫�', dataIndex: 'eivm', width: 100 },
+  { title: '鏈�澶у皠鍘�', dataIndex: 'eipm', width: 100 },
+  { title: '杞繚鍘嬫椂闂�', dataIndex: 'esipt', width: 110 },
+  { title: '杞繚鍘嬪帇鍔�', dataIndex: 'esipp', width: 110 },
+  { title: '鍌ㄦ枡鏃堕棿', dataIndex: 'eplst', width: 100 },
+  { title: '娓╁害', key: 'etTemps', minWidth: 220 },
+  { title: '鍘熷鏁版嵁', key: 'raw', width: 100 },
+];
+
+const operateLogColumns = [
+  { title: '鍙橀噺缂栫爜', dataIndex: 'varId', width: 120 },
+  { title: '淇敼鍓�', dataIndex: 'oldValue', width: 150 },
+  { title: '淇敼鍚�', dataIndex: 'newValue', width: 150 },
+  { title: '淇敼鏃堕棿', dataIndex: 'modifyTime', key: 'modifyTime', width: 180 },
+  { title: '閲囬泦鏃堕棿', dataIndex: 'collectTime', key: 'collectTime', width: 180 },
+  { title: '鍘熷鏁版嵁', key: 'raw', width: 100 },
+];
+
+const alarmColumns = [
+  { title: '鎶ヨ鏃堕棿', dataIndex: 'wmTime', key: 'wmTime', width: 180 },
+  { title: '鎶ヨ鍐呭', dataIndex: 'wmMsg', minWidth: 220 },
+  { title: '鐘舵��', key: 'status', width: 100 },
+  { title: '娑堥櫎鏃堕棿', dataIndex: 'clearTime', key: 'clearTime', width: 180 },
+  { title: '閲囬泦鏃堕棿', dataIndex: 'collectTime', key: 'collectTime', width: 180 },
+  { title: '鎿嶄綔', key: 'action', width: 120, fixed: 'right' as const },
+];
+
+async function loadDevices() {
+  deviceLoading.value = true;
+  try {
+    devices.value = await getCollectDeviceList();
+    lastRefreshAt.value = formatDateTime(new Date());
+  } catch {
+    message.error('鏁伴噰璁惧鐘舵�佸姞杞藉け璐ワ紝璇风偣鍑烩�滃埛鏂扳�濋噸璇�');
+  } finally {
+    deviceLoading.value = false;
+  }
+}
+
+function resetDetailData() {
+  realtime.value = null;
+  tech.value = null;
+  realtimeHistory.reset();
+  cyclePage.reset();
+  operateLogPage.reset();
+  alarmPage.reset();
+}
+
+async function loadRealtime() {
+  const deviceCode = currentDevice.value?.collectDeviceCode;
+  if (!deviceCode) {
+    return;
+  }
+  realtimeLoading.value = true;
+  try {
+    realtime.value = await getCollectRealtime(deviceCode);
+  } catch {
+    realtime.value = null;
+    message.error('璁惧瀹炴椂鐘舵�佸姞杞藉け璐ワ紝璇峰叧闂鎯呭悗閲嶆柊鎵撳紑');
+  } finally {
+    realtimeLoading.value = false;
+  }
+  void realtimeHistory.load();
+}
+
+async function loadTech() {
+  const deviceCode = currentDevice.value?.collectDeviceCode;
+  if (!deviceCode) {
+    return;
+  }
+  techLoading.value = true;
+  try {
+    tech.value = await getCollectTech(deviceCode);
+  } catch {
+    tech.value = null;
+    message.error('宸ヨ壓鍙傛暟鍔犺浇澶辫触锛岃鍒囨崲鍒板叾瀹冮〉绛惧悗閲嶈瘯');
+  } finally {
+    techLoading.value = false;
+  }
+}
+
+function openDetail(device: MesCollectApi.Device) {
+  currentDevice.value = device;
+  activeTab.value = 'realtime';
+  resetDetailData();
+  drawerOpen.value = true;
+  void loadRealtime();
+}
+
+function handleTabChange(key: number | string) {
+  const tab = String(key);
+  if (tab === 'realtime') {
+    void loadRealtime();
+  } else if (tab === 'cycle') {
+    void cyclePage.load();
+  } else if (tab === 'tech') {
+    void loadTech();
+  } else if (tab === 'oplog') {
+    void operateLogPage.load();
+  } else if (tab === 'alarm') {
+    void alarmPage.load();
+  }
+}
+
+async function handleAck(row: MesCollectApi.Alarm) {
+  if (!row.id) {
+    return;
+  }
+  try {
+    await ackCollectAlarm(row.id);
+    message.success('鎶ヨ宸茬‘璁ゆ秷闄�');
+    void alarmPage.load();
+    void loadDevices();
+  } catch {
+    message.error('鎶ヨ纭澶辫触锛岃褰曞彲鑳藉凡琚澶囪嚜鍔ㄦ秷闄わ紝璇峰埛鏂板悗閲嶈瘯');
+    void alarmPage.load();
+  }
+}
+
+function safeParseObject(text?: string): null | Record<string, unknown> {
+  if (!text) {
+    return null;
+  }
+  try {
+    const value = JSON.parse(text) as unknown;
+    return value && typeof value === 'object'
+      ? (value as Record<string, unknown>)
+      : null;
+  } catch {
+    return null;
+  }
+}
+
+function formatNumberedFields(text?: string) {
+  const value = safeParseObject(text);
+  if (!value) {
+    return '';
+  }
+  return Object.entries(value)
+    .map(([key, item]) => `${key}: ${String(item ?? '')}`)
+    .join('锛�');
+}
+
+function showRaw(title: string, json?: string) {
+  rawJsonTitle.value = title;
+  rawJsonContent.value = prettyJson(json);
+  rawJsonOpen.value = true;
+}
+
+function prettyJson(json?: string) {
+  if (!json) {
+    return '鏆傛棤鍘熷鏁版嵁';
+  }
+  try {
+    return JSON.stringify(JSON.parse(json), null, 2);
+  } catch {
+    return json;
+  }
+}
+
+onMounted(() => {
+  void loadDevices();
+  refreshTimer = setInterval(() => {
+    if (!drawerOpen.value) {
+      void loadDevices();
+    }
+  }, 30_000);
+});
+
+onUnmounted(() => {
+  if (refreshTimer) {
+    clearInterval(refreshTimer);
+  }
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <div
+      class="mb-4 flex flex-wrap items-center justify-between gap-3"
+    >
+      <div class="flex flex-wrap items-center gap-4">
+        <h2 class="text-lg font-semibold">鏁伴噰鐩戞帶</h2>
+        <span class="text-sm text-gray-500">
+          鍦ㄧ嚎 {{ onlineCount }} / {{ devices.length }} 鍙�
+        </span>
+        <span class="text-sm text-gray-500">
+          鏈秷闄ゆ姤璀� {{ alarmCount }} 鏉�
+        </span>
+        <span v-if="lastRefreshAt" class="text-sm text-gray-400">
+          鍒锋柊浜� {{ lastRefreshAt }}
+        </span>
+      </div>
+      <Button type="primary" :loading="deviceLoading" @click="loadDevices">
+        鍒锋柊
+      </Button>
+    </div>
+
+    <Table
+      :columns="deviceColumns"
+      :data-source="devices"
+      :loading="deviceLoading"
+      :pagination="false"
+      :scroll="{ x: 1600 }"
+      row-key="machineryId"
+      size="middle"
+    >
+      <template #bodyCell="{ column, record }">
+        <template v-if="column.key === 'device'">
+          <div class="font-medium">
+            {{ (record as MesCollectApi.Device).machineryName }}
+          </div>
+          <div class="text-xs text-gray-500">
+            {{ (record as MesCollectApi.Device).machineryCode }}
+            <span v-if="(record as MesCollectApi.Device).workstationName">
+              路 {{ (record as MesCollectApi.Device).workstationName }}
+            </span>
+          </div>
+        </template>
+        <template v-else-if="column.key === 'online'">
+          <Tag
+            :color="(record as MesCollectApi.Device).online ? 'success' : 'default'"
+          >
+            {{ (record as MesCollectApi.Device).online ? '鍦ㄧ嚎' : '绂荤嚎' }}
+          </Tag>
+        </template>
+        <template v-else-if="column.key === 'sts'">
+          <Tag
+            :color="
+              (record as MesCollectApi.Device).sts === 2 ? 'processing' : 'default'
+            "
+          >
+            {{ (record as MesCollectApi.Device).stsName || '-' }}
+          </Tag>
+        </template>
+        <template v-else-if="column.key === 'opm'">
+          {{ (record as MesCollectApi.Device).opmName || '-' }}
+        </template>
+        <template v-else-if="column.key === 'task'">
+          <div v-if="(record as MesCollectApi.Device).taskId">
+            <div>{{ (record as MesCollectApi.Device).taskName }}</div>
+            <div class="text-xs text-gray-500">
+              {{ (record as MesCollectApi.Device).taskCode }}
+            </div>
+          </div>
+          <span v-else class="text-gray-400">鏃犺繍琛屼换鍔�</span>
+        </template>
+        <template v-else-if="column.key === 'alarm'">
+          <Tag
+            :color="
+              Number((record as MesCollectApi.Device).activeAlarmCount) > 0
+                ? 'error'
+                : 'default'
+            "
+          >
+            {{ (record as MesCollectApi.Device).activeAlarmCount ?? 0 }}
+          </Tag>
+        </template>
+        <template v-else-if="column.key === 'lastCollectTime'">
+          {{ formatDateTime((record as MesCollectApi.Device).lastCollectTime) || '-' }}
+        </template>
+        <template v-else-if="column.key === 'action'">
+          <Button type="link" @click="openDetail(record as MesCollectApi.Device)">
+            鏌ョ湅璇︽儏
+          </Button>
+        </template>
+      </template>
+    </Table>
+
+    <Drawer
+      v-model:open="drawerOpen"
+      :title="drawerTitle"
+      destroy-on-close
+      width="90%"
+    >
+      <Tabs v-model:active-key="activeTab" @change="handleTabChange">
+        <Tabs.TabPane key="realtime" tab="瀹炴椂鐘舵��">
+          <div v-loading="realtimeLoading" class="mb-4">
+            <Descriptions v-if="realtime" bordered size="small" :column="4">
+              <Descriptions.Item label="閲囬泦鏃堕棿">
+                {{ formatDateTime(realtime.collectTime) || '-' }}
+              </Descriptions.Item>
+              <Descriptions.Item label="杩愯鐘舵��">
+                {{ realtime.stsName || '-' }}
+              </Descriptions.Item>
+              <Descriptions.Item label="杩愯妯″紡">
+                {{ realtime.opmName || '-' }}
+              </Descriptions.Item>
+              <Descriptions.Item label="娌规俯">
+                {{ realtime.ot ?? '-' }}
+              </Descriptions.Item>
+              <Descriptions.Item label="鍚勬娓╁害" :span="4">
+                <div class="max-h-24 overflow-auto">
+                  {{ formatNumberedFields(realtime.temps) || '-' }}
+                </div>
+              </Descriptions.Item>
+            </Descriptions>
+            <Empty v-else description="鏆傛棤瀹炴椂鏁版嵁" />
+          </div>
+          <Table
+            :columns="realtimeHistoryColumns"
+            :data-source="realtimeHistory.rows.value"
+            :loading="realtimeHistory.loading.value"
+            :pagination="{
+              current: realtimeHistory.pageNo.value,
+              pageSize: realtimeHistory.pageSize.value,
+              total: realtimeHistory.total.value,
+              showSizeChanger: true,
+              showTotal: (total: number) => `鍏� ${total} 鏉,
+            }"
+            :scroll="{ x: 1200 }"
+            row-key="id"
+            size="small"
+            @change="realtimeHistory.changePage"
+          >
+            <template #bodyCell="{ column, record }">
+              <template v-if="column.key === 'temps'">
+                <div class="max-h-20 overflow-auto text-xs">
+                  {{ formatNumberedFields((record as MesCollectApi.Realtime).temps) || '-' }}
+                </div>
+              </template>
+              <template v-else-if="column.key === 'raw'">
+                <Button
+                  type="link"
+                  size="small"
+                  @click="
+                    showRaw(
+                      '瀹炴椂鐘舵�佸師濮嬫暟鎹�',
+                      (record as MesCollectApi.Realtime).dataJson,
+                    )
+                  "
+                >
+                  鏌ョ湅
+                </Button>
+              </template>
+              <template v-else-if="column.key === 'collectTime'">
+                {{ formatDateTime((record as MesCollectApi.Realtime).collectTime) || '-' }}
+              </template>
+            </template>
+          </Table>
+        </Tabs.TabPane>
+
+        <Tabs.TabPane key="cycle" tab="鍛ㄦ湡杩囩▼">
+          <Table
+            :columns="cycleColumns"
+            :data-source="cyclePage.rows.value"
+            :loading="cyclePage.loading.value"
+            :pagination="{
+              current: cyclePage.pageNo.value,
+              pageSize: cyclePage.pageSize.value,
+              total: cyclePage.total.value,
+              showSizeChanger: true,
+              showTotal: (total: number) => `鍏� ${total} 鏉,
+            }"
+            :scroll="{ x: 1500 }"
+            row-key="id"
+            size="small"
+            @change="cyclePage.changePage"
+          >
+            <template #bodyCell="{ column, record }">
+              <template v-if="column.key === 'etTemps'">
+                <div class="max-h-20 overflow-auto text-xs">
+                  {{ formatNumberedFields((record as MesCollectApi.Cycle).etTemps) || '-' }}
+                </div>
+              </template>
+              <template v-else-if="column.key === 'raw'">
+                <Button
+                  type="link"
+                  size="small"
+                  @click="
+                    showRaw(
+                      '鍛ㄦ湡杩囩▼鍘熷鏁版嵁',
+                      (record as MesCollectApi.Cycle).dataJson,
+                    )
+                  "
+                >
+                  鏌ョ湅
+                </Button>
+              </template>
+              <template v-else-if="column.key === 'collectTime'">
+                {{ formatDateTime((record as MesCollectApi.Cycle).collectTime) || '-' }}
+              </template>
+            </template>
+          </Table>
+        </Tabs.TabPane>
+
+        <Tabs.TabPane key="tech" tab="宸ヨ壓鍙傛暟">
+          <div v-loading="techLoading">
+            <div v-if="tech" class="mb-3 text-sm text-gray-500">
+              閲囬泦鏃堕棿锛歿{ formatDateTime(tech.collectTime) || '-' }}
+            </div>
+            <pre
+              v-if="tech"
+              class="max-h-[65vh] overflow-auto rounded bg-gray-50 p-4 text-xs"
+            >{{ prettyJson(tech.dataJson) }}</pre>
+            <Empty v-else description="鏆傛棤宸ヨ壓鍙傛暟" />
+          </div>
+        </Tabs.TabPane>
+
+        <Tabs.TabPane key="oplog" tab="鍙傛暟鍙樻洿">
+          <Table
+            :columns="operateLogColumns"
+            :data-source="operateLogPage.rows.value"
+            :loading="operateLogPage.loading.value"
+            :pagination="{
+              current: operateLogPage.pageNo.value,
+              pageSize: operateLogPage.pageSize.value,
+              total: operateLogPage.total.value,
+              showSizeChanger: true,
+              showTotal: (total: number) => `鍏� ${total} 鏉,
+            }"
+            :scroll="{ x: 1000 }"
+            row-key="id"
+            size="small"
+            @change="operateLogPage.changePage"
+          >
+            <template #bodyCell="{ column, record }">
+              <template v-if="column.key === 'raw'">
+                <Button
+                  type="link"
+                  size="small"
+                  @click="
+                    showRaw(
+                      '鍙傛暟鍙樻洿鍘熷鏁版嵁',
+                      (record as MesCollectApi.OperateLog).dataJson,
+                    )
+                  "
+                >
+                  鏌ョ湅
+                </Button>
+              </template>
+              <template v-else-if="column.key === 'modifyTime'">
+                {{ formatDateTime((record as MesCollectApi.OperateLog).modifyTime) || '-' }}
+              </template>
+              <template v-else-if="column.key === 'collectTime'">
+                {{ formatDateTime((record as MesCollectApi.OperateLog).collectTime) || '-' }}
+              </template>
+            </template>
+          </Table>
+        </Tabs.TabPane>
+
+        <Tabs.TabPane key="alarm" tab="鎶ヨ璁板綍">
+          <Table
+            :columns="alarmColumns"
+            :data-source="alarmPage.rows.value"
+            :loading="alarmPage.loading.value"
+            :pagination="{
+              current: alarmPage.pageNo.value,
+              pageSize: alarmPage.pageSize.value,
+              total: alarmPage.total.value,
+              showSizeChanger: true,
+              showTotal: (total: number) => `鍏� ${total} 鏉,
+            }"
+            :scroll="{ x: 1000 }"
+            row-key="id"
+            size="small"
+            @change="alarmPage.changePage"
+          >
+            <template #bodyCell="{ column, record }">
+              <template v-if="column.key === 'status'">
+                <Tag
+                  :color="
+                    (record as MesCollectApi.Alarm).status === 0
+                      ? 'error'
+                      : 'success'
+                  "
+                >
+                  {{ (record as MesCollectApi.Alarm).statusName || '-' }}
+                </Tag>
+              </template>
+              <template v-else-if="column.key === 'action'">
+                <Popconfirm
+                  v-if="(record as MesCollectApi.Alarm).status === 0"
+                  title="纭宸插鐞嗗苟娑堥櫎璇ユ姤璀︼紵"
+                  @confirm="handleAck(record as MesCollectApi.Alarm)"
+                >
+                  <Button
+                    v-access:code="['mes:collect-alarm:ack']"
+                    type="link"
+                    size="small"
+                  >
+                    纭娑堥櫎
+                  </Button>
+                </Popconfirm>
+                <span v-else class="text-gray-400">宸叉秷闄�</span>
+              </template>
+              <template v-else-if="column.key === 'wmTime'">
+                {{ formatDateTime((record as MesCollectApi.Alarm).wmTime) || '-' }}
+              </template>
+              <template v-else-if="column.key === 'clearTime'">
+                {{ formatDateTime((record as MesCollectApi.Alarm).clearTime) || '-' }}
+              </template>
+              <template v-else-if="column.key === 'collectTime'">
+                {{ formatDateTime((record as MesCollectApi.Alarm).collectTime) || '-' }}
+              </template>
+            </template>
+          </Table>
+        </Tabs.TabPane>
+      </Tabs>
+    </Drawer>
+
+    <Modal
+      v-model:open="rawJsonOpen"
+      :title="rawJsonTitle"
+      :footer="null"
+      width="800px"
+    >
+      <pre class="max-h-[70vh] overflow-auto rounded bg-gray-50 p-4 text-xs">{{
+        rawJsonContent
+      }}</pre>
+    </Modal>
+  </Page>
+</template>
diff --git a/src/views/mes/dv/machinery/data.ts b/src/views/mes/dv/machinery/data.ts
index ad0c421..056497d 100644
--- a/src/views/mes/dv/machinery/data.ts
+++ b/src/views/mes/dv/machinery/data.ts
@@ -113,6 +113,25 @@
       rules: z.number().default(MesDvMachineryStatusEnum.STOP),
     },
     {
+      fieldName: 'collectDeviceCode',
+      label: '鏁伴噰璁惧缂栧彿',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ユ暟閲囪澶囩紪鍙凤紝濡� C01',
+      },
+      help: '涓庢暟閲囩綉鍏充笂鎶ョ殑 devId 涓�鑷达紝鍏ㄥ眬鍞竴锛涚暀绌鸿〃绀烘湭鎺ュ叆鏁伴噰',
+    },
+    {
+      fieldName: 'collectEnabled',
+      label: '鍚敤鏁伴噰',
+      component: 'Switch',
+      componentProps: {
+        checkedChildren: '鏄�',
+        unCheckedChildren: '鍚�',
+      },
+    },
+    {
       fieldName: 'productionEfficiency',
       label: '鐢熶骇鏁堢巼',
       component: 'InputNumber',
@@ -219,6 +238,28 @@
       },
     },
     {
+      fieldName: 'collectDeviceCode',
+      label: '鏁伴噰璁惧缂栧彿',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ユ暟閲囪澶囩紪鍙�',
+      },
+    },
+    {
+      fieldName: 'collectEnabled',
+      label: '鍚敤鏁伴噰',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: [
+          { label: '鏄�', value: true },
+          { label: '鍚�', value: false },
+        ],
+        placeholder: '璇烽�夋嫨鏄惁鍚敤鏁伴噰',
+      },
+    },
+    {
       fieldName: 'status',
       label: '璁惧鐘舵��',
       component: 'Select',
@@ -247,6 +288,13 @@
     { field: 'specification', title: '瑙勬牸鍨嬪彿', minWidth: 140 },
     { field: 'machineryTypeName', title: '璁惧绫诲瀷', minWidth: 140 },
     { field: 'workshopName', title: '鎵�灞炶溅闂�', minWidth: 140 },
+    { field: 'collectDeviceCode', title: '鏁伴噰璁惧缂栧彿', minWidth: 130 },
+    {
+      field: 'collectEnabled',
+      title: '鍚敤鏁伴噰',
+      width: 100,
+      slots: { default: 'collectEnabled' },
+    },
     {
       field: 'status',
       title: '璁惧鐘舵��',
diff --git a/src/views/mes/dv/machinery/index.vue b/src/views/mes/dv/machinery/index.vue
index b2e4ee5..92c037c 100644
--- a/src/views/mes/dv/machinery/index.vue
+++ b/src/views/mes/dv/machinery/index.vue
@@ -8,7 +8,7 @@
 import { Page, useVbenModal } from '@vben/common-ui';
 import { downloadFileFromBlobPart } from '@vben/utils';
 
-import { Button, message } from 'ant-design-vue';
+import { Button, message, Tag } from 'ant-design-vue';
 
 import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
 import {
@@ -160,6 +160,11 @@
               {{ row.code }}
             </Button>
           </template>
+          <template #collectEnabled="{ row }">
+            <Tag :color="row.collectEnabled ? 'green' : 'default'">
+              {{ row.collectEnabled ? '鏄�' : '鍚�' }}
+            </Tag>
+          </template>
           <template #actions="{ row }">
             <TableAction
               :actions="[

--
Gitblit v1.9.3