云
2026-10-20 df2dfebf9f5525fdf6b162cb0ed2f0a5c6fa6146
feat(mes): 添加数采监控功能并完善设备管理

- 新增数采设备编号和启用数采字段到设备管理表单
- 集成数采设备状态显示和实时监控功能
- 添加数采设备API接口定义和实现
- 创建数采监控页面,支持实时状态、周期过程、工艺参数查看
- 实现报警记录管理和参数变更日志功能
- 添加数采设备在线状态和报警统计展示
- 集成原始数据查看和报警确认消除功能
已添加2个文件
已修改3个文件
996 ■■■■■ 文件已修改
src/api/mes/collect/index.ts 220 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/dv/machinery/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/collect/monitor/index.vue 719 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/machinery/data.ts 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/machinery/index.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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}`);
}
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; // åˆ›å»ºæ—¶é—´
  }
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>
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: '设备状态',
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="[