|
|
|
@ -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"
|
|
|
|
|
|
|
|
:region-name="true"
|
|
|
|
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
|
|
|
|
</el-select>
|
|
|
|
</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) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
|
|
|
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
|
|
|
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,130 +204,257 @@ 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,
|
|
|
|
const countryOptions = ref<GeoNamesCountry[]>([])
|
|
|
|
checkStrictly: true
|
|
|
|
const provinceOptions = ref<GeoNamesPlace[]>([])
|
|
|
|
}
|
|
|
|
const cityOptions = ref<GeoNamesPlace[]>([])
|
|
|
|
const areaIdToNode = ref(new Map<number, AreaVO>())
|
|
|
|
|
|
|
|
const areaIdToParentId = ref(new Map<number, number | undefined>())
|
|
|
|
const countryLoading = ref(false)
|
|
|
|
const areaCodeToId = ref(new Map<string, number>())
|
|
|
|
const provinceLoading = ref(false)
|
|
|
|
|
|
|
|
const cityLoading = ref(false)
|
|
|
|
const rebuildAreaIndex = (tree: AreaVO[]) => {
|
|
|
|
|
|
|
|
const idToNode = new Map<number, AreaVO>()
|
|
|
|
const provinceLabel = computed(() => {
|
|
|
|
const idToParent = new Map<number, number | undefined>()
|
|
|
|
return formData.value.countryCode === 'CN'
|
|
|
|
const codeToId = new Map<string, number>()
|
|
|
|
? t('cus.management.fields.provinceCN')
|
|
|
|
const walk = (nodes: AreaVO[], parentId?: number) => {
|
|
|
|
: t('cus.management.fields.stateProvince')
|
|
|
|
nodes.forEach((n) => {
|
|
|
|
})
|
|
|
|
idToNode.set(n.id, n)
|
|
|
|
|
|
|
|
idToParent.set(n.id, parentId)
|
|
|
|
const provincePlaceholder = computed(() => {
|
|
|
|
if (n.code) {
|
|
|
|
return formData.value.countryCode === 'CN'
|
|
|
|
codeToId.set(n.code, n.id)
|
|
|
|
? `${t('common.selectText')} ${t('cus.management.fields.provinceCN')}`
|
|
|
|
}
|
|
|
|
: `${t('common.selectText')} ${t('cus.management.fields.stateProvince')}`
|
|
|
|
if (n.children?.length) walk(n.children, n.id)
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 loadCitiesByProvinceGeonameId = async (provinceGeonameId?: string) => {
|
|
|
|
|
|
|
|
if (!provinceGeonameId) {
|
|
|
|
|
|
|
|
cityOptions.value = []
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const id = Number(provinceGeonameId)
|
|
|
|
|
|
|
|
if (!Number.isFinite(id)) {
|
|
|
|
|
|
|
|
cityOptions.value = []
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
cityLoading.value = true
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
cityOptions.value = await loadChildrenByGeonameId(id)
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
|
|
cityLoading.value = false
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
walk(tree)
|
|
|
|
|
|
|
|
areaIdToNode.value = idToNode
|
|
|
|
const syncCountryNameFromCode = () => {
|
|
|
|
areaIdToParentId.value = idToParent
|
|
|
|
const code = formData.value.countryCode
|
|
|
|
areaCodeToId.value = codeToId
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const buildPathById = (id: number) => {
|
|
|
|
const syncCountryCodeFromName = () => {
|
|
|
|
const path: number[] = []
|
|
|
|
const name = formData.value.countryName
|
|
|
|
let current: number | undefined = id
|
|
|
|
if (!name || formData.value.countryCode) return
|
|
|
|
while (current !== undefined) {
|
|
|
|
const item = countryOptions.value.find((c) => c.name === name)
|
|
|
|
path.push(current)
|
|
|
|
if (item) formData.value.countryCode = item.code
|
|
|
|
current = areaIdToParentId.value.get(current)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return path.reverse()
|
|
|
|
|
|
|
|
|
|
|
|
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 syncAreaIdsFromCodes = () => {
|
|
|
|
const syncPlaceNameByCode = (code: string | undefined, list: GeoNamesPlace[]) => {
|
|
|
|
const cityId = formData.value.cityCode
|
|
|
|
if (!code) return undefined
|
|
|
|
? areaCodeToId.value.get(formData.value.cityCode)
|
|
|
|
const item = list.find((c) => String(c.geonameId) === String(code))
|
|
|
|
: undefined
|
|
|
|
return item?.name
|
|
|
|
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[]) => {
|
|
|
|
const handleCountryChange = async (val?: string) => {
|
|
|
|
if (!value?.length) {
|
|
|
|
const code = val || undefined
|
|
|
|
|
|
|
|
formData.value.countryCode = code
|
|
|
|
formData.value.provinceCode = undefined
|
|
|
|
formData.value.provinceCode = undefined
|
|
|
|
formData.value.provinceName = undefined
|
|
|
|
formData.value.provinceName = undefined
|
|
|
|
|
|
|
|
syncCountryNameFromCode()
|
|
|
|
|
|
|
|
provinceOptions.value = []
|
|
|
|
|
|
|
|
cityOptions.value = []
|
|
|
|
formData.value.cityCode = undefined
|
|
|
|
formData.value.cityCode = undefined
|
|
|
|
formData.value.cityName = undefined
|
|
|
|
formData.value.cityName = undefined
|
|
|
|
return
|
|
|
|
await loadProvincesByCountryCode(code)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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
|
|
|
|
const loadCityDetail = async (geonameId: string) => {
|
|
|
|
formData.value.provinceName = provinceNode?.name
|
|
|
|
const lang = getGeoNamesLang()
|
|
|
|
formData.value.cityCode = cityNode?.code
|
|
|
|
const url = buildGeoNamesUrl('getJSON', {
|
|
|
|
formData.value.cityName = cityNode?.name
|
|
|
|
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 handleCountryChange = async (countryCode?: string) => {
|
|
|
|
const handleProvinceChange = async (val?: string) => {
|
|
|
|
const code = countryCode || undefined
|
|
|
|
const code = val || undefined
|
|
|
|
formData.value.countryName = code ? countryCodeToName.value.get(code) || code : undefined
|
|
|
|
formData.value.provinceCode = code
|
|
|
|
formData.value.areaIds = undefined
|
|
|
|
formData.value.cityCode = undefined
|
|
|
|
handleAreaChange(undefined)
|
|
|
|
formData.value.cityName = undefined
|
|
|
|
if (code) await ensureAreaTreeLoaded(code)
|
|
|
|
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) {}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
watch(
|
|
|
|
() => formData.value.countryCode,
|
|
|
|
() => locale.value,
|
|
|
|
(code) => {
|
|
|
|
() => {
|
|
|
|
if (!dialogVisible.value) return
|
|
|
|
if (!dialogVisible.value) return
|
|
|
|
if (syncingForm.value) return
|
|
|
|
void loadCountries().then(async () => {
|
|
|
|
void handleCountryChange(code)
|
|
|
|
syncCountryNameFromCode()
|
|
|
|
}
|
|
|
|
await loadProvincesByCountryCode(formData.value.countryCode)
|
|
|
|
|
|
|
|
if (!formData.value.provinceCode && formData.value.provinceName) {
|
|
|
|
|
|
|
|
formData.value.provinceCode = syncPlaceCodeByName(
|
|
|
|
|
|
|
|
formData.value.provinceName,
|
|
|
|
|
|
|
|
provinceOptions.value
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
formData.value.provinceName =
|
|
|
|
const tree = (await getAreaTree(countryCode)) || []
|
|
|
|
syncPlaceNameByCode(formData.value.provinceCode, provinceOptions.value) ||
|
|
|
|
areaTree.value = tree
|
|
|
|
formData.value.provinceName
|
|
|
|
rebuildAreaIndex(tree)
|
|
|
|
await loadCitiesByProvinceGeonameId(formData.value.provinceCode)
|
|
|
|
loadedCountryCode.value = countryCode
|
|
|
|
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) => {
|
|
|
|
@ -306,31 +462,45 @@ const open = async (type: string, id?: number) => {
|
|
|
|
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()
|
|
|
|
formData.value.countryName = formData.value.countryCode
|
|
|
|
await loadProvincesByCountryCode(formData.value.countryCode)
|
|
|
|
? countryCodeToName.value.get(formData.value.countryCode) || formData.value.countryName
|
|
|
|
if (!formData.value.provinceCode && formData.value.provinceName) {
|
|
|
|
: formData.value.countryName
|
|
|
|
formData.value.provinceCode = syncPlaceCodeByName(
|
|
|
|
await ensureAreaTreeLoaded(formData.value.countryCode)
|
|
|
|
formData.value.provinceName,
|
|
|
|
syncAreaIdsFromCodes()
|
|
|
|
provinceOptions.value
|
|
|
|
handleAreaChange(formData.value.areaIds as any)
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
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) {}
|
|
|
|
|
|
|
|
}
|
|
|
|
} 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>
|
|
|
|
|
|
|
|
|