Browse Source

完善数据统计的相关逻辑

zxlie 6 months ago
parent
commit
20fd5b7d54

+ 28 - 21
apps/background/statistics.js

@@ -96,13 +96,36 @@ let Statistics = (function() {
     };
     
     /**
-     * 获取客户端详细信息(仅background可用字段)
+     * 获取客户端详细信息(仅background可用字段,兼容service worker环境,字段与服务端一致
      * @returns {Object}
      */
-    const getClientInfo = () => {
+    const getClientInfo = async () => {
+        let tabInfo = {};
+        try {
+            // 获取当前活动tab的页面信息
+            const tabs = await new Promise(resolve => {
+                chrome.tabs.query({active: true, currentWindow: true}, resolve);
+            });
+            if (tabs && tabs.length > 0) {
+                const tab = tabs[0];
+                tabInfo = {
+                    pageUrl: tab.url || '',
+                    pageTitle: tab.title || ''
+                };
+            }
+        } catch (e) {
+            // 忽略tab获取异常
+        }
+        const nav = self.navigator || {};
+        // 只采集服务端需要的字段
         return {
+            userAgent: nav.userAgent || '',
+            language: nav.language || '',
+            platform: nav.platform || '',
+            vendor: nav.vendor || '',
+            online: nav.onLine,
             extensionVersion: chrome.runtime.getManifest().version,
-            timeOpened: FH_TIME_OPENED
+            ...tabInfo
         };
     };
 
@@ -113,28 +136,12 @@ let Statistics = (function() {
      */
     const sendToServer = async (eventName, params = {}) => {
         const uid = await getUserId();
-        const clientInfo = getClientInfo();
-        // 合并background全局的FH_CLIENT_INFO
-        let extraInfo = {};
-        try {
-            if (typeof chrome !== 'undefined' && chrome && chrome.runtime && chrome.runtime.getBackgroundPage) {
-                await new Promise(resolve => {
-                    chrome.runtime.getBackgroundPage(bg => {
-                        if (bg && bg.FH_CLIENT_INFO) {
-                            extraInfo = bg.FH_CLIENT_INFO;
-                        }
-                        resolve();
-                    });
-                });
-            }
-        } catch(e) {}
+        const clientInfo = await getClientInfo();
+        // 只保留服务端 TrackSchema 需要的字段
         const payload = {
             event: eventName,
             userId: uid,
-            date: todayStr,
-            timestamp: Date.now(),
             ...clientInfo,
-            ...extraInfo,
             ...params
         };
         try {

+ 1377 - 0
apps/options/market.js

@@ -0,0 +1,1377 @@
+import Awesome from '../background/awesome.js'
+import MSG_TYPE from '../static/js/common.js';
+import Settings from './settings.js';
+import Statistics from '../background/statistics.js';
+import toolMap from '../background/tools.js';
+
+// 工具分类定义
+const TOOL_CATEGORIES = [
+    { key: 'dev', name: '开发工具类', tools: ['json-format', 'json-diff', 'code-beautify', 'code-compress', 'postman', 'websocket', 'regexp','page-timing'] },
+    { key: 'encode', name: '编解码转换类', tools: ['en-decode', 'trans-radix', 'timestamp', 'trans-color'] },
+    { key: 'image', name: '图像处理类', tools: ['qr-code', 'image-base64', 'svg-converter', 'chart-maker', 'poster-maker' ,'screenshot', 'color-picker'] },
+    { key: 'productivity', name: '效率工具类', tools: ['aiagent', 'sticky-notes', 'html2markdown', 'page-monkey'] },
+    { key: 'calculator', name: '计算工具类', tools: ['crontab', 'loan-rate', 'password'] },
+    { key: 'other', name: '其他工具', tools: [] }
+];
+
+// Vue实例
+new Vue({
+    el: '#marketContainer',
+    data: {
+        manifest: { version: '0.0.0' },
+        searchKey: '',
+        currentCategory: '',
+        sortType: 'default',
+        viewMode: 'list', // 默认网格视图
+        categories: TOOL_CATEGORIES,
+        favorites: new Set(),
+        recentUsed: [],
+        loading: true,
+        originalTools: {}, // 保存原始工具数据
+        currentView: 'all', // 当前视图类型(all/installed/favorites/recent)
+        activeTools: {}, // 当前显示的工具列表
+        installedCount: 0, // 已安装工具数量
+        
+        // 版本相关
+        latestVersion: '', // 最新版本号
+        needUpdate: false, // 是否需要更新
+        
+        // 设置相关
+        showSettingsModal: false,
+        defaultKey: 'Alt+Shift+J', // 默认快捷键
+        countDown: 0, // 夜间模式倒计时
+        selectedOpts: [], // 选中的选项
+        menuDownloadCrx: false, // 菜单-插件下载
+        menuFeHelperSeting: false, // 菜单-FeHelper设置
+        isFirefox: false, // 是否Firefox浏览器
+
+        // 打赏相关
+        showDonateModal: false,
+        donate: {
+            text: '感谢你对FeHelper的认可和支持!',
+            image: './donate.jpeg'
+        },
+
+        // 确认对话框
+        confirmDialog: {
+            show: false,
+            title: '操作确认',
+            message: '',
+            callback: null,
+            data: null
+        },
+
+        recentCount: 0,
+        showDashboard: false, // 是否显示DashBoard
+        dashboardData: null, // DashBoard数据
+    },
+
+    async created() {
+        await this.initData();
+        this.recentCount = (await Statistics.getRecentUsedTools(10)).length;
+        // 初始化后更新已安装工具数量
+        this.updateInstalledCount();
+        // 恢复用户的视图模式设置
+        this.loadViewMode();
+        // 加载设置项
+        this.loadSettings();
+        // 检查浏览器类型
+        this.checkBrowserType();
+        // 检查版本更新
+        this.checkVersionUpdate();
+        
+        // 检查URL中是否有donate_from参数
+        this.checkDonateParam();
+    },
+
+    computed: {
+        filteredTools() {
+            if (this.loading) {
+                return [];
+            }
+
+            // 获取当前工具列表
+            let result = Object.values(this.activeTools).map(tool => ({
+                ...tool,
+                favorite: this.favorites.has(tool.key)
+            }));
+
+            // 搜索过滤
+            if (this.searchKey) {
+                const key = this.searchKey.toLowerCase();
+                result = result.filter(tool => 
+                    tool.name.toLowerCase().includes(key) || 
+                    tool.tips.toLowerCase().includes(key)
+                );
+            }
+
+            // 分类过滤,在所有视图下生效
+            if (this.currentCategory) {
+                const category = TOOL_CATEGORIES.find(c => c.key === this.currentCategory);
+                const categoryTools = category ? category.tools : [];
+                result = result.filter(tool => categoryTools.includes(tool.key));
+            }
+
+            // 排序
+            switch (this.sortType) {
+                case 'newest':
+                    result.sort((a, b) => (b.updateTime || 0) - (a.updateTime || 0));
+                    break;
+                case 'hot':
+                    result.sort((a, b) => (b.updateTime || 0) - (a.updateTime || 0));
+                    break;
+                default:
+                    const allTools = TOOL_CATEGORIES.reduce((acc, category) => {
+                        acc.push(...category.tools);
+                        return acc;
+                    }, []);
+                    
+                    result.sort((a, b) => {
+                        const indexA = allTools.indexOf(a.key);
+                        const indexB = allTools.indexOf(b.key);
+                        
+                        // 如果工具不在任何类别中,放到最后
+                        if (indexA === -1 && indexB === -1) {
+                            return a.key.localeCompare(b.key); // 字母顺序排序
+                        }
+                        if (indexA === -1) return 1;
+                        if (indexB === -1) return -1;
+                        
+                        return indexA - indexB;
+                    });
+            }
+
+            return result;
+        }
+    },
+
+    methods: {
+        async initData() {
+            try {
+                this.loading = true;
+
+                // 获取manifest信息
+                const manifest = await chrome.runtime.getManifest();
+                this.manifest = manifest;
+
+                // 从 Awesome.getAllTools 获取工具列表
+                const tools = await Awesome.getAllTools();
+                
+                // 获取收藏数据
+                const favorites = await this.getFavoritesData();
+                this.favorites = new Set(favorites);
+
+                // 获取最近使用数据
+                const recentUsed = await this.getRecentUsedData();
+                this.recentUsed = recentUsed;
+                this.recentCount = recentUsed.length;
+
+                // 获取已安装工具列表
+                const installedTools = await Awesome.getInstalledTools();
+
+                // 处理工具数据
+                const processedTools = {};
+                Object.entries(tools).forEach(([key, tool]) => {
+                    // 检查工具是否已安装
+                    const isInstalled = installedTools.hasOwnProperty(key);
+                    // 检查是否有右键菜单
+                    const hasMenu = tool.menu || false;
+                    
+                    processedTools[key] = {
+                        ...tool,
+                        key, // 添加key到工具对象中
+                        updateTime: Date.now() - Math.floor(Math.random() * 30) * 24 * 60 * 60 * 1000,
+                        installed: isInstalled, // 使用实时安装状态
+                        inContextMenu: hasMenu, // 使用实时菜单状态
+                        systemInstalled: tool.systemInstalled || false, // 是否系统预装
+                        favorite: this.favorites.has(key)
+                    };
+                });
+
+                this.originalTools = processedTools;
+                
+                // 初始化activeTools为所有工具
+                this.activeTools = { ...processedTools };
+                
+                // 更新"其他工具"类别
+                this.updateOtherCategory(Object.keys(processedTools));
+
+                // 默认选中"全部分类"
+                this.currentCategory = '';
+            } catch (error) {
+                console.error('初始化数据失败:', error);
+            } finally {
+                this.loading = false;
+            }
+        },
+        
+        // 更新"其他工具"类别,将未分类的工具添加到此类别
+        updateOtherCategory(allToolKeys) {
+            // 获取所有已分类的工具
+            const categorizedTools = new Set();
+            TOOL_CATEGORIES.forEach(category => {
+                if (category.key !== 'other') {
+                    category.tools.forEach(tool => categorizedTools.add(tool));
+                }
+            });
+            
+            // 找出未分类的工具
+            const uncategorizedTools = allToolKeys.filter(key => !categorizedTools.has(key));
+            
+            // 更新"其他工具"类别
+            const otherCategory = TOOL_CATEGORIES.find(category => category.key === 'other');
+            if (otherCategory) {
+                otherCategory.tools = uncategorizedTools;
+            }
+        },
+
+        // 检查版本更新
+        async checkVersionUpdate() {
+            try {
+                // 获取已安装的版本号
+                const currentVersion = this.manifest.version;
+                
+                // 尝试从本地存储获取最新版本信息,避免频繁请求
+                const cachedData = await new Promise(resolve => {
+                    chrome.storage.local.get('fehelper_latest_version_data', data => {
+                        resolve(data.fehelper_latest_version_data || null);
+                    });
+                });
+        
+                // 检查是否需要重新获取版本信息:
+                // 1. 缓存不存在
+                // 2. 缓存已过期(超过24小时)
+                // 3. 缓存的当前版本与实际版本不同(说明插件已更新)
+                const now = Date.now();
+                const cacheExpired = !cachedData || !cachedData.timestamp || (now - cachedData.timestamp > 24 * 60 * 60 * 1000);
+                const versionChanged = cachedData && cachedData.currentVersion !== currentVersion;
+                
+                if (cacheExpired || versionChanged) {
+                    try {
+                        console.log('开始获取最新版本信息...');
+                        // 使用shields.io的JSON API获取最新版本号
+                        const response = await fetch('https://img.shields.io/chrome-web-store/v/pkgccpejnmalmdinmhkkfafefagiiiad.json');
+                        if (!response.ok) {
+                            throw new Error(`HTTP错误:${response.status}`);
+                        }
+                        
+                        const data = await response.json();
+                        // 提取版本号 - shields.io返回的数据中包含版本信息
+                        let latestVersion = '';
+                        if (data && data.value) {
+                            // 去掉版本号前的'v'字符(如果有)
+                            latestVersion = data.value.replace(/^v/, '');
+                            console.log('获取到最新版本号:', latestVersion);
+                        }
+                        
+                        // 比较版本号
+                        const needUpdate = this.compareVersions(currentVersion, latestVersion) < 0;
+                        console.log('当前版本:', currentVersion, '最新版本:', latestVersion, '需要更新:', needUpdate);
+                        
+                        // 保存到本地存储中
+                        await chrome.storage.local.set({
+                            'fehelper_latest_version_data': {
+                                timestamp: now,
+                                currentVersion, // 保存当前检查时的版本号
+                                latestVersion,
+                                needUpdate
+                            }
+                        });
+                        
+                        this.latestVersion = latestVersion;
+                        this.needUpdate = needUpdate;
+                    } catch (fetchError) {
+                        console.error('获取最新版本信息失败:', fetchError);
+                        // 获取失败时不显示更新按钮
+                        this.needUpdate = false;
+                        
+                        // 如果是版本变更导致的重新检查,但获取失败,则使用缓存数据
+                        if (versionChanged && cachedData) {
+                            this.latestVersion = cachedData.latestVersion || '';
+                            // 比较新的currentVersion和缓存的latestVersion
+                            this.needUpdate = this.compareVersions(currentVersion, cachedData.latestVersion) < 0;
+                        }
+                    }
+                } else {
+                    // 使用缓存数据
+                    console.log('使用缓存的版本信息');
+                    this.latestVersion = cachedData.latestVersion || '';
+                    this.needUpdate = cachedData.needUpdate || false;
+                }
+            } catch (error) {
+                console.error('检查版本更新失败:', error);
+                this.needUpdate = false; // 出错时不显示更新提示
+            }
+        },
+        
+        // 比较版本号:如果v1 < v2返回-1,v1 = v2返回0,v1 > v2返回1
+        compareVersions(v1, v2) {
+            // 将版本号拆分为数字数组
+            const v1Parts = v1.split('.').map(Number);
+            const v2Parts = v2.split('.').map(Number);
+            
+            // 计算两个版本号中较长的长度
+            const maxLength = Math.max(v1Parts.length, v2Parts.length);
+            
+            // 比较每一部分
+            for (let i = 0; i < maxLength; i++) {
+                // 获取当前部分,如果不存在则视为0
+                const part1 = v1Parts[i] || 0;
+                const part2 = v2Parts[i] || 0;
+                
+                // 比较当前部分
+                if (part1 < part2) return -1;
+                if (part1 > part2) return 1;
+            }
+            
+            // 所有部分都相等
+            return 0;
+        },
+        
+        // 打开Chrome商店页面
+        openStorePage() {
+            try {
+                console.log('开始请求检查更新...');
+                
+                // 使用Chrome Extension API请求检查更新
+                // Manifest V3中requestUpdateCheck返回Promise,结果是一个对象而不是数组
+                chrome.runtime.requestUpdateCheck().then(result => {
+                    // 正确获取status和details,它们是result对象的属性
+                    console.log('更新检查结果:', result);
+                    const status = result.status;
+                    const details = result.details;
+                    
+                    console.log('更新检查状态:', status, '详情:', details);
+                    this.handleUpdateStatus(status, details);
+                }).catch(error => {
+                    console.error('更新检查失败:', error);
+                    this.handleUpdateError(error);
+                });
+            } catch (error) {
+                console.error('请求更新出错:', error);
+                this.handleUpdateError(error);
+            }
+        },
+
+        // 处理更新状态
+        handleUpdateStatus(status, details) {
+            console.log(`处理更新状态: ${status}`, details);
+            
+            if (status === 'update_available') {
+                console.log('发现更新:', details);
+                
+                // 显示更新通知
+                this.showNotification({
+                    title: 'FeHelper 更新',
+                    message: '已发现新版本,正在更新...'
+                });
+                
+                // 重新加载扩展以应用更新
+                setTimeout(() => {
+                    console.log('重新加载扩展...');
+                    chrome.runtime.reload();
+                }, 1000);
+            } else if (status === 'no_update') {
+                // 如果没有可用更新,但用户点击了更新按钮
+                this.showNotification({
+                    title: 'FeHelper 更新',
+                    message: '您的FeHelper已经是最新版本。'
+                });
+            } else {
+                // 其他情况,如更新检查失败等
+                console.log('其他更新状态:', status);
+                
+                // 备选方案:跳转到官方网站
+                chrome.tabs.create({ 
+                    url: 'https://baidufe.com/fehelper'
+                });
+                
+                this.showNotification({
+                    title: 'FeHelper 更新',
+                    message: '自动更新失败,请访问FeHelper官网手动获取最新版本。'
+                });
+            }
+        },
+
+        // 处理更新错误
+        handleUpdateError(error) {
+            console.error('更新过程中出错:', error);
+            
+            // 出错时跳转到官方网站
+            chrome.tabs.create({ 
+                url: 'https://baidufe.com/fehelper'
+            });
+            
+            this.showNotification({
+                title: 'FeHelper 更新错误',
+                message: '更新过程中出现错误,请手动检查更新。'
+            });
+        },
+
+        // 显示通知的统一方法
+        showNotification(options) {
+            try {
+                console.log('准备显示通知:', options);
+                
+                // 定义通知ID,方便后续关闭
+                const notificationId = 'fehelper-update-notification';
+                const simpleNotificationId = 'fehelper-simple-notification';
+
+                // 直接尝试创建通知,不检查权限
+                // Chrome扩展在manifest中已声明notifications权限,应该可以直接使用
+                const notificationOptions = {
+                    type: 'basic',
+                    iconUrl: chrome.runtime.getURL('static/img/fe-48.png'),
+                    title: options.title || 'FeHelper',
+                    message: options.message || '',
+                    priority: 2,
+                    requireInteraction: false, // 改为false,因为我们会手动关闭
+                    silent: false // 播放音效
+                };
+                
+                console.log('通知选项:', notificationOptions);
+                
+                // 首先尝试直接创建通知
+                chrome.notifications.create(notificationId, notificationOptions, (createdId) => {
+                    const error = chrome.runtime.lastError;
+                    if (error) {
+                        console.error('创建通知出错:', error);
+                        
+                        // 通知创建失败,尝试使用alert作为备选方案
+                        alert(`${options.title}: ${options.message}`);
+                        
+                        // 再尝试使用不同的选项创建通知
+                        const simpleOptions = {
+                            type: 'basic',
+                            iconUrl: chrome.runtime.getURL('static/img/fe-48.png'),
+                            title: options.title || 'FeHelper',
+                            message: options.message || ''
+                        };
+                        
+                        // 使用简化选项再次尝试
+                        chrome.notifications.create(simpleNotificationId, simpleOptions, (simpleId) => {
+                            if (chrome.runtime.lastError) {
+                                console.error('简化通知创建也失败:', chrome.runtime.lastError);
+                            } else {
+                                console.log('简化通知已创建,ID:', simpleId);
+                                
+                                // 3秒后自动关闭简化通知
+                                setTimeout(() => {
+                                    chrome.notifications.clear(simpleId, (wasCleared) => {
+                                        console.log('简化通知已关闭:', wasCleared);
+                                    });
+                                }, 3000);
+                            }
+                        });
+                    } else {
+                        console.log('通知已成功创建,ID:', createdId);
+                        
+                        // 3秒后自动关闭通知
+                        setTimeout(() => {
+                            chrome.notifications.clear(createdId, (wasCleared) => {
+                                console.log('通知已关闭:', wasCleared);
+                            });
+                        }, 3000);
+                    }
+                });
+                
+                // 同时使用内置UI显示消息
+                this.showInPageNotification(options);
+            } catch (error) {
+                console.error('显示通知时出错:', error);
+                // 降级为alert
+                alert(`${options.title}: ${options.message}`);
+            }
+        },
+
+        // 在页面内显示通知消息
+        showInPageNotification(options) {
+            try {
+                // 创建一个通知元素
+                const notificationEl = document.createElement('div');
+                notificationEl.className = 'in-page-notification';
+                notificationEl.innerHTML = `
+                    <div class="notification-content">
+                        <div class="notification-title">${options.title || 'FeHelper'}</div>
+                        <div class="notification-message">${options.message || ''}</div>
+                    </div>
+                    <button class="notification-close">×</button>
+                `;
+                
+                // 添加样式
+                const style = document.createElement('style');
+                style.textContent = `
+                    .in-page-notification {
+                        position: fixed;
+                        bottom: 20px;
+                        right: 20px;
+                        background-color: #4285f4;
+                        color: white;
+                        padding: 15px;
+                        border-radius: 8px;
+                        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+                        z-index: 9999;
+                        display: flex;
+                        align-items: center;
+                        justify-content: space-between;
+                        min-width: 300px;
+                        animation: slideIn 0.3s ease-out;
+                    }
+                    .notification-content {
+                        flex: 1;
+                    }
+                    .notification-title {
+                        font-weight: bold;
+                        margin-bottom: 5px;
+                    }
+                    .notification-message {
+                        font-size: 14px;
+                    }
+                    .notification-close {
+                        background: none;
+                        border: none;
+                        color: white;
+                        font-size: 20px;
+                        cursor: pointer;
+                        margin-left: 10px;
+                        padding: 0 5px;
+                    }
+                    @keyframes slideIn {
+                        from { transform: translateX(100%); opacity: 0; }
+                        to { transform: translateX(0); opacity: 1; }
+                    }
+                    @keyframes slideOut {
+                        from { transform: translateX(0); opacity: 1; }
+                        to { transform: translateX(100%); opacity: 0; }
+                    }
+                `;
+                
+                // 添加到页面
+                document.head.appendChild(style);
+                document.body.appendChild(notificationEl);
+                
+                // 点击关闭按钮移除通知
+                const closeBtn = notificationEl.querySelector('.notification-close');
+                if (closeBtn) {
+                    closeBtn.addEventListener('click', () => {
+                        notificationEl.style.animation = 'slideOut 0.3s ease-out forwards';
+                        notificationEl.addEventListener('animationend', () => {
+                            notificationEl.remove();
+                        });
+                    });
+                }
+                
+                // 3秒后自动移除(从5秒改为3秒)
+                setTimeout(() => {
+                    notificationEl.style.animation = 'slideOut 0.3s ease-out forwards';
+                    notificationEl.addEventListener('animationend', () => {
+                        notificationEl.remove();
+                    });
+                }, 3000);
+                
+                console.log('页内通知已显示,将在3秒后自动关闭');
+            } catch (error) {
+                console.error('创建页内通知出错:', error);
+            }
+        },
+
+        async getFavoritesData() {
+            return new Promise((resolve) => {
+                chrome.storage.local.get('favorites', (result) => {
+                    resolve(result.favorites || []);
+                });
+            });
+        },
+
+        async getRecentUsedData() {
+            // 直接从Statistics模块获取最近使用的工具
+            return await Statistics.getRecentUsedTools(10);
+        },
+
+        async saveFavorites() {
+            try {
+                await chrome.storage.local.set({
+                    favorites: Array.from(this.favorites)
+                });
+                // 更新工具的收藏状态
+                Object.keys(this.originalTools).forEach(key => {
+                    this.originalTools[key].favorite = this.favorites.has(key);
+                });
+            } catch (error) {
+                console.error('保存收藏失败:', error);
+            }
+        },
+
+        handleSearch() {
+            // 搜索时不重置视图类型,允许在已过滤的结果中搜索
+        },
+
+        handleCategoryChange(category) {
+            // 切换到全部工具视图
+            if (this.currentView !== 'all') {
+                this.currentView = 'all';
+                this.updateActiveTools('all');
+            }
+            this.currentCategory = category;
+            this.searchKey = '';
+            // 确保工具显示正确
+            this.activeTools = { ...this.originalTools };
+            this.showDashboard = false;
+        },
+
+        handleSort() {
+            // 排序逻辑已在computed中实现
+        },
+
+        getCategoryCount(categoryKey) {
+            const category = TOOL_CATEGORIES.find(c => c.key === categoryKey);
+            const categoryTools = category ? category.tools : [];
+            return categoryTools.length;
+        },
+
+        async getInstalledCount() {
+            try {
+                // 使用Awesome.getInstalledTools实时获取已安装工具数量
+                const installedTools = await Awesome.getInstalledTools();
+                return Object.keys(installedTools).length;
+            } catch (error) {
+                console.error('获取已安装工具数量失败:', error);
+                // 回退到本地数据
+                return Object.values(this.originalTools).filter(tool => 
+                    tool.installed || tool.systemInstalled || false
+                ).length;
+            }
+        },
+
+        getFavoritesCount() {
+            return this.favorites.size;
+        },
+
+        getToolCategory(toolKey) {
+            for (const category of TOOL_CATEGORIES) {
+                if (category.tools.includes(toolKey)) {
+                    return category.key;
+                }
+            }
+            return 'other';
+        },
+
+        async showMyInstalled() {
+            this.currentView = 'installed';
+            this.currentCategory = '';
+            this.searchKey = '';
+            await this.updateActiveTools('installed');
+            // 更新已安装工具数量
+            await this.updateInstalledCount();
+            this.showDashboard = false;
+        },
+
+        showMyFavorites() {
+            this.currentView = 'favorites';
+            this.currentCategory = '';
+            this.searchKey = '';
+            this.updateActiveTools('favorites');
+            this.showDashboard = false;
+        },
+
+        async showRecentUsed() {
+            this.currentView = 'recent';
+            this.currentCategory = '';
+            this.searchKey = '';
+            // 拉取DashBoard数据并显示
+            this.dashboardData = await Statistics.getDashboardData();
+            this.showDashboard = true;
+            // 不再更新工具列表
+        },
+
+        // 关闭DashBoard,恢复工具列表
+        closeDashboard() {
+            this.showDashboard = false;
+            this.currentView = 'all';
+            this.updateActiveTools('all');
+        },
+
+        // 重置工具列表到原始状态
+        resetTools() {
+            this.currentView = 'all';
+        },
+
+        // 安装工具
+        async installTool(toolKey) {
+            try {
+                // 查找可能存在的按钮元素
+                const btnElement = document.querySelector(`button[data-tool="${toolKey}"]`);
+                let elProgress = null;
+                
+                // 如果是通过按钮点击调用的,获取进度条元素
+                if (btnElement) {
+                    if (btnElement.getAttribute('data-undergoing') === '1') {
+                        return false;
+                    }
+                    btnElement.setAttribute('data-undergoing', '1');
+                    elProgress = btnElement.querySelector('span.x-progress');
+                }
+                
+                // 显示安装进度
+                let pt = 1;
+                await Awesome.install(toolKey);
+                
+                // 只有当进度条元素存在时才更新文本内容
+                if (elProgress) {
+                    elProgress.textContent = `(${pt}%)`;
+                    let ptInterval = setInterval(() => {
+                        elProgress.textContent = `(${pt}%)`;
+                        pt += Math.floor(Math.random() * 20);
+                        if(pt > 100) {
+                            clearInterval(ptInterval);
+                            elProgress.textContent = ``;
+                            
+                            // 在进度条完成后显示安装成功的通知
+                            this.showInPageNotification({
+                                message: `${this.originalTools[toolKey].name} 安装成功!`,
+                                type: 'success',
+                                duration: 3000
+                            });
+                        }
+                    }, 100);
+                } else {
+                    // 如果没有进度条元素,直接显示通知
+                    this.showInPageNotification({
+                        message: `${this.originalTools[toolKey].name} 安装成功!`,
+                        type: 'success',
+                        duration: 3000
+                    });
+                }
+                
+                // 更新原始数据和当前活动数据
+                this.originalTools[toolKey].installed = true;
+                if (this.activeTools[toolKey]) {
+                    this.activeTools[toolKey].installed = true;
+                }
+                
+                // 更新已安装工具数量
+                this.updateInstalledCount();
+                
+                // 如果按钮存在,更新其状态
+                if (btnElement) {
+                    btnElement.setAttribute('data-undergoing', '0');
+                }
+                
+                // 发送消息通知后台更新
+                chrome.runtime.sendMessage({
+                    type: MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD,
+                    toolName: toolKey,
+                    action: 'install',
+                    showTips: true
+                });
+                
+            } catch (error) {
+                console.error('安装工具失败:', error);
+                
+                // 显示安装失败的通知
+                this.showInPageNotification({
+                    message: `安装失败:${error.message || '未知错误'}`,
+                    type: 'error',
+                    duration: 5000
+                });
+            }
+        },
+
+        // 卸载工具
+        async uninstallTool(toolKey) {
+            try {
+                // 使用自定义确认对话框而非浏览器原生的confirm
+                this.showConfirm({
+                    title: '卸载确认',
+                    message: `确定要卸载"${this.originalTools[toolKey].name}"工具吗?`,
+                    callback: async (key) => {
+                        try {
+                            await chrome.runtime.sendMessage({
+                                type: MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD,
+                                toolName: key,
+                                action: 'offload',
+                                showTips: true
+                            });
+                            
+                            // 调用Awesome.offLoad卸载工具
+                            await Awesome.offLoad(key);
+                            
+                            // 更新原始数据和当前活动数据
+                            this.originalTools[key].installed = false;
+                            this.originalTools[key].inContextMenu = false;
+                            
+                            if (this.activeTools[key]) {
+                                this.activeTools[key].installed = false;
+                                this.activeTools[key].inContextMenu = false;
+                            }
+                            
+                            // 更新已安装工具数量
+                            this.updateInstalledCount();
+                            
+                            // 显示卸载成功的通知
+                            this.showInPageNotification({
+                                message: `${this.originalTools[key].name} 已成功卸载!`,
+                                type: 'success',
+                                duration: 3000
+                            });
+                        } catch (error) {
+                            console.error('卸载工具失败:', error);
+                            
+                            // 显示卸载失败的通知
+                            this.showInPageNotification({
+                                message: `卸载失败:${error.message || '未知错误'}`,
+                                type: 'error',
+                                duration: 5000
+                            });
+                        }
+                    },
+                    data: toolKey
+                });
+            } catch (error) {
+                console.error('准备卸载过程中出错:', error);
+            }
+        },
+
+        // 切换右键菜单
+        async toggleContextMenu(toolKey) {
+            try {
+                const tool = this.originalTools[toolKey];
+                const newState = !tool.inContextMenu;
+                
+                // 更新菜单状态
+                await Awesome.menuMgr(toolKey, newState ? 'install' : 'offload');
+                
+                // 更新原始数据和当前活动数据
+                tool.inContextMenu = newState;
+                if (this.activeTools[toolKey]) {
+                    this.activeTools[toolKey].inContextMenu = newState;
+                }
+                
+                // 发送消息通知后台更新右键菜单
+                chrome.runtime.sendMessage({
+                    type: MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD,
+                    action: `menu-${newState ? 'install' : 'offload'}`,
+                    showTips: false,
+                    menuOnly: true
+                });
+            } catch (error) {
+                console.error('切换右键菜单失败:', error);
+            }
+        },
+
+        // 切换收藏状态
+        async toggleFavorite(toolKey) {
+            try {
+                if (this.favorites.has(toolKey)) {
+                    this.favorites.delete(toolKey);
+                    // 更新原始数据和当前活动数据
+                    this.originalTools[toolKey].favorite = false;
+                    if (this.activeTools[toolKey]) {
+                        this.activeTools[toolKey].favorite = false;
+                    }
+                } else {
+                    this.favorites.add(toolKey);
+                    // 更新原始数据和当前活动数据
+                    this.originalTools[toolKey].favorite = true;
+                    if (this.activeTools[toolKey]) {
+                        this.activeTools[toolKey].favorite = true;
+                    }
+                }
+                await this.saveFavorites();
+                
+                // 如果是在收藏视图,需要更新视图
+                if (this.currentView === 'favorites') {
+                    this.updateActiveTools('favorites');
+                }
+            } catch (error) {
+                console.error('切换收藏状态失败:', error);
+            }
+        },
+
+        async updateActiveTools(view) {
+            if (this.loading || Object.keys(this.originalTools).length === 0) {
+                return;
+            }
+
+            switch (view) {
+                case 'installed':
+                    // 使用Awesome.getInstalledTools实时获取已安装工具
+                    try {
+                        const installedTools = await Awesome.getInstalledTools();
+                        // 合并installedTools与originalTools的数据
+                        this.activeTools = Object.fromEntries(
+                            Object.entries(this.originalTools).filter(([key]) => 
+                                installedTools.hasOwnProperty(key)
+                            )
+                        );
+                    } catch (error) {
+                        console.error('获取已安装工具失败:', error);
+                        // 回退到本地数据
+                        this.activeTools = Object.fromEntries(
+                            Object.entries(this.originalTools).filter(([_, tool]) => 
+                                tool.installed || tool.systemInstalled || false
+                            )
+                        );
+                    }
+                    break;
+                case 'favorites':
+                    this.activeTools = Object.fromEntries(
+                        Object.entries(this.originalTools).filter(([key]) => this.favorites.has(key))
+                    );
+                    break;
+                case 'recent':
+                    // 切换recent时,recentUsed已在showRecentUsed中实时拉取
+                    this.activeTools = Object.fromEntries(
+                        Object.entries(this.originalTools).filter(([key]) => this.recentUsed.includes(key))
+                    );
+                    break;
+                case 'all':
+                default:
+                    this.activeTools = { ...this.originalTools };
+                    // 分类过滤在computed属性中处理
+                    break;
+            }
+        },
+
+        // 新增更新已安装工具数量的方法
+        async updateInstalledCount() {
+            this.installedCount = await this.getInstalledCount();
+        },
+
+        // 加载用户保存的视图模式
+        async loadViewMode() {
+            try {
+                const result = await new Promise(resolve => {
+                    chrome.storage.local.get('fehelper_view_mode', result => {
+                        resolve(result.fehelper_view_mode);
+                    });
+                });
+                
+                if (result) {
+                    this.viewMode = result;
+                }
+            } catch (error) {
+                console.error('加载视图模式失败:', error);
+            }
+        },
+
+        // 保存用户的视图模式选择
+        async saveViewMode(mode) {
+            try {
+                this.viewMode = mode;
+                await chrome.storage.local.set({
+                    'fehelper_view_mode': mode
+                });
+            } catch (error) {
+                console.error('保存视图模式失败:', error);
+            }
+        },
+
+        // 加载设置项
+        async loadSettings() {
+            try {
+                Settings.getOptions(async (opts) => {
+                    let selectedOpts = [];
+                    Object.keys(opts).forEach(key => {
+                        if(String(opts[key]) === 'true') {
+                            selectedOpts.push(key);
+                        }
+                    });
+                    this.selectedOpts = selectedOpts;
+                    
+                    // 加载右键菜单设置
+                    this.menuDownloadCrx = await Awesome.menuMgr('download-crx', 'get') === '1';
+                    this.menuFeHelperSeting = await Awesome.menuMgr('fehelper-setting', 'get') !== '0';
+                    
+                    // 获取快捷键
+                    chrome.commands.getAll((commands) => {
+                        for (let command of commands) {
+                            if (command.name === '_execute_action') {
+                                this.defaultKey = command.shortcut || 'Alt+Shift+J';
+                                break;
+                            }
+                        }
+                    });
+                });
+            } catch (error) {
+                console.error('加载设置项失败:', error);
+            }
+        },
+        
+        // 检查浏览器类型
+        checkBrowserType() {
+            try {
+                this.isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
+            } catch (error) {
+                console.error('检查浏览器类型失败:', error);
+                this.isFirefox = false;
+            }
+        },
+        
+        // 显示设置模态框
+        showSettings() {
+            this.showSettingsModal = true;
+        },
+
+        // 关闭设置模态框
+        closeSettings() {
+            this.showSettingsModal = false;
+        },
+
+        // 显示打赏模态框
+        openDonateModal() {
+            this.showDonateModal = true;
+        },
+
+        // 关闭打赏模态框
+        closeDonateModal() {
+            this.showDonateModal = false;
+        },
+
+        // 显示确认对话框
+        showConfirm(options) {
+            this.confirmDialog = {
+                show: true,
+                title: options.title || '操作确认',
+                message: options.message || '确定要执行此操作吗?',
+                callback: options.callback || null,
+                data: options.data || null
+            };
+        },
+
+        // 确认操作
+        confirmAction() {
+            if (this.confirmDialog.callback) {
+                this.confirmDialog.callback(this.confirmDialog.data);
+            }
+            this.confirmDialog.show = false;
+        },
+
+        // 取消确认
+        cancelConfirm() {
+            this.confirmDialog.show = false;
+        },
+
+        // 保存设置
+        async saveSettings() {
+            try {
+                // 构建设置对象
+                let opts = {};
+                ['OPT_ITEM_CONTEXTMENUS', 'FORBID_OPEN_IN_NEW_TAB', 'CONTENT_SCRIPT_ALLOW_ALL_FRAMES', 
+                 'JSON_PAGE_FORMAT', 'AUTO_DARK_MODE', 'ALWAYS_DARK_MODE'].forEach(key => {
+                    opts[key] = this.selectedOpts.includes(key).toString();
+                });
+                
+                // 保存设置 - 直接传递对象,settings.js已增加对对象类型的支持
+                Settings.setOptions(opts, async () => {
+                    try {
+                        // 处理右键菜单
+                        const crxAction = this.menuDownloadCrx ? 'install' : 'offload';
+                        const settingAction = this.menuFeHelperSeting ? 'install' : 'offload';
+                        
+                        await Promise.all([
+                            Awesome.menuMgr('download-crx', crxAction),
+                            Awesome.menuMgr('fehelper-setting', settingAction)
+                        ]);
+                        
+                        // 通知后台更新右键菜单
+                        chrome.runtime.sendMessage({
+                            type: MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD,
+                            action: 'menu-change',
+                            menuOnly: true
+                        });
+                        
+                        // 关闭弹窗
+                        this.closeSettings();
+                        
+                        // 显示提示
+                        this.showNotification({
+                            title: 'FeHelper 设置',
+                            message: '设置已保存!'
+                        });
+                    } catch (innerError) {
+                        console.error('保存菜单设置失败:', innerError);
+                        this.showNotification({
+                            title: 'FeHelper 设置错误',
+                            message: '保存菜单设置失败: ' + innerError.message
+                        });
+                    }
+                });
+            } catch (error) {
+                console.error('保存设置失败:', error);
+                this.showNotification({
+                    title: 'FeHelper 设置错误',
+                    message: '保存设置失败: ' + error.message
+                });
+            }
+        },
+        
+        // 设置快捷键
+        setShortcuts() {
+            chrome.tabs.create({
+                url: 'chrome://extensions/shortcuts'
+            });
+        },
+        
+        // 体验夜间模式
+        turnLight(event) {
+            event.preventDefault();
+            
+            // 获取body元素
+            const body = document.body;
+            
+            // 切换夜间模式
+            if (body.classList.contains('dark-mode')) {
+                body.classList.remove('dark-mode');
+            } else {
+                body.classList.add('dark-mode');
+                
+                // 设置倒计时
+                this.countDown = 10;
+                
+                // 启动倒计时
+                const timer = setInterval(() => {
+                    this.countDown--;
+                    if (this.countDown <= 0) {
+                        clearInterval(timer);
+                        body.classList.remove('dark-mode');
+                    }
+                }, 1000);
+            }
+        },
+
+        // 检查URL中的donate_from参数并显示打赏弹窗
+        checkDonateParam() {
+            try {
+                const urlParams = new URLSearchParams(window.location.search);
+                const donateFrom = urlParams.get('donate_from');
+                
+                if (donateFrom) {
+                    console.log('检测到打赏来源参数:', donateFrom);
+                    
+                    // 记录打赏来源
+                    chrome.storage.local.set({
+                        'fehelper_donate_from': donateFrom,
+                        'fehelper_donate_time': Date.now()
+                    });
+                    
+                    // 等待工具数据加载完成
+                    this.$nextTick(() => {
+                        // 在所有工具中查找匹配项
+                        let matchedTool = null;
+                        
+                        // 首先尝试直接匹配工具key
+                        if (this.originalTools && this.originalTools[donateFrom]) {
+                            matchedTool = this.originalTools[donateFrom];
+                        } else if (this.originalTools) {
+                            // 如果没有直接匹配,尝试在所有工具中查找部分匹配
+                            for (const [key, tool] of Object.entries(this.originalTools)) {
+                                if (key.includes(donateFrom) || donateFrom.includes(key) ||
+                                    (tool.name && tool.name.includes(donateFrom)) || 
+                                    (donateFrom && donateFrom.includes(tool.name))) {
+                                    matchedTool = tool;
+                                    break;
+                                }
+                            }
+                        }
+                        
+                        // 更新打赏文案
+                        if (matchedTool) {
+                            this.donate.text = `看起来【${matchedTool.name}】工具帮助到了你,感谢你的认可!`;
+                        } else {
+                            // 没有匹配到特定工具,使用通用文案
+                            this.donate.text = `感谢你对FeHelper的认可和支持!`;
+                        }
+                        
+                        // 显示打赏弹窗
+                        this.showDonateModal = true;
+                    });
+                }
+            } catch (error) {
+                console.error('处理打赏参数时出错:', error);
+            }
+        },
+
+        // 补充 getRecentCount,保证模板调用不报错,且数据源唯一
+        async getRecentCount() {
+            const recent = await Statistics.getRecentUsedTools(10);
+            return recent.length;
+        },
+
+        renderDashboard() {
+            const dashboardContainerId = 'fh-dashboard-panel';
+            let container = document.getElementById(dashboardContainerId);
+            // 只在showDashboard且currentView为recent时隐藏工具列表
+            const grid = document.querySelector('.tools-grid');
+            if (!this.showDashboard || this.currentView !== 'recent') {
+                if (container) container.style.display = 'none';
+                if (grid) grid.style.display = '';
+                return;
+            }
+            if (grid) grid.style.display = 'none';
+            if (!container) {
+                container = document.createElement('div');
+                container.id = dashboardContainerId;
+                container.style = 'padding:32px; background:#fff; border-radius:8px; margin:24px; box-shadow:0 2px 12px #eee; min-width:700px;';
+                const main = document.querySelector('.market-main') || document.querySelector('.market-content');
+                if (main) main.prepend(container);
+                else document.body.appendChild(container);
+            }
+            container.style.display = 'block';
+            const data = this.dashboardData || {};
+            // 工具ID转中文名和icon
+            const toolName = (key) => (this.originalTools && this.originalTools[key] && this.originalTools[key].name) ? this.originalTools[key].name : key;
+            const toolIcon = (key) => {
+                if (toolMap[key] && toolMap[key].menuConfig && toolMap[key].menuConfig[0] && toolMap[key].menuConfig[0].icon) {
+                    return toolMap[key].menuConfig[0].icon;
+                }
+                return toolName(key).slice(0,1);
+            };
+            // 插入美观样式
+            if (!document.getElementById('fh-dashboard-style')) {
+                const style = document.createElement('style');
+                style.id = 'fh-dashboard-style';
+                style.innerHTML = `
+                .fh-dashboard-cards { display: flex; flex-wrap: wrap; gap: 18px; margin-bottom: 24px;}
+                .fh-card { background: linear-gradient(135deg,#f7f9fa 60%,#e3eafc 100%); border-radius: 12px; box-shadow:0 2px 8px #f0f0f0; padding:18px 24px; min-width:120px; flex:1; text-align:center; font-size:15px;}
+                .fh-card.main { background: linear-gradient(135deg,#e3fcec 60%,#e3eafc 100%);}
+                .fh-card-num { font-size:32px; font-weight:bold; margin-bottom:4px;}
+                .fh-calendar { display:inline-block; margin-left:12px; }
+                .fh-cal-cell { display:inline-block; width:18px; height:18px; line-height:18px; text-align:center; border-radius:3px; margin:1px; background:#eee; color:#888; font-size:12px;}
+                .fh-cal-cell.used { background:#4285f4; color:#fff; font-weight:bold;}
+                .fh-dashboard-section { background:#fff; border-radius:12px; box-shadow:0 1px 4px #f0f0f0; padding:18px 24px; margin-bottom:24px;}
+                .fh-dashboard-header { margin-bottom:24px; }
+                .fh-dashboard-header h2 { font-size:22px; margin:0; }
+                .fh-tool-bar { display:inline-block; width:18px; height:18px; border-radius:3px; background:#e3eafc; margin-right:6px; vertical-align:middle; }
+                .fh-tool-bar-inner { display:inline-block; height:100%; border-radius:3px; background:#4285f4; }
+                .fh-tool-list { margin:0; padding:0; list-style:none; }
+                .fh-tool-list li { margin-bottom:10px; }
+                .fh-tool-icon { display:inline-block; width:18px; height:18px; border-radius:3px; background:#e3eafc; margin-right:6px; vertical-align:middle; text-align:center; font-size:14px; }
+                .fh-dashboard-sub { color:#888; font-size:13px; margin-bottom:8px; }
+                `;
+                document.head.appendChild(style);
+            }
+            // 30天活跃日历
+            const today = new Date();
+            let calendar = '<div class="fh-calendar">';
+            for(let i=29;i>=0;i--){
+                const d = new Date(today.getTime()-i*86400000);
+                const ds = d.toISOString().slice(0,10);
+                const used = data.allDates && data.allDates.includes(ds);
+                calendar += `<span class="fh-cal-cell${used?' used':''}" title="${ds}">${d.getDate()}</span>`;
+            }
+            calendar += '</div>';
+            // 主卡片区块
+            let html = `
+            <div class="fh-dashboard-header">
+              <h2>FeHelper 使用统计仪表盘 <span style="font-size:16px;color:#bbb;">(近30天)</span></h2>
+            </div>
+            <div class="fh-dashboard-cards">
+              <div class="fh-card main"><div class="fh-card-num">${data.totalCount||0}</div><div>总使用次数</div></div>
+              <div class="fh-card main"><div class="fh-card-num">${data.activeDays||0}</div><div>活跃天数</div></div>
+              <div class="fh-card"><div>${data.firstDate||'-'}<br>~<br>${data.lastDate||'-'}</div><div>统计区间</div></div>
+              <div class="fh-card"><div class="fh-card-num">${data.maxStreak||0}</div><div>最长连续活跃天数</div></div>
+              <div class="fh-card"><div class="fh-card-num">${data.monthCount||0}</div><div>本月使用次数</div></div>
+              <div class="fh-card"><div class="fh-card-num">${data.weekCount||0}</div><div>本周使用次数</div></div>
+              <div class="fh-card"><div class="fh-card-num">${data.avgPerDay||0}</div><div>平均每日使用</div></div>
+              <div class="fh-card"><div>${data.maxDay.date||'-'}<br><b>${data.maxDay.count||0}</b></div><div>最活跃日</div></div>
+              <div class="fh-card"><div class="fh-card-num">${data.daysSinceLast||0}</div><div>最近未使用天数</div></div>
+            </div>
+            <div class="fh-dashboard-section">
+                <div class="fh-dashboard-sub">近30天活跃日历:</div>${calendar}
+            </div>
+            <div class="fh-dashboard-section" style="display:flex;gap:32px;flex-wrap:wrap;">
+                <div style="flex:2;min-width:320px;">
+                    <div class="fh-dashboard-sub"><b>最近10天活跃趋势:</b></div>
+                    <div style="display:flex;align-items:end;height:80px;margin-top:8px;">
+                        ${
+                            (data.dailyTrend||[]).map(d=>{
+                                const max = Math.max(...(data.dailyTrend||[]).map(x=>x.count),1);
+                                return `<div title='${d.date}: ${d.count}' style='width:20px;height:${d.count/max*60}px;background:#4285f4;margin-right:4px;border-radius:2px;'></div>`;
+                            }).join('')
+                        }
+                    </div>
+                    <div style="font-size:12px;color:#888;margin-top:4px;">
+                        ${(data.dailyTrend||[]).map(d=>`<span style='display:inline-block;width:20px;text-align:center;'>${d.date.slice(5)}</span>`).join('')}
+                    </div>
+                </div>
+                <div style="flex:3;min-width:320px;">
+                    <div class="fh-dashboard-sub"><b>使用最多的工具:</b></div>
+                    <ul class="fh-tool-list">
+                        ${(data.mostUsed||[]).map(t=>{
+                            const percent = data.totalCount ? Math.round(t.count/data.totalCount*100) : 0;
+                            return `<li style='margin-bottom:12px;display:flex;align-items:center;'>
+                                <span class='fh-tool-icon'>${toolIcon(t.name)}</span>
+                                <span style='display:inline-block;width:100px;'>${toolName(t.name)}</span>
+                                <span style='display:inline-block;width:60px;color:#888;'>(x${t.count})</span>
+                                <span class='fh-tool-bar' style='width:80px;height:10px;margin:0 8px;'>
+                                    <span class='fh-tool-bar-inner' style='width:${percent*0.8}px;'></span>
+                                </span>
+                                <span style='color:#888;'>${percent}%</span>
+                            </li>`;
+                        }).join('')}
+                    </ul>
+                </div>
+            </div>
+            <div class="fh-dashboard-section">
+                <div class="fh-dashboard-sub"><b>最近10次使用的工具:</b></div>
+                <ul style="margin:8px 0 0 0;padding:0;list-style:none;">
+                    ${(data.recentDetail||[]).map(t=>`<li style='display:inline-block;margin-right:24px;'>${toolName(t.tool)} <span style='color:#888;'>(${t.date})</span></li>`).join('')}
+                </ul>
+            </div>
+            `;
+            container.innerHTML = html;
+            window.__vue__ = this;
+        },
+    },
+
+    watch: {
+        // 监听currentView变化
+        currentView: {
+            immediate: true,
+            handler(newView) {
+                this.updateActiveTools(newView);
+            }
+        },
+        
+        // 监听currentCategory变化
+        currentCategory: {
+            handler(newCategory) {
+                // 保证在视图模式之外的分类切换也能正确显示
+                if (this.currentView === 'all') {
+                    this.activeTools = { ...this.originalTools };
+                }
+                // 重置搜索条件
+                if (this.searchKey) {
+                    this.searchKey = '';
+                }
+            }
+        },
+        showDashboard(val) {
+            this.renderDashboard();
+        },
+        dashboardData(val) {
+            this.renderDashboard();
+        },
+    },
+
+    mounted() {
+        this.$nextTick(() => {
+            this.renderDashboard();
+        });
+    },
+});
+
+// 添加滚动事件监听
+window.addEventListener('scroll', () => {
+    const header = document.querySelector('.market-header');
+    const sidebar = document.querySelector('.market-sidebar');
+    
+    if (window.scrollY > 10) {
+        header.classList.add('scrolled');
+        sidebar && sidebar.classList.add('scrolled');
+    } else {
+        header.classList.remove('scrolled');
+        sidebar && sidebar.classList.remove('scrolled');
+    }
+}); 

+ 6 - 2
package.json

@@ -4,7 +4,9 @@
   "description": "FeHelper-前端助手",
   "main": "index.js",
   "dependencies": {
-    "crypto": "^1.0.1"
+    "crypto": "^1.0.1",
+    "geoip-lite": "^1.4.10",
+    "ua-parser-js": "^2.0.3"
   },
   "devDependencies": {
     "@babel/core": "^7.26.10",
@@ -19,6 +21,7 @@
     "gulp-uglify-es": "^3.0.0",
     "gulp-uglifycss": "^1.0.9",
     "gulp-zip": "^4.1.0",
+    "nodemon": "^3.1.10",
     "pretty-bytes": "^4.0.2",
     "run-sequence": "^2.2.1",
     "shelljs": "^0.8.1"
@@ -26,7 +29,8 @@
   "scripts": {
     "watch": "gulp watch",
     "build": "gulp",
-    "preinstall": "npx npm-force-resolutions"
+    "preinstall": "npx npm-force-resolutions",
+    "dev": "nodemon server/index.js"
   },
   "repository": {
     "type": "git",

+ 47 - 0
server/admin/css/admin.css

@@ -0,0 +1,47 @@
+/* 保证主内容区不出现横向滚动条 */
+#main-content {
+  overflow-x: hidden;
+}
+
+/* 图表区域优化:让grid自适应且不超屏 */
+.grid {
+  min-width: 0;
+}
+
+/* 图表容器自适应宽度,防止溢出 */
+.echart-container, .echarts {
+  max-width: 100%;
+  box-sizing: border-box;
+}
+
+/* 响应式优化,防止大屏下间距过大 */
+@media (min-width: 1024px) {
+  .grid-cols-2 > * {
+    min-width: 0;
+  }
+  .grid-cols-3 > * {
+    min-width: 0;
+  }
+}
+
+/* 让body和html始终100vw,防止外部溢出 */
+html, body {
+  max-width: 100vw;
+  overflow-x: hidden;
+}
+
+.echart-parent {
+  max-width: 220px;
+  min-width: 0;
+  width: 220px;
+  height: 220px;
+  overflow: hidden;
+  margin: 0 auto;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.echart-parent > div {
+  width: 100% !important;
+  height: 100% !important;
+} 

BIN
server/admin/img/favicon.ico


BIN
server/admin/img/fe-48.png


+ 17 - 0
server/admin/index.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="zh">
+<head>
+  <meta charset="UTF-8">
+  <title>FeHelper 数据统计后台</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <link rel="icon" href="./img/favicon.ico">
+  <link rel="stylesheet" href="css/admin.css">
+  <script src="https://cdn.tailwindcss.com"></script>
+  <script src="js/vue.global.prod.js"></script>
+  <script src="js/echarts.min.js"></script>
+</head>
+<body class="bg-gray-100">
+  <div id="app"></div>
+  <script src="js/admin.js" type="module"></script>
+</body>
+</html> 

+ 360 - 0
server/admin/js/admin.js

@@ -0,0 +1,360 @@
+console.log('Admin.js script execution started.'); // 最顶部的日志
+
+// 管理后台前端主JS(Vue 3 组件化重构)
+
+const { createApp, ref, reactive, onMounted, defineComponent, watch } = Vue;
+console.log('Vue library loaded, createApp function:', typeof createApp); // 检查 Vue 是否加载成功
+
+const apiBase = '/api/admin';
+console.log('Defining components...'); // 日志点 1
+
+// 顶部导航栏(无打赏按钮,仅限本人使用)
+const HeaderNav = defineComponent({
+  template: `
+    <header class="w-full h-14 bg-white shadow flex items-center justify-between px-6 fixed top-0 left-0 z-10">
+      <div class="flex items-center space-x-3">
+        <img src="./img/fe-48.png" alt="FeHelper" class="h-8 w-8">
+        <span class="text-xl font-bold tracking-wide">FeHelper 数据统计后台</span>
+      </div>
+      <div class="flex items-center space-x-4">
+        <span class="text-gray-500 text-sm">仅限本人使用</span>
+        <svg class="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5.121 17.804A13.937 13.937 0 0112 15c2.485 0 4.797.607 6.879 1.804M15 11a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
+      </div>
+    </header>
+  `
+});
+
+// 统计总览卡片
+const OverviewPanel = defineComponent({
+  props: ['overview'],
+  template: `
+    <div class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-6">
+      <div class="bg-white rounded shadow p-4 flex flex-col items-center">
+        <div class="text-2xl font-bold">{{overview.userCount || 0}}</div>
+        <div class="text-xs text-gray-500 mt-1">累计用户数</div>
+      </div>
+      <div class="bg-white rounded shadow p-4 flex flex-col items-center">
+        <div class="text-2xl font-bold">{{overview.todayActive || 0}}</div>
+        <div class="text-xs text-gray-500 mt-1">今日活跃用户</div>
+      </div>
+      <div class="bg-white rounded shadow p-4 flex flex-col items-center">
+        <div class="text-2xl font-bold">{{overview.monthUserCount || 0}}</div>
+        <div class="text-xs text-gray-500 mt-1">近一月活跃用户</div>
+      </div>
+      <div class="bg-white rounded shadow p-4 flex flex-col items-center">
+        <div class="text-2xl font-bold">{{overview.monthUserRate || '0%'}}</div>
+        <div class="text-xs text-gray-500 mt-1">近一月用户占比</div>
+      </div>
+      <div class="bg-white rounded shadow p-4 flex flex-col items-center">
+        <div class="text-2xl font-bold">{{overview.eventCount || 0}}</div>
+        <div class="text-xs text-gray-500 mt-1">累计埋点事件数</div>
+      </div>
+    </div>
+  `
+});
+
+// 工具英文名到中文名映射
+const toolNameMap = {
+  'json-format': 'JSON美化工具',
+  'json-diff': 'JSON比对工具',
+  'qr-code': '二维码/解码',
+  'image-base64': '图片转Base64',
+  'en-decode': '信息编码转换',
+  'code-beautify': '代码美化工具',
+  'code-compress': '代码压缩工具',
+  'aiagent': 'AI,请帮帮忙',
+  'timestamp': '时间(戳)转换',
+  'password': '随机密码生成',
+  'sticky-notes': '我的便签笔记',
+  'html2markdown': 'Markdown转换',
+  'postman': '简易Postman',
+  'websocket': 'Websocket工具',
+  'regexp': '正则公式速查',
+  'trans-radix': '进制转换工具',
+  'trans-color': '颜色转换工具',
+  'crontab': 'Crontab工具',
+  'loan-rate': '贷(还)款利率',
+  'devtools': 'FH开发者工具',
+  'page-monkey': '网页油猴工具',
+  'screenshot': '网页截屏工具',
+  'color-picker': '页面取色工具',
+  'naotu': '便捷思维导图',
+  'grid-ruler': '网页栅格标尺',
+  'page-timing': '网站性能优化',
+  'excel2json': 'Excel转JSON',
+  'chart-maker': '图表制作工具',
+  'svg-converter': 'SVG转为图片',
+  'poster-maker': '海报快速生成',
+
+  "popup": "FH Popup页面",
+  "options": "FH插件市场",
+};
+
+// 工具排行
+const TopTools = defineComponent({
+  props: ['tools'],
+  template: `
+    <div class="bg-white rounded shadow p-4">
+      <div class="font-bold mb-2">FeHelper工具使用排名</div>
+      <ol class="list-decimal ml-6 text-sm text-gray-700">
+        <li v-if="tools.length === 0">暂无数据</li>
+        <li v-for="tool in tools" :key="tool.name">
+          {{tool.name}} <span class="text-gray-400">({{tool.pv}})</span>
+        </li>
+      </ol>
+    </div>
+  `
+});
+
+// 分布表格
+const SimpleTable = defineComponent({
+  props: ['title', 'data', 'label'],
+  template: `
+    <div class="bg-white rounded shadow p-4">
+      <div class="font-bold mb-2">{{title}}</div>
+      <table class="min-w-full text-xs border border-gray-200">
+        <thead>
+          <tr>
+            <th class="px-2 py-1 border-b border-gray-200 bg-gray-50">{{label}}</th>
+            <th class="px-2 py-1 border-b border-gray-200 bg-gray-50">UV(用户数)</th>
+            <th class="px-2 py-1 border-b border-gray-200 bg-gray-50">PV(访问次数)</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-if="data.length === 0">
+            <td colspan="3" class="px-2 py-1 text-center">暂无数据</td>
+          </tr>
+          <tr v-for="row in data" :key="row._id" class="border-b border-gray-100">
+            <td class="px-2 py-1 border-r border-gray-100">{{row._id}}</td>
+            <td class="px-2 py-1 border-r border-gray-100">{{row.uv}}</td>
+            <td class="px-2 py-1">{{row.pv}}</td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+  `
+});
+
+// 错误提示
+const ErrorAlert = defineComponent({
+  props: ['message'],
+  template: `
+    <div v-if="message" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4 relative">
+      <strong class="font-bold">错误:</strong>
+      <span class="block sm:inline">{{message}}</span>
+    </div>
+  `
+});
+
+const eventTrend = ref([]);
+
+// 新增事件趋势表格
+const EventTrendTable = defineComponent({
+  props: ['data'],
+  template: `
+    <div class="bg-white rounded shadow p-4">
+      <div class="font-bold mb-2">事件趋势(最近30天)</div>
+      <table class="min-w-full text-xs border border-gray-200">
+        <thead>
+          <tr>
+            <th class="px-2 py-1 border-b border-gray-200 bg-gray-50">日期</th>
+            <th class="px-2 py-1 border-b border-gray-200 bg-gray-50">UV(用户数)</th>
+            <th class="px-2 py-1 border-b border-gray-200 bg-gray-50">PV(访问次数)</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-if="data.length === 0">
+            <td colspan="3" class="px-2 py-1 text-center">暂无数据</td>
+          </tr>
+          <tr v-for="row in data" :key="row._id" class="border-b border-gray-100">
+            <td class="px-2 py-1 border-r border-gray-100">{{row._id}}</td>
+            <td class="px-2 py-1 border-r border-gray-100">{{row.uv}}</td>
+            <td class="px-2 py-1">{{row.pv}}</td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+  `
+});
+
+const App = defineComponent({
+  components: { HeaderNav, OverviewPanel, TopTools, SimpleTable, ErrorAlert, EventTrendTable },
+  setup() {
+    // 数据定义
+    const overview = ref({});
+    // 直接存储原始数组
+    const browserDist = ref([]);
+    const osDist = ref([]);
+    const deviceTypeDist = ref([]);
+    const fhVerDist = ref([]);
+    const langDist = ref([]);
+    const eventPieDist = ref([]);
+    const countryDist = ref([]);
+    const provinceDist = ref([]);
+    const cityDist = ref([]);
+    const tools = ref([]);
+    const eventDist = ref([]);
+    const errorMsg = ref('');
+    const loading = ref(true);
+
+    // API请求工具函数
+    const fetchApi = async (url) => {
+      try {
+        const res = await fetch(url);
+        if (!res.ok) throw new Error(`${url} 请求失败: ${res.status} ${res.statusText}`);
+        return await res.json();
+      } catch (err) {
+        throw err;
+      }
+    };
+
+    // 加载所有首页数据
+    const loadAll = async () => {
+      try {
+        loading.value = true;
+        errorMsg.value = '';
+
+        // 1. 总览
+        overview.value = await fetchApi(apiBase + '/overview');
+
+        // 2. 浏览器分布
+        const rawBrowserDist = await fetchApi(apiBase + '/browser-distribution');
+        browserDist.value = rawBrowserDist.map(i => ({
+          _id: (i._id && i._id.browser)
+            ? `${i._id.browser} ${i._id.version}`
+            : (typeof i._id === 'string' ? i._id : '未知'),
+          uv: i.uv || 0,
+          pv: i.pv || 0
+        }));
+
+        // 3. 操作系统分布
+        const rawOsDist = await fetchApi(apiBase + '/os-distribution');
+        osDist.value = rawOsDist.map(i => ({
+          _id: (i._id && i._id.os)
+            ? `${i._id.os} ${i._id.version}`
+            : (typeof i._id === 'string' ? i._id : '未知'),
+          uv: i.uv || 0,
+          pv: i.pv || 0
+        }));
+
+        // 4. 设备类型分布
+        const rawDeviceTypeDist = await fetchApi(apiBase + '/device-type-distribution');
+        deviceTypeDist.value = rawDeviceTypeDist.map(i => ({
+          _id: i._id || '未知',
+          uv: i.uv || 0,
+          pv: i.pv || 0
+        }));
+
+        // 5. 插件版本分布
+        const rawFhVerDist = await fetchApi(apiBase + '/fh-version-distribution');
+        fhVerDist.value = rawFhVerDist.map(i => ({
+          _id: (i._id ? `${i._id}` : '未知'),
+          uv: i.uv || 0,
+          pv: i.pv || 0
+        }));
+
+        // 6. 用户语言分布
+        const users = await fetchApi(apiBase + '/users');
+        langDist.value = (users.lang || []).map(i => ({
+          _id: i._id || '未知',
+          uv: i.uv || 0,
+          pv: i.pv || 0
+        }));
+
+        // 7. 事件类型分布(主区域)
+        const rawEventPieDist = await fetchApi(apiBase + '/event-distribution');
+        eventPieDist.value = (rawEventPieDist || []).map(i => ({
+          _id: i._id || '未知',
+          uv: i.uv || 0,
+          pv: i.pv || 0
+        }));
+
+        // 8. 地理分布
+        const userDist = await fetchApi(apiBase + '/user-distribution');
+        countryDist.value = (userDist.country || []).map(i => ({
+          _id: i._id || '未知',
+          uv: i.uv || 0,
+          pv: i.pv || 0
+        }));
+        provinceDist.value = (userDist.province || []).map(i => ({
+          _id: i._id || '未知',
+          uv: i.uv || 0,
+          pv: i.pv || 0
+        }));
+        cityDist.value = (userDist.city || []).map(i => ({
+          _id: i._id || '未知',
+          uv: i.uv || 0,
+          pv: i.pv || 0
+        }));
+
+        // 9. 工具排行
+        const toolsList = await fetchApi(apiBase + '/tools');
+        tools.value = toolsList.map(t => ({ name: toolNameMap[t._id] || (t._id ? t._id : '插件更新或安装'), pv: t.pv || 0 }));
+
+        // 10. 事件类型分布(表格)
+        eventDist.value = eventPieDist.value;
+
+        // 11. 事件趋势
+        const trendList = await fetchApi(apiBase + '/event-trend');
+        eventTrend.value = trendList.map(i => ({
+          _id: i._id,
+          uv: i.uv || 0,
+          pv: i.pv || 0
+        }));
+
+      } catch (error) {
+        errorMsg.value = '数据加载失败: ' + error.message;
+      } finally {
+        loading.value = false;
+      }
+    };
+
+    onMounted(() => {
+      loadAll();
+    });
+
+    return {
+      overview, browserDist, osDist, deviceTypeDist, fhVerDist,
+      langDist, eventPieDist, countryDist, provinceDist, cityDist,
+      tools, eventDist, errorMsg, loading, eventTrend
+    };
+  },
+  template: `
+    <div>
+      <HeaderNav />
+      <div class="flex pt-14 h-screen">
+        <main class="flex-1 p-8 overflow-auto bg-gray-50 min-h-screen" id="main-content">
+          <ErrorAlert :message="errorMsg" />
+          <div v-if="loading" class="text-center py-8">
+            <div class="text-xl text-gray-600">加载中...</div>
+          </div>
+          <div v-else>
+            <OverviewPanel :overview="overview" />
+            <!-- 表格区域:两行,每行4个表格,全部可见 -->
+            <div class="w-full mx-auto">
+              <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
+                <SimpleTable title="浏览器类型/版本分布" :data="browserDist" label="浏览器/版本" />
+                <SimpleTable title="操作系统分布" :data="osDist" label="操作系统/版本" />
+                <SimpleTable title="设备类型分布" :data="deviceTypeDist" label="设备类型" />
+                <SimpleTable title="FeHelper版本分布" :data="fhVerDist" label="版本号" />
+                <SimpleTable title="用户语言分布" :data="langDist" label="语言" />
+                <SimpleTable title="事件类型分布(主区域)" :data="eventPieDist" label="事件类型" />
+                <SimpleTable title="国家分布" :data="countryDist" label="国家" />
+                <SimpleTable title="省份分布" :data="provinceDist" label="省份" />
+                <SimpleTable title="城市分布" :data="cityDist" label="城市" />
+              </div>
+            </div>
+            <!-- 其它内容 -->
+            <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
+              <TopTools :tools="tools" />
+              <EventTrendTable :data="eventTrend" />
+            </div>
+          </div>
+        </main>
+      </div>
+    </div>
+  `
+});
+
+console.log('Components defined. Mounting app...'); // 日志点 2
+createApp(App).mount('#app'); 

File diff suppressed because it is too large
+ 34 - 0
server/admin/js/echarts.min.js


File diff suppressed because it is too large
+ 4 - 0
server/admin/js/vue.global.prod.js


+ 0 - 9
server/api.js

@@ -1,9 +0,0 @@
-// 预留管理后台API扩展
-// 示例:后续可添加统计分析、用户管理等接口
-
-module.exports = function(app) {
-    // 例如:获取所有埋点数据
-    // app.get('/api/tracks', async (req, res) => {
-    //     // ...
-    // });
-}; 

+ 331 - 0
server/api/admin.js

@@ -0,0 +1,331 @@
+const express = require('express');
+const router = express.Router();
+const Track = require('../models/track'); // 引入Track模型
+
+console.log('admin.js 已加载'); // 日志A
+
+// admin 路由全局日志
+router.use((req, res, next) => {
+  console.log('admin 路由收到请求:', req.path);
+  next();
+});
+
+// 累计用户、近一月用户、占比
+router.get('/overview', async (req, res) => {
+  try {
+    const now = Date.now();
+    const monthAgo = now - 30 * 24 * 3600 * 1000;
+    const todayStart = new Date().setHours(0, 0, 0, 0);
+
+    // 使用 Promise.all 并行执行数据库查询
+    const [totalUserIds, monthUserIds, todayActiveUserIds, estimatedTotalDocs, allEvents] = await Promise.all([
+      Track.distinct('userId'),
+      Track.distinct('userId', { createdAt: { $gte: new Date(monthAgo) } }),
+      Track.distinct('userId', { createdAt: { $gte: new Date(todayStart) } }),
+      Track.estimatedDocumentCount(),
+      Track.distinct('event'),
+    ]);
+
+    const userCount = totalUserIds.length;
+    const monthUserCount = monthUserIds.length;
+    const todayActive = todayActiveUserIds.length;
+    const eventCount = allEvents.length;
+
+    res.json({
+      total: estimatedTotalDocs,
+      userCount: userCount,
+      monthUserCount: monthUserCount,
+      monthUserRate: userCount ? ((monthUserCount / userCount) * 100).toFixed(2) + '%' : '0%',
+      todayActive: todayActive,
+      eventCount: eventCount
+    });
+  } catch (error) {
+    console.error('Error fetching overview data:', error);
+    res.status(500).json({ error: 'Failed to fetch overview data' });
+  }
+});
+
+// 浏览器类型和版本分布
+router.get('/browser-distribution', async (req, res) => {
+  const agg = await Track.aggregate([
+    {
+      $group: {
+        _id: { browser: '$browser', version: '$browserVersion' },
+        pv: { $sum: 1 },
+        userIds: { $addToSet: '$userId' }
+      }
+    },
+    {
+      $project: {
+        _id: 1,
+        pv: 1,
+        uv: { $size: '$userIds' }
+      }
+    },
+    { $sort: { pv: -1 } }
+  ]);
+  res.json(agg);
+});
+
+// FeHelper版本分布
+router.get('/fh-version-distribution', async (req, res) => {
+  const agg = await Track.aggregate([
+    {
+      $group: {
+        _id: '$extensionVersion',
+        pv: { $sum: 1 },
+        userIds: { $addToSet: '$userId' }
+      }
+    },
+    {
+      $project: {
+        _id: 1,
+        pv: 1,
+        uv: { $size: '$userIds' }
+      }
+    },
+    { $sort: { pv: -1 } }
+  ]);
+  res.json(agg);
+});
+
+// 操作系统及版本分布
+router.get('/os-distribution', async (req, res) => {
+  const agg = await Track.aggregate([
+    {
+      $group: {
+        _id: { os: '$os', version: '$osVersion' },
+        pv: { $sum: 1 },
+        userIds: { $addToSet: '$userId' }
+      }
+    },
+    {
+      $project: {
+        _id: 1,
+        pv: 1,
+        uv: { $size: '$userIds' }
+      }
+    },
+    { $sort: { pv: -1 } }
+  ]);
+  res.json(agg);
+});
+
+// 设备类型分布
+router.get('/device-type-distribution', async (req, res) => {
+  const agg = await Track.aggregate([
+    {
+      $group: {
+        _id: '$deviceType',
+        pv: { $sum: 1 },
+        userIds: { $addToSet: '$userId' }
+      }
+    },
+    {
+      $project: {
+        _id: 1,
+        pv: 1,
+        uv: { $size: '$userIds' }
+      }
+    },
+    { $sort: { pv: -1 } }
+  ]);
+  res.json(agg);
+});
+
+// 事件类型分布
+router.get('/event-distribution', async (req, res) => {
+  const agg = await Track.aggregate([
+    {
+      $group: {
+        _id: '$event',
+        pv: { $sum: 1 },
+        userIds: { $addToSet: '$userId' }
+      }
+    },
+    {
+      $project: {
+        _id: 1,
+        pv: 1,
+        uv: { $size: '$userIds' }
+      }
+    },
+    { $sort: { pv: -1 } }
+  ]);
+  res.json(agg);
+});
+
+// 国家、省份、城市分布
+router.get('/user-distribution', async (req, res) => {
+  const country = await Track.aggregate([
+    {
+      $group: {
+        _id: '$country',
+        pv: { $sum: 1 },
+        userIds: { $addToSet: '$userId' }
+      }
+    },
+    {
+      $project: {
+        _id: 1,
+        pv: 1,
+        uv: { $size: '$userIds' }
+      }
+    },
+    { $sort: { pv: -1 } }
+  ]);
+  const province = await Track.aggregate([
+    {
+      $group: {
+        _id: '$province',
+        pv: { $sum: 1 },
+        userIds: { $addToSet: '$userId' }
+      }
+    },
+    {
+      $project: {
+        _id: 1,
+        pv: 1,
+        uv: { $size: '$userIds' }
+      }
+    },
+    { $sort: { pv: -1 } }
+  ]);
+  const city = await Track.aggregate([
+    {
+      $group: {
+        _id: '$city',
+        pv: { $sum: 1 },
+        userIds: { $addToSet: '$userId' }
+      }
+    },
+    {
+      $project: {
+        _id: 1,
+        pv: 1,
+        uv: { $size: '$userIds' }
+      }
+    },
+    { $sort: { pv: -1 } }
+  ]);
+  res.json({ country, province, city });
+});
+
+// 工具使用排名
+router.get('/tools', async (req, res) => {
+  const agg = await Track.aggregate([
+    {
+      $group: {
+        _id: '$tool_name',
+        pv: { $sum: 1 },
+        userIds: { $addToSet: '$userId' }
+      }
+    },
+    {
+      $project: {
+        _id: 1,
+        pv: 1,
+        uv: { $size: '$userIds' }
+      }
+    },
+    { $sort: { pv: -1 } }
+  ]);
+  res.json(agg);
+});
+
+// 原始数据分页
+router.get('/raw', async (req, res) => {
+  const page = parseInt(req.query.page || 1);
+  const pageSize = parseInt(req.query.pageSize || 20);
+  const total = await Track.countDocuments();
+  const list = await Track.find().sort({ createdAt: -1 }).skip((page - 1) * pageSize).limit(pageSize);
+  res.json({ total, list });
+});
+
+// 用户画像(语言、平台、浏览器)
+router.get('/users', async (req, res) => {
+  const lang = await Track.aggregate([
+    {
+      $group: {
+        _id: '$language',
+        pv: { $sum: 1 },
+        userIds: { $addToSet: '$userId' }
+      }
+    },
+    {
+      $project: {
+        _id: 1,
+        pv: 1,
+        uv: { $size: '$userIds' }
+      }
+    },
+    { $sort: { pv: -1 } }
+  ]);
+  const platform = await Track.aggregate([
+    {
+      $group: {
+        _id: '$os',
+        pv: { $sum: 1 },
+        userIds: { $addToSet: '$userId' }
+      }
+    },
+    {
+      $project: {
+        _id: 1,
+        pv: 1,
+        uv: { $size: '$userIds' }
+      }
+    },
+    { $sort: { pv: -1 } }
+  ]);
+  const browser = await Track.aggregate([
+    {
+      $group: {
+        _id: '$browser',
+        pv: { $sum: 1 },
+        userIds: { $addToSet: '$userId' }
+      }
+    },
+    {
+      $project: {
+        _id: 1,
+        pv: 1,
+        uv: { $size: '$userIds' }
+      }
+    },
+    { $sort: { pv: -1 } }
+  ]);
+  res.json({ lang, platform, browser });
+});
+
+// 事件趋势(最近30天)
+router.get('/event-trend', async (req, res) => {
+  const days = 30;
+  const start = new Date();
+  start.setHours(0, 0, 0, 0);
+  start.setDate(start.getDate() - days + 1);
+
+  const agg = await Track.aggregate([
+    { $match: { createdAt: { $gte: start } } },
+    {
+      $group: {
+        _id: {
+          day: { $dateToString: { format: '%Y-%m-%d', date: '$createdAt' } },
+          userId: '$userId'
+        },
+        pv: { $sum: 1 }
+      }
+    },
+    {
+      $group: {
+        _id: '$_id.day',
+        uv: { $sum: 1 },
+        pv: { $sum: '$pv' }
+      }
+    },
+    { $sort: { _id: 1 } }
+  ]);
+  res.json(agg);
+});
+
+module.exports = router; 

+ 71 - 0
server/api/track.js

@@ -0,0 +1,71 @@
+const express = require('express');
+const router = express.Router();
+const UAParser = require('ua-parser-js');
+const geoip = require('geoip-lite');
+const Track = require('../models/track');
+
+// 埋点上报接口
+router.post('/', async (req, res) => {
+    try {
+        const body = req.body || {};
+        const headers = req.headers || {};
+        const userAgentStr = body.userAgent || headers['user-agent'] || '';
+        const parser = new UAParser(userAgentStr);
+        const uaResult = parser.getResult();
+
+        // IP获取与地理位置解析
+        let ip = body.IP || req.ip || headers['x-forwarded-for'] || '';
+        if (ip && ip.includes(',')) ip = ip.split(',')[0].trim();
+        if (ip.startsWith('::ffff:')) ip = ip.replace('::ffff:', '');
+        const geo = geoip.lookup(ip) || {};
+
+        // 设备类型判断
+        const deviceType = uaResult.device.type || (uaResult.os.name && uaResult.os.name.match(/Android|iOS|iPhone|iPad/i) ? 'mobile' : 'desktop');
+
+        // 只保留 TrackSchema 中定义的字段
+        const data = {
+            // 用户与会话
+            userId: body.userId || '',
+
+            // 事件与页面
+            event: body.event || '',
+
+            // 页面信息
+            pageUrl: body.pageUrl || '',
+            pageTitle: body.pageTitle || '',
+
+            // 设备与环境
+            userAgent: userAgentStr,
+            browser: uaResult.browser.name || '',
+            browserVersion: uaResult.browser.version || '',
+            os: uaResult.os.name || '',
+            osVersion: uaResult.os.version || '',
+            deviceType: deviceType || '',
+            deviceVendor: uaResult.device.vendor || '',
+            deviceModel: uaResult.device.model || '',
+            language: body.language || headers['accept-language'] || '',
+            timezone: body.timezone || '',
+            platform: body.platform || uaResult.os.name || '',
+
+            // 网络与地理
+            IP: ip,
+            country: geo.country || '',
+            province: geo.region || '',
+            city: geo.city || '',
+            online: typeof body.online === 'boolean' ? body.online : null,
+
+            // 扩展相关
+            extensionVersion: body.extensionVersion || '',
+            previous_version: body.previous_version || '',
+            tool_name: body.tool_name || '',
+        };
+        
+        await Track.create(data);
+        res.json({ code: 0, msg: '上报成功' });
+    } catch (err) {
+        console.error('埋点上报失败:', err);
+        res.status(500).json({ code: 1, msg: '上报失败' });
+    }
+});
+
+module.exports = router; 

+ 10 - 54
server/index.js

@@ -3,7 +3,9 @@ const express = require('express');
 const mongoose = require('mongoose');
 const cors = require('cors');
 const bodyParser = require('body-parser');
-const { mongoUri } = require('./config');
+const { mongoUri } = require('./models/config');
+const path = require('path');
+const UAParser = require('ua-parser-js');
 
 const app = express();
 const PORT = 3001;
@@ -12,6 +14,13 @@ const PORT = 3001;
 app.use(cors());
 app.use(bodyParser.json());
 
+// 注册API路由(必须在静态资源之前)
+app.use('/api/admin', require('./api/admin'));
+app.use('/api/track', require('./api/track'));
+
+// 静态资源
+app.use('/admin', express.static(path.join(__dirname, 'admin')));
+
 // 连接MongoDB
 mongoose.connect(mongoUri, {
     useNewUrlParser: true,
@@ -22,64 +31,11 @@ mongoose.connect(mongoUri, {
     console.error('MongoDB连接失败:', err);
 });
 
-// 定义埋点数据模型
-const TrackSchema = new mongoose.Schema({
-    event: String,
-    userId: String,
-    date: String,
-    timestamp: Number,
-    tool_name: String,
-    previous_version: String,
-    top_tools: String,
-    // 客户端信息
-    language: String,
-    timezone: String,
-    userAgent: String,
-    platform: String,
-    vendor: String,
-    colorDepth: Number,
-    screenWidth: Number,
-    screenHeight: Number,
-    deviceMemory: String,
-    hardwareConcurrency: String,
-    extensionVersion: String,
-    networkType: String,
-    downlink: String,
-    rtt: String,
-    online: Boolean,
-    touchSupport: Boolean,
-    cookieEnabled: Boolean,
-    doNotTrack: String,
-    appVersion: String,
-    appName: String,
-    product: String,
-    vendorSub: String,
-    screenOrientation: String,
-    memoryJSHeapSize: String,
-    timeOpened: Number
-}, { timestamps: true });
-
-const Track = mongoose.model('Track', TrackSchema);
-
-// 埋点上报接口
-app.post('/api/track', async (req, res) => {
-    try {
-        const data = req.body;
-        await Track.create(data);
-        res.json({ code: 0, msg: '上报成功' });
-    } catch (err) {
-        console.error('埋点上报失败:', err);
-        res.status(500).json({ code: 1, msg: '上报失败' });
-    }
-});
-
 // 健康检查
 app.get('/api/ping', (req, res) => {
     res.send('pong');
 });
 
-// 加载管理后台API扩展
-require('./api')(app);
 
 app.listen(PORT, () => {
     console.log(`FeHelper统计服务已启动,端口:${PORT}`);

+ 1 - 0
server/config.js → server/models/config.js

@@ -1,4 +1,5 @@
 // MongoDB配置
 module.exports = {
     mongoUri: 'mongodb://localhost:27017/fehelper', // 请根据实际情况修改
+    mongoDbName: 'fehelper'
 }; 

+ 17 - 0
server/models/mongo.js

@@ -0,0 +1,17 @@
+const { MongoClient } = require('mongodb');
+const config = require('./config');
+
+let client = null;
+let db = null;
+
+async function getDb() {
+  if (db) return db;
+  if (!client) {
+    client = new MongoClient(config.mongoUri, { useUnifiedTopology: true });
+    await client.connect();
+  }
+  db = client.db(config.mongoDbName || 'fehelper');
+  return db;
+}
+
+module.exports = { getDb }; 

+ 29 - 0
server/models/track.js

@@ -0,0 +1,29 @@
+const mongoose = require('mongoose');
+
+const TrackSchema = new mongoose.Schema({
+    userId: String,
+    event: String,
+    pageUrl: String,
+    pageTitle: String,
+    userAgent: String,
+    browser: String,
+    browserVersion: String,
+    os: String,
+    osVersion: String,
+    deviceType: String,
+    deviceVendor: String,
+    deviceModel: String,
+    language: String,
+    timezone: String,
+    platform: String,
+    IP: String,
+    country: String,
+    province: String,
+    city: String,
+    online: Boolean,
+    extensionVersion: String,
+    previous_version: String,
+    tool_name: String,
+}, { timestamps: true });
+
+module.exports = mongoose.models.Track || mongoose.model('Track', TrackSchema); 

+ 9 - 4
server/package.json

@@ -4,12 +4,17 @@
   "description": "FeHelper数据统计与管理后台服务端",
   "main": "index.js",
   "scripts": {
-    "start": "node index.js"
+    "start": "node index.js",
+    "dev": "nodemon index.js"
   },
   "dependencies": {
+    "body-parser": "^1.20.2",
+    "cors": "^2.8.5",
     "express": "^4.18.2",
     "mongoose": "^7.6.3",
-    "cors": "^2.8.5",
-    "body-parser": "^1.20.2"
+    "ua-parser-js": "^2.0.3"
+  },
+  "devDependencies": {
+    "nodemon": "^3.1.10"
   }
-} 
+}

Some files were not shown because too many files changed in this diff