Files
ss-crm-manage-web/src/views/Clue/Pool/Comp/DialogClue.vue
2024-08-13 16:09:44 +08:00

624 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="800px" @close="destroyMap">
<template #btn>
<el-button class="mr-20px" type="primary" size="small" @click="handleSave">保存</el-button>
</template>
<el-tabs v-model="tabName" @tab-change="changeTab">
<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.userId != useUserStore().getUser.id"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</template>
</el-table-column>
<el-table-column prop="content" label="跟进内容">
<template #default="{ row }">
<el-input
v-model="row.content"
type="textarea"
:autoSize="{ minRows: 2 }"
placeholder="输入跟进内容"
:disabled="row.userId != useUserStore().getUser.id"
/>
</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-autocomplete
v-model="areaValue"
clearable
style="width: 250px"
placeholder="输入并搜索位置"
:fetch-suggestions="remoteMethod"
@select="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-autocomplete>
<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>
<template #footer>
<el-button @click="dialogVisible = false"> </el-button>
<el-button :disabled="formLoading" type="primary" @click="handleSave"> </el-button>
</template>
<DialogSchoolInfo ref="schoolInfoDialog" />
</Dialog>
</template>
<script setup name="DialogClue">
import { useAppStore } from '@/store/modules/app'
import { useUserStore } from '@/store/modules/user'
import { getPlaceList } from '@/api/school/place'
import { getConfigByConfigKey } from '@/api/system/set'
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_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 appStore = useAppStore()
const props = defineProps({
schema: {
type: Array
},
userOptions: {
type: Array
},
allUserOptions: {
type: Array
}
})
const formSchema = computed(() => {
const newSchema = [...props.schema]
newSchema.forEach((it) => {
if (it.field == 'consultTime') {
it.componentProps['disabled-date'] = dateAfterToday
// it.componentProps['disabled'] = formType.value != 'create'
it.componentProps['disabled'] = true
}
// else if (it.field == 'convertPeople' && formType.value == 'update') {
// it.componentProps['disabled'] = true
// }
if (it.field == 'convertPeople') {
it.options = props.allUserOptions.map((it) => ({ ...it, name: it.nickname }))
}
})
return [
...newSchema,
{
component: 'Input',
label: '诉求',
field: 'requirement',
componentProps: {
type: 'textarea'
},
colProps: {
span: 24
}
},
{
component: 'Editor',
label: '备注',
field: 'remark',
colProps: {
span: 24
}
}
]
})
const rules = computed(() => {
let ruleObj = {}
props.schema.map((it) => {
if (it.isRequired) {
Reflect.set(ruleObj, it.field, {
required: true,
message: `${it.label}不可为空`,
trigger: 'blur, change'
})
}
})
return ruleObj
})
const dateAfterToday = (t) => {
return t.getTime() > Date.now()
}
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formRef = ref() // 表单 Ref
const tabName = ref('info')
const followList = ref([])
const areaValue = ref('')
const address = ref('')
const defaultLatLng = ref({
lat: 31.86119,
lng: 117.283042
})
const defaultCity = ref('合肥')
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 }
info.value.remark = info.value.remark || ''
defaultLatLng.value = {
lat: data.lat,
lng: data.lng
}
nextTick(() => {
followList.value = data.followUser
address.value = data.address || ''
formRef.value.setValues(info.value, false)
})
} finally {
formLoading.value = false
}
} else {
followList.value = [
{
userId: useUserStore().getUser.id,
content: undefined,
nextFollowTime: formatDate(new Date()),
editable: true
}
]
address.value = ''
if (appStore.getAppInfo?.instanceType == 1) {
const data = await getConfigByConfigKey({ configKey: 'defaultLocation' })
const cityInfo = JSON.parse(data.configValue)
defaultLatLng.value = {
lat: cityInfo.lat,
lng: cityInfo.lng
}
defaultCity.value = cityInfo.locationName
}
nextTick(() => {
formRef.value.setValues(info.value, true)
})
}
}
function changeTab() {
if (tabName.value == 'map') {
if (!dialogMap.value) {
nextTick(async () => {
await getSchoolPlace()
initMap(info.value)
// remoteMethod(address.value)
})
}
} else {
destroyMap()
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
function resetForm() {
info.value = {
address: undefined,
lat: undefined,
lng: undefined,
consultTime: formatDate(new Date()),
followUsers: [],
diyParams: {}
}
}
const placeList = ref([])
async function getSchoolPlace() {
const data = await getPlaceList({ placeStatus: 0 })
placeList.value = data.placeList
}
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') {
const data = await ClueApi.createClue(params)
message.success(data)
} else {
await ClueApi.updateClue(params)
message.success('修改成功')
}
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]
})
addmark(defaultLatLng.value.lng, defaultLatLng.value.lat, AMap)
AutoComplete.value = new AMap.AutoComplete({
city: defaultCity.value
})
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,
offset: [-16, -32]
})
dialogMap.value.add(marker.value)
dialogMap.value.setCenter([lat, lng], true)
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, cb) {
if (searchValue) {
AutoComplete.value?.search(searchValue, (status, result) => {
if (result.tips?.length) {
// areaList.value = result?.tips
const list = result.tips.map((it) => ({
...it,
value: it.name
}))
cb(list)
} else {
cb([])
}
})
} else {
cb([])
}
}
function currentSelect(val) {
if (val) {
defaultLatLng.value = {
lng: val.location?.lng,
lat: val.location?.lat
}
addmark(val.location?.lng, val.location?.lat, aMap.value)
regeoCode(val.location?.lng, val.location?.lat)
}
}
function destroyMap() {
areaValue.value = undefined
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>