crm 客户管理
parent
584d6978ae
commit
7f8f02ebf7
5
.env
5
.env
|
|
@ -25,4 +25,7 @@ VITE_APP_DEFAULT_LOGIN_USERNAME = admin
|
|||
VITE_APP_DEFAULT_LOGIN_PASSWORD = admin123
|
||||
|
||||
# 租户开关
|
||||
VUE_APP_TENANT_ENABLE=false
|
||||
VUE_APP_TENANT_ENABLE=false
|
||||
|
||||
# 腾讯地图key
|
||||
VITE_APP_TENCENT_MAP_KEY= EE5BZ-SMO63-I6G3P-OL4UN-BE34O-QCBOS
|
||||
|
|
@ -4,7 +4,7 @@ NODE_ENV=development
|
|||
VITE_DEV=true
|
||||
|
||||
# 请求路径
|
||||
VITE_BASE_URL='http://localhost:48080'
|
||||
VITE_BASE_URL='http://www.woyaoshouchong.asia:48080'
|
||||
|
||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
|
||||
VITE_UPLOAD_TYPE=client
|
||||
|
|
|
|||
12
.env.test
12
.env.test
|
|
@ -1,10 +1,10 @@
|
|||
# 测试环境:只在打包时使用
|
||||
NODE_ENV=production
|
||||
|
||||
VITE_DEV=false
|
||||
VITE_DEV=true
|
||||
|
||||
# 请求路径
|
||||
VITE_BASE_URL='http://www.woyaoshouchong.asia:48080'
|
||||
VITE_BASE_URL='http://www.woyaoshouchong.asia:88'
|
||||
|
||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
||||
VITE_UPLOAD_TYPE=client
|
||||
|
|
@ -13,16 +13,16 @@ VITE_UPLOAD_TYPE=client
|
|||
VITE_API_URL=/admin-api
|
||||
|
||||
# 是否删除debugger
|
||||
VITE_DROP_DEBUGGER=true
|
||||
VITE_DROP_DEBUGGER=false
|
||||
|
||||
# 是否删除console.log
|
||||
VITE_DROP_CONSOLE=true
|
||||
VITE_DROP_CONSOLE=false
|
||||
|
||||
# 是否sourcemap
|
||||
VITE_SOURCEMAP=false
|
||||
VITE_SOURCEMAP=true
|
||||
|
||||
# 打包路径
|
||||
VITE_BASE_PATH=/admin-ui-vue3/
|
||||
VITE_BASE_PATH=/
|
||||
|
||||
# 输出路径
|
||||
VITE_OUT_DIR=dist-test
|
||||
|
|
|
|||
|
|
@ -35,6 +35,16 @@ export const FollowUpRecordApi = {
|
|||
return await request.get({ url: `/crm/follow-up-record/page`, params })
|
||||
},
|
||||
|
||||
// 查询单条跟进记录
|
||||
getFollowUpRecord: async (id: number) => {
|
||||
return await request.get({ url: `/crm/follow-up-record/get?id=${id}` })
|
||||
},
|
||||
|
||||
// 更新跟进记录
|
||||
updateFollowUpRecord: async (data: FollowUpRecordVO) => {
|
||||
return await request.put({ url: `/crm/follow-up-record/update`, data })
|
||||
},
|
||||
|
||||
// 新增跟进记录
|
||||
createFollowUpRecord: async (data: FollowUpRecordVO) => {
|
||||
return await request.post({ url: `/crm/follow-up-record/create`, data })
|
||||
|
|
|
|||
|
|
@ -49,17 +49,6 @@
|
|||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="商机名称" prop="businessName" width="130">
|
||||
<template #default="scope">
|
||||
<el-link
|
||||
:underline="false"
|
||||
type="primary"
|
||||
@click="openBusinessDetail(scope.row.businessId)"
|
||||
>
|
||||
{{ scope.row.businessName }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
align="center"
|
||||
label="合同金额(元)"
|
||||
|
|
@ -103,14 +92,14 @@
|
|||
<el-table-column align="center" label="备注" prop="remark" width="200" />
|
||||
<el-table-column
|
||||
align="center"
|
||||
label="已回款金额(元)"
|
||||
label="已付款金额(元)"
|
||||
prop="totalReceivablePrice"
|
||||
width="140"
|
||||
:formatter="erpPriceTableColumnFormatter"
|
||||
/>
|
||||
<el-table-column
|
||||
align="center"
|
||||
label="未回款金额(元)"
|
||||
label="未付款金额(元)"
|
||||
prop="totalReceivablePrice"
|
||||
width="140"
|
||||
:formatter="erpPriceTableColumnFormatter"
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
<el-button v-if="permissionListRef?.validateWrite" @click="openForm('update', contact.id)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button v-if="permissionListRef?.validateOwnerUser" type="primary" @click="transfer">
|
||||
<!-- <el-button v-if="permissionListRef?.validateOwnerUser" type="primary" @click="transfer">
|
||||
转移
|
||||
</el-button>
|
||||
</el-button> -->
|
||||
</ContactDetailsHeader>
|
||||
<el-col>
|
||||
<el-tabs>
|
||||
|
|
|
|||
|
|
@ -47,11 +47,19 @@ const uploadRef = ref()
|
|||
|
||||
const props = defineProps<{ customerId: number }>()
|
||||
|
||||
const { uploadUrl, httpRequest } = useUpload(undefined, 'crm_consumer', props.customerId )
|
||||
const { uploadUrl, httpRequest } = useUpload(undefined, 'crm_consumer', Number(props.customerId))
|
||||
|
||||
// 保证 data.value.categoryId 始终为最新 customerId
|
||||
watch(() => props.customerId, (val) => {
|
||||
if (val && Number(val) >= 0) {
|
||||
data.value.categoryId = Number(val)
|
||||
} else {
|
||||
data.value.categoryId = undefined
|
||||
}
|
||||
})
|
||||
/** 打开弹窗 */
|
||||
const open = (customerId: number) => {
|
||||
data.value.categoryId = customerId
|
||||
const open = (customerId: number | string) => {
|
||||
data.value.categoryId = customerId && Number(customerId) > 0 ? Number(customerId) : undefined
|
||||
dialogVisible.value = true
|
||||
resetForm()
|
||||
}
|
||||
|
|
@ -66,7 +74,7 @@ const resetForm = () => {
|
|||
const handleFileChange = (file) => {
|
||||
data.value.path = file.name
|
||||
data.value.category = 'crm_consumer'
|
||||
data.value.categoryId = props.customerId
|
||||
data.value.categoryId = Number(props.customerId)
|
||||
}
|
||||
|
||||
/** 提交上传 */
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@
|
|||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['infra:file:delete']"
|
||||
v-hasPermi="['crm:customer:file:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button v-if="permissionListRef?.validateOwnerUser" type="primary" @click="transfer">
|
||||
<el-button v-if="permissionListRef?.validateOwnerUser" v-hasPermi="['crm:customer:transfor']" type="primary" @click="transfer">
|
||||
转移
|
||||
</el-button>
|
||||
<!-- <el-button v-if="permissionListRef?.validateWrite" @click="handleUpdateDealStatus">
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="附件">
|
||||
<FileList :customerId="customerId" />
|
||||
<FileList v-if="customerId" :customerId="customerId" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="操作日志">
|
||||
<OperateLogV2 :log-list="logList" />
|
||||
|
|
@ -208,7 +208,7 @@ onMounted(() => {
|
|||
close()
|
||||
return
|
||||
}
|
||||
customerId.value = params.id as unknown as number
|
||||
customerId.value = Number(params.id)
|
||||
getCustomer()
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -21,18 +21,8 @@
|
|||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="下次联系时间" prop="nextTime">
|
||||
<el-date-picker
|
||||
v-model="formData.nextTime"
|
||||
placeholder="选择下次联系时间"
|
||||
type="date"
|
||||
value-format="x"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="formData.bizType == BizTypeEnum.CRM_CUSTOMER">
|
||||
<el-form-item label="关联联系人" prop="contactIds">
|
||||
<el-form-item label="选择联系人" prop="contactIds">
|
||||
<el-select v-model="formData.contactIds" filterable placeholder="请选择联系人">
|
||||
<el-option v-for="item in contactOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
|
|
@ -43,11 +33,6 @@
|
|||
<Editor v-model="formData.content" height="150px" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<!-- <el-col :span="12">
|
||||
<el-form-item label="图片" prop="picUrls">
|
||||
<UploadImgs v-model="formData.picUrls" class="min-w-80px" />
|
||||
</el-form-item>
|
||||
</el-col> -->
|
||||
<el-col :span="12">
|
||||
<el-form-item label="附件" prop="fileUrls">
|
||||
<UploadFile v-model="formData.fileUrls" class="min-w-80px" />
|
||||
|
|
@ -108,13 +93,13 @@ const formData = ref<FollowUpFormData>({
|
|||
const formRules = reactive({
|
||||
type: [{ required: true, message: '跟进类型不能为空', trigger: 'change' }],
|
||||
content: [{ required: true, message: '跟进内容不能为空', trigger: 'blur' }],
|
||||
nextTime: [{ required: true, message: '下次联系时间不能为空', trigger: 'blur' }],
|
||||
contactIds: [{ required: true, message: '联系人不能为空', trigger: 'change' }]
|
||||
|
||||
})
|
||||
|
||||
const formRef = ref() // 表单 Ref
|
||||
const contactOptions = ref<ContactApi.ContactVO[]>([])
|
||||
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
||||
|
||||
watch(
|
||||
() => formData.value.bizId,
|
||||
|
|
@ -123,19 +108,38 @@ watch(
|
|||
// 获取客户下联系人
|
||||
const res = await ContactApi.getContactPageByCustomer({ customerId: val, pageSize: 100 })
|
||||
contactOptions.value = res.list || []
|
||||
// 自动选中第一个联系人
|
||||
if (contactOptions.value.length > 0) {
|
||||
formData.value.contactIds = [contactOptions.value[0].id]
|
||||
} else {
|
||||
formData.value.contactIds = []
|
||||
}
|
||||
} else {
|
||||
contactOptions.value = []
|
||||
formData.value.contactIds = []
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (bizType: number, bizId: number) => {
|
||||
const open = async (bizType: number, bizId: number, id?: number) => {
|
||||
dialogVisible.value = true
|
||||
resetForm()
|
||||
formData.value.bizType = bizType
|
||||
formData.value.bizId = bizId
|
||||
if (id) {
|
||||
formType.value = 'update'
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = await FollowUpRecordApi.getFollowUpRecord(id)
|
||||
Object.assign(formData.value, data)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
} else {
|
||||
formType.value = 'create'
|
||||
}
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
|
|
@ -156,7 +160,15 @@ const submitForm = async () => {
|
|||
contactIds,
|
||||
businessIds: (formData.value.businesses || []).map((item: any) => item.id)
|
||||
} as unknown as FollowUpRecordVO
|
||||
await FollowUpRecordApi.createFollowUpRecord(data)
|
||||
|
||||
if (formType.value === 'create') {
|
||||
await FollowUpRecordApi.createFollowUpRecord(data)
|
||||
message.success(t('common.createSuccess'))
|
||||
} else {
|
||||
await FollowUpRecordApi.updateFollowUpRecord(data)
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
|
||||
message.success(t('common.createSuccess'))
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
:timestamp="formatDate(item.createTime)"
|
||||
placement="top"
|
||||
>
|
||||
<span>跟进人:</span>{{ item.creatorName }}
|
||||
<template #timestamp>
|
||||
<div style="color:#999;font-size:13px;margin-bottom:2px;">跟进时间</div>
|
||||
<span>{{ formatDate(item.createTime) }}</span>
|
||||
|
|
@ -65,11 +66,12 @@
|
|||
</el-link>
|
||||
</div>
|
||||
</div>
|
||||
<span>跟进人:</span>{{ item.creatorName }}
|
||||
<br />
|
||||
<el-button link type="primary" class="mr-10px followup-edit-btn" @click="openForm('update', item.id)">
|
||||
<Icon icon="ep:edit" class="mr-3px" /> 编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" class="mr-20px followup-delete-btn" @click="handleDelete(item.id)">
|
||||
<Icon icon="ep:delete" class="mr-3px" /> 删除
|
||||
</el-button>
|
||||
<Icon icon="ep:delete" class="mr-3px" /> 删除
|
||||
</el-button>
|
||||
<template #dot>
|
||||
<span style="background-color: #67C23A;" class="dot-node-style">
|
||||
{{ item.creatorName ? item.creatorName[0] : '' }}
|
||||
|
|
@ -129,8 +131,13 @@ const getList = async () => {
|
|||
|
||||
/** 添加/修改操作 */
|
||||
const formRef = ref<InstanceType<typeof FollowUpRecordForm>>()
|
||||
const openForm = () => {
|
||||
formRef.value?.open(props.bizType, props.bizId)
|
||||
// 支持编辑和新增
|
||||
const openForm = (type: 'create' | 'update' = 'create', id?: number) => {
|
||||
if (type === 'update' && id) {
|
||||
formRef.value?.open(props.bizType, props.bizId, id)
|
||||
} else {
|
||||
formRef.value?.open(props.bizType, props.bizId)
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
|
|
@ -264,4 +271,24 @@ watch(
|
|||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.followup-update-btn {
|
||||
color: #7492f3 !important;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
border: none;
|
||||
background: #fef0f0;
|
||||
border-radius: 4px;
|
||||
padding: 2px 10px;
|
||||
margin: 10px 0 0 0;
|
||||
&:hover {
|
||||
background: #fef0f0;
|
||||
color: #c0392b !important;
|
||||
}
|
||||
.icon {
|
||||
font-size: 15px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -36,13 +36,6 @@
|
|||
</template>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="bizType === BizTypeEnum.CRM_CUSTOMER" label="同时转移">
|
||||
<el-checkbox-group v-model="formData.toBizTypes">
|
||||
<el-checkbox :value="BizTypeEnum.CRM_CONTACT">联系人</el-checkbox>
|
||||
<el-checkbox :value="BizTypeEnum.CRM_BUSINESS">商机</el-checkbox>
|
||||
<el-checkbox :value="BizTypeEnum.CRM_CONTRACT">合同</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
|
|
@ -87,6 +80,10 @@ const open = async (bizId: number) => {
|
|||
dialogTitle.value = getDialogTitle()
|
||||
resetForm()
|
||||
formData.value.id = bizId
|
||||
// 客户转移时默认勾选联系人和合同
|
||||
if (props.bizType === BizTypeEnum.CRM_CUSTOMER) {
|
||||
formData.value.toBizTypes = [BizTypeEnum.CRM_CONTACT, BizTypeEnum.CRM_CONTRACT, BizTypeEnum.CRM_CUSTOMER]
|
||||
}
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
// 老负责人负责方式
|
||||
|
|
@ -154,6 +151,10 @@ const getDialogTitle = () => {
|
|||
const resetForm = () => {
|
||||
formRef.value?.resetFields()
|
||||
formData.value = {} as TransferReqVO
|
||||
// 客户转移时默认勾选联系人和合同
|
||||
if (props.bizType === BizTypeEnum.CRM_CUSTOMER) {
|
||||
formData.value.toBizTypes = [BizTypeEnum.CRM_CONTACT, BizTypeEnum.CRM_CONTRACT]
|
||||
}
|
||||
}
|
||||
onMounted(async () => {
|
||||
// 获得用户列表
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@
|
|||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="填报人" align="center" prop="creatorName" />
|
||||
<el-table-column label="操作" fixed="right" align="center" min-width="120px">
|
||||
<el-table-column label="操作" fixed="right" align="center" min-width="180px">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
|
|
@ -108,6 +108,13 @@
|
|||
>
|
||||
删除
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="success"
|
||||
@click="openMapDialog(scope.row)"
|
||||
>
|
||||
获取经纬度
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
|
@ -118,6 +125,10 @@
|
|||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
<!-- 地图弹窗 -->
|
||||
<el-dialog v-model="mapDialogVisible" title="获取经纬度" append-to-body>
|
||||
<IFrame class="h-609px" :src="tencentLbsUrl" />
|
||||
</el-dialog>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
|
|
@ -153,6 +164,10 @@ const queryParams = reactive({
|
|||
const queryFormRef = ref() // 搜索的表单
|
||||
const exportLoading = ref(false) // 导出的加载中
|
||||
const areaList = ref([])
|
||||
const mapDialogVisible = ref(false)
|
||||
const tencentLbsUrl = ref('')
|
||||
const currentRow = ref<any>(null)
|
||||
const TENCENT_MAP_KEY = import.meta.env.VITE_APP_TENCENT_MAP_KEY // 你的腾讯地图key
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
|
|
@ -212,11 +227,60 @@ const handleExport = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
/** 地图弹窗相关操作 */
|
||||
const openMapDialog = (row: any) => {
|
||||
currentRow.value = row
|
||||
// 拼接带初始点的 url
|
||||
|
||||
let url = `https://apis.map.qq.com/tools/locpicker?type=1&key=${TENCENT_MAP_KEY}&referer=myapp`
|
||||
if (row.locationLat && row.locationLng) {
|
||||
url += `&coord=${row.locationLat},${row.locationLng}`
|
||||
}
|
||||
tencentLbsUrl.value = url
|
||||
mapDialogVisible.value = true
|
||||
}
|
||||
|
||||
watch(mapDialogVisible, (val) => {
|
||||
if (!val) {
|
||||
tencentLbsUrl.value = '' // 关闭时重置,避免缓存
|
||||
}
|
||||
})
|
||||
|
||||
// 选择经纬度回填
|
||||
function selectAddress(loc: any): void {
|
||||
if (loc.latlng && loc.latlng.lat) {
|
||||
currentRow.value.locationLat = loc.latlng.lat
|
||||
}
|
||||
if (loc.latlng && loc.latlng.lng) {
|
||||
currentRow.value.locationLng = loc.latlng.lng
|
||||
}
|
||||
mapDialogVisible.value = false
|
||||
// 可选:刷新表格数据
|
||||
getList()
|
||||
}
|
||||
|
||||
// 初始化腾讯地图监听
|
||||
const initTencentLbsMap = () => {
|
||||
// @ts-ignore
|
||||
window.selectAddress = selectAddress
|
||||
window.addEventListener(
|
||||
'message',
|
||||
function (event) {
|
||||
let loc = event.data
|
||||
if (loc && loc.module === 'locationPicker') {
|
||||
// @ts-ignore
|
||||
window.selectAddress(loc)
|
||||
}
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
AreaApi.getAreaTree().then(res => {
|
||||
areaList.value = res
|
||||
})
|
||||
initTencentLbsMap()
|
||||
})
|
||||
</script>
|
||||
Loading…
Reference in New Issue