新增:IP定位服务抽取和请求去重优化

This commit is contained in:
2026-01-29 20:26:08 +08:00
parent 01d1ce5ba8
commit 520b9e93bd
4 changed files with 295 additions and 65 deletions

View File

@@ -6,10 +6,12 @@
import { CityPicker, Modal } from '../ui/index.js';
import { Validator, Formatter } from '../utils/index.js';
import { DraftManager, FormIdGenerator, UserCache } from '../core/index.js';
import { ASSET_CONFIG, BASIC_INFO_CONFIG, PROVINCE_CITY_DATA, CACHE_CONFIG } from '../config/index.js';
import { ASSET_CONFIG, BASIC_INFO_CONFIG, PROVINCE_CITY_DATA, CACHE_CONFIG, API_CONFIG } from '../config/index.js';
import { showToast } from '../ui/toast.js';
import { AreaService } from '../services/area.service.js';
import { getLocationService } from '../services/location.service.js';
import { AuthFlowService, AUTH_STATUS } from '../services/index.js';
import { ApiClient } from '../core/api.js';
export class BasicInfoPage {
constructor() {
@@ -71,6 +73,9 @@ export class BasicInfoPage {
this.renderForm();
this.bindEvents();
this.updateProgress();
// IP定位自动填充城市
this.autoFillCityByLocation();
}
/**
@@ -687,29 +692,27 @@ export class BasicInfoPage {
// 如果没有 h5Urls显示成功提示
if (!h5Urls || h5Urls.length === 0) {
// 有 redirectUrl 跳转到指定地址,否则返回首页
const finalUrl = redirectUrl || window.location.href.split('?')[0];
const btnText = redirectUrl ? '立即跳转' : '返回首页';
authContainer.innerHTML = `
<div class="auth-complete-section">
<div class="auth-complete-icon">✓</div>
<div class="auth-complete-title">信息提交成功</div>
<div class="auth-complete-desc">您的申请已提交成功!</div>
${redirectUrl ? `
<div class="auth-countdown">
<span id="countdownSeconds">5</span> 秒后自动跳转...
</div>
<button class="auth-redirect-btn" id="redirectNowBtn">立即跳转</button>
` : `
<button class="auth-redirect-btn" onclick="window.location.reload()">返回首页</button>
`}
<div class="auth-countdown">
<span id="countdownSeconds">5</span> 秒后自动跳转...
</div>
<button class="auth-redirect-btn" id="redirectNowBtn">${btnText}</button>
</div>
`;
const mainContainer = document.querySelector('.container') || document.body;
mainContainer.insertBefore(authContainer, mainContainer.firstChild);
// 如果有 redirectUrl启动倒计时
if (redirectUrl) {
this.startFinalCountdown(redirectUrl);
}
// 启动 5 秒倒计时
this.startFinalCountdown(finalUrl);
return;
}
@@ -919,6 +922,82 @@ export class BasicInfoPage {
}
}
/**
* 通过IP定位自动填充城市
*/
async autoFillCityByLocation() {
try {
// 如果已经有城市值,跳过
if (this.basicInfoValues.city) {
console.log('[BasicInfoPage] 城市已存在跳过IP定位填充');
return;
}
console.log('[BasicInfoPage] 开始IP定位...');
const locationService = getLocationService();
const location = await locationService.getLocation();
if (location) {
const { province, city } = location;
console.log('[BasicInfoPage] IP定位成功:', province, city);
// 查找城市代码
const cityCode = await this.findCityCode(province, city);
if (cityCode) {
// 自动填充城市
this.handleCityConfirm({
value: city,
province: province,
city: city,
provinceCode: cityCode.provinceCode,
cityCode: cityCode.cityCode
});
console.log('[BasicInfoPage] 城市自动填充成功:', city);
} else {
console.warn('[BasicInfoPage] 未找到城市代码:', province, city);
}
} else {
console.warn('[BasicInfoPage] IP定位失败: 未获取到位置信息');
}
} catch (error) {
console.error('[BasicInfoPage] IP定位异常:', error);
// 静默失败,不影响用户正常使用
}
}
/**
* 查找城市代码
*/
async findCityCode(provinceName, cityName) {
try {
// 获取省份数据
const provinces = await AreaService.getProvinces();
const province = provinces.find(p => p.name === provinceName);
if (!province) {
return null;
}
// 获取城市数据
const cities = await AreaService.getCities(province.code);
const city = cities.find(c => c.name === cityName && c.code.length === 4);
if (city) {
return {
provinceCode: province.code,
cityCode: city.code
};
}
return null;
} catch (error) {
console.error('[BasicInfoPage] 查找城市代码失败:', error);
return null;
}
}
/**
* 销毁页面
*/

View File

@@ -13,23 +13,50 @@ const CACHE_DURATION = 10 * 60 * 1000; // 10分钟
* 区域服务
*/
export class AreaService {
// 正在进行的请求缓存
static pendingRequests = new Map();
/**
* 获取区域列表
* @param {string} provincecode - 省份代码(可选)
* @returns {Promise<Array>} 区域列表 [{code, name}]
*/
static async getAreaList(provincecode) {
// 检查缓存
const cacheKey = `${CACHE_KEY_PREFIX}${provincecode || 'all'}`;
const cached = localStorage.getItem(cacheKey);
// 检查 localStorage 缓存
const cached = localStorage.getItem(cacheKey);
if (cached) {
const { data, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp < CACHE_DURATION) {
console.log(`[AreaService] 使用缓存 ${provincecode || 'all'}`);
return data;
}
}
// 检查是否有正在进行的请求
if (this.pendingRequests.has(cacheKey)) {
console.log(`[AreaService] 等待正在进行的请求 ${provincecode || 'all'}`);
return this.pendingRequests.get(cacheKey);
}
// 创建新请求
const requestPromise = this.doGetAreaList(provincecode, cacheKey);
this.pendingRequests.set(cacheKey, requestPromise);
try {
return await requestPromise;
} finally {
// 请求完成后清除缓存
this.pendingRequests.delete(cacheKey);
}
}
/**
* 执行实际的区域列表请求
* @private
*/
static async doGetAreaList(provincecode, cacheKey) {
try {
// 构建请求参数
const params = {};
@@ -37,6 +64,8 @@ export class AreaService {
params.provincecode = provincecode;
}
console.log(`[AreaService] 请求区域数据 ${provincecode || 'all'}`);
// 使用 ApiClient 发送请求
const result = await ApiClient.get(API_CONFIG.ENDPOINTS.AREA_LIST, params);
@@ -46,7 +75,7 @@ export class AreaService {
const areaList = result.result || [];
// 保存到缓存
// 保存到 localStorage 缓存
localStorage.setItem(cacheKey, JSON.stringify({
data: areaList,
timestamp: Date.now()

View File

@@ -0,0 +1,124 @@
/**
* 定位服务
* 提供全局 IP 定位功能,避免重复请求
*/
import { API_CONFIG } from '../config/api.config.js';
import { ApiClient } from '../core/api.js';
/**
* 定位服务(单例模式)
*/
export class LocationService {
static instance = null;
// 定位缓存
locationCache = null;
// 正在定位的 Promise
locatingPromise = null;
// 缓存有效期10分钟
CACHE_DURATION = 10 * 60 * 1000;
/**
* 获取单例实例
*/
static getInstance() {
if (!this.instance) {
this.instance = new LocationService();
}
return this.instance;
}
/**
* 获取当前定位(带缓存)
* @returns {Promise<Object>} 定位结果 {province, city, code}
*/
async getLocation() {
// 如果有缓存且未过期,直接返回
if (this.locationCache) {
const { data, timestamp } = this.locationCache;
if (Date.now() - timestamp < this.CACHE_DURATION) {
console.log('[LocationService] 使用缓存定位:', data);
return data;
}
}
// 如果正在定位,返回同一个 Promise
if (this.locatingPromise) {
console.log('[LocationService] 定位进行中,等待结果...');
return this.locatingPromise;
}
// 开始定位
this.locatingPromise = this.doLocation();
try {
const result = await this.locatingPromise;
// 缓存结果
this.locationCache = {
data: result,
timestamp: Date.now()
};
return result;
} finally {
// 清除定位 Promise
this.locatingPromise = null;
}
}
/**
* 执行 IP 定位
* @private
*/
async doLocation() {
try {
console.log('[LocationService] 开始 IP 定位...');
const result = await ApiClient.get(API_CONFIG.ENDPOINTS.IP_LOCATION);
if (result.retcode === 0 && result.result) {
const { province, city } = result.result;
if (province && city) {
console.log('[LocationService] IP 定位成功:', province, city);
return {
province,
city,
code: null // 稍后可以添加城市代码
};
}
}
throw new Error('定位失败:未获取到省市信息');
} catch (error) {
console.error('[LocationService] IP 定位失败:', error);
throw error;
}
}
/**
* 清除缓存
*/
clearCache() {
this.locationCache = null;
console.log('[LocationService] 缓存已清除');
}
/**
* 设置定位结果(手动设置)
* @param {Object} location 定位结果 {province, city, code}
*/
setLocation(location) {
this.locationCache = {
data: location,
timestamp: Date.now()
};
console.log('[LocationService] 手动设置定位:', location);
}
}
// 导出单例获取函数
export const getLocationService = () => LocationService.getInstance();

View File

@@ -3,23 +3,25 @@
* 提供省份和城市的联动选择功能,支持热门城市快速选择和定位
*/
import { AreaService } from '../services/area.service.js';
import { API_CONFIG } from '../config/api.config.js';
import {AreaService} from '../services/area.service.js';
import {getLocationService} from '../services/location.service.js';
import {API_CONFIG} from '../config/api.config.js';
import {ApiClient} from '../core/api.js';
// 热门城市列表12个核心城市
const HOT_CITIES = [
{ name: '北京市', province: '北京市', code: '1100' },
{ name: '上海市', province: '上海市', code: '3100' },
{ name: '广州市', province: '广东省', code: '4401' },
{ name: '深圳市', province: '广东省', code: '4403' },
{ name: '成都市', province: '四川省', code: '5101' },
{ name: '杭州市', province: '浙江省', code: '3301' },
{ name: '重庆市', province: '重庆市', code: '5000' },
{ name: '武汉市', province: '湖北省', code: '4201' },
{ name: '西安市', province: '陕西省', code: '6101' },
{ name: '南京市', province: '江苏省', code: '3201' },
{ name: '苏州市', province: '江苏省', code: '3205' },
{ name: '天津市', province: '天津市', code: '1200' }
{name: '北京市', province: '北京市', code: '1100'},
{name: '上海市', province: '上海市', code: '3100'},
{name: '广州市', province: '广东省', code: '4401'},
{name: '深圳市', province: '广东省', code: '4403'},
{name: '成都市', province: '四川省', code: '5101'},
{name: '杭州市', province: '浙江省', code: '3301'},
{name: '重庆市', province: '重庆市', code: '5000'},
{name: '武汉市', province: '湖北省', code: '4201'},
{name: '西安市', province: '陕西省', code: '6101'},
{name: '南京市', province: '江苏省', code: '3201'},
{name: '苏州市', province: '江苏省', code: '3205'},
{name: '天津市', province: '天津市', code: '1200'}
];
// 直辖市列表(省和市同名)
@@ -194,7 +196,7 @@ export class CityPicker {
*/
async selectHotCity(city) {
const isMunicipality = MUNICIPALITIES.includes(city.province);
this.tempSelectedProvince = city.province;
this.tempSelectedCity = city.name;
this.tempSelectedProvinceCode = city.provinceCode || city.code.substring(0, 2) + '00';
@@ -216,7 +218,7 @@ export class CityPicker {
if (province) {
this.tempSelectedProvince = province.name;
this.tempSelectedProvinceCode = province.code;
// 更新省份列表选中状态
const provinceItems = this.provinceColumn.querySelectorAll('.province-item');
provinceItems.forEach(item => {
@@ -225,7 +227,7 @@ export class CityPicker {
// 加载并渲染城市列表
await this.loadCities(province.code);
// 选中对应的城市
const cityItems = this.cityColumn.querySelectorAll('.city-item');
if (isMunicipality) {
@@ -240,7 +242,7 @@ export class CityPicker {
});
}
}
// 更新确认按钮状态
this.updateConfirmButtonState();
}
@@ -344,8 +346,8 @@ export class CityPicker {
}];
} else {
// 普通省份显示市级数据4位代码排除"市辖区"
cities = this.cities.filter(area =>
area.code.length === 4 &&
cities = this.cities.filter(area =>
area.code.length === 4 &&
area.name !== '市辖区'
);
}
@@ -383,7 +385,7 @@ export class CityPicker {
this.selectCity(existingCity.name, existingCity.code);
}
}
// 对于直辖市,自动选中唯一的选项
if (isMunicipality && cities.length === 1 && !this.tempSelectedCityCode) {
this.selectCity(cities[0].name, cities[0].code);
@@ -660,39 +662,35 @@ export class CityPicker {
}
try {
// 调用后端IP定位接口
const response = await fetch(API_CONFIG.ENDPOINTS.IP_LOCATION);
const result = await response.json();
// 使用全局定位服务
const locationService = getLocationService();
const location = await locationService.getLocation();
if (result.retcode === 0 && result.result) {
const { province, city } = result.result;
if (location) {
const {province, city} = location;
console.log('[CityPicker] IP定位成功:', province, city);
if (province && city) {
console.log('[CityPicker] IP定位成功:', province, city);
// 查找城市代码
const cityCode = await this.findCityCode(province, city);
// 查找城市代码
const cityCode = await this.findCityCode(province, city);
if (cityCode) {
this.currentLocationCity = {
name: city,
province: province,
code: cityCode.cityCode,
provinceCode: cityCode.provinceCode
};
console.log('[CityPicker] 定位成功:', this.currentLocationCity.name);
} else {
// 如果找不到代码,返回基本信息(代码为空)
this.currentLocationCity = {
name: city,
province: province,
code: '',
provinceCode: ''
};
}
if (cityCode) {
this.currentLocationCity = {
name: city,
province: province,
code: cityCode.cityCode,
provinceCode: cityCode.provinceCode
};
console.log('[CityPicker] 定位成功:', this.currentLocationCity.name);
} else {
// 如果找不到代码,返回基本信息(代码为空)
this.currentLocationCity = {
name: city,
province: province,
code: '',
provinceCode: ''
};
}
}
} catch (error) {
console.error('[CityPicker] IP定位失败:', error);
// 定位失败时不显示错误,静默处理