修改代码格式

This commit is contained in:
浅念
2025-07-23 14:40:02 +08:00
parent 4a39387865
commit 7979a31e36
4 changed files with 1022 additions and 957 deletions
+222 -211
View File
@@ -1,8 +1,8 @@
<template> <template>
<div class="customer-form-page"> <div class="customer-form-page">
<div class="form-header"> <div class="form-header">
<el-link :underline="false" @click="goBack"> <el-link underline="never" @click="goBack">
<i class="iconfont-sys icon-reverse">&#xe772;</i> 返回 <i class="iconfont-sys icon-reverse">&#xe772;</i> 返回
</el-link> </el-link>
<span class="border"></span><span class="form-title">新增客户资料</span> <span class="border"></span><span class="form-title">新增客户资料</span>
</div> </div>
@@ -18,19 +18,29 @@
<el-col :span="24"> <el-col :span="24">
<el-form-item label="客户渠道" prop="channel_id"> <el-form-item label="客户渠道" prop="channel_id">
<el-select v-model="form.channel_id" filterable placeholder="请选择客户渠道"> <el-select v-model="form.channel_id" filterable placeholder="请选择客户渠道">
<el-option v-for="item in channelOptions" :key="item.id" :label="item.name" :value="item.id" /> <el-option
v-for="item in channelOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="客户商机" prop="magnitude"> <el-form-item label="客户商机" prop="magnitude">
<el-select v-model="form.magnitude" placeholder="请选择客户商机"> <el-select v-model="form.magnitude" placeholder="请选择客户商机">
<el-option v-for="item in opportunityOptions" :key="item.id" :label="item.name" :value="item.id" /> <el-option
v-for="item in opportunityOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="客户名称" prop="contacts"> <el-form-item label="客户名称" prop="contacts">
<el-input v-model="form.contacts" placeholder="请输入客户名称" /> <el-input v-model="form.contacts" placeholder="请输入客户名称" />
</el-form-item> </el-form-item>
<el-form-item label="客户性别" prop="sex" > <el-form-item label="客户性别" prop="sex">
<el-radio-group v-model="form.sex" style="justify-content: flex-end;width: 100%;"> <el-radio-group v-model="form.sex" style="justify-content: flex-end; width: 100%">
<el-radio label="0">未知</el-radio> <el-radio label="0">未知</el-radio>
<el-radio label="1">男</el-radio> <el-radio label="1">男</el-radio>
<el-radio label="2">女</el-radio> <el-radio label="2">女</el-radio>
@@ -52,16 +62,31 @@
</el-form-item> --> </el-form-item> -->
<el-form-item label="客户分级" prop="grade"> <el-form-item label="客户分级" prop="grade">
<el-select v-model="form.grade" placeholder="请选择客户分级"> <el-select v-model="form.grade" placeholder="请选择客户分级">
<el-option v-for="item in levelOptions" :key="item.id" :label="item.name" :value="item.id" /> <el-option
v-for="item in levelOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="国家" prop="country_id"> <el-form-item label="国家" prop="country_id">
<el-select v-model="form.country_id" filterable placeholder="请选择国家"> <el-select v-model="form.country_id" filterable placeholder="请选择国家">
<el-option v-for="item in countryOptions" :key="item.id" :label="item.cn_addr" :value="item.id" /> <el-option
v-for="item in countryOptions"
:key="item.id"
:label="item.cn_addr"
:value="item.id"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="公司" prop="company"> <el-form-item label="公司" prop="company">
<el-input v-model="form.company" placeholder="请输入公司" @click="showMapDialog = true" readonly /> <el-input
v-model="form.company"
placeholder="请输入公司"
@click="showMapDialog = true"
readonly
/>
</el-form-item> </el-form-item>
<el-form-item label="备注" prop="describe"> <el-form-item label="备注" prop="describe">
<el-input v-model="form.describe" type="textarea" :rows="3" placeholder="请输入备注" /> <el-input v-model="form.describe" type="textarea" :rows="3" placeholder="请输入备注" />
@@ -69,15 +94,15 @@
</el-col> </el-col>
</el-row> </el-row>
<el-form-item class="submit-row"> <el-form-item class="submit-row">
<el-button type="primary" style="width:100%" @click="submitForm">提交</el-button> <el-button type="primary" style="width: 100%" @click="submitForm">提交</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
<!-- 谷歌地图选点弹窗 --> <!-- 谷歌地图选点弹窗 -->
<el-dialog v-model="showMapDialog" title="选择公司地址" width="800px"> <el-dialog v-model="showMapDialog" title="选择公司地址" width="800px">
<div style="height:400px;position:relative;"> <div style="height: 400px; position: relative">
<!-- <input id="map-search" type="text" placeholder="搜索地点" style="position:absolute;z-index:2;top:10px;left:10px;width:300px;" /> --> <!-- <input id="map-search" type="text" placeholder="搜索地点" style="position:absolute;z-index:2;top:10px;left:10px;width:300px;" /> -->
<div id="google-map" style="width:100%;height:100%;"></div> <div id="google-map" style="width: 100%; height: 100%"></div>
</div> </div>
<template #footer> <template #footer>
<el-button @click="showMapDialog = false">取消</el-button> <el-button @click="showMapDialog = false">取消</el-button>
@@ -87,221 +112,207 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, watch, nextTick } from 'vue' import { ref, onMounted, watch, nextTick } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { CrmService } from '@/api/crmApi' import { CrmService } from '@/api/crmApi'
const router = useRouter() const router = useRouter()
const formRef = ref() const formRef = ref()
const form = ref({ const form = ref({
channel_id: '', channel_id: '',
magnitude: '', magnitude: '',
contacts: '', contacts: '',
sex: '0', sex: '0',
phone: '', phone: '',
email: '', email: '',
WhatsApp: '', WhatsApp: '',
grade: '', grade: '',
country_id: '', country_id: '',
company: '', company: '',
describe: '' describe: ''
})
const rules = {
channel: [{ required: true, message: '请选择客户渠道', trigger: 'change' }],
name: [{ required: true, message: '请输入客户名称', trigger: 'blur' }],
// car_type_id: [{ required: true, message: '请选择客户偏好', trigger: 'change' }]
}
const channelOptions = ref<any>()
const opportunityOptions = ref<any>()
const intentionOptions = ref<any>()
const levelOptions = ref<any>()
const countryOptions = ref<any>()
onMounted(() => {
getgetChannel()
getMagnitude()
getGrade()
getarea()
})
//渠道
const getgetChannel = async () => {
try {
// 这里调用获取品牌列表的接口
const res = await CrmService.getChannel()
channelOptions.value = res||[]
} catch (error) { channelOptions.value = [] }
}
//商机
const getMagnitude = async () => {
try {
// 这里调用获取品牌列表的接口
const res = await CrmService.getMagnitude()
opportunityOptions.value = res||[]
} catch (error) { opportunityOptions.value = [] }
}
//客户等级
const getGrade = async () => {
try {
// 这里调用获取品牌列表的接口
const res = await CrmService.getGrade()
levelOptions.value = res||[]
} catch (error) { levelOptions.value = [] }
}
//国家
const getarea = async () => {
try {
// 这里调用获取品牌列表的接口
const res = await CrmService.getCountry()
countryOptions.value = res||[]
} catch (error) { countryOptions.value = [] }
}
function goBack() {
router.back()
}
function submitForm() {
formRef.value.validate(async (valid: boolean) => {
if (valid) {
try {
const res = await CrmService.customerAdd(form.value)
ElMessage.success(res?.message || '提交成功!')
router.push('/crmpc/customer')
} catch (e: any) {
ElMessage.error(e?.message || '提交失败')
}
}
}) })
}
const showMapDialog = ref(false) const rules = {
const selectedAddress = ref('') channel: [{ required: true, message: '请选择客户渠道', trigger: 'change' }],
const selectedLatLng = ref({ lat: null, lng: null }) name: [{ required: true, message: '请输入客户名称', trigger: 'blur' }]
// car_type_id: [{ required: true, message: '请选择客户偏好', trigger: 'change' }]
function confirmMapSelect() {
form.value.company = selectedAddress.value
// 你也可以保存 selectedLatLng.value 到 form
showMapDialog.value = false
}
let map: any, marker: any, autocomplete: any
function initMap() {
const google = (window as any).google;
const input = document.getElementById('map-search') as HTMLInputElement;
if (!input) {
setTimeout(initMap, 100);
return;
} }
map = new google.maps.Map(document.getElementById('google-map'), {
center: { lat: 39.9042, lng: 116.4074 }, // 默认北京
zoom: 12
})
marker = new google.maps.Marker({ const channelOptions = ref<any>()
map, const opportunityOptions = ref<any>()
draggable: true const levelOptions = ref<any>()
const countryOptions = ref<any>()
onMounted(() => {
getgetChannel()
getMagnitude()
getGrade()
getarea()
}) })
//渠道
const getgetChannel = async () => {
const res = await CrmService.getChannel()
channelOptions.value = res || []
}
//商机
const getMagnitude = async () => {
const res = await CrmService.getMagnitude()
opportunityOptions.value = res || []
}
//客户等级
const getGrade = async () => {
const res = await CrmService.getGrade()
levelOptions.value = res || []
}
//国家
const getarea = async () => {
const res = await CrmService.getCountry()
countryOptions.value = res || []
}
function goBack() {
router.back()
}
map.addListener('click', (e: any) => { function submitForm() {
marker.setPosition(e.latLng) formRef.value.validate(async (valid: boolean) => {
getAddressByLatLng(e.latLng) if (valid) {
}) try {
const res = await CrmService.customerAdd(form.value)
marker.addListener('dragend', (e: any) => { ElMessage.success(res?.message || '提交成功!')
getAddressByLatLng(e.latLng) router.push('/crmpc/customer')
}) } catch (e: any) {
ElMessage.error(e?.message || '提交失败')
autocomplete = new google.maps.places.Autocomplete(input)
autocomplete.bindTo('bounds', map)
autocomplete.addListener('place_changed', () => {
const place = autocomplete.getPlace()
if (!place.geometry) return
map.panTo(place.geometry.location)
marker.setPosition(place.geometry.location)
getAddressByLatLng(place.geometry.location)
})
}
function getAddressByLatLng(latlng: any) {
const google = (window as any).google;
const geocoder = new google.maps.Geocoder()
geocoder.geocode({ location: latlng }, (results: any, status: any) => {
if (status === 'OK' && results[0]) {
selectedAddress.value = results[0].formatted_address
selectedLatLng.value = {
lat: latlng.lat(),
lng: latlng.lng()
}
}
})
}
watch(showMapDialog, (val) => {
if (val) {
nextTick(() => {
setTimeout(() => {
const google = (window as any).google;
if (!google || !google.maps) {
const script = document.createElement('script')
script.src = `https://maps.googleapis.com/maps/api/js?key=AIzaSyCiQdUlA2QrEBEaZthoj7B18HTz6sxU_3I&libraries=places`
script.async = true
script.onload = initMap
document.head.appendChild(script)
} else {
initMap()
} }
}, 100) }
}) })
} }
})
const showMapDialog = ref(false)
const selectedAddress = ref('')
const selectedLatLng = ref({ lat: null, lng: null })
function confirmMapSelect() {
form.value.company = selectedAddress.value
// 你也可以保存 selectedLatLng.value 到 form
showMapDialog.value = false
}
let map: any, marker: any, autocomplete: any
function initMap() {
const google = (window as any).google
const input = document.getElementById('map-search') as HTMLInputElement
if (!input) {
setTimeout(initMap, 100)
return
}
map = new google.maps.Map(document.getElementById('google-map'), {
center: { lat: 39.9042, lng: 116.4074 }, // 默认北京
zoom: 12
})
marker = new google.maps.Marker({
map,
draggable: true
})
map.addListener('click', (e: any) => {
marker.setPosition(e.latLng)
getAddressByLatLng(e.latLng)
})
marker.addListener('dragend', (e: any) => {
getAddressByLatLng(e.latLng)
})
autocomplete = new google.maps.places.Autocomplete(input)
autocomplete.bindTo('bounds', map)
autocomplete.addListener('place_changed', () => {
const place = autocomplete.getPlace()
if (!place.geometry) return
map.panTo(place.geometry.location)
marker.setPosition(place.geometry.location)
getAddressByLatLng(place.geometry.location)
})
}
function getAddressByLatLng(latlng: any) {
const google = (window as any).google
const geocoder = new google.maps.Geocoder()
geocoder.geocode({ location: latlng }, (results: any, status: any) => {
if (status === 'OK' && results[0]) {
selectedAddress.value = results[0].formatted_address
selectedLatLng.value = {
lat: latlng.lat(),
lng: latlng.lng()
}
}
})
}
watch(showMapDialog, (val) => {
if (val) {
nextTick(() => {
setTimeout(() => {
const google = (window as any).google
if (!google || !google.maps) {
const script = document.createElement('script')
script.src = `https://maps.googleapis.com/maps/api/js?key=AIzaSyCiQdUlA2QrEBEaZthoj7B18HTz6sxU_3I&libraries=places`
script.async = true
script.onload = initMap
document.head.appendChild(script)
} else {
initMap()
}
}, 100)
})
}
})
</script> </script>
<style scoped> <style scoped>
.border{ .border {
margin: 0px 10px 0px 40px;
margin: 0px 10px 0px 40px; }
} .submit-row .el-form-item__content {
.submit-row .el-form-item__content { display: flex;
display: flex; justify-content: center;
justify-content: center; }
} .icon-reverse {
.icon-reverse { display: inline-block;
display: inline-block; transform: rotate(270deg); /* 旋转180度 */
transform: rotate(270deg); /* 旋转180度 */ }
} .customer-form-page {
.customer-form-page { min-height: 100vh;
min-height: 100vh; background: #fff;
background: #fff; display: flex;
display: flex; flex-direction: column;
flex-direction: column; align-items: center;
align-items: center; padding-top: 18px;
padding-top: 18px; }
} .form-header {
.form-header {
width: 98%; width: 98%;
margin-bottom: 24px; margin-bottom: 24px;
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 18px; font-size: 18px;
justify-content: flex-start; /* 新增,左对齐 */ justify-content: flex-start; /* 新增,左对齐 */
} }
.form-title{ .form-title {
font-weight: bold; font-weight: bold;
color: #222; color: #222;
} }
.customer-form { .customer-form {
width: 600px; width: 600px;
background: #fff; background: #fff;
padding: 10px 24px 16px 24px; padding: 10px 24px 16px 24px;
} }
.el-form-item { .el-form-item {
margin-bottom: 18px; margin-bottom: 18px;
} }
.el-form-item__label { .el-form-item__label {
color: #222; color: #222;
font-weight: 500; font-weight: 500;
} }
</style> </style>
+2 -2
View File
@@ -107,7 +107,7 @@
const rules = { const rules = {
channel: [{ required: true, message: '请选择客户渠道', trigger: 'change' }], channel: [{ required: true, message: '请选择客户渠道', trigger: 'change' }],
name: [{ required: true, message: '请输入客户名称', trigger: 'blur' }], name: [{ required: true, message: '请输入客户名称', trigger: 'blur' }]
// car_type_id: [{ required: true, message: '请选择客户偏好', trigger: 'change' }] // car_type_id: [{ required: true, message: '请选择客户偏好', trigger: 'change' }]
} }
@@ -245,4 +245,4 @@
color: #222; color: #222;
font-weight: 500; font-weight: 500;
} }
</style> </style>
+388 -359
View File
@@ -1,41 +1,53 @@
<template> <template>
<div class="follow-add-page"> <div class="follow-add-page">
<div class="boxshadow pad20"> <div class="boxshadow pad20">
<el-link :underline="false" @click="goBack"> <el-link underline="never" @click="goBack">
<i class="iconfont-sys icon-reverse">&#xe772;</i> 返回 <i class="iconfont-sys icon-reverse">&#xe772;</i> 返回
</el-link> </el-link>
<span class="border"></span><span class="edit-title" style="margin-left: 12px;">添加跟进</span> <span class="border"></span><span class="edit-title" style="margin-left: 12px">添加跟进</span>
</div> </div>
<div class="edit-header boxshadow" style="position:relative;"> <div class="edit-header boxshadow" style="position: relative">
<!-- 右上角三角形角标 --> <!-- 右上角三角形角标 -->
<div class="corner-tag"><span class="corner-text">{{customerInfo.follow_txt}}</span></div> <div class="corner-tag"
<el-row :gutter="20" style="flex: 1;"> ><span class="corner-text">{{ customerInfo.follow_txt }}</span></div
<el-col :sm="24" :md="6" :lg="4"> >
<span>客户名称:{{ customerInfo.contacts }}</span> <el-row :gutter="20" style="flex: 1">
</el-col> <el-col :sm="24" :md="6" :lg="4">
<el-col :sm="24" :md="6" :lg="4"> <span>客户名称:{{ customerInfo.contacts }}</span>
<span>渠道:{{customerInfo.channel_name}}</span> </el-col>
</el-col> <el-col :sm="24" :md="6" :lg="4">
<el-col :sm="24" :md="6" :lg="5"> <span>渠道:{{ customerInfo.channel_name }}</span>
<span>联系方式:{{customerInfo.phone}}</span> </el-col>
</el-col> <el-col :sm="24" :md="6" :lg="5">
<el-col :sm="24" :md="6" :lg="4"> <span>联系方式:{{ customerInfo.phone }}</span>
<span>客户偏好:{{customerInfo.car_type_name}}</span> </el-col>
</el-col> <el-col :sm="24" :md="6" :lg="4">
<el-col :sm="24" :md="6" :lg="4"> <span>客户偏好:{{ customerInfo.car_type_name }}</span>
<span>客户商机:{{customerInfo.magnitude_txt}}</span> </el-col>
</el-col> <el-col :sm="24" :md="6" :lg="4">
</el-row> <span>客户商机:{{ customerInfo.magnitude_txt }}</span>
</el-col>
</el-row>
</div> </div>
<el-card class="info-card" shadow="never" style="margin-top: 16px;"> <el-card class="info-card" shadow="never" style="margin-top: 16px">
<div class="info-row"> <div class="info-row">
<span class="edit-title">客户分级</span> <span class="edit-title">客户分级</span>
<el-select v-model="customerInfo.grade" placeholder="请选择客户分级" style="width: 120px; margin-left: 16px;" @change="handleGradeChange"> <el-select
<el-option v-for="item in levelOptions" :key="item.id" :label="item.name" :value="item.id" /> 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> </el-select>
</div> </div>
</el-card> </el-card>
<el-card class="info-card" shadow="never" style="margin-top: 16px;"> <el-card class="info-card" shadow="never" style="margin-top: 16px">
<div class="info-row"> <div class="info-row">
<span class="edit-title">跟进方式</span> <span class="edit-title">跟进方式</span>
<el-radio-group v-model="form.type"> <el-radio-group v-model="form.type">
@@ -44,18 +56,23 @@
</el-radio-group> </el-radio-group>
</div> </div>
</el-card> </el-card>
<el-card class="info-card" shadow="never" style="margin-top: 16px;"> <el-card class="info-card" shadow="never" style="margin-top: 16px">
<div class="info-row"> <div class="info-row">
<span class="edit-title">跟进时间</span> <span class="edit-title">跟进时间</span>
<el-date-picker v-model="form.firstFollowtime" type="datetime" :value-format="'YYYY-MM-DD HH:mm:ss'" style="width: 200px;" /> <el-date-picker
v-model="form.firstFollowtime"
type="datetime"
:value-format="'YYYY-MM-DD HH:mm:ss'"
style="width: 200px"
/>
</div> </div>
</el-card> </el-card>
<el-card v-if="form.type=='1'" class="info-card" shadow="never" style="margin-top: 16px;"> <el-card v-if="form.type == '1'" class="info-card" shadow="never" style="margin-top: 16px">
<div class="info-top"> <div class="info-top">
<span class="edit-title">上传跟进截图/聊天截图</span> <span class="edit-title">上传跟进截图/聊天截图</span>
<el-upload <el-upload
class="upload-demo" class="upload-demo"
:action="actionImageUplode" :action="actionImageUplode"
:headers="uploadHeaders" :headers="uploadHeaders"
:on-success="handleUploadSuccess" :on-success="handleUploadSuccess"
:show-file-list="false" :show-file-list="false"
@@ -64,25 +81,31 @@
</el-upload> </el-upload>
</div> </div>
</el-card> </el-card>
<el-card v-if="form.type=='1'" class="info-card" shadow="never" style="margin-top: 16px;"> <el-card v-if="form.type == '1'" class="info-card" shadow="never" style="margin-top: 16px">
<div class="info-top"> <div class="info-top">
<span class="edit-title">跟进描述</span> <span class="edit-title">跟进描述</span>
<el-input v-model="form.audio_text" type="textarea" placeholder="请输入跟进描述" /> <el-input v-model="form.audio_text" type="textarea" placeholder="请输入跟进描述" />
</div> </div>
</el-card> </el-card>
<el-card v-if="form.type=='0'" class="info-card" shadow="never" style="margin-top: 16px;"> <el-card v-if="form.type == '0'" class="info-card" shadow="never" style="margin-top: 16px">
<div class="info-row"> <div class="info-row">
<span class="edit-title">当前位置</span> <span class="edit-title">当前位置</span>
<el-input v-model="followDesc" type="input" placeholder="请输入当前位置" style="width: 200px;" disabled> <el-input
v-model="followDesc"
type="input"
placeholder="请输入当前位置"
style="width: 200px"
disabled
>
</el-input> </el-input>
</div> </div>
</el-card> </el-card>
<el-card v-if="form.type=='0'" class="info-card" shadow="never" style="margin-top: 16px;"> <el-card v-if="form.type == '0'" class="info-card" shadow="never" style="margin-top: 16px">
<div class="info-top"> <div class="info-top">
<span class="edit-title">当前位置拍照上传</span> <span class="edit-title">当前位置拍照上传</span>
<el-upload <el-upload
class="upload-demo" class="upload-demo"
:action="actionImageUplode" :action="actionImageUplode"
:headers="uploadHeaders" :headers="uploadHeaders"
:on-success="handleUploadSuccess1" :on-success="handleUploadSuccess1"
:show-file-list="false" :show-file-list="false"
@@ -91,24 +114,25 @@
</el-upload> </el-upload>
</div> </div>
</el-card> </el-card>
<el-card v-if="form.type=='0'" class="info-card" shadow="never" style="margin-top: 16px;"> <el-card v-if="form.type == '0'" class="info-card" shadow="never" style="margin-top: 16px">
<div class="info-top"> <div class="info-top">
<span class="edit-title">拜访内容</span> <span class="edit-title">拜访内容</span>
<el-input v-model="form.audio_text" type="textarea" placeholder="请输入拜访内容" /> <el-input v-model="form.audio_text" type="textarea" placeholder="请输入拜访内容" />
</div> </div>
</el-card> </el-card>
<el-card class="info-card" shadow="never" style="margin-top: 16px;"> <el-card class="info-card" shadow="never" style="margin-top: 16px">
<div class="info-top"> <div class="info-top">
<span class="edit-title">备注</span> <span class="edit-title">备注</span>
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" /> <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
</div> </div>
</el-card> </el-card>
<el-card class="info-card" shadow="never" style="margin-top: 16px;"> <el-card class="info-card" shadow="never" style="margin-top: 16px">
<div> <div>
<span class="border" style="margin: 0px 10px 0px 0px;"></span><span class="edit-title">跟进记录</span> <span class="border" style="margin: 0px 10px 0px 0px"></span
><span class="edit-title">跟进记录</span>
</div> </div>
<div class="follow-list"> <div class="follow-list">
<ElScrollbar > <ElScrollbar>
<template v-if="followList.length > 0"> <template v-if="followList.length > 0">
<ElTimeline> <ElTimeline>
<ElTimelineItem <ElTimelineItem
@@ -121,10 +145,21 @@
<div class="timeline-item"> <div class="timeline-item">
<div class="timeline-content-row"> <div class="timeline-content-row">
<div class="timeline-content"> <div class="timeline-content">
<p class="timeline-text">{{item.type_txt}} <span class="timeline-mom">{{ item.created_at }}</span></p> <p class="timeline-text"
<p class="timeline-mom"><span v-if="item.type==1">跟进描述</span><span v-else>拜访内容</span> {{ item.audio_text }}</p> >{{ 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> </div>
<img v-if="item.picture_full" :src="item.picture_full" class="timeline-img" alt="图片" /> <img
v-if="item.picture_full"
:src="item.picture_full"
class="timeline-img"
alt="图片"
/>
</div> </div>
</div> </div>
</ElTimelineItem> </ElTimelineItem>
@@ -132,7 +167,7 @@
</template> </template>
<template v-else> <template v-else>
<!-- 使用 Element Plus 的空状态组件 --> <!-- 使用 Element Plus 的空状态组件 -->
<el-empty :image="emptyimg" description="暂无跟进记录" /> <el-empty :image="emptyimg" description="暂无跟进记录" />
<!-- <el-empty /> --> <!-- <el-empty /> -->
<!-- 或者自定义图片 --> <!-- 或者自定义图片 -->
<!-- <img src="你的空图片地址.png" alt="暂无数据" class="empty-img" /> --> <!-- <img src="你的空图片地址.png" alt="暂无数据" class="empty-img" /> -->
@@ -158,7 +193,7 @@
<div class="remind-title">下次跟进提醒设置</div> <div class="remind-title">下次跟进提醒设置</div>
<div class="remind-options"> <div class="remind-options">
<div <div
v-for="(item, idx) in remindOptions.filter(opt => opt.value !== 'custom')" v-for="item in remindOptions.filter((opt) => opt.value !== 'custom')"
:key="item.value" :key="item.value"
:class="['remind-btn', { active: remindValue === item.value }]" :class="['remind-btn', { active: remindValue === item.value }]"
@click="remindValue = item.value" @click="remindValue = item.value"
@@ -167,12 +202,12 @@
</div> </div>
</div> </div>
<el-date-picker <el-date-picker
v-model="customDate" v-model="customDate"
type="datetime" type="datetime"
placeholder="自定义" placeholder="自定义"
style="width:100%;" style="width: 100%"
CircleClose CircleClose
:disabled-date="disabledPastDate" :disabled-date="disabledPastDate"
/> />
<el-button type="primary" class="confirm-btn" @click="handleSuccessConfirm">确定</el-button> <el-button type="primary" class="confirm-btn" @click="handleSuccessConfirm">确定</el-button>
</div> </div>
@@ -181,335 +216,329 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import emptyimg from '@/assets/img/crm/empty.png' import emptyimg from '@/assets/img/crm/empty.png'
import imageUrl1 from '@/assets/img/crm/upimg.png' import imageUrl1 from '@/assets/img/crm/upimg.png'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { ElTimeline, ElTimelineItem, ElScrollbar } from 'element-plus' import { ElTimeline, ElTimelineItem, ElScrollbar } from 'element-plus'
import { CrmService } from '@/api/crmApi' import { CrmService } from '@/api/crmApi'
import { useUploadHeaders } from '@/composables/useUploadHeaders' import { useUploadHeaders } from '@/composables/useUploadHeaders'
const { uploadHeaders } = useUploadHeaders() const { uploadHeaders } = useUploadHeaders()
let actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload` let actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload`
const TIMELINE_PLACEMENT = 'top' const TIMELINE_PLACEMENT = 'top'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const customerId = route.query.id const customerId = route.query.id
const levelOptions = ref<any>() const levelOptions = ref<any>()
const followDesc = ref('') const followDesc = ref('')
const activecolor = "#3471FF" const activecolor = '#3471FF'
const successDialogVisible = ref(false) // 控制弹窗显示 const successDialogVisible = ref(false) // 控制弹窗显示
const remindValue = ref('1') const remindValue = ref('1')
const customDate = ref('') const customDate = ref('')
const imageUrl = ref(imageUrl1) const imageUrl = ref(imageUrl1)
const imageUrls = ref(imageUrl1) const imageUrls = ref(imageUrl1)
function getNowStr() { function getNowStr() {
const d = new Date() const d = new Date()
const pad = (n: any) => n < 10 ? '0' + n : n const pad = (n: any) => (n < 10 ? '0' + n : n)
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}` return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
} }
const form=ref({ const form = ref({
customer_id:route.query.id, customer_id: route.query.id,
type: '1', type: '1',
address: '', address: '',
lat: '', lat: '',
lng: '', lng: '',
firstFollowtime: getNowStr(), firstFollowtime: getNowStr(),
audio_text:'', audio_text: '',
remark:'', remark: '',
picture:'' picture: ''
}) })
// 定义客户详情类型 // 定义客户详情类型
interface CustomerInfo { interface CustomerInfo {
contacts?: string contacts?: string
// 可根据实际接口补充更多字段 // 可根据实际接口补充更多字段
[key: string]: any [key: string]: any
} }
const customerInfo = ref<CustomerInfo>({}) const customerInfo = ref<CustomerInfo>({})
const followList = ref<CustomerInfo>({}) const followList = ref<CustomerInfo>({})
//获取详情 //获取详情
const getdetilfn = async() => { const getdetilfn = async () => {
try { const res = await CrmService.getcustomerdetail({ id: customerId })
// 这里调用获取品牌列表的接口
const res = await CrmService.getcustomerdetail({id:customerId})
customerInfo.value = res customerInfo.value = res
console.log(res) }
} catch (error) { } //客户等级
} const getGrade = async () => {
//客户等级
const getGrade = async () => {
try {
// 这里调用获取品牌列表的接口
const res = await CrmService.getGrade() const res = await CrmService.getGrade()
levelOptions.value = res||[] levelOptions.value = res || []
} catch (error) { levelOptions.value = [] } }
} onMounted(async () => {
onMounted(async () => { await getGrade()
await getGrade() await getdetilfn()
await getdetilfn() getCurrentLocation()
getCurrentLocation() getfollowlist()
getfollowlist()
})
//修改等级
function handleGradeChange(newGrade: number) {
if (!customerId) return
CrmService.customergrade({ id: customerId, grade: newGrade }).then(res => {
ElMessage.success(res?.message || '修改成功')
}).catch(() => {
ElMessage.error('修改失败')
}) })
} //修改等级
//跟进记录getfollowlist function handleGradeChange(newGrade: number) {
const getfollowlist = async () => { if (!customerId) return
try { CrmService.customergrade({ id: customerId, grade: newGrade })
const res = await CrmService.getfollowlist({customer_id:route.query.id}) .then((res) => {
followList.value = res||[] ElMessage.success(res?.message || '修改成功')
} catch (error) { followList.value = [] } })
} .catch(() => {
ElMessage.error('修改失败')
})
}
//跟进记录getfollowlist
const getfollowlist = async () => {
const res = await CrmService.getfollowlist({ customer_id: route.query.id })
followList.value = res || []
}
const remindOptions = [
{ label: '1天后', value: '1' },
{ label: '3天后', value: '3' },
{ label: '7天后', value: '7' }
]
function disabledPastDate(time: Date) {
const remindOptions = [ return time.getTime() < Date.now() - 86400000
{ label: '1天后', value: '1' }, }
{ label: '3天后', value: '3' }, const handleSuccessConfirm = async () => {
{ label: '7天后', value: '7' } let remindTimestamp = null
] if (remindValue.value === 'custom') {
if (!customDate.value) {
function disabledPastDate(time: Date) { return ElMessage.warning('请选择自定义日期时间')
return time.getTime() < Date.now() - 86400000 }
} // 自定义日期转时间戳
const handleSuccessConfirm = async () =>{ remindTimestamp = Math.floor(new Date(customDate.value).getTime() / 1000)
let remindTimestamp = null } else {
if (remindValue.value === 'custom') { // 固定天数,当前时间加天数
if (!customDate.value) { const days = Number(remindValue.value)
return ElMessage.warning('请选择自定义日期时间') remindTimestamp = Math.floor((Date.now() + days * 24 * 60 * 60 * 1000) / 1000)
} }
// 自定义日期转时间戳 const res = await CrmService.followNext({ id: customerId, follow_next: remindTimestamp })
remindTimestamp = Math.floor(new Date(customDate.value).getTime() / 1000)
} else {
// 固定天数,当前时间加天数
const days = Number(remindValue.value)
remindTimestamp = Math.floor((Date.now() + days * 24 * 60 * 60 * 1000) / 1000)
}
const res = await CrmService.followNext({id:customerId,follow_next:remindTimestamp})
ElMessage.success(res?.message || '提交成功!')
successDialogVisible.value = false
setTimeout(() => {
router.push('/crmpc/customer')
}, 1000);
}
function goBack() {
router.back()
}
const handleSubmit = async () => {
try {
form.value.address=followDesc.value
const res = await CrmService.followAdd(form.value)
ElMessage.success(res?.message || '提交成功!') ElMessage.success(res?.message || '提交成功!')
successDialogVisible.value=true successDialogVisible.value = false
// router.push('/crmpc/customer') setTimeout(() => {
} catch (e: any) { router.push('/crmpc/customer')
ElMessage.error(e?.message || '提交失败') }, 1000)
} }
} function goBack() {
// 在 <script setup> 里添加获取地理位置方法 router.back()
function getCurrentLocation() { }
if (navigator.geolocation) { const handleSubmit = async () => {
navigator.geolocation.getCurrentPosition( try {
async (position) => { form.value.address = followDesc.value
const { latitude, longitude } = position.coords const res = await CrmService.followAdd(form.value)
// 可选:调用第三方逆地理编码API获取详细地址 ElMessage.success(res?.message || '提交成功!')
const googleKey = 'AIzaSyCiQdUlA2QrEBEaZthoj7B18HTz6sxU_3I' successDialogVisible.value = true
const url = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&key=${googleKey}` // router.push('/crmpc/customer')
try { } catch (e: any) {
const res = await fetch(url) ElMessage.error(e?.message || '提交失败')
const data = await res.json() }
if (data.status === 'OK' && data.plus_code) { }
followDesc.value = data.plus_code.compound_code // 在 <script setup> 里添加获取地理位置方法
} else { function getCurrentLocation() {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
async (position) => {
const { latitude, longitude } = position.coords
// 可选:调用第三方逆地理编码API获取详细地址
const googleKey = 'AIzaSyCiQdUlA2QrEBEaZthoj7B18HTz6sxU_3I'
const url = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&key=${googleKey}`
try {
const res = await fetch(url)
const data = await res.json()
if (data.status === 'OK' && data.plus_code) {
followDesc.value = data.plus_code.compound_code
} else {
followDesc.value = `纬度: ${latitude}, 经度: ${longitude}`
}
} catch (e) {
console.error('获取地址失败:', e)
followDesc.value = `纬度: ${latitude}, 经度: ${longitude}` followDesc.value = `纬度: ${latitude}, 经度: ${longitude}`
} }
} catch (e) { },
followDesc.value = `纬度: ${latitude}, 经度: ${longitude}` (error) => {
console.log('定位失败:', error)
followDesc.value = '无法获取当前位置'
} }
}, )
(error) => { } else {
followDesc.value = '无法获取当前位置' followDesc.value = '浏览器不支持定位功能'
} }
) }
} else { function handleUploadSuccess(response: any) {
followDesc.value = '浏览器不支持定位功能' // 假设返回图片地址字段为 response.url 或 response.data.url
imageUrl.value = response.url || (response.data && response.data.file_url) || ''
// 如需同步到 form.picture
form.value.picture = response.url || (response.data && response.data.file_name) || ''
}
function handleUploadSuccess1(response: any) {
// 假设返回图片地址字段为 response.url 或 response.data.url
imageUrls.value = response.url || (response.data && response.data.file_url) || ''
// 如需同步到 form.picture
form.value.picture = response.url || (response.data && response.data.file_name) || ''
} }
}
function handleUploadSuccess(response: any) {
// 假设返回图片地址字段为 response.url 或 response.data.url
imageUrl.value = response.url || (response.data && response.data.file_url) || ''
// 如需同步到 form.picture
form.value.picture = response.url || (response.data && response.data.file_name) || ''
}
function handleUploadSuccess1(response: any) {
// 假设返回图片地址字段为 response.url 或 response.data.url
imageUrls.value = response.url || (response.data && response.data.file_url) || ''
// 如需同步到 form.picture
form.value.picture = response.url || (response.data && response.data.file_name) || ''
}
</script> </script>
<style scoped> <style scoped>
.timeline-content-row{ .timeline-content-row {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
} }
.avatar,.timeline-img{ .avatar,
width: 80px; .timeline-img {
} width: 80px;
.border{ }
margin-left: 30px; .border {
} margin-left: 30px;
.info-row { }
display: flex; .info-row {
align-items: center; display: flex;
justify-content: space-between; /* 两端对齐 */ align-items: center;
font-size: 15px; justify-content: space-between; /* 两端对齐 */
padding: 0; font-size: 15px;
} padding: 0;
.edit-header{ }
padding: 40px 20px; .edit-header {
} padding: 40px 20px;
.follow-add-page { }
background: #f7f9fa; .follow-add-page {
min-height: 100vh; background: #f7f9fa;
padding: 24px; min-height: 100vh;
} padding: 24px;
.boxshadow { }
background: #fff; .boxshadow {
border-radius: 8px; background: #fff;
box-shadow: 0 2px 8px 0 rgba(0,0,0,0.04); border-radius: 8px;
} box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.04);
.pad20 { }
padding: 20px; .pad20 {
} padding: 20px;
.edit-title { }
font-size: 18px; .edit-title {
font-weight: bold; font-size: 18px;
} font-weight: bold;
.info-card { }
margin-bottom: 0; .info-card {
border-radius: 8px; margin-bottom: 0;
} border-radius: 8px;
}
.submit-btn { .submit-btn {
display: block; display: block;
width: 240px; width: 240px;
margin: 32px auto 0 auto; margin: 32px auto 0 auto;
font-size: 18px; font-size: 18px;
height: 48px; height: 48px;
border-radius: 6px; border-radius: 6px;
} }
.corner-tag { .corner-tag {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
width: 90px; width: 90px;
height: 90px; height: 90px;
background: transparent; background: transparent;
z-index: 2; z-index: 2;
pointer-events: none; pointer-events: none;
} }
.corner-tag::before { .corner-tag::before {
content: ''; content: '';
position: absolute; position: absolute;
top: -9px; top: -9px;
right: -9px; right: -9px;
width: 90px; width: 90px;
height: 75px; height: 75px;
background: #3471FF; background: #3471ff;
clip-path: polygon(100% 0, 0 0, 100% 100%); clip-path: polygon(100% 0, 0 0, 100% 100%);
border-top-right-radius: 12px; border-top-right-radius: 12px;
} }
.corner-tag { .corner-tag {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
justify-content: flex-end; justify-content: flex-end;
color: #fff; color: #fff;
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
position: absolute; position: absolute;
top: 8px; top: 8px;
right: 8px; right: 8px;
width: 55px; width: 55px;
height: 40px; height: 40px;
z-index: 3; z-index: 3;
padding-top: 6px; padding-top: 6px;
padding-right: 0px; padding-right: 0px;
pointer-events: none; pointer-events: none;
} }
.corner-text { .corner-text {
display: inline-block; display: inline-block;
transform: rotate(45deg); transform: rotate(45deg);
} }
.info-top span{ .info-top span {
margin-bottom: 15px; margin-bottom: 15px;
display: inline-block; display: inline-block;
} }
/* 成功弹出层 */ /* 成功弹出层 */
.success-dialog-content { .success-dialog-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 12px 0 0 0; padding: 12px 0 0 0;
} }
.success-img { .success-img {
width:55%; width: 55%;
margin-bottom: 12px; margin-bottom: 12px;
} }
.conts{ .conts {
margin-top: -55px; margin-top: -55px;
} }
.success-title { .success-title {
font-size: 20px; font-size: 20px;
font-weight: 500; font-weight: 500;
margin-bottom: 40px; margin-bottom: 40px;
color: #333; color: #333;
text-align: center; text-align: center;
} }
.remind-title { .remind-title {
font-size: 16px; font-size: 16px;
color: #333; color: #333;
margin-bottom: 20px; margin-bottom: 20px;
} }
.remind-options { .remind-options {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
margin-bottom: 20px; margin-bottom: 20px;
} }
.remind-btn { .remind-btn {
background: #f4f7fe; background: #f4f7fe;
color: #3471FF; color: #3471ff;
border-radius: 8px; border-radius: 8px;
padding: 6px 18px; padding: 6px 18px;
cursor: pointer; cursor: pointer;
font-size: 15px; font-size: 15px;
border: 1px solid transparent; border: 1px solid transparent;
transition: all 0.2s; transition: all 0.2s;
min-width: 64px; min-width: 64px;
text-align: center; text-align: center;
flex: 1; flex: 1;
} }
.remind-btn.active { .remind-btn.active {
background: #3471FF; background: #3471ff;
color: #fff; color: #fff;
border: 1px solid #3471FF; border: 1px solid #3471ff;
} }
.confirm-btn { .confirm-btn {
width: 100%; width: 100%;
height: 44px; height: 44px;
font-size: 18px; font-size: 18px;
border-radius: 6px; border-radius: 6px;
margin-top: 16px; margin-top: 16px;
} }
</style> </style>
+410 -385
View File
@@ -1,62 +1,82 @@
<template> <template>
<div class="customer-edit-page"> <div class="customer-edit-page">
<div class="boxshadow pad20"> <div class="boxshadow pad20">
<el-link :underline="false" @click="goBack"> <el-link underline="never" @click="goBack">
<i class="iconfont-sys icon-reverse">&#xe772;</i> 返回 <i class="iconfont-sys icon-reverse">&#xe772;</i> 返回
</el-link> </el-link>
</div> </div>
<div class="edit-header boxshadow pad20" style="position:relative;"> <div class="edit-header boxshadow pad20" style="position: relative">
<span class="border"></span><span class="edit-title">客户资料</span> <span class="border"></span><span class="edit-title">客户资料</span>
<!-- 右上角三角形角标 --> <!-- 右上角三角形角标 -->
<div class="corner-tag"><span class="corner-text">{{customerInfo.follow_txt}}</span></div> <div class="corner-tag"
><span class="corner-text">{{ customerInfo.follow_txt }}</span></div
>
<div class="info-row-bar"> <div class="info-row-bar">
<el-row :gutter="20" style="flex: 1;"> <el-row :gutter="20" style="flex: 1">
<el-col :sm="24" :md="6" :lg="4"> <el-col :sm="24" :md="6" :lg="4">
<span>客户名称:{{ customerInfo.contacts }}</span> <span>客户名称:{{ customerInfo.contacts }}</span>
</el-col> </el-col>
<el-col :sm="24" :md="6" :lg="4"> <el-col :sm="24" :md="6" :lg="4">
<span>渠道:{{customerInfo.channel_name}}</span> <span>渠道:{{ customerInfo.channel_name }}</span>
</el-col> </el-col>
<el-col :sm="24" :md="6" :lg="5"> <el-col :sm="24" :md="6" :lg="5">
<span>联系方式:{{customerInfo.phone}}</span> <span>联系方式:{{ customerInfo.phone }}</span>
</el-col> </el-col>
<el-col :sm="24" :md="6" :lg="4"> <el-col :sm="24" :md="6" :lg="4">
<span>客户偏好:{{customerInfo.car_type_name}}</span> <span>客户偏好:{{ customerInfo.car_type_name }}</span>
</el-col> </el-col>
<el-col :sm="24" :md="6" :lg="4"> <el-col :sm="24" :md="6" :lg="4">
<span>客户商机:{{customerInfo.magnitude_txt}}</span> <span>客户商机:{{ customerInfo.magnitude_txt }}</span>
</el-col> </el-col>
</el-row> </el-row>
<el-button @click="goEdit" type="primary" class="edit-btn" size="small"><i class="iconfont-sys"> &#xe6bf;</i> 编辑资料</el-button> <el-button @click="goEdit" type="primary" class="edit-btn" size="small"
><i class="iconfont-sys"> &#xe6bf;</i> 编辑资料</el-button
>
</div> </div>
</div> </div>
<el-card class="info-card" shadow="never" style="margin-top: 16px;"> <el-card class="info-card" shadow="never" style="margin-top: 16px">
<div class=""> <div class="">
<span class="border"></span><span class="edit-title">客户画像</span> <span class="border"></span><span class="edit-title">客户画像</span>
<el-button @click="addtag" type="primary" size="small" style="float: right;">添加标签</el-button> <el-button @click="addtag" type="primary" size="small" style="float: right"
>添加标签</el-button
>
</div> </div>
<div class="tag-list"> <div class="tag-list">
<el-tag type="primary" v-for="(item, index) in customerInfo.tags" :key="index">{{item.name}}</el-tag> <el-tag type="primary" v-for="(item, index) in customerInfo.tags" :key="index">{{
item.name
}}</el-tag>
</div> </div>
</el-card> </el-card>
<el-card class="info-card" shadow="never" style="margin-top: 16px;"> <el-card class="info-card" shadow="never" style="margin-top: 16px">
<div class="info-row"> <div class="info-row">
<span class="edit-title">客户分级</span> <span class="edit-title">客户分级</span>
<el-select v-model="customerInfo.grade" placeholder="请选择客户分级" style="width: 120px; margin-left: 16px;" @change="handleGradeChange"> <el-select
<el-option v-for="item in levelOptions" :key="item.id" :label="item.name" :value="item.id" /> 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> </el-select>
</div> </div>
</el-card> </el-card>
<el-card class="info-card" shadow="never" style="margin-top: 16px;"> <el-card class="info-card" shadow="never" style="margin-top: 16px">
<div class="info-row"> <div class="info-row">
<span class="edit-title">跟进记录</span> <span class="edit-title">跟进记录</span>
<el-button type="primary" size="small" style="float: right;" @click="goAddFollow">+ 添加跟进</el-button> <el-button type="primary" size="small" style="float: right" @click="goAddFollow"
>+ 添加跟进</el-button
>
</div> </div>
<div class="follow-list"> <div class="follow-list">
<ElScrollbar > <ElScrollbar>
<template v-if="followList.length > 0"> <template v-if="followList.length > 0">
<ElTimeline> <ElTimeline>
<ElTimelineItem <ElTimelineItem
@@ -69,10 +89,21 @@
<div class="timeline-item"> <div class="timeline-item">
<div class="timeline-content-row"> <div class="timeline-content-row">
<div class="timeline-content"> <div class="timeline-content">
<p class="timeline-text">{{item.type_txt}} <span class="timeline-mom">{{ item.created_at }}</span></p> <p class="timeline-text"
<p class="timeline-mom"><span v-if="item.type==1">跟进描述</span><span v-else>拜访内容</span> {{ item.audio_text }}</p> >{{ 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> </div>
<img v-if="item.picture_full" :src="item.picture_full" class="timeline-img" alt="图片" /> <img
v-if="item.picture_full"
:src="item.picture_full"
class="timeline-img"
alt="图片"
/>
</div> </div>
</div> </div>
</ElTimelineItem> </ElTimelineItem>
@@ -80,7 +111,7 @@
</template> </template>
<template v-else> <template v-else>
<!-- 使用 Element Plus 的空状态组件 --> <!-- 使用 Element Plus 的空状态组件 -->
<el-empty :image="emptyimg" description="暂无跟进记录" /> <el-empty :image="emptyimg" description="暂无跟进记录" />
<!-- <el-empty /> --> <!-- <el-empty /> -->
<!-- 或者自定义图片 --> <!-- 或者自定义图片 -->
<!-- <img src="你的空图片地址.png" alt="暂无数据" class="empty-img" /> --> <!-- <img src="你的空图片地址.png" alt="暂无数据" class="empty-img" /> -->
@@ -97,12 +128,12 @@
:show-close="false" :show-close="false"
> >
<div class="tag-dialog-scroll"> <div class="tag-dialog-scroll">
<div v-for="group in tagGroups" :key="group.name" style="margin-bottom: 12px;"> <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="font-weight: bold; margin-bottom: 6px">{{ group.name }}</div>
<div style="display: flex; flex-wrap: wrap; gap: 8px;"> <div style="display: flex; flex-wrap: wrap; gap: 8px">
<div <div
class="col4 tagitem" class="col4 tagitem"
style="cursor: pointer;" style="cursor: pointer"
v-for="(item1, index1) in group.tags" v-for="(item1, index1) in group.tags"
:class="{ 'tagactive': activeMap[group.id] === item1.id }" :class="{ 'tagactive': activeMap[group.id] === item1.id }"
@click="tagSelect(item1)" @click="tagSelect(item1)"
@@ -123,73 +154,61 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref,reactive } from 'vue' import { ref, reactive } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import emptyimg from '@/assets/img/crm/empty.png' import emptyimg from '@/assets/img/crm/empty.png'
import { CrmService } from '@/api/crmApi' import { CrmService } from '@/api/crmApi'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
// 常量配置 // 常量配置
const TIMELINE_PLACEMENT = 'top' const TIMELINE_PLACEMENT = 'top'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const customerLevel = ref('A') const activecolor = '#3471ff'
const activecolor = "#3471FF" const tagDialogVisible = ref(false)
const tagDialogVisible = ref(false) const customerId = route.query.id
const selectedTags = ref<string[]>([]) const levelOptions = ref<any>()
const customerId = route.query.id const tagGroups = ref<any[]>([])
const levelOptions = ref<any>() const activeMap = reactive<{ [groupId: string]: number }>({})
const tagGroups = ref<any[]>([]) // 定义客户详情类型
const activeMap = reactive<{ [groupId: string]: number }>({}) interface CustomerInfo {
// 定义客户详情类型 contacts?: string
interface CustomerInfo { // 可根据实际接口补充更多字段
contacts?: string [key: string]: any
// 可根据实际接口补充更多字段 }
[key: string]: any const customerInfo = ref<CustomerInfo>({})
} //获取详情
const customerInfo = ref<CustomerInfo>({}) const getdetilfn = async () => {
//获取详情 const res = await CrmService.getcustomerdetail({ id: customerId })
const getdetilfn = async() => {
try {
// 这里调用获取品牌列表的接口
const res = await CrmService.getcustomerdetail({id:customerId})
customerInfo.value = res customerInfo.value = res
console.log(res) }
} catch (error) { } //客户等级
} const getGrade = async () => {
//客户等级
const getGrade = async () => {
try {
// 这里调用获取品牌列表的接口
const res = await CrmService.getGrade() const res = await CrmService.getGrade()
levelOptions.value = res||[] levelOptions.value = res || []
} catch (error) { levelOptions.value = [] } }
} //跟进记录getfollowlist
//跟进记录getfollowlist const followList = ref<CustomerInfo>({})
const followList = ref<CustomerInfo>({}) const getfollowlist = async () => {
const getfollowlist = async () => { const res = await CrmService.getfollowlist({ customer_id: route.query.id })
try { followList.value = res || []
const res = await CrmService.getfollowlist({customer_id:route.query.id}) }
followList.value = res||[] onMounted(async () => {
} catch (error) { followList.value = [] } await getGrade()
} await getdetilfn()
onMounted(async () => { // 在 getdetilfn 之后调用 tagGrouplistfn,确保 customerInfo 有数据
await getGrade() await tagGrouplistfn()
await getdetilfn() getfollowlist()
// 在 getdetilfn 之后调用 tagGrouplistfn,确保 customerInfo 有数据 })
await tagGrouplistfn() function addtag() {
getfollowlist() tagDialogVisible.value = true
}) tagGrouplistfn()
function addtag(){ }
tagDialogVisible.value = true const tagGrouplistfn = async () => {
tagGrouplistfn()
}
const tagGrouplistfn = async () => {
try {
const res = await CrmService.tagGrouplist({}) const res = await CrmService.tagGrouplist({})
tagGroups.value = res || [] tagGroups.value = res || []
// 回显选中 // 回显选中
if (customerInfo.value && Array.isArray(customerInfo.value.tags)) { if (customerInfo.value && Array.isArray(customerInfo.value.tags)) {
tagGroups.value.forEach(group => { tagGroups.value.forEach((group) => {
if (group.tags && Array.isArray(group.tags)) { if (group.tags && Array.isArray(group.tags)) {
const matched = group.tags.find((item: any) => const matched = group.tags.find((item: any) =>
customerInfo.value.tags.some((t: any) => t.id === item.id) customerInfo.value.tags.some((t: any) => t.id === item.id)
@@ -200,301 +219,307 @@ const tagGrouplistfn = async () => {
} }
}) })
} }
} 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 }) // 4. 点击标签
ElMessage.success(res?.message || '操作成功') async function tagSelect(item: { group_id: string | number; id: number }) {
await getdetilfn() const key = String(item.group_id)
await tagGrouplistfn() let tag_id = ''
} catch (e) { if (activeMap[key] === item.id) {
ElMessage.error('操作失败') // 取消选中
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) {
console.error(e)
ElMessage.error('操作失败')
}
} }
}
// 5. 获取所有已选中的标签 // 5. 获取所有已选中的标签
// function getSelectedTags() { // function getSelectedTags() {
// const selected: any[] = [] // const selected: any[] = []
// for (const group of tagGroups) { // for (const group of tagGroups) {
// const key = String(group.id) // const key = String(group.id)
// const tagId = activeMap[key] // const tagId = activeMap[key]
// if (tagId) { // if (tagId) {
// const tag = group.tags.find(t => t.id === tagId) // const tag = group.tags.find(t => t.id === tagId)
// if (tag) selected.push(tag) // if (tag) selected.push(tag)
// } // }
// } // }
// return selected // return selected
// } // }
// 6. 示例:确定按钮 // 6. 示例:确定按钮
function handleConfirm() { function handleConfirm() {
// const selectedTags = getSelectedTags() // const selectedTags = getSelectedTags()
// console.log('选中的标签:', selectedTags) // console.log('选中的标签:', selectedTags)
// 处理选中的标签 // 处理选中的标签
tagDialogVisible.value = false tagDialogVisible.value = false
} }
function handleCancel() { function handleCancel() {
tagDialogVisible.value = false tagDialogVisible.value = false
} }
//编辑 //编辑
function goEdit() { function goEdit() {
router.push({ router.push({
path: '/crmpc/customer/edit', path: '/crmpc/customer/edit',
query: { id: customerId } query: { id: customerId }
}) })
} }
function goBack() { function goBack() {
router.back() router.back()
} }
//修改等级 //修改等级
function handleGradeChange(newGrade: number) { function handleGradeChange(newGrade: number) {
if (!customerId) return if (!customerId) return
CrmService.customergrade({ id: customerId, grade: newGrade }).then(res => { CrmService.customergrade({ id: customerId, grade: newGrade })
ElMessage.success(res?.message || '修改成功') .then((res) => {
}).catch(() => { ElMessage.success(res?.message || '修改成功')
ElMessage.error('修改失败') })
}) .catch(() => {
} ElMessage.error('修改失败')
//添加跟进 })
function goAddFollow() { }
router.push({ //添加跟进
path: '/crmpc/customer/followadd', function goAddFollow() {
query: { id: customerId } router.push({
}) path: '/crmpc/customer/followadd',
} query: { id: customerId }
})
}
</script> </script>
<style scoped> <style scoped>
.timeline-content-row{ .timeline-content-row {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
} }
.timeline-img{ .timeline-img {
width: 80px; width: 80px;
} }
.tag-list { .tag-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 12px; /* 标签间距 */ gap: 12px; /* 标签间距 */
margin-top: 20px; margin-top: 20px;
} }
.border{ .border {
margin-right: 10px; margin-right: 10px;
} }
.pad20{ .pad20 {
padding: 20px; padding: 20px;
} }
.icon-reverse { .icon-reverse {
display: inline-block; display: inline-block;
transform: rotate(270deg); /* 旋转180度 */ transform: rotate(270deg); /* 旋转180度 */
} }
.customer-edit-page { .customer-edit-page {
background: #f7f9fa; background: #f7f9fa;
min-height: 100vh; min-height: 100vh;
padding: 24px; padding: 24px;
} }
.edit-header { .edit-header {
/* display: flex; */ /* display: flex; */
/* align-items: center; */ /* align-items: center; */
/* gap: 12px; */ /* gap: 12px; */
margin-bottom: 16px; margin-bottom: 16px;
} }
.edit-title { .edit-title {
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
} }
.edit-status { .edit-status {
margin-left: 16px; margin-left: 16px;
} }
.edit-btn { .edit-btn {
margin-left: auto; margin-left: auto;
} }
.info-card { .info-card {
margin-bottom: 0; margin-bottom: 0;
border-radius: 8px; border-radius: 8px;
} }
.info-row { .info-row {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; /* 两端对齐 */ justify-content: space-between; /* 两端对齐 */
font-size: 15px; font-size: 15px;
padding: 0; padding: 0;
} }
.follow-list { .follow-list {
margin-top: 16px; margin-top: 16px;
} }
.follow-item { .follow-item {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
margin-bottom: 16px; margin-bottom: 16px;
background: #f7f9fa; background: #f7f9fa;
border-radius: 6px; border-radius: 6px;
padding: 12px; padding: 12px;
} }
.follow-main { .follow-main {
flex: 1; flex: 1;
} }
.follow-title { .follow-title {
font-size: 14px; font-size: 14px;
color: #222; color: #222;
margin-bottom: 4px; margin-bottom: 4px;
display: flex; display: flex;
gap: 16px; gap: 16px;
} }
.follow-type { .follow-type {
color: #3471FF; color: #3471FF;
font-weight: bold; font-weight: bold;
} }
.follow-time { .follow-time {
color: #888; color: #888;
} }
.follow-content { .follow-content {
color: #333; color: #333;
font-size: 14px; font-size: 14px;
} }
.follow-img { .follow-img {
width: 60px; width: 60px;
height: 60px; height: 60px;
object-fit: cover; object-fit: cover;
border-radius: 6px; border-radius: 6px;
margin-left: 16px; margin-left: 16px;
} }
.info-row-bar { .info-row-bar {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin: 16px 0px; margin: 16px 0px;
} }
.corner-tag { .corner-tag {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
width: 90px; width: 90px;
height: 90px; height: 90px;
background: transparent; background: transparent;
z-index: 2; z-index: 2;
pointer-events: none; pointer-events: none;
} }
.corner-tag::before { .corner-tag::before {
content: ''; content: '';
position: absolute; position: absolute;
top: -10px; top: -10px;
right: -10px; right: -10px;
width: 90px; width: 90px;
height: 80px; height: 80px;
background: #3471FF; background: #3471FF;
clip-path: polygon(100% 0, 0 0, 100% 100%); clip-path: polygon(100% 0, 0 0, 100% 100%);
border-top-right-radius: 12px; border-top-right-radius: 12px;
} }
.corner-tag { .corner-tag {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
justify-content: flex-end; justify-content: flex-end;
color: #fff; color: #fff;
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
position: absolute; position: absolute;
top: 8px; top: 8px;
right: 8px; right: 8px;
width: 55px; width: 55px;
height: 40px; height: 40px;
z-index: 3; z-index: 3;
padding-top: 6px; padding-top: 6px;
padding-right: 0px; padding-right: 0px;
pointer-events: none; pointer-events: none;
} }
.corner-text { .corner-text {
display: inline-block; display: inline-block;
transform: rotate(45deg); transform: rotate(45deg);
} }
.timeline-content-row { .timeline-content-row {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
} }
.timeline-text{ .timeline-text {
font-size: 16px; font-size: 16px;
margin-bottom: 10px; margin-bottom: 10px;
} }
.timeline-mom{ .timeline-mom {
color: #999; color: #999;
font-size: 12px; font-size: 12px;
} }
.timeline-img { .timeline-img {
width: 48px; width: 48px;
height: 48px; height: 48px;
object-fit: cover; object-fit: cover;
border-radius: 6px; border-radius: 6px;
} }
.tagitem{ .tagitem {
background: #DCE7FF; background: #DCE7FF;
border-radius: 8px; border-radius: 8px;
color: #3471FF; color: #3471FF;
text-align: center; text-align: center;
padding: 5px 13px; padding: 5px 13px;
} }
.col4{ .col4 {
display: inline-block; display: inline-block;
margin: 4px 3px; margin: 4px 3px;
} }
.tagactive{ .tagactive {
background: #3471FF !important; background: #3471FF !important;
color: #fff !important; color: #fff !important;
} }
.tag-dialog-scroll { .tag-dialog-scroll {
max-height: 60vh; max-height: 60vh;
overflow-y: auto; overflow-y: auto;
padding-right: 8px; padding-right: 8px;
} }
.tag-dialog-scroll { .tag-dialog-scroll {
max-height: 60vh; max-height: 60vh;
overflow-y: auto; overflow-y: auto;
padding-right: 8px; padding-right: 8px;
/* 让内容不被滚动条遮挡 */ /* 让内容不被滚动条遮挡 */
} }
/* 优化滚动条样式(Webkit内核浏览器) */ /* 优化滚动条样式(Webkit内核浏览器) */
.tag-dialog-scroll::-webkit-scrollbar { .tag-dialog-scroll::-webkit-scrollbar {
width: 6px; width: 6px;
background: transparent; background: transparent;
} }
.tag-dialog-scroll::-webkit-scrollbar-thumb { .tag-dialog-scroll::-webkit-scrollbar-thumb {
background: #d4e0f7; background: #d4e0f7;
border-radius: 4px; border-radius: 4px;
transition: background 0.2s; transition: background 0.2s;
} }
.tag-dialog-scroll::-webkit-scrollbar-thumb:hover { .tag-dialog-scroll::-webkit-scrollbar-thumb:hover {
background: #b3c8e6; background: #b3c8e6;
} }
.tag-dialog-scroll::-webkit-scrollbar-track { .tag-dialog-scroll::-webkit-scrollbar-track {
background: transparent; background: transparent;
} }
/* Firefox 滚动条优化 */ /* Firefox 滚动条优化 */
.tag-dialog-scroll { .tag-dialog-scroll {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: #d4e0f7 transparent; scrollbar-color: #d4e0f7 transparent;
} }
.dialog-footer-center { .dialog-footer-center {
display: flex; display: flex;
justify-content: center; justify-content: center;
gap: 16px; gap: 16px;
} }
</style> </style>