fix: 升级了一些视觉、各种单图相册效果、日历效果、多种模板任选
This commit is contained in:
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user