客户详情页处理完成

This commit is contained in:
jiangjunhong 2025-04-25 22:42:29 +08:00
parent 2cc4715ae0
commit f06f1f29c9
16 changed files with 443 additions and 565 deletions

View File

@ -22,9 +22,9 @@ export interface CustomerInforVO {
ownerUserId: number // 负责人ID
ownerTime: Date // 成为负责人时间
depId: number // 部门ID
contactLastTime: Date // 最后跟进时间
contactLastContent: string // 最后跟进内容
contactNextTime: Date // 下次联系时间
latestFollowTime: Date // 最后跟进时间
latestFollowContent: string // 最后跟进内容
nextFollowTime: Date // 下次联系时间
lockStatus: number // 锁定状态0:未锁1:锁定)
openSeaId: number // 公海ID
}

View File

@ -9,33 +9,15 @@ export interface CustomerLabelsVO {
// 客户标签 API
export const CustomerLabelsApi = {
// 查询客户标签分页
getCustomerLabelsPage: async (params: any) => {
return await request.get({ url: `/crm/customer-labels/page`, params })
},
// 查询客户标签详情
getCustomerLabels: async (id: number) => {
return await request.get({ url: `/crm/customer-labels/get?id=` + id })
},
getCustomerLabelsList: async (params: any) => {
return await request.get({ url: `/crm/customer-labels/list`, params })
},
// 新增客户标签
createCustomerLabels: async (data: CustomerLabelsVO) => {
return await request.post({ url: `/crm/customer-labels/create`, data })
createCustomerLabels: async (params: any) => {
return await request.post({ url: `/crm/customer-labels/create`, params })
},
// 修改客户标签
updateCustomerLabels: async (data: CustomerLabelsVO) => {
return await request.put({ url: `/crm/customer-labels/update`, data })
},
// 删除客户标签
deleteCustomerLabels: async (id: number) => {
return await request.delete({ url: `/crm/customer-labels/delete?id=` + id })
},
// 导出客户标签 Excel
exportCustomerLabels: async (params) => {
return await request.download({ url: `/crm/customer-labels/export-excel`, params })
}
}

View File

@ -5,9 +5,7 @@ export interface FollwRecordVO {
id: number // 唯一标识
customerId: number // 客户ID
content: string // 跟进内容
createTime: string // 创建时间
creator: string // 创建人
userId: string // 客户经理
userId: number // 客户经理
}
// 跟进记录 API
@ -17,6 +15,11 @@ export const FollwRecordApi = {
return await request.get({ url: `/crm/follw-record/page`, params })
},
// 新增跟进记录
createFollwRecord: async (data: FollwRecordVO) => {
return await request.post({ url: `/crm/follw-record/create`, data })
},
// 查询跟进记录详情
getFollwRecord: async (id: number) => {
return await request.get({ url: `/crm/follw-record/get?id=` + id })

View File

@ -3,13 +3,32 @@
<!-- 顶部标题和操作按钮区域 -->
<div class="flex justify-between mb-2">
<div class="flex items-center">
<h2 class="text-xl font-bold mr-10px min-w-[100px]" >{{ customerForm.customerName }}({{ customerForm.mobile }})</h2>
<h2 >{{ customerForm.customerName }}({{ customerForm.mobile }})</h2>
<span class="ml-2px text-sm">撞库次数: {{ customerForm.repeatCount }}</span>
<span class="text-sm ml-2">跟进次数: {{ customerForm.followCount }}</span>
</div>
<div class="flex">
<el-button size="small" class="mr-1" :disabled="!props.prevId" @click="emit('prev')">上一条</el-button>
<el-button size="small" :disabled="!props.nextId" @click="emit('next')">下一条</el-button>
<el-button
size="small"
class="mr-2"
type="primary"
plain
:disabled="!props.prevId"
@click="emit('prev')"
>
<Icon icon="ep:arrow-left" class="mr-1" />
上一条
</el-button>
<el-button
size="small"
type="primary"
plain
:disabled="!props.nextId"
@click="emit('next')"
>
下一条
<Icon icon="ep:arrow-right" class="ml-1" />
</el-button>
</div>
</div>
<!-- 操作按钮栏 -->
@ -50,16 +69,16 @@
<div class="rightContent w-[calc(100%-300px)]">
<el-tabs>
<el-tab-pane label="客户资料">
<div class="mt-4">
<QuickFollow />
<Infor />
<div class="mt-4">
<QuickFollow :customer-id="props.customerId" :customer-form="customerForm" />
<Infor :customer-id="props.customerId" :customer-form="customerForm" />
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</ContentWrap>
</ContentWrap>
<!-- 分配弹窗 -->
<AllocateForm ref="formRef" />
<!-- 转公海弹窗 -->
@ -92,25 +111,9 @@ const transferFormRef = ref() // 转公海表单的 Ref
//
const customerForm = ref({
id: undefined as number | undefined,
sex: 1,
age: undefined as string | undefined,
customerName: '',
expectAmount: undefined as string | undefined,
city: '',
customerLevel: 0,
customerType: '潜在',
followContent: '',
mobile: '',
remark: '',
customerSourceId: undefined,
customerTypeId: undefined,
importLevelId: undefined,
followStatusId: undefined,
ownerUserId: undefined,
depId: undefined,
contactLastTime: undefined,
contactLastContent: undefined,
createTime: undefined,
repeatCount: 0,
followCount: 0
})

View File

@ -11,7 +11,7 @@
<div v-if="loading" class="text-center py-4">加载中...</div>
<div v-if="finished" class="text-center text-gray-400 py-4">没有更多了</div>
</el-timeline>
<div v-else class="text-center text-gray-400 py-4">暂无跟进记录</div>
<div v-else class="text-center text-gray-400 py-4">暂无记录</div>
</div>
</template>

View File

@ -4,10 +4,10 @@
<div class="w-1 h-3.5 bg-blue-500 rounded-full mr-2"></div>
基本信息
</div>
<el-button type="primary" size="small" class="text-xs !px-3 !py-1.5">保存</el-button>
<el-button type="primary" size="small" class="text-xs !px-3 !py-1.5" @click="handleSave">保存</el-button>
</div>
<el-form :model="form" label-width="80px" class="bg-white p-4 rounded-lg text-xs">
<el-form :model="form" label-width="80px" class="bg-white p-4 rounded-lg text-xs">
<!-- 选择器组 -->
<div class="flex items-center gap-6 mb-3 pb-2 border-b border-gray-100">
@ -25,21 +25,24 @@
placeholder="请输入"
class="!w-[120px]"
size="small"
type="number"
min="0"
max="150"
@input="(val) => form.age = val ? Number(val) : 0"
/>
</el-form-item>
</el-form-item>
<el-form-item label-class="text-gray-600 text-xs" label="性别" prop="sex" class="!mb-0 !text-xs" >
<el-select
v-model="form.sex"
placeholder="请选择"
class="!w-[120px]"
size="small"
>
<el-option label="未知" value="0" class="!text-xs" />
<el-option label="男" value="1" class="!text-xs" />
<el-option label="女" value="2" class="!text-xs" />
</el-select>
<el-select v-model="form.sex" placeholder="请选择" class="!w-120px">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</div>
<div class="flex items-center gap-6 mb-3 pb-2 border-b border-gray-100">
@ -51,9 +54,9 @@
size="small"
/>
</el-form-item>
<el-form-item label-class="text-gray-600 text-xs" label="居住地址" prop="adress" class="!mb-0 !text-xs" >
<el-form-item label-class="text-gray-600 text-xs" label="居住地址" prop="address" class="!mb-0 !text-xs" >
<el-input
v-model="form.adress"
v-model="form.address"
placeholder="请输入"
class="!w-[350px]"
size="small"
@ -61,8 +64,6 @@
</el-form-item>
</div>
<!-- 备注 -->
<div class="flex items-start">
<el-form-item label-class="text-gray-600 text-xs" label="客户信息" class="flex-1 !text-xs">
@ -83,65 +84,75 @@
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
import { useCrmStore } from '@/store/modules/crm'
import { storeToRefs } from 'pinia'
import { buildTree } from '@/utils/tree'
import {CustomerInforApi,CustomerInforVO}from '@/api/crm/customer/customer'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { useMessage } from '@/hooks/web/useMessage'
const props = defineProps<{
customerId?: number
customerForm?: {
id?: number
customerName: string
followContent: string
mobile: string
repeatCount: number
followCount: number
}
}>()
// CRM store
const crmStore = useCrmStore()
const { importLevelList, customerTypeList, customerLabelList, followLabelList } = storeToRefs(crmStore)
const customerTypeOptions = ref<any>()
// cascader
const cascaderProps = {
emitPath: false
}
const message = useMessage()
const form = ref({
sex: 1,
age: undefined as string | undefined,
id: 0,
age: 0,
sex: 0,
customerName: '',
expectAmount: undefined as string | undefined,
adress: '',
remark: '',
expectAmount: undefined,
address: '',
remark: ''
})
// customerId
watch(() => props.customerId, async (id) => {
watch([() => props.customerId, () => props.customerForm], async ([id, customerForm]) => {
if (id) {
try {
// API
// const data = await CustomerApi.getCustomerFollowInfo(id)
// Object.assign(form.value, data)
const data = await CustomerInforApi.getCustomer(id)
Object.assign(form.value, data)
// If customerForm is provided, update relevant fields
if (customerForm) {
form.value = {
...form.value,
customerName: customerForm.customerName,
id: customerForm.id || form.value.id,
// Keep other fields from the API response
age: form.value.age,
sex: form.value.sex,
expectAmount: form.value.expectAmount,
address: form.value.address,
remark: form.value.remark
}
}
console.log(form.value)
} catch (error) {
console.error('Failed to fetch customer follow info:', error)
message.error('获取客户信息失败')
}
}
}, { immediate: true })
//
//
onMounted(async () => {
try {
await Promise.all([
crmStore.getImportLevelList(),
crmStore.getCustomerTypeList(),
crmStore.getCustomerLabelList(),
crmStore.getFollowLabelList()
])
//
customerTypeOptions.value = buildTree(customerTypeList.value)
} catch (error) {
console.error('Failed to initialize data:', error)
}
})
//
const handleSave = () => {
// TODO:
const handleSave = async () => {
try {
const data = form.value as unknown as CustomerInforVO
await CustomerInforApi.updateCustomer(data)
message.success('保存成功')
} catch (error) {
message.error('保存失败')
}
}
</script>

View File

@ -1,219 +1,369 @@
<template>
<div class="flex justify-between items-center mb-2 bg-gray-50 px-4 py-2 rounded-md border border-gray-100">
<div class="text-xs font-medium text-gray-700 flex items-center">
<div class="w-1 h-3.5 bg-blue-500 rounded-full mr-2"></div>
快捷输入
<div>
<div class="flex justify-between items-center mb-2 bg-gray-50 px-4 py-2 rounded-md border border-gray-100">
<div class="text-xs font-medium text-gray-700 flex items-center">
<div class="w-1 h-3.5 bg-blue-500 rounded-full mr-2"></div>
快捷输入
</div>
<div class="flex items-center gap-2">
<el-button type="primary" size="small" class="text-xs !px-3 !py-1.5" @click="handleSave">保存</el-button>
</div>
</div>
<el-button type="primary" size="small" class="text-xs !px-3 !py-1.5">保存</el-button>
</div>
<el-form :model="form" label-width="80px" class="bg-white p-4 rounded-lg text-xs">
<!-- 开关选项组 -->
<div class="flex items-center gap-4 mb-3 pb-2 border-b border-gray-100">
<el-form-item label="车" class="!mb-0 !text-xs">
<el-switch v-model="form.haveCar" size="small" />
</el-form-item>
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px" class="bg-white p-4 rounded-lg text-xs">
<!-- 开关选项组 -->
<div class="flex items-center gap-4 mb-3 pb-2 border-b border-gray-100">
<el-form-item label="车" class="!mb-0 !text-xs">
<el-switch
v-model="form.haveCar"
size="small"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
<el-form-item label="房" class="!mb-0 !text-xs">
<el-switch v-model="form.haveHouse" size="small" />
</el-form-item>
<el-form-item label="房" class="!mb-0 !text-xs">
<el-switch
v-model="form.haveHouse"
size="small"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
<el-form-item label="社保" class="!mb-0 !text-xs">
<el-switch v-model="form.haveSocialSecurity" size="small" />
</el-form-item>
<el-form-item label="社保" class="!mb-0 !text-xs">
<el-switch
v-model="form.haveSocialSecurity"
size="small"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
<el-form-item label="公积" class="!mb-0 !text-xs">
<el-switch v-model="form.haveProvidentFund" size="small" />
</el-form-item>
<el-form-item label="公积金" class="!mb-0 !text-xs">
<el-switch
v-model="form.haveProvidentFund"
size="small"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
<el-form-item label="保单" class="!mb-0 !text-xs">
<el-switch v-model="form.haveGuaranteeSlip" size="small" />
</el-form-item>
</div>
<el-form-item label="保单" class="!mb-0 !text-xs">
<el-switch
v-model="form.haveGuaranteeSlip"
size="small"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
</div>
<!-- 选择器组 -->
<div class="flex items-center gap-6 mb-3 pb-2 border-b border-gray-100">
<el-form-item label-class="text-gray-600 text-xs" label="客户类型" prop="customerTypeId" class="!mb-0 !text-xs">
<el-cascader
v-model="form.customerTypeId"
:show-all-levels="false"
:options="customerTypeOptions"
:props="cascaderProps"
clearable
placeholder="请选择"
class="!w-[150px]"
size="small"
/>
</el-form-item>
<el-form-item label="重要程度" prop="importLevelId" class="!mb-0 !text-xs">
<el-select
v-model="form.importLevelId"
placeholder="请选择"
clearable
class="!w-[150px]"
size="small"
>
<el-option
v-for="iml in importLevelList"
:key="iml.id"
:value="iml.id"
:label="iml.name"
class="!text-xs"
/>
</el-select>
</el-form-item>
<el-form-item label="重点客户" class="!mb-0 !text-xs">
<el-switch v-model="form.haveGuaranteeSlip" size="small" />
</el-form-item>
</div>
<div class="flex items-center gap-6 mb-3 pb-2 border-b border-gray-100">
<el-form-item label-class="text-gray-600 text-xs" label="下次跟进" prop="nextFollowTime" class="!mb-0 !text-xs">
<div class="flex items-center gap-2">
<el-date-picker
v-model="form.nextFollowTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="datetime"
placeholder="请选择时间"
<!-- 选择器组 -->
<div class="flex items-center gap-6 mb-3 pb-2 border-b border-gray-100">
<el-form-item label-class="text-gray-600 text-xs" label="客户类型" prop="customerTypeId" class="!mb-0 !text-xs">
<el-cascader
v-model="form.customerTypeId"
:show-all-levels="false"
:options="customerTypeOptions"
:props="cascaderProps"
clearable
placeholder="请选择"
class="!w-[150px]"
size="small"
/>
<el-button
type="danger"
size="small"
class="!text-xs"
plain
@click="setNextFollowTime(15)"
>15分钟</el-button>
<el-button
type="danger"
size="small"
class="!text-xs"
plain
@click="setNextFollowTime(30)"
>30分钟</el-button>
<el-button
type="danger"
size="small"
class="!text-xs"
plain
@click="setNextFollowTime(60)"
>1小时</el-button>
<el-button
type="danger"
size="small"
class="!text-xs"
plain
@click="setNextFollowTime(1440)"
>1天后</el-button>
<el-button
type="danger"
size="small"
class="!text-xs"
plain
@click="setNextFollowTime(4320)"
>3天后</el-button>
</div>
</el-form-item>
</div>
</el-form-item>
<el-form-item label="重要程度" prop="importLevelId" class="!mb-0 !text-xs">
<el-select
v-model="form.importLevelId"
placeholder="请选择"
clearable
class="!w-[150px]"
size="small"
>
<el-option
v-for="item in importLevelList"
:key="item.id"
:label="item.name"
:value="item.id"
class="!text-xs"
/>
</el-select>
</el-form-item>
<el-form-item label="重点客户" class="!mb-0 !text-xs">
<el-switch v-model="form.isImport" size="small" :active-value="1" :inactive-value="0"/>
</el-form-item>
</div>
<!-- 标签组 -->
<div class="mb-3 pb-2 border-b border-gray-100">
<el-form-item label-class="text-gray-600 text-xs" label="客户标签" class="!mb-0 !text-xs">
<div class="flex items-center gap-6 mb-3 pb-2 border-b border-gray-100">
<el-form-item label-class="text-gray-600 text-xs" label="下次跟进" prop="nextFollowTime" class="!mb-0 !text-xs">
<div class="flex items-center gap-2">
<el-date-picker
v-model="form.nextFollowTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="datetime"
placeholder="请选择时间"
class="!w-[150px]"
size="small"
/>
<el-button
type="danger"
size="small"
class="!text-xs"
plain
@click="setNextFollowTime(15)"
>15分钟</el-button>
<el-button
type="danger"
size="small"
class="!text-xs"
plain
@click="setNextFollowTime(30)"
>30分钟</el-button>
<el-button
type="danger"
size="small"
class="!text-xs"
plain
@click="setNextFollowTime(60)"
>1小时</el-button>
<el-button
type="danger"
size="small"
class="!text-xs"
plain
@click="setNextFollowTime(1440)"
>1天后</el-button>
<el-button
type="danger"
size="small"
class="!text-xs"
plain
@click="setNextFollowTime(4320)"
>3天后</el-button>
</div>
</el-form-item>
</div>
<!-- 标签组 -->
<div class="mb-3 pb-2 border-b border-gray-100">
<el-form-item label-class="text-gray-600 text-xs" label="客户标签" class="!mb-0 !text-xs">
<div class="flex flex-wrap gap-1.5">
<el-check-tag
v-for="label in customerLabelList"
:key="label.id"
:checked="customerLabels.includes(label.id)"
@change="(checked) => handleCustomerLablesChange(label.id, checked)"
class="!cursor-pointer hover:!border-primary !text-xs !py-0 !px-1.5"
>
{{ label.name }}
</el-check-tag>
</div>
</el-form-item>
</div>
<!-- 备注 -->
<div class="flex items-start">
<el-form-item label-class="text-gray-600 text-xs" label="跟进备注" class="flex-1 !text-xs">
<el-input
v-model="content"
type="textarea"
:rows="3"
placeholder="请输入跟进备注"
resize="none"
size="small"
class="[&_.el-textarea__inner]:!text-xs"
/>
</el-form-item>
</div>
<div class="flex items-center">
<label class="text-gray-600 w-[80px] text-xs"></label>
<div class="flex flex-wrap gap-1.5">
<el-check-tag
v-for="label in customerLabelList"
v-for="label in followLabelList"
:key="label.id"
:modelValue="form.labelIds?.includes(label.id)"
@change="(checked) => handleLabelChange(label.id, checked)"
@click.stop="handleLabelClick(label)"
class="!cursor-pointer hover:!border-primary !text-xs !py-0 !px-1.5"
>
{{ label.name }}
{{ label.name }}
</el-check-tag>
</div>
</el-form-item>
</div>
<!-- 备注 -->
<div class="flex items-start">
<el-form-item label-class="text-gray-600 text-xs" label="跟进备注" class="flex-1 !text-xs">
<el-input
v-model="form.content"
type="textarea"
:rows="3"
placeholder="请输入跟进备注"
resize="none"
size="small"
class="[&_.el-textarea__inner]:!text-xs"
/>
</el-form-item>
</div>
<div class="flex items-center">
<label class="text-gray-600 w-[80px] text-xs"></label>
<div class="flex flex-wrap gap-1.5">
<el-check-tag
v-for="label in followLabelList"
:key="label.id"
:modelValue="form.labelIds?.includes(label.id)"
@change="(checked) => handleLabelChange(label.id, checked)"
class="!cursor-pointer hover:!border-primary !text-xs !py-0 !px-1.5"
>
{{ label.name }}
</el-check-tag>
</div>
</div>
</el-form>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
import { useCrmStore } from '@/store/modules/crm'
import { storeToRefs } from 'pinia'
import { buildTree } from '@/utils/tree'
import dayjs from 'dayjs'
import { CustomerInforApi, type CustomerInforVO } from '@/api/crm/customer/customer'
import { CustomerLabelsApi } from '@/api/crm/customer/labels'
import { useMessage } from '@/hooks/web/useMessage'
import { FollwRecordApi, FollwRecordVO } from '@/api/crm/follw'
import { useUserStore } from '@/store/modules/user'
import request from '@/config/axios'
const props = defineProps<{
customerId?: number
const props = defineProps<{
customerId?: number,
prevId?: number,
nextId?: number,
customerForm: any
}>()
const emit = defineEmits(['prev', 'next'])
const message = useMessage()
// CRM store
const crmStore = useCrmStore()
const { importLevelList, customerTypeList, customerLabelList, followLabelList } = storeToRefs(crmStore)
const customerTypeOptions = ref<any>()
const content = ref()
const customerLabels = ref<number[]>([])
const formRef = ref()
// cascader
const cascaderProps = {
emitPath: false
}
const form = ref({
const userStore = useUserStore()
//
const form = ref<{
id: number,
haveCar: number | undefined
haveHouse: number | undefined
haveProvidentFund: number | undefined
haveSocialSecurity: number | undefined
haveGuaranteeSlip: number | undefined
customerTypeId: number | undefined
importLevelId: number | undefined
isImport: number | undefined
customerLabelIds: number[]
nextFollowTime: string | undefined
}>({
id: 0,
haveCar: undefined,
haveHouse: undefined,
haveProvidentFund: undefined,
haveSocialSecurity: undefined,
haveProvidentFund: undefined,
haveSocialSecurity: undefined,
haveGuaranteeSlip: undefined,
customerTypeId: undefined,
importLevelId: undefined,
labelIds: [] as number[],
content: undefined,
nextFollowTime: undefined as string | undefined
isImport: undefined,
customerLabelIds: [],
nextFollowTime: undefined
})
//
const rules = {
importLevelId: [{ required: true, message: '请选择重要程度', trigger: 'change' }]
}
// customerId
watch(() => props.customerId, async (id) => {
watch([() => props.customerId, () => props.customerForm], async ([id, customerForm]) => {
if (id) {
try {
// API
// const data = await CustomerApi.getCustomerFollowInfo(id)
// Object.assign(form.value, data)
//
const data = await CustomerInforApi.getCustomer(id)
//
Object.assign(form.value, data)
// importLevelId null 0 undefined
form.value.importLevelId = data.importLevelId && data.importLevelId !== 0 ? data.importLevelId : undefined
//
customerLabels.value = []
content.value = ''
//
const lbs = await CustomerLabelsApi.getCustomerLabelsList({ customerId: props.customerId })
//
customerLabels.value = lbs.map((label: any) => label.customerLabelId)
} catch (error) {
console.error('Failed to fetch customer follow info:', error)
message.error('获取客户信息失败')
}
} else {
//
customerLabels.value = []
content.value = ''
}
}, { immediate: true })
//
const handleLabelClick = (label: any) => {
content.value = content.value ? `${content.value} ${label.name}` : label.name
}
//
const handleCustomerLablesChange = (labelId: number, checked: boolean) => {
if (checked) {
//
customerLabels.value.push(labelId)
} else {
//
customerLabels.value = customerLabels.value.filter(id => id !== labelId)
}
}
//
const setNextFollowTime = (minutes: number) => {
form.value.nextFollowTime = minutes ? dayjs().add(minutes, 'minute').format('YYYY-MM-DD HH:mm:ss') : undefined
}
//
const handleSave = async () => {
//
try {
if (!form.value.importLevelId) {
message.warning('请选择重要程度')
formRef.value.validateField('importLevelId')
return
}
//1:
const data = form.value as unknown as CustomerInforVO
await CustomerInforApi.updateCustomer(data)
//2:
if (content.value) {
const followRecord: FollwRecordVO = {
id: 0,
customerId: props.customerId!,
content: content.value,
userId: userStore.getUser.id
}
await FollwRecordApi.createFollwRecord(followRecord)
}
//3:
if (props.customerId && customerLabels.value.length > 0) {
await request.post({
url: '/crm/customer-labels/create',
data: {
customerId: props.customerId,
customerLabelIds: customerLabels.value
}
})
}
message.success('保存成功')
} catch (error: any) {
//
if (error?.message) {
message.error(error.message)
} else {
message.error('保存失败')
}
}
}
//
onMounted(async () => {
try {
@ -221,36 +371,14 @@ onMounted(async () => {
crmStore.getImportLevelList(),
crmStore.getCustomerTypeList(),
crmStore.getCustomerLabelList(),
crmStore.getFollowLabelList()
crmStore.getFollowLabelList()
])
//
customerTypeOptions.value = buildTree(customerTypeList.value)
} catch (error) {
console.error('Failed to initialize data:', error)
message.error('初始化失败')
}
})
//
const handleLabelChange = (labelId: number, checked: boolean) => {
if (checked) {
form.value.labelIds.push(labelId)
} else {
const index = form.value.labelIds.indexOf(labelId)
if (index !== -1) {
form.value.labelIds.splice(index, 1)
}
}
}
//
const setNextFollowTime = (minutes: number) => {
form.value.nextFollowTime = dayjs().add(minutes, 'minute').format('YYYY-MM-DD HH:mm:ss')
}
//
const handleSave = () => {
// TODO:
}
</script>

View File

@ -1,15 +1,8 @@
<template>
<div class="transfer-record-container">
<el-timeline
v-if="transferRecordList.length > 0"
v-infinite-scroll="loadMore"
:infinite-scroll-disabled="disabled"
:infinite-scroll-immediate="false"
:infinite-scroll-distance="50"
class="timeline-wrapper"
>
<div class="mt-4">
<el-timeline v-if="transferRecordList.length > 0" v-infinite-scroll="loadMore" :infinite-scroll-disabled="disabled">
<el-timeline-item v-for="item in transferRecordList" :key="item.id">
<div class="flex flex-col gap-2">
<div class="flex flex-col gap-2">
<div class="flex items-center">
<span class="text-gray-500 w-[80px]">数据位置</span>
<span>{{ getDictLabel(DICT_TYPE.CRM_CUSTOMER_DATA_BELONG, item.type) }}</span>
@ -32,7 +25,7 @@
<div v-if="loading" class="text-center py-4">加载中...</div>
<div v-if="finished" class="text-center text-gray-400 py-4">没有更多了</div>
</el-timeline>
<div v-else class="text-center text-gray-400 py-4">暂无流转记录</div>
<div v-else class="text-center text-gray-400 py-4">暂无记录</div>
</div>
</template>
@ -130,28 +123,9 @@ watch(() => props.customerId, async (id) => {
</script>
<style scoped>
.transfer-record-container {
height: calc(100vh - 240px);
overflow: hidden;
}
.timeline-wrapper {
height: 100%;
.el-timeline {
padding-right: 10px;
max-height: calc(100vh - 240px);
overflow-y: auto;
scrollbar-width: thin;
}
.timeline-wrapper::-webkit-scrollbar {
width: 6px;
}
.timeline-wrapper::-webkit-scrollbar-thumb {
background-color: #dcdfe6;
border-radius: 3px;
}
.timeline-wrapper::-webkit-scrollbar-track {
background-color: transparent;
}
}
</style>

View File

@ -302,13 +302,7 @@
<!-- 客户详情抽屉 -->
<el-drawer v-model="drawerVisible" size="80%" :destroy-on-close="true" :with-header="false" :show-close="true">
<CustomerDetail
:customer-id="customerId"
:prev-id="prevId"
:next-id="nextId"
@prev="handlePrevNextClick('prev')"
@next="handlePrevNextClick('next')"
/>
<CustomerDetail :customer-id="customerId" />
</el-drawer>
<!-- 转公海弹窗 -->
@ -320,7 +314,6 @@
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { buildTree } from '@/utils/tree'
import { handlePrevNext } from '@/utils/crm'
import { CustomerInforApi, CustomerInforVO } from '@/api/crm/customer/customer'
import CustomerDetail from '@/views/crm/components/Customer/Detail.vue'
import TransferForm from '@/views/crm/components/Transfer/TransferForm.vue'
@ -370,8 +363,6 @@ let customerSourceOptions = ref<any>();
let customerTypeOptions = ref<any>();
const drawerVisible = ref(false) //
const customerId = ref<number>() // ID
const prevId = ref<number>(0) // ID
const nextId = ref<number>(0) // ID
const cascaderProps= {
emitPath: false,
}
@ -404,31 +395,9 @@ const resetQuery = () => {
/** 打开跟进抽屉 */
const openInforForm = (id?: number) => {
customerId.value = id
//
const currentIndex = list.value.findIndex(item => item.id === id)
// ID
prevId.value = currentIndex > 0 ? list.value[currentIndex - 1].id : 0
nextId.value = currentIndex < list.value.length - 1 ? list.value[currentIndex + 1].id : 0
drawerVisible.value = true
}
/** 处理上一条/下一条切换 */
const handlePrevNextClick = (type: 'prev' | 'next') => {
if (customerId.value === undefined) return
handlePrevNext(type, {
list: list.value,
currentId: customerId.value,
onUpdateIds: ({ currentId, prevId: newPrevId, nextId: newNextId }) => {
customerId.value = currentId
prevId.value = newPrevId
nextId.value = newNextId
}
})
}
/** 打开转公海弹窗 */
const openTransferForm = () => {
if (multipleSelection.value.length === 0) {

View File

@ -281,13 +281,7 @@
<!-- 客户详情抽屉 -->
<el-drawer v-model="drawerVisible" size="80%" :destroy-on-close="true" :with-header="false" :show-close="true">
<CustomerDetail
:customer-id="customerId"
:prev-id="prevId"
:next-id="nextId"
@prev="handlePrevNextClick('prev')"
@next="handlePrevNextClick('next')"
/>
<CustomerDetail :customer-id="customerId" />
</el-drawer>
<!-- 转公海弹窗 -->
@ -299,7 +293,6 @@
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { buildTree } from '@/utils/tree'
import { handlePrevNext } from '@/utils/crm'
import { CustomerInforApi, CustomerInforVO } from '@/api/crm/customer/customer'
import CustomerDetail from '@/views/crm/components/Customer/Detail.vue'
import TransferForm from '@/views/crm/components/Transfer/TransferForm.vue'
@ -349,8 +342,6 @@ let customerSourceOptions = ref<any>()
let customerTypeOptions = ref<any>()
const drawerVisible = ref(false) //
const customerId = ref<number>() // ID
const prevId = ref<number>(0) // ID
const nextId = ref<number>(0) // ID
const cascaderProps= {
emitPath: false,
}
@ -384,31 +375,9 @@ const resetQuery = () => {
/** 打开跟进抽屉 */
const openInforForm = (id?: number) => {
customerId.value = id
//
const currentIndex = list.value.findIndex(item => item.id === id)
// ID
prevId.value = currentIndex > 0 ? list.value[currentIndex - 1].id : 0
nextId.value = currentIndex < list.value.length - 1 ? list.value[currentIndex + 1].id : 0
drawerVisible.value = true
}
/** 处理上一条/下一条切换 */
const handlePrevNextClick = (type: 'prev' | 'next') => {
if (customerId.value === undefined) return
handlePrevNext(type, {
list: list.value,
currentId: customerId.value,
onUpdateIds: ({ currentId, prevId: newPrevId, nextId: newNextId }) => {
customerId.value = currentId
prevId.value = newPrevId
nextId.value = newNextId
}
})
}
/** 打开转公海弹窗 */
const openTransferForm = () => {
if (multipleSelection.value.length === 0) {

View File

@ -280,13 +280,7 @@
<!-- 客户详情抽屉 -->
<el-drawer v-model="drawerVisible" size="80%" :destroy-on-close="true" :with-header="false" :show-close="true">
<CustomerDetail
:customer-id="customerId"
:prev-id="prevId"
:next-id="nextId"
@prev="handlePrevNextClick('prev')"
@next="handlePrevNextClick('next')"
/>
<CustomerDetail :customer-id="customerId" />
</el-drawer>
<!-- 转公海弹窗 -->
@ -298,7 +292,6 @@
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { buildTree } from '@/utils/tree'
import { handlePrevNext } from '@/utils/crm'
import { CustomerInforApi, CustomerInforVO } from '@/api/crm/customer/customer'
import CustomerDetail from '@/views/crm/components/Customer/Detail.vue'
import TransferForm from '@/views/crm/components/Transfer/TransferForm.vue'
@ -349,8 +342,6 @@ let customerSourceOptions = ref<any>()
let customerTypeOptions = ref<any>()
const drawerVisible = ref(false) //
const customerId = ref<number>() // ID
const prevId = ref<number>(0) // ID
const nextId = ref<number>(0) // ID
const cascaderProps= {
emitPath: false,
}
@ -389,31 +380,9 @@ const resetQuery = () => {
/** 打开跟进抽屉 */
const openInforForm = (id?: number) => {
customerId.value = id
//
const currentIndex = list.value.findIndex(item => item.id === id)
// ID
prevId.value = currentIndex > 0 ? list.value[currentIndex - 1].id : 0
nextId.value = currentIndex < list.value.length - 1 ? list.value[currentIndex + 1].id : 0
drawerVisible.value = true
}
/** 处理上一条/下一条切换 */
const handlePrevNextClick = (type: 'prev' | 'next') => {
if (customerId.value === undefined) return
handlePrevNext(type, {
list: list.value,
currentId: customerId.value,
onUpdateIds: ({ currentId, prevId: newPrevId, nextId: newNextId }) => {
customerId.value = currentId
prevId.value = newPrevId
nextId.value = newNextId
}
})
}
/** 打开转公海弹窗 */
const openTransferForm = () => {
if (multipleSelection.value.length === 0) {

View File

@ -280,13 +280,7 @@
<!-- 客户详情抽屉 -->
<el-drawer v-model="drawerVisible" size="80%" :destroy-on-close="true" :with-header="false" :show-close="true">
<CustomerDetail
:customer-id="customerId"
:prev-id="prevId"
:next-id="nextId"
@prev="handlePrevNextClick('prev')"
@next="handlePrevNextClick('next')"
/>
<CustomerDetail :customer-id="customerId" />
</el-drawer>
<!-- 转公海弹窗 -->
@ -298,7 +292,6 @@
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { buildTree } from '@/utils/tree'
import { handlePrevNext } from '@/utils/crm'
import { CustomerInforApi, CustomerInforVO } from '@/api/crm/customer/customer'
import CustomerDetail from '@/views/crm/components/Customer/Detail.vue'
import TransferForm from '@/views/crm/components/Transfer/TransferForm.vue'
@ -349,8 +342,6 @@ let customerSourceOptions = ref<any>()
let customerTypeOptions = ref<any>()
const drawerVisible = ref(false) //
const customerId = ref<number>() // ID
const prevId = ref<number>(0) // ID
const nextId = ref<number>(0) // ID
const cascaderProps= {
emitPath: false,
}
@ -389,31 +380,9 @@ const resetQuery = () => {
/** 打开跟进抽屉 */
const openInforForm = (id?: number) => {
customerId.value = id
//
const currentIndex = list.value.findIndex(item => item.id === id)
// ID
prevId.value = currentIndex > 0 ? list.value[currentIndex - 1].id : 0
nextId.value = currentIndex < list.value.length - 1 ? list.value[currentIndex + 1].id : 0
drawerVisible.value = true
}
/** 处理上一条/下一条切换 */
const handlePrevNextClick = (type: 'prev' | 'next') => {
if (customerId.value === undefined) return
handlePrevNext(type, {
list: list.value,
currentId: customerId.value,
onUpdateIds: ({ currentId, prevId: newPrevId, nextId: newNextId }) => {
customerId.value = currentId
prevId.value = newPrevId
nextId.value = newNextId
}
})
}
/** 打开转公海弹窗 */
const openTransferForm = () => {
if (multipleSelection.value.length === 0) {

View File

@ -277,13 +277,7 @@
<!-- 客户详情抽屉 -->
<el-drawer v-model="drawerVisible" size="80%" :destroy-on-close="true" :with-header="false" :show-close="true">
<CustomerDetail
:customer-id="customerId"
:prev-id="prevId"
:next-id="nextId"
@prev="handlePrevNextClick('prev')"
@next="handlePrevNextClick('next')"
/>
<CustomerDetail :customer-id="customerId" />
</el-drawer>
<!-- 转公海弹窗 -->
@ -295,7 +289,6 @@
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { buildTree } from '@/utils/tree'
import { handlePrevNext } from '@/utils/crm'
import { CustomerInforApi, CustomerInforVO } from '@/api/crm/customer/customer'
import CustomerDetail from '@/views/crm/components/Customer/Detail.vue'
import TransferForm from '@/views/crm/components/Transfer/TransferForm.vue'
@ -345,8 +338,6 @@ let customerSourceOptions = ref<any>()
let customerTypeOptions = ref<any>()
const drawerVisible = ref(false) //
const customerId = ref<number>() // ID
const prevId = ref<number>(0) // ID
const nextId = ref<number>(0) // ID
const cascaderProps= {
emitPath: false,
}
@ -381,31 +372,9 @@ const resetQuery = () => {
/** 打开跟进抽屉 */
const openInforForm = (id?: number) => {
customerId.value = id
//
const currentIndex = list.value.findIndex(item => item.id === id)
// ID
prevId.value = currentIndex > 0 ? list.value[currentIndex - 1].id : 0
nextId.value = currentIndex < list.value.length - 1 ? list.value[currentIndex + 1].id : 0
drawerVisible.value = true
}
/** 处理上一条/下一条切换 */
const handlePrevNextClick = (type: 'prev' | 'next') => {
if (customerId.value === undefined) return
handlePrevNext(type, {
list: list.value,
currentId: customerId.value,
onUpdateIds: ({ currentId, prevId: newPrevId, nextId: newNextId }) => {
customerId.value = currentId
prevId.value = newPrevId
nextId.value = newNextId
}
})
}
/** 打开转公海弹窗 */
const openTransferForm = () => {
if (multipleSelection.value.length === 0) {

View File

@ -9,12 +9,12 @@
:inline="true"
label-width="68px"
>
<el-form-item label="客户" prop="customerName">
<el-form-item label="客户名" prop="customerName">
<el-input
v-model="queryParams.customerName"
placeholder="请输入客户"
placeholder="请输入客户名"
clearable
@keyup.enter="handleQuery"
class="!w-120px"
/>
</el-form-item>
<el-form-item label="手机号码" prop="mobile">
@ -22,17 +22,10 @@
v-model="queryParams.mobile"
placeholder="请输入手机号码"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="座机号码" prop="telephone">
<el-input
v-model="queryParams.telephone"
placeholder="请输入座机号码"
clearable
@keyup.enter="handleQuery"
class="!w-120px"
/>
</el-form-item>
<el-form-item label="客户类型" prop="customerTypeId">
<el-cascader
v-model="queryParams.customerTypeId"
@ -62,8 +55,9 @@
<el-form-item label="跟进状态" prop="followStatus">
<el-select
v-model="queryParams.followStatus"
placeholder="请选择跟进状态"
placeholder="请选择"
clearable
class="!w-120px"
>
<el-option
v-for="dict in followStatusList"

View File

@ -265,13 +265,7 @@
</ContentWrap>
<!-- 客户详情抽屉 -->
<el-drawer v-model="drawerVisible" size="80%" :destroy-on-close="true" :with-header="false" :show-close="true">
<CustomerDetail
:customer-id="customerId"
:prev-id="prevId"
:next-id="nextId"
@prev="handlePrevNextClick('prev')"
@next="handlePrevNextClick('next')"
/>
<CustomerDetail :customer-id="customerId" />
</el-drawer>
<!-- 分配弹窗 -->
@ -281,8 +275,7 @@
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { buildTree } from '@/utils/tree'
import { handlePrevNext } from '@/utils/crm'
import { buildTree } from '@/utils/tree'
import { CustomerInforApi, CustomerInforVO } from '@/api/crm/customer/customer'
import { useCrmStore } from '@/store/modules/crm'
import { storeToRefs } from 'pinia'
@ -309,8 +302,6 @@ let customerTypeOptions = ref<any>()
const drawerVisible = ref(false) //
const customerId = ref<number>() // ID
const prevId = ref<number>(0) // ID
const nextId = ref<number>(0) // ID
const multipleSelection = ref<CustomerInforVO[]>([])
const formRef = ref() // Ref
@ -367,13 +358,6 @@ const resetQuery = () => {
/** 打开跟进抽屉 */
const openInforForm = (id?: number) => {
customerId.value = id
//
const currentIndex = list.value.findIndex(item => item.id === id)
// ID
prevId.value = currentIndex > 0 ? list.value[currentIndex - 1].id : 0
nextId.value = currentIndex < list.value.length - 1 ? list.value[currentIndex + 1].id : 0
drawerVisible.value = true
}
/** 打开分配弹窗 */
@ -433,19 +417,4 @@ const handleReceive = async () => {
message.error('领取失败')
}
}
/** 处理上一条/下一条切换 */
const handlePrevNextClick = (type: 'prev' | 'next') => {
if (customerId.value === undefined) return
handlePrevNext(type, {
list: list.value,
currentId: customerId.value,
onUpdateIds: ({ currentId, prevId: newPrevId, nextId: newNextId }) => {
customerId.value = currentId
prevId.value = newPrevId
nextId.value = newNextId
}
})
}
</script>

View File

@ -265,13 +265,7 @@
</ContentWrap>
<!-- 客户详情抽屉 -->
<el-drawer v-model="drawerVisible" size="80%" :destroy-on-close="true" :with-header="false" :show-close="true">
<CustomerDetail
:customer-id="customerId"
:prev-id="prevId"
:next-id="nextId"
@prev="handlePrevNextClick('prev')"
@next="handlePrevNextClick('next')"
/>
<CustomerDetail :customer-id="customerId" />
</el-drawer>
<!-- 分配弹窗 -->
@ -281,8 +275,7 @@
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { buildTree } from '@/utils/tree'
import { handlePrevNext } from '@/utils/crm'
import { buildTree } from '@/utils/tree'
import { CustomerInforApi, CustomerInforVO } from '@/api/crm/customer/customer'
import { useCrmStore } from '@/store/modules/crm'
import { storeToRefs } from 'pinia'
@ -310,8 +303,6 @@ let customerTypeOptions = ref<any>()
const drawerVisible = ref(false) //
const customerId = ref<number>() // ID
const prevId = ref<number>(0) // ID
const nextId = ref<number>(0) // ID
const multipleSelection = ref<CustomerInforVO[]>([])
const formRef = ref() // Ref
@ -368,13 +359,6 @@ const resetQuery = () => {
/** 打开跟进抽屉 */
const openInforForm = (id?: number) => {
customerId.value = id
//
const currentIndex = list.value.findIndex(item => item.id === id)
// ID
prevId.value = currentIndex > 0 ? list.value[currentIndex - 1].id : 0
nextId.value = currentIndex < list.value.length - 1 ? list.value[currentIndex + 1].id : 0
drawerVisible.value = true
}
/** 打开分配弹窗 */
@ -434,19 +418,4 @@ const handleReceive = async () => {
message.error('领取失败')
}
}
/** 处理上一条/下一条切换 */
const handlePrevNextClick = (type: 'prev' | 'next') => {
if (customerId.value === undefined) return
handlePrevNext(type, {
list: list.value,
currentId: customerId.value,
onUpdateIds: ({ currentId, prevId: newPrevId, nextId: newNextId }) => {
customerId.value = currentId
prevId.value = newPrevId
nextId.value = newNextId
}
})
}
</script>