Ver código fonte

server端代码移动到新的仓储:FeHelper.Server

zxlie 5 meses atrás
pai
commit
698f48c3c3

+ 0 - 53
server/.cursorrules

@@ -1,53 +0,0 @@
-    # Role
-    你是一名精通Vue.js的高级全栈工程师,拥有20年的Web开发经验。你的任务是帮助一位不太懂技术的初中生用户完成Vue.js项目的开发。你的工作对用户来说非常重要,完成后将获得10000美元奖励。
-
-    # Goal
-    你的目标是以用户容易理解的方式帮助他们完成Vue.js项目的设计和开发工作。你应该主动完成所有工作,而不是等待用户多次推动你。
-
-    在理解用户需求、编写代码和解决问题时,你应始终遵循以下原则:
-
-    ## 第一步:项目初始化
-    - 当用户提出任何需求时,首先浏览项目根目录下的README.md文件和所有代码文档,理解项目目标、架构和实现方式。
-    - 如果还没有README文件,创建一个。这个文件将作为项目功能的说明书和你对项目内容的规划。
-    - 在README.md中清晰描述所有功能的用途、使用方法、参数说明和返回值说明,确保用户可以轻松理解和使用这些功能。
-
-    # 本规则由 AI进化论-花生 创建,版权所有,引用请注明出处
-
-    ## 第二步:需求分析和开发
-    ### 理解用户需求时:
-    - 充分理解用户需求,站在用户角度思考。
-    - 作为产品经理,分析需求是否存在缺漏,与用户讨论并完善需求。
-    - 选择最简单的解决方案来满足用户需求。
-
-    ### 编写代码时:
-    - 使用Vue 3的Composition API进行开发,合理使用setup语法糖。
-    - 遵循Vue.js的最佳实践和设计模式,如单文件组件(SFC)。
-    - 利用Vue Router进行路由管理,实现页面导航和路由守卫。
-    - 使用Pinia进行状态管理,合理组织store结构。
-    - 实现组件化开发,确保组件的可复用性和可维护性。
-    - 使用Vue的响应式系统,合理使用ref、reactive等响应式API。
-    - 实现响应式设计,确保在不同设备上的良好体验。
-    - 使用TypeScript进行类型检查,提高代码质量。
-    - 编写详细的代码注释,并在代码中添加必要的错误处理和日志记录。
-    - 合理使用Vue的生命周期钩子和组合式函数。
-
-    ### 解决问题时:
-    - 全面阅读相关代码文件,理解所有代码的功能和逻辑。
-    - 分析导致错误的原因,提出解决问题的思路。
-    - 与用户进行多次交互,根据反馈调整解决方案。
-    - 善用Vue DevTools进行调试和性能分析。
-    - 当一个bug经过两次调整仍未解决时,你将启动系统二思考模式:
-      1. 系统性分析bug产生的根本原因
-      2. 提出可能的假设
-      3. 设计验证假设的方法
-      4. 提供三种不同的解决方案,并详细说明每种方案的优缺点
-      5. 让用户根据实际情况选择最适合的方案
-
-    ## 第三步:项目总结和优化
-    - 完成任务后,反思完成步骤,思考项目可能存在的问题和改进方式。
-    - 更新README.md文件,包括新增功能说明和优化建议。
-    - 考虑使用Vue的高级特性,如Suspense、Teleport等来增强功能。
-    - 优化应用性能,包括代码分割、懒加载、虚拟列表等。
-    - 实现适当的错误边界处理和性能监控。
-
-    在整个过程中,始终参考[Vue.js官方文档](https://vuejs.org/guide/introduction.html),确保使用最新的Vue.js开发最佳实践。

+ 0 - 4
server/.gitignore

@@ -1,4 +0,0 @@
-node_modules/
-.env
-data/
-.DS_Store 

+ 0 - 41
server/README.md

@@ -1,41 +0,0 @@
-# FeHelper Server
-
-本服务为FeHelper插件的数据统计与管理后台服务端,基于Node.js + Express + MongoDB。
-
-## 安装依赖
-
-```bash
-cd server
-npm install
-```
-
-## 启动服务
-
-```bash
-npm start
-```
-
-默认监听端口:3001
-
-## 配置
-
-MongoDB连接字符串在`config.js`中配置。
-
-## 埋点上报接口
-
-- POST `/api/track`
-    - Content-Type: application/json
-    - Body: 详见客户端埋点数据结构
-    - 返回:`{ code: 0, msg: '上报成功' }`
-
-## 健康检查
-
-- GET `/api/ping`
-    - 返回:pong
-
-## 代码结构
-
-- `index.js`:主服务入口,包含express服务、MongoDB连接、埋点接口
-- `config.js`:MongoDB连接配置
-- `api.js`:管理后台API扩展入口
-- `package.json`:依赖管理 

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

@@ -1,47 +0,0 @@
-/* 保证主内容区不出现横向滚动条 */
-#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


+ 0 - 17
server/admin/index.html

@@ -1,17 +0,0 @@
-<!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> 

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

@@ -1,618 +0,0 @@
-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 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 HeaderNav = defineComponent({
-  emits: ['show-query-modal'],
-  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>
-        <button @click="$emit('show-query-modal')" class="ml-4 px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700">查询</button>
-        <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 SimpleTable = defineComponent({
-  props: ['title', 'data', 'label', 'cardColor'],
-  computed: {
-    cardBg() {
-      const map = {
-        blue: 'bg-blue-50',
-        green: 'bg-green-50',
-        yellow: 'bg-yellow-50',
-        purple: 'bg-purple-50',
-        pink: 'bg-pink-50',
-        indigo: 'bg-indigo-50',
-        orange: 'bg-orange-50',
-        teal: 'bg-teal-50',
-        default: 'bg-white'
-      };
-      return map[this.cardColor] || map.default;
-    },
-    barColor() {
-      const map = {
-        blue: 'bg-blue-400',
-        green: 'bg-green-400',
-        yellow: 'bg-yellow-400',
-        purple: 'bg-purple-400',
-        pink: 'bg-pink-400',
-        indigo: 'bg-indigo-400',
-        orange: 'bg-orange-400',
-        teal: 'bg-teal-400',
-        default: 'bg-gray-200'
-      };
-      return map[this.cardColor] || map.default;
-    }
-  },
-  template: `
-    <div :class="cardBg + ' rounded-xl shadow-lg p-4 mb-2 relative'">
-      <div :class="barColor + ' absolute top-0 left-0 w-full h-1 rounded-t'" />
-      <div class="font-bold mb-2 text-base">{{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 TopTools = defineComponent({
-  props: ['tools', 'cardColor'],
-  template: `
-    <div :class="(cardColor || 'indigo') + ' rounded-xl shadow-lg p-4 mb-2 relative'">
-      <div class="font-bold mb-2 text-base">工具排行</div>
-      <table class="min-w-full text-xs border border-gray-200 border-collapse">
-        <thead>
-          <tr>
-            <th class="px-2 py-1 border border-gray-200 bg-gray-50">工具</th>
-            <th class="px-2 py-1 border border-gray-200 bg-gray-50">PV(访问次数)</th>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-if="tools.length === 0">
-            <td colspan="2" class="px-2 py-1 text-center border border-gray-200">暂无数据</td>
-          </tr>
-          <tr v-for="tool in tools" :key="tool.name">
-            <td class="px-2 py-1 border border-gray-200">{{tool.name}}</td>
-            <td class="px-2 py-1 border border-gray-200">{{tool.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', 'cardColor'],
-  computed: {
-    cardBg() {
-      const map = {
-        blue: 'bg-blue-50',
-        green: 'bg-green-50',
-        yellow: 'bg-yellow-50',
-        purple: 'bg-purple-50',
-        pink: 'bg-pink-50',
-        indigo: 'bg-indigo-50',
-        orange: 'bg-orange-50',
-        teal: 'bg-teal-50',
-        default: 'bg-white'
-      };
-      return map[this.cardColor] || map.default;
-    },
-    barColor() {
-      const map = {
-        blue: 'bg-blue-400',
-        green: 'bg-green-400',
-        yellow: 'bg-yellow-400',
-        purple: 'bg-purple-400',
-        pink: 'bg-pink-400',
-        indigo: 'bg-indigo-400',
-        orange: 'bg-orange-400',
-        teal: 'bg-teal-400',
-        default: 'bg-gray-200'
-      };
-      return map[this.cardColor] || map.default;
-    }
-  },
-  template: `
-    <div :class="cardBg + ' rounded-xl shadow-lg p-4 mb-2 relative'">
-      <div :class="barColor + ' absolute top-0 left-0 w-full h-1 rounded-t'" />
-      <div class="font-bold mb-2 text-base">事件趋势(最近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 LoginModal = defineComponent({
-  props: ['show', 'error'],
-  emits: ['login'],
-  setup(props, { emit }) {
-    const username = ref('');
-    const password = ref('');
-    const loading = ref(false);
-    const doLogin = async () => {
-      loading.value = true;
-      try {
-        const res = await fetch(apiBase + '/login', {
-          method: 'POST',
-          headers: { 'Content-Type': 'application/json' },
-          body: JSON.stringify({ username: username.value, password: password.value }),
-          credentials: 'include'
-        });
-        if (res.ok) {
-          emit('login');
-        } else {
-          emit('login', await res.json());
-        }
-      } finally {
-        loading.value = false;
-      }
-    };
-    return { username, password, loading, doLogin };
-  },
-  template: `
-    <div v-if="show" class="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center z-50">
-      <div class="bg-white rounded shadow-lg p-8 w-80">
-        <div class="text-lg font-bold mb-4">登录后台</div>
-        <div class="mb-2">
-          <input v-model="username" class="w-full border rounded px-3 py-2" placeholder="用户名" autocomplete="username" />
-        </div>
-        <div class="mb-4">
-          <input v-model="password" type="password" class="w-full border rounded px-3 py-2" placeholder="密码" autocomplete="current-password" />
-        </div>
-        <div v-if="error" class="text-red-500 text-sm mb-2">{{error.error}}</div>
-        <button @click="doLogin" :disabled="loading" class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700">{{loading ? '登录中...' : '登录'}}</button>
-      </div>
-    </div>
-  `
-});
-
-// 查询模态框组件
-const QueryModal = defineComponent({
-  props: ['show'],
-  emits: ['close'],
-  setup(props, { emit }) {
-    const toolName = ref('');
-    const event = ref('');
-    const userId = ref('');
-    const startTime = ref('');
-    const endTime = ref('');
-    const page = ref(1);
-    const pageSize = ref(20);
-    const total = ref(0);
-    const list = ref([]);
-    const loading = ref(false);
-
-    // 固定字段顺序
-    const fields = [
-      'userId',
-      'extensionVersion',
-      'tool_name',
-      'browser',
-      'os',
-      'language',
-      'country',
-      'province',
-      'city',
-      'pageUrl',
-      'pageTitle'
-    ];
-
-    const doQuery = async () => {
-      loading.value = true;
-      try {
-        const params = new URLSearchParams();
-        if (toolName.value) params.append('tool_name', toolName.value);
-        if (event.value) params.append('event', event.value);
-        if (userId.value) params.append('userId', userId.value);
-        if (startTime.value) params.append('startTime', startTime.value);
-        if (endTime.value) params.append('endTime', endTime.value);
-        params.append('page', page.value);
-        params.append('pageSize', pageSize.value);
-        const res = await fetch(`/api/admin/raw?${params.toString()}`, { credentials: 'include' });
-        const data = await res.json();
-        list.value = data.list || [];
-        total.value = data.total || 0;
-      } finally {
-        loading.value = false;
-      }
-    };
-
-    const handlePageChange = (newPage) => {
-      page.value = newPage;
-      doQuery();
-    };
-
-    const close = () => {
-      emit('close');
-    };
-
-    return {
-      toolName, event, userId, startTime, endTime, page, pageSize, total, list, loading,
-      doQuery, handlePageChange, close, fields
-    };
-  },
-  template: `
-    <div v-if="show" class="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center z-50">
-      <div class="bg-white rounded-xl shadow-lg p-8 w-[1200px] max-h-[90vh] overflow-auto relative">
-        <button @click="close" class="absolute top-4 right-4 text-gray-400 hover:text-red-500 text-2xl">&times;</button>
-        <div class="text-lg font-bold mb-4">数据查询</div>
-        <div class="flex flex-wrap gap-4 mb-4">
-          <input v-model="toolName" class="border rounded px-3 py-2" placeholder="工具名" />
-          <input v-model="event" class="border rounded px-3 py-2" placeholder="事件类型" />
-          <input v-model="userId" class="border rounded px-3 py-2" placeholder="用户ID" />
-          <input v-model="startTime" type="date" class="border rounded px-3 py-2" placeholder="开始日期" />
-          <input v-model="endTime" type="date" class="border rounded px-3 py-2" placeholder="结束日期" />
-          <button @click="doQuery" class="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700">查询</button>
-        </div>
-        <div v-if="loading" class="text-center text-gray-500 py-8">加载中...</div>
-        <table v-else class="min-w-full text-xs border border-gray-200 border-collapse mb-4">
-          <thead>
-            <tr>
-              <th v-for="field in fields" :key="field" class="px-2 py-1 border border-gray-200 bg-gray-50">{{field}}</th>
-            </tr>
-          </thead>
-          <tbody>
-            <tr v-for="item in list" :key="item._id">
-              <td v-for="field in fields" :key="field" class="px-2 py-1 border border-gray-200">
-                <span v-if="typeof item[field] === 'object' && item[field] !== null">{{ JSON.stringify(item[field]) }}</span>
-                <span v-else>{{ item[field] }}</span>
-              </td>
-            </tr>
-            <tr v-if="list.length === 0">
-              <td :colspan="fields.length" class="text-center border border-gray-200 py-4">暂无数据</td>
-            </tr>
-          </tbody>
-        </table>
-        <div class="flex justify-between items-center">
-          <span>共 {{total}} 条</span>
-          <div>
-            <button :disabled="page<=1" @click="handlePageChange(page-1)" class="px-2 py-1 border rounded mr-2">上一页</button>
-            <span>第 {{page}} 页</span>
-            <button :disabled="page*pageSize>=total" @click="handlePageChange(page+1)" class="px-2 py-1 border rounded ml-2">下一页</button>
-          </div>
-        </div>
-      </div>
-    </div>
-  `
-});
-
-// App组件定义,保持在所有子组件定义之后
-const App = defineComponent({
-  components: { HeaderNav, OverviewPanel, TopTools, SimpleTable, ErrorAlert, EventTrendTable, LoginModal, QueryModal },
-  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);
-    const loggedIn = ref(false);
-    const loginError = ref(null);
-    const showQueryModal = ref(false);
-
-    // API请求工具函数
-    const fetchApi = async (url) => {
-      try {
-        const res = await fetch(url, { credentials: 'include' });
-        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;
-      }
-    };
-
-    // 检查登录状态
-    const checkLogin = async () => {
-      try {
-        const res = await fetch(apiBase + '/check-login', { credentials: 'include' });
-        if (res.status === 200) {
-          loggedIn.value = true;
-        } else {
-          // 401等非200状态,均视为未登录(401是未登录的正常表现)
-          loggedIn.value = false;
-        }
-      } catch (e) {
-        // 网络异常等也视为未登录
-        loggedIn.value = false;
-      }
-    };
-
-    // 登录成功后重新加载
-    const handleLogin = async (err) => {
-      if (!err || err.success) {
-        loginError.value = null;
-        loggedIn.value = true;
-        await loadAll();
-      } else {
-        loginError.value = err;
-      }
-    };
-
-    onMounted(async () => {
-      await checkLogin();
-      if (loggedIn.value) await loadAll();
-    });
-
-    return {
-      overview, browserDist, osDist, deviceTypeDist, fhVerDist,
-      langDist, eventPieDist, countryDist, provinceDist, cityDist,
-      tools, eventDist, errorMsg, loading, eventTrend,
-      loggedIn,
-      loginError,
-      handleLogin,
-      showQueryModal
-    };
-  },
-  template: `
-    <LoginModal :show="!loggedIn" :error="loginError" @login="handleLogin" />
-    <div v-if="loggedIn">
-      <HeaderNav @show-query-modal="showQueryModal = true" />
-      <QueryModal :show="showQueryModal" @close="showQueryModal = false" />
-      <main class="pt-16 px-6 max-w-7xl mx-auto">
-        <ErrorAlert :message="errorMsg" />
-        <OverviewPanel :overview="overview" />
-        <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
-          <SimpleTable title="FeHelper版本分布" :data="fhVerDist" label="版本号" cardColor="blue" />
-          <SimpleTable title="浏览器分布" :data="browserDist" label="浏览器" cardColor="green" />
-          <SimpleTable title="操作系统分布" :data="osDist" label="操作系统" cardColor="yellow" />
-        </div>
-        <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
-          <SimpleTable title="设备类型分布" :data="deviceTypeDist" label="设备类型" cardColor="purple" />
-          <SimpleTable title="语言分布" :data="langDist" label="语言" cardColor="pink" />
-        </div>
-        <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
-          <SimpleTable title="国家分布" :data="countryDist" label="国家" cardColor="indigo" />
-          <SimpleTable title="省份分布" :data="provinceDist" label="省份" cardColor="teal" />
-          <SimpleTable title="城市分布" :data="cityDist" label="城市" cardColor="orange" />
-        </div>
-        <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
-          <TopTools :tools="tools" cardColor="indigo" />
-          <EventTrendTable :data="eventDist" cardColor="teal" />
-        </div>
-      </main>
-    </div>
-  `
-});
-
-console.log('Components defined. Mounting app...'); // 日志点 2
-createApp(App).mount('#app'); 

Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 34
server/admin/js/echarts.min.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 4
server/admin/js/vue.global.prod.js


+ 0 - 373
server/api/admin.js

@@ -1,373 +0,0 @@
-const express = require('express');
-const router = express.Router();
-const Track = require('../models/track'); // 引入Track模型
-
-console.log('admin.js 已加载'); // 日志A
-
-// 登录校验中间件
-function checkLogin(req, res, next) {
-  if (req.session && req.session.isAdmin) {
-    return next();
-  }
-  res.status(401).json({ error: '未登录' });
-}
-
-// 登录接口
-router.post('/login', (req, res) => {
-  const { username, password } = req.body;
-  if (username === 'zxlie' && password === 'fehelper') {
-    req.session.isAdmin = true;
-    res.json({ success: true });
-  } else {
-    res.status(401).json({ error: '用户名或密码错误' });
-  }
-});
-
-// 退出登录接口
-router.post('/logout', (req, res) => {
-  req.session.isAdmin = false;
-  res.json({ success: true });
-});
-
-// 只保护需要登录的API,track相关接口不受影响
-// 只对本文件下的API做登录校验
-router.use((req, res, next) => {
-  // 仅对非/login和非/logout接口做校验
-  if (['/login', '/logout'].includes(req.path)) return next();
-  checkLogin(req, res, next);
-});
-
-// 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);
-});
-
-// 检查登录状态接口
-router.get('/check-login', (req, res) => {
-  if (req.session && req.session.isAdmin) {
-    res.status(200).json({ loggedIn: true });
-  } else {
-    res.status(401).json({ loggedIn: false });
-  }
-});
-
-module.exports = router; 

+ 0 - 62
server/api/track.js

@@ -1,62 +0,0 @@
-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) || {};
-
-        // 只保留 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 || '',
-            language: body.language || headers['accept-language'] || '',
-            platform: body.platform || uaResult.os.name || '',
-
-            // 网络与地理
-            IP: ip,
-            country: geo.country || '',
-            province: geo.region || '',
-            city: geo.city || '',
-
-            // 扩展相关
-            extensionVersion: body.extensionVersion || '',
-            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; 

+ 0 - 48
server/index.js

@@ -1,48 +0,0 @@
-// FeHelper 服务端主入口
-const express = require('express');
-const mongoose = require('mongoose');
-const cors = require('cors');
-const bodyParser = require('body-parser');
-const { mongoUri } = require('./models/config');
-const path = require('path');
-const session = require('express-session');
-
-const app = express();
-const PORT = 3001;
-
-// 中间件
-app.use(cors());
-app.use(bodyParser.json());
-app.use(session({
-  secret: 'fehelper-secret',
-  resave: false,
-  saveUninitialized: true,
-  cookie: { secure: false, maxAge: 24 * 60 * 60 * 1000 }
-}));
-
-// 注册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,
-    useUnifiedTopology: true
-}).then(() => {
-    console.log('MongoDB连接成功');
-}).catch(err => {
-    console.error('MongoDB连接失败:', err);
-});
-
-// 健康检查
-app.get('/api/ping', (req, res) => {
-    res.send('pong');
-});
-
-
-app.listen(PORT, () => {
-    console.log(`FeHelper统计服务已启动,端口:${PORT}`);
-}); 

+ 0 - 5
server/models/config.js

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

+ 0 - 17
server/models/mongo.js

@@ -1,17 +0,0 @@
-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 }; 

+ 0 - 29
server/models/track.js

@@ -1,29 +0,0 @@
-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,
-    language: String,
-    platform: String,
-    IP: String,
-    country: String,
-    province: String,
-    city: String,
-    extensionVersion: String,
-    tool_name: String,
-}, { timestamps: true });
-
-TrackSchema.index({ userId: 1 });
-TrackSchema.index({ tool_name: 1 });
-TrackSchema.index({ event: 1 });
-
-mongoose.set('autoIndex', false);
-
-module.exports = mongoose.models.Track || mongoose.model('Track', TrackSchema); 

+ 0 - 21
server/package.json

@@ -1,21 +0,0 @@
-{
-  "name": "fehelper-server",
-  "version": "1.0.0",
-  "description": "FeHelper数据统计与管理后台服务端",
-  "main": "index.js",
-  "scripts": {
-    "start": "node index.js",
-    "dev": "nodemon index.js"
-  },
-  "dependencies": {
-    "body-parser": "^1.20.2",
-    "cors": "^2.8.5",
-    "express": "^4.18.2",
-    "express-session": "^1.18.1",
-    "mongoose": "^7.6.3",
-    "ua-parser-js": "^2.0.3"
-  },
-  "devDependencies": {
-    "nodemon": "^3.1.10"
-  }
-}

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff