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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.name }}
+
+
+
+
+ {{ row.customerName }}
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.name }}
+
+
+
+
+ {{ row.customerName }}
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.name }}
+
+
+
+
+ {{ row.customerName }}
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+