Files
xgy-oa/klp-ui/src/components/ChinaAreaSelect/index.vue
砂糖 6bd525813f feat(客户管理): 添加中国区域选择组件并替换地址输入框
- 新增 ChinaAreaSelect 组件,支持省市区三级选择与详细地址输入
- 在客户编辑和客户列表页面替换原有的地址输入框
- 组件支持标准地址与自定义地址组合格式存储
- 自动处理直辖市和港澳特别行政区的特殊区域结构
2025-12-22 16:36:19 +08:00

256 lines
7.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>