feat: 访问埋点统计——VisitLog模型+/api GET埋点中间件(小程序/H5/直接访问来源识别)+访问统计接口+看板PV/UV/来源分布卡+日志页访问埋点Tab

This commit is contained in:
Hermes Agent
2026-09-15 21:29:30 +08:00
parent 62fb40e415
commit f91d8f5418
6 changed files with 137 additions and 3 deletions
+21
View File
@@ -7,6 +7,21 @@
<div class="card"><b>{{ d.stats?.aiChatUsed ?? '-' }}</b><span>AI 问答次数</span><i>{{ d.stats?.aiChatUsers ?? 0 }} 人用过</i></div>
<div class="card"><b>{{ d.stats?.totalTrends ?? '-' }}</b><span>趋势资讯</span><i>含人话解读 {{ d.stats?.trendsWithExplain ?? 0 }}</i></div>
<div class="card"><b>{{ d.stats?.paidOrders ?? '-' }}</b><span>付费订单</span><i>共 {{ d.stats?.totalOrders ?? 0 }} 单</i></div>
<div class="card"><b>{{ visit.pvToday ?? '-' }}</b><span>今日访问 PV</span><i>小程序 + H5</i></div>
<div class="card"><b>{{ visit.uvToday ?? '-' }}</b><span>今日访客 UV</span><i>按 IP 去重</i></div>
<div class="card"><b>{{ visit.h5Today ?? '-' }}</b><span>H5 访问</span><i>小程序 {{ visit.miniToday ?? 0 }} 次</i></div>
</div>
<div class="row" v-if="(visit.sources7d || []).length">
<div class="panel">
<h3>访问来源分布(近 7 天)</h3>
<div v-for="s in visit.sources7d" :key="s.source" class="bar-row">
<span class="bar-name">{{ s.source }}</span>
<div class="bar-track"><div class="bar-fill" :style="{ width: srcPct(s.count) }"></div></div>
<span class="bar-num">{{ s.count }}</span>
</div>
</div>
<div class="panel grow"></div>
</div>
<div class="row">
@@ -47,13 +62,19 @@
import { ref, onMounted } from 'vue'
import { apiGet } from '../api'
const d = ref({})
const visit = ref({})
function pct(n) {
const max = Math.max(1, ...(d.value.litByDim || []).map(b => b.count))
return Math.round((n / max) * 100) + '%'
}
function srcPct(n) {
const max = Math.max(1, ...(visit.value.sources7d || []).map(s => s.count))
return Math.round((n / max) * 100) + '%'
}
function fmt(t) { return t ? new Date(t).toLocaleString('zh-CN', { hour12: false }) : '-' }
onMounted(async () => {
d.value = (await apiGet('/api/admin/dashboard-v2')) || {}
try { visit.value = (await apiGet('/api/admin/visit-stats')) || {} } catch (e) { visit.value = {} }
})
</script>
+12 -1
View File
@@ -2,6 +2,7 @@
<div class="page">
<el-tabs v-model="tab" @tab-change="load">
<el-tab-pane label="登录/访问日志" name="login" />
<el-tab-pane label="访问埋点" name="visit" />
<el-tab-pane label="管理操作日志" name="admin" />
</el-tabs>
<el-table :data="list" stripe v-loading="loading" size="small">
@@ -20,6 +21,16 @@
<template #default="{ row }">{{ [row.os, row.browser].filter(Boolean).join(' / ') || row.device || '—' }}</template>
</el-table-column>
</template>
<template v-else-if="tab === 'visit'">
<el-table-column label="时间" width="160"><template #default="{ row }">{{ fmt(row.createdAt) }}</template></el-table-column>
<el-table-column prop="source" label="来源" width="100" />
<el-table-column label="终端" width="150">
<template #default="{ row }">{{ [row.os, row.browser].filter(Boolean).join(' / ') || '—' }}</template>
</el-table-column>
<el-table-column prop="device" label="设备" width="90" />
<el-table-column prop="path" label="路径" min-width="200" show-overflow-tooltip />
<el-table-column prop="ip" label="IP" width="130" />
</template>
<template v-else>
<el-table-column label="时间" width="160"><template #default="{ row }">{{ fmt(row.timestamp || row.createdAt) }}</template></el-table-column>
<el-table-column prop="adminName" label="管理员" width="100" />
@@ -44,7 +55,7 @@ function fmt(t) { return t ? new Date(t).toLocaleString('zh-CN', { hour12: false
async function load() {
loading.value = true
try {
const ep = tab.value === 'login' ? 'login-logs' : 'logs'
const ep = tab.value === 'login' ? 'login-logs' : tab.value === 'visit' ? 'visits' : 'logs'
const r = await apiGet(`/api/admin/${ep}?page=${page.value}&limit=${pageSize}`)
list.value = r.items || []
total.value = r.pagination?.total || 0