| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503 |
- 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({
- 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 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>
- `
- });
- // App组件定义,保持在所有子组件定义之后
- const App = defineComponent({
- components: { HeaderNav, OverviewPanel, TopTools, SimpleTable, ErrorAlert, EventTrendTable, LoginModal },
- 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);
- // 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
- };
- },
- template: `
- <LoginModal :show="!loggedIn" :error="loginError" @login="handleLogin" />
- <div v-if="loggedIn">
- <HeaderNav />
- <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');
|