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/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/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..a583db684 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,44 @@ - + + 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..d7289dcd9 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,204 @@ - + 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..6abc70a99 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,13 @@ 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 +22,6 @@ const [BaseDescription] = useDescription({ class: 'mx-4', }, schema: useDetailBaseSchema(), - data: clue, }); const [SystemDescription] = useDescription({ @@ -31,15 +31,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..4b8c54d32 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/clue/modules/transfer.vue b/apps/web-antd/src/views/crm/clue/modules/transfer.vue deleted file mode 100644 index d076ec4e6..000000000 --- a/apps/web-antd/src/views/crm/clue/modules/transfer.vue +++ /dev/null @@ -1,78 +0,0 @@ - - - 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..bf0532c80 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,44 @@ - + + 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..1822f6e7e 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..da17394b4 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,44 @@ - + + 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..72335ca22 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,167 @@ - + 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/modules/detail-info.vue b/apps/web-antd/src/views/crm/customer/modules/detail-info.vue index c651e23e0..12b2526d2 100644 --- a/apps/web-antd/src/views/crm/customer/modules/detail-info.vue +++ b/apps/web-antd/src/views/crm/customer/modules/detail-info.vue @@ -1,5 +1,44 @@ - + diff --git a/apps/web-antd/src/views/crm/customer/modules/detail.vue b/apps/web-antd/src/views/crm/customer/modules/detail.vue index 19c2b2695..9a945705b 100644 --- a/apps/web-antd/src/views/crm/customer/modules/detail.vue +++ b/apps/web-antd/src/views/crm/customer/modules/detail.vue @@ -5,15 +5,15 @@ import type { SystemOperateLogApi } from '#/api/system/operate-log'; import { defineAsyncComponent, onMounted, ref } from 'vue'; import { useRoute, useRouter } from 'vue-router'; -import { Page } from '@vben/common-ui'; +import { confirm, Page, useVbenModal } from '@vben/common-ui'; +import { useTabs } from '@vben/hooks'; -import { Button, Card, Modal, Tabs } from 'ant-design-vue'; +import { Button, Card, message, Tabs } from 'ant-design-vue'; import { getCustomer, updateCustomerDealStatus } from '#/api/crm/customer'; import { getOperateLogPage } from '#/api/crm/operateLog'; import { BizTypeEnum } from '#/api/crm/permission'; import { useDescription } from '#/components/description'; -import { OperateLog } from '#/components/operate-log'; import { useDetailSchema } from '../data'; @@ -21,15 +21,49 @@ const CustomerDetailsInfo = defineAsyncComponent( () => import('./detail-info.vue'), ); +const ContactDetailsList = defineAsyncComponent( + () => import('#/views/crm/contact/modules/detail-list.vue'), +); + +const ContractDetailsList = defineAsyncComponent( + () => import('#/views/crm/contract/modules/detail-list.vue'), +); + +const CustomerForm = defineAsyncComponent( + () => import('#/views/crm/customer/modules/form.vue'), +); + +const BusinessList = defineAsyncComponent( + () => import('#/views/crm/business/modules/detail-list.vue'), +); + +const FollowUp = defineAsyncComponent( + () => import('#/views/crm/followup/index.vue'), +); + +const PermissionList = defineAsyncComponent( + () => import('#/views/crm/permission/modules/permission-list.vue'), +); + +const TransferForm = defineAsyncComponent( + () => import('#/views/crm/permission/modules/transfer-form.vue'), +); + +const OperateLog = defineAsyncComponent( + () => import('#/components/operate-log'), +); + const loading = ref(false); const route = useRoute(); const router = useRouter(); +const tabs = useTabs(); const customerId = ref(0); const customer = ref({} as CrmCustomerApi.Customer); -const permissionListRef = ref(); // 团队成员列表 Ref +const customerLogList = ref([]); +const permissionListRef = ref>(); // 团队成员列表 Ref const [Description] = useDescription({ componentProps: { @@ -40,91 +74,107 @@ const [Description] = useDescription({ schema: useDetailSchema(), }); +const [FormModal, formModalApi] = useVbenModal({ + connectedComponent: CustomerForm, + destroyOnClose: true, +}); + +const [TransferModal, transferModalApi] = useVbenModal({ + connectedComponent: TransferForm, + destroyOnClose: true, +}); + /** 加载详情 */ async function loadCustomerDetail() { loading.value = true; customerId.value = Number(route.params.id); const data = await getCustomer(customerId.value); - await getOperateLog(); + const logList = await getOperateLogPage({ + bizType: BizTypeEnum.CRM_CUSTOMER, + bizId: customerId.value, + }); + customerLogList.value = logList.list; customer.value = data; loading.value = false; } +/** 返回列表页 */ +function handleBack() { + tabs.closeCurrentTab(); + router.push('/crm/customer'); +} + /** 编辑 */ function handleEdit() { - // formModalApi.setData({ id: clueId }).open(); + formModalApi.setData({ id: customerId.value }).open(); } /** 转移线索 */ function handleTransfer() { - // transferModalApi.setData({ id: clueId }).open(); + transferModalApi.setData({ id: customerId.value }).open(); } /** 锁定客户 */ function handleLock() { - // transferModalApi.setData({ id: clueId }).open(); + transferModalApi.setData({ id: customerId.value }).open(); } /** 解锁客户 */ function handleUnlock() { - // transferModalApi.setData({ id: clueId }).open(); + transferModalApi.setData({ id: customerId.value }).open(); } /** 领取客户 */ function handleReceive() { - // transferModalApi.setData({ id: clueId }).open(); + transferModalApi.setData({ id: customerId.value }).open(); } /** 分配客户 */ function handleDistributeForm() { - // transferModalApi.setData({ id: clueId }).open(); + transferModalApi.setData({ id: customerId.value }).open(); } /** 客户放入公海 */ function handlePutPool() { - // transferModalApi.setData({ id: clueId }).open(); + transferModalApi.setData({ id: customerId.value }).open(); } /** 更新成交状态操作 */ -async function handleUpdateDealStatus() { - const dealStatus = !customer.value.dealStatus; - try { - await Modal.confirm({ - title: '提示', +async function handleUpdateDealStatus(): Promise { + return new Promise((resolve, reject) => { + const dealStatus = !customer.value.dealStatus; + confirm({ content: `确定更新成交状态为【${dealStatus ? '已成交' : '未成交'}】吗?`, - }); - await updateCustomerDealStatus(customerId.value, dealStatus); - Modal.success({ - title: '成功', - content: '更新成交状态成功', - }); - await loadCustomerDetail(); - } catch { - // 用户取消操作 - } -} - -/** 获取操作日志 */ -const logList = ref([]); // 操作日志列表 -async function getOperateLog() { - if (!customerId.value) { - return; - } - const data = await getOperateLogPage({ - bizType: BizTypeEnum.CRM_CUSTOMER, - bizId: customerId.value, + }) + .then(async () => { + const res = await updateCustomerDealStatus( + customerId.value, + dealStatus, + ); + if (res) { + message.success('更新成交状态成功'); + resolve(true); + } else { + reject(new Error('更新成交状态失败')); + } + }) + .catch(() => { + reject(new Error('取消操作')); + }); }); - logList.value = data.list; } // 加载数据 onMounted(async () => { + customerId.value = Number(route.params.id); await loadCustomerDetail(); });