<template> <Dialog :title="dialogTitle" v-model="dialogVisible" width="800px" @close="destroyMap"> <el-tabs v-model="tabName"> <el-tab-pane label="线索信息" name="info"> <Form ref="formRef" v-loading="formLoading" :rules="rules" isCol :schema="formSchema" /> </el-tab-pane> <el-tab-pane label="跟进信息" name="follow"> <el-button type="primary" @click="handleAppendFollow">新增跟进人</el-button> <el-table :data="followList"> <el-table-column label="跟进人" width="180px"> <template #default="{ row }"> <el-select v-model="row.userId" placeholder="选择跟进人" filterable :disabled="!row.editable" > <el-option v-for="item in props.userOptions" :key="item.id" :label="item.nickname" :value="item.id" /> </el-select> </template> </el-table-column> <el-table-column prop="nextFollowTime" label="下次跟进时间" width="180px"> <template #default="{ row }"> <el-date-picker v-model="row.nextFollowTime" type="date" placeholder="选择日期时间" :disabled="!row.editable" style="width: 100%" /> </template> </el-table-column> <el-table-column prop="content" label="跟进内容"> <template #default="{ row }"> <el-input v-model="row.content" placeholder="输入跟进内容" :disabled="!row.editable" /> </template> </el-table-column> <el-table-column label="操作" width="80"> <template #default="{ row, $index }"> <Icon v-if="row.editable" icon="ep:remove-filled" class="text-red-500" @click="handleRemove($index)" /> </template> </el-table-column> </el-table> </el-tab-pane> <el-tab-pane v-if="appStore.getAppInfo?.instanceType == 1" label="位置信息" name="map"> <div class="flex justify-between items-center"> <el-select v-model="areaValue" filterable clearable remote style="width: 250px" reserve-keyword placeholder="输入并搜索位置" :remote-method="remoteMethod" @change="currentSelect" > <el-option v-for="item in areaList" :key="item.id" :label="item.name" :value="item.name" class="one-text" > <span style="float: left">{{ item.name }}</span> <span style="float: right; color: #8492a6; font-size: 13px">{{ item.district }}</span> </el-option> </el-select> <div class="flex-1 flex items-center ml-10px mr-10px"> <div class="w-100px">线索位置:</div> <el-input v-model="address" disabled placeholder="请输入线索位置" clearable /> </div> <el-checkbox v-model="showSchool" :label="true" @change="handleShowSchool"> 展示场地 </el-checkbox> </div> <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> </el-tab-pane> </el-tabs> <DialogSchoolInfo ref="schoolInfoDialog" /> <template #footer> <span> <el-button @click="dialogVisible = false">取 消</el-button> <el-button :disabled="formLoading" type="primary" @click="handleSave">保 存</el-button> </span> </template> </Dialog> </template> <script setup name="DialogClue"> import { useAppStore } from '@/store/modules/app' import { getPlaceList } from '@/api/school/place' import * as ClueApi from '@/api/clue' import { getDiyFieldList } from '@/api/clue/clueField' import { formatDate } from '@/utils/formatTime' import AMapLoader from '@amap/amap-jsapi-loader' import DialogSchoolInfo from './DialogSchoolInfo.vue' import ImgPostion from '@/assets/imgs/flag/position_blue.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 props = defineProps({ schema: { type: Array }, userOptions: { type: Array } }) const formSchema = computed(() => { return [ ...props.schema, { component: 'Input', label: '诉求', field: 'requirement', componentProps: { type: 'textarea' }, colProps: { span: 24 } }, { component: 'Editor', label: '备注', field: 'remark', colProps: { span: 24 } } ] }) const dialogVisible = ref(false) // 弹窗的是否展示 const dialogTitle = ref('') // 弹窗的标题 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 const formType = ref('') // 表单的类型:create - 新增;update - 修改 const formRef = ref() // 表单 Ref const rules = { name: { required: true, message: '线索名称不可为空', trigger: 'blur' }, phone: { required: true, message: '联系方式不可为空', trigger: 'blur' }, source: { required: true, message: '线索来源不可为空', trigger: 'change' }, intentionState: { required: true, message: '意向状态不可为空', trigger: 'change' }, consultTime: { required: true, message: '咨询日期不可为空', trigger: 'change' } } const tabName = ref('info') const followList = ref([]) const areaValue = ref('') const areaList = ref([]) const address = ref('') const defaultLatLng = ref({ lat: 31.86119, lng: 117.283042 }) const info = ref({}) const diyFieldArr = ref([]) const open = async (type, id) => { dialogVisible.value = true tabName.value = 'info' dialogTitle.value = type == 'create' ? '新增线索' : '修改线索' formType.value = type resetForm() // 修改时,设置数据 if (id) { formLoading.value = true try { const data = await ClueApi.getClue(id) info.value = { ...data, ...data.diyParams } nextTick(() => { followList.value = data.followUser address.value = data.address || '' formRef.value.setValues(info.value) }) } finally { formLoading.value = false } } else { followList.value = [] address.value = '' defaultLatLng.value = { lat: 31.86119, lng: 117.283042 } } if (appStore.getAppInfo?.instanceType == 1 && !dialogMap.value) { nextTick(async () => { await getSchoolPlace() initMap(info.value) remoteMethod(address.value) }) } } defineExpose({ open }) // 提供 open 方法,用于打开弹窗 function resetForm() { info.value.address = undefined info.value.lat = undefined info.value.lng = undefined info.value.followUsers = [] info.value.diyParams = {} } const placeList = ref([]) async function getSchoolPlace() { const data = await getPlaceList() placeList.value = data.placeList.filter((it) => it.status == 0 && it.schoolStatus == 0) } const emit = defineEmits(['success']) async function handleSave() { // 校验表单 if (!formRef.value) return const valid = await formRef.value.getElFormRef().validate() if (!valid) return if (!followList.value && followList.value.length == 0) { message.info('请添加跟进人') return } if (followList.value && followList.value.length && followList.value.some((it) => !it.userId)) { message.info('请将跟进人填写完整!') return } if (appStore.getAppInfo?.instanceType == 1 && !address.value) { message.info('请选择学员位置!') return } // 提交请求 formLoading.value = true try { let params = { ...formRef.value.formModel, address: address.value } params.lat = defaultLatLng.value.lat params.lng = defaultLatLng.value.lng params.followUsers = [...followList.value] params.diyParams = {} diyFieldArr.value.map((it) => { params.diyParams[it.field] = undefined }) for (const key in params.diyParams) { if (Object.hasOwnProperty.call(params, key)) { params.diyParams[key] = params[key] } } if (formType.value === 'create') { await ClueApi.createClue(params) message.success(t('common.createSuccess')) } else { await ClueApi.updateClue(params) message.success(t('common.updateSuccess')) } dialogVisible.value = false // 发送操作成功的事件 emit('success') } finally { formLoading.value = false } } function handleAppendFollow() { followList.value.push({ userId: undefined, content: undefined, nextFollowTime: formatDate(new Date()), editable: true }) } function handleRemove(index) { followList.value.splice(index, 1) } // 地图相关 const dialogMap = ref(null) const aMap = ref(null) let AutoComplete = ref(null) let geoCoder = ref(null) function initMap(data) { AMapLoader.load({ key: '713d839ff505943b0f18e6df45f3b0dc', //设置您的key version: '2.0', plugins: ['AMap.Geocoder', 'AMap.AutoComplete'] }).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) } AutoComplete.value = new AMap.AutoComplete({ city: '全国' }) geoCoder.value = new AMap.Geocoder() dialogMap.value.on('click', (e) => { defaultLatLng.value = { lng: e.lnglat.getLng(), lat: e.lnglat.getLat() } addmark(e.lnglat.getLng(), e.lnglat.getLat(), AMap) regeoCode(e.lnglat.getLng(), e.lnglat.getLat()) }) }) } 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('步行数据无法确定') } }) }) }) } function regeoCode(lng, lat) { try { geoCoder.value.getAddress([lng, lat], (status, result) => { if (status === 'complete' && result.regeocode) { address.value = result.regeocode.formattedAddress } else { message.error('根据经纬度查询地址失败') } }) } catch (error) { console.log(error) } } 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 }) dialogMap.value.add(marker.value) dialogMap.value.setCenter([lat, lng], '', 500) getNearbySchool({ lat: lng, lng: lat }) } function removeMarker() { dialogMap.value.remove(marker.value) } 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) } } const schoolInfoDialog = ref() function showSchoolInfo(val) { schoolInfoDialog.value.open(val) } function remoteMethod(searchValue) { if (searchValue !== '') { setTimeout(() => { AutoComplete.value?.search(searchValue, (status, result) => { if (result.tips?.length) { areaList.value = result?.tips } }) }, 200) } } function currentSelect(val) { const area = areaList.value.find((it) => it.name == val) if (area) { addmark(area.location?.lng, area.location?.lat, aMap.value) dialogMap.value.setCenter([area.location?.lng, area.location?.lat], '', 500) regeoCode(area.location?.lng, area.location?.lat) } } function destroyMap() { dialogMap.value = null aMap.value = null } function getDiyList() { getDiyFieldList().then((data) => { diyFieldArr.value = data }) } onMounted(() => { getDiyList() }) </script> <style scoped> :deep() .amap-logo { display: none !important; } :deep() .amap-copyright { display: none !important; } </style>