From 8b2d5ad1120551d9398a3aa1cf7e6467d9f40e7d Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期四, 13 八月 2026 10:49:23 +0800
Subject: [PATCH] feat(mes): 添加计量器具管理功能

---
 src/api/mes/dv/meteringcheck/index.ts                  |   60 ++
 dist.zip                                               |    0 
 src/views/mes/dv/metering/data.ts                      |  437 +++++++++++++++
 src/views/mes/dv/metering/components/select.vue        |  142 ++++
 src/views/mes/dv/meteringcheck/modules/form.vue        |   99 +++
 src/views/mes/dv/metering/modules/form.vue             |   99 +++
 src/views/mes/dv/metering/components/index.ts          |    2 
 src/packages/constants/src/biz-mes-enum.ts             |   20 
 src/views/mes/dv/metering/index.vue                    |  176 ++++++
 src/api/mes/dv/metering/index.ts                       |   72 ++
 src/views/mes/dv/metering/components/select-dialog.vue |  213 +++++++
 src/packages/constants/src/dict-enum.ts                |    3 
 src/views/mes/dv/meteringcheck/index.vue               |  158 +++++
 src/views/mes/dv/meteringcheck/data.ts                 |  218 +++++++
 14 files changed, 1,699 insertions(+), 0 deletions(-)

diff --git a/dist.zip b/dist.zip
new file mode 100644
index 0000000..b695091
--- /dev/null
+++ b/dist.zip
Binary files differ
diff --git a/src/api/mes/dv/metering/index.ts b/src/api/mes/dv/metering/index.ts
new file mode 100644
index 0000000..55f0e7e
--- /dev/null
+++ b/src/api/mes/dv/metering/index.ts
@@ -0,0 +1,72 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesDvMeteringApi {
+  /** MES 璁¢噺鍣ㄥ叿 */
+  export interface Metering {
+    id?: number; // 涓婚敭ID
+    code?: string; // 璁¢噺鍣ㄥ叿缂栫爜
+    name?: string; // 璁¢噺鍣ㄥ叿鍚嶇О
+    specification?: string; // 瑙勬牸鍨嬪彿
+    brand?: string; // 鍝佺墝/鐢熶骇鍘傚
+    category?: number; // 鍣ㄥ叿绫诲埆锛堝瓧鍏革級
+    precision?: string; // 绮惧害
+    range?: string; // 閲忕▼
+    deptId?: number; // 浣跨敤閮ㄩ棬缂栧彿
+    deptName?: string; // 浣跨敤閮ㄩ棬鍚嶇О
+    chargeUserId?: number; // 璐d换浜虹紪鍙�
+    chargeUserNickname?: string; // 璐d换浜烘樀绉�
+    checkCycleCount?: number; // 鏍″噯鍛ㄦ湡锛堟湀锛�
+    useDate?: string; // 鎶曞叆浣跨敤鏃ユ湡
+    lastCheckDate?: string; // 涓婃妫�娴嬫棩鏈�
+    nextCheckDate?: string; // 涓嬫妫�娴嬫棩鏈燂紙鍒版湡鏃ワ級
+    status?: number; // 鐘舵�侊紙瀛楀吀锛�
+    expireStatus?: number; // 鍒版湡鐘舵�侊細1 宸茶繃鏈� / 2 鍗冲皢鍒版湡 / 3 姝e父
+    expireDays?: number; // 璺濆埌鏈熷ぉ鏁�
+    remark?: string; // 澶囨敞
+    createTime?: Date; // 鍒涘缓鏃堕棿
+  }
+}
+
+/** 鏌ヨ璁¢噺鍣ㄥ叿鍒嗛〉 */
+export function getMeteringPage(params: PageParam) {
+  return requestClient.get<PageResult<MesDvMeteringApi.Metering>>(
+    '/mes/dv/metering/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ璁¢噺鍣ㄥ叿璇︽儏 */
+export function getMetering(id: number) {
+  return requestClient.get<MesDvMeteringApi.Metering>(
+    `/mes/dv/metering/get?id=${id}`,
+  );
+}
+
+/** 鏂板璁¢噺鍣ㄥ叿 */
+export function createMetering(data: MesDvMeteringApi.Metering) {
+  return requestClient.post<number>('/mes/dv/metering/create', data);
+}
+
+/** 淇敼璁¢噺鍣ㄥ叿 */
+export function updateMetering(data: MesDvMeteringApi.Metering) {
+  return requestClient.put('/mes/dv/metering/update', data);
+}
+
+/** 鍒犻櫎璁¢噺鍣ㄥ叿 */
+export function deleteMetering(id: number) {
+  return requestClient.delete(`/mes/dv/metering/delete?id=${id}`);
+}
+
+/** 瀵煎嚭璁¢噺鍣ㄥ叿 */
+export function exportMetering(params: any) {
+  return requestClient.download('/mes/dv/metering/export-excel', { params });
+}
+
+/** 鏌ヨ璁¢噺鍣ㄥ叿绮剧畝鍒楄〃锛堜笅鎷夐�夐」锛� */
+export function getMeteringSimpleList() {
+  return requestClient.get<MesDvMeteringApi.Metering[]>(
+    '/mes/dv/metering/simple-list',
+  );
+}
diff --git a/src/api/mes/dv/meteringcheck/index.ts b/src/api/mes/dv/meteringcheck/index.ts
new file mode 100644
index 0000000..e8995ec
--- /dev/null
+++ b/src/api/mes/dv/meteringcheck/index.ts
@@ -0,0 +1,60 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesDvMeteringCheckApi {
+  /** MES 璁¢噺妫�娴嬭褰� */
+  export interface MeteringCheck {
+    id?: number; // 涓婚敭ID
+    meteringId?: number; // 璁¢噺鍣ㄥ叿ID
+    meteringCode?: string; // 璁¢噺鍣ㄥ叿缂栫爜
+    meteringName?: string; // 璁¢噺鍣ㄥ叿鍚嶇О
+    checkCode?: string; // 妫�娴嬬紪鍙�
+    checkDate?: string; // 妫�娴嬫棩鏈�
+    checkOrg?: string; // 妫�娴嬫満鏋�
+    checkResult?: number; // 妫�娴嬬粨璁猴紙瀛楀吀锛�
+    certNo?: string; // 璇佷功缂栧彿
+    checkUserId?: number; // 妫�娴嬩汉缂栧彿
+    checkUserNickname?: string; // 妫�娴嬩汉鏄电О
+    nextCheckDate?: string; // 涓嬫妫�娴嬫棩鏈�
+    remark?: string; // 澶囨敞
+    createTime?: Date; // 鍒涘缓鏃堕棿
+  }
+}
+
+/** 鏌ヨ璁¢噺妫�娴嬭褰曞垎椤� */
+export function getMeteringCheckPage(params: PageParam) {
+  return requestClient.get<PageResult<MesDvMeteringCheckApi.MeteringCheck>>(
+    '/mes/dv/metering-check/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ璁¢噺妫�娴嬭褰曡鎯� */
+export function getMeteringCheck(id: number) {
+  return requestClient.get<MesDvMeteringCheckApi.MeteringCheck>(
+    `/mes/dv/metering-check/get?id=${id}`,
+  );
+}
+
+/** 鏂板璁¢噺妫�娴嬭褰� */
+export function createMeteringCheck(data: MesDvMeteringCheckApi.MeteringCheck) {
+  return requestClient.post<number>('/mes/dv/metering-check/create', data);
+}
+
+/** 淇敼璁¢噺妫�娴嬭褰� */
+export function updateMeteringCheck(data: MesDvMeteringCheckApi.MeteringCheck) {
+  return requestClient.put('/mes/dv/metering-check/update', data);
+}
+
+/** 鍒犻櫎璁¢噺妫�娴嬭褰� */
+export function deleteMeteringCheck(id: number) {
+  return requestClient.delete(`/mes/dv/metering-check/delete?id=${id}`);
+}
+
+/** 瀵煎嚭璁¢噺妫�娴嬭褰� */
+export function exportMeteringCheck(params: any) {
+  return requestClient.download('/mes/dv/metering-check/export-excel', {
+    params,
+  });
+}
diff --git a/src/packages/constants/src/biz-mes-enum.ts b/src/packages/constants/src/biz-mes-enum.ts
index 1ac585c..435b1db 100644
--- a/src/packages/constants/src/biz-mes-enum.ts
+++ b/src/packages/constants/src/biz-mes-enum.ts
@@ -104,6 +104,26 @@
   ABNORMAL: 2,
 } as const;
 
+/** MES 璁¢噺鍣ㄥ叿鐘舵�佹灇涓� */
+export const MesDvMeteringStatusEnum = {
+  IN_USE: 1, // 鍦ㄧ敤
+  STOPPED: 2, // 鍋滅敤
+  SCRAPPED: 3, // 鎶ュ簾
+} as const;
+
+/** MES 璁¢噺妫�娴嬬粨璁烘灇涓� */
+export const MesDvMeteringCheckResultEnum = {
+  QUALIFIED: 1, // 鍚堟牸
+  UNQUALIFIED: 2, // 涓嶅悎鏍�
+} as const;
+
+/** MES 璁¢噺鍣ㄥ叿鍒版湡鐘舵�佹灇涓� */
+export const MesDvMeteringExpireStatusEnum = {
+  OVERDUE: 1, // 宸茶繃鏈�
+  WITHIN: 2, // 鍗冲皢鍒版湡
+  NORMAL: 3, // 姝e父
+} as const;
+
 /** MES 缁翠慨宸ュ崟鐘舵�佹灇涓� */
 export const MesDvRepairStatusEnum = {
   PREPARE: MesOrderStatusConstants.DRAFT,
diff --git a/src/packages/constants/src/dict-enum.ts b/src/packages/constants/src/dict-enum.ts
index 8731055..e74a983 100644
--- a/src/packages/constants/src/dict-enum.ts
+++ b/src/packages/constants/src/dict-enum.ts
@@ -234,6 +234,9 @@
   MES_DV_REPAIR_RESULT: 'mes_dv_repair_result', // MES 缁翠慨缁撴灉
   MES_DV_CHECK_RECORD_STATUS: 'mes_dv_check_record_status', // MES 鐐规璁板綍鐘舵��
   MES_DV_CHECK_RESULT: 'mes_dv_check_result', // MES 鐐规缁撴灉
+  MES_DV_METERING_CATEGORY: 'mes_dv_metering_category', // MES 璁¢噺鍣ㄥ叿绫诲埆
+  MES_DV_METERING_STATUS: 'mes_dv_metering_status', // MES 璁¢噺鍣ㄥ叿鐘舵��
+  MES_DV_METERING_CHECK_RESULT: 'mes_dv_metering_check_result', // MES 璁¢噺妫�娴嬬粨璁�
   MES_PRO_LINK_TYPE: 'mes_pro_link_type', // MES 宸ュ簭鍏崇郴绫诲瀷
   MES_PRO_WORK_ORDER_STATUS: 'mes_pro_work_order_status', // MES 鐢熶骇宸ュ崟鐘舵��
   MES_PRO_WORK_ORDER_TYPE: 'mes_pro_work_order_type', // MES 宸ュ崟绫诲瀷
diff --git a/src/views/mes/dv/metering/components/index.ts b/src/views/mes/dv/metering/components/index.ts
new file mode 100644
index 0000000..feacb92
--- /dev/null
+++ b/src/views/mes/dv/metering/components/index.ts
@@ -0,0 +1,2 @@
+export { default as DvMeteringSelectDialog } from './select-dialog.vue';
+export { default as DvMeteringSelect } from './select.vue';
diff --git a/src/views/mes/dv/metering/components/select-dialog.vue b/src/views/mes/dv/metering/components/select-dialog.vue
new file mode 100644
index 0000000..5d2cd21
--- /dev/null
+++ b/src/views/mes/dv/metering/components/select-dialog.vue
@@ -0,0 +1,213 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesDvMeteringApi } from '#/api/mes/dv/metering';
+
+import { nextTick, ref } from 'vue';
+
+import { message, Modal } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getMeteringPage } from '#/api/mes/dv/metering';
+
+import {
+  useMeteringSelectGridColumns,
+  useMeteringSelectGridFormSchema,
+} from '../data';
+
+defineOptions({ name: 'DvMeteringSelectDialog' });
+
+const emit = defineEmits<{
+  selected: [rows: MesDvMeteringApi.Metering[]];
+}>();
+
+const open = ref(false); // 寮圭獥鏄惁鎵撳紑
+const multiple = ref(false); // 鏄惁澶氶��
+const selectedRows = ref<MesDvMeteringApi.Metering[]>([]); // 宸查�夊櫒鍏峰垪琛�
+const preSelectedIds = ref<number[]>([]); // 棰勯�夊櫒鍏风紪鍙峰垪琛�
+
+/** 鑾峰彇褰撳墠琛ㄦ牸鏁版嵁 */
+function getTableRows() {
+  return gridApi.grid.getTableData().fullData as MesDvMeteringApi.Metering[];
+}
+
+/** 鑾峰彇澶氶�夎褰曪紝鍖呭惈 VXE reserve 璺ㄩ〉璁板綍 */
+function getMultipleSelectedRows() {
+  const selectedMap = new Map<number, MesDvMeteringApi.Metering>();
+  const records = [
+    ...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
+    ...(gridApi.grid.getCheckboxRecords?.() ?? []),
+  ] as MesDvMeteringApi.Metering[];
+  records.forEach((row) => {
+    const rowId = row.id;
+    if (rowId !== undefined && rowId !== null) {
+      selectedMap.set(rowId, row);
+    }
+  });
+  return [...selectedMap.values()];
+}
+
+/** 澶勭悊鍕鹃�夊彉鍖� */
+function handleCheckboxSelectChange() {
+  selectedRows.value = getMultipleSelectedRows();
+}
+
+/** 澶勭悊鍗曢�夊彉鍖� */
+function handleRadioChange(row: MesDvMeteringApi.Metering) {
+  selectedRows.value = [row];
+}
+
+/** 澶氶�夋ā寮忎笅鍒囨崲琛屽嬀閫� */
+async function toggleMultipleRow(row: MesDvMeteringApi.Metering) {
+  const selected = gridApi.grid.isCheckedByCheckboxRow(row);
+  await gridApi.grid.setCheckboxRow(row, !selected);
+  selectedRows.value = getMultipleSelectedRows();
+}
+
+/** 澶勭悊琛屽弻鍑� */
+async function handleCellDblclick({
+  row,
+}: {
+  row: MesDvMeteringApi.Metering;
+}) {
+  if (multiple.value) {
+    await toggleMultipleRow(row);
+    return;
+  }
+  selectedRows.value = [row];
+  await gridApi.grid.setRadioRow(row);
+  handleConfirm();
+}
+
+/** 鍥炴樉棰勯�夊櫒鍏� */
+async function applyPreSelection() {
+  if (preSelectedIds.value.length === 0) {
+    return;
+  }
+  const rows = getTableRows();
+  for (const row of rows) {
+    if (
+      row.id === undefined ||
+      row.id === null ||
+      !preSelectedIds.value.includes(row.id)
+    ) {
+      continue;
+    }
+    if (multiple.value) {
+      await gridApi.grid.setCheckboxRow(row, true);
+    } else {
+      await gridApi.grid.setRadioRow(row);
+      selectedRows.value = [row];
+      return;
+    }
+  }
+  if (multiple.value) {
+    selectedRows.value = getMultipleSelectedRows();
+  }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useMeteringSelectGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useMeteringSelectGridColumns(false),
+    height: 480,
+    keepSource: true,
+    checkboxConfig: {
+      highlight: true,
+      range: true,
+      reserve: true,
+    },
+    radioConfig: {
+      highlight: true,
+      trigger: 'row',
+    },
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getMeteringPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<MesDvMeteringApi.Metering>,
+  gridEvents: {
+    cellDblclick: handleCellDblclick,
+    checkboxAll: handleCheckboxSelectChange,
+    checkboxChange: handleCheckboxSelectChange,
+    radioChange: ({ row }: { row: MesDvMeteringApi.Metering }) => {
+      handleRadioChange(row);
+    },
+  },
+});
+
+/** 閲嶇疆鏌ヨ鍜岄�夋嫨鐘舵�� */
+async function resetQueryState() {
+  selectedRows.value = [];
+  await gridApi.grid.clearCheckboxRow();
+  await gridApi.grid.clearCheckboxReserve();
+  await gridApi.grid.clearRadioRow();
+  await gridApi.formApi.resetForm();
+}
+
+/** 鎵撳紑璁¢噺鍣ㄥ叿閫夋嫨寮圭獥 */
+async function openModal(
+  selectedIds?: number[],
+  options?: { multiple?: boolean },
+) {
+  open.value = true;
+  multiple.value = options?.multiple ?? false;
+  preSelectedIds.value = selectedIds || [];
+  await nextTick();
+  gridApi.setGridOptions({
+    columns: useMeteringSelectGridColumns(multiple.value),
+  });
+  await resetQueryState();
+  await gridApi.query();
+  await nextTick();
+  await applyPreSelection();
+}
+
+/** 鍏抽棴璁¢噺鍣ㄥ叿閫夋嫨寮圭獥 */
+async function closeModal() {
+  open.value = false;
+  await resetQueryState();
+}
+
+/** 纭閫夋嫨璁¢噺鍣ㄥ叿 */
+function handleConfirm() {
+  const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value;
+  if (rows.length === 0) {
+    message.warning(multiple.value ? '璇疯嚦灏戦�夋嫨涓�鏉℃暟鎹�' : '璇烽�夋嫨涓�鏉℃暟鎹�');
+    return;
+  }
+  emit('selected', multiple.value ? rows : [rows[0]!]);
+  open.value = false;
+}
+
+defineExpose({ open: openModal });
+</script>
+
+<template>
+  <Modal
+    v-model:open="open"
+    title="璁¢噺鍣ㄥ叿閫夋嫨"
+    width="70%"
+    :destroy-on-close="true"
+    @ok="handleConfirm"
+    @cancel="closeModal"
+  >
+    <Grid table-title="璁¢噺鍣ㄥ叿鍒楄〃" />
+  </Modal>
+</template>
diff --git a/src/views/mes/dv/metering/components/select.vue b/src/views/mes/dv/metering/components/select.vue
new file mode 100644
index 0000000..018e89d
--- /dev/null
+++ b/src/views/mes/dv/metering/components/select.vue
@@ -0,0 +1,142 @@
+<script lang="ts" setup>
+import type { MesDvMeteringApi } from '#/api/mes/dv/metering';
+
+import { computed, ref, useAttrs, watch } from 'vue';
+
+import { IconifyIcon } from '@vben/icons';
+
+import { Input, Tooltip } from 'ant-design-vue';
+
+import { getMetering } from '#/api/mes/dv/metering';
+
+import DvMeteringSelectDialog from './select-dialog.vue';
+
+defineOptions({ name: 'DvMeteringSelect', inheritAttrs: false });
+
+const props = withDefaults(
+  defineProps<{
+    allowClear?: boolean;
+    disabled?: boolean;
+    modelValue?: number;
+    placeholder?: string;
+  }>(),
+  {
+    allowClear: true,
+    disabled: false,
+    modelValue: undefined,
+    placeholder: '璇烽�夋嫨璁¢噺鍣ㄥ叿',
+  },
+);
+const emit = defineEmits<{
+  change: [item: MesDvMeteringApi.Metering | undefined];
+  'update:modelValue': [value: number | undefined];
+}>();
+const attrs = useAttrs(); // 閫忎紶灞炴��
+const dialogRef = ref<InstanceType<typeof DvMeteringSelectDialog>>(); // 璁¢噺鍣ㄥ叿閫夋嫨寮圭獥
+const hovering = ref(false); // 鏄惁鎮仠
+const selectedItem = ref<MesDvMeteringApi.Metering>(); // 褰撳墠閫変腑鐨勮閲忓櫒鍏�
+
+const displayLabel = computed(() => selectedItem.value?.name ?? ''); // 閫夋嫨鍣ㄥ睍绀哄悕绉�
+const showClear = computed(
+  () =>
+    props.allowClear &&
+    !props.disabled &&
+    hovering.value &&
+    props.modelValue !== null,
+);
+
+/** 鏍规嵁鍣ㄥ叿缂栧彿鍥炴樉閫夋嫨鍣� */
+async function resolveItemById(id: number | undefined) {
+  if (!id) {
+    selectedItem.value = undefined;
+    return;
+  }
+  if (selectedItem.value?.id === id) {
+    return;
+  }
+  selectedItem.value = await getMetering(id);
+}
+
+watch(
+  () => props.modelValue,
+  (value) => {
+    resolveItemById(value);
+  },
+  { immediate: true },
+);
+
+/** 娓呯┖宸查�夊櫒鍏� */
+function clearSelected() {
+  selectedItem.value = undefined;
+  emit('update:modelValue', undefined);
+  emit('change', undefined);
+}
+
+/** 鎵撳紑璁¢噺鍣ㄥ叿閫夋嫨寮圭獥 */
+function handleClick(event: MouseEvent) {
+  if (props.disabled) {
+    return;
+  }
+  const target = event.target as HTMLElement;
+  if (showClear.value && target.closest('.ant-input-suffix')) {
+    event.stopPropagation();
+    clearSelected();
+    return;
+  }
+  const selectedIds = (
+    props.modelValue === null ? [] : [props.modelValue]
+  ) as number[];
+  dialogRef.value?.open(selectedIds, { multiple: false });
+}
+
+/** 鍥炲~閫変腑鐨勮閲忓櫒鍏� */
+function handleSelected(rows: MesDvMeteringApi.Metering[]) {
+  const item = rows[0];
+  if (!item) {
+    return;
+  }
+  selectedItem.value = item;
+  emit('update:modelValue', item.id);
+  emit('change', item);
+}
+</script>
+
+<template>
+  <div
+    v-bind="attrs"
+    class="w-full"
+    :class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
+    @click="handleClick"
+    @mouseenter="hovering = true"
+    @mouseleave="hovering = false"
+  >
+    <Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
+      <template #title>
+        <div v-if="selectedItem" class="leading-6">
+          <div>鍣ㄥ叿缂栫爜锛歿{ selectedItem.code || '-' }}</div>
+          <div>鍣ㄥ叿鍚嶇О锛歿{ selectedItem.name || '-' }}</div>
+          <div v-if="selectedItem.specification">
+            瑙勬牸鍨嬪彿锛歿{ selectedItem.specification }}
+          </div>
+          <div v-if="selectedItem.deptName">
+            浣跨敤閮ㄩ棬锛歿{ selectedItem.deptName }}
+          </div>
+        </div>
+      </template>
+      <Input
+        :disabled="disabled"
+        :placeholder="placeholder"
+        :value="displayLabel"
+        readonly
+      >
+        <template #suffix>
+          <IconifyIcon
+            class="size-4"
+            :icon="showClear ? 'lucide:circle-x' : 'lucide:search'"
+          />
+        </template>
+      </Input>
+    </Tooltip>
+  </div>
+  <DvMeteringSelectDialog ref="dialogRef" @selected="handleSelected" />
+</template>
diff --git a/src/views/mes/dv/metering/data.ts b/src/views/mes/dv/metering/data.ts
new file mode 100644
index 0000000..1f57561
--- /dev/null
+++ b/src/views/mes/dv/metering/data.ts
@@ -0,0 +1,437 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesDvMeteringApi } from '#/api/mes/dv/metering';
+
+import {
+  DICT_TYPE,
+  MesDvMeteringExpireStatusEnum,
+  MesDvMeteringStatusEnum,
+} from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { handleTree } from '@vben/utils';
+
+import { z } from '#/adapter/form';
+import { getSimpleDeptList } from '#/api/system/dept';
+import { getSimpleUserList } from '#/api/system/user';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+/** 鍒版湡鐘舵�佸睍绀洪厤缃� */
+export const METERING_EXPIRE_STATUS_OPTIONS: {
+  color: string;
+  label: string;
+  value: number;
+}[] = [
+  {
+    label: '宸茶繃鏈�',
+    value: MesDvMeteringExpireStatusEnum.OVERDUE,
+    color: 'red',
+  },
+  {
+    label: '鍗冲皢鍒版湡',
+    value: MesDvMeteringExpireStatusEnum.WITHIN,
+    color: 'orange',
+  },
+  {
+    label: '姝e父',
+    value: MesDvMeteringExpireStatusEnum.NORMAL,
+    color: 'green',
+  },
+];
+
+/** 鏂板/淇敼璁¢噺鍣ㄥ叿鐨勮〃鍗� */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'code',
+      label: '鍣ㄥ叿缂栫爜',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ櫒鍏风紪鐮�',
+      },
+      dependencies: {
+        triggerFields: ['id'],
+        componentProps: (values) => ({ disabled: !!values.id }),
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'name',
+      label: '鍣ㄥ叿鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ櫒鍏峰悕绉�',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'category',
+      label: '鍣ㄥ叿绫诲埆',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_DV_METERING_CATEGORY, 'number'),
+        placeholder: '璇烽�夋嫨鍣ㄥ叿绫诲埆',
+      },
+      help: '濡傚崱灏恒�佸帇鍔涜〃銆佹俯搴﹁绛夛紝涓嶅悓绫诲埆鏈夊父瑙佺殑鏍″噯鍛ㄦ湡',
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'specification',
+      label: '瑙勬牸鍨嬪彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ鏍煎瀷鍙�',
+      },
+    },
+    {
+      fieldName: 'brand',
+      label: '鍝佺墝/鐢熶骇鍘傚',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ搧鐗�/鐢熶骇鍘傚',
+      },
+    },
+    {
+      fieldName: 'precision',
+      label: '绮惧害',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ョ簿搴�',
+      },
+      help: '娴嬮噺绮惧害锛屽 0.01mm',
+    },
+    {
+      fieldName: 'range',
+      label: '閲忕▼',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ラ噺绋�',
+      },
+      help: '娴嬮噺鑼冨洿锛屽 0~150mm',
+    },
+    {
+      fieldName: 'deptId',
+      label: '浣跨敤閮ㄩ棬',
+      component: 'ApiTreeSelect',
+      componentProps: {
+        api: async () => handleTree(await getSimpleDeptList()),
+        childrenField: 'children',
+        labelField: 'name',
+        placeholder: '璇烽�夋嫨浣跨敤閮ㄩ棬',
+        valueField: 'id',
+      },
+    },
+    {
+      fieldName: 'chargeUserId',
+      label: '璐d换浜�',
+      component: 'ApiSelect',
+      componentProps: {
+        allowClear: true,
+        api: getSimpleUserList,
+        labelField: 'nickname',
+        placeholder: '璇烽�夋嫨璐d换浜�',
+        valueField: 'id',
+      },
+      help: '璇ュ櫒鍏风殑缁存姢璐d换浜猴紝鍒版湡鎻愰啋鐨勭珯鍐呬俊浼氬彂缁欎粬',
+    },
+    {
+      fieldName: 'checkCycleCount',
+      label: '鏍″噯鍛ㄦ湡',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 1,
+        precision: 0,
+        placeholder: '璇疯緭鍏ユ牎鍑嗗懆鏈�',
+        suffix: '鏈�',
+      },
+      help: '鍣ㄥ叿姣忛殧 N 涓湀椤绘牎鍑�/妫�瀹氫竴娆★紱鏂板妫�娴嬭褰曟椂涓嶅~涓嬫妫�娴嬫棩鏈燂紝灏嗘寜銆屾娴嬫棩鏈� + 鏍″噯鍛ㄦ湡銆嶈嚜鍔ㄨ绠�',
+      rules: 'required',
+    },
+    {
+      fieldName: 'useDate',
+      label: '鎶曞叆浣跨敤鏃ユ湡',
+      component: 'DatePicker',
+      componentProps: {
+        class: '!w-full',
+        format: 'YYYY-MM-DD',
+        placeholder: '璇烽�夋嫨鎶曞叆浣跨敤鏃ユ湡',
+        valueFormat: 'YYYY-MM-DD',
+      },
+      help: '鍣ㄥ叿寮�濮嬫姇鍏ヤ娇鐢ㄧ殑鏃ユ湡锛堜粎浣滆褰曪級',
+    },
+    {
+      fieldName: 'lastCheckDate',
+      label: '涓婃妫�娴嬫棩鏈�',
+      component: 'DatePicker',
+      componentProps: {
+        class: '!w-full',
+        disabled: true,
+        format: 'YYYY-MM-DD',
+        placeholder: '涓婃妫�娴嬫棩鏈�',
+        valueFormat: 'YYYY-MM-DD',
+      },
+      help: '鐢辨渶鏂颁竴鏉℃娴嬭褰曡嚜鍔ㄥ洖鍐欙紝涓嶅彲鎵嬪姩淇敼',
+      dependencies: {
+        triggerFields: ['id'],
+        show: () => formType === 'detail',
+      },
+    },
+    {
+      fieldName: 'nextCheckDate',
+      label: '涓嬫妫�娴嬫棩鏈�',
+      component: 'DatePicker',
+      componentProps: {
+        class: '!w-full',
+        disabled: true,
+        format: 'YYYY-MM-DD',
+        placeholder: '涓嬫妫�娴嬫棩鏈�',
+        valueFormat: 'YYYY-MM-DD',
+      },
+      help: '鍒版湡鏃ワ紝鐢辨娴嬭褰曡嚜鍔ㄨ绠楀洖鍐欙紱鍒版湡鍓� N 澶╋紙鍩虹璁炬柦閰嶇疆锛夊皢鎻愰啋璐d换浜�',
+      dependencies: {
+        triggerFields: ['id'],
+        show: () => formType === 'detail',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鍣ㄥ叿鐘舵��',
+      component: 'RadioGroup',
+      componentProps: {
+        buttonStyle: 'solid',
+        optionType: 'button',
+        options: getDictOptions(DICT_TYPE.MES_DV_METERING_STATUS, 'number'),
+      },
+      help: '鍦ㄧ敤/鍋滅敤/鎶ュ簾',
+      rules: z.number().default(MesDvMeteringStatusEnum.IN_USE),
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-3',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 3,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'code',
+      label: '鍣ㄥ叿缂栫爜',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ュ櫒鍏风紪鐮�',
+      },
+    },
+    {
+      fieldName: 'name',
+      label: '鍣ㄥ叿鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ュ櫒鍏峰悕绉�',
+      },
+    },
+    {
+      fieldName: 'category',
+      label: '鍣ㄥ叿绫诲埆',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_DV_METERING_CATEGORY, 'number'),
+        placeholder: '璇烽�夋嫨鍣ㄥ叿绫诲埆',
+      },
+    },
+    {
+      fieldName: 'deptId',
+      label: '浣跨敤閮ㄩ棬',
+      component: 'ApiTreeSelect',
+      componentProps: {
+        allowClear: true,
+        api: async () => handleTree(await getSimpleDeptList()),
+        childrenField: 'children',
+        labelField: 'name',
+        placeholder: '璇烽�夋嫨浣跨敤閮ㄩ棬',
+        valueField: 'id',
+      },
+    },
+    {
+      fieldName: 'chargeUserId',
+      label: '璐d换浜�',
+      component: 'ApiSelect',
+      componentProps: {
+        allowClear: true,
+        api: getSimpleUserList,
+        labelField: 'nickname',
+        placeholder: '璇烽�夋嫨璐d换浜�',
+        valueField: 'id',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鍣ㄥ叿鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_DV_METERING_STATUS, 'number'),
+        placeholder: '璇烽�夋嫨鍣ㄥ叿鐘舵��',
+      },
+    },
+    {
+      fieldName: 'expireStatus',
+      label: '鍒版湡鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: METERING_EXPIRE_STATUS_OPTIONS,
+        placeholder: '璇烽�夋嫨鍒版湡鐘舵��',
+      },
+      help: '涓嬫妫�娴嬫棩鏈熻窛浠婂ぉ 鈮� 鎻愰啋澶╂暟锛堝熀纭�璁炬柦閰嶇疆 mes.dv-metering.remind-days锛夊嵆瑙嗕负銆屽嵆灏嗗埌鏈熴��',
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesDvMeteringApi.Metering>['columns'] {
+  return [
+    {
+      field: 'code',
+      title: '鍣ㄥ叿缂栫爜',
+      minWidth: 150,
+      slots: {
+        default: 'code',
+      },
+    },
+    { field: 'name', title: '鍣ㄥ叿鍚嶇О', minWidth: 150 },
+    {
+      field: 'category',
+      title: '鍣ㄥ叿绫诲埆',
+      width: 110,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_DV_METERING_CATEGORY },
+      },
+    },
+    { field: 'specification', title: '瑙勬牸鍨嬪彿', minWidth: 130 },
+    { field: 'brand', title: '鍝佺墝/鐢熶骇鍘傚', minWidth: 130 },
+    { field: 'deptName', title: '浣跨敤閮ㄩ棬', minWidth: 130 },
+    { field: 'chargeUserNickname', title: '璐d换浜�', minWidth: 100 },
+    {
+      field: 'expireStatus',
+      title: '鍒版湡鐘舵��',
+      width: 110,
+      slots: {
+        default: 'expireStatus',
+      },
+    },
+    {
+      field: 'nextCheckDate',
+      title: '涓嬫妫�娴嬫棩鏈�',
+      width: 130,
+      formatter: 'formatDate',
+    },
+    {
+      field: 'status',
+      title: '鍣ㄥ叿鐘舵��',
+      width: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_DV_METERING_STATUS },
+      },
+    },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 180,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 210,
+      fixed: 'right',
+      slots: {
+        default: 'actions',
+      },
+    },
+  ];
+}
+
+/** 璁¢噺鍣ㄥ叿閫夋嫨寮圭獥鐨勬悳绱㈣〃鍗� */
+export function useMeteringSelectGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'code',
+      label: '鍣ㄥ叿缂栫爜',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ュ櫒鍏风紪鐮�',
+      },
+    },
+    {
+      fieldName: 'name',
+      label: '鍣ㄥ叿鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ュ櫒鍏峰悕绉�',
+      },
+    },
+  ];
+}
+
+/** 璁¢噺鍣ㄥ叿閫夋嫨寮圭獥鐨勫瓧娈� */
+export function useMeteringSelectGridColumns(
+  multiple = false,
+): VxeTableGridOptions<MesDvMeteringApi.Metering>['columns'] {
+  return [
+    { type: multiple ? 'checkbox' : 'radio', width: 50 },
+    { field: 'code', title: '鍣ㄥ叿缂栫爜', width: 140 },
+    { field: 'name', title: '鍣ㄥ叿鍚嶇О', minWidth: 140 },
+    {
+      field: 'category',
+      title: '鍣ㄥ叿绫诲埆',
+      width: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_DV_METERING_CATEGORY },
+      },
+    },
+    { field: 'specification', title: '瑙勬牸鍨嬪彿', minWidth: 120 },
+    { field: 'deptName', title: '浣跨敤閮ㄩ棬', width: 120 },
+    { field: 'chargeUserNickname', title: '璐d换浜�', width: 100 },
+    {
+      field: 'nextCheckDate',
+      title: '涓嬫妫�娴嬫棩鏈�',
+      width: 120,
+      formatter: 'formatDate',
+    },
+    {
+      field: 'status',
+      title: '鍣ㄥ叿鐘舵��',
+      width: 90,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_DV_METERING_STATUS },
+      },
+    },
+  ];
+}
diff --git a/src/views/mes/dv/metering/index.vue b/src/views/mes/dv/metering/index.vue
new file mode 100644
index 0000000..27cbd3c
--- /dev/null
+++ b/src/views/mes/dv/metering/index.vue
@@ -0,0 +1,176 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesDvMeteringApi } from '#/api/mes/dv/metering';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { Button, message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  deleteMetering,
+  exportMetering,
+  getMeteringPage,
+} from '#/api/mes/dv/metering';
+import { $t } from '#/locales';
+
+import {
+  METERING_EXPIRE_STATUS_OPTIONS,
+  useGridColumns,
+  useGridFormSchema,
+} from './data';
+import Form from './modules/form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+  gridApi.query();
+}
+
+/** 鍒涘缓璁¢噺鍣ㄥ叿 */
+function handleCreate() {
+  formModalApi.setData({ formType: 'create' }).open();
+}
+
+/** 鏌ョ湅璁¢噺鍣ㄥ叿 */
+function handleDetail(row: MesDvMeteringApi.Metering) {
+  formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+}
+
+/** 缂栬緫璁¢噺鍣ㄥ叿 */
+function handleEdit(row: MesDvMeteringApi.Metering) {
+  formModalApi.setData({ id: row.id, formType: 'update' }).open();
+}
+
+/** 鍒犻櫎璁¢噺鍣ㄥ叿 */
+async function handleDelete(row: MesDvMeteringApi.Metering) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting', [row.name]),
+    duration: 0,
+  });
+  try {
+    await deleteMetering(row.id!);
+    message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 瀵煎嚭璁¢噺鍣ㄥ叿 */
+async function handleExport() {
+  const data = await exportMetering(await gridApi.formApi.getValues());
+  downloadFileFromBlobPart({ fileName: '璁¢噺鍣ㄥ叿.xls', source: data });
+}
+
+/** 鍒版湡鐘舵�佸睍绀� */
+function getExpireStatusConfig(status?: number) {
+  return METERING_EXPIRE_STATUS_OPTIONS.find((item) => item.value === status);
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) =>
+          await getMeteringPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          }),
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<MesDvMeteringApi.Metering>,
+});
+</script>
+<template>
+  <Page auto-content-height>
+    <FormModal @success="handleRefresh" />
+    <Grid table-title="璁¢噺鍣ㄥ叿鍒楄〃">
+      <template #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: $t('ui.actionTitle.create', ['璁¢噺鍣ㄥ叿']),
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['mes:dv-metering:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: $t('ui.actionTitle.export'),
+              type: 'primary',
+              icon: ACTION_ICON.DOWNLOAD,
+              auth: ['mes:dv-metering:export'],
+              onClick: handleExport,
+            },
+          ]"
+        />
+      </template>
+      <template #code="{ row }">
+        <Button type="link" @click="handleDetail(row)">
+          {{ row.code }}
+        </Button>
+      </template>
+      <template #expireStatus="{ row }">
+        <Tag
+          v-if="getExpireStatusConfig(row.expireStatus)"
+          :color="getExpireStatusConfig(row.expireStatus)!.color"
+        >
+          {{ getExpireStatusConfig(row.expireStatus)!.label }}
+        </Tag>
+        <span v-else>-</span>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['mes:dv-metering:update'],
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['mes:dv-metering:delete'],
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.name]),
+                confirm: handleDelete.bind(null, row),
+              },
+            },
+            {
+              label: $t('common.detail'),
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['mes:dv-metering:query'],
+              onClick: handleDetail.bind(null, row),
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/mes/dv/metering/modules/form.vue b/src/views/mes/dv/metering/modules/form.vue
new file mode 100644
index 0000000..62535f4
--- /dev/null
+++ b/src/views/mes/dv/metering/modules/form.vue
@@ -0,0 +1,99 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { MesDvMeteringApi } from '#/api/mes/dv/metering';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createMetering,
+  getMetering,
+  updateMetering,
+} from '#/api/mes/dv/metering';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formType = ref<FormType>('create');
+const formData = ref<MesDvMeteringApi.Metering>();
+const isDetail = computed(() => formType.value === 'detail');
+const getTitle = computed(() => {
+  if (formType.value === 'detail') {
+    return '鏌ョ湅璁¢噺鍣ㄥ叿';
+  }
+  return formType.value === 'update' ? '淇敼璁¢噺鍣ㄥ叿' : '鏂板璁¢噺鍣ㄥ叿';
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    formItemClass: 'col-span-1',
+    labelWidth: 110,
+  },
+  wrapperClass: 'grid-cols-3',
+  layout: 'horizontal',
+  schema: [],
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    if (isDetail.value) {
+      await modalApi.close();
+      return;
+    }
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    const data = (await formApi.getValues()) as MesDvMeteringApi.Metering;
+    try {
+      if (formType.value === 'create') {
+        await createMetering(data);
+      } else {
+        await updateMetering(data);
+      }
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+      await modalApi.close();
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      formData.value = undefined;
+      return;
+    }
+    const data = modalApi.getData<{ formType: FormType; id?: number }>();
+    formType.value = data.formType;
+    formApi.setState({ schema: useFormSchema(formType.value) });
+    formApi.setDisabled(formType.value === 'detail');
+    modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
+    if (!data?.id) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getMetering(data.id);
+      await formApi.setValues(formData.value);
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+<template>
+  <Modal :title="getTitle" class="w-3/5">
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/mes/dv/meteringcheck/data.ts b/src/views/mes/dv/meteringcheck/data.ts
new file mode 100644
index 0000000..4e1e4e9
--- /dev/null
+++ b/src/views/mes/dv/meteringcheck/data.ts
@@ -0,0 +1,218 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesDvMeteringCheckApi } from '#/api/mes/dv/meteringcheck';
+
+import { markRaw } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getSimpleUserList } from '#/api/system/user';
+import { DvMeteringSelect } from '#/views/mes/dv/metering/components';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+/** 鏂板/淇敼璁¢噺妫�娴嬭褰曠殑琛ㄥ崟 */
+export function useFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'meteringId',
+      label: '璁¢噺鍣ㄥ叿',
+      component: markRaw(DvMeteringSelect),
+      componentProps: {
+        placeholder: '璇烽�夋嫨璁¢噺鍣ㄥ叿',
+      },
+      help: '閫夋嫨鍣ㄥ叿鍚庯紝鍏舵牎鍑嗗懆鏈熷皢鐢ㄤ簬鑷姩璁$畻涓嬫妫�娴嬫棩鏈�',
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'checkCode',
+      label: '妫�娴嬬紪鍙�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ娴嬬紪鍙�',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'checkDate',
+      label: '妫�娴嬫棩鏈�',
+      component: 'DatePicker',
+      componentProps: {
+        class: '!w-full',
+        format: 'YYYY-MM-DD',
+        placeholder: '璇烽�夋嫨妫�娴嬫棩鏈�',
+        valueFormat: 'YYYY-MM-DD',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'checkOrg',
+      label: '妫�娴嬫満鏋�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ娴嬫満鏋�',
+      },
+    },
+    {
+      fieldName: 'checkResult',
+      label: '妫�娴嬬粨璁�',
+      component: 'RadioGroup',
+      componentProps: {
+        buttonStyle: 'solid',
+        optionType: 'button',
+        options: getDictOptions(DICT_TYPE.MES_DV_METERING_CHECK_RESULT, 'number'),
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'certNo',
+      label: '璇佷功缂栧彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ瘉涔︾紪鍙�',
+      },
+    },
+    {
+      fieldName: 'checkUserId',
+      label: '妫�娴嬩汉',
+      component: 'ApiSelect',
+      componentProps: {
+        allowClear: true,
+        api: getSimpleUserList,
+        labelField: 'nickname',
+        placeholder: '璇烽�夋嫨妫�娴嬩汉',
+        valueField: 'id',
+      },
+    },
+    {
+      fieldName: 'nextCheckDate',
+      label: '涓嬫妫�娴嬫棩鏈�',
+      component: 'DatePicker',
+      componentProps: {
+        class: '!w-full',
+        format: 'YYYY-MM-DD',
+        placeholder: '璇烽�夋嫨涓嬫妫�娴嬫棩鏈�',
+        valueFormat: 'YYYY-MM-DD',
+      },
+      help: '鍒版湡鏃ワ紝寤鸿涓嶅~锛氬皢鎸夈�屾娴嬫棩鏈� + 鏍″噯鍛ㄦ湡銆嶈嚜鍔ㄨ绠楀苟鍥炲啓鍒板櫒鍏凤紱涔熷彲鎵嬪姩鎸囧畾',
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-3',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 3,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'meteringId',
+      label: '璁¢噺鍣ㄥ叿',
+      component: markRaw(DvMeteringSelect),
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇烽�夋嫨璁¢噺鍣ㄥ叿',
+      },
+    },
+    {
+      fieldName: 'checkCode',
+      label: '妫�娴嬬紪鍙�',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ユ娴嬬紪鍙�',
+      },
+    },
+    {
+      fieldName: 'checkResult',
+      label: '妫�娴嬬粨璁�',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_DV_METERING_CHECK_RESULT, 'number'),
+        placeholder: '璇烽�夋嫨妫�娴嬬粨璁�',
+      },
+    },
+    {
+      fieldName: 'checkDate',
+      label: '妫�娴嬫棩鏈�',
+      component: 'RangePicker',
+      componentProps: {
+        allowClear: true,
+        format: 'YYYY-MM-DD',
+        valueFormat: 'YYYY-MM-DD',
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesDvMeteringCheckApi.MeteringCheck>['columns'] {
+  return [
+    {
+      field: 'meteringCode',
+      title: '鍣ㄥ叿缂栫爜',
+      minWidth: 140,
+      slots: {
+        default: 'meteringCode',
+      },
+    },
+    { field: 'meteringName', title: '鍣ㄥ叿鍚嶇О', minWidth: 150 },
+    { field: 'checkCode', title: '妫�娴嬬紪鍙�', minWidth: 140 },
+    {
+      field: 'checkDate',
+      title: '妫�娴嬫棩鏈�',
+      width: 120,
+      formatter: 'formatDate',
+    },
+    { field: 'checkOrg', title: '妫�娴嬫満鏋�', minWidth: 140 },
+    {
+      field: 'checkResult',
+      title: '妫�娴嬬粨璁�',
+      width: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_DV_METERING_CHECK_RESULT },
+      },
+    },
+    { field: 'certNo', title: '璇佷功缂栧彿', minWidth: 130 },
+    { field: 'checkUserNickname', title: '妫�娴嬩汉', minWidth: 100 },
+    {
+      field: 'nextCheckDate',
+      title: '涓嬫妫�娴嬫棩鏈�',
+      width: 130,
+      formatter: 'formatDate',
+    },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 180,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 200,
+      fixed: 'right',
+      slots: {
+        default: 'actions',
+      },
+    },
+  ];
+}
diff --git a/src/views/mes/dv/meteringcheck/index.vue b/src/views/mes/dv/meteringcheck/index.vue
new file mode 100644
index 0000000..c8039f3
--- /dev/null
+++ b/src/views/mes/dv/meteringcheck/index.vue
@@ -0,0 +1,158 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesDvMeteringCheckApi } from '#/api/mes/dv/meteringcheck';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { Button, message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  deleteMeteringCheck,
+  exportMeteringCheck,
+  getMeteringCheckPage,
+} from '#/api/mes/dv/meteringcheck';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+  gridApi.query();
+}
+
+/** 鍒涘缓璁¢噺妫�娴嬭褰� */
+function handleCreate() {
+  formModalApi.setData({ formType: 'create' }).open();
+}
+
+/** 鏌ョ湅璁¢噺妫�娴嬭褰� */
+function handleDetail(row: MesDvMeteringCheckApi.MeteringCheck) {
+  formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+}
+
+/** 缂栬緫璁¢噺妫�娴嬭褰� */
+function handleEdit(row: MesDvMeteringCheckApi.MeteringCheck) {
+  formModalApi.setData({ id: row.id, formType: 'update' }).open();
+}
+
+/** 鍒犻櫎璁¢噺妫�娴嬭褰� */
+async function handleDelete(row: MesDvMeteringCheckApi.MeteringCheck) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting', [row.checkCode]),
+    duration: 0,
+  });
+  try {
+    await deleteMeteringCheck(row.id!);
+    message.success($t('ui.actionMessage.deleteSuccess', [row.checkCode]));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 瀵煎嚭璁¢噺妫�娴嬭褰� */
+async function handleExport() {
+  const data = await exportMeteringCheck(await gridApi.formApi.getValues());
+  downloadFileFromBlobPart({ fileName: '璁¢噺妫�娴嬭褰�.xls', source: data });
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) =>
+          await getMeteringCheckPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          }),
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<MesDvMeteringCheckApi.MeteringCheck>,
+});
+</script>
+<template>
+  <Page auto-content-height>
+    <FormModal @success="handleRefresh" />
+    <Grid table-title="璁¢噺妫�娴嬭褰曞垪琛�">
+      <template #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: $t('ui.actionTitle.create', ['妫�娴嬭褰�']),
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['mes:dv-metering-check:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: $t('ui.actionTitle.export'),
+              type: 'primary',
+              icon: ACTION_ICON.DOWNLOAD,
+              auth: ['mes:dv-metering-check:export'],
+              onClick: handleExport,
+            },
+          ]"
+        />
+      </template>
+      <template #meteringCode="{ row }">
+        <Button type="link" @click="handleDetail(row)">
+          {{ row.meteringCode }}
+        </Button>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['mes:dv-metering-check:update'],
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['mes:dv-metering-check:delete'],
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.checkCode]),
+                confirm: handleDelete.bind(null, row),
+              },
+            },
+            {
+              label: $t('common.detail'),
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['mes:dv-metering-check:query'],
+              onClick: handleDetail.bind(null, row),
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/mes/dv/meteringcheck/modules/form.vue b/src/views/mes/dv/meteringcheck/modules/form.vue
new file mode 100644
index 0000000..308d312
--- /dev/null
+++ b/src/views/mes/dv/meteringcheck/modules/form.vue
@@ -0,0 +1,99 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { MesDvMeteringCheckApi } from '#/api/mes/dv/meteringcheck';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createMeteringCheck,
+  getMeteringCheck,
+  updateMeteringCheck,
+} from '#/api/mes/dv/meteringcheck';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formType = ref<FormType>('create');
+const formData = ref<MesDvMeteringCheckApi.MeteringCheck>();
+const isDetail = computed(() => formType.value === 'detail');
+const getTitle = computed(() => {
+  if (formType.value === 'detail') {
+    return '鏌ョ湅璁¢噺妫�娴嬭褰�';
+  }
+  return formType.value === 'update' ? '淇敼璁¢噺妫�娴嬭褰�' : '鏂板璁¢噺妫�娴嬭褰�';
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    formItemClass: 'col-span-1',
+    labelWidth: 110,
+  },
+  wrapperClass: 'grid-cols-3',
+  layout: 'horizontal',
+  schema: [],
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    if (isDetail.value) {
+      await modalApi.close();
+      return;
+    }
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    const data = (await formApi.getValues()) as MesDvMeteringCheckApi.MeteringCheck;
+    try {
+      if (formType.value === 'create') {
+        await createMeteringCheck(data);
+      } else {
+        await updateMeteringCheck(data);
+      }
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+      await modalApi.close();
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      formData.value = undefined;
+      return;
+    }
+    const data = modalApi.getData<{ formType: FormType; id?: number }>();
+    formType.value = data.formType;
+    formApi.setState({ schema: useFormSchema() });
+    formApi.setDisabled(formType.value === 'detail');
+    modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
+    if (!data?.id) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getMeteringCheck(data.id);
+      await formApi.setValues(formData.value);
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+<template>
+  <Modal :title="getTitle" class="w-3/5">
+    <Form class="mx-4" />
+  </Modal>
+</template>

--
Gitblit v1.9.3