客户详情完善

This commit is contained in:
puhui999 2024-01-06 12:29:35 +08:00
parent 7fd564307d
commit 17be1abb65
7 changed files with 209 additions and 176 deletions

View File

@ -69,11 +69,23 @@ export const queryAllList = async () => {
} }
// 查询客户操作日志 // 查询客户操作日志
export const getOperateLogPage = async (params: any) => { export const getOperateLogPage = async (id: number) => {
return await request.get({ url: '/crm/customer/operate-log-page', params }) return await request.get({ url: '/crm/customer/operate-log-page?id=' + id })
} }
//======================= 业务操作 =======================
// 锁定/解锁客户 // 锁定/解锁客户
export const lockCustomer = async (id: number, lockStatus: boolean) => { export const lockCustomer = async (id: number, lockStatus: boolean) => {
return await request.put({ url: `/crm/customer/lock`, data: { id, lockStatus } }) return await request.put({ url: `/crm/customer/lock`, data: { id, lockStatus } })
} }
// 领取公海客户
export const receive = async (ids: any[]) => {
return await request.put({ url: '/crm/customer/receive', params: { ids: ids.join(',') } })
}
// 客户放入公海
export const putPool = async (id: number) => {
return await request.put({ url: `/crm/customer/put-pool?id=${id}` })
}

View File

@ -58,15 +58,3 @@ export const deletePermissionBatch = async (params) => {
export const deleteSelfPermission = async (id) => { export const deleteSelfPermission = async (id) => {
return await request.delete({ url: '/crm/permission/quit-team?id=' + id }) return await request.delete({ url: '/crm/permission/quit-team?id=' + id })
} }
// TODO @puhui999调整下位置
// 领取公海数据
export const receive = async (data: { bizType: number; bizId: number }) => {
return await request.put({ url: `/crm/permission/receive`, data })
}
// TODO @puhui999调整下位置
// 数据放入公海
export const putPool = async (data: { bizType: number; bizId: number }) => {
return await request.put({ url: `/crm/permission/put-pool`, data })
}

View File

@ -2,36 +2,14 @@
<div class="p-20px"> <div class="p-20px">
<el-timeline> <el-timeline>
<el-timeline-item <el-timeline-item
v-for="(log, index) in logDataList" v-for="(log, index) in logList"
:key="index" :key="index"
:timestamp="formatDate(log.createTime)" :timestamp="formatDate(log.createTime)"
placement="top" placement="top"
> >
<div class="el-timeline-right-content"> <div class="el-timeline-right-content">
<el-row>
<el-col :span="24" class="mb-10px">
=======================
<el-tag class="mr-10px" type="success">{{ log.userName }}</el-tag> <el-tag class="mr-10px" type="success">{{ log.userName }}</el-tag>
<span>{{ log.title }}</span> {{ log.action }}
=======================
</el-col>
<!-- 先处理一下有几行-->
<template v-for="colNum in log.colSize" :key="colNum + 'col'">
<el-col :span="24" class="mb-10px">
<!-- 处理每一行-->
<template
v-for="(tagVal, index2) in log.tagsContentList.slice(
(colNum - 1) * 3,
3 * colNum
)"
:key="index2"
>
<el-tag class="mx-10px"> {{ tagVal }}</el-tag>
<span>{{ log.contentStrList[index2] }}</span>
</template>
</el-col>
</template>
</el-row>
</div> </div>
<template #dot> <template #dot>
<span :style="{ backgroundColor: getUserTypeColor(log.userType) }" class="dot-node-style"> <span :style="{ backgroundColor: getUserTypeColor(log.userType) }" class="dot-node-style">
@ -51,11 +29,13 @@ import { ElTag } from 'element-plus'
defineOptions({ name: 'OperateLogV2' }) defineOptions({ name: 'OperateLogV2' })
const props = defineProps<{ interface Props {
logList: OperateLogV2VO[] // logList: OperateLogV2VO[] //
}>() }
const logDataList = ref<OperateLogV2VO[]>([]) // withDefaults(defineProps<Props>(), {
logList: () => []
})
/** 获得 userType 颜色 */ /** 获得 userType 颜色 */
const getUserTypeColor = (type: number) => { const getUserTypeColor = (type: number) => {
@ -72,57 +52,6 @@ const getUserTypeColor = (type: number) => {
} }
return '#409EFF' return '#409EFF'
} }
// tag
const renderTags = (content: string) => {
let newStr = unref(content).slice() //
newStr = newStr.replaceAll('【】', '【空】').replaceAll('', '') //
const regex = /【([^【】]+)】/g
const fg = '|' //
let match: any[] | null
let matchStr: string[] = []
let oldStr: string[] = []
while ((match = regex.exec(newStr)) !== null) {
matchStr.push(match[1]) //
oldStr.push(match[0]) //
}
// while match
oldStr.forEach((item) => {
newStr = newStr.replace(item, fg)
})
return [newStr.split(fg), matchStr]
}
const initLog = () => {
logDataList.value = props.logList.map((logItem) => {
const keyValue = renderTags(logItem.action)
//
logItem.contentStrList = keyValue[0]
if (keyValue[0][0] === '从') {
logItem.title = logItem.subType
} else {
logItem.title = keyValue[0][0]
logItem.contentStrList.splice(0, 1)
}
logItem.colSize = keyValue[0].length / 3 //
logItem.tagsContentList = keyValue[1]
return logItem
})
}
watch(
() => props.logList.length,
(newObj) => {
if (newObj) {
initLog()
console.log(logDataList.value)
}
},
{
immediate: true,
deep: true
}
)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -11,19 +11,7 @@
</div> </div>
<div> <div>
<!-- 右上按钮 --> <!-- 右上按钮 -->
<el-button <slot></slot>
type="primary"
v-hasPermi="['crm:customer:update']"
@click="openForm(customer.id)"
>
编辑
</el-button>
<!-- TODO @puhui999转移的操作接入 -->
<el-button type="primary" @click="transfer">转移</el-button>
<!-- TODO @puhui999修改成交状态的接入 -->
<el-button>更改成交状态</el-button>
<el-button v-if="customer.lockStatus" @click="handleUnlock(customer.id!)">解锁</el-button>
<el-button v-else @click="handleLock(customer.id!)">锁定</el-button>
</div> </div>
</div> </div>
</div> </div>
@ -42,43 +30,14 @@
<el-descriptions-item label="首要联系人电话">{{ customer.mobile }}</el-descriptions-item> <el-descriptions-item label="首要联系人电话">{{ customer.mobile }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
</ContentWrap> </ContentWrap>
<!-- 表单弹窗添加/修改 -->
<CustomerForm ref="formRef" @success="emit('refresh')" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { DICT_TYPE } from '@/utils/dict' import { DICT_TYPE } from '@/utils/dict'
import * as CustomerApi from '@/api/crm/customer' import * as CustomerApi from '@/api/crm/customer'
import CustomerForm from '../CustomerForm.vue'
defineOptions({ name: 'CustomerDetailsHeader' }) defineOptions({ name: 'CustomerDetailsHeader' })
defineProps<{
const { customer, loading } = defineProps<{
customer: CustomerApi.CustomerVO // customer: CustomerApi.CustomerVO //
loading: boolean // loading: boolean //
}>() }>()
const message = useMessage() //
/** 修改操作 */
const formRef = ref()
const openForm = (id?: number) => {
formRef.value.open('update', id)
}
/** 锁定操作 */
const handleLock = async (id: number) => {
await CustomerApi.lockCustomer(id, true)
message.success('锁定成功')
emit('refresh')
}
/** 解锁操作 */
const handleUnlock = async (id: number) => {
console.log(customer, '=======')
await CustomerApi.lockCustomer(id, false)
message.success('解锁成功')
emit('refresh')
}
const emit = defineEmits(['refresh']) // success
</script> </script>

View File

@ -1,5 +1,19 @@
<template> <template>
<CustomerDetailsHeader :customer="customer" :loading="loading" @refresh="getCustomer(id)" /> <CustomerDetailsHeader :customer="customer" :loading="loading">
<el-button @click="close">返回</el-button>
<!-- TODO puhui999: 按钮数据权限收尾统一完善需要按权限分级和客户状态来动态显示匹配的按钮 -->
<el-button v-hasPermi="['crm:customer:update']" type="primary" @click="openForm">
编辑
</el-button>
<!-- TODO @puhui999转移的操作接入 -->
<el-button type="primary" @click="transfer">转移</el-button>
<!-- TODO @puhui999修改成交状态的接入 -->
<el-button>更改成交状态</el-button>
<el-button v-if="customer.lockStatus" @click="handleUnlock">解锁</el-button>
<el-button v-if="!customer.lockStatus" @click="handleLock">锁定</el-button>
<el-button v-if="!customer.ownerUserId" type="primary" @click="receive">领取客户</el-button>
<el-button v-if="customer.ownerUserId" type="primary" @click="putPool">客户放入公海</el-button>
</CustomerDetailsHeader>
<el-col> <el-col>
<el-tabs> <el-tabs>
<el-tab-pane label="详细资料"> <el-tab-pane label="详细资料">
@ -11,7 +25,7 @@
<el-tab-pane label="联系人" lazy> <el-tab-pane label="联系人" lazy>
<ContactList :biz-id="customer.id!" :biz-type="BizTypeEnum.CRM_CUSTOMER" /> <ContactList :biz-id="customer.id!" :biz-type="BizTypeEnum.CRM_CUSTOMER" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="团队成员" lazy> <el-tab-pane label="团队成员">
<PermissionList :biz-id="customer.id!" :biz-type="BizTypeEnum.CRM_CUSTOMER" /> <PermissionList :biz-id="customer.id!" :biz-type="BizTypeEnum.CRM_CUSTOMER" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="商机" lazy> <el-tab-pane label="商机" lazy>
@ -27,10 +41,14 @@
<el-tab-pane label="回访" lazy>TODO 待开发</el-tab-pane> <el-tab-pane label="回访" lazy>TODO 待开发</el-tab-pane>
</el-tabs> </el-tabs>
</el-col> </el-col>
<!-- 表单弹窗添加/修改 -->
<CustomerForm ref="formRef" @success="getCustomer" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useTagsViewStore } from '@/store/modules/tagsView' import { useTagsViewStore } from '@/store/modules/tagsView'
import * as CustomerApi from '@/api/crm/customer' import * as CustomerApi from '@/api/crm/customer'
import CustomerForm from '@/views/crm/customer/CustomerForm.vue'
import CustomerDetailsInfo from './CustomerDetailsInfo.vue' // - import CustomerDetailsInfo from './CustomerDetailsInfo.vue' // -
import CustomerDetailsHeader from './CustomerDetailsHeader.vue' // - import CustomerDetailsHeader from './CustomerDetailsHeader.vue' // -
import ContactList from '@/views/crm/contact/components/ContactList.vue' // import ContactList from '@/views/crm/contact/components/ContactList.vue' //
@ -40,48 +58,86 @@ import ReceivableList from '@/views/crm/receivable/components/ReceivableList.vue
import ReceivablePlanList from '@/views/crm/receivable/plan/components/ReceivablePlanList.vue' // import ReceivablePlanList from '@/views/crm/receivable/plan/components/ReceivablePlanList.vue' //
import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // import PermissionList from '@/views/crm/permission/components/PermissionList.vue' //
import { BizTypeEnum } from '@/api/crm/permission' import { BizTypeEnum } from '@/api/crm/permission'
import { OperateLogV2VO } from '@/api/system/operatelog' import type { OperateLogV2VO } from '@/api/system/operatelog'
defineOptions({ name: 'CrmCustomerDetail' }) defineOptions({ name: 'CrmCustomerDetail' })
const route = useRoute() const customerId = ref(0) //
const id = Number(route.params.id) //
const loading = ref(true) // const loading = ref(true) //
const message = useMessage() //
const { delView } = useTagsViewStore() //
const { currentRoute, push } = useRouter() //
/** 获取详情 */ /** 获取详情 */
const customer = ref<CustomerApi.CustomerVO>({} as CustomerApi.CustomerVO) // const customer = ref<CustomerApi.CustomerVO>({} as CustomerApi.CustomerVO) //
const getCustomer = async (id: number) => { const getCustomer = async () => {
loading.value = true loading.value = true
try { try {
customer.value = await CustomerApi.getCustomer(id) customer.value = await CustomerApi.getCustomer(customerId.value)
await getOperateLog(id) await getOperateLog()
} finally { } finally {
loading.value = false loading.value = false
} }
} }
const formRef = ref<InstanceType<typeof CustomerForm>>() // Ref
//
const openForm = () => {
formRef.value?.open('update', customerId.value)
}
//
const transfer = () => {}
//
const handleLock = async () => {
await message.confirm(`确定锁定客户【${customer.value.name}】 吗?`)
await CustomerApi.lockCustomer(unref(customerId.value), true)
message.success(`锁定客户【${customer.value.name}】成功`)
await getCustomer()
}
//
const handleUnlock = async () => {
await message.confirm(`确定解锁客户【${customer.value.name}】 吗?`)
await CustomerApi.lockCustomer(unref(customerId.value), false)
message.success(`解锁客户【${customer.value.name}】成功`)
await getCustomer()
}
//
const receive = async () => {
await message.confirm(`确定领取客户【${customer.value.name}】 吗?`)
await CustomerApi.receive([unref(customerId.value)])
message.success(`领取客户【${customer.value.name}】成功`)
await getCustomer()
}
//
const putPool = async () => {
await message.confirm(`确定将客户【${customer.value.name}】放入公海吗?`)
await CustomerApi.putPool(unref(customerId.value))
message.success(`客户【${customer.value.name}】放入公海成功`)
close()
}
const logList = ref<OperateLogV2VO[]>([]) // const logList = ref<OperateLogV2VO[]>([]) //
/** /**
* 获取操作日志 * 获取操作日志
*/ */
const getOperateLog = async (customerId: number) => { const getOperateLog = async () => {
if (!customerId) { if (!customerId.value) {
return return
} }
const data = await CustomerApi.getOperateLogPage({ const data = await CustomerApi.getOperateLogPage(customerId.value)
pageNo: 1,
pageSize: 10,
bizId: customerId
})
logList.value = data.list logList.value = data.list
} }
/** 初始化 */ const close = () => {
const { delView } = useTagsViewStore() //
const { currentRoute } = useRouter() //
onMounted(() => {
if (!id) {
ElMessage.warning('参数错误,客户不能为空!')
delView(unref(currentRoute)) delView(unref(currentRoute))
// TODO
push({ name: 'CrmCustomer' })
}
/** 初始化 */
const { params } = useRoute()
onMounted(() => {
if (!params.id) {
ElMessage.warning('参数错误,客户不能为空!')
close()
return return
} }
getCustomer(id) customerId.value = params.id as unknown as number
getCustomer()
}) })
</script> </script>

View File

@ -72,10 +72,17 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button @click="handleQuery"> <Icon class="mr-5px" icon="ep:search" /> 搜索 </el-button> <el-button @click="handleQuery">
<el-button @click="resetQuery"> <Icon class="mr-5px" icon="ep:refresh" />重置 </el-button> <Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery(undefined)">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
<el-button v-hasPermi="['crm:customer:create']" type="primary" @click="openForm('create')"> <el-button v-hasPermi="['crm:customer:create']" type="primary" @click="openForm('create')">
<Icon class="mr-5px" icon="ep:plus" /> 新增 <Icon class="mr-5px" icon="ep:plus" />
新增
</el-button> </el-button>
<el-button <el-button
v-hasPermi="['crm:customer:export']" v-hasPermi="['crm:customer:export']"
@ -84,7 +91,8 @@
type="success" type="success"
@click="handleExport" @click="handleExport"
> >
<Icon class="mr-5px" icon="ep:download" /> 导出 <Icon class="mr-5px" icon="ep:download" />
导出
</el-button> </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
@ -92,11 +100,19 @@
<!-- 列表 --> <!-- 列表 -->
<ContentWrap> <ContentWrap>
<el-tabs v-model="activeName" @tab-click="handleClick">
<el-tab-pane label="客户列表" name="1" />
<el-tab-pane label="我负责人的" name="2" />
<el-tab-pane label="我关注的" name="3" />
<el-tab-pane label="我参与的" name="4" />
<el-tab-pane label="下属负责的" name="5" />
<el-tab-pane label="客户公海" name="6" />
</el-tabs>
<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" />
<el-table-column align="center" label="客户名称" prop="name" width="160"> <el-table-column align="center" label="客户名称" prop="name" width="160">
<template #default="scope"> <template #default="scope">
<el-link type="primary" :underline="false" @click="openDetail(scope.row.id)"> <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
{{ scope.row.name }} {{ scope.row.name }}
</el-link> </el-link>
</template> </template>
@ -197,6 +213,7 @@ import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download' import download from '@/utils/download'
import * as CustomerApi from '@/api/crm/customer' import * as CustomerApi from '@/api/crm/customer'
import CustomerForm from './CustomerForm.vue' import CustomerForm from './CustomerForm.vue'
import { TabsPaneContext } from 'element-plus'
defineOptions({ name: 'CrmCustomer' }) defineOptions({ name: 'CrmCustomer' })
@ -206,24 +223,75 @@ const { t } = useI18n() // 国际化
const loading = ref(true) // const loading = ref(true) //
const total = ref(0) // const total = ref(0) //
const list = ref([]) // const list = ref([]) //
const queryParams = reactive({ const queryParams = ref<{
pageNo: number
pageSize: number
name: string
mobile: string
industryId: number | undefined
level: number | undefined
source: number | undefined
sceneType: number | undefined
pool: boolean | undefined
}>({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
pool: false,
name: '', name: '',
mobile: '', mobile: '',
industryId: undefined, industryId: undefined,
level: undefined, level: undefined,
source: undefined source: undefined,
sceneType: undefined,
pool: undefined
}) })
const queryFormRef = ref() // const queryFormRef = ref() //
const exportLoading = ref(false) // const exportLoading = ref(false) //
const activeName = ref('1') // tab
enum CrmSceneTypeEnum {
OWNER = 1,
FOLLOW = 2,
INVOLVED = 3,
SUBORDINATE = 4
}
const handleClick = (tab: TabsPaneContext) => {
switch (tab.paneName) {
case '1':
resetQuery()
break
case '2':
resetQuery(() => {
queryParams.value.sceneType = CrmSceneTypeEnum.OWNER
})
break
case '3':
resetQuery(() => {
queryParams.value.sceneType = CrmSceneTypeEnum.FOLLOW
})
break
case '4':
resetQuery(() => {
queryParams.value.sceneType = CrmSceneTypeEnum.INVOLVED
})
break
case '5':
resetQuery(() => {
queryParams.value.sceneType = CrmSceneTypeEnum.SUBORDINATE
})
break
case '6':
resetQuery(() => {
queryParams.value.pool = true
})
break
}
}
/** 查询列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
const data = await CustomerApi.getCustomerPage(queryParams) const data = await CustomerApi.getCustomerPage(queryParams.value)
list.value = data.list list.value = data.list
total.value = data.total total.value = data.total
} finally { } finally {
@ -233,19 +301,30 @@ const getList = async () => {
/** 搜索按钮操作 */ /** 搜索按钮操作 */
const handleQuery = () => { const handleQuery = () => {
queryParams.pageNo = 1 queryParams.value.pageNo = 1
getList() getList()
} }
/** 重置按钮操作 */ /** 重置按钮操作 */
const resetQuery = () => { const resetQuery = (func: Function | undefined = undefined) => {
queryFormRef.value.resetFields() queryFormRef.value.resetFields()
queryParams.pool = false queryParams.value = {
pageNo: 1,
pageSize: 10,
name: '',
mobile: '',
industryId: undefined,
level: undefined,
source: undefined,
sceneType: undefined,
pool: undefined
}
func && func()
handleQuery() handleQuery()
} }
/** 打开客户详情 */ /** 打开客户详情 */
const { push } = useRouter() const { currentRoute, push } = useRouter()
const openDetail = (id: number) => { const openDetail = (id: number) => {
push({ name: 'CrmCustomerDetail', params: { id } }) push({ name: 'CrmCustomerDetail', params: { id } })
} }
@ -276,14 +355,20 @@ const handleExport = async () => {
await message.exportConfirm() await message.exportConfirm()
// //
exportLoading.value = true exportLoading.value = true
const data = await CustomerApi.exportCustomer(queryParams) const data = await CustomerApi.exportCustomer(queryParams.value)
download.excel(data, '客户.xls') download.excel(data, '客户.xls')
} catch { } catch {
} finally { } finally {
exportLoading.value = false exportLoading.value = false
} }
} }
//
watch(
() => currentRoute.value,
() => {
getList()
}
)
/** 初始化 **/ /** 初始化 **/
onMounted(() => { onMounted(() => {
getList() getList()

View File

@ -2,7 +2,8 @@
<!-- 操作栏 --> <!-- 操作栏 -->
<el-row justify="end"> <el-row justify="end">
<el-button @click="openForm"> <el-button @click="openForm">
<Icon class="mr-5px" icon="fluent:people-team-add-20-filled" /> 添加团队成员 <Icon class="mr-5px" icon="fluent:people-team-add-20-filled" />
添加团队成员
</el-button> </el-button>
<el-button @click="handleUpdate"> <el-button @click="handleUpdate">
<Icon class="mr-5px" icon="ep:edit" /> <Icon class="mr-5px" icon="ep:edit" />
@ -105,14 +106,14 @@ const handleDelete = async () => {
message.warning('请先选择团队成员后操作!') message.warning('请先选择团队成员后操作!')
return return
} }
// TODO @puhui999 await message.delConfirm('是否删除选择的团队成员?')
await message.delConfirm()
const ids = multipleSelection.value?.map((item) => item.id) const ids = multipleSelection.value?.map((item) => item.id)
await PermissionApi.deletePermissionBatch({ await PermissionApi.deletePermissionBatch({
bizType: props.bizType, bizType: props.bizType,
bizId: props.bizId, bizId: props.bizId,
ids ids
}) })
message.success('删除成功')
} }
/** 退出团队 */ /** 退出团队 */
@ -125,7 +126,7 @@ const handleQuit = async () => {
message.warning('负责人不能退出团队!') message.warning('负责人不能退出团队!')
return return
} }
// TODO @puhui999 await message.confirm('确认退出团队吗?')
const userPermission = list.value.find((item) => item.userId === userStore.getUser.id) const userPermission = list.value.find((item) => item.userId === userStore.getUser.id)
await PermissionApi.deleteSelfPermission(userPermission?.id) await PermissionApi.deleteSelfPermission(userPermission?.id)
} }
@ -133,7 +134,10 @@ const handleQuit = async () => {
/** 监听打开的 bizId + bizType从而加载最新的列表 */ /** 监听打开的 bizId + bizType从而加载最新的列表 */
watch( watch(
() => [props.bizId, props.bizType], () => [props.bizId, props.bizType],
() => { (val) => {
if (!val[0]) {
return
}
getList() getList()
}, },
{ immediate: true, deep: true } { immediate: true, deep: true }