From 58659b0ec91c78dab3fa8f1d6e9d43933c20bc4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Wed, 16 Sep 2026 14:33:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AE=BF=E5=AE=A2=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E7=AB=99=E7=82=B9=E5=BD=92=E5=B1=9E=E6=98=BE=E7=A4=BA=E4=B8=8E?= =?UTF-8?q?=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- admin/src/api/tracker/visitors.ts | 5 +++ admin/src/views/tracker/visitor/index.vue | 45 +++++++++++++++++++++++ server/tracker/api/visitors.py | 23 +++++++++--- server/tracker/tests/test_visitors.py | 37 +++++++++++++++++-- 4 files changed, 100 insertions(+), 10 deletions(-) diff --git a/admin/src/api/tracker/visitors.ts b/admin/src/api/tracker/visitors.ts index 7ad50ef..54f5ddb 100644 --- a/admin/src/api/tracker/visitors.ts +++ b/admin/src/api/tracker/visitors.ts @@ -11,6 +11,9 @@ import request from '@/utils/http' /** 访客行(visitors.py `_row`:列表/详情共用行字段) */ export interface VisitorRow { visitor_id: string + /** 站点归属(多站点下分辨访客来自哪个站) */ + site_id: string + site_name: string country: string province: string city: string @@ -97,6 +100,8 @@ export interface VisitorListResp { */ export interface VisitorListParams { page?: number + /** 站点过滤(site_id;不传 = 全部站点,站点不存在时后端 404 site_not_found) */ + site?: string country?: string device_type?: string /** 仅 "1" 生效:5 分钟内有行为的在线访客 */ diff --git a/admin/src/views/tracker/visitor/index.vue b/admin/src/views/tracker/visitor/index.vue index 998d249..52906c3 100644 --- a/admin/src/views/tracker/visitor/index.vue +++ b/admin/src/views/tracker/visitor/index.vue @@ -4,6 +4,20 @@
+ + + + ([]) + const sitesLoading = ref(false) + + const loadSites = async () => { + sitesLoading.value = true + try { + sites.value = await getSites() + } catch { + // 失败已由请求层统一 ElMessage 提示,下拉保持"全部站点" + } finally { + sitesLoading.value = false + } + } + const filters = reactive({ + site: ALL_SITES, search: '', deviceType: '', country: '', @@ -119,6 +151,7 @@ /** 组装请求参数:online/show_bot 用后端哨兵 '1'(类型已锁字面量),空值不发送 */ const buildFilterParams = (): VisitorListParams => { const params: VisitorListParams = {} + if (filters.site !== ALL_SITES) params.site = filters.site const search = filters.search.trim() if (search) params.search = search if (filters.deviceType) params.device_type = filters.deviceType @@ -190,6 +223,9 @@ const renderSource = (val: string) => h('span', { class: 'whitespace-nowrap' }, val || '—') + const renderSite = (row: VisitorRow) => + h('span', { class: 'whitespace-nowrap' }, row.site_name || '—') + // ---------- 表格 ---------- /** 后端页大小固定 20(PAGE_SIZE):useTable 侧同步 size=20 才能算对总页数;size 不实际发送 */ const fetchVisitors = (params: VisitorListParams & { size?: number }) => getVisitors(params) @@ -226,6 +262,12 @@ minWidth: 200, formatter: renderLocation }, + { + prop: 'site_name', + label: '所属站点', + minWidth: 110, + formatter: (row: VisitorRow) => renderSite(row) + }, { prop: 'device_type', label: '设备', @@ -282,6 +324,7 @@ const resetFilters = () => { Object.assign(filters, { + site: ALL_SITES, search: '', deviceType: '', country: '', @@ -291,6 +334,8 @@ }) applyFilters() } + + onMounted(loadSites)