feat: 访客列表站点归属显示与筛选

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-16 14:33:39 +08:00
parent 15ad3fb200
commit 58659b0ec9
4 changed files with 100 additions and 10 deletions
+5
View File
@@ -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 分钟内有行为的在线访客 */
+45
View File
@@ -4,6 +4,20 @@
<div class="visitor-page art-full-height">
<!-- 筛选栏 -->
<div class="art-card flex-c flex-wrap gap-x-4 gap-y-3 mb-3 px-5 py-4">
<ElSelect
v-model="filters.site"
class="w-52"
:loading="sitesLoading"
@change="applyFilters"
>
<ElOption label="全部站点" :value="ALL_SITES" />
<ElOption
v-for="site in sites"
:key="site.id"
:label="`${site.name}(${site.site_id})`"
:value="site.site_id"
/>
</ElSelect>
<ElInput
v-model="filters.search"
class="w-60"
@@ -84,6 +98,7 @@
import { ElButton } from 'element-plus'
import { useTable } from '@/hooks/core/useTable'
import { getVisitors, type VisitorListParams, type VisitorRow } from '@/api/tracker/visitors'
import { getSites, type SiteRow } from '@/api/tracker/sites'
import { countryFlag, formatDateTime, formatStay } from '@/utils/tracker'
defineOptions({ name: 'TrackerVisitor' })
@@ -107,7 +122,24 @@
}
// ---------- 筛选 ----------
// 站点筛选(哨兵值 ALL_SITES = 全部站点,请求时映射为不传 site;与 dashboard 同款模式)
const ALL_SITES = 'ALL_SITES'
const sites = ref<SiteRow[]>([])
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)
</script>
<style lang="scss" scoped>