crm 客户管理

pull/853/head
wersd 2025-06-08 13:58:25 +08:00
parent 584d6978ae
commit 7f8f02ebf7
13 changed files with 179 additions and 65 deletions

5
.env
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 })

View File

@ -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"

View File

@ -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>

View File

@ -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)
}
/** 提交上传 */

View File

@ -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>

View File

@ -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>

View File

@ -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')

View File

@ -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>

View File

@ -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 () => {
//

View File

@ -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>