feat: 访客列表站点归属显示与筛选
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -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 分钟内有行为的在线访客 */
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user