feat:客户管理-国家、省、城市组件修改

master
黄伟杰 2 months ago
parent 585bb26971
commit d4b8bc4c94

@ -141,9 +141,9 @@ export default {
qrcode: 'Scan the QR code to log in', qrcode: 'Scan the QR code to log in',
btnRegister: 'Sign up', btnRegister: 'Sign up',
SmsSendMsg: 'code has been sent', SmsSendMsg: 'code has been sent',
resetPassword: "Reset Password", resetPassword: 'Reset Password',
resetPasswordSuccess: "Reset Password Success", resetPasswordSuccess: 'Reset Password Success',
invalidTenantName:"Invalid Tenant Name" invalidTenantName: 'Invalid Tenant Name'
}, },
captcha: { captcha: {
verify: 'Verify', verify: 'Verify',
@ -315,6 +315,33 @@ export default {
dataUpdate: 'Dict Data Eidt', dataUpdate: 'Dict Data Eidt',
fileUpload: 'File Upload' fileUpload: 'File Upload'
}, },
cus: {
management: {
fields: {
customerCode: 'Customer Code',
customerName: 'Customer Name',
region: 'Region',
afterSalesManager: 'After-sales Manager',
country: 'Country',
provinceCN: 'Province',
stateProvince: 'State/Province',
city: 'City',
address: 'Address',
longitude: 'Longitude',
latitude: 'Latitude'
},
actions: {
batchDelete: 'Batch Delete'
},
sections: {
regionInfo: 'Location'
},
loading: {
countries: 'Loading countries...'
},
exportFileName: 'customer-management.xls'
}
},
dialog: { dialog: {
dialog: 'Dialog', dialog: 'Dialog',
open: 'Open', open: 'Open',
@ -459,4 +486,4 @@ export default {
btn_zoom_out: 'Zoom out', btn_zoom_out: 'Zoom out',
preview: 'Preivew' preview: 'Preivew'
} }
} }

@ -316,6 +316,33 @@ export default {
dataCreate: '字典数据新增', dataCreate: '字典数据新增',
dataUpdate: '字典数据编辑' dataUpdate: '字典数据编辑'
}, },
cus: {
management: {
fields: {
customerCode: '客户编码',
customerName: '客户名称',
region: '区域',
afterSalesManager: '售后负责人',
country: '国家',
provinceCN: '省',
stateProvince: '省/州',
city: '城市',
address: '详细地址',
longitude: '经度',
latitude: '纬度'
},
actions: {
batchDelete: '批量删除'
},
sections: {
regionInfo: '区域信息'
},
loading: {
countries: '加载国家列表...'
},
exportFileName: '客户管理.xls'
}
},
dialog: { dialog: {
dialog: '弹窗', dialog: '弹窗',
open: '打开', open: '打开',
@ -455,4 +482,4 @@ export default {
preview: '预览' preview: '预览'
}, },
'OAuth 2.0': 'OAuth 2.0' // 避免菜单名是 OAuth 2.0 时,一直 warn 报错 'OAuth 2.0': 'OAuth 2.0' // 避免菜单名是 OAuth 2.0 时,一直 warn 报错
} }

@ -4,30 +4,42 @@
ref="formRef" ref="formRef"
:model="formData" :model="formData"
:rules="formRules" :rules="formRules"
label-width="100px" min-label-width="100px"
v-loading="formLoading" v-loading="formLoading || geoInitLoading"
:element-loading-text="t('cus.management.loading.countries')"
> >
<el-divider content-position="left">基本信息</el-divider>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="客户编码" prop="customerCode"> <el-form-item :label="t('cus.management.fields.customerCode')" prop="customerCode">
<el-input v-model="formData.customerCode" placeholder="请输入客户编码" /> <el-input
v-model="formData.customerCode"
:placeholder="`${t('common.inputText')} ${t('cus.management.fields.customerCode')}`"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="客户名称" prop="customerName"> <el-form-item :label="t('cus.management.fields.customerName')" prop="customerName">
<el-input v-model="formData.customerName" placeholder="请输入客户名称" /> <el-input
v-model="formData.customerName"
:placeholder="`${t('common.inputText')} ${t('cus.management.fields.customerName')}`"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="售后负责人" prop="afterSalesManager"> <el-form-item
<el-input v-model="formData.afterSalesManager" placeholder="请输入售后负责人" /> :label="t('cus.management.fields.afterSalesManager')"
prop="afterSalesManager"
>
<el-input
v-model="formData.afterSalesManager"
:placeholder="`${t('common.inputText')} ${t('cus.management.fields.afterSalesManager')}`"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="状态" prop="status"> <el-form-item :label="t('common.status')" prop="status">
<el-radio-group v-model="formData.status"> <el-radio-group v-model="formData.status">
<el-radio <el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
@ -40,93 +52,113 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-form-item label="备注" prop="remark"> <el-form-item :label="t('form.remark')" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入备注" type="textarea" /> <el-input
v-model="formData.remark"
:placeholder="`${t('common.inputText')} ${t('form.remark')}`"
type="textarea"
/>
</el-form-item> </el-form-item>
<el-divider content-position="left">区域信息</el-divider> <el-divider content-position="left">{{ t('cus.management.sections.regionInfo') }}</el-divider>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="8">
<el-form-item label="国家" prop="countryCode"> <el-form-item :label="t('cus.management.fields.country')" prop="countryCode">
<country-select <el-select
v-if="countrySelectReady"
v-model="formData.countryCode" 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-input
v-else
class="w-full" class="w-full"
:model-value="formData.countryName || formData.countryCode" :placeholder="`${t('common.selectText')} ${t('cus.management.fields.country')}`"
placeholder="加载中..." filterable
disabled clearable
/> :loading="countryLoading"
@change="handleCountryChange"
>
<el-option
v-for="item in countryOptions"
:key="item.code"
:label="item.name"
:value="item.code"
/>
</el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="8">
<el-form-item <el-form-item :label="provinceLabel" prop="provinceCode">
:label="isChina ? '省市' : '省/州'" <el-select
:prop="isChina ? 'areaIds' : 'provinceName'" v-model="formData.provinceCode"
>
<el-cascader
v-if="isChina"
v-model="formData.areaIds"
class="w-full" class="w-full"
:options="areaTree" :placeholder="provincePlaceholder"
:props="areaCascaderProps"
clearable
filterable filterable
placeholder="请选择省/市" clearable
@change="handleAreaChange" :loading="provinceLoading"
/> :disabled="!formData.countryCode"
<region-select @change="handleProvinceChange"
v-else >
v-model="formData.provinceName" <el-option
:region="formData.provinceName" v-for="item in provinceOptions"
:country="formData.countryCode" :key="item.geonameId"
placeholder="请选择省/州" :label="item.name"
class-name="w-full el-country-select" :value="String(item.geonameId)"
:search-able="true" />
:disable-placeholder="true" </el-select>
:region-name="true"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> <el-col :span="8">
<el-row v-if="!isChina" :gutter="20"> <el-form-item :label="t('cus.management.fields.city')" prop="cityCode">
<el-col :span="12"> <el-select
<el-form-item label="城市" prop="cityName"> v-model="formData.cityCode"
<el-input v-model="formData.cityName" placeholder="请输入城市" /> class="w-full"
:placeholder="`${t('common.selectText')} ${t('cus.management.fields.city')}`"
filterable
clearable
:loading="cityLoading"
:disabled="!formData.provinceCode"
@change="handleCityChange"
>
<el-option
v-for="item in cityOptions"
:key="item.geonameId"
:label="item.name"
:value="String(item.geonameId)"
/>
</el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="详细地址" prop="address"> <el-form-item :label="t('cus.management.fields.address')" prop="address">
<el-input v-model="formData.address" placeholder="请输入详细地址" /> <el-input
v-model="formData.address"
:placeholder="`${t('common.inputText')} ${t('cus.management.fields.address')}`"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="经度" prop="longitude"> <el-form-item :label="t('cus.management.fields.longitude')" prop="longitude">
<el-input v-model="formData.longitude" placeholder="请输入经度" /> <el-input
v-model="formData.longitude"
:placeholder="`${t('common.inputText')} ${t('cus.management.fields.longitude')}`"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="纬度" prop="latitude"> <el-form-item :label="t('cus.management.fields.latitude')" prop="latitude">
<el-input v-model="formData.latitude" placeholder="请输入纬度" /> <el-input
v-model="formData.latitude"
:placeholder="`${t('common.inputText')} ${t('cus.management.fields.latitude')}`"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button> <el-button @click="submitForm" type="primary" :disabled="formLoading">{{
<el-button @click="dialogVisible = false"> </el-button> t('common.ok')
}}</el-button>
<el-button @click="dialogVisible = false">{{ t('common.cancel') }}</el-button>
</template> </template>
</Dialog> </Dialog>
</template> </template>
@ -134,12 +166,11 @@
import { ManagementApi, Management } from '@/api/cus/management' import { ManagementApi, Management } from '@/api/cus/management'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { CommonStatusEnum } from '@/utils/constants' import { CommonStatusEnum } from '@/utils/constants'
import { getAreaTree } from '@/api/system/area'
/** 客户管理 表单 */ /** 客户管理 表单 */
defineOptions({ name: 'ManagementForm' }) defineOptions({ name: 'ManagementForm' })
const { t } = useI18n() // const { t, locale } = useI18n() //
const message = useMessage() // const message = useMessage() //
const dialogVisible = ref(false) // const dialogVisible = ref(false) //
@ -147,10 +178,9 @@ const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 12 const formLoading = ref(false) // 12
const formType = ref('') // create - update - const formType = ref('') // create - update -
const countrySelectReady = ref(false) type FormModel = Partial<Management>
type FormModel = Partial<Management> & {
areaIds?: number[] const geoInitLoading = ref(false)
}
const formData = ref<FormModel>({ const formData = ref<FormModel>({
id: undefined, id: undefined,
@ -160,8 +190,7 @@ const formData = ref<FormModel>({
remark: undefined, remark: undefined,
afterSalesManager: undefined, afterSalesManager: undefined,
countryCode: 'CN', countryCode: 'CN',
countryName: '中国', countryName: undefined,
areaIds: undefined,
provinceCode: undefined, provinceCode: undefined,
provinceName: undefined, provinceName: undefined,
cityCode: undefined, cityCode: undefined,
@ -175,162 +204,303 @@ const formData = ref<FormModel>({
dbPassword: undefined dbPassword: undefined
}) })
const formRules = reactive({ const formRules = reactive({
customerName: [{ required: true, message: '客户名称不能为空', trigger: 'blur' }], customerName: [{ required: true, message: t('common.required'), trigger: 'blur' }],
customerCode: [{ required: true, message: '客户编码不能为空', trigger: 'blur' }], customerCode: [{ required: true, message: t('common.required'), trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }] status: [{ required: true, message: t('common.required'), trigger: 'blur' }]
}) })
const formRef = ref() // Ref 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) const syncingForm = ref(false)
type AreaVO = { type GeoNamesCountry = {
id: number
name: string
code: string code: string
parentId?: number name: string
children?: AreaVO[] geonameId: number
} }
const areaTree = ref<AreaVO[]>([]) type GeoNamesPlace = {
const areaCascaderProps = { geonameId: number
label: 'name', name: string
value: 'id',
children: 'children',
emitPath: true,
checkStrictly: true
} }
const areaIdToNode = ref(new Map<number, AreaVO>())
const areaIdToParentId = ref(new Map<number, number | undefined>()) const countryOptions = ref<GeoNamesCountry[]>([])
const areaCodeToId = ref(new Map<string, number>()) const provinceOptions = ref<GeoNamesPlace[]>([])
const cityOptions = ref<GeoNamesPlace[]>([])
const rebuildAreaIndex = (tree: AreaVO[]) => {
const idToNode = new Map<number, AreaVO>() const countryLoading = ref(false)
const idToParent = new Map<number, number | undefined>() const provinceLoading = ref(false)
const codeToId = new Map<string, number>() const cityLoading = ref(false)
const walk = (nodes: AreaVO[], parentId?: number) => {
nodes.forEach((n) => { const provinceLabel = computed(() => {
idToNode.set(n.id, n) return formData.value.countryCode === 'CN'
idToParent.set(n.id, parentId) ? t('cus.management.fields.provinceCN')
if (n.code) { : t('cus.management.fields.stateProvince')
codeToId.set(n.code, n.id) })
}
if (n.children?.length) walk(n.children, n.id) const provincePlaceholder = computed(() => {
return formData.value.countryCode === 'CN'
? `${t('common.selectText')} ${t('cus.management.fields.provinceCN')}`
: `${t('common.selectText')} ${t('cus.management.fields.stateProvince')}`
})
const getGeoNamesLang = () => {
const current = String(locale.value || '').toLowerCase()
return current.startsWith('zh') ? 'zh' : 'en'
}
const buildGeoNamesUrl = (path: string, params: Record<string, string>) => {
const url = new URL(`http://api.geonames.org/${path}`)
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v))
return url.toString()
}
const countryCache = new Map<string, GeoNamesCountry[]>()
const childrenCache = new Map<string, GeoNamesPlace[]>()
const loadCountries = async () => {
const lang = getGeoNamesLang()
if (countryCache.has(lang)) {
countryOptions.value = countryCache.get(lang) || []
return
}
countryLoading.value = true
try {
const url = buildGeoNamesUrl('countryInfoJSON', {
username: 'withcomb',
lang
}) })
const res = await fetch(url)
const data = (await res.json()) as { geonames?: any[] }
const items: GeoNamesCountry[] = (data.geonames || [])
.map((n) => ({
code: String(n.countryCode || ''),
name: String(n.countryName || ''),
geonameId: Number(n.geonameId)
}))
.filter((n) => n.code && n.name && Number.isFinite(n.geonameId))
.sort((a, b) => a.name.localeCompare(b.name, lang === 'zh' ? 'zh-Hans' : 'en'))
countryCache.set(lang, items)
countryOptions.value = items
} catch (e) {
countryOptions.value = []
} finally {
countryLoading.value = false
} }
walk(tree)
areaIdToNode.value = idToNode
areaIdToParentId.value = idToParent
areaCodeToId.value = codeToId
} }
const buildPathById = (id: number) => { const loadChildrenByGeonameId = async (parentGeonameId: number) => {
const path: number[] = [] const lang = getGeoNamesLang()
let current: number | undefined = id const cacheKey = `${lang}|${parentGeonameId}`
while (current !== undefined) { if (childrenCache.has(cacheKey)) return childrenCache.get(cacheKey) || []
path.push(current) const url = buildGeoNamesUrl('childrenJSON', {
current = areaIdToParentId.value.get(current) geonameId: String(parentGeonameId),
username: 'withcomb',
lang
})
try {
const res = await fetch(url)
const data = (await res.json()) as { geonames?: any[] }
const items: GeoNamesPlace[] = (data.geonames || [])
.map((n) => ({
geonameId: Number(n.geonameId),
name: String(n.name || '')
}))
.filter((n) => n.name && Number.isFinite(n.geonameId))
.sort((a, b) => a.name.localeCompare(b.name, lang === 'zh' ? 'zh-Hans' : 'en'))
childrenCache.set(cacheKey, items)
return items
} catch (e) {
return []
} finally {
} }
return path.reverse()
} }
const syncAreaIdsFromCodes = () => { const loadProvincesByCountryCode = async (countryCode?: string) => {
const cityId = formData.value.cityCode if (!countryCode) {
? areaCodeToId.value.get(formData.value.cityCode) provinceOptions.value = []
: undefined return
const provinceId = formData.value.provinceCode }
? areaCodeToId.value.get(formData.value.provinceCode) const country = countryOptions.value.find((c) => c.code === countryCode)
: undefined if (!country) {
const targetId = cityId ?? provinceId provinceOptions.value = []
if (!targetId) return return
formData.value.areaIds = buildPathById(targetId) }
provinceLoading.value = true
try {
provinceOptions.value = await loadChildrenByGeonameId(country.geonameId)
} finally {
provinceLoading.value = false
}
} }
const handleAreaChange = (value?: number[]) => { const loadCitiesByProvinceGeonameId = async (provinceGeonameId?: string) => {
if (!value?.length) { if (!provinceGeonameId) {
formData.value.provinceCode = undefined cityOptions.value = []
formData.value.provinceName = undefined
formData.value.cityCode = undefined
formData.value.cityName = undefined
return return
} }
const ids = value const id = Number(provinceGeonameId)
const provinceNode = ids[0] ? areaIdToNode.value.get(ids[0]) : undefined if (!Number.isFinite(id)) {
const cityNode = ids[1] ? areaIdToNode.value.get(ids[1]) : undefined cityOptions.value = []
return
formData.value.provinceCode = provinceNode?.code }
formData.value.provinceName = provinceNode?.name cityLoading.value = true
formData.value.cityCode = cityNode?.code try {
formData.value.cityName = cityNode?.name cityOptions.value = await loadChildrenByGeonameId(id)
} finally {
cityLoading.value = false
}
} }
const handleCountryChange = async (countryCode?: string) => { const syncCountryNameFromCode = () => {
const code = countryCode || undefined const code = formData.value.countryCode
formData.value.countryName = code ? countryCodeToName.value.get(code) || code : undefined if (!code) {
formData.value.areaIds = undefined formData.value.countryName = undefined
handleAreaChange(undefined) return
if (code) await ensureAreaTreeLoaded(code) }
const item = countryOptions.value.find((c) => c.code === code)
formData.value.countryName = item?.name || formData.value.countryName || code
} }
watch( const syncCountryCodeFromName = () => {
() => formData.value.countryCode, const name = formData.value.countryName
(code) => { if (!name || formData.value.countryCode) return
if (!dialogVisible.value) return const item = countryOptions.value.find((c) => c.name === name)
if (syncingForm.value) return if (item) formData.value.countryCode = item.code
void handleCountryChange(code) }
const syncPlaceCodeByName = (name: string | undefined, list: GeoNamesPlace[]) => {
if (!name) return undefined
const item = list.find((c) => c.name === name)
return item ? String(item.geonameId) : undefined
}
const syncPlaceNameByCode = (code: string | undefined, list: GeoNamesPlace[]) => {
if (!code) return undefined
const item = list.find((c) => String(c.geonameId) === String(code))
return item?.name
}
const handleCountryChange = async (val?: string) => {
const code = val || undefined
formData.value.countryCode = code
formData.value.provinceCode = undefined
formData.value.provinceName = undefined
syncCountryNameFromCode()
provinceOptions.value = []
cityOptions.value = []
formData.value.cityCode = undefined
formData.value.cityName = undefined
await loadProvincesByCountryCode(code)
}
const loadCityDetail = async (geonameId: string) => {
const lang = getGeoNamesLang()
const url = buildGeoNamesUrl('getJSON', {
geonameId: String(geonameId),
username: 'withcomb',
lang
})
const res = await fetch(url)
const data = (await res.json()) as any
if (data && data.geonameId) {
if (data.name) formData.value.cityName = String(data.name)
if (data.lng !== undefined && data.lng !== null) formData.value.longitude = String(data.lng)
if (data.lat !== undefined && data.lat !== null) formData.value.latitude = String(data.lat)
} }
) }
const loadedCountryCode = ref<string | undefined>(undefined) const handleProvinceChange = async (val?: string) => {
const ensureAreaTreeLoaded = async (countryCode?: string) => { const code = val || undefined
if (countryCode && countryCode !== 'CN') { formData.value.provinceCode = code
areaTree.value = [] formData.value.cityCode = undefined
rebuildAreaIndex([]) formData.value.cityName = undefined
loadedCountryCode.value = countryCode formData.value.provinceName = syncPlaceNameByCode(code, provinceOptions.value) || undefined
return cityOptions.value = []
await loadCitiesByProvinceGeonameId(code)
}
const handleCityChange = async (val?: string) => {
const code = val || undefined
formData.value.cityCode = code
formData.value.cityName = syncPlaceNameByCode(code, cityOptions.value) || undefined
if (code) {
try {
await loadCityDetail(code)
} catch (e) {}
} }
if (loadedCountryCode.value === countryCode && areaTree.value.length) return
const tree = (await getAreaTree(countryCode)) || []
areaTree.value = tree
rebuildAreaIndex(tree)
loadedCountryCode.value = countryCode
} }
watch(
() => locale.value,
() => {
if (!dialogVisible.value) return
void loadCountries().then(async () => {
syncCountryNameFromCode()
await loadProvincesByCountryCode(formData.value.countryCode)
if (!formData.value.provinceCode && formData.value.provinceName) {
formData.value.provinceCode = syncPlaceCodeByName(
formData.value.provinceName,
provinceOptions.value
)
}
formData.value.provinceName =
syncPlaceNameByCode(formData.value.provinceCode, provinceOptions.value) ||
formData.value.provinceName
await loadCitiesByProvinceGeonameId(formData.value.provinceCode)
if (!formData.value.cityCode && formData.value.cityName) {
formData.value.cityCode = syncPlaceCodeByName(formData.value.cityName, cityOptions.value)
}
formData.value.cityName =
syncPlaceNameByCode(formData.value.cityCode, cityOptions.value) || formData.value.cityName
})
}
)
/** 打开弹窗 */ /** 打开弹窗 */
const open = async (type: string, id?: number) => { const open = async (type: string, id?: number) => {
dialogVisible.value = true dialogVisible.value = true
dialogTitle.value = t('action.' + type) dialogTitle.value = t('action.' + type)
formType.value = type formType.value = type
syncingForm.value = true syncingForm.value = true
countrySelectReady.value = false
resetForm() resetForm()
void nextTick(() => { geoInitLoading.value = true
requestAnimationFrame(() => { try {
countrySelectReady.value = true await loadCountries()
}) } finally {
}) geoInitLoading.value = false
if (!formData.value.countryCode && formData.value.countryName) {
formData.value.countryCode = countryNameToCode.value.get(formData.value.countryName)
} }
await ensureAreaTreeLoaded(formData.value.countryCode) syncCountryCodeFromName()
syncCountryNameFromCode()
await loadProvincesByCountryCode(formData.value.countryCode)
// //
if (id) { if (id) {
formLoading.value = true formLoading.value = true
try { try {
formData.value = await ManagementApi.getManagement(id) formData.value = await ManagementApi.getManagement(id)
if (!formData.value.countryCode && formData.value.countryName) { await loadCountries()
formData.value.countryCode = countryNameToCode.value.get(formData.value.countryName) syncCountryCodeFromName()
syncCountryNameFromCode()
await loadProvincesByCountryCode(formData.value.countryCode)
if (!formData.value.provinceCode && formData.value.provinceName) {
formData.value.provinceCode = syncPlaceCodeByName(
formData.value.provinceName,
provinceOptions.value
)
}
formData.value.provinceName =
syncPlaceNameByCode(formData.value.provinceCode, provinceOptions.value) ||
formData.value.provinceName
await loadCitiesByProvinceGeonameId(formData.value.provinceCode)
if (!formData.value.cityCode && formData.value.cityName) {
formData.value.cityCode = syncPlaceCodeByName(formData.value.cityName, cityOptions.value)
}
formData.value.cityName =
syncPlaceNameByCode(formData.value.cityCode, cityOptions.value) || formData.value.cityName
if (formData.value.cityCode) {
try {
await loadCityDetail(String(formData.value.cityCode))
} catch (e) {}
} }
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 { } finally {
formLoading.value = false formLoading.value = false
} }
@ -374,7 +544,6 @@ const resetForm = () => {
afterSalesManager: undefined, afterSalesManager: undefined,
countryCode: 'CN', countryCode: 'CN',
countryName: '中国', countryName: '中国',
areaIds: undefined,
provinceCode: undefined, provinceCode: undefined,
provinceName: undefined, provinceName: undefined,
cityCode: undefined, cityCode: undefined,
@ -390,32 +559,3 @@ const resetForm = () => {
formRef.value?.resetFields() formRef.value?.resetFields()
} }
</script> </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>

@ -6,28 +6,33 @@
:model="queryParams" :model="queryParams"
ref="queryFormRef" ref="queryFormRef"
:inline="true" :inline="true"
label-width="68px" min-label-width="68px"
> >
<el-form-item label="客户编码" prop="customerCode"> <el-form-item :label="t('cus.management.fields.customerCode')" prop="customerCode">
<el-input <el-input
v-model="queryParams.customerCode" v-model="queryParams.customerCode"
placeholder="请输入客户编码" :placeholder="`${t('common.inputText')}${t('cus.management.fields.customerCode')}`"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
class="!w-240px" class="!w-240px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="客户名称" prop="customerName"> <el-form-item :label="t('cus.management.fields.customerName')" prop="customerName">
<el-input <el-input
v-model="queryParams.customerName" v-model="queryParams.customerName"
placeholder="请输入客户名称" :placeholder="`${t('common.inputText')}${t('cus.management.fields.customerName')}`"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
class="!w-240px" class="!w-240px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="状态" prop="status"> <el-form-item :label="t('common.status')" prop="status">
<el-select v-model="queryParams.status" clearable placeholder="请选择状态" class="!w-240px"> <el-select
v-model="queryParams.status"
clearable
:placeholder="`${t('common.selectText')}${t('common.status')}`"
class="!w-240px"
>
<el-option <el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value" :key="dict.value"
@ -37,15 +42,19 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> <el-button @click="handleQuery">
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> <Icon icon="ep:search" class="mr-5px" /> {{ t('common.query') }}
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> {{ t('common.reset') }}
</el-button>
<el-button <el-button
type="primary" type="primary"
plain plain
@click="openForm('create')" @click="openForm('create')"
v-hasPermi="['cus:management:create']" v-hasPermi="['cus:management:create']"
> >
<Icon icon="ep:plus" class="mr-5px" /> 新增 <Icon icon="ep:plus" class="mr-5px" /> {{ t('action.create') }}
</el-button> </el-button>
<el-button <el-button
type="success" type="success"
@ -54,7 +63,7 @@
:loading="exportLoading" :loading="exportLoading"
v-hasPermi="['cus:management:export']" v-hasPermi="['cus:management:export']"
> >
<Icon icon="ep:download" class="mr-5px" /> 导出 <Icon icon="ep:download" class="mr-5px" /> {{ t('action.export') }}
</el-button> </el-button>
<el-button <el-button
type="danger" type="danger"
@ -63,7 +72,7 @@
@click="handleDeleteBatch" @click="handleDeleteBatch"
v-hasPermi="['cus:management:delete']" v-hasPermi="['cus:management:delete']"
> >
<Icon icon="ep:delete" class="mr-5px" /> 批量删除 <Icon icon="ep:delete" class="mr-5px" /> {{ t('cus.management.actions.batchDelete') }}
</el-button> </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
@ -80,9 +89,19 @@
@selection-change="handleRowCheckboxChange" @selection-change="handleRowCheckboxChange"
> >
<el-table-column type="selection" width="55" /> <el-table-column type="selection" width="55" />
<el-table-column label="客户编码" align="center" prop="customerCode" min-width="160px" /> <el-table-column
<el-table-column label="客户名称" align="center" prop="customerName" min-width="160px" /> :label="t('cus.management.fields.customerCode')"
<el-table-column label="区域" align="center" min-width="160px"> align="center"
prop="customerCode"
min-width="160px"
/>
<el-table-column
:label="t('cus.management.fields.customerName')"
align="center"
prop="customerName"
min-width="160px"
/>
<el-table-column :label="t('cus.management.fields.region')" align="center" min-width="160px">
<template #default="scope"> <template #default="scope">
{{ {{
[scope.row.provinceName, scope.row.cityName].filter((v) => !!v).join(' ') || [scope.row.provinceName, scope.row.cityName].filter((v) => !!v).join(' ') ||
@ -92,48 +111,32 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
label="售后负责人" :label="t('cus.management.fields.afterSalesManager')"
align="center" align="center"
prop="afterSalesManager" prop="afterSalesManager"
min-width="140px" min-width="140px"
/> />
<el-table-column label="状态" align="center" prop="status" min-width="100px"> <el-table-column :label="t('common.status')" align="center" prop="status" min-width="100px">
<template #default="scope"> <template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
label="创建时间" :label="t('common.createTime')"
align="center" align="center"
prop="createTime" prop="createTime"
:formatter="dateFormatter" :formatter="dateFormatter"
width="180px" width="180px"
/> />
<el-table-column label="操作" align="center" min-width="120px"> <el-table-column :label="t('table.action')" align="center" min-width="120px">
<template #default="scope"> <template #default="scope">
<el-button
link
type="primary"
@click="openConfig(scope.row)"
v-hasPermi="['cus:management:update']"
>
配置
</el-button>
<el-button
link
type="primary"
@click="handleSyncData(scope.row)"
v-hasPermi="['cus:management:update']"
>
同步数据
</el-button>
<el-button <el-button
link link
type="primary" type="primary"
@click="openForm('update', scope.row.id)" @click="openForm('update', scope.row.id)"
v-hasPermi="['cus:management:update']" v-hasPermi="['cus:management:update']"
> >
编辑 {{ t('action.edit') }}
</el-button> </el-button>
<el-button <el-button
link link
@ -141,7 +144,7 @@
@click="handleDelete(scope.row.id)" @click="handleDelete(scope.row.id)"
v-hasPermi="['cus:management:delete']" v-hasPermi="['cus:management:delete']"
> >
删除 {{ t('action.del') }}
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
@ -157,46 +160,6 @@
<!-- 表单弹窗添加/修改 --> <!-- 表单弹窗添加/修改 -->
<ManagementForm ref="formRef" @success="getList" /> <ManagementForm ref="formRef" @success="getList" />
<Dialog v-model="configDialogVisible" title="数据库配置" width="40%">
<el-form
ref="configFormRef"
v-loading="configLoading"
:model="configFormData"
:rules="configRules"
label-width="100px"
>
<el-form-item label="数据库IP" prop="dbIp">
<el-input v-model="configFormData.dbIp" placeholder="请输入数据库IP" />
</el-form-item>
<el-form-item label="数据库端口" prop="dbPort">
<el-input-number
v-model="configFormData.dbPort"
:min="1"
:max="65535"
controls-position="right"
class="w-full"
placeholder="请输入数据库端口"
/>
</el-form-item>
<el-form-item label="数据库账号" prop="dbUsername">
<el-input v-model="configFormData.dbUsername" placeholder="请输入数据库账号" />
</el-form-item>
<el-form-item label="数据库密码" prop="dbPassword">
<el-input
v-model="configFormData.dbPassword"
placeholder="请输入数据库密码"
show-password
type="password"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleTestConnection" :disabled="configLoading">测试连接</el-button>
<el-button type="primary" @click="submitConfig" :disabled="configLoading"> </el-button>
<el-button @click="configDialogVisible = false"> </el-button>
</template>
</Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -291,83 +254,6 @@ const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: Management[]) => { const handleRowCheckboxChange = (records: Management[]) => {
checkedIds.value = records.map((item) => item.id!) checkedIds.value = records.map((item) => item.id!)
} }
const configDialogVisible = ref(false)
const configLoading = ref(false)
const configFormRef = ref()
const configOriginRow = ref<Management | undefined>(undefined)
const configFormData = reactive({
dbIp: undefined,
dbPort: undefined,
dbUsername: undefined,
dbPassword: undefined
})
const configRules = reactive({
dbIp: [{ required: true, message: '数据库IP不能为空', trigger: 'blur' }],
dbPort: [{ required: true, message: '数据库端口不能为空', trigger: 'blur' }],
dbUsername: [{ required: true, message: '数据库账号不能为空', trigger: 'blur' }]
})
const openConfig = (row: Management) => {
configOriginRow.value = row
configFormData.dbIp = row.dbIp
configFormData.dbPort = row.dbPort
configFormData.dbUsername = row.dbUsername
configFormData.dbPassword = row.dbPassword
configDialogVisible.value = true
nextTick(() => configFormRef.value?.clearValidate())
}
const handleTestConnection = async () => {
try {
await configFormRef.value.validate()
configLoading.value = true
await ManagementApi.testDbConnection({
id: configOriginRow.value?.id,
...configFormData
})
message.success('连接成功')
} catch (e: any) {
const tip = e?.msg || e?.message
if (tip) {
message.error(tip)
}
} finally {
configLoading.value = false
}
}
const submitConfig = async () => {
await configFormRef.value.validate()
if (!configOriginRow.value?.id) return
configLoading.value = true
try {
await ManagementApi.updateManagement({
...(configOriginRow.value as any),
...configFormData,
id: configOriginRow.value.id
})
message.success(t('common.updateSuccess'))
configDialogVisible.value = false
await getList()
} finally {
configLoading.value = false
}
}
const handleSyncData = async (row: Management) => {
try {
await message.confirm('确认同步该客户的数据?')
await ManagementApi.syncData(row.id)
message.success('同步成功')
} catch (e: any) {
const tip = e?.msg || e?.message
if (tip) {
message.error(tip)
}
}
}
/** 导出按钮操作 */ /** 导出按钮操作 */
const handleExport = async () => { const handleExport = async () => {
try { try {
@ -376,7 +262,7 @@ const handleExport = async () => {
// //
exportLoading.value = true exportLoading.value = true
const data = await ManagementApi.exportManagement(queryParams as any) const data = await ManagementApi.exportManagement(queryParams as any)
download.excel(data, '客户管理.xls') download.excel(data, t('cus.management.exportFileName'))
} catch { } catch {
} finally { } finally {
exportLoading.value = false exportLoading.value = false

Loading…
Cancel
Save