diff --git a/apps/web-ele/src/router/routes/modules/crm.ts b/apps/web-ele/src/router/routes/modules/crm.ts
index 0b36acbf7..a5d7f3212 100644
--- a/apps/web-ele/src/router/routes/modules/crm.ts
+++ b/apps/web-ele/src/router/routes/modules/crm.ts
@@ -38,15 +38,15 @@ const routes: RouteRecordRaw[] = [
// },
// component: () => import('#/views/crm/business/detail/index.vue'),
// },
- // {
- // path: 'contract/detail/:id',
- // name: 'CrmContractDetail',
- // meta: {
- // title: '合同详情',
- // activePath: '/crm/contract',
- // },
- // component: () => import('#/views/crm/contract/detail/index.vue'),
- // },
+ {
+ path: 'contract/detail/:id',
+ name: 'CrmContractDetail',
+ meta: {
+ title: '合同详情',
+ activePath: '/crm/contract',
+ },
+ component: () => import('#/views/crm/contract/detail/index.vue'),
+ },
{
path: 'receivable-plan/detail/:id',
name: 'CrmReceivablePlanDetail',
diff --git a/apps/web-ele/src/views/crm/contract/components/data.ts b/apps/web-ele/src/views/crm/contract/components/data.ts
new file mode 100644
index 000000000..c314faa99
--- /dev/null
+++ b/apps/web-ele/src/views/crm/contract/components/data.ts
@@ -0,0 +1,92 @@
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { erpPriceInputFormatter } from '@vben/utils';
+
+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: 'formatAmount2',
+ },
+ {
+ title: '合同开始时间',
+ field: 'startTime',
+ minWidth: 150,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '合同结束时间',
+ field: 'endTime',
+ minWidth: 150,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '已回款金额(元)',
+ field: 'totalReceivablePrice',
+ minWidth: 150,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '未回款金额(元)',
+ field: 'unpaidPrice',
+ minWidth: 150,
+ formatter: ({ row }) => {
+ return erpPriceInputFormatter(
+ 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-ele/src/views/crm/contract/components/detail-list.vue b/apps/web-ele/src/views/crm/contract/components/detail-list.vue
new file mode 100644
index 000000000..63dc35ca2
--- /dev/null
+++ b/apps/web-ele/src/views/crm/contract/components/detail-list.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.name }}
+
+
+
+
+
diff --git a/apps/web-ele/src/views/crm/contract/components/index.ts b/apps/web-ele/src/views/crm/contract/components/index.ts
new file mode 100644
index 000000000..d9f61793c
--- /dev/null
+++ b/apps/web-ele/src/views/crm/contract/components/index.ts
@@ -0,0 +1 @@
+export { default as ContractDetailsList } from './detail-list.vue';
diff --git a/apps/web-ele/src/views/crm/contract/config/data.ts b/apps/web-ele/src/views/crm/contract/config/data.ts
new file mode 100644
index 000000000..8900429de
--- /dev/null
+++ b/apps/web-ele/src/views/crm/contract/config/data.ts
@@ -0,0 +1,39 @@
+import type { VbenFormSchema } from '#/adapter/form';
+
+export const schema: VbenFormSchema[] = [
+ {
+ component: 'RadioGroup',
+ fieldName: 'notifyEnabled',
+ label: '提前提醒设置',
+ componentProps: {
+ options: [
+ { label: '提醒', value: true },
+ { label: '不提醒', value: false },
+ ],
+ },
+ defaultValue: true,
+ },
+ {
+ component: 'InputNumber',
+ fieldName: 'notifyDays',
+ componentProps: {
+ min: 0,
+ precision: 0,
+ controlsPosition: 'right',
+ class: '!w-full',
+ },
+ renderComponentContent: () => ({
+ addonBefore: () => '提前',
+ addonAfter: () => '天提醒',
+ }),
+ dependencies: {
+ triggerFields: ['notifyEnabled'],
+ show: (values) => values.notifyEnabled,
+ trigger(values) {
+ if (!values.notifyEnabled) {
+ values.notifyDays = undefined;
+ }
+ },
+ },
+ },
+];
diff --git a/apps/web-ele/src/views/crm/contract/config/index.vue b/apps/web-ele/src/views/crm/contract/config/index.vue
new file mode 100644
index 000000000..c3dbb0845
--- /dev/null
+++ b/apps/web-ele/src/views/crm/contract/config/index.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-ele/src/views/crm/contract/data.ts b/apps/web-ele/src/views/crm/contract/data.ts
new file mode 100644
index 000000000..e271ee6b4
--- /dev/null
+++ b/apps/web-ele/src/views/crm/contract/data.ts
@@ -0,0 +1,415 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { useUserStore } from '@vben/stores';
+import { erpPriceInputFormatter, erpPriceMultiply } from '@vben/utils';
+
+import { z } from '#/adapter/form';
+import { getSimpleBusinessList } from '#/api/crm/business';
+import { getSimpleContactList } from '#/api/crm/contact';
+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: 'no',
+ label: '合同编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '保存时自动生成',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '合同名称',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入合同名称',
+ },
+ },
+ {
+ fieldName: 'ownerUserId',
+ label: '负责人',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: (values) => values.id,
+ },
+ defaultValue: userStore.userInfo?.id,
+ rules: 'required',
+ },
+ {
+ fieldName: 'customerId',
+ label: '客户名称',
+ component: 'ApiSelect',
+ rules: 'required',
+ componentProps: {
+ api: getCustomerSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择客户',
+ },
+ },
+ {
+ fieldName: 'businessId',
+ label: '商机名称',
+ component: 'Select',
+ componentProps: {
+ options: [],
+ placeholder: '请选择商机',
+ },
+ dependencies: {
+ triggerFields: ['customerId'],
+ disabled: (values) => !values.customerId,
+ async componentProps(values) {
+ if (!values.customerId) {
+ return {
+ options: [],
+ placeholder: '请选择客户',
+ };
+ }
+ const res = await getSimpleBusinessList();
+ const list = res.filter(
+ (item) => item.customerId === values.customerId,
+ );
+ return {
+ options: list.map((item) => ({
+ label: item.name,
+ value: item.id,
+ })),
+ placeholder: '请选择商机',
+ };
+ },
+ },
+ },
+ {
+ fieldName: 'orderDate',
+ label: '下单日期',
+ component: 'DatePicker',
+ rules: 'required',
+ componentProps: {
+ format: 'YYYY-MM-DD',
+ valueFormat: 'x',
+ },
+ },
+ {
+ fieldName: 'startTime',
+ label: '合同开始时间',
+ component: 'DatePicker',
+ componentProps: {
+ format: 'YYYY-MM-DD',
+ valueFormat: 'x',
+ },
+ },
+ {
+ fieldName: 'endTime',
+ label: '合同结束时间',
+ component: 'DatePicker',
+ componentProps: {
+ format: 'YYYY-MM-DD',
+ valueFormat: 'x',
+ },
+ },
+ {
+ fieldName: 'signUserId',
+ label: '公司签约人',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ defaultValue: userStore.userInfo?.id,
+ },
+ {
+ fieldName: 'signContactId',
+ label: '客户签约人',
+ component: 'Select',
+ componentProps: {
+ options: [],
+ placeholder: '请选择客户签约人',
+ },
+ dependencies: {
+ triggerFields: ['customerId'],
+ disabled: (values) => !values.customerId,
+ async componentProps(values) {
+ if (!values.customerId) {
+ return {
+ options: [],
+ placeholder: '请选择客户',
+ };
+ }
+ const res = await getSimpleContactList();
+ const list = res.filter(
+ (item) => item.customerId === values.customerId,
+ );
+ return {
+ options: list.map((item) => ({
+ label: item.name,
+ value: item.id,
+ })),
+ placeholder: '请选择客户签约人',
+ };
+ },
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ rows: 4,
+ },
+ },
+ {
+ fieldName: 'product',
+ label: '产品清单',
+ component: 'Input',
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'totalProductPrice',
+ label: '产品总金额',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ precision: 2,
+ 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,
+ 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: 'no',
+ label: '合同编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入合同编号',
+ clearable: true,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '合同名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入合同名称',
+ clearable: true,
+ },
+ },
+ {
+ fieldName: 'customerId',
+ label: '客户',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getCustomerSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择客户',
+ clearable: true,
+ },
+ },
+ ];
+}
+
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ title: '合同编号',
+ field: 'no',
+ minWidth: 180,
+ fixed: 'left',
+ },
+ {
+ title: '合同名称',
+ field: 'name',
+ minWidth: 160,
+ fixed: 'left',
+ slots: { default: 'name' },
+ },
+ {
+ title: '客户名称',
+ field: 'customerName',
+ minWidth: 120,
+ slots: { default: 'customerName' },
+ },
+ {
+ title: '商机名称',
+ field: 'businessName',
+ minWidth: 130,
+ slots: { default: 'businessName' },
+ },
+ {
+ title: '合同金额(元)',
+ field: 'totalPrice',
+ minWidth: 140,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '下单时间',
+ field: 'orderDate',
+ minWidth: 120,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '合同开始时间',
+ field: 'startTime',
+ minWidth: 120,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '合同结束时间',
+ field: 'endTime',
+ minWidth: 120,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '客户签约人',
+ field: 'signContactName',
+ minWidth: 130,
+ slots: { default: 'signContactName' },
+ },
+ {
+ title: '公司签约人',
+ field: 'signUserName',
+ minWidth: 130,
+ },
+ {
+ title: '备注',
+ field: 'remark',
+ minWidth: 200,
+ },
+ {
+ title: '已回款金额(元)',
+ field: 'totalReceivablePrice',
+ minWidth: 140,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '未回款金额(元)',
+ field: 'unReceivablePrice',
+ minWidth: 140,
+ formatter: ({ row }) => {
+ return erpPriceInputFormatter(
+ row.totalPrice - row.totalReceivablePrice,
+ );
+ },
+ },
+ {
+ title: '最后跟进时间',
+ field: 'contactLastTime',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '负责人',
+ field: 'ownerUserName',
+ minWidth: 120,
+ },
+ {
+ title: '所属部门',
+ field: 'ownerUserDeptName',
+ minWidth: 100,
+ },
+ {
+ title: '更新时间',
+ field: 'updateTime',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '创建时间',
+ field: 'createTime',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '创建人',
+ field: 'creatorName',
+ minWidth: 120,
+ },
+ {
+ title: '合同状态',
+ field: 'auditStatus',
+ fixed: 'right',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
+ },
+ },
+ {
+ title: '操作',
+ field: 'actions',
+ fixed: 'right',
+ minWidth: 130,
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-ele/src/views/crm/contract/detail/data.ts b/apps/web-ele/src/views/crm/contract/detail/data.ts
new file mode 100644
index 000000000..bfe7b5606
--- /dev/null
+++ b/apps/web-ele/src/views/crm/contract/detail/data.ts
@@ -0,0 +1,100 @@
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
+
+import { DictTag } from '#/components/dict-tag';
+
+/** 详情头部的配置 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'customerName',
+ label: '客户名称',
+ },
+ {
+ field: 'totalPrice',
+ label: '合同金额(元)',
+ render: (val) => erpPriceInputFormatter(val) as string,
+ },
+ {
+ field: 'orderDate',
+ label: '下单时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'totalReceivablePrice',
+ label: '回款金额(元)',
+ render: (val) => erpPriceInputFormatter(val) 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: '合同金额(元)',
+ render: (val) => erpPriceInputFormatter(val) as string,
+ },
+ {
+ field: 'orderDate',
+ label: '下单时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'startTime',
+ label: '合同开始时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'endTime',
+ label: '合同结束时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'signContactName',
+ label: '客户签约人',
+ },
+ {
+ field: 'signUserName',
+ label: '公司签约人',
+ },
+ {
+ field: 'remark',
+ label: '备注',
+ },
+ {
+ field: 'auditStatus',
+ label: '合同状态',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.CRM_AUDIT_STATUS,
+ value: val,
+ }),
+ },
+ ];
+}
diff --git a/apps/web-ele/src/views/crm/contract/detail/index.vue b/apps/web-ele/src/views/crm/contract/detail/index.vue
new file mode 100644
index 000000000..a172738db
--- /dev/null
+++ b/apps/web-ele/src/views/crm/contract/detail/index.vue
@@ -0,0 +1,175 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-ele/src/views/crm/contract/detail/modules/info.vue b/apps/web-ele/src/views/crm/contract/detail/modules/info.vue
new file mode 100644
index 000000000..a87db739f
--- /dev/null
+++ b/apps/web-ele/src/views/crm/contract/detail/modules/info.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-ele/src/views/crm/contract/index.vue b/apps/web-ele/src/views/crm/contract/index.vue
new file mode 100644
index 000000000..d583162ed
--- /dev/null
+++ b/apps/web-ele/src/views/crm/contract/index.vue
@@ -0,0 +1,257 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.name }}
+
+
+
+
+ {{ row.customerName }}
+
+
+
+
+ {{ row.businessName }}
+
+
+
+
+ {{ row.signContactName }}
+
+
+
+
+
+
+
+
diff --git a/apps/web-ele/src/views/crm/contract/modules/form.vue b/apps/web-ele/src/views/crm/contract/modules/form.vue
new file mode 100644
index 000000000..b39065435
--- /dev/null
+++ b/apps/web-ele/src/views/crm/contract/modules/form.vue
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+