479 lines
14 KiB
Vue
479 lines
14 KiB
Vue
<template>
|
||
<el-drawer
|
||
v-model="show"
|
||
direction="rtl"
|
||
size="60%"
|
||
style="min-width: 700px"
|
||
:with-header="false"
|
||
:destroy-on-close="true"
|
||
:show-close="true"
|
||
:wrapperClosable="true"
|
||
@close="destroyMap"
|
||
>
|
||
<!-- header -->
|
||
<el-skeleton :loading="loading" animated>
|
||
<div class="flex justify-between" style="height: 32px">
|
||
<div class="flex" style="align-items: center">
|
||
<b class="mr-5px text-24px">{{ info.name }}</b>
|
||
<div class="mr-5px text-16px">{{ info.phone }}</div>
|
||
<el-tag type="success">{{ info.intentionStateName }}</el-tag>
|
||
</div>
|
||
<div>
|
||
<el-button type="primary" v-hasPermi="['clue:pool:update']" plain @click="handleUpdate">
|
||
修改
|
||
</el-button>
|
||
<el-button
|
||
type="danger"
|
||
v-if="info.state != '成交'"
|
||
v-hasPermi="['clue:pool:delete']"
|
||
plain
|
||
@click="handleRemove"
|
||
>
|
||
删除
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</el-skeleton>
|
||
<!-- 基础信息 -->
|
||
<el-skeleton :loading="loading" animated>
|
||
<el-table :data="followList" size="small" border class="mt-10px">
|
||
<el-table-column prop="userName" label="跟进人" />
|
||
<el-table-column prop="followTime" label="最新跟进时间" :formatter="dateFormatter" />
|
||
<el-table-column prop="nextFollowTime" label="下次跟进时间" />
|
||
<el-table-column prop="signSate" label="成交状态" />
|
||
</el-table>
|
||
</el-skeleton>
|
||
<el-divider direction="horizontal" />
|
||
<!-- 详细信息 -->
|
||
<el-tabs v-model="infoIndex" type="border-card">
|
||
<el-tab-pane label="跟进记录" name="followRecord">
|
||
<el-button
|
||
v-if="followList.length"
|
||
v-hasPermi="['clue:pool:update']"
|
||
class="mb-10px"
|
||
type="primary"
|
||
@click="addFollow"
|
||
>添加跟进记录</el-button
|
||
>
|
||
<el-timeline>
|
||
<el-timeline-item
|
||
v-for="item in followRecordList"
|
||
:key="item.recordId"
|
||
:timestamp="item.operateDate"
|
||
placement="top"
|
||
>
|
||
<el-card shadow="always" :body-style="{ padding: '10px' }">
|
||
<div>
|
||
<div>
|
||
<b class="text-18px" style="line-height: 36px">{{ item.operateUserName }}</b>
|
||
</div>
|
||
<div>{{ item.content }}</div>
|
||
<div class="flex mt-10px" style="align-items: center">
|
||
<div class="flex" style="color: #666; align-items: center">
|
||
<Icon icon="ep:clock" class="mr-5px" />
|
||
<span>本次跟进时间:{{ item.followTime }}</span>
|
||
</div>
|
||
<div class="flex ml-50px" style="color: #666; align-items: center">
|
||
<Icon icon="ep:clock" class="mr-5px" />
|
||
<span>下次跟进时间:{{ item.nextFollowTime }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-timeline-item>
|
||
</el-timeline>
|
||
</el-tab-pane>
|
||
<el-tab-pane label="详细信息" name="infoDetail">
|
||
<Descriptions :data="info" :schema="showSchema" :columns="2" />
|
||
<div v-if="appStore.getAppInfo?.instanceType == 1">
|
||
<el-checkbox v-model="showSchool" :label="true" :value="true" @change="handleShowSchool">
|
||
展示场地
|
||
</el-checkbox>
|
||
<div id="dialogMap" class="mt-20px" style="height: 400px; width: 100%"></div>
|
||
<el-collapse v-model="collaspeKey" class="box-card">
|
||
<el-collapse-item title="附近驾校" name="nearbySchool">
|
||
<template #header>附近驾校</template>
|
||
<div style="padding: 10px">
|
||
<div v-if="nearbySchoolSearching">正在搜索中...</div>
|
||
<template v-else>
|
||
<div v-for="p in nearbySchoolList" :key="p.index">
|
||
<div
|
||
class="hover-pointer"
|
||
style="font-size: 14px; color: blue"
|
||
@click="getClassType(p)"
|
||
>
|
||
<i v-if="p.recommend" class="el-icon-star-off"></i>
|
||
驾校: {{ p.deptName }}-{{ p.name }}
|
||
</div>
|
||
<div class="mt5">地址:{{ p.address }}</div>
|
||
<div class="mt5">
|
||
直线距离: {{ p.distance }} 公里;
|
||
<span class="ml0">步行距离:{{ p.walkdistance }}</span>
|
||
</div>
|
||
<el-divider style="margin: 6px 0 !important" />
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</el-collapse-item>
|
||
</el-collapse>
|
||
</div>
|
||
</el-tab-pane>
|
||
<el-tab-pane label="操作记录" name="operateRecord">
|
||
<el-timeline>
|
||
<el-timeline-item
|
||
v-for="item in operateRecordList"
|
||
:key="item.recordId"
|
||
:timestamp="item.operateDate"
|
||
placement="top"
|
||
>
|
||
<el-card shadow="always" :body-style="{ padding: '10px' }">
|
||
<div style="color: #666">
|
||
<div class="pt-5px">
|
||
<span>操作人:{{ item.operateUserName }}</span>
|
||
</div>
|
||
<div class="pt-5px pb-5px">
|
||
<!-- <span>{{ item.content }}</span> -->
|
||
<span v-dompurify-html="item.content"></span>
|
||
</div>
|
||
<div class="flex" style="align-items: center">
|
||
<Icon icon="ep:clock" class="mr-5px" />
|
||
<span>操作时间:{{ item.followTime }}</span>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-timeline-item>
|
||
</el-timeline>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
<!-- 新建编辑跟进信息 -->
|
||
<DialogFollow ref="followRef" @success="followSuccess" />
|
||
<DialogSchoolInfo ref="schoolInfoDialog" />
|
||
</el-drawer>
|
||
</template>
|
||
|
||
<script setup name="DrawerClue">
|
||
import { useAppStore } from '@/store/modules/app'
|
||
import * as ClueApi from '@/api/clue'
|
||
import * as FollowApi from '@/api/clue/followRecord'
|
||
import { getPlaceList } from '@/api/school/place'
|
||
|
||
import DialogFollow from './DialogFollow.vue'
|
||
import DialogSchoolInfo from './DialogSchoolInfo.vue'
|
||
import AMapLoader from '@amap/amap-jsapi-loader'
|
||
import { formatDate, dateFormatter } from '@/utils/formatTime'
|
||
|
||
import ImgPostion from '@/assets/imgs/flag/position_black.png'
|
||
import FlagRed from '@/assets/imgs/flag/flag_red.png'
|
||
import FlagYellow from '@/assets/imgs/flag/flag_yellow.png'
|
||
import FlagPurple from '@/assets/imgs/flag/flag_purple.png'
|
||
import FlagGreen from '@/assets/imgs/flag/flag_green.png'
|
||
import FlagBlue from '@/assets/imgs/flag/flag_blue.png'
|
||
import FlagBlack from '@/assets/imgs/flag/flag_black.png'
|
||
|
||
const message = useMessage() // 消息弹窗
|
||
const { t } = useI18n() // 国际化
|
||
const appStore = useAppStore()
|
||
|
||
const show = ref(false)
|
||
const info = ref(null)
|
||
const loading = ref(false)
|
||
|
||
const props = defineProps({
|
||
schema: {
|
||
type: Array
|
||
}
|
||
})
|
||
|
||
const showSchema = computed(() => {
|
||
const arr1 = [...props.schema]
|
||
arr1.forEach((it) => {
|
||
if (it.label.includes('日期')) {
|
||
it.dateFormat = 'YYYY-MM-DD'
|
||
}
|
||
})
|
||
if (arr1.length % 2 != 0) {
|
||
arr1.push({})
|
||
}
|
||
const arr2 = [
|
||
{
|
||
field: 'requirement',
|
||
label: '诉求',
|
||
span: 2
|
||
},
|
||
{
|
||
field: 'remark',
|
||
label: '备注',
|
||
span: 2,
|
||
isEditor: true
|
||
}
|
||
]
|
||
return [...arr1, ...arr2]
|
||
})
|
||
|
||
const followList = ref([])
|
||
|
||
const followRecordList = ref([])
|
||
const operateRecordList = ref([])
|
||
|
||
// 地图相关
|
||
const dialogMap = ref(null)
|
||
const aMap = ref(null)
|
||
|
||
const clueId = ref('')
|
||
|
||
function getFollowList() {
|
||
FollowApi.getFollowList({ clueId: clueId.value }).then((data) => {
|
||
followRecordList.value = data.map((item) => ({
|
||
operateUserName: item.operateUserName,
|
||
content: item.content,
|
||
operateDate: formatDate(item.operateTime),
|
||
followTime: formatDate(item.operateTime, 'YYYY-MM-DD HH:mm'),
|
||
nextFollowTime: formatDate(item.nextFollowTime)
|
||
}))
|
||
})
|
||
}
|
||
|
||
function getFollowUsers(id) {
|
||
ClueApi.getFollowUserList({ id }).then((data) => {
|
||
followList.value = data
|
||
})
|
||
}
|
||
|
||
async function open(id) {
|
||
clueId.value = id
|
||
try {
|
||
getFollowUsers(id)
|
||
getFollowList()
|
||
ClueApi.getOpearateRecord({ clueId: id }).then((data) => {
|
||
operateRecordList.value = data.map((item) => ({
|
||
operateUserName: item.operateUserName,
|
||
content: item.content,
|
||
operateDate: formatDate(item.operateTime),
|
||
followTime: formatDate(item.operateTime, 'YYYY-MM-DD HH:mm')
|
||
}))
|
||
})
|
||
const data = await ClueApi.getClue(id)
|
||
info.value = { ...data, ...data.diyParams }
|
||
show.value = true
|
||
infoIndex.value = 'followRecord'
|
||
|
||
if (appStore.getAppInfo?.instanceType == 1 && !dialogMap.value) {
|
||
nextTick(async () => {
|
||
await getSchoolPlace()
|
||
initMap(info.value)
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.log(error)
|
||
}
|
||
}
|
||
|
||
function followSuccess() {
|
||
ClueApi.getClue(clueId.value).then((data) => {
|
||
info.value = { ...data, ...data.diyParams }
|
||
})
|
||
getFollowList()
|
||
emit('getList')
|
||
}
|
||
|
||
const placeList = ref([])
|
||
async function getSchoolPlace() {
|
||
const data = await getPlaceList({ placeStatus: 0 })
|
||
placeList.value = data.placeList
|
||
}
|
||
|
||
const defaultLatLng = ref({
|
||
lat: 31.86119,
|
||
lng: 117.283042
|
||
})
|
||
|
||
function initMap(data) {
|
||
AMapLoader.load({
|
||
key: import.meta.env.VITE_AMAP_KEY, //设置您的key
|
||
version: '2.0'
|
||
}).then((AMap) => {
|
||
aMap.value = AMap
|
||
if (data.lng || data.lat) {
|
||
defaultLatLng.value = {
|
||
lng: data.lng,
|
||
lat: data.lat
|
||
}
|
||
}
|
||
dialogMap.value = new AMap.Map('dialogMap', {
|
||
zoom: 14,
|
||
zooms: [2, 22],
|
||
center: [defaultLatLng.value.lng, defaultLatLng.value.lat]
|
||
})
|
||
if (data.lng || data.lat) {
|
||
addmark(data.lng, data.lat, AMap)
|
||
}
|
||
})
|
||
}
|
||
|
||
const showSchool = ref(false)
|
||
const schoolMarkers = ref([])
|
||
function handleShowSchool() {
|
||
if (showSchool.value) {
|
||
const flagMap = {
|
||
red: FlagRed,
|
||
yellow: FlagYellow,
|
||
purple: FlagPurple,
|
||
green: FlagGreen,
|
||
blue: FlagBlue,
|
||
black: FlagBlack
|
||
}
|
||
schoolMarkers.value = []
|
||
for (let i = 0; i < placeList.value.length; i++) {
|
||
const place = placeList.value[i]
|
||
const marker = new aMap.value.Marker({
|
||
map: dialogMap.value,
|
||
position: [place.lng, place.lat],
|
||
label: {
|
||
content: place.name,
|
||
direction: 'left'
|
||
},
|
||
icon: flagMap[place.flagColor || 'red'],
|
||
extData: place,
|
||
clickable: true
|
||
})
|
||
marker.on('click', (ev) => showSchoolInfo(ev.target.getExtData()))
|
||
schoolMarkers.value.push(marker)
|
||
}
|
||
} else {
|
||
dialogMap.value.remove(schoolMarkers.value)
|
||
}
|
||
}
|
||
|
||
let marker = ref(null)
|
||
function addmark(lat, lng, AMap) {
|
||
marker.value && removeMarker()
|
||
marker.value = new AMap.Marker({
|
||
position: new AMap.LngLat(lat, lng),
|
||
zoom: 13,
|
||
icon: ImgPostion,
|
||
offset: [-16, -32]
|
||
})
|
||
dialogMap.value.add(marker.value)
|
||
dialogMap.value.setCenter([lat, lng], '', 500)
|
||
getNearbySchool({ lat: lng, lng: lat })
|
||
}
|
||
|
||
const collaspeKey = ref('nearbySchool')
|
||
const nearbySchoolSearching = ref(false)
|
||
const nearbySchoolList = ref([])
|
||
function getNearbySchool(info) {
|
||
if (info.lng && info.lat) {
|
||
nearbySchoolList.value = []
|
||
nearbySchoolSearching.value = true
|
||
// 推荐的场地
|
||
let places1 = []
|
||
// 普通的场地
|
||
let places2 = []
|
||
|
||
const p2 = [info.lng, info.lat]
|
||
for (let i = 0; i < placeList.value.length; i++) {
|
||
const element = placeList.value[i]
|
||
const p1 = [element.lng, element.lat]
|
||
// 计算直线距离
|
||
element.distance = (aMap.value.GeometryUtil.distance(p1, p2) / 1000).toFixed(2)
|
||
element.recommend ? places1.push(element) : places2.push(element)
|
||
}
|
||
// 按直线距离排序
|
||
// 排序
|
||
if (places1.length > 1) {
|
||
places1 = places1.sort((a, b) => a.distance - b.distance)
|
||
}
|
||
// 排序
|
||
if (places2.length > 1) {
|
||
places2 = places2.sort((a, b) => a.distance - b.distance)
|
||
}
|
||
// 取普通场地和推荐场地,组合, 取四个
|
||
nearbySchoolList.value = []
|
||
for (let i = 0; i < 4; i++) {
|
||
places1.length > i && nearbySchoolList.value.push(places1[i])
|
||
places2.length > i && nearbySchoolList.value.push(places2[i])
|
||
if (nearbySchoolList.value.length === 4) {
|
||
break
|
||
}
|
||
}
|
||
// 计算步行距离
|
||
nearbySchoolList.value.map(async (item) => {
|
||
const p1 = [item.lng, item.lat]
|
||
const resp = await getWalkingDistance(p1, p2)
|
||
item.walkdistance = resp
|
||
})
|
||
nearbySchoolSearching.value = false
|
||
}
|
||
}
|
||
|
||
// 获取两点之间的步行距离
|
||
async function getWalkingDistance(start, end) {
|
||
return new Promise((resolve) => {
|
||
aMap.value.plugin('AMap.Walking', () => {
|
||
const walking = new aMap.value.Walking()
|
||
let num = 0
|
||
walking.search(start, end, (status, result) => {
|
||
if (status === 'complete') {
|
||
result.routes.forEach((item) => {
|
||
num += item.distance
|
||
})
|
||
resolve(num > 1000 ? `${(num / 1000).toFixed(2)} 公里` : `${num} 米`)
|
||
} else {
|
||
resolve('步行数据无法确定')
|
||
}
|
||
})
|
||
})
|
||
})
|
||
}
|
||
|
||
const schoolInfoDialog = ref()
|
||
function showSchoolInfo(val) {
|
||
schoolInfoDialog.value.open(val)
|
||
}
|
||
|
||
function removeMarker() {
|
||
dialogMap.value.remove(marker.value)
|
||
}
|
||
|
||
const infoIndex = ref('followRecord')
|
||
|
||
defineExpose({
|
||
open
|
||
})
|
||
|
||
const followRef = ref()
|
||
function addFollow() {
|
||
followRef.value.open(info.value.clueId, info.value.intentionState)
|
||
}
|
||
|
||
function destroyMap() {
|
||
dialogMap.value = null
|
||
aMap.value = null
|
||
}
|
||
|
||
const emit = defineEmits(['update', 'getList'])
|
||
// 修改
|
||
function handleUpdate() {
|
||
emit('update', info.value)
|
||
show.value = false
|
||
}
|
||
|
||
// 删除
|
||
async function handleRemove() {
|
||
try {
|
||
// 删除的二次确认
|
||
await message.delConfirm()
|
||
// 发起删除
|
||
await ClueApi.deleteClue(info.value.clueId)
|
||
message.success(t('common.delSuccess'))
|
||
// 刷新列表
|
||
emit('getList')
|
||
show.value = false
|
||
} catch (err) {
|
||
console.log(err)
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped></style>
|