diff --git a/src/api/bpm/model/index.ts b/src/api/bpm/model/index.ts index 2e1d4e64..fea5ecf0 100644 --- a/src/api/bpm/model/index.ts +++ b/src/api/bpm/model/index.ts @@ -32,7 +32,9 @@ export const getModelPage = async (params) => { export const getModel = async (id: number) => { return await request.get({ url: '/bpm/model/get?id=' + id }) } - +export const getModelByKey = async (key: string) => { + return await request.get({ url: '/bpm/model/get-by-key?key=' + key }) +} export const updateModel = async (data: ModelVO) => { return await request.put({ url: '/bpm/model/update', data: data }) } diff --git a/src/api/crm/business/index.ts b/src/api/crm/business/index.ts index 810ec6e9..314eb389 100644 --- a/src/api/crm/business/index.ts +++ b/src/api/crm/business/index.ts @@ -1,4 +1,5 @@ import request from '@/config/axios' +import { TransferReqVO } from '@/api/crm/customer' export interface BusinessVO { id: number @@ -70,3 +71,8 @@ export const getBusinessPageByContact = async (params) => { export const getBusinessListByIds = async (val: number[]) => { return await request.get({ url: '/crm/business/list-by-ids', params: { ids: val.join(',') } }) } + +// 商机转移 +export const transfer = async (data: TransferReqVO) => { + return await request.put({ url: '/crm/business/transfer', data }) +} diff --git a/src/api/crm/contact/index.ts b/src/api/crm/contact/index.ts index 4144c931..6edb90a1 100644 --- a/src/api/crm/contact/index.ts +++ b/src/api/crm/contact/index.ts @@ -1,4 +1,5 @@ import request from '@/config/axios' +import { TransferReqVO } from '@/api/crm/customer' export interface ContactVO { name: string @@ -86,7 +87,7 @@ export const deleteContactBusinessList = async (data: ContactBusinessReqVO) => { return await request.delete({ url: `/crm/contact/delete-business-list`, data }) } -// 查询联系人操作日志 -export const getOperateLogPage = async (params: any) => { - return await request.get({ url: '/crm/contact/operate-log-page', params }) +// 联系人转移 +export const transfer = async (data: TransferReqVO) => { + return await request.put({ url: '/crm/contact/transfer', data }) } diff --git a/src/api/crm/contract/index.ts b/src/api/crm/contract/index.ts index 3498e843..731750c1 100644 --- a/src/api/crm/contract/index.ts +++ b/src/api/crm/contract/index.ts @@ -1,4 +1,6 @@ import request from '@/config/axios' +import { ProductExpandVO } from '@/api/crm/product' +import { TransferReqVO } from '@/api/crm/customer' export interface ContractVO { id: number @@ -14,12 +16,18 @@ export interface ContractVO { price: number discountPercent: number productPrice: number - roUserIds: string - rwUserIds: string contactId: number signUserId: number contactLastTime: Date + status: number remark: string + productItems: ProductExpandVO[] + creatorName: string + updateTime?: Date + createTime?: Date + customerName: string + contactName: string + ownerUserName: string } // 查询 CRM 合同列表 @@ -56,3 +64,13 @@ export const deleteContract = async (id: number) => { export const exportContract = async (params) => { return await request.download({ url: `/crm/contract/export-excel`, params }) } + +// 提交审核 +export const handleApprove = async (id: number) => { + return await request.put({ url: `/crm/contract/approve?id=${id}` }) +} + +// 合同转移 +export const transfer = async (data: TransferReqVO) => { + return await request.put({ url: '/crm/contract/transfer', data }) +} diff --git a/src/api/crm/customer/index.ts b/src/api/crm/customer/index.ts index eb3445cf..a6fb489b 100644 --- a/src/api/crm/customer/index.ts +++ b/src/api/crm/customer/index.ts @@ -63,16 +63,16 @@ export const exportCustomer = async (params: any) => { return await request.download({ url: `/crm/customer/export-excel`, params }) } +// 下载客户导入模板 +export const importCustomerTemplate = () => { + return request.download({ url: '/crm/customer/get-import-template' }) +} + // 客户列表 export const getSimpleCustomerList = async () => { return await request.get({ url: `/crm/customer/list-all-simple` }) } -// 查询客户操作日志 -export const getOperateLogPage = async (id: number) => { - return await request.get({ url: '/crm/customer/operate-log-page?id=' + id }) -} - // ======================= 业务操作 ======================= export interface TransferReqVO { diff --git a/src/api/crm/operateLog/index.ts b/src/api/crm/operateLog/index.ts new file mode 100644 index 00000000..d0f25b6b --- /dev/null +++ b/src/api/crm/operateLog/index.ts @@ -0,0 +1,11 @@ +import request from '@/config/axios' + +export interface OperateLogVO extends PageParam { + bizType: number + bizId: number +} + +// 获得操作日志 +export const getOperateLogPage = async (params: OperateLogVO) => { + return await request.get({ url: `/crm/operate-log/page`, params }) +} diff --git a/src/api/crm/permission/index.ts b/src/api/crm/permission/index.ts index e616a404..5c829b6a 100644 --- a/src/api/crm/permission/index.ts +++ b/src/api/crm/permission/index.ts @@ -22,8 +22,11 @@ export enum BizTypeEnum { CRM_LEADS = 1, // 线索 CRM_CUSTOMER = 2, // 客户 CRM_CONTACT = 3, // 联系人 - CRM_BUSINESS = 5, // 商机 - CRM_CONTRACT = 6 // 合同 + CRM_BUSINESS = 4, // 商机 + CRM_CONTRACT = 5, // 合同 + CRM_PRODUCT = 6, // 产品 + CRM_RECEIVABLE = 7, // 回款 + CRM_RECEIVABLE_PLAN = 8 // 回款计划 } /** diff --git a/src/api/crm/product/index.ts b/src/api/crm/product/index.ts index 2d88cb09..e6508fb4 100644 --- a/src/api/crm/product/index.ts +++ b/src/api/crm/product/index.ts @@ -12,6 +12,12 @@ export interface ProductVO { ownerUserId: number } +export interface ProductExpandVO extends ProductVO { + count: number + discountPercent: number + totalPrice: number +} + // 查询产品列表 export const getProductPage = async (params) => { return await request.get({ url: `/crm/product/page`, params }) @@ -41,8 +47,3 @@ export const deleteProduct = async (id: number) => { export const exportProduct = async (params) => { return await request.download({ url: `/crm/product/export-excel`, params }) } - -// 查询产品操作日志 -export const getOperateLogPage = async (params: any) => { - return await request.get({ url: '/crm/product/operate-log-page', params }) -} diff --git a/src/components/Table/index.ts b/src/components/Table/index.ts index 689f64a8..9f893171 100644 --- a/src/components/Table/index.ts +++ b/src/components/Table/index.ts @@ -1,6 +1,7 @@ import Table from './src/Table.vue' import { ElTable } from 'element-plus' import { TableSetPropsType } from '@/types/table' +import TableSelectForm from './src/TableSelectForm.vue' export interface TableExpose { setProps: (props: Recordable) => void @@ -9,4 +10,4 @@ export interface TableExpose { elTableRef: ComponentRef<typeof ElTable> } -export { Table } +export { Table, TableSelectForm } diff --git a/src/components/Table/src/TableSelectForm.vue b/src/components/Table/src/TableSelectForm.vue new file mode 100644 index 00000000..55b2855e --- /dev/null +++ b/src/components/Table/src/TableSelectForm.vue @@ -0,0 +1,90 @@ +<template> + <Dialog v-model="dialogVisible" :appendToBody="true" :scroll="true" :title="title" width="60%"> + <el-table + ref="multipleTableRef" + v-loading="loading" + :data="list" + :show-overflow-tooltip="true" + :stripe="true" + @selection-change="handleSelectionChange" + > + <el-table-column type="selection" width="55" /> + <slot></slot> + </el-table> + <!-- 分页 --> + <Pagination + v-model:limit="queryParams.pageSize" + v-model:page="queryParams.pageNo" + :total="total" + @pagination="getList" + /> + <template #footer> + <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button> + <el-button @click="dialogVisible = false">取 消</el-button> + </template> + </Dialog> +</template> + +<script lang="ts" setup> +import { ElTable } from 'element-plus' + +defineOptions({ name: 'TableSelectForm' }) +withDefaults( + defineProps<{ + modelValue: any[] + title: string + }>(), + { modelValue: () => [], title: '选择' } +) +const list = ref([]) // 列表的数据 +const total = ref(0) // 列表的总页数 +const loading = ref(false) // 列表的加载中 +const dialogVisible = ref(false) // 弹窗的是否展示 +const formLoading = ref(false) +const queryParams = reactive({ + pageNo: 1, + pageSize: 10 +}) +// 确认选择时的触发事件 +const emits = defineEmits<{ + (e: 'update:modelValue', v: number[]): void +}>() +const multipleTableRef = ref<InstanceType<typeof ElTable>>() +const multipleSelection = ref<any[]>([]) +const handleSelectionChange = (val: any[]) => { + multipleSelection.value = val +} +/** 触发 */ +const submitForm = () => { + formLoading.value = true + try { + emits('update:modelValue', multipleSelection.value) // 返回选择的原始数据由使用方处理 + } finally { + formLoading.value = false + // 关闭弹窗 + dialogVisible.value = false + } +} + +const getList = async (getListFunc: Function) => { + loading.value = true + try { + const data = await getListFunc(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } +} + +/** 打开弹窗 */ +const open = async (getListFunc: Function) => { + dialogVisible.value = true + await nextTick() + if (multipleSelection.value.length > 0) { + multipleTableRef.value!.clearSelection() + } + await getList(getListFunc) +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 +</script> diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts index 7bcd81d2..edda3820 100644 --- a/src/router/modules/remaining.ts +++ b/src/router/modules/remaining.ts @@ -507,6 +507,17 @@ const remainingRouter: AppRouteRecordRaw[] = [ }, component: () => import('@/views/crm/customer/detail/index.vue') }, + { + path: 'contract/detail/:id', + name: 'CrmContractDetail', + meta: { + title: '合同详情', + noCache: true, + hidden: true, + activeMenu: '/crm/contract' + }, + component: () => import('@/views/crm/contract/detail/index.vue') + }, { path: 'contact/detail/:id', name: 'CrmContactDetail', diff --git a/src/views/crm/contact/detail/ContactDetailsHeader.vue b/src/views/crm/contact/detail/ContactDetailsHeader.vue index 31daa499..86fb42a5 100644 --- a/src/views/crm/contact/detail/ContactDetailsHeader.vue +++ b/src/views/crm/contact/detail/ContactDetailsHeader.vue @@ -10,9 +10,7 @@ </div> <div> <!-- 右上:按钮 --> - <el-button @click="openForm('update', contact.id)" v-hasPermi="['crm:contact:update']"> - 编辑 - </el-button> + <slot></slot> </div> </div> </div> @@ -32,18 +30,10 @@ </el-descriptions-item> </el-descriptions> </ContentWrap> - <!-- 表单弹窗:添加/修改 --> - <ContactForm ref="formRef" @success="emit('refresh')" /> </template> -<script setup lang="ts"> +<script lang="ts" setup> import * as ContactApi from '@/api/crm/contact' -import ContactForm from '@/views/crm/contact/ContactForm.vue' import { formatDate } from '@/utils/formatTime' -//操作修改 -const formRef = ref() -const openForm = (type: string, id?: number) => { - formRef.value.open(type, id) -} + const { contact } = defineProps<{ contact: ContactApi.ContactVO }>() -const emit = defineEmits(['refresh']) // 定义 success 事件,用于操作成功后的回调 </script> diff --git a/src/views/crm/contact/detail/index.vue b/src/views/crm/contact/detail/index.vue index cb8eea16..65678a3d 100644 --- a/src/views/crm/contact/detail/index.vue +++ b/src/views/crm/contact/detail/index.vue @@ -1,5 +1,12 @@ <template> - <ContactDetailsHeader :contact="contact" :loading="loading" @refresh="getContactData(id)" /> + <ContactDetailsHeader v-loading="loading" :contact="contact"> + <el-button v-if="permissionListRef?.validateWrite" @click="openForm('update', contact.id)"> + 编辑 + </el-button> + <el-button v-if="permissionListRef?.validateOwnerUser" type="primary" @click="transfer"> + 转移 + </el-button> + </ContactDetailsHeader> <el-col> <el-tabs> <el-tab-pane label="详细资料"> @@ -8,8 +15,14 @@ <el-tab-pane label="操作日志"> <OperateLogV2 :log-list="logList" /> </el-tab-pane> - <el-tab-pane label="团队成员" lazy> - <PermissionList :biz-id="contact.id!" :biz-type="BizTypeEnum.CRM_CONTACT" /> + <el-tab-pane label="团队成员"> + <PermissionList + ref="permissionListRef" + :biz-id="contact.id!" + :biz-type="BizTypeEnum.CRM_CONTACT" + :show-action="!permissionListRef?.isPool || false" + @quit-team="close" + /> </el-tab-pane> <el-tab-pane label="商机" lazy> <BusinessList @@ -20,8 +33,11 @@ </el-tab-pane> </el-tabs> </el-col> + <!-- 表单弹窗:添加/修改 --> + <ContactForm ref="formRef" @success="getContactData(contact.id)" /> + <CrmTransferForm ref="crmTransferFormRef" @success="close" /> </template> -<script setup lang="ts"> +<script lang="ts" setup> import { useTagsViewStore } from '@/store/modules/tagsView' import * as ContactApi from '@/api/crm/contact' import ContactDetailsHeader from '@/views/crm/contact/detail/ContactDetailsHeader.vue' @@ -30,10 +46,14 @@ import BusinessList from '@/views/crm/business/components/BusinessList.vue' // import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // 团队成员列表(权限) import { BizTypeEnum } from '@/api/crm/permission' import { OperateLogV2VO } from '@/api/system/operatelog' +import { getOperateLogPage } from '@/api/crm/operateLog' +import ContactForm from '@/views/crm/contact/ContactForm.vue' +import CrmTransferForm from '@/views/crm/permission/components/TransferForm.vue' defineOptions({ name: 'CrmContactDetail' }) const route = useRoute() +const message = useMessage() const id = Number(route.params.id) // 联系人编号 const loading = ref(true) // 加载中 const contact = ref<ContactApi.ContactVO>({} as ContactApi.ContactVO) // 联系人详情 @@ -48,6 +68,18 @@ const getContactData = async (id: number) => { loading.value = false } } +/** 编辑 */ +const formRef = ref() +const openForm = (type: string, id?: number) => { + formRef.value.open(type, id) +} +/** 联系人转移 */ +const crmTransferFormRef = ref<InstanceType<typeof CrmTransferForm>>() // 联系人转移表单 ref +const transfer = () => { + crmTransferFormRef.value?.open('联系人转移', contact.value.id, ContactApi.transfer) +} + +const permissionListRef = ref<InstanceType<typeof PermissionList>>() // 团队成员列表 Ref /** * 获取操作日志 @@ -57,19 +89,22 @@ const getOperateLog = async (contactId: number) => { if (!contactId) { return } - const data = await ContactApi.getOperateLogPage({ + const data = await getOperateLogPage({ + bizType: BizTypeEnum.CRM_CONTACT, bizId: contactId }) logList.value = data.list } - +const close = () => { + delView(unref(currentRoute)) +} /** 初始化 */ const { delView } = useTagsViewStore() // 视图操作 const { currentRoute } = useRouter() // 路由 onMounted(async () => { if (!id) { - ElMessage.warning('参数错误,联系人不能为空!') - delView(unref(currentRoute)) + message.warning('参数错误,联系人不能为空!') + close() return } await getContactData(id) diff --git a/src/views/crm/contract/ContractForm.vue b/src/views/crm/contract/ContractForm.vue index 5d5578f9..26a597f5 100644 --- a/src/views/crm/contract/ContractForm.vue +++ b/src/views/crm/contract/ContractForm.vue @@ -1,54 +1,108 @@ <template> - <Dialog :title="dialogTitle" v-model="dialogVisible"> + <Dialog v-model="dialogVisible" :title="dialogTitle" width="70%"> <el-form ref="formRef" + v-loading="formLoading" :model="formData" :rules="formRules" - label-width="100px" - v-loading="formLoading" + label-width="110px" > <el-row> + <el-col :span="24" class="mb-10px"> + <CardTitle title="基本信息" /> + </el-col> <el-col :span="12"> <el-form-item label="合同名称" prop="name"> <el-input v-model="formData.name" placeholder="请输入合同名称" /> </el-form-item> </el-col> <el-col :span="12"> - <el-form-item label="客户" prop="customerId"> - <el-input v-model="formData.customerId" placeholder="请选择对应客户" /> + <el-form-item label="合同编号" prop="no"> + <el-input v-model="formData.no" placeholder="请输入合同编号" /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="客户" prop="customerId"> + <el-select v-model="formData.customerId"> + <el-option + v-for="item in customerList" + :key="item.id" + :label="item.name" + :value="item.id!" + /> + </el-select> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="客户签约人" prop="contactId"> + <el-select v-model="formData.contactId" :disabled="!formData.customerId"> + <el-option + v-for="item in getContactOptions" + :key="item.id" + :label="item.name" + :value="item.id!" + /> + </el-select> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="公司签约人" prop="signUserId"> + <el-select v-model="formData.signUserId"> + <el-option + v-for="item in userList" + :key="item.id" + :label="item.nickname" + :value="item.id!" + /> + </el-select> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="负责人" prop="ownerUserId"> + <el-select v-model="formData.ownerUserId"> + <el-option + v-for="item in userList" + :key="item.id" + :label="item.nickname" + :value="item.id!" + /> + </el-select> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="商机名称" prop="businessId"> + <el-select v-model="formData.businessId"> + <el-option + v-for="item in businessList" + :key="item.id" + :label="item.name" + :value="item.id!" + /> + </el-select> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="合同金额(元)" prop="price"> + <el-input v-model="formData.price" placeholder="请输入合同金额" /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="下单日期" prop="orderDate"> + <el-date-picker + v-model="formData.orderDate" + placeholder="选择下单日期" + type="date" + value-format="x" + /> </el-form-item> </el-col> - </el-row> - - <el-form-item label="商机名称" prop="businessId"> - <el-input v-model="formData.businessId" placeholder="请选择对应商机" /> - </el-form-item> - <el-form-item label="工作流" prop="processInstanceId"> - <el-input v-model="formData.processInstanceId" placeholder="请选择工作流" /> - </el-form-item> - <el-form-item label="下单日期" prop="orderDate"> - <el-date-picker - v-model="formData.orderDate" - type="date" - value-format="x" - placeholder="选择下单日期" - /> - </el-form-item> - <el-form-item label="负责人" prop="ownerUserId"> - <el-input v-model="formData.ownerUserId" placeholder="请选择负责人" /> - </el-form-item> - <el-form-item label="合同编号" prop="no"> - <el-input v-model="formData.no" placeholder="请输入合同编号" /> - </el-form-item> - - <el-row> <el-col :span="12"> <el-form-item label="开始时间" prop="startTime"> <el-date-picker v-model="formData.startTime" + placeholder="选择开始时间" type="date" value-format="x" - placeholder="选择开始时间" /> </el-form-item> </el-col> @@ -56,72 +110,67 @@ <el-form-item label="结束时间" prop="endTime"> <el-date-picker v-model="formData.endTime" + placeholder="选择结束时间" type="date" value-format="x" - placeholder="选择结束时间" /> </el-form-item> </el-col> - </el-row> - - <el-row> - <el-col :span="8"> - <el-form-item label="合同金额" prop="price"> - <el-input v-model="formData.price" placeholder="请输入合同金额" /> + <el-col :span="24"> + <el-form-item label="备注" prop="remark"> + <el-input + v-model="formData.remark" + :rows="3" + placeholder="请输入备注" + type="textarea" + /> </el-form-item> </el-col> - <el-col :span="8"> - <el-form-item label="整单折扣" prop="discountPercent"> + <el-col :span="24"> + <el-form-item label="产品列表" prop="productList"> + <ProductList v-model="formData.productItems" /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="整单折扣(%)" prop="discountPercent"> <el-input v-model="formData.discountPercent" placeholder="请输入整单折扣" /> </el-form-item> </el-col> - <el-col :span="8"> - <el-form-item label="产品总金额" prop="productPrice"> + <el-col :span="12"> + <el-form-item label="产品总金额(元)" prop="productPrice"> <el-input v-model="formData.productPrice" placeholder="请输入产品总金额" /> </el-form-item> </el-col> - </el-row> - - <el-form-item label="只读权限的用户" prop="roUserIds"> - <el-input v-model="formData.roUserIds" placeholder="请输入只读权限的用户" /> - </el-form-item> - <el-form-item label="读写权限的用户" prop="rwUserIds"> - <el-input v-model="formData.rwUserIds" placeholder="请输入读写权限的用户" /> - </el-form-item> - - <el-row> - <el-col :span="12"> - <el-form-item label="联系人编号" prop="contactId"> - <el-input v-model="formData.contactId" placeholder="请输入联系人编号" /> - </el-form-item> + <el-col :span="24"> + <CardTitle class="mb-10px" title="审批信息" /> </el-col> <el-col :span="12"> - <el-form-item label="公司签约人" prop="signUserId"> - <el-input v-model="formData.signUserId" placeholder="请输入公司签约人" /> - </el-form-item> + <el-button + class="m-20px" + link + type="primary" + @click="BPMLModelRef?.handleBpmnDetail('contract-approve')" + > + 查看工作流 + </el-button> </el-col> </el-row> - - <el-form-item label="最后跟进时间" prop="contactLastTime"> - <el-date-picker - v-model="formData.contactLastTime" - type="date" - value-format="x" - placeholder="选择最后跟进时间" - /> - </el-form-item> - <el-form-item label="备注" prop="remark"> - <el-input v-model="formData.remark" placeholder="请输入备注" /> - </el-form-item> </el-form> <template #footer> - <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button :disabled="formLoading" type="primary" @click="submitForm">保存</el-button> <el-button @click="dialogVisible = false">取 消</el-button> </template> </Dialog> + <BPMLModel ref="BPMLModelRef" /> </template> -<script setup lang="ts"> +<script lang="ts" setup> +import * as CustomerApi from '@/api/crm/customer' import * as ContractApi from '@/api/crm/contract' +import * as UserApi from '@/api/system/user' +import * as ContactApi from '@/api/crm/contact' +import * as BusinessApi from '@/api/crm/business' +import ProductList from './components/ProductList.vue' +import BPMLModel from '@/views/crm/contract/components/BPMLModel.vue' const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 @@ -130,38 +179,39 @@ const dialogVisible = ref(false) // 弹窗的是否展示 const dialogTitle = ref('') // 弹窗的标题 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 const formType = ref('') // 表单的类型:create - 新增;update - 修改 -const formData = ref({ - id: undefined, - name: undefined, - customerId: undefined, - businessId: undefined, - processInstanceId: undefined, - orderDate: undefined, - ownerUserId: undefined, - no: undefined, - startTime: undefined, - endTime: undefined, - price: undefined, - discountPercent: undefined, - productPrice: undefined, - roUserIds: undefined, - rwUserIds: undefined, - contactId: undefined, - signUserId: undefined, - contactLastTime: undefined, - remark: undefined -}) +const formData = ref<ContractApi.ContractVO>({} as ContractApi.ContractVO) const formRules = reactive({ - name: [{ required: true, message: '合同名称不能为空', trigger: 'blur' }] + name: [{ required: true, message: '合同名称不能为空', trigger: 'blur' }], + customerId: [{ required: true, message: '客户不能为空', trigger: 'blur' }], + orderDate: [{ required: true, message: '下单日期不能为空', trigger: 'blur' }], + ownerUserId: [{ required: true, message: '负责人不能为空', trigger: 'blur' }], + no: [{ required: true, message: '合同编号不能为空', trigger: 'blur' }] }) const formRef = ref() // 表单 Ref - +const BPMLModelRef = ref<InstanceType<typeof BPMLModel>>() +watch( + () => formData.value.productItems, + (val) => { + if (!val || val.length === 0) { + formData.value.productPrice = 0 + return + } + // 使用reduce函数进行累加 + formData.value.productPrice = val.reduce( + (accumulator, currentValue) => + isNaN(accumulator + currentValue.totalPrice) ? 0 : accumulator + currentValue.totalPrice, + 0 + ) + }, + { deep: true } +) /** 打开弹窗 */ const open = async (type: string, id?: number) => { dialogVisible.value = true dialogTitle.value = t('action.' + type) formType.value = type resetForm() + await getAllApi() // 修改时,设置数据 if (id) { formLoading.value = true @@ -172,6 +222,9 @@ const open = async (type: string, id?: number) => { } } } +const getAllApi = async () => { + await Promise.all([getCustomerList(), getUserList(), getContactListList(), getBusinessList()]) +} defineExpose({ open }) // 提供 open 方法,用于打开弹窗 /** 提交表单 */ @@ -184,7 +237,7 @@ const submitForm = async () => { // 提交请求 formLoading.value = true try { - const data = formData.value as unknown as ContractApi.ContractVO + const data = unref(formData.value) as unknown as ContractApi.ContractVO if (formType.value === 'create') { await ContractApi.createContract(data) message.success(t('common.createSuccess')) @@ -199,30 +252,32 @@ const submitForm = async () => { formLoading.value = false } } - +const customerList = ref<CustomerApi.CustomerVO[]>([]) +/** 获取客户 */ +const getCustomerList = async () => { + customerList.value = await CustomerApi.getSimpleCustomerList() +} +const contactList = ref<ContactApi.ContactVO[]>([]) +/** 动态获取客户联系人 */ +const getContactOptions = computed(() => + contactList.value.filter((item) => item.customerId === formData.value.customerId) +) +const getContactListList = async () => { + contactList.value = await ContactApi.getSimpleContactList() +} +const userList = ref<UserApi.UserVO[]>([]) +/** 获取用户列表 */ +const getUserList = async () => { + userList.value = await UserApi.getSimpleUserList() +} +const businessList = ref<BusinessApi.BusinessVO[]>([]) +/** 获取商机 */ +const getBusinessList = async () => { + businessList.value = await BusinessApi.getSimpleBusinessList() +} /** 重置表单 */ const resetForm = () => { - formData.value = { - id: undefined, - name: undefined, - customerId: undefined, - businessId: undefined, - processInstanceId: undefined, - orderDate: undefined, - ownerUserId: undefined, - no: undefined, - startTime: undefined, - endTime: undefined, - price: undefined, - discountPercent: undefined, - productPrice: undefined, - roUserIds: undefined, - rwUserIds: undefined, - contactId: undefined, - signUserId: undefined, - contactLastTime: undefined, - remark: undefined - } + formData.value = {} as ContractApi.ContractVO formRef.value?.resetFields() } </script> diff --git a/src/views/crm/contract/components/BPMLModel.vue b/src/views/crm/contract/components/BPMLModel.vue new file mode 100644 index 00000000..5a8e7ecc --- /dev/null +++ b/src/views/crm/contract/components/BPMLModel.vue @@ -0,0 +1,31 @@ +<template> + <!-- 弹窗:流程模型图的预览 --> + <Dialog v-model="bpmnDetailVisible" :append-to-body="true" title="流程图" width="800"> + <MyProcessViewer + key="designer" + v-model="bpmnXML" + :prefix="bpmnControlForm.prefix" + :value="bpmnXML as any" + v-bind="bpmnControlForm" + /> + </Dialog> +</template> + +<script lang="ts" setup> +import * as ModelApi from '@/api/bpm/model' +import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package' + +defineOptions({ name: 'BPMLModel' }) +/** 流程图的详情按钮操作 */ +const bpmnDetailVisible = ref(false) +const bpmnXML = ref(null) +const bpmnControlForm = ref({ + prefix: 'flowable' +}) +const handleBpmnDetail = async (key: string) => { + const data = await ModelApi.getModelByKey(key) + bpmnXML.value = data.bpmnXml || '' + bpmnDetailVisible.value = true +} +defineExpose({ handleBpmnDetail }) +</script> diff --git a/src/views/crm/contract/components/ProductList.vue b/src/views/crm/contract/components/ProductList.vue new file mode 100644 index 00000000..6c4ca350 --- /dev/null +++ b/src/views/crm/contract/components/ProductList.vue @@ -0,0 +1,112 @@ +<template> + <el-row justify="end"> + <el-button plain type="primary" @click="openForm">添加产品</el-button> + </el-row> + <el-table :data="list" :show-overflow-tooltip="true" :stripe="true"> + <el-table-column align="center" label="产品名称" prop="name" width="160" /> + <el-table-column align="center" label="产品类型" prop="categoryName" width="160" /> + <el-table-column align="center" label="产品单位" prop="unit"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="scope.row.unit" /> + </template> + </el-table-column> + <el-table-column align="center" label="产品编码" prop="no" /> + <el-table-column + :formatter="fenToYuanFormat" + align="center" + label="价格(元)" + prop="price" + width="100" + /> + <el-table-column align="center" label="数量" prop="count" width="200"> + <template #default="{ row }: { row: ProductApi.ProductExpandVO }"> + <el-input-number v-model="row.count" class="!w-100%" /> + </template> + </el-table-column> + <el-table-column align="center" label="折扣(%)" prop="discountPercent" width="200"> + <template #default="{ row }: { row: ProductApi.ProductExpandVO }"> + <el-input-number v-model="row.discountPercent" class="!w-100%" /> + </template> + </el-table-column> + <el-table-column align="center" label="合计" prop="totalPrice" width="100"> + <template #default="{ row }: { row: ProductApi.ProductExpandVO }"> + {{ getTotalPrice(row) }} + </template> + </el-table-column> + <el-table-column align="center" fixed="right" label="操作" width="130"> + <template #default="scope"> + <el-button link type="danger" @click="handleDelete(scope.row.id)"> 移除</el-button> + </template> + </el-table-column> + </el-table> + + <!-- table 选择表单 --> + <TableSelectForm ref="tableSelectFormRef" v-model="multipleSelection" title="选择商品"> + <el-table-column align="center" label="产品名称" prop="name" width="160" /> + <el-table-column align="center" label="产品类型" prop="categoryName" width="160" /> + <el-table-column align="center" label="产品单位" prop="unit"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="scope.row.unit" /> + </template> + </el-table-column> + <el-table-column align="center" label="产品编码" prop="no" /> + <el-table-column + :formatter="fenToYuanFormat" + align="center" + label="价格(元)" + prop="price" + width="100" + /> + </TableSelectForm> +</template> + +<script lang="ts" setup> +import * as ProductApi from '@/api/crm/product' +import { DICT_TYPE } from '@/utils/dict' +import { fenToYuanFormat } from '@/utils/formatter' +import { TableSelectForm } from '@/components/Table/index' + +defineOptions({ name: 'ProductList' }) +withDefaults(defineProps<{ modelValue: any[] }>(), { modelValue: () => [] }) +const emits = defineEmits<{ + (e: 'update:modelValue', v: any[]): void +}>() +const list = ref<ProductApi.ProductExpandVO[]>([]) +const handleDelete = (id: number) => { + const index = list.value.findIndex((item) => item.id === id) + if (index !== -1) { + list.value.splice(index, 1) + } +} +const tableSelectFormRef = ref<InstanceType<typeof TableSelectForm>>() +const multipleSelection = ref<ProductApi.ProductExpandVO[]>([]) +const openForm = () => { + tableSelectFormRef.value?.open(ProductApi.getProductPage) +} +const getTotalPrice = computed(() => (row: ProductApi.ProductExpandVO) => { + const totalPrice = (row.price * row.count * row.discountPercent) / 100 + row.totalPrice = isNaN(totalPrice) ? 0 : totalPrice + return isNaN(totalPrice) ? 0 : totalPrice +}) +watch( + list, + (val) => { + if (!val || val.length === 0) { + return + } + emits('update:modelValue', list.value) + }, + { deep: true } +) +watch( + multipleSelection, + (val) => { + if (!val || val.length === 0) { + return + } + const ids = list.value.map((item) => item.id) + list.value.push(...multipleSelection.value.filter((item) => ids.indexOf(item.id) === -1)) + }, + { deep: true } +) +</script> diff --git a/src/views/crm/contract/detail/ContractDetailsHeader.vue b/src/views/crm/contract/detail/ContractDetailsHeader.vue new file mode 100644 index 00000000..b3552735 --- /dev/null +++ b/src/views/crm/contract/detail/ContractDetailsHeader.vue @@ -0,0 +1,40 @@ +<template> + <div> + <div class="flex items-start justify-between"> + <div> + <el-col> + <el-row> + <span class="text-xl font-bold">{{ contract.name }}</span> + </el-row> + </el-col> + </div> + <div> + <!-- 右上:按钮 --> + <slot></slot> + </div> + </div> + </div> + <ContentWrap class="mt-10px"> + <el-descriptions :column="5" direction="vertical"> + <el-descriptions-item label="客户"> + {{ contract.customerName }} + </el-descriptions-item> + <el-descriptions-item label="客户签约人"> + {{ contract.contactName }} + </el-descriptions-item> + <el-descriptions-item label="合同金额"> + {{ contract.productPrice }} + </el-descriptions-item> + <el-descriptions-item label="创建时间"> + {{ contract.createTime ? formatDate(contract.createTime) : '空' }} + </el-descriptions-item> + </el-descriptions> + </ContentWrap> +</template> +<script lang="ts" setup> +import * as ContractApi from '@/api/crm/contract' +import { formatDate } from '@/utils/formatTime' + +defineOptions({ name: 'ContractDetailsHeader' }) +defineProps<{ contract: ContractApi.ContractVO }>() +</script> diff --git a/src/views/crm/contract/detail/ContractDetailsInfo.vue b/src/views/crm/contract/detail/ContractDetailsInfo.vue new file mode 100644 index 00000000..f18f7c08 --- /dev/null +++ b/src/views/crm/contract/detail/ContractDetailsInfo.vue @@ -0,0 +1,51 @@ +<template> + <ContentWrap> + <el-collapse v-model="activeNames"> + <el-collapse-item name="contractInfo"> + <template #title> + <span class="text-base font-bold">基本信息</span> + </template> + <!-- TODO puhui999: 先出详情样式后补全 --> + <el-descriptions :column="4"> + <el-descriptions-item label="合同名称"> + {{ contract.name }} + </el-descriptions-item> + <el-descriptions-item label="备注"> + {{ contract.remark }} + </el-descriptions-item> + </el-descriptions> + </el-collapse-item> + <el-collapse-item name="systemInfo"> + <template #title> + <span class="text-base font-bold">系统信息</span> + </template> + <el-descriptions :column="2"> + <el-descriptions-item label="负责人"> + {{ contract.ownerUserName }} + </el-descriptions-item> + <el-descriptions-item label="创建人"> + {{ contract.creatorName }} + </el-descriptions-item> + <el-descriptions-item label="创建时间"> + {{ contract.createTime ? formatDate(contract.createTime) : '空' }} + </el-descriptions-item> + <el-descriptions-item label="更新时间"> + {{ contract.updateTime ? formatDate(contract.updateTime) : '空' }} + </el-descriptions-item> + </el-descriptions> + </el-collapse-item> + </el-collapse> + </ContentWrap> +</template> +<script lang="ts" setup> +import * as ContractApi from '@/api/crm/contract' +import { formatDate } from '@/utils/formatTime' + +defineOptions({ name: 'ContractDetailsInfo' }) +defineProps<{ + contract: ContractApi.ContractVO +}>() + +// 展示的折叠面板 +const activeNames = ref(['contractInfo', 'systemInfo']) +</script> diff --git a/src/views/crm/contract/detail/index.vue b/src/views/crm/contract/detail/index.vue new file mode 100644 index 00000000..fde35ba2 --- /dev/null +++ b/src/views/crm/contract/detail/index.vue @@ -0,0 +1,111 @@ +<template> + <ContractDetailsHeader v-loading="loading" :contract="contract"> + <el-button v-if="permissionListRef?.validateWrite" @click="openForm('update', contract.id)"> + 编辑 + </el-button> + <el-button v-if="permissionListRef?.validateOwnerUser" type="primary" @click="transfer"> + 转移 + </el-button> + </ContractDetailsHeader> + <el-col> + <el-tabs> + <el-tab-pane label="详细资料"> + <ContractDetailsInfo :contract="contract" /> + </el-tab-pane> + <el-tab-pane label="操作日志"> + <OperateLogV2 :log-list="logList" /> + </el-tab-pane> + <el-tab-pane label="团队成员"> + <PermissionList + ref="permissionListRef" + :biz-id="contract.id!" + :biz-type="BizTypeEnum.CRM_CONTRACT" + :show-action="!permissionListRef?.isPool || false" + @quit-team="close" + /> + </el-tab-pane> + <el-tab-pane label="商机" lazy> + <BusinessList + :biz-id="contract.id!" + :biz-type="BizTypeEnum.CRM_CONTRACT" + :customer-id="contract.customerId" + /> + </el-tab-pane> + </el-tabs> + </el-col> + <!-- 表单弹窗:添加/修改 --> + <ContractForm ref="formRef" @success="getContractData" /> + <CrmTransferForm ref="crmTransferFormRef" @success="close" /> +</template> +<script lang="ts" setup> +import { useTagsViewStore } from '@/store/modules/tagsView' +import { OperateLogV2VO } from '@/api/system/operatelog' +import * as ContractApi from '@/api/crm/contract' +import ContractDetailsHeader from './ContractDetailsHeader.vue' +import ContractDetailsInfo from './ContractDetailsInfo.vue' +import { BizTypeEnum } from '@/api/crm/permission' +import { getOperateLogPage } from '@/api/crm/operateLog' +import ContractForm from '@/views/crm/contract/ContractForm.vue' +import CrmTransferForm from '@/views/crm/permission/components/TransferForm.vue' +import PermissionList from '@/views/crm/permission/components/PermissionList.vue' +import BusinessList from '@/views/crm/business/components/BusinessList.vue' + +defineOptions({ name: 'CrmContractDetail' }) + +const route = useRoute() +const message = useMessage() +const contractId = ref(0) // 编号 +const loading = ref(true) // 加载中 +const contract = ref<ContractApi.ContractVO>({} as ContractApi.ContractVO) // 详情 +/** 编辑 */ +const formRef = ref() +const openForm = (type: string, id?: number) => { + formRef.value.open(type, id) +} +/** 获取详情 */ +const getContractData = async () => { + loading.value = true + try { + await getOperateLog(contractId.value) + contract.value = await ContractApi.getContract(contractId.value) + } finally { + loading.value = false + } +} + +/** 获取操作日志 */ +const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表 +const getOperateLog = async (contractId: number) => { + if (!contractId) { + return + } + const data = await getOperateLogPage({ + bizType: BizTypeEnum.CRM_CONTRACT, + bizId: contractId + }) + logList.value = data.list +} + +const crmTransferFormRef = ref<InstanceType<typeof CrmTransferForm>>() // 合同转移表单 ref +const transfer = () => { + crmTransferFormRef.value?.open('合同转移', contract.value.id, ContractApi.transfer) +} + +const permissionListRef = ref<InstanceType<typeof PermissionList>>() // 团队成员列表 Ref +/** 初始化 */ +const { delView } = useTagsViewStore() // 视图操作 +const { currentRoute } = useRouter() // 路由 +const close = () => { + delView(unref(currentRoute)) +} +onMounted(async () => { + const id = route.params.id + if (!id) { + message.warning('参数错误,合同不能为空!') + close() + return + } + contractId.value = id as unknown as number + await getContractData() +}) +</script> diff --git a/src/views/crm/contract/index.vue b/src/views/crm/contract/index.vue index 26ff403a..da3aeaad 100644 --- a/src/views/crm/contract/index.vue +++ b/src/views/crm/contract/index.vue @@ -2,44 +2,52 @@ <ContentWrap> <!-- 搜索工作栏 --> <el-form - class="-mb-15px" - :model="queryParams" ref="queryFormRef" :inline="true" + :model="queryParams" + class="-mb-15px" label-width="68px" > <el-form-item label="合同编号" prop="no"> <el-input v-model="queryParams.no" - placeholder="请输入合同编号" - clearable - @keyup.enter="handleQuery" class="!w-240px" + clearable + placeholder="请输入合同编号" + @keyup.enter="handleQuery" /> </el-form-item> <el-form-item label="合同名称" prop="name"> <el-input v-model="queryParams.name" - placeholder="请输入合同名称" - clearable - @keyup.enter="handleQuery" class="!w-240px" + clearable + placeholder="请输入合同名称" + @keyup.enter="handleQuery" /> </el-form-item> <el-form-item> - <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> - <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> - <el-button type="primary" @click="openForm('create')" v-hasPermi="['crm:contract:create']"> - <Icon icon="ep:plus" class="mr-5px" /> 新增 + <el-button @click="handleQuery"> + <Icon class="mr-5px" icon="ep:search" /> + 搜索 + </el-button> + <el-button @click="resetQuery"> + <Icon class="mr-5px" icon="ep:refresh" /> + 重置 + </el-button> + <el-button v-hasPermi="['crm:contract:create']" type="primary" @click="openForm('create')"> + <Icon class="mr-5px" icon="ep:plus" /> + 新增 </el-button> <el-button - type="success" - plain - @click="handleExport" - :loading="exportLoading" v-hasPermi="['crm:contract:export']" + :loading="exportLoading" + plain + type="success" + @click="handleExport" > - <Icon icon="ep:download" class="mr-5px" /> 导出 + <Icon class="mr-5px" icon="ep:download" /> + 导出 </el-button> </el-form-item> </el-form> @@ -48,70 +56,86 @@ <!-- 列表 --> <!-- TODO 芋艿:各种字段要调整 --> <ContentWrap> - <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> - <el-table-column label="合同编号" align="center" prop="id" /> - <el-table-column label="合同名称" align="center" prop="name" /> - <el-table-column label="客户名称" align="center" prop="customerId" /> - <el-table-column label="商机名称" align="center" prop="businessId" /> - <el-table-column label="工作流名称" align="center" prop="processInstanceId" /> + <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true"> + <el-table-column align="center" label="合同编号" prop="id" /> + <el-table-column align="center" label="合同名称" prop="name" /> + <el-table-column align="center" label="客户名称" prop="customerId" /> + <el-table-column align="center" label="商机名称" prop="businessId" /> + <el-table-column align="center" label="工作流名称" prop="processInstanceId" /> <el-table-column + :formatter="dateFormatter" + align="center" label="下单时间" - align="center" prop="orderDate" - :formatter="dateFormatter" width="180px" /> - <el-table-column label="负责人" align="center" prop="ownerUserId" /> - <el-table-column label="合同编号" align="center" prop="no" /> + <el-table-column align="center" label="负责人" prop="ownerUserId" /> + <el-table-column align="center" label="合同编号" prop="no" /> <el-table-column + :formatter="dateFormatter" + align="center" label="开始时间" - align="center" prop="startTime" - :formatter="dateFormatter" width="180px" /> <el-table-column + :formatter="dateFormatter" + align="center" label="结束时间" - align="center" prop="endTime" - :formatter="dateFormatter" width="180px" /> - <el-table-column label="合同金额" align="center" prop="price" /> - <el-table-column label="整单折扣" align="center" prop="discountPercent" /> - <el-table-column label="产品总金额" align="center" prop="productPrice" /> - <el-table-column label="联系人" align="center" prop="contactId" /> - <el-table-column label="公司签约人" align="center" prop="signUserId" /> + <el-table-column align="center" label="合同金额" prop="price" /> + <el-table-column align="center" label="整单折扣" prop="discountPercent" /> + <el-table-column align="center" label="产品总金额" prop="productPrice" /> + <el-table-column align="center" label="联系人" prop="contactId" /> + <el-table-column align="center" label="公司签约人" prop="signUserId" /> <el-table-column + :formatter="dateFormatter" + align="center" label="最后跟进时间" - align="center" prop="contactLastTime" - :formatter="dateFormatter" width="180px" /> <el-table-column - label="创建时间" - align="center" - prop="createTime" :formatter="dateFormatter" + align="center" + label="创建时间" + prop="createTime" width="180px" /> - <el-table-column label="备注" align="center" prop="remark" /> - <el-table-column label="操作" width="120px"> + <el-table-column align="center" label="备注" prop="remark" /> + <el-table-column fixed="right" label="操作" width="250"> <template #default="scope"> <el-button + v-hasPermi="['crm:contract:update']" link type="primary" @click="openForm('update', scope.row.id)" - v-hasPermi="['crm:contract:update']" > 编辑 </el-button> <el-button + v-hasPermi="['crm:contract:update']" + link + type="primary" + @click="handleApprove(scope.row)" + > + 提交审核 + </el-button> + <el-button + v-hasPermi="['crm:contract:query']" + link + type="primary" + @click="openDetail(scope.row.id)" + > + 详情 + </el-button> + <el-button + v-hasPermi="['crm:contract:delete']" link type="danger" @click="handleDelete(scope.row.id)" - v-hasPermi="['crm:contract:delete']" > 删除 </el-button> @@ -120,9 +144,9 @@ </el-table> <!-- 分页 --> <Pagination - :total="total" - v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" + v-model:page="queryParams.pageNo" + :total="total" @pagination="getList" /> </ContentWrap> @@ -130,7 +154,7 @@ <!-- 表单弹窗:添加/修改 --> <ContractForm ref="formRef" @success="getList" /> </template> -<script setup lang="ts"> +<script lang="ts" setup> import { dateFormatter } from '@/utils/formatTime' import download from '@/utils/download' import * as ContractApi from '@/api/crm/contract' @@ -216,6 +240,17 @@ const handleExport = async () => { } } +/** 提交审核 **/ +const handleApprove = async (row: ContractApi.ContractVO) => { + await message.confirm(`您确定提交【${row.name}】审核吗?`) + await ContractApi.handleApprove(row.id) + message.success('提交审核成功!') + await getList() +} +const { push } = useRouter() +const openDetail = (id: number) => { + push({ name: 'CrmContractDetail', params: { id } }) +} /** 初始化 **/ onMounted(() => { getList() diff --git a/src/views/crm/contract/oa/ContractDetail/index.vue b/src/views/crm/contract/oa/ContractDetail/index.vue new file mode 100644 index 00000000..ac8a4f63 --- /dev/null +++ b/src/views/crm/contract/oa/ContractDetail/index.vue @@ -0,0 +1,220 @@ +<template> + <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="110px"> + <el-row> + <el-col :span="24" class="mb-10px"> + <CardTitle title="基本信息" /> + </el-col> + <el-col :span="12"> + <el-form-item label="合同名称" prop="name"> + <el-input v-model="formData.name" placeholder="请输入合同名称" /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="合同编号" prop="no"> + <el-input v-model="formData.no" placeholder="请输入合同编号" /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="客户" prop="customerId"> + <el-select v-model="formData.customerId"> + <el-option + v-for="item in customerList" + :key="item.id" + :label="item.name" + :value="item.id!" + /> + </el-select> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="客户签约人" prop="contactId"> + <el-select v-model="formData.contactId" :disabled="!formData.customerId"> + <el-option + v-for="item in getContactOptions" + :key="item.id" + :label="item.name" + :value="item.id!" + /> + </el-select> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="公司签约人" prop="signUserId"> + <el-select v-model="formData.signUserId"> + <el-option + v-for="item in userList" + :key="item.id" + :label="item.nickname" + :value="item.id!" + /> + </el-select> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="负责人" prop="ownerUserId"> + <el-select v-model="formData.ownerUserId"> + <el-option + v-for="item in userList" + :key="item.id" + :label="item.nickname" + :value="item.id!" + /> + </el-select> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="商机名称" prop="businessId"> + <el-select v-model="formData.businessId"> + <el-option + v-for="item in businessList" + :key="item.id" + :label="item.name" + :value="item.id!" + /> + </el-select> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="合同金额(元)" prop="price"> + <el-input v-model="formData.price" placeholder="请输入合同金额" /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="下单日期" prop="orderDate"> + <el-date-picker + v-model="formData.orderDate" + placeholder="选择下单日期" + type="date" + value-format="x" + /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="开始时间" prop="startTime"> + <el-date-picker + v-model="formData.startTime" + placeholder="选择开始时间" + type="date" + value-format="x" + /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="结束时间" prop="endTime"> + <el-date-picker + v-model="formData.endTime" + placeholder="选择结束时间" + type="date" + value-format="x" + /> + </el-form-item> + </el-col> + <el-col :span="24"> + <el-form-item label="备注" prop="remark"> + <el-input v-model="formData.remark" :rows="3" placeholder="请输入备注" type="textarea" /> + </el-form-item> + </el-col> + <el-col :span="24"> + <el-form-item label="产品列表" prop="productList"> + <ProductList v-model="formData.productItems" /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="整单折扣(%)" prop="discountPercent"> + <el-input v-model="formData.discountPercent" placeholder="请输入整单折扣" /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="产品总金额(元)" prop="productPrice"> + <el-input v-model="formData.productPrice" placeholder="请输入产品总金额" /> + </el-form-item> + </el-col> + <el-col :span="24"> + <CardTitle class="mb-10px" title="审批信息" /> + </el-col> + <el-col :span="12"> + <el-button + class="m-20px" + link + type="primary" + @click="BPMLModelRef?.handleBpmnDetail('contract-approve')" + > + 查看工作流 + </el-button> + </el-col> + </el-row> + </el-form> + <BPMLModel ref="BPMLModelRef" /> +</template> +<script lang="ts" setup> +import * as CustomerApi from '@/api/crm/customer' +import * as ContractApi from '@/api/crm/contract' +import * as UserApi from '@/api/system/user' +import * as ContactApi from '@/api/crm/contact' +import * as BusinessApi from '@/api/crm/business' +import ProductList from '@/views/crm/contract/components/ProductList.vue' +import BPMLModel from '@/views/crm/contract/components/BPMLModel.vue' + +defineOptions({ name: 'ContractDetailOA' }) +const props = defineProps<{ id?: number }>() +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formData = ref<ContractApi.ContractVO>({} as ContractApi.ContractVO) +const formRef = ref() // 表单 Ref +const BPMLModelRef = ref<InstanceType<typeof BPMLModel>>() +watch( + () => formData.value.productItems, + (val) => { + if (!val || val.length === 0) { + formData.value.productPrice = 0 + return + } + // 使用reduce函数进行累加 + formData.value.productPrice = val.reduce( + (accumulator, currentValue) => + isNaN(accumulator + currentValue.totalPrice) ? 0 : accumulator + currentValue.totalPrice, + 0 + ) + }, + { deep: true } +) +/** 打开弹窗 */ +const getFormData = async () => { + await getAllApi() + formLoading.value = true + try { + formData.value = await ContractApi.getContract(props.id!) + } finally { + formLoading.value = false + } +} +const getAllApi = async () => { + await Promise.all([getCustomerList(), getUserList(), getContactListList(), getBusinessList()]) +} +const customerList = ref<CustomerApi.CustomerVO[]>([]) +/** 获取客户 */ +const getCustomerList = async () => { + customerList.value = await CustomerApi.getSimpleCustomerList() +} +const contactList = ref<ContactApi.ContactVO[]>([]) +/** 动态获取客户联系人 */ +const getContactOptions = computed(() => + contactList.value.filter((item) => item.customerId === formData.value.customerId) +) +const getContactListList = async () => { + contactList.value = await ContactApi.getSimpleContactList() +} +const userList = ref<UserApi.UserVO[]>([]) +/** 获取用户列表 */ +const getUserList = async () => { + userList.value = await UserApi.getSimpleUserList() +} +const businessList = ref<BusinessApi.BusinessVO[]>([]) +/** 获取商机 */ +const getBusinessList = async () => { + businessList.value = await BusinessApi.getSimpleBusinessList() +} + +onMounted(() => { + getFormData() +}) +</script> diff --git a/src/views/crm/customer/CustomerImportForm.vue b/src/views/crm/customer/CustomerImportForm.vue new file mode 100644 index 00000000..7a74acf9 --- /dev/null +++ b/src/views/crm/customer/CustomerImportForm.vue @@ -0,0 +1,134 @@ +<template> + <Dialog v-model="dialogVisible" title="客户导入" width="400"> + <el-upload + ref="uploadRef" + v-model:file-list="fileList" + :action="importUrl + '?updateSupport=' + updateSupport" + :auto-upload="false" + :disabled="formLoading" + :headers="uploadHeaders" + :limit="1" + :on-error="submitFormError" + :on-exceed="handleExceed" + :on-success="submitFormSuccess" + accept=".xlsx, .xls" + drag + > + <Icon icon="ep:upload" /> + <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div> + <template #tip> + <div class="el-upload__tip text-center"> + <div class="el-upload__tip"> + <el-checkbox v-model="updateSupport" /> + 是否更新已经存在的客户数据 + </div> + <span>仅允许导入 xls、xlsx 格式文件。</span> + <el-link + :underline="false" + style="font-size: 12px; vertical-align: baseline" + type="primary" + @click="importTemplate" + > + 下载模板 + </el-link> + </div> + </template> + </el-upload> + <template #footer> + <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button> + <el-button @click="dialogVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script lang="ts" setup> +import * as CustomerApi from '@/api/crm/customer' +import { getAccessToken, getTenantId } from '@/utils/auth' +import download from '@/utils/download' + +defineOptions({ name: 'SystemUserImportForm' }) + +const message = useMessage() // 消息弹窗 + +const dialogVisible = ref(false) // 弹窗的是否展示 +const formLoading = ref(false) // 表单的加载中 +const uploadRef = ref() +const importUrl = + import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/crm/customer/import' +const uploadHeaders = ref() // 上传 Header 头 +const fileList = ref([]) // 文件列表 +const updateSupport = ref(0) // 是否更新已经存在的客户数据 + +/** 打开弹窗 */ +const open = () => { + dialogVisible.value = true + fileList.value = [] + resetForm() +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const submitForm = async () => { + if (fileList.value.length == 0) { + message.error('请上传文件') + return + } + // 提交请求 + uploadHeaders.value = { + Authorization: 'Bearer ' + getAccessToken(), + 'tenant-id': getTenantId() + } + formLoading.value = true + uploadRef.value!.submit() +} + +/** 文件上传成功 */ +const emits = defineEmits(['success']) +const submitFormSuccess = (response: any) => { + if (response.code !== 0) { + message.error(response.msg) + formLoading.value = false + return + } + // 拼接提示语 + const data = response.data + let text = '上传成功数量:' + data.createCustomerNames.length + ';' + for (let customerName of data.createCustomerNames) { + text += '< ' + customerName + ' >' + } + text += '更新成功数量:' + data.updateCustomerNames.length + ';' + for (const customerName of data.updateCustomerNames) { + text += '< ' + customerName + ' >' + } + text += '更新失败数量:' + Object.keys(data.failureCustomerNames).length + ';' + for (const customerName in data.failureCustomerNames) { + text += '< ' + customerName + ': ' + data.failureCustomerNames[customerName] + ' >' + } + message.alert(text) + // 发送操作成功的事件 + emits('success') +} + +/** 上传错误提示 */ +const submitFormError = (): void => { + message.error('上传失败,请您重新上传!') + formLoading.value = false +} + +/** 重置表单 */ +const resetForm = () => { + // 重置上传状态和文件 + formLoading.value = false + uploadRef.value?.clearFiles() +} + +/** 文件数超出提示 */ +const handleExceed = (): void => { + message.error('最多只能上传一个文件!') +} + +/** 下载模板操作 */ +const importTemplate = async () => { + const res = await CustomerApi.importCustomerTemplate() + download.excel(res, '客户导入模版.xls') +} +</script> diff --git a/src/views/crm/customer/detail/index.vue b/src/views/crm/customer/detail/index.vue index 23c169f6..44e62314 100644 --- a/src/views/crm/customer/detail/index.vue +++ b/src/views/crm/customer/detail/index.vue @@ -91,6 +91,7 @@ import CrmTransferForm from '@/views/crm/permission/components/TransferForm.vue' import FollowUpList from '@/views/crm/followup/index.vue' import { BizTypeEnum } from '@/api/crm/permission' import type { OperateLogV2VO } from '@/api/system/operatelog' +import { getOperateLogPage } from '@/api/crm/operateLog' defineOptions({ name: 'CrmCustomerDetail' }) @@ -164,7 +165,10 @@ const getOperateLog = async () => { if (!customerId.value) { return } - const data = await CustomerApi.getOperateLogPage(customerId.value) + const data = await getOperateLogPage({ + bizType: BizTypeEnum.CRM_CUSTOMER, + bizId: customerId.value + }) logList.value = data.list } diff --git a/src/views/crm/customer/index.vue b/src/views/crm/customer/index.vue index b2cee2f7..2c441d99 100644 --- a/src/views/crm/customer/index.vue +++ b/src/views/crm/customer/index.vue @@ -84,6 +84,10 @@ <Icon class="mr-5px" icon="ep:plus" /> 新增 </el-button> + <el-button v-hasPermi="['crm:customer:import']" plain type="warning" @click="handleImport"> + <Icon icon="ep:upload" /> + 导入 + </el-button> <el-button v-hasPermi="['crm:customer:export']" :loading="exportLoading" @@ -204,6 +208,7 @@ <!-- 表单弹窗:添加/修改 --> <CustomerForm ref="formRef" @success="getList" /> + <CustomerImportForm ref="customerImportFormRef" @success="getList" /> </template> <script lang="ts" setup> @@ -212,6 +217,7 @@ import { dateFormatter } from '@/utils/formatTime' import download from '@/utils/download' import * as CustomerApi from '@/api/crm/customer' import CustomerForm from './CustomerForm.vue' +import CustomerImportForm from './CustomerImportForm.vue' import { TabsPaneContext } from 'element-plus' defineOptions({ name: 'CrmCustomer' }) @@ -333,7 +339,10 @@ const handleDelete = async (id: number) => { await getList() } catch {} } - +const customerImportFormRef = ref<InstanceType<typeof CustomerImportForm>>() +const handleImport = () => { + customerImportFormRef.value?.open() +} /** 导出按钮操作 */ const handleExport = async () => { try { diff --git a/src/views/crm/product/detail/index.vue b/src/views/crm/product/detail/index.vue index 847e9445..0c3c03d5 100644 --- a/src/views/crm/product/detail/index.vue +++ b/src/views/crm/product/detail/index.vue @@ -1,5 +1,5 @@ <template> - <ProductDetailsHeader :product="product" :loading="loading" @refresh="getProductData(id)" /> + <ProductDetailsHeader :loading="loading" :product="product" @refresh="getProductData(id)" /> <el-col> <el-tabs> <el-tab-pane label="详细资料"> @@ -11,16 +11,19 @@ </el-tabs> </el-col> </template> -<script setup lang="ts"> +<script lang="ts" setup> import { useTagsViewStore } from '@/store/modules/tagsView' import { OperateLogV2VO } from '@/api/system/operatelog' import * as ProductApi from '@/api/crm/product' import ProductDetailsHeader from '@/views/crm/product/detail/ProductDetailsHeader.vue' import ProductDetailsInfo from '@/views/crm/product/detail/ProductDetailsInfo.vue' +import { BizTypeEnum } from '@/api/crm/permission' +import { getOperateLogPage } from '@/api/crm/operateLog' defineOptions({ name: 'CrmProductDetail' }) const route = useRoute() +const message = useMessage() const id = Number(route.params.id) // 编号 const loading = ref(true) // 加载中 const product = ref<ProductApi.ProductVO>({} as ProductApi.ProductVO) // 详情 @@ -42,7 +45,8 @@ const getOperateLog = async (productId: number) => { if (!productId) { return } - const data = await ProductApi.getOperateLogPage({ + const data = await getOperateLogPage({ + bizType: BizTypeEnum.CRM_PRODUCT, bizId: productId }) logList.value = data.list @@ -53,7 +57,7 @@ const { delView } = useTagsViewStore() // 视图操作 const { currentRoute } = useRouter() // 路由 onMounted(async () => { if (!id) { - ElMessage.warning('参数错误,产品不能为空!') + message.warning('参数错误,产品不能为空!') delView(unref(currentRoute)) return } diff --git a/src/views/system/user/UserImportForm.vue b/src/views/system/user/UserImportForm.vue index ad9eae38..9b82d51f 100644 --- a/src/views/system/user/UserImportForm.vue +++ b/src/views/system/user/UserImportForm.vue @@ -61,6 +61,7 @@ const updateSupport = ref(0) // 是否更新已经存在的用户数据 /** 打开弹窗 */ const open = () => { dialogVisible.value = true + fileList.value = [] resetForm() } defineExpose({ open }) // 提供 open 方法,用于打开弹窗