莳松crm管理系统
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ss-crm-manage-web/src/views/Clue/Pool/Comp/DialogClue.vue

590 lines
17 KiB

11 months ago
<template>
9 months ago
<Dialog :title="dialogTitle" v-model="dialogVisible" width="800px" @close="destroyMap">
8 months ago
<el-tabs v-model="tabName" @tab-change="changeTab">
11 months ago
<el-tab-pane label="线索信息" name="info">
9 months ago
<Form ref="formRef" v-loading="formLoading" :rules="rules" isCol :schema="formSchema" />
11 months ago
</el-tab-pane>
<el-tab-pane label="跟进信息" name="follow">
<el-button type="primary" @click="handleAppendFollow">新增跟进人</el-button>
<el-table :data="followList">
9 months ago
<el-table-column label="跟进人" width="180px">
11 months ago
<template #default="{ row }">
9 months ago
<el-select
v-model="row.userId"
placeholder="选择跟进人"
filterable
:disabled="!row.editable"
>
11 months ago
<el-option
9 months ago
v-for="item in props.userOptions"
9 months ago
:key="item.id"
:label="item.nickname"
:value="item.id"
11 months ago
/>
</el-select>
</template>
</el-table-column>
9 months ago
<el-table-column prop="nextFollowTime" label="下次跟进时间" width="180px">
11 months ago
<template #default="{ row }">
9 months ago
<el-date-picker
9 months ago
v-model="row.nextFollowTime"
9 months ago
type="date"
placeholder="选择日期时间"
:disabled="!row.editable"
8 months ago
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
9 months ago
style="width: 100%"
/>
</template>
</el-table-column>
<el-table-column prop="content" label="跟进内容">
<template #default="{ row }">
<el-input
v-model="row.content"
8 months ago
type="textarea"
:autoSize="{ minRows: 2 }"
9 months ago
placeholder="输入跟进内容"
:disabled="!row.editable"
9 months ago
/>
11 months ago
</template>
</el-table-column>
<el-table-column label="操作" width="80">
9 months ago
<template #default="{ row, $index }">
<Icon
v-if="row.editable"
icon="ep:remove-filled"
class="text-red-500"
@click="handleRemove($index)"
/>
11 months ago
</template>
</el-table-column>
</el-table>
</el-tab-pane>
9 months ago
<el-tab-pane v-if="appStore.getAppInfo?.instanceType == 1" label="位置信息" name="map">
9 months ago
<div class="flex justify-between items-center">
8 months ago
<el-autocomplete
11 months ago
v-model="areaValue"
clearable
9 months ago
style="width: 250px"
placeholder="输入并搜索位置"
8 months ago
:fetch-suggestions="remoteMethod"
@select="currentSelect"
11 months ago
>
8 months ago
<!-- <el-option
11 months ago
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>
8 months ago
</el-option> -->
</el-autocomplete>
9 months ago
<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>
9 months ago
<el-checkbox v-model="showSchool" :label="true" @change="handleShowSchool">
展示场地
</el-checkbox>
11 months ago
</div>
<div id="dialogMap" class="mt-20px" style="height: 400px; width: 100%"></div>
9 months ago
<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>
11 months ago
</el-tab-pane>
</el-tabs>
8 months ago
<div style="position: absolute; top: 75px; right: 20px">
<el-button @click="dialogVisible = false"> </el-button>
<el-button :disabled="formLoading" type="primary" @click="handleSave"> </el-button>
</div>
9 months ago
<DialogSchoolInfo ref="schoolInfoDialog" />
10 months ago
</Dialog>
11 months ago
</template>
9 months ago
<script setup name="DialogClue">
import { useAppStore } from '@/store/modules/app'
8 months ago
import { useUserStore } from '@/store/modules/user'
9 months ago
import { getPlaceList } from '@/api/school/place'
import * as ClueApi from '@/api/clue'
9 months ago
import { getDiyFieldList } from '@/api/clue/clueField'
11 months ago
import { formatDate } from '@/utils/formatTime'
import AMapLoader from '@amap/amap-jsapi-loader'
9 months ago
import DialogSchoolInfo from './DialogSchoolInfo.vue'
8 months ago
import ImgPostion from '@/assets/imgs/flag/position_black.png'
9 months ago
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
9 months ago
},
userOptions: {
type: Array
9 months ago
}
})
const formSchema = computed(() => {
8 months ago
const newSchema = [...props.schema]
newSchema.forEach((it) => {
if (it.field == 'consultTime') {
it.componentProps['disabled-date'] = dateAfterToday
}
})
9 months ago
return [
8 months ago
...newSchema,
9 months ago
{
component: 'Input',
label: '诉求',
field: 'requirement',
componentProps: {
type: 'textarea'
},
colProps: {
span: 24
}
},
{
component: 'Editor',
label: '备注',
field: 'remark',
colProps: {
span: 24
}
}
]
})
11 months ago
8 months ago
const dateAfterToday = (t) => {
return t.getTime() > Date.now()
}
11 months ago
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formRef = ref() // 表单 Ref
9 months ago
const rules = {
name: { required: true, message: '线索名称不可为空', trigger: 'blur' },
phone: { required: true, message: '联系方式不可为空', trigger: 'blur' },
source: { required: true, message: '线索来源不可为空', trigger: 'change' },
9 months ago
intentionState: { required: true, message: '意向状态不可为空', trigger: 'change' },
consultTime: { required: true, message: '咨询日期不可为空', trigger: 'change' }
9 months ago
}
11 months ago
const tabName = ref('info')
const followList = ref([])
9 months ago
const areaValue = ref('')
const address = ref('')
const defaultLatLng = ref({
lat: 31.86119,
lng: 117.283042
})
const info = ref({})
11 months ago
9 months ago
const diyFieldArr = ref([])
9 months ago
const open = async (type, id) => {
11 months ago
dialogVisible.value = true
9 months ago
tabName.value = 'info'
11 months ago
dialogTitle.value = type == 'create' ? '新增线索' : '修改线索'
formType.value = type
9 months ago
resetForm()
11 months ago
// 修改时,设置数据
9 months ago
if (id) {
11 months ago
formLoading.value = true
try {
9 months ago
const data = await ClueApi.getClue(id)
info.value = { ...data, ...data.diyParams }
11 months ago
nextTick(() => {
9 months ago
followList.value = data.followUser
address.value = data.address || ''
formRef.value.setValues(info.value)
11 months ago
})
} finally {
formLoading.value = false
}
9 months ago
} else {
8 months ago
followList.value = [
{
userId: useUserStore().getUser.id,
content: undefined,
nextFollowTime: formatDate(new Date()),
editable: true
}
]
9 months ago
address.value = ''
9 months ago
defaultLatLng.value = {
lat: 31.86119,
lng: 117.283042
}
8 months ago
nextTick(() => {
formRef.value.setValues(info.value)
11 months ago
})
}
}
9 months ago
8 months ago
function changeTab() {
if (tabName.value == 'map') {
if (!dialogMap.value) {
nextTick(async () => {
await getSchoolPlace()
initMap(info.value)
// remoteMethod(address.value)
})
}
} else {
destroyMap()
}
}
11 months ago
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
9 months ago
function resetForm() {
info.value.address = undefined
info.value.lat = undefined
info.value.lng = undefined
8 months ago
info.value.consultTime = formatDate(new Date())
9 months ago
info.value.followUsers = []
info.value.diyParams = {}
}
9 months ago
const placeList = ref([])
9 months ago
async function getSchoolPlace() {
8 months ago
const data = await getPlaceList({ placeStatus: 0 })
placeList.value = data.placeList
9 months ago
}
9 months ago
const emit = defineEmits(['success'])
9 months ago
async function handleSave() {
// 校验表单
if (!formRef.value) return
const valid = await formRef.value.getElFormRef().validate()
if (!valid) return
8 months ago
if (!followList.value || followList.value.length == 0) {
9 months ago
message.info('请添加跟进人')
return
}
9 months ago
if (followList.value && followList.value.length && followList.value.some((it) => !it.userId)) {
message.info('请将跟进人填写完整!')
return
}
9 months ago
if (appStore.getAppInfo?.instanceType == 1 && !address.value) {
9 months ago
message.info('请选择学员位置!')
return
}
9 months ago
// 提交请求
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 = {}
9 months ago
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]
9 months ago
}
}
if (formType.value === 'create') {
8 months ago
const data = await ClueApi.createClue(params)
message.success(data)
9 months ago
} else {
8 months ago
const data = await ClueApi.updateClue(params)
message.success(data)
9 months ago
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
11 months ago
}
function handleAppendFollow() {
followList.value.push({
9 months ago
userId: undefined,
9 months ago
content: undefined,
9 months ago
nextFollowTime: formatDate(new Date()),
9 months ago
editable: true
11 months ago
})
}
function handleRemove(index) {
followList.value.splice(index, 1)
}
// 地图相关
const dialogMap = ref(null)
const aMap = ref(null)
let AutoComplete = ref(null)
let geoCoder = ref(null)
9 months ago
function initMap(data) {
11 months ago
AMapLoader.load({
9 months ago
key: '713d839ff505943b0f18e6df45f3b0dc', //设置您的key
11 months ago
version: '2.0',
plugins: ['AMap.Geocoder', 'AMap.AutoComplete']
}).then((AMap) => {
aMap.value = AMap
9 months ago
if (data.lng || data.lat) {
defaultLatLng.value = {
lng: data.lng,
lat: data.lat
}
}
11 months ago
dialogMap.value = new AMap.Map('dialogMap', {
9 months ago
zoom: 14,
11 months ago
zooms: [2, 22],
9 months ago
center: [defaultLatLng.value.lng, defaultLatLng.value.lat]
11 months ago
})
8 months ago
addmark(defaultLatLng.value.lng, defaultLatLng.value.lat, AMap)
11 months ago
AutoComplete.value = new AMap.AutoComplete({
8 months ago
city: '合肥'
11 months ago
})
geoCoder.value = new AMap.Geocoder()
dialogMap.value.on('click', (e) => {
9 months ago
defaultLatLng.value = {
lng: e.lnglat.getLng(),
lat: e.lnglat.getLat()
}
11 months ago
addmark(e.lnglat.getLng(), e.lnglat.getLat(), AMap)
9 months ago
regeoCode(e.lnglat.getLng(), e.lnglat.getLat())
11 months ago
})
})
}
9 months ago
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('步行数据无法确定')
}
})
})
})
}
9 months ago
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)
}
}
11 months ago
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,
8 months ago
icon: ImgPostion,
offset: [-16, -32]
11 months ago
})
dialogMap.value.add(marker.value)
8 months ago
dialogMap.value.setCenter([lat, lng], true)
9 months ago
getNearbySchool({ lat: lng, lng: lat })
11 months ago
}
function removeMarker() {
dialogMap.value.remove(marker.value)
}
const showSchool = ref(false)
const schoolMarkers = ref([])
function handleShowSchool() {
if (showSchool.value) {
9 months ago
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
})
9 months ago
marker.on('click', (ev) => showSchoolInfo(ev.target.getExtData()))
9 months ago
schoolMarkers.value.push(marker)
}
11 months ago
} else {
dialogMap.value.remove(schoolMarkers.value)
}
}
9 months ago
const schoolInfoDialog = ref()
function showSchoolInfo(val) {
schoolInfoDialog.value.open(val)
}
8 months ago
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([])
11 months ago
}
}
function currentSelect(val) {
8 months ago
if (val) {
8 months ago
defaultLatLng.value = {
8 months ago
lng: val.location?.lng,
lat: val.location?.lat
8 months ago
}
8 months ago
addmark(val.location?.lng, val.location?.lat, aMap.value)
regeoCode(val.location?.lng, val.location?.lat)
11 months ago
}
}
9 months ago
9 months ago
function destroyMap() {
8 months ago
areaValue.value = undefined
9 months ago
dialogMap.value = null
aMap.value = null
}
function getDiyList() {
getDiyFieldList().then((data) => {
diyFieldArr.value = data
9 months ago
})
9 months ago
}
onMounted(() => {
getDiyList()
9 months ago
})
11 months ago
</script>
<style scoped>
:deep() .amap-logo {
display: none !important;
}
:deep() .amap-copyright {
display: none !important;
}
</style>