zhangwencui
5 天以前 6fbec5eebedf6d14cf1b262c608d586957180bb7
Merge branch 'dev_pro2.0' of http://114.132.189.42:9002/r/mom-pro2-before into dev_pro2.0
已添加5个文件
已修改14个文件
1452 ■■■■■ 文件已修改
src/api/crm/customer/index.ts 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/sale/order/index.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/mps/index.ts 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/crm-customer-select-dialog.vue 236 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/crm-customer-select.vue 166 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/business/data.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contact/data.ts 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contract/data.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/receivable/data.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/receivable/plan/data.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/saleQuotation/data.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/data.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/index.vue 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/data.ts 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/modules/item-form.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/mps/data.ts 299 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/mps/index.vue 321 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/mps/modules/form.vue 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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(',') },
  });
}
/** åˆ†é…å…¬æµ·ç»™å¯¹åº”负责人 */
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编号
  }
}
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; // é”€å”®è®¢å•ID
    saleOrderNo?: string; // é”€å”®è®¢å•号
    saleOrderItemId?: number; // é”€å”®è®¢å•明细ID
    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}`);
}
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="我负责的" />
      <Tabs.TabPane key="2" tab="我参与的" />
      <Tabs.TabPane key="3" tab="下属负责的" />
    </Tabs>
    <Grid table-title="客户列表" />
  </Modal>
</template>
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>
  <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>
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,
      },
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: '请选择客户',
      },
    },
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,
      },
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,
      },
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,
      },
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,
      },
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' },
    },
    {
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="[
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',
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="[
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"
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: '销售订单ID',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入销售订单ID(可选)',
        disabled: !isCreate && !isEdit,
      },
    },
    {
      fieldName: 'saleOrderNo',
      label: '销售订单号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入销售订单号(可选)',
        disabled: !isCreate && !isEdit,
      },
    },
    {
      fieldName: 'saleOrderItemId',
      label: '销售订单明细ID',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入销售订单明细ID(可选)',
        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: '请输入MPS编码',
      },
    },
    {
      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' },
    },
  ];
}
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('只能选择相同产品的MPS进行合并下发');
    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: '确认合并下发选中的MPS吗?',
                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>
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="销售订单明细ID">
          {{ 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>