feat(客户管理): 添加中国区域选择组件并替换地址输入框
- 新增 ChinaAreaSelect 组件,支持省市区三级选择与详细地址输入 - 在客户编辑和客户列表页面替换原有的地址输入框 - 组件支持标准地址与自定义地址组合格式存储 - 自动处理直辖市和港澳特别行政区的特殊区域结构
This commit is contained in:
3453
klp-ui/src/components/ChinaAreaSelect/data.js
Normal file
3453
klp-ui/src/components/ChinaAreaSelect/data.js
Normal file
File diff suppressed because it is too large
Load Diff
256
klp-ui/src/components/ChinaAreaSelect/index.vue
Normal file
256
klp-ui/src/components/ChinaAreaSelect/index.vue
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
<template>
|
||||||
|
<div class="area-select-container">
|
||||||
|
<!-- 区域选择器 -->
|
||||||
|
<el-cascader
|
||||||
|
v-model="areaValue"
|
||||||
|
:options="areaTree"
|
||||||
|
:props="cascaderProps"
|
||||||
|
filterable
|
||||||
|
clearable
|
||||||
|
placeholder="请选择中国区域"
|
||||||
|
collapse-tags
|
||||||
|
@change="handleAreaChange"
|
||||||
|
v-if="isAreaTreeReady"
|
||||||
|
style="width: 100%; margin-bottom: 8px;"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 详细地址输入框 -->
|
||||||
|
<el-input
|
||||||
|
v-model="detailAddress"
|
||||||
|
placeholder="请输入详细地址(如:XX街道XX小区XX号楼)"
|
||||||
|
@input="handleDetailChange"
|
||||||
|
style="width: 100%;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import areaData from './data.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ChinaAreaSelectWithDetail',
|
||||||
|
props: {
|
||||||
|
// 外部传入的组合值:格式为「[标准地址](自定义地址)」
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 配置绑定值类型:code(行政区划代码)/ name(区域名称)
|
||||||
|
keyType: {
|
||||||
|
type: String,
|
||||||
|
default: 'code',
|
||||||
|
validator: val => ['code', 'name'].includes(val)
|
||||||
|
},
|
||||||
|
// 是否只选择到市级(省/市),默认选到区级(省/市/区)
|
||||||
|
onlyCityLevel: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
areaTree: [],
|
||||||
|
// 特殊区域代码(直辖市+港澳特别行政区)
|
||||||
|
specialAreaCodes: ['110000', '120000', '310000', '500000', '810000', '820000'],
|
||||||
|
isAreaTreeReady: false,
|
||||||
|
// 区域选择器的内部值(数组格式,存储code)
|
||||||
|
areaValue: [],
|
||||||
|
// 详细地址输入值
|
||||||
|
detailAddress: '',
|
||||||
|
cascaderProps: {
|
||||||
|
value: 'code',
|
||||||
|
label: 'name',
|
||||||
|
children: 'children',
|
||||||
|
checkStrictly: false,
|
||||||
|
expandTrigger: 'click',
|
||||||
|
multiple: false,
|
||||||
|
leaf: node => !node.children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// 监听外部传入的value,自动拆分并回显
|
||||||
|
value: {
|
||||||
|
immediate: true,
|
||||||
|
handler(val) {
|
||||||
|
this.parseCombineValue(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
// 组合值:对外输出「[标准地址](自定义地址)」格式
|
||||||
|
combineValue() {
|
||||||
|
// 1. 转换区域值为标准字符串(code/name格式)
|
||||||
|
let standardAddress = ''
|
||||||
|
if (this.areaValue.length) {
|
||||||
|
standardAddress = this.keyType === 'name'
|
||||||
|
? this.convertCodeToName(this.areaValue).join('/')
|
||||||
|
: this.areaValue.join('/')
|
||||||
|
}
|
||||||
|
// 2. 组合最终格式:[标准地址](自定义地址)
|
||||||
|
if (!standardAddress && !this.detailAddress) return ''
|
||||||
|
if (standardAddress && !this.detailAddress) return `[${standardAddress}]()`
|
||||||
|
if (!standardAddress && this.detailAddress) return `[()](${this.detailAddress})`
|
||||||
|
return `[${standardAddress}](${this.detailAddress})`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.buildAreaTree()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* 正则解析组合值:提取[标准地址]和(自定义地址)部分
|
||||||
|
* @param {String} val - 外部传入的「[标准地址](自定义地址)」格式值
|
||||||
|
*/
|
||||||
|
parseCombineValue(val) {
|
||||||
|
if (!val) {
|
||||||
|
this.areaValue = []
|
||||||
|
this.detailAddress = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 核心正则:匹配「[xxx](yyy)」格式,分组提取xxx和yyy(无匹配则为空)
|
||||||
|
const reg = /\[([^\]]*)\]\(([^)]*)\)/
|
||||||
|
const matchResult = val.match(reg)
|
||||||
|
|
||||||
|
// 提取标准地址(第一个分组)和自定义地址(第二个分组)
|
||||||
|
const standardAddress = matchResult?.[1] || ''
|
||||||
|
const customAddress = matchResult?.[2] || ''
|
||||||
|
|
||||||
|
// 1. 处理自定义地址:直接赋值
|
||||||
|
this.detailAddress = customAddress.trim()
|
||||||
|
|
||||||
|
// 2. 处理标准地址:转换为区域选择器的code数组
|
||||||
|
if (standardAddress) {
|
||||||
|
const standardArr = standardAddress.split('/').filter(Boolean)
|
||||||
|
this.areaValue = this.keyType === 'name'
|
||||||
|
? this.convertNameToCode(standardArr) // name转code
|
||||||
|
: standardArr.filter(code => !!areaData[code]) // code直接过滤无效值
|
||||||
|
} else {
|
||||||
|
this.areaValue = []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建树形结构:兼容直辖市+港澳特别行政区的两级结构
|
||||||
|
*/
|
||||||
|
buildAreaTree() {
|
||||||
|
const tree = {};
|
||||||
|
|
||||||
|
// 第一步:初始化所有省级节点(非叶子节点,带children)
|
||||||
|
Object.keys(areaData).forEach(code => {
|
||||||
|
if (code.endsWith('0000') && !tree[code]) {
|
||||||
|
tree[code] = {
|
||||||
|
code: code,
|
||||||
|
name: areaData[code],
|
||||||
|
children: [] // 省级节点是父节点,保留children
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 第二步:遍历所有非省级节点,构建层级
|
||||||
|
Object.entries(areaData).forEach(([code, name]) => {
|
||||||
|
if (code.length !== 6) return;
|
||||||
|
const provinceCode = code.substring(0, 2) + '0000';
|
||||||
|
|
||||||
|
// 跳过省级节点(已初始化)
|
||||||
|
if (code === provinceCode) return;
|
||||||
|
|
||||||
|
// 判断是否为特殊区域(直辖市/港澳)
|
||||||
|
const isSpecialArea = this.specialAreaCodes.includes(provinceCode);
|
||||||
|
|
||||||
|
// 1. 特殊区域处理:区级直接挂载到省级,且区级是叶子节点(无children)
|
||||||
|
if (isSpecialArea) {
|
||||||
|
if (tree[provinceCode]) {
|
||||||
|
tree[provinceCode].children.push({ code, name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2. 普通省份处理:省→市→区三级
|
||||||
|
else {
|
||||||
|
const cityCode = code.substring(0, 4) + '00';
|
||||||
|
const provinceNode = tree[provinceCode];
|
||||||
|
|
||||||
|
if (!provinceNode) return;
|
||||||
|
|
||||||
|
// 2.1 初始化市级节点(非叶子节点,带children)
|
||||||
|
if (!provinceNode.children.some(item => item.code === cityCode)) {
|
||||||
|
provinceNode.children.push({
|
||||||
|
code: cityCode,
|
||||||
|
name: areaData[cityCode] || '',
|
||||||
|
children: [] // 市级节点是父节点,保留children
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.2 挂载区级节点(叶子节点,无children)
|
||||||
|
if (code !== cityCode) {
|
||||||
|
const cityNode = provinceNode.children.find(item => item.code === cityCode);
|
||||||
|
if (cityNode) {
|
||||||
|
cityNode.children.push({ code, name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 第三步:格式化树形结构,过滤空节点
|
||||||
|
this.areaTree = Object.values(tree)
|
||||||
|
.filter(province => province.name && province.children.length > 0)
|
||||||
|
.map(province => {
|
||||||
|
province.children = province.children.filter(city => city.name);
|
||||||
|
return province;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 标记数据就绪,渲染组件
|
||||||
|
this.isAreaTreeReady = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* code转name(容错处理)
|
||||||
|
*/
|
||||||
|
convertCodeToName(codeArr) {
|
||||||
|
if (!Array.isArray(codeArr) || !codeArr.length) return [];
|
||||||
|
return codeArr.map(code => areaData[code] || '').filter(Boolean);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* name转code(容错处理)
|
||||||
|
*/
|
||||||
|
convertNameToCode(nameArr) {
|
||||||
|
if (!Array.isArray(nameArr) || !nameArr.length) return [];
|
||||||
|
const nameToCodeMap = Object.fromEntries(
|
||||||
|
Object.entries(areaData).map(([code, name]) => [name, code])
|
||||||
|
);
|
||||||
|
return nameArr.map(name => nameToCodeMap[name] || '').filter(Boolean);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 区域选择器变化回调
|
||||||
|
*/
|
||||||
|
handleAreaChange() {
|
||||||
|
this.$emit('input', this.combineValue)
|
||||||
|
this.$emit('change', this.combineValue, {
|
||||||
|
standard: this.keyType === 'name' ? this.convertCodeToName(this.areaValue).join('/') : this.areaValue.join('/'),
|
||||||
|
custom: this.detailAddress
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 详细地址变化回调
|
||||||
|
*/
|
||||||
|
handleDetailChange() {
|
||||||
|
this.$emit('input', this.combineValue)
|
||||||
|
this.$emit('change', this.combineValue, {
|
||||||
|
standard: this.keyType === 'name' ? this.convertCodeToName(this.areaValue).join('/') : this.areaValue.join('/'),
|
||||||
|
custom: this.detailAddress
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.area-select-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -86,11 +86,16 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="24" v-hasPermi="['crm:customer:address']">
|
<el-col :span="24" v-hasPermi="['crm:customer:address']">
|
||||||
<el-form-item label="客户地址" prop="address">
|
<el-form-item label="客户地址" prop="address">
|
||||||
<el-input
|
<!-- <el-input
|
||||||
type="textarea"
|
type="textarea"
|
||||||
v-model="customer.address"
|
v-model="customer.address"
|
||||||
placeholder="请输入客户地址"
|
placeholder="请输入客户地址"
|
||||||
@input="handleInputChange"
|
@input="handleInputChange"
|
||||||
|
/> -->
|
||||||
|
<ChinaAreaSelect
|
||||||
|
v-model="customer.address"
|
||||||
|
placeholder="请选择客户地址"
|
||||||
|
@change="handleInputChange"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -110,11 +115,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import JSONTableInput from './JSONTableInput.vue'
|
import JSONTableInput from './JSONTableInput.vue'
|
||||||
|
import ChinaAreaSelect from '@/components/ChinaAreaSelect/index.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CustomerEdit',
|
name: 'CustomerEdit',
|
||||||
components: {
|
components: {
|
||||||
JSONTableInput
|
JSONTableInput,
|
||||||
|
ChinaAreaSelect
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
// 客户信息对象(双向绑定)
|
// 客户信息对象(双向绑定)
|
||||||
|
|||||||
@@ -135,7 +135,10 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="客户地址" prop="address">
|
<el-form-item label="客户地址" prop="address">
|
||||||
<el-input v-model="form.address" placeholder="请输入客户地址" />
|
<ChinaAreaSelect
|
||||||
|
v-model="form.address"
|
||||||
|
placeholder="请选择客户地址"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="备注" prop="remark">
|
<el-form-item label="备注" prop="remark">
|
||||||
<el-input v-model="form.remark" placeholder="请输入备注" />
|
<el-input v-model="form.remark" placeholder="请输入备注" />
|
||||||
@@ -161,6 +164,7 @@ import JSONTableInput from '../components/JSONTableInput.vue'
|
|||||||
import CustomerDetail from '../components/CustomerInfo.vue'
|
import CustomerDetail from '../components/CustomerInfo.vue'
|
||||||
import CustomerEdit from '../components/CustomerEdit.vue'
|
import CustomerEdit from '../components/CustomerEdit.vue'
|
||||||
import CustomerOrder from '../components/CustomerOrder.vue'
|
import CustomerOrder from '../components/CustomerOrder.vue'
|
||||||
|
import ChinaAreaSelect from '@/components/ChinaAreaSelect/index.vue'
|
||||||
|
|
||||||
import { listCustomer, addCustomer, updateCustomer, delCustomer } from '@/api/crm/customer'
|
import { listCustomer, addCustomer, updateCustomer, delCustomer } from '@/api/crm/customer'
|
||||||
|
|
||||||
@@ -171,7 +175,8 @@ export default {
|
|||||||
JSONTableInput,
|
JSONTableInput,
|
||||||
CustomerDetail,
|
CustomerDetail,
|
||||||
CustomerEdit,
|
CustomerEdit,
|
||||||
CustomerOrder
|
CustomerOrder,
|
||||||
|
ChinaAreaSelect
|
||||||
},
|
},
|
||||||
dicts: ['customer_industry', 'customer_level'],
|
dicts: ['customer_industry', 'customer_level'],
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
Reference in New Issue
Block a user