fix: 升级了一些视觉、各种单图相册效果、日历效果、多种模板任选

This commit is contained in:
2026-08-29 14:19:52 +08:00
parent 1914dad39a
commit 51b40026df
2 changed files with 525 additions and 49 deletions

View File

@@ -853,10 +853,26 @@ func resolveVisitCity(city, raw, province string) string {
return "未知"
}
func handleVisitList(c *gin.Context) {
if !requireAdmin(c) {
return
}
type visitListQuery struct {
Page int
PageSize int
Region string
City string
HasInteraction bool
HasGift bool
Sort string
}
type visitEnrichment struct {
GuestNames []string
GiftCount int64
LikeCount int64
RsvpCount int64
DanmakuCount int64
HasInteraction bool
}
func parseVisitListQuery(c *gin.Context) visitListQuery {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
if page < 1 {
@@ -865,37 +881,369 @@ func handleVisitList(c *gin.Context) {
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
sort := strings.TrimSpace(c.DefaultQuery("sort", "last_seen_desc"))
switch sort {
case "last_seen_desc", "last_seen_asc", "visit_count_desc", "visit_count_asc":
default:
sort = "last_seen_desc"
}
return visitListQuery{
Page: page,
PageSize: pageSize,
Region: strings.TrimSpace(c.Query("region")),
City: strings.TrimSpace(c.Query("city")),
HasInteraction: c.Query("has_interaction") == "1" || strings.EqualFold(c.Query("has_interaction"), "true"),
HasGift: c.Query("has_gift") == "1" || strings.EqualFold(c.Query("has_gift"), "true"),
Sort: sort,
}
}
func applyRegionWhere(q *gorm.DB, region string) *gorm.DB {
target := normalizeRegionName(region)
if target == "未知" {
return q.Where("region = '' OR region IS NULL OR region = ? OR region LIKE ?", "未知", "%未知%")
}
if target == "本地" {
return q.Where("region = ? OR raw_location = ?", "本地", "本地")
}
like := "%" + target + "%"
return q.Where("region = ? OR region LIKE ? OR raw_location LIKE ?", target, like, like)
}
// fingerprintsForRegionCity 按指纹去重,与市级分布统计口径一致
func fingerprintsForRegionCity(region, city string) []string {
targetRegion := normalizeRegionName(region)
targetCity := strings.TrimSpace(city)
type locRow struct {
Fingerprint string
Region string
City string
RawLocation string
}
var rows []locRow
q := db.Model(&SiteVisit{}).Select("fingerprint, region, city, raw_location")
q = applyRegionWhere(q, targetRegion)
if err := q.Find(&rows).Error; err != nil {
return nil
}
fpCity := map[string]string{}
for _, row := range rows {
if !rowBelongsToRegion(row.Region, row.RawLocation, targetRegion) {
continue
}
c := resolveVisitCity(row.City, row.RawLocation, targetRegion)
if prev, ok := fpCity[row.Fingerprint]; ok {
if prev != "未知" || c == "未知" {
continue
}
}
fpCity[row.Fingerprint] = c
}
out := make([]string, 0)
for fp, c := range fpCity {
if targetCity != "" && c != targetCity {
continue
}
out = append(out, fp)
}
return out
}
func visitorFingerprintsWithGift() map[string]struct{} {
result := map[string]struct{}{}
var fps []string
db.Model(&models.SiteGift{}).Distinct().Where("fingerprint != ''").Pluck("fingerprint", &fps)
for _, fp := range fps {
result[fp] = struct{}{}
}
var cids []string
db.Model(&models.SiteGift{}).Distinct().Where("client_id != ''").Pluck("client_id", &cids)
if len(cids) > 0 {
var visitFps []string
db.Model(&SiteVisit{}).Where("client_id IN ?", cids).Distinct().Pluck("fingerprint", &visitFps)
for _, fp := range visitFps {
result[fp] = struct{}{}
}
}
return result
}
func visitorFingerprintsWithInteraction() map[string]struct{} {
result := visitorFingerprintsWithGift()
addByField := func(model interface{}, fpField, cidField string) {
var fps []string
db.Model(model).Distinct().Where(fpField+" != ''").Pluck(fpField, &fps)
for _, fp := range fps {
result[fp] = struct{}{}
}
var cids []string
db.Model(model).Distinct().Where(cidField+" != ''").Pluck(cidField, &cids)
if len(cids) == 0 {
return
}
var visitFps []string
db.Model(&SiteVisit{}).Where("client_id IN ?", cids).Distinct().Pluck("fingerprint", &visitFps)
for _, fp := range visitFps {
result[fp] = struct{}{}
}
}
addByField(&models.SiteLike{}, "fingerprint", "client_id")
addByField(&models.Rsvp{}, "fingerprint", "client_id")
addByField(&models.Danmaku{}, "fingerprint", "client_id")
return result
}
func mapKeys(m map[string]struct{}) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}
func visitListOrder(sort string) string {
switch sort {
case "visit_count_desc":
return "visit_count desc, last_seen_at desc"
case "visit_count_asc":
return "visit_count asc, last_seen_at desc"
case "last_seen_asc":
return "last_seen_at asc"
default:
return "last_seen_at desc"
}
}
func appendUniqueName(names []string, name string) []string {
name = strings.TrimSpace(name)
if name == "" {
return names
}
for _, n := range names {
if n == name {
return names
}
}
return append(names, name)
}
func batchEnrichVisits(rows []SiteVisit) map[string]visitEnrichment {
result := map[string]visitEnrichment{}
if len(rows) == 0 {
return result
}
fps := make([]string, 0, len(rows))
cids := make([]string, 0, len(rows))
cidToFp := map[string]string{}
seenFp := map[string]struct{}{}
for _, row := range rows {
if row.Fingerprint != "" {
if _, ok := seenFp[row.Fingerprint]; !ok {
seenFp[row.Fingerprint] = struct{}{}
fps = append(fps, row.Fingerprint)
}
}
if row.ClientID != "" {
cids = append(cids, row.ClientID)
cidToFp[row.ClientID] = row.Fingerprint
}
}
apply := func(fp string, fn func(*visitEnrichment)) {
if fp == "" {
return
}
e := result[fp]
fn(&e)
result[fp] = e
}
var gifts []models.SiteGift
if len(fps) > 0 || len(cids) > 0 {
q := db.Model(&models.SiteGift{})
if len(fps) > 0 && len(cids) > 0 {
q = q.Where("fingerprint IN ? OR client_id IN ?", fps, cids)
} else if len(fps) > 0 {
q = q.Where("fingerprint IN ?", fps)
} else {
q = q.Where("client_id IN ?", cids)
}
q.Find(&gifts)
}
for _, g := range gifts {
fp := g.Fingerprint
if fp == "" {
fp = cidToFp[g.ClientID]
}
apply(fp, func(e *visitEnrichment) {
e.GiftCount++
e.GuestNames = appendUniqueName(e.GuestNames, g.Name)
e.HasInteraction = true
})
}
var likes []models.SiteLike
if len(fps) > 0 || len(cids) > 0 {
q := db.Model(&models.SiteLike{})
if len(fps) > 0 && len(cids) > 0 {
q = q.Where("fingerprint IN ? OR client_id IN ?", fps, cids)
} else if len(fps) > 0 {
q = q.Where("fingerprint IN ?", fps)
} else {
q = q.Where("client_id IN ?", cids)
}
q.Find(&likes)
}
for _, row := range likes {
fp := row.Fingerprint
if fp == "" {
fp = cidToFp[row.ClientID]
}
apply(fp, func(e *visitEnrichment) {
e.LikeCount++
e.HasInteraction = true
})
}
var rsvps []models.Rsvp
if len(fps) > 0 || len(cids) > 0 {
q := db.Model(&models.Rsvp{})
if len(fps) > 0 && len(cids) > 0 {
q = q.Where("fingerprint IN ? OR client_id IN ?", fps, cids)
} else if len(fps) > 0 {
q = q.Where("fingerprint IN ?", fps)
} else {
q = q.Where("client_id IN ?", cids)
}
q.Find(&rsvps)
}
for _, r := range rsvps {
fp := r.Fingerprint
if fp == "" {
fp = cidToFp[r.ClientID]
}
apply(fp, func(e *visitEnrichment) {
e.RsvpCount++
e.GuestNames = appendUniqueName(e.GuestNames, r.Name)
e.HasInteraction = true
})
}
var danmakus []models.Danmaku
if len(fps) > 0 || len(cids) > 0 {
q := db.Model(&models.Danmaku{})
if len(fps) > 0 && len(cids) > 0 {
q = q.Where("fingerprint IN ? OR client_id IN ?", fps, cids)
} else if len(fps) > 0 {
q = q.Where("fingerprint IN ?", fps)
} else {
q = q.Where("client_id IN ?", cids)
}
q.Find(&danmakus)
}
for _, d := range danmakus {
fp := d.Fingerprint
if fp == "" {
fp = cidToFp[d.ClientID]
}
apply(fp, func(e *visitEnrichment) {
e.DanmakuCount++
e.GuestNames = appendUniqueName(e.GuestNames, d.Name)
e.HasInteraction = true
})
}
return result
}
func buildVisitListDB(q visitListQuery) *gorm.DB {
query := db.Model(&SiteVisit{})
if q.Region != "" {
if q.City != "" {
fps := fingerprintsForRegionCity(q.Region, q.City)
if len(fps) == 0 {
return query.Where("1 = 0")
}
query = query.Where("fingerprint IN ?", fps)
} else {
query = applyRegionWhere(query, q.Region)
}
}
if q.HasGift {
fps := mapKeys(visitorFingerprintsWithGift())
if len(fps) == 0 {
return query.Where("1 = 0")
}
query = query.Where("fingerprint IN ?", fps)
}
if q.HasInteraction {
fps := mapKeys(visitorFingerprintsWithInteraction())
if len(fps) == 0 {
return query.Where("1 = 0")
}
query = query.Where("fingerprint IN ?", fps)
}
return query
}
func handleVisitList(c *gin.Context) {
if !requireAdmin(c) {
return
}
q := parseVisitListQuery(c)
baseQuery := buildVisitListDB(q)
var total int64
db.Model(&SiteVisit{}).Count(&total)
baseQuery.Count(&total)
var list []SiteVisit
db.Order("last_seen_at desc").
Offset((page - 1) * pageSize).
Limit(pageSize).
baseQuery.Order(visitListOrder(q.Sort)).
Offset((q.Page - 1) * q.PageSize).
Limit(q.PageSize).
Find(&list)
if list == nil {
list = []SiteVisit{}
}
enrichMap := batchEnrichVisits(list)
enriched := make([]gin.H, 0, len(list))
for _, row := range list {
e := enrichMap[row.Fingerprint]
guestNames := e.GuestNames
if guestNames == nil {
guestNames = []string{}
}
enriched = append(enriched, gin.H{
"id": row.ID,
"fingerprint": row.Fingerprint,
"client_id": row.ClientID,
"ip": row.IP,
"country": row.Country,
"region": row.Region,
"city": row.City,
"isp": row.ISP,
"raw_location": row.RawLocation,
"user_agent": row.UserAgent,
"device": parseDeviceFromUA(row.UserAgent),
"referer": row.Referer,
"visit_count": row.VisitCount,
"first_seen_at": row.FirstSeenAt,
"last_seen_at": row.LastSeenAt,
"id": row.ID,
"fingerprint": row.Fingerprint,
"client_id": row.ClientID,
"ip": row.IP,
"country": row.Country,
"region": row.Region,
"city": row.City,
"isp": row.ISP,
"raw_location": row.RawLocation,
"user_agent": row.UserAgent,
"device": parseDeviceFromUA(row.UserAgent),
"referer": row.Referer,
"visit_count": row.VisitCount,
"first_seen_at": row.FirstSeenAt,
"last_seen_at": row.LastSeenAt,
"guest_names": guestNames,
"guest_name": strings.Join(guestNames, "、"),
"gift_count": e.GiftCount,
"like_count": e.LikeCount,
"rsvp_count": e.RsvpCount,
"danmaku_count": e.DanmakuCount,
"has_interaction": e.HasInteraction,
})
}
@@ -904,8 +1252,8 @@ func handleVisitList(c *gin.Context) {
"data": gin.H{
"list": enriched,
"total": total,
"page": page,
"pageSize": pageSize,
"page": q.Page,
"pageSize": q.PageSize,
},
})
}

View File

@@ -199,19 +199,40 @@
<div class="text-[12px] text-[#7a6550] tracking-wide">
{{ selectedRegion }} · 市级分布
<span v-if="cityStatsLoading" class="text-[10px] text-[#a89a88] ml-2">加载中</span>
<span v-else class="text-[10px] text-[#a89a88] ml-2">点击城市筛选下方访客</span>
</div>
<a-button size="small" type="text" class="!text-[var(--c-primary)]" @click="clearSelectedRegion">
<i class="fa-solid fa-xmark mr-1"></i>关闭
<a-button size="small" type="text" class="!text-[var(--c-primary)]" @click="clearVisitRegionFilter">
<i class="fa-solid fa-xmark mr-1"></i>清除地区筛选
</a-button>
</div>
<div ref="cityChartRef" class="h-[280px] w-full"></div>
<div ref="cityChartRef" class="h-[280px] w-full cursor-pointer"></div>
</div>
<div v-if="selectedRegion || visitFilter.hasInteraction || visitFilter.hasGift" class="flex flex-wrap items-center gap-2 mb-3">
<span class="text-[11px] text-[var(--c-muted)]">当前筛选</span>
<a-tag v-if="selectedRegion" closable color="processing" @close="clearVisitRegionFilter">
{{ selectedRegion }}{{ selectedCity ? ` · ${selectedCity}` : '' }}
</a-tag>
<a-tag v-if="visitFilter.hasInteraction" closable @close="visitFilter.hasInteraction = false; onVisitFilterChange()">有互动</a-tag>
<a-tag v-if="visitFilter.hasGift" closable @close="visitFilter.hasGift = false; onVisitFilterChange()">有礼物</a-tag>
</div>
<div class="flex justify-between items-center mb-2 gap-2 flex-wrap">
<div class="text-[12px] text-[#7a6550] tracking-wide">近期访客预览</div>
<a-button size="small" type="link" class="!text-[var(--c-primary)] !px-0" @click="activeTab = 'visits'">
查看全部访客记录 <i class="fa-solid fa-arrow-right ml-1 text-[10px]"></i>
</a-button>
<div class="flex items-center gap-2 flex-wrap">
<a-checkbox v-model:checked="visitFilter.hasInteraction" @change="onVisitFilterChange">有互动</a-checkbox>
<a-checkbox v-model:checked="visitFilter.hasGift" @change="onVisitFilterChange">有礼物</a-checkbox>
<a-select
v-model:value="visitFilter.sort"
size="small"
class="w-[130px]"
:options="visitSortOptions"
@change="onVisitFilterChange"
/>
<a-button size="small" type="link" class="!text-[var(--c-primary)] !px-0" @click="activeTab = 'visits'">
查看全部访客记录 <i class="fa-solid fa-arrow-right ml-1 text-[10px]"></i>
</a-button>
</div>
</div>
<div class="visit-preview-wrap">
<a-table
@@ -229,12 +250,18 @@
<template v-if="column.key === 'fingerprint'">
<span class="font-mono text-[11px] text-[var(--c-primary)] underline-offset-2 hover:underline" :title="record.fingerprint">{{ shortFp(record.fingerprint) }}</span>
</template>
<template v-else-if="column.key === 'guest_name'">
<span class="text-[12px]" :title="formatGuestName(record)">{{ formatGuestName(record) }}</span>
</template>
<template v-else-if="column.key === 'location'">
<span>{{ record.raw_location || [record.region, record.city].filter(Boolean).join(' ') || '未知' }}</span>
</template>
<template v-else-if="column.key === 'device'">
<span class="text-[12px]">{{ record.device || '未知' }}</span>
</template>
<template v-else-if="column.key === 'gift_count'">
<span :class="record.gift_count > 0 ? 'text-[var(--c-primary)]' : 'text-[var(--c-muted)]'">{{ record.gift_count || 0 }}</span>
</template>
<template v-else-if="column.key === 'last_seen_at'">
<span class="text-[12px] text-[var(--c-muted)]">{{ formatTime(record.last_seen_at) }}</span>
</template>
@@ -339,6 +366,24 @@
<i class="fa-solid fa-rotate-right mr-1"></i>刷新
</a-button>
</div>
<div class="flex flex-wrap items-center gap-3 mb-3">
<a-checkbox v-model:checked="visitFilter.hasInteraction" @change="onVisitFilterChange">有互动</a-checkbox>
<a-checkbox v-model:checked="visitFilter.hasGift" @change="onVisitFilterChange">有礼物</a-checkbox>
<a-select
v-model:value="visitFilter.sort"
size="small"
class="w-[140px]"
:options="visitSortOptions"
@change="onVisitFilterChange"
/>
<template v-if="selectedRegion">
<a-tag closable color="processing" @close="clearVisitRegionFilter">
{{ selectedRegion }}{{ selectedCity ? ` · ${selectedCity}` : '' }}
</a-tag>
</template>
</div>
<div class="text-[11px] text-[var(--c-muted)] mb-2">点击行查看该访客的回执弹幕点赞与礼物详情</div>
<div class="visit-list-wrap">
<a-table
@@ -349,7 +394,7 @@
row-key="id"
size="middle"
:pagination="visitPagination"
:scroll="{ y: 'calc(100dvh - 280px)', x: 960 }"
:scroll="{ y: 'calc(100dvh - 320px)', x: 1100 }"
:custom-row="visitTableCustomRow"
@change="onVisitTableChange"
>
@@ -357,12 +402,21 @@
<template v-if="column.key === 'fingerprint'">
<span class="font-mono text-[11px] text-[var(--c-primary)] underline-offset-2 hover:underline" :title="record.fingerprint">{{ shortFp(record.fingerprint) }}</span>
</template>
<template v-else-if="column.key === 'guest_name'">
<span class="text-[12px]" :title="formatGuestName(record)">{{ formatGuestName(record) }}</span>
</template>
<template v-else-if="column.key === 'location'">
<span>{{ record.raw_location || [record.region, record.city].filter(Boolean).join(' ') || '未知' }}</span>
</template>
<template v-else-if="column.key === 'device'">
<span class="text-[12px]" :title="record.user_agent">{{ record.device || '未知' }}</span>
</template>
<template v-else-if="column.key === 'gift_count'">
<span :class="record.gift_count > 0 ? 'text-[var(--c-primary)]' : 'text-[var(--c-muted)]'">{{ record.gift_count || 0 }}</span>
</template>
<template v-else-if="column.key === 'like_count'">
<span :class="record.like_count > 0 ? 'text-[var(--c-primary)]' : 'text-[var(--c-muted)]'">{{ record.like_count || 0 }}</span>
</template>
<template v-else-if="column.key === 'user_agent'">
<span class="text-[11px] text-[var(--c-muted)]" :title="record.user_agent">{{ shortUa(record.user_agent) }}</span>
</template>
@@ -2085,8 +2139,20 @@ let regionChart = null
let deviceChart = null
let cityChart = null
const selectedRegion = ref('')
const selectedCity = ref('')
const cityStats = ref([])
const cityStatsLoading = ref(false)
const visitFilter = reactive({
hasInteraction: false,
hasGift: false,
sort: 'last_seen_desc',
})
const visitSortOptions = [
{ label: '最近访问', value: 'last_seen_desc' },
{ label: '最早访问', value: 'last_seen_asc' },
{ label: '访问最多', value: 'visit_count_desc' },
{ label: '访问最少', value: 'visit_count_asc' },
]
const cfg = reactive(defaultConfig())
const uploadCfg = reactive(defaultUploadConfig())
@@ -3433,21 +3499,26 @@ async function onSave() {
}
const visitPreviewColumns = [
{ title: '指纹', key: 'fingerprint', width: 110 },
{ title: '指纹', key: 'fingerprint', width: 100 },
{ title: '姓名', key: 'guest_name', width: 90, ellipsis: true },
{ title: '归属地', key: 'location', ellipsis: true },
{ title: '机型', key: 'device', width: 120, ellipsis: true },
{ title: '次数', dataIndex: 'visit_count', key: 'visit_count', width: 64 },
{ title: '最近访问', key: 'last_seen_at', width: 160 },
{ title: '机型', key: 'device', width: 110, ellipsis: true },
{ title: '次数', dataIndex: 'visit_count', key: 'visit_count', width: 56 },
{ title: '礼物', key: 'gift_count', width: 56 },
{ title: '最近访问', key: 'last_seen_at', width: 150 },
]
const visitColumns = [
{ title: '指纹', key: 'fingerprint', width: 120 },
{ title: 'IP', dataIndex: 'ip', key: 'ip', width: 130 },
{ title: '指纹', key: 'fingerprint', width: 110 },
{ title: '姓名', key: 'guest_name', width: 100, ellipsis: true },
{ title: 'IP', dataIndex: 'ip', key: 'ip', width: 120 },
{ title: '归属地', key: 'location', ellipsis: true },
{ title: '机型', key: 'device', width: 140, ellipsis: true },
{ title: '次数', dataIndex: 'visit_count', key: 'visit_count', width: 70 },
{ title: '机型', key: 'device', width: 130, ellipsis: true },
{ title: '次数', dataIndex: 'visit_count', key: 'visit_count', width: 64, sorter: true },
{ title: '礼物', key: 'gift_count', width: 64 },
{ title: '点赞', key: 'like_count', width: 64 },
{ title: 'UA', key: 'user_agent', ellipsis: true },
{ title: '最近访问', key: 'last_seen_at', width: 170 },
{ title: '最近访问', key: 'last_seen_at', width: 160 },
]
function shortFp(fp) {
@@ -3459,6 +3530,39 @@ function shortUa(ua) {
return ua.length > 42 ? `${ua.slice(0, 42)}` : ua
}
function buildVisitListParams(extra = {}) {
const params = {
page: extra.page ?? visitPage.value,
pageSize: extra.pageSize ?? visitPageSize.value,
sort: visitFilter.sort,
}
if (selectedRegion.value) params.region = selectedRegion.value
if (selectedCity.value) params.city = selectedCity.value
if (visitFilter.hasInteraction) params.has_interaction = '1'
if (visitFilter.hasGift) params.has_gift = '1'
return params
}
function formatGuestName(record) {
if (record.guest_name) return record.guest_name
const names = Array.isArray(record.guest_names) ? record.guest_names : []
return names.length ? names.join('、') : '—'
}
function onVisitFilterChange() {
visitPage.value = 1
loadVisitList()
if (activeTab.value === 'overview') loadVisitPreview()
}
function clearVisitRegionFilter() {
clearSelectedRegion()
selectedCity.value = ''
visitPage.value = 1
loadVisitList()
if (activeTab.value === 'overview') loadVisitPreview()
}
const visitTableCustomRow = (record) => ({
onClick: () => openVisitDetail(record?.fingerprint),
style: { cursor: 'pointer' },
@@ -3577,28 +3681,53 @@ function renderCityChart() {
cityChart,
Array.isArray(cityStats.value) ? cityStats.value : [],
'暂无市级数据',
{ selectable: true, selectedName: selectedCity.value },
)
if (!cityChart) return
cityChart.off('click')
cityChart.on('click', (params) => {
const name = params?.name
if (!name || name === '暂无市级数据') return
if (selectedCity.value === name) {
selectedCity.value = ''
} else {
selectedCity.value = name
}
renderCityChart()
visitPage.value = 1
loadVisitList()
if (activeTab.value === 'overview') loadVisitPreview()
})
}
function clearSelectedRegion(opts = {}) {
selectedRegion.value = ''
selectedCity.value = ''
cityStats.value = []
disposeCityChart()
if (!opts.silent && regionChart) renderRegionChart()
if (!opts.silent) {
if (regionChart) renderRegionChart()
visitPage.value = 1
loadVisitList()
if (activeTab.value === 'overview') loadVisitPreview()
}
}
async function onSelectRegion(name) {
selectedRegion.value = name
selectedCity.value = ''
cityStatsLoading.value = true
cityStats.value = []
disposeCityChart()
renderRegionChart()
visitPage.value = 1
try {
const res = await getVisitCityStats(name)
const d = res.data || {}
cityStats.value = Array.isArray(d.cities) ? d.cities : []
await nextTick()
renderCityChart()
await Promise.all([loadVisitPreview(), loadVisitList()])
} catch (e) {
message.error(e?.message || '加载市级分布失败')
cityStats.value = []
@@ -3684,7 +3813,7 @@ async function loadVisitStats() {
async function loadVisitPreview() {
visitListLoading.value = true
try {
const listRes = await getVisitList({ page: 1, pageSize: 10 })
const listRes = await getVisitList(buildVisitListParams({ page: 1, pageSize: 10 }))
const listData = listRes.data || {}
visitPreviewList.value = listData.list || []
visitTotal.value = Number(listData.total) || 0
@@ -3698,10 +3827,7 @@ async function loadVisitPreview() {
async function loadVisitList() {
visitListLoading.value = true
try {
const listRes = await getVisitList({
page: visitPage.value,
pageSize: visitPageSize.value,
})
const listRes = await getVisitList(buildVisitListParams())
const listData = listRes.data || {}
visitList.value = listData.list || []
visitTotal.value = Number(listData.total) || 0
@@ -3716,16 +3842,18 @@ async function loadOverview() {
await Promise.all([loadVisitStats(), loadVisitPreview()])
}
function onVisitTableChange(pag) {
function onVisitTableChange(pag, _filters, sorter) {
visitPage.value = pag?.current || 1
visitPageSize.value = pag?.pageSize || visitPageSize.value
if (sorter?.field === 'visit_count' || sorter?.columnKey === 'visit_count') {
visitFilter.sort = sorter.order === 'ascend' ? 'visit_count_asc' : 'visit_count_desc'
}
loadVisitList()
}
async function onTabActivate(tab) {
if (!adminStore.isAdmin) return
if (tab !== 'overview') {
clearSelectedRegion({ silent: true })
disposeVisitCharts()
}
if (tab === 'overview') {