feat:【antd】【crm】商机的整体代码结构优化

This commit is contained in:
YunaiV
2025-09-28 21:06:08 +08:00
parent c60c2a5b76
commit 1a3441b662
5 changed files with 501 additions and 1 deletions

View File

@@ -36,7 +36,7 @@ const routes: RouteRecordRaw[] = [
title: '商机详情',
activePath: '/crm/business',
},
component: () => import('#/views/crm/business/modules/detail.vue'),
component: () => import('#/views/crm/business/detail/index.vue'),
},
{
path: 'contract/detail/:id',

View File

@@ -0,0 +1,190 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { VbenFormSchema } from '#/adapter/form';
import type { Ref } from 'vue';
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: '商机金额(元)',
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 useStatusFormSchema(
formData: Ref<CrmBusinessApi.Business | undefined>,
): 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',
},
];
}
/** 详情列表的字段 */
// TODO @AI放在 components 的 data.ts 下,更合适
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: 'formatAmount2',
},
{
field: 'dealTime',
title: '预计成交日期',
formatter: 'formatDate',
},
{
field: 'ownerUserName',
title: '负责人',
},
{
field: 'ownerUserDeptName',
title: '所属部门',
},
{
field: 'statusTypeName',
title: '商机状态组',
fixed: 'right',
},
{
field: 'statusName',
title: '商机阶段',
fixed: 'right',
},
];
}

View File

@@ -0,0 +1,183 @@
<script setup lang="ts">
import type { CrmBusinessApi } from '#/api/crm/business';
import type { SystemOperateLogApi } from '#/api/system/operate-log';
import { onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { useTabs } from '@vben/hooks';
import { Button, Card, Tabs } from 'ant-design-vue';
import { getBusiness } from '#/api/crm/business';
import { getOperateLogPage } from '#/api/crm/operateLog';
import { BizTypeEnum } from '#/api/crm/permission';
import { useDescription } from '#/components/description';
import { OperateLog } from '#/components/operate-log';
import { $t } from '#/locales';
import { ContactDetailsList } from '#/views/crm/contact/components';
import { ContractDetailsList } from '#/views/crm/contract';
import { FollowUp } from '#/views/crm/followup';
import { PermissionList, TransferForm } from '#/views/crm/permission';
import { ProductDetailsList } from '#/views/crm/product/components';
import Form from '../modules/form.vue';
import UpStatusForm from './modules/status-form.vue';
import { useDetailSchema } from './data';
import BusinessDetailsInfo from './modules/info.vue';
const route = useRoute();
const router = useRouter();
const tabs = useTabs();
const loading = ref(false); // 加载中
const businessId = ref(0); // 商机编号
const business = ref<CrmBusinessApi.Business>({} as CrmBusinessApi.Business); // 商机详情
const logList = ref<SystemOperateLogApi.OperateLog[]>([]);
const permissionListRef = ref<InstanceType<typeof PermissionList>>(); // 团队成员列表 Ref
const [Descriptions] = useDescription({
componentProps: {
bordered: false,
column: 4,
class: 'mx-4',
},
schema: useDetailSchema(),
});
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
const [TransferModal, transferModalApi] = useVbenModal({
connectedComponent: TransferForm,
destroyOnClose: true,
});
const [UpStatusModal, upStatusModalApi] = useVbenModal({
connectedComponent: UpStatusForm,
destroyOnClose: true,
});
/** 加载详情 */
async function getBusinessDetail() {
loading.value = true;
try {
business.value = await getBusiness(businessId.value);
// 操作日志
const res = await getOperateLogPage({
bizType: BizTypeEnum.CRM_BUSINESS,
bizId: businessId.value,
});
logList.value = res.list;
} finally {
loading.value = false;
}
}
/** 返回列表页 */
function handleBack() {
tabs.closeCurrentTab();
router.push('/crm/business');
}
/** 编辑商机 */
function handleEdit() {
formModalApi.setData({ id: businessId.value }).open();
}
/** 转移商机 */
function handleTransfer() {
transferModalApi.setData({ bizType: BizTypeEnum.CRM_BUSINESS }).open();
}
/** 更新商机状态操作 */
async function handleUpdateStatus() {
upStatusModalApi.setData(business.value).open();
}
/** 加载数据 */
onMounted(() => {
businessId.value = Number(route.params.id);
getBusinessDetail();
});
</script>
<template>
<Page auto-content-height :title="business?.name" :loading="loading">
<FormModal @success="getBusinessDetail" />
<TransferModal @success="getBusinessDetail" />
<UpStatusModal @success="getBusinessDetail" />
<template #extra>
<div class="flex items-center gap-2">
<Button
v-if="permissionListRef?.validateWrite"
type="primary"
@click="handleEdit"
>
{{ $t('ui.actionTitle.edit') }}
</Button>
<Button
v-if="permissionListRef?.validateWrite"
@click="handleUpdateStatus"
>
变更商机状态
</Button>
<Button
v-if="permissionListRef?.validateOwnerUser"
@click="handleTransfer"
>
转移
</Button>
</div>
</template>
<Card class="min-h-[10%]">
<Descriptions :data="business" />
</Card>
<Card class="mt-4 min-h-[60%]">
<Tabs>
<Tabs.TabPane tab="跟进记录" key="1" :force-render="true">
<FollowUp :biz-id="businessId" :biz-type="BizTypeEnum.CRM_BUSINESS" />
</Tabs.TabPane>
<Tabs.TabPane tab="详细资料" key="2" :force-render="true">
<BusinessDetailsInfo :business="business" />
</Tabs.TabPane>
<Tabs.TabPane tab="联系人" key="3" :force-render="true">
<ContactDetailsList
:biz-id="businessId"
:biz-type="BizTypeEnum.CRM_BUSINESS"
:business-id="businessId"
:customer-id="business.customerId"
/>
</Tabs.TabPane>
<Tabs.TabPane tab="产品" key="4" :force-render="true">
<ProductDetailsList
:biz-id="businessId"
:biz-type="BizTypeEnum.CRM_BUSINESS"
:business="business"
/>
</Tabs.TabPane>
<Tabs.TabPane tab="合同" key="5" :force-render="true">
<ContractDetailsList
:biz-id="businessId"
:biz-type="BizTypeEnum.CRM_BUSINESS"
/>
</Tabs.TabPane>
<Tabs.TabPane tab="团队成员" key="6" :force-render="true">
<PermissionList
ref="permissionListRef"
:biz-id="businessId"
:biz-type="BizTypeEnum.CRM_BUSINESS"
:show-action="true"
@quit-team="handleBack"
/>
</Tabs.TabPane>
<Tabs.TabPane tab="操作日志" key="7" :force-render="true">
<OperateLog :log-list="logList" />
</Tabs.TabPane>
</Tabs>
</Card>
</Page>
</template>

View File

@@ -0,0 +1,42 @@
<script lang="ts" setup>
import type { CrmBusinessApi } from '#/api/crm/business';
import { Divider } from 'ant-design-vue';
import { useDescription } from '#/components/description';
import { useFollowUpDetailSchema } from '#/views/crm/followup/data';
import { useDetailBaseSchema } from '../data';
defineProps<{
business: CrmBusinessApi.Business; // 商机信息
}>();
const [BaseDescriptions] = useDescription({
componentProps: {
title: '基本信息',
bordered: false,
column: 4,
class: 'mx-4',
},
schema: useDetailBaseSchema(),
});
const [SystemDescriptions] = useDescription({
componentProps: {
title: '系统信息',
bordered: false,
column: 3,
class: 'mx-4',
},
schema: useFollowUpDetailSchema(),
});
</script>
<template>
<div class="p-4">
<BaseDescriptions :data="business" />
<Divider />
<SystemDescriptions :data="business" />
</div>
</template>

View File

@@ -0,0 +1,85 @@
<script lang="ts" setup>
import type { CrmBusinessApi } from '#/api/crm/business';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { updateBusinessStatus } from '#/api/crm/business';
import { $t } from '#/locales';
import { useStatusFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<CrmBusinessApi.Business>();
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 120,
},
layout: 'horizontal',
schema: useStatusFormSchema(formData),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as CrmBusinessApi.Business;
try {
if (!data.status) {
return;
}
await updateBusinessStatus({
id: data.id,
statusId: data.status > 0 ? data.status : undefined,
endStatus: data.status < 0 ? -data.status : undefined,
});
// 关闭并提示
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}
// 加载数据
const data = modalApi.getData<CrmBusinessApi.Business>();
if (!data || !data.id) {
return;
}
data.status = data.endStatus === null ? data.statusId : -data.endStatus;
formData.value = data;
modalApi.lock();
try {
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal title="变更商机状态" class="w-2/5">
<Form class="mx-4" />
</Modal>
</template>