feat(erp): 销售订单审批接入协同办公前端支持
- 列表页移除审批/反审批,新增提交审批、审批进度(只读查看模式)
- 状态支持 未提交/审批中/已驳回
- 新增销售订单协同办公审批详情组件 detail/index.vue
- 明细表格启用横向滚动,避免列被压缩显示不完整
Co-Authored-By: Claude <noreply@anthropic.com>
| | |
| | | totalPrice: number; // å计éé¢ï¼åä½ï¼å
|
| | | status: number; // ç¶æ |
| | | remark: string; // 夿³¨ |
| | | processInstanceId?: string; // BPM æµç¨å®ä¾ç¼å·ï¼æäº¤åååå
¬å®¡æ¹åçæï¼ |
| | | outCount: number; // éå®åºåºæ°é |
| | | outStatus?: number; // åºåºç¶æï¼0-æªåºåºï¼1-é¨ååºåºï¼2-å
¨é¨åºåº |
| | | attachmentList?: { id: number; name?: string; url?: string }[]; // éä»¶å表 |
| | |
| | | return requestClient.put('/erp/sale-order/update', data); |
| | | } |
| | | |
| | | /** æ´æ°éå®è®¢åçç¶æ */ |
| | | export function updateSaleOrderStatus(id: number, status: number) { |
| | | return requestClient.put('/erp/sale-order/update-status', null, { |
| | | params: { id, status }, |
| | | /** æäº¤éå®è®¢å审æ¹ï¼åååå
¬ï¼ */ |
| | | export function submitSaleOrder(id: number, processDefinitionKey: string) { |
| | | return requestClient.post('/erp/sale-order/submit', null, { |
| | | params: { id, processDefinitionKey }, |
| | | }); |
| | | } |
| | | |
| | | /** è·åéå®è®¢åå®¡æ¹æµç¨å®ä¹å表 */ |
| | | export function getSaleOrderApproveProcessList() { |
| | | return requestClient.get< |
| | | { id: string; key: string; name: string }[] |
| | | >('/erp/sale-order/approve-process-list'); |
| | | } |
| | | |
| | | /** å é¤éå®è®¢å */ |
| | | export function deleteSaleOrder(ids: number[]) { |
| | | return requestClient.delete('/erp/sale-order/delete', { |
| | |
| | | |
| | | import { nextTick, onMounted, ref, shallowRef, watch } from 'vue'; |
| | | |
| | | import { useRoute } from 'vue-router'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { |
| | | BpmFieldPermissionType, |
| | |
| | | |
| | | defineOptions({ name: 'BpmProcessInstanceDetail' }); |
| | | |
| | | const route = useRoute(); |
| | | |
| | | const props = defineProps<{ |
| | | activityId?: string; // æµç¨æ´»å¨ç¼å·ï¼ç¨äºæéæ¥ç |
| | | id: string; // æµç¨å®ä¾çç¼å· |
| | | taskId?: string; // ä»»å¡ç¼å· |
| | | }>(); |
| | | |
| | | /** åªè¯»æ¥ç模å¼ï¼ä»ä¸å¡æ¨¡åï¼å¦éå®è®¢å审æ¹è¿åº¦ï¼è·³è½¬æ¶éèå®¡æ¹æä½æé®ï¼ä»
æ¥ç */ |
| | | const viewOnly = route.query.viewOnly === '1'; |
| | | |
| | | const processInstanceLoading = ref(false); // æµç¨å®ä¾çå è½½ä¸ |
| | | const processInstance = ref<BpmProcessInstanceApi.ProcessInstance>(); // æµç¨å®ä¾ |
| | |
| | | </div> |
| | | |
| | | <template #actions> |
| | | <div class="px-4"> |
| | | <div v-if="!viewOnly" class="px-4"> |
| | | <ProcessInstanceOperationButton |
| | | ref="operationButtonRef" |
| | | :process-instance="processInstance" |
| | |
| | | @success="refresh" |
| | | /> |
| | | </div> |
| | | <div v-else class="px-4 py-2 text-sm text-muted-foreground"> |
| | | <span>åªè¯»æ¥ç模å¼ï¼å®¡æ¹æä½è¯·å¨ãåååå
¬ â å¾
åä»»å¡ãä¸å¤ç</span> |
| | | </div> |
| | | </template> |
| | | </Card> |
| | | <!-- æå°å¯¹è¯æ¡ --> |
| | |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: [ |
| | | { label: 'æªå®¡æ ¸', value: 10 }, |
| | | { label: 'æªæäº¤', value: 0 }, |
| | | { label: '审æ¹ä¸', value: 15 }, |
| | | { label: 'å·²å®¡æ ¸', value: 20 }, |
| | | { label: '已驳å', value: 35 }, |
| | | { label: 'å·²ä½åº', value: 30 }, |
| | | ], |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { ErpSaleOrderApi } from '#/api/erp/sale/order'; |
| | | import type { DescriptionItemSchema } from '#/components/description'; |
| | | |
| | | import { h, onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { |
| | | erpCountInputFormatter, |
| | | erpPriceInputFormatter, |
| | | formatDateTime, |
| | | } from '@vben/utils'; |
| | | |
| | | import { Spin, Tag } from 'ant-design-vue'; |
| | | |
| | | import { getSaleOrder } from '#/api/erp/sale/order'; |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | import { useDescription } from '#/components/description'; |
| | | |
| | | import ItemForm from '../modules/item-form.vue'; |
| | | |
| | | defineOptions({ name: 'ErpSaleOrderDetail' }); |
| | | |
| | | const props = defineProps<{ id: string }>(); |
| | | |
| | | const loading = ref(false); |
| | | const formData = ref<ErpSaleOrderApi.SaleOrder>(); |
| | | |
| | | /** ææ¬åæ®µç©ºå¼å ä½ */ |
| | | function textRender(val: unknown) { |
| | | return val === 0 || val ? String(val) : '-'; |
| | | } |
| | | |
| | | /** åºåºç¶ææ¸²æ */ |
| | | function renderOutStatus(val?: number) { |
| | | const map: Record<number, { text: string; color: string }> = { |
| | | 0: { text: 'æªåºåº', color: 'default' }, |
| | | 1: { text: 'å·²é¢ç', color: 'processing' }, |
| | | 2: { text: 'é¨ååºåº', color: 'warning' }, |
| | | 3: { text: 'å
¨é¨åºåº', color: 'success' }, |
| | | }; |
| | | const item = map[val ?? 0]!; |
| | | return h(Tag, { color: item.color }, () => item.text); |
| | | } |
| | | |
| | | /** çäº§ç¶ææ¸²æ */ |
| | | function renderProductionStatus(val?: number) { |
| | | if (formData.value?.needProduction !== 1) { |
| | | return h(Tag, { color: 'default' }, () => 'æ éç产'); |
| | | } |
| | | const map: Record<number, { text: string; color: string }> = { |
| | | 0: { text: 'æªå®æ', color: 'warning' }, |
| | | 1: { text: '已宿', color: 'success' }, |
| | | }; |
| | | const item = map[val ?? 0]!; |
| | | return h(Tag, { color: item.color }, () => item.text); |
| | | } |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: true, |
| | | column: 2, |
| | | schema: [ |
| | | { label: '订ååå·', field: 'no', render: textRender }, |
| | | { label: '客æ·åç§°', field: 'customerName', render: textRender }, |
| | | { label: 'å
³èåå', field: 'contractNo', render: textRender }, |
| | | { |
| | | label: 'è®¢åæ¶é´', |
| | | field: 'orderTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { |
| | | label: 'å计æ°é', |
| | | field: 'totalCount', |
| | | render: (val) => erpCountInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: 'å计éé¢', |
| | | field: 'totalPrice', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: '伿 éé¢', |
| | | field: 'discountPrice', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: 'å®ééé¢', |
| | | field: 'depositPrice', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: '审æ¹ç¶æ', |
| | | field: 'status', |
| | | render: (val) => |
| | | h(DictTag, { type: DICT_TYPE.ERP_AUDIT_STATUS, value: val }), |
| | | }, |
| | | { |
| | | label: 'åºåºç¶æ', |
| | | field: 'outStatus', |
| | | render: (val) => renderOutStatus(val as number), |
| | | }, |
| | | { |
| | | label: 'çäº§ç¶æ', |
| | | field: 'productionStatus', |
| | | render: (val) => renderProductionStatus(val as number), |
| | | }, |
| | | { label: 'å建人', field: 'creatorName', render: textRender }, |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | try { |
| | | loading.value = true; |
| | | formData.value = await getSaleOrder(Number(props.id)); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <ContentWrap class="m-2"> |
| | | <Spin :spinning="loading" tip="å è½½ä¸..."> |
| | | <div class="space-y-4"> |
| | | <!-- åºæ¬ä¿¡æ¯ --> |
| | | <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> |
| | | <!-- éå®æç» --> |
| | | <div |
| | | v-if="formData?.items?.length" |
| | | 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> |
| | | <ItemForm :items="formData.items" :disabled="true" /> |
| | | </div> |
| | | </div> |
| | | </Spin> |
| | | </ContentWrap> |
| | | </template> |
| | |
| | | deleteSaleOrder, |
| | | enableSaleOrder, |
| | | exportSaleOrder, |
| | | getSaleOrderApproveProcessList, |
| | | getSaleOrderPage, |
| | | updateSaleOrderStatus, |
| | | submitSaleOrder, |
| | | } from '#/api/erp/sale/order'; |
| | | import { generateFromSaleOrder } from '#/api/mes/wm/salesnotice'; |
| | | import { $t } from '#/locales'; |
| | |
| | | } |
| | | } |
| | | |
| | | /** 审æ¹/åå®¡æ¹æä½ */ |
| | | async function handleUpdateStatus( |
| | | row: ErpSaleOrderApi.SaleOrder, |
| | | status: number, |
| | | ) { |
| | | /** æäº¤éå®è®¢å审æ¹ï¼åååå
¬ï¼ */ |
| | | async function handleSubmit(row: ErpSaleOrderApi.SaleOrder) { |
| | | // è·åå¯ç¨çå®¡æ¹æµç¨å®ä¹ |
| | | const processList = await getSaleOrderApproveProcessList(); |
| | | if (isEmpty(processList)) { |
| | | message.error('éå®è®¢åå®¡æ¹æµç¨æªé
ç½®ï¼è¯·å
å¨ãåååå
¬âæµç¨ç®¡çãä¸å建并åå¸éå®è®¢åå®¡æ¹æµç¨'); |
| | | return; |
| | | } |
| | | const hideLoading = message.loading({ |
| | | content: `ç¡®å®${status === 20 ? '审æ¹' : 'å审æ¹'}该订ååï¼`, |
| | | content: 'æ£å¨æäº¤å®¡æ¹...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await updateSaleOrderStatus(row.id!, status); |
| | | message.success(`${status === 20 ? '审æ¹' : 'å审æ¹'}æå`); |
| | | await submitSaleOrder(row.id!, processList[0].key); |
| | | message.success('æäº¤å®¡æ¹æåï¼è¯·å°åååå
¬ä¸å¤ç'); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æ¥ç审æ¹è¿åº¦ï¼è·³è½¬åååå
¬æµç¨è¯¦æ
ï¼åªè¯»æ¨¡å¼ï¼ä¸æ¾ç¤ºå®¡æ¹æä½ï¼ */ |
| | | function handleViewApproval(row: ErpSaleOrderApi.SaleOrder) { |
| | | if (row.processInstanceId) { |
| | | router.push({ |
| | | name: 'BpmProcessInstanceDetail', |
| | | query: { id: row.processInstanceId, viewOnly: '1' }, |
| | | }); |
| | | } |
| | | } |
| | | |
| | |
| | | <span v-else>-</span> |
| | | </template> |
| | | <template #status="{ row }"> |
| | | <Tag v-if="row.status === 10" color="warning">æªå®¡æ ¸</Tag> |
| | | <Tag v-if="row.status === 0" color="default">æªæäº¤</Tag> |
| | | <Tag v-if="row.status === 15" color="processing">审æ¹ä¸</Tag> |
| | | <Tag v-if="row.status === 20" color="success">å·²å®¡æ ¸</Tag> |
| | | <Tag v-if="row.status === 35" color="warning">已驳å</Tag> |
| | | <Tag v-if="row.status === 30" color="error">å·²ä½åº</Tag> |
| | | </template> |
| | | <template #needProduction="{ row }"> |
| | |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['erp:sale-order:update'], |
| | | ifShow: () => row.status === 10, |
| | | ifShow: () => row.status === 0 || row.status === 35, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: '审æ¹', |
| | | label: 'æäº¤å®¡æ¹', |
| | | type: 'link', |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['erp:sale-order:update-status'], |
| | | ifShow: () => row.status === 10, |
| | | ifShow: () => row.status === 0 || row.status === 35, |
| | | popConfirm: { |
| | | title: `确认审æ¹${row.no}åï¼`, |
| | | confirm: handleUpdateStatus.bind(null, row, 20), |
| | | title: `确认æäº¤${row.no}ç审æ¹åï¼æäº¤å审æ¹å°å¨åååå
¬ä¸å®æ`, |
| | | confirm: handleSubmit.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: 'å审æ¹', |
| | | label: '审æ¹è¿åº¦', |
| | | type: 'link', |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['erp:sale-order:update-status'], |
| | | ifShow: () => row.status === 20, |
| | | popConfirm: { |
| | | title: `确认å审æ¹${row.no}åï¼`, |
| | | confirm: handleUpdateStatus.bind(null, row, 10), |
| | | }, |
| | | icon: 'lucide:file-clock', |
| | | auth: ['erp:sale-order:query'], |
| | | ifShow: () => row.status === 15 && !!row.processInstanceId, |
| | | onClick: handleViewApproval.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'ä½åº', |
| | |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['erp:sale-order:delete'], |
| | | ifShow: () => row.status === 10, |
| | | ifShow: () => row.status === 0 || row.status === 35, |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.no]), |
| | | confirm: handleDelete.bind(null, [row.id!]), |
| | |
| | | minHeight: 250, |
| | | autoResize: true, |
| | | border: true, |
| | | // æç»åè¾å¤ï¼å¯ç¨æ¨ªåæ»å¨ï¼é¿å
å被åç¼©å¯¼è´æ¾ç¤ºä¸å®æ´ï¼åååå
¬å®¡æ¹è¯¦æ
ççªå®¹å¨åºæ¯ï¼ |
| | | scroll: { |
| | | x: 'auto', |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'seq', |
| | | isHover: true, |