salary
qsh 5 months ago
parent cff4280705
commit a3657f86ab
  1. 16
      src/api/clue/followRecord.js
  2. 4
      src/api/clue/index.js
  3. 3
      src/components/Dialog/src/Dialog.vue
  4. 61
      src/views/Clue/Pool/Comp/DialogClue.vue
  5. 282
      src/views/Clue/Pool/Comp/DrawerClue.vue
  6. 199
      src/views/Clue/Pool/cluePool.data.js
  7. 11
      src/views/Clue/Pool/index.vue
  8. 6
      src/views/Clue/Set/index.vue
  9. 6
      src/views/MiniMall/MallSet/index.vue

@ -0,0 +1,16 @@
import request from '@/config/axios'
// 查询(精简)列表
export const getFollowList = async (params) => {
return await request.get({ url: '/admin-api/crm/clue-follow-record/list', params })
}
// 新增
export const createFollow = async (data) => {
return await request.post({ url: '/admin-api/crm/clue-follow-record/create', data: data })
}
// 删除
export const deleteFollow = async (id) => {
return await request.delete({ url: '/admin-api/crm/clue-follow-record/delete?id=' + id })
}

@ -34,3 +34,7 @@ export const deleteClue = async (id) => {
export const getClueCount = async () => {
return await request.get({ url: '/admin-api/crm/sch-clue/get-clue-num' })
}
export const getOpearateRecord = async (params) => {
return await request.get({ url: '/admin-api/crm/clue-operate-record/list', params })
}

@ -55,6 +55,8 @@ const dialogStyle = computed(() => {
height: unref(dialogHeight)
}
})
const emit = defineEmits(['close'])
</script>
<template>
@ -66,6 +68,7 @@ const dialogStyle = computed(() => {
draggable
lock-scroll
v-bind="getBindValue"
@close="emit('close')"
>
<template #header>
<div class="flex justify-between">

@ -1,5 +1,5 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="800px">
<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" />
@ -16,7 +16,7 @@
:disabled="!row.editable"
>
<el-option
v-for="item in userOptions"
v-for="item in props.userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
@ -92,9 +92,9 @@
<script setup name="DialogClue">
import { useAppStore } from '@/store/modules/app'
import { getSimpleUserList as getUserOption } from '@/api/system/user'
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 ImgPostion from '@/assets/imgs/flag/flag_red.png'
@ -112,6 +112,9 @@ const appStore = useAppStore()
const props = defineProps({
schema: {
type: Array
},
userOptions: {
type: Array
}
})
@ -156,7 +159,6 @@ const rules = {
const tabName = ref('info')
const followList = ref([])
const userOptions = ref([])
const areaValue = ref('')
const areaList = ref([])
@ -167,10 +169,14 @@ const defaultLatLng = 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
@ -185,6 +191,13 @@ const open = async (type, id) => {
} 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(() => {
@ -194,8 +207,17 @@ const open = async (type, id) => {
})
}
}
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([])
function getSchoolPlace() {
getPlaceList().then((data) => {
@ -203,6 +225,8 @@ function getSchoolPlace() {
})
}
const emit = defineEmits(['success'])
async function handleSave() {
//
if (!formRef.value) return
@ -220,10 +244,12 @@ async function handleSave() {
params.lng = defaultLatLng.value.lng
params.followUsers = [...followList.value]
params.diyParams = {}
debugger
for (const key in info.value.diyParams) {
if (Object.hasOwnProperty.call(info.value.diyParams, key)) {
params.diyParams[key] = params.key
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') {
@ -270,13 +296,15 @@ function initMap(data) {
lng: data.lng,
lat: data.lat
}
addmark(data.lng, data.lat, AMap)
}
dialogMap.value = new AMap.Map('dialogMap', {
zoom: 12,
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: '全国'
})
@ -375,10 +403,19 @@ function currentSelect(val) {
}
}
onMounted(() => {
getUserOption().then((data) => {
userOptions.value = data
function destroyMap() {
dialogMap.value = null
aMap.value = null
}
function getDiyList() {
getDiyFieldList().then((data) => {
diyFieldArr.value = data
})
}
onMounted(() => {
getDiyList()
})
</script>

@ -39,55 +39,26 @@
<el-tab-pane label="跟进记录" name="followRecord">
<el-button class="mb-10px" type="primary" @click="addFollow">添加跟进记录</el-button>
<el-timeline>
<el-timeline-item timestamp="2024-04-01" placement="top">
<el-card shadow="always" :body-style="{ padding: '10px' }">
<div>
<div class="flex justify-between" style="align-items: center">
<div class="flex align-baseline">
<b class="text-18px">张三</b>
<span class="text-14 ml-10px">2024-04-01 09:00:00</span>
</div>
<div>
<el-button type="primary" plain @click="updateFollow()">修改</el-button>
<el-button type="danger" plain>删除</el-button>
</div>
</div>
<div>{{ followContent }}</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>本次跟进时间2024-04-05 10:57</span>
</div>
<div class="flex ml-50px" style="color: #666; align-items: center">
<Icon icon="ep:clock" class="mr-5px" />
<span>下次跟进时间2024-04-10 10:00</span>
</div>
</div>
</div>
</el-card>
</el-timeline-item>
<el-timeline-item timestamp="2024-02-01" placement="top">
<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 class="flex justify-between" style="align-items: center">
<div class="flex align-baseline">
<b class="text-18px">李四</b>
<span class="text-14 ml-10px">2024-02-01 09:00:00</span>
</div>
<div>
<el-button type="primary" plain>修改</el-button>
<el-button type="danger" plain>删除</el-button>
</div>
<b class="text-18px" style="line-height: 36px">{{ item.operateUserName }}</b>
</div>
<div>{{ followContent2 }}</div>
<div>{{ item.centent }}</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>本次跟进时间2024-02-05 10:57</span>
<span>本次跟进时间{{ item.followTime }}</span>
</div>
<div class="flex ml-50px" style="color: #666; align-items: center">
<Icon icon="ep:clock" class="mr-5px" />
<span>下次跟进时间2024-02-10 10:00</span>
<span>下次跟进时间{{ item.nextFollowTime }}</span>
</div>
</div>
</div>
@ -96,74 +67,33 @@
</el-timeline>
</el-tab-pane>
<el-tab-pane label="详细信息" name="infoDetail">
<Descriptions :data="info" :schema="schema" :columns="2" />
<el-checkbox v-model="showSchool" :label="true" @change="handleShowSchool"
>展示场地</el-checkbox
>
<Descriptions :data="info" :schema="showSchema" :columns="2" />
<div v-if="appStore.getAppInfo?.instanceType == 1">
<el-checkbox v-model="showSchool" :label="true" @change="handleShowSchool">
展示场地
</el-checkbox>
<div id="dialogMap" class="mt-20px" style="height: 400px; width: 100%"></div>
</div>
</el-tab-pane>
<el-tab-pane label="操作记录" name="operateRecord">
<el-timeline>
<el-timeline-item timestamp="2024-04-01" placement="top">
<el-card shadow="always" :body-style="{ padding: '10px' }">
<div style="color: #666">
<div class="pt-5px">
<span>操作人机器人1号</span>
</div>
<div class="pt-5px pb-5px">
<span>成交线索</span>
</div>
<div class="flex" style="align-items: center">
<Icon icon="ep:clock" class="mr-5px" />
<span>操作时间2024-04-05 10:57</span>
</div>
</div>
</el-card>
</el-timeline-item>
<el-timeline-item timestamp="2024-02-01" placement="top">
<el-card shadow="always" :body-style="{ padding: '10px' }">
<div style="color: #666">
<div class="pt-5px">
<span>操作人机器人2号</span>
</div>
<div class="pt-5px pb-5px">
<span>修改意向状态为高意向</span>
</div>
<div class="flex" style="align-items: center">
<Icon icon="ep:clock" class="mr-5px" />
<span>操作时间2024-04-1 10:57</span>
</div>
</div>
</el-card>
</el-timeline-item>
<el-timeline-item timestamp="2024-02-01" placement="top">
<el-card shadow="always" :body-style="{ padding: '10px' }">
<div style="color: #666">
<div class="pt-5px">
<span>操作人机器人1号</span>
</div>
<div class="pt-5px pb-5px">
<span>跟进线索</span>
</div>
<div class="flex" style="align-items: center">
<Icon icon="ep:clock" class="mr-5px" />
<span>操作时间2024-04-1 10:57</span>
</div>
</div>
</el-card>
</el-timeline-item>
<el-timeline-item timestamp="2024-02-01" placement="top">
<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>操作人机器人2号</span>
<span>操作人{{ item.operateUserName }}</span>
</div>
<div class="pt-5px pb-5px">
<span>创建线索</span>
<span>{{ item.centent }}</span>
</div>
<div class="flex" style="align-items: center">
<Icon icon="ep:clock" class="mr-5px" />
<span>操作时间2024-04-1 10:57</span>
<span>操作时间{{ item.followTime }}</span>
</div>
</div>
</el-card>
@ -177,47 +107,50 @@
</template>
<script setup>
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 ImgFlag from '@/assets/imgs/flag/position_blue.png'
import AMapLoader from '@amap/amap-jsapi-loader'
import { formatDate } from '@/utils/formatTime'
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 appStore = useAppStore()
const show = ref(false)
const info = ref(null)
const loading = ref(false)
const schema = ref([
{
field: 'name',
label: '线索名称'
},
{
field: 'contact',
label: '联系方式'
},
{
field: 'supplier',
label: '意向状态'
},
{
field: 'supplier',
label: '创建时间'
},
const props = defineProps({
schema: {
type: Array
}
})
const showSchema = computed(() => {
const arr = [
{
field: 'purchaseCount',
field: 'requirement',
label: '诉求',
span: 2
},
{
field: 'remark',
label: '备注',
isEditor: true,
span: 2
}
])
const followContent = ``
const followContent2 = ``
]
return [...props.schema, ...arr]
})
const followList = ref([
{
@ -234,17 +167,41 @@ const followList = ref([
}
])
const followRecordList = ref([])
const operateRecordList = ref([])
//
const dialogMap = ref(null)
const aMap = ref(null)
async function open(id) {
try {
info.value = await ClueApi.getClue(id)
FollowApi.getFollowList({ clueId: id }).then((data) => {
followRecordList.value = data.map((item) => ({
operateUserName: item.operateUserName,
centent: item.centent,
operateDate: formatDate(item.operateTime),
followTime: formatDate(item.operateTime, 'YYYY-MM-DD HH:mm'),
nextFollowTime: formatDate(item.nextFollowTime)
}))
})
ClueApi.getOpearateRecord({ clueId: id }).then((data) => {
operateRecordList.value = data.map((item) => ({
operateUserName: item.operateUserName,
centent: item.centent,
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
if (!dialogMap.value) {
infoIndex.value = 'followRecord'
if (appStore.getAppInfo?.instanceType == 1 && !dialogMap.value) {
nextTick(() => {
initMap()
getSchoolPlace()
initMap(info.value)
})
}
} catch (error) {
@ -252,53 +209,89 @@ async function open(id) {
}
}
function initMap() {
const placeList = ref([])
function getSchoolPlace() {
getPlaceList().then((data) => {
placeList.value = data.placeList
})
}
const defaultLatLng = ref({
lat: 31.86119,
lng: 117.283042
})
function initMap(data) {
AMapLoader.load({
key: '2ffb0e2ea90b1df0b8be48ed66e18fc8', //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: 12,
zooms: [2, 22],
center: [117.283042, 31.86119]
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) {
let marker1 = new aMap.value.Marker({
map: dialogMap.value,
position: [117.258001, 31.895216],
label: {
content: '慧安驾校桃花社区训练基地',
direction: 'left'
},
icon: ImgFlag,
// extData: element,
clickable: true
})
let marker2 = new aMap.value.Marker({
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: [117.286731, 31.902396],
position: [place.lng, place.lat],
label: {
content: '(皖西)瑞星驾校总校(D)',
content: place.name,
direction: 'left'
},
icon: ImgFlag,
// extData: element,
icon: flagMap[place.flagColor || 'red'],
extData: place,
clickable: true
})
schoolMarkers.value = [marker1, marker2]
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
})
dialogMap.value.add(marker.value)
dialogMap.value.setCenter([lat, lng], '', 500)
}
function removeMarker() {
dialogMap.value.remove(marker.value)
}
const infoIndex = ref('followRecord')
defineExpose({
@ -309,9 +302,6 @@ const followRef = ref()
function addFollow() {
followRef.value.open('create', null)
}
function updateFollow() {
followRef.value.open('update', { nextFollowTime: '2024-04-01 12:12' })
}
function destroyMap() {
dialogMap.value = null

@ -1,199 +0,0 @@
// import { CrudSchema } from '@/hooks/web/useCrudSchemas'
import { dateFormatter } from '@/utils/formatTime'
// import * as MailAccountApi from '@/api/system/mail/account'
// 表单校验
export const rules = reactive({
name: [required],
phone: [required],
source: [required],
intentionState: [required]
})
// const userList = await MailAccountApi.getSimpleMailAccountList()
const userList = []
// CrudSchema:https://doc.iocoder.cn/vue3/crud-schema/
const crudSchemas = reactive([
{
label: '线索名称',
field: 'name',
isSearch: true,
isTable: true
},
{
label: '联系方式',
field: 'contact',
isSearch: true,
isTable: true
},
{
label: '线索位置',
field: 'address',
isSearch: true,
isTable: true,
isForm: false
},
{
label: '线索来源',
field: 'resource',
isSearch: true,
isTable: true,
search: {
component: 'Select',
api: () => userList,
componentProps: {
optionsAlias: {
labelField: 'name',
valueField: 'id'
}
}
},
form: {
component: 'Select',
api: () => userList,
componentProps: {
optionsAlias: {
labelField: 'name',
valueField: 'id'
}
}
}
},
{
label: '意向状态',
field: 'intention',
isSearch: true,
isTable: true,
table: {
fixed: 'left'
},
search: {
component: 'Select',
api: () => userList,
componentProps: {
optionsAlias: {
labelField: 'name',
valueField: 'id'
}
}
},
form: {
component: 'Select',
api: () => userList,
componentProps: {
optionsAlias: {
labelField: 'name',
valueField: 'id'
}
}
}
},
{
label: '跟进人员',
field: 'userId',
isSearch: true,
isTable: true,
isForm: false,
search: {
component: 'Select',
api: () => userList,
componentProps: {
optionsAlias: {
labelField: 'name',
valueField: 'id'
}
}
}
},
{
label: '下次跟进时间',
field: 'nextTime',
isSearch: true,
isTable: true,
isForm: false,
formatter: dateFormatter,
detail: {
dateFormat: 'YYYY-MM-DD'
},
search: {
component: 'DatePicker',
componentProps: {
type: 'daterange',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
startPlaceholder: '下次跟进时间',
endPlaceholder: '下次跟进时间'
}
}
},
{
label: '诉求',
field: 'need',
isTable: true,
form: {
component: 'Input',
componentProps: {
type: 'textarea'
},
colProps: {
span: 24
}
}
},
{
label: '最新跟进时间',
field: 'latestFollowTime',
isTable: true,
isForm: false
},
{
label: '创建时间',
field: 'createTime',
isSearch: true,
isTable: true,
table: {
fixed: 'left'
},
formatter: dateFormatter,
detail: {
dateFormat: 'YYYY-MM-DD'
},
search: {
component: 'DatePicker',
componentProps: {
type: 'daterange',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
startPlaceholder: '创建时间',
endPlaceholder: '创建时间'
}
},
form: {
component: 'DatePicker',
componentProps: {
type: 'date',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
placeholder: '创建时间'
}
}
},
{
label: '跟进记录',
field: 'followRecord',
isTable: true
},
{
label: '备注',
field: 'remark',
isTable: true,
form: {
component: 'Editor',
colProps: {
span: 24
}
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

@ -1,7 +1,7 @@
<template>
<div>
<div class="relative">
<el-tabs v-model="queryType" size="small">
<el-tabs v-model="queryType" size="small" @tab-change="getTableList">
<el-tab-pane label="全部" name="0" />
<el-tab-pane name="1">
<template #label>
@ -118,10 +118,11 @@
<DialogClue
v-if="!loading"
ref="formRef"
:userOptions="userOptions"
:schema="allSchemas.formSchema"
@sucess="getTableList"
/>
<DrawerClue ref="drawerRef" />
<DrawerClue v-if="!loading" ref="drawerRef" :schema="allSchemas.formSchema" />
<DialogSuccess ref="successRef" />
<DialogFollow ref="followRef" />
</div>
@ -133,6 +134,7 @@ import DialogClue from './Comp/DialogClue.vue'
import DrawerClue from './Comp/DrawerClue.vue'
import DialogSuccess from './Comp/DialogSuccess.vue'
import DialogFollow from './Comp/DialogFollow.vue'
import { getSimpleUserList as getUserOption } from '@/api/system/user'
import { removeNullField } from '@/utils'
import { formatDate } from '@/utils/formatTime'
@ -240,8 +242,11 @@ async function makeCall(phone) {
function handleSuccess(row) {
successRef.value.open(row)
}
const userOptions = ref([])
onMounted(() => {
getUserOption().then((data) => {
userOptions.value = data
})
getSearchCount()
getCurdSchemas()
})

@ -27,9 +27,9 @@
<el-tab-pane label="常规设置" :name="40" v-if="checkPermi(['clue:setting:general-setting'])">
<GeneralSet v-if="tabIndex == 40" />
</el-tab-pane>
<el-tab-pane label="消息通知" :name="50" v-if="checkPermi(['mall:setting:prod'])">
<!-- <el-tab-pane label="消息通知" :name="50" v-if="checkPermi(['mall:setting:prod'])">
<MsgSend v-if="tabIndex == 50" />
</el-tab-pane>
</el-tab-pane> -->
</el-tabs>
</div>
</template>
@ -40,7 +40,7 @@ import FieldOrder from './Comp/FieldOrder.vue'
import ClueSource from './Comp/ClueSource.vue'
// import ClueGet from './Comp/ClueGet.vue'
// import ClueSend from './Comp/ClueSend.vue'
import MsgSend from './Comp/MsgSend.vue'
// import MsgSend from './Comp/MsgSend.vue'
import GeneralSet from './Comp/GeneralSet.vue'
import { checkPermi } from '@/utils/permission'

@ -15,16 +15,16 @@
<el-tab-pane label="常规设置" :name="4" v-if="checkPermi(['mall:setting:general'])">
<GeneralSet v-if="tabIndex == 4" />
</el-tab-pane>
<el-tab-pane label="消息通知" :name="9" v-if="checkPermi(['mall:setting:msg'])">
<!-- <el-tab-pane label="消息通知" :name="9" v-if="checkPermi(['mall:setting:msg'])">
<MsgSend v-if="tabIndex == 9" />
</el-tab-pane>
</el-tab-pane> -->
</el-tabs>
</template>
<script setup>
import GeneralSet from './Comp/GeneralSet.vue'
import FieldProduct from './Comp/FieldProduct.vue'
import MsgSend from './Comp/MsgSend.vue'
// import MsgSend from './Comp/MsgSend.vue'
import CategorySet from './Comp/CategorySet.vue'
import BrandSet from './Comp/BrandSet.vue'
import SupplierSet from './Comp/SupplierSet.vue'

Loading…
Cancel
Save