mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
- 将 CookieCloud 同步间隔从 30 分钟缩短至 10 分钟,提高同步频率 - 在 BiliCookie store 初始化时立即执行一次检查,强制尝试从 CookieCloud 同步 - 新增弹幕客户端启动失败时的错误通知提示 - 在管理页面加载时检测身份码失效状态并显示错误提示 - 优化邮箱验证状态检查时机,延迟 500ms 执行避免初始化冲突 - 自动注册 NTable 组件到全
767 lines
24 KiB
Vue
767 lines
24 KiB
Vue
<script lang="ts" setup>
|
||
import {
|
||
BarChartOutline,
|
||
ChatbubblesOutline,
|
||
PeopleOutline,
|
||
RefreshOutline,
|
||
TimeOutline,
|
||
TrendingDown,
|
||
TrendingUp,
|
||
WalletOutline,
|
||
} from '@vicons/ionicons5'
|
||
import { BarChart, LineChart } from 'echarts/charts'
|
||
import {
|
||
DataZoomComponent,
|
||
GridComponent,
|
||
LegendComponent,
|
||
MarkLineComponent,
|
||
MarkPointComponent,
|
||
TitleComponent,
|
||
TooltipComponent,
|
||
} from 'echarts/components'
|
||
import * as echarts from 'echarts/core'
|
||
import { CanvasRenderer } from 'echarts/renderers'
|
||
import {
|
||
NButton,
|
||
NCard,
|
||
NCheckbox,
|
||
NCheckboxGroup,
|
||
NDescriptions,
|
||
NDescriptionsItem,
|
||
NEmpty,
|
||
NGrid,
|
||
NGridItem,
|
||
NIcon,
|
||
NNumberAnimation,
|
||
NProgress,
|
||
NSkeleton,
|
||
NSpace,
|
||
NTag,
|
||
NTime,
|
||
NTooltip,
|
||
useMessage,
|
||
useThemeVars,
|
||
} from 'naive-ui'
|
||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||
import { QueryGetAPI } from '@/api/query'
|
||
import { ANALYZE_API_URL } from '@/data/constants'
|
||
|
||
// 注册必要的组件
|
||
echarts.use([
|
||
TitleComponent,
|
||
TooltipComponent,
|
||
GridComponent,
|
||
LegendComponent,
|
||
LineChart,
|
||
BarChart,
|
||
CanvasRenderer,
|
||
MarkPointComponent,
|
||
MarkLineComponent,
|
||
DataZoomComponent,
|
||
])
|
||
|
||
// types.ts
|
||
interface ChartItem {
|
||
income: number
|
||
interactionCount: number
|
||
danmakuCount: number
|
||
payingUsers: number
|
||
interactionUsers: number
|
||
liveMinutes: number
|
||
likeCount: number
|
||
}
|
||
|
||
interface Summary {
|
||
last7Days: {
|
||
totalIncome: number
|
||
totalInteractions: number
|
||
totalDanmakuCount: number
|
||
totalLiveMinutes: number
|
||
dailyAvgIncome: number
|
||
dailyAvgDanmaku: number
|
||
incomeTrend: number
|
||
interactionTrend: number
|
||
danmakuTrend: number
|
||
activeLiveDays: number
|
||
interactionUsers: number
|
||
payingUsers: number
|
||
interactionUsersTrend: number
|
||
payingUsersTrend: number
|
||
}
|
||
last30Days: {
|
||
totalIncome: number
|
||
totalInteractions: number
|
||
totalDanmakuCount: number
|
||
totalLiveMinutes: number
|
||
dailyAvgIncome: number
|
||
dailyAvgDanmaku: number
|
||
incomeTrend: number
|
||
interactionTrend: number
|
||
danmakuTrend: number
|
||
activeLiveDays: number
|
||
interactionUsers: number
|
||
payingUsers: number
|
||
}
|
||
}
|
||
|
||
interface AnalyzeData {
|
||
summary: Summary
|
||
chartData: Record<number, ChartItem> // 键是Unix时间戳,值是对应的数据
|
||
}
|
||
|
||
// 状态管理
|
||
const loading = ref(true)
|
||
const refreshing = ref(false)
|
||
const message = useMessage()
|
||
const analyzeData = ref<AnalyzeData>()
|
||
const summaryData = computed(() => analyzeData.value?.summary)
|
||
const themeVars = useThemeVars()
|
||
const lastUpdateTime = ref<number>(0)
|
||
const hasData = computed(() => analyzeData.value && Object.keys(analyzeData.value.chartData || {}).length > 0)
|
||
|
||
// 图表配置状态
|
||
const chartRef = ref<HTMLElement | null>(null)
|
||
let mainChart: echarts.ECharts | null = null
|
||
const selectedMetrics = ref<string[]>(['income', 'interactionCount'])
|
||
const chartMetrics = [
|
||
{ label: '收入', value: 'income', color: '#f5a623', type: 'line', yAxisIndex: 1 },
|
||
{ label: '互动数', value: 'interactionCount', color: '#2080f0', type: 'line', yAxisIndex: 0 },
|
||
{ label: '弹幕数', value: 'danmakuCount', color: '#18a058', type: 'line', yAxisIndex: 0 },
|
||
{ label: '点赞数', value: 'likeCount', color: '#d03050', type: 'line', yAxisIndex: 0 },
|
||
{ label: '互动人数', value: 'interactionUsers', color: '#8a2be2', type: 'bar', yAxisIndex: 0 },
|
||
{ label: '付费人数', value: 'payingUsers', color: '#ff69b4', type: 'bar', yAxisIndex: 0 },
|
||
]
|
||
|
||
// 格式化工具函数
|
||
function formatCurrency(value: number): string {
|
||
return `¥${value.toFixed(2)}`
|
||
}
|
||
|
||
function formatNumber(value: number): string {
|
||
return value.toLocaleString()
|
||
}
|
||
|
||
function getTrendType(value: number): 'success' | 'error' | 'info' {
|
||
if (value > 0) return 'success'
|
||
if (value < 0) return 'error'
|
||
return 'info'
|
||
}
|
||
|
||
// 格式化时间戳为日期
|
||
function formatDate(timestamp: number): string {
|
||
const date = new Date(timestamp * 1000)
|
||
return `${date.getMonth() + 1}/${date.getDate()}`
|
||
}
|
||
|
||
// 从ChartData对象转换为数组,并按时间戳排序
|
||
function getChartDataArray() {
|
||
if (!analyzeData.value?.chartData) return []
|
||
|
||
return Object.entries(analyzeData.value.chartData)
|
||
.map(([timestamp, data]) => ({
|
||
timestamp: Number.parseInt(timestamp, 10),
|
||
date: formatDate(Number.parseInt(timestamp, 10)),
|
||
...data,
|
||
}))
|
||
.sort((a, b) => a.timestamp - b.timestamp)
|
||
}
|
||
|
||
// 获取主题色配置
|
||
function getThemeColors() {
|
||
return {
|
||
textColor: themeVars.value.textColor2,
|
||
axisLineColor: themeVars.value.borderColor,
|
||
splitLineColor: themeVars.value.dividerColor,
|
||
cardColor: themeVars.value.cardColor,
|
||
}
|
||
}
|
||
|
||
// 初始化图表
|
||
function initChart() {
|
||
if (!chartRef.value) return
|
||
const chartData = getChartDataArray()
|
||
if (chartData.length === 0) return
|
||
|
||
mainChart = echarts.init(chartRef.value)
|
||
updateChartOption()
|
||
}
|
||
|
||
// 更新图表配置
|
||
function updateChartOption() {
|
||
if (!mainChart) return
|
||
const chartData = getChartDataArray()
|
||
const dates = chartData.map(item => item.date)
|
||
const themeColors = getThemeColors()
|
||
|
||
// 确定哪些 Y 轴需要显示
|
||
const showRightAxis = selectedMetrics.value.includes('income')
|
||
const showLeftAxis = selectedMetrics.value.some(m => m !== 'income')
|
||
|
||
const series = selectedMetrics.value.map((metricKey) => {
|
||
const metricConfig = chartMetrics.find(m => m.value === metricKey)
|
||
if (!metricConfig) return null
|
||
|
||
return {
|
||
name: metricConfig.label,
|
||
type: metricConfig.type,
|
||
data: chartData.map(item => (item as any)[metricKey]),
|
||
smooth: true,
|
||
yAxisIndex: (metricKey === 'income' && showLeftAxis) ? 1 : 0, // 如果同时显示左轴,收入走右轴;否则走左轴
|
||
itemStyle: {
|
||
color: metricConfig.color,
|
||
},
|
||
areaStyle: metricConfig.type === 'line' ? {
|
||
opacity: 0.1,
|
||
color: metricConfig.color,
|
||
} : undefined,
|
||
barMaxWidth: metricConfig.type === 'bar' ? '20%' : undefined,
|
||
}
|
||
}).filter(Boolean)
|
||
|
||
const yAxis = []
|
||
|
||
// 左轴 (默认)
|
||
if (showLeftAxis) {
|
||
yAxis.push({
|
||
type: 'value',
|
||
position: 'left',
|
||
name: '数量',
|
||
axisLine: { show: true, lineStyle: { color: themeColors.axisLineColor } },
|
||
axisLabel: { color: themeColors.textColor },
|
||
splitLine: { lineStyle: { color: themeColors.splitLineColor } },
|
||
nameTextStyle: { color: themeColors.textColor },
|
||
})
|
||
} else if (showRightAxis) {
|
||
// 只有金额,显示在左边
|
||
yAxis.push({
|
||
type: 'value',
|
||
position: 'left',
|
||
name: '金额',
|
||
axisLine: { show: true, lineStyle: { color: themeColors.axisLineColor } },
|
||
axisLabel: { color: themeColors.textColor },
|
||
splitLine: { lineStyle: { color: themeColors.splitLineColor } },
|
||
nameTextStyle: { color: themeColors.textColor },
|
||
})
|
||
}
|
||
|
||
// 右轴 (仅当同时显示数量和金额时)
|
||
if (showLeftAxis && showRightAxis) {
|
||
yAxis.push({
|
||
type: 'value',
|
||
position: 'right',
|
||
name: '金额',
|
||
axisLine: { show: true, lineStyle: { color: themeColors.axisLineColor } },
|
||
axisLabel: { color: themeColors.textColor },
|
||
splitLine: { show: false },
|
||
nameTextStyle: { color: themeColors.textColor },
|
||
})
|
||
}
|
||
|
||
const option = {
|
||
backgroundColor: 'transparent',
|
||
textStyle: {
|
||
color: themeColors.textColor,
|
||
},
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: themeVars.value.cardColor,
|
||
borderColor: themeVars.value.borderColor,
|
||
textStyle: { color: themeColors.textColor },
|
||
axisPointer: { type: 'shadow' },
|
||
},
|
||
legend: {
|
||
data: series.map(s => s!.name),
|
||
textStyle: { color: themeColors.textColor },
|
||
bottom: 0,
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '3%',
|
||
bottom: '10%',
|
||
top: '10%',
|
||
containLabel: true,
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: dates,
|
||
axisLine: { lineStyle: { color: themeColors.axisLineColor } },
|
||
axisLabel: { color: themeColors.textColor },
|
||
},
|
||
yAxis: yAxis,
|
||
series: series,
|
||
dataZoom: [
|
||
{
|
||
type: 'inside',
|
||
start: 0,
|
||
end: 100,
|
||
},
|
||
{
|
||
show: true,
|
||
type: 'slider',
|
||
bottom: 35,
|
||
height: 20,
|
||
start: 0,
|
||
end: 100,
|
||
borderColor: themeColors.axisLineColor,
|
||
textStyle: { color: themeColors.textColor },
|
||
},
|
||
],
|
||
}
|
||
|
||
mainChart.setOption(option, true)
|
||
}
|
||
|
||
// 监听指标选择变化
|
||
watch(selectedMetrics, () => {
|
||
updateChartOption()
|
||
})
|
||
|
||
// 获取分析数据
|
||
async function fetchAnalyzeData(isRefresh = false) {
|
||
try {
|
||
if (isRefresh) {
|
||
refreshing.value = true
|
||
} else {
|
||
loading.value = true
|
||
}
|
||
const data = await QueryGetAPI<AnalyzeData>(`${ANALYZE_API_URL}all`)
|
||
|
||
if (data.code === 200) {
|
||
analyzeData.value = data.data
|
||
lastUpdateTime.value = Date.now()
|
||
nextTick(() => initChart())
|
||
if (isRefresh) {
|
||
message.success('数据已刷新')
|
||
}
|
||
} else {
|
||
message.error(`获取数据失败: ${data.message}`)
|
||
}
|
||
} catch (error) {
|
||
message.error(`获取数据出错:${(error as Error).message}`)
|
||
console.error('获取数据出错:', error)
|
||
} finally {
|
||
loading.value = false
|
||
refreshing.value = false
|
||
}
|
||
}
|
||
|
||
// 刷新数据
|
||
function handleRefresh() {
|
||
fetchAnalyzeData(true)
|
||
}
|
||
|
||
// 窗口大小变化时重绘图表
|
||
function handleResize() {
|
||
mainChart?.resize()
|
||
}
|
||
|
||
// 监听主题变化
|
||
watch(() => themeVars.value, () => {
|
||
nextTick(() => updateChartOption())
|
||
}, { deep: true })
|
||
|
||
// 组件挂载时初始化
|
||
onMounted(() => {
|
||
fetchAnalyzeData()
|
||
window.addEventListener('resize', handleResize)
|
||
})
|
||
|
||
// 组件卸载时清理
|
||
onUnmounted(() => {
|
||
window.removeEventListener('resize', handleResize)
|
||
mainChart?.dispose()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="analyze-container">
|
||
<!-- 顶部操作栏 -->
|
||
<NSpace align="center" justify="space-between" class="header-actions">
|
||
<NSpace align="center">
|
||
<h2 style="margin: 0; font-weight: 500; font-size: 20px;">
|
||
<NIcon :component="BarChartOutline" style="vertical-align: middle; margin-right: 8px;" />
|
||
数据分析
|
||
</h2>
|
||
</NSpace>
|
||
<NSpace align="center">
|
||
<EventFetcherAlert />
|
||
<NTooltip v-if="lastUpdateTime > 0">
|
||
<template #trigger>
|
||
<NTag size="small" :bordered="false">
|
||
<NIcon :component="TrendingUp" style="margin-right: 4px;" />
|
||
<NTime :time="lastUpdateTime" type="relative" />更新
|
||
</NTag>
|
||
</template>
|
||
<NTime :time="lastUpdateTime" />
|
||
</NTooltip>
|
||
<NButton :loading="refreshing" :disabled="loading" size="small" @click="handleRefresh">
|
||
<template #icon>
|
||
<NIcon :component="RefreshOutline" />
|
||
</template>
|
||
刷新
|
||
</NButton>
|
||
</NSpace>
|
||
</NSpace>
|
||
|
||
<!-- 加载骨架屏 -->
|
||
<div v-if="loading" class="skeleton-container">
|
||
<NGrid cols="1 800:2 1200:4" :x-gap="16" :y-gap="16">
|
||
<NGridItem v-for="i in 4" :key="i">
|
||
<NCard size="small">
|
||
<NSkeleton text width="40%" style="margin-bottom: 12px;" />
|
||
<NSkeleton text height="30px" width="80%" />
|
||
</NCard>
|
||
</NGridItem>
|
||
</NGrid>
|
||
<div style="margin-top: 20px;">
|
||
<NSkeleton height="400px" width="100%" border-radius="8px" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 空状态 -->
|
||
<NEmpty v-else-if="!hasData" description="暂无数据" size="large" style="margin: 60px 0">
|
||
<template #extra>
|
||
<NButton @click="() => fetchAnalyzeData()">重新加载</NButton>
|
||
</template>
|
||
</NEmpty>
|
||
|
||
<!-- 数据展示 -->
|
||
<template v-else>
|
||
<!-- 核心指标卡片 -->
|
||
<div class="core-metrics">
|
||
<NGrid cols="1 600:2 1000:4" :x-gap="16" :y-gap="16">
|
||
<!-- 收入 -->
|
||
<NGridItem>
|
||
<NCard size="small" class="metric-card income-card">
|
||
<div class="metric-content">
|
||
<div class="metric-header">
|
||
<span class="metric-label">近30天收入</span>
|
||
<NIcon :component="WalletOutline" class="metric-icon" />
|
||
</div>
|
||
<div class="metric-value">
|
||
<NNumberAnimation :from="0" :to="summaryData?.last30Days?.totalIncome || 0" :precision="2" />
|
||
<span class="currency-symbol">¥</span>
|
||
</div>
|
||
<div class="metric-footer">
|
||
<div class="trend-info">
|
||
<span :class="getTrendType(summaryData?.last30Days?.incomeTrend || 0)">
|
||
<NIcon :component="(summaryData?.last30Days?.incomeTrend || 0) >= 0 ? TrendingUp : TrendingDown" />
|
||
{{ Math.abs(summaryData?.last30Days?.incomeTrend || 0) }}%
|
||
</span>
|
||
<span class="trend-label">环比</span>
|
||
</div>
|
||
<div class="sub-stat">
|
||
日均 ¥{{ (summaryData?.last30Days?.dailyAvgIncome || 0).toFixed(0) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</NCard>
|
||
</NGridItem>
|
||
|
||
<!-- 互动 -->
|
||
<NGridItem>
|
||
<NCard size="small" class="metric-card interaction-card">
|
||
<div class="metric-content">
|
||
<div class="metric-header">
|
||
<span class="metric-label">近30天互动</span>
|
||
<NIcon :component="ChatbubblesOutline" class="metric-icon" />
|
||
</div>
|
||
<div class="metric-value">
|
||
<NNumberAnimation :from="0" :to="summaryData?.last30Days?.totalInteractions || 0" show-separator />
|
||
</div>
|
||
<div class="metric-footer">
|
||
<div class="trend-info">
|
||
<span :class="getTrendType(summaryData?.last30Days?.interactionTrend || 0)">
|
||
<NIcon :component="(summaryData?.last30Days?.interactionTrend || 0) >= 0 ? TrendingUp : TrendingDown" />
|
||
{{ Math.abs(summaryData?.last30Days?.interactionTrend || 0) }}%
|
||
</span>
|
||
<span class="trend-label">环比</span>
|
||
</div>
|
||
<div class="sub-stat">
|
||
{{ formatNumber(summaryData?.last30Days?.totalDanmakuCount || 0) }} 弹幕
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</NCard>
|
||
</NGridItem>
|
||
|
||
<!-- 用户 -->
|
||
<NGridItem>
|
||
<NCard size="small" class="metric-card users-card">
|
||
<div class="metric-content">
|
||
<div class="metric-header">
|
||
<span class="metric-label">互动/付费人数</span>
|
||
<NIcon :component="PeopleOutline" class="metric-icon" />
|
||
</div>
|
||
<div class="metric-value">
|
||
<span>{{ formatNumber(summaryData?.last30Days?.interactionUsers || 0) }}</span>
|
||
<span class="separator">/</span>
|
||
<span class="highlight">{{ formatNumber(summaryData?.last30Days?.payingUsers || 0) }}</span>
|
||
</div>
|
||
<div class="metric-footer">
|
||
<div class="trend-info">
|
||
<NProgress
|
||
type="line"
|
||
:percentage="Math.min(100, Math.round(((summaryData?.last30Days?.payingUsers || 0) / (summaryData?.last30Days?.interactionUsers || 1) * 100) * 10) / 10)"
|
||
:height="6"
|
||
color="#ff69b4"
|
||
rail-color="rgba(255, 105, 180, 0.2)"
|
||
:show-indicator="false"
|
||
style="width: 60px; margin-right: 8px;"
|
||
/>
|
||
<span class="trend-label">{{ ((summaryData?.last30Days?.payingUsers || 0) / (summaryData?.last30Days?.interactionUsers || 1) * 100).toFixed(1) }}% 付费率</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</NCard>
|
||
</NGridItem>
|
||
|
||
<!-- 直播 -->
|
||
<NGridItem>
|
||
<NCard size="small" class="metric-card time-card">
|
||
<div class="metric-content">
|
||
<div class="metric-header">
|
||
<span class="metric-label">近30天直播</span>
|
||
<NIcon :component="TimeOutline" class="metric-icon" />
|
||
</div>
|
||
<div class="metric-value">
|
||
{{ ((summaryData?.last30Days?.totalLiveMinutes || 0) / 60).toFixed(1) }}
|
||
<span class="unit">小时</span>
|
||
</div>
|
||
<div class="metric-footer">
|
||
<div class="trend-info">
|
||
<span class="trend-label">活跃 {{ summaryData?.last30Days?.activeLiveDays || 0 }} 天</span>
|
||
</div>
|
||
<div class="sub-stat">
|
||
场均 {{ ((summaryData?.last30Days?.totalLiveMinutes || 0) / (summaryData?.last30Days?.activeLiveDays || 1) / 60).toFixed(1) }}h
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</NCard>
|
||
</NGridItem>
|
||
</NGrid>
|
||
</div>
|
||
|
||
<!-- 图表区域 -->
|
||
<NCard size="small" title="趋势分析" class="chart-card" style="margin-top: 16px;">
|
||
<template #header-extra>
|
||
<NCheckboxGroup v-model:value="selectedMetrics">
|
||
<NSpace>
|
||
<NCheckbox v-for="metric in chartMetrics" :key="metric.value" :value="metric.value">
|
||
<span :style="{ color: metric.color }">{{ metric.label }}</span>
|
||
</NCheckbox>
|
||
</NSpace>
|
||
</NCheckboxGroup>
|
||
</template>
|
||
<div ref="chartRef" class="main-chart" />
|
||
</NCard>
|
||
|
||
<!-- 详细数据对比 -->
|
||
<div class="details-section" style="margin-top: 16px;">
|
||
<NGrid cols="1 900:2" :x-gap="16" :y-gap="16">
|
||
<NGridItem>
|
||
<NCard size="small" title="近7天详细数据">
|
||
<template #header-extra>
|
||
<NTag type="info" size="small" :bordered="false">短期表现</NTag>
|
||
</template>
|
||
<NDescriptions label-placement="left" :column="2" bordered>
|
||
<NDescriptionsItem label="总收入">{{ formatCurrency(summaryData?.last7Days?.totalIncome || 0) }}</NDescriptionsItem>
|
||
<NDescriptionsItem label="日均收入">{{ formatCurrency(summaryData?.last7Days?.dailyAvgIncome || 0) }}</NDescriptionsItem>
|
||
<NDescriptionsItem label="总互动">{{ formatNumber(summaryData?.last7Days?.totalInteractions || 0) }}</NDescriptionsItem>
|
||
<NDescriptionsItem label="弹幕数">{{ formatNumber(summaryData?.last7Days?.totalDanmakuCount || 0) }}</NDescriptionsItem>
|
||
<NDescriptionsItem label="直播时长">{{ ((summaryData?.last7Days?.totalLiveMinutes || 0) / 60).toFixed(1) }}h</NDescriptionsItem>
|
||
<NDescriptionsItem label="活跃天数">{{ summaryData?.last7Days?.activeLiveDays || 0 }}天</NDescriptionsItem>
|
||
<NDescriptionsItem label="互动人数">{{ formatNumber(summaryData?.last7Days?.interactionUsers || 0) }}</NDescriptionsItem>
|
||
<NDescriptionsItem label="付费人数">{{ formatNumber(summaryData?.last7Days?.payingUsers || 0) }}</NDescriptionsItem>
|
||
</NDescriptions>
|
||
|
||
<div class="mini-funnel" style="margin-top: 16px;">
|
||
<div class="funnel-row">
|
||
<span class="label">互动转化</span>
|
||
<NProgress
|
||
type="line"
|
||
:percentage="Math.min(100, Math.round(((summaryData?.last7Days?.payingUsers || 0) / (summaryData?.last7Days?.interactionUsers || 1) * 100) * 10) / 10)"
|
||
:height="12"
|
||
color="#18a058"
|
||
rail-color="rgba(24, 160, 88, 0.1)"
|
||
>
|
||
{{ ((summaryData?.last7Days?.payingUsers || 0) / (summaryData?.last7Days?.interactionUsers || 1) * 100).toFixed(1) }}%
|
||
</NProgress>
|
||
</div>
|
||
</div>
|
||
</NCard>
|
||
</NGridItem>
|
||
|
||
<NGridItem>
|
||
<NCard size="small" title="近30天详细数据">
|
||
<template #header-extra>
|
||
<NTag type="warning" size="small" :bordered="false">中期表现</NTag>
|
||
</template>
|
||
<NDescriptions label-placement="left" :column="2" bordered>
|
||
<NDescriptionsItem label="总收入">{{ formatCurrency(summaryData?.last30Days?.totalIncome || 0) }}</NDescriptionsItem>
|
||
<NDescriptionsItem label="日均收入">{{ formatCurrency(summaryData?.last30Days?.dailyAvgIncome || 0) }}</NDescriptionsItem>
|
||
<NDescriptionsItem label="总互动">{{ formatNumber(summaryData?.last30Days?.totalInteractions || 0) }}</NDescriptionsItem>
|
||
<NDescriptionsItem label="弹幕数">{{ formatNumber(summaryData?.last30Days?.totalDanmakuCount || 0) }}</NDescriptionsItem>
|
||
<NDescriptionsItem label="直播时长">{{ ((summaryData?.last30Days?.totalLiveMinutes || 0) / 60).toFixed(1) }}h</NDescriptionsItem>
|
||
<NDescriptionsItem label="活跃天数">{{ summaryData?.last30Days?.activeLiveDays || 0 }}天</NDescriptionsItem>
|
||
<NDescriptionsItem label="互动人数">{{ formatNumber(summaryData?.last30Days?.interactionUsers || 0) }}</NDescriptionsItem>
|
||
<NDescriptionsItem label="付费人数">{{ formatNumber(summaryData?.last30Days?.payingUsers || 0) }}</NDescriptionsItem>
|
||
</NDescriptions>
|
||
|
||
<div class="mini-funnel" style="margin-top: 16px;">
|
||
<div class="funnel-row">
|
||
<span class="label">互动转化</span>
|
||
<NProgress
|
||
type="line"
|
||
:percentage="Math.min(100, Math.round(((summaryData?.last30Days?.payingUsers || 0) / (summaryData?.last30Days?.interactionUsers || 1) * 100) * 10) / 10)"
|
||
:height="12"
|
||
color="#f5a623"
|
||
rail-color="rgba(245, 166, 35, 0.1)"
|
||
>
|
||
{{ ((summaryData?.last30Days?.payingUsers || 0) / (summaryData?.last30Days?.interactionUsers || 1) * 100).toFixed(1) }}%
|
||
</NProgress>
|
||
</div>
|
||
</div>
|
||
</NCard>
|
||
</NGridItem>
|
||
</NGrid>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.analyze-container {
|
||
width: 100%;
|
||
max-width: 1600px;
|
||
margin: 0 auto;
|
||
padding: 0 4px;
|
||
}
|
||
|
||
.header-actions {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.metric-card {
|
||
height: 100%;
|
||
transition: all 0.3s ease;
|
||
border: 1px solid transparent;
|
||
}
|
||
|
||
.metric-card:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.metric-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.metric-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
color: var(--n-text-color-3);
|
||
}
|
||
|
||
.metric-label {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.metric-icon {
|
||
font-size: 16px;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.metric-value {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
line-height: 1.2;
|
||
display: flex;
|
||
align-items: baseline;
|
||
}
|
||
|
||
.currency-symbol {
|
||
font-size: 14px;
|
||
margin-left: 4px;
|
||
font-weight: normal;
|
||
color: var(--n-text-color-3);
|
||
}
|
||
|
||
.unit {
|
||
font-size: 14px;
|
||
margin-left: 4px;
|
||
font-weight: normal;
|
||
color: var(--n-text-color-3);
|
||
}
|
||
|
||
.separator {
|
||
margin: 0 4px;
|
||
color: var(--n-text-color-3);
|
||
font-size: 16px;
|
||
}
|
||
|
||
.highlight {
|
||
color: #ff69b4;
|
||
}
|
||
|
||
.metric-footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-top: 4px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.trend-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.success { color: var(--n-success-color); }
|
||
.error { color: var(--n-error-color); }
|
||
.info { color: var(--n-text-color-3); }
|
||
|
||
.trend-label {
|
||
color: var(--n-text-color-3);
|
||
}
|
||
|
||
.sub-stat {
|
||
color: var(--n-text-color-3);
|
||
}
|
||
|
||
/* 特定卡片样式微调 */
|
||
.income-card .metric-icon { color: #f5a623; }
|
||
.interaction-card .metric-icon { color: #2080f0; }
|
||
.users-card .metric-icon { color: #ff69b4; }
|
||
.time-card .metric-icon { color: #18a058; }
|
||
|
||
.chart-card {
|
||
min-height: 400px;
|
||
}
|
||
|
||
.main-chart {
|
||
width: 100%;
|
||
height: 400px;
|
||
}
|
||
|
||
.mini-funnel {
|
||
padding: 0 8px;
|
||
}
|
||
|
||
.funnel-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.funnel-row .label {
|
||
font-size: 12px;
|
||
color: var(--n-text-color-3);
|
||
width: 60px;
|
||
}
|
||
</style>
|