Files
art-design/src/views/crmpc/customer/Show.vue
T
2025-07-21 11:24:06 +08:00

500 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="customer-edit-page">
<div class="boxshadow pad20">
<el-link :underline="false" @click="goBack">
<i class="iconfont-sys icon-reverse">&#xe772;</i> 返回
</el-link>
</div>
<div class="edit-header boxshadow pad20" style="position:relative;">
<span class="border"></span><span class="edit-title">客户资料</span>
<!-- 右上角三角形角标 -->
<div class="corner-tag"><span class="corner-text">{{customerInfo.follow_txt}}</span></div>
<div class="info-row-bar">
<el-row :gutter="20" style="flex: 1;">
<el-col :sm="24" :md="6" :lg="4">
<span>客户名称:{{ customerInfo.contacts }}</span>
</el-col>
<el-col :sm="24" :md="6" :lg="4">
<span>渠道:{{customerInfo.channel_name}}</span>
</el-col>
<el-col :sm="24" :md="6" :lg="5">
<span>联系方式:{{customerInfo.phone}}</span>
</el-col>
<el-col :sm="24" :md="6" :lg="4">
<span>客户偏好:{{customerInfo.car_type_name}}</span>
</el-col>
<el-col :sm="24" :md="6" :lg="4">
<span>客户商机:{{customerInfo.magnitude_txt}}</span>
</el-col>
</el-row>
<el-button @click="goEdit" type="primary" class="edit-btn" size="small"><i class="iconfont-sys"> &#xe6bf;</i> 编辑资料</el-button>
</div>
</div>
<el-card class="info-card" shadow="never" style="margin-top: 16px;">
<div class="">
<span class="border"></span><span class="edit-title">客户画像</span>
<el-button @click="addtag" type="primary" size="small" style="float: right;">添加标签</el-button>
</div>
<div class="tag-list">
<el-tag type="primary" v-for="(item, index) in customerInfo.tags" :key="index">{{item.name}}</el-tag>
</div>
</el-card>
<el-card class="info-card" shadow="never" style="margin-top: 16px;">
<div class="info-row">
<span class="edit-title">客户分级</span>
<el-select v-model="customerInfo.grade" placeholder="请选择客户分级" style="width: 120px; margin-left: 16px;" @change="handleGradeChange">
<el-option v-for="item in levelOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</div>
</el-card>
<el-card class="info-card" shadow="never" style="margin-top: 16px;">
<div class="info-row">
<span class="edit-title">跟进记录</span>
<el-button type="primary" size="small" style="float: right;" @click="goAddFollow">+ 添加跟进</el-button>
</div>
<div class="follow-list">
<ElScrollbar >
<template v-if="followList.length > 0">
<ElTimeline>
<ElTimelineItem
v-for="item in followList"
:key="item.time"
:placement="TIMELINE_PLACEMENT"
:color="activecolor"
:center="true"
>
<div class="timeline-item">
<div class="timeline-content-row">
<div class="timeline-content">
<p class="timeline-text">{{item.type_txt}} <span class="timeline-mom">{{ item.created_at }}</span></p>
<p class="timeline-mom"><span v-if="item.type==1">跟进描述</span><span v-else>拜访内容</span> {{ item.audio_text }}</p>
</div>
<img v-if="item.picture_full" :src="item.picture_full" class="timeline-img" alt="图片" />
</div>
</div>
</ElTimelineItem>
</ElTimeline>
</template>
<template v-else>
<!-- 使用 Element Plus 的空状态组件 -->
<el-empty :image="emptyimg" description="暂无跟进记录" />
<!-- <el-empty /> -->
<!-- 或者自定义图片 -->
<!-- <img src="你的空图片地址.png" alt="暂无数据" class="empty-img" /> -->
</template>
</ElScrollbar>
</div>
</el-card>
</div>
<el-dialog
v-model="tagDialogVisible"
title="添加标签"
width="420px"
align-center
:show-close="false"
>
<div class="tag-dialog-scroll">
<div v-for="group in tagGroups" :key="group.name" style="margin-bottom: 12px;">
<div style="font-weight: bold; margin-bottom: 6px;">{{ group.name }}</div>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
<div
class="col4 tagitem"
style="cursor: pointer;"
v-for="(item1, index1) in group.tags"
:class="{ 'tagactive': activeMap[group.id] === item1.id }"
@click="tagSelect(item1)"
:key="index1"
>
{{ item1.name }}
</div>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer-center">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleConfirm">确定</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref,reactive } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import emptyimg from '@/assets/img/crm/empty.png'
import { CrmService } from '@/api/crmApi'
import { ElMessage } from 'element-plus'
// 常量配置
const TIMELINE_PLACEMENT = 'top'
const router = useRouter()
const route = useRoute()
const customerLevel = ref('A')
const activecolor = "#3471FF"
const tagDialogVisible = ref(false)
const selectedTags = ref<string[]>([])
const customerId = route.query.id
const levelOptions = ref<any>()
const tagGroups = ref<any[]>([])
const activeMap = reactive<{ [groupId: string]: number }>({})
// 定义客户详情类型
interface CustomerInfo {
contacts?: string
// 可根据实际接口补充更多字段
[key: string]: any
}
const customerInfo = ref<CustomerInfo>({})
//获取详情
const getdetilfn = async() => {
try {
// 这里调用获取品牌列表的接口
const res = await CrmService.getcustomerdetail({id:customerId})
customerInfo.value = res
console.log(res)
} catch (error) { }
}
//客户等级
const getGrade = async () => {
try {
// 这里调用获取品牌列表的接口
const res = await CrmService.getGrade()
levelOptions.value = res||[]
} catch (error) { levelOptions.value = [] }
}
//跟进记录getfollowlist
const followList = ref<CustomerInfo>({})
const getfollowlist = async () => {
try {
const res = await CrmService.getfollowlist({customer_id:route.query.id})
followList.value = res||[]
} catch (error) { followList.value = [] }
}
onMounted(async () => {
await getGrade()
await getdetilfn()
// 在 getdetilfn 之后调用 tagGrouplistfn,确保 customerInfo 有数据
await tagGrouplistfn()
getfollowlist()
})
function addtag(){
tagDialogVisible.value = true
tagGrouplistfn()
}
const tagGrouplistfn = async () => {
try {
const res = await CrmService.tagGrouplist()
tagGroups.value = res || []
// 回显选中
if (customerInfo.value && Array.isArray(customerInfo.value.tags)) {
tagGroups.value.forEach(group => {
if (group.tags && Array.isArray(group.tags)) {
const matched = group.tags.find((item: any) =>
customerInfo.value.tags.some((t: any) => t.id === item.id)
)
if (matched) {
activeMap[group.id] = matched.id
}
}
})
}
} catch (error) { }
}
// 4. 点击标签
async function tagSelect(item: { group_id: string | number; id: number }) {
const key = String(item.group_id)
let tag_id = ''
if (activeMap[key] === item.id) {
// 取消选中
delete activeMap[key]
tag_id = ''
} else {
// 选中
activeMap[key] = item.id
tag_id = String(item.id)
}
try {
const res = await CrmService.tagGroupupdatePivot({ customer_id: customerId, group_id: key, tag_id })
ElMessage.success(res?.message || '操作成功')
await getdetilfn()
await tagGrouplistfn()
} catch (e) {
ElMessage.error('操作失败')
}
}
// 5. 获取所有已选中的标签
// function getSelectedTags() {
// const selected: any[] = []
// for (const group of tagGroups) {
// const key = String(group.id)
// const tagId = activeMap[key]
// if (tagId) {
// const tag = group.tags.find(t => t.id === tagId)
// if (tag) selected.push(tag)
// }
// }
// return selected
// }
// 6. 示例:确定按钮
function handleConfirm() {
// const selectedTags = getSelectedTags()
// console.log('选中的标签:', selectedTags)
// 处理选中的标签
tagDialogVisible.value = false
}
function handleCancel() {
tagDialogVisible.value = false
}
//编辑
function goEdit() {
router.push({
path: '/crmpc/customer/edit',
query: { id: customerId }
})
}
function goBack() {
router.back()
}
//修改等级
function handleGradeChange(newGrade: number) {
if (!customerId) return
CrmService.customergrade({ id: customerId, grade: newGrade }).then(res => {
ElMessage.success(res?.message || '修改成功')
}).catch(() => {
ElMessage.error('修改失败')
})
}
//添加跟进
function goAddFollow() {
router.push({
path: '/crmpc/customer/followadd',
query: { id: customerId }
})
}
</script>
<style scoped>
.timeline-content-row{
display: flex;
align-items: center;
justify-content: space-between;
}
.timeline-img{
width: 80px;
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 12px; /* 标签间距 */
margin-top: 20px;
}
.border{
margin-right: 10px;
}
.pad20{
padding: 20px;
}
.icon-reverse {
display: inline-block;
transform: rotate(270deg); /* 旋转180度 */
}
.customer-edit-page {
background: #f7f9fa;
min-height: 100vh;
padding: 24px;
}
.edit-header {
/* display: flex; */
/* align-items: center; */
/* gap: 12px; */
margin-bottom: 16px;
}
.edit-title {
font-size: 18px;
font-weight: bold;
}
.edit-status {
margin-left: 16px;
}
.edit-btn {
margin-left: auto;
}
.info-card {
margin-bottom: 0;
border-radius: 8px;
}
.info-row {
display: flex;
align-items: center;
justify-content: space-between; /* 两端对齐 */
font-size: 15px;
padding: 0;
}
.follow-list {
margin-top: 16px;
}
.follow-item {
display: flex;
align-items: flex-start;
margin-bottom: 16px;
background: #f7f9fa;
border-radius: 6px;
padding: 12px;
}
.follow-main {
flex: 1;
}
.follow-title {
font-size: 14px;
color: #222;
margin-bottom: 4px;
display: flex;
gap: 16px;
}
.follow-type {
color: #3471FF;
font-weight: bold;
}
.follow-time {
color: #888;
}
.follow-content {
color: #333;
font-size: 14px;
}
.follow-img {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 6px;
margin-left: 16px;
}
.info-row-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin: 16px 0px;
}
.corner-tag {
position: absolute;
top: 0;
right: 0;
width: 90px;
height: 90px;
background: transparent;
z-index: 2;
pointer-events: none;
}
.corner-tag::before {
content: '';
position: absolute;
top: -10px;
right: -10px;
width: 90px;
height: 80px;
background: #3471FF;
clip-path: polygon(100% 0, 0 0, 100% 100%);
border-top-right-radius: 12px;
}
.corner-tag {
display: flex;
align-items: flex-start;
justify-content: flex-end;
color: #fff;
font-size: 16px;
font-weight: bold;
position: absolute;
top: 8px;
right: 8px;
width: 55px;
height: 40px;
z-index: 3;
padding-top: 6px;
padding-right: 0px;
pointer-events: none;
}
.corner-text {
display: inline-block;
transform: rotate(45deg);
}
.timeline-content-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.timeline-text{
font-size: 16px;
margin-bottom: 10px;
}
.timeline-mom{
color: #999;
font-size: 12px;
}
.timeline-img {
width: 48px;
height: 48px;
object-fit: cover;
border-radius: 6px;
}
.tagitem{
background: #DCE7FF;
border-radius: 8px;
color: #3471FF;
text-align: center;
padding: 5px 13px;
}
.col4{
display: inline-block;
margin: 4px 3px;
}
.tagactive{
background: #3471FF !important;
color: #fff !important;
}
.tag-dialog-scroll {
max-height: 60vh;
overflow-y: auto;
padding-right: 8px;
}
.tag-dialog-scroll {
max-height: 60vh;
overflow-y: auto;
padding-right: 8px;
/* 让内容不被滚动条遮挡 */
}
/* 优化滚动条样式(Webkit内核浏览器) */
.tag-dialog-scroll::-webkit-scrollbar {
width: 6px;
background: transparent;
}
.tag-dialog-scroll::-webkit-scrollbar-thumb {
background: #d4e0f7;
border-radius: 4px;
transition: background 0.2s;
}
.tag-dialog-scroll::-webkit-scrollbar-thumb:hover {
background: #b3c8e6;
}
.tag-dialog-scroll::-webkit-scrollbar-track {
background: transparent;
}
/* Firefox 滚动条优化 */
.tag-dialog-scroll {
scrollbar-width: thin;
scrollbar-color: #d4e0f7 transparent;
}
.dialog-footer-center {
display: flex;
justify-content: center;
gap: 16px;
}
</style>