diff --git a/apps/web-ele/src/router/routes/modules/crm.ts b/apps/web-ele/src/router/routes/modules/crm.ts index a5d7f3212..b7ce01f94 100644 --- a/apps/web-ele/src/router/routes/modules/crm.ts +++ b/apps/web-ele/src/router/routes/modules/crm.ts @@ -29,15 +29,15 @@ const routes: RouteRecordRaw[] = [ // }, // component: () => import('#/views/crm/customer/detail/index.vue'), // }, - // { - // path: 'business/detail/:id', - // name: 'CrmBusinessDetail', - // meta: { - // title: '商机详情', - // activePath: '/crm/business', - // }, - // component: () => import('#/views/crm/business/detail/index.vue'), - // }, + { + path: 'business/detail/:id', + name: 'CrmBusinessDetail', + meta: { + title: '商机详情', + activePath: '/crm/business', + }, + component: () => import('#/views/crm/business/detail/index.vue'), + }, { path: 'contract/detail/:id', name: 'CrmContractDetail', diff --git a/apps/web-ele/src/views/crm/business/components/data.ts b/apps/web-ele/src/views/crm/business/components/data.ts new file mode 100644 index 000000000..10ecb5a0f --- /dev/null +++ b/apps/web-ele/src/views/crm/business/components/data.ts @@ -0,0 +1,52 @@ +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +/** 商机关联列表列定义 */ +export function useBusinessDetailListColumns(): 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: 'formatAmount2', + }, + { + 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-ele/src/views/crm/business/components/detail-list-modal.vue b/apps/web-ele/src/views/crm/business/components/detail-list-modal.vue new file mode 100644 index 000000000..9b3bfee91 --- /dev/null +++ b/apps/web-ele/src/views/crm/business/components/detail-list-modal.vue @@ -0,0 +1,149 @@ + + + + diff --git a/apps/web-ele/src/views/crm/business/components/detail-list.vue b/apps/web-ele/src/views/crm/business/components/detail-list.vue new file mode 100644 index 000000000..e57ac8f0c --- /dev/null +++ b/apps/web-ele/src/views/crm/business/components/detail-list.vue @@ -0,0 +1,215 @@ + + + + diff --git a/apps/web-ele/src/views/crm/business/components/index.ts b/apps/web-ele/src/views/crm/business/components/index.ts new file mode 100644 index 000000000..a63de4701 --- /dev/null +++ b/apps/web-ele/src/views/crm/business/components/index.ts @@ -0,0 +1 @@ +export { default as BusinessDetailsList } from './detail-list.vue'; diff --git a/apps/web-ele/src/views/crm/business/data.ts b/apps/web-ele/src/views/crm/business/data.ts new file mode 100644 index 000000000..8055d34dd --- /dev/null +++ b/apps/web-ele/src/views/crm/business/data.ts @@ -0,0 +1,280 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { useUserStore } from '@vben/stores'; +import { erpPriceMultiply } from '@vben/utils'; + +import { z } from '#/adapter/form'; +import { getBusinessStatusTypeSimpleList } from '#/api/crm/business/status'; +import { getCustomerSimpleList } from '#/api/crm/customer'; +import { getSimpleUserList } from '#/api/system/user'; + +/** 新增/修改的表单 */ +export function useFormSchema(): VbenFormSchema[] { + const userStore = useUserStore(); + return [ + { + fieldName: 'id', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'name', + label: '商机名称', + component: 'Input', + rules: 'required', + componentProps: { + placeholder: '请输入商机名称', + allowClear: true, + }, + }, + { + fieldName: 'ownerUserId', + label: '负责人', + component: 'ApiSelect', + dependencies: { + triggerFields: ['id'], + disabled: (values) => values.id, + }, + componentProps: { + api: getSimpleUserList, + labelField: 'nickname', + valueField: 'id', + placeholder: '请选择负责人', + allowClear: true, + }, + defaultValue: userStore.userInfo?.id, + rules: 'required', + }, + { + fieldName: 'customerId', + label: '客户名称', + component: 'ApiSelect', + componentProps: { + api: getCustomerSimpleList, + labelField: 'name', + valueField: 'id', + placeholder: '请选择客户', + allowClear: true, + }, + dependencies: { + triggerFields: ['id'], + disabled: (values) => values.customerDefault, + }, + rules: 'required', + }, + { + fieldName: 'contactId', + label: '合同名称', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'statusTypeId', + label: '商机状态组', + component: 'ApiSelect', + componentProps: { + api: getBusinessStatusTypeSimpleList, + labelField: 'name', + valueField: 'id', + placeholder: '请选择商机状态组', + allowClear: true, + }, + dependencies: { + triggerFields: ['id'], + disabled: (values) => values.id, + }, + rules: 'required', + }, + { + fieldName: 'dealTime', + label: '预计成交日期', + component: 'DatePicker', + componentProps: { + showTime: false, + format: 'YYYY-MM-DD HH:mm:ss', + valueFormat: 'x', + placeholder: '请选择预计成交日期', + }, + }, + { + fieldName: 'product', + label: '产品清单', + component: 'Input', + formItemClass: 'col-span-3', + componentProps: { + placeholder: '请输入产品清单', + allowClear: true, + }, + }, + { + fieldName: 'totalProductPrice', + label: '产品总金额', + component: 'InputNumber', + componentProps: { + min: 0, + precision: 2, + disabled: true, + placeholder: '请输入产品总金额', + controlsPosition: 'right', + class: '!w-full', + }, + rules: z.number().min(0).optional().default(0), + }, + { + fieldName: 'discountPercent', + label: '整单折扣(%)', + component: 'InputNumber', + componentProps: { + min: 0, + precision: 2, + placeholder: '请输入整单折扣', + controlsPosition: 'right', + class: '!w-full', + }, + rules: z.number().min(0).max(100).optional().default(0), + }, + { + fieldName: 'totalPrice', + label: '折扣后金额', + component: 'InputNumber', + componentProps: { + min: 0, + precision: 2, + disabled: true, + placeholder: '请输入折扣后金额', + controlsPosition: 'right', + class: '!w-full', + }, + dependencies: { + triggerFields: ['totalProductPrice', 'discountPercent'], + trigger(values, form) { + const discountPrice = + erpPriceMultiply( + values.totalProductPrice, + values.discountPercent / 100, + ) ?? 0; + form.setFieldValue( + 'totalPrice', + values.totalProductPrice - discountPrice, + ); + }, + }, + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'name', + label: '商机名称', + component: 'Input', + componentProps: { + placeholder: '请输入商机名称', + allowClear: true, + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'name', + title: '商机名称', + fixed: 'left', + width: 160, + slots: { default: 'name' }, + }, + { + field: 'customerName', + title: '客户名称', + fixed: 'left', + width: 120, + slots: { default: 'customerName' }, + }, + { + field: 'totalPrice', + title: '商机金额(元)', + width: 140, + formatter: 'formatAmount2', + }, + { + field: 'dealTime', + title: '预计成交日期', + formatter: 'formatDate', + width: 180, + }, + { + field: 'remark', + title: '备注', + width: 200, + }, + { + field: 'contactNextTime', + title: '下次联系时间', + formatter: 'formatDateTime', + width: 180, + }, + { + field: 'ownerUserName', + title: '负责人', + width: 100, + }, + { + field: 'ownerUserDeptName', + title: '所属部门', + width: 100, + }, + { + field: 'contactLastTime', + title: '最后跟进时间', + formatter: 'formatDateTime', + width: 180, + }, + { + field: 'updateTime', + title: '更新时间', + formatter: 'formatDateTime', + width: 180, + }, + { + field: 'createTime', + title: '创建时间', + formatter: 'formatDateTime', + width: 180, + }, + { + field: 'creatorName', + title: '创建人', + width: 100, + }, + { + field: 'statusTypeName', + title: '商机状态组', + fixed: 'right', + width: 140, + }, + { + field: 'statusName', + title: '商机阶段', + fixed: 'right', + width: 120, + }, + { + title: '操作', + width: 130, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/crm/business/detail/data.ts b/apps/web-ele/src/views/crm/business/detail/data.ts new file mode 100644 index 000000000..1cb1c4949 --- /dev/null +++ b/apps/web-ele/src/views/crm/business/detail/data.ts @@ -0,0 +1,141 @@ +import type { Ref } from 'vue'; + +import type { VbenFormSchema } from '#/adapter/form'; +import type { CrmBusinessApi } from '#/api/crm/business'; +import type { DescriptionItemSchema } from '#/components/description'; + +import { erpPriceInputFormatter, formatDateTime } from '@vben/utils'; + +import { + DEFAULT_STATUSES, + getBusinessStatusSimpleList, +} from '#/api/crm/business/status'; + +/** 详情页的字段 */ +export function useDetailSchema(): DescriptionItemSchema[] { + return [ + { + field: 'customerName', + label: '客户名称', + }, + { + field: 'totalPrice', + label: '商机金额(元)', + render: (val) => erpPriceInputFormatter(val), + }, + { + field: 'statusTypeName', + label: '商机组', + }, + { + field: 'ownerUserName', + label: '负责人', + }, + { + field: 'createTime', + label: '创建时间', + render: (val) => formatDateTime(val) as string, + }, + ]; +} + +/** 详情页的基础字段 */ +export function useDetailBaseSchema(): DescriptionItemSchema[] { + return [ + { + field: 'name', + label: '商机名称', + }, + { + field: 'customerName', + label: '客户名称', + }, + { + field: 'totalPrice', + label: '商机金额(元)', + render: (val) => erpPriceInputFormatter(val), + }, + { + field: 'dealTime', + label: '预计成交日期', + render: (val) => formatDateTime(val) as string, + }, + { + field: 'contactNextTime', + label: '下次联系时间', + render: (val) => formatDateTime(val) as string, + }, + { + field: 'statusTypeName', + label: '商机状态组', + }, + { + field: 'statusName', + label: '商机阶段', + }, + { + field: 'remark', + label: '备注', + }, + ]; +} + +/** 商机状态更新表单 */ +export function useStatusFormSchema( + formData: Ref, +): VbenFormSchema[] { + return [ + { + fieldName: 'id', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'statusId', + label: '商机状态', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'endStatus', + label: '商机状态', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'status', + label: '商机阶段', + component: 'Select', + dependencies: { + triggerFields: [''], + async componentProps() { + const statusList = await getBusinessStatusSimpleList( + formData.value?.statusTypeId ?? 0, + ); + const statusOptions = statusList.map((item) => ({ + label: `${item.name}(赢单率:${item.percent}%)`, + value: item.id, + })); + const options = DEFAULT_STATUSES.map((item) => ({ + label: `${item.name}(赢单率:${item.percent}%)`, + value: item.endStatus, + })); + statusOptions.push(...options); + return { + options: statusOptions, + }; + }, + }, + rules: 'required', + }, + ]; +} diff --git a/apps/web-ele/src/views/crm/business/detail/index.vue b/apps/web-ele/src/views/crm/business/detail/index.vue new file mode 100644 index 000000000..c10c047f3 --- /dev/null +++ b/apps/web-ele/src/views/crm/business/detail/index.vue @@ -0,0 +1,192 @@ + + + diff --git a/apps/web-ele/src/views/crm/business/detail/modules/info.vue b/apps/web-ele/src/views/crm/business/detail/modules/info.vue new file mode 100644 index 000000000..d498507d9 --- /dev/null +++ b/apps/web-ele/src/views/crm/business/detail/modules/info.vue @@ -0,0 +1,38 @@ + + + diff --git a/apps/web-ele/src/views/crm/business/detail/modules/status-form.vue b/apps/web-ele/src/views/crm/business/detail/modules/status-form.vue new file mode 100644 index 000000000..53df20aed --- /dev/null +++ b/apps/web-ele/src/views/crm/business/detail/modules/status-form.vue @@ -0,0 +1,85 @@ + + + diff --git a/apps/web-ele/src/views/crm/business/index.vue b/apps/web-ele/src/views/crm/business/index.vue new file mode 100644 index 000000000..377fbf978 --- /dev/null +++ b/apps/web-ele/src/views/crm/business/index.vue @@ -0,0 +1,202 @@ + + + diff --git a/apps/web-ele/src/views/crm/business/modules/form.vue b/apps/web-ele/src/views/crm/business/modules/form.vue new file mode 100644 index 000000000..051cfe75d --- /dev/null +++ b/apps/web-ele/src/views/crm/business/modules/form.vue @@ -0,0 +1,121 @@ + + + diff --git a/apps/web-ele/src/views/crm/business/status/data.ts b/apps/web-ele/src/views/crm/business/status/data.ts new file mode 100644 index 000000000..86f20d819 --- /dev/null +++ b/apps/web-ele/src/views/crm/business/status/data.ts @@ -0,0 +1,112 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { handleTree } from '@vben/utils'; + +import { getDeptList } from '#/api/system/dept'; + +/** 新增/修改的表单 */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'id', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'name', + label: '状态组名', + component: 'Input', + rules: 'required', + componentProps: { + placeholder: '请输入状态组名', + }, + }, + { + fieldName: 'deptIds', + label: '应用部门', + component: 'ApiTreeSelect', + componentProps: { + api: async () => { + const data = await getDeptList(); + return handleTree(data); + }, + multiple: true, + fieldNames: { label: 'name', value: 'id', children: 'children' }, + placeholder: '请选择应用部门', + treeDefaultExpandAll: true, + }, + help: '不选择部门时,默认全公司生效', + }, + { + fieldName: 'statuses', + label: '阶段设置', + component: 'Input', + rules: 'required', + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'name', + title: '状态组名', + }, + { + field: 'deptNames', + title: '应用部门', + formatter: ({ cellValue }) => + cellValue?.length > 0 ? cellValue.join(' ') : '全公司', + }, + { + field: 'creator', + title: '创建人', + }, + { + field: 'createTime', + title: '创建时间', + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 160, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} + +/** 商机状态阶段列表列配置 */ +export function useFormColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'defaultStatus', + title: '阶段', + minWidth: 100, + slots: { default: 'defaultStatus' }, + }, + { + field: 'name', + title: '阶段名称', + minWidth: 100, + slots: { default: 'name' }, + }, + { + field: 'percent', + title: '赢单率(%)', + minWidth: 100, + slots: { default: 'percent' }, + }, + { + title: '操作', + width: 130, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/crm/business/status/index.vue b/apps/web-ele/src/views/crm/business/status/index.vue new file mode 100644 index 000000000..f04101cc1 --- /dev/null +++ b/apps/web-ele/src/views/crm/business/status/index.vue @@ -0,0 +1,136 @@ + + + diff --git a/apps/web-ele/src/views/crm/business/status/modules/form.vue b/apps/web-ele/src/views/crm/business/status/modules/form.vue new file mode 100644 index 000000000..c79a19856 --- /dev/null +++ b/apps/web-ele/src/views/crm/business/status/modules/form.vue @@ -0,0 +1,208 @@ + + +