银川
1.销售报价页面开发、联调
2.销售订单页面开发、联调
3.合同管理页面开发联调
| | |
| | | updateTime?: Date; |
| | | products?: ContractProduct[]; |
| | | contactName?: string; |
| | | orderId?: number; // å
³è ERP éå®è®¢åç¼å· |
| | | orderNo?: string; // ERP éå®è®¢ååå· |
| | | } |
| | | |
| | | /** åå产åä¿¡æ¯ */ |
| | | export interface ContractProduct { |
| | | id: number; |
| | | productId: number; |
| | | productName: string; |
| | | productNo: string; |
| | | productUnit: number; |
| | | productPrice: number; |
| | | itemId: number; |
| | | itemName: string; |
| | | itemCode: string; |
| | | itemBarCode: string; |
| | | itemSpecification?: string; |
| | | itemUnitName: string; |
| | | itemUnitName2?: string; |
| | | itemUnitName3?: string; |
| | | itemPrice: number; |
| | | contractPrice: number; |
| | | count: number; |
| | | totalPrice: number; |
| | | } |
| | | |
| | | /** å®¡æ¹æµç¨ */ |
| | | export interface ApproveProcess { |
| | | key: string; |
| | | name: string; |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | /** æäº¤å®¡æ ¸ */ |
| | | export function submitContract(id: number) { |
| | | return requestClient.put(`/crm/contract/submit?id=${id}`); |
| | | export function submitContract(id: number, processDefinitionKey: string) { |
| | | return requestClient.put('/crm/contract/submit', null, { |
| | | params: { id, processDefinitionKey }, |
| | | }); |
| | | } |
| | | |
| | | /** è·åååå®¡æ¹æµç¨å表 */ |
| | | export function getContractApproveProcessList() { |
| | | return requestClient.get<CrmContractApi.ApproveProcess[]>( |
| | | '/crm/contract/approve-process-list', |
| | | ); |
| | | } |
| | | |
| | | /** åå转移 */ |
| | |
| | | export function getRemindContractCount() { |
| | | return requestClient.get<number>('/crm/contract/remind-count'); |
| | | } |
| | | |
| | | /** æ ¹æ®ååçæéå®è®¢å */ |
| | | export function generateSaleOrder(id: number) { |
| | | return requestClient.post<number>('/crm/contract/generate-sale-order', null, { params: { id } }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace CrmReportApi { |
| | | /** 客æ·åæç»è®¡ */ |
| | | export interface CustomerAnalyze { |
| | | totalCount: number; |
| | | todayCount: number; |
| | | dealCount: number; |
| | | dealRate: number; |
| | | lockCount: number; |
| | | poolCount: number; |
| | | } |
| | | |
| | | /** åæºåæç»è®¡ */ |
| | | export interface BusinessAnalyze { |
| | | totalCount: number; |
| | | todayCount: number; |
| | | totalPrice: number; |
| | | successCount: number; |
| | | successPrice: number; |
| | | successRate: number; |
| | | processingCount: number; |
| | | failCount: number; |
| | | } |
| | | |
| | | /** åååæç»è®¡ */ |
| | | export interface ContractAnalyze { |
| | | totalCount: number; |
| | | todayCount: number; |
| | | totalPrice: number; |
| | | approveCount: number; |
| | | approvePrice: number; |
| | | processCount: number; |
| | | receivablePrice: number; |
| | | receivableRate: number; |
| | | } |
| | | |
| | | /** é宿¼æé¶æ®µ */ |
| | | export interface SalesFunnelStage { |
| | | name: string; |
| | | count: number | null; |
| | | price: number; |
| | | conversionRate: number; |
| | | } |
| | | |
| | | /** é宿¼æç»è®¡ */ |
| | | export interface SalesFunnel { |
| | | stages: SalesFunnelStage[]; |
| | | } |
| | | |
| | | /** éå®ä¸ç»©é¡¹ */ |
| | | export interface SalesPerformanceItem { |
| | | userId: number; |
| | | userName: string; |
| | | deptName: string; |
| | | contractCount: number; |
| | | contractPrice: number; |
| | | receivablePrice: number; |
| | | businessCount: number; |
| | | businessPrice: number; |
| | | } |
| | | |
| | | /** éå®ä¸ç»©ç»è®¡ */ |
| | | export interface SalesPerformance { |
| | | items: SalesPerformanceItem[]; |
| | | } |
| | | } |
| | | |
| | | /** è·å客æ·åæç»è®¡ */ |
| | | export function getCustomerAnalyze(startTime?: string, endTime?: string) { |
| | | return requestClient.get<CrmReportApi.CustomerAnalyze>( |
| | | '/crm/report/get-customer-analyze', |
| | | { params: { startTime, endTime } }, |
| | | ); |
| | | } |
| | | |
| | | /** è·ååæºåæç»è®¡ */ |
| | | export function getBusinessAnalyze(startTime?: string, endTime?: string) { |
| | | return requestClient.get<CrmReportApi.BusinessAnalyze>( |
| | | '/crm/report/get-business-analyze', |
| | | { params: { startTime, endTime } }, |
| | | ); |
| | | } |
| | | |
| | | /** è·ååååæç»è®¡ */ |
| | | export function getContractAnalyze(startTime?: string, endTime?: string) { |
| | | return requestClient.get<CrmReportApi.ContractAnalyze>( |
| | | '/crm/report/get-contract-analyze', |
| | | { params: { startTime, endTime } }, |
| | | ); |
| | | } |
| | | |
| | | /** è·åé宿¼æç»è®¡ */ |
| | | export function getSalesFunnel() { |
| | | return requestClient.get<CrmReportApi.SalesFunnel>( |
| | | '/crm/report/get-sales-funnel', |
| | | ); |
| | | } |
| | | |
| | | /** è·åéå®ä¸ç»©ç»è®¡ */ |
| | | export function getSalesPerformance(startTime?: string, endTime?: string) { |
| | | return requestClient.get<CrmReportApi.SalesPerformance>( |
| | | '/crm/report/get-sales-performance', |
| | | { params: { startTime, endTime } }, |
| | | ); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '../../../packages/effects/request/src'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace CrmSaleQuotationApi { |
| | | /** æ¥ä»·åä¿¡æ¯ */ |
| | | export interface SaleQuotation { |
| | | id: number; |
| | | name: string; |
| | | no: string; |
| | | customerId: number; |
| | | customerName?: string; |
| | | businessId?: number; |
| | | businessName?: string; |
| | | contactId?: number; |
| | | contactName?: string; |
| | | quotationTime: Date; |
| | | validUntil?: Date; |
| | | totalProductPrice: number; |
| | | discountPercent: number; |
| | | totalPrice: number; |
| | | taxRate?: number; |
| | | fileUrl?: string; |
| | | remark?: string; |
| | | status: number; |
| | | contractId?: number; |
| | | contractNo?: string; |
| | | ownerUserId: number; |
| | | ownerUserName?: string; |
| | | creator: string; |
| | | creatorName: string; |
| | | createTime?: Date; |
| | | updateTime?: Date; |
| | | items?: SaleQuotationItem[]; |
| | | } |
| | | |
| | | /** æ¥ä»·åç©ææç» */ |
| | | export interface SaleQuotationItem { |
| | | id: number; |
| | | itemId: number; |
| | | itemName: string; |
| | | itemCode: string; |
| | | itemBarCode?: string; |
| | | itemSpecification?: string; |
| | | itemUnitName: string; |
| | | itemUnitName2?: string; |
| | | itemUnitName3?: string; |
| | | itemPrice: number; |
| | | quotationPrice: number; |
| | | count: number; |
| | | totalPrice: number; |
| | | taxPercent?: number; |
| | | taxPrice?: number; |
| | | remark?: string; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¥ä»·åå页å表 */ |
| | | export function getSaleQuotationPage(params: PageParam) { |
| | | return requestClient.get<PageResult<CrmSaleQuotationApi.SaleQuotation>>( |
| | | '/crm/sale-quotation/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¥ä»·åå页å表ï¼åºäºå®¢æ·ï¼ */ |
| | | export function getSaleQuotationPageByCustomer(params: PageParam) { |
| | | return requestClient.get<PageResult<CrmSaleQuotationApi.SaleQuotation>>( |
| | | '/crm/sale-quotation/page-by-customer', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¥ä»·åå页å表ï¼åºäºåæºï¼ */ |
| | | export function getSaleQuotationPageByBusiness(params: PageParam) { |
| | | return requestClient.get<PageResult<CrmSaleQuotationApi.SaleQuotation>>( |
| | | '/crm/sale-quotation/page-by-business', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¥ä»·å详æ
*/ |
| | | export function getSaleQuotation(id: number) { |
| | | return requestClient.get<CrmSaleQuotationApi.SaleQuotation>( |
| | | `/crm/sale-quotation/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢æ¥ä»·å */ |
| | | export function createSaleQuotation(data: CrmSaleQuotationApi.SaleQuotation) { |
| | | return requestClient.post('/crm/sale-quotation/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹æ¥ä»·å */ |
| | | export function updateSaleQuotation(data: CrmSaleQuotationApi.SaleQuotation) { |
| | | return requestClient.put('/crm/sale-quotation/update', data); |
| | | } |
| | | |
| | | /** å 餿¥ä»·å */ |
| | | export function deleteSaleQuotation(id: number) { |
| | | return requestClient.delete(`/crm/sale-quotation/delete?id=${id}`); |
| | | } |
| | | |
| | | /** 转为åå */ |
| | | export function convertSaleQuotationToContract(id: number) { |
| | | return requestClient.post<number>('/crm/sale-quotation/convert-contract', null, { |
| | | params: { id }, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace CrmSaleTraceApi { |
| | | /** éå®è¿½æº¯ä¿¡æ¯ */ |
| | | export interface SaleTrace { |
| | | businessId?: number; |
| | | businessName?: string; |
| | | businessPrice?: number; |
| | | businessCreateTime?: Date; |
| | | customerId?: number; |
| | | customerName?: string; |
| | | contractId?: number; |
| | | contractName?: string; |
| | | contractNo?: string; |
| | | contractPrice?: number; |
| | | contractCreateTime?: Date; |
| | | contractStatus?: number; |
| | | orderId?: number; |
| | | orderNo?: string; |
| | | orderPrice?: number; |
| | | orderCreateTime?: Date; |
| | | receivablePrice?: number; |
| | | receivableItems?: ReceivableItem[]; |
| | | } |
| | | |
| | | /** 忬¾æç» */ |
| | | export interface ReceivableItem { |
| | | receivableId: number; |
| | | receivableNo: string; |
| | | price: number; |
| | | createTime?: Date; |
| | | } |
| | | } |
| | | |
| | | /** æ ¹æ®åæºç¼å·è·åé宿µç¨è¿½æº¯ä¿¡æ¯ */ |
| | | export function getTraceByBusinessId(businessId: number) { |
| | | return requestClient.get<CrmSaleTraceApi.SaleTrace>( |
| | | '/crm/sale-trace/get-by-business-id', |
| | | { params: { businessId } }, |
| | | ); |
| | | } |
| | | |
| | | /** æ ¹æ®ååç¼å·è·åé宿µç¨è¿½æº¯ä¿¡æ¯ */ |
| | | export function getTraceByContractId(contractId: number) { |
| | | return requestClient.get<CrmSaleTraceApi.SaleTrace>( |
| | | '/crm/sale-trace/get-by-contract-id', |
| | | { params: { contractId } }, |
| | | ); |
| | | } |
| | | |
| | | /** æ ¹æ®å®¢æ·ç¼å·è·åé宿µç¨è¿½æº¯å表 */ |
| | | export function getTraceListByCustomerId(customerId: number) { |
| | | return requestClient.get<CrmSaleTraceApi.SaleTrace[]>( |
| | | '/crm/sale-trace/get-list-by-customer-id', |
| | | { params: { customerId } }, |
| | | ); |
| | | } |
| | |
| | | id?: number; // 订åå·¥åç¼å· |
| | | no: string; // éå®è®¢åå· |
| | | customerId: number; // 客æ·ç¼å· |
| | | customerName?: string; // 客æ·åç§° |
| | | accountId?: number; // æ¶æ¬¾è´¦æ·ç¼å· |
| | | orderTime: Date; // è®¢åæ¶é´ |
| | | totalCount: number; // å计æ°é |
| | |
| | | discountPercent?: number; // 伿 çï¼ç¾åæ¯ |
| | | discountPrice?: number; // 伿 éé¢ï¼åä½ï¼å
|
| | | depositPrice?: number; // å®ééé¢ï¼åä½ï¼å
|
| | | contractId?: number; // å
³è CRM ååç¼å· |
| | | contractNo?: string; // CRM åååå· |
| | | items?: SaleOrderItem[]; // éå®è®¢å产åæç»å表 |
| | | } |
| | | |
| | |
| | | categoryName?: string; |
| | | unitMeasureId?: number; |
| | | unitMeasureName?: string; |
| | | unitMeasureId2?: number; |
| | | unitMeasureName2?: string; |
| | | unitMeasureRate1?: number; |
| | | unitMeasureId3?: number; |
| | | unitMeasureName3?: string; |
| | | unitMeasureRate2?: number; |
| | | brandId?: number; |
| | | brandName?: string; |
| | | itemType: number; |
| | |
| | | maxStock?: number; |
| | | isBatchManaged: boolean; |
| | | expiryDay?: number; |
| | | warehouseId?: number; |
| | | warehouseCode?: string; |
| | | warehouseName?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { CrmCustomerApi } from '#/api/crm/customer'; |
| | | |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { Select } from 'ant-design-vue'; |
| | | |
| | | import { getCustomerSimpleList } from '#/api/crm/customer'; |
| | | |
| | | interface Props { |
| | | value?: number; |
| | | disabled?: boolean; |
| | | } |
| | | |
| | | const props = withDefaults(defineProps<Props>(), { |
| | | value: undefined, |
| | | disabled: false, |
| | | }); |
| | | |
| | | const emit = defineEmits(['update:value', 'change']); |
| | | |
| | | 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; |
| | | }, |
| | | ); |
| | | |
| | | watch(selectedValue, (val) => { |
| | | emit('update:value', val); |
| | | const customer = customerList.value.find((item) => item.id === val); |
| | | emit('change', customer); |
| | | }); |
| | | |
| | | async function loadCustomers() { |
| | | if (customerList.value.length > 0) return; |
| | | loading.value = true; |
| | | try { |
| | | customerList.value = await getCustomerSimpleList(); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | function filterOption(input: string, option: any) { |
| | | return option.label.toLowerCase().includes(input.toLowerCase()); |
| | | } |
| | | |
| | | loadCustomers(); |
| | | </script> |
| | | |
| | | <template> |
| | | <Select |
| | | v-model:value="selectedValue" |
| | | :options="options" |
| | | :loading="loading" |
| | | :disabled="disabled" |
| | | placeholder="è¯·éæ©å®¢æ·" |
| | | show-search |
| | | :filter-option="filterOption" |
| | | allow-clear |
| | | /> |
| | | </template> |
| | |
| | | |
| | | import { z } from '#/adapter/form'; |
| | | import { getUnitPage } from '#/api/mdm/unit'; |
| | | import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse'; |
| | | |
| | | /** ç©æç±»åé项 */ |
| | | export const ITEM_TYPE_OPTIONS = [ |
| | |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | component: 'Input', |
| | | fieldName: 'specification', |
| | | label: 'è§æ ¼åå·', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥è§æ ¼åå·', |
| | | }, |
| | | }, |
| | | { |
| | | component: 'ApiSelect', |
| | | fieldName: 'unitMeasureId', |
| | | label: '计éåä½', |
| | | rules: 'required', |
| | | formItemClass: 'col-span-1', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©è®¡éåä½', |
| | | allowClear: true, |
| | |
| | | const res = await getUnitPage({ pageNo: 1, pageSize: 100, status: 0 }); |
| | | return res.list || []; |
| | | }, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | }, |
| | | }, |
| | | { |
| | | component: 'ApiSelect', |
| | | fieldName: 'unitMeasureId2', |
| | | label: 'è¾
åä½1', |
| | | formItemClass: 'col-span-1', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©è¾
åä½1', |
| | | allowClear: true, |
| | | api: async () => { |
| | | const res = await getUnitPage({ pageNo: 1, pageSize: 100, status: 0 }); |
| | | return res.list || []; |
| | | }, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | }, |
| | | }, |
| | | { |
| | | component: 'InputNumber', |
| | | fieldName: 'unitMeasureRate1', |
| | | label: 'è¾
åä½1æ¢ç®æ¯ç', |
| | | formItemClass: 'col-span-1', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | min: 0, |
| | | precision: 6, |
| | | placeholder: '请è¾å
¥æ¢ç®æ¯ç', |
| | | }, |
| | | }, |
| | | { |
| | | component: 'ApiSelect', |
| | | fieldName: 'unitMeasureId3', |
| | | label: 'è¾
åä½2', |
| | | formItemClass: 'col-span-1', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©è¾
åä½2', |
| | | allowClear: true, |
| | | api: async () => { |
| | | const res = await getUnitPage({ pageNo: 1, pageSize: 100, status: 0 }); |
| | | return res.list || []; |
| | | }, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | }, |
| | | }, |
| | | { |
| | | component: 'InputNumber', |
| | | fieldName: 'unitMeasureRate2', |
| | | label: 'è¾
åä½2æ¢ç®æ¯ç', |
| | | formItemClass: 'col-span-1', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | min: 0, |
| | | precision: 6, |
| | | placeholder: '请è¾å
¥æ¢ç®æ¯ç', |
| | | }, |
| | | }, |
| | | { |
| | | component: 'ApiSelect', |
| | | fieldName: 'warehouseId', |
| | | label: 'é»è®¤ä»åº', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©é»è®¤ä»åº', |
| | | allowClear: true, |
| | | api: getWarehouseSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | }, |
| | |
| | | minWidth: 80, |
| | | }, |
| | | { |
| | | field: 'unitMeasureName2', |
| | | title: 'è¾
åä½1', |
| | | minWidth: 80, |
| | | }, |
| | | { |
| | | field: 'unitMeasureRate1', |
| | | title: 'è¾
åä½1æ¢ç®æ¯ç', |
| | | minWidth: 130, |
| | | }, |
| | | { |
| | | field: 'unitMeasureName3', |
| | | title: 'è¾
åä½2', |
| | | minWidth: 80, |
| | | }, |
| | | { |
| | | field: 'unitMeasureRate2', |
| | | title: 'è¾
åä½2æ¢ç®æ¯ç', |
| | | minWidth: 130, |
| | | }, |
| | | { |
| | | field: 'warehouseName', |
| | | title: 'é»è®¤ä»åº', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'itemType', |
| | | title: 'ç©æç±»å', |
| | | minWidth: 100, |
| | |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | labelWidth: 100, |
| | | labelWidth: 110, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: useFormSchema(), |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal class="w-[800px]" :title="getTitle"> |
| | | <Modal class="w-[1000px]" :title="getTitle"> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| | |
| | | import { getDictOptions } from '#/packages/effects/hooks/src'; |
| | | |
| | | import { z } from '#/adapter/form'; |
| | | import { getUnitPage } from '#/api/mdm/unit'; |
| | | |
| | | /** æ°å¢/ä¿®æ¹ç表å */ |
| | | export function useFormSchema(): VbenFormSchema[] { |
| | |
| | | rules: 'required', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥åä½åç§°', |
| | | }, |
| | | }, |
| | | { |
| | | component: 'Switch', |
| | | fieldName: 'primaryFlag', |
| | | label: 'æ¯å¦ä¸»åä½', |
| | | componentProps: { |
| | | class: 'w-auto', |
| | | checkedChildren: 'æ¯', |
| | | unCheckedChildren: 'å¦', |
| | | }, |
| | | rules: z.boolean().default(false), |
| | | }, |
| | | { |
| | | component: 'ApiSelect', |
| | | fieldName: 'primaryId', |
| | | label: '主åä½', |
| | | dependencies: { |
| | | triggerFields: ['primaryFlag'], |
| | | show: (values) => { |
| | | return values.primaryFlag === false; |
| | | }, |
| | | required: (values) => { |
| | | return values.primaryFlag === false; |
| | | }, |
| | | }, |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ä¸»åä½', |
| | | allowClear: true, |
| | | api: async () => { |
| | | const res = await getUnitPage({ pageNo: 1, pageSize: 100, primaryFlag: true, status: 0 }); |
| | | return res.list || []; |
| | | }, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | }, |
| | | }, |
| | | { |
| | | component: 'InputNumber', |
| | | fieldName: 'changeRate', |
| | | label: 'æ¢ç®æ¯ä¾', |
| | | dependencies: { |
| | | triggerFields: ['primaryFlag'], |
| | | show: (values) => { |
| | | return values.primaryFlag === false; |
| | | }, |
| | | required: (values) => { |
| | | return values.primaryFlag === false; |
| | | }, |
| | | }, |
| | | componentProps: { |
| | | class: '!w-full', |
| | | min: 0, |
| | | precision: 4, |
| | | placeholder: '请è¾å
¥æ¢ç®æ¯ä¾', |
| | | }, |
| | | }, |
| | | { |
| | |
| | | field: 'name', |
| | | title: 'åä½åç§°', |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | field: 'primaryFlag', |
| | | title: '主åä½', |
| | | minWidth: 80, |
| | | slots: { default: 'primaryFlag' }, |
| | | }, |
| | | { |
| | | field: 'changeRate', |
| | | title: 'æ¢ç®æ¯ä¾', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'status', |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | import { DICT_TYPE } from '../../../packages/constants/src'; |
| | | import { useUserStore } from '../../../packages/stores/src'; |
| | | import { erpPriceInputFormatter, erpPriceMultiply } from '../../../packages/utils/src'; |
| | | |
| | |
| | | field: 'auditStatus', |
| | | fixed: 'right', |
| | | minWidth: 120, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.CRM_AUDIT_STATUS }, |
| | | }, |
| | | slots: { default: 'auditStatus' }, |
| | | }, |
| | | { |
| | | title: 'å
³è订å', |
| | | field: 'orderNo', |
| | | fixed: 'right', |
| | | minWidth: 150, |
| | | slots: { default: 'orderNo' }, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | field: 'actions', |
| | | fixed: 'right', |
| | | minWidth: 130, |
| | | minWidth: 220, |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | |
| | | const contract = ref<CrmContractApi.Contract>({} as CrmContractApi.Contract); // åå详æ
|
| | | const logList = ref<SystemOperateLogApi.OperateLog[]>([]); // æä½æ¥å¿ |
| | | const permissionListRef = ref<InstanceType<typeof PermissionList>>(); // å¢éæåå表 Ref |
| | | const activeTab = ref('1'); // å½åæ¿æ´»ç Tab |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: false, |
| | |
| | | <Descriptions :data="contract" /> |
| | | </Card> |
| | | <Card class="mt-4 min-h-[60%]"> |
| | | <Tabs> |
| | | <Tabs.TabPane tab="è·è¿è®°å½" key="1" :force-render="true"> |
| | | <FollowUp :biz-id="contractId" :biz-type="BizTypeEnum.CRM_CONTRACT" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="åºæ¬ä¿¡æ¯" key="2" :force-render="true"> |
| | | <Tabs v-model:activeKey="activeTab"> |
| | | <Tabs.TabPane tab="åºæ¬ä¿¡æ¯" key="1" :force-render="true"> |
| | | <ContractDetailsInfo :contract="contract" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="è·è¿è®°å½" key="2" :force-render="true"> |
| | | <FollowUp :biz-id="contractId" :biz-type="BizTypeEnum.CRM_CONTRACT" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="产å" key="3" :force-render="true"> |
| | | <ProductDetailsList |
| | | :biz-id="contractId" |
| | |
| | | import { Page, useVbenModal } from '../../../packages/effects/common-ui/src'; |
| | | import { downloadFileFromBlobPart } from '../../../packages/utils/src'; |
| | | |
| | | import { Button, message, Tabs } from 'ant-design-vue'; |
| | | import { Button, message, Modal, Select, Tabs, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteContract, |
| | | exportContract, |
| | | generateSaleOrder, |
| | | getContractApproveProcessList, |
| | | getContractPage, |
| | | submitContract, |
| | | } from '#/api/crm/contract'; |
| | |
| | | } |
| | | } |
| | | |
| | | /** æäº¤å®¡æ ¸ */ |
| | | /** æäº¤å®¡æ¹å¼¹çª */ |
| | | const submitModalVisible = ref(false); |
| | | const currentRow = ref<CrmContractApi.Contract>(); |
| | | const processList = ref<CrmContractApi.ApproveProcess[]>([]); |
| | | const selectedProcessKey = ref<string>(); |
| | | const submitLoading = ref(false); |
| | | |
| | | async function handleSubmit(row: CrmContractApi.Contract) { |
| | | const hideLoading = message.loading({ |
| | | content: 'æäº¤å®¡æ ¸ä¸...', |
| | | duration: 0, |
| | | }); |
| | | const res = await getContractApproveProcessList(); |
| | | processList.value = res; |
| | | currentRow.value = row; |
| | | selectedProcessKey.value = undefined; |
| | | submitModalVisible.value = true; |
| | | } |
| | | |
| | | async function confirmSubmit() { |
| | | if (!selectedProcessKey.value) { |
| | | message.warning('è¯·éæ©å®¡æ¹æµç¨'); |
| | | return; |
| | | } |
| | | submitLoading.value = true; |
| | | try { |
| | | await submitContract(row.id!); |
| | | message.success('æäº¤å®¡æ ¸æå'); |
| | | await submitContract(currentRow.value!.id!, selectedProcessKey.value); |
| | | message.success('æäº¤å®¡æ¹æå'); |
| | | submitModalVisible.value = false; |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | submitLoading.value = false; |
| | | } |
| | | } |
| | | |
| | |
| | | name: 'BpmProcessInstanceDetail', |
| | | query: { id: row.processInstanceId }, |
| | | }); |
| | | } |
| | | |
| | | /** çæéå®è®¢å */ |
| | | async function handleGenerateOrder(row: CrmContractApi.Contract) { |
| | | if (row.auditStatus !== 20) { |
| | | message.warning('åªæå·²å®¡æ¹çååæè½çæéå®è®¢å'); |
| | | return; |
| | | } |
| | | if (row.orderId) { |
| | | message.warning('该ååå·²çæéå®è®¢å'); |
| | | return; |
| | | } |
| | | const hideLoading = message.loading({ |
| | | content: 'æ£å¨çæéå®è®¢å...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | const orderId = await generateSaleOrder(row.id!); |
| | | message.success('çæéå®è®¢åæå'); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æ¥çéå®è®¢å */ |
| | | function handleViewOrder(row: CrmContractApi.Contract) { |
| | | if (row.orderId) { |
| | | push({ |
| | | name: 'ErpSaleOrder', |
| | | query: { id: row.orderId }, |
| | | }); |
| | | } |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height><FormModal @success="handleRefresh" /> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <Grid> |
| | | <template #toolbar-actions> |
| | | <Tabs class="w-full" @change="handleChangeSceneType"> |
| | |
| | | {{ row.signContactName }} |
| | | </Button> |
| | | </template> |
| | | <template #auditStatus="{ row }"> |
| | | <Tag v-if="row.auditStatus === 0" color="default">æªæäº¤</Tag> |
| | | <Tag v-if="row.auditStatus === 10" color="warning">审æ¹ä¸</Tag> |
| | | <Tag v-if="row.auditStatus === 20" color="success">å®¡æ ¸éè¿</Tag> |
| | | <Tag v-if="row.auditStatus === 30" color="error">å®¡æ ¸ä¸éè¿</Tag> |
| | | </template> |
| | | <template #orderNo="{ row }"> |
| | | <Button v-if="row.orderId" type="link" @click="handleViewOrder(row)"> |
| | | {{ row.orderNo }} |
| | | </Button> |
| | | <span v-else>-</span> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | |
| | | ifShow: row.auditStatus === 0, |
| | | }, |
| | | { |
| | | label: 'æäº¤å®¡æ ¸', |
| | | label: 'æäº¤å®¡æ¹', |
| | | type: 'link', |
| | | auth: ['crm:contract:update'], |
| | | onClick: handleSubmit.bind(null, row), |
| | |
| | | ifShow: row.auditStatus !== 0, |
| | | }, |
| | | { |
| | | label: 'çæéå®è®¢å', |
| | | type: 'link', |
| | | auth: ['crm:contract:update'], |
| | | onClick: handleGenerateOrder.bind(null, row), |
| | | ifShow: row.auditStatus === 20 && !row.orderId, |
| | | }, |
| | | { |
| | | label: 'æ¥ç订å', |
| | | type: 'link', |
| | | auth: ['erp:sale-order:query'], |
| | | onClick: handleViewOrder.bind(null, row), |
| | | ifShow: !!row.orderId, |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.name]), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | ifShow: row.auditStatus === 0, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | |
| | | <!-- æäº¤å®¡æ¹å¼¹çª --> |
| | | <Modal |
| | | v-model:open="submitModalVisible" |
| | | title="éæ©å®¡æ¹æµç¨" |
| | | width="400px" |
| | | @ok="confirmSubmit" |
| | | :confirmLoading="submitLoading" |
| | | > |
| | | <Select |
| | | v-model:value="selectedProcessKey" |
| | | placeholder="è¯·éæ©å®¡æ¹æµç¨" |
| | | class="w-full" |
| | | > |
| | | <Select.Option |
| | | v-for="item in processList" |
| | | :key="item.key" |
| | | :value="item.key" |
| | | > |
| | | {{ item.name }} |
| | | </Select.Option> |
| | | </Select> |
| | | </Modal> |
| | | </Page> |
| | | </template> |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-1/2"> |
| | | <Modal :title="getTitle" class="w-[80%]"> |
| | | <Form class="mx-4"> |
| | | <template #product="slotProps"> |
| | | <ProductEditTable |
| | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | import { DICT_TYPE } from '../../../../packages/constants/src'; |
| | | |
| | | /** 产å详æ
å表çåå®ä¹ */ |
| | | export function useDetailListColumns( |
| | | showBusinessPrice: boolean, |
| | | ): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { |
| | | field: 'productName', |
| | | field: 'itemName', |
| | | title: '产ååç§°', |
| | | }, |
| | | { |
| | | field: 'productNo', |
| | | title: 'äº§åæ¡ç ', |
| | | field: 'itemCode', |
| | | title: 'ç©æç¼ç ', |
| | | }, |
| | | { |
| | | field: 'productUnit', |
| | | title: '产ååä½', |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.CRM_PRODUCT_UNIT }, |
| | | }, |
| | | field: 'itemBarCode', |
| | | title: 'ç©ææ¡ç ', |
| | | }, |
| | | { |
| | | field: 'productPrice', |
| | | title: '产åä»·æ ¼ï¼å
ï¼', |
| | | field: 'itemUnitName', |
| | | title: 'åä½', |
| | | }, |
| | | { |
| | | field: 'itemPrice', |
| | | title: 'åä»·ï¼å
ï¼', |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | |
| | | return [ |
| | | { type: 'seq', title: 'åºå·', minWidth: 50 }, |
| | | { |
| | | field: 'productId', |
| | | field: 'itemId', |
| | | title: '产ååç§°', |
| | | minWidth: 100, |
| | | slots: { default: 'productId' }, |
| | | slots: { default: 'itemId' }, |
| | | }, |
| | | { |
| | | field: 'productNo', |
| | | field: 'itemSpecification', |
| | | title: 'è§æ ¼åå·', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'itemBarCode', |
| | | title: 'æ¡ç ', |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | field: 'productUnit', |
| | | field: 'itemUnitName', |
| | | title: 'åä½', |
| | | minWidth: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.CRM_PRODUCT_UNIT }, |
| | | }, |
| | | minWidth: 80, |
| | | }, |
| | | { |
| | | field: 'productPrice', |
| | | title: 'ä»·æ ¼ï¼å
ï¼', |
| | | field: 'itemUnitName2', |
| | | title: 'è¾
åä½1', |
| | | minWidth: 80, |
| | | }, |
| | | { |
| | | field: 'itemUnitName3', |
| | | title: 'è¾
åä½2', |
| | | minWidth: 80, |
| | | }, |
| | | { |
| | | field: 'itemPrice', |
| | | title: 'åä»·ï¼å
ï¼', |
| | | minWidth: 100, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | field: 'sellingPrice', |
| | | title: 'å®ä»·ï¼å
ï¼', |
| | | field: 'contractPrice', |
| | | title: 'ååä»·ï¼å
ï¼', |
| | | minWidth: 100, |
| | | slots: { default: 'sellingPrice' }, |
| | | slots: { default: 'contractPrice' }, |
| | | }, |
| | | { |
| | | field: 'count', |
| | |
| | | <script lang="ts" setup> |
| | | import type { CrmBusinessApi } from '#/api/crm/business'; |
| | | import type { CrmContractApi } from '#/api/crm/contract'; |
| | | import type { CrmProductApi } from '#/api/crm/product'; |
| | | import type { MdmItemApi } from '#/api/mdm/item'; |
| | | |
| | | import { nextTick, onMounted, ref, watch } from 'vue'; |
| | | |
| | |
| | | |
| | | import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { BizTypeEnum } from '#/api/crm/permission'; |
| | | import { getProductSimpleList } from '#/api/crm/product'; |
| | | import { getItemPage } from '#/api/mdm/item'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useProductEditTableColumns } from './data'; |
| | |
| | | } |
| | | |
| | | /** åæ¢äº§åæ¶åæ¥åºç¡ä¿¡æ¯ */ |
| | | function handleProductChange(productId: any, row: any) { |
| | | const product = productOptions.value.find((p) => p.id === productId); |
| | | function handleProductChange(itemId: any, row: any) { |
| | | const product = productOptions.value.find((p) => p.id === itemId); |
| | | if (!product) { |
| | | return; |
| | | } |
| | | row.productUnit = product.unit; |
| | | row.productNo = product.no; |
| | | row.productPrice = product.price; |
| | | row.sellingPrice = product.price; |
| | | row.count = 0; |
| | | row.totalPrice = 0; |
| | | row.itemId = itemId; |
| | | row.itemName = product.name; |
| | | row.itemCode = product.code; |
| | | row.itemBarCode = product.barCode; |
| | | row.itemUnitName = product.unitMeasureName; |
| | | row.itemUnitName2 = product.unitMeasureName2; |
| | | row.itemUnitName3 = product.unitMeasureName3; |
| | | row.itemSpecification = product.specification; |
| | | row.itemPrice = product.salesPrice; |
| | | row.contractPrice = product.salesPrice; |
| | | row.count = row.count || 1; |
| | | row.totalPrice = erpPriceMultiply(row.contractPrice, row.count) ?? 0; |
| | | handleUpdateValue(row); |
| | | } |
| | | |
| | | /** éé¢å卿¶éæ°è®¡ç®å计 */ |
| | | function handlePriceChange(row: any) { |
| | | row.totalPrice = erpPriceMultiply(row.sellingPrice, row.count) ?? 0; |
| | | row.totalPrice = erpPriceMultiply(row.contractPrice, row.count) ?? 0; |
| | | handleUpdateValue(row); |
| | | } |
| | | |
| | | /** å°ææ°æ°æ®ååå¹¶éç¥ç¶ç»ä»¶ */ |
| | | function handleUpdateValue(row: any) { |
| | | const index = tableData.value.findIndex((item) => item.id === row.id); |
| | | if (props.bizType === BizTypeEnum.CRM_BUSINESS) { |
| | | row.businessPrice = row.sellingPrice; |
| | | } else if (props.bizType === BizTypeEnum.CRM_CONTRACT) { |
| | | row.contractPrice = row.sellingPrice; |
| | | } |
| | | row.totalPrice = erpPriceMultiply(row.contractPrice, row.count) ?? 0; |
| | | if (index === -1) { |
| | | row.id = tableData.value.length + 1; |
| | | tableData.value.push(row); |
| | |
| | | } |
| | | await nextTick(); |
| | | tableData.value = products; |
| | | if (props.bizType === BizTypeEnum.CRM_BUSINESS) { |
| | | tableData.value.forEach((item) => { |
| | | item.sellingPrice = item.businessPrice; |
| | | }); |
| | | } else if (props.bizType === BizTypeEnum.CRM_CONTRACT) { |
| | | tableData.value.forEach((item) => { |
| | | item.sellingPrice = item.contractPrice; |
| | | }); |
| | | } |
| | | await gridApi.grid.reloadData(tableData.value); |
| | | }, |
| | | { |
| | |
| | | ); |
| | | |
| | | /** åå§å */ |
| | | const productOptions = ref<CrmProductApi.Product[]>([]); // 产å䏿é项 |
| | | const productOptions = ref<MdmItemApi.Item[]>([]); // ç©æä¸æé项 |
| | | onMounted(async () => { |
| | | productOptions.value = await getProductSimpleList(); |
| | | const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 }); |
| | | productOptions.value = res.list || []; |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Grid class="w-full"> |
| | | <template #productId="{ row }"> |
| | | <template #itemId="{ row }"> |
| | | <Select |
| | | v-model:value="row.productId" |
| | | v-model:value="row.itemId" |
| | | :options="productOptions" |
| | | :field-names="{ label: 'name', value: 'id' }" |
| | | class="w-full" |
| | | @change="handleProductChange($event, row)" |
| | | /> |
| | | </template> |
| | | <template #sellingPrice="{ row }"> |
| | | <template #contractPrice="{ row }"> |
| | | <InputNumber |
| | | v-model:value="row.sellingPrice" |
| | | v-model:value="row.contractPrice" |
| | | :min="0.001" |
| | | :precision="2" |
| | | @change="handlePriceChange(row)" |
| | |
| | | type: 'link', |
| | | danger: true, |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.name]), |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.itemName]), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { CrmReportApi } from '#/api/crm/report'; |
| | | |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | |
| | | import { Page } from '../../../packages/effects/common-ui/src'; |
| | | |
| | | import { Card, Col, DatePicker, Row, Statistic, Table } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { |
| | | getBusinessAnalyze, |
| | | getContractAnalyze, |
| | | getCustomerAnalyze, |
| | | getSalesFunnel, |
| | | getSalesPerformance, |
| | | } from '#/api/crm/report'; |
| | | |
| | | defineOptions({ name: 'CrmReportDashboard' }); |
| | | |
| | | const loading = ref(false); |
| | | const dateRange = ref<[string, string] | null>(null); |
| | | |
| | | const customerAnalyze = ref<CrmReportApi.CustomerAnalyze>(); |
| | | const businessAnalyze = ref<CrmReportApi.BusinessAnalyze>(); |
| | | const contractAnalyze = ref<CrmReportApi.ContractAnalyze>(); |
| | | const salesFunnel = ref<CrmReportApi.SalesFunnel>(); |
| | | const salesPerformance = ref<CrmReportApi.SalesPerformance>(); |
| | | |
| | | const startTime = computed(() => |
| | | dateRange.value?.[0] ? dayjs(dateRange.value[0]).format('YYYY-MM-DD') : undefined, |
| | | ); |
| | | const endTime = computed(() => |
| | | dateRange.value?.[1] ? dayjs(dateRange.value[1]).format('YYYY-MM-DD') : undefined, |
| | | ); |
| | | |
| | | const performanceColumns = [ |
| | | { title: 'å§å', dataIndex: 'userName', width: 120 }, |
| | | { title: 'é¨é¨', dataIndex: 'deptName', width: 150 }, |
| | | { title: 'ååæ°é', dataIndex: 'contractCount', width: 100 }, |
| | | { title: 'ååéé¢', dataIndex: 'contractPrice', width: 120, formatter: 'formatAmount2' }, |
| | | { title: '忬¾éé¢', dataIndex: 'receivablePrice', width: 120, formatter: 'formatAmount2' }, |
| | | { title: 'åæºæ°é', dataIndex: 'businessCount', width: 100 }, |
| | | { title: 'åæºéé¢', dataIndex: 'businessPrice', width: 120, formatter: 'formatAmount2' }, |
| | | ]; |
| | | |
| | | async function loadData() { |
| | | loading.value = true; |
| | | try { |
| | | const [customer, business, contract, funnel, performance] = await Promise.all([ |
| | | getCustomerAnalyze(startTime.value, endTime.value), |
| | | getBusinessAnalyze(startTime.value, endTime.value), |
| | | getContractAnalyze(startTime.value, endTime.value), |
| | | getSalesFunnel(), |
| | | getSalesPerformance(startTime.value, endTime.value), |
| | | ]); |
| | | customerAnalyze.value = customer; |
| | | businessAnalyze.value = business; |
| | | contractAnalyze.value = contract; |
| | | salesFunnel.value = funnel; |
| | | salesPerformance.value = performance; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | function handleDateChange() { |
| | | loadData(); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | loadData(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <div class="p-4"> |
| | | <Card title="CRM æ°æ®æ¦è§" class="mb-4"> |
| | | <div class="mb-4"> |
| | | <DatePicker.RangePicker |
| | | v-model:value="dateRange" |
| | | :placeholder="['å¼å§æ¥æ', 'ç»ææ¥æ']" |
| | | format="YYYY-MM-DD" |
| | | @change="handleDateChange" |
| | | /> |
| | | </div> |
| | | </Card> |
| | | |
| | | <Row :gutter="16" class="mb-4"> |
| | | <Col :span="6"> |
| | | <Card title="客æ·åæ" :loading="loading"> |
| | | <Statistic title="æ»å®¢æ·æ°" :value="customerAnalyze?.totalCount || 0" /> |
| | | <Statistic title="仿¥æ°å¢" :value="customerAnalyze?.todayCount || 0" class="mt-2" /> |
| | | <Statistic title="æäº¤å®¢æ·" :value="customerAnalyze?.dealCount || 0" class="mt-2" /> |
| | | <Statistic |
| | | title="æäº¤ç" |
| | | :value="customerAnalyze?.dealRate || 0" |
| | | :precision="2" |
| | | suffix="%" |
| | | class="mt-2" |
| | | /> |
| | | </Card> |
| | | </Col> |
| | | <Col :span="6"> |
| | | <Card title="åæºåæ" :loading="loading"> |
| | | <Statistic title="æ»åæºæ°" :value="businessAnalyze?.totalCount || 0" /> |
| | | <Statistic title="åæºéé¢" :value="businessAnalyze?.totalPrice || 0" :precision="2" class="mt-2" /> |
| | | <Statistic title="æååæº" :value="businessAnalyze?.successCount || 0" class="mt-2" /> |
| | | <Statistic |
| | | title="æåç" |
| | | :value="businessAnalyze?.successRate || 0" |
| | | :precision="2" |
| | | suffix="%" |
| | | class="mt-2" |
| | | /> |
| | | </Card> |
| | | </Col> |
| | | <Col :span="6"> |
| | | <Card title="åååæ" :loading="loading"> |
| | | <Statistic title="æ»ååæ°" :value="contractAnalyze?.totalCount || 0" /> |
| | | <Statistic title="ååéé¢" :value="contractAnalyze?.totalPrice || 0" :precision="2" class="mt-2" /> |
| | | <Statistic title="审æ¹ä¸" :value="contractAnalyze?.processCount || 0" class="mt-2" /> |
| | | <Statistic |
| | | title="忬¾ç" |
| | | :value="contractAnalyze?.receivableRate || 0" |
| | | :precision="2" |
| | | suffix="%" |
| | | class="mt-2" |
| | | /> |
| | | </Card> |
| | | </Col> |
| | | <Col :span="6"> |
| | | <Card title="å¾
å¤ç" :loading="loading"> |
| | | <Statistic title="客æ·å
¬æµ·" :value="customerAnalyze?.poolCount || 0" /> |
| | | <Statistic title="éå®å®¢æ·" :value="customerAnalyze?.lockCount || 0" class="mt-2" /> |
| | | <Statistic title="è¿è¡ä¸åæº" :value="businessAnalyze?.processingCount || 0" class="mt-2" /> |
| | | <Statistic title="å¤±è´¥åæº" :value="businessAnalyze?.failCount || 0" class="mt-2" /> |
| | | </Card> |
| | | </Col> |
| | | </Row> |
| | | |
| | | <Row :gutter="16"> |
| | | <Col :span="12"> |
| | | <Card title="é宿¼æ" :loading="loading"> |
| | | <Table |
| | | :dataSource="salesFunnel?.stages || []" |
| | | :columns="[ |
| | | { title: 'é¶æ®µ', dataIndex: 'name', width: 150 }, |
| | | { title: 'æ°é', dataIndex: 'count', width: 100 }, |
| | | { title: 'éé¢', dataIndex: 'price', width: 120 }, |
| | | { title: '转åç', dataIndex: 'conversionRate', width: 100, customRender: ({ text }) => `${text}%` }, |
| | | ]" |
| | | :pagination="false" |
| | | size="small" |
| | | rowKey="name" |
| | | /> |
| | | </Card> |
| | | </Col> |
| | | <Col :span="12"> |
| | | <Card title="éå®ä¸ç»©" :loading="loading"> |
| | | <Table |
| | | :dataSource="salesPerformance?.items || []" |
| | | :columns="performanceColumns" |
| | | :pagination="false" |
| | | size="small" |
| | | rowKey="userId" |
| | | /> |
| | | </Card> |
| | | </Col> |
| | | </Row> |
| | | </div> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | import { erpPriceMultiply } from '#/packages/utils/src'; |
| | | |
| | | 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'; |
| | | |
| | | /** æ¥ä»·åç¶æ */ |
| | | export const QUOTATION_STATUS = { |
| | | DRAFT: 0, // è稿 |
| | | CONVERTED: 10, // 已转åå |
| | | }; |
| | | |
| | | /** æ°å¢/ä¿®æ¹ç表å */ |
| | | export function useFormSchema(): VbenFormSchema[] { |
| | | const userStore = useUserStore(); |
| | | return [ |
| | | { |
| | | component: 'Input', |
| | | fieldName: 'id', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'no', |
| | | label: 'æ¥ä»·åå·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'ä¿åæ¶èªå¨çæ', |
| | | disabled: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'name', |
| | | label: 'æ¥ä»·ååç§°', |
| | | component: 'Input', |
| | | rules: 'required', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æ¥ä»·ååç§°', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'ownerUserId', |
| | | label: 'è´è´£äºº', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | api: getSimpleUserList, |
| | | labelField: 'nickname', |
| | | valueField: 'id', |
| | | }, |
| | | defaultValue: userStore.userInfo?.id, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'customerId', |
| | | label: '客æ·åç§°', |
| | | component: 'ApiSelect', |
| | | rules: 'required', |
| | | componentProps: { |
| | | api: getCustomerSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'businessId', |
| | | label: 'åæºåç§°', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: [], |
| | | placeholder: 'è¯·éæ©åæº', |
| | | allowClear: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['customerId'], |
| | | disabled: (values) => !values.customerId, |
| | | async componentProps(values) { |
| | | if (!values.customerId) { |
| | | return { options: [], placeholder: 'è¯·éæ©å®¢æ·' }; |
| | | } |
| | | const res = await getSimpleBusinessList(); |
| | | const list = res.filter((item) => item.customerId === values.customerId); |
| | | return { |
| | | options: list.map((item) => ({ label: item.name, value: item.id })), |
| | | placeholder: 'è¯·éæ©åæº', |
| | | }; |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'contactId', |
| | | label: 'è系人', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: [], |
| | | placeholder: 'è¯·éæ©è系人', |
| | | allowClear: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['customerId'], |
| | | disabled: (values) => !values.customerId, |
| | | async componentProps(values) { |
| | | if (!values.customerId) { |
| | | return { options: [], placeholder: 'è¯·éæ©å®¢æ·' }; |
| | | } |
| | | const res = await getSimpleContactList(); |
| | | const list = res.filter((item) => item.customerId === values.customerId); |
| | | return { |
| | | options: list.map((item) => ({ label: item.name, value: item.id })), |
| | | placeholder: 'è¯·éæ©è系人', |
| | | }; |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'quotationTime', |
| | | label: 'æ¥ä»·æ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | showTime: false, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'x', |
| | | placeholder: 'è¯·éæ©æ¥ä»·æ¥æ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'validUntil', |
| | | label: 'æææè³', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | showTime: false, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'x', |
| | | placeholder: 'è¯·éæ©æææ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'taxRate', |
| | | label: 'ç¨ç(%)', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | min: 0, |
| | | max: 100, |
| | | precision: 2, |
| | | placeholder: '请è¾å
¥ç¨ç', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | rows: 3, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'items', |
| | | label: 'ç©ææ¸
å', |
| | | component: 'Input', |
| | | formItemClass: 'col-span-3', |
| | | }, |
| | | { |
| | | fieldName: 'totalProductPrice', |
| | | label: 'ç©ææ»éé¢', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | min: 0, |
| | | precision: 2, |
| | | disabled: true, |
| | | }, |
| | | rules: z.number().min(0).optional().default(0), |
| | | }, |
| | | { |
| | | fieldName: 'discountPercent', |
| | | label: 'æ´åææ£ï¼%ï¼', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | min: 0, |
| | | precision: 2, |
| | | placeholder: '请è¾å
¥ææ£', |
| | | }, |
| | | rules: z.number().min(0).max(100).optional().default(0), |
| | | }, |
| | | { |
| | | fieldName: 'totalPrice', |
| | | label: 'ææ£åéé¢', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | min: 0, |
| | | precision: 2, |
| | | disabled: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['totalProductPrice', 'discountPercent'], |
| | | trigger(values, form) { |
| | | const discountPrice = |
| | | erpPriceMultiply(values.totalProductPrice, values.discountPercent / 100) ?? 0; |
| | | form.setFieldValue('totalPrice', values.totalProductPrice - discountPrice); |
| | | }, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'no', |
| | | label: 'æ¥ä»·åå·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æ¥ä»·åå·', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'name', |
| | | label: 'æ¥ä»·ååç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æ¥ä»·ååç§°', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'customerId', |
| | | label: '客æ·', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | api: getCustomerSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | allowClear: true, |
| | | options: [ |
| | | { label: 'è稿', value: 0 }, |
| | | { label: '已转åå', value: 10 }, |
| | | ], |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çåæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { |
| | | type: 'checkbox', |
| | | width: 50, |
| | | fixed: 'left', |
| | | }, |
| | | { |
| | | field: 'no', |
| | | title: 'æ¥ä»·åå·', |
| | | minWidth: 180, |
| | | fixed: 'left', |
| | | }, |
| | | { |
| | | field: 'name', |
| | | title: 'æ¥ä»·ååç§°', |
| | | minWidth: 160, |
| | | fixed: 'left', |
| | | }, |
| | | { |
| | | field: 'customerName', |
| | | title: '客æ·åç§°', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'businessName', |
| | | title: 'åæºåç§°', |
| | | minWidth: 130, |
| | | }, |
| | | { |
| | | field: 'totalPrice', |
| | | title: 'æ¥ä»·éé¢ï¼å
ï¼', |
| | | minWidth: 140, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | field: 'quotationTime', |
| | | title: 'æ¥ä»·æ¥æ', |
| | | minWidth: 120, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | field: 'validUntil', |
| | | title: 'æææè³', |
| | | minWidth: 120, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | minWidth: 100, |
| | | slots: { default: 'status' }, |
| | | }, |
| | | { |
| | | field: 'contractNo', |
| | | title: 'å
³èåå', |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | field: 'ownerUserName', |
| | | title: 'è´è´£äºº', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 260, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** ç©ææç»è¡¨æ ¼å */ |
| | | export function useItemColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { type: 'seq', title: 'åºå·', minWidth: 50, fixed: 'left' }, |
| | | { |
| | | field: 'itemId', |
| | | title: 'ç©æåç§°', |
| | | minWidth: 200, |
| | | slots: { default: 'itemId' }, |
| | | }, |
| | | { |
| | | field: 'itemSpecification', |
| | | title: 'è§æ ¼åå·', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'itemBarCode', |
| | | title: 'æ¡ç ', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'itemUnitName', |
| | | title: 'åä½', |
| | | minWidth: 80, |
| | | }, |
| | | { |
| | | field: 'itemUnitName2', |
| | | title: 'è¾
åä½1', |
| | | minWidth: 80, |
| | | }, |
| | | { |
| | | field: 'itemUnitName3', |
| | | title: 'è¾
åä½2', |
| | | minWidth: 80, |
| | | }, |
| | | { |
| | | field: 'itemPrice', |
| | | title: 'åä»·ï¼å
ï¼', |
| | | minWidth: 100, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | field: 'quotationPrice', |
| | | title: 'æ¥ä»·ï¼å
ï¼', |
| | | minWidth: 100, |
| | | slots: { default: 'quotationPrice' }, |
| | | }, |
| | | { |
| | | field: 'count', |
| | | title: 'æ°é', |
| | | minWidth: 100, |
| | | slots: { default: 'count' }, |
| | | }, |
| | | { |
| | | field: 'totalPrice', |
| | | title: 'å计', |
| | | minWidth: 100, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 50, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { CrmSaleQuotationApi } from '#/api/crm/saleQuotation'; |
| | | |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page, useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | |
| | | import { Button, message, Modal, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | convertSaleQuotationToContract, |
| | | deleteSaleQuotation, |
| | | getSaleQuotationPage, |
| | | } from '#/api/crm/saleQuotation'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { QUOTATION_STATUS, useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | const { push } = useRouter(); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** å建æ¥ä»·å */ |
| | | function handleCreate() { |
| | | formModalApi.setData(null).open(); |
| | | } |
| | | |
| | | /** ç¼è¾æ¥ä»·å */ |
| | | function handleEdit(row: CrmSaleQuotationApi.SaleQuotation) { |
| | | formModalApi.setData(row).open(); |
| | | } |
| | | |
| | | /** å 餿¥ä»·å */ |
| | | async function handleDelete(row: CrmSaleQuotationApi.SaleQuotation) { |
| | | const hideLoading = message.loading({ |
| | | content: $t('ui.actionMessage.deleting', [row.name]), |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await deleteSaleQuotation(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', [row.name])); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æ¥çæ¥ä»·å详æ
*/ |
| | | function handleDetail(row: CrmSaleQuotationApi.SaleQuotation) { |
| | | push({ name: 'CrmSaleQuotationDetail', params: { id: row.id } }); |
| | | } |
| | | |
| | | /** æ¥ç客æ·è¯¦æ
*/ |
| | | function handleCustomerDetail(row: CrmSaleQuotationApi.SaleQuotation) { |
| | | push({ name: 'CrmCustomerDetail', params: { id: row.customerId } }); |
| | | } |
| | | |
| | | /** æ¥çåæºè¯¦æ
*/ |
| | | function handleBusinessDetail(row: CrmSaleQuotationApi.SaleQuotation) { |
| | | if (row.businessId) { |
| | | push({ name: 'CrmBusinessDetail', params: { id: row.businessId } }); |
| | | } |
| | | } |
| | | |
| | | /** æ¥çåå详æ
*/ |
| | | function handleContractDetail(row: CrmSaleQuotationApi.SaleQuotation) { |
| | | if (row.contractId) { |
| | | push({ name: 'CrmContractDetail', params: { id: row.contractId } }); |
| | | } |
| | | } |
| | | |
| | | /** 转为åå */ |
| | | async function handleConvertContract(row: CrmSaleQuotationApi.SaleQuotation) { |
| | | Modal.confirm({ |
| | | title: '确认转åå', |
| | | content: '确认å°è¯¥æ¥ä»·å转为ååï¼', |
| | | async onOk() { |
| | | const contractId = await convertSaleQuotationToContract(row.id!); |
| | | message.success('转ååæå'); |
| | | push({ name: 'CrmContractDetail', params: { id: contractId } }); |
| | | }, |
| | | }); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getSaleQuotationPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<CrmSaleQuotationApi.SaleQuotation>, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <Grid> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['æ¥ä»·å']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['crm:sale-quotation:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #name="{ row }"> |
| | | <Button type="link" @click="handleDetail(row)"> |
| | | {{ row.name }} |
| | | </Button> |
| | | </template> |
| | | <template #customerName="{ row }"> |
| | | <Button type="link" @click="handleCustomerDetail(row)"> |
| | | {{ row.customerName }} |
| | | </Button> |
| | | </template> |
| | | <template #businessName="{ row }"> |
| | | <Button v-if="row.businessId" type="link" @click="handleBusinessDetail(row)"> |
| | | {{ row.businessName }} |
| | | </Button> |
| | | <span v-else>-</span> |
| | | </template> |
| | | <template #contractNo="{ row }"> |
| | | <Button v-if="row.contractId" type="link" @click="handleContractDetail(row)"> |
| | | {{ row.contractNo }} |
| | | </Button> |
| | | <span v-else>-</span> |
| | | </template> |
| | | <template #status="{ row }"> |
| | | <Tag v-if="row.status === QUOTATION_STATUS.DRAFT" color="default">è稿</Tag> |
| | | <Tag v-if="row.status === QUOTATION_STATUS.CONVERTED" color="success">已转åå</Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['crm:sale-quotation:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | ifShow: row.status === QUOTATION_STATUS.DRAFT, |
| | | }, |
| | | { |
| | | label: '转åå', |
| | | type: 'link', |
| | | auth: ['crm:sale-quotation:update'], |
| | | onClick: handleConvertContract.bind(null, row), |
| | | ifShow: row.status === QUOTATION_STATUS.DRAFT && !row.contractId, |
| | | }, |
| | | { |
| | | label: 'æ¥çåå', |
| | | type: 'link', |
| | | onClick: handleContractDetail.bind(null, row), |
| | | ifShow: !!row.contractId, |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | auth: ['crm:sale-quotation:delete'], |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.name]), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | ifShow: row.status === QUOTATION_STATUS.DRAFT, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { CrmSaleQuotationApi } from '#/api/crm/saleQuotation'; |
| | | |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import { erpPriceMultiply } from '#/packages/utils/src'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createSaleQuotation, |
| | | getSaleQuotation, |
| | | updateSaleQuotation, |
| | | } from '#/api/crm/saleQuotation'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import ItemForm from './item-form.vue'; |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formData = ref<CrmSaleQuotationApi.SaleQuotation>(); |
| | | const itemFormRef = ref(); |
| | | |
| | | const getTitle = computed(() => { |
| | | return formData.value?.id |
| | | ? $t('ui.actionTitle.edit', ['æ¥ä»·å']) |
| | | : $t('ui.actionTitle.create', ['æ¥ä»·å']); |
| | | }); |
| | | |
| | | const isEdit = computed(() => !!formData.value?.id); |
| | | const isDisabled = computed(() => isEdit.value && formData.value?.status !== 0); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | labelWidth: 120, |
| | | }, |
| | | wrapperClass: 'grid-cols-3', |
| | | layout: 'vertical', |
| | | schema: useFormSchema(), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | /** 计ç®ç©ææ»éé¢ */ |
| | | async function calculateTotalPrice() { |
| | | const items = itemFormRef.value?.getData() || []; |
| | | const totalProductPrice = items.reduce( |
| | | (prev: number, curr: any) => prev + (curr.totalPrice || 0), |
| | | 0, |
| | | ); |
| | | await formApi.setFieldValue('totalProductPrice', totalProductPrice); |
| | | const discountPercent = (await formApi.getValues())?.discountPercent || 0; |
| | | const discountPrice = erpPriceMultiply(totalProductPrice, discountPercent / 100) ?? 0; |
| | | await formApi.setFieldValue('totalPrice', totalProductPrice - discountPrice); |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | try { |
| | | await itemFormRef.value?.validate(); |
| | | } catch (e: any) { |
| | | message.error(e.message); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = (await formApi.getValues()) as CrmSaleQuotationApi.SaleQuotation; |
| | | data.items = itemFormRef.value?.getData(); |
| | | try { |
| | | await (formData.value?.id ? updateSaleQuotation(data) : createSaleQuotation(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<CrmSaleQuotationApi.SaleQuotation>(); |
| | | if (!data || !data.id) { |
| | | formData.value = {} as CrmSaleQuotationApi.SaleQuotation; |
| | | await formApi.resetForm(); |
| | | // æ°å¢æ¶ï¼é置并添å ä¸è¡ç©ºæ°æ® |
| | | itemFormRef.value?.resetData(); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getSaleQuotation(data.id); |
| | | await formApi.setValues(formData.value); |
| | | // ç¼è¾æ¶è®¾ç½®ç©ææ°æ® |
| | | itemFormRef.value?.setData(formData.value.items || []); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-[80%]"> |
| | | <Form class="mx-4"> |
| | | <template #items> |
| | | <ItemForm |
| | | ref="itemFormRef" |
| | | class="w-full" |
| | | :disabled="isDisabled" |
| | | @change="calculateTotalPrice" |
| | | /> |
| | | </template> |
| | | </Form> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MdmItemApi } from '#/api/mdm/item'; |
| | | import type { CrmSaleQuotationApi } from '#/api/crm/saleQuotation'; |
| | | |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | |
| | | import { erpPriceMultiply } from '#/packages/utils/src'; |
| | | |
| | | import { InputNumber, Select } from 'ant-design-vue'; |
| | | |
| | | import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getItemPage } from '#/api/mdm/item'; |
| | | |
| | | import { useItemColumns } from '../data'; |
| | | |
| | | interface Props { |
| | | disabled?: boolean; |
| | | } |
| | | |
| | | const props = withDefaults(defineProps<Props>(), { |
| | | disabled: false, |
| | | }); |
| | | |
| | | const emit = defineEmits(['change']); |
| | | |
| | | const tableData = ref<CrmSaleQuotationApi.SaleQuotationItem[]>([]); |
| | | const itemOptions = ref<MdmItemApi.Item[]>([]); |
| | | |
| | | /** è·åè¡¨æ ¼åè®¡æ°æ® */ |
| | | const summaries = computed(() => { |
| | | return tableData.value.reduce( |
| | | (acc, item) => { |
| | | acc.count += item.count || 0; |
| | | acc.totalPrice += item.totalPrice || 0; |
| | | return acc; |
| | | }, |
| | | { count: 0, totalPrice: 0 }, |
| | | ); |
| | | }); |
| | | |
| | | /** è¡¨æ ¼é
ç½® */ |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: useItemColumns(), |
| | | data: [], |
| | | minHeight: 250, |
| | | autoResize: true, |
| | | border: true, |
| | | rowConfig: { |
| | | keyField: 'seq', |
| | | isHover: true, |
| | | }, |
| | | pagerConfig: { |
| | | enabled: false, |
| | | }, |
| | | toolbarConfig: { |
| | | enabled: false, |
| | | }, |
| | | }, |
| | | }); |
| | | |
| | | /** 触ååæ´äºä»¶ */ |
| | | function triggerChange() { |
| | | emit('change'); |
| | | } |
| | | |
| | | /** å¤çæ°å¢ */ |
| | | function handleAdd() { |
| | | tableData.value.push({ |
| | | itemId: undefined, |
| | | itemName: undefined, |
| | | itemCode: undefined, |
| | | itemBarCode: undefined, |
| | | itemSpecification: undefined, |
| | | itemUnitName: undefined, |
| | | itemUnitName2: undefined, |
| | | itemUnitName3: undefined, |
| | | itemPrice: undefined, |
| | | quotationPrice: undefined, |
| | | count: 1, |
| | | totalPrice: 0, |
| | | }); |
| | | gridApi.grid.loadData(tableData.value); |
| | | } |
| | | |
| | | /** å¤çå é¤ */ |
| | | function handleDelete(row: CrmSaleQuotationApi.SaleQuotationItem) { |
| | | const index = tableData.value.findIndex((item) => item.seq === row.seq); |
| | | if (index !== -1) { |
| | | tableData.value.splice(index, 1); |
| | | gridApi.grid.loadData(tableData.value); |
| | | triggerChange(); |
| | | } |
| | | } |
| | | |
| | | /** å¤çç©æåæ´ */ |
| | | function handleItemChange(itemId: number, row: any) { |
| | | const item = itemOptions.value.find((p) => p.id === itemId); |
| | | if (!item) { |
| | | return; |
| | | } |
| | | row.itemId = itemId; |
| | | row.itemName = item.name; |
| | | row.itemCode = item.code; |
| | | row.itemBarCode = item.barCode; |
| | | row.itemSpecification = item.specification; |
| | | row.itemUnitName = item.unitMeasureName; |
| | | row.itemUnitName2 = item.unitMeasureName2; |
| | | row.itemUnitName3 = item.unitMeasureName3; |
| | | row.itemPrice = item.salesPrice; |
| | | row.quotationPrice = item.salesPrice; |
| | | row.totalPrice = erpPriceMultiply(row.quotationPrice, row.count) ?? 0; |
| | | triggerChange(); |
| | | } |
| | | |
| | | /** å¤çæ¥ä»·åæ´ */ |
| | | function handleQuotationPriceChange(value: number, row: any) { |
| | | row.quotationPrice = value; |
| | | row.totalPrice = erpPriceMultiply(value, row.count) ?? 0; |
| | | triggerChange(); |
| | | } |
| | | |
| | | /** å¤çæ°éåæ´ */ |
| | | function handleCountChange(value: number, row: any) { |
| | | row.count = value; |
| | | row.totalPrice = erpPriceMultiply(row.quotationPrice, value) ?? 0; |
| | | triggerChange(); |
| | | } |
| | | |
| | | /** è¡¨åæ ¡éª */ |
| | | function validate() { |
| | | for (let i = 0; i < tableData.value.length; i++) { |
| | | const item = tableData.value[i]; |
| | | if (item) { |
| | | if (!item.itemId) { |
| | | throw new Error(`第 ${i + 1} è¡ï¼ç©æä¸è½ä¸ºç©º`); |
| | | } |
| | | if (!item.count || item.count <= 0) { |
| | | throw new Error(`第 ${i + 1} è¡ï¼æ°éä¸è½ä¸ºç©º`); |
| | | } |
| | | if (!item.quotationPrice || item.quotationPrice <= 0) { |
| | | throw new Error(`第 ${i + 1} è¡ï¼æ¥ä»·ä¸è½ä¸ºç©º`); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** è®¾ç½®æ°æ®ï¼ä¾ç¶ç»ä»¶è°ç¨ï¼ */ |
| | | function setData(items: CrmSaleQuotationApi.SaleQuotationItem[]) { |
| | | items.forEach((item) => { |
| | | if (item.quotationPrice && item.count) { |
| | | item.totalPrice = erpPriceMultiply(item.quotationPrice, item.count) ?? 0; |
| | | } |
| | | }); |
| | | tableData.value = [...items]; |
| | | gridApi.grid.loadData(tableData.value); |
| | | } |
| | | |
| | | /** è·åæ°æ® */ |
| | | function getData() { |
| | | return [...tableData.value]; |
| | | } |
| | | |
| | | /** éç½®æ°æ® */ |
| | | function resetData() { |
| | | tableData.value = []; |
| | | handleAdd(); |
| | | } |
| | | |
| | | defineExpose({ |
| | | validate, |
| | | setData, |
| | | getData, |
| | | resetData, |
| | | }); |
| | | |
| | | /** åå§å */ |
| | | onMounted(async () => { |
| | | const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 }); |
| | | itemOptions.value = res.list || []; |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Grid class="w-full"> |
| | | <template #itemId="{ row }"> |
| | | <Select |
| | | v-if="!disabled" |
| | | v-model:value="row.itemId" |
| | | :options="itemOptions" |
| | | :field-names="{ label: 'name', value: 'id' }" |
| | | class="w-full" |
| | | placeholder="è¯·éæ©ç©æ" |
| | | @change="(val) => handleItemChange(val, row)" |
| | | /> |
| | | <span v-else>{{ row.itemName || '-' }}</span> |
| | | </template> |
| | | <template #quotationPrice="{ row }"> |
| | | <InputNumber |
| | | v-if="!disabled" |
| | | v-model:value="row.quotationPrice" |
| | | :min="0" |
| | | :precision="2" |
| | | @change="(val) => handleQuotationPriceChange(val, row)" |
| | | /> |
| | | <span v-else>{{ row.quotationPrice || '-' }}</span> |
| | | </template> |
| | | <template #count="{ row }"> |
| | | <InputNumber |
| | | v-if="!disabled" |
| | | v-model:value="row.count" |
| | | :min="0.001" |
| | | :precision="3" |
| | | @change="(val) => handleCountChange(val, row)" |
| | | /> |
| | | <span v-else>{{ row.count || '-' }}</span> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | v-if="!disabled" |
| | | :actions="[ |
| | | { |
| | | label: 'å é¤', |
| | | type: 'link', |
| | | danger: true, |
| | | popConfirm: { |
| | | title: '确认å é¤è¯¥ç©æåï¼', |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | |
| | | <template #bottom> |
| | | <div class="mt-2 rounded border border-border bg-muted p-2"> |
| | | <div class="flex justify-between text-sm text-muted-foreground"> |
| | | <span class="font-medium text-foreground">å计ï¼</span> |
| | | <div class="flex space-x-4"> |
| | | <span>æ°éï¼{{ summaries.count }}</span> |
| | | <span>éé¢ï¼{{ summaries.totalPrice.toFixed(2) }}å
</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <TableAction |
| | | v-if="!disabled" |
| | | class="mt-2 flex justify-center" |
| | | :actions="[ |
| | | { |
| | | label: 'æ·»å ç©æ', |
| | | type: 'default', |
| | | onClick: handleAdd, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </template> |
| | |
| | | |
| | | import { z } from '#/adapter/form'; |
| | | import { getAccountSimpleList } from '#/api/erp/finance/account'; |
| | | import { getProductSimpleList } from '#/api/erp/product/product'; |
| | | import { getCustomerSimpleList } from '#/api/erp/sale/customer'; |
| | | import { getItemPage } from '#/api/mdm/item'; |
| | | import { getCustomerSimpleList as getCrmCustomerSimpleList } from '#/api/crm/customer'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | | |
| | |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | allowClear: true, |
| | | showSearch: true, |
| | | api: getCustomerSimpleList, |
| | | api: getCrmCustomerSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | }, |
| | |
| | | slots: { default: 'productId' }, |
| | | }, |
| | | { |
| | | field: 'productSpecification', |
| | | title: 'è§æ ¼åå·', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'stockCount', |
| | | title: 'åºå', |
| | | minWidth: 80, |
| | |
| | | { |
| | | field: 'productUnitName', |
| | | title: 'åä½', |
| | | minWidth: 80, |
| | | }, |
| | | { |
| | | field: 'productUnitName2', |
| | | title: 'è¾
åä½1', |
| | | minWidth: 80, |
| | | }, |
| | | { |
| | | field: 'productUnitName3', |
| | | title: 'è¾
åä½2', |
| | | minWidth: 80, |
| | | }, |
| | | { |
| | |
| | | placeholder: 'è¯·éæ©äº§å', |
| | | allowClear: true, |
| | | showSearch: true, |
| | | api: getProductSimpleList, |
| | | api: async () => { |
| | | const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 }); |
| | | return res.list || []; |
| | | }, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | }, |
| | |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | allowClear: true, |
| | | showSearch: true, |
| | | api: getCustomerSimpleList, |
| | | api: getCrmCustomerSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | }, |
| | |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'contractNo', |
| | | title: 'å
³èåå', |
| | | minWidth: 150, |
| | | slots: { default: 'contractNo' }, |
| | | }, |
| | | { |
| | | field: 'orderTime', |
| | | title: 'è®¢åæ¶é´', |
| | | width: 160, |
| | |
| | | import type { ErpSaleOrderApi } from '#/api/erp/sale/order'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page, useVbenModal } from '../../../../packages/effects/common-ui/src'; |
| | | import { downloadFileFromBlobPart, isEmpty } from '../../../../packages/utils/src'; |
| | |
| | | |
| | | /** ERP éå®è®¢åå表 */ |
| | | defineOptions({ name: 'ErpSaleOrder' }); |
| | | |
| | | const router = useRouter(); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | |
| | | formModalApi.setData({ formType: 'detail', id: row.id }).open(); |
| | | } |
| | | |
| | | /** æ¥çå
³èåå */ |
| | | function handleViewContract(row: ErpSaleOrderApi.SaleOrder) { |
| | | if (row.contractId) { |
| | | router.push({ |
| | | path: '/crm/contract/detail', |
| | | query: { id: row.contractId }, |
| | | }); |
| | | } |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #contractNo="{ row }"> |
| | | <a v-if="row.contractId" class="text-primary cursor-pointer" @click="handleViewContract(row)"> |
| | | {{ row.contractNo }} |
| | | </a> |
| | | <span v-else>-</span> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | |
| | | <script lang="ts" setup> |
| | | import type { ErpProductApi } from '#/api/erp/product/product'; |
| | | import type { MdmItemApi } from '#/api/mdm/item'; |
| | | import type { ErpSaleOrderApi } from '#/api/erp/sale/order'; |
| | | |
| | | import { computed, nextTick, onMounted, ref, watch } from 'vue'; |
| | |
| | | import { Input, InputNumber, Select } from 'ant-design-vue'; |
| | | |
| | | import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getProductSimpleList } from '#/api/erp/product/product'; |
| | | import { getItemPage } from '#/api/mdm/item'; |
| | | import { getStockCount } from '#/api/erp/stock/stock'; |
| | | |
| | | import { useFormItemColumns } from '../data'; |
| | |
| | | ]); |
| | | |
| | | const tableData = ref<ErpSaleOrderApi.SaleOrderItem[]>([]); // è¡¨æ ¼æ°æ® |
| | | const productOptions = ref<ErpProductApi.Product[]>([]); // 产å䏿é项 |
| | | const productOptions = ref<MdmItemApi.Item[]>([]); // ç©æä¸æé项 |
| | | |
| | | /** è·åè¡¨æ ¼åè®¡æ°æ® */ |
| | | const summaries = computed(() => { |
| | |
| | | return; |
| | | } |
| | | row.productId = productId; |
| | | row.productUnitId = product.unitId; |
| | | row.productUnitId = product.unitMeasureId; |
| | | row.productBarCode = product.barCode; |
| | | row.productUnitName = product.unitName; |
| | | row.productUnitName = product.unitMeasureName; |
| | | row.productUnitName2 = product.unitMeasureName2; |
| | | row.productUnitName3 = product.unitMeasureName3; |
| | | row.productSpecification = product.specification; |
| | | row.productName = product.name; |
| | | row.stockCount = (await getStockCount(productId)) || 0; |
| | | row.productPrice = product.salePrice || 0; |
| | | row.productPrice = product.salesPrice || 0; |
| | | row.count = row.count || 1; |
| | | handleRowChange(row); |
| | | } |
| | |
| | | |
| | | /** åå§å */ |
| | | onMounted(async () => { |
| | | productOptions.value = await getProductSimpleList(); |
| | | const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 }); |
| | | productOptions.value = res.list || []; |
| | | // ç®çï¼æ°å¢æ¶ï¼é»è®¤æ·»å ä¸è¡ |
| | | if (tableData.value.length === 0) { |
| | | handleAdd(); |
| | |
| | | |
| | | import { z } from '#/adapter/form'; |
| | | import { getAccountSimpleList } from '#/api/erp/finance/account'; |
| | | import { getProductSimpleList } from '#/api/erp/product/product'; |
| | | import { getCustomerSimpleList } from '#/api/erp/sale/customer'; |
| | | import { getItemPage } from '#/api/mdm/item'; |
| | | import { getCustomerSimpleList as getCrmCustomerSimpleList } from '#/api/crm/customer'; |
| | | import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | allowClear: true, |
| | | showSearch: true, |
| | | api: getCustomerSimpleList, |
| | | api: getCrmCustomerSimpleList, |
| | | fieldNames: { |
| | | label: 'name', |
| | | value: 'id', |
| | |
| | | placeholder: 'è¯·éæ©äº§å', |
| | | allowClear: true, |
| | | showSearch: true, |
| | | api: getProductSimpleList, |
| | | fieldNames: { |
| | | label: 'name', |
| | | value: 'id', |
| | | api: async () => { |
| | | const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 }); |
| | | return res.list || []; |
| | | }, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | }, |
| | | }, |
| | | { |
| | |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | allowClear: true, |
| | | showSearch: true, |
| | | api: getCustomerSimpleList, |
| | | api: getCrmCustomerSimpleList, |
| | | fieldNames: { |
| | | label: 'name', |
| | | value: 'id', |
| | |
| | | placeholder: 'è¯·éæ©äº§å', |
| | | allowClear: true, |
| | | showSearch: true, |
| | | api: getProductSimpleList, |
| | | api: async () => { |
| | | const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 }); |
| | | return res.list || []; |
| | | }, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | }, |
| | |
| | | } |
| | | |
| | | /** å表çåæ®µ */ |
| | | |
| | | /** å表çåæ®µ */ |
| | | export function useOrderGridColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { |
| | |
| | | <script lang="ts" setup> |
| | | import type { MdmItemApi } from '#/api/mdm/item'; |
| | | import type { ErpSaleOutApi } from '#/api/erp/sale/out'; |
| | | |
| | | import { computed, nextTick, onMounted, ref, watch } from 'vue'; |
| | |
| | | import { Input, InputNumber, Select } from 'ant-design-vue'; |
| | | |
| | | import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getProductSimpleList } from '#/api/erp/product/product'; |
| | | import { getItemPage } from '#/api/mdm/item'; |
| | | import { getWarehouseStockCount } from '#/api/erp/stock/stock'; |
| | | import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse'; |
| | | |
| | |
| | | ]); |
| | | |
| | | const tableData = ref<ErpSaleOutApi.SaleOutItem[]>([]); // è¡¨æ ¼æ°æ® |
| | | const productOptions = ref<any[]>([]); // 产å䏿é项 |
| | | const productOptions = ref<MdmItemApi.Item[]>([]); // ç©æä¸æé项 |
| | | const warehouseOptions = ref<any[]>([]); // ä»åºä¸æé项 |
| | | |
| | | /** è·åè¡¨æ ¼åè®¡æ°æ® */ |
| | |
| | | |
| | | /** åå§å */ |
| | | onMounted(async () => { |
| | | productOptions.value = await getProductSimpleList(); |
| | | const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 }); |
| | | productOptions.value = res.list || []; |
| | | warehouseOptions.value = await getWarehouseSimpleList(); |
| | | }); |
| | | </script> |
| | |
| | | |
| | | import { z } from '#/adapter/form'; |
| | | import { getAccountSimpleList } from '#/api/erp/finance/account'; |
| | | import { getProductSimpleList } from '#/api/erp/product/product'; |
| | | import { getCustomerSimpleList } from '#/api/erp/sale/customer'; |
| | | import { getItemPage } from '#/api/mdm/item'; |
| | | import { getCustomerSimpleList as getCrmCustomerSimpleList } from '#/api/crm/customer'; |
| | | import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | allowClear: true, |
| | | showSearch: true, |
| | | api: getCustomerSimpleList, |
| | | api: getCrmCustomerSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | }, |
| | |
| | | placeholder: 'è¯·éæ©äº§å', |
| | | allowClear: true, |
| | | showSearch: true, |
| | | api: getProductSimpleList, |
| | | api: async () => { |
| | | const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 }); |
| | | return res.list || []; |
| | | }, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | }, |
| | |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | allowClear: true, |
| | | showSearch: true, |
| | | api: getCustomerSimpleList, |
| | | api: getCrmCustomerSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | }, |
| | |
| | | placeholder: 'è¯·éæ©äº§å', |
| | | allowClear: true, |
| | | showSearch: true, |
| | | api: getProductSimpleList, |
| | | api: async () => { |
| | | const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 }); |
| | | return res.list || []; |
| | | }, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | }, |
| | |
| | | <script lang="ts" setup> |
| | | import type { MdmItemApi } from '#/api/mdm/item'; |
| | | import type { ErpSaleReturnApi } from '#/api/erp/sale/return'; |
| | | |
| | | import { computed, nextTick, onMounted, ref, watch } from 'vue'; |
| | |
| | | import { Input, InputNumber, Select } from 'ant-design-vue'; |
| | | |
| | | import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getProductSimpleList } from '#/api/erp/product/product'; |
| | | import { getItemPage } from '#/api/mdm/item'; |
| | | import { getWarehouseStockCount } from '#/api/erp/stock/stock'; |
| | | import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse'; |
| | | |
| | |
| | | ]); |
| | | |
| | | const tableData = ref<ErpSaleReturnApi.SaleReturnItem[]>([]); // è¡¨æ ¼æ°æ® |
| | | const productOptions = ref<any[]>([]); // 产å䏿é项 |
| | | const productOptions = ref<MdmItemApi.Item[]>([]); // ç©æä¸æé项 |
| | | const warehouseOptions = ref<any[]>([]); // ä»åºä¸æé项 |
| | | |
| | | /** è·åè¡¨æ ¼åè®¡æ°æ® */ |
| | |
| | | |
| | | /** åå§å */ |
| | | onMounted(async () => { |
| | | productOptions.value = await getProductSimpleList(); |
| | | const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 }); |
| | | productOptions.value = res.list || []; |
| | | warehouseOptions.value = await getWarehouseSimpleList(); |
| | | }); |
| | | </script> |