1. 迎宾海报

This commit is contained in:
李琦
2026-08-04 10:26:00 +08:00
parent 05ff2cc7aa
commit 5a25304774
7 changed files with 929 additions and 29 deletions

View File

@@ -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(() => {