From 6fbec5eebedf6d14cf1b262c608d586957180bb7 Mon Sep 17 00:00:00 2001
From: zhangwencui <1064582902@qq.com>
Date: 星期五, 17 七月 2026 17:40:17 +0800
Subject: [PATCH] Merge branch 'dev_pro2.0' of http://114.132.189.42:9002/r/mom-pro2-before into dev_pro2.0

---
 src/views/mes/pro/mps/data.ts                  |  299 +++++++++++
 src/views/erp/purchase/order/index.vue         |    7 
 src/views/crm/business/data.ts                 |    9 
 src/views/erp/sale/order/modules/item-form.vue |   22 
 src/views/crm/receivable/data.ts               |   14 
 src/views/mes/pro/mps/index.vue                |  321 ++++++++++++
 src/api/crm/customer/index.ts                  |    4 
 src/views/crm/receivable/plan/data.ts          |   14 
 src/views/erp/sale/order/data.ts               |   29 
 src/views/crm/contact/data.ts                  |   12 
 src/views/crm/contract/data.ts                 |   14 
 src/views/erp/purchase/order/data.ts           |    2 
 src/views/mes/pro/mps/modules/form.vue         |  175 +++++++
 src/api/erp/sale/order/index.ts                |    3 
 src/components/crm-customer-select.vue         |  166 ++++--
 src/views/crm/saleQuotation/data.ts            |   14 
 src/views/erp/sale/order/index.vue             |    4 
 src/components/crm-customer-select-dialog.vue  |  236 +++++++++
 src/api/mes/pro/mps/index.ts                   |  107 ++++
 19 files changed, 1,334 insertions(+), 118 deletions(-)

diff --git a/src/api/crm/customer/index.ts b/src/api/crm/customer/index.ts
index 038625f..4c17fde 100644
--- a/src/api/crm/customer/index.ts
+++ b/src/api/crm/customer/index.ts
@@ -110,7 +110,9 @@
 
 /** 棰嗗彇鍏捣瀹㈡埛 */
 export function receiveCustomer(ids: number[]) {
-  return requestClient.put('/crm/customer/receive', { ids: ids.join(',') });
+  return requestClient.put('/crm/customer/receive', null, {
+    params: { ids: ids.join(',') },
+  });
 }
 
 /** 鍒嗛厤鍏捣缁欏搴旇礋璐d汉 */
diff --git a/src/api/erp/sale/order/index.ts b/src/api/erp/sale/order/index.ts
index 2d36e54..5126cf7 100644
--- a/src/api/erp/sale/order/index.ts
+++ b/src/api/erp/sale/order/index.ts
@@ -25,6 +25,7 @@
     depositPrice?: number; // 瀹氶噾閲戦锛屽崟浣嶏細鍏�
     contractId?: number; // 鍏宠仈 CRM 鍚堝悓缂栧彿
     contractNo?: string; // CRM 鍚堝悓鍗曞彿
+    needProduction?: number; // 鏄惁闇�瑕佺敓浜э細0-涓嶉渶瑕侊紝1-闇�瑕�
     items?: SaleOrderItem[]; // 閿�鍞鍗曚骇鍝佹槑缁嗗垪琛�
   }
 
@@ -47,6 +48,8 @@
     totalTaxPrice?: number; // 鍚◣鎬讳环锛屽崟浣嶏細鍏�
     remark?: string; // 澶囨敞
     stockCount?: number; // 搴撳瓨鏁伴噺锛堟樉绀哄瓧娈碉級
+    needProduction?: number; // 鏄惁闇�瑕佺敓浜э細0-涓嶉渶瑕侊紝1-闇�瑕�
+    mpsId?: number; // MPS缂栧彿
   }
 }
 
diff --git a/src/api/mes/pro/mps/index.ts b/src/api/mes/pro/mps/index.ts
new file mode 100644
index 0000000..c8b4b8e
--- /dev/null
+++ b/src/api/mes/pro/mps/index.ts
@@ -0,0 +1,107 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesProMpsApi {
+  /** MPS 涓荤敓浜ц鍒� */
+  export interface Mps {
+    id?: number; // MPS 缂栧彿
+    code?: string; // MPS 缂栫爜
+    status?: number; // 鐘舵�侊紙0=鑽夌 1=宸蹭笅鍙� 2=宸插畬鎴� 3=宸插彇娑堬級
+    saleOrderId?: number; // 閿�鍞鍗旾D
+    saleOrderNo?: string; // 閿�鍞鍗曞彿
+    saleOrderItemId?: number; // 閿�鍞鍗曟槑缁咺D
+    productId?: number; // 浜у搧ID
+    productCode?: string; // 浜у搧缂栫爜
+    productName?: string; // 浜у搧鍚嶇О
+    productSpecification?: string; // 瑙勬牸鍨嬪彿
+    unitMeasureName?: string; // 鍗曚綅
+    quantity?: number; // 璁″垝鐢熶骇鏁伴噺
+    quantityProduced?: number; // 宸茬敓浜ф暟閲�
+    clientId?: number; // 瀹㈡埛ID
+    clientCode?: string; // 瀹㈡埛缂栫爜
+    clientName?: string; // 瀹㈡埛鍚嶇О
+    requestDate?: number; // 闇�姹傛棩鏈�
+    issueDate?: number; // 涓嬪彂鏃ユ湡
+    finishDate?: number; // 瀹屾垚鏃ユ湡
+    workOrderId?: number; // 鐢熶骇宸ュ崟ID
+    workOrderCode?: string; // 鐢熶骇宸ュ崟缂栫爜
+    mergeFlag?: number; // 鏄惁鍚堝苟涓嬪彂锛�0-鍚︼紝1-鏄級
+    mergeBatchNo?: string; // 鍚堝苟鎵规鍙�
+    remark?: string; // 澶囨敞
+    createTime?: number; // 鍒涘缓鏃堕棿
+  }
+
+  /** MPS 鍒嗛〉鏌ヨ鍙傛暟 */
+  export interface PageParams extends PageParam {
+    code?: string;
+    saleOrderNo?: string;
+    productId?: number;
+    clientId?: number;
+    status?: number;
+    requestDate?: number[];
+  }
+
+  /** MPS 鍒涘缓璇锋眰 */
+  export interface CreateMpsReq {
+    code?: string;
+    saleOrderId?: number;
+    saleOrderNo?: string;
+    saleOrderItemId?: number;
+    productId?: number;
+    quantity?: number;
+    unitMeasureId?: number;
+    clientId?: number;
+    requestDate?: string;
+    remark?: string;
+  }
+}
+
+/** 鏌ヨ MPS 鍒嗛〉 */
+export function getMpsPage(params: MesProMpsApi.PageParams) {
+  return requestClient.get<PageResult<MesProMpsApi.Mps>>('/mes/pro-mps/page', {
+    params,
+  });
+}
+
+/** 鏌ヨ MPS 璇︽儏 */
+export function getMps(id: number) {
+  return requestClient.get<MesProMpsApi.Mps>(`/mes/pro-mps/get?id=${id}`);
+}
+
+/** 鍒涘缓 MPS */
+export function createMps(data: MesProMpsApi.CreateMpsReq) {
+  return requestClient.post('/mes/pro-mps/create', data);
+}
+
+/** 鏇存柊 MPS */
+export function updateMps(data: MesProMpsApi.CreateMpsReq & { id: number }) {
+  return requestClient.put('/mes/pro-mps/update', data);
+}
+
+/** 鍒犻櫎 MPS */
+export function deleteMps(ids: number[]) {
+  return requestClient.delete('/mes/pro-mps/delete', {
+    params: { ids: ids.join(',') },
+  });
+}
+
+/** 瀵煎嚭 MPS Excel */
+export function exportMps(params: any) {
+  return requestClient.download('/mes/pro-mps/export-excel', { params });
+}
+
+/** 涓嬪彂 MPS */
+export function issueMps(id: number) {
+  return requestClient.post(`/mes/pro-mps/issue?id=${id}`);
+}
+
+/** 鍚堝苟涓嬪彂 MPS */
+export function mergeIssueMps(ids: number[]) {
+  return requestClient.post('/mes/pro-mps/merge-issue', ids);
+}
+
+/** 鍙栨秷 MPS */
+export function cancelMps(id: number) {
+  return requestClient.post(`/mes/pro-mps/cancel?id=${id}`);
+}
diff --git a/src/components/crm-customer-select-dialog.vue b/src/components/crm-customer-select-dialog.vue
new file mode 100644
index 0000000..a514f9c
--- /dev/null
+++ b/src/components/crm-customer-select-dialog.vue
@@ -0,0 +1,236 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmCustomerApi } from '#/api/crm/customer';
+
+import { nextTick, ref } from 'vue';
+
+import { message, Modal, Tabs } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getCustomerPage } from '#/api/crm/customer';
+
+defineOptions({ name: 'CrmCustomerSelectDialog' });
+
+const emit = defineEmits<{
+  selected: [rows: CrmCustomerApi.Customer[]];
+}>();
+
+const open = ref(false);
+const multiple = ref(true);
+const selectedRows = ref<CrmCustomerApi.Customer[]>([]);
+const preSelectedIds = ref<number[]>([]);
+const sceneType = ref('1');
+
+function getTableRows() {
+  return gridApi.grid.getTableData().fullData as CrmCustomerApi.Customer[];
+}
+
+function getMultipleSelectedRows() {
+  const selectedMap = new Map<number, CrmCustomerApi.Customer>();
+  const records = [
+    ...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
+    ...(gridApi.grid.getCheckboxRecords?.() ?? []),
+  ] as CrmCustomerApi.Customer[];
+  records.forEach((row) => {
+    const rowId = row.id;
+    if (rowId) {
+      selectedMap.set(rowId, row);
+    }
+  });
+  return [...selectedMap.values()];
+}
+
+function handleCheckboxSelectChange() {
+  selectedRows.value = getMultipleSelectedRows();
+}
+
+function handleRadioChange(row: CrmCustomerApi.Customer) {
+  selectedRows.value = [row];
+}
+
+async function toggleMultipleRow(row: CrmCustomerApi.Customer) {
+  const selected = gridApi.grid.isCheckedByCheckboxRow(row);
+  await gridApi.grid.setCheckboxRow(row, !selected);
+  selectedRows.value = getMultipleSelectedRows();
+}
+
+async function handleCellDblclick({
+  row,
+}: {
+  row: CrmCustomerApi.Customer;
+}) {
+  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();
+  }
+}
+
+function handleSceneTypeChange(key: number | string) {
+  sceneType.value = key.toString();
+  gridApi.query();
+}
+
+const gridColumns = [
+  { field: 'name', title: '瀹㈡埛鍚嶇О', minWidth: 180 },
+  { field: 'mobile', title: '鎵嬫満鍙�', width: 140 },
+  { field: 'telephone', title: '鐢佃瘽', width: 140 },
+  { field: 'areaName', title: '鎵�鍦ㄥ湴', width: 160 },
+  { field: 'detailAddress', title: '璇︾粏鍦板潃', minWidth: 200 },
+];
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: [
+      {
+        fieldName: 'name',
+        label: '瀹㈡埛鍚嶇О',
+        component: 'Input',
+        componentProps: {
+          allowClear: true,
+          placeholder: '璇疯緭鍏ュ鎴峰悕绉�',
+        },
+      },
+    ],
+  },
+  gridOptions: {
+    columns: [{ type: 'radio', width: 50 }, ...gridColumns],
+    height: 460,
+    keepSource: true,
+    checkboxConfig: {
+      highlight: true,
+      range: true,
+      reserve: true,
+    },
+    radioConfig: {
+      highlight: true,
+      trigger: 'row',
+    },
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getCustomerPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            sceneType: sceneType.value,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<CrmCustomerApi.Customer>,
+  gridEvents: {
+    cellDblclick: handleCellDblclick,
+    checkboxAll: handleCheckboxSelectChange,
+    checkboxChange: handleCheckboxSelectChange,
+    radioChange: ({
+      row,
+    }: {
+      row: CrmCustomerApi.Customer;
+    }) => {
+      handleRadioChange(row);
+    },
+  },
+});
+
+async function resetQueryState() {
+  sceneType.value = '1';
+  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 ?? true;
+  preSelectedIds.value = selectedIds || [];
+  await nextTick();
+  gridApi.setGridOptions({
+    columns: multiple.value
+      ? [{ type: 'checkbox', width: 50 }, ...gridColumns]
+      : [{ type: 'radio', width: 50 }, ...gridColumns],
+  });
+  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"
+  >
+    <Tabs class="mb-2" @change="handleSceneTypeChange">
+      <Tabs.TabPane key="1" tab="鎴戣礋璐g殑" />
+      <Tabs.TabPane key="2" tab="鎴戝弬涓庣殑" />
+      <Tabs.TabPane key="3" tab="涓嬪睘璐熻矗鐨�" />
+    </Tabs>
+    <Grid table-title="瀹㈡埛鍒楄〃" />
+  </Modal>
+</template>
diff --git a/src/components/crm-customer-select.vue b/src/components/crm-customer-select.vue
index 8f982bc..64dcc12 100644
--- a/src/components/crm-customer-select.vue
+++ b/src/components/crm-customer-select.vue
@@ -1,75 +1,133 @@
 <script lang="ts" setup>
 import type { CrmCustomerApi } from '#/api/crm/customer';
 
-import { computed, ref, watch } from 'vue';
+import { computed, ref, useAttrs, watch } from 'vue';
 
-import { Select } from 'ant-design-vue';
+import { IconifyIcon } from '@vben/icons';
 
-import { getCustomerSimpleList } from '#/api/crm/customer';
+import { Input, Tooltip } from 'ant-design-vue';
 
-interface Props {
-  value?: number;
-  disabled?: boolean;
-}
+import { getCustomer } from '#/api/crm/customer';
 
-const props = withDefaults(defineProps<Props>(), {
-  value: undefined,
-  disabled: false,
-});
+import CrmCustomerSelectDialog from './crm-customer-select-dialog.vue';
 
-const emit = defineEmits(['update:value', 'change']);
+defineOptions({ name: 'CrmCustomerSelect', inheritAttrs: false });
 
-const customerList = ref<CrmCustomerApi.Customer[]>([]);
-const loading = ref(false);
-
-const options = computed(() => {
-  return customerList.value.map((item) => ({
-    label: item.name,
-    value: item.id,
-  }));
-});
-
-const selectedValue = ref(props.value);
-
-watch(
-  () => props.value,
-  (val) => {
-    selectedValue.value = val;
+const props = withDefaults(
+  defineProps<{
+    allowClear?: boolean;
+    disabled?: boolean;
+    modelValue?: number;
+    placeholder?: string;
+  }>(),
+  {
+    allowClear: true,
+    disabled: false,
+    modelValue: undefined,
+    placeholder: '璇烽�夋嫨瀹㈡埛',
   },
 );
+const emit = defineEmits<{
+  change: [item: CrmCustomerApi.Customer | undefined];
+  'update:modelValue': [value: number | undefined];
+}>();
+const attrs = useAttrs();
+const dialogRef = ref<InstanceType<typeof CrmCustomerSelectDialog>>();
+const hovering = ref(false);
+const selectedItem = ref<CrmCustomerApi.Customer>();
 
-watch(selectedValue, (val) => {
-  emit('update:value', val);
-  const customer = customerList.value.find((item) => item.id === val);
-  emit('change', customer);
-});
+const displayLabel = computed(() => selectedItem.value?.name ?? '');
+const showClear = computed(
+  () =>
+    props.allowClear &&
+    !props.disabled &&
+    hovering.value &&
+    props.modelValue !== null,
+);
 
-async function loadCustomers() {
-  if (customerList.value.length > 0) return;
-  loading.value = true;
-  try {
-    customerList.value = await getCustomerSimpleList();
-  } finally {
-    loading.value = false;
+async function resolveCustomerById(id: number | undefined) {
+  if (!id) {
+    selectedItem.value = undefined;
+    return;
   }
+  if (selectedItem.value?.id === id) {
+    return;
+  }
+  selectedItem.value = await getCustomer(id);
 }
 
-function filterOption(input: string, option: any) {
-  return option.label.toLowerCase().includes(input.toLowerCase());
+watch(
+  () => props.modelValue,
+  (value) => {
+    resolveCustomerById(value);
+  },
+  { immediate: true },
+);
+
+function clearSelected() {
+  selectedItem.value = undefined;
+  emit('update:modelValue', undefined);
+  emit('change', undefined);
 }
 
-loadCustomers();
+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: CrmCustomerApi.Customer[]) {
+  const item = rows[0];
+  if (!item) {
+    return;
+  }
+  selectedItem.value = item;
+  emit('update:modelValue', item.id);
+  emit('change', item);
+}
 </script>
 
 <template>
-  <Select
-    v-model:value="selectedValue"
-    :options="options"
-    :loading="loading"
-    :disabled="disabled"
-    placeholder="璇烽�夋嫨瀹㈡埛"
-    show-search
-    :filter-option="filterOption"
-    allow-clear
-  />
-</template>
\ No newline at end of file
+  <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.name || '-' }}</div>
+          <div>鎵嬫満锛歿{ selectedItem.mobile || '-' }}</div>
+          <div>鍦板尯锛歿{ selectedItem.areaName || '-' }}</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>
+  <CrmCustomerSelectDialog ref="dialogRef" @selected="handleSelected" />
+</template>
diff --git a/src/views/crm/business/data.ts b/src/views/crm/business/data.ts
index 818f1a0..5dd221d 100644
--- a/src/views/crm/business/data.ts
+++ b/src/views/crm/business/data.ts
@@ -6,8 +6,10 @@
 
 import { z } from '#/adapter/form';
 import { getBusinessStatusTypeSimpleList } from '#/api/crm/business/status';
-import { getCustomerSimpleList } from '#/api/crm/customer';
 import { getSimpleUserList } from '#/api/system/user';
+import CrmCustomerSelect from '#/components/crm-customer-select.vue';
+
+import { markRaw } from 'vue';
 
 /** 鏂板/淇敼鐨勮〃鍗� */
 export function useFormSchema(): VbenFormSchema[] {
@@ -52,11 +54,8 @@
     {
       fieldName: 'customerId',
       label: '瀹㈡埛鍚嶇О',
-      component: 'ApiSelect',
+      component: markRaw(CrmCustomerSelect),
       componentProps: {
-        api: getCustomerSimpleList,
-        labelField: 'name',
-        valueField: 'id',
         placeholder: '璇烽�夋嫨瀹㈡埛',
         allowClear: true,
       },
diff --git a/src/views/crm/contact/data.ts b/src/views/crm/contact/data.ts
index b798a12..a95a655 100644
--- a/src/views/crm/contact/data.ts
+++ b/src/views/crm/contact/data.ts
@@ -8,9 +8,9 @@
 import { useUserStore } from '@vben/stores';
 
 import { getSimpleContactList } from '#/api/crm/contact';
-import { getCustomerSimpleList } from '#/api/crm/customer';
 import { getSimpleUserList } from '#/api/system/user';
 import { AreaCascader } from '#/components/area';
+import CrmCustomerSelect from '#/components/crm-customer-select.vue';
 
 /** 鏂板/淇敼鐨勮〃鍗� */
 export function useFormSchema(): VbenFormSchema[] {
@@ -53,12 +53,9 @@
     {
       fieldName: 'customerId',
       label: '瀹㈡埛鍚嶇О',
-      component: 'ApiSelect',
+      component: markRaw(CrmCustomerSelect),
       rules: 'required',
       componentProps: {
-        api: getCustomerSimpleList,
-        labelField: 'name',
-        valueField: 'id',
         placeholder: '璇烽�夋嫨瀹㈡埛',
       },
     },
@@ -189,11 +186,8 @@
     {
       fieldName: 'customerId',
       label: '瀹㈡埛',
-      component: 'ApiSelect',
+      component: markRaw(CrmCustomerSelect),
       componentProps: {
-        api: getCustomerSimpleList,
-        labelField: 'name',
-        valueField: 'id',
         placeholder: '璇烽�夋嫨瀹㈡埛',
       },
     },
diff --git a/src/views/crm/contract/data.ts b/src/views/crm/contract/data.ts
index cc5afc9..de4629d 100644
--- a/src/views/crm/contract/data.ts
+++ b/src/views/crm/contract/data.ts
@@ -7,8 +7,10 @@
 import { z } from '#/adapter/form';
 import { getSimpleBusinessList } from '#/api/crm/business';
 import { getSimpleContactList } from '#/api/crm/contact';
-import { getCustomerSimpleList } from '#/api/crm/customer';
 import { getSimpleUserList } from '#/api/system/user';
+import CrmCustomerSelect from '#/components/crm-customer-select.vue';
+
+import { markRaw } from 'vue';
 
 /** 鏂板/淇敼鐨勮〃鍗� */
 export function useFormSchema(): VbenFormSchema[] {
@@ -59,12 +61,9 @@
     {
       fieldName: 'customerId',
       label: '瀹㈡埛鍚嶇О',
-      component: 'ApiSelect',
+      component: markRaw(CrmCustomerSelect),
       rules: 'required',
       componentProps: {
-        api: getCustomerSimpleList,
-        labelField: 'name',
-        valueField: 'id',
         placeholder: '璇烽�夋嫨瀹㈡埛',
       },
     },
@@ -280,11 +279,8 @@
     {
       fieldName: 'customerId',
       label: '瀹㈡埛',
-      component: 'ApiSelect',
+      component: markRaw(CrmCustomerSelect),
       componentProps: {
-        api: getCustomerSimpleList,
-        labelField: 'name',
-        valueField: 'id',
         placeholder: '璇烽�夋嫨瀹㈡埛',
         allowClear: true,
       },
diff --git a/src/views/crm/receivable/data.ts b/src/views/crm/receivable/data.ts
index b85771d..e1e0255 100644
--- a/src/views/crm/receivable/data.ts
+++ b/src/views/crm/receivable/data.ts
@@ -6,12 +6,14 @@
 import { useUserStore } from '@vben/stores';
 
 import { getContractSimpleList } from '#/api/crm/contract';
-import { getCustomerSimpleList } from '#/api/crm/customer';
 import {
   getReceivablePlan,
   getReceivablePlanSimpleList,
 } from '#/api/crm/receivable/plan';
 import { getSimpleUserList } from '#/api/system/user';
+import CrmCustomerSelect from '#/components/crm-customer-select.vue';
+
+import { markRaw } from 'vue';
 
 /** 鏂板/淇敼鐨勮〃鍗� */
 export function useFormSchema(): VbenFormSchema[] {
@@ -55,12 +57,9 @@
     {
       fieldName: 'customerId',
       label: '瀹㈡埛鍚嶇О',
-      component: 'ApiSelect',
+      component: markRaw(CrmCustomerSelect),
       rules: 'required',
       componentProps: (_values, form) => ({
-        api: getCustomerSimpleList,
-        labelField: 'name',
-        valueField: 'id',
         placeholder: '璇烽�夋嫨瀹㈡埛',
         onChange: () => {
           form.setFieldValue('contractId', undefined);
@@ -216,11 +215,8 @@
     {
       fieldName: 'customerId',
       label: '瀹㈡埛',
-      component: 'ApiSelect',
+      component: markRaw(CrmCustomerSelect),
       componentProps: {
-        api: getCustomerSimpleList,
-        labelField: 'name',
-        valueField: 'id',
         placeholder: '璇烽�夋嫨瀹㈡埛',
         allowClear: true,
       },
diff --git a/src/views/crm/receivable/plan/data.ts b/src/views/crm/receivable/plan/data.ts
index bd5ef22..6527ce2 100644
--- a/src/views/crm/receivable/plan/data.ts
+++ b/src/views/crm/receivable/plan/data.ts
@@ -7,8 +7,10 @@
 import { erpPriceInputFormatter } from '@vben/utils';
 
 import { getContractSimpleList } from '#/api/crm/contract';
-import { getCustomerSimpleList } from '#/api/crm/customer';
 import { getSimpleUserList } from '#/api/system/user';
+import CrmCustomerSelect from '#/components/crm-customer-select.vue';
+
+import { markRaw } from 'vue';
 
 /** 鏂板/淇敼鐨勮〃鍗� */
 export function useFormSchema(): VbenFormSchema[] {
@@ -42,12 +44,9 @@
     {
       fieldName: 'customerId',
       label: '瀹㈡埛',
-      component: 'ApiSelect',
+      component: markRaw(CrmCustomerSelect),
       rules: 'required',
       componentProps: {
-        api: getCustomerSimpleList,
-        labelField: 'name',
-        valueField: 'id',
         placeholder: '璇烽�夋嫨瀹㈡埛',
         allowClear: true,
       },
@@ -153,11 +152,8 @@
     {
       fieldName: 'customerId',
       label: '瀹㈡埛',
-      component: 'ApiSelect',
+      component: markRaw(CrmCustomerSelect),
       componentProps: {
-        api: getCustomerSimpleList,
-        labelField: 'name',
-        valueField: 'id',
         placeholder: '璇烽�夋嫨瀹㈡埛',
         allowClear: true,
       },
diff --git a/src/views/crm/saleQuotation/data.ts b/src/views/crm/saleQuotation/data.ts
index ca86211..09cf646 100644
--- a/src/views/crm/saleQuotation/data.ts
+++ b/src/views/crm/saleQuotation/data.ts
@@ -6,10 +6,12 @@
 import { z } from '#/adapter/form';
 import { getSimpleBusinessList } from '#/api/crm/business';
 import { getSimpleContactList } from '#/api/crm/contact';
-import { getCustomerSimpleList } from '#/api/crm/customer';
 import { getItemPage } from '#/api/mdm/item';
 import { getSimpleUserList } from '#/api/system/user';
 import { useUserStore } from '#/packages/stores/src';
+import CrmCustomerSelect from '#/components/crm-customer-select.vue';
+
+import { markRaw } from 'vue';
 
 /** 鎶ヤ环鍗曠姸鎬� */
 export const QUOTATION_STATUS = {
@@ -62,12 +64,9 @@
     {
       fieldName: 'customerId',
       label: '瀹㈡埛鍚嶇О',
-      component: 'ApiSelect',
+      component: markRaw(CrmCustomerSelect),
       rules: 'required',
       componentProps: {
-        api: getCustomerSimpleList,
-        labelField: 'name',
-        valueField: 'id',
         placeholder: '璇烽�夋嫨瀹㈡埛',
       },
     },
@@ -240,11 +239,8 @@
     {
       fieldName: 'customerId',
       label: '瀹㈡埛',
-      component: 'ApiSelect',
+      component: markRaw(CrmCustomerSelect),
       componentProps: {
-        api: getCustomerSimpleList,
-        labelField: 'name',
-        valueField: 'id',
         placeholder: '璇烽�夋嫨瀹㈡埛',
         allowClear: true,
       },
diff --git a/src/views/erp/purchase/order/data.ts b/src/views/erp/purchase/order/data.ts
index c97f2c2..cf47ffd 100644
--- a/src/views/erp/purchase/order/data.ts
+++ b/src/views/erp/purchase/order/data.ts
@@ -431,6 +431,7 @@
       field: 'status',
       title: '鐘舵��',
       minWidth: 120,
+      fixed: 'right',
       cellRender: {
         name: 'CellDict',
         props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
@@ -440,6 +441,7 @@
       field: 'inStatus',
       title: '鍏ュ簱鐘舵��',
       minWidth: 100,
+      fixed: 'right',
       slots: { default: 'inStatus' },
     },
     {
diff --git a/src/views/erp/purchase/order/index.vue b/src/views/erp/purchase/order/index.vue
index 1d76f59..cc275a3 100644
--- a/src/views/erp/purchase/order/index.vue
+++ b/src/views/erp/purchase/order/index.vue
@@ -7,7 +7,7 @@
 import { Page, useVbenModal } from '@vben/common-ui';
 import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
 
-import { message } from 'ant-design-vue';
+import { message, Tag } from 'ant-design-vue';
 
 import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
 import {
@@ -183,6 +183,11 @@
           ]"
         />
       </template>
+      <template #inStatus="{ row }">
+        <Tag v-if="row.inStatus === 0" color="default">鏈叆搴�</Tag>
+        <Tag v-else-if="row.inStatus === 1" color="warning">閮ㄥ垎鍏ュ簱</Tag>
+        <Tag v-else-if="row.inStatus === 2" color="success">鍏ㄩ儴鍏ュ簱</Tag>
+      </template>
       <template #actions="{ row }">
         <TableAction
           :actions="[
diff --git a/src/views/erp/sale/order/data.ts b/src/views/erp/sale/order/data.ts
index 4428be2..0cf418a 100644
--- a/src/views/erp/sale/order/data.ts
+++ b/src/views/erp/sale/order/data.ts
@@ -6,9 +6,11 @@
 import { z } from '#/adapter/form';
 import { getAccountSimpleList } from '#/api/erp/finance/account';
 import { getItemPage } from '#/api/mdm/item';
-import { getCustomerSimpleList as getCrmCustomerSimpleList } from '#/api/crm/customer';
 import { getSimpleUserList } from '#/api/system/user';
 import { getRangePickerDefaultProps } from '#/utils';
+import CrmCustomerSelect from '#/components/crm-customer-select.vue';
+
+import { markRaw } from 'vue';
 
 /** 琛ㄥ崟绫诲瀷 */
 export type FormType = 'create' | 'detail' | 'edit';
@@ -48,14 +50,10 @@
     {
       label: '瀹㈡埛',
       fieldName: 'customerId',
-      component: 'ApiSelect',
+      component: markRaw(CrmCustomerSelect),
       componentProps: {
         placeholder: '璇烽�夋嫨瀹㈡埛',
         allowClear: true,
-        showSearch: true,
-        api: getCrmCustomerSimpleList,
-        labelField: 'name',
-        valueField: 'id',
       },
       rules: 'required',
     },
@@ -221,6 +219,12 @@
       minWidth: 80,
     },
     {
+      field: 'needProduction',
+      title: '闇�瑕佺敓浜�',
+      minWidth: 100,
+      slots: { default: 'needProduction' },
+    },
+    {
       field: 'remark',
       title: '澶囨敞',
       minWidth: 150,
@@ -318,14 +322,10 @@
     {
       fieldName: 'supplierId',
       label: '瀹㈡埛',
-      component: 'ApiSelect',
+      component: markRaw(CrmCustomerSelect),
       componentProps: {
         placeholder: '璇烽�夋嫨瀹㈡埛',
         allowClear: true,
-        showSearch: true,
-        api: getCrmCustomerSimpleList,
-        labelField: 'name',
-        valueField: 'id',
       },
     },
     {
@@ -482,6 +482,13 @@
       slots: { default: 'status' },
     },
     {
+      field: 'needProduction',
+      title: '闇�瑕佺敓浜�',
+      minWidth: 100,
+      fixed: 'right',
+      slots: { default: 'needProduction' },
+    },
+    {
       title: '鎿嶄綔',
       width: 260,
       fixed: 'right',
diff --git a/src/views/erp/sale/order/index.vue b/src/views/erp/sale/order/index.vue
index 99cf346..fa775a4 100644
--- a/src/views/erp/sale/order/index.vue
+++ b/src/views/erp/sale/order/index.vue
@@ -248,6 +248,10 @@
         <Tag v-if="row.status === 20" color="success">宸插鏍�</Tag>
         <Tag v-if="row.status === 30" color="error">宸蹭綔搴�</Tag>
       </template>
+      <template #needProduction="{ row }">
+        <Tag v-if="row.needProduction === 1" color="success">鏄�</Tag>
+        <Tag v-else color="default">鍚�</Tag>
+      </template>
       <template #actions="{ row }">
         <TableAction
           :actions="[
diff --git a/src/views/erp/sale/order/modules/item-form.vue b/src/views/erp/sale/order/modules/item-form.vue
index f84d5ac..f877805 100644
--- a/src/views/erp/sale/order/modules/item-form.vue
+++ b/src/views/erp/sale/order/modules/item-form.vue
@@ -10,7 +10,7 @@
   erpPriceMultiply,
 } from '@vben/utils';
 
-import { Input, InputNumber, Select } from 'ant-design-vue';
+import { Input, InputNumber, Select, Tag } from 'ant-design-vue';
 
 import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
 import { getItemPage } from '#/api/mdm/item';
@@ -134,6 +134,7 @@
     taxPrice: undefined,
     totalPrice: undefined,
     remark: undefined,
+    needProduction: 1, // 榛樿闇�瑕佺敓浜�
   };
   tableData.value.push(newRow);
   // 閫氱煡鐖剁粍浠舵洿鏂�
@@ -190,6 +191,10 @@
       erpPriceMultiply(row.totalProductPrice, (row.taxPercent || 0) / 100) ?? 0;
     row.totalPrice = row.totalProductPrice + row.taxPrice;
   }
+  // 榛樿闇�瑕佺敓浜�
+  if (row.needProduction === undefined) {
+    row.needProduction = 1;
+  }
 }
 
 /** 琛ㄥ崟鏍¢獙 */
@@ -240,6 +245,21 @@
       />
       <span v-else>{{ row.productName || '-' }}</span>
     </template>
+    <template #needProduction="{ row }">
+      <Select
+        v-if="!disabled"
+        v-model:value="row.needProduction"
+        :options="[
+          { label: '鏄�', value: 1 },
+          { label: '鍚�', value: 0 },
+        ]"
+        class="w-full"
+        placeholder="璇烽�夋嫨"
+        @change="handleRowChange(row)"
+      />
+      <Tag v-else-if="row.needProduction === 1" color="success">鏄�</Tag>
+      <Tag v-else color="default">鍚�</Tag>
+    </template>
     <template #count="{ row }">
       <InputNumber
         v-if="!disabled"
diff --git a/src/views/mes/pro/mps/data.ts b/src/views/mes/pro/mps/data.ts
new file mode 100644
index 0000000..7e0a1a9
--- /dev/null
+++ b/src/views/mes/pro/mps/data.ts
@@ -0,0 +1,299 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProMpsApi } from '#/api/mes/pro/mps';
+
+import { markRaw } from 'vue';
+
+import { getRangePickerDefaultProps } from '#/utils';
+import { MdItemSelect } from '#/views/mes/md/item/components';
+import CrmCustomerSelect from '#/components/crm-customer-select.vue';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'edit';
+
+/** MPS 鐘舵�佹灇涓� */
+export const MpsStatusEnum = {
+  DRAFT: 0, // 鑽夌
+  ISSUED: 1, // 宸蹭笅鍙�
+  FINISHED: 2, // 宸插畬鎴�
+  CANCELLED: 3, // 宸插彇娑�
+};
+
+/** 鏂板/缂栬緫琛ㄥ崟 */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+  const isCreate = formType === 'create';
+  const isEdit = formType === 'edit';
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'code',
+      label: 'MPS缂栫爜',
+      component: 'Input',
+      componentProps: {
+        placeholder: '绯荤粺鑷姩鐢熸垚',
+        disabled: true,
+      },
+    },
+    {
+      fieldName: 'saleOrderId',
+      label: '閿�鍞鍗旾D',
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '璇疯緭鍏ラ攢鍞鍗旾D锛堝彲閫夛級',
+        disabled: !isCreate && !isEdit,
+      },
+    },
+    {
+      fieldName: 'saleOrderNo',
+      label: '閿�鍞鍗曞彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ラ攢鍞鍗曞彿锛堝彲閫夛級',
+        disabled: !isCreate && !isEdit,
+      },
+    },
+    {
+      fieldName: 'saleOrderItemId',
+      label: '閿�鍞鍗曟槑缁咺D',
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '璇疯緭鍏ラ攢鍞鍗曟槑缁咺D锛堝彲閫夛級',
+        disabled: !isCreate && !isEdit,
+      },
+    },
+    {
+      fieldName: 'productId',
+      label: '浜у搧',
+      component: markRaw(MdItemSelect),
+      componentProps: {
+        placeholder: '璇烽�夋嫨浜у搧',
+        disabled: !isCreate && !isEdit,
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'quantity',
+      label: '璁″垝鐢熶骇鏁伴噺',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        placeholder: '璇疯緭鍏ヨ鍒掔敓浜ф暟閲�',
+        min: 1,
+        precision: 2,
+        disabled: !isCreate && !isEdit,
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'clientId',
+      label: '瀹㈡埛',
+      component: markRaw(CrmCustomerSelect),
+      componentProps: {
+        placeholder: '璇烽�夋嫨瀹㈡埛',
+        disabled: !isCreate && !isEdit,
+      },
+    },
+    {
+      fieldName: 'requestDate',
+      label: '闇�姹傛棩鏈�',
+      component: 'DatePicker',
+      componentProps: {
+        class: '!w-full',
+        placeholder: '璇烽�夋嫨闇�姹傛棩鏈�',
+        format: 'YYYY-MM-DD',
+        valueFormat: 'x',
+        disabled: !isCreate && !isEdit,
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-2',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 3,
+        disabled: !isCreate && !isEdit,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'code',
+      label: 'MPS缂栫爜',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏PS缂栫爜',
+      },
+    },
+    {
+      fieldName: 'saleOrderNo',
+      label: '閿�鍞鍗曞彿',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ラ攢鍞鍗曞彿',
+      },
+    },
+    {
+      fieldName: 'productId',
+      label: '浜у搧',
+      component: markRaw(MdItemSelect),
+      componentProps: {
+        placeholder: '璇烽�夋嫨浜у搧',
+      },
+    },
+    {
+      fieldName: 'clientId',
+      label: '瀹㈡埛',
+      component: markRaw(CrmCustomerSelect),
+      componentProps: {
+        placeholder: '璇烽�夋嫨瀹㈡埛',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: [
+          { label: '鑽夌', value: 0 },
+          { label: '宸蹭笅鍙�', value: 1 },
+          { label: '宸插畬鎴�', value: 2 },
+          { label: '宸插彇娑�', value: 3 },
+        ],
+        placeholder: '璇烽�夋嫨鐘舵��',
+      },
+    },
+    {
+      fieldName: 'requestDate',
+      label: '闇�姹傛棩鏈�',
+      component: 'RangePicker',
+      componentProps: {
+        ...getRangePickerDefaultProps(),
+        allowClear: true,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesProMpsApi.Mps>['columns'] {
+  return [
+    {
+      type: 'checkbox',
+      width: 50,
+      fixed: 'left',
+    },
+    {
+      field: 'code',
+      title: 'MPS缂栫爜',
+      fixed: 'left',
+      width: 180,
+      slots: { default: 'code' },
+    },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      width: 100,
+      slots: { default: 'status' },
+    },
+    {
+      field: 'saleOrderNo',
+      title: '閿�鍞鍗曞彿',
+      width: 180,
+    },
+    {
+      field: 'productCode',
+      title: '浜у搧缂栫爜',
+      width: 120,
+    },
+    {
+      field: 'productName',
+      title: '浜у搧鍚嶇О',
+      minWidth: 150,
+    },
+    {
+      field: 'productSpecification',
+      title: '瑙勬牸鍨嬪彿',
+      width: 120,
+    },
+    {
+      field: 'unitMeasureName',
+      title: '鍗曚綅',
+      width: 80,
+    },
+    {
+      field: 'quantity',
+      title: '璁″垝鏁伴噺',
+      width: 100,
+      formatter: 'formatAmount3',
+    },
+    {
+      field: 'quantityProduced',
+      title: '宸茬敓浜ф暟閲�',
+      width: 100,
+      formatter: 'formatAmount3',
+    },
+    {
+      field: 'clientCode',
+      title: '瀹㈡埛缂栫爜',
+      width: 120,
+    },
+    {
+      field: 'clientName',
+      title: '瀹㈡埛鍚嶇О',
+      minWidth: 120,
+    },
+    {
+      field: 'requestDate',
+      title: '闇�姹傛棩鏈�',
+      width: 120,
+      formatter: 'formatDate',
+    },
+    {
+      field: 'issueDate',
+      title: '涓嬪彂鏃ユ湡',
+      width: 120,
+      formatter: 'formatDate',
+    },
+    {
+      field: 'workOrderCode',
+      title: '鐢熶骇宸ュ崟缂栫爜',
+      width: 180,
+      slots: { default: 'workOrderCode' },
+    },
+    {
+      field: 'mergeFlag',
+      title: '鍚堝苟涓嬪彂',
+      width: 100,
+      slots: { default: 'mergeFlag' },
+    },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 180,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 280,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
diff --git a/src/views/mes/pro/mps/index.vue b/src/views/mes/pro/mps/index.vue
new file mode 100644
index 0000000..c0dcdbf
--- /dev/null
+++ b/src/views/mes/pro/mps/index.vue
@@ -0,0 +1,321 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProMpsApi } from '#/api/mes/pro/mps';
+
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
+
+import { Button, message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  cancelMps,
+  deleteMps,
+  exportMps,
+  getMpsPage,
+  issueMps,
+  mergeIssueMps,
+} from '#/api/mes/pro/mps';
+import { $t } from '#/locales';
+
+import { MpsStatusEnum, useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+/** MPS 鐢熶骇涓昏鍒掑垪琛� */
+defineOptions({ name: 'MesProMps' });
+
+const router = useRouter();
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const checkedIds = ref<number[]>([]);
+const checkedRows = ref<MesProMpsApi.Mps[]>([]);
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+  gridApi.query();
+  checkedIds.value = [];
+  checkedRows.value = [];
+}
+
+/** 鏂板 MPS */
+function handleCreate() {
+  formModalApi.setData({ formType: 'create' }).open();
+}
+
+/** 瀵煎嚭琛ㄦ牸 */
+async function handleExport() {
+  const data = await exportMps(await gridApi.formApi.getValues());
+  downloadFileFromBlobPart({ fileName: '鐢熶骇涓昏鍒�.xls', source: data });
+}
+
+/** 鏌ョ湅璇︽儏 */
+function handleDetail(row: MesProMpsApi.Mps) {
+  formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+/** 缂栬緫 */
+function handleEdit(row: MesProMpsApi.Mps) {
+  formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+/** 鍒犻櫎 */
+async function handleDelete(ids: number[]) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting'),
+    duration: 0,
+  });
+  try {
+    await deleteMps(ids);
+    message.success($t('ui.actionMessage.deleteSuccess'));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 涓嬪彂 MPS */
+async function handleIssue(row: MesProMpsApi.Mps) {
+  const hideLoading = message.loading({
+    content: '涓嬪彂涓�...',
+    duration: 0,
+  });
+  try {
+    await issueMps(row.id!);
+    message.success('涓嬪彂鎴愬姛');
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 鍚堝苟涓嬪彂 */
+async function handleMergeIssue() {
+  if (checkedIds.value.length < 2) {
+    message.warning('璇疯嚦灏戦�夋嫨2鏉¤褰�');
+    return;
+  }
+  // 鏍¢獙锛氱浉鍚屼骇鍝併�佽崏绋跨姸鎬�
+  const firstRow = checkedRows.value[0];
+  const isSameProduct = checkedRows.value.every(
+    (row) => row.productId === firstRow.productId,
+  );
+  if (!isSameProduct) {
+    message.error('鍙兘閫夋嫨鐩稿悓浜у搧鐨凪PS杩涜鍚堝苟涓嬪彂');
+    return;
+  }
+  const isAllDraft = checkedRows.value.every(
+    (row) => row.status === MpsStatusEnum.DRAFT,
+  );
+  if (!isAllDraft) {
+    message.error('鍙兘閫夋嫨鑽夌鐘舵�佺殑MPS杩涜鍚堝苟涓嬪彂');
+    return;
+  }
+  const hideLoading = message.loading({
+    content: '鍚堝苟涓嬪彂涓�...',
+    duration: 0,
+  });
+  try {
+    await mergeIssueMps(checkedIds.value);
+    message.success('鍚堝苟涓嬪彂鎴愬姛');
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 鍙栨秷 MPS */
+async function handleCancel(row: MesProMpsApi.Mps) {
+  const hideLoading = message.loading({
+    content: '鍙栨秷涓�...',
+    duration: 0,
+  });
+  try {
+    await cancelMps(row.id!);
+    message.success('鍙栨秷鎴愬姛');
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 璺宠浆鍒扮敓浜у伐鍗� */
+function handleGoToWorkOrder(row: MesProMpsApi.Mps) {
+  if (row.workOrderId) {
+    router.push({
+      path: '/mes/pro/workorder',
+      query: { id: row.workOrderId },
+    });
+  }
+}
+
+/** 琛岄�夋嫨鍙樺寲 */
+function handleRowCheckboxChange({
+  records,
+}: {
+  records: MesProMpsApi.Mps[];
+}) {
+  checkedIds.value = records.map((item) => item.id!);
+  checkedRows.value = records;
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getMpsPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<MesProMpsApi.Mps>,
+  gridEvents: {
+    checkboxAll: handleRowCheckboxChange,
+    checkboxChange: handleRowCheckboxChange,
+  },
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormModal @success="handleRefresh" />
+    <Grid table-title="鐢熶骇涓昏鍒�(MPS)鍒楄〃">
+      <template #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: $t('ui.actionTitle.create', ['MPS']),
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['mes:pro-mps:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: $t('ui.actionTitle.export'),
+              type: 'primary',
+              icon: ACTION_ICON.DOWNLOAD,
+              auth: ['mes:pro-mps:export'],
+              onClick: handleExport,
+            },
+            {
+              label: '鍚堝苟涓嬪彂',
+              type: 'primary',
+              disabled: isEmpty(checkedIds) || checkedIds.length < 2,
+              icon: ACTION_ICON.AUDIT,
+              auth: ['mes:pro-mps:merge-issue'],
+              popConfirm: {
+                title: '纭鍚堝苟涓嬪彂閫変腑鐨凪PS鍚楋紵',
+                confirm: handleMergeIssue,
+              },
+            },
+          ]"
+        />
+      </template>
+      <template #code="{ row }">
+        <Button type="link" @click="handleDetail(row)">
+          {{ row.code }}
+        </Button>
+      </template>
+      <template #status="{ row }">
+        <Tag v-if="row.status === 0" color="blue">鑽夌</Tag>
+        <Tag v-else-if="row.status === 1" color="success">宸蹭笅鍙�</Tag>
+        <Tag v-else-if="row.status === 2" color="purple">宸插畬鎴�</Tag>
+        <Tag v-else-if="row.status === 3" color="red">宸插彇娑�</Tag>
+      </template>
+      <template #workOrderCode="{ row }">
+        <Button
+          v-if="row.workOrderCode"
+          type="link"
+          @click="handleGoToWorkOrder(row)"
+        >
+          {{ row.workOrderCode }}
+        </Button>
+        <span v-else>-</span>
+      </template>
+      <template #mergeFlag="{ row }">
+        <Tag v-if="row.mergeFlag === 1" color="success">鏄�</Tag>
+        <Tag v-else color="default">鍚�</Tag>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: $t('common.detail'),
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['mes:pro-mps:query'],
+              onClick: handleDetail.bind(null, row),
+            },
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['mes:pro-mps:update'],
+              ifShow: () => row.status === MpsStatusEnum.DRAFT,
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: '涓嬪彂',
+              type: 'link',
+              icon: ACTION_ICON.AUDIT,
+              auth: ['mes:pro-mps:issue'],
+              ifShow: () => row.status === MpsStatusEnum.DRAFT,
+              popConfirm: {
+                title: `纭涓嬪彂${row.code}鍚楋紵`,
+                confirm: handleIssue.bind(null, row),
+              },
+            },
+            {
+              label: '鍙栨秷',
+              type: 'link',
+              danger: true,
+              icon: 'ant-design:stop-outlined',
+              auth: ['mes:pro-mps:cancel'],
+              ifShow: () => row.status === MpsStatusEnum.ISSUED,
+              popConfirm: {
+                title: `纭鍙栨秷${row.code}鍚楋紵`,
+                confirm: handleCancel.bind(null, row),
+              },
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['mes:pro-mps:delete'],
+              ifShow: () => row.status === MpsStatusEnum.DRAFT,
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.code]),
+                confirm: handleDelete.bind(null, [row.id!]),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/mes/pro/mps/modules/form.vue b/src/views/mes/pro/mps/modules/form.vue
new file mode 100644
index 0000000..4cc8308
--- /dev/null
+++ b/src/views/mes/pro/mps/modules/form.vue
@@ -0,0 +1,175 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { MesProMpsApi } from '#/api/mes/pro/mps';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { Descriptions, message, Tag } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { createMps, getMps, updateMps } from '#/api/mes/pro/mps';
+import { $t } from '#/locales';
+
+import { MpsStatusEnum, useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<MesProMpsApi.Mps>();
+const formType = ref<FormType>('detail');
+
+const isEditable = computed(() => ['create', 'edit'].includes(formType.value));
+
+const getTitle = computed(() => {
+  switch (formType.value) {
+    case 'create':
+      return '鏂板MPS';
+    case 'edit':
+      return '缂栬緫MPS';
+    default:
+      return 'MPS璇︽儏';
+  }
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    formItemClass: 'col-span-1',
+    labelWidth: 110,
+  },
+  layout: 'horizontal',
+  schema: [],
+  showDefaultActions: false,
+  wrapperClass: 'grid-cols-3',
+});
+
+/** 閲嶆柊鎸傝浇 schema 骞舵寜琛ㄥ崟鎬佹帶鍒跺簳閮ㄧ‘璁ゆ寜閽� */
+function applySchema() {
+  formApi.setState({ schema: useFormSchema(formType.value) });
+  modalApi.setState({ showConfirmButton: isEditable.value });
+}
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    if (!isEditable.value) {
+      await modalApi.close();
+      return;
+    }
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      const data = (await formApi.getValues()) as MesProMpsApi.CreateMpsReq;
+      if (formData.value?.id) {
+        await updateMps({ ...data, id: formData.value.id });
+      } else {
+        await createMps(data);
+      }
+      await modalApi.close();
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+    } 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;
+    applySchema();
+    if (data?.id) {
+      modalApi.lock();
+      try {
+        formData.value = await getMps(data.id);
+        await formApi.setValues(formData.value);
+      } finally {
+        modalApi.unlock();
+      }
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-3/5">
+    <Form class="mx-4" />
+    <!-- 璇︽儏妯″紡棰濆灞曠ず鍙淇℃伅 -->
+    <div v-if="formType === 'detail' && formData" class="mx-4">
+      <Descriptions bordered :column="2">
+        <Descriptions.Item label="MPS缂栫爜">
+          {{ formData.code }}
+        </Descriptions.Item>
+        <Descriptions.Item label="鐘舵��">
+          <Tag v-if="formData.status === 0" color="blue">鑽夌</Tag>
+          <Tag v-else-if="formData.status === 1" color="success">宸蹭笅鍙�</Tag>
+          <Tag v-else-if="formData.status === 2" color="purple">宸插畬鎴�</Tag>
+          <Tag v-else-if="formData.status === 3" color="red">宸插彇娑�</Tag>
+        </Descriptions.Item>
+        <Descriptions.Item label="閿�鍞鍗曞彿">
+          {{ formData.saleOrderNo }}
+        </Descriptions.Item>
+        <Descriptions.Item label="閿�鍞鍗曟槑缁咺D">
+          {{ formData.saleOrderItemId }}
+        </Descriptions.Item>
+        <Descriptions.Item label="浜у搧缂栫爜">
+          {{ formData.productCode }}
+        </Descriptions.Item>
+        <Descriptions.Item label="浜у搧鍚嶇О">
+          {{ formData.productName }}
+        </Descriptions.Item>
+        <Descriptions.Item label="瑙勬牸鍨嬪彿">
+          {{ formData.productSpecification }}
+        </Descriptions.Item>
+        <Descriptions.Item label="鍗曚綅">
+          {{ formData.unitMeasureName }}
+        </Descriptions.Item>
+        <Descriptions.Item label="璁″垝鐢熶骇鏁伴噺">
+          {{ formData.quantity }}
+        </Descriptions.Item>
+        <Descriptions.Item label="宸茬敓浜ф暟閲�">
+          {{ formData.quantityProduced }}
+        </Descriptions.Item>
+        <Descriptions.Item label="瀹㈡埛缂栫爜">
+          {{ formData.clientCode }}
+        </Descriptions.Item>
+        <Descriptions.Item label="瀹㈡埛鍚嶇О">
+          {{ formData.clientName }}
+        </Descriptions.Item>
+        <Descriptions.Item label="闇�姹傛棩鏈�">
+          {{ formData.requestDate ? new Date(formData.requestDate).toLocaleDateString() : '-' }}
+        </Descriptions.Item>
+        <Descriptions.Item label="涓嬪彂鏃ユ湡">
+          {{ formData.issueDate ? new Date(formData.issueDate).toLocaleDateString() : '-' }}
+        </Descriptions.Item>
+        <Descriptions.Item label="瀹屾垚鏃ユ湡">
+          {{ formData.finishDate ? new Date(formData.finishDate).toLocaleDateString() : '-' }}
+        </Descriptions.Item>
+        <Descriptions.Item label="鐢熶骇宸ュ崟缂栫爜">
+          {{ formData.workOrderCode || '-' }}
+        </Descriptions.Item>
+        <Descriptions.Item label="鏄惁鍚堝苟涓嬪彂">
+          <Tag v-if="formData.mergeFlag === 1" color="success">鏄�</Tag>
+          <Tag v-else color="default">鍚�</Tag>
+        </Descriptions.Item>
+        <Descriptions.Item label="鍚堝苟鎵规鍙�">
+          {{ formData.mergeBatchNo || '-' }}
+        </Descriptions.Item>
+        <Descriptions.Item label="澶囨敞" :span="2">
+          {{ formData.remark || '-' }}
+        </Descriptions.Item>
+        <Descriptions.Item label="鍒涘缓鏃堕棿" :span="2">
+          {{ formData.createTime ? new Date(formData.createTime).toLocaleString() : '-' }}
+        </Descriptions.Item>
+      </Descriptions>
+    </div>
+  </Modal>
+</template>

--
Gitblit v1.9.3