📖 CRM:线索的转化逻辑

(cherry picked from commit 0ae6139e92)
This commit is contained in:
YunaiV 2024-02-19 20:40:37 +08:00 committed by shizhong
parent ade44f18e1
commit 9ddf89b570
10 changed files with 309 additions and 11 deletions

View File

@ -20,11 +20,16 @@ export interface ClueVO {
wechat: string // wechat wechat: string // wechat
email: string // email email: string // email
areaId: number // 所在地 areaId: number // 所在地
areaName?: string // 所在地名称
detailAddress: string // 详细地址 detailAddress: string // 详细地址
industryId: number // 所属行业 industryId: number // 所属行业
level: number // 客户等级 level: number // 客户等级
source: number // 客户来源 source: number // 客户来源
remark: string // 备注 remark: string // 备注
creator: string // 创建人
creatorName?: string // 创建人名称
createTime: Date // 创建时间
updateTime: Date // 更新时间
} }
// 查询线索列表 // 查询线索列表
@ -61,3 +66,8 @@ export const exportClue = async (params) => {
export const transferClue = async (data: TransferReqVO) => { export const transferClue = async (data: TransferReqVO) => {
return await request.put({ url: '/crm/clue/transfer', data }) return await request.put({ url: '/crm/clue/transfer', data })
} }
// 线索转化为客户
export const transformClue = async (ids: number[]) => {
return await request.put({ url: '/crm/clue/transform?ids=' + ids.join(',') })
}

View File

@ -19,7 +19,7 @@ export interface PermissionVO {
* @author HUIHUI * @author HUIHUI
*/ */
export enum BizTypeEnum { export enum BizTypeEnum {
CRM_LEADS = 1, // 线索 CRM_CLUE = 1, // 线索
CRM_CUSTOMER = 2, // 客户 CRM_CUSTOMER = 2, // 客户
CRM_CONTACT = 3, // 联系人 CRM_CONTACT = 3, // 联系人
CRM_BUSINESS = 4, // 商机 CRM_BUSINESS = 4, // 商机

View File

@ -496,6 +496,17 @@ const remainingRouter: AppRouteRecordRaw[] = [
name: 'CrmCenter', name: 'CrmCenter',
meta: { hidden: true }, meta: { hidden: true },
children: [ children: [
{
path: 'clue/detail/:id',
name: 'CrmClueDetail',
meta: {
title: '线索详情',
noCache: true,
hidden: true,
activeMenu: '/crm/clue'
},
component: () => import('@/views/crm/clue/detail/index.vue')
},
{ {
path: 'customer/detail/:id', path: 'customer/detail/:id',
name: 'CrmCustomerDetail', name: 'CrmCustomerDetail',

View File

@ -128,12 +128,12 @@
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12">
<el-form-item label="备注" prop="remark">
<el-input type="textarea" v-model="formData.remark" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row> </el-row>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button> <el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>

View File

@ -0,0 +1,43 @@
<template>
<div v-loading="loading">
<div class="flex items-start justify-between">
<div>
<!-- 左上线索基本信息 -->
<el-col>
<el-row>
<span class="text-xl font-bold">{{ clue.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="线索来源">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="clue.source" />
</el-descriptions-item>
<el-descriptions-item label="手机"> {{ clue.mobile }} </el-descriptions-item>
<el-descriptions-item label="负责人">
{{ clue.ownerUserName }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDate(clue.createTime) }}
</el-descriptions-item>
</el-descriptions>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE } from '@/utils/dict'
import * as ClueApi from '@/api/crm/clue'
import { formatDate } from '@/utils/formatTime'
defineOptions({ name: 'ClueDetailsHeader' })
defineProps<{
clue: ClueApi.ClueVO // 线
loading: boolean //
}>()
</script>

View File

@ -0,0 +1,72 @@
<template>
<ContentWrap>
<el-collapse v-model="activeNames" class="">
<el-collapse-item name="basicInfo">
<template #title>
<span class="text-base font-bold">基本信息</span>
</template>
<el-descriptions :column="4">
<el-descriptions-item label="线索名称">
{{ clue.name }}
</el-descriptions-item>
<el-descriptions-item label="客户来源">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="clue.source" />
</el-descriptions-item>
<el-descriptions-item label="手机">{{ clue.mobile }}</el-descriptions-item>
<el-descriptions-item label="电话">{{ clue.telephone }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ clue.email }}</el-descriptions-item>
<el-descriptions-item label="地址">
{{ clue.areaName }} {{ clue.detailAddress }}
</el-descriptions-item>
<el-descriptions-item label="QQ">{{ clue.qq }}</el-descriptions-item>
<el-descriptions-item label="微信">{{ clue.wechat }}</el-descriptions-item>
<el-descriptions-item label="客户行业">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="clue.industryId" />
</el-descriptions-item>
<el-descriptions-item label="客户级别">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="clue.level" />
</el-descriptions-item>
<el-descriptions-item label="下次联系时间">
{{ formatDate(clue.contactNextTime) }}
</el-descriptions-item>
<el-descriptions-item label="备注">{{ clue.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="4">
<el-descriptions-item label="负责人">{{ clue.ownerUserName }}</el-descriptions-item>
<el-descriptions-item label="最后跟进记录">
{{ clue.contactLastContent }}
</el-descriptions-item>
<el-descriptions-item label="最后跟进时间">
{{ formatDate(clue.contactLastTime) }}
</el-descriptions-item>
<el-descriptions-item label="">&nbsp;</el-descriptions-item>
<el-descriptions-item label="创建人">{{ clue.creatorName }}</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDate(clue.createTime) }}
</el-descriptions-item>
<el-descriptions-item label="更新时间">
{{ formatDate(clue.updateTime) }}
</el-descriptions-item>
</el-descriptions>
</el-collapse-item>
</el-collapse>
</ContentWrap>
</template>
<script lang="ts" setup>
import * as ClueApi from '@/api/crm/clue'
import { DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime'
defineOptions({ name: 'ClueDetailsInfo' })
const { clue } = defineProps<{
clue: ClueApi.ClueVO // 线
}>()
const activeNames = ref(['basicInfo', 'systemInfo']) //
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,130 @@
<template>
<ClueDetailsHeader :clue="clue" :loading="loading">
<el-button
v-if="permissionListRef?.validateWrite"
v-hasPermi="['crm:clue:update']"
type="primary"
@click="openForm"
>
编辑
</el-button>
<el-button v-if="permissionListRef?.validateOwnerUser" type="primary" @click="transfer">
转移
</el-button>
<el-button
v-if="permissionListRef?.validateOwnerUser && !clue.transformStatus"
type="success"
@click="handleTransform"
>
转化为客户
</el-button>
<el-button type="success" disabled>已转化客户</el-button>
</ClueDetailsHeader>
<el-col>
<el-tabs>
<el-tab-pane label="跟进记录">
<FollowUpList :biz-id="clueId" :biz-type="BizTypeEnum.CRM_CLUE" />
</el-tab-pane>
<el-tab-pane label="基本信息">
<ClueDetailsInfo :clue="clue" />
</el-tab-pane>
<el-tab-pane label="团队成员">
<PermissionList
ref="permissionListRef"
:biz-id="clue.id!"
:biz-type="BizTypeEnum.CRM_CLUE"
:show-action="!permissionListRef?.isPool || false"
@quit-team="close"
/>
</el-tab-pane>
<el-tab-pane label="操作日志">
<OperateLogV2 :log-list="logList" />
</el-tab-pane>
</el-tabs>
</el-col>
<!-- 表单弹窗添加/修改 -->
<ClueForm ref="formRef" @success="getClue" />
<CrmTransferForm ref="transferFormRef" @success="close" />
</template>
<script lang="ts" setup>
import { useTagsViewStore } from '@/store/modules/tagsView'
import * as ClueApi from '@/api/crm/clue'
import ClueForm from '@/views/crm/clue/ClueForm.vue'
import ClueDetailsHeader from './ClueDetailsHeader.vue' // 线 -
import ClueDetailsInfo from './ClueDetailsInfo.vue' // 线 -
import PermissionList from '@/views/crm/permission/components/PermissionList.vue' //
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: 'CrmClueDetail' })
const clueId = ref(0) // 线
const loading = ref(true) //
const message = useMessage() //
const { delView } = useTagsViewStore() //
const { currentRoute } = useRouter() //
const permissionListRef = ref<InstanceType<typeof PermissionList>>() // Ref
/** 获取详情 */
const clue = ref<ClueApi.ClueVO>({} as ClueApi.ClueVO) // 线
const getClue = async () => {
loading.value = true
try {
clue.value = await ClueApi.getClue(clueId.value)
await getOperateLog()
} finally {
loading.value = false
}
}
/** 编辑线索 */
const formRef = ref<InstanceType<typeof ClueForm>>() // 线 Ref
const openForm = () => {
formRef.value?.open('update', clueId.value)
}
/** 线索转移 */
const transferFormRef = ref<InstanceType<typeof CrmTransferForm>>() // 线 ref
const transfer = () => {
transferFormRef.value?.open('线索转移', clueId.value, ClueApi.transferClue)
}
/** 转化为客户 */
const handleTransform = async () => {
await message.confirm(`确定将【${clue.value.name}】转化为客户吗?`)
await ClueApi.transformClue([clueId.value])
message.success(`转化客户【${clue.value.name}】成功`)
await getClue()
}
/** 获取操作日志 */
const logList = ref<OperateLogV2VO[]>([]) //
const getOperateLog = async () => {
const data = await getOperateLogPage({
bizType: BizTypeEnum.CRM_CLUE,
bizId: clueId.value
})
logList.value = data.list
}
const close = () => {
delView(unref(currentRoute))
}
/** 初始化 */
const { params } = useRoute()
onMounted(() => {
if (!params.id) {
message.warning('参数错误,线索不能为空!')
close()
return
}
clueId.value = params.id as unknown as number
getClue()
})
</script>

View File

@ -17,6 +17,12 @@
class="!w-240px" class="!w-240px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="转化状态" prop="transformStatus">
<el-select v-model="queryParams.transformStatus" class="!w-240px">
<el-option :value="false" label="未转化" />
<el-option :value="true" label="已转化" />
</el-select>
</el-form-item>
<el-form-item label="手机号" prop="mobile"> <el-form-item label="手机号" prop="mobile">
<el-input <el-input
v-model="queryParams.mobile" v-model="queryParams.mobile"
@ -56,9 +62,19 @@
<!-- 列表 --> <!-- 列表 -->
<ContentWrap> <ContentWrap>
<el-tabs v-model="activeName" @tab-click="handleTabClick">
<el-tab-pane label="我负责的" name="1" />
<el-tab-pane label="我参与的" name="2" />
<el-tab-pane label="下属负责的" name="3" />
</el-tabs>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<!-- TODO 芋艿打开详情 --> <el-table-column label="线索名称" align="center" prop="name" fixed="left" width="120">
<el-table-column label="线索名称" align="center" prop="name" fixed="left" width="120" /> <template #default="scope">
<el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
{{ scope.row.name }}
</el-link>
</template>
</el-table-column>
<el-table-column label="线索来源" align="center" prop="source" width="100"> <el-table-column label="线索来源" align="center" prop="source" width="100">
<template #default="scope"> <template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" /> <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
@ -146,11 +162,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { DICT_TYPE, getBoolDictOptions } from '@/utils/dict' import { DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download' import download from '@/utils/download'
import * as ClueApi from '@/api/crm/clue' import * as ClueApi from '@/api/crm/clue'
import ClueForm from './ClueForm.vue' import ClueForm from './ClueForm.vue'
import { TabsPaneContext } from 'element-plus'
defineOptions({ name: 'CrmClue' }) defineOptions({ name: 'CrmClue' })
@ -163,12 +180,15 @@ const list = ref([]) // 列表的数据
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
sceneType: '1', // activeName
name: null, name: null,
telephone: null, telephone: null,
mobile: null mobile: null,
transformStatus: false
}) })
const queryFormRef = ref() // const queryFormRef = ref() //
const exportLoading = ref(false) // const exportLoading = ref(false) //
const activeName = ref('1') // tab
/** 查询列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
@ -194,6 +214,18 @@ const resetQuery = () => {
handleQuery() handleQuery()
} }
/** tab 切换 */
const handleTabClick = (tab: TabsPaneContext) => {
queryParams.sceneType = tab.paneName
handleQuery()
}
/** 打开线索详情 */
const { push } = useRouter()
const openDetail = (id: number) => {
push({ name: 'CrmClueDetail', params: { id } })
}
/** 添加/修改操作 */ /** 添加/修改操作 */
const formRef = ref() const formRef = ref()
const openForm = (type: string, id?: number) => { const openForm = (type: string, id?: number) => {

View File

@ -67,7 +67,6 @@
<el-tab-pane label="操作日志"> <el-tab-pane label="操作日志">
<OperateLogV2 :log-list="logList" /> <OperateLogV2 :log-list="logList" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="回访" lazy>TODO 待开发</el-tab-pane>
</el-tabs> </el-tabs>
</el-col> </el-col>

View File

@ -11,6 +11,7 @@
<ContentWrap> <ContentWrap>
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true"> <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="id" />
<!-- TODO @puhui999展示不出来 -->
<el-table-column align="center" label="跟进人" prop="creatorName" /> <el-table-column align="center" label="跟进人" prop="creatorName" />
<el-table-column align="center" label="跟进类型" prop="type"> <el-table-column align="center" label="跟进类型" prop="type">
<template #default="scope"> <template #default="scope">