1. 迎宾海报
This commit is contained in:
48
vite-tailwindcss/pnpm-lock.yaml
generated
48
vite-tailwindcss/pnpm-lock.yaml
generated
@@ -17,12 +17,30 @@ importers:
|
||||
'@fingerprintjs/fingerprintjs':
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0
|
||||
'@fontsource/cinzel':
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
'@fontsource/great-vibes':
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
'@fontsource/liu-jian-mao-cao':
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
'@fontsource/long-cang':
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
'@fontsource/ma-shan-zheng':
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
'@fontsource/pinyon-script':
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
'@fontsource/playfair-display':
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
'@fontsource/zhi-mang-xing':
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
'@fortawesome/fontawesome-free':
|
||||
specifier: ^6.7.2
|
||||
version: 6.7.2
|
||||
@@ -296,12 +314,30 @@ packages:
|
||||
'@fingerprintjs/fingerprintjs@5.2.0':
|
||||
resolution: {integrity: sha512-j+2nInkwCQNTJcNhOjvkGM/nLRTuGJTC6xai4quqvUpjob2ssrGwBZjS7k55nOmKvge7qvJT2nS3i/IRvQSTQA==}
|
||||
|
||||
'@fontsource/cinzel@5.3.0':
|
||||
resolution: {integrity: sha512-KEOYTsrRppW0uRIPWBDPrQgunCS6f8u4nzZxYRBfo9SCknKojLYjK7B0ZoVmJhyVLefz271ZQRF9EOpHZlfhRw==}
|
||||
|
||||
'@fontsource/great-vibes@5.3.0':
|
||||
resolution: {integrity: sha512-1w60cXzXnC9V4y943po8044O1mqRQBiTTK75h7OpZE+1VQtx+KlAAzoIyEE5KNzu05gUpxaCQg7cbL7ITSwegQ==}
|
||||
|
||||
'@fontsource/liu-jian-mao-cao@5.3.0':
|
||||
resolution: {integrity: sha512-OaFm4okm77XoGA9jp1J89tDjdWlzb68yJCUL7LV75QxUWF/qOBRVqmhP7XFFsgcn/PL+nc2dPvRGT/8yit5LZg==}
|
||||
|
||||
'@fontsource/long-cang@5.3.0':
|
||||
resolution: {integrity: sha512-lCcBJb+LWZpYCVtGvQRiZPy5/EaGPuYLycpJXUIOz28EYGGqhTVWBRqY6ucaW8l1F8gvfjutY1PoAPUrFiP0wg==}
|
||||
|
||||
'@fontsource/ma-shan-zheng@5.3.0':
|
||||
resolution: {integrity: sha512-3p0aUEYOFBa4mvHIRDksZZ9Uf35jQdq5mpwp6NZHiNYt/jDj/g83OwTAWjzy+9ZJAxYC9NLLobIKuW1BEcnv0Q==}
|
||||
|
||||
'@fontsource/pinyon-script@5.3.0':
|
||||
resolution: {integrity: sha512-ilhy4l5apzyDkfuKb7PZBUT5Sf6nfxucdXT5TQ0wQDyWtDPlouLy1lQq3lMEPk1lPsHHVrd11EkPnEG8FqDgUA==}
|
||||
|
||||
'@fontsource/playfair-display@5.3.0':
|
||||
resolution: {integrity: sha512-uJSABalvdWc10fEPppQjQocamT21LEyRfzzWfRfqbkPz+sRv+kKpYDKyluBUskvJREkJTos9epj1WjQROGu99Q==}
|
||||
|
||||
'@fontsource/zhi-mang-xing@5.3.0':
|
||||
resolution: {integrity: sha512-V21yCCm5o2PHdXF2P9KKh7uiqL8QFFuII4DRQkpG24tfyDzJPchXKsxkcvKM43xEPvQKdbhHCBbXEFjcm/TUCQ==}
|
||||
|
||||
'@fortawesome/fontawesome-free@6.7.2':
|
||||
resolution: {integrity: sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1483,10 +1519,22 @@ snapshots:
|
||||
|
||||
'@fingerprintjs/fingerprintjs@5.2.0': {}
|
||||
|
||||
'@fontsource/cinzel@5.3.0': {}
|
||||
|
||||
'@fontsource/great-vibes@5.3.0': {}
|
||||
|
||||
'@fontsource/liu-jian-mao-cao@5.3.0': {}
|
||||
|
||||
'@fontsource/long-cang@5.3.0': {}
|
||||
|
||||
'@fontsource/ma-shan-zheng@5.3.0': {}
|
||||
|
||||
'@fontsource/pinyon-script@5.3.0': {}
|
||||
|
||||
'@fontsource/playfair-display@5.3.0': {}
|
||||
|
||||
'@fontsource/zhi-mang-xing@5.3.0': {}
|
||||
|
||||
'@fortawesome/fontawesome-free@6.7.2': {}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
|
||||
@@ -72,6 +72,10 @@ export function getVisitStats() {
|
||||
return service.get('/visit/stats')
|
||||
}
|
||||
|
||||
export function getVisitCityStats(region) {
|
||||
return service.get('/visit/stats/cities', { params: { region } })
|
||||
}
|
||||
|
||||
export function getVisitList(params) {
|
||||
return service.get('/visit/list', { params })
|
||||
}
|
||||
|
||||
@@ -197,6 +197,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 爱心拼接:绝对定位心形轮廓 + 串行挂载 -->
|
||||
<div v-else-if="page.type === 'heart-collage'" ref="multiRootRef" class="g-heart mt-6">
|
||||
<div
|
||||
v-for="(slot, imgIdx) in HEART_SLOTS"
|
||||
:key="imgIdx"
|
||||
class="g-heart-slot"
|
||||
:class="{ 'g-heart-placeholder': imgIdx >= shownCount }"
|
||||
:style="heartSlotStyle(slot)"
|
||||
>
|
||||
<div
|
||||
v-if="imgIdx < shownCount"
|
||||
v-reveal="{ variant: heartReveal(imgIdx), immediate: true }"
|
||||
class="absolute inset-0 g-heart-cell"
|
||||
:class="frameClass"
|
||||
>
|
||||
<img
|
||||
:src="displayAt(imgIdx)"
|
||||
class="preview-img g-heart-img"
|
||||
decoding="async"
|
||||
@click.stop="previewImage(originalAt(imgIdx))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-center text-[#8A8680] text-[12px] mt-6">未识别的版式:{{ page.type }}</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -236,8 +261,26 @@ const SEQUENTIAL_SELF_TYPES = new Set([
|
||||
'polaroid',
|
||||
'nine-grid',
|
||||
'one-large-five-small',
|
||||
'heart-collage',
|
||||
])
|
||||
|
||||
/** 爱心拼接槽位:百分比绝对定位,轮廓接近 ♥(顶开叉、中最宽、底收尖) */
|
||||
const HEART_SLOTS = [
|
||||
{ l: 10, t: 0, w: 32, h: 13 }, // 0 左上叶
|
||||
{ l: 58, t: 0, w: 32, h: 13 }, // 1 右上叶(中间约 16% 开叉)
|
||||
{ l: 0, t: 14, w: 14, h: 15.5 }, // 2 左外上
|
||||
{ l: 0, t: 30.5, w: 14, h: 15.5 }, // 3 左外下
|
||||
{ l: 15, t: 14, w: 26, h: 32 }, // 4 左大图
|
||||
{ l: 42, t: 14, w: 16, h: 32 }, // 5 中轴
|
||||
{ l: 59, t: 14, w: 26, h: 32 }, // 6 右大图
|
||||
{ l: 86, t: 14, w: 14, h: 15.5 }, // 7 右外上
|
||||
{ l: 86, t: 30.5, w: 14, h: 15.5 }, // 8 右外下
|
||||
{ l: 10, t: 47, w: 13, h: 16 }, // 9 左下收腰
|
||||
{ l: 24, t: 47, w: 52, h: 16 }, // 10 中下横
|
||||
{ l: 77, t: 47, w: 13, h: 16 }, // 11 右下收腰
|
||||
{ l: 38, t: 64, w: 24, h: 14 }, // 12 心尖
|
||||
]
|
||||
|
||||
const props = defineProps({
|
||||
page: { type: Object, required: true },
|
||||
/** 九宫格:开启串行 v-reveal;关闭则一次展示 */
|
||||
@@ -447,7 +490,7 @@ const runSequentialByType = async () => {
|
||||
await runOlfsSequential()
|
||||
return
|
||||
}
|
||||
if (type === 'masonry' || type === 'polaroid') {
|
||||
if (type === 'masonry' || type === 'polaroid' || type === 'heart-collage') {
|
||||
await runMultiSequential()
|
||||
}
|
||||
}
|
||||
@@ -608,6 +651,23 @@ function polaroidTransform(i) {
|
||||
return `rotate(${a}deg) translateY(${s}px)`
|
||||
}
|
||||
|
||||
/** 爱心:左右对称入场,中轴与心尖用缩放 */
|
||||
function heartReveal(i) {
|
||||
if (i === 5 || i === 12) return 'scale'
|
||||
if (i === 0 || i === 2 || i === 3 || i === 4 || i === 9) return 'left'
|
||||
if (i === 1 || i === 6 || i === 7 || i === 8 || i === 11) return 'right'
|
||||
return 'up'
|
||||
}
|
||||
|
||||
function heartSlotStyle(slot) {
|
||||
return {
|
||||
left: `${slot.l}%`,
|
||||
top: `${slot.t}%`,
|
||||
width: `${slot.w}%`,
|
||||
height: `${slot.h}%`,
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -706,6 +766,9 @@ function polaroidTransform(i) {
|
||||
max-width: 400px;
|
||||
height: clamp(340px, 48vh, 460px);
|
||||
}
|
||||
.g-heart {
|
||||
max-width: 400px;
|
||||
}
|
||||
.g-carousel-slide {
|
||||
height: min(52vh, 420px);
|
||||
}
|
||||
@@ -724,6 +787,75 @@ function polaroidTransform(i) {
|
||||
.g-dot { width: 7px; height: 7px; border-radius: 9999px; border: none; background: rgba(168,140,107,0.3); padding: 0; cursor: pointer; transition: all 0.3s; }
|
||||
.g-dot.active { background: #a88c6b; width: 18px; border-radius: 9999px; }
|
||||
|
||||
/* 爱心拼接:绝对定位块状心形 + 淡囍底纹 */
|
||||
.g-heart {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin-inline: auto;
|
||||
aspect-ratio: 0.8 / 1;
|
||||
padding: 4px 2px 8px;
|
||||
box-sizing: border-box;
|
||||
isolation: isolate;
|
||||
}
|
||||
.g-heart::before,
|
||||
.g-heart::after {
|
||||
content: '囍';
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
font-family: var(--font-calligraphy), serif;
|
||||
font-weight: 700;
|
||||
color: #c97878;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
line-height: 1;
|
||||
}
|
||||
.g-heart::before {
|
||||
top: 22%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: clamp(64px, 24vw, 108px);
|
||||
opacity: 0.07;
|
||||
}
|
||||
.g-heart::after {
|
||||
bottom: 10%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: clamp(36px, 12vw, 56px);
|
||||
opacity: 0.05;
|
||||
}
|
||||
.g-heart-slot {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
contain: layout paint;
|
||||
}
|
||||
.g-heart-placeholder {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.g-heart-cell {
|
||||
overflow: hidden;
|
||||
transition: transform 0.35s ease, box-shadow 0.35s ease;
|
||||
}
|
||||
@media (hover: hover) {
|
||||
.g-heart-cell:hover {
|
||||
transform: scale(1.03);
|
||||
z-index: 3;
|
||||
box-shadow: 0 6px 18px rgba(80, 50, 40, 0.16);
|
||||
}
|
||||
}
|
||||
.g-heart-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.g-polaroid { display: flex; flex-wrap: wrap; justify-content: center; gap: 14px 10px; padding: 14px 4px 4px; }
|
||||
.g-polaroid-slot {
|
||||
width: 42%;
|
||||
|
||||
@@ -238,6 +238,14 @@ export function getSlotResizeSpecs(page) {
|
||||
boxForIndex = () => ({ maxWidth: 1200, maxHeight: 780 })
|
||||
} else if (page.type === 'polaroid') {
|
||||
boxForIndex = () => ({ maxWidth: 800, maxHeight: 1000 })
|
||||
} else if (page.type === 'heart-collage') {
|
||||
// 按槽位比例:大图 / 竖中轴 / 横图 / 小方图
|
||||
boxForIndex = (i) => {
|
||||
if (i === 4 || i === 6) return { maxWidth: 900, maxHeight: 900 }
|
||||
if (i === 5) return { maxWidth: 560, maxHeight: 960 }
|
||||
if (i === 0 || i === 1 || i === 10) return { maxWidth: 900, maxHeight: 680 }
|
||||
return { maxWidth: 520, maxHeight: 520 }
|
||||
}
|
||||
}
|
||||
|
||||
return list.map((_, i) => {
|
||||
|
||||
@@ -150,8 +150,11 @@
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-5">
|
||||
<div class="chart-card">
|
||||
<div class="text-[12px] text-[#7a6550] mb-2 tracking-wide">地区分布</div>
|
||||
<div ref="regionChartRef" class="h-[280px] w-full"></div>
|
||||
<div class="flex items-baseline justify-between gap-2 mb-2">
|
||||
<div class="text-[12px] text-[#7a6550] tracking-wide">地区分布</div>
|
||||
<div class="text-[10px] text-[#a89a88]">点击省份查看市级</div>
|
||||
</div>
|
||||
<div ref="regionChartRef" class="h-[280px] w-full cursor-pointer"></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<div class="text-[12px] text-[#7a6550] mb-2 tracking-wide">机型画像</div>
|
||||
@@ -159,6 +162,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedRegion" class="chart-card mb-5">
|
||||
<div class="flex items-center justify-between gap-2 mb-2 flex-wrap">
|
||||
<div class="text-[12px] text-[#7a6550] tracking-wide">
|
||||
{{ selectedRegion }} · 市级分布
|
||||
<span v-if="cityStatsLoading" class="text-[10px] text-[#a89a88] ml-2">加载中…</span>
|
||||
</div>
|
||||
<a-button size="small" type="text" class="!text-[#a88c6b]" @click="clearSelectedRegion">
|
||||
<i class="fa-solid fa-xmark mr-1"></i>关闭
|
||||
</a-button>
|
||||
</div>
|
||||
<div ref="cityChartRef" class="h-[280px] w-full"></div>
|
||||
</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-[#a88c6b] !px-0" @click="activeTab = 'visits'">
|
||||
@@ -982,6 +998,7 @@
|
||||
<a-select-option value="masonry">瀑布流</a-select-option>
|
||||
<a-select-option value="carousel">轮播滑动</a-select-option>
|
||||
<a-select-option value="polaroid">拍立得拼贴</a-select-option>
|
||||
<a-select-option value="heart-collage">爱心拼接</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="页面高度">
|
||||
@@ -1078,9 +1095,12 @@
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="page.type === 'heart-collage'" class="w-full text-[11px] text-[#8A8680] mb-1">
|
||||
共 13 张:按心形槽位排列(上叶 → 中段对称 → 下横 → 心尖)。建议竖图放中轴,横图放上下叶。
|
||||
</div>
|
||||
<div v-for="(img, iidx) in page.images" :key="iidx" class="relative w-[96px]">
|
||||
<image-field v-model="page.images[iidx]"
|
||||
:label="`图${iidx+1}`"
|
||||
:label="heartSlotLabel(page.type, iidx)"
|
||||
@pick="openUpload(u => setOriginalImageAt(page, iidx, u))"
|
||||
:uploading="uploading" />
|
||||
<div v-if="page.imagesResized?.[iidx]" class="text-center mt-1">
|
||||
@@ -1436,7 +1456,7 @@ import { TooltipComponent, LegendComponent } from 'echarts/components'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import {
|
||||
getConfigSection, saveConfigSection, uploadImage, getRsvpList, deleteRsvp, getUploadConfig, saveUploadConfig,
|
||||
getDanmakuList, updateDanmakuStatus, getVisitStats, getVisitList,
|
||||
getDanmakuList, updateDanmakuStatus, getVisitStats, getVisitCityStats, getVisitList,
|
||||
getPosterConfig, savePosterConfig, getGiftStats, getGiftList, deleteGift, saveGiftCosts,
|
||||
getCashGiftList, updateCashGift, deleteCashGift, submitCashGift,
|
||||
getCashGiftNotes, saveCashGiftNotes,
|
||||
@@ -1649,8 +1669,13 @@ const visitPageSize = ref(20)
|
||||
const visitTotal = ref(0)
|
||||
const regionChartRef = ref(null)
|
||||
const deviceChartRef = ref(null)
|
||||
const cityChartRef = ref(null)
|
||||
let regionChart = null
|
||||
let deviceChart = null
|
||||
let cityChart = null
|
||||
const selectedRegion = ref('')
|
||||
const cityStats = ref([])
|
||||
const cityStatsLoading = ref(false)
|
||||
|
||||
const cfg = reactive(defaultConfig())
|
||||
const uploadCfg = reactive(defaultUploadConfig())
|
||||
@@ -1954,12 +1979,12 @@ function ensurePageShape(p) {
|
||||
delete p.imagesResized
|
||||
delete p.imagesResizedMeta
|
||||
} else {
|
||||
// 固定数量版式:一大五小(6) / 九宫格(9);柔性版式:瀑布流/轮播/拍立得,仅首次补齐
|
||||
const fixed = { 'one-large-five-small': 6, 'nine-grid': 9, 'masonry': 6, 'carousel': 5, 'polaroid': 6 }
|
||||
// 固定数量版式:一大五小(6) / 九宫格(9) / 爱心拼接(13);柔性版式:瀑布流/轮播/拍立得,仅首次补齐
|
||||
const fixed = { 'one-large-five-small': 6, 'nine-grid': 9, 'heart-collage': 13, 'masonry': 6, 'carousel': 5, 'polaroid': 6 }
|
||||
const need = fixed[p.type] || 6
|
||||
p.images = Array.isArray(p.images) ? p.images : []
|
||||
if (p.images.length === 0) while (p.images.length < need) p.images.push('')
|
||||
else if (['one-large-five-small', 'nine-grid'].includes(p.type)) {
|
||||
else if (['one-large-five-small', 'nine-grid', 'heart-collage'].includes(p.type)) {
|
||||
while (p.images.length < need) p.images.push('')
|
||||
if (p.images.length > need) p.images.length = need
|
||||
}
|
||||
@@ -1984,6 +2009,15 @@ function ensurePageShape(p) {
|
||||
function isFlexible(type) {
|
||||
return ['masonry', 'carousel', 'polaroid'].includes(type)
|
||||
}
|
||||
|
||||
const HEART_SLOT_LABELS = [
|
||||
'左上叶', '右上叶', '左外上', '左外下', '左大图', '中轴', '右大图',
|
||||
'右外上', '右外下', '左下', '中下横', '右下', '心尖',
|
||||
]
|
||||
function heartSlotLabel(type, i) {
|
||||
if (type === 'heart-collage' && HEART_SLOT_LABELS[i]) return HEART_SLOT_LABELS[i]
|
||||
return `图${i + 1}`
|
||||
}
|
||||
function addImage(page) {
|
||||
if (!Array.isArray(page.images)) page.images = []
|
||||
page.images.push('')
|
||||
@@ -2611,9 +2645,20 @@ function disposeVisitCharts() {
|
||||
deviceChart.dispose()
|
||||
deviceChart = null
|
||||
}
|
||||
if (cityChart) {
|
||||
cityChart.dispose()
|
||||
cityChart = null
|
||||
}
|
||||
}
|
||||
|
||||
function renderPieChart(chartRef, holder, seriesData, emptyName = '暂无数据') {
|
||||
function disposeCityChart() {
|
||||
if (cityChart) {
|
||||
cityChart.dispose()
|
||||
cityChart = null
|
||||
}
|
||||
}
|
||||
|
||||
function renderPieChart(chartRef, holder, seriesData, emptyName = '暂无数据', opts = {}) {
|
||||
if (!chartRef.value) return null
|
||||
if (holder && holder.getDom() !== chartRef.value) {
|
||||
holder.dispose()
|
||||
@@ -2621,7 +2666,11 @@ function renderPieChart(chartRef, holder, seriesData, emptyName = '暂无数据'
|
||||
}
|
||||
if (!holder) holder = echarts.init(chartRef.value)
|
||||
const data = seriesData.length
|
||||
? seriesData.map((r) => ({ name: r.name || '未知', value: Number(r.value) || 0 }))
|
||||
? seriesData.map((r) => ({
|
||||
name: r.name || '未知',
|
||||
value: Number(r.value) || 0,
|
||||
selected: opts.selectedName ? (r.name || '未知') === opts.selectedName : false,
|
||||
}))
|
||||
: [{ name: emptyName, value: 0 }]
|
||||
holder.setOption({
|
||||
color: PIE_COLORS,
|
||||
@@ -2635,11 +2684,13 @@ function renderPieChart(chartRef, holder, seriesData, emptyName = '暂无数据'
|
||||
type: 'pie',
|
||||
radius: ['38%', '62%'],
|
||||
center: ['50%', '46%'],
|
||||
selectedMode: opts.selectable ? 'single' : false,
|
||||
selectedOffset: 8,
|
||||
itemStyle: { borderRadius: 4, borderColor: '#fff', borderWidth: 2 },
|
||||
label: { color: '#5c5348', fontSize: 11 },
|
||||
data,
|
||||
}],
|
||||
})
|
||||
}, { notMerge: true })
|
||||
return holder
|
||||
}
|
||||
|
||||
@@ -2648,7 +2699,20 @@ function renderRegionChart() {
|
||||
regionChartRef,
|
||||
regionChart,
|
||||
Array.isArray(visitStats.regions) ? visitStats.regions : [],
|
||||
'暂无数据',
|
||||
{ selectable: true, selectedName: selectedRegion.value },
|
||||
)
|
||||
if (!regionChart) return
|
||||
regionChart.off('click')
|
||||
regionChart.on('click', (params) => {
|
||||
const name = params?.name
|
||||
if (!name || name === '暂无数据') return
|
||||
if (selectedRegion.value === name) {
|
||||
clearSelectedRegion()
|
||||
return
|
||||
}
|
||||
onSelectRegion(name)
|
||||
})
|
||||
}
|
||||
|
||||
function renderDeviceChart() {
|
||||
@@ -2659,6 +2723,42 @@ function renderDeviceChart() {
|
||||
)
|
||||
}
|
||||
|
||||
function renderCityChart() {
|
||||
cityChart = renderPieChart(
|
||||
cityChartRef,
|
||||
cityChart,
|
||||
Array.isArray(cityStats.value) ? cityStats.value : [],
|
||||
'暂无市级数据',
|
||||
)
|
||||
}
|
||||
|
||||
function clearSelectedRegion(opts = {}) {
|
||||
selectedRegion.value = ''
|
||||
cityStats.value = []
|
||||
disposeCityChart()
|
||||
if (!opts.silent && regionChart) renderRegionChart()
|
||||
}
|
||||
|
||||
async function onSelectRegion(name) {
|
||||
selectedRegion.value = name
|
||||
cityStatsLoading.value = true
|
||||
cityStats.value = []
|
||||
disposeCityChart()
|
||||
renderRegionChart()
|
||||
try {
|
||||
const res = await getVisitCityStats(name)
|
||||
const d = res.data || {}
|
||||
cityStats.value = Array.isArray(d.cities) ? d.cities : []
|
||||
await nextTick()
|
||||
renderCityChart()
|
||||
} catch (e) {
|
||||
message.error(e?.message || '加载市级分布失败')
|
||||
cityStats.value = []
|
||||
} finally {
|
||||
cityStatsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGiftStats() {
|
||||
giftStatsLoading.value = true
|
||||
try {
|
||||
@@ -2723,6 +2823,9 @@ async function loadVisitStats() {
|
||||
await nextTick()
|
||||
renderRegionChart()
|
||||
renderDeviceChart()
|
||||
if (selectedRegion.value) {
|
||||
await onSelectRegion(selectedRegion.value)
|
||||
}
|
||||
} catch (e) {
|
||||
message.error(e?.message || '加载访客统计失败')
|
||||
} finally {
|
||||
@@ -2773,7 +2876,10 @@ function onVisitTableChange(pag) {
|
||||
|
||||
async function onTabActivate(tab) {
|
||||
if (!adminStore.isAdmin) return
|
||||
if (tab !== 'overview') disposeVisitCharts()
|
||||
if (tab !== 'overview') {
|
||||
clearSelectedRegion({ silent: true })
|
||||
disposeVisitCharts()
|
||||
}
|
||||
if (tab === 'overview') {
|
||||
await loadOverview()
|
||||
return
|
||||
@@ -3084,6 +3190,7 @@ watch(activeTab, (tab) => {
|
||||
const onResize = () => {
|
||||
regionChart?.resize()
|
||||
deviceChart?.resize()
|
||||
cityChart?.resize()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
Reference in New Issue
Block a user