Pre Merge pull request !199 from XuZhiqiang/feat-erp
						commit
						0b5c01d028
					
				|  | @ -2,7 +2,7 @@ import type { PageParam, PageResult } from '@vben/request'; | |||
| 
 | ||||
| import { requestClient } from '#/api/request'; | ||||
| 
 | ||||
| namespace ErpStockOutApi { | ||||
| export namespace ErpStockOutApi { | ||||
|   /** 其它出库单信息 */ | ||||
|   export interface StockOut { | ||||
|     id?: number; // 出库编号
 | ||||
|  | @ -13,6 +13,24 @@ namespace ErpStockOutApi { | |||
|     totalPrice: number; // 合计金额,单位:元
 | ||||
|     status: number; // 状态
 | ||||
|     remark: string; // 备注
 | ||||
|     fileUrl?: string; // 附件
 | ||||
|     items?: StockOutItem[]; // 出库产品清单
 | ||||
|   } | ||||
| 
 | ||||
|   /** 其它出库单产品信息 */ | ||||
|   export interface StockOutItem { | ||||
|     id?: number; // 编号
 | ||||
|     warehouseId?: number; // 仓库编号
 | ||||
|     productId?: number; // 产品编号
 | ||||
|     productName?: string; // 产品名称
 | ||||
|     productUnitId?: number; // 产品单位编号
 | ||||
|     productUnitName?: string; // 产品单位名称
 | ||||
|     productBarCode?: string; // 产品条码
 | ||||
|     count: number; // 数量
 | ||||
|     productPrice: number; // 产品单价
 | ||||
|     totalPrice: number; // 总价
 | ||||
|     stockCount?: number; // 库存数量
 | ||||
|     remark?: string; // 备注
 | ||||
|   } | ||||
| 
 | ||||
|   /** 其它出库单分页查询参数 */ | ||||
|  |  | |||
|  | @ -88,7 +88,10 @@ const [Modal, modalApi] = useVbenModal({ | |||
|     modalApi.lock(); | ||||
|     // 提交表单 | ||||
|     const data = (await formApi.getValues()) as ErpStockInApi.StockIn; | ||||
|     data.items = formData.value?.items; | ||||
|     data.items = formData.value?.items?.map((item) => ({ | ||||
|       ...item, | ||||
|       id: undefined, | ||||
|     })); | ||||
|     // 将文件数组转换为字符串 | ||||
|     if (data.fileUrl && Array.isArray(data.fileUrl)) { | ||||
|       data.fileUrl = data.fileUrl.length > 0 ? data.fileUrl[0] : ''; | ||||
|  |  | |||
|  | @ -0,0 +1,329 @@ | |||
| import type { VbenFormSchema } from '#/adapter/form'; | ||||
| import type { VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| 
 | ||||
| import { createRequiredValidation } from '#/adapter/vxe-table'; | ||||
| import { getProductSimpleList } from '#/api/erp/product/product'; | ||||
| import { getCustomerSimpleList } from '#/api/erp/sale/customer'; | ||||
| import { getSimpleUserList } from '#/api/system/user'; | ||||
| import { DICT_TYPE, getDictOptions } from '#/utils'; | ||||
| 
 | ||||
| /** 表单的配置项 */ | ||||
| export function useFormSchema(formType: string): VbenFormSchema[] { | ||||
|   return [ | ||||
|     { | ||||
|       component: 'Input', | ||||
|       componentProps: { | ||||
|         style: { display: 'none' }, | ||||
|       }, | ||||
|       fieldName: 'id', | ||||
|       label: 'ID', | ||||
|       hideLabel: true, | ||||
|       formItemClass: 'hidden', | ||||
|     }, | ||||
|     { | ||||
|       component: 'Input', | ||||
|       componentProps: { | ||||
|         placeholder: '系统自动生成', | ||||
|         disabled: true, | ||||
|       }, | ||||
|       fieldName: 'no', | ||||
|       label: '出库单号', | ||||
|       disabled: formType === 'detail', | ||||
|     }, | ||||
|     { | ||||
|       component: 'DatePicker', | ||||
|       componentProps: { | ||||
|         placeholder: '选择出库时间', | ||||
|         showTime: true, | ||||
|         format: 'YYYY-MM-DD HH:mm:ss', | ||||
|         valueFormat: 'x', | ||||
|         style: { width: '100%' }, | ||||
|       }, | ||||
|       disabled: formType === 'detail', | ||||
|       fieldName: 'outTime', | ||||
|       label: '出库时间', | ||||
|       rules: 'required', | ||||
|     }, | ||||
|     { | ||||
|       component: 'ApiSelect', | ||||
|       componentProps: { | ||||
|         placeholder: '请选择客户', | ||||
|         allowClear: true, | ||||
|         showSearch: true, | ||||
|         api: getCustomerSimpleList, | ||||
|         fieldNames: { | ||||
|           label: 'name', | ||||
|           value: 'id', | ||||
|         }, | ||||
|       }, | ||||
|       disabled: formType === 'detail', | ||||
|       fieldName: 'customerId', | ||||
|       label: '客户', | ||||
|     }, | ||||
| 
 | ||||
|     { | ||||
|       component: 'Textarea', | ||||
|       componentProps: { | ||||
|         placeholder: '请输入备注', | ||||
|         autoSize: { minRows: 2, maxRows: 4 }, | ||||
|         class: 'w-full', | ||||
|       }, | ||||
|       disabled: formType === 'detail', | ||||
|       fieldName: 'remark', | ||||
|       label: '备注', | ||||
|       formItemClass: 'col-span-3', | ||||
|     }, | ||||
|     { | ||||
|       component: 'FileUpload', | ||||
|       disabled: formType === 'detail', | ||||
|       componentProps: { | ||||
|         maxNumber: 1, | ||||
|         maxSize: 10, | ||||
|         accept: [ | ||||
|           'pdf', | ||||
|           'doc', | ||||
|           'docx', | ||||
|           'xls', | ||||
|           'xlsx', | ||||
|           'txt', | ||||
|           'jpg', | ||||
|           'jpeg', | ||||
|           'png', | ||||
|         ], | ||||
|         showDescription: true, | ||||
|       }, | ||||
|       fieldName: 'fileUrl', | ||||
|       label: '附件', | ||||
|       formItemClass: 'col-span-3', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'product', | ||||
|       disabled: formType === 'detail', | ||||
|       label: '产品清单', | ||||
|       component: 'Input', | ||||
|       formItemClass: 'col-span-3', | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
| 
 | ||||
| /** 出库产品清单表格列定义 */ | ||||
| export function useStockInItemTableColumns( | ||||
|   isValidating?: any, | ||||
| ): VxeTableGridOptions['columns'] { | ||||
|   return [ | ||||
|     { type: 'seq', title: '序号', minWidth: 50, fixed: 'left' }, | ||||
|     { | ||||
|       field: 'warehouseId', | ||||
|       title: '仓库名称', | ||||
|       minWidth: 150, | ||||
|       slots: { default: 'warehouseId' }, | ||||
|       className: createRequiredValidation(isValidating, 'warehouseId'), | ||||
|     }, | ||||
|     { | ||||
|       field: 'productId', | ||||
|       title: '产品名称', | ||||
|       minWidth: 200, | ||||
|       slots: { default: 'productId' }, | ||||
|       className: createRequiredValidation(isValidating, 'productId'), | ||||
|     }, | ||||
|     { | ||||
|       field: 'stockCount', | ||||
|       title: '库存', | ||||
|       minWidth: 100, | ||||
|     }, | ||||
|     { | ||||
|       field: 'productBarCode', | ||||
|       title: '条码', | ||||
|       minWidth: 120, | ||||
|     }, | ||||
|     { | ||||
|       field: 'productUnitName', | ||||
|       title: '单位', | ||||
|       minWidth: 80, | ||||
|     }, | ||||
|     { | ||||
|       field: 'count', | ||||
|       title: '数量', | ||||
|       minWidth: 120, | ||||
|       slots: { default: 'count' }, | ||||
|       className: createRequiredValidation(isValidating, 'count'), | ||||
|     }, | ||||
|     { | ||||
|       field: 'productPrice', | ||||
|       title: '产品单价', | ||||
|       minWidth: 120, | ||||
|       slots: { default: 'productPrice' }, | ||||
|     }, | ||||
|     { | ||||
|       field: 'totalPrice', | ||||
|       title: '金额', | ||||
|       minWidth: 120, | ||||
|       formatter: 'formatAmount2', | ||||
|     }, | ||||
|     { | ||||
|       field: 'remark', | ||||
|       title: '备注', | ||||
|       minWidth: 150, | ||||
|       slots: { default: 'remark' }, | ||||
|     }, | ||||
|     { | ||||
|       title: '操作', | ||||
|       width: 50, | ||||
|       fixed: 'right', | ||||
|       slots: { default: 'actions' }, | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
| 
 | ||||
| /** 列表的搜索表单 */ | ||||
| export function useGridFormSchema(): VbenFormSchema[] { | ||||
|   return [ | ||||
|     { | ||||
|       fieldName: 'no', | ||||
|       label: '出库单号', | ||||
|       component: 'Input', | ||||
|       componentProps: { | ||||
|         placeholder: '请输入出库单号', | ||||
|         allowClear: true, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'productId', | ||||
|       label: '产品', | ||||
|       component: 'ApiSelect', | ||||
|       componentProps: { | ||||
|         placeholder: '请选择产品', | ||||
|         allowClear: true, | ||||
|         showSearch: true, | ||||
|         api: getProductSimpleList, | ||||
|         labelField: 'name', | ||||
|         valueField: 'id', | ||||
|         filterOption: false, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'outTime', | ||||
|       label: '出库时间', | ||||
|       component: 'RangePicker', | ||||
|       componentProps: { | ||||
|         placeholder: ['开始日期', '结束日期'], | ||||
|         showTime: true, | ||||
|         format: 'YYYY-MM-DD HH:mm:ss', | ||||
|         valueFormat: 'YYYY-MM-DD HH:mm:ss', | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'customerId', | ||||
|       label: '客户', | ||||
|       component: 'ApiSelect', | ||||
|       componentProps: { | ||||
|         placeholder: '请选择客户', | ||||
|         allowClear: true, | ||||
|         showSearch: true, | ||||
|         api: getCustomerSimpleList, | ||||
|         labelField: 'name', | ||||
|         valueField: 'id', | ||||
|         filterOption: false, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'status', | ||||
|       label: '状态', | ||||
|       component: 'Select', | ||||
|       componentProps: { | ||||
|         placeholder: '请选择状态', | ||||
|         allowClear: true, | ||||
|         options: getDictOptions(DICT_TYPE.ERP_AUDIT_STATUS, 'number'), | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'remark', | ||||
|       label: '备注', | ||||
|       component: 'Input', | ||||
|       componentProps: { | ||||
|         placeholder: '请输入备注', | ||||
|         allowClear: true, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'creator', | ||||
|       label: '创建人', | ||||
|       component: 'ApiSelect', | ||||
|       componentProps: { | ||||
|         placeholder: '请选择创建人', | ||||
|         allowClear: true, | ||||
|         showSearch: true, | ||||
|         api: getSimpleUserList, | ||||
|         labelField: 'nickname', | ||||
|         valueField: 'id', | ||||
|         filterOption: false, | ||||
|       }, | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
| 
 | ||||
| /** 列表的字段 */ | ||||
| export function useGridColumns(): VxeTableGridOptions['columns'] { | ||||
|   return [ | ||||
|     { | ||||
|       type: 'checkbox', | ||||
|       width: 50, | ||||
|       fixed: 'left', | ||||
|     }, | ||||
|     { | ||||
|       field: 'no', | ||||
|       title: '出库单号', | ||||
|       minWidth: 180, | ||||
|     }, | ||||
|     { | ||||
|       field: 'productNames', | ||||
|       title: '产品信息', | ||||
|       minWidth: 200, | ||||
|       showOverflow: 'tooltip', | ||||
|     }, | ||||
|     { | ||||
|       field: 'customerName', | ||||
|       title: '客户', | ||||
|       minWidth: 120, | ||||
|     }, | ||||
|     { | ||||
|       field: 'outTime', | ||||
|       title: '出库时间', | ||||
|       minWidth: 180, | ||||
|       cellRender: { | ||||
|         name: 'CellDateTime', | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       field: 'creatorName', | ||||
|       title: '创建人', | ||||
|       minWidth: 100, | ||||
|     }, | ||||
|     { | ||||
|       field: 'totalCount', | ||||
|       title: '数量', | ||||
|       minWidth: 100, | ||||
|     }, | ||||
|     { | ||||
|       field: 'totalPrice', | ||||
|       title: '价格', | ||||
|       minWidth: 100, | ||||
|     }, | ||||
|     { | ||||
|       field: 'status', | ||||
|       title: '状态', | ||||
|       minWidth: 90, | ||||
|       fixed: 'right', | ||||
|       cellRender: { | ||||
|         name: 'CellDict', | ||||
|         props: { type: DICT_TYPE.ERP_AUDIT_STATUS }, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       title: '操作', | ||||
|       width: 300, | ||||
|       fixed: 'right', | ||||
|       slots: { default: 'actions' }, | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
|  | @ -1,34 +1,219 @@ | |||
| <script lang="ts" setup> | ||||
| import { DocAlert, Page } from '@vben/common-ui'; | ||||
| import type { VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| import type { ErpStockOutApi } from '#/api/erp/stock/out'; | ||||
| 
 | ||||
| import { Button } from 'ant-design-vue'; | ||||
| import { DocAlert, Page, useVbenModal } from '@vben/common-ui'; | ||||
| import { downloadFileFromBlobPart } from '@vben/utils'; | ||||
| 
 | ||||
| import { message } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; | ||||
| import { | ||||
|   deleteStockOut, | ||||
|   exportStockOut, | ||||
|   getStockOutPage, | ||||
|   updateStockOutStatus, | ||||
| } from '#/api/erp/stock/out'; | ||||
| import { $t } from '#/locales'; | ||||
| 
 | ||||
| import { useGridColumns, useGridFormSchema } from './data'; | ||||
| import StockInForm from './modules/form.vue'; | ||||
| 
 | ||||
| /** 其它出库单管理 */ | ||||
| defineOptions({ name: 'ErpStockOut' }); | ||||
| 
 | ||||
| const [FormModal, formModalApi] = useVbenModal({ | ||||
|   connectedComponent: StockInForm, | ||||
|   destroyOnClose: true, | ||||
| }); | ||||
| 
 | ||||
| /** 刷新表格 */ | ||||
| function onRefresh() { | ||||
|   gridApi.query(); | ||||
| } | ||||
| 
 | ||||
| /** 导出出库单 */ | ||||
| async function handleExport() { | ||||
|   const data = await exportStockOut(await gridApi.formApi.getValues()); | ||||
|   downloadFileFromBlobPart({ fileName: '其它出库单.xls', source: data }); | ||||
| } | ||||
| 
 | ||||
| /** 新增/编辑/详情 */ | ||||
| function openForm(type: string, id?: number) { | ||||
|   formModalApi.setData({ type, id }).open(); | ||||
| } | ||||
| 
 | ||||
| /** 删除 */ | ||||
| async function handleDelete(ids: any[]) { | ||||
|   const hideLoading = message.loading({ | ||||
|     content: '删除中...', | ||||
|     duration: 0, | ||||
|     key: 'action_process_msg', | ||||
|   }); | ||||
|   try { | ||||
|     await deleteStockOut(ids); | ||||
|     message.success({ | ||||
|       content: '删除成功', | ||||
|       key: 'action_process_msg', | ||||
|     }); | ||||
|     onRefresh(); | ||||
|   } finally { | ||||
|     hideLoading(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 审核/反审核 */ | ||||
| async function handleUpdateStatus(id: any, status: number) { | ||||
|   const statusText = status === 20 ? '审核' : '反审核'; | ||||
|   const hideLoading = message.loading({ | ||||
|     content: `${statusText}中...`, | ||||
|     duration: 0, | ||||
|     key: 'action_process_msg', | ||||
|   }); | ||||
|   try { | ||||
|     await updateStockOutStatus({ id, status }); | ||||
|     message.success({ | ||||
|       content: `${statusText}成功`, | ||||
|       key: 'action_process_msg', | ||||
|     }); | ||||
|     onRefresh(); | ||||
|   } finally { | ||||
|     hideLoading(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const [Grid, gridApi] = useVbenVxeGrid({ | ||||
|   formOptions: { | ||||
|     schema: useGridFormSchema(), | ||||
|   }, | ||||
|   gridOptions: { | ||||
|     columns: useGridColumns(), | ||||
|     height: 'auto', | ||||
|     keepSource: true, | ||||
|     proxyConfig: { | ||||
|       ajax: { | ||||
|         query: async ({ page }, formValues) => { | ||||
|           return await getStockOutPage({ | ||||
|             pageNo: page.currentPage, | ||||
|             pageSize: page.pageSize, | ||||
|             ...formValues, | ||||
|           }); | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     rowConfig: { | ||||
|       keyField: 'id', | ||||
|     }, | ||||
|     toolbarConfig: { | ||||
|       refresh: true, | ||||
|       search: true, | ||||
|     }, | ||||
|     checkboxConfig: { | ||||
|       reserve: true, | ||||
|     }, | ||||
|   } as VxeTableGridOptions<ErpStockOutApi.StockOut>, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Page> | ||||
|   <Page auto-content-height> | ||||
|     <template #doc> | ||||
|       <DocAlert | ||||
|         title="【库存】其它入库、其它出库" | ||||
|         url="https://doc.iocoder.cn/erp/stock-in-out/" | ||||
|       /> | ||||
|     </template> | ||||
|     <Button | ||||
|       danger | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3" | ||||
|     > | ||||
|       该功能支持 Vue3 + element-plus 版本! | ||||
|     </Button> | ||||
|     <br /> | ||||
|     <Button | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/erp/stock/out/index" | ||||
|     > | ||||
|       可参考 | ||||
|       https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/erp/stock/out/index | ||||
|       代码,pull request 贡献给我们! | ||||
|     </Button> | ||||
|     <Grid table-title="其它出库单列表"> | ||||
|       <template #toolbar-tools> | ||||
|         <TableAction | ||||
|           :actions="[ | ||||
|             { | ||||
|               label: $t('ui.actionTitle.create', ['其它出库']), | ||||
|               type: 'primary', | ||||
|               icon: ACTION_ICON.ADD, | ||||
|               auth: ['erp:stock-out:create'], | ||||
|               onClick: () => openForm('create'), | ||||
|             }, | ||||
|             { | ||||
|               label: $t('ui.actionTitle.export'), | ||||
|               type: 'default', | ||||
|               icon: ACTION_ICON.DOWNLOAD, | ||||
|               auth: ['erp:stock-out:export'], | ||||
|               onClick: handleExport, | ||||
|             }, | ||||
|             { | ||||
|               label: '批量删除', | ||||
|               type: 'default', | ||||
|               danger: true, | ||||
|               icon: ACTION_ICON.DELETE, | ||||
|               auth: ['erp:stock-out:delete'], | ||||
|               popConfirm: { | ||||
|                 title: '是否删除所选中数据?', | ||||
|                 confirm: () => { | ||||
|                   const checkboxRecords = gridApi.grid.getCheckboxRecords(); | ||||
|                   if (checkboxRecords.length === 0) { | ||||
|                     message.warning('请选择要删除的数据'); | ||||
|                     return; | ||||
|                   } | ||||
|                   handleDelete(checkboxRecords.map((item) => item.id)); | ||||
|                 }, | ||||
|               }, | ||||
|             }, | ||||
|           ]" | ||||
|         /> | ||||
|       </template> | ||||
| 
 | ||||
|       <template #actions="{ row }"> | ||||
|         <TableAction | ||||
|           :actions="[ | ||||
|             { | ||||
|               label: '详情', | ||||
|               icon: ACTION_ICON.VIEW, | ||||
|               auth: ['erp:stock-out:query'], | ||||
|               onClick: () => openForm('detail', row.id), | ||||
|             }, | ||||
|             { | ||||
|               label: '编辑', | ||||
|               auth: ['erp:stock-out:update'], | ||||
|               icon: ACTION_ICON.EDIT, | ||||
|               disabled: row.status !== 10, | ||||
|               onClick: () => openForm('update', row.id), | ||||
|             }, | ||||
|             { | ||||
|               label: '审核', | ||||
|               auth: ['erp:stock-out:update'], | ||||
|               ifShow: row.status === 10, | ||||
|               popConfirm: { | ||||
|                 title: '确认要审核该入库单吗?', | ||||
|                 confirm: () => handleUpdateStatus(row.id, 20), | ||||
|               }, | ||||
|             }, | ||||
|             { | ||||
|               label: '反审核', | ||||
|               danger: true, | ||||
|               auth: ['erp:stock-out:update'], | ||||
|               ifShow: row.status === 20, | ||||
|               popConfirm: { | ||||
|                 title: '确认要反审核该入库单吗?', | ||||
|                 confirm: () => handleUpdateStatus(row.id, 10), | ||||
|               }, | ||||
|             }, | ||||
|             { | ||||
|               label: '删除', | ||||
|               danger: true, | ||||
|               auth: ['erp:stock-out:delete'], | ||||
|               disabled: row.status !== 10, | ||||
|               popConfirm: { | ||||
|                 title: '确认要删除该出库单吗?', | ||||
|                 confirm: () => handleDelete([row.id]), | ||||
|               }, | ||||
|             }, | ||||
|           ]" | ||||
|         /> | ||||
|       </template> | ||||
|     </Grid> | ||||
| 
 | ||||
|     <!-- 表单弹窗 --> | ||||
|     <FormModal @success="onRefresh" /> | ||||
|   </Page> | ||||
| </template> | ||||
|  |  | |||
|  | @ -0,0 +1,196 @@ | |||
| <script lang="ts" setup> | ||||
| import type { ErpStockOutApi } from '#/api/erp/stock/out'; | ||||
| 
 | ||||
| import { computed, nextTick, ref } from 'vue'; | ||||
| 
 | ||||
| import { useVbenModal } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { message } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { useVbenForm } from '#/adapter/form'; | ||||
| import { | ||||
|   createStockOut, | ||||
|   getStockOut, | ||||
|   updateStockOut, | ||||
|   updateStockOutStatus, | ||||
| } from '#/api/erp/stock/out'; | ||||
| 
 | ||||
| import { useFormSchema } from '../data'; | ||||
| import StockInItemForm from './stock-out-item-form.vue'; | ||||
| 
 | ||||
| const emit = defineEmits(['success']); | ||||
| const formData = ref<ErpStockOutApi.StockOut>(); | ||||
| const formType = ref(''); | ||||
| const itemFormRef = ref(); | ||||
| 
 | ||||
| const getTitle = computed(() => { | ||||
|   if (formType.value === 'create') return '添加其它出库单'; | ||||
|   if (formType.value === 'update') return '编辑其它出库单'; | ||||
|   return '其它出库单详情'; | ||||
| }); | ||||
| 
 | ||||
| const [Form, formApi] = useVbenForm({ | ||||
|   commonConfig: { | ||||
|     componentProps: { | ||||
|       class: 'w-full', | ||||
|     }, | ||||
|     labelWidth: 120, | ||||
|   }, | ||||
|   wrapperClass: 'grid-cols-3', | ||||
|   layout: 'vertical', | ||||
|   schema: useFormSchema(formType.value), | ||||
|   showDefaultActions: false, | ||||
| }); | ||||
| 
 | ||||
| const handleUpdateItems = (items: ErpStockOutApi.StockOutItem[]) => { | ||||
|   formData.value = modalApi.getData<ErpStockOutApi.StockOut>(); | ||||
|   if (formData.value) { | ||||
|     formData.value.items = items; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * 创建或更新其它出库单 | ||||
|  */ | ||||
| const [Modal, modalApi] = useVbenModal({ | ||||
|   async onConfirm() { | ||||
|     const { valid } = await formApi.validate(); | ||||
|     if (!valid) { | ||||
|       return; | ||||
|     } | ||||
|     await nextTick(); | ||||
| 
 | ||||
|     const itemFormInstance = Array.isArray(itemFormRef.value) | ||||
|       ? itemFormRef.value[0] | ||||
|       : itemFormRef.value; | ||||
|     if (itemFormInstance && typeof itemFormInstance.validate === 'function') { | ||||
|       try { | ||||
|         const isValid = await itemFormInstance.validate(); | ||||
|         if (!isValid) { | ||||
|           message.error('产品清单验证失败,请检查必填项'); | ||||
|           return; | ||||
|         } | ||||
|       } catch (error: any) { | ||||
|         message.error(error.message || '产品清单验证失败'); | ||||
|         return; | ||||
|       } | ||||
|     } else { | ||||
|       message.error('产品清单验证方法不存在'); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     // 验证产品清单不能为空 | ||||
|     if (!formData.value?.items || formData.value.items.length === 0) { | ||||
|       message.error('产品清单不能为空,请至少添加一个产品'); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     modalApi.lock(); | ||||
|     // 提交表单 | ||||
|     const data = (await formApi.getValues()) as ErpStockOutApi.StockOut; | ||||
|     data.items = formData.value?.items?.map((item) => ({ | ||||
|       ...item, | ||||
|       id: undefined, | ||||
|     })); | ||||
|     // 将文件数组转换为字符串 | ||||
|     if (data.fileUrl && Array.isArray(data.fileUrl)) { | ||||
|       data.fileUrl = data.fileUrl.length > 0 ? data.fileUrl[0] : ''; | ||||
|     } | ||||
|     try { | ||||
|       await (formType.value === 'create' | ||||
|         ? createStockOut(data) | ||||
|         : updateStockOut(data)); | ||||
|       // 关闭并提示 | ||||
|       await modalApi.close(); | ||||
|       emit('success'); | ||||
|       message.success(formType.value === 'create' ? '新增成功' : '更新成功'); | ||||
|     } finally { | ||||
|       modalApi.unlock(); | ||||
|     } | ||||
|   }, | ||||
|   async onOpenChange(isOpen: boolean) { | ||||
|     if (!isOpen) { | ||||
|       formData.value = undefined; | ||||
|       return; | ||||
|     } | ||||
|     // 加载数据 | ||||
|     const data = modalApi.getData<{ id?: number; type: string }>(); | ||||
|     if (!data) { | ||||
|       return; | ||||
|     } | ||||
|     formType.value = data.type; | ||||
|     formApi.updateSchema(useFormSchema(formType.value)); | ||||
|     if (!data.id) { | ||||
|       // 初始化空的表单数据 | ||||
|       formData.value = { items: [] } as unknown as ErpStockOutApi.StockOut; | ||||
|       await nextTick(); | ||||
|       const itemFormInstance = Array.isArray(itemFormRef.value) | ||||
|         ? itemFormRef.value[0] | ||||
|         : itemFormRef.value; | ||||
|       if (itemFormInstance && typeof itemFormInstance.init === 'function') { | ||||
|         itemFormInstance.init([]); | ||||
|       } | ||||
|       // 如果是新增,自动添加一行 | ||||
|       if (formType.value === 'create' && itemFormInstance) { | ||||
|         itemFormInstance.handleAdd(); | ||||
|       } | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     modalApi.lock(); | ||||
|     try { | ||||
|       formData.value = await getStockOut(data.id); | ||||
|       // 设置到 values | ||||
|       await formApi.setValues(formData.value); | ||||
|       // 初始化子表单 | ||||
|       await nextTick(); | ||||
|       const itemFormInstance = Array.isArray(itemFormRef.value) | ||||
|         ? itemFormRef.value[0] | ||||
|         : itemFormRef.value; | ||||
|       if (itemFormInstance && typeof itemFormInstance.init === 'function') { | ||||
|         itemFormInstance.init(formData.value.items || []); | ||||
|       } | ||||
|     } finally { | ||||
|       modalApi.unlock(); | ||||
|     } | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| /** 审核/反审核 */ | ||||
| async function handleUpdateStatus(id: number, status: number) { | ||||
|   try { | ||||
|     await updateStockOutStatus({ id, status }); | ||||
|     message.success(status === 20 ? '审核成功' : '反审核成功'); | ||||
|     emit('success'); | ||||
|     await modalApi.close(); | ||||
|   } catch (error: any) { | ||||
|     message.error(error.message || '操作失败'); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| defineExpose({ modalApi, handleUpdateStatus }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Modal | ||||
|     v-bind="$attrs" | ||||
|     :title="getTitle" | ||||
|     class="w-4/5" | ||||
|     :closable="true" | ||||
|     :mask-closable="true" | ||||
|     :show-confirm-button="formType !== 'detail'" | ||||
|   > | ||||
|     <Form class="mx-3"> | ||||
|       <template #product="slotProps"> | ||||
|         <StockInItemForm | ||||
|           v-bind="slotProps" | ||||
|           ref="itemFormRef" | ||||
|           class="w-full" | ||||
|           :items="formData?.items ?? []" | ||||
|           :disabled="formType === 'detail'" | ||||
|           @update:items="handleUpdateItems" | ||||
|         /> | ||||
|       </template> | ||||
|     </Form> | ||||
|   </Modal> | ||||
| </template> | ||||
|  | @ -0,0 +1,367 @@ | |||
| <script lang="ts" setup> | ||||
| import type { VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| import type { ErpStockOutApi } from '#/api/erp/stock/out'; | ||||
| 
 | ||||
| import { nextTick, onMounted, ref, watch } from 'vue'; | ||||
| 
 | ||||
| import { erpPriceMultiply } from '@vben/utils'; | ||||
| 
 | ||||
| import { Input, InputNumber, Select } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; | ||||
| import { getProductSimpleList } from '#/api/erp/product/product'; | ||||
| import { getStockCount } from '#/api/erp/stock/stock'; | ||||
| import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse'; | ||||
| 
 | ||||
| import { useStockInItemTableColumns } from '../data'; | ||||
| 
 | ||||
| const props = withDefaults(defineProps<Props>(), { | ||||
|   items: () => [], | ||||
|   disabled: false, | ||||
| }); | ||||
| 
 | ||||
| const emit = defineEmits(['update:items']); | ||||
| 
 | ||||
| interface Props { | ||||
|   items?: ErpStockOutApi.StockOutItem[]; | ||||
|   disabled?: boolean; | ||||
| } | ||||
| 
 | ||||
| const tableData = ref<ErpStockOutApi.StockOutItem[]>([]); | ||||
| const productOptions = ref<any[]>([]); | ||||
| const warehouseOptions = ref<any[]>([]); | ||||
| const isValidating = ref(false); | ||||
| 
 | ||||
| /** 表格配置 */ | ||||
| const [Grid, gridApi] = useVbenVxeGrid({ | ||||
|   gridOptions: { | ||||
|     editConfig: { | ||||
|       trigger: 'click', | ||||
|       mode: 'cell', | ||||
|     }, | ||||
|     columns: useStockInItemTableColumns(isValidating), | ||||
|     data: tableData.value, | ||||
|     border: true, | ||||
|     showOverflow: true, | ||||
|     autoResize: true, | ||||
|     minHeight: 250, | ||||
|     keepSource: true, | ||||
|     rowConfig: { | ||||
|       keyField: 'id', | ||||
|     }, | ||||
|     pagerConfig: { | ||||
|       enabled: false, | ||||
|     }, | ||||
|     toolbarConfig: { | ||||
|       enabled: false, | ||||
|     }, | ||||
|     showFooter: true, | ||||
|     footerCellClassName: 'stock-in-footer-cell', | ||||
|     footerMethod: ({ | ||||
|       columns, | ||||
|     }: { | ||||
|       columns: VxeTableGridOptions['columns']; | ||||
|     }) => { | ||||
|       const footers: any[][] = []; | ||||
|       const sums = getSummaries(); | ||||
|       const footerData: any[] = []; | ||||
|       columns!.forEach((column, columnIndex: number) => { | ||||
|         if (columnIndex === 0) { | ||||
|           footerData.push('合计'); | ||||
|         } else if (column.field === 'count') { | ||||
|           footerData.push(sums.count); | ||||
|         } else if (column.field === 'totalPrice') { | ||||
|           footerData.push(sums.totalPrice); | ||||
|         } else { | ||||
|           footerData.push(''); | ||||
|         } | ||||
|       }); | ||||
|       footers.push(footerData); | ||||
|       return footers; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| /** 监听外部传入的列数据 */ | ||||
| watch( | ||||
|   () => props.items, | ||||
|   async (items) => { | ||||
|     if (!items) { | ||||
|       return; | ||||
|     } | ||||
|     await nextTick(); | ||||
|     tableData.value = [...items]; | ||||
|     await nextTick(); | ||||
|     gridApi.grid.reloadData(tableData.value); | ||||
|   }, | ||||
|   { | ||||
|     immediate: true, | ||||
|   }, | ||||
| ); | ||||
| 
 | ||||
| /** 初始化 */ | ||||
| onMounted(async () => { | ||||
|   productOptions.value = await getProductSimpleList(); | ||||
|   warehouseOptions.value = await getWarehouseSimpleList(); | ||||
| }); | ||||
| 
 | ||||
| function handleAdd() { | ||||
|   const newRow = { | ||||
|     warehouseId: undefined, | ||||
|     productId: undefined, | ||||
|     productName: '', | ||||
|     productUnitId: undefined, | ||||
|     productUnitName: '', | ||||
|     productBarCode: '', | ||||
|     count: 1, | ||||
|     productPrice: 0, | ||||
|     totalPrice: 0, | ||||
|     stockCount: 0, | ||||
|     remark: '', | ||||
|   }; | ||||
|   tableData.value.push(newRow); | ||||
|   gridApi.grid.insertAt(newRow, -1); | ||||
|   emit('update:items', [...tableData.value]); | ||||
|   // 触发表格重新渲染以更新cellClassName | ||||
|   nextTick(() => { | ||||
|     gridApi.grid.refreshColumn(); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function handleDelete(row: ErpStockOutApi.StockOutItem) { | ||||
|   gridApi.grid.remove(row); | ||||
|   const index = tableData.value.findIndex((item) => item.id === row.id); | ||||
|   if (index !== -1) { | ||||
|     tableData.value.splice(index, 1); | ||||
|   } | ||||
|   emit('update:items', [...tableData.value]); | ||||
| } | ||||
| 
 | ||||
| async function handleWarehouseChange(warehouseId: any, row: any) { | ||||
|   const warehouse = warehouseOptions.value.find((w) => w.id === warehouseId); | ||||
|   if (!warehouse) { | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   row.warehouseId = warehouseId; | ||||
| 
 | ||||
|   // 如果已选择产品,重新获取库存 | ||||
|   if (row.productId) { | ||||
|     const stockCount = await getStockCount(row.productId, warehouseId); | ||||
|     row.stockCount = stockCount || 0; | ||||
|   } | ||||
| 
 | ||||
|   handleUpdateValue(row); | ||||
| } | ||||
| 
 | ||||
| async function handleProductChange(productId: any, row: any) { | ||||
|   const product = productOptions.value.find((p) => p.id === productId); | ||||
|   if (!product) { | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   // 获取库存数量 | ||||
|   const stockCount = row.warehouseId | ||||
|     ? await getStockCount(productId, row.warehouseId) | ||||
|     : await getStockCount(productId); | ||||
| 
 | ||||
|   row.productId = productId; | ||||
|   row.productUnitId = product.unitId; | ||||
|   row.productBarCode = product.barCode; | ||||
|   row.productUnitName = product.unitName; | ||||
|   row.productName = product.name; | ||||
|   row.stockCount = stockCount || 0; | ||||
|   row.productPrice = product.purchasePrice || 0; | ||||
|   row.count = row.count || 1; | ||||
| 
 | ||||
|   handlePriceChange(row); | ||||
| } | ||||
| 
 | ||||
| function handlePriceChange(row: any) { | ||||
|   if (row.productPrice && row.count) { | ||||
|     row.totalPrice = erpPriceMultiply(row.productPrice, row.count) ?? 0; | ||||
|   } | ||||
|   handleUpdateValue(row); | ||||
| } | ||||
| 
 | ||||
| function handleUpdateValue(row: any) { | ||||
|   const index = tableData.value.findIndex((item) => item.id === row.id); | ||||
|   if (index === -1) { | ||||
|     tableData.value.push(row); | ||||
|   } else { | ||||
|     tableData.value[index] = row; | ||||
|   } | ||||
|   emit('update:items', [...tableData.value]); | ||||
|   // 触发表格重新渲染以更新cellClassName | ||||
|   nextTick(() => { | ||||
|     gridApi.grid.refreshColumn(); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| const getSummaries = (): { | ||||
|   count: number; | ||||
|   totalPrice: number; | ||||
| } => { | ||||
|   return { | ||||
|     count: tableData.value.reduce((sum, item) => sum + (item.count || 0), 0), | ||||
|     totalPrice: tableData.value.reduce( | ||||
|       (sum, item) => sum + (item.totalPrice || 0), | ||||
|       0, | ||||
|     ), | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| /** 验证表单 */ | ||||
| function validate(): Promise<boolean> { | ||||
|   return new Promise((resolve) => { | ||||
|     isValidating.value = true; | ||||
| 
 | ||||
|     // 触发表格重新渲染以显示验证错误 | ||||
|     nextTick(() => { | ||||
|       gridApi.grid.refreshColumn(); | ||||
|     }); | ||||
| 
 | ||||
|     // 验证是否有产品清单 | ||||
|     if (!tableData.value || tableData.value.length === 0) { | ||||
|       resolve(false); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     // 验证每一行的必填字段 | ||||
|     for (const item of tableData.value) { | ||||
|       if ( | ||||
|         !item.warehouseId || | ||||
|         !item.productId || | ||||
|         !item.count || | ||||
|         item.count <= 0 | ||||
|       ) { | ||||
|         resolve(false); | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // 验证通过,清除验证状态 | ||||
|     isValidating.value = false; | ||||
|     nextTick(() => { | ||||
|       gridApi.grid.refreshColumn(); | ||||
|     }); | ||||
| 
 | ||||
|     resolve(true); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| /** 初始化表格数据 */ | ||||
| function init(items: ErpStockOutApi.StockOutItem[]) { | ||||
|   tableData.value = items || []; | ||||
|   gridApi.grid.reloadData(tableData.value); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
|   validate, | ||||
|   init, | ||||
|   handleAdd, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <div class="w-full"> | ||||
|     <div class="mb-4 flex justify-between"> | ||||
|       <span class="text-lg font-medium"></span> | ||||
|     </div> | ||||
| 
 | ||||
|     <Grid> | ||||
|       <template #warehouseId="{ row }"> | ||||
|         <Select | ||||
|           v-model:value="row.warehouseId" | ||||
|           :options="warehouseOptions" | ||||
|           :field-names="{ label: 'name', value: 'id' }" | ||||
|           placeholder="请选择仓库" | ||||
|           :disabled="disabled" | ||||
|           show-search | ||||
|           @change="(value) => handleWarehouseChange(value, row)" | ||||
|           class="w-full" | ||||
|         /> | ||||
|       </template> | ||||
| 
 | ||||
|       <template #productId="{ row }"> | ||||
|         <Select | ||||
|           v-model:value="row.productId" | ||||
|           :options="productOptions" | ||||
|           :field-names="{ label: 'name', value: 'id' }" | ||||
|           placeholder="请选择产品" | ||||
|           :disabled="disabled" | ||||
|           show-search | ||||
|           @change="(value) => handleProductChange(value, row)" | ||||
|           class="w-full" | ||||
|         /> | ||||
|       </template> | ||||
| 
 | ||||
|       <template #count="{ row }"> | ||||
|         <InputNumber | ||||
|           v-model:value="row.count" | ||||
|           :disabled="disabled" | ||||
|           :min="0.001" | ||||
|           :precision="3" | ||||
|           @change="() => handlePriceChange(row)" | ||||
|           class="w-full" | ||||
|         /> | ||||
|       </template> | ||||
| 
 | ||||
|       <template #productPrice="{ row }"> | ||||
|         <InputNumber | ||||
|           v-model:value="row.productPrice" | ||||
|           :disabled="disabled" | ||||
|           :min="0.01" | ||||
|           :precision="2" | ||||
|           @change="() => handlePriceChange(row)" | ||||
|           class="w-full" | ||||
|         /> | ||||
|       </template> | ||||
| 
 | ||||
|       <template #remark="{ row }"> | ||||
|         <Input | ||||
|           v-model:value="row.remark" | ||||
|           :disabled="disabled" | ||||
|           placeholder="请输入备注" | ||||
|         /> | ||||
|       </template> | ||||
| 
 | ||||
|       <template #actions="{ row }"> | ||||
|         <TableAction | ||||
|           v-if="!disabled" | ||||
|           :actions="[ | ||||
|             { | ||||
|               label: '删除', | ||||
|               type: 'link', | ||||
|               danger: true, | ||||
|               popConfirm: { | ||||
|                 title: '确认删除该产品吗?', | ||||
|                 confirm: handleDelete.bind(null, row), | ||||
|               }, | ||||
|             }, | ||||
|           ]" | ||||
|         /> | ||||
|       </template> | ||||
| 
 | ||||
|       <template #bottom> | ||||
|         <TableAction | ||||
|           v-if="!disabled" | ||||
|           class="mt-4 flex justify-center" | ||||
|           :actions="[ | ||||
|             { | ||||
|               label: '添加产品', | ||||
|               type: 'default', | ||||
|               onClick: handleAdd, | ||||
|             }, | ||||
|           ]" | ||||
|         /> | ||||
|       </template> | ||||
|     </Grid> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
| :deep(.vxe-table .vxe-footer--column.stock-in-footer-cell .vxe-cell) { | ||||
|   background-color: #f5f5f5 !important; | ||||
| } | ||||
| </style> | ||||
		Loading…
	
		Reference in New Issue
	
	 XuZhiqiang
						XuZhiqiang