#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
高德地图POI采集工具
功能：采集高德地图POI数据
作者：AI Assistant
版本：1.0
"""

import sys
import os
import json
import time
import math
import threading
import webbrowser
import csv
from datetime import datetime
from typing import List, Dict, Any, Optional

import requests
import pymysql
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QLabel, QPushButton, QLineEdit, QTextEdit, QProgressBar,
    QComboBox, QSlider, QGroupBox, QMenuBar, QAction, QDialog,
    QMessageBox, QFrame, QScrollArea, QSpinBox, QCheckBox,
    QSplitter, QTableWidget, QTableWidgetItem, QHeaderView,
    QFileDialog, QFormLayout, QAbstractItemView
)
from PyQt5.QtCore import (
    Qt, QThread, pyqtSignal, QTimer, QPropertyAnimation, QEasingCurve, QSettings
)
from PyQt5.QtGui import QIcon, QPixmap, QFont, QPalette, QColor

# 导入认证模块
from auth_module_integration import integrate_auth


def resource_path(relative_path):
    """获取资源文件的绝对路径，适用于开发和打包后的环境"""
    try:
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")
    return os.path.join(base_path, relative_path)


class UsageInstructions(QDialog):
    """使用说明对话框"""
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("使用说明")
        self.setGeometry(150, 150, 600, 500)
        self.setWindowIcon(QIcon(resource_path("icon.ico")))

        layout = QVBoxLayout()

        # 使用说明文本
        instructions = """应用场景：

本工具用于采集高德地图POI（兴趣点）数据，支持按区域、关键词进行精准采集。

功能介绍：
1. 用户认证：程序启动时需要进行用户认证，确保合法使用。
2. API Key设置：输入您的高德地图API Key（32位字符串）。
3. 区域选择：选择省份、城市、区县进行精准定位。
4. 搜索范围：设置搜索半径（1-50公里）。
5. 关键词设置：输入要搜索的POI关键词（如：餐厅、酒店、加油站等）。
6. 数据存储：工具已预配置数据存储，无需手动设置。
7. 开始采集：点击"开始采集"按钮开始数据采集。
8. 停止采集：点击"停止采集"按钮可随时停止。
9. 进度监控：实时查看采集进度和状态。
10. 日志查看：查看详细的采集日志信息。

注意事项：
- 程序使用需要有效的用户账号和权限
- 请确保API Key有效且有足够的调用次数
- 采集过程中请保持网络连接稳定
- 大范围采集可能需要较长时间，请耐心等待
- 数据会自动去重，避免重复采集
- 关键操作会重新验证用户权限
"""

        text_edit = QTextEdit()
        text_edit.setReadOnly(True)
        text_edit.setPlainText(instructions)
        text_edit.setFontFamily("微软雅黑")
        text_edit.setFontPointSize(10)
        layout.addWidget(text_edit)

        # 二维码区域
        qr_layout = QHBoxLayout()

        # 打赏二维码
        qr_dashang = QVBoxLayout()
        try:
            dashang_pixmap = QPixmap(resource_path("dashang.jpg"))
            dashang_pixmap = dashang_pixmap.scaled(120, 120, Qt.KeepAspectRatio, Qt.SmoothTransformation)
            lbl_dashang = QLabel()
            lbl_dashang.setPixmap(dashang_pixmap)
            lbl_dashang.setAlignment(Qt.AlignCenter)
            qr_dashang.addWidget(lbl_dashang)
        except Exception as e:
            print(f"加载打赏二维码失败: {e}")

        lbl_dashang_text = QLabel("觉得软件不错，打赏一下吧。")
        lbl_dashang_text.setAlignment(Qt.AlignCenter)
        qr_dashang.addWidget(lbl_dashang_text)
        qr_layout.addLayout(qr_dashang)

        # BUG反馈二维码
        qr_haoyou = QVBoxLayout()
        try:
            haoyou_pixmap = QPixmap(resource_path("haoyou.jpg"))
            haoyou_pixmap = haoyou_pixmap.scaled(120, 120, Qt.KeepAspectRatio, Qt.SmoothTransformation)
            lbl_haoyou = QLabel()
            lbl_haoyou.setPixmap(haoyou_pixmap)
            lbl_haoyou.setAlignment(Qt.AlignCenter)
            qr_haoyou.addWidget(lbl_haoyou)
        except Exception as e:
            print(f"加载反馈二维码失败: {e}")

        lbl_haoyou_text = QLabel("BUG反馈，程序定制。")
        lbl_haoyou_text.setAlignment(Qt.AlignCenter)
        qr_haoyou.addWidget(lbl_haoyou_text)
        qr_layout.addLayout(qr_haoyou)

        layout.addLayout(qr_layout)
        self.setLayout(layout)


class DatabaseManager:
    """数据管理器"""
    def __init__(self):
        self.connection = None
        self.config = {
            'host': '123.57.245.64',
            'user': 'maps1',
            'password': 'wxcwxc',
            'database': 'maps1',
            'charset': 'utf8mb4',
            'autocommit': True
        }

    def connect(self):
        """连接数据存储"""
        try:
            self.connection = pymysql.connect(**self.config)
            return True
        except Exception as e:
            print(f"连接失败: {e}")
            return False

    def disconnect(self):
        """断开数据存储连接"""
        if self.connection:
            self.connection.close()
            self.connection = None

    def save_poi_data(self, poi_data: Dict[str, Any], keywords: str, search_region: str, user_id: int = 1) -> tuple:
        """保存POI数据，返回(是否新增, POI完整数据)"""
        if not self.connection:
            return False, None

        try:
            cursor = self.connection.cursor()
            
            # 解析位置信息
            location = poi_data.get('location', '') or ''
            longitude, latitude = None, None
            if location and ',' in location:
                try:
                    lng_str, lat_str = location.split(',')
                    longitude = float(lng_str)
                    latitude = float(lat_str)
                except:
                    longitude, latitude = None, None

            # 安全处理JSON字段
            def safe_json_dumps(data, default_value=''):
                if data is None:
                    return default_value
                try:
                    return json.dumps(data, ensure_ascii=False)
                except:
                    return default_value

            # 安全获取字段值
            def safe_get(key, default=''):
                value = poi_data.get(key)
                return value if value is not None else default

            # 将可能为列表/元组/集合的值安全转换为字符串，避免作为SQL参数时被当作序列处理
            def safe_text(v, sep=', '):
                if v is None:
                    return ''
                if isinstance(v, (list, tuple, set)):
                    try:
                        return sep.join(str(x) for x in v if x is not None and str(x) != '')
                    except Exception:
                        return str(v)
                try:
                    return str(v)
                except Exception:
                    return ''

            # 准备插入数据
            sql = """
                INSERT IGNORE INTO `poi_data` (
                    `poi_id`, `name`, `type`, `typecode`, `address`, `location`, `longitude`, `latitude`,
                    `pname`, `cityname`, `adname`, `pcode`, `citycode`, `adcode`, `business_area`,
                    `tel`, `opentime_today`, `opentime_week`, `tag`, `rating`, `cost`, `parking_type`, `alias`,
                    `photos`, `indoor_info`, `navi_info`, `children_info`,
                    `keywords`, `search_region`, `user_id`
                ) VALUES (
                    %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
                    %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
                )
            """
            
            # 处理business_area字段 - 确保是字符串
            business_area_value = poi_data.get('business_area', '')
            if isinstance(business_area_value, (list, tuple)):
                business_area_str = ', '.join(str(item) for item in business_area_value if item)
            else:
                business_area_str = str(business_area_value) if business_area_value else ''
            
            # 处理tag字段 - 确保是字符串
            tag_value = poi_data.get('tag', '')
            if isinstance(tag_value, (list, tuple)):
                tag_str = ', '.join(str(item) for item in tag_value if item)
            else:
                tag_str = str(tag_value) if tag_value else ''
            
            values = (
                poi_data.get('poi_id') or poi_data.get('id') or '',
                safe_get('name'),
                safe_get('type'),
                safe_get('typecode'),
                safe_get('address'),
                location,
                longitude,
                latitude,
                safe_get('pname'),
                safe_get('cityname'),
                safe_get('adname'),
                safe_get('pcode'),
                safe_get('citycode'),
                safe_get('adcode'),
                business_area_str,  # 使用处理后的字符串
                safe_text(poi_data.get('tel')),
                safe_text(poi_data.get('opentime_today')),
                safe_json_dumps(poi_data.get('opentime_week', [])),
                tag_str,  # 使用处理后的字符串
                safe_text(poi_data.get('rating')),
                safe_text(poi_data.get('cost')),
                safe_text(poi_data.get('parking_type')),
                safe_text(poi_data.get('alias')),
                safe_json_dumps(poi_data.get('photos', [])),
                safe_json_dumps(poi_data.get('indoor_info', {})),
                safe_json_dumps(poi_data.get('navi_info', {})),
                safe_json_dumps(poi_data.get('children_info', [])),
                keywords or '',
                search_region or '',
                user_id
            )
            
            cursor.execute(sql, values)
            # 检查是否真正插入了数据（不是被IGNORE忽略的重复数据）
            rows_affected = cursor.rowcount
            is_new = rows_affected > 0
            
            # 无论是否新增，都从数据库中获取完整的POI数据
            poi_id = poi_data.get('poi_id') or poi_data.get('id') or ''
            if poi_id:
                # 查询已存在的POI数据
                query_sql = """
                    SELECT poi_id, name, type, typecode, address, location, longitude, latitude,
                           pname, cityname, adname, pcode, citycode, adcode, business_area,
                           tel, opentime_today, opentime_week, tag, rating, cost, parking_type, alias,
                           photos, indoor_info, navi_info, children_info,
                           keywords, search_region, created_at
                    FROM poi_data 
                    WHERE poi_id = %s AND user_id = %s
                    ORDER BY created_at DESC
                    LIMIT 1
                """
                cursor.execute(query_sql, (poi_id, user_id))
                result = cursor.fetchone()
                
                if result:
                    # 构建完整的POI数据字典
                    columns = ['poi_id', 'name', 'type', 'typecode', 'address', 'location', 'longitude', 'latitude',
                              'pname', 'cityname', 'adname', 'pcode', 'citycode', 'adcode', 'business_area',
                              'tel', 'opentime_today', 'opentime_week', 'tag', 'rating', 'cost', 'parking_type', 'alias',
                              'photos', 'indoor_info', 'navi_info', 'children_info',
                              'keywords', 'search_region', 'created_at']
                    
                    complete_poi_data = dict(zip(columns, result))
                    # 添加id字段以保持兼容性
                    complete_poi_data['id'] = complete_poi_data.get('poi_id', '')
                    cursor.close()
                    return is_new, complete_poi_data
            
            cursor.close()
            # 如果没有查询到数据，返回原始POI数据
            return is_new, poi_data
            
        except Exception as e:
            try:
                # 输出完整的SQL与参数拼接后的效果，便于定位语法错误
                if 'cursor' in locals() and cursor:
                    debug_sql = cursor.mogrify(sql, values)
                    print("执行的SQL预览:", debug_sql)
            except Exception as e2:
                print(f"生成SQL预览失败: {e2}")
            print(f"保存POI数据失败: {e}")
            return False, None

    def save_api_key(self, user_id: int, api_key: str) -> bool:
        """保存API Key"""
        if not self.connection:
            return False
            
        try:
            cursor = self.connection.cursor()
            
            # 检查是否已存在相同的API Key（避免重复保存）
            duplicate_check_sql = "SELECT id FROM api_keys WHERE api_key = %s"
            cursor.execute(duplicate_check_sql, (api_key,))
            duplicate = cursor.fetchone()
            
            if duplicate:
                cursor.close()
                return True  # API Key已存在，不需要重复保存
            
            # 检查是否已存在该用户的API Key
            check_sql = "SELECT id FROM api_keys WHERE user_id = %s"
            cursor.execute(check_sql, (user_id,))
            existing = cursor.fetchone()
            
            if existing:
                # 更新现有记录
                update_sql = "UPDATE api_keys SET api_key = %s, updated_at = NOW() WHERE user_id = %s"
                cursor.execute(update_sql, (api_key, user_id))
            else:
                # 插入新记录
                insert_sql = "INSERT INTO api_keys (user_id, api_key, created_at, updated_at) VALUES (%s, %s, NOW(), NOW())"
                cursor.execute(insert_sql, (user_id, api_key))
            
            cursor.close()
            return True
            
        except Exception as e:
            print(f"保存API Key失败: {e}")
            return False
    
    def get_api_key(self, user_id: int) -> Optional[str]:
        """获取用户的API Key"""
        if not self.connection:
            return None
            
        try:
            cursor = self.connection.cursor()
            sql = "SELECT api_key FROM api_keys WHERE user_id = %s"
            cursor.execute(sql, (user_id,))
            result = cursor.fetchone()
            cursor.close()
            
            if result:
                return result[0]
            return None
            
        except Exception as e:
            print(f"获取API Key失败: {e}")
            return None
    
    def get_user_pois(self, user_id: int, limit: int = 1000) -> List[Dict[str, Any]]:
        """获取用户的POI数据"""
        if not self.connection:
            return []
            
        try:
            cursor = self.connection.cursor()
            sql = """
            SELECT poi_id, name, type, address, location, longitude, latitude,
                   pname, cityname, adname, tel, tag, rating, cost, 
                   keywords, search_region, created_at
            FROM poi_data 
            WHERE user_id = %s 
            ORDER BY created_at DESC 
            LIMIT %s
            """
            cursor.execute(sql, (user_id, limit))
            results = cursor.fetchall()
            cursor.close()
            
            # 转换为字典列表
            columns = ['poi_id', 'name', 'type', 'address', 'location', 'longitude', 'latitude',
                      'pname', 'cityname', 'adname', 'tel', 'tag', 'rating', 'cost',
                      'keywords', 'search_region', 'created_at']
            
            return [dict(zip(columns, row)) for row in results]
            
        except Exception as e:
            print(f"获取POI数据失败: {e}")
            return []


class AmapAPI:
    """高德地图API接口"""
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.base_url = 'https://restapi.amap.com'
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        })

    def get_regions(self, region_type: str, parent: str = '') -> List[Dict]:
        """获取行政区域信息"""
        try:
            if region_type == 'province':
                url = f"{self.base_url}/v3/config/district"
                params = {
                    'keywords': '中华人民共和国',
                    'subdistrict': 1,
                    'key': self.api_key
                }
            else:
                url = f"{self.base_url}/v3/config/district"
                params = {
                    'keywords': parent,
                    'subdistrict': 1,
                    'key': self.api_key
                }

            response = self.session.get(url, params=params, timeout=10)
            data = response.json()
            
            if data.get('status') == '1':
                districts = data.get('districts', [])
                if districts:
                    return districts[0].get('districts', [])
            return []
            
        except Exception as e:
            print(f"获取区域信息失败: {e}")
            return []

    def search_around_point(self, location: str, radius: int, keywords: str = '') -> List[Dict]:
        """周边搜索POI - 与网站系统完全一致的分页实现"""
        radius_in_meters = radius * 1000  # 转换为米
        all_pois = []
        page = 1
        page_size = 25  # 每页最大25条
        max_pages = 8   # 最大页数限制（25条/页 × 8页 = 200条，符合高德API限制）
        
        try:
            while page <= max_pages:
                url = f"{self.base_url}/v3/place/around"
                params = {
                    'location': location,
                    'radius': radius_in_meters,
                    'key': self.api_key,
                    'offset': page_size,
                    'page': page,
                    'extensions': 'all'
                }
                
                if keywords:
                    params['keywords'] = keywords
                
                response = self.session.get(url, params=params, timeout=10)
                data = response.json()
                
                if data.get('status') != '1':
                    if page == 1:
                        # 第一页失败，抛出错误
                        raise Exception(data.get('info', 'POI搜索失败'))
                    else:
                        # 后续页面失败，可能是没有更多数据，停止搜索
                        print(f"第{page}页搜索失败，可能已无更多数据: {data.get('info', '未知错误')}")
                        break
                
                pois = data.get('pois', [])
                if not pois:
                    # 没有更多数据，停止搜索
                    break
                
                all_pois.extend(pois)
                print(f"第{page}页获取到{len(pois)}个POI，累计{len(all_pois)}个")
                
                # 如果返回的POI数量少于page_size，说明已经是最后一页
                if len(pois) < page_size:
                    break
                
                page += 1
                
                # 添加延迟以避免频率限制
                if page <= max_pages:
                    time.sleep(0.2)  # 200ms延迟，与网站系统一致
                
            print(f"位置{location}周边搜索完成，共获取{len(all_pois)}个POI，搜索了{page-1}页")
            return all_pois
            
        except Exception as e:
            print(f"高德地图POI搜索失败: {e}")
            raise e

    def get_district_bounds(self, district_name: str) -> Optional[Dict]:
        """获取行政区域边界"""
        try:
            url = f"{self.base_url}/v3/config/district"
            params = {
                'keywords': district_name,
                'subdistrict': 0,
                'extensions': 'all',
                'key': self.api_key
            }
            
            response = self.session.get(url, params=params, timeout=10)
            data = response.json()
            
            if data.get('status') == '1':
                districts = data.get('districts', [])
                if districts:
                    district = districts[0]
                    polyline = district.get('polyline', '')
                    if polyline:
                        # 解析边界坐标
                        coords = []
                        for line in polyline.split('|'):
                            points = line.split(';')
                            for point in points:
                                if ',' in point:
                                    lng, lat = map(float, point.split(','))
                                    coords.append([lng, lat])
                        
                        if coords:
                            lngs = [coord[0] for coord in coords]
                            lats = [coord[1] for coord in coords]
                            return {
                                'west': min(lngs),
                                'east': max(lngs),
                                'south': min(lats),
                                'north': max(lats),
                                'center': district.get('center', '').split(',') if district.get('center') else None
                            }
                        else:
                            print(f"获取区域边界失败: 解析坐标为空, query={district_name}, info={data.get('info')}, infocode={data.get('infocode')}")
                            return None
                    else:
                        print(f"获取区域边界失败: polyline为空, query={district_name}, info={data.get('info')}, infocode={data.get('infocode')}")
                        return None
                else:
                    print(f"获取区域边界失败: districts为空, query={district_name}, info={data.get('info')}, infocode={data.get('infocode')}")
                    return None
            else:
                print(f"获取区域边界失败: status!=1, query={district_name}, info={data.get('info')}, infocode={data.get('infocode')}")
                return None
            
        except Exception as e:
            print(f"获取区域边界失败: {e}")
            return None


class CollectionWorker(QThread):
    """数据采集工作线程"""
    progress_updated = pyqtSignal(int, int, int)  # 当前进度, 总数, 成功数
    log_message = pyqtSignal(str, str)  # 消息, 级别
    finished = pyqtSignal()
    poi_collected = pyqtSignal(dict)  # 新增信号，用于实时更新POI数据
    
    def __init__(self, api_key, province, city, district, radius, keywords, user_id=1, admin_code=None):
        super().__init__()
        self.api_key = api_key
        self.province = province
        self.city = city
        self.district = district
        self.radius = radius
        self.keywords = keywords
        self.user_id = user_id
        self.is_running = False
        self.db_manager = DatabaseManager()
        self.amap_api = AmapAPI(api_key)
        self.admin_code = admin_code
        
    def run(self):
        """执行数据采集"""
        self.is_running = True
        
        # 连接数据存储
        if not self.db_manager.connect():
            self.log_message.emit("连接失败", "error")
            self.finished.emit()
            return
        
        self.log_message.emit("开始数据采集...", "info")
        
        try:
            # 构建搜索区域名称
            search_region = f"{self.province} {self.city}"
            if self.district and self.district != "全部":
                search_region += f" {self.district}"
                district_name = f"{self.city}{self.district}"
            else:
                district_name = self.city
            
            # 获取区域边界
            if self.admin_code:
                self.log_message.emit(f"优先使用adcode获取区域边界: {self.admin_code}", "info")
                bounds = self.amap_api.get_district_bounds(self.admin_code)
                if not bounds:
                    self.log_message.emit(f"adcode查询失败，回退到名称查询: {district_name}", "warning")
                    bounds = self.amap_api.get_district_bounds(district_name)
            else:
                self.log_message.emit(f"使用名称获取区域边界: {district_name}", "info")
                bounds = self.amap_api.get_district_bounds(district_name)
            
            if not bounds:
                self.log_message.emit("获取区域边界失败", "error")
                self.finished.emit()
                return
            
            # 生成搜索网格点
            grid_points = self.generate_grid_points(bounds, self.radius)
            total_points = len(grid_points)
            
            self.log_message.emit(f"生成{total_points}个搜索点", "info")
            
            total_collected = 0
            successful_saves = 0
            all_pois = []  # 收集所有POI用于去重
            
            # 第一阶段：搜索所有POI（不保存）
            for i, point in enumerate(grid_points):
                if not self.is_running:
                    break
                
                location = point['location']  # 直接使用生成的location字段
                self.log_message.emit(f"搜索点 {i+1}/{total_points}: {location}", "info")
                
                # 搜索POI
                pois = self.amap_api.search_around_point(location, self.radius, self.keywords)
                
                if pois:
                    self.log_message.emit(f"找到{len(pois)}个POI", "success")
                    all_pois.extend(pois)
                    total_collected += len(pois)
                
                # 更新搜索进度（前50%）
                progress = int((i + 1) / total_points * 50)
                self.progress_updated.emit(progress, total_collected, 0)
                
                # 避免频率限制
                time.sleep(0.5)
            
            # 第二阶段：去重POI
            if not self.is_running:
                return
                
            self.log_message.emit(f"搜索完成，共找到{len(all_pois)}个POI，正在去重...", "info")
            unique_pois = self.deduplicate_pois(all_pois)
            self.log_message.emit(f"去重完成，共{len(unique_pois)}个唯一POI", "success")
            
            # 第三阶段：保存POI数据并显示在界面上
            total_unique = len(unique_pois)
            displayed_count = 0  # 界面显示的POI计数
            
            for i, poi in enumerate(unique_pois):
                if not self.is_running:
                    break
                
                # 保存POI数据，无论是否为新数据都获取完整信息
                save_result = self.db_manager.save_poi_data(poi, self.keywords, search_region, self.user_id)
                
                if save_result and len(save_result) == 2:
                    is_new, complete_poi_data = save_result
                    if is_new:
                        successful_saves += 1
                    
                    # 无论是否为新POI，都发出信号显示在界面上
                    if complete_poi_data:
                        self.poi_collected.emit(complete_poi_data)
                        displayed_count += 1
                
                # 更新保存进度（后50%）
                progress = 50 + int((i + 1) / total_unique * 50)
                self.progress_updated.emit(progress, len(unique_pois), displayed_count)
                
                # 避免频率限制
                time.sleep(0.1)
            
            self.log_message.emit(f"采集完成！共发现{total_collected}个POI，去重后{len(unique_pois)}个，界面显示{displayed_count}个", "success")
            
        except Exception as e:
            self.log_message.emit(f"采集过程出错: {str(e)}", "error")
        
        finally:
            self.db_manager.disconnect()
            self.finished.emit()
    
    def generate_grid_points(self, bounds, radius_km):
        """生成网格搜索点 - 与网站系统完全一致的算法"""
        # 将搜索半径从公里转换为米
        radius_meters = radius_km * 1000
        
        # 计算区域的实际大小（度）
        lat_diff = bounds['north'] - bounds['south']
        lng_diff = bounds['east'] - bounds['west']
        
        # 计算区域的实际距离（米）
        avg_lat = (bounds['north'] + bounds['south']) / 2
        lat_distance_meters = lat_diff * 111000  # 纬度1度≈111km
        lng_distance_meters = lng_diff * 111000 * math.cos(math.radians(avg_lat))  # 经度需要纬度修正
        
        # 计算最优网格间距：使用六边形最优覆盖模式
        # 相邻圆心距离 = 半径 * √3 ≈ 半径 * 1.732，确保完全覆盖
        optimal_spacing = radius_meters * 1.732
        
        # 计算每个方向需要的点数
        lat_steps = max(1, math.ceil(lat_distance_meters / optimal_spacing))
        lng_steps = max(1, math.ceil(lng_distance_meters / optimal_spacing))
        
        points = []
        
        # 生成搜索中心点（与网站系统保持一致）
        for i in range(lat_steps + 1):
            for j in range(lng_steps + 1):
                # 六边形排列：奇数行偏移半个间距，提高覆盖效率
                lng_offset = 0.5 if i % 2 == 1 else 0
                
                lat = bounds['south'] + (lat_distance_meters / lat_steps) * i / 111000
                lng = bounds['west'] + (lng_distance_meters / lng_steps) * (j + lng_offset) / (111000 * math.cos(math.radians(avg_lat)))
                
                # 确保点在区域边界内
                if (lat >= bounds['south'] and lat <= bounds['north'] and 
                    lng >= bounds['west'] and lng <= bounds['east']):
                    points.append({
                        'lat': lat,
                        'lng': lng,
                        'location': f"{lng},{lat}",  # 与网站系统格式一致
                        'radius_km': radius_km
                    })
        
        return points
    
    def deduplicate_pois(self, pois):
        """POI去重 - 与网站系统保持一致"""
        if not isinstance(pois, list):
            return []
        
        seen = set()
        unique_pois = []
        
        for poi in pois:
            # 使用POI ID作为主要去重键，如果没有ID则使用名称+位置
            key = poi.get('id') or f"{poi.get('name', '')}_{poi.get('location', '')}"
            
            if key not in seen:
                seen.add(key)
                unique_pois.append(poi)
        
        return unique_pois
    
    def stop(self):
        """停止采集"""
        self.is_running = False


class MainWindow(QMainWindow):
    """主窗口"""
    def __init__(self):
        super().__init__()
        self.collection_worker = None
        self.amap_api = None
        self.db_manager = DatabaseManager()
        
        # 进行用户认证
        self.user_info = None
        if not self.authenticate_user():
            sys.exit(0)  # 认证失败，退出程序
        
        # 从认证信息中获取用户ID
        self.user_id = self.user_info.get('user_id', 1) if self.user_info else 1
        
        self.init_ui()
        self.setup_styles()
        self.update_title_with_user_info()
        
    def init_ui(self):
        """初始化紧凑型现代化界面"""
        self.setWindowTitle("高德地图POI采集工具")
        self.setGeometry(100, 100, 1200, 700)
        self.setMinimumSize(1000, 600)
        
        try:
            self.setWindowIcon(QIcon(resource_path("icon.ico")))
        except:
            pass
        
        # 创建菜单栏
        self.create_menu_bar()
        
        # 创建主窗口部件
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        
        # 主布局 - 紧凑型布局
        main_layout = QHBoxLayout(central_widget)
        main_layout.setSpacing(5)
        main_layout.setContentsMargins(5, 5, 5, 5)
        
        # 左侧控制面板 - 紧凑型
        left_panel = self.create_compact_left_panel()
        left_panel.setFixedWidth(350)
        
        # 右侧数据显示区域
        right_panel = self.create_modern_right_panel()
        
        main_layout.addWidget(left_panel)
        main_layout.addWidget(right_panel)
        
    def create_menu_bar(self):
        """创建菜单栏"""
        menubar = self.menuBar()
        
        # 使用说明菜单
        usage_action = QAction("使用说明", self)
        usage_action.triggered.connect(self.show_usage_instructions)
        menubar.addAction(usage_action)
        
        # 更多工具菜单
        tools_action = QAction("更多工具", self)
        tools_action.triggered.connect(self.open_more_tools)
        menubar.addAction(tools_action)
    
    def create_config_card(self, parent_layout):
        """创建配置卡片"""
        card = self.create_card("⚙️ 参数配置")
        layout = QVBoxLayout(card)
        
        # API Key输入 - 重新设计布局
        api_section_layout = QVBoxLayout()
        api_section_layout.setSpacing(6)
        
        # API Key标签和申请链接行
        api_header_layout = QHBoxLayout()
        api_header_layout.setContentsMargins(0, 0, 0, 0)
        api_header_layout.addWidget(QLabel("API Key:"))
        
        apply_link = QLabel('<a href="https://console.amap.com/dev/key/app" style="color: #ffffff; text-decoration: none; font-size: 11px;">申请API Key</a>')
        apply_link.setOpenExternalLinks(True)
        apply_link.setToolTip("点击前往高德地图开放平台申请API Key")
        api_header_layout.addWidget(apply_link)
        help_link = QLabel('<a href="https://tools.yikeaigc.com/blog/shuoming/22.html" style="color: #ffffff; text-decoration: none; font-size: 11px;">使用说明</a>')
        help_link.setOpenExternalLinks(True)
        help_link.setToolTip("点击查看使用说明")
        api_header_layout.addWidget(help_link)
        api_header_layout.addStretch()
        
        api_section_layout.addLayout(api_header_layout)
        
        # API Key输入框和保存按钮行
        api_input_layout = QHBoxLayout()
        self.api_key_input = QLineEdit()
        self.api_key_input.setPlaceholderText("请输入32位高德地图API Key")
        self.api_key_input.textChanged.connect(self.on_api_key_changed)
        api_input_layout.addWidget(self.api_key_input)
        
        self.save_api_key_btn = QPushButton("保存")
        self.save_api_key_btn.setMaximumWidth(60)
        self.save_api_key_btn.clicked.connect(self.save_api_key)
        api_input_layout.addWidget(self.save_api_key_btn)
        
        api_section_layout.addLayout(api_input_layout)
        layout.addLayout(api_section_layout)
        
        # 区域选择
        region_layout = QHBoxLayout()
        region_layout.addWidget(QLabel("省份:"))
        self.province_combo = QComboBox()
        self.province_combo.currentTextChanged.connect(self.on_province_changed)
        region_layout.addWidget(self.province_combo)
        
        region_layout.addWidget(QLabel("城市:"))
        self.city_combo = QComboBox()
        self.city_combo.currentTextChanged.connect(self.on_city_changed)
        region_layout.addWidget(self.city_combo)
        
        region_layout.addWidget(QLabel("区县:"))
        self.district_combo = QComboBox()
        region_layout.addWidget(self.district_combo)
        layout.addLayout(region_layout)
        
        # 搜索范围
        radius_layout = QHBoxLayout()
        radius_layout.addWidget(QLabel("搜索范围:"))
        self.radius_slider = QSlider(Qt.Horizontal)
        self.radius_slider.setRange(1, 50)
        self.radius_slider.setValue(10)
        self.radius_slider.valueChanged.connect(self.update_radius_label)
        radius_layout.addWidget(self.radius_slider)
        
        self.radius_label = QLabel("10公里")
        radius_layout.addWidget(self.radius_label)
        layout.addLayout(radius_layout)
        
        # 关键词输入
        keyword_layout = QHBoxLayout()
        keyword_layout.addWidget(QLabel("关键词:"))
        self.keywords_input = QLineEdit()
        self.keywords_input.setPlaceholderText("请输入搜索关键词（如：餐厅、酒店、加油站等）")
        keyword_layout.addWidget(self.keywords_input)
        layout.addLayout(keyword_layout)
        
        parent_layout.addWidget(card)
    
    def create_control_card(self, parent_layout):
        """创建控制卡片"""
        card = self.create_card("🎮 操作控制")
        layout = QHBoxLayout(card)
        
        # 开始按钮
        self.start_btn = QPushButton("开始采集")
        self.start_btn.clicked.connect(self.start_collection)
        self.start_btn.setObjectName("startButton")
        layout.addWidget(self.start_btn)
        
        # 停止按钮
        self.stop_btn = QPushButton("停止采集")
        self.stop_btn.clicked.connect(self.stop_collection)
        self.stop_btn.setEnabled(False)
        self.stop_btn.setObjectName("stopButton")
        layout.addWidget(self.stop_btn)
        
        # 导出按钮
        self.export_csv_btn = QPushButton("导出CSV")
        self.export_csv_btn.clicked.connect(self.export_csv)
        self.export_csv_btn.setObjectName("exportButton")
        layout.addWidget(self.export_csv_btn)
        
        parent_layout.addWidget(card)
    
    def create_progress_card(self, parent_layout):
        """创建进度和日志卡片"""
        card = self.create_card("📊 进度监控")
        layout = QVBoxLayout(card)
        
        # 状态和进度条
        progress_layout = QHBoxLayout()
        
        self.status_label = QLabel("就绪")
        self.status_label.setObjectName("statusLabel")
        progress_layout.addWidget(self.status_label)
        
        self.progress_bar = QProgressBar()
        self.progress_bar.setRange(0, 100)
        progress_layout.addWidget(self.progress_bar)
        
        layout.addLayout(progress_layout)
        
        # 统计信息
        stats_layout = QHBoxLayout()
        self.stats_label = QLabel("总数: 0")
        stats_layout.addWidget(self.stats_label)
        layout.addLayout(stats_layout)
        
        # 日志显示
        log_label = QLabel("📝 采集日志:")
        layout.addWidget(log_label)
        
        self.log_text = QTextEdit()
        self.log_text.setMaximumHeight(200)
        self.log_text.setObjectName("logText")
        layout.addWidget(self.log_text)
        
        parent_layout.addWidget(card)
    
    def create_data_card(self, parent_layout):
        """创建数据列表卡片"""
        card = self.create_card("📊 数据列表")
        layout = QVBoxLayout(card)
        
        # 数据统计
        stats_layout = QHBoxLayout()
        self.total_count_label = QLabel("总计: 0 条")
        self.selected_count_label = QLabel("已选: 0 条")
        stats_layout.addWidget(self.total_count_label)
        stats_layout.addWidget(self.selected_count_label)
        stats_layout.addStretch()
        
        # 已移除刷新数据按钮，只显示当次采集的POI
        
        layout.addLayout(stats_layout)
        
        # POI数据表格
        self.poi_table = QTableWidget()
        self.setup_poi_table()
        layout.addWidget(self.poi_table)
        
        parent_layout.addWidget(card)
    
    def create_card(self, title):
        """创建卡片容器"""
        card = QGroupBox(title)
        card.setObjectName("card")
        return card
    
    def create_compact_left_panel(self):
        """创建紧凑型左侧控制面板"""
        panel = QWidget()
        panel.setObjectName("leftPanel")
        layout = QVBoxLayout(panel)
        layout.setSpacing(8)
        layout.setContentsMargins(10, 10, 10, 10)
        
        # 标题区域 - 紧凑型
        title_label = QLabel("POI采集工具")
        title_label.setObjectName("compactTitle")
        layout.addWidget(title_label)
        
        # 创建紧凑型功能区域
        self.create_compact_config_section(layout)
        self.create_compact_control_section(layout)
        self.create_compact_progress_section(layout)
        
        layout.addStretch()
        
        return panel
    
    def create_compact_config_section(self, layout):
        """创建紧凑型配置区域"""
        group = QGroupBox("配置")
        group.setObjectName("compactGroup")
        form_layout = QVBoxLayout(group)
        form_layout.setSpacing(6)
        form_layout.setContentsMargins(8, 8, 8, 8)
        
        # API Key - 重新设计紧凑布局
        api_header_layout = QHBoxLayout()
        api_header_layout.setSpacing(8)
        api_header_layout.setContentsMargins(0, 0, 0, 2)
        
        api_label = QLabel("API Key:")
        api_label.setObjectName("compactLabel")
        api_header_layout.addWidget(api_label)
        
        apply_link = QLabel('<a href="https://console.amap.com/dev/key/app" style="color: #ffffff; text-decoration: none; font-size: 10px;">申请</a>')
        apply_link.setOpenExternalLinks(True)
        apply_link.setToolTip("点击前往高德地图开放平台申请API Key")
        api_header_layout.addWidget(apply_link)
        help_link = QLabel('<a href="https://tools.yikeaigc.com/blog/shuoming/22.html" style="color: #ffffff; text-decoration: none; font-size: 10px;">使用说明</a>')
        help_link.setOpenExternalLinks(True)
        help_link.setToolTip("点击查看使用说明")
        api_header_layout.addWidget(help_link)
        api_header_layout.addStretch()
        
        self.api_key_input = QLineEdit()
        self.api_key_input.setObjectName("compactInput")
        self.api_key_input.setPlaceholderText("请输入高德地图API Key")
        self.api_key_input.textChanged.connect(self.on_api_key_changed)
        
        # 省市区选择
        location_label = QLabel("地区选择:")
        location_label.setObjectName("compactLabel")
        self.province_combo = QComboBox()
        self.province_combo.setObjectName("compactCombo")
        self.province_combo.currentTextChanged.connect(self.on_province_changed)
        self.city_combo = QComboBox()
        self.city_combo.setObjectName("compactCombo")
        self.city_combo.currentTextChanged.connect(self.on_city_changed)
        self.district_combo = QComboBox()
        self.district_combo.setObjectName("compactCombo")
        
        # 关键词
        keyword_label = QLabel("关键词:")
        keyword_label.setObjectName("compactLabel")
        self.keywords_input = QLineEdit()
        self.keywords_input.setObjectName("compactInput")
        self.keywords_input.setPlaceholderText("如：餐厅|酒店|银行")
        
        # 搜索半径
        radius_label = QLabel("搜索半径:")
        radius_label.setObjectName("compactLabel")
        self.radius_slider = QSlider(Qt.Horizontal)
        self.radius_slider.setRange(1, 50)
        self.radius_slider.setValue(3)
        self.radius_slider.setObjectName("compactSlider")
        self.radius_label = QLabel("3公里")
        self.radius_label.setObjectName("compactValueLabel")
        self.radius_slider.valueChanged.connect(self.update_radius_label)
        
        # 添加控件
        form_layout.addLayout(api_header_layout)
        form_layout.addWidget(self.api_key_input)
        form_layout.addWidget(location_label)
        form_layout.addWidget(self.province_combo)
        form_layout.addWidget(self.city_combo)
        form_layout.addWidget(self.district_combo)
        form_layout.addWidget(keyword_label)
        form_layout.addWidget(self.keywords_input)
        form_layout.addWidget(radius_label)
        
        radius_container = QHBoxLayout()
        radius_container.addWidget(self.radius_slider)
        radius_container.addWidget(self.radius_label)
        form_layout.addLayout(radius_container)
        
        layout.addWidget(group)
    
    def create_compact_control_section(self, layout):
        """创建紧凑型控制区域"""
        group = QGroupBox("控制")
        group.setObjectName("compactGroup")
        control_layout = QVBoxLayout(group)
        control_layout.setSpacing(6)
        control_layout.setContentsMargins(8, 8, 8, 8)
        
        # 按钮布局
        button_layout = QHBoxLayout()
        
        self.start_button = QPushButton("开始采集")
        self.start_button.setObjectName("compactStartButton")
        self.start_button.clicked.connect(self.start_collection)
        
        self.stop_button = QPushButton("停止")
        self.stop_button.setObjectName("compactStopButton")
        self.stop_button.clicked.connect(self.stop_collection)
        self.stop_button.setEnabled(False)
        
        button_layout.addWidget(self.start_button)
        button_layout.addWidget(self.stop_button)
        
        control_layout.addLayout(button_layout)
        layout.addWidget(group)
    
    def create_compact_progress_section(self, layout):
        """创建紧凑型进度区域"""
        group = QGroupBox("进度")
        group.setObjectName("compactGroup")
        progress_layout = QVBoxLayout(group)
        progress_layout.setSpacing(6)
        progress_layout.setContentsMargins(8, 8, 8, 8)
        
        # 进度条
        self.progress_bar = QProgressBar()
        self.progress_bar.setObjectName("compactProgress")
        self.progress_bar.setValue(0)
        
        # 状态标签
        self.status_label = QLabel("就绪")
        self.status_label.setObjectName("compactStatusLabel")
        
        # 统计信息
        self.stats_label = QLabel("总数: 0")
        self.stats_label.setObjectName("compactStatusLabel")
        
        # 日志文本框
        self.log_text = QTextEdit()
        self.log_text.setObjectName("logText")
        self.log_text.setMaximumHeight(120)
        self.log_text.setPlaceholderText("运行日志...")
        
        progress_layout.addWidget(self.progress_bar)
        progress_layout.addWidget(self.status_label)
        progress_layout.addWidget(self.stats_label)
        progress_layout.addWidget(self.log_text)
        
        layout.addWidget(group)
    
    def create_modern_right_panel(self):
        """创建现代化右侧数据面板"""
        panel = QWidget()
        panel.setObjectName("rightPanel")
        layout = QVBoxLayout(panel)
        layout.setSpacing(0)
        layout.setContentsMargins(0, 0, 0, 0)
        
        # 标题栏
        title_widget = QWidget()
        title_widget.setObjectName("rightTitleBar")
        title_layout = QHBoxLayout(title_widget)
        title_layout.setContentsMargins(30, 25, 30, 25)
        
        title_label = QLabel("数据管理")
        title_label.setObjectName("rightTitleLabel")
        title_layout.addWidget(title_label)
        
        # 导出按钮
        export_btn = QPushButton("导出数据")
        export_btn.setObjectName("exportButton")
        export_btn.clicked.connect(self.export_csv)
        title_layout.addWidget(export_btn)
        
        layout.addWidget(title_widget)
        
        # 数据表格区域
        self.create_modern_data_card(layout)
        
        return panel
    
    def setup_styles(self):
        """设置现代化样式"""
        style = """
            /* 主窗口样式 - 深色主题 */
            QMainWindow {
                background: qlineargradient(x1:0, y1:0, x2:1, y2:1, 
                    stop:0 #1a1a1a, stop:1 #2d2d2d);
                color: #ffffff;
            }
            
            /* 左侧面板 - 深色主题 */
            QWidget#leftPanel {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, 
                    stop:0 #1e293b, stop:1 #0f172a);
                border-right: 2px solid #334155;
            }
            
            QWidget#titleBar {
                background: qlineargradient(x1:0, y1:0, x2:1, y2:0, 
                    stop:0 #3b82f6, stop:1 #1d4ed8);
                border: none;
            }
            
            QLabel#titleLabel {
                color: white;
                font-size: 18px;
                font-weight: bold;
                font-family: 'Microsoft YaHei', sans-serif;
            }
            
            QScrollArea#leftScroll {
                background: transparent;
                border: none;
            }
            
            QScrollArea#leftScroll QScrollBar:vertical {
                background: #334155;
                width: 8px;
                border-radius: 4px;
            }
            
            QScrollArea#leftScroll QScrollBar::handle:vertical {
                background: #64748b;
                border-radius: 4px;
                min-height: 20px;
            }
            
            QScrollArea#leftScroll QScrollBar::handle:vertical:hover {
                background: #94a3b8;
            }
            
            /* 右侧面板 - 深色主题 */
            QWidget#rightPanel {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, 
                    stop:0 #2d2d2d, stop:1 #1a1a1a);
            }
            
            QWidget#rightTitleBar {
                background: qlineargradient(x1:0, y1:0, x2:1, y2:0, 
                    stop:0 #374151, stop:1 #1f2937);
                border-bottom: 1px solid #4b5563;
            }
            
            QLabel#rightTitleLabel {
                color: #ffffff;
                font-size: 18px;
                font-weight: bold;
                font-family: 'Microsoft YaHei', sans-serif;
            }
            
            QPushButton#exportButton {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, 
                    stop:0 #10b981, stop:1 #059669);
                color: white;
                border: none;
                border-radius: 8px;
                padding: 10px 20px;
                font-size: 14px;
                font-weight: bold;
            }
            
            QPushButton#exportButton:hover {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, 
                    stop:0 #059669, stop:1 #047857);
            }
            
            QPushButton#exportButton:pressed {
                background: #047857;
            }
            
            /* 现代化卡片样式 */
            QGroupBox#modernCard {
                background: rgba(255, 255, 255, 0.1);
                border: 1px solid rgba(255, 255, 255, 0.2);
                border-radius: 12px;
                margin-top: 15px;
                padding-top: 20px;
                font-weight: bold;
                font-size: 16px;
                color: white;
            }
            
            QGroupBox#modernCard::title {
                subcontrol-origin: margin;
                left: 15px;
                padding: 0 10px 0 10px;
                color: #e2e8f0;
            }
            
            /* 左侧面板内的所有标签使用白色文字 */
            QWidget#leftPanel QLabel {
                color: #ffffff;
            }
            
            /* 右侧面板内的所有标签使用白色文字 */
            QWidget#rightPanel QLabel {
                color: #ffffff;
            }
            
            /* 卡片内的标签文字颜色 */
            QGroupBox#modernCard QLabel {
                color: #ffffff;
            }
            
            /* 输入框样式 - 深色主题 */
            QLineEdit {
                background: rgba(55, 65, 81, 0.9);
                border: 2px solid transparent;
                border-radius: 8px;
                padding: 12px 15px;
                font-size: 14px;
                color: #ffffff;
            }
            
            QLineEdit:focus {
                border-color: #3b82f6;
                background: rgba(75, 85, 99, 0.9);
                /* box-shadow removed: not supported in Qt style sheets */
            }
            
            /* 下拉框样式 - 深色主题 */
            QComboBox {
                background: rgba(55, 65, 81, 0.9);
                border: 2px solid transparent;
                border-radius: 8px;
                padding: 12px 15px;
                font-size: 14px;
                color: #ffffff;
                min-width: 120px;
            }
            
            QComboBox:focus {
                border-color: #3b82f6;
                background: rgba(75, 85, 99, 0.9);
            }
            
            QComboBox::drop-down {
                border: none;
                width: 30px;
            }
            
            QComboBox::down-arrow {
                image: none;
                border: 5px solid transparent;
                border-top: 8px solid #ffffff;
                width: 0;
                height: 0;
            }
            
            /* 按钮样式 */
            QPushButton#startButton {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, 
                    stop:0 #3b82f6, stop:1 #1d4ed8);
                color: white;
                border: none;
                border-radius: 10px;
                padding: 15px 25px;
                font-size: 16px;
                font-weight: bold;
                min-height: 20px;
            }
            
            QPushButton#startButton:hover {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, 
                    stop:0 #2563eb, stop:1 #1e40af);
                transform: translateY(-2px);
            }
            
            QPushButton#startButton:pressed {
                background: #1e40af;
                transform: translateY(0px);
            }
            
            QPushButton#stopButton {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, 
                    stop:0 #ef4444, stop:1 #dc2626);
                color: white;
                border: none;
                border-radius: 10px;
                padding: 15px 25px;
                font-size: 16px;
                font-weight: bold;
                min-height: 20px;
            }
            
            QPushButton#stopButton:hover {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, 
                    stop:0 #dc2626, stop:1 #b91c1c);
            }
            
            QPushButton#saveApiButton {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, 
                    stop:0 #10b981, stop:1 #059669);
                color: white;
                border: none;
                border-radius: 8px;
                padding: 10px 20px;
                font-size: 14px;
                font-weight: bold;
            }
            
            QPushButton#saveApiButton:hover {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, 
                    stop:0 #059669, stop:1 #047857);
            }
            
            /* 滑块样式 */
            QSlider::groove:horizontal {
                border: none;
                height: 8px;
                background: rgba(255, 255, 255, 0.3);
                border-radius: 4px;
            }
            
            QSlider::handle:horizontal {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, 
                    stop:0 #3b82f6, stop:1 #1d4ed8);
                border: 2px solid white;
                width: 20px;
                height: 20px;
                border-radius: 12px;
                margin: -8px 0;
            }
            
            QSlider::handle:horizontal:hover {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, 
                    stop:0 #2563eb, stop:1 #1e40af);
            }
            
            QSlider::sub-page:horizontal {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, 
                    stop:0 #3b82f6, stop:1 #1d4ed8);
                border-radius: 4px;
            }
            
            /* 进度条样式 */
            QProgressBar {
                background: rgba(255, 255, 255, 0.2);
                border: none;
                border-radius: 8px;
                text-align: center;
                font-size: 14px;
                font-weight: bold;
                color: white;
                min-height: 16px;
            }
            
            QProgressBar::chunk {
                background: qlineargradient(x1:0, y1:0, x2:1, y2:0, 
                    stop:0 #10b981, stop:1 #059669);
                border-radius: 8px;
            }
            
            /* 标签样式 */
            QLabel {
                color: #e2e8f0;
                font-size: 14px;
                font-family: 'Microsoft YaHei', sans-serif;
            }
            
            QLabel#valueLabel {
                color: #3b82f6;
                font-weight: bold;
                font-size: 16px;
            }
            
            /* 现代化表格样式 */
            QTableWidget {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, 
                    stop:0 #1e293b, stop:1 #0f172a);
                border: 2px solid #334155;
                border-radius: 12px;
                gridline-color: #475569;
                font-size: 13px;
                font-family: 'Microsoft YaHei', 'Segoe UI', sans-serif;
                selection-background-color: rgba(59, 130, 246, 0.3);
                color: #e2e8f0;
                alternate-background-color: rgba(71, 85, 105, 0.2);
            }
            
            QTableWidget::item {
                padding: 12px 8px;
                border: none;
                border-bottom: 1px solid rgba(71, 85, 105, 0.3);
                color: #e2e8f0;
            }
            
            QTableWidget::item:hover {
                background: rgba(59, 130, 246, 0.1);
                color: #ffffff;
            }
            
            QTableWidget::item:selected {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, 
                    stop:0 rgba(59, 130, 246, 0.4), stop:1 rgba(29, 78, 216, 0.4));
                color: #ffffff;
                border: 1px solid #3b82f6;
            }
            
            QHeaderView::section {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, 
                    stop:0 #475569, stop:1 #334155);
                border: none;
                border-right: 1px solid #64748b;
                border-bottom: 3px solid #3b82f6;
                padding: 15px 10px;
                font-weight: bold;
                color: #ffffff;
                font-size: 14px;
                text-align: center;
            }
            
            QHeaderView::section:hover {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, 
                    stop:0 #64748b, stop:1 #475569);
            }
            
            QHeaderView::section:first {
                border-top-left-radius: 10px;
            }
            
            QHeaderView::section:last {
                border-top-right-radius: 10px;
                border-right: none;
            }
            
            /* 序号列特殊样式 */
            QTableWidget::item:first-child {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, 
                    stop:0 rgba(59, 130, 246, 0.05), stop:1 rgba(29, 78, 216, 0.05));
                font-weight: bold;
                color: #3b82f6;
                text-align: center;
            }
            
            QTableWidget::item:first-child:selected {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, 
                    stop:0 rgba(59, 130, 246, 0.6), stop:1 rgba(29, 78, 216, 0.6));
                color: #ffffff;
            }
            
            /* 表格滚动条样式 */
            QTableWidget QScrollBar:vertical {
                background: #334155;
                width: 12px;
                border-radius: 6px;
                margin: 0;
            }
            
            QTableWidget QScrollBar::handle:vertical {
                background: qlineargradient(x1:0, y1:0, x2:1, y2:0, 
                    stop:0 #64748b, stop:1 #475569);
                border-radius: 6px;
                min-height: 30px;
                margin: 2px;
            }
            
            QTableWidget QScrollBar::handle:vertical:hover {
                background: qlineargradient(x1:0, y1:0, x2:1, y2:0, 
                    stop:0 #94a3b8, stop:1 #64748b);
            }
            
            QTableWidget QScrollBar:horizontal {
                background: #334155;
                height: 12px;
                border-radius: 6px;
                margin: 0;
            }
            
            QTableWidget QScrollBar::handle:horizontal {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, 
                    stop:0 #64748b, stop:1 #475569);
                border-radius: 6px;
                min-width: 30px;
                margin: 2px;
            }
            
            QTableWidget QScrollBar::handle:horizontal:hover {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, 
                    stop:0 #94a3b8, stop:1 #64748b);
            }
            
            QTableWidget QScrollBar::add-line, QTableWidget QScrollBar::sub-line {
                border: none;
                background: none;
            }
            
            /* 文本编辑器样式 - 深色主题 */
            QTextEdit {
                background: rgba(55, 65, 81, 0.9);
                border: 1px solid rgba(75, 85, 99, 0.3);
                border-radius: 8px;
                padding: 10px;
                font-size: 13px;
                color: #ffffff;
                font-family: 'Consolas', 'Monaco', monospace;
            }
            
            /* 紧凑型样式 */
            #compactTitle {
                font-size: 16px;
                font-weight: bold;
                color: #ffffff;
                padding: 8px 0px;
                border-bottom: 2px solid #3b82f6;
                margin-bottom: 8px;
            }
            
            QGroupBox#compactGroup {
                font-size: 12px;
                font-weight: bold;
                color: #ffffff;
                border: 1px solid #374151;
                border-radius: 6px;
                margin-top: 8px;
                padding-top: 8px;
                background: rgba(55, 65, 81, 0.3);
            }
            
            QGroupBox#compactGroup::title {
                subcontrol-origin: margin;
                left: 8px;
                padding: 0 4px 0 4px;
                color: #ffffff;
            }
            
            QLabel#compactLabel {
                color: #d1d5db;
                font-size: 11px;
                margin: 2px 0px;
            }
            
            QLineEdit#compactInput {
                padding: 4px 8px;
                border: 1px solid #4b5563;
                border-radius: 4px;
                background: #1f2937;
                color: #ffffff;
                font-size: 11px;
                min-height: 20px;
            }
            
            QLineEdit#compactInput:focus {
                border-color: #3b82f6;
                background: #111827;
            }
            
            QComboBox#compactCombo {
                padding: 4px 8px;
                border: 1px solid #4b5563;
                border-radius: 4px;
                background: #1f2937;
                color: #ffffff;
                font-size: 11px;
                min-height: 20px;
            }
            
            QComboBox#compactCombo:hover {
                border-color: #6b7280;
            }
            
            QComboBox#compactCombo::drop-down {
                border: none;
                width: 20px;
            }
            
            QSlider#compactSlider::groove:horizontal {
                border: 1px solid #4b5563;
                height: 6px;
                background: #374151;
                border-radius: 3px;
            }
            
            QSlider#compactSlider::handle:horizontal {
                background: #3b82f6;
                border: 1px solid #1e40af;
                width: 14px;
                margin: -4px 0;
                border-radius: 7px;
            }
            
            QLabel#compactValueLabel {
                color: #3b82f6;
                font-size: 11px;
                font-weight: bold;
                min-width: 50px;
            }
            
            QPushButton#compactStartButton {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #10b981, stop:1 #059669);
                border: none;
                color: white;
                padding: 6px 12px;
                border-radius: 4px;
                font-size: 11px;
                font-weight: bold;
                min-height: 24px;
            }
            
            QPushButton#compactStartButton:hover {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #059669, stop:1 #047857);
            }
            
            QPushButton#compactStopButton {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #ef4444, stop:1 #dc2626);
                border: none;
                color: white;
                padding: 6px 12px;
                border-radius: 4px;
                font-size: 11px;
                font-weight: bold;
                min-height: 24px;
            }
            
            QPushButton#compactStopButton:hover {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #dc2626, stop:1 #b91c1c);
            }
            
            QProgressBar#compactProgress {
                border: 1px solid #4b5563;
                border-radius: 4px;
                text-align: center;
                background: #374151;
                color: #ffffff;
                font-size: 11px;
                min-height: 16px;
            }
            
            QProgressBar#compactProgress::chunk {
                background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #3b82f6, stop:1 #1d4ed8);
                border-radius: 3px;
            }
            
            QLabel#compactStatusLabel {
                color: #9ca3af;
                font-size: 11px;
                padding: 2px 0px;
            }
        """
        self.setStyleSheet(style)
    
    def create_modern_config_card(self, layout):
        """创建现代化配置卡片"""
        card = QGroupBox("采集配置")
        card.setObjectName("modernCard")
        card_layout = QVBoxLayout(card)
        card_layout.setSpacing(20)
        
        # API Key 配置 - 垂直布局
        api_container_layout = QVBoxLayout()
        api_container_layout.setSpacing(8)
        
        # API Key标签行
        api_label_row = QHBoxLayout()
        api_label_row.setContentsMargins(0, 0, 0, 0)
        api_label_row.addWidget(QLabel("API Key:"))
        
        # 申请链接
        apply_link = QLabel('<a href="https://console.amap.com/dev/key/app" style="color: #ffffff; text-decoration: none; font-size: 12px;">申请API Key</a>')
        apply_link.setOpenExternalLinks(True) 
        apply_link.setToolTip("点击前往高德地图开放平台申请API Key")
        api_label_row.addWidget(apply_link)
        help_link = QLabel('<a href="https://tools.yikeaigc.com/blog/shuoming/22.html" style="color: #ffffff; text-decoration: none; font-size: 12px;">使用说明</a>')
        help_link.setOpenExternalLinks(True)
        help_link.setToolTip("点击查看使用说明")
        api_label_row.addWidget(help_link)
        api_label_row.addStretch()
        
        api_container_layout.addLayout(api_label_row)
        
        # API Key输入框行
        api_input_layout = QHBoxLayout()
        self.api_key_input = QLineEdit()
        self.api_key_input.setPlaceholderText("请输入高德地图API Key")
        api_input_layout.addWidget(self.api_key_input)
        
        save_api_btn = QPushButton("保存")
        save_api_btn.setObjectName("saveApiButton")
        save_api_btn.clicked.connect(self.save_api_key)
        api_input_layout.addWidget(save_api_btn)
        
        api_container_layout.addLayout(api_input_layout)
        card_layout.addLayout(api_container_layout)
        
        # 搜索配置
        search_layout = QFormLayout()
        
        self.keyword_input = QLineEdit()
        self.keyword_input.setPlaceholderText("如：餐厅、酒店、加油站等")
        search_layout.addRow("搜索关键词:", self.keyword_input)
        
        self.city_input = QLineEdit()
        self.city_input.setPlaceholderText("如：北京市、上海市等")
        search_layout.addRow("城市:", self.city_input)
        
        # 搜索半径滑块
        radius_layout = QHBoxLayout()
        self.radius_slider = QSlider(Qt.Horizontal)
        self.radius_slider.setRange(1, 50)
        self.radius_slider.setValue(5)
        self.radius_slider.valueChanged.connect(self.update_radius_label)
        
        self.radius_label = QLabel("5 公里")
        self.radius_label.setObjectName("valueLabel")
        
        radius_layout.addWidget(self.radius_slider)
        radius_layout.addWidget(self.radius_label)
        search_layout.addRow("搜索半径:", radius_layout)
        
        # 延迟设置
        delay_layout = QHBoxLayout()
        self.delay_slider = QSlider(Qt.Horizontal)
        self.delay_slider.setRange(1, 10)
        self.delay_slider.setValue(2)
        self.delay_slider.valueChanged.connect(self.update_delay_label)
        
        self.delay_label = QLabel("2 秒")
        self.delay_label.setObjectName("valueLabel")
        
        delay_layout.addWidget(self.delay_slider)
        delay_layout.addWidget(self.delay_label)
        search_layout.addRow("请求延迟:", delay_layout)
        
        card_layout.addLayout(search_layout)
        layout.addWidget(card)
    
    def create_modern_control_card(self, layout):
        """创建现代化控制卡片"""
        card = QGroupBox("采集控制")
        card.setObjectName("modernCard")
        card_layout = QVBoxLayout(card)
        card_layout.setSpacing(15)
        
        # 数据量预估
        estimate_btn = QPushButton("预估数据量")
        estimate_btn.setObjectName("startButton")
        estimate_btn.clicked.connect(self.calculate_and_confirm_data_amount)
        card_layout.addWidget(estimate_btn)
        
        # 预估信息显示
        self.estimate_info = QTextEdit()
        self.estimate_info.setMaximumHeight(120)
        self.estimate_info.setPlaceholderText("点击预估数据量按钮查看采集信息...")
        card_layout.addWidget(self.estimate_info)
        
        # 控制按钮
        button_layout = QHBoxLayout()
        
        self.start_button = QPushButton("开始采集")
        self.start_button.setObjectName("startButton")
        self.start_button.clicked.connect(self.start_collection)
        
        self.stop_button = QPushButton("停止采集")
        self.stop_button.setObjectName("stopButton")
        self.stop_button.clicked.connect(self.stop_collection)
        self.stop_button.setEnabled(False)
        
        button_layout.addWidget(self.start_button)
        button_layout.addWidget(self.stop_button)
        
        card_layout.addLayout(button_layout)
        layout.addWidget(card)
    
    def create_modern_progress_card(self, layout):
        """创建现代化进度卡片"""
        card = QGroupBox("采集进度")
        card.setObjectName("modernCard")
        card_layout = QVBoxLayout(card)
        card_layout.setSpacing(15)
        
        # 进度条
        self.progress_bar = QProgressBar()
        self.progress_bar.setRange(0, 100)
        self.progress_bar.setValue(0)
        card_layout.addWidget(self.progress_bar)
        
        # 状态信息
        self.status_label = QLabel("等待开始采集...")
        card_layout.addWidget(self.status_label)
        
        # 统计信息
        stats_layout = QFormLayout()
        
        self.total_label = QLabel("0")
        self.total_label.setObjectName("valueLabel")
        stats_layout.addRow("总计划:", self.total_label)
        
        self.completed_label = QLabel("0")
        self.completed_label.setObjectName("valueLabel")
        stats_layout.addRow("已完成:", self.completed_label)
        

        
        card_layout.addLayout(stats_layout)
        layout.addWidget(card)
    
    def create_modern_data_card(self, layout):
        """创建现代化数据卡片"""
        card = QGroupBox("采集数据")
        card.setObjectName("modernCard")
        card_layout = QVBoxLayout(card)
        card_layout.setSpacing(15)
        card_layout.setContentsMargins(20, 25, 20, 20)
        
        # 数据表格
        self.data_table = QTableWidget()
        self.data_table.setColumnCount(16)  # 增加一列用于序号
        self.data_table.setHorizontalHeaderLabels([
            "序号", "POI ID", "名称", "类型", "类型编码", "地址", "省份", "城市", "区县", 
            "经度", "纬度", "电话", "商圈", "标签", "评分", "采集时间"
        ])
        
        # 设置表格属性
        header = self.data_table.horizontalHeader()
        # 设置列宽
        self.data_table.setColumnWidth(0, 60)   # 序号
        self.data_table.setColumnWidth(1, 120)  # POI ID
        self.data_table.setColumnWidth(2, 200)  # 名称
        self.data_table.setColumnWidth(3, 120)  # 类型
        self.data_table.setColumnWidth(4, 100)  # 类型编码
        self.data_table.setColumnWidth(5, 300)  # 地址
        self.data_table.setColumnWidth(6, 80)   # 省份
        self.data_table.setColumnWidth(7, 80)   # 城市
        self.data_table.setColumnWidth(8, 80)   # 区县
        self.data_table.setColumnWidth(9, 100)  # 经度
        self.data_table.setColumnWidth(10, 100) # 纬度
        self.data_table.setColumnWidth(11, 120) # 电话
        self.data_table.setColumnWidth(12, 120) # 商圈
        self.data_table.setColumnWidth(13, 150) # 标签
        self.data_table.setColumnWidth(14, 80)  # 评分
        self.data_table.setColumnWidth(15, 150) # 采集时间
        
        # 启用水平滚动条
        self.data_table.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.data_table.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        
        self.data_table.setAlternatingRowColors(True)
        self.data_table.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.data_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
        
        # 隐藏默认的行选择器，使序号列成为真正的最左侧列
        self.data_table.verticalHeader().setVisible(False)
        
        card_layout.addWidget(self.data_table)
        layout.addWidget(card)
    
    def create_modern_export_card(self, layout):
        """创建现代化导出卡片"""
        card = QGroupBox("数据导出")
        card.setObjectName("modernCard")
        card_layout = QVBoxLayout(card)
        card_layout.setSpacing(15)
        
        # 导出按钮
        export_btn = QPushButton("导出为CSV")
        export_btn.setObjectName("exportButton")
        export_btn.clicked.connect(self.export_to_csv)
        card_layout.addWidget(export_btn)
        
        # 导出统计
        stats_layout = QFormLayout()
        
        self.export_count_label = QLabel("0")
        self.export_count_label.setObjectName("valueLabel")
        stats_layout.addRow("可导出数据:", self.export_count_label)
        
        card_layout.addLayout(stats_layout)
        layout.addWidget(card)
    
    def create_modern_log_card(self, layout):
        """创建现代化日志卡片"""
        card = QGroupBox("运行日志")
        card.setObjectName("modernCard")
        card_layout = QVBoxLayout(card)
        card_layout.setSpacing(15)
        card_layout.setContentsMargins(20, 25, 20, 20)
        
        # 日志文本框
        self.log_text = QTextEdit()
        self.log_text.setObjectName("logText")
        self.log_text.setMaximumHeight(200)
        self.log_text.setPlaceholderText("运行日志将在这里显示...")
        
        card_layout.addWidget(self.log_text)
        layout.addWidget(card)
    
    def update_radius_label(self, value):
        """更新半径标签"""
        self.radius_label.setText(f"{value} 公里")
    
    def update_delay_label(self, value):
        """更新延迟标签"""
        self.delay_label.setText(f"{value} 秒")
    
    def show_usage_instructions(self):
        """显示使用说明"""
        dialog = UsageInstructions(self)
        dialog.exec_()
    
    def open_more_tools(self):
        """打开更多工具链接"""
        webbrowser.open('https://tools.yikeaigc.com/')
    
    def on_api_key_changed(self, text):
        """API Key变化时的处理"""
        if len(text) == 32:
            self.amap_api = AmapAPI(text)
            try:
                # 尝试加载省份来验证API Key
                provinces = self.amap_api.get_regions('province')
                if provinces:  # 如果成功获取省份数据，说明API Key有效
                    self.load_provinces()
                    # 自动保存有效的API Key
                    self.auto_save_api_key(text)
            except Exception as e:
                self.log_message(f"API Key验证失败: {e}", "error")
    
    def auto_save_api_key(self, api_key):
        """自动保存有效的API Key"""
        try:
            if self.db_manager.connect():
                if self.db_manager.save_api_key(self.user_id, api_key):
                    self.log_message("API Key验证成功", "success")
                else:
                    self.log_message("API Key自动保存失败", "warning")
                self.db_manager.disconnect()
        except Exception as e:
            self.log_message(f"自动保存API Key失败: {str(e)}", "error")
    
    def load_provinces(self):
        """加载省份列表"""
        if not self.amap_api:
            return
        
        self.province_combo.clear()
        self.province_combo.addItem("请选择省份")
        
        try:
            provinces = self.amap_api.get_regions('province')
            for province in provinces:
                self.province_combo.addItem(province['name'], province.get('adcode'))
        except Exception as e:
            self.log_message(f"加载省份失败: {e}", "error")
    
    def on_province_changed(self, province_name):
        """省份变化时的处理"""
        if not self.amap_api or province_name == "请选择省份":
            return
        
        self.city_combo.clear()
        self.city_combo.addItem("请选择城市")
        
        try:
            # 优先使用省份adcode作为父查询键
            parent_key = self.province_combo.itemData(self.province_combo.currentIndex()) or province_name
            cities = self.amap_api.get_regions('city', parent_key)
            for city in cities:
                self.city_combo.addItem(city['name'], city.get('adcode'))
        except Exception as e:
            self.log_message(f"加载城市失败: {e}", "error")
    
    def on_city_changed(self, city_name):
        """城市变化时的处理"""
        if not self.amap_api or city_name == "请选择城市":
            return
        
        self.district_combo.clear()
        self.district_combo.addItem("全部")
        
        try:
            # 优先使用城市adcode作为父查询键
            parent_key = self.city_combo.itemData(self.city_combo.currentIndex()) or city_name
            districts = self.amap_api.get_regions('district', parent_key)
            for district in districts:
                self.district_combo.addItem(district['name'], district.get('adcode'))
        except Exception as e:
            self.log_message(f"加载区县失败: {e}", "error")
    
    def update_radius_label(self, value):
        """更新搜索范围标签"""
        self.radius_label.setText(f"{value}公里")
    
    def start_collection(self):
        """开始数据采集"""
        # 验证输入
        if not self.validate_inputs():
            return
        
        # 先计算预计采集数据量
        self.calculate_and_confirm_data_amount()
    
    def calculate_and_confirm_data_amount(self):
        """计算并确认数据量"""
        try:
            # 获取参数
            province = self.province_combo.currentText()
            city = self.city_combo.currentText()
            district = self.district_combo.currentText()
            radius = self.radius_slider.value()
            
            # 构建区域名称
            if district and district != "全部":
                region_name = f"{city}{district}"
            else:
                region_name = city
            
            # 获取区域边界
            if not self.amap_api:
                msg = QMessageBox()
                msg.setIcon(QMessageBox.Warning)
                msg.setWindowTitle("错误")
                msg.setText("请先输入有效的API Key")
                msg.setStyleSheet(self.get_message_box_style())
                msg.exec_()
                return
            
            # 优先用adcode查询
            admin_code = None
            if district and district != "全部":
                admin_code = self.district_combo.itemData(self.district_combo.currentIndex())
            else:
                admin_code = self.city_combo.itemData(self.city_combo.currentIndex())
            query_key = admin_code or region_name
            bounds = self.amap_api.get_district_bounds(query_key)
            if not bounds:
                msg = QMessageBox()
                msg.setIcon(QMessageBox.Warning)
                msg.setWindowTitle("错误")
                msg.setText("无法获取区域边界信息")
                msg.setStyleSheet(self.get_message_box_style())
                msg.exec_()
                return
            
            # 计算网格点数量
            grid_points = self.calculate_grid_points(bounds, radius)
            
            # 显示确认对话框
            msg = QMessageBox()
            msg.setWindowTitle("数据量确认")
            msg.setIcon(QMessageBox.Question)
            msg.setText(f"预计采集信息：\n\n"
                       f"区域：{region_name}\n"
                       f"搜索半径：{radius} 公里\n"
                       f"网格点数量：{len(grid_points)} 个\n\n"
                       f"是否开始采集？")
            msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
            msg.setDefaultButton(QMessageBox.Yes)
            
            # 设置弹窗样式，确保文字为黑色
            msg.setStyleSheet(self.get_message_box_style())
            
            if msg.exec_() == QMessageBox.Yes:
                self.start_actual_collection()
                
        except Exception as e:
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Critical)
            msg.setWindowTitle("错误")
            msg.setText(f"计算数据量失败：{str(e)}")
            msg.setStyleSheet(self.get_message_box_style())
            msg.exec_()
    
    def calculate_grid_points(self, bounds, radius_km):
        """计算网格点 - 与generate_grid_points保持一致"""
        return self.generate_grid_points(bounds, radius_km)
    
    def generate_grid_points(self, bounds, radius_km):
        """生成网格搜索点 - 与网站系统完全一致的算法"""
        # 将搜索半径从公里转换为米
        radius_meters = radius_km * 1000
        
        # 计算区域的实际大小（度）
        lat_diff = bounds['north'] - bounds['south']
        lng_diff = bounds['east'] - bounds['west']
        
        # 计算区域的实际距离（米）
        avg_lat = (bounds['north'] + bounds['south']) / 2
        lat_distance_meters = lat_diff * 111000  # 纬度1度≈111km
        lng_distance_meters = lng_diff * 111000 * math.cos(math.radians(avg_lat))  # 经度需要纬度修正
        
        # 计算最优网格间距：使用六边形最优覆盖模式
        # 相邻圆心距离 = 半径 * √3 ≈ 半径 * 1.732，确保完全覆盖
        optimal_spacing = radius_meters * 1.732
        
        # 计算每个方向需要的点数
        lat_steps = max(1, math.ceil(lat_distance_meters / optimal_spacing))
        lng_steps = max(1, math.ceil(lng_distance_meters / optimal_spacing))
        
        points = []
        
        # 生成搜索中心点（与网站系统保持一致）
        for i in range(lat_steps + 1):
            for j in range(lng_steps + 1):
                # 六边形排列：奇数行偏移半个间距，提高覆盖效率
                lng_offset = 0.5 if i % 2 == 1 else 0
                
                lat = bounds['south'] + (lat_distance_meters / lat_steps) * i / 111000
                lng = bounds['west'] + (lng_distance_meters / lng_steps) * (j + lng_offset) / (111000 * math.cos(math.radians(avg_lat)))
                
                # 确保点在区域边界内
                if (lat >= bounds['south'] and lat <= bounds['north'] and 
                    lng >= bounds['west'] and lng <= bounds['east']):
                    points.append({
                        'lat': lat,
                        'lng': lng,
                        'location': f"{lng},{lat}",  # 与网站系统格式一致
                        'radius_km': radius_km
                    })
        
        return points
    
    def get_message_box_style(self):
        """获取统一的MessageBox样式"""
        return """
            QMessageBox {
                background-color: white;
                color: black;
            }
            QMessageBox QLabel {
                color: black;
                font-size: 14px;
            }
            QMessageBox QPushButton {
                background-color: #3b82f6;
                color: white;
                border: none;
                border-radius: 4px;
                padding: 8px 16px;
                font-size: 14px;
                min-width: 80px;
            }
            QMessageBox QPushButton:hover {
                background-color: #2563eb;
            }
            QMessageBox QPushButton:pressed {
                background-color: #1d4ed8;
            }
        """
    
    def start_actual_collection(self):
        """实际开始采集"""
        # 验证用户权限
        if not self.verify_user_permission("数据采集"):
            return
        
        # 获取参数
        api_key = self.api_key_input.text().strip()
        province = self.province_combo.currentText()
        city = self.city_combo.currentText()
        district = self.district_combo.currentText()
        radius = self.radius_slider.value()
        keywords = self.keywords_input.text().strip()
        
        # 创建工作线程
        admin_code = self.district_combo.itemData(self.district_combo.currentIndex()) if (district and district != "全部") else self.city_combo.itemData(self.city_combo.currentIndex())
        self.collection_worker = CollectionWorker(
            api_key, province, city, district, radius, keywords, self.user_id, admin_code=admin_code
        )
        
        # 连接信号
        self.collection_worker.progress_updated.connect(self.update_progress)
        self.collection_worker.log_message.connect(self.log_message)
        self.collection_worker.finished.connect(self.collection_finished)
        self.collection_worker.poi_collected.connect(self.on_poi_collected)
        
        # 重置界面计数器和清空表格
        self.ui_display_count = 0
        self.data_table.setRowCount(0)  # 清空现有数据
        
        # 更新界面状态
        if hasattr(self, 'start_btn'):
            self.start_btn.setEnabled(False)
            self.stop_btn.setEnabled(True)
        elif hasattr(self, 'start_button'):
            self.start_button.setEnabled(False)
            self.stop_button.setEnabled(True)
        self.status_label.setText("采集中...")
        self.progress_bar.setValue(0)
        
        # 启动线程
        self.collection_worker.start()
        self.log_message("开始数据采集...", "info")
    
    def stop_collection(self):
        """停止数据采集"""
        if self.collection_worker:
            self.collection_worker.stop()
            self.log_message("正在停止采集...", "warning")
    
    def validate_inputs(self):
        """验证输入参数"""
        if len(self.api_key_input.text().strip()) != 32:
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Warning)
            msg.setWindowTitle("参数错误")
            msg.setText("请输入有效的32位API Key")
            msg.setStyleSheet(self.get_message_box_style())
            msg.exec_()
            return False
        
        if self.province_combo.currentText() == "请选择省份":
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Warning)
            msg.setWindowTitle("参数错误")
            msg.setText("请选择省份")
            msg.setStyleSheet(self.get_message_box_style())
            msg.exec_()
            return False
        
        if self.city_combo.currentText() == "请选择城市":
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Warning)
            msg.setWindowTitle("参数错误")
            msg.setText("请选择城市")
            msg.setStyleSheet(self.get_message_box_style())
            msg.exec_()
            return False
        
        return True
    
    def update_progress(self, progress, total, success):
        """更新进度"""
        self.progress_bar.setValue(progress)
        self.stats_label.setText(f"总数: {total}")
    
    def log_message(self, message, level):
        """添加日志消息"""
        timestamp = datetime.now().strftime("%H:%M:%S")
        color_map = {
            "info": "#ffffff",
            "success": "#52c41a",
            "warning": "#faad14",
            "error": "#ff4d4f"
        }
        
        color = color_map.get(level, "#ffffff")
        formatted_message = f'<span style="color: {color}">[{timestamp}] {message}</span>'
        
        self.log_text.append(formatted_message)
        
        # 保持最新日志在底部
        cursor = self.log_text.textCursor()
        cursor.movePosition(cursor.End)
        self.log_text.setTextCursor(cursor)
        
        # 限制日志行数
        if self.log_text.document().blockCount() > 200:
            cursor.movePosition(cursor.Start)
            cursor.select(cursor.BlockUnderCursor)
            cursor.removeSelectedText()
    
    def collection_finished(self):
        """采集完成"""
        if hasattr(self, 'start_btn'):
            self.start_btn.setEnabled(True)
            self.stop_btn.setEnabled(False)
        elif hasattr(self, 'start_button'):
            self.start_button.setEnabled(True)
            self.stop_button.setEnabled(False)
        self.status_label.setText("就绪")
        
        # 显示最终的界面统计信息
        display_count = getattr(self, 'ui_display_count', 0)
        if display_count > 0:
            self.log_message(f"界面显示 {display_count} 个新POI", "info")
        
        self.log_message("采集任务结束", "info")
        
        if self.collection_worker:
            self.collection_worker.deleteLater()
            self.collection_worker = None
        
        # 不刷新数据列表，保持只显示本次采集的POI
        # self.refresh_data_list()  # 已注释，避免显示历史数据
    
    def on_poi_collected(self, poi_data):
        """处理新采集的POI数据"""
        # 实时添加到数据表格
        row_count = self.data_table.rowCount()
        self.data_table.insertRow(row_count)
        
        # 更新界面计数器
        self.ui_display_count += 1
        
        # 填充数据 - 对应16列：序号, POI ID, 名称, 类型, 类型编码, 地址, 省份, 城市, 区县, 经度, 纬度, 电话, 商圈, 标签, 评分, 采集时间
        # 序号
        sequence_item = QTableWidgetItem(str(self.ui_display_count))
        sequence_item.setTextAlignment(Qt.AlignCenter)
        sequence_item.setData(Qt.UserRole, self.ui_display_count)  # 存储序号数据，仅用于显示
        self.data_table.setItem(row_count, 0, sequence_item)
        # POI ID
        self.data_table.setItem(row_count, 1, QTableWidgetItem(poi_data.get('id', '')))
        # 名称
        self.data_table.setItem(row_count, 2, QTableWidgetItem(poi_data.get('name', '')))
        # 类型
        self.data_table.setItem(row_count, 3, QTableWidgetItem(poi_data.get('type', '')))
        # 类型编码
        self.data_table.setItem(row_count, 4, QTableWidgetItem(poi_data.get('typecode', '')))
        # 地址
        self.data_table.setItem(row_count, 5, QTableWidgetItem(poi_data.get('address', '')))
        # 省份
        self.data_table.setItem(row_count, 6, QTableWidgetItem(poi_data.get('pname', '')))
        # 城市
        self.data_table.setItem(row_count, 7, QTableWidgetItem(poi_data.get('cityname', '')))
        # 区县
        self.data_table.setItem(row_count, 8, QTableWidgetItem(poi_data.get('adname', '')))
        
        # 经纬度处理
        location = poi_data.get('location', '')
        if isinstance(location, str) and ',' in location:
            lon, lat = location.split(',')
            self.data_table.setItem(row_count, 9, QTableWidgetItem(f"{float(lon):.6f}"))  # 经度
            self.data_table.setItem(row_count, 10, QTableWidgetItem(f"{float(lat):.6f}"))  # 纬度
        else:
            self.data_table.setItem(row_count, 9, QTableWidgetItem(''))  # 经度
            self.data_table.setItem(row_count, 10, QTableWidgetItem(''))  # 纬度
        
        # 电话
        tel_val = poi_data.get('tel', '')
        if isinstance(tel_val, (list, tuple)):
            tel_str = ', '.join(str(x) for x in tel_val if x)
        else:
            tel_str = str(tel_val) if tel_val is not None else ''
        self.data_table.setItem(row_count, 11, QTableWidgetItem(tel_str))
        # 商圈 - 处理可能的list类型
        business_area = poi_data.get('business_area', '')
        if isinstance(business_area, list):
            business_area_str = ', '.join(str(item) for item in business_area if item)
        else:
            business_area_str = str(business_area) if business_area else ''
        self.data_table.setItem(row_count, 12, QTableWidgetItem(business_area_str))
        
        # 标签 - 处理可能的list类型
        tag = poi_data.get('tag', '')
        if isinstance(tag, list):
            tag_str = ', '.join(str(item) for item in tag if item)
        else:
            tag_str = str(tag) if tag else ''
        self.data_table.setItem(row_count, 13, QTableWidgetItem(tag_str))
        # 评分
        rating_val = poi_data.get('rating', '')
        rating_str = '' if rating_val is None else str(rating_val)
        self.data_table.setItem(row_count, 14, QTableWidgetItem(rating_str))
        # 采集时间
        self.data_table.setItem(row_count, 15, QTableWidgetItem(datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
    
    def refresh_data_list(self):
        """刷新数据列表"""
        try:
            if self.db_manager.connect():
                pois = self.db_manager.get_user_pois(self.user_id)
                self.update_poi_table(pois)
                self.db_manager.disconnect()
        except Exception as e:
            self.log_message(f"刷新数据列表失败: {str(e)}", "error")
    
    def export_csv(self):
        """导出CSV文件"""
        try:
            # 选择保存路径
            file_path, _ = QFileDialog.getSaveFileName(
                self, "导出CSV文件", "poi_data.csv", "CSV文件 (*.csv)"
            )
            
            if not file_path:
                return
            
            # 获取用户POI数据
            pois = self.db_manager.get_user_pois(self.user_id)
            
            if not pois:
                msg = QMessageBox()
                msg.setIcon(QMessageBox.Information)
                msg.setWindowTitle("提示")
                msg.setText("没有数据可导出")
                msg.setStyleSheet(self.get_message_box_style())
                msg.exec_()
                return
            
            # 写入CSV文件
            with open(file_path, 'w', newline='', encoding='utf-8-sig') as csvfile:
                # CSV头部
                headers = [
                    'POI名称', 'POI类型', '地址', '经度', '纬度', '省份', '城市', '区县',
                    '电话', '营业时间', '关键词', '搜索区域', '创建时间'
                ]
                
                writer = csv.writer(csvfile)
                writer.writerow(headers)
                
                # 写入数据行
                for poi in pois:
                    location = poi.get('location', '')
                    longitude, latitude = '', ''
                    if location and ',' in location:
                        longitude, latitude = location.split(',')
                    
                    row = [
                        poi.get('name', ''),
                        poi.get('type', ''),
                        poi.get('address', ''),
                        longitude,
                        latitude,
                        poi.get('pname', ''),
                        poi.get('cityname', ''),
                        poi.get('adname', ''),
                        poi.get('tel', ''),
                        poi.get('business_hours', ''),
                        poi.get('keywords', ''),
                        poi.get('search_region', ''),
                        poi.get('created_at', '')
                    ]
                    writer.writerow(row)
            
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Information)
            msg.setWindowTitle("成功")
            msg.setText(f"CSV文件已导出到: {file_path}")
            msg.setStyleSheet(self.get_message_box_style())
            msg.exec_()
            self.log_message(f"CSV导出成功: {file_path}", "info")
            
        except Exception as e:
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Critical)
            msg.setWindowTitle("错误")
            msg.setText(f"导出CSV失败: {str(e)}")
            msg.setStyleSheet(self.get_message_box_style())
            msg.exec_()
            self.log_message(f"CSV导出失败: {str(e)}", "error")
    
    def load_saved_api_key(self):
        """加载已保存的API Key"""
        try:
            if not self.db_manager.connect():
                return
            
            api_key = self.db_manager.get_api_key(self.user_id)
            if api_key:
                self.api_key_input.setText(api_key)
                self.amap_api = AmapAPI(api_key)
                self.load_provinces()
                self.log_message("已加载保存的API Key", "info")
            
            self.db_manager.disconnect()
            
        except Exception as e:
            self.log_message(f"加载API Key失败: {str(e)}", "error")
    
    def save_api_key_to_db(self):
        """保存API Key"""
        api_key = self.api_key_input.text().strip()
        
        if len(api_key) != 32:
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Warning)
            msg.setWindowTitle("错误")
            msg.setText("请输入有效的32位API Key")
            msg.setStyleSheet(self.get_message_box_style())
            msg.exec_()
            return
        
        try:
            if not self.db_manager.connect():
                msg = QMessageBox()
                msg.setIcon(QMessageBox.Critical)
                msg.setWindowTitle("错误")
                msg.setText("连接失败")
                msg.setStyleSheet(self.get_message_box_style())
                msg.exec_()
                return
            
            if self.db_manager.save_api_key(self.user_id, api_key):
                msg = QMessageBox()
                msg.setIcon(QMessageBox.Information)
                msg.setWindowTitle("成功")
                msg.setText("API Key已保存")
                msg.setStyleSheet(self.get_message_box_style())
                msg.exec_()
                self.log_message("API Key保存成功", "success")
            else:
                msg = QMessageBox()
                msg.setIcon(QMessageBox.Critical)
                msg.setWindowTitle("错误")
                msg.setText("API Key保存失败")
                msg.setStyleSheet(self.get_message_box_style())
                msg.exec_()
                self.log_message("API Key保存失败", "error")
            
            self.db_manager.disconnect()
            
        except Exception as e:
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Critical)
            msg.setWindowTitle("错误")
            msg.setText(f"保存API Key失败: {str(e)}")
            msg.setStyleSheet(self.get_message_box_style())
            msg.exec_()
            self.log_message(f"保存API Key失败: {str(e)}", "error")
    
    def load_existing_data(self):
        """加载已有数据"""
        self.refresh_data_list()
    
    def setup_poi_table(self):
        """设置POI数据表格（兼容旧方法名）"""
        # 这个方法现在由create_modern_data_card处理
        pass
    
    def load_saved_api_key(self):
        """加载保存的API Key"""
        if self.db_manager.connect():
            api_key = self.db_manager.get_api_key(self.user_id)
            if api_key:
                self.api_key_input.setText(api_key)
            self.db_manager.disconnect()
    
    def save_api_key(self):
        """保存API Key"""
        api_key = self.api_key_input.text().strip()
        if len(api_key) != 32:
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Warning)
            msg.setWindowTitle("错误")
            msg.setText("API Key必须是32位字符")
            msg.setStyleSheet(self.get_message_box_style())
            msg.exec_()
            return
        
        if self.db_manager.connect():
            if self.db_manager.save_api_key(self.user_id, api_key):
                msg = QMessageBox()
                msg.setIcon(QMessageBox.Information)
                msg.setWindowTitle("成功")
                msg.setText("API Key保存成功")
                msg.setStyleSheet(self.get_message_box_style())
                msg.exec_()
            else:
                msg = QMessageBox()
                msg.setIcon(QMessageBox.Warning)
                msg.setWindowTitle("错误")
                msg.setText("API Key保存失败")
                msg.setStyleSheet(self.get_message_box_style())
                msg.exec_()
            self.db_manager.disconnect()
    
    def load_existing_data(self):
        """加载已有数据"""
        if self.db_manager.connect():
            pois = self.db_manager.get_user_pois(self.user_id, 1000)
            if hasattr(self, 'data_table'):
                self.update_poi_table(pois)
            self.db_manager.disconnect()
    
    def update_poi_table(self, pois):
        """更新POI表格"""
        self.data_table.setRowCount(len(pois))
        
        for row, poi in enumerate(pois):
            # 序号
            sequence_item = QTableWidgetItem(str(row + 1))
            sequence_item.setTextAlignment(Qt.AlignCenter)
            sequence_item.setData(Qt.UserRole, row + 1)  # 存储序号数据，仅用于显示
            self.data_table.setItem(row, 0, sequence_item)
            
            # POI ID
            poi_id_val = poi.get('poi_id', poi.get('id', ''))
            self.data_table.setItem(row, 1, QTableWidgetItem(str(poi_id_val)))
            
            # 名称
            self.data_table.setItem(row, 2, QTableWidgetItem(poi.get('name', '')))
            
            # 类型
            self.data_table.setItem(row, 3, QTableWidgetItem(poi.get('type', '')))
            
            # 类型编码
            self.data_table.setItem(row, 4, QTableWidgetItem(poi.get('typecode', '')))
            
            # 地址
            self.data_table.setItem(row, 5, QTableWidgetItem(poi.get('address', '')))
            
            # 省份
            self.data_table.setItem(row, 6, QTableWidgetItem(poi.get('pname', '')))
            
            # 城市
            self.data_table.setItem(row, 7, QTableWidgetItem(poi.get('cityname', '')))
            
            # 区县
            self.data_table.setItem(row, 8, QTableWidgetItem(poi.get('adname', '')))
            
            # 经度和纬度
            location = poi.get('location', '')
            if location and ',' in location:
                lng, lat = location.split(',')
                self.data_table.setItem(row, 9, QTableWidgetItem(lng))
                self.data_table.setItem(row, 10, QTableWidgetItem(lat))
            else:
                self.data_table.setItem(row, 9, QTableWidgetItem(''))
                self.data_table.setItem(row, 10, QTableWidgetItem(''))
            
            # 电话
            self.data_table.setItem(row, 11, QTableWidgetItem(poi.get('tel', '')))
            
            # 商圈
            business_area_val = poi.get('business_area', '')
            if isinstance(business_area_val, (list, tuple)):
                business_area_val = ','.join(map(str, business_area_val))
            self.data_table.setItem(row, 12, QTableWidgetItem(str(business_area_val)))
            
            # 标签
            tag_val = poi.get('tag', '')
            if isinstance(tag_val, (list, tuple)):
                tag_val = ','.join(map(str, tag_val))
            self.data_table.setItem(row, 13, QTableWidgetItem(str(tag_val)))
            
            # 评分
            self.data_table.setItem(row, 14, QTableWidgetItem(str(poi.get('rating', ''))))
            
            # 采集时间
            created_at = poi.get('created_at', '')
            if created_at:
                # 如果是datetime对象，转换为字符串
                if hasattr(created_at, 'strftime'):
                    created_at = created_at.strftime('%Y-%m-%d %H:%M:%S')
            self.data_table.setItem(row, 15, QTableWidgetItem(str(created_at)))
        
        # 更新导出计数
        if hasattr(self, 'export_count_label'):
            self.export_count_label.setText(str(len(pois)))
    
    def update_selection_count(self):
        """更新选择计数"""
        if hasattr(self, 'data_table'):
            selected_rows = len(self.data_table.selectionModel().selectedRows())
            if hasattr(self, 'selected_count_label'):
                self.selected_count_label.setText(f"已选: {selected_rows} 条")
    
    def export_to_csv(self):
        """导出CSV文件"""
        if not hasattr(self, 'data_table') or self.data_table.rowCount() == 0:
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Warning)
            msg.setWindowTitle("提示")
            msg.setText("没有数据可导出")
            msg.setStyleSheet(self.get_message_box_style())
            msg.exec_()
            return
        
        # 选择保存路径
        file_path, _ = QFileDialog.getSaveFileName(
            self, "导出CSV文件", f"POI数据_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
            "CSV文件 (*.csv)"
        )
        
        if not file_path:
            return
        
        try:
            with open(file_path, 'w', newline='', encoding='utf-8-sig') as csvfile:
                writer = csv.writer(csvfile)
                
                # 写入表头
                headers = []
                for col in range(self.data_table.columnCount()):
                    headers.append(self.data_table.horizontalHeaderItem(col).text())
                writer.writerow(headers)
                
                # 写入数据
                for row in range(self.data_table.rowCount()):
                    row_data = []
                    for col in range(self.data_table.columnCount()):
                        item = self.data_table.item(row, col)
                        row_data.append(item.text() if item else '')
                    writer.writerow(row_data)
            
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Information)
            msg.setWindowTitle("成功")
            msg.setText(f"数据已导出到: {file_path}")
            msg.setStyleSheet(self.get_message_box_style())
            msg.exec_()
            
        except Exception as e:
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Critical)
            msg.setWindowTitle("错误")
            msg.setText(f"导出失败: {str(e)}")
            msg.setStyleSheet(self.get_message_box_style())
            msg.exec_()
    
    def export_csv(self):
        """兼容旧方法名"""
        self.export_to_csv()
    
    def authenticate_user(self):
        """用户认证方法"""
        software_name = "高德地图商户采集工具"
        try:
            success, user_info = integrate_auth(software_name, vip_only=False)
            if success and user_info:
                self.user_info = user_info
                print(f"用户认证成功: {user_info.get('username', '未知用户')}")
                return True
            else:
                print("用户认证失败")
                return False
        except Exception as e:
            print(f"认证过程出错: {str(e)}")
            QMessageBox.critical(None, "认证错误", f"认证过程出错: {str(e)}")
            return False
    
    def update_title_with_user_info(self):
        """更新标题栏显示用户信息"""
        if self.user_info:
            username = self.user_info.get('username', '未知用户')
            role = self.user_info.get('role', '普通用户')
            original_title = "高德地图商户采集工具"
            new_title = f"{original_title} - 用户: {username} ({role})"
            self.setWindowTitle(new_title)
        else:
            self.setWindowTitle("高德地图商户采集工具")
    
    def verify_user_permission(self, operation_name="操作"):
        """验证用户权限（在关键操作前调用）"""
        from auth_module import token_manager
        software_name = "高德地图商户采集工具"
        try:
            allowed, message = token_manager.verify_permission_with_server(software_name)
            if not allowed:
                QMessageBox.warning(self, "权限不足", f"执行{operation_name}需要有效权限：{message}")
                return False
            return True
        except Exception as e:
            QMessageBox.warning(self, "权限验证失败", f"无法验证权限，请重新启动程序：{str(e)}")
            return False


def main():
    """主函数"""
    app = QApplication(sys.argv)
    app.setApplicationName("高德地图POI采集工具")
    app.setApplicationVersion("1.0")
    
    # 设置应用图标
    try:
        app.setWindowIcon(QIcon(resource_path("icon.ico")))
    except:
        pass
    
    # 创建主窗口
    window = MainWindow()
    window.show()
    
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()