Compare commits

...

4 Commits

9 changed files with 1037 additions and 152 deletions

View File

@@ -669,9 +669,9 @@ body {
bottom: 0;
left: 0;
width: 100%;
background-color: #f5f5f5;
border-radius: 16px 16px 0 0;
max-height: 70vh;
background-color: #fff;
border-radius: 20px 20px 0 0;
max-height: 80vh;
display: flex;
flex-direction: column;
animation: slideUp 0.3s ease;
@@ -690,11 +690,319 @@ body {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #e5e5e5;
padding: 6px 20px;
border-bottom: 1px solid #f0f0f0;
background-color: #fff;
}
.city-picker-body {
flex: 1;
overflow-y: auto;
padding: 0;
background-color: #fff;
display: flex;
flex-direction: column;
}
.city-picker-body::-webkit-scrollbar {
width: 4px;
}
.city-picker-body::-webkit-scrollbar-thumb {
background: #e0e0e0;
border-radius: 2px;
}
/* ==================== 定位和热门城市区域 ==================== */
.city-picker-hot-section {
padding: 8px;
border-bottom: 1px solid #f0f0f0;
flex-shrink: 0;
}
/* 定位区域 */
.location-section {
margin-bottom: 12px;
}
/* 定位标签 */
.location-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: #fff;
border: 1.5px solid #e5e5e5;
border-radius: 18px;
font-size: 14px;
color: #333;
font-weight: 500;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
user-select: none;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.location-badge:hover {
border-color: #3474fe;
color: #3474fe;
background: #E8F0FF;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(52, 116, 254, 0.2);
}
.location-badge:active {
transform: translateY(0);
}
.location-badge.locating {
opacity: 0.7;
cursor: not-allowed;
pointer-events: none;
}
.location-badge .location-icon {
font-size: 16px;
display: inline-block;
filter: hue-rotate(0deg) saturate(1.2);
line-height: 1;
}
.location-badge .location-text {
font-size: 13px;
}
/* 热门城市区域 */
.hot-cities-section {
margin-bottom: 0;
}
.section-title {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 6px;
}
.section-title::before {
content: '';
width: 3px;
height: 14px;
background: #3474fe;
border-radius: 2px;
}
.hot-cities-grid {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
/* 热门城市标签 */
.city-tag {
padding: 8px 16px;
background: #fff;
border: 1.5px solid #e5e5e5;
border-radius: 20px;
font-size: 13px;
color: #333;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
user-select: none;
white-space: nowrap;
}
.city-tag:hover {
border-color: #3474fe;
color: #3474fe;
background: #E8F0FF;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(52, 116, 254, 0.2);
}
.city-tag:active {
transform: translateY(0);
}
.city-tag.active {
background: #3474fe;
border-color: #3474fe;
color: #fff;
font-weight: 500;
box-shadow: 0 4px 12px rgba(52, 116, 254, 0.35);
}
/* ==================== 省份城市联动区域 ==================== */
.province-city-section {
padding: 0;
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.province-city-section > .section-title {
padding: 12px 16px 10px;
margin-bottom: 0;
background: #fff;
}
.province-city-container {
display: flex;
flex: 1;
overflow: hidden;
border-top: 1px solid #f0f0f0;
}
/* 省份列表 */
.province-list-wrapper {
width: 100px;
flex-shrink: 0;
overflow: hidden;
background: #f5f5f5;
border-right: 1px solid #e5e5e5;
}
.province-list {
display: flex;
flex-direction: column;
gap: 0;
max-height: 280px;
overflow-y: auto;
padding-right: 0;
background: #f5f5f5;
}
.province-list::-webkit-scrollbar {
width: 4px;
}
.province-list::-webkit-scrollbar-thumb {
background: #e0e0e0;
border-radius: 2px;
}
.province-item {
padding: 12px 10px;
font-size: 14px;
color: #333;
cursor: pointer;
border-radius: 0;
transition: all 0.2s ease;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
background: transparent;
flex-shrink: 0;
}
.province-item:hover {
background: rgba(255, 255, 255, 0.5);
color: #333;
}
.province-item.active {
background: #3474fe;
color: #fff;
font-weight: 500;
}
/* 城市网格 */
.city-grid-wrapper {
flex: 1;
overflow: hidden;
background: #fff;
}
.city-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
max-height: 280px;
overflow-y: auto;
padding: 12px;
align-content: start;
}
.city-grid::-webkit-scrollbar {
width: 6px;
}
.city-grid::-webkit-scrollbar-thumb {
background: #e0e0e0;
border-radius: 3px;
}
.city-item {
padding: 10px 6px;
text-align: center;
font-size: 13px;
color: #333;
background: #fff;
border: 1px solid #e5e5e5;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
white-space: normal;
word-break: break-all;
line-height: 1.3;
min-height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.city-item:hover {
background: #E8F0FF;
border-color: #3474fe;
color: #3474fe;
}
.city-item.active {
background: #3474fe;
border-color: #3474fe;
color: #fff;
font-weight: 500;
}
/* 淡入动画 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.province-list-wrapper {
width: 90px;
}
.city-grid {
grid-template-columns: repeat(3, 1fr);
gap: 8px;
padding: 10px;
}
.hot-cities-grid {
gap: 8px;
}
.city-tag {
padding: 8px 16px;
font-size: 13px;
}
}
.city-picker-btn {
background: none;
border: none;
@@ -713,50 +1021,16 @@ body {
}
.city-picker-confirm {
color: #666;
font-weight: 400;
transition: all 0.2s ease;
}
.city-picker-confirm.has-selection {
color: #3474fe;
font-weight: 500;
}
.city-picker-body {
display: flex;
flex: 1;
overflow: hidden;
background-color: #f5f5f5;
}
.city-picker-column {
flex: 1;
overflow-y: auto;
background-color: #fff;
}
.city-picker-column:first-child {
border-right: 1px solid #e5e5e5;
}
.city-picker-item {
padding: 12px 16px;
font-size: 14px;
color: #333;
cursor: pointer;
transition: background-color 0.2s ease;
border-bottom: 1px solid #f5f5f5;
}
.city-picker-item:active {
background-color: #f0f0f0;
}
.city-picker-item.active {
background-color: #e8f0ff;
color: #3474fe;
font-weight: 600;
}
body.modal-open {
overflow: hidden;
}
/* Toast 提示样式 */
.toast {
position: fixed;

View File

@@ -74,12 +74,42 @@
<button class="city-picker-btn city-picker-cancel" id="cityCancelBtn">取消</button>
<button class="city-picker-btn city-picker-confirm" id="cityConfirmBtn">确认</button>
</div>
<div class="city-picker-body">
<div class="city-picker-column" id="provinceColumn">
<!-- 省份列表会通过JS动态添加 -->
<!-- 定位和热门城市区域 -->
<div class="city-picker-hot-section">
<!-- 定位标签 -->
<div class="location-section" id="locationSection">
<!-- 定位标签会通过JS动态添加 -->
</div>
<!-- 热门城市 -->
<div class="hot-cities-section">
<div class="section-title">热门城市</div>
<div class="hot-cities-grid" id="hotCitiesList">
<!-- 热门城市标签会通过JS动态添加 -->
</div>
</div>
</div>
<div class="city-picker-column" id="cityColumn">
<!-- 城市列表会通过JS动态添加 -->
<!-- 省份城市联动区域 -->
<div class="province-city-section">
<div class="section-title">选择地区</div>
<div class="province-city-container">
<!-- 省份列表 -->
<div class="province-list-wrapper">
<div class="province-list" id="provinceColumn">
<!-- 省份列表会通过JS动态添加 -->
</div>
</div>
<!-- 城市网格 -->
<div class="city-grid-wrapper">
<div class="city-grid" id="cityColumn">
<!-- 城市列表会通过JS动态添加 -->
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,50 +0,0 @@
# flux-web Nginx 配置示例
#
# 使用方法:
# 1. 将下面配置复制到 /etc/nginx/sites-available/flux-web
# 2. 修改 server_name你的域名和 root项目路径
# 3. 执行ln -s /etc/nginx/sites-available/flux-web /etc/nginx/sites-enabled/
# 4. 测试nginx -t
# 5. 重载systemctl reload nginx
server {
listen 80;
server_name your-domain.com; # ← 改成你的域名
root /var/www/flux-web; # ← 改成项目路径
index index.html;
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 30d;
}
# 主路由
location / {
try_files $uri $uri/ /index.html;
}
}
# HTTPS 配置(可选,使用 Let's Encrypt 免费证书)
# 先执行apt install certbot python3-certbot-nginx
# 然后执行certbot --nginx -d your-domain.com
# 完整 HTTPS 配置示例:
#
# server {
# listen 443 ssl;
# server_name your-domain.com;
# root /var/www/flux-web;
# index index.html;
#
# ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
#
# location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
# expires 30d;
# }
#
# location / {
# try_files $uri $uri/ /index.html;
# }
# }

View File

@@ -32,6 +32,9 @@ export const API_CONFIG = {
// 区域数据接口
AREA_LIST: '/api/partnerh5/area_list',
// IP定位接口
IP_LOCATION: '/api/partnerh5/ip_location',
},
// 请求超时配置(毫秒)

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();
}
/**
@@ -100,6 +105,7 @@ export class BasicInfoPage {
modalId: 'cityPickerModal',
provinceColumnId: 'provinceColumn',
cityColumnId: 'cityColumn',
hotCitiesId: 'hotCitiesList',
cancelBtnId: 'cityCancelBtn',
confirmBtnId: 'cityConfirmBtn',
onConfirm: (result) => {
@@ -686,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;
}
@@ -918,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()
@@ -121,7 +150,7 @@ export class AreaService {
provinceCode: province.code,
cityCode: city ? city.code : '',
districtCode: district ? district.code : '',
value: city ? `${province.name}/${city.name}` : province.name
value: city ? city.name : province.name // 只返回市名或省名
};
}

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

@@ -1,9 +1,31 @@
/**
* 城市选择器组件
* 提供省份和城市的联动选择功能
* 提供省份和城市的联动选择功能,支持热门城市快速选择和定位
*/
import { AreaService } from '../services/area.service.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'}
];
// 直辖市列表(省和市同名)
const MUNICIPALITIES = ['北京市', '上海市', '天津市', '重庆市'];
export class CityPicker {
/**
@@ -12,6 +34,7 @@ export class CityPicker {
* @param {string} options.modalId - 模态框元素ID
* @param {string} options.provinceColumnId - 省份列元素ID
* @param {string} options.cityColumnId - 城市列元素ID
* @param {string} options.hotCitiesId - 热门城市区域元素ID
* @param {string} options.cancelBtnId - 取消按钮ID
* @param {string} options.confirmBtnId - 确认按钮ID
* @param {Function} options.onConfirm - 确认回调
@@ -20,6 +43,7 @@ export class CityPicker {
this.modal = document.getElementById(options.modalId);
this.provinceColumn = document.getElementById(options.provinceColumnId);
this.cityColumn = document.getElementById(options.cityColumnId);
this.hotCitiesArea = document.getElementById(options.hotCitiesId);
this.cancelBtn = document.getElementById(options.cancelBtnId);
this.confirmBtn = document.getElementById(options.confirmBtnId);
this.onConfirm = options.onConfirm || null;
@@ -37,6 +61,11 @@ export class CityPicker {
// 数据缓存
this.provinces = [];
this.cities = [];
this.hotCities = HOT_CITIES;
// 定位相关
this.currentLocationCity = null; // 当前定位城市
this.isLocating = false; // 是否正在定位
if (!this.modal) {
console.error(`[CityPicker] 找不到模态框元素: ${options.modalId}`);
@@ -68,6 +97,154 @@ export class CityPicker {
// 预加载省份数据
this.loadProvinces();
// 获取定位
this.getCurrentLocation();
}
/**
* 渲染定位标签
*/
renderLocationBadge() {
const locationSection = document.getElementById('locationSection');
if (!locationSection) return;
locationSection.innerHTML = '';
if (this.currentLocationCity) {
// 已定位,显示当前城市
const badge = document.createElement('div');
badge.className = 'location-badge';
badge.innerHTML = `
<span class="location-icon">📍</span>
<span class="location-text">${this.currentLocationCity.name}</span>
`;
badge.addEventListener('click', async () => {
await this.selectHotCity(this.currentLocationCity);
});
locationSection.appendChild(badge);
} else if (this.isLocating) {
// 正在定位
const badge = document.createElement('div');
badge.className = 'location-badge locating';
badge.innerHTML = `
<span class="location-icon">⌛</span>
<span class="location-text">定位中...</span>
`;
locationSection.appendChild(badge);
} else {
// 未定位,显示定位按钮
const badge = document.createElement('div');
badge.className = 'location-badge';
badge.innerHTML = `
<span class="location-icon">📍</span>
<span class="location-text">获取定位</span>
`;
badge.addEventListener('click', () => {
this.getCurrentLocation();
});
locationSection.appendChild(badge);
}
}
/**
* 渲染热门城市区域
*/
renderHotCities() {
// 渲染定位标签
this.renderLocationBadge();
if (!this.hotCitiesArea) return;
this.hotCitiesArea.innerHTML = '';
// 渲染热门城市列表
this.hotCities.forEach(city => {
const cityTag = document.createElement('div');
cityTag.className = 'city-tag';
cityTag.textContent = city.name;
cityTag.dataset.cityName = city.name;
cityTag.dataset.provinceName = city.province;
cityTag.dataset.cityCode = city.code;
cityTag.dataset.provinceCode = city.code.substring(0, 2) + '00';
// 添加淡入动画
cityTag.style.animationDelay = `${Math.floor(Math.random() * 100)}ms`;
// 单击选中热门城市
cityTag.addEventListener('click', async () => {
await this.selectHotCity(city);
});
// 双击直接确认选择
cityTag.addEventListener('dblclick', async () => {
await this.selectHotCity(city);
this.confirmSelection();
});
this.hotCitiesArea.appendChild(cityTag);
});
}
/**
* 选择热门城市
* @param {Object} city - 热门城市对象
*/
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';
this.tempSelectedCityCode = city.code;
// 更新热门城市选中状态
const tags = this.hotCitiesArea.querySelectorAll('.city-tag');
tags.forEach(tag => {
tag.classList.toggle('active', tag.dataset.cityCode === city.code);
});
// 同步更新省份和城市的选中状态
if (this.provinces.length === 0) {
await this.loadProvinces();
}
// 选中对应的省份
const province = this.provinces.find(p => p.name === city.province || p.code === this.tempSelectedProvinceCode);
if (province) {
this.tempSelectedProvince = province.name;
this.tempSelectedProvinceCode = province.code;
// 更新省份列表选中状态
const provinceItems = this.provinceColumn.querySelectorAll('.province-item');
provinceItems.forEach(item => {
item.classList.toggle('active', item.dataset.provinceCode === province.code);
});
// 加载并渲染城市列表
await this.loadCities(province.code);
// 选中对应的城市
const cityItems = this.cityColumn.querySelectorAll('.city-item');
if (isMunicipality) {
// 直辖市:自动选中唯一选项(省份名称)
if (cityItems.length > 0) {
cityItems[0].classList.add('active');
}
} else {
// 普通城市:选中对应的城市
cityItems.forEach(item => {
item.classList.toggle('active', item.dataset.cityCode === city.code);
});
}
}
// 更新确认按钮状态
this.updateConfirmButtonState();
}
/**
@@ -90,13 +267,16 @@ export class CityPicker {
this.provinceColumn.innerHTML = '';
this.provinces.forEach(province => {
this.provinces.forEach((province, index) => {
const provinceItem = document.createElement('div');
provinceItem.className = 'city-picker-item';
provinceItem.className = 'province-item';
provinceItem.textContent = province.name;
provinceItem.dataset.province = province.name;
provinceItem.dataset.provinceCode = province.code;
// 添加淡入动画
provinceItem.style.animation = `fadeIn 0.3s ease ${index * 20}ms backwards`;
provinceItem.addEventListener('click', () => {
this.selectProvince(province.name, province.code);
});
@@ -115,13 +295,20 @@ export class CityPicker {
this.tempSelectedProvinceCode = provinceCode;
// 更新省份选中状态
const items = this.provinceColumn.querySelectorAll('.city-picker-item');
const items = this.provinceColumn.querySelectorAll('.province-item');
items.forEach(item => {
item.classList.toggle('active', item.dataset.provinceCode === provinceCode);
});
// 清除城市选中状态(切换省份时)
this.tempSelectedCity = '';
this.tempSelectedCityCode = '';
// 加载并渲染城市列表
await this.loadCities(provinceCode);
// 更新确认按钮状态
this.updateConfirmButtonState();
}
/**
@@ -146,20 +333,48 @@ export class CityPicker {
this.cityColumn.innerHTML = '';
// 只显示市4位不显示区6位
const cities = this.cities.filter(area => area.code.length === 4);
// 判断是否为直辖市
const isMunicipality = MUNICIPALITIES.includes(this.tempSelectedProvince);
cities.forEach(city => {
let cities;
if (isMunicipality) {
// 直辖市:直接显示省份名称作为唯一的城市选项
// 这样用户选择后返回的就是"北京市"、"上海市"等
cities = [{
name: this.tempSelectedProvince,
code: this.tempSelectedProvinceCode
}];
} else {
// 普通省份显示市级数据4位代码排除"市辖区"
cities = this.cities.filter(area =>
area.code.length === 4 &&
area.name !== '市辖区'
);
}
cities.forEach((city, index) => {
const cityItem = document.createElement('div');
cityItem.className = 'city-picker-item';
cityItem.className = 'city-item';
cityItem.textContent = city.name;
cityItem.dataset.city = city.name;
cityItem.dataset.cityCode = city.code;
// 添加 title 属性,鼠标悬停时显示完整城市名称
cityItem.title = city.name;
// 添加淡入动画
cityItem.style.animation = `fadeIn 0.3s ease ${index * 15}ms backwards`;
// 单击选中城市
cityItem.addEventListener('click', () => {
this.selectCity(city.name, city.code);
});
// 双击直接确认选择
cityItem.addEventListener('dblclick', () => {
this.selectCity(city.name, city.code);
this.confirmSelection();
});
this.cityColumn.appendChild(cityItem);
});
@@ -168,12 +383,11 @@ export class CityPicker {
const existingCity = cities.find(c => c.code === this.tempSelectedCityCode);
if (existingCity) {
this.selectCity(existingCity.name, existingCity.code);
} else if (cities.length > 0) {
// 默认选择第一个
this.selectCity(cities[0].name, cities[0].code);
}
} else if (cities.length > 0) {
// 默认选择第一个
}
// 对于直辖市,自动选中唯一的选项
if (isMunicipality && cities.length === 1 && !this.tempSelectedCityCode) {
this.selectCity(cities[0].name, cities[0].code);
}
}
@@ -188,35 +402,59 @@ export class CityPicker {
this.tempSelectedCityCode = cityCode;
// 更新城市选中状态
const items = this.cityColumn.querySelectorAll('.city-picker-item');
const items = this.cityColumn.querySelectorAll('.city-item');
items.forEach(item => {
item.classList.toggle('active', item.dataset.cityCode === cityCode);
});
// 更新热门城市选中状态
this.updateHotCitiesSelection();
}
/**
* 打开选择器
* @param {string} currentValue - 当前值(格式:"省/市"
* @param {string} currentValue - 当前值(格式:"市名" 或 "省/市"
*/
async open(currentValue = '') {
// 解析当前值,并查找对应的代码
if (currentValue) {
const parts = currentValue.split('/');
if (parts.length === 2) {
const provinceName = parts[0];
const cityName = parts[1];
let cityName = '';
let provinceName = '';
// 支持两种格式:纯市名 "北京市" 或 省市格式 "北京市/北京市"
if (currentValue.includes('/')) {
const parts = currentValue.split('/');
provinceName = parts[0];
cityName = parts[1];
} else {
cityName = currentValue;
}
// 先在热门城市中查找
const hotCity = this.hotCities.find(c => c.name === cityName);
if (hotCity) {
this.tempSelectedProvince = hotCity.province;
this.tempSelectedCity = hotCity.name;
this.tempSelectedProvinceCode = hotCity.code.substring(0, 2) + '00';
this.tempSelectedCityCode = hotCity.code;
} else {
// 查找省份代码
if (this.provinces.length === 0) {
await this.loadProvinces();
}
const province = this.provinces.find(p => p.name === provinceName);
if (province) {
this.tempSelectedProvince = provinceName;
this.tempSelectedProvinceCode = province.code;
// 加载城市数据并查找城市代码
await this.loadCities(province.code);
// 如果有省份名,直接查找
if (provinceName) {
const province = this.provinces.find(p => p.name === provinceName);
if (province) {
this.tempSelectedProvince = provinceName;
this.tempSelectedProvinceCode = province.code;
}
}
// 如果有省代码,加载城市数据并查找城市代码
if (this.tempSelectedProvinceCode) {
await this.loadCities(this.tempSelectedProvinceCode);
const city = this.cities.find(c => c.name === cityName && c.code.length === 4);
if (city) {
this.tempSelectedCity = cityName;
@@ -238,16 +476,51 @@ export class CityPicker {
}
// 渲染列表
this.renderLocationBadge(); // 渲染定位标签
this.renderHotCities();
this.renderProvinceList();
// 更新热门城市选中状态
this.updateHotCitiesSelection();
if (this.tempSelectedProvinceCode) {
await this.selectProvince(this.tempSelectedProvince, this.tempSelectedProvinceCode);
}
// 更新确认按钮状态
this.updateConfirmButtonState();
// 显示模态框
document.body.classList.add('modal-open');
this.modal.classList.add('show');
}
/**
* 更新热门城市选中状态
*/
updateHotCitiesSelection() {
if (!this.hotCitiesArea) return;
const tags = this.hotCitiesArea.querySelectorAll('.city-tag');
tags.forEach(tag => {
const isSelected = this.tempSelectedCityCode && tag.dataset.cityCode === this.tempSelectedCityCode;
tag.classList.toggle('active', isSelected);
});
// 更新确认按钮状态
this.updateConfirmButtonState();
}
/**
* 更新确认按钮状态
*/
updateConfirmButtonState() {
if (this.confirmBtn) {
const hasSelection = this.tempSelectedProvince && this.tempSelectedCity;
this.confirmBtn.classList.toggle('has-selection', hasSelection);
}
}
/**
* 关闭选择器
*/
@@ -274,7 +547,8 @@ export class CityPicker {
this.selectedProvinceCode = this.tempSelectedProvinceCode;
this.selectedCityCode = this.tempSelectedCityCode;
const cityValue = `${this.tempSelectedProvince}/${this.tempSelectedCity}`;
// 只返回市名,如:北京市、武汉市、曲靖市
const cityValue = this.tempSelectedCity;
if (this.onConfirm) {
this.onConfirm({
@@ -292,18 +566,38 @@ export class CityPicker {
/**
* 设置选中值
* @param {string} value - 值(格式:"省/市"
* @param {string} value - 值(格式:"市名" 或 "省/市"
*/
setValue(value) {
if (!value) return;
const parts = value.split('/');
if (parts.length === 2) {
this.selectedProvince = parts[0];
this.selectedCity = parts[1];
this.tempSelectedProvince = parts[0];
this.tempSelectedCity = parts[1];
// 代码需要通过查找获取,这里暂时留空
// 支持两种格式:纯市名或省/市格式
if (value.includes('/')) {
const parts = value.split('/');
if (parts.length === 2) {
this.selectedProvince = parts[0];
this.selectedCity = parts[1];
this.tempSelectedProvince = parts[0];
this.tempSelectedCity = parts[1];
}
} else {
// 纯市名,从热门城市中查找省份信息
const hotCity = this.hotCities.find(c => c.name === value);
if (hotCity) {
const provinceCode = hotCity.code.substring(0, 2) + '00';
this.selectedProvince = hotCity.province;
this.selectedCity = hotCity.name;
this.selectedProvinceCode = provinceCode;
this.selectedCityCode = hotCity.code;
this.tempSelectedProvince = hotCity.province;
this.tempSelectedCity = hotCity.name;
this.tempSelectedProvinceCode = provinceCode;
this.tempSelectedCityCode = hotCity.code;
} else {
// 如果不在热门城市中,只记录城市名
this.selectedCity = value;
this.tempSelectedCity = value;
}
}
}
@@ -317,7 +611,7 @@ export class CityPicker {
city: this.selectedCity,
provinceCode: this.selectedProvinceCode,
cityCode: this.selectedCityCode,
value: `${this.selectedProvince}/${this.selectedCity}`
value: this.selectedCity // 只返回市名
};
}
@@ -344,4 +638,105 @@ export class CityPicker {
this.provinceColumn = null;
this.cityColumn = null;
}
/**
* 获取当前定位使用IP定位
*/
async getCurrentLocation() {
// 如果已经定位过,不再重复定位
if (this.currentLocationCity) {
return;
}
// 如果正在定位,避免重复请求
if (this.isLocating) {
return;
}
this.isLocating = true;
// 如果定位区域已渲染,更新状态显示"定位中"
const locationSection = document.getElementById('locationSection');
if (locationSection) {
this.renderLocationBadge();
}
try {
// 使用全局定位服务
const locationService = getLocationService();
const location = await locationService.getLocation();
if (location) {
const {province, city} = location;
console.log('[CityPicker] IP定位成功:', 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: ''
};
}
}
} catch (error) {
console.error('[CityPicker] IP定位失败:', error);
// 定位失败时不显示错误,静默处理
} finally {
this.isLocating = false;
// 更新UI显示
const locationSection = document.getElementById('locationSection');
if (locationSection) {
this.renderLocationBadge();
}
}
}
/**
* 查找城市代码
* @param {string} provinceName - 省份名称
* @param {string} cityName - 城市名称
* @returns {Promise<Object|null>} - 包含省代码和市代码的对象
*/
async findCityCode(provinceName, cityName) {
try {
// 先在省份数据中查找省份代码
if (this.provinces.length === 0) {
await this.loadProvinces();
}
const province = this.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('[CityPicker] 查找城市代码失败:', error);
return null;
}
}
}

BIN
static/image.zip Normal file

Binary file not shown.