diff --git a/src/locales/en.ts b/src/locales/en.ts
index bd4c0b4..2dbfbd0 100644
--- a/src/locales/en.ts
+++ b/src/locales/en.ts
@@ -141,9 +141,9 @@ export default {
qrcode: 'Scan the QR code to log in',
btnRegister: 'Sign up',
SmsSendMsg: 'code has been sent',
- resetPassword: "Reset Password",
- resetPasswordSuccess: "Reset Password Success",
- invalidTenantName:"Invalid Tenant Name"
+ resetPassword: 'Reset Password',
+ resetPasswordSuccess: 'Reset Password Success',
+ invalidTenantName: 'Invalid Tenant Name'
},
captcha: {
verify: 'Verify',
@@ -315,6 +315,33 @@ export default {
dataUpdate: 'Dict Data Eidt',
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',
open: 'Open',
@@ -459,4 +486,4 @@ export default {
btn_zoom_out: 'Zoom out',
preview: 'Preivew'
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts
index 3b6c2e9..080a7c1 100644
--- a/src/locales/zh-CN.ts
+++ b/src/locales/zh-CN.ts
@@ -316,6 +316,33 @@ export default {
dataCreate: '字典数据新增',
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: '弹窗',
open: '打开',
@@ -455,4 +482,4 @@ export default {
preview: '预览'
},
'OAuth 2.0': 'OAuth 2.0' // 避免菜单名是 OAuth 2.0 时,一直 warn 报错
-}
\ No newline at end of file
+}
diff --git a/src/views/cus/management/ManagementForm.vue b/src/views/cus/management/ManagementForm.vue
index 7d97e26..62fa932 100644
--- a/src/views/cus/management/ManagementForm.vue
+++ b/src/views/cus/management/ManagementForm.vue
@@ -4,30 +4,42 @@
ref="formRef"
:model="formData"
:rules="formRules"
- label-width="100px"
- v-loading="formLoading"
+ min-label-width="100px"
+ v-loading="formLoading || geoInitLoading"
+ :element-loading-text="t('cus.management.loading.countries')"
>
- 基本信息
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
-
+
+
- 区域信息
+ {{ t('cus.management.sections.regionInfo') }}
-
-
-
+
+
-
+ :placeholder="`${t('common.selectText')} ${t('cus.management.fields.country')}`"
+ filterable
+ clearable
+ :loading="countryLoading"
+ @change="handleCountryChange"
+ >
+
+
-
-
-
+
+
-
+ clearable
+ :loading="provinceLoading"
+ :disabled="!formData.countryCode"
+ @change="handleProvinceChange"
+ >
+
+
-
-
-
-
-
+
+
+
+
+
-
-
+
+
-
-
+
+
-
-
+
+
- 确 定
- 取 消
+ {{
+ t('common.ok')
+ }}
+ {{ t('common.cancel') }}
@@ -134,12 +166,11 @@
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 { t, locale } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
@@ -147,10 +178,9 @@ const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const countrySelectReady = ref(false)
-type FormModel = Partial & {
- areaIds?: number[]
-}
+type FormModel = Partial
+
+const geoInitLoading = ref(false)
const formData = ref({
id: undefined,
@@ -160,8 +190,7 @@ const formData = ref({
remark: undefined,
afterSalesManager: undefined,
countryCode: 'CN',
- countryName: '中国',
- areaIds: undefined,
+ countryName: undefined,
provinceCode: undefined,
provinceName: undefined,
cityCode: undefined,
@@ -175,162 +204,303 @@ const formData = ref({
dbPassword: undefined
})
const formRules = reactive({
- customerName: [{ required: true, message: '客户名称不能为空', trigger: 'blur' }],
- customerCode: [{ required: true, message: '客户编码不能为空', trigger: 'blur' }],
- status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
+ customerName: [{ required: true, message: t('common.required'), trigger: 'blur' }],
+ customerCode: [{ required: true, message: t('common.required'), trigger: 'blur' }],
+ status: [{ required: true, message: t('common.required'), trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
-const isChina = computed(() => formData.value.countryCode === 'CN')
-
-const countryMetaLoaded = ref(false)
-const countryCodeToName = ref(new Map([['CN', '中国']]))
-const countryNameToCode = ref(new Map([['中国', 'CN']]))
const syncingForm = ref(false)
-type AreaVO = {
- id: number
- name: string
+type GeoNamesCountry = {
code: string
- parentId?: number
- children?: AreaVO[]
+ name: string
+ geonameId: number
}
-const areaTree = ref([])
-const areaCascaderProps = {
- label: 'name',
- value: 'id',
- children: 'children',
- emitPath: true,
- checkStrictly: true
+type GeoNamesPlace = {
+ geonameId: number
+ name: string
}
-const areaIdToNode = ref(new Map())
-const areaIdToParentId = ref(new Map())
-const areaCodeToId = ref(new Map())
-
-const rebuildAreaIndex = (tree: AreaVO[]) => {
- const idToNode = new Map()
- const idToParent = new Map()
- const codeToId = new Map()
- 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)
+
+const countryOptions = ref([])
+const provinceOptions = ref([])
+const cityOptions = ref([])
+
+const countryLoading = ref(false)
+const provinceLoading = ref(false)
+const cityLoading = ref(false)
+
+const provinceLabel = computed(() => {
+ return formData.value.countryCode === 'CN'
+ ? t('cus.management.fields.provinceCN')
+ : t('cus.management.fields.stateProvince')
+})
+
+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) => {
+ 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()
+const childrenCache = new Map()
+
+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 path: number[] = []
- let current: number | undefined = id
- while (current !== undefined) {
- path.push(current)
- current = areaIdToParentId.value.get(current)
+const loadChildrenByGeonameId = async (parentGeonameId: number) => {
+ const lang = getGeoNamesLang()
+ const cacheKey = `${lang}|${parentGeonameId}`
+ if (childrenCache.has(cacheKey)) return childrenCache.get(cacheKey) || []
+ const url = buildGeoNamesUrl('childrenJSON', {
+ 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 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 loadProvincesByCountryCode = async (countryCode?: string) => {
+ if (!countryCode) {
+ provinceOptions.value = []
+ return
+ }
+ const country = countryOptions.value.find((c) => c.code === countryCode)
+ if (!country) {
+ provinceOptions.value = []
+ return
+ }
+ provinceLoading.value = true
+ try {
+ provinceOptions.value = await loadChildrenByGeonameId(country.geonameId)
+ } finally {
+ provinceLoading.value = false
+ }
}
-const handleAreaChange = (value?: number[]) => {
- if (!value?.length) {
- formData.value.provinceCode = undefined
- formData.value.provinceName = undefined
- formData.value.cityCode = undefined
- formData.value.cityName = undefined
+const loadCitiesByProvinceGeonameId = async (provinceGeonameId?: string) => {
+ if (!provinceGeonameId) {
+ cityOptions.value = []
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 id = Number(provinceGeonameId)
+ if (!Number.isFinite(id)) {
+ cityOptions.value = []
+ return
+ }
+ cityLoading.value = true
+ try {
+ cityOptions.value = await loadChildrenByGeonameId(id)
+ } finally {
+ cityLoading.value = false
+ }
}
-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)
+const syncCountryNameFromCode = () => {
+ const code = formData.value.countryCode
+ if (!code) {
+ formData.value.countryName = undefined
+ return
+ }
+ const item = countryOptions.value.find((c) => c.code === code)
+ formData.value.countryName = item?.name || formData.value.countryName || code
}
-watch(
- () => formData.value.countryCode,
- (code) => {
- if (!dialogVisible.value) return
- if (syncingForm.value) return
- void handleCountryChange(code)
+const syncCountryCodeFromName = () => {
+ const name = formData.value.countryName
+ if (!name || formData.value.countryCode) return
+ const item = countryOptions.value.find((c) => c.name === name)
+ if (item) formData.value.countryCode = item.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(undefined)
-const ensureAreaTreeLoaded = async (countryCode?: string) => {
- if (countryCode && countryCode !== 'CN') {
- areaTree.value = []
- rebuildAreaIndex([])
- loadedCountryCode.value = countryCode
- return
+const handleProvinceChange = async (val?: string) => {
+ const code = val || undefined
+ formData.value.provinceCode = code
+ formData.value.cityCode = undefined
+ formData.value.cityName = undefined
+ formData.value.provinceName = syncPlaceNameByCode(code, provinceOptions.value) || undefined
+ 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) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
syncingForm.value = true
- countrySelectReady.value = false
resetForm()
- void nextTick(() => {
- requestAnimationFrame(() => {
- countrySelectReady.value = true
- })
- })
- if (!formData.value.countryCode && formData.value.countryName) {
- formData.value.countryCode = countryNameToCode.value.get(formData.value.countryName)
+ geoInitLoading.value = true
+ try {
+ await loadCountries()
+ } finally {
+ geoInitLoading.value = false
}
- await ensureAreaTreeLoaded(formData.value.countryCode)
+ syncCountryCodeFromName()
+ syncCountryNameFromCode()
+ await loadProvincesByCountryCode(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)
+ await loadCountries()
+ 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 {
formLoading.value = false
}
@@ -374,7 +544,6 @@ const resetForm = () => {
afterSalesManager: undefined,
countryCode: 'CN',
countryName: '中国',
- areaIds: undefined,
provinceCode: undefined,
provinceName: undefined,
cityCode: undefined,
@@ -390,32 +559,3 @@ const resetForm = () => {
formRef.value?.resetFields()
}
-
-
diff --git a/src/views/cus/management/index.vue b/src/views/cus/management/index.vue
index 10b136c..ddb530f 100644
--- a/src/views/cus/management/index.vue
+++ b/src/views/cus/management/index.vue
@@ -6,28 +6,33 @@
:model="queryParams"
ref="queryFormRef"
:inline="true"
- label-width="68px"
+ min-label-width="68px"
>
-
+
-
+
-
-
+
+
- 搜索
- 重置
+
+ {{ t('common.query') }}
+
+
+ {{ t('common.reset') }}
+
- 新增
+ {{ t('action.create') }}
- 导出
+ {{ t('action.export') }}
- 批量删除
+ {{ t('cus.management.actions.batchDelete') }}
@@ -80,9 +89,19 @@
@selection-change="handleRowCheckboxChange"
>
-
-
-
+
+
+
{{
[scope.row.provinceName, scope.row.cityName].filter((v) => !!v).join(' ') ||
@@ -92,48 +111,32 @@
-
+
-
+
-
- 配置
-
-
- 同步数据
-
- 编辑
+ {{ t('action.edit') }}
- 删除
+ {{ t('action.del') }}
@@ -157,46 +160,6 @@
-
-