Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
admin
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
0ae6139e
authored
Feb 19, 2024
by
YunaiV
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
📖
CRM:线索的转化逻辑
parent
1047c5b0
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
307 additions
and
9 deletions
+307
-9
src/api/crm/clue/index.ts
+10
-0
src/api/crm/permission/index.ts
+1
-1
src/router/modules/remaining.ts
+11
-0
src/views/crm/clue/ClueForm.vue
+3
-3
src/views/crm/clue/detail/ClueDetailsHeader.vue
+43
-0
src/views/crm/clue/detail/ClueDetailsInfo.vue
+72
-0
src/views/crm/clue/detail/index.vue
+130
-0
src/views/crm/clue/index.vue
+36
-4
src/views/crm/customer/detail/index.vue
+0
-1
src/views/crm/followup/index.vue
+1
-0
No files found.
src/api/crm/clue/index.ts
View file @
0ae6139e
...
...
@@ -20,11 +20,16 @@ export interface ClueVO {
wechat
:
string
// wechat
email
:
string
// email
areaId
:
number
// 所在地
areaName
?:
string
// 所在地名称
detailAddress
:
string
// 详细地址
industryId
:
number
// 所属行业
level
:
number
// 客户等级
source
:
number
// 客户来源
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
)
=>
{
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
(
','
)
})
}
src/api/crm/permission/index.ts
View file @
0ae6139e
...
...
@@ -19,7 +19,7 @@ export interface PermissionVO {
* @author HUIHUI
*/
export
enum
BizTypeEnum
{
CRM_
LEADS
=
1
,
// 线索
CRM_
CLUE
=
1
,
// 线索
CRM_CUSTOMER
=
2
,
// 客户
CRM_CONTACT
=
3
,
// 联系人
CRM_BUSINESS
=
4
,
// 商机
...
...
src/router/modules/remaining.ts
View file @
0ae6139e
...
...
@@ -497,6 +497,17 @@ const remainingRouter: AppRouteRecordRaw[] = [
meta
:
{
hidden
:
true
},
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'
,
name
:
'CrmCustomerDetail'
,
meta
:
{
...
...
src/views/crm/clue/ClueForm.vue
View file @
0ae6139e
...
...
@@ -128,12 +128,12 @@
/>
</el-form-item>
</el-col>
</el-row>
<el-col
:span=
"24"
>
<el-col
:span=
"12"
>
<el-form-item
label=
"备注"
prop=
"remark"
>
<el-input
v-model=
"formData.remark"
placeholder=
"请输入备注"
/>
<el-input
type=
"textarea"
v-model=
"formData.remark"
placeholder=
"请输入备注"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template
#
footer
>
<el-button
:disabled=
"formLoading"
type=
"primary"
@
click=
"submitForm"
>
确 定
</el-button>
...
...
src/views/crm/clue/detail/ClueDetailsHeader.vue
0 → 100644
View file @
0ae6139e
<
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
>
src/views/crm/clue/detail/ClueDetailsInfo.vue
0 → 100644
View file @
0ae6139e
<
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=
""
>
</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
>
src/views/crm/clue/detail/index.vue
0 → 100644
View file @
0ae6139e
<
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
>
src/views/crm/clue/index.vue
View file @
0ae6139e
...
...
@@ -17,6 +17,12 @@
class=
"!w-240px"
/>
</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-input
v-model=
"queryParams.mobile"
...
...
@@ -56,9 +62,19 @@
<!-- 列表 -->
<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"
>
<!-- 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"
>
<
template
#
default=
"scope"
>
<dict-tag
:type=
"DICT_TYPE.CRM_CUSTOMER_SOURCE"
:value=
"scope.row.source"
/>
...
...
@@ -146,11 +162,12 @@
</template>
<
script
setup
lang=
"ts"
>
import
{
DICT_TYPE
,
getBoolDictOptions
}
from
'@/utils/dict'
import
{
DICT_TYPE
}
from
'@/utils/dict'
import
{
dateFormatter
}
from
'@/utils/formatTime'
import
download
from
'@/utils/download'
import
*
as
ClueApi
from
'@/api/crm/clue'
import
ClueForm
from
'./ClueForm.vue'
import
{
TabsPaneContext
}
from
'element-plus'
defineOptions
({
name
:
'CrmClue'
})
...
...
@@ -163,12 +180,15 @@ const list = ref([]) // 列表的数据
const
queryParams
=
reactive
({
pageNo
:
1
,
pageSize
:
10
,
sceneType
:
'1'
,
// 默认和 activeName 相等
name
:
null
,
telephone
:
null
,
mobile
:
null
mobile
:
null
,
transformStatus
:
false
})
const
queryFormRef
=
ref
()
// 搜索的表单
const
exportLoading
=
ref
(
false
)
// 导出的加载中
const
activeName
=
ref
(
'1'
)
// 列表 tab
/** 查询列表 */
const
getList
=
async
()
=>
{
...
...
@@ -194,6 +214,18 @@ const resetQuery = () => {
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
openForm
=
(
type
:
string
,
id
?:
number
)
=>
{
...
...
src/views/crm/customer/detail/index.vue
View file @
0ae6139e
...
...
@@ -67,7 +67,6 @@
<el-tab-pane
label=
"操作日志"
>
<OperateLogV2
:log-list=
"logList"
/>
</el-tab-pane>
<el-tab-pane
label=
"回访"
lazy
>
TODO 待开发
</el-tab-pane>
</el-tabs>
</el-col>
...
...
src/views/crm/followup/index.vue
View file @
0ae6139e
...
...
@@ -11,6 +11,7 @@
<ContentWrap>
<el-table
v-loading=
"loading"
:data=
"list"
:show-overflow-tooltip=
"true"
:stripe=
"true"
>
<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=
"type"
>
<template
#
default=
"scope"
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment