|
|
|
|
@ -0,0 +1,405 @@
|
|
|
|
|
<template>
|
|
|
|
|
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
|
|
|
|
<el-form
|
|
|
|
|
ref="formRef"
|
|
|
|
|
:model="formData"
|
|
|
|
|
:rules="formRules"
|
|
|
|
|
label-width="100px"
|
|
|
|
|
v-loading="formLoading"
|
|
|
|
|
>
|
|
|
|
|
<el-divider content-position="left">基本信息</el-divider>
|
|
|
|
|
<el-row :gutter="20">
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="客户名称" prop="customerName">
|
|
|
|
|
<el-input v-model="formData.customerName" placeholder="请输入客户名称" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="客户编码" prop="customerCode">
|
|
|
|
|
<el-input v-model="formData.customerCode" placeholder="请输入客户编码" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
<el-row :gutter="20">
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="售后负责人" prop="afterSalesManager">
|
|
|
|
|
<el-input v-model="formData.afterSalesManager" placeholder="请输入售后负责人" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="状态" prop="status">
|
|
|
|
|
<el-radio-group v-model="formData.status">
|
|
|
|
|
<el-radio
|
|
|
|
|
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
|
|
|
|
:key="dict.value"
|
|
|
|
|
:value="dict.value"
|
|
|
|
|
>
|
|
|
|
|
{{ dict.label }}
|
|
|
|
|
</el-radio>
|
|
|
|
|
</el-radio-group>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
<el-form-item label="备注" prop="remark">
|
|
|
|
|
<el-input v-model="formData.remark" placeholder="请输入备注" type="textarea" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
|
|
<el-divider content-position="left">区域信息</el-divider>
|
|
|
|
|
<el-row :gutter="20">
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="国家" prop="countryCode">
|
|
|
|
|
<country-select
|
|
|
|
|
v-model="formData.countryCode"
|
|
|
|
|
:country="formData.countryCode"
|
|
|
|
|
top-country="CN"
|
|
|
|
|
placeholder="请选择国家"
|
|
|
|
|
class-name="w-full el-country-select"
|
|
|
|
|
:search-able="true"
|
|
|
|
|
:disable-placeholder="true"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item
|
|
|
|
|
:label="isChina ? '省市' : '省/州'"
|
|
|
|
|
:prop="isChina ? 'areaIds' : 'provinceName'"
|
|
|
|
|
>
|
|
|
|
|
<el-cascader
|
|
|
|
|
v-if="isChina"
|
|
|
|
|
v-model="formData.areaIds"
|
|
|
|
|
class="w-full"
|
|
|
|
|
:options="areaTree"
|
|
|
|
|
:props="areaCascaderProps"
|
|
|
|
|
clearable
|
|
|
|
|
filterable
|
|
|
|
|
placeholder="请选择省/市"
|
|
|
|
|
@change="handleAreaChange"
|
|
|
|
|
/>
|
|
|
|
|
<region-select
|
|
|
|
|
v-else
|
|
|
|
|
v-model="formData.provinceName"
|
|
|
|
|
:region="formData.provinceName"
|
|
|
|
|
:country="formData.countryCode"
|
|
|
|
|
placeholder="请选择省/州"
|
|
|
|
|
class-name="w-full el-country-select"
|
|
|
|
|
:search-able="true"
|
|
|
|
|
:disable-placeholder="true"
|
|
|
|
|
:region-name="true"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
<el-row v-if="!isChina" :gutter="20">
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="城市" prop="cityName">
|
|
|
|
|
<el-input v-model="formData.cityName" placeholder="请输入城市" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
<el-row :gutter="20">
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="详细地址" prop="address">
|
|
|
|
|
<el-input v-model="formData.address" placeholder="请输入详细地址" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
<el-row :gutter="20">
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="经度" prop="longitude">
|
|
|
|
|
<el-input v-model="formData.longitude" placeholder="请输入经度" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="纬度" prop="latitude">
|
|
|
|
|
<el-input v-model="formData.latitude" placeholder="请输入纬度" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
</el-form>
|
|
|
|
|
<template #footer>
|
|
|
|
|
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
|
|
|
|
<el-button @click="dialogVisible = false">取 消</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</Dialog>
|
|
|
|
|
</template>
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ManagementApi, Management } from '@/api/cus/management'
|
|
|
|
|
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
|
|
|
|
import { CommonStatusEnum } from '@/utils/constants'
|
|
|
|
|
import { getAreaTree } from '@/api/system/area'
|
|
|
|
|
|
|
|
|
|
/** 客户管理 表单 */
|
|
|
|
|
defineOptions({ name: 'ManagementForm' })
|
|
|
|
|
|
|
|
|
|
const { t } = useI18n() // 国际化
|
|
|
|
|
const message = useMessage() // 消息弹窗
|
|
|
|
|
|
|
|
|
|
const dialogVisible = ref(false) // 弹窗的是否展示
|
|
|
|
|
const dialogTitle = ref('') // 弹窗的标题
|
|
|
|
|
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
|
|
|
|
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
|
|
|
|
type FormModel = Partial<Management> & {
|
|
|
|
|
areaIds?: number[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const formData = ref<FormModel>({
|
|
|
|
|
id: undefined,
|
|
|
|
|
customerCode: undefined,
|
|
|
|
|
customerName: undefined,
|
|
|
|
|
status: CommonStatusEnum.ENABLE,
|
|
|
|
|
remark: undefined,
|
|
|
|
|
afterSalesManager: undefined,
|
|
|
|
|
countryCode: 'CN',
|
|
|
|
|
countryName: '中国',
|
|
|
|
|
areaIds: undefined,
|
|
|
|
|
provinceCode: undefined,
|
|
|
|
|
provinceName: undefined,
|
|
|
|
|
cityCode: undefined,
|
|
|
|
|
cityName: undefined,
|
|
|
|
|
address: undefined,
|
|
|
|
|
longitude: undefined,
|
|
|
|
|
latitude: undefined,
|
|
|
|
|
dbIp: undefined,
|
|
|
|
|
dbPort: undefined,
|
|
|
|
|
dbUsername: undefined,
|
|
|
|
|
dbPassword: undefined
|
|
|
|
|
})
|
|
|
|
|
const formRules = reactive({
|
|
|
|
|
customerName: [{ required: true, message: '客户名称不能为空', trigger: 'blur' }],
|
|
|
|
|
customerCode: [{ required: true, message: '客户编码不能为空', trigger: 'blur' }],
|
|
|
|
|
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
|
|
|
|
|
})
|
|
|
|
|
const formRef = ref() // 表单 Ref
|
|
|
|
|
|
|
|
|
|
const isChina = computed(() => formData.value.countryCode === 'CN')
|
|
|
|
|
|
|
|
|
|
const countryMetaLoaded = ref(false)
|
|
|
|
|
const countryCodeToName = ref(new Map<string, string>([['CN', '中国']]))
|
|
|
|
|
const countryNameToCode = ref(new Map<string, string>([['中国', 'CN']]))
|
|
|
|
|
const syncingForm = ref(false)
|
|
|
|
|
|
|
|
|
|
type AreaVO = {
|
|
|
|
|
id: number
|
|
|
|
|
name: string
|
|
|
|
|
code: string
|
|
|
|
|
parentId?: number
|
|
|
|
|
children?: AreaVO[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const areaTree = ref<AreaVO[]>([])
|
|
|
|
|
const areaCascaderProps = {
|
|
|
|
|
label: 'name',
|
|
|
|
|
value: 'id',
|
|
|
|
|
children: 'children',
|
|
|
|
|
emitPath: true,
|
|
|
|
|
checkStrictly: true
|
|
|
|
|
}
|
|
|
|
|
const areaIdToNode = ref(new Map<number, AreaVO>())
|
|
|
|
|
const areaIdToParentId = ref(new Map<number, number | undefined>())
|
|
|
|
|
const areaCodeToId = ref(new Map<string, number>())
|
|
|
|
|
|
|
|
|
|
const rebuildAreaIndex = (tree: AreaVO[]) => {
|
|
|
|
|
const idToNode = new Map<number, AreaVO>()
|
|
|
|
|
const idToParent = new Map<number, number | undefined>()
|
|
|
|
|
const codeToId = new Map<string, number>()
|
|
|
|
|
const walk = (nodes: AreaVO[], parentId?: number) => {
|
|
|
|
|
nodes.forEach((n) => {
|
|
|
|
|
idToNode.set(n.id, n)
|
|
|
|
|
idToParent.set(n.id, parentId)
|
|
|
|
|
if (n.code) {
|
|
|
|
|
codeToId.set(n.code, n.id)
|
|
|
|
|
}
|
|
|
|
|
if (n.children?.length) walk(n.children, n.id)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
walk(tree)
|
|
|
|
|
areaIdToNode.value = idToNode
|
|
|
|
|
areaIdToParentId.value = idToParent
|
|
|
|
|
areaCodeToId.value = codeToId
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const buildPathById = (id: number) => {
|
|
|
|
|
const path: number[] = []
|
|
|
|
|
let current: number | undefined = id
|
|
|
|
|
while (current !== undefined) {
|
|
|
|
|
path.push(current)
|
|
|
|
|
current = areaIdToParentId.value.get(current)
|
|
|
|
|
}
|
|
|
|
|
return path.reverse()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const syncAreaIdsFromCodes = () => {
|
|
|
|
|
const cityId = formData.value.cityCode
|
|
|
|
|
? areaCodeToId.value.get(formData.value.cityCode)
|
|
|
|
|
: undefined
|
|
|
|
|
const provinceId = formData.value.provinceCode
|
|
|
|
|
? areaCodeToId.value.get(formData.value.provinceCode)
|
|
|
|
|
: undefined
|
|
|
|
|
const targetId = cityId ?? provinceId
|
|
|
|
|
if (!targetId) return
|
|
|
|
|
formData.value.areaIds = buildPathById(targetId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleAreaChange = (value?: number[]) => {
|
|
|
|
|
if (!value?.length) {
|
|
|
|
|
formData.value.provinceCode = undefined
|
|
|
|
|
formData.value.provinceName = undefined
|
|
|
|
|
formData.value.cityCode = undefined
|
|
|
|
|
formData.value.cityName = undefined
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const ids = value
|
|
|
|
|
const provinceNode = ids[0] ? areaIdToNode.value.get(ids[0]) : undefined
|
|
|
|
|
const cityNode = ids[1] ? areaIdToNode.value.get(ids[1]) : undefined
|
|
|
|
|
|
|
|
|
|
formData.value.provinceCode = provinceNode?.code
|
|
|
|
|
formData.value.provinceName = provinceNode?.name
|
|
|
|
|
formData.value.cityCode = cityNode?.code
|
|
|
|
|
formData.value.cityName = cityNode?.name
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleCountryChange = async (countryCode?: string) => {
|
|
|
|
|
const code = countryCode || undefined
|
|
|
|
|
formData.value.countryName = code ? countryCodeToName.value.get(code) || code : undefined
|
|
|
|
|
formData.value.areaIds = undefined
|
|
|
|
|
handleAreaChange(undefined)
|
|
|
|
|
if (code) await ensureAreaTreeLoaded(code)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => formData.value.countryCode,
|
|
|
|
|
(code) => {
|
|
|
|
|
if (!dialogVisible.value) return
|
|
|
|
|
if (syncingForm.value) return
|
|
|
|
|
void handleCountryChange(code)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const loadedCountryCode = ref<string | undefined>(undefined)
|
|
|
|
|
const ensureAreaTreeLoaded = async (countryCode?: string) => {
|
|
|
|
|
if (countryCode && countryCode !== 'CN') {
|
|
|
|
|
areaTree.value = []
|
|
|
|
|
rebuildAreaIndex([])
|
|
|
|
|
loadedCountryCode.value = countryCode
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (loadedCountryCode.value === countryCode && areaTree.value.length) return
|
|
|
|
|
const tree = (await getAreaTree(countryCode)) || []
|
|
|
|
|
areaTree.value = tree
|
|
|
|
|
rebuildAreaIndex(tree)
|
|
|
|
|
loadedCountryCode.value = countryCode
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 打开弹窗 */
|
|
|
|
|
const open = async (type: string, id?: number) => {
|
|
|
|
|
dialogVisible.value = true
|
|
|
|
|
dialogTitle.value = t('action.' + type)
|
|
|
|
|
formType.value = type
|
|
|
|
|
syncingForm.value = true
|
|
|
|
|
resetForm()
|
|
|
|
|
if (!formData.value.countryCode && formData.value.countryName) {
|
|
|
|
|
formData.value.countryCode = countryNameToCode.value.get(formData.value.countryName)
|
|
|
|
|
}
|
|
|
|
|
await ensureAreaTreeLoaded(formData.value.countryCode)
|
|
|
|
|
// 修改时,设置数据
|
|
|
|
|
if (id) {
|
|
|
|
|
formLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
formData.value = await ManagementApi.getManagement(id)
|
|
|
|
|
if (!formData.value.countryCode && formData.value.countryName) {
|
|
|
|
|
formData.value.countryCode = countryNameToCode.value.get(formData.value.countryName)
|
|
|
|
|
}
|
|
|
|
|
formData.value.countryName = formData.value.countryCode
|
|
|
|
|
? countryCodeToName.value.get(formData.value.countryCode) || formData.value.countryName
|
|
|
|
|
: formData.value.countryName
|
|
|
|
|
await ensureAreaTreeLoaded(formData.value.countryCode)
|
|
|
|
|
syncAreaIdsFromCodes()
|
|
|
|
|
handleAreaChange(formData.value.areaIds as any)
|
|
|
|
|
} finally {
|
|
|
|
|
formLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
syncingForm.value = false
|
|
|
|
|
}
|
|
|
|
|
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
|
|
|
|
|
|
|
|
|
/** 提交表单 */
|
|
|
|
|
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
|
|
|
|
const submitForm = async () => {
|
|
|
|
|
// 校验表单
|
|
|
|
|
await formRef.value.validate()
|
|
|
|
|
// 提交请求
|
|
|
|
|
formLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
const data = formData.value as unknown as Management
|
|
|
|
|
if (formType.value === 'create') {
|
|
|
|
|
await ManagementApi.createManagement(data)
|
|
|
|
|
message.success(t('common.createSuccess'))
|
|
|
|
|
} else {
|
|
|
|
|
await ManagementApi.updateManagement(data)
|
|
|
|
|
message.success(t('common.updateSuccess'))
|
|
|
|
|
}
|
|
|
|
|
dialogVisible.value = false
|
|
|
|
|
// 发送操作成功的事件
|
|
|
|
|
emit('success')
|
|
|
|
|
} finally {
|
|
|
|
|
formLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 重置表单 */
|
|
|
|
|
const resetForm = () => {
|
|
|
|
|
formData.value = {
|
|
|
|
|
id: undefined,
|
|
|
|
|
customerCode: undefined,
|
|
|
|
|
customerName: undefined,
|
|
|
|
|
status: CommonStatusEnum.ENABLE,
|
|
|
|
|
remark: undefined,
|
|
|
|
|
afterSalesManager: undefined,
|
|
|
|
|
countryCode: 'CN',
|
|
|
|
|
countryName: '中国',
|
|
|
|
|
areaIds: undefined,
|
|
|
|
|
provinceCode: undefined,
|
|
|
|
|
provinceName: undefined,
|
|
|
|
|
cityCode: undefined,
|
|
|
|
|
cityName: undefined,
|
|
|
|
|
address: undefined,
|
|
|
|
|
longitude: undefined,
|
|
|
|
|
latitude: undefined,
|
|
|
|
|
dbIp: undefined,
|
|
|
|
|
dbPort: undefined,
|
|
|
|
|
dbUsername: undefined,
|
|
|
|
|
dbPassword: undefined
|
|
|
|
|
}
|
|
|
|
|
formRef.value?.resetFields()
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
:deep(select.el-country-select) {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: var(--el-component-size, 32px);
|
|
|
|
|
padding: 0 30px 0 11px;
|
|
|
|
|
border: 1px solid var(--el-border-color);
|
|
|
|
|
border-radius: var(--el-border-radius-base);
|
|
|
|
|
background-color: var(--el-input-bg-color, var(--el-bg-color));
|
|
|
|
|
color: var(--el-text-color-regular);
|
|
|
|
|
font-size: var(--el-font-size-base);
|
|
|
|
|
outline: none;
|
|
|
|
|
transition:
|
|
|
|
|
border-color 0.2s ease,
|
|
|
|
|
box-shadow 0.2s ease,
|
|
|
|
|
background-color 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(select.el-country-select:focus) {
|
|
|
|
|
border-color: var(--el-color-primary);
|
|
|
|
|
box-shadow: 0 0 0 1px var(--el-color-primary) inset;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(select.el-country-select:disabled) {
|
|
|
|
|
background-color: var(--el-disabled-bg-color);
|
|
|
|
|
color: var(--el-disabled-text-color);
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
</style>
|