feat: 客户详情 + review 修改

(cherry picked from commit 23f483ac45)
This commit is contained in:
Wanwan 2023-11-04 03:21:01 +08:00 committed by shizhong
parent e65a853cda
commit f780cf0c7b
9 changed files with 508 additions and 107 deletions

View File

@ -1,13 +1,14 @@
import request from '@/config/axios'
export interface CustomerVO {
id: number
id?: number
name: string
industryId: number
level: number
source: number
followUpStatus: boolean
lockStatus: boolean
followUpStatus?: boolean
lockStatus?: boolean
dealStatus?: boolean
mobile: string
telephone: string
website: string
@ -16,13 +17,20 @@ export interface CustomerVO {
email: string
description: string
remark: string
ownerUserId: number
roUserIds: string
rwUserIds: string
areaId: number
ownerUserId?: number
ownerUserName?: string
ownerUserDept?: string
roUserIds?: string
rwUserIds?: string
areaId?: number
areaName?: string
detailAddress: string
contactLastTime: Date
contactLastTime?: Date
contactNextTime: Date
createTime?: Date
updateTime?: Date
creator?: string
creatorName?: string
}
// 查询客户列表

View File

@ -487,6 +487,24 @@ const remainingRouter: AppRouteRecordRaw[] = [
component: () => import('@/views/mall/promotion/diy/page/decorate.vue')
}
]
},
{
path: '/crm',
component: Layout,
name: 'CrmCenter',
meta: { hidden: true },
children: [
{
path: 'customer/detail/:id',
name: 'CrmCustomerDetail',
meta: {
title: '客户详情',
noCache: true,
hidden: true
},
component: () => import('@/views/crm/customer/detail/index.vue')
}
]
}
]

View File

@ -10,7 +10,7 @@
<el-form-item label="线索名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入线索名称" />
</el-form-item>
<!-- TODO 客户选择 -->
<!-- TODO wanwan 客户选择 -->
<el-form-item label="客户" prop="customerId">
<el-input v-model="formData.customerId" placeholder="请选择客户" />
</el-form-item>
@ -31,7 +31,7 @@
<el-form-item label="地址" prop="address">
<el-input v-model="formData.address" placeholder="请输入地址" />
</el-form-item>
<!-- TODO 负责人选择 -->
<!-- TODO wanwan 负责人选择 -->
<el-form-item label="负责人" prop="ownerUserId">
<el-input v-model="formData.ownerUserId" placeholder="请输入负责人" />
</el-form-item>
@ -46,7 +46,6 @@
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE, getBoolDictOptions } from '@/utils/dict'
import * as ClueApi from '@/api/crm/clue'
const { t } = useI18n() //

View File

@ -96,7 +96,7 @@
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center">
<el-table-column label="操作" align="center" min-width="110" fixed="right">
<template #default="scope">
<el-button
link

View File

@ -8,10 +8,12 @@
v-loading="formLoading"
>
<el-row>
<!-- TODO @wanwanname 参数校验必填 -->
<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="industryId">
<el-select v-model="formData.industryId" placeholder="请选择所属行业">
<el-option
@ -22,8 +24,10 @@
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="客户来源" prop="source">
<el-select v-model="formData.source" placeholder="请选择客户来源">
<el-option
@ -34,6 +38,8 @@
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户等级" prop="level">
<el-select v-model="formData.level" placeholder="请选择客户等级">
<el-option
@ -44,42 +50,73 @@
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="手机" prop="mobile">
<el-input v-model="formData.mobile" placeholder="请输入手机" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="电话" prop="telephone">
<el-input v-model="formData.telephone" placeholder="请输入电话" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="formData.email" placeholder="请输入邮箱" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="QQ" prop="qq">
<el-input v-model="formData.qq" placeholder="请输入QQ" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="微信" prop="wechat">
<el-input v-model="formData.wechat" placeholder="请输入微信" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="网址" prop="website">
<el-input v-model="formData.website" placeholder="请输入网址" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<!-- TODO @wanwan地区的多级选择可以参考 UserForm.vue 所在地 areaId -->
<el-form-item label="地区编号" prop="areaId">
<el-input v-model="formData.areaId" placeholder="请输入地区编号" />
<el-col :span="12">
<el-form-item label="所在地" prop="areaId">
<el-tree-select
v-model="formData.areaId"
:data="areaList"
:props="defaultProps"
:render-after-expand="true"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="详细地址" prop="detailAddress">
<el-input v-model="formData.detailAddress" placeholder="请输入详细地址" />
</el-form-item>
</el-col>
</el-row>
<!-- TODO @wanwan 少一个负责人字段默认先选中自己可以先参考 TaskAssignRuleForm.vue formData.userIds注意新增的时候可以选择修改的时候只展示 -->
<el-form-item v-if="formType === 'create'" label="负责人" prop="userIds" span="24">
<el-select v-model="formData.ownerUserId">
<el-option
v-for="item in userOptions"
:key="parseInt(item.id)"
:label="item.nickname"
:value="parseInt(item.id)"
/>
</el-select>
</el-form-item>
<el-row>
<el-col :span="12">
<el-form-item label="下次联系时间" prop="contactNextTime">
<el-date-picker
v-model="formData.contactNextTime"
@ -88,13 +125,18 @@
placeholder="选择下次联系时间"
/>
</el-form-item>
</el-col>
</el-row>
<el-col :span="24">
<el-form-item label="客户描述" prop="description">
<el-input v-model="formData.description" placeholder="请输入客户描述" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
@ -105,6 +147,10 @@
<script setup lang="ts">
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
import * as CustomerApi from '@/api/crm/customer'
import * as AreaApi from '@/api/system/area'
import { defaultProps } from '@/utils/tree'
import * as UserApi from '@/api/system/user'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
const { t } = useI18n() //
const message = useMessage() //
@ -113,6 +159,8 @@ const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const areaList = ref([]) //
const userOptions = ref<UserApi.UserVO[]>([]) //
const formData = ref({
id: undefined,
name: undefined,
@ -129,10 +177,12 @@ const formData = ref({
remark: undefined,
areaId: undefined,
detailAddress: undefined,
contactNextTime: undefined
contactNextTime: undefined,
ownerUserId: undefined
})
const formRules = reactive({
name: [{ require: true, message: '客户名称不能为空', trigger: 'blur' }]
name: [{ required: true, message: '客户名称不能为空', trigger: 'blur' }],
ownerUserId: [{ required: true, message: '负责人不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
@ -151,6 +201,16 @@ const open = async (type: string, id?: number) => {
formLoading.value = false
}
}
//
areaList.value = await AreaApi.getAreaTree()
//
userOptions.value = await UserApi.getSimpleUserList()
//
if (formType.value === 'create') {
const { wsCache } = useCache()
const user = wsCache.get(CACHE_KEY.USER).user
formData.value.ownerUserId = user.id
}
}
defineExpose({ open }) // open
@ -198,7 +258,8 @@ const resetForm = () => {
remark: undefined,
areaId: undefined,
detailAddress: undefined,
contactNextTime: undefined
contactNextTime: undefined,
ownerUserId: undefined
}
formRef.value?.resetFields()
}

View File

@ -0,0 +1,18 @@
<template>
<el-col> 客户 </el-col>
<el-col>
<el-row>
<span class="text-xl font-bold">{{ customer.name }}</span>
</el-row>
</el-col>
<el-col class="mt-10px">
<Icon icon="ant-design:tag-filled" />
<!-- TODO 标签 -->
</el-col>
</template>
<script setup lang="ts">
import * as CustomerApi from '@/api/crm/customer'
const { customer } = defineProps<{ customer: CustomerApi.CustomerVO }>()
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,91 @@
<template>
<el-collapse v-model="activeNames">
<el-collapse-item name="basicInfo">
<template #title>
<span class="text-base font-bold">基本信息</span>
</template>
<el-descriptions :column="2">
<el-descriptions-item label="客户名称">
{{ customer.name }}
</el-descriptions-item>
<el-descriptions-item label="所属行业">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="customer.industryId" />
</el-descriptions-item>
<el-descriptions-item label="客户来源">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="customer.source" />
</el-descriptions-item>
<el-descriptions-item label="客户等级">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="customer.level" />
</el-descriptions-item>
<el-descriptions-item label="手机">
{{ customer.mobile }}
</el-descriptions-item>
<el-descriptions-item label="电话">
{{ customer.telephone }}
</el-descriptions-item>
<el-descriptions-item label="邮箱">
{{ customer.email }}
</el-descriptions-item>
<el-descriptions-item label="QQ">
{{ customer.qq }}
</el-descriptions-item>
<el-descriptions-item label="微信">
{{ customer.wechat }}
</el-descriptions-item>
<el-descriptions-item label="网址">
{{ customer.website }}
</el-descriptions-item>
<el-descriptions-item label="所在地">
{{ customer.areaName }}
</el-descriptions-item>
<el-descriptions-item label="详细地址">
{{ customer.detailAddress }}
</el-descriptions-item>
<el-descriptions-item label="下次联系时间">
{{ customer.contactNextTime ? formatDate(customer.contactNextTime, 'YYYY-MM-DD') : '空' }}
</el-descriptions-item>
</el-descriptions>
<el-descriptions :column="1">
<el-descriptions-item label="客户描述">
{{ customer.description }}
</el-descriptions-item>
<el-descriptions-item label="备注">
{{ customer.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="负责人">
{{ customer.ownerUserName }}
</el-descriptions-item>
<el-descriptions-item label="创建人">
{{ customer.creatorName }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ customer.createTime ? formatDate(customer.createTime) : '空' }}
</el-descriptions-item>
<el-descriptions-item label="更新时间">
{{ customer.updateTime ? formatDate(customer.updateTime) : '空' }}
</el-descriptions-item>
<el-descriptions-item label="最后跟进时间">
{{ customer.contactLastTime ? formatDate(customer.contactLastTime) : '空' }}
</el-descriptions-item>
</el-descriptions>
</el-collapse-item>
</el-collapse>
</template>
<script setup lang="ts">
import * as CustomerApi from '@/api/crm/customer'
import { DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime'
const { customer } = defineProps<{ customer: CustomerApi.CustomerVO }>()
//
const activeNames = ref(['basicInfo', 'systemInfo'])
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,154 @@
<template>
<div v-loading="loading">
<div class="flex items-start justify-between">
<div>
<!-- 左上客户基本信息 -->
<CustomerBasicInfo :customer="customer" />
</div>
<div>
<!-- 右上按钮 -->
<el-button @click="openForm('update', customer.id)" v-hasPermi="['crm:customer:update']">
编辑
</el-button>
<el-button>更改成交状态</el-button>
</div>
</div>
<el-row class="mt-10px">
<el-button>
<Icon icon="ph:calendar-fill" class="mr-5px" />
创建任务
</el-button>
<el-button>
<Icon icon="carbon:email" class="mr-5px" />
发送邮件
</el-button>
<el-button>
<Icon icon="system-uicons:contacts" class="mr-5px" />
创建联系人
</el-button>
<el-button>
<Icon icon="ep:opportunity" class="mr-5px" />
创建商机
</el-button>
<el-button>
<Icon icon="clarity:contract-line" class="mr-5px" />
创建合同
</el-button>
<el-button>
<Icon icon="icon-park:income-one" class="mr-5px" />
创建回款
</el-button>
<el-button>
<Icon icon="fluent:people-team-add-20-filled" class="mr-5px" />
添加团队成员
</el-button>
</el-row>
</div>
<ContentWrap class="mt-10px">
<el-descriptions :column="5" direction="vertical">
<el-descriptions-item label="客户级别">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="customer.level" />
</el-descriptions-item>
<el-descriptions-item label="成交状态">
{{ customer.dealStatus ? '已成交' : '未成交' }}
</el-descriptions-item>
<el-descriptions-item label="负责人">
{{ customer.ownerUserName }}
</el-descriptions-item>
<!-- TODO wanwan 首要联系人? -->
<el-descriptions-item label="首要联系人" />
<!-- TODO wanwan 首要联系人电话? -->
<el-descriptions-item label="首要联系人电话">
{{ customer.mobile }}
</el-descriptions-item>
</el-descriptions>
</ContentWrap>
<el-col :span="18">
<el-tabs>
<el-tab-pane label="详细资料">
<CustomerDetails class="ml-2" :customer="customer" />
</el-tab-pane>
<el-tab-pane label="活动" lazy> 活动</el-tab-pane>
<el-tab-pane label="邮件" lazy> 邮件</el-tab-pane>
<el-tab-pane label="工商信息" lazy> 工商信息</el-tab-pane>
<el-tab-pane label="客户关系" lazy> 客户关系</el-tab-pane>
<!-- TODO wanwan 以下标签上的数量需要接口统计返回 -->
<el-tab-pane label="联系人" lazy>
<template #label> 联系人<el-badge :value="12" class="item" type="primary" /> </template>
联系人
</el-tab-pane>
<el-tab-pane label="团队成员" lazy>
<template #label> 团队成员<el-badge :value="2" class="item" type="primary" /> </template>
团队成员
</el-tab-pane>
<el-tab-pane label="商机" lazy> 商机</el-tab-pane>
<el-tab-pane label="合同" lazy>
<template #label> 合同<el-badge :value="3" class="item" type="primary" /> </template>
合同
</el-tab-pane>
<el-tab-pane label="回款" lazy>
<template #label> 回款<el-badge :value="4" class="item" type="primary" /> </template>
回款
</el-tab-pane>
<el-tab-pane label="回访" lazy> 回访</el-tab-pane>
<el-tab-pane label="发票" lazy> 发票</el-tab-pane>
</el-tabs>
</el-col>
<!-- 表单弹窗添加/修改 -->
<CustomerForm ref="formRef" @success="getCustomerData(id)" />
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { useTagsViewStore } from '@/store/modules/tagsView'
import * as CustomerApi from '@/api/crm/customer'
import CustomerBasicInfo from '@/views/crm/customer/detail/CustomerBasicInfo.vue'
import { DICT_TYPE } from '@/utils/dict'
import CustomerDetails from '@/views/crm/customer/detail/CustomerDetails.vue'
import CustomerForm from '@/views/crm/customer/CustomerForm.vue'
defineOptions({ name: 'CustomerDetail' })
const { delView } = useTagsViewStore() //
const route = useRoute()
const { currentRoute } = useRouter() //
const id = Number(route.params.id)
const loading = ref(true) //
//
const customer = ref<CustomerApi.CustomerVO>({} as CustomerApi.CustomerVO)
/**
* 获取详情
*
* @param id
*/
const getCustomerData = async (id: number) => {
loading.value = true
try {
customer.value = await CustomerApi.getCustomer(id)
} finally {
loading.value = false
}
}
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/**
* 初始化
*/
onMounted(() => {
if (!id) {
ElMessage.warning('参数错误,客户不能为空!')
delView(unref(currentRoute))
return
}
getCustomerData(id)
})
</script>
<style scoped lang="scss"></style>

View File

@ -1,7 +1,6 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<!-- TODO @wanwan看看怎么表单可以对齐一点 -->
<el-form
class="-mb-15px"
:model="queryParams"
@ -27,7 +26,51 @@
class="!w-240px"
/>
</el-form-item>
<!-- TODO @wanwan筛选字段加所属行业客户等级客户来源 -->
<el-form-item label="所属行业" prop="industryId">
<el-select
v-model="queryParams.industryId"
placeholder="请选择所属行业"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="客户等级" prop="level">
<el-select
v-model="queryParams.level"
placeholder="请选择客户等级"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="客户来源" prop="source">
<el-select
v-model="queryParams.source"
placeholder="请选择客户来源"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</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>
@ -48,7 +91,6 @@
</ContentWrap>
<!-- 列表 -->
<!-- TODO @wanwan列表按照 docs 的再改下 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="编号" align="center" prop="id" />
@ -70,8 +112,9 @@
</el-table-column>
<el-table-column label="手机" align="center" prop="mobile" width="120" />
<el-table-column label="详细地址" align="center" prop="detailAddress" width="200" />
<!-- TODO @Wanwan 负责人回显所属部门创建人 -->
<el-table-column label="负责人" align="center" prop="ownerUserId" />
<el-table-column label="负责人" align="center" prop="ownerUserName" />
<el-table-column label="所属部门" align="center" prop="ownerUserDept" />
<el-table-column label="创建人" align="center" prop="creatorName" />
<el-table-column
label="创建时间"
align="center"
@ -104,9 +147,9 @@
</template>
</el-table-column>
<!-- TODO @wanwan 距进入公海天数 -->
<!-- TODO @wanwan操作要 fixed 再右侧哈 -->
<el-table-column label="操作" align="center" width="160">
<el-table-column label="操作" align="center" min-width="150" fixed="right">
<template #default="scope">
<el-button link type="primary" @click="openDetail(scope.row.id)">详情</el-button>
<el-button
link
type="primary"
@ -140,7 +183,7 @@
</template>
<script setup lang="ts">
import { DICT_TYPE, getBoolDictOptions } from '@/utils/dict'
import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import * as CustomerApi from '@/api/crm/customer'
@ -158,7 +201,10 @@ const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: null,
mobile: null
mobile: null,
industryId: null,
level: null,
source: null
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
@ -187,6 +233,12 @@ const resetQuery = () => {
handleQuery()
}
/** 打开客户详情 */
const { push } = useRouter()
const openDetail = (id: number) => {
push({ name: 'CrmCustomerDetail', params: { id } })
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {