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

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

@ -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',

@ -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: '打开',

@ -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')"
>
<el-divider content-position="left">基本信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="客户编码" prop="customerCode">
<el-input v-model="formData.customerCode" placeholder="请输入客户编码" />
<el-form-item :label="t('cus.management.fields.customerCode')" prop="customerCode">
<el-input
v-model="formData.customerCode"
:placeholder="`${t('common.inputText')} ${t('cus.management.fields.customerCode')}`"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户名称" prop="customerName">
<el-input v-model="formData.customerName" placeholder="请输入客户名称" />
<el-form-item :label="t('cus.management.fields.customerName')" prop="customerName">
<el-input
v-model="formData.customerName"
:placeholder="`${t('common.inputText')} ${t('cus.management.fields.customerName')}`"
/>
</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
: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-col>
<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
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
@ -40,93 +52,113 @@
</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 :label="t('form.remark')" prop="remark">
<el-input
v-model="formData.remark"
:placeholder="`${t('common.inputText')} ${t('form.remark')}`"
type="textarea"
/>
</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-col :span="12">
<el-form-item label="国家" prop="countryCode">
<country-select
v-if="countrySelectReady"
<el-col :span="8">
<el-form-item :label="t('cus.management.fields.country')" prop="countryCode">
<el-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-input
v-else
class="w-full"
:model-value="formData.countryName || formData.countryCode"
placeholder="加载中..."
disabled
/>
:placeholder="`${t('common.selectText')} ${t('cus.management.fields.country')}`"
filterable
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-col>
<el-col :span="12">
<el-form-item
:label="isChina ? '省市' : '省/州'"
:prop="isChina ? 'areaIds' : 'provinceName'"
>
<el-cascader
v-if="isChina"
v-model="formData.areaIds"
<el-col :span="8">
<el-form-item :label="provinceLabel" prop="provinceCode">
<el-select
v-model="formData.provinceCode"
class="w-full"
:options="areaTree"
:props="areaCascaderProps"
clearable
:placeholder="provincePlaceholder"
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"
/>
clearable
:loading="provinceLoading"
:disabled="!formData.countryCode"
@change="handleProvinceChange"
>
<el-option
v-for="item in provinceOptions"
:key="item.geonameId"
:label="item.name"
:value="String(item.geonameId)"
/>
</el-select>
</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-col :span="8">
<el-form-item :label="t('cus.management.fields.city')" prop="cityCode">
<el-select
v-model="formData.cityCode"
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-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 :label="t('cus.management.fields.address')" prop="address">
<el-input
v-model="formData.address"
:placeholder="`${t('common.inputText')} ${t('cus.management.fields.address')}`"
/>
</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 :label="t('cus.management.fields.longitude')" prop="longitude">
<el-input
v-model="formData.longitude"
:placeholder="`${t('common.inputText')} ${t('cus.management.fields.longitude')}`"
/>
</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 :label="t('cus.management.fields.latitude')" prop="latitude">
<el-input
v-model="formData.latitude"
:placeholder="`${t('common.inputText')} ${t('cus.management.fields.latitude')}`"
/>
</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>
<el-button @click="submitForm" type="primary" :disabled="formLoading">{{
t('common.ok')
}}</el-button>
<el-button @click="dialogVisible = false">{{ t('common.cancel') }}</el-button>
</template>
</Dialog>
</template>
@ -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) // 12
const formType = ref('') // create - update -
const countrySelectReady = ref(false)
type FormModel = Partial<Management> & {
areaIds?: number[]
}
type FormModel = Partial<Management>
const geoInitLoading = ref(false)
const formData = ref<FormModel>({
id: undefined,
@ -160,8 +190,7 @@ const formData = ref<FormModel>({
remark: undefined,
afterSalesManager: undefined,
countryCode: 'CN',
countryName: '中国',
areaIds: undefined,
countryName: undefined,
provinceCode: undefined,
provinceName: undefined,
cityCode: undefined,
@ -175,162 +204,303 @@ const formData = ref<FormModel>({
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<string, string>([['CN', '中国']]))
const countryNameToCode = ref(new Map<string, string>([['中国', '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<AreaVO[]>([])
const areaCascaderProps = {
label: 'name',
value: 'id',
children: 'children',
emitPath: true,
checkStrictly: true
type GeoNamesPlace = {
geonameId: number
name: string
}
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)
const countryOptions = ref<GeoNamesCountry[]>([])
const provinceOptions = ref<GeoNamesPlace[]>([])
const cityOptions = ref<GeoNamesPlace[]>([])
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<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 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<string | undefined>(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()
}
</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"
ref="queryFormRef"
: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
v-model="queryParams.customerCode"
placeholder="请输入客户编码"
:placeholder="`${t('common.inputText')}${t('cus.management.fields.customerCode')}`"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="客户名称" prop="customerName">
<el-form-item :label="t('cus.management.fields.customerName')" prop="customerName">
<el-input
v-model="queryParams.customerName"
placeholder="请输入客户名称"
:placeholder="`${t('common.inputText')}${t('cus.management.fields.customerName')}`"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" clearable placeholder="请选择状态" class="!w-240px">
<el-form-item :label="t('common.status')" prop="status">
<el-select
v-model="queryParams.status"
clearable
:placeholder="`${t('common.selectText')}${t('common.status')}`"
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
@ -37,15 +42,19 @@
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button @click="handleQuery">
<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
type="primary"
plain
@click="openForm('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
type="success"
@ -54,7 +63,7 @@
:loading="exportLoading"
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
type="danger"
@ -63,7 +72,7 @@
@click="handleDeleteBatch"
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-form-item>
</el-form>
@ -80,9 +89,19 @@
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="客户编码" align="center" prop="customerCode" min-width="160px" />
<el-table-column label="客户名称" align="center" prop="customerName" min-width="160px" />
<el-table-column label="区域" align="center" min-width="160px">
<el-table-column
:label="t('cus.management.fields.customerCode')"
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">
{{
[scope.row.provinceName, scope.row.cityName].filter((v) => !!v).join(' ') ||
@ -92,48 +111,32 @@
</template>
</el-table-column>
<el-table-column
label="售后负责人"
:label="t('cus.management.fields.afterSalesManager')"
align="center"
prop="afterSalesManager"
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">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="创建时间"
:label="t('common.createTime')"
align="center"
prop="createTime"
:formatter="dateFormatter"
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">
<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
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['cus:management:update']"
>
编辑
{{ t('action.edit') }}
</el-button>
<el-button
link
@ -141,7 +144,7 @@
@click="handleDelete(scope.row.id)"
v-hasPermi="['cus:management:delete']"
>
删除
{{ t('action.del') }}
</el-button>
</template>
</el-table-column>
@ -157,46 +160,6 @@
<!-- 表单弹窗添加/修改 -->
<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>
<script setup lang="ts">
@ -291,83 +254,6 @@ const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: Management[]) => {
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 () => {
try {
@ -376,7 +262,7 @@ const handleExport = async () => {
//
exportLoading.value = true
const data = await ManagementApi.exportManagement(queryParams as any)
download.excel(data, '客户管理.xls')
download.excel(data, t('cus.management.exportFileName'))
} catch {
} finally {
exportLoading.value = false

Loading…
Cancel
Save