diff --git a/apps/web-antd/package.json b/apps/web-antd/package.json index f9a0862ed..fcc647708 100644 --- a/apps/web-antd/package.json +++ b/apps/web-antd/package.json @@ -1,6 +1,6 @@ { "name": "@vben/web-antd", - "version": "5.5.6", + "version": "5.5.7", "homepage": "https://vben.pro", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "repository": { 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/table-action/table-action.vue b/apps/web-antd/src/components/table-action/table-action.vue index 1ad69eb26..7595b87c1 100644 --- a/apps/web-antd/src/components/table-action/table-action.vue +++ b/apps/web-antd/src/components/table-action/table-action.vue @@ -204,7 +204,9 @@ function handleMenuClick(e: any) { " > - {{ action.text }} + + {{ action.text }} + diff --git a/apps/web-antd/src/utils/formatNumber.ts b/apps/web-antd/src/utils/formatNumber.ts index 13fdab6a7..f500183d0 100644 --- a/apps/web-antd/src/utils/formatNumber.ts +++ b/apps/web-antd/src/utils/formatNumber.ts @@ -80,3 +80,107 @@ export function calculateRelativeRate( ((100 * ((value || 0) - reference)) / reference).toFixed(0), ); } + +// ========== ERP 专属方法 ========== + +const ERP_COUNT_DIGIT = 3; +const ERP_PRICE_DIGIT = 2; + +/** + * 【ERP】格式化 Input 数字 + * + * 例如说:库存数量 + * + * @param num 数量 + * @package + * @return 格式化后的数量 + */ +export function erpNumberFormatter( + num: number | string | undefined, + digit: number, +) { + if (num === null || num === undefined) { + return ''; + } + if (typeof num === 'string') { + num = Number.parseFloat(num); + } + // 如果非 number,则直接返回空串 + if (Number.isNaN(num)) { + return ''; + } + return num.toFixed(digit); +} + +/** + * 【ERP】格式化数量,保留三位小数 + * + * 例如说:库存数量 + * + * @param num 数量 + * @return 格式化后的数量 + */ +export function erpCountInputFormatter(num: number | string | undefined) { + return erpNumberFormatter(num, ERP_COUNT_DIGIT); +} + +// noinspection JSCommentMatchesSignature +/** + * 【ERP】格式化数量,保留三位小数 + * + * @param cellValue 数量 + * @return 格式化后的数量 + */ +export function erpCountTableColumnFormatter(cellValue: any) { + return erpNumberFormatter(cellValue, ERP_COUNT_DIGIT); +} + +/** + * 【ERP】格式化金额,保留二位小数 + * + * 例如说:库存数量 + * + * @param num 数量 + * @return 格式化后的数量 + */ +export function erpPriceInputFormatter(num: number | string | undefined) { + return erpNumberFormatter(num, ERP_PRICE_DIGIT); +} + +// noinspection JSCommentMatchesSignature +/** + * 【ERP】格式化金额,保留二位小数 + * + * @param cellValue 数量 + * @return 格式化后的数量 + */ +export function erpPriceTableColumnFormatter(cellValue: any) { + return erpNumberFormatter(cellValue, ERP_PRICE_DIGIT); +} + +/** + * 【ERP】价格计算,四舍五入保留两位小数 + * + * @param price 价格 + * @param count 数量 + * @return 总价格。如果有任一为空,则返回 undefined + */ +export function erpPriceMultiply(price: number, count: number) { + if (price === null || count === null) { + return undefined; + } + return Number.parseFloat((price * count).toFixed(ERP_PRICE_DIGIT)); +} + +/** + * 【ERP】百分比计算,四舍五入保留两位小数 + * + * 如果 total 为 0,则返回 0 + * + * @param value 当前值 + * @param total 总值 + */ +export function erpCalculatePercentage(value: number, total: number) { + if (total === 0) return 0; + return ((value / total) * 100).toFixed(2); +} 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..9505414f8 100644 --- a/apps/web-antd/src/views/crm/business/data.ts +++ b/apps/web-antd/src/views/crm/business/data.ts @@ -1,5 +1,13 @@ import type { VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; +import type { DescriptionItemSchema } from '#/components/description'; + +import { formatDateTime } from '@vben/utils'; + +import { getBusinessStatusTypeSimpleList } from '#/api/crm/business/status'; +import { getCustomerSimpleList } from '#/api/crm/customer'; +import { getSimpleUserList } from '#/api/system/user'; +import { erpPriceInputFormatter, erpPriceMultiply } from '#/utils'; /** 新增/修改的表单 */ export function useFormSchema(): VbenFormSchema[] { @@ -19,18 +27,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 +94,43 @@ export function useFormSchema(): VbenFormSchema[] { }, }, { - fieldName: 'remark', - label: '备注', - component: 'Textarea', + 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, + ); + }, + }, }, ]; } @@ -143,3 +225,123 @@ export function useGridColumns(): VxeTableGridOptions['columns'] { }, ]; } + +/** 详情页的字段 */ +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..2728d32c3 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..61f21b893 --- /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..c4f41745d 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..aa91e7c76 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,211 @@ - + 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..8d3b0d553 100644 --- a/apps/web-antd/src/views/crm/business/modules/form.vue +++ b/apps/web-antd/src/views/crm/business/modules/form.vue @@ -64,12 +64,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 { 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..bd2924aad 100644 --- a/apps/web-antd/src/views/crm/clue/data.ts +++ b/apps/web-antd/src/views/crm/clue/data.ts @@ -7,6 +7,7 @@ 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'; @@ -32,7 +33,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 +45,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 +79,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 +87,7 @@ export function useFormSchema(): VbenFormSchema[] { label: '客户级别', component: 'Select', componentProps: { - options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL), + options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL, 'number'), }, }, { @@ -299,10 +303,6 @@ export function useDetailBaseSchema(): DescriptionItemSchema[] { field: 'mobile', label: '手机', }, - { - field: 'ownerUserName', - label: '负责人', - }, { field: 'telephone', label: '电话', @@ -312,13 +312,18 @@ export function useDetailBaseSchema(): DescriptionItemSchema[] { label: '邮箱', }, { - field: 'wechat', - label: '微信', + field: 'areaName', + label: '地址', + content: (data) => data?.areaName + data?.detailAddress, }, { field: 'qq', label: 'QQ', }, + { + field: 'wechat', + label: '微信', + }, { field: 'industryId', label: '客户行业', @@ -337,14 +342,6 @@ export function useDetailBaseSchema(): DescriptionItemSchema[] { value: data?.level, }), }, - { - field: 'areaId', - label: '地址', - }, - { - field: 'detailAddress', - label: '详细地址', - }, { field: 'contactNextTime', label: '下次联系时间', @@ -356,36 +353,3 @@ export function useDetailBaseSchema(): DescriptionItemSchema[] { }, ]; } - -/** 详情系统信息的配置 */ -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-info.vue b/apps/web-antd/src/views/crm/clue/modules/detail-info.vue index 36784a1df..2e9c68c00 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 '../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..012c97254 100644 --- a/apps/web-antd/src/views/crm/clue/modules/detail.vue +++ b/apps/web-antd/src/views/crm/clue/modules/detail.vue @@ -1,23 +1,40 @@ diff --git a/apps/web-antd/src/views/crm/contact/data.ts b/apps/web-antd/src/views/crm/contact/data.ts index 956f32333..d6d9eaec8 100644 --- a/apps/web-antd/src/views/crm/contact/data.ts +++ b/apps/web-antd/src/views/crm/contact/data.ts @@ -279,9 +279,27 @@ export function useGridColumns(): VxeTableGridOptions['columns'] { ]; } -/** 详情页的字段 */ +/** 详情页的基础字段 */ export function useDetailSchema(): DescriptionItemSchema[] { - return [...useDetailBaseSchema(), ...useDetailSystemSchema()]; + return [ + { + field: 'name', + label: '客户名称', + }, + { + field: 'post', + label: '职务', + }, + { + field: 'mobile', + label: '手机', + }, + { + field: 'createTime', + label: '下次联系时间', + content: (data) => formatDateTime(data?.createTime) as string, + }, + ]; } /** 详情页的基础字段 */ @@ -289,16 +307,11 @@ export function useDetailBaseSchema(): DescriptionItemSchema[] { return [ { field: 'name', - label: '客户名称', + label: '姓名', }, { - field: 'source', - label: '客户来源', - content: (data) => - h(DictTag, { - type: DICT_TYPE.CRM_CUSTOMER_SOURCE, - value: data?.source, - }), + field: 'customerName', + label: '客户名称', }, { field: 'mobile', @@ -312,28 +325,13 @@ export function useDetailBaseSchema(): DescriptionItemSchema[] { 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: 'wechat', + label: '微信', }, { field: 'areaName', @@ -343,6 +341,29 @@ export function useDetailBaseSchema(): DescriptionItemSchema[] { 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: '下次联系时间', @@ -355,31 +376,61 @@ export function useDetailBaseSchema(): DescriptionItemSchema[] { ]; } -/** 详情页的系统字段 */ -export function useDetailSystemSchema(): DescriptionItemSchema[] { +/** 详情列表的字段 */ +export function useDetailListColumns(): VxeTableGridOptions['columns'] { return [ { - field: 'ownerUserName', - label: '负责人', + type: 'checkbox', + width: 50, + fixed: 'left', }, { - field: 'ownerUserDeptName', - label: '所属部门', + field: 'name', + title: '姓名', + fixed: 'left', + slots: { default: 'name' }, }, { - field: 'contactLastTime', - label: '最后跟进时间', - content: (data) => formatDateTime(data?.contactLastTime) as string, + field: 'customerName', + title: '客户名称', + fixed: 'left', + slots: { default: 'customerName' }, }, { - field: 'createTime', - label: '创建时间', - content: (data) => formatDateTime(data?.createTime) as string, + field: 'sex', + title: '性别', + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.SYSTEM_USER_SEX }, + }, }, { - field: 'updateTime', - label: '更新时间', - content: (data) => formatDateTime(data?.updateTime) as string, + 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..9ca08aaa7 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..f816d64a4 --- /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..a68a7129d 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..b7c6e0498 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,172 @@ - + diff --git a/apps/web-antd/src/views/crm/contract/data.ts b/apps/web-antd/src/views/crm/contract/data.ts index 79118dd3f..626ae073e 100644 --- a/apps/web-antd/src/views/crm/contract/data.ts +++ b/apps/web-antd/src/views/crm/contract/data.ts @@ -1,10 +1,16 @@ 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 { getSimpleBusinessList } from '#/api/crm/business'; import { getSimpleContactList } from '#/api/crm/contact'; import { getCustomerSimpleList } from '#/api/crm/customer'; -import { floatToFixed2 } from '#/utils'; +import { DictTag } from '#/components/dict-tag'; +import { erpPriceInputFormatter, floatToFixed2 } from '#/utils'; import { DICT_TYPE } from '#/utils/dict'; /** 新增/修改的表单 */ @@ -274,3 +280,182 @@ export function useGridColumns(): VxeTableGridOptions['columns'] { }, ]; } + +/** 详情头部的配置 */ +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..f3b879888 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..ee8637eb5 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..bf49f9a0c 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,193 @@ - + diff --git a/apps/web-antd/src/views/crm/customer/data.ts b/apps/web-antd/src/views/crm/customer/data.ts index 551569da0..840d56636 100644 --- a/apps/web-antd/src/views/crm/customer/data.ts +++ b/apps/web-antd/src/views/crm/customer/data.ts @@ -243,7 +243,24 @@ export function useGridColumns(): VxeTableGridOptions['columns'] { /** 详情页的字段 */ export function useDetailSchema(): DescriptionItemSchema[] { - return [...useDetailBaseSchema(), ...useDetailSystemSchema()]; + return [ + { + field: 'level', + label: '客户级别', + content: (data) => + h(DictTag, { type: DICT_TYPE.CRM_CUSTOMER_LEVEL, value: data?.level }), + }, + { + field: 'dealStatus', + label: '成交状态', + content: (data) => (data.dealStatus ? '已成交' : '未成交'), + }, + { + field: 'createTime', + label: '创建时间', + content: (data) => formatDateTime(data?.createTime) as string, + }, + ]; } /** 详情页的基础字段 */ @@ -275,13 +292,21 @@ export function useDetailBaseSchema(): DescriptionItemSchema[] { label: '邮箱', }, { - field: 'wechat', - label: '微信', + field: 'areaName', + label: '地址', + }, + { + field: 'detailAddress', + label: '详细地址', }, { field: 'qq', label: 'QQ', }, + { + field: 'wechat', + label: '微信', + }, { field: 'industryId', label: '客户行业', @@ -297,14 +322,6 @@ export function useDetailBaseSchema(): DescriptionItemSchema[] { content: (data) => h(DictTag, { type: DICT_TYPE.CRM_CUSTOMER_LEVEL, value: data?.level }), }, - { - field: 'areaName', - label: '地址', - }, - { - field: 'detailAddress', - label: '详细地址', - }, { field: 'contactNextTime', label: '下次联系时间', @@ -316,32 +333,3 @@ export function useDetailBaseSchema(): DescriptionItemSchema[] { }, ]; } - -/** 详情页的系统字段 */ -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.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) { +