优化个人中心页面
This commit is contained in:
352
utils/addressDataLoader.js
Normal file
352
utils/addressDataLoader.js
Normal file
@@ -0,0 +1,352 @@
|
||||
/**
|
||||
* 地址数据加载器 - 支持缓存和版本控制
|
||||
* 优化90M+地址JSON文件的加载性能
|
||||
*/
|
||||
import IndexedDBHelper from '@/common/IndexedDBHelper.js';
|
||||
|
||||
class AddressDataLoader {
|
||||
constructor() {
|
||||
this.dbHelper = null;
|
||||
this.dbName = 'AddressDataDB';
|
||||
this.storeName = 'addressData';
|
||||
this.cacheKey = 'address_data_cache';
|
||||
this.versionKey = 'address_data_version';
|
||||
this.cacheExpireDays = 7; // 缓存有效期7天
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据库
|
||||
*/
|
||||
async init() {
|
||||
if (this.isInitialized && this.dbHelper) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.dbHelper = new IndexedDBHelper(this.dbName, 1);
|
||||
await this.dbHelper.openDB([{
|
||||
name: this.storeName,
|
||||
keyPath: 'key',
|
||||
indexes: [
|
||||
{ name: 'version', key: 'version', unique: false },
|
||||
{ name: 'updateTime', key: 'updateTime', unique: false }
|
||||
]
|
||||
}]);
|
||||
this.isInitialized = true;
|
||||
console.log('✅ 地址数据加载器初始化成功');
|
||||
} catch (error) {
|
||||
console.error('❌ 地址数据加载器初始化失败:', error);
|
||||
// 降级到 uni.storage
|
||||
this.isInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存版本号
|
||||
*/
|
||||
async getCacheVersion() {
|
||||
try {
|
||||
const cached = await this.dbHelper?.get(this.storeName, this.versionKey);
|
||||
return cached?.version || null;
|
||||
} catch (e) {
|
||||
// 降级方案:从 uni.storage 读取
|
||||
return uni.getStorageSync(this.versionKey) || null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存版本号
|
||||
*/
|
||||
async saveVersion(version) {
|
||||
try {
|
||||
if (this.dbHelper) {
|
||||
const versionData = {
|
||||
key: this.versionKey,
|
||||
version: version,
|
||||
updateTime: Date.now()
|
||||
};
|
||||
// 先尝试获取,如果不存在则添加,存在则更新
|
||||
try {
|
||||
const existing = await this.dbHelper.get(this.storeName, this.versionKey);
|
||||
if (existing) {
|
||||
await this.dbHelper.update(this.storeName, versionData);
|
||||
} else {
|
||||
await this.dbHelper.add(this.storeName, versionData);
|
||||
}
|
||||
} catch (e) {
|
||||
// 如果获取失败,尝试直接添加
|
||||
await this.dbHelper.add(this.storeName, versionData);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 降级方案:保存到 uni.storage
|
||||
uni.setStorageSync(this.versionKey, version);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存获取数据
|
||||
*/
|
||||
async getCachedData() {
|
||||
try {
|
||||
if (this.dbHelper) {
|
||||
const cached = await this.dbHelper.get(this.storeName, this.cacheKey);
|
||||
if (cached && cached.data) {
|
||||
// 检查缓存是否过期
|
||||
const updateTime = cached.updateTime || 0;
|
||||
const expireTime = this.cacheExpireDays * 24 * 60 * 60 * 1000;
|
||||
if (Date.now() - updateTime < expireTime) {
|
||||
console.log('✅ 从 IndexedDB 缓存加载地址数据');
|
||||
return cached.data;
|
||||
} else {
|
||||
console.log('⚠️ 缓存已过期,需要重新加载');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('从 IndexedDB 读取缓存失败,尝试 uni.storage:', e);
|
||||
}
|
||||
|
||||
// 降级方案:从 uni.storage 读取
|
||||
try {
|
||||
const cached = uni.getStorageSync(this.cacheKey);
|
||||
if (cached && cached.data) {
|
||||
const updateTime = cached.updateTime || 0;
|
||||
const expireTime = this.cacheExpireDays * 24 * 60 * 60 * 1000;
|
||||
if (Date.now() - updateTime < expireTime) {
|
||||
console.log('✅ 从 uni.storage 缓存加载地址数据');
|
||||
return cached.data;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('从 uni.storage 读取缓存失败:', e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存数据到缓存
|
||||
*/
|
||||
async saveToCache(data) {
|
||||
const cacheData = {
|
||||
key: this.cacheKey,
|
||||
data: data,
|
||||
updateTime: Date.now()
|
||||
};
|
||||
|
||||
try {
|
||||
if (this.dbHelper) {
|
||||
// 先尝试获取,如果不存在则添加,存在则更新
|
||||
try {
|
||||
const existing = await this.dbHelper.get(this.storeName, this.cacheKey);
|
||||
if (existing) {
|
||||
await this.dbHelper.update(this.storeName, cacheData);
|
||||
} else {
|
||||
await this.dbHelper.add(this.storeName, cacheData);
|
||||
}
|
||||
console.log('✅ 地址数据已保存到 IndexedDB');
|
||||
} catch (e) {
|
||||
// 如果获取失败,尝试直接添加
|
||||
await this.dbHelper.add(this.storeName, cacheData);
|
||||
console.log('✅ 地址数据已保存到 IndexedDB');
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('保存到 IndexedDB 失败,降级到 uni.storage:', e);
|
||||
}
|
||||
|
||||
// 降级方案:保存到 uni.storage
|
||||
try {
|
||||
uni.setStorageSync(this.cacheKey, cacheData);
|
||||
console.log('✅ 地址数据已保存到 uni.storage');
|
||||
} catch (e) {
|
||||
console.error('❌ 保存缓存失败,可能超出存储限制:', e);
|
||||
// 如果存储空间不足,尝试清理旧数据
|
||||
if (e.errMsg?.includes('exceed')) {
|
||||
await this.clearOldCache();
|
||||
// 重试保存
|
||||
try {
|
||||
uni.setStorageSync(this.cacheKey, cacheData);
|
||||
} catch (e2) {
|
||||
console.error('❌ 重试保存仍然失败:', e2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期缓存
|
||||
*/
|
||||
async clearOldCache() {
|
||||
try {
|
||||
if (this.dbHelper) {
|
||||
await this.dbHelper.delete(this.storeName, this.cacheKey);
|
||||
}
|
||||
uni.removeStorageSync(this.cacheKey);
|
||||
console.log('✅ 已清理旧缓存');
|
||||
} catch (e) {
|
||||
console.warn('清理缓存失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从服务器获取数据版本号(如果服务器支持)
|
||||
*/
|
||||
async fetchRemoteVersion() {
|
||||
try {
|
||||
// 如果服务器提供版本接口,可以在这里实现
|
||||
// const res = await uni.request({
|
||||
// url: 'http://124.243.245.42/ks_cms/address_version.json',
|
||||
// method: 'GET'
|
||||
// });
|
||||
// return res.data?.version || null;
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从服务器加载地址数据
|
||||
*/
|
||||
async fetchRemoteData(url, onProgress) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('📥 开始从服务器加载地址数据...');
|
||||
const startTime = Date.now();
|
||||
|
||||
uni.request({
|
||||
url: url,
|
||||
method: 'GET',
|
||||
timeout: 300000, // 5分钟超时
|
||||
success: (res) => {
|
||||
const endTime = Date.now();
|
||||
const duration = ((endTime - startTime) / 1000).toFixed(2);
|
||||
console.log(`✅ 地址数据加载完成,耗时 ${duration} 秒`);
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
// 如果返回的是字符串,尝试解析JSON
|
||||
let data = res.data;
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
reject(new Error('JSON解析失败: ' + e.message));
|
||||
return;
|
||||
}
|
||||
}
|
||||
resolve(data);
|
||||
} else {
|
||||
reject(new Error(`请求失败,状态码: ${res.statusCode}`));
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('❌ 地址数据加载失败:', err);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
// 注意:uni.request 不支持进度回调,如果需要进度提示,可以考虑:
|
||||
// 1. 使用流式请求(如果服务器支持)
|
||||
// 2. 显示固定加载提示
|
||||
if (onProgress) {
|
||||
onProgress({ loaded: 0, total: 0, percent: 0 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载地址数据(带缓存)
|
||||
* @param {string} url - 地址数据URL
|
||||
* @param {boolean} forceRefresh - 是否强制刷新
|
||||
* @param {Function} onProgress - 进度回调
|
||||
*/
|
||||
async loadAddressData(url = 'http://124.243.245.42/ks_cms/address.json', forceRefresh = false, onProgress = null) {
|
||||
// 初始化数据库
|
||||
await this.init();
|
||||
|
||||
// 如果不是强制刷新,先尝试从缓存加载
|
||||
if (!forceRefresh) {
|
||||
const cachedData = await this.getCachedData();
|
||||
if (cachedData) {
|
||||
return cachedData;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查版本(如果服务器支持版本控制)
|
||||
const remoteVersion = await this.fetchRemoteVersion();
|
||||
if (remoteVersion) {
|
||||
const cachedVersion = await this.getCacheVersion();
|
||||
if (cachedVersion === remoteVersion && !forceRefresh) {
|
||||
const cachedData = await this.getCachedData();
|
||||
if (cachedData) {
|
||||
console.log('✅ 使用缓存数据(版本匹配)');
|
||||
return cachedData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 显示加载提示
|
||||
uni.showLoading({
|
||||
title: '正在加载地址数据...',
|
||||
mask: true
|
||||
});
|
||||
|
||||
try {
|
||||
// 从服务器加载数据
|
||||
const data = await this.fetchRemoteData(url, onProgress);
|
||||
|
||||
// 保存到缓存
|
||||
await this.saveToCache(data);
|
||||
|
||||
// 保存版本号
|
||||
if (remoteVersion) {
|
||||
await this.saveVersion(remoteVersion);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('❌ 加载地址数据失败:', error);
|
||||
|
||||
// 如果加载失败,尝试使用缓存数据(即使已过期)
|
||||
if (!forceRefresh) {
|
||||
try {
|
||||
const cachedData = await this.getCachedData();
|
||||
if (cachedData) {
|
||||
console.log('⚠️ 使用过期缓存数据');
|
||||
uni.showToast({
|
||||
title: '使用缓存数据',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
return cachedData;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('获取缓存数据失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有缓存
|
||||
*/
|
||||
async clearCache() {
|
||||
await this.init();
|
||||
await this.clearOldCache();
|
||||
await this.saveVersion(null);
|
||||
console.log('✅ 已清除所有地址数据缓存');
|
||||
}
|
||||
}
|
||||
|
||||
// 单例模式
|
||||
const addressDataLoader = new AddressDataLoader();
|
||||
|
||||
export default addressDataLoader;
|
||||
|
||||
688
utils/addressDataLoaderLazy.js
Normal file
688
utils/addressDataLoaderLazy.js
Normal file
@@ -0,0 +1,688 @@
|
||||
/**
|
||||
* 地址数据懒加载器 - 按需加载,大幅减少首次加载时间
|
||||
* 优化方案:
|
||||
* 1. 首次只加载省份列表(数据量很小,< 1MB)
|
||||
* 2. 选择省份后,按需加载该省份的市数据
|
||||
* 3. 逐级懒加载,避免一次性加载90M+数据
|
||||
*/
|
||||
import IndexedDBHelper from '@/common/IndexedDBHelper.js';
|
||||
|
||||
class AddressDataLoaderLazy {
|
||||
constructor() {
|
||||
this.dbHelper = null;
|
||||
this.dbName = 'AddressDataLazyDB';
|
||||
this.storeName = 'addressDataLazy';
|
||||
this.isInitialized = false;
|
||||
|
||||
// 缓存配置
|
||||
this.cacheExpireDays = 7;
|
||||
|
||||
// 数据源配置
|
||||
this.baseUrl = 'http://124.243.245.42/ks_cms';
|
||||
this.fullDataUrl = `${this.baseUrl}/address.json`; // 完整地址数据
|
||||
this.provinceListUrl = `${this.baseUrl}/address_provinces.json`; // 省份列表(轻量级,可选)
|
||||
this.provinceDetailUrl = `${this.baseUrl}/address_province_{code}.json`; // 省份详情(按需加载,可选)
|
||||
|
||||
// 是否启用分片接口(如果服务器提供了分片接口,设置为true)
|
||||
this.useSplitApi = false; // 默认false,从完整数据中提取
|
||||
|
||||
// 内存缓存(避免重复请求)
|
||||
this.memoryCache = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据库
|
||||
*/
|
||||
async init() {
|
||||
if (this.isInitialized && this.dbHelper) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.dbHelper = new IndexedDBHelper(this.dbName, 1);
|
||||
await this.dbHelper.openDB([{
|
||||
name: this.storeName,
|
||||
keyPath: 'key',
|
||||
indexes: [
|
||||
{ name: 'type', key: 'type', unique: false },
|
||||
{ name: 'updateTime', key: 'updateTime', unique: false }
|
||||
]
|
||||
}]);
|
||||
this.isInitialized = true;
|
||||
console.log('✅ 地址数据懒加载器初始化成功');
|
||||
} catch (error) {
|
||||
console.error('❌ 地址数据懒加载器初始化失败:', error);
|
||||
this.isInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存获取数据
|
||||
*/
|
||||
async getCachedData(key) {
|
||||
// 先检查内存缓存
|
||||
if (this.memoryCache.has(key)) {
|
||||
const cached = this.memoryCache.get(key);
|
||||
if (cached.expireTime > Date.now()) {
|
||||
return cached.data;
|
||||
} else {
|
||||
this.memoryCache.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 IndexedDB 缓存
|
||||
try {
|
||||
if (this.dbHelper) {
|
||||
const cached = await this.dbHelper.get(this.storeName, key);
|
||||
if (cached && cached.data) {
|
||||
const updateTime = cached.updateTime || 0;
|
||||
const expireTime = this.cacheExpireDays * 24 * 60 * 60 * 1000;
|
||||
if (Date.now() - updateTime < expireTime) {
|
||||
// 同时更新内存缓存
|
||||
this.memoryCache.set(key, {
|
||||
data: cached.data,
|
||||
expireTime: Date.now() + expireTime
|
||||
});
|
||||
return cached.data;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('从 IndexedDB 读取缓存失败:', e);
|
||||
}
|
||||
|
||||
// 降级方案:从 uni.storage 读取
|
||||
try {
|
||||
const cached = uni.getStorageSync(key);
|
||||
if (cached && cached.data) {
|
||||
const updateTime = cached.updateTime || 0;
|
||||
const expireTime = this.cacheExpireDays * 24 * 60 * 60 * 1000;
|
||||
if (Date.now() - updateTime < expireTime) {
|
||||
this.memoryCache.set(key, {
|
||||
data: cached.data,
|
||||
expireTime: Date.now() + expireTime
|
||||
});
|
||||
return cached.data;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('从 uni.storage 读取缓存失败:', e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存数据到缓存
|
||||
*/
|
||||
async saveToCache(key, data, type = 'province') {
|
||||
const cacheData = {
|
||||
key: key,
|
||||
type: type,
|
||||
data: data,
|
||||
updateTime: Date.now()
|
||||
};
|
||||
|
||||
// 更新内存缓存(1小时有效期)
|
||||
this.memoryCache.set(key, {
|
||||
data: data,
|
||||
expireTime: Date.now() + 60 * 60 * 1000
|
||||
});
|
||||
|
||||
try {
|
||||
if (this.dbHelper) {
|
||||
try {
|
||||
const existing = await this.dbHelper.get(this.storeName, key);
|
||||
if (existing) {
|
||||
await this.dbHelper.update(this.storeName, cacheData);
|
||||
} else {
|
||||
await this.dbHelper.add(this.storeName, cacheData);
|
||||
}
|
||||
} catch (e) {
|
||||
await this.dbHelper.add(this.storeName, cacheData);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('保存到 IndexedDB 失败,降级到 uni.storage:', e);
|
||||
}
|
||||
|
||||
// 降级方案:保存到 uni.storage
|
||||
try {
|
||||
uni.setStorageSync(key, cacheData);
|
||||
} catch (e) {
|
||||
if (e.errMsg?.includes('exceed')) {
|
||||
console.warn('存储空间不足,清理部分缓存');
|
||||
await this.clearOldCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从服务器加载数据
|
||||
*/
|
||||
async fetchData(url, cacheKey = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`📥 开始加载: ${url}`);
|
||||
const startTime = Date.now();
|
||||
|
||||
uni.request({
|
||||
url: url,
|
||||
method: 'GET',
|
||||
timeout: 60000, // 60秒超时
|
||||
success: (res) => {
|
||||
const endTime = Date.now();
|
||||
const duration = ((endTime - startTime) / 1000).toFixed(2);
|
||||
console.log(`✅ 数据加载完成,耗时 ${duration} 秒`);
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
let data = res.data;
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
reject(new Error('JSON解析失败: ' + e.message));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果提供了缓存key,自动保存
|
||||
if (cacheKey) {
|
||||
this.saveToCache(cacheKey, data).catch(e => {
|
||||
console.warn('保存缓存失败:', e);
|
||||
});
|
||||
}
|
||||
|
||||
resolve(data);
|
||||
} else {
|
||||
reject(new Error(`请求失败,状态码: ${res.statusCode}`));
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('❌ 数据加载失败:', err);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载省份列表(轻量级,只包含省份基本信息,不包含children)
|
||||
* 这是首次加载,数据量很小(< 1MB)
|
||||
*/
|
||||
async loadProvinceList(forceRefresh = false) {
|
||||
await this.init();
|
||||
|
||||
const cacheKey = 'address_provinces_list';
|
||||
|
||||
// 如果不是强制刷新,先尝试从缓存加载
|
||||
if (!forceRefresh) {
|
||||
const cached = await this.getCachedData(cacheKey);
|
||||
if (cached) {
|
||||
console.log('✅ 从缓存加载省份列表');
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
uni.showLoading({
|
||||
title: '加载省份列表...',
|
||||
mask: true
|
||||
});
|
||||
|
||||
try {
|
||||
let data = null;
|
||||
|
||||
// 方案1:如果启用了分片接口,尝试从分片接口加载
|
||||
if (this.useSplitApi) {
|
||||
try {
|
||||
data = await this.fetchData(this.provinceListUrl, cacheKey);
|
||||
if (data && Array.isArray(data) && data.length > 0) {
|
||||
console.log('✅ 从分片接口加载省份列表');
|
||||
return data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ 分片接口不可用,降级到完整数据提取:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 方案2:从完整数据中提取省份列表(默认方案)
|
||||
// 如果完整数据已缓存,提取会很快;如果未缓存,需要加载完整数据
|
||||
data = await this.loadProvinceListFromFullData(forceRefresh);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('❌ 加载省份列表失败:', error);
|
||||
|
||||
// 如果加载失败,尝试使用缓存(即使已过期)
|
||||
if (!forceRefresh) {
|
||||
const cached = await this.getCachedData(cacheKey);
|
||||
if (cached) {
|
||||
console.log('⚠️ 使用过期缓存数据');
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从完整数据中提取省份列表(默认方案)
|
||||
* 优化策略:
|
||||
* 1. 优先从缓存获取已提取的省份列表(最快)
|
||||
* 2. 如果未缓存,尝试从缓存的完整数据中提取(快速)
|
||||
* 3. 如果完整数据也未缓存,使用分段加载策略:
|
||||
* - 先尝试只加载前2MB数据来提取省份列表(快速)
|
||||
* - 同时在后台加载完整数据(不阻塞用户)
|
||||
*/
|
||||
async loadProvinceListFromFullData(forceRefresh = false) {
|
||||
const cacheKey = 'address_full_data';
|
||||
const provinceListCacheKey = 'address_provinces_extracted';
|
||||
|
||||
// 先尝试从缓存获取已提取的省份列表
|
||||
if (!forceRefresh) {
|
||||
const cached = await this.getCachedData(provinceListCacheKey);
|
||||
if (cached) {
|
||||
console.log('✅ 从缓存加载已提取的省份列表');
|
||||
// 如果完整数据未缓存,在后台预加载
|
||||
this.preloadFullDataInBackground();
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试从缓存获取完整数据(如果已缓存,提取会很快)
|
||||
let fullData = await this.getCachedData(cacheKey);
|
||||
|
||||
if (!fullData || forceRefresh) {
|
||||
// 如果完整数据未缓存,使用分段加载策略
|
||||
console.log('📥 完整数据未缓存,使用分段加载策略...');
|
||||
|
||||
// 方案1:尝试分段加载(只加载前2MB来提取省份列表)
|
||||
try {
|
||||
const provinceList = await this.loadProvinceListFromPartialData();
|
||||
if (provinceList && provinceList.length > 0) {
|
||||
console.log('✅ 从部分数据提取省份列表成功');
|
||||
// 缓存提取的省份列表
|
||||
await this.saveToCache(provinceListCacheKey, provinceList, 'province_list');
|
||||
// 在后台预加载完整数据
|
||||
this.preloadFullDataInBackground();
|
||||
return provinceList;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ 分段加载失败,降级到完整加载:', error.message);
|
||||
}
|
||||
|
||||
// 方案2:如果分段加载失败,加载完整数据
|
||||
console.log('📥 开始加载完整数据...');
|
||||
console.log('💡 提示:首次加载需要一些时间,加载后会缓存,后续使用会很快');
|
||||
|
||||
uni.showLoading({
|
||||
title: '加载地址数据...',
|
||||
mask: true
|
||||
});
|
||||
|
||||
try {
|
||||
fullData = await this.fetchData(this.fullDataUrl, cacheKey);
|
||||
console.log('✅ 完整数据加载完成,已缓存');
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
} else {
|
||||
console.log('✅ 从缓存提取省份列表(快速)');
|
||||
}
|
||||
|
||||
// 提取省份列表(只保留基本信息,移除children)
|
||||
let provinceList = [];
|
||||
if (Array.isArray(fullData)) {
|
||||
provinceList = fullData.map(province => ({
|
||||
code: province.code,
|
||||
name: province.name,
|
||||
// 不包含children,减少数据量
|
||||
_hasChildren: !!province.children && province.children.length > 0
|
||||
}));
|
||||
|
||||
// 缓存提取的省份列表
|
||||
await this.saveToCache(provinceListCacheKey, provinceList, 'province_list');
|
||||
}
|
||||
|
||||
return provinceList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试从部分数据中提取省份列表(只加载前2MB)
|
||||
* 这样可以快速显示省份列表,而不需要等待90M数据加载完成
|
||||
*/
|
||||
async loadProvinceListFromPartialData() {
|
||||
// 尝试使用Range请求只加载前2MB数据
|
||||
const maxBytes = 2 * 1024 * 1024; // 2MB
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('📥 尝试分段加载(前2MB)...');
|
||||
|
||||
// 在H5环境可以使用fetch + Range请求
|
||||
// #ifdef H5
|
||||
if (typeof fetch !== 'undefined') {
|
||||
fetch(this.fullDataUrl, {
|
||||
headers: {
|
||||
'Range': `bytes=0-${maxBytes - 1}`
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status === 206 || response.status === 200) {
|
||||
return response.text();
|
||||
} else {
|
||||
throw new Error(`Range请求失败,状态码: ${response.status}`);
|
||||
}
|
||||
})
|
||||
.then(text => {
|
||||
// 尝试解析部分JSON
|
||||
const provinceList = this.extractProvincesFromPartialJson(text);
|
||||
if (provinceList && provinceList.length > 0) {
|
||||
console.log(`✅ 从部分数据提取到 ${provinceList.length} 个省份`);
|
||||
resolve(provinceList);
|
||||
} else {
|
||||
reject(new Error('无法从部分数据中提取省份列表'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
} else {
|
||||
reject(new Error('当前环境不支持Range请求'));
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
// 小程序环境不支持Range请求,直接拒绝
|
||||
reject(new Error('小程序环境不支持Range请求,将使用完整加载'));
|
||||
// #endif
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从部分JSON字符串中提取省份列表
|
||||
* 由于JSON可能不完整,需要智能解析
|
||||
*/
|
||||
extractProvincesFromPartialJson(partialJson) {
|
||||
try {
|
||||
const provinces = [];
|
||||
const seenCodes = new Set();
|
||||
|
||||
// 方法1:使用正则表达式提取所有省份(code和name)
|
||||
// 匹配模式:{"code":"31","name":"上海市",...} 或 "code":"31","name":"上海市"
|
||||
const patterns = [
|
||||
// 标准格式:{"code":"31","name":"上海市"}
|
||||
/\{\s*"code"\s*:\s*"([^"]+)"\s*,\s*"name"\s*:\s*"([^"]+)"[^}]*\}/g,
|
||||
// 简化格式:"code":"31","name":"上海市"
|
||||
/"code"\s*:\s*"([^"]+)"\s*,\s*"name"\s*:\s*"([^"]+)"/g
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
let match;
|
||||
while ((match = pattern.exec(partialJson)) !== null) {
|
||||
const code = match[1];
|
||||
const name = match[2];
|
||||
|
||||
// 省份code通常是2位数字(如"31"),或者6位数字(如"310000")
|
||||
// 根据实际数据结构,code可能是"31"格式
|
||||
if (code && name && !seenCodes.has(code)) {
|
||||
// 判断是否为省份code(2位或6位数字)
|
||||
if ((code.length === 2 && /^\d{2}$/.test(code)) ||
|
||||
(code.length === 6 && /^\d{6}$/.test(code))) {
|
||||
seenCodes.add(code);
|
||||
provinces.push({
|
||||
code: code,
|
||||
name: name,
|
||||
_hasChildren: true // 假设都有下级
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 方法2:如果正则提取失败,尝试解析JSON(可能不完整)
|
||||
if (provinces.length === 0) {
|
||||
try {
|
||||
// 尝试修复不完整的JSON
|
||||
let jsonStr = partialJson.trim();
|
||||
|
||||
// 如果以[开头,尝试找到所有完整的省份对象
|
||||
if (jsonStr.startsWith('[')) {
|
||||
// 查找所有省份对象的开始位置
|
||||
let pos = 1; // 跳过开头的[
|
||||
while (pos < jsonStr.length) {
|
||||
// 找到下一个{的位置
|
||||
const objStart = jsonStr.indexOf('{', pos);
|
||||
if (objStart === -1) break;
|
||||
|
||||
// 尝试找到这个对象的结束位置(简单的匹配)
|
||||
let braceCount = 0;
|
||||
let objEnd = objStart;
|
||||
for (let i = objStart; i < jsonStr.length; i++) {
|
||||
if (jsonStr[i] === '{') braceCount++;
|
||||
if (jsonStr[i] === '}') braceCount--;
|
||||
if (braceCount === 0) {
|
||||
objEnd = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (objEnd > objStart) {
|
||||
const objStr = jsonStr.substring(objStart, objEnd);
|
||||
try {
|
||||
const obj = JSON.parse(objStr);
|
||||
if (obj.code && obj.name && !seenCodes.has(obj.code)) {
|
||||
seenCodes.add(obj.code);
|
||||
provinces.push({
|
||||
code: obj.code,
|
||||
name: obj.name,
|
||||
_hasChildren: true
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// 解析失败,继续下一个
|
||||
}
|
||||
}
|
||||
|
||||
pos = objEnd;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 解析失败,忽略
|
||||
}
|
||||
}
|
||||
|
||||
// 去重并排序
|
||||
const uniqueProvinces = Array.from(new Map(provinces.map(p => [p.code, p])).values());
|
||||
|
||||
return uniqueProvinces.length > 0 ? uniqueProvinces : null;
|
||||
} catch (error) {
|
||||
console.warn('从部分JSON提取省份列表失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在后台预加载完整数据(不阻塞用户操作)
|
||||
*/
|
||||
async preloadFullDataInBackground() {
|
||||
const cacheKey = 'address_full_data';
|
||||
|
||||
// 检查是否已缓存
|
||||
const cached = await this.getCachedData(cacheKey);
|
||||
if (cached) {
|
||||
console.log('✅ 完整数据已缓存,无需预加载');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔄 开始在后台预加载完整数据...');
|
||||
|
||||
// 使用setTimeout让出主线程,避免阻塞
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await this.fetchData(this.fullDataUrl, cacheKey);
|
||||
console.log('✅ 后台预加载完成,完整数据已缓存');
|
||||
} catch (error) {
|
||||
console.warn('⚠️ 后台预加载失败:', error);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载省份详情(包含该省份的所有下级数据)
|
||||
* 按需加载,只有用户选择该省份时才加载
|
||||
*/
|
||||
async loadProvinceDetail(provinceCode, forceRefresh = false) {
|
||||
await this.init();
|
||||
|
||||
const cacheKey = `address_province_${provinceCode}`;
|
||||
|
||||
// 如果不是强制刷新,先尝试从缓存加载
|
||||
if (!forceRefresh) {
|
||||
const cached = await this.getCachedData(cacheKey);
|
||||
if (cached) {
|
||||
console.log(`✅ 从缓存加载省份详情: ${provinceCode}`);
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
uni.showLoading({
|
||||
title: '加载城市数据...',
|
||||
mask: true
|
||||
});
|
||||
|
||||
try {
|
||||
let data = null;
|
||||
|
||||
// 方案1:如果启用了分片接口,尝试从分片接口加载
|
||||
if (this.useSplitApi) {
|
||||
try {
|
||||
const url = this.provinceDetailUrl.replace('{code}', provinceCode);
|
||||
data = await this.fetchData(url, cacheKey);
|
||||
if (data && data.code === provinceCode) {
|
||||
console.log(`✅ 从分片接口加载省份详情: ${provinceCode}`);
|
||||
return data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ 分片接口不可用,降级到完整数据提取: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 方案2:从完整数据中提取省份详情(默认方案)
|
||||
data = await this.loadProvinceDetailFromFullData(provinceCode, forceRefresh);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`❌ 加载省份详情失败: ${provinceCode}`, error);
|
||||
|
||||
// 如果加载失败,尝试使用缓存
|
||||
if (!forceRefresh) {
|
||||
const cached = await this.getCachedData(cacheKey);
|
||||
if (cached) {
|
||||
console.log('⚠️ 使用过期缓存数据');
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从完整数据中提取省份详情(默认方案)
|
||||
* 如果完整数据已缓存,提取会很快
|
||||
*/
|
||||
async loadProvinceDetailFromFullData(provinceCode, forceRefresh = false) {
|
||||
const cacheKey = 'address_full_data';
|
||||
|
||||
// 先尝试从缓存获取完整数据(如果已缓存,提取会很快)
|
||||
let fullData = await this.getCachedData(cacheKey);
|
||||
|
||||
if (!fullData || forceRefresh) {
|
||||
// 如果完整数据未缓存,需要加载完整数据
|
||||
console.log(`📥 完整数据未缓存,开始加载完整数据以获取省份 ${provinceCode}...`);
|
||||
|
||||
uni.showLoading({
|
||||
title: '加载地址数据...',
|
||||
mask: true
|
||||
});
|
||||
|
||||
try {
|
||||
fullData = await this.fetchData(this.fullDataUrl, cacheKey);
|
||||
console.log('✅ 完整数据加载完成,已缓存');
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
} else {
|
||||
console.log(`✅ 从缓存提取省份详情: ${provinceCode}(快速)`);
|
||||
}
|
||||
|
||||
// 查找并返回指定省份的完整数据
|
||||
// 注意:根据实际数据结构,code可能是"31"格式,需要精确匹配
|
||||
if (Array.isArray(fullData)) {
|
||||
const province = fullData.find(p => p.code === provinceCode || p.code === String(provinceCode));
|
||||
if (province) {
|
||||
return province;
|
||||
} else {
|
||||
console.warn(`⚠️ 未找到省份: ${provinceCode}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
async clearCache() {
|
||||
await this.init();
|
||||
|
||||
try {
|
||||
if (this.dbHelper) {
|
||||
await this.dbHelper.clearStore(this.storeName);
|
||||
}
|
||||
// 清除 uni.storage
|
||||
const keys = uni.getStorageInfoSync().keys;
|
||||
keys.forEach(key => {
|
||||
if (key.startsWith('address_')) {
|
||||
uni.removeStorageSync(key);
|
||||
}
|
||||
});
|
||||
// 清除内存缓存
|
||||
this.memoryCache.clear();
|
||||
console.log('✅ 已清除所有地址数据缓存');
|
||||
} catch (e) {
|
||||
console.error('清除缓存失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除旧缓存
|
||||
*/
|
||||
async clearOldCache() {
|
||||
try {
|
||||
// 清除7天前的缓存
|
||||
const expireTime = this.cacheExpireDays * 24 * 60 * 60 * 1000;
|
||||
const now = Date.now();
|
||||
|
||||
if (this.dbHelper) {
|
||||
const allData = await this.dbHelper.getAll(this.storeName);
|
||||
for (const item of allData) {
|
||||
if (item.updateTime && (now - item.updateTime) > expireTime) {
|
||||
await this.dbHelper.delete(this.storeName, item.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('清理旧缓存失败:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 单例模式
|
||||
const addressDataLoaderLazy = new AddressDataLoaderLazy();
|
||||
|
||||
export default addressDataLoaderLazy;
|
||||
|
||||
Reference in New Issue
Block a user