refactor(mes,wls): 生产计划审核改在协同办公,精简报工/工单归档入口
- 生产计划页去掉页面内审核弹窗与接口,新增审批详情页(detail/approve),审批在协同办公完成
- 生产报工、工单列表去掉「归档」入口(方案未要求)
- 工单操作列加宽到 440,避免多个链接按钮被裁切
- 生领料单弹窗改用 Modal onConfirm 提交,修复点「生成领料单」无反应
- 销售通知明细去掉「是否检验」列
Co-Authored-By: Claude Code <noreply@anthropic.com>
| | |
| | | remark?: string; |
| | | } |
| | | |
| | | /** å®¡æ ¸è¯·æ± */ |
| | | export interface AuditReq { |
| | | id: number; |
| | | approved: boolean; |
| | | auditRemark?: string; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢ MPS å页 */ |
| | |
| | | /** æäº¤å®¡æ ¸ */ |
| | | export function submitMps(id: number) { |
| | | return requestClient.post(`/mes/pro-mps/submit?id=${id}`); |
| | | } |
| | | |
| | | /** å®¡æ ¸ï¼éè¿/驳åï¼ */ |
| | | export function auditMps(data: MesProMpsApi.AuditReq) { |
| | | return requestClient.post('/mes/pro-mps/audit', data); |
| | | } |
| | | |
| | | /** æ¥è¯¢è®¡åçè¾
æéæ±æç» */ |
| | |
| | | exportFeedback, |
| | | getFeedbackPage, |
| | | } from '#/api/mes/pro/feedback'; |
| | | import { getWorkOrder } from '#/api/mes/pro/workorder'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import ArchiveDetail from '#/views/mes/pd/archive/modules/detail.vue'; |
| | | |
| | | const userStore = useUserStore(); |
| | | const currentUserId = userStore.userInfo?.id; // å½åç»å½ç¨æ· IDï¼ç¨äºå®¡æ¹äººæé夿 |
| | |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const archiveDetailRef = ref<InstanceType<typeof ArchiveDetail>>(); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | |
| | | async function handleExport() { |
| | | const data = await exportFeedback(await gridApi.formApi.getValues()); |
| | | downloadFileFromBlobPart({ fileName: 'ç产æ¥å·¥.xls', source: data }); |
| | | } |
| | | |
| | | /** æ¥ç彿¡£ */ |
| | | async function handleArchive(row: MesProFeedbackApi.Feedback) { |
| | | if (!row.workOrderId) return; |
| | | try { |
| | | const workOrder = await getWorkOrder(row.workOrderId); |
| | | archiveDetailRef.value?.open(workOrder.productCode); |
| | | } catch { |
| | | message.error('è·åå·¥åä¿¡æ¯å¤±è´¥'); |
| | | } |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: '彿¡£', |
| | | type: 'link', |
| | | onClick: handleArchive.bind(null, row), |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | <ArchiveDetail ref="archiveDetailRef" /> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesProMpsApi } from '#/api/mes/pro/mps'; |
| | | import type { DescriptionItemSchema } from '#/components/description'; |
| | | |
| | | import { h, onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { Spin, Tag } from 'ant-design-vue'; |
| | | |
| | | import { getMps } from '#/api/mes/pro/mps'; |
| | | import { useDescription } from '#/components/description'; |
| | | |
| | | defineOptions({ name: 'MesProMpsApproveDetail' }); |
| | | |
| | | const props = defineProps<{ id: string }>(); |
| | | |
| | | const loading = ref(false); |
| | | const formData = ref<MesProMpsApi.Mps>(); |
| | | |
| | | /** ææ¬åæ®µç©ºå¼å ä½ */ |
| | | function textRender(val: unknown) { |
| | | return val === 0 || val ? String(val) : '-'; |
| | | } |
| | | |
| | | /** ç¶ææ¸²æ */ |
| | | function renderStatus(val?: number) { |
| | | const map: Record<number, { text: string; color: string }> = { |
| | | 0: { text: 'æªå¼å§', color: 'blue' }, |
| | | 1: { text: 'å·²ä¸å', color: 'success' }, |
| | | 2: { text: '已宿', color: 'purple' }, |
| | | 3: { text: '已忶', color: 'red' }, |
| | | 4: { text: 'å®¡æ ¸ä¸', color: 'warning' }, |
| | | 5: { text: 'å·²å®¡æ ¸', color: 'cyan' }, |
| | | }; |
| | | const item = map[val ?? 0]!; |
| | | return h(Tag, { color: item.color }, () => item.text); |
| | | } |
| | | |
| | | /** æ°é渲æï¼å»æå¤ä½å°æ°ä½ */ |
| | | function quantityRender(val?: number) { |
| | | if (val === null || val === undefined) { |
| | | return '-'; |
| | | } |
| | | return Number(val).toString(); |
| | | } |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: true, |
| | | column: 2, |
| | | schema: [ |
| | | { label: '计åç¼å·', field: 'code', render: textRender }, |
| | | { label: 'ç¶æ', field: 'status', render: (val) => renderStatus(val as number) }, |
| | | { label: 'éå®è®¢åå·', field: 'saleOrderNo', render: textRender }, |
| | | { label: '客æ·åç§°', field: 'clientName', render: textRender }, |
| | | { label: '产åç¼ç ', field: 'productCode', render: textRender }, |
| | | { label: '产ååç§°', field: 'productName', render: textRender }, |
| | | { label: 'è§æ ¼åå·', field: 'productSpecification', render: textRender }, |
| | | { label: 'åä½', field: 'unitMeasureName', render: textRender }, |
| | | { |
| | | label: '计åç产æ°é', |
| | | field: 'quantity', |
| | | render: (val) => quantityRender(val as number), |
| | | }, |
| | | { |
| | | label: 'å·²ç产æ°é', |
| | | field: 'quantityProduced', |
| | | render: (val) => quantityRender(val as number), |
| | | }, |
| | | { |
| | | label: '计åå¼å§æ¥æ', |
| | | field: 'planStartDate', |
| | | render: (val) => (val ? (formatDate(val) as string) : '-'), |
| | | }, |
| | | { |
| | | label: 'éæ±æ¥æ', |
| | | field: 'requestDate', |
| | | render: (val) => (val ? (formatDate(val) as string) : '-'), |
| | | }, |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => (val ? (formatDate(val) as string) : '-'), |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | try { |
| | | loading.value = true; |
| | | formData.value = await getMps(Number(props.id)); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <ContentWrap class="m-2"> |
| | | <Spin :spinning="loading" tip="å è½½ä¸..."> |
| | | <div class="rounded-lg border border-border bg-background p-4"> |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">ç产计åä¿¡æ¯</span> |
| | | </div> |
| | | <Descriptions :data="formData" /> |
| | | </div> |
| | | </Spin> |
| | | </ContentWrap> |
| | | </template> |
| | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart, isEmpty } from '@vben/utils'; |
| | | |
| | | import { |
| | | Button, |
| | | message, |
| | | Modal, |
| | | Radio, |
| | | RadioGroup, |
| | | Tag, |
| | | Textarea, |
| | | } from 'ant-design-vue'; |
| | | import { Button, message, Modal, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | auditMps, |
| | | cancelMps, |
| | | deleteMps, |
| | | exportMps, |
| | |
| | | const itemListOpen = ref(false); |
| | | const currentMpsId = ref<number>(); |
| | | |
| | | /** å®¡æ ¸å¼¹çª */ |
| | | const auditOpen = ref(false); |
| | | const auditSubmitting = ref(false); |
| | | const auditForm = ref<{ approved: boolean; remark: string }>({ |
| | | approved: true, |
| | | remark: '', |
| | | }); |
| | | const currentAuditRow = ref<MesProMpsApi.Mps>(); |
| | | |
| | | /** æå¼è¾
ææç» */ |
| | | function handleShowItems(row: MesProMpsApi.Mps) { |
| | | currentMpsId.value = row.id; |
| | |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æå¼å®¡æ ¸å¼¹çª */ |
| | | function handleAudit(row: MesProMpsApi.Mps) { |
| | | currentAuditRow.value = row; |
| | | auditForm.value = { approved: true, remark: '' }; |
| | | auditOpen.value = true; |
| | | } |
| | | |
| | | /** ç¡®è®¤å®¡æ ¸ */ |
| | | async function handleAuditSubmit() { |
| | | if (!currentAuditRow.value?.id) { |
| | | return; |
| | | } |
| | | if (!auditForm.value.approved && !auditForm.value.remark.trim()) { |
| | | message.warning('驳忶å¿
须填å驳ååå '); |
| | | return; |
| | | } |
| | | auditSubmitting.value = true; |
| | | try { |
| | | await auditMps({ |
| | | id: currentAuditRow.value.id, |
| | | approved: auditForm.value.approved, |
| | | auditRemark: auditForm.value.remark.trim() || undefined, |
| | | }); |
| | | message.success(auditForm.value.approved ? 'å®¡æ ¸éè¿' : '已驳å'); |
| | | auditOpen.value = false; |
| | | handleRefresh(); |
| | | } finally { |
| | | auditSubmitting.value = false; |
| | | } |
| | | } |
| | | |
| | |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['mes:pro-mps:update'], |
| | | ifShow: () => |
| | | row.status === MpsStatusEnum.DRAFT || |
| | | row.status === MpsStatusEnum.AUDITING, |
| | | // ä»
è稿å¯ç¼è¾ï¼å®¡æ ¸ä¸ç计åéå¨åååå
¬å®æå®¡æ¹ |
| | | ifShow: () => row.status === MpsStatusEnum.DRAFT, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | |
| | | type: 'link', |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['mes:pro-mps:submit'], |
| | | ifShow: () => |
| | | row.status === MpsStatusEnum.DRAFT || |
| | | row.status === MpsStatusEnum.AUDITING, |
| | | // æäº¤åè¿å
¥åååå
¬å®¡æ¹ï¼å®¡æ ¸ä¸ä¸åéå¤æäº¤ |
| | | ifShow: () => row.status === MpsStatusEnum.DRAFT, |
| | | popConfirm: { |
| | | title: `确认æäº¤${row.code}å®¡æ ¸åï¼`, |
| | | title: `确认æäº¤${row.code}å®¡æ ¸åï¼æäº¤å请å°ãåååå
¬ã宿审æ¹`, |
| | | confirm: handleSubmit.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: 'å®¡æ ¸', |
| | | type: 'link', |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['mes:pro-mps:audit'], |
| | | ifShow: () => row.status === MpsStatusEnum.AUDITING, |
| | | onClick: handleAudit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'çæéè´å', |
| | |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['mes:pro-mps:delete'], |
| | | ifShow: () => |
| | | row.status === MpsStatusEnum.DRAFT || |
| | | row.status === MpsStatusEnum.AUDITING, |
| | | ifShow: () => row.status === MpsStatusEnum.DRAFT, |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.code]), |
| | | confirm: handleDelete.bind(null, row), |
| | |
| | | destroy-on-close |
| | | > |
| | | <ItemList v-if="itemListOpen && currentMpsId" :mps-id="currentMpsId" /> |
| | | </Modal> |
| | | |
| | | <Modal |
| | | v-model:open="auditOpen" |
| | | title="å®¡æ ¸ç产计å" |
| | | :confirm-loading="auditSubmitting" |
| | | ok-text="æäº¤" |
| | | cancel-text="åæ¶" |
| | | width="520px" |
| | | @ok="handleAuditSubmit" |
| | | > |
| | | <div class="mb-3">{{ currentAuditRow?.code }} · {{ currentAuditRow?.productName }}</div> |
| | | <div class="mb-2 flex items-center gap-4"> |
| | | <span>å®¡æ ¸ç»æï¼</span> |
| | | <RadioGroup v-model:value="auditForm.approved"> |
| | | <Radio :value="true">éè¿</Radio> |
| | | <Radio :value="false">驳å</Radio> |
| | | </RadioGroup> |
| | | </div> |
| | | <Textarea |
| | | v-model:value="auditForm.remark" |
| | | :rows="3" |
| | | :maxlength="500" |
| | | :placeholder="auditForm.approved ? 'å®¡æ ¸æè§ï¼å¯éï¼' : '请填å驳ååå ï¼å¿
å¡«ï¼'" |
| | | show-count |
| | | /> |
| | | </Modal> |
| | | </Page> |
| | | </template> |
| | |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 240, |
| | | // å·²ç¡®è®¤è¡æå¤åæ¶åºç° 7 ä¸ªé¾æ¥æé®ï¼æ°å¢å订å/æ¹æ¬¡ç®¡ç/æ§è¡æ
åµ/æå°é¢æå/å¯å¨ç产/宿/åæ¶ï¼ï¼ |
| | | // æ 14px åå·å®æµæ»å®½çº¦ 420ï¼å åå
æ ¼å·¦å³ 6px å
è¾¹è·çå° 440ï¼é¿å
被è£å |
| | | width: 440, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | |
| | | startWorkOrder, |
| | | } from "#/api/mes/pro/workorder"; |
| | | import { $t } from "#/locales"; |
| | | import ArchiveDetail from '#/views/mes/pd/archive/modules/detail.vue'; |
| | | // import { BarcodeDetail } from '#/views/wls/barcode/components'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from "./data"; |
| | |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const archiveVisible = ref(false); |
| | | const archiveProductCode = ref(''); |
| | | // const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); // æ¡ç 详æ
å¼¹çª |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | |
| | | await cancelWorkOrder(row.id!); |
| | | message.success("订å已忶"); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | /** æå¼å½æ¡£è¯¦æ
*/ |
| | | function handleArchive(row: MesProWorkOrderApi.WorkOrder) { |
| | | archiveProductCode.value = row.productCode || ''; |
| | | archiveVisible.value = true; |
| | | } |
| | | |
| | | /** æ¥çè®¢åæ¡ç */ |
| | |
| | | // auth: ['mes:pro-work-order:query'], |
| | | // onClick: handleBarcode.bind(null, row), |
| | | // }, |
| | | { |
| | | label: '彿¡£', |
| | | type: 'link', |
| | | onClick: handleArchive.bind(null, row), |
| | | }, |
| | | ]" /> |
| | | </template> |
| | | </Grid> |
| | | <!-- <BarcodeDetail ref="barcodeDetailRef" /> --> |
| | | <ArchiveDetail :visible="archiveVisible" :product-code="archiveProductCode" @update:visible="archiveVisible = $event" /> |
| | | </Page> |
| | | </template> |
| | | |
| | |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | // å¼¹çªåºé¨ãçæé¢æåãæé®ç¹å»åè°ï¼Vben Modal çç¡®å®æé®åªä¼è°ç¨ modalApi.onConfirmï¼ |
| | | // 模æ¿ä¸å @confirm ä¸ä¼è§¦åï¼ç»ä»¶æ²¡ææ´é² confirm äºä»¶ï¼ï¼å¿
é¡»å¨è¿é注å |
| | | onConfirm: handleGenerate, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | workOrderId.value = undefined; |
| | |
| | | :confirm-text="'çæé¢æå'" |
| | | :confirm-disabled="!workOrderId || !hasGap || loading" |
| | | :confirm-loading="submitting" |
| | | @confirm="handleGenerate" |
| | | > |
| | | <div class="mx-4 flex flex-col gap-3"> |
| | | <div class="flex items-center gap-2"> |
| | |
| | | |
| | | import { nextTick, ref } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | |
| | | import { message, Modal } from 'ant-design-vue'; |
| | | |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | |
| | | field: 'batchCode', |
| | | title: 'æ¹æ¬¡å·', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'oqcCheckFlag', |
| | | title: 'æ¯å¦æ£éª', |
| | | width: 90, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING }, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | | |
| | | import { z } from '#/adapter/form'; |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import CrmCustomerSelect from '#/components/crm-customer-select.vue'; |
| | | import { ErpSaleOrderSelect } from '#/views/erp/sale/order/components'; |
| | |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'oqcCheckFlag', |
| | | title: 'æ¯å¦æ£éª', |
| | | width: 90, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'remark', |
| | | title: '夿³¨', |
| | | minWidth: 120, |
| | |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æ¹æ¬¡å·', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'oqcCheckFlag', |
| | | label: 'æ¯å¦æ£éª', |
| | | component: 'Switch', |
| | | rules: z.boolean().default(true), |
| | | }, |
| | | { |
| | | fieldName: 'remark', |