359 lines
12 KiB
Vue
359 lines
12 KiB
Vue
<template>
|
|
<el-dialog
|
|
v-model="dialog.visible"
|
|
:title="dialog.title"
|
|
width="1160px"
|
|
class="dialog-form-layout"
|
|
align-center
|
|
destroy-on-close
|
|
:close-on-click-modal="false"
|
|
@close="reset"
|
|
>
|
|
<el-form
|
|
ref="formRef"
|
|
:model="form"
|
|
:rules="rules"
|
|
label-position="right"
|
|
label-width="100px"
|
|
class="crm-dialog-form"
|
|
scroll-to-error
|
|
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
|
>
|
|
<el-divider content-position="left">基本信息</el-divider>
|
|
<div class="form-grid-4">
|
|
<el-form-item label="线索姓名" prop="clueName">
|
|
<el-input v-model="form.clueName" placeholder="请输入线索姓名" maxlength="100" />
|
|
</el-form-item>
|
|
<el-form-item label="手机号" prop="mobile">
|
|
<el-input v-model="form.mobile" placeholder="请输入手机号" maxlength="11" />
|
|
</el-form-item>
|
|
<el-form-item label="职位" prop="position">
|
|
<el-input v-model="form.position" placeholder="请输入职位" maxlength="50" />
|
|
</el-form-item>
|
|
<el-form-item label="邮箱" prop="email">
|
|
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="100" />
|
|
</el-form-item>
|
|
<el-form-item label="公司名称" prop="company">
|
|
<el-input v-model="form.company" placeholder="请输入公司名称" maxlength="200" />
|
|
</el-form-item>
|
|
<el-form-item label="客户类型" prop="customerType">
|
|
<el-select v-model="form.customerType" placeholder="请选择客户类型" clearable>
|
|
<el-option
|
|
v-for="opt in dictStore.getOptions('crm_customer_type')"
|
|
:key="opt.value"
|
|
:label="opt.label"
|
|
:value="opt.value"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="客户网站" prop="website" class="form-field-span-2">
|
|
<el-input v-model="form.website" placeholder="请输入客户网站" maxlength="500" />
|
|
</el-form-item>
|
|
</div>
|
|
|
|
<el-divider content-position="left">详细信息</el-divider>
|
|
<div class="form-grid-4">
|
|
<el-form-item label="线索来源" prop="source">
|
|
<el-select v-model="form.source" placeholder="请选择线索来源" clearable>
|
|
<el-option
|
|
v-for="opt in dictStore.getOptions('crm_clue_source')"
|
|
:key="opt.value"
|
|
:label="opt.label"
|
|
:value="opt.value"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="线索等级" prop="level">
|
|
<el-select v-model="form.level" placeholder="请选择线索等级" clearable>
|
|
<el-option
|
|
v-for="opt in dictStore.getOptions('crm_clue_level')"
|
|
:key="opt.value"
|
|
:label="opt.label"
|
|
:value="opt.value"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="所属行业" prop="industry">
|
|
<el-select v-model="form.industry" placeholder="请选择所属行业" clearable>
|
|
<el-option
|
|
v-for="opt in dictStore.getOptions('crm_customer_industry')"
|
|
:key="opt.value"
|
|
:label="opt.label"
|
|
:value="opt.value"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="所在园区" prop="parkArea">
|
|
<DictSelect
|
|
v-model="form.parkArea"
|
|
dict-type="crm_customer_park"
|
|
placeholder="请选择或输入所在园区"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item label="省份" prop="province">
|
|
<el-input v-model="form.province" placeholder="请输入省份" maxlength="50" />
|
|
</el-form-item>
|
|
<el-form-item label="城市" prop="city">
|
|
<el-input v-model="form.city" placeholder="请输入城市" maxlength="50" />
|
|
</el-form-item>
|
|
<el-form-item label="区县" prop="district">
|
|
<el-input v-model="form.district" placeholder="请输入区县" maxlength="50" />
|
|
</el-form-item>
|
|
<el-form-item label="详细地址" class="form-field-span-2">
|
|
<el-input v-model="form.address" placeholder="请输入详细地址" maxlength="255" />
|
|
</el-form-item>
|
|
</div>
|
|
|
|
<el-divider content-position="left">需求信息</el-divider>
|
|
<div class="form-grid-4">
|
|
<el-form-item label="意向产品" class="form-field-span-2">
|
|
<el-input
|
|
v-model="form.intentProducts"
|
|
placeholder="请输入意向产品,多个用逗号分隔"
|
|
maxlength="500"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item label="需求描述" class="form-field-span-2">
|
|
<el-input
|
|
v-model="form.requirement"
|
|
placeholder="请输入需求描述"
|
|
type="textarea"
|
|
:rows="2"
|
|
maxlength="500"
|
|
/>
|
|
</el-form-item>
|
|
</div>
|
|
|
|
<el-divider content-position="left">分配信息</el-divider>
|
|
<div class="form-grid-4">
|
|
<el-form-item label="负责人" prop="ownerUserId">
|
|
<el-select v-model="form.ownerUserId" placeholder="请选择负责人" clearable filterable>
|
|
<el-option
|
|
v-for="user in userList"
|
|
:key="user.id"
|
|
:label="user.nickName"
|
|
:value="user.id"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="下次联系时间" prop="nextTime">
|
|
<el-date-picker
|
|
v-model="form.nextTime"
|
|
type="datetime"
|
|
placeholder="请选择下次联系时间"
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
style="width: 100%"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item label="备注">
|
|
<el-input v-model="form.remark" placeholder="请输入备注" maxlength="500" />
|
|
</el-form-item>
|
|
</div>
|
|
</el-form>
|
|
|
|
<template #footer>
|
|
<div class="dialog-footer">
|
|
<el-button class="btn-ghost" @click="cancel">
|
|
<el-icon><Close /></el-icon>取消
|
|
</el-button>
|
|
<el-button class="btn-main" type="primary" :loading="submitting" @click="submitForm">
|
|
<el-icon><Check /></el-icon>确定
|
|
</el-button>
|
|
</div>
|
|
</template>
|
|
</el-dialog>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { Close, Check } from '@element-plus/icons-vue'
|
|
import { getClue, addClue, updateClue, checkDuplicateClue } from '@/api/crm/clue'
|
|
import type { ClueForm as ClueFormType } from '@/api/crm/clue/types'
|
|
import { fetchUserOptions } from '@/api/admin'
|
|
import { useDictStore } from '@/store/modules/dict'
|
|
import DictSelect from '@/components/DictSelect/index.vue'
|
|
import { useUserStore } from '@/store/modules/user'
|
|
|
|
const dictStore = useDictStore()
|
|
const userStore = useUserStore()
|
|
const emit = defineEmits(['success'])
|
|
|
|
const form = ref<ClueFormType>({
|
|
id: undefined,
|
|
clueName: '',
|
|
mobile: '',
|
|
company: '',
|
|
customerType: '',
|
|
email: undefined,
|
|
position: '',
|
|
source: '',
|
|
level: 'unrated',
|
|
industry: '',
|
|
parkArea: '',
|
|
province: '',
|
|
city: '',
|
|
district: '',
|
|
address: '',
|
|
website: undefined,
|
|
intentProducts: '',
|
|
requirement: '',
|
|
ownerUserId: undefined,
|
|
nextTime: undefined,
|
|
remark: undefined
|
|
})
|
|
|
|
const rules = ref({
|
|
clueName: [{ required: true, message: '线索姓名不能为空', trigger: 'blur' }],
|
|
mobile: [
|
|
{ required: true, message: '手机号不能为空', trigger: 'blur' },
|
|
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
|
],
|
|
company: [{ required: true, message: '公司名称不能为空', trigger: 'blur' }],
|
|
customerType: [{ required: true, message: '客户类型不能为空', trigger: 'change' }],
|
|
source: [{ required: true, message: '线索来源不能为空', trigger: 'change' }],
|
|
ownerUserId: [{ required: true, message: '负责人不能为空', trigger: 'change' }],
|
|
email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }]
|
|
})
|
|
|
|
const userList = ref<any[]>([])
|
|
|
|
const dialog = reactive({ visible: false, title: '' })
|
|
const submitting = ref(false)
|
|
const formRef = ref()
|
|
|
|
const resolveDefaultOwnerUserId = () => {
|
|
const info = userStore.info
|
|
const userId = info?.userId ?? info?.id
|
|
return userId != null && userId !== '' ? String(userId) : undefined
|
|
}
|
|
|
|
const applyDefaultOwnerUserId = async () => {
|
|
const defaultOwnerUserId = resolveDefaultOwnerUserId()
|
|
if (!defaultOwnerUserId) return
|
|
await nextTick()
|
|
form.value.ownerUserId = defaultOwnerUserId
|
|
}
|
|
|
|
const open = async (id?: string | number) => {
|
|
reset()
|
|
dialog.visible = true
|
|
await loadUserList()
|
|
|
|
if (id) {
|
|
dialog.title = '修改线索'
|
|
try {
|
|
const res = await getClue(id)
|
|
const data = (res as any)?.data ?? res ?? {}
|
|
Object.assign(form.value, data)
|
|
form.value.nextTime = data.nextTime || undefined
|
|
form.value.ownerUserId = data.ownerUserId || undefined
|
|
} catch (error) {
|
|
console.error(error)
|
|
dialog.visible = false
|
|
return
|
|
}
|
|
} else {
|
|
dialog.title = '新增线索'
|
|
await applyDefaultOwnerUserId()
|
|
}
|
|
}
|
|
|
|
const loadUserList = async () => {
|
|
try {
|
|
const res = await fetchUserOptions()
|
|
const options = (res as any)?.data ?? res ?? []
|
|
userList.value = (Array.isArray(options) ? options : []).map((item: any) => ({
|
|
id: item.value,
|
|
nickName: item.label
|
|
}))
|
|
} catch (error) {
|
|
console.error(error)
|
|
userList.value = []
|
|
}
|
|
}
|
|
|
|
|
|
const submitForm = () => {
|
|
formRef.value?.validate(async (valid: boolean) => {
|
|
if (!valid) return
|
|
submitting.value = true
|
|
try {
|
|
const dup = await checkDuplicateClue({
|
|
mobile: form.value.mobile,
|
|
company: form.value.company,
|
|
excludeId: form.value.id
|
|
})
|
|
if (dup?.isDuplicate && dup?.duplicates?.length) {
|
|
const names = dup.duplicates
|
|
.slice(0, 3)
|
|
.map((item: any) => item.clueName || item.company || item.mobile)
|
|
.join('、')
|
|
try {
|
|
await ElMessageBox.confirm(
|
|
`检测到可能重复的线索:${names}。是否仍要继续保存?`,
|
|
'重复提示',
|
|
{ type: 'warning', confirmButtonText: '继续保存', cancelButtonText: '取消' }
|
|
)
|
|
} catch {
|
|
return
|
|
}
|
|
}
|
|
|
|
if (form.value.id) {
|
|
await updateClue(form.value)
|
|
ElMessage.success('修改成功')
|
|
} else {
|
|
await addClue(form.value)
|
|
ElMessage.success('新增成功')
|
|
}
|
|
dialog.visible = false
|
|
emit('success')
|
|
} catch (error) {
|
|
console.error(error)
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
})
|
|
}
|
|
|
|
const cancel = () => {
|
|
dialog.visible = false
|
|
reset()
|
|
}
|
|
|
|
const reset = () => {
|
|
form.value = {
|
|
id: undefined,
|
|
clueName: '',
|
|
mobile: '',
|
|
company: '',
|
|
customerType: '',
|
|
email: undefined,
|
|
position: '',
|
|
source: '',
|
|
level: 'unrated',
|
|
industry: '',
|
|
parkArea: '',
|
|
province: '',
|
|
city: '',
|
|
district: '',
|
|
address: '',
|
|
website: undefined,
|
|
intentProducts: '',
|
|
requirement: '',
|
|
ownerUserId: undefined,
|
|
nextTime: undefined,
|
|
remark: undefined
|
|
}
|
|
submitting.value = false
|
|
formRef.value?.resetFields()
|
|
}
|
|
|
|
defineExpose({ open })
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.form-grid-4 .form-field-span-2 {
|
|
grid-column: span 2;
|
|
}
|
|
</style>
|