diff --git a/apps/web-antd/src/api/crm/business/index.ts b/apps/web-antd/src/api/crm/business/index.ts index ec544b92d..a1ff09e6f 100644 --- a/apps/web-antd/src/api/crm/business/index.ts +++ b/apps/web-antd/src/api/crm/business/index.ts @@ -40,6 +40,7 @@ export namespace CrmBusinessApi { totalProductPrice: number; totalPrice: number; discountPercent: number; + status?: number; remark: string; creator: string; // 创建人 creatorName?: string; // 创建人名称 @@ -47,6 +48,12 @@ export namespace CrmBusinessApi { updateTime: Date; // 更新时间 products?: BusinessProduct[]; } + + export interface BusinessStatus { + id: number; + statusId: number | undefined; + endStatus: number | undefined; + } } /** 查询商机列表 */ @@ -90,7 +97,7 @@ export function updateBusiness(data: CrmBusinessApi.Business) { } /** 修改商机状态 */ -export function updateBusinessStatus(data: CrmBusinessApi.Business) { +export function updateBusinessStatus(data: CrmBusinessApi.BusinessStatus) { return requestClient.put('/crm/business/update-status', data); } diff --git a/apps/web-antd/src/api/crm/business/status/index.ts b/apps/web-antd/src/api/crm/business/status/index.ts index e1c8cfea3..9445938fd 100644 --- a/apps/web-antd/src/api/crm/business/status/index.ts +++ b/apps/web-antd/src/api/crm/business/status/index.ts @@ -4,46 +4,50 @@ import { requestClient } from '#/api/request'; export namespace CrmBusinessStatusApi { /** 商机状态信息 */ - export interface BusinessStatus { - id: number; - name: string; - percent: number; - } - - /** 商机状态组信息 */ export interface BusinessStatusType { id: number; name: string; - deptIds: number[]; - statuses?: BusinessStatus[]; + percent: number; + sort: number; } - /** 默认商机状态 */ - export const DEFAULT_STATUSES = [ - { - endStatus: 1, - key: '结束', - name: '赢单', - percent: 100, - }, - { - endStatus: 2, - key: '结束', - name: '输单', - percent: 0, - }, - { - endStatus: 3, - key: '结束', - name: '无效', - percent: 0, - }, - ] as const; + /** 商机状态组信息 */ + export interface BusinessStatus { + id: number; + name: string; + deptIds: number[]; + deptNames: string[]; + creator: string; + createTime: Date; + statuses?: BusinessStatusType[]; + } } +/** 默认商机状态 */ +export const DEFAULT_STATUSES = [ + { + endStatus: 1, + key: '结束', + name: '赢单', + percent: 100, + }, + { + endStatus: 2, + key: '结束', + name: '输单', + percent: 0, + }, + { + endStatus: 3, + key: '结束', + name: '无效', + percent: 0, + }, +]; + /** 查询商机状态组列表 */ export function getBusinessStatusPage(params: PageParam) { - return requestClient.get>( + return requestClient.get>( '/crm/business-status/page', { params }, ); @@ -51,21 +55,21 @@ export function getBusinessStatusPage(params: PageParam) { /** 新增商机状态组 */ export function createBusinessStatus( - data: CrmBusinessStatusApi.BusinessStatusType, + data: CrmBusinessStatusApi.BusinessStatus, ) { return requestClient.post('/crm/business-status/create', data); } /** 修改商机状态组 */ export function updateBusinessStatus( - data: CrmBusinessStatusApi.BusinessStatusType, + data: CrmBusinessStatusApi.BusinessStatus, ) { return requestClient.put('/crm/business-status/update', data); } /** 查询商机状态类型详情 */ export function getBusinessStatus(id: number) { - return requestClient.get( + return requestClient.get( `/crm/business-status/get?id=${id}`, ); } @@ -77,14 +81,14 @@ export function deleteBusinessStatus(id: number) { /** 获得商机状态组列表 */ export function getBusinessStatusTypeSimpleList() { - return requestClient.get( + return requestClient.get( '/crm/business-status/type-simple-list', ); } /** 获得商机阶段列表 */ export function getBusinessStatusSimpleList(typeId: number) { - return requestClient.get( + return requestClient.get( '/crm/business-status/status-simple-list', { params: { typeId } }, ); diff --git a/apps/web-antd/src/api/crm/clue/index.ts b/apps/web-antd/src/api/crm/clue/index.ts index 9d3447b88..a3a378b67 100644 --- a/apps/web-antd/src/api/crm/clue/index.ts +++ b/apps/web-antd/src/api/crm/clue/index.ts @@ -77,7 +77,7 @@ export function transferClue(data: CrmPermissionApi.TransferReq) { /** 线索转化为客户 */ export function transformClue(id: number) { - return requestClient.put('/crm/clue/transform', { id }); + return requestClient.put(`/crm/clue/transform?id=${id}`); } /** 获得分配给我的、待跟进的线索数量 */ diff --git a/apps/web-antd/src/api/crm/customer/index.ts b/apps/web-antd/src/api/crm/customer/index.ts index 3f7faaab5..611d56bc2 100644 --- a/apps/web-antd/src/api/crm/customer/index.ts +++ b/apps/web-antd/src/api/crm/customer/index.ts @@ -35,6 +35,11 @@ export namespace CrmCustomerApi { createTime: Date; // 创建时间 updateTime: Date; // 更新时间 } + export interface CustomerImport { + ownerUserId: number; + file: File; + updateSupport: boolean; + } } /** 查询客户列表 */ @@ -78,8 +83,8 @@ export function importCustomerTemplate() { } /** 导入客户 */ -export function importCustomer(file: File) { - return requestClient.upload('/crm/customer/import', { file }); +export function importCustomer(data: CrmCustomerApi.CustomerImport) { + return requestClient.upload('/crm/customer/import', data); } /** 获取客户精简信息列表 */ diff --git a/apps/web-antd/src/api/crm/receivable/index.ts b/apps/web-antd/src/api/crm/receivable/index.ts index 96936c910..8a10c7390 100644 --- a/apps/web-antd/src/api/crm/receivable/index.ts +++ b/apps/web-antd/src/api/crm/receivable/index.ts @@ -34,10 +34,21 @@ export namespace CrmReceivableApi { createTime: Date; // 创建时间 updateTime: Date; // 更新时间 } + + export interface ReceivablePageParam extends PageParam { + no?: string; + planId?: number; + customerId?: number; + contractId?: number; + sceneType?: number; + auditStatus?: number; + } } /** 查询回款列表 */ -export function getReceivablePage(params: PageParam) { +export function getReceivablePage( + params: CrmReceivableApi.ReceivablePageParam, +) { return requestClient.get>( '/crm/receivable/page', { params }, @@ -45,7 +56,9 @@ export function getReceivablePage(params: PageParam) { } /** 查询回款列表,基于指定客户 */ -export function getReceivablePageByCustomer(params: PageParam) { +export function getReceivablePageByCustomer( + params: CrmReceivableApi.ReceivablePageParam, +) { return requestClient.get>( '/crm/receivable/page-by-customer', { params }, diff --git a/apps/web-antd/src/api/crm/receivable/plan/index.ts b/apps/web-antd/src/api/crm/receivable/plan/index.ts index d237c1ed9..63e00f271 100644 --- a/apps/web-antd/src/api/crm/receivable/plan/index.ts +++ b/apps/web-antd/src/api/crm/receivable/plan/index.ts @@ -29,10 +29,20 @@ export namespace CrmReceivablePlanApi { returnTime: Date; }; } + + export interface PlanPageParam extends PageParam { + customerId?: number; + contractId?: number; + contractNo?: string; + sceneType?: number; + remindType?: number; + } } /** 查询回款计划列表 */ -export function getReceivablePlanPage(params: PageParam) { +export function getReceivablePlanPage( + params: CrmReceivablePlanApi.PlanPageParam, +) { return requestClient.get>( '/crm/receivable-plan/page', { params }, @@ -40,7 +50,9 @@ export function getReceivablePlanPage(params: PageParam) { } /** 查询回款计划列表(按客户) */ -export function getReceivablePlanPageByCustomer(params: PageParam) { +export function getReceivablePlanPageByCustomer( + params: CrmReceivablePlanApi.PlanPageParam, +) { return requestClient.get>( '/crm/receivable-plan/page-by-customer', { params }, diff --git a/apps/web-antd/src/api/infra/codegen/index.ts b/apps/web-antd/src/api/infra/codegen/index.ts index d8fea0453..1c2d97ba3 100644 --- a/apps/web-antd/src/api/infra/codegen/index.ts +++ b/apps/web-antd/src/api/infra/codegen/index.ts @@ -112,26 +112,19 @@ export function updateCodegenTable(data: InfraCodegenApi.CodegenUpdateReqVO) { /** 基于数据库的表结构,同步数据库的表和字段定义 */ export function syncCodegenFromDB(tableId: number) { - return requestClient.put('/infra/codegen/sync-from-db', { - params: { tableId }, - }); + return requestClient.put(`/infra/codegen/sync-from-db?tableId=${tableId}`); } /** 预览生成代码 */ export function previewCodegen(tableId: number) { return requestClient.get( - '/infra/codegen/preview', - { - params: { tableId }, - }, + `/infra/codegen/preview?tableId=${tableId}`, ); } /** 下载生成代码 */ export function downloadCodegen(tableId: number) { - return requestClient.download('/infra/codegen/download', { - params: { tableId }, - }); + return requestClient.download(`/infra/codegen/download?tableId=${tableId}`); } /** 获得表定义 */ diff --git a/apps/web-antd/src/api/infra/job/index.ts b/apps/web-antd/src/api/infra/job/index.ts index 3bd20fdb5..bacdbc4a1 100644 --- a/apps/web-antd/src/api/infra/job/index.ts +++ b/apps/web-antd/src/api/infra/job/index.ts @@ -57,7 +57,7 @@ export function updateJobStatus(id: number, status: number) { id, status, }; - return requestClient.put('/infra/job/update-status', { params }); + return requestClient.put('/infra/job/update-status', {}, { params }); } /** 定时任务立即执行一次 */ diff --git a/apps/web-antd/src/components/operate-log/index.ts b/apps/web-antd/src/components/operate-log/index.ts index cf38b5e70..a134eaaab 100644 --- a/apps/web-antd/src/components/operate-log/index.ts +++ b/apps/web-antd/src/components/operate-log/index.ts @@ -1,3 +1,9 @@ +import { defineAsyncComponent } from 'vue'; + +export const AsyncOperateLog = defineAsyncComponent( + () => import('./operate-log.vue'), +); + export { default as OperateLog } from './operate-log.vue'; export type { OperateLogProps } from './typing'; diff --git a/apps/web-antd/src/components/operate-log/operate-log.vue b/apps/web-antd/src/components/operate-log/operate-log.vue index e1e9eb448..2493bc5d5 100644 --- a/apps/web-antd/src/components/operate-log/operate-log.vue +++ b/apps/web-antd/src/components/operate-log/operate-log.vue @@ -1,7 +1,9 @@ + diff --git a/apps/web-antd/src/views/crm/backlog/index.vue b/apps/web-antd/src/views/crm/backlog/index.vue index 1173703e7..f1e45938f 100644 --- a/apps/web-antd/src/views/crm/backlog/index.vue +++ b/apps/web-antd/src/views/crm/backlog/index.vue @@ -21,8 +21,6 @@ import CustomerTodayContactList from './modules/customer-today-contact-list.vue' import ReceivableAuditList from './modules/receivable-audit-list.vue'; import ReceivablePlanRemindList from './modules/receivable-plan-remind-list.vue'; -defineOptions({ name: 'CrmBacklog' }); - const leftMenu = ref('customerTodayContact'); const clueFollowCount = ref(0); diff --git a/apps/web-antd/src/views/crm/business/data.ts b/apps/web-antd/src/views/crm/business/data.ts index bf65a743c..0cfd32130 100644 --- a/apps/web-antd/src/views/crm/business/data.ts +++ b/apps/web-antd/src/views/crm/business/data.ts @@ -1,6 +1,11 @@ import type { VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; +import { getBusinessStatusTypeSimpleList } from '#/api/crm/business/status'; +import { getCustomerSimpleList } from '#/api/crm/customer'; +import { getSimpleUserList } from '#/api/system/user'; +import { erpPriceMultiply } from '#/utils'; + /** 新增/修改的表单 */ export function useFormSchema(): VbenFormSchema[] { return [ @@ -19,18 +24,58 @@ export function useFormSchema(): VbenFormSchema[] { rules: 'required', }, { - fieldName: 'customerId', - label: '客户', - component: 'Input', + fieldName: 'ownerUserId', + label: '负责人', + component: 'ApiSelect', + componentProps: { + api: () => getSimpleUserList(), + fieldNames: { + label: 'nickname', + value: 'id', + }, + }, rules: 'required', }, { - fieldName: 'totalPrice', - label: '商机金额', - component: 'InputNumber', + fieldName: 'customerId', + label: '客户名称', + component: 'ApiSelect', componentProps: { - min: 0, - placeholder: '请输入商机金额', + api: () => getCustomerSimpleList(), + fieldNames: { + label: 'name', + value: 'id', + }, + }, + dependencies: { + triggerFields: ['id'], + disabled: (values) => !values.customerId, + }, + rules: 'required', + }, + { + fieldName: 'contactId', + label: '合同名称', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'statusTypeId', + label: '商机状态组', + component: 'ApiSelect', + componentProps: { + api: () => getBusinessStatusTypeSimpleList(), + fieldNames: { + label: 'name', + value: 'id', + }, + }, + dependencies: { + triggerFields: ['id'], + disabled: (values) => values.id, }, rules: 'required', }, @@ -46,9 +91,49 @@ export function useFormSchema(): VbenFormSchema[] { }, }, { - fieldName: 'remark', - label: '备注', - component: 'Textarea', + fieldName: 'product', + label: '产品清单', + component: 'Input', + formItemClass: 'col-span-3', + }, + { + fieldName: 'totalProductPrice', + label: '产品总金额', + component: 'InputNumber', + componentProps: { + min: 0, + }, + rules: 'required', + }, + { + fieldName: 'discountPercent', + label: '整单折扣(%)', + component: 'InputNumber', + componentProps: { + min: 0, + precision: 2, + }, + rules: 'required', + }, + { + fieldName: 'totalPrice', + label: '折扣后金额', + component: 'InputNumber', + dependencies: { + triggerFields: ['totalProductPrice', 'discountPercent'], + disabled: () => true, + trigger(values, form) { + const discountPrice = + erpPriceMultiply( + values.totalProductPrice, + values.discountPercent / 100, + ) ?? 0; + form.setFieldValue( + 'totalPrice', + values.totalProductPrice - discountPrice, + ); + }, + }, }, ]; } diff --git a/apps/web-antd/src/views/crm/business/index.ts b/apps/web-antd/src/views/crm/business/index.ts new file mode 100644 index 000000000..6ff97edf2 --- /dev/null +++ b/apps/web-antd/src/views/crm/business/index.ts @@ -0,0 +1,25 @@ +import { defineAsyncComponent } from 'vue'; + +export const BusinessForm = defineAsyncComponent( + () => import('./modules/form.vue'), +); + +export const BusinessDetailsInfo = defineAsyncComponent( + () => import('./modules/detail-info.vue'), +); + +export const BusinessDetailsList = defineAsyncComponent( + () => import('./modules/detail-list.vue'), +); + +export const BusinessDetails = defineAsyncComponent( + () => import('./modules/detail.vue'), +); + +export const BusinessDetailsListModal = defineAsyncComponent( + () => import('./modules/detail-list-modal.vue'), +); + +export const UpStatusForm = defineAsyncComponent( + () => import('./modules/up-status-form.vue'), +); diff --git a/apps/web-antd/src/views/crm/business/modules/detail-data.ts b/apps/web-antd/src/views/crm/business/modules/detail-data.ts new file mode 100644 index 000000000..cab0f65be --- /dev/null +++ b/apps/web-antd/src/views/crm/business/modules/detail-data.ts @@ -0,0 +1,126 @@ +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; +import type { DescriptionItemSchema } from '#/components/description'; + +import { formatDateTime } from '@vben/utils'; + +import { erpPriceInputFormatter } from '#/utils'; + +/** 详情页的字段 */ +export function useDetailSchema(): DescriptionItemSchema[] { + return [ + { + field: 'customerName', + label: '客户名称', + }, + { + field: 'totalPrice', + label: '商机金额(元)', + content: (data) => erpPriceInputFormatter(data.totalPrice), + }, + { + field: 'statusTypeName', + label: '商机组', + }, + { + field: 'ownerUserName', + label: '负责人', + }, + { + field: 'createTime', + label: '创建时间', + content: (data) => formatDateTime(data?.createTime) as string, + }, + ]; +} + +/** 详情页的基础字段 */ +export function useDetailBaseSchema(): DescriptionItemSchema[] { + return [ + { + field: 'name', + label: '商机名称', + }, + { + field: 'customerName', + label: '客户名称', + }, + { + field: 'totalPrice', + label: '商机金额(元)', + content: (data) => erpPriceInputFormatter(data.totalPrice), + }, + { + field: 'dealTime', + label: '预计成交日期', + content: (data) => formatDateTime(data?.dealTime) as string, + }, + { + field: 'contactNextTime', + label: '下次联系时间', + content: (data) => formatDateTime(data?.contactNextTime) as string, + }, + { + field: 'statusTypeName', + label: '商机状态组', + }, + { + field: 'statusName', + label: '商机阶段', + }, + { + field: 'remark', + label: '备注', + }, + ]; +} + +/** 详情列表的字段 */ +export function useDetailListColumns(): VxeTableGridOptions['columns'] { + return [ + { + type: 'checkbox', + width: 50, + fixed: 'left', + }, + { + field: 'name', + title: '商机名称', + fixed: 'left', + slots: { default: 'name' }, + }, + { + field: 'customerName', + title: '客户名称', + fixed: 'left', + slots: { default: 'customerName' }, + }, + { + field: 'totalPrice', + title: '商机金额(元)', + formatter: 'formatNumber', + }, + { + field: 'dealTime', + title: '预计成交日期', + formatter: 'formatDate', + }, + { + field: 'ownerUserName', + title: '负责人', + }, + { + field: 'ownerUserDeptName', + title: '所属部门', + }, + { + field: 'statusTypeName', + title: '商机状态组', + fixed: 'right', + }, + { + field: 'statusName', + title: '商机阶段', + fixed: 'right', + }, + ]; +} diff --git a/apps/web-antd/src/views/crm/business/modules/detail-info.vue b/apps/web-antd/src/views/crm/business/modules/detail-info.vue index da6c78661..64e4a9fe7 100644 --- a/apps/web-antd/src/views/crm/business/modules/detail-info.vue +++ b/apps/web-antd/src/views/crm/business/modules/detail-info.vue @@ -1,4 +1,42 @@ - + + diff --git a/apps/web-antd/src/views/crm/business/modules/detail-list-modal.vue b/apps/web-antd/src/views/crm/business/modules/detail-list-modal.vue new file mode 100644 index 000000000..902065437 --- /dev/null +++ b/apps/web-antd/src/views/crm/business/modules/detail-list-modal.vue @@ -0,0 +1,163 @@ + + + diff --git a/apps/web-antd/src/views/crm/business/modules/detail-list.vue b/apps/web-antd/src/views/crm/business/modules/detail-list.vue index b6466c322..81cbaa1ea 100644 --- a/apps/web-antd/src/views/crm/business/modules/detail-list.vue +++ b/apps/web-antd/src/views/crm/business/modules/detail-list.vue @@ -1,4 +1,206 @@ - + + diff --git a/apps/web-antd/src/views/crm/business/modules/detail.vue b/apps/web-antd/src/views/crm/business/modules/detail.vue index 99ad6b6f9..81ff5863d 100644 --- a/apps/web-antd/src/views/crm/business/modules/detail.vue +++ b/apps/web-antd/src/views/crm/business/modules/detail.vue @@ -1,7 +1,183 @@ - + diff --git a/apps/web-antd/src/views/crm/business/modules/form.vue b/apps/web-antd/src/views/crm/business/modules/form.vue index dd8dbd61b..3a25fb663 100644 --- a/apps/web-antd/src/views/crm/business/modules/form.vue +++ b/apps/web-antd/src/views/crm/business/modules/form.vue @@ -13,7 +13,10 @@ import { getBusiness, updateBusiness, } from '#/api/crm/business'; +import { BizTypeEnum } from '#/api/crm/permission'; import { $t } from '#/locales'; +import { erpPriceMultiply } from '#/utils'; +import { ProductEditTable } from '#/views/crm/product'; import { useFormSchema } from '../data'; @@ -25,15 +28,37 @@ const getTitle = computed(() => { : $t('ui.actionTitle.create', ['商机']); }); +function handleUpdateProducts(products: any) { + formData.value = modalApi.getData(); + formData.value!.products = products; + if (formData.value) { + const totalProductPrice = + formData.value.products?.reduce( + (prev, curr) => prev + curr.totalPrice, + 0, + ) ?? 0; + const discountPercent = formData.value.discountPercent; + const discountPrice = + discountPercent === null + ? 0 + : erpPriceMultiply(totalProductPrice, discountPercent / 100); + const totalPrice = totalProductPrice - (discountPrice ?? 0); + formData.value!.totalProductPrice = totalProductPrice; + formData.value!.totalPrice = totalPrice; + formApi.setValues(formData.value!); + } +} + const [Form, formApi] = useVbenForm({ commonConfig: { componentProps: { class: 'w-full', }, - formItemClass: 'col-span-2', labelWidth: 120, }, - layout: 'horizontal', + // 一共3列 + wrapperClass: 'grid-cols-3', + layout: 'vertical', schema: useFormSchema(), showDefaultActions: false, }); @@ -47,6 +72,7 @@ const [Modal, modalApi] = useVbenModal({ modalApi.lock(); // 提交表单 const data = (await formApi.getValues()) as CrmBusinessApi.Business; + data.products = formData.value?.products; try { await (formData.value?.id ? updateBusiness(data) : createBusiness(data)); // 关闭并提示 @@ -64,12 +90,12 @@ const [Modal, modalApi] = useVbenModal({ } // 加载数据 const data = modalApi.getData(); - if (!data || !data.id) { + if (!data) { return; } modalApi.lock(); try { - formData.value = await getBusiness(data.id as number); + formData.value = data.id ? await getBusiness(data.id as number) : data; // 设置到 values await formApi.setValues(formData.value); } finally { @@ -80,7 +106,17 @@ const [Modal, modalApi] = useVbenModal({ diff --git a/apps/web-antd/src/views/crm/business/modules/up-status-form.vue b/apps/web-antd/src/views/crm/business/modules/up-status-form.vue new file mode 100644 index 000000000..c8ea4300f --- /dev/null +++ b/apps/web-antd/src/views/crm/business/modules/up-status-form.vue @@ -0,0 +1,140 @@ + + + diff --git a/apps/web-antd/src/views/crm/business/status/modules/form.vue b/apps/web-antd/src/views/crm/business/status/modules/form.vue index aeb211723..0097e1693 100644 --- a/apps/web-antd/src/views/crm/business/status/modules/form.vue +++ b/apps/web-antd/src/views/crm/business/status/modules/form.vue @@ -18,7 +18,7 @@ import { $t } from '#/locales'; import { useFormSchema } from '../data'; const emit = defineEmits(['success']); -const formData = ref(); +const formData = ref(); const getTitle = computed(() => { return formData.value?.id ? $t('ui.actionTitle.edit', ['商机状态']) @@ -47,7 +47,7 @@ const [Modal, modalApi] = useVbenModal({ modalApi.lock(); // 提交表单 const data = - (await formApi.getValues()) as CrmBusinessStatusApi.BusinessStatusType; + (await formApi.getValues()) as CrmBusinessStatusApi.BusinessStatus; try { await (formData.value?.id ? updateBusinessStatus(data) @@ -66,7 +66,7 @@ const [Modal, modalApi] = useVbenModal({ return; } // 加载数据 - const data = modalApi.getData(); + const data = modalApi.getData(); if (!data || !data.id) { return; } diff --git a/apps/web-antd/src/views/crm/clue/data.ts b/apps/web-antd/src/views/crm/clue/data.ts index abc05ed4e..ef35115ba 100644 --- a/apps/web-antd/src/views/crm/clue/data.ts +++ b/apps/web-antd/src/views/crm/clue/data.ts @@ -1,13 +1,8 @@ import type { VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; -import type { DescriptionItemSchema } from '#/components/description'; - -import { h } from 'vue'; - -import { formatDateTime } from '@vben/utils'; import { getAreaTree } from '#/api/system/area'; -import { DictTag } from '#/components/dict-tag'; +import { getSimpleUserList } from '#/api/system/user'; import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils'; /** 新增/修改的表单 */ @@ -32,7 +27,7 @@ export function useFormSchema(): VbenFormSchema[] { label: '客户来源', component: 'Select', componentProps: { - options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE), + options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE, 'number'), }, rules: 'required', }, @@ -44,9 +39,12 @@ export function useFormSchema(): VbenFormSchema[] { { fieldName: 'ownerUserId', label: '负责人', - component: 'Select', + component: 'ApiSelect', componentProps: { - api: 'getSimpleUserList', + api: getSimpleUserList, + labelField: 'nickname', + valueField: 'id', + allowClear: true, }, rules: 'required', }, @@ -75,7 +73,7 @@ export function useFormSchema(): VbenFormSchema[] { label: '客户行业', component: 'Select', componentProps: { - options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY), + options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, 'number'), }, }, { @@ -83,7 +81,7 @@ export function useFormSchema(): VbenFormSchema[] { label: '客户级别', component: 'Select', componentProps: { - options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL), + options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL, 'number'), }, }, { @@ -250,142 +248,3 @@ export function useGridColumns(): VxeTableGridOptions['columns'] { }, ]; } - -/** 详情头部的配置 */ -export function useDetailSchema(): DescriptionItemSchema[] { - return [ - { - field: 'source', - label: '线索来源', - content: (data) => - h(DictTag, { - type: DICT_TYPE.CRM_CUSTOMER_SOURCE, - value: data?.source, - }), - }, - { - field: 'mobile', - label: '手机', - }, - { - field: 'ownerUserName', - label: '负责人', - }, - { - field: 'createTime', - label: '创建时间', - content: (data) => formatDateTime(data?.createTime) as string, - }, - ]; -} - -/** 详情基本信息的配置 */ -export function useDetailBaseSchema(): DescriptionItemSchema[] { - return [ - { - field: 'name', - label: '线索名称', - }, - { - field: 'source', - label: '客户来源', - content: (data) => - h(DictTag, { - type: DICT_TYPE.CRM_CUSTOMER_SOURCE, - value: data?.source, - }), - }, - { - field: 'mobile', - label: '手机', - }, - { - field: 'ownerUserName', - label: '负责人', - }, - { - field: 'telephone', - label: '电话', - }, - { - field: 'email', - label: '邮箱', - }, - { - field: 'wechat', - label: '微信', - }, - { - field: 'qq', - label: 'QQ', - }, - { - field: 'industryId', - label: '客户行业', - content: (data) => - h(DictTag, { - type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY, - value: data?.industryId, - }), - }, - { - field: 'level', - label: '客户级别', - content: (data) => - h(DictTag, { - type: DICT_TYPE.CRM_CUSTOMER_LEVEL, - value: data?.level, - }), - }, - { - field: 'areaId', - label: '地址', - }, - { - field: 'detailAddress', - label: '详细地址', - }, - { - field: 'contactNextTime', - label: '下次联系时间', - content: (data) => formatDateTime(data?.contactNextTime) as string, - }, - { - field: 'remark', - label: '备注', - }, - ]; -} - -/** 详情系统信息的配置 */ -export function useDetailSystemSchema(): DescriptionItemSchema[] { - return [ - { - field: 'ownerUserName', - label: '负责人', - }, - { - field: 'contactLastContent', - label: '最后跟进记录', - }, - { - field: 'contactLastContent', - label: '最后跟进时间', - content: (data) => formatDateTime(data?.contactLastContent) as string, - }, - { - field: 'creatorName', - label: '创建人', - }, - { - field: 'createTime', - label: '创建时间', - content: (data) => formatDateTime(data?.createTime) as string, - }, - { - field: 'updateTime', - label: '更新时间', - content: (data) => formatDateTime(data?.updateTime) as string, - }, - ]; -} diff --git a/apps/web-antd/src/views/crm/clue/modules/detail-data.ts b/apps/web-antd/src/views/crm/clue/modules/detail-data.ts new file mode 100644 index 000000000..73102e9b1 --- /dev/null +++ b/apps/web-antd/src/views/crm/clue/modules/detail-data.ts @@ -0,0 +1,107 @@ +import type { DescriptionItemSchema } from '#/components/description'; + +import { h } from 'vue'; + +import { formatDateTime } from '@vben/utils'; + +import { DictTag } from '#/components/dict-tag'; +import { DICT_TYPE } from '#/utils'; + +/** 详情头部的配置 */ +export function useDetailSchema(): DescriptionItemSchema[] { + return [ + { + field: 'source', + label: '线索来源', + content: (data) => + h(DictTag, { + type: DICT_TYPE.CRM_CUSTOMER_SOURCE, + value: data?.source, + }), + }, + { + field: 'mobile', + label: '手机', + }, + { + field: 'ownerUserName', + label: '负责人', + }, + { + field: 'createTime', + label: '创建时间', + content: (data) => formatDateTime(data?.createTime) as string, + }, + ]; +} + +/** 详情基本信息的配置 */ +export function useDetailBaseSchema(): DescriptionItemSchema[] { + return [ + { + field: 'name', + label: '线索名称', + }, + { + field: 'source', + label: '客户来源', + content: (data) => + h(DictTag, { + type: DICT_TYPE.CRM_CUSTOMER_SOURCE, + value: data?.source, + }), + }, + { + field: 'mobile', + label: '手机', + }, + { + field: 'telephone', + label: '电话', + }, + { + field: 'email', + label: '邮箱', + }, + { + field: 'areaName', + label: '地址', + content: (data) => data?.areaName + data?.detailAddress, + }, + { + field: 'qq', + label: 'QQ', + }, + { + field: 'wechat', + label: '微信', + }, + { + field: 'industryId', + label: '客户行业', + content: (data) => + h(DictTag, { + type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY, + value: data?.industryId, + }), + }, + { + field: 'level', + label: '客户级别', + content: (data) => + h(DictTag, { + type: DICT_TYPE.CRM_CUSTOMER_LEVEL, + value: data?.level, + }), + }, + { + field: 'contactNextTime', + label: '下次联系时间', + content: (data) => formatDateTime(data?.contactNextTime) as string, + }, + { + field: 'remark', + label: '备注', + }, + ]; +} diff --git a/apps/web-antd/src/views/crm/clue/modules/detail-info.vue b/apps/web-antd/src/views/crm/clue/modules/detail-info.vue index 36784a1df..fc9dbe3b4 100644 --- a/apps/web-antd/src/views/crm/clue/modules/detail-info.vue +++ b/apps/web-antd/src/views/crm/clue/modules/detail-info.vue @@ -4,12 +4,11 @@ import type { CrmClueApi } from '#/api/crm/clue'; import { Divider } from 'ant-design-vue'; import { useDescription } from '#/components/description'; +import { useFollowUpDetailSchema } from '#/views/crm/followup/data'; -import { useDetailBaseSchema, useDetailSystemSchema } from '../data'; +import { useDetailBaseSchema } from './detail-data'; -defineOptions({ name: 'CrmClueDetailsInfo' }); - -const { clue } = defineProps<{ +defineProps<{ clue: CrmClueApi.Clue; // 线索信息 }>(); @@ -21,7 +20,6 @@ const [BaseDescription] = useDescription({ class: 'mx-4', }, schema: useDetailBaseSchema(), - data: clue, }); const [SystemDescription] = useDescription({ @@ -31,15 +29,14 @@ const [SystemDescription] = useDescription({ column: 3, class: 'mx-4', }, - schema: useDetailSystemSchema(), - data: clue, + schema: useFollowUpDetailSchema(), }); diff --git a/apps/web-antd/src/views/crm/clue/modules/detail.vue b/apps/web-antd/src/views/crm/clue/modules/detail.vue index 7c9d29b2a..b2415bab5 100644 --- a/apps/web-antd/src/views/crm/clue/modules/detail.vue +++ b/apps/web-antd/src/views/crm/clue/modules/detail.vue @@ -1,21 +1,25 @@ diff --git a/apps/web-antd/src/views/crm/contact/data.ts b/apps/web-antd/src/views/crm/contact/data.ts index 956f32333..31dee6a36 100644 --- a/apps/web-antd/src/views/crm/contact/data.ts +++ b/apps/web-antd/src/views/crm/contact/data.ts @@ -1,16 +1,10 @@ import type { VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; -import type { DescriptionItemSchema } from '#/components/description'; - -import { h } from 'vue'; - -import { formatDateTime } from '@vben/utils'; import { getSimpleContactList } from '#/api/crm/contact'; import { getCustomerSimpleList } from '#/api/crm/customer'; import { getAreaTree } from '#/api/system/area'; import { getSimpleUserList } from '#/api/system/user'; -import { DictTag } from '#/components/dict-tag'; import { DICT_TYPE, getDictOptions } from '#/utils'; /** 新增/修改的表单 */ @@ -278,108 +272,3 @@ export function useGridColumns(): VxeTableGridOptions['columns'] { }, ]; } - -/** 详情页的字段 */ -export function useDetailSchema(): DescriptionItemSchema[] { - return [...useDetailBaseSchema(), ...useDetailSystemSchema()]; -} - -/** 详情页的基础字段 */ -export function useDetailBaseSchema(): DescriptionItemSchema[] { - return [ - { - field: 'name', - label: '客户名称', - }, - { - field: 'source', - label: '客户来源', - content: (data) => - h(DictTag, { - type: DICT_TYPE.CRM_CUSTOMER_SOURCE, - value: data?.source, - }), - }, - { - field: 'mobile', - label: '手机', - }, - { - field: 'telephone', - label: '电话', - }, - { - field: 'email', - label: '邮箱', - }, - { - field: 'wechat', - label: '微信', - }, - { - field: 'qq', - label: 'QQ', - }, - { - field: 'industryId', - label: '客户行业', - content: (data) => - h(DictTag, { - type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY, - value: data?.industryId, - }), - }, - { - field: 'level', - label: '客户级别', - content: (data) => - h(DictTag, { type: DICT_TYPE.CRM_CUSTOMER_LEVEL, value: data?.level }), - }, - { - field: 'areaName', - label: '地址', - }, - { - field: 'detailAddress', - label: '详细地址', - }, - { - field: 'contactNextTime', - label: '下次联系时间', - content: (data) => formatDateTime(data?.contactNextTime) as string, - }, - { - field: 'remark', - label: '备注', - }, - ]; -} - -/** 详情页的系统字段 */ -export function useDetailSystemSchema(): DescriptionItemSchema[] { - return [ - { - field: 'ownerUserName', - label: '负责人', - }, - { - field: 'ownerUserDeptName', - label: '所属部门', - }, - { - field: 'contactLastTime', - label: '最后跟进时间', - content: (data) => formatDateTime(data?.contactLastTime) as string, - }, - { - field: 'createTime', - label: '创建时间', - content: (data) => formatDateTime(data?.createTime) as string, - }, - { - field: 'updateTime', - label: '更新时间', - content: (data) => formatDateTime(data?.updateTime) as string, - }, - ]; -} diff --git a/apps/web-antd/src/views/crm/contact/index.ts b/apps/web-antd/src/views/crm/contact/index.ts new file mode 100644 index 000000000..e19127cf3 --- /dev/null +++ b/apps/web-antd/src/views/crm/contact/index.ts @@ -0,0 +1,17 @@ +import { defineAsyncComponent } from 'vue'; + +export const ContactDetailsInfo = defineAsyncComponent( + () => import('./modules/detail-info.vue'), +); + +export const ContactForm = defineAsyncComponent( + () => import('./modules/form.vue'), +); + +export const ContactDetails = defineAsyncComponent( + () => import('./modules/detail.vue'), +); + +export const ContactDetailsList = defineAsyncComponent( + () => import('./modules/detail-list.vue'), +); diff --git a/apps/web-antd/src/views/crm/contact/modules/detail-data.ts b/apps/web-antd/src/views/crm/contact/modules/detail-data.ts new file mode 100644 index 000000000..e4de3e2ff --- /dev/null +++ b/apps/web-antd/src/views/crm/contact/modules/detail-data.ts @@ -0,0 +1,165 @@ +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; +import type { DescriptionItemSchema } from '#/components/description'; + +import { h } from 'vue'; + +import { formatDateTime } from '@vben/utils'; + +import { DictTag } from '#/components/dict-tag'; +import { DICT_TYPE } from '#/utils'; + +/** 详情页的基础字段 */ +export function useDetailSchema(): DescriptionItemSchema[] { + return [ + { + field: 'name', + label: '客户名称', + }, + { + field: 'post', + label: '职务', + }, + { + field: 'mobile', + label: '手机', + }, + { + field: 'createTime', + label: '下次联系时间', + content: (data) => formatDateTime(data?.createTime) as string, + }, + ]; +} + +/** 详情页的基础字段 */ +export function useDetailBaseSchema(): DescriptionItemSchema[] { + return [ + { + field: 'name', + label: '姓名', + }, + { + field: 'customerName', + label: '客户名称', + }, + { + field: 'mobile', + label: '手机', + }, + { + field: 'telephone', + label: '电话', + }, + { + field: 'email', + label: '邮箱', + }, + { + field: 'qq', + label: 'QQ', + }, + { + field: 'wechat', + label: '微信', + }, + { + field: 'areaName', + label: '地址', + }, + { + field: 'detailAddress', + label: '详细地址', + }, + { + field: 'post', + label: '职务', + }, + { + field: 'parentName', + label: '直属上级', + }, + { + field: 'master', + label: '关键决策人', + content: (data) => + h(DictTag, { + type: DICT_TYPE.INFRA_BOOLEAN_STRING, + value: data?.master, + }), + }, + { + field: 'sex', + label: '性别', + content: (data) => + h(DictTag, { type: DICT_TYPE.SYSTEM_USER_SEX, value: data?.sex }), + }, + { + field: 'contactNextTime', + label: '下次联系时间', + content: (data) => formatDateTime(data?.contactNextTime) as string, + }, + { + field: 'remark', + label: '备注', + }, + ]; +} + +/** 详情列表的字段 */ +export function useDetailListColumns(): VxeTableGridOptions['columns'] { + return [ + { + type: 'checkbox', + width: 50, + fixed: 'left', + }, + { + field: 'name', + title: '姓名', + fixed: 'left', + slots: { default: 'name' }, + }, + { + field: 'customerName', + title: '客户名称', + fixed: 'left', + slots: { default: 'customerName' }, + }, + { + field: 'sex', + title: '性别', + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.SYSTEM_USER_SEX }, + }, + }, + { + field: 'mobile', + title: '手机', + }, + { + field: 'telephone', + title: '电话', + }, + { + field: 'email', + title: '邮箱', + }, + { + field: 'post', + title: '职位', + }, + { + field: 'detailAddress', + title: '地址', + }, + { + field: 'master', + title: '关键决策人', + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING }, + }, + }, + ]; +} diff --git a/apps/web-antd/src/views/crm/contact/modules/detail-info.vue b/apps/web-antd/src/views/crm/contact/modules/detail-info.vue index a9a8f4cb1..9e71bd593 100644 --- a/apps/web-antd/src/views/crm/contact/modules/detail-info.vue +++ b/apps/web-antd/src/views/crm/contact/modules/detail-info.vue @@ -1,4 +1,42 @@ - + + diff --git a/apps/web-antd/src/views/crm/contact/modules/detail-list-modal.vue b/apps/web-antd/src/views/crm/contact/modules/detail-list-modal.vue new file mode 100644 index 000000000..8fc58057f --- /dev/null +++ b/apps/web-antd/src/views/crm/contact/modules/detail-list-modal.vue @@ -0,0 +1,163 @@ + + + diff --git a/apps/web-antd/src/views/crm/contact/modules/detail-list.vue b/apps/web-antd/src/views/crm/contact/modules/detail-list.vue index 1f5fe358f..2ad66d514 100644 --- a/apps/web-antd/src/views/crm/contact/modules/detail-list.vue +++ b/apps/web-antd/src/views/crm/contact/modules/detail-list.vue @@ -1,4 +1,205 @@ - + + diff --git a/apps/web-antd/src/views/crm/contact/modules/detail.vue b/apps/web-antd/src/views/crm/contact/modules/detail.vue index 99ad6b6f9..9aeb21eda 100644 --- a/apps/web-antd/src/views/crm/contact/modules/detail.vue +++ b/apps/web-antd/src/views/crm/contact/modules/detail.vue @@ -1,7 +1,149 @@ - + diff --git a/apps/web-antd/src/views/crm/contract/data.ts b/apps/web-antd/src/views/crm/contract/data.ts index 79118dd3f..2c20e7a71 100644 --- a/apps/web-antd/src/views/crm/contract/data.ts +++ b/apps/web-antd/src/views/crm/contract/data.ts @@ -1,22 +1,32 @@ import type { VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; +import { z } from '#/adapter/form'; import { getSimpleBusinessList } from '#/api/crm/business'; import { getSimpleContactList } from '#/api/crm/contact'; import { getCustomerSimpleList } from '#/api/crm/customer'; -import { floatToFixed2 } from '#/utils'; +import { getSimpleUserList } from '#/api/system/user'; +import { erpPriceMultiply, floatToFixed2 } from '#/utils'; import { DICT_TYPE } from '#/utils/dict'; /** 新增/修改的表单 */ export function useFormSchema(): VbenFormSchema[] { return [ + { + fieldName: 'id', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, { fieldName: 'no', label: '合同编号', component: 'Input', - rules: 'required', componentProps: { - placeholder: '请输入合同编号', + placeholder: '保存时自动生成', + disabled: () => true, }, }, { @@ -28,9 +38,22 @@ export function useFormSchema(): VbenFormSchema[] { placeholder: '请输入合同名称', }, }, + { + fieldName: 'ownerUserId', + label: '负责人', + component: 'ApiSelect', + componentProps: { + api: () => getSimpleUserList(), + fieldNames: { + label: 'nickname', + value: 'id', + }, + }, + rules: 'required', + }, { fieldName: 'customerId', - label: '客户', + label: '客户名称', component: 'ApiSelect', rules: 'required', componentProps: { @@ -42,9 +65,8 @@ export function useFormSchema(): VbenFormSchema[] { }, { fieldName: 'businessId', - label: '商机', + label: '商机名称', component: 'ApiSelect', - rules: 'required', componentProps: { api: getSimpleBusinessList, labelField: 'name', @@ -52,49 +74,53 @@ export function useFormSchema(): VbenFormSchema[] { placeholder: '请选择商机', }, }, - { - fieldName: 'totalPrice', - label: '合同金额', - component: 'InputNumber', - rules: 'required', - componentProps: { - placeholder: '请输入合同金额', - min: 0, - precision: 2, - }, - }, { fieldName: 'orderDate', - label: '下单时间', + label: '下单日期', component: 'DatePicker', rules: 'required', componentProps: { - placeholder: '请选择下单时间', + showTime: false, + format: 'YYYY-MM-DD', + valueFormat: 'x', }, }, { fieldName: 'startTime', label: '合同开始时间', component: 'DatePicker', - rules: 'required', componentProps: { - placeholder: '请选择合同开始时间', + showTime: false, + format: 'YYYY-MM-DD', + valueFormat: 'x', }, }, { fieldName: 'endTime', label: '合同结束时间', component: 'DatePicker', - rules: 'required', componentProps: { - placeholder: '请选择合同结束时间', + showTime: false, + format: 'YYYY-MM-DD', + valueFormat: 'x', + }, + }, + { + fieldName: 'signUserId', + label: '公司签约人', + component: 'ApiSelect', + componentProps: { + api: () => getSimpleUserList(), + fieldNames: { + label: 'nickname', + value: 'id', + }, }, }, { fieldName: 'signContactId', label: '客户签约人', component: 'ApiSelect', - rules: 'required', componentProps: { api: getSimpleContactList, labelField: 'name', @@ -111,6 +137,50 @@ export function useFormSchema(): VbenFormSchema[] { rows: 4, }, }, + { + fieldName: 'product', + label: '产品清单', + component: 'Input', + formItemClass: 'col-span-3', + }, + { + fieldName: 'totalProductPrice', + label: '产品总金额', + component: 'InputNumber', + componentProps: { + min: 0, + }, + }, + { + fieldName: 'discountPercent', + label: '整单折扣(%)', + component: 'InputNumber', + rules: z.number().min(0).max(100).default(0), + componentProps: { + min: 0, + precision: 2, + }, + }, + { + fieldName: 'totalPrice', + label: '折扣后金额', + component: 'InputNumber', + dependencies: { + triggerFields: ['totalProductPrice', 'discountPercent'], + disabled: () => true, + trigger(values, form) { + const discountPrice = + erpPriceMultiply( + values.totalProductPrice, + values.discountPercent / 100, + ) ?? 0; + form.setFieldValue( + 'totalPrice', + values.totalProductPrice - discountPrice, + ); + }, + }, + }, ]; } diff --git a/apps/web-antd/src/views/crm/contract/index.ts b/apps/web-antd/src/views/crm/contract/index.ts new file mode 100644 index 000000000..7306f3e20 --- /dev/null +++ b/apps/web-antd/src/views/crm/contract/index.ts @@ -0,0 +1,17 @@ +import { defineAsyncComponent } from 'vue'; + +export const ContractDetailsInfo = defineAsyncComponent( + () => import('./modules/detail-info.vue'), +); + +export const ContractForm = defineAsyncComponent( + () => import('./modules/form.vue'), +); + +export const ContractDetails = defineAsyncComponent( + () => import('./modules/detail.vue'), +); + +export const ContractDetailsList = defineAsyncComponent( + () => import('./modules/detail-list.vue'), +); diff --git a/apps/web-antd/src/views/crm/contract/modules/detail-data.ts b/apps/web-antd/src/views/crm/contract/modules/detail-data.ts new file mode 100644 index 000000000..cffb31427 --- /dev/null +++ b/apps/web-antd/src/views/crm/contract/modules/detail-data.ts @@ -0,0 +1,188 @@ +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; +import type { DescriptionItemSchema } from '#/components/description'; + +import { h } from 'vue'; + +import { formatDateTime } from '@vben/utils'; + +import { DictTag } from '#/components/dict-tag'; +import { DICT_TYPE, erpPriceInputFormatter, floatToFixed2 } from '#/utils'; + +/** 详情头部的配置 */ +export function useDetailSchema(): DescriptionItemSchema[] { + return [ + { + field: 'customerName', + label: '客户名称', + }, + { + field: 'totalPrice', + label: '合同金额(元)', + content: (data) => erpPriceInputFormatter(data?.totalPrice) as string, + }, + { + field: 'orderDate', + label: '下单时间', + content: (data) => formatDateTime(data?.orderDate) as string, + }, + { + field: 'totalReceivablePrice', + label: '回款金额(元)', + content: (data) => + erpPriceInputFormatter(data?.totalReceivablePrice) as string, + }, + { + field: 'ownerUserName', + label: '负责人', + }, + ]; +} + +/** 详情基本信息的配置 */ +export function useDetailBaseSchema(): DescriptionItemSchema[] { + return [ + { + field: 'no', + label: '合同编号', + }, + { + field: 'name', + label: '合同名称', + }, + { + field: 'customerName', + label: '客户名称', + }, + { + field: 'businessName', + label: '商机名称', + }, + { + field: 'totalPrice', + label: '合同金额(元)', + content: (data) => erpPriceInputFormatter(data?.totalPrice) as string, + }, + { + field: 'orderDate', + label: '下单时间', + content: (data) => formatDateTime(data?.orderDate) as string, + }, + { + field: 'startTime', + label: '合同开始时间', + content: (data) => formatDateTime(data?.startTime) as string, + }, + { + field: 'endTime', + label: '合同结束时间', + content: (data) => formatDateTime(data?.endTime) as string, + }, + { + field: 'signContactName', + label: '客户签约人', + }, + { + field: 'signUserName', + label: '公司签约人', + }, + { + field: 'remark', + label: '备注', + }, + { + field: 'auditStatus', + label: '合同状态', + content: (data) => + h(DictTag, { + type: DICT_TYPE.CRM_AUDIT_STATUS, + value: data?.auditStatus, + }), + }, + ]; +} + +export function useDetailListColumns(): VxeTableGridOptions['columns'] { + return [ + { + title: '合同编号', + field: 'no', + minWidth: 150, + fixed: 'left', + }, + { + title: '合同名称', + field: 'name', + minWidth: 150, + fixed: 'left', + slots: { default: 'name' }, + }, + { + title: '合同金额(元)', + field: 'totalPrice', + minWidth: 150, + formatter: 'formatNumber', + }, + { + title: '合同开始时间', + field: 'startTime', + minWidth: 150, + formatter: 'formatDateTime', + }, + { + title: '合同结束时间', + field: 'endTime', + minWidth: 150, + formatter: 'formatDateTime', + }, + { + title: '已回款金额(元)', + field: 'totalReceivablePrice', + minWidth: 150, + formatter: 'formatNumber', + }, + { + title: '未回款金额(元)', + field: 'unpaidPrice', + minWidth: 150, + formatter: ({ row }) => { + return floatToFixed2(row.totalPrice - row.totalReceivablePrice); + }, + }, + { + title: '负责人', + field: 'ownerUserName', + minWidth: 150, + }, + { + title: '所属部门', + field: 'ownerUserDeptName', + minWidth: 150, + }, + { + title: '创建时间', + field: 'createTime', + minWidth: 150, + formatter: 'formatDateTime', + }, + { + title: '创建人', + field: 'creatorName', + minWidth: 150, + }, + { + title: '备注', + field: 'remark', + minWidth: 150, + }, + { + title: '合同状态', + field: 'auditStatus', + fixed: 'right', + minWidth: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.CRM_AUDIT_STATUS }, + }, + }, + ]; +} diff --git a/apps/web-antd/src/views/crm/contract/modules/detail-info.vue b/apps/web-antd/src/views/crm/contract/modules/detail-info.vue index f952e7f2f..7968e6dad 100644 --- a/apps/web-antd/src/views/crm/contract/modules/detail-info.vue +++ b/apps/web-antd/src/views/crm/contract/modules/detail-info.vue @@ -1,4 +1,42 @@ - + + diff --git a/apps/web-antd/src/views/crm/contract/modules/detail-list.vue b/apps/web-antd/src/views/crm/contract/modules/detail-list.vue index 3114fe4bc..d6ceee285 100644 --- a/apps/web-antd/src/views/crm/contract/modules/detail-list.vue +++ b/apps/web-antd/src/views/crm/contract/modules/detail-list.vue @@ -1,4 +1,123 @@ - + + diff --git a/apps/web-antd/src/views/crm/contract/modules/detail.vue b/apps/web-antd/src/views/crm/contract/modules/detail.vue index 99ad6b6f9..ad283e4e5 100644 --- a/apps/web-antd/src/views/crm/contract/modules/detail.vue +++ b/apps/web-antd/src/views/crm/contract/modules/detail.vue @@ -1,7 +1,169 @@ - + diff --git a/apps/web-antd/src/views/crm/contract/modules/form.vue b/apps/web-antd/src/views/crm/contract/modules/form.vue index 5d2b0b78a..b96f22c02 100644 --- a/apps/web-antd/src/views/crm/contract/modules/form.vue +++ b/apps/web-antd/src/views/crm/contract/modules/form.vue @@ -12,7 +12,10 @@ import { getContract, updateContract, } from '#/api/crm/contract'; +import { BizTypeEnum } from '#/api/crm/permission'; import { $t } from '#/locales'; +import { erpPriceMultiply } from '#/utils'; +import { ProductEditTable } from '#/views/crm/product'; import { useFormSchema } from '../data'; @@ -24,15 +27,37 @@ const getTitle = computed(() => { : $t('ui.actionTitle.create', ['合同']); }); +function handleUpdateProducts(products: any) { + formData.value = modalApi.getData(); + formData.value!.products = products; + if (formData.value) { + const totalProductPrice = + formData.value.products?.reduce( + (prev, curr) => prev + curr.totalPrice, + 0, + ) ?? 0; + const discountPercent = formData.value.discountPercent; + const discountPrice = + discountPercent === null + ? 0 + : erpPriceMultiply(totalProductPrice, discountPercent / 100); + const totalPrice = totalProductPrice - (discountPrice ?? 0); + formData.value!.totalProductPrice = totalProductPrice; + formData.value!.totalPrice = totalPrice; + formApi.setValues(formData.value!); + } +} + const [Form, formApi] = useVbenForm({ commonConfig: { componentProps: { class: 'w-full', }, + labelWidth: 120, }, - // 一共2列 - wrapperClass: 'grid-cols-2', - layout: 'horizontal', + // 一共3列 + wrapperClass: 'grid-cols-3', + layout: 'vertical', schema: useFormSchema(), showDefaultActions: false, }); @@ -46,6 +71,7 @@ const [Modal, modalApi] = useVbenModal({ modalApi.lock(); // 提交表单 const data = (await formApi.getValues()) as CrmContractApi.Contract; + data.products = formData.value?.products; try { await (formData.value?.id ? updateContract(data) : createContract(data)); // 关闭并提示 @@ -79,7 +105,17 @@ const [Modal, modalApi] = useVbenModal({ diff --git a/apps/web-antd/src/views/crm/customer/data.ts b/apps/web-antd/src/views/crm/customer/data.ts index 551569da0..61731e20b 100644 --- a/apps/web-antd/src/views/crm/customer/data.ts +++ b/apps/web-antd/src/views/crm/customer/data.ts @@ -1,14 +1,8 @@ import type { VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; -import type { DescriptionItemSchema } from '#/components/description'; - -import { h } from 'vue'; - -import { formatDateTime } from '@vben/utils'; import { getAreaTree } from '#/api/system/area'; import { getSimpleUserList } from '#/api/system/user'; -import { DictTag } from '#/components/dict-tag'; import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils'; /** 新增/修改的表单 */ @@ -240,108 +234,3 @@ export function useGridColumns(): VxeTableGridOptions['columns'] { }, ]; } - -/** 详情页的字段 */ -export function useDetailSchema(): DescriptionItemSchema[] { - return [...useDetailBaseSchema(), ...useDetailSystemSchema()]; -} - -/** 详情页的基础字段 */ -export function useDetailBaseSchema(): DescriptionItemSchema[] { - return [ - { - field: 'name', - label: '客户名称', - }, - { - field: 'source', - label: '客户来源', - content: (data) => - h(DictTag, { - type: DICT_TYPE.CRM_CUSTOMER_SOURCE, - value: data?.source, - }), - }, - { - field: 'mobile', - label: '手机', - }, - { - field: 'telephone', - label: '电话', - }, - { - field: 'email', - label: '邮箱', - }, - { - field: 'wechat', - label: '微信', - }, - { - field: 'qq', - label: 'QQ', - }, - { - field: 'industryId', - label: '客户行业', - content: (data) => - h(DictTag, { - type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY, - value: data?.industryId, - }), - }, - { - field: 'level', - label: '客户级别', - content: (data) => - h(DictTag, { type: DICT_TYPE.CRM_CUSTOMER_LEVEL, value: data?.level }), - }, - { - field: 'areaName', - label: '地址', - }, - { - field: 'detailAddress', - label: '详细地址', - }, - { - field: 'contactNextTime', - label: '下次联系时间', - content: (data) => formatDateTime(data?.contactNextTime) as string, - }, - { - field: 'remark', - label: '备注', - }, - ]; -} - -/** 详情页的系统字段 */ -export function useDetailSystemSchema(): DescriptionItemSchema[] { - return [ - { - field: 'ownerUserName', - label: '负责人', - }, - { - field: 'ownerUserDeptName', - label: '所属部门', - }, - { - field: 'contactLastTime', - label: '最后跟进时间', - content: (data) => formatDateTime(data?.contactLastTime) as string, - }, - { - field: 'createTime', - label: '创建时间', - content: (data) => formatDateTime(data?.createTime) as string, - }, - { - field: 'updateTime', - label: '更新时间', - content: (data) => formatDateTime(data?.updateTime) as string, - }, - ]; -} diff --git a/apps/web-antd/src/views/crm/customer/index.ts b/apps/web-antd/src/views/crm/customer/index.ts new file mode 100644 index 000000000..3c2e882ae --- /dev/null +++ b/apps/web-antd/src/views/crm/customer/index.ts @@ -0,0 +1,17 @@ +import { defineAsyncComponent } from 'vue'; + +export const CustomerDetailsInfo = defineAsyncComponent( + () => import('./modules/detail-info.vue'), +); + +export const CustomerForm = defineAsyncComponent( + () => import('./modules/form.vue'), +); + +export const CustomerDetails = defineAsyncComponent( + () => import('./modules/detail.vue'), +); + +export const DistributeForm = defineAsyncComponent( + () => import('./poolConfig/distribute-form.vue'), +); diff --git a/apps/web-antd/src/views/crm/customer/index.vue b/apps/web-antd/src/views/crm/customer/index.vue index 5795fdb0c..1e4f6b3bb 100644 --- a/apps/web-antd/src/views/crm/customer/index.vue +++ b/apps/web-antd/src/views/crm/customer/index.vue @@ -21,6 +21,7 @@ import { $t } from '#/locales'; import { useGridColumns, useGridFormSchema } from './data'; import Form from './modules/form.vue'; +import ImportForm from './modules/import-form.vue'; const { push } = useRouter(); const sceneType = ref('1'); @@ -35,6 +36,16 @@ function onRefresh() { gridApi.query(); } +const [ImportModal, importModalApi] = useVbenModal({ + connectedComponent: ImportForm, + destroyOnClose: true, +}); + +/** 导入客户 */ +function handleImport() { + importModalApi.open(); +} + /** 导出表格 */ async function handleExport() { const data = await exportCustomer(await gridApi.formApi.getValues()); @@ -124,6 +135,7 @@ function onChangeSceneType(key: number | string) { +