admin.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. console.log('Admin.js script execution started.'); // 最顶部的日志
  2. // 管理后台前端主JS(Vue 3 组件化重构)
  3. const { createApp, ref, reactive, onMounted, defineComponent, watch } = Vue;
  4. console.log('Vue library loaded, createApp function:', typeof createApp); // 检查 Vue 是否加载成功
  5. const apiBase = '/api/admin';
  6. console.log('Defining components...'); // 日志点 1
  7. // 工具英文名到中文名映射
  8. const toolNameMap = {
  9. 'json-format': 'JSON美化工具',
  10. 'json-diff': 'JSON比对工具',
  11. 'qr-code': '二维码/解码',
  12. 'image-base64': '图片转Base64',
  13. 'en-decode': '信息编码转换',
  14. 'code-beautify': '代码美化工具',
  15. 'code-compress': '代码压缩工具',
  16. 'aiagent': 'AI,请帮帮忙',
  17. 'timestamp': '时间(戳)转换',
  18. 'password': '随机密码生成',
  19. 'sticky-notes': '我的便签笔记',
  20. 'html2markdown': 'Markdown转换',
  21. 'postman': '简易Postman',
  22. 'websocket': 'Websocket工具',
  23. 'regexp': '正则公式速查',
  24. 'trans-radix': '进制转换工具',
  25. 'trans-color': '颜色转换工具',
  26. 'crontab': 'Crontab工具',
  27. 'loan-rate': '贷(还)款利率',
  28. 'devtools': 'FH开发者工具',
  29. 'page-monkey': '网页油猴工具',
  30. 'screenshot': '网页截屏工具',
  31. 'color-picker': '页面取色工具',
  32. 'naotu': '便捷思维导图',
  33. 'grid-ruler': '网页栅格标尺',
  34. 'page-timing': '网站性能优化',
  35. 'excel2json': 'Excel转JSON',
  36. 'chart-maker': '图表制作工具',
  37. 'svg-converter': 'SVG转为图片',
  38. 'poster-maker': '海报快速生成',
  39. 'popup': 'FH Popup页面',
  40. 'options': 'FH插件市场'
  41. };
  42. // 顶部导航栏(无打赏按钮,仅限本人使用)
  43. const HeaderNav = defineComponent({
  44. template: `
  45. <header class="w-full h-14 bg-white shadow flex items-center justify-between px-6 fixed top-0 left-0 z-10">
  46. <div class="flex items-center space-x-3">
  47. <img src="./img/fe-48.png" alt="FeHelper" class="h-8 w-8">
  48. <span class="text-xl font-bold tracking-wide">FeHelper 数据统计后台</span>
  49. </div>
  50. <div class="flex items-center space-x-4">
  51. <span class="text-gray-500 text-sm">仅限本人使用</span>
  52. <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>
  53. </div>
  54. </header>
  55. `
  56. });
  57. // 统计总览卡片
  58. const OverviewPanel = defineComponent({
  59. props: ['overview'],
  60. template: `
  61. <div class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-6">
  62. <div class="bg-white rounded shadow p-4 flex flex-col items-center">
  63. <div class="text-2xl font-bold">{{overview.userCount || 0}}</div>
  64. <div class="text-xs text-gray-500 mt-1">累计用户数</div>
  65. </div>
  66. <div class="bg-white rounded shadow p-4 flex flex-col items-center">
  67. <div class="text-2xl font-bold">{{overview.todayActive || 0}}</div>
  68. <div class="text-xs text-gray-500 mt-1">今日活跃用户</div>
  69. </div>
  70. <div class="bg-white rounded shadow p-4 flex flex-col items-center">
  71. <div class="text-2xl font-bold">{{overview.monthUserCount || 0}}</div>
  72. <div class="text-xs text-gray-500 mt-1">近一月活跃用户</div>
  73. </div>
  74. <div class="bg-white rounded shadow p-4 flex flex-col items-center">
  75. <div class="text-2xl font-bold">{{overview.monthUserRate || '0%'}}</div>
  76. <div class="text-xs text-gray-500 mt-1">近一月用户占比</div>
  77. </div>
  78. <div class="bg-white rounded shadow p-4 flex flex-col items-center">
  79. <div class="text-2xl font-bold">{{overview.eventCount || 0}}</div>
  80. <div class="text-xs text-gray-500 mt-1">累计埋点事件数</div>
  81. </div>
  82. </div>
  83. `
  84. });
  85. // 分布表格
  86. const SimpleTable = defineComponent({
  87. props: ['title', 'data', 'label', 'cardColor'],
  88. computed: {
  89. cardBg() {
  90. const map = {
  91. blue: 'bg-blue-50',
  92. green: 'bg-green-50',
  93. yellow: 'bg-yellow-50',
  94. purple: 'bg-purple-50',
  95. pink: 'bg-pink-50',
  96. indigo: 'bg-indigo-50',
  97. orange: 'bg-orange-50',
  98. teal: 'bg-teal-50',
  99. default: 'bg-white'
  100. };
  101. return map[this.cardColor] || map.default;
  102. },
  103. barColor() {
  104. const map = {
  105. blue: 'bg-blue-400',
  106. green: 'bg-green-400',
  107. yellow: 'bg-yellow-400',
  108. purple: 'bg-purple-400',
  109. pink: 'bg-pink-400',
  110. indigo: 'bg-indigo-400',
  111. orange: 'bg-orange-400',
  112. teal: 'bg-teal-400',
  113. default: 'bg-gray-200'
  114. };
  115. return map[this.cardColor] || map.default;
  116. }
  117. },
  118. template: `
  119. <div :class="cardBg + ' rounded-xl shadow-lg p-4 mb-2 relative'">
  120. <div :class="barColor + ' absolute top-0 left-0 w-full h-1 rounded-t'" />
  121. <div class="font-bold mb-2 text-base">{{title}}</div>
  122. <table class="min-w-full text-xs border border-gray-200">
  123. <thead>
  124. <tr>
  125. <th class="px-2 py-1 border-b border-gray-200 bg-gray-50">{{label}}</th>
  126. <th class="px-2 py-1 border-b border-gray-200 bg-gray-50">UV(用户数)</th>
  127. <th class="px-2 py-1 border-b border-gray-200 bg-gray-50">PV(访问次数)</th>
  128. </tr>
  129. </thead>
  130. <tbody>
  131. <tr v-if="data.length === 0">
  132. <td colspan="3" class="px-2 py-1 text-center">暂无数据</td>
  133. </tr>
  134. <tr v-for="row in data" :key="row._id" class="border-b border-gray-100">
  135. <td class="px-2 py-1 border-r border-gray-100">{{row._id}}</td>
  136. <td class="px-2 py-1 border-r border-gray-100">{{row.uv}}</td>
  137. <td class="px-2 py-1">{{row.pv}}</td>
  138. </tr>
  139. </tbody>
  140. </table>
  141. </div>
  142. `
  143. });
  144. // 工具排行
  145. const TopTools = defineComponent({
  146. props: ['tools', 'cardColor'],
  147. template: `
  148. <div :class="(cardColor || 'indigo') + ' rounded-xl shadow-lg p-4 mb-2 relative'">
  149. <div class="font-bold mb-2 text-base">工具排行</div>
  150. <table class="min-w-full text-xs border border-gray-200 border-collapse">
  151. <thead>
  152. <tr>
  153. <th class="px-2 py-1 border border-gray-200 bg-gray-50">工具</th>
  154. <th class="px-2 py-1 border border-gray-200 bg-gray-50">PV(访问次数)</th>
  155. </tr>
  156. </thead>
  157. <tbody>
  158. <tr v-if="tools.length === 0">
  159. <td colspan="2" class="px-2 py-1 text-center border border-gray-200">暂无数据</td>
  160. </tr>
  161. <tr v-for="tool in tools" :key="tool.name">
  162. <td class="px-2 py-1 border border-gray-200">{{tool.name}}</td>
  163. <td class="px-2 py-1 border border-gray-200">{{tool.pv}}</td>
  164. </tr>
  165. </tbody>
  166. </table>
  167. </div>
  168. `
  169. });
  170. // 错误提示
  171. const ErrorAlert = defineComponent({
  172. props: ['message'],
  173. template: `
  174. <div v-if="message" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4 relative">
  175. <strong class="font-bold">错误:</strong>
  176. <span class="block sm:inline">{{message}}</span>
  177. </div>
  178. `
  179. });
  180. const eventTrend = ref([]);
  181. // 新增事件趋势表格
  182. const EventTrendTable = defineComponent({
  183. props: ['data', 'cardColor'],
  184. computed: {
  185. cardBg() {
  186. const map = {
  187. blue: 'bg-blue-50',
  188. green: 'bg-green-50',
  189. yellow: 'bg-yellow-50',
  190. purple: 'bg-purple-50',
  191. pink: 'bg-pink-50',
  192. indigo: 'bg-indigo-50',
  193. orange: 'bg-orange-50',
  194. teal: 'bg-teal-50',
  195. default: 'bg-white'
  196. };
  197. return map[this.cardColor] || map.default;
  198. },
  199. barColor() {
  200. const map = {
  201. blue: 'bg-blue-400',
  202. green: 'bg-green-400',
  203. yellow: 'bg-yellow-400',
  204. purple: 'bg-purple-400',
  205. pink: 'bg-pink-400',
  206. indigo: 'bg-indigo-400',
  207. orange: 'bg-orange-400',
  208. teal: 'bg-teal-400',
  209. default: 'bg-gray-200'
  210. };
  211. return map[this.cardColor] || map.default;
  212. }
  213. },
  214. template: `
  215. <div :class="cardBg + ' rounded-xl shadow-lg p-4 mb-2 relative'">
  216. <div :class="barColor + ' absolute top-0 left-0 w-full h-1 rounded-t'" />
  217. <div class="font-bold mb-2 text-base">事件趋势(最近30天)</div>
  218. <table class="min-w-full text-xs border border-gray-200">
  219. <thead>
  220. <tr>
  221. <th class="px-2 py-1 border-b border-gray-200 bg-gray-50">日期</th>
  222. <th class="px-2 py-1 border-b border-gray-200 bg-gray-50">UV(用户数)</th>
  223. <th class="px-2 py-1 border-b border-gray-200 bg-gray-50">PV(访问次数)</th>
  224. </tr>
  225. </thead>
  226. <tbody>
  227. <tr v-if="data.length === 0">
  228. <td colspan="3" class="px-2 py-1 text-center">暂无数据</td>
  229. </tr>
  230. <tr v-for="row in data" :key="row._id" class="border-b border-gray-100">
  231. <td class="px-2 py-1 border-r border-gray-100">{{row._id}}</td>
  232. <td class="px-2 py-1 border-r border-gray-100">{{row.uv}}</td>
  233. <td class="px-2 py-1">{{row.pv}}</td>
  234. </tr>
  235. </tbody>
  236. </table>
  237. </div>
  238. `
  239. });
  240. const LoginModal = defineComponent({
  241. props: ['show', 'error'],
  242. emits: ['login'],
  243. setup(props, { emit }) {
  244. const username = ref('');
  245. const password = ref('');
  246. const loading = ref(false);
  247. const doLogin = async () => {
  248. loading.value = true;
  249. try {
  250. const res = await fetch(apiBase + '/login', {
  251. method: 'POST',
  252. headers: { 'Content-Type': 'application/json' },
  253. body: JSON.stringify({ username: username.value, password: password.value }),
  254. credentials: 'include'
  255. });
  256. if (res.ok) {
  257. emit('login');
  258. } else {
  259. emit('login', await res.json());
  260. }
  261. } finally {
  262. loading.value = false;
  263. }
  264. };
  265. return { username, password, loading, doLogin };
  266. },
  267. template: `
  268. <div v-if="show" class="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center z-50">
  269. <div class="bg-white rounded shadow-lg p-8 w-80">
  270. <div class="text-lg font-bold mb-4">登录后台</div>
  271. <div class="mb-2">
  272. <input v-model="username" class="w-full border rounded px-3 py-2" placeholder="用户名" autocomplete="username" />
  273. </div>
  274. <div class="mb-4">
  275. <input v-model="password" type="password" class="w-full border rounded px-3 py-2" placeholder="密码" autocomplete="current-password" />
  276. </div>
  277. <div v-if="error" class="text-red-500 text-sm mb-2">{{error.error}}</div>
  278. <button @click="doLogin" :disabled="loading" class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700">{{loading ? '登录中...' : '登录'}}</button>
  279. </div>
  280. </div>
  281. `
  282. });
  283. // App组件定义,保持在所有子组件定义之后
  284. const App = defineComponent({
  285. components: { HeaderNav, OverviewPanel, TopTools, SimpleTable, ErrorAlert, EventTrendTable, LoginModal },
  286. setup() {
  287. // 数据定义
  288. const overview = ref({});
  289. // 直接存储原始数组
  290. const browserDist = ref([]);
  291. const osDist = ref([]);
  292. const deviceTypeDist = ref([]);
  293. const fhVerDist = ref([]);
  294. const langDist = ref([]);
  295. const eventPieDist = ref([]);
  296. const countryDist = ref([]);
  297. const provinceDist = ref([]);
  298. const cityDist = ref([]);
  299. const tools = ref([]);
  300. const eventDist = ref([]);
  301. const errorMsg = ref('');
  302. const loading = ref(true);
  303. const loggedIn = ref(false);
  304. const loginError = ref(null);
  305. // API请求工具函数
  306. const fetchApi = async (url) => {
  307. try {
  308. const res = await fetch(url, { credentials: 'include' });
  309. if (!res.ok) throw new Error(`${url} 请求失败: ${res.status} ${res.statusText}`);
  310. return await res.json();
  311. } catch (err) {
  312. throw err;
  313. }
  314. };
  315. // 加载所有首页数据
  316. const loadAll = async () => {
  317. try {
  318. loading.value = true;
  319. errorMsg.value = '';
  320. // 1. 总览
  321. overview.value = await fetchApi(apiBase + '/overview');
  322. // 2. 浏览器分布
  323. const rawBrowserDist = await fetchApi(apiBase + '/browser-distribution');
  324. browserDist.value = rawBrowserDist.map(i => ({
  325. _id: (i._id && i._id.browser)
  326. ? `${i._id.browser} ${i._id.version}`
  327. : (typeof i._id === 'string' ? i._id : '未知'),
  328. uv: i.uv || 0,
  329. pv: i.pv || 0
  330. }));
  331. // 3. 操作系统分布
  332. const rawOsDist = await fetchApi(apiBase + '/os-distribution');
  333. osDist.value = rawOsDist.map(i => ({
  334. _id: (i._id && i._id.os)
  335. ? `${i._id.os} ${i._id.version}`
  336. : (typeof i._id === 'string' ? i._id : '未知'),
  337. uv: i.uv || 0,
  338. pv: i.pv || 0
  339. }));
  340. // 4. 设备类型分布
  341. const rawDeviceTypeDist = await fetchApi(apiBase + '/device-type-distribution');
  342. deviceTypeDist.value = rawDeviceTypeDist.map(i => ({
  343. _id: i._id || '未知',
  344. uv: i.uv || 0,
  345. pv: i.pv || 0
  346. }));
  347. // 5. 插件版本分布
  348. const rawFhVerDist = await fetchApi(apiBase + '/fh-version-distribution');
  349. fhVerDist.value = rawFhVerDist.map(i => ({
  350. _id: (i._id ? `${i._id}` : '未知'),
  351. uv: i.uv || 0,
  352. pv: i.pv || 0
  353. }));
  354. // 6. 用户语言分布
  355. const users = await fetchApi(apiBase + '/users');
  356. langDist.value = (users.lang || []).map(i => ({
  357. _id: i._id || '未知',
  358. uv: i.uv || 0,
  359. pv: i.pv || 0
  360. }));
  361. // 7. 事件类型分布(主区域)
  362. const rawEventPieDist = await fetchApi(apiBase + '/event-distribution');
  363. eventPieDist.value = (rawEventPieDist || []).map(i => ({
  364. _id: i._id || '未知',
  365. uv: i.uv || 0,
  366. pv: i.pv || 0
  367. }));
  368. // 8. 地理分布
  369. const userDist = await fetchApi(apiBase + '/user-distribution');
  370. countryDist.value = (userDist.country || []).map(i => ({
  371. _id: i._id || '未知',
  372. uv: i.uv || 0,
  373. pv: i.pv || 0
  374. }));
  375. provinceDist.value = (userDist.province || []).map(i => ({
  376. _id: i._id || '未知',
  377. uv: i.uv || 0,
  378. pv: i.pv || 0
  379. }));
  380. cityDist.value = (userDist.city || []).map(i => ({
  381. _id: i._id || '未知',
  382. uv: i.uv || 0,
  383. pv: i.pv || 0
  384. }));
  385. // 9. 工具排行
  386. const toolsList = await fetchApi(apiBase + '/tools');
  387. tools.value = toolsList.map(t => ({ name: toolNameMap[t._id] || (t._id ? t._id : '插件更新或安装'), pv: t.pv || 0 }));
  388. // 10. 事件类型分布(表格)
  389. eventDist.value = eventPieDist.value;
  390. // 11. 事件趋势
  391. const trendList = await fetchApi(apiBase + '/event-trend');
  392. eventTrend.value = trendList.map(i => ({
  393. _id: i._id,
  394. uv: i.uv || 0,
  395. pv: i.pv || 0
  396. }));
  397. } catch (error) {
  398. errorMsg.value = '数据加载失败: ' + error.message;
  399. } finally {
  400. loading.value = false;
  401. }
  402. };
  403. // 检查登录状态
  404. const checkLogin = async () => {
  405. try {
  406. const res = await fetch(apiBase + '/check-login', { credentials: 'include' });
  407. if (res.status === 200) {
  408. loggedIn.value = true;
  409. } else {
  410. // 401等非200状态,均视为未登录(401是未登录的正常表现)
  411. loggedIn.value = false;
  412. }
  413. } catch (e) {
  414. // 网络异常等也视为未登录
  415. loggedIn.value = false;
  416. }
  417. };
  418. // 登录成功后重新加载
  419. const handleLogin = async (err) => {
  420. if (!err || err.success) {
  421. loginError.value = null;
  422. loggedIn.value = true;
  423. await loadAll();
  424. } else {
  425. loginError.value = err;
  426. }
  427. };
  428. onMounted(async () => {
  429. await checkLogin();
  430. if (loggedIn.value) await loadAll();
  431. });
  432. return {
  433. overview, browserDist, osDist, deviceTypeDist, fhVerDist,
  434. langDist, eventPieDist, countryDist, provinceDist, cityDist,
  435. tools, eventDist, errorMsg, loading, eventTrend,
  436. loggedIn,
  437. loginError,
  438. handleLogin
  439. };
  440. },
  441. template: `
  442. <LoginModal :show="!loggedIn" :error="loginError" @login="handleLogin" />
  443. <div v-if="loggedIn">
  444. <HeaderNav />
  445. <main class="pt-16 px-6 max-w-7xl mx-auto">
  446. <ErrorAlert :message="errorMsg" />
  447. <OverviewPanel :overview="overview" />
  448. <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
  449. <SimpleTable title="FeHelper版本分布" :data="fhVerDist" label="版本号" cardColor="blue" />
  450. <SimpleTable title="浏览器分布" :data="browserDist" label="浏览器" cardColor="green" />
  451. <SimpleTable title="操作系统分布" :data="osDist" label="操作系统" cardColor="yellow" />
  452. </div>
  453. <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
  454. <SimpleTable title="设备类型分布" :data="deviceTypeDist" label="设备类型" cardColor="purple" />
  455. <SimpleTable title="语言分布" :data="langDist" label="语言" cardColor="pink" />
  456. </div>
  457. <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
  458. <SimpleTable title="国家分布" :data="countryDist" label="国家" cardColor="indigo" />
  459. <SimpleTable title="省份分布" :data="provinceDist" label="省份" cardColor="teal" />
  460. <SimpleTable title="城市分布" :data="cityDist" label="城市" cardColor="orange" />
  461. </div>
  462. <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
  463. <TopTools :tools="tools" cardColor="indigo" />
  464. <EventTrendTable :data="eventDist" cardColor="teal" />
  465. </div>
  466. </main>
  467. </div>
  468. `
  469. });
  470. console.log('Components defined. Mounting app...'); // 日志点 2
  471. createApp(App).mount('#app');