Browse Source

再次修改

zxlie 9 months ago
parent
commit
18f20c6fa3
4 changed files with 288 additions and 96 deletions
  1. 238 95
      server/admin/js/admin.js
  2. 42 0
      server/api/admin.js
  3. 7 1
      server/index.js
  4. 1 0
      server/package.json

+ 238 - 95
server/admin/js/admin.js

@@ -8,6 +8,42 @@ console.log('Vue library loaded, createApp function:', typeof createApp); // 检
 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({
   template: `
@@ -53,65 +89,43 @@ const OverviewPanel = defineComponent({
   `
 });
 
-// 工具英文名到中文名映射
-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'],
+  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="bg-white rounded shadow p-4">
-      <div class="font-bold mb-2">{{title}}</div>
+    <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>
@@ -135,6 +149,33 @@ const SimpleTable = defineComponent({
   `
 });
 
+// 工具排行
+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'],
@@ -150,10 +191,41 @@ const eventTrend = ref([]);
 
 // 新增事件趋势表格
 const EventTrendTable = defineComponent({
-  props: ['data'],
+  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="bg-white rounded shadow p-4">
-      <div class="font-bold mb-2">事件趋势(最近30天)</div>
+    <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>
@@ -177,8 +249,53 @@ const EventTrendTable = defineComponent({
   `
 });
 
+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>
+  `
+});
+
+// App组件定义,保持在所有子组件定义之后
 const App = defineComponent({
-  components: { HeaderNav, OverviewPanel, TopTools, SimpleTable, ErrorAlert, EventTrendTable },
+  components: { HeaderNav, OverviewPanel, TopTools, SimpleTable, ErrorAlert, EventTrendTable, LoginModal },
   setup() {
     // 数据定义
     const overview = ref({});
@@ -196,11 +313,13 @@ const App = defineComponent({
     const eventDist = ref([]);
     const errorMsg = ref('');
     const loading = ref(true);
+    const loggedIn = ref(false);
+    const loginError = ref(null);
 
     // API请求工具函数
     const fetchApi = async (url) => {
       try {
-        const res = await fetch(url);
+        const res = await fetch(url, { credentials: 'include' });
         if (!res.ok) throw new Error(`${url} 请求失败: ${res.status} ${res.statusText}`);
         return await res.json();
       } catch (err) {
@@ -309,49 +428,73 @@ const App = defineComponent({
       }
     };
 
-    onMounted(() => {
-      loadAll();
+    // 检查登录状态
+    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
+      tools, eventDist, errorMsg, loading, eventTrend,
+      loggedIn,
+      loginError,
+      handleLogin
     };
   },
   template: `
-    <div>
+    <LoginModal :show="!loggedIn" :error="loginError" @login="handleLogin" />
+    <div v-if="loggedIn">
       <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>
+      <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>
   `
 });

+ 42 - 0
server/api/admin.js

@@ -4,6 +4,39 @@ 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);
@@ -328,4 +361,13 @@ router.get('/event-trend', async (req, res) => {
   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; 

+ 7 - 1
server/index.js

@@ -5,7 +5,7 @@ const cors = require('cors');
 const bodyParser = require('body-parser');
 const { mongoUri } = require('./models/config');
 const path = require('path');
-const UAParser = require('ua-parser-js');
+const session = require('express-session');
 
 const app = express();
 const PORT = 3001;
@@ -13,6 +13,12 @@ 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'));

+ 1 - 0
server/package.json

@@ -11,6 +11,7 @@
     "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"
   },