修改代码格式

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
+221 -210
View File
@@ -1,8 +1,8 @@
<template>
<div class="customer-form-page">
<div class="form-header">
<el-link :underline="false" @click="goBack">
<i class="iconfont-sys icon-reverse">&#xe772;</i> 返回
<el-link underline="never" @click="goBack">
<i class="iconfont-sys icon-reverse">&#xe772;</i> 返回
</el-link>
<span class="border"></span><span class="form-title">新增客户资料</span>
</div>
@@ -18,19 +18,29 @@
<el-col :span="24">
<el-form-item label="客户渠道" prop="channel_id">
<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-form-item>
<el-form-item label="客户商机" prop="magnitude">
<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-form-item>
<el-form-item label="客户名称" prop="contacts">
<el-input v-model="form.contacts" placeholder="请输入客户名称" />
</el-form-item>
<el-form-item label="客户性别" prop="sex" >
<el-radio-group v-model="form.sex" style="justify-content: flex-end;width: 100%;">
<el-form-item label="客户性别" prop="sex">
<el-radio-group v-model="form.sex" style="justify-content: flex-end; width: 100%">
<el-radio label="0">未知</el-radio>
<el-radio label="1">男</el-radio>
<el-radio label="2">女</el-radio>
@@ -52,16 +62,31 @@
</el-form-item> -->
<el-form-item label="客户分级" prop="grade">
<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-form-item>
<el-form-item label="国家" prop="country_id">
<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-form-item>
<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 label="备注" prop="describe">
<el-input v-model="form.describe" type="textarea" :rows="3" placeholder="请输入备注" />
@@ -69,15 +94,15 @@
</el-col>
</el-row>
<el-form-item class="submit-row">
<el-button type="primary" style="width:100%" @click="submitForm">提交</el-button>
</el-form-item>
<el-button type="primary" style="width: 100%" @click="submitForm">提交</el-button>
</el-form-item>
</el-form>
</div>
<!-- 谷歌地图选点弹窗 -->
<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;" /> -->
<div id="google-map" style="width:100%;height:100%;"></div>
<div id="google-map" style="width: 100%; height: 100%"></div>
</div>
<template #footer>
<el-button @click="showMapDialog = false">取消</el-button>
@@ -87,221 +112,207 @@
</template>
<script setup lang="ts">
import { ref, onMounted, watch, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import { CrmService } from '@/api/crmApi'
import { ref, onMounted, watch, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import { CrmService } from '@/api/crmApi'
const router = useRouter()
const formRef = ref()
const form = ref({
channel_id: '',
magnitude: '',
contacts: '',
sex: '0',
phone: '',
email: '',
WhatsApp: '',
grade: '',
country_id: '',
company: '',
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 router = useRouter()
const formRef = ref()
const form = ref({
channel_id: '',
magnitude: '',
contacts: '',
sex: '0',
phone: '',
email: '',
WhatsApp: '',
grade: '',
country_id: '',
company: '',
describe: ''
})
}
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;
const rules = {
channel: [{ required: true, message: '请选择客户渠道', trigger: 'change' }],
name: [{ required: true, message: '请输入客户名称', trigger: 'blur' }]
// car_type_id: [{ required: true, message: '请选择客户偏好', trigger: 'change' }]
}
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
const channelOptions = ref<any>()
const opportunityOptions = ref<any>()
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) => {
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()
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 || '提交失败')
}
}, 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>
<style scoped>
.border{
margin: 0px 10px 0px 40px;
}
.submit-row .el-form-item__content {
display: flex;
justify-content: center;
}
.icon-reverse {
display: inline-block;
transform: rotate(270deg); /* 旋转180度 */
}
.customer-form-page {
min-height: 100vh;
background: #fff;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 18px;
}
.form-header {
.border {
margin: 0px 10px 0px 40px;
}
.submit-row .el-form-item__content {
display: flex;
justify-content: center;
}
.icon-reverse {
display: inline-block;
transform: rotate(270deg); /* 旋转180度 */
}
.customer-form-page {
min-height: 100vh;
background: #fff;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 18px;
}
.form-header {
width: 98%;
margin-bottom: 24px;
display: flex;
align-items: center;
font-size: 18px;
justify-content: flex-start; /* 新增,左对齐 */
}
.form-title{
font-weight: bold;
color: #222;
}
}
.form-title {
font-weight: bold;
color: #222;
}
.customer-form {
width: 600px;
background: #fff;
padding: 10px 24px 16px 24px;
}
.el-form-item {
margin-bottom: 18px;
}
.el-form-item__label {
color: #222;
font-weight: 500;
}
.customer-form {
width: 600px;
background: #fff;
padding: 10px 24px 16px 24px;
}
.el-form-item {
margin-bottom: 18px;
}
.el-form-item__label {
color: #222;
font-weight: 500;
}
</style>
+1 -1
View File
@@ -107,7 +107,7 @@
const rules = {
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' }]
}
+385 -356
View File
@@ -1,41 +1,53 @@
<template>
<div class="follow-add-page">
<div class="boxshadow pad20">
<el-link :underline="false" @click="goBack">
<el-link underline="never" @click="goBack">
<i class="iconfont-sys icon-reverse">&#xe772;</i> 返回
</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 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>
<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>
<div class="corner-tag"
><span class="corner-text">{{ customerInfo.follow_txt }}</span></div
>
<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>
</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">
<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
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;">
<el-card class="info-card" shadow="never" style="margin-top: 16px">
<div class="info-row">
<span class="edit-title">跟进方式</span>
<el-radio-group v-model="form.type">
@@ -44,13 +56,18 @@
</el-radio-group>
</div>
</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">
<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>
</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">
<span class="edit-title">上传跟进截图/聊天截图</span>
<el-upload
@@ -64,20 +81,26 @@
</el-upload>
</div>
</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">
<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>
</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">
<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>
</div>
</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">
<span class="edit-title">当前位置拍照上传</span>
<el-upload
@@ -91,24 +114,25 @@
</el-upload>
</div>
</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">
<span class="edit-title">拜访内容</span>
<el-input v-model="form.audio_text" type="textarea" placeholder="请输入拜访内容" />
</div>
</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">
<span class="edit-title">备注</span>
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
</div>
</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>
<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 class="follow-list">
<ElScrollbar >
<ElScrollbar>
<template v-if="followList.length > 0">
<ElTimeline>
<ElTimelineItem
@@ -121,10 +145,21 @@
<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>
<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="图片" />
<img
v-if="item.picture_full"
:src="item.picture_full"
class="timeline-img"
alt="图片"
/>
</div>
</div>
</ElTimelineItem>
@@ -132,7 +167,7 @@
</template>
<template v-else>
<!-- 使用 Element Plus 的空状态组件 -->
<el-empty :image="emptyimg" description="暂无跟进记录" />
<el-empty :image="emptyimg" description="暂无跟进记录" />
<!-- <el-empty /> -->
<!-- 或者自定义图片 -->
<!-- <img src="你的空图片地址.png" alt="暂无数据" class="empty-img" /> -->
@@ -158,7 +193,7 @@
<div class="remind-title">下次跟进提醒设置</div>
<div class="remind-options">
<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"
:class="['remind-btn', { active: remindValue === item.value }]"
@click="remindValue = item.value"
@@ -167,12 +202,12 @@
</div>
</div>
<el-date-picker
v-model="customDate"
type="datetime"
placeholder="自定义"
style="width:100%;"
CircleClose
:disabled-date="disabledPastDate"
v-model="customDate"
type="datetime"
placeholder="自定义"
style="width: 100%"
CircleClose
:disabled-date="disabledPastDate"
/>
<el-button type="primary" class="confirm-btn" @click="handleSuccessConfirm">确定</el-button>
</div>
@@ -181,335 +216,329 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import emptyimg from '@/assets/img/crm/empty.png'
import imageUrl1 from '@/assets/img/crm/upimg.png'
import { ElMessage } from 'element-plus'
import { ElTimeline, ElTimelineItem, ElScrollbar } from 'element-plus'
import { CrmService } from '@/api/crmApi'
import { useUploadHeaders } from '@/composables/useUploadHeaders'
const { uploadHeaders } = useUploadHeaders()
let actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload`
const TIMELINE_PLACEMENT = 'top'
const router = useRouter()
const route = useRoute()
const customerId = route.query.id
const levelOptions = ref<any>()
const followDesc = ref('')
const activecolor = "#3471FF"
import { ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import emptyimg from '@/assets/img/crm/empty.png'
import imageUrl1 from '@/assets/img/crm/upimg.png'
import { ElMessage } from 'element-plus'
import { ElTimeline, ElTimelineItem, ElScrollbar } from 'element-plus'
import { CrmService } from '@/api/crmApi'
import { useUploadHeaders } from '@/composables/useUploadHeaders'
const { uploadHeaders } = useUploadHeaders()
let actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload`
const TIMELINE_PLACEMENT = 'top'
const router = useRouter()
const route = useRoute()
const customerId = route.query.id
const levelOptions = ref<any>()
const followDesc = ref('')
const activecolor = '#3471FF'
const successDialogVisible = ref(false) // 控制弹窗显示
const remindValue = ref('1')
const customDate = ref('')
const imageUrl = ref(imageUrl1)
const imageUrls = ref(imageUrl1)
function getNowStr() {
const d = new Date()
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())}`
}
const form=ref({
customer_id:route.query.id,
const successDialogVisible = ref(false) // 控制弹窗显示
const remindValue = ref('1')
const customDate = ref('')
const imageUrl = ref(imageUrl1)
const imageUrls = ref(imageUrl1)
function getNowStr() {
const d = new Date()
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())}`
}
const form = ref({
customer_id: route.query.id,
type: '1',
address: '',
lat: '',
lng: '',
firstFollowtime: getNowStr(),
audio_text:'',
remark:'',
picture:''
audio_text: '',
remark: '',
picture: ''
})
// 定义客户详情类型
interface CustomerInfo {
contacts?: string
// 可根据实际接口补充更多字段
[key: string]: any
}
const customerInfo = ref<CustomerInfo>({})
const followList = ref<CustomerInfo>({})
//获取详情
const getdetilfn = async() => {
try {
// 这里调用获取品牌列表的接口
const res = await CrmService.getcustomerdetail({id:customerId})
// 定义客户详情类型
interface CustomerInfo {
contacts?: string
// 可根据实际接口补充更多字段
[key: string]: any
}
const customerInfo = ref<CustomerInfo>({})
const followList = ref<CustomerInfo>({})
//获取详情
const getdetilfn = async () => {
const res = await CrmService.getcustomerdetail({ id: customerId })
customerInfo.value = res
console.log(res)
} catch (error) { }
}
//客户等级
const getGrade = async () => {
try {
// 这里调用获取品牌列表的接口
}
//客户等级
const getGrade = async () => {
const res = await CrmService.getGrade()
levelOptions.value = res||[]
} catch (error) { levelOptions.value = [] }
}
onMounted(async () => {
await getGrade()
await getdetilfn()
getCurrentLocation()
getfollowlist()
})
//修改等级
function handleGradeChange(newGrade: number) {
if (!customerId) return
CrmService.customergrade({ id: customerId, grade: newGrade }).then(res => {
ElMessage.success(res?.message || '修改成功')
}).catch(() => {
ElMessage.error('修改失败')
levelOptions.value = res || []
}
onMounted(async () => {
await getGrade()
await getdetilfn()
getCurrentLocation()
getfollowlist()
})
}
//跟进记录getfollowlist
const getfollowlist = async () => {
try {
const res = await CrmService.getfollowlist({customer_id:route.query.id})
followList.value = res||[]
} catch (error) { followList.value = [] }
}
//修改等级
function handleGradeChange(newGrade: number) {
if (!customerId) return
CrmService.customergrade({ id: customerId, grade: newGrade })
.then((res) => {
ElMessage.success(res?.message || '修改成功')
})
.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' }
]
const remindOptions = [
{ label: '1天后', value: '1' },
{ label: '3天后', value: '3' },
{ label: '7天后', value: '7' }
]
function disabledPastDate(time: Date) {
return time.getTime() < Date.now() - 86400000
}
const handleSuccessConfirm = async () =>{
let remindTimestamp = null
if (remindValue.value === 'custom') {
if (!customDate.value) {
return ElMessage.warning('请选择自定义日期时间')
function disabledPastDate(time: Date) {
return time.getTime() < Date.now() - 86400000
}
const handleSuccessConfirm = async () => {
let remindTimestamp = null
if (remindValue.value === 'custom') {
if (!customDate.value) {
return ElMessage.warning('请选择自定义日期时间')
}
// 自定义日期转时间戳
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)
}
// 自定义日期转时间戳
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)
const res = await CrmService.followNext({ id: customerId, follow_next: remindTimestamp })
ElMessage.success(res?.message || '提交成功!')
successDialogVisible.value=true
// router.push('/crmpc/customer')
} catch (e: any) {
ElMessage.error(e?.message || '提交失败')
successDialogVisible.value = false
setTimeout(() => {
router.push('/crmpc/customer')
}, 1000)
}
}
// 在 <script setup> 里添加获取地理位置方法
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 {
function goBack() {
router.back()
}
const handleSubmit = async () => {
try {
form.value.address = followDesc.value
const res = await CrmService.followAdd(form.value)
ElMessage.success(res?.message || '提交成功!')
successDialogVisible.value = true
// router.push('/crmpc/customer')
} catch (e: any) {
ElMessage.error(e?.message || '提交失败')
}
}
// 在 <script setup> 里添加获取地理位置方法
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}`
}
} catch (e) {
followDesc.value = `纬度: ${latitude}, 经度: ${longitude}`
},
(error) => {
console.log('定位失败:', error)
followDesc.value = '无法获取当前位置'
}
},
(error) => {
followDesc.value = '无法获取当前位置'
}
)
} else {
followDesc.value = '浏览器不支持定位功能'
)
} else {
followDesc.value = '浏览器不支持定位功能'
}
}
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) || ''
}
}
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>
<style scoped>
.timeline-content-row{
display: flex;
align-items: center;
justify-content: space-between;
}
.avatar,.timeline-img{
width: 80px;
}
.border{
margin-left: 30px;
}
.info-row {
display: flex;
align-items: center;
justify-content: space-between; /* 两端对齐 */
font-size: 15px;
padding: 0;
}
.edit-header{
padding: 40px 20px;
}
.follow-add-page {
background: #f7f9fa;
min-height: 100vh;
padding: 24px;
}
.boxshadow {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px 0 rgba(0,0,0,0.04);
}
.pad20 {
padding: 20px;
}
.edit-title {
font-size: 18px;
font-weight: bold;
}
.info-card {
margin-bottom: 0;
border-radius: 8px;
}
.timeline-content-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.avatar,
.timeline-img {
width: 80px;
}
.border {
margin-left: 30px;
}
.info-row {
display: flex;
align-items: center;
justify-content: space-between; /* 两端对齐 */
font-size: 15px;
padding: 0;
}
.edit-header {
padding: 40px 20px;
}
.follow-add-page {
background: #f7f9fa;
min-height: 100vh;
padding: 24px;
}
.boxshadow {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.04);
}
.pad20 {
padding: 20px;
}
.edit-title {
font-size: 18px;
font-weight: bold;
}
.info-card {
margin-bottom: 0;
border-radius: 8px;
}
.submit-btn {
display: block;
width: 240px;
margin: 32px auto 0 auto;
font-size: 18px;
height: 48px;
border-radius: 6px;
}
.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: -9px;
right: -9px;
width: 90px;
height: 75px;
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);
}
.info-top span{
margin-bottom: 15px;
display: inline-block;
}
/* 成功弹出层 */
.success-dialog-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 0 0 0;
}
.success-img {
width:55%;
margin-bottom: 12px;
}
.conts{
margin-top: -55px;
}
.success-title {
font-size: 20px;
font-weight: 500;
margin-bottom: 40px;
color: #333;
text-align: center;
}
.remind-title {
font-size: 16px;
color: #333;
margin-bottom: 20px;
}
.remind-options {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.remind-btn {
background: #f4f7fe;
color: #3471FF;
border-radius: 8px;
padding: 6px 18px;
cursor: pointer;
font-size: 15px;
border: 1px solid transparent;
transition: all 0.2s;
min-width: 64px;
text-align: center;
flex: 1;
}
.remind-btn.active {
background: #3471FF;
color: #fff;
border: 1px solid #3471FF;
}
.confirm-btn {
width: 100%;
height: 44px;
font-size: 18px;
border-radius: 6px;
margin-top: 16px;
}
.submit-btn {
display: block;
width: 240px;
margin: 32px auto 0 auto;
font-size: 18px;
height: 48px;
border-radius: 6px;
}
.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: -9px;
right: -9px;
width: 90px;
height: 75px;
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);
}
.info-top span {
margin-bottom: 15px;
display: inline-block;
}
/* 成功弹出层 */
.success-dialog-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 0 0 0;
}
.success-img {
width: 55%;
margin-bottom: 12px;
}
.conts {
margin-top: -55px;
}
.success-title {
font-size: 20px;
font-weight: 500;
margin-bottom: 40px;
color: #333;
text-align: center;
}
.remind-title {
font-size: 16px;
color: #333;
margin-bottom: 20px;
}
.remind-options {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.remind-btn {
background: #f4f7fe;
color: #3471ff;
border-radius: 8px;
padding: 6px 18px;
cursor: pointer;
font-size: 15px;
border: 1px solid transparent;
transition: all 0.2s;
min-width: 64px;
text-align: center;
flex: 1;
}
.remind-btn.active {
background: #3471ff;
color: #fff;
border: 1px solid #3471ff;
}
.confirm-btn {
width: 100%;
height: 44px;
font-size: 18px;
border-radius: 6px;
margin-top: 16px;
}
</style>
+409 -384
View File
@@ -1,62 +1,82 @@
<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>
<el-link underline="never" @click="goBack">
<i class="iconfont-sys icon-reverse">&#xe772;</i> 返回
</el-link>
</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>
<!-- 右上角三角形角标 -->
<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">
<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>
<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;">
<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>
<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>
<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;">
<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
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;">
<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>
<el-button type="primary" size="small" style="float: right" @click="goAddFollow"
>+ 添加跟进</el-button
>
</div>
<div class="follow-list">
<ElScrollbar >
<ElScrollbar>
<template v-if="followList.length > 0">
<ElTimeline>
<ElTimelineItem
@@ -69,10 +89,21 @@
<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>
<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="图片" />
<img
v-if="item.picture_full"
:src="item.picture_full"
class="timeline-img"
alt="图片"
/>
</div>
</div>
</ElTimelineItem>
@@ -80,7 +111,7 @@
</template>
<template v-else>
<!-- 使用 Element Plus 的空状态组件 -->
<el-empty :image="emptyimg" description="暂无跟进记录" />
<el-empty :image="emptyimg" description="暂无跟进记录" />
<!-- <el-empty /> -->
<!-- 或者自定义图片 -->
<!-- <img src="你的空图片地址.png" alt="暂无数据" class="empty-img" /> -->
@@ -97,12 +128,12 @@
: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 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;"
style="cursor: pointer"
v-for="(item1, index1) in group.tags"
:class="{ 'tagactive': activeMap[group.id] === item1.id }"
@click="tagSelect(item1)"
@@ -123,73 +154,61 @@
</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})
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 activecolor = '#3471ff'
const tagDialogVisible = ref(false)
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 () => {
const res = await CrmService.getcustomerdetail({ id: customerId })
customerInfo.value = res
console.log(res)
} catch (error) { }
}
//客户等级
const getGrade = async () => {
try {
// 这里调用获取品牌列表的接口
}
//客户等级
const getGrade = async () => {
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 {
levelOptions.value = res || []
}
//跟进记录getfollowlist
const followList = ref<CustomerInfo>({})
const getfollowlist = async () => {
const res = await CrmService.getfollowlist({ customer_id: route.query.id })
followList.value = res || []
}
onMounted(async () => {
await getGrade()
await getdetilfn()
// 在 getdetilfn 之后调用 tagGrouplistfn,确保 customerInfo 有数据
await tagGrouplistfn()
getfollowlist()
})
function addtag() {
tagDialogVisible.value = true
tagGrouplistfn()
}
const tagGrouplistfn = async () => {
const res = await CrmService.tagGrouplist({})
tagGroups.value = res || []
// 回显选中
if (customerInfo.value && Array.isArray(customerInfo.value.tags)) {
tagGroups.value.forEach(group => {
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)
@@ -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 })
ElMessage.success(res?.message || '操作成功')
await getdetilfn()
await tagGrouplistfn()
} catch (e) {
ElMessage.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) {
console.error(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
// }
// 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
}
// 6. 示例:确定按钮
function handleConfirm() {
// const selectedTags = getSelectedTags()
// console.log('选中的标签:', selectedTags)
// 处理选中的标签
tagDialogVisible.value = false
}
function handleCancel() {
tagDialogVisible.value = false
}
//编辑
function goEdit() {
router.push({
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 }
})
}
})
}
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{
.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;
}
}
.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;
/* 让内容不被滚动条遮挡 */
}
.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;
}
/* 优化滚动条样式(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;
}
/* Firefox 滚动条优化 */
.tag-dialog-scroll {
scrollbar-width: thin;
scrollbar-color: #d4e0f7 transparent;
}
.dialog-footer-center {
display: flex;
justify-content: center;
gap: 16px;
}
</style>