主包体积太大,相关代码迁移

This commit is contained in:
francis-fh
2026-06-06 19:14:01 +08:00
parent 087c686d81
commit b4ff618477
104 changed files with 7336 additions and 8973 deletions

View File

@@ -0,0 +1,335 @@
# 五级联动地址选择器组件
## 组件简介
`area-cascade-picker` 是一个支持省市区县街道社区的五级联动地址选择组件,适用于需要选择详细地址的场景。
## 功能特点
- ✅ 五级联动选择(省/市/区/街道/社区)
- ✅ 自动级联更新
- ✅ 支持取消和确认操作
- ✅ 底部弹出式交互
- ✅ 支持自定义标题
- ✅ 返回完整的地址信息和各级代码
## 使用方法
### 1. 引入组件
```vue
<template>
<view>
<button @click="openPicker">选择地址</button>
<area-cascade-picker ref="areaPicker"></area-cascade-picker>
</view>
</template>
<script setup>
import { ref } from 'vue'
import AreaCascadePicker from '@/components/area-cascade-picker/area-cascade-picker.vue'
const areaPicker = ref(null)
</script>
```
### 2. 打开选择器
```javascript
const openPicker = () => {
areaPicker.value?.open({
title: '选择地址',
maskClick: true,
success: (addressData) => {
console.log('选择的地址:', addressData)
// 处理选择结果
},
cancel: () => {
console.log('取消选择')
},
change: (addressData) => {
console.log('地址变化:', addressData)
}
})
}
```
## API 说明
### open(config)
打开地址选择器
#### 参数 config
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| title | String | 否 | '选择地址' | 选择器标题 |
| maskClick | Boolean | 否 | false | 是否允许点击遮罩关闭 |
| success | Function | 否 | - | 确认选择的回调函数 |
| cancel | Function | 否 | - | 取消选择的回调函数 |
| change | Function | 否 | - | 选择变化的回调函数 |
| defaultValue | Object | 否 | null | 默认选中的地址(暂未实现) |
| forceRefresh | Boolean | 否 | false | 是否强制刷新数据(忽略缓存) |
#### success 回调参数
```javascript
{
address: "新疆维吾尔自治区/喀什地区/喀什市/学府街道/学府社区居委会",
province: {
code: "650000",
name: "新疆维吾尔自治区"
},
city: {
code: "653100",
name: "喀什地区"
},
district: {
code: "653101",
name: "喀什市"
},
street: {
code: "65310101",
name: "学府街道"
},
community: {
code: "6531010101",
name: "学府社区居委会"
}
}
```
### close()
关闭地址选择器
```javascript
areaPicker.value?.close()
```
### clearCache()
清除地址数据缓存
```javascript
areaPicker.value?.clearCache()
```
## 数据格式
组件使用树形结构的地址数据,格式如下:
```javascript
[
{
code: '650000', // 行政区划代码
name: '新疆维吾尔自治区', // 名称
children: [ // 下级行政区
{
code: '653100',
name: '喀什地区',
children: [
{
code: '653101',
name: '喀什市',
children: [
{
code: '65310101',
name: '学府街道',
children: [
{
code: '6531010101',
name: '学府社区居委会'
}
]
}
]
}
]
}
]
}
]
```
## 完整示例
### 企业注册地址选择
```vue
<template>
<view class="form-item" @click="selectLocation">
<text class="label">企业注册地点</text>
<view class="input-content">
<text :class="{ placeholder: !formData.address }">
{{ formData.address || '请选择注册地点' }}
</text>
<uni-icons type="arrowright" size="16"></uni-icons>
</view>
</view>
<area-cascade-picker ref="areaPicker"></area-cascade-picker>
</template>
<script setup>
import { ref, reactive } from 'vue'
import AreaCascadePicker from '@/components/area-cascade-picker/area-cascade-picker.vue'
const areaPicker = ref(null)
const formData = reactive({
address: '',
provinceCode: '',
provinceName: '',
cityCode: '',
cityName: '',
districtCode: '',
districtName: '',
streetCode: '',
streetName: '',
communityCode: '',
communityName: ''
})
const selectLocation = () => {
areaPicker.value?.open({
title: '选择企业注册地点',
maskClick: true,
success: (addressData) => {
// 保存完整地址
formData.address = addressData.address
// 保存各级信息
formData.provinceCode = addressData.province?.code
formData.provinceName = addressData.province?.name
formData.cityCode = addressData.city?.code
formData.cityName = addressData.city?.name
formData.districtCode = addressData.district?.code
formData.districtName = addressData.district?.name
formData.streetCode = addressData.street?.code
formData.streetName = addressData.street?.name
formData.communityCode = addressData.community?.code
formData.communityName = addressData.community?.name
console.log('已选择地址:', formData)
}
})
}
</script>
```
## 性能优化
### 懒加载方案(已实现)⭐
组件已实现**懒加载**机制大幅优化90M+地址数据的加载性能:
#### 核心优化
1. **首次加载**:只加载省份列表(< 1MB3-5秒完成
2. **按需加载**用户选择省份后再加载该省份的详细数据2-5MB5-10秒
3. **智能缓存**已加载的数据会缓存切换省份时秒开
4. **自动降级**如果服务器不支持分片接口自动从完整数据中提取
#### 性能对比
| 场景 | 优化前 | 优化后懒加载 |
|------|--------|------------------|
| 首次打开选择器 | 加载90M+3-5分钟 | 加载省份列表< 1MB3-5秒 |
| 选择省份 | 无需加载 | 加载该省份数据2-5MB5-10秒 |
| 切换省份 | 无需加载 | 从缓存读取< 1秒 |
#### 使用方式
组件已自动使用懒加载模式无需修改调用代码
```javascript
// 正常使用,自动懒加载
areaPicker.value?.open({
success: (addressData) => {
console.log('选择的地址:', addressData)
}
})
// 强制刷新数据(忽略缓存)
areaPicker.value?.open({
forceRefresh: true, // 强制从服务器重新加载
success: (addressData) => {
console.log('选择的地址:', addressData)
}
})
// 清除缓存
areaPicker.value?.clearCache()
```
### 服务器分片接口(最佳方案)🚀
如果服务器可以提供分片接口性能会进一步提升详见[地址数据懒加载优化方案.md](../../docs/地址数据懒加载优化方案.md)
需要的接口
- 省份列表接口`/address_provinces.json`轻量级< 1MB
- 省份详情接口`/address_province_{code}.json`按需加载每个2-5MB
### 缓存机制
1. **自动缓存**已加载的数据会自动缓存到 IndexedDBH5 uni.storage小程序
2. **缓存有效期**默认7天过期后自动重新加载
3. **离线支持**网络失败时自动使用缓存数据
4. **存储方案**优先使用 IndexedDB自动降级到 uni.storage
## 注意事项
1. **数据来源**当前从远程JSON文件加载生产环境建议接入后端API
2. **数据更新**如需接入后端API修改 `loadAreaData` 方法即可
3. **性能优化**已集成缓存机制首次加载后速度大幅提升
4. **兼容性**支持 H5微信小程序等多端
5. **存储限制**小程序环境有存储限制如遇问题会自动清理旧缓存
## 接入后端API
在组件的 `loadAreaData` 方法中取消注释并配置API
```javascript
async loadAreaData() {
try {
const resp = await uni.request({
url: '/app/common/area/cascade',
method: 'GET'
});
if (resp.statusCode === 200 && resp.data && resp.data.data) {
this.areaData = resp.data.data;
return;
}
} catch (error) {
console.error('加载地区数据失败:', error);
}
// 失败时使用模拟数据
this.areaData = this.getMockData();
}
```
## 更新日志
### v1.2.0 (2025-01-XX)
- 🚀 **懒加载优化**实现按需加载首次加载从几分钟减少到几秒
- 首次只加载省份列表< 1MB3-5秒
- 按需加载省份详情选择省份时才加载
- 智能缓存已加载的数据切换省份秒开
- 支持服务器分片接口最佳性能
- 自动降级方案兼容完整数据
### v1.1.0 (2025-01-XX)
- 🚀 **性能优化**集成智能缓存系统优化90M+地址数据加载
- 支持 IndexedDB uni.storage 双缓存方案
- 支持缓存过期管理和自动清理
- 支持强制刷新数据
- 优化首次加载体验后续加载秒开
### v1.0.0 (2025-10-21)
- 初始版本
- 实现五级联动选择功能
- 支持省市区县街道社区选择
- 提供完整的地址信息返回

View File

@@ -0,0 +1,621 @@
<template>
<uni-popup
ref="popup"
type="bottom"
borderRadius="10px 10px 0 0"
background-color="#FFFFFF"
:mask-click="maskClick"
>
<view class="popup-content">
<view class="popup-header">
<view class="btn-cancel" @click="cancel">取消</view>
<view class="title">{{ title }}</view>
<view class="btn-confirm" @click="confirm">确认</view>
</view>
<view class="popup-list">
<picker-view
indicator-style="height: 84rpx;"
:value="selectedIndex"
@change="bindChange"
class="picker-view"
>
<!-- -->
<picker-view-column>
<view
v-for="(item, index) in provinceList"
:key="item.code"
class="item"
:class="{ 'item-active': selectedIndex[0] === index }"
>
<text>{{ item.name }}</text>
</view>
</picker-view-column>
<!-- -->
<picker-view-column>
<view
v-for="(item, index) in cityList"
:key="item.code"
class="item"
:class="{ 'item-active': selectedIndex[1] === index }"
>
<text>{{ item.name }}</text>
</view>
</picker-view-column>
<!-- 区县 -->
<picker-view-column>
<view
v-for="(item, index) in districtList"
:key="item.code"
class="item"
:class="{ 'item-active': selectedIndex[2] === index }"
>
<text>{{ item.name }}</text>
</view>
</picker-view-column>
<!-- 街道 -->
<picker-view-column>
<view
v-for="(item, index) in streetList"
:key="item.code"
class="item"
:class="{ 'item-active': selectedIndex[3] === index }"
>
<text>{{ item.name }}</text>
</view>
</picker-view-column>
<!-- 社区/居委会 -->
<picker-view-column>
<view
v-for="(item, index) in communityList"
:key="item.code"
class="item"
:class="{ 'item-active': selectedIndex[4] === index }"
>
<text>{{ item.name }}</text>
</view>
</picker-view-column>
</picker-view>
</view>
</view>
</uni-popup>
</template>
<script>
import { createRequest } from '@/utils/request';
export default {
name: 'AreaCascadePicker',
data() {
return {
maskClick: false,
title: '选择地址',
confirmCallback: null,
cancelCallback: null,
changeCallback: null,
selectedIndex: [0, 0, 0, 0, 0],
// 原始数据(懒加载模式下,只存储省份列表)
areaData: [],
// 各级列表
provinceList: [],
cityList: [],
districtList: [],
streetList: [],
communityList: [],
// 当前选中的项
selectedProvince: null,
selectedCity: null,
selectedDistrict: null,
selectedStreet: null,
selectedCommunity: null,
// 加载状态
isLoading: false,
};
},
methods: {
async open(newConfig = {}) {
const {
title,
success,
cancel,
change,
maskClick = false,
defaultValue = null,
forceRefresh = false, // 是否强制刷新数据
} = newConfig;
this.reset();
if (title) this.title = title;
if (typeof success === 'function') this.confirmCallback = success;
if (typeof cancel === 'function') this.cancelCallback = cancel;
if (typeof change === 'function') this.changeCallback = change;
this.maskClick = maskClick;
// 加载地区数据
this.isLoading = true;
try {
// 先显示弹窗,避免长时间等待
this.$nextTick(() => {
this.$refs.popup.open();
});
// 加载省份数据
await this.loadAreaData(forceRefresh);
// 只有当provinceList有数据时才初始化后续列表
if (this.areaData && this.areaData.length > 0) {
await this.initLists();
console.log('地址选择器初始化完成');
} else {
console.warn('没有加载到省份数据');
// 即使没有数据,也保持弹窗打开,让用户可以关闭它
}
} catch (error) {
console.error('打开地址选择器失败:', error);
// 注意由于loadAreaData已经处理了错误这里不应该会被触发
// 但保留作为额外的安全措施
} finally {
this.isLoading = false;
}
},
async loadAreaData(forceRefresh = false) {
try {
console.log('正在加载省份列表...');
const resp = await createRequest('/cms/dict/sysarea/list', { parentCode: '' }, 'GET', false);
console.log('省份列表接口响应:', resp);
// 处理正确的数据格式
if (resp && resp.code === 200 && resp.data) {
// 数据在data字段中
this.areaData = resp.data.map(item => ({
code: item.code,
name: item.name
}));
console.log('省份列表加载成功:', this.areaData);
if (this.areaData.length === 0) {
console.warn('省份列表为空');
}
} else {
console.error('获取省份列表失败:', resp);
throw new Error(`获取省份列表失败: ${resp?.msg || '未知错误'}`);
}
} catch (error) {
console.error('加载省份列表失败:', error);
// 不抛出错误,避免阻止页面打开
// 而是使用默认空数据,让用户可以继续操作
this.areaData = [];
// 显示更友好的错误提示
uni.showToast({
title: error.message || '加载地址数据失败,请检查网络连接',
icon: 'none',
duration: 3000
});
}
},
async initLists() {
// 初始化省列表
this.provinceList = this.areaData || [];
if (this.provinceList.length > 0) {
this.selectedProvince = this.provinceList[0];
// 懒加载:首次选择第一个省份时,加载其详情
await this.updateCityList();
// 如果有城市数据,初始化第一个城市的区县数据
if (this.cityList.length > 0) {
this.selectedCity = this.cityList[0];
await this.updateDistrictList();
// 如果有区县数据,初始化第一个区县的街道数据
if (this.districtList.length > 0) {
this.selectedDistrict = this.districtList[0];
await this.updateStreetList();
// 如果有街道数据,初始化第一个街道的社区数据
if (this.streetList.length > 0) {
this.selectedStreet = this.streetList[0];
await this.updateCommunityList();
}
}
}
// 更新选中索引
this.selectedIndex = [0, 0, 0, 0, 0];
}
},
async updateCityList() {
if (!this.selectedProvince) {
this.cityList = [];
this.districtList = [];
this.streetList = [];
this.communityList = [];
return;
}
try {
console.log(`正在加载城市列表,父级编码: ${this.selectedProvince.code}`);
// 使用createRequest工具调用接口
const resp = await createRequest('/cms/dict/sysarea/list', { parentCode: this.selectedProvince.code }, 'GET', false);
console.log('城市列表接口响应:', resp);
// 处理正确的数据格式
let cityData = [];
if (resp && resp.code === 200 && resp.data) {
cityData = resp.data;
this.cityList = cityData.map(item => ({
code: item.code,
name: item.name
}));
console.log('城市列表加载成功:', this.cityList);
} else {
console.error('获取城市列表失败:', resp);
this.cityList = [];
}
} catch (error) {
console.error('加载城市列表失败:', error);
this.cityList = [];
}
this.selectedIndex[1] = 0;
if (this.cityList.length > 0) {
this.selectedCity = this.cityList[0];
await this.updateDistrictList();
} else {
this.districtList = [];
this.streetList = [];
this.communityList = [];
}
},
async updateDistrictList() {
if (!this.selectedCity) {
this.districtList = [];
this.streetList = [];
this.communityList = [];
return;
}
try {
console.log(`正在加载区县列表,父级编码: ${this.selectedCity.code}`);
// 使用createRequest工具调用接口
const resp = await createRequest('/cms/dict/sysarea/list', { parentCode: this.selectedCity.code }, 'GET', false);
console.log('区县列表接口响应:', resp);
// 处理正确的数据格式
let districtData = [];
if (resp && resp.code === 200 && resp.data) {
districtData = resp.data;
this.districtList = districtData.map(item => ({
code: item.code,
name: item.name
}));
console.log('区县列表加载成功:', this.districtList);
} else {
console.error('获取区县列表失败:', resp);
this.districtList = [];
}
} catch (error) {
console.error('加载区县列表失败:', error);
this.districtList = [];
}
this.selectedIndex[2] = 0;
if (this.districtList.length > 0) {
this.selectedDistrict = this.districtList[0];
await this.updateStreetList();
} else {
this.streetList = [];
this.communityList = [];
}
},
async updateStreetList() {
if (!this.selectedDistrict) {
this.streetList = [];
this.communityList = [];
return;
}
try {
console.log(`正在加载街道列表,父级编码: ${this.selectedDistrict.code}`);
// 使用createRequest工具调用接口
const resp = await createRequest('/cms/dict/sysarea/list', { parentCode: this.selectedDistrict.code }, 'GET', false);
console.log('街道列表接口响应:', resp);
// 处理正确的数据格式
let streetData = [];
if (resp && resp.code === 200 && resp.data) {
streetData = resp.data;
this.streetList = streetData.map(item => ({
code: item.code,
name: item.name
}));
console.log('街道列表加载成功:', this.streetList);
} else {
console.error('获取街道列表失败:', resp);
this.streetList = [];
}
} catch (error) {
console.error('加载街道列表失败:', error);
this.streetList = [];
}
this.selectedIndex[3] = 0;
if (this.streetList.length > 0) {
this.selectedStreet = this.streetList[0];
await this.updateCommunityList();
} else {
this.communityList = [];
}
},
async updateCommunityList() {
if (!this.selectedStreet) {
this.communityList = [];
return;
}
try {
console.log(`正在加载社区列表,父级编码: ${this.selectedStreet.code}`);
// 使用createRequest工具调用接口
const resp = await createRequest('/cms/dict/sysarea/list', { parentCode: this.selectedStreet.code }, 'GET', false);
console.log('社区列表接口响应:', resp);
// 处理正确的数据格式
let communityData = [];
if (resp && resp.code === 200 && resp.data) {
communityData = resp.data;
this.communityList = communityData.map(item => ({
code: item.code,
name: item.name
}));
console.log('社区列表加载成功:', this.communityList);
} else {
console.error('获取社区列表失败:', resp);
this.communityList = [];
}
} catch (error) {
console.error('加载社区列表失败:', error);
this.communityList = [];
}
this.selectedIndex[4] = 0;
if (this.communityList.length > 0) {
this.selectedCommunity = this.communityList[0];
}
},
async bindChange(e) {
const newIndex = e.detail.value;
// 检查哪一列发生了变化
for (let i = 0; i < 5; i++) {
if (newIndex[i] !== this.selectedIndex[i]) {
this.selectedIndex[i] = newIndex[i];
// 根据变化的列更新后续列
if (i === 0) {
// 省变化 - 需要加载新省份的城市
this.selectedProvince = this.provinceList[newIndex[0]];
await this.updateCityList();
} else if (i === 1) {
// 市变化
this.selectedCity = this.cityList[newIndex[1]];
await this.updateDistrictList();
} else if (i === 2) {
// 区县变化
this.selectedDistrict = this.districtList[newIndex[2]];
await this.updateStreetList();
} else if (i === 3) {
// 街道变化
this.selectedStreet = this.streetList[newIndex[3]];
await this.updateCommunityList();
} else if (i === 4) {
// 社区变化
this.selectedCommunity = this.communityList[newIndex[4]];
}
break;
}
}
if (this.changeCallback) {
this.changeCallback(this.getSelectedAddress());
}
},
getSelectedAddress() {
const parts = [];
if (this.selectedProvince) parts.push(this.selectedProvince.name);
if (this.selectedCity) parts.push(this.selectedCity.name);
if (this.selectedDistrict) parts.push(this.selectedDistrict.name);
if (this.selectedStreet) parts.push(this.selectedStreet.name);
if (this.selectedCommunity) parts.push(this.selectedCommunity.name);
return {
address: parts.join('/'),
province: this.selectedProvince,
city: this.selectedCity,
district: this.selectedDistrict,
street: this.selectedStreet,
community: this.selectedCommunity,
};
},
close() {
this.$refs.popup.close();
},
cancel() {
this.clickCallback(this.cancelCallback);
},
confirm() {
this.clickCallback(this.confirmCallback);
},
async clickCallback(callback) {
if (typeof callback !== 'function') {
this.$refs.popup.close();
return;
}
try {
const result = await callback(this.getSelectedAddress());
if (result !== false) {
this.$refs.popup.close();
}
} catch (error) {
console.error('callback 执行出错:', error);
}
},
/**
* 重置所有状态(内部使用)
*/
reset() {
this.maskClick = false;
this.confirmCallback = null;
this.cancelCallback = null;
this.changeCallback = null;
this.selectedIndex = [0, 0, 0, 0, 0];
this.selectedProvince = null;
this.selectedCity = null;
this.selectedDistrict = null;
this.selectedStreet = null;
this.selectedCommunity = null;
this.provinceList = [];
this.cityList = [];
this.districtList = [];
this.streetList = [];
this.communityList = [];
this.areaData = [];
},
/**
* 清除地址数据缓存(供外部调用)
*/
async clearCache() {
try {
// 清除内存缓存
this.reset();
uni.showToast({
title: '缓存已清除',
icon: 'success',
duration: 2000
});
} catch (error) {
console.error('清除缓存失败:', error);
uni.showToast({
title: '清除缓存失败',
icon: 'none',
duration: 2000
});
}
}
},
};
</script>
<style lang="scss" scoped>
.popup-content {
color: #000000;
height: 60vh;
}
.popup-list {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: space-evenly;
flex: 1;
overflow: hidden;
.picker-view {
width: 100%;
height: calc(60vh - 100rpx);
margin-top: 20rpx;
.uni-picker-view-mask {
background: rgba(0, 0, 0, 0);
}
.item {
line-height: 84rpx;
height: 84rpx;
text-align: center;
font-weight: 400;
font-size: 28rpx;
color: #cccccc;
padding: 0 4rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-active {
color: #333333;
font-weight: 500;
}
.uni-picker-view-indicator:after {
border-color: #e3e3e3;
}
.uni-picker-view-indicator:before {
border-color: #e3e3e3;
}
}
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 40rpx 40rpx 10rpx 40rpx;
.title {
font-weight: 500;
font-size: 36rpx;
color: #333333;
text-align: center;
}
.btn-cancel {
font-weight: 400;
font-size: 32rpx;
color: #666d7f;
line-height: 38rpx;
}
.btn-confirm {
font-weight: 400;
font-size: 32rpx;
color: #256bfa;
}
}
</style>