980 lines
27 KiB
Vue
980 lines
27 KiB
Vue
<template>
|
||
<div class="dev-page">
|
||
<!-- 顶部横幅 -->
|
||
<div class="analytics-hero">
|
||
<div class="analytics-hero-content">
|
||
<h1 class="analytics-hero-title">📊 数据统计</h1>
|
||
<p class="analytics-hero-desc">实时监控应用安装量、用户活跃、收入趋势等核心指标。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="analytics-body">
|
||
<!-- 时间筛选 -->
|
||
<div class="filter-bar">
|
||
<a-segmented
|
||
v-model:value="dateRange"
|
||
:options="dateRangeOptions"
|
||
@change="onDateRangeChange"
|
||
/>
|
||
<a-space class="ml-auto">
|
||
<a-button :loading="loading" @click="refresh">刷新</a-button>
|
||
<a-button @click="exportReport">导出报告</a-button>
|
||
</a-space>
|
||
</div>
|
||
|
||
<!-- 核心指标卡片 -->
|
||
<a-row :gutter="[16, 16]" class="mb-6">
|
||
<a-col :xs="12" :md="6" v-for="metric in coreMetrics" :key="metric.label">
|
||
<div class="metric-card" :class="metric.color">
|
||
<div class="metric-header">
|
||
<span class="metric-icon">{{ metric.icon }}</span>
|
||
<a-tooltip :title="metric.tooltip">
|
||
<span class="metric-trend" :class="metric.trendDir">
|
||
{{ metric.trendDir === 'up' ? '↑' : metric.trendDir === 'down' ? '↓' : '-' }}
|
||
{{ metric.trend }}
|
||
</span>
|
||
</a-tooltip>
|
||
</div>
|
||
<div class="metric-value">{{ metric.value }}</div>
|
||
<div class="metric-label">{{ metric.label }}</div>
|
||
</div>
|
||
</a-col>
|
||
</a-row>
|
||
|
||
<a-row :gutter="[16, 16]">
|
||
<!-- 安装量趋势 -->
|
||
<a-col :xs="24" :lg="16">
|
||
<div class="panel">
|
||
<div class="panel-header">
|
||
<span class="panel-title">📈 安装量趋势</span>
|
||
<a-radio-group v-model:value="installChartType" size="small">
|
||
<a-radio-button value="line">折线图</a-radio-button>
|
||
<a-radio-button value="bar">柱状图</a-radio-button>
|
||
</a-radio-group>
|
||
</div>
|
||
<div class="chart-area">
|
||
<div class="chart-placeholder">
|
||
<div class="chart-bar-group">
|
||
<div
|
||
v-for="(bar, i) in installData"
|
||
:key="i"
|
||
class="chart-bar-col"
|
||
>
|
||
<div
|
||
class="chart-bar"
|
||
:style="{ height: bar.percent + '%' }"
|
||
:class="{ active: bar.highlight }"
|
||
/>
|
||
<span class="chart-bar-label">{{ bar.label }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="chart-legend">
|
||
<span class="legend-dot green" /> 新安装
|
||
<span class="legend-dot blue ml-4" /> 累计安装
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-col>
|
||
|
||
<!-- 收入概览 -->
|
||
<a-col :xs="24" :lg="8">
|
||
<div class="panel">
|
||
<div class="panel-header">
|
||
<span class="panel-title">💰 收入概览</span>
|
||
<a-tag color="green">本月</a-tag>
|
||
</div>
|
||
<div class="revenue-content">
|
||
<div class="revenue-total">
|
||
<div class="revenue-amount">¥{{ revenueData.total }}</div>
|
||
<div class="revenue-label">本月总收入</div>
|
||
</div>
|
||
<a-divider style="margin: 16px 0" />
|
||
<div class="revenue-items">
|
||
<div v-for="item in revenueData.items" :key="item.label" class="revenue-item">
|
||
<div class="revenue-item-left">
|
||
<span class="revenue-item-icon">{{ item.icon }}</span>
|
||
<span class="revenue-item-label">{{ item.label }}</span>
|
||
</div>
|
||
<div class="revenue-item-value">
|
||
¥{{ item.value }}
|
||
<span class="revenue-item-trend" :class="item.trendDir">
|
||
{{ item.trend }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-col>
|
||
</a-row>
|
||
|
||
<a-row :gutter="[16, 16]" class="mt-4">
|
||
<!-- 应用排行 -->
|
||
<a-col :xs="24" :lg="12">
|
||
<div class="panel">
|
||
<div class="panel-header">
|
||
<span class="panel-title">🏆 应用排行</span>
|
||
<a-select
|
||
v-model:value="rankType"
|
||
size="small"
|
||
style="min-width: 120px"
|
||
@change="refresh"
|
||
>
|
||
<a-select-option value="installs">按安装量</a-select-option>
|
||
<a-select-option value="revenue">按收入</a-select-option>
|
||
<a-select-option value="rating">按评分</a-select-option>
|
||
</a-select>
|
||
</div>
|
||
<div class="rank-list">
|
||
<div v-for="(app, index) in topApps" :key="app.name" class="rank-item">
|
||
<div class="rank-pos" :class="{ top3: index < 3 }">{{ index + 1 }}</div>
|
||
<div class="rank-info">
|
||
<div class="rank-icon">{{ app.icon }}</div>
|
||
<div class="rank-detail">
|
||
<div class="rank-name">{{ app.name }}</div>
|
||
<div class="rank-desc">{{ app.desc }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="rank-value">{{ app.value }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-col>
|
||
|
||
<!-- 用户活跃分布 -->
|
||
<a-col :xs="24" :lg="12">
|
||
<div class="panel">
|
||
<div class="panel-header">
|
||
<span class="panel-title">👥 用户活跃</span>
|
||
</div>
|
||
<div class="activity-content">
|
||
<!-- 活跃度概要 -->
|
||
<div class="activity-summary">
|
||
<div class="activity-item">
|
||
<div class="activity-value blue">{{ activityData.dau }}</div>
|
||
<div class="activity-label">日活跃 (DAU)</div>
|
||
</div>
|
||
<div class="activity-item">
|
||
<div class="activity-value green">{{ activityData.wau }}</div>
|
||
<div class="activity-label">周活跃 (WAU)</div>
|
||
</div>
|
||
<div class="activity-item">
|
||
<div class="activity-value purple">{{ activityData.mau }}</div>
|
||
<div class="activity-label">月活跃 (MAU)</div>
|
||
</div>
|
||
</div>
|
||
<a-divider style="margin: 12px 0" />
|
||
<!-- 活跃度分布 -->
|
||
<div class="activity-dist">
|
||
<div class="dist-title">活跃度分布</div>
|
||
<div v-for="bar in activityDist" :key="bar.label" class="dist-row">
|
||
<span class="dist-label">{{ bar.label }}</span>
|
||
<div class="dist-bar-wrap">
|
||
<div
|
||
class="dist-bar"
|
||
:style="{ width: bar.percent + '%', background: bar.color }"
|
||
/>
|
||
</div>
|
||
<span class="dist-value">{{ bar.value }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-col>
|
||
</a-row>
|
||
|
||
<!-- API 调用统计 -->
|
||
<div class="panel mt-4">
|
||
<div class="panel-header">
|
||
<span class="panel-title">⚡ API 调用统计</span>
|
||
<a-space>
|
||
<a-tag color="blue">近7天</a-tag>
|
||
<a-button size="small" type="link">查看详情</a-button>
|
||
</a-space>
|
||
</div>
|
||
<div class="api-stats">
|
||
<a-row :gutter="[16, 16]">
|
||
<a-col :xs="24" :sm="8" v-for="apiStat in apiStats" :key="apiStat.label">
|
||
<div class="api-stat-card">
|
||
<div class="api-stat-header">
|
||
<span class="api-stat-label">{{ apiStat.label }}</span>
|
||
<a-tag :color="apiStat.tagColor" size="small">{{ apiStat.status }}</a-tag>
|
||
</div>
|
||
<div class="api-stat-value">{{ apiStat.value }}</div>
|
||
<div class="api-stat-bar-wrap">
|
||
<div class="api-stat-bar" :style="{ width: apiStat.usage + '%', background: apiStat.barColor }" />
|
||
</div>
|
||
<div class="api-stat-footer">
|
||
<span>已用 {{ apiStat.usage }}%</span>
|
||
<span>限额 {{ apiStat.limit }}</span>
|
||
</div>
|
||
</div>
|
||
</a-col>
|
||
</a-row>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed } from 'vue'
|
||
// TODO: 后端接口就绪后解除注释
|
||
// import { getAppAnalytics, exportAnalyticsReport } from '@/api/developer'
|
||
import { message } from 'ant-design-vue'
|
||
|
||
definePageMeta({ layout: 'developer' })
|
||
useHead({ title: '数据统计 - 开发者中心' })
|
||
|
||
const loading = ref(false)
|
||
const dateRange = ref('7d')
|
||
const installChartType = ref('line')
|
||
const rankType = ref('installs')
|
||
|
||
// ========== Mock 数据(后端接口就绪后替换为真实 API 调用) ==========
|
||
const MOCK_ANALYTICS = {
|
||
totalInstalls: 128460,
|
||
activeUsers: 38920,
|
||
monthlyRevenue: 156780.50,
|
||
averageRating: 4.7,
|
||
installTrend: [
|
||
{ date: '03/28', newInstalls: 820, cumulativeInstalls: 124500 },
|
||
{ date: '03/29', newInstalls: 960, cumulativeInstalls: 125460 },
|
||
{ date: '03/30', newInstalls: 1100, cumulativeInstalls: 126560 },
|
||
{ date: '03/31', newInstalls: 750, cumulativeInstalls: 127310 },
|
||
{ date: '04/01', newInstalls: 680, cumulativeInstalls: 127990 },
|
||
{ date: '04/02', newInstalls: 920, cumulativeInstalls: 128910 },
|
||
{ date: '04/03', newInstalls: 550, cumulativeInstalls: 128460 },
|
||
] as Array<{ date: string; newInstalls: number; cumulativeInstalls: number }>,
|
||
revenueBreakdown: [
|
||
{ type: 'subscription', label: '订阅收入', value: 98400, trend: 18.5 },
|
||
{ type: 'one-time', label: '一次性购买', value: 32600, trend: 5.2 },
|
||
{ type: 'addon', label: '增值服务', value: 15800, trend: 32.1 },
|
||
{ type: 'license', label: '授权许可', value: 9980.50, trend: -3.4 },
|
||
] as Array<{ type: string; label: string; value: number; trend: number }>,
|
||
topApps: [
|
||
{ name: '企业官网建站', description: '一站式企业官网生成平台', installs: 45800, revenue: 52000, rating: 4.8 },
|
||
{ name: '电商系统', description: '全功能电商解决方案', installs: 32100, revenue: 45000, rating: 4.6 },
|
||
{ name: '小程序助手', description: '微信小程序快速开发工具', installs: 28900, revenue: 38000, rating: 4.9 },
|
||
{ name: 'AI客服机器人', description: '智能客服对话系统', installs: 15600, revenue: 21500, rating: 4.5 },
|
||
{ name: '数据分析平台', description: '业务数据可视化分析', installs: 6060, revenue: 12800, rating: 4.7 },
|
||
] as Array<{ name: string; description: string; installs: number; revenue: number; rating: number }>,
|
||
userActivity: {
|
||
dau: 5830,
|
||
wau: 18200,
|
||
mau: 38920,
|
||
activityDistribution: [
|
||
{ level: 'daily', label: '每日活跃', value: 5830, percentage: 15 },
|
||
{ level: 'weekly', label: '每周活跃', value: 12370, percentage: 32 },
|
||
{ level: 'monthly', label: '每月活跃', value: 20720, percentage: 53 },
|
||
] as Array<{ level: string; label: string; value: number; percentage: number }>,
|
||
},
|
||
apiUsage: [
|
||
{ apiName: 'app-list', label: '应用列表 API', calls: 156800, usagePercentage: 52, dailyLimit: 300000, status: '正常' as const },
|
||
{ apiName: 'user-auth', label: '用户认证 API', calls: 89200, usagePercentage: 75, dailyLimit: 120000, status: '警告' as const },
|
||
{ apiName: 'data-query', label: '数据查询 API', calls: 210500, usagePercentage: 88, dailyLimit: 240000, status: '警告' as const },
|
||
] as Array<{ apiName: string; label: string; calls: number; usagePercentage: number; dailyLimit: number; status: '正常' | '警告' | '超限' }>,
|
||
}
|
||
|
||
// API 数据状态
|
||
const analyticsData = ref({ ...MOCK_ANALYTICS })
|
||
|
||
const dateRangeOptions = [
|
||
{ label: '近7天', value: '7d' },
|
||
{ label: '近30天', value: '30d' },
|
||
{ label: '近90天', value: '90d' },
|
||
{ label: '全部', value: 'all' },
|
||
]
|
||
|
||
// 核心指标 - 使用API数据
|
||
const coreMetrics = computed(() => {
|
||
const formatNumber = (num: number) => {
|
||
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'
|
||
if (num >= 1000) return (num / 1000).toFixed(1) + 'K'
|
||
return num.toString()
|
||
}
|
||
|
||
const formatCurrency = (amount: number) => {
|
||
return '¥' + amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||
}
|
||
|
||
return [
|
||
{
|
||
icon: '📦',
|
||
label: '总安装量',
|
||
value: formatNumber(analyticsData.value.totalInstalls),
|
||
trend: '+12.5%', trendDir: 'up', color: 'blue',
|
||
tooltip: '所有应用的累计安装次数'
|
||
},
|
||
{
|
||
icon: '👥',
|
||
label: '活跃用户',
|
||
value: formatNumber(analyticsData.value.activeUsers),
|
||
trend: '+8.3%', trendDir: 'up', color: 'purple',
|
||
tooltip: '近30天活跃用户数'
|
||
},
|
||
{
|
||
icon: '💰',
|
||
label: '本月收入',
|
||
value: formatCurrency(analyticsData.value.monthlyRevenue),
|
||
trend: '+23.1%', trendDir: 'up', color: 'green',
|
||
tooltip: '本月累计收入'
|
||
},
|
||
{
|
||
icon: '⭐',
|
||
label: '平均评分',
|
||
value: analyticsData.value.averageRating.toFixed(1),
|
||
trend: '+0.2', trendDir: 'up', color: 'orange',
|
||
tooltip: '所有应用的平均评分'
|
||
},
|
||
]
|
||
})
|
||
|
||
// 安装量趋势 - 使用API数据
|
||
const installData = computed(() => {
|
||
const trendData = analyticsData.value.installTrend
|
||
if (!trendData.length) {
|
||
return []
|
||
}
|
||
|
||
// 计算最大安装量用于百分比
|
||
const maxCumulative = Math.max(...trendData.map(item => item.cumulativeInstalls))
|
||
const maxNew = Math.max(...trendData.map(item => item.newInstalls))
|
||
const maxValue = Math.max(maxCumulative, maxNew)
|
||
|
||
return trendData.map((item, index) => {
|
||
// 使用新安装量作为图表高度
|
||
const percent = maxValue > 0 ? (item.newInstalls / maxValue) * 100 : 0
|
||
|
||
return {
|
||
label: item.date,
|
||
percent: Math.min(100, Math.max(5, percent)), // 确保在5-100%范围内
|
||
highlight: index === trendData.length - 1,
|
||
newInstalls: item.newInstalls,
|
||
cumulativeInstalls: item.cumulativeInstalls
|
||
}
|
||
})
|
||
})
|
||
|
||
// 收入数据 - 使用API数据
|
||
const revenueData = computed(() => {
|
||
const formatCurrency = (amount: number) => {
|
||
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||
}
|
||
|
||
const getIcon = (type: string) => {
|
||
const icons: Record<string, string> = {
|
||
'subscription': '🛒',
|
||
'one-time': '一次性',
|
||
'addon': '🎁',
|
||
'license': '📄',
|
||
'support': '🆘'
|
||
}
|
||
return icons[type] || '💰'
|
||
}
|
||
|
||
const breakdown = analyticsData.value.revenueBreakdown
|
||
const total = breakdown.reduce((sum, item) => sum + item.value, 0)
|
||
|
||
return {
|
||
total: formatCurrency(total),
|
||
items: breakdown.map(item => ({
|
||
icon: getIcon(item.type),
|
||
label: item.label,
|
||
value: formatCurrency(item.value),
|
||
trend: item.trend > 0 ? `+${item.trend}%` : `${item.trend}%`,
|
||
trendDir: item.trend >= 0 ? 'up' : 'down' as 'up' | 'down'
|
||
}))
|
||
}
|
||
})
|
||
|
||
// 应用排行 - 使用API数据
|
||
const topApps = computed(() => {
|
||
const apps = analyticsData.value.topApps
|
||
const getDisplayValue = (app: typeof apps[0]) => {
|
||
if (rankType.value === 'installs') return `${app.installs} 次`
|
||
if (rankType.value === 'revenue') return `¥${app.revenue.toLocaleString()}`
|
||
if (rankType.value === 'rating') return app.rating.toFixed(1)
|
||
return `${app.installs} 次`
|
||
}
|
||
|
||
const getIcon = (name: string) => {
|
||
const icons: Record<string, string> = {
|
||
'企业官网': '🌐',
|
||
'电商': '🛒',
|
||
'小程序': '📱',
|
||
'AI': '🤖',
|
||
'数据': '📊',
|
||
'官网': '🌐',
|
||
'电商系统': '🛒',
|
||
'小程序助手': '📱',
|
||
'AI客服': '🤖',
|
||
'数据分析': '📊'
|
||
}
|
||
|
||
for (const [key, icon] of Object.entries(icons)) {
|
||
if (name.includes(key)) return icon
|
||
}
|
||
return '📦'
|
||
}
|
||
|
||
return apps.map(app => ({
|
||
icon: getIcon(app.name),
|
||
name: app.name,
|
||
desc: app.description,
|
||
value: getDisplayValue(app)
|
||
}))
|
||
})
|
||
|
||
// 用户活跃 - 使用API数据
|
||
const activityData = computed(() => ({
|
||
dau: analyticsData.value.userActivity.dau.toLocaleString(),
|
||
wau: analyticsData.value.userActivity.wau.toLocaleString(),
|
||
mau: analyticsData.value.userActivity.mau.toLocaleString(),
|
||
}))
|
||
|
||
const activityDist = computed(() => {
|
||
return analyticsData.value.userActivity.activityDistribution.map(item => ({
|
||
label: item.label,
|
||
value: `${item.percentage}%`,
|
||
percent: item.percentage,
|
||
color: getDistributionColor(item.level)
|
||
}))
|
||
})
|
||
|
||
// 根据活跃度级别获取颜色
|
||
function getDistributionColor(level: string) {
|
||
const colors: Record<string, string> = {
|
||
'high': '#4f46e5', // 高活跃
|
||
'medium': '#8b5cf6', // 中活跃
|
||
'low': '#a78bfa', // 低活跃
|
||
'inactive': '#e5e7eb', // 沉默
|
||
'daily': '#4f46e5', // 每日活跃
|
||
'weekly': '#8b5cf6', // 每周活跃
|
||
'monthly': '#a78bfa', // 每月活跃
|
||
'silent': '#e5e7eb' // 沉默用户
|
||
}
|
||
return colors[level] || '#e5e7eb'
|
||
}
|
||
|
||
// API 调用统计 - 使用API数据
|
||
const apiStats = computed(() => {
|
||
return analyticsData.value.apiUsage.map(item => {
|
||
let tagColor = 'green'
|
||
let statusText = '正常'
|
||
|
||
if (item.status === '警告') {
|
||
tagColor = 'orange'
|
||
statusText = '警告'
|
||
} else if (item.status === '超限') {
|
||
tagColor = 'red'
|
||
statusText = '超限'
|
||
}
|
||
|
||
return {
|
||
label: item.label,
|
||
value: `${item.calls.toLocaleString()} 次`,
|
||
usage: item.usagePercentage,
|
||
limit: `${item.dailyLimit.toLocaleString()}/日`,
|
||
status: statusText,
|
||
tagColor,
|
||
barColor: getApiBarColor(item.usagePercentage)
|
||
}
|
||
})
|
||
})
|
||
|
||
// 根据使用率获取进度条颜色
|
||
function getApiBarColor(usage: number) {
|
||
if (usage < 60) return '#22c55e' // 绿色:正常
|
||
if (usage < 85) return '#f59e0b' // 黄色:警告
|
||
return '#ef4444' // 红色:超限
|
||
}
|
||
|
||
// 加载统计数据(TODO: 后端接口就绪后替换 Mock 为真实 API 调用)
|
||
// async function loadAnalyticsData() {
|
||
// loading.value = true
|
||
// try {
|
||
// const response = await getAppAnalytics({ dateRange: dateRange.value })
|
||
// if (response.data?.success) {
|
||
// analyticsData.value = response.data.data
|
||
// }
|
||
// } catch (error) {
|
||
// console.error('加载统计数据失败:', error)
|
||
// message.error('加载统计数据失败,请稍后重试')
|
||
// } finally {
|
||
// loading.value = false
|
||
// }
|
||
// }
|
||
|
||
function onDateRangeChange() {
|
||
// TODO: 后端接口就绪后调用 loadAnalyticsData()
|
||
}
|
||
|
||
async function refresh() {
|
||
loading.value = true
|
||
// TODO: 后端接口就绪后替换为 loadAnalyticsData()
|
||
setTimeout(() => {
|
||
analyticsData.value = { ...MOCK_ANALYTICS }
|
||
loading.value = false
|
||
message.success('数据已刷新')
|
||
}, 300)
|
||
}
|
||
|
||
async function exportReport() {
|
||
message.info('导出功能将在后端接口就绪后开放')
|
||
}
|
||
|
||
// 页面无需初始化加载,直接使用 Mock 数据渲染
|
||
</script>
|
||
|
||
<style scoped>
|
||
.dev-page {
|
||
min-height: 100%;
|
||
}
|
||
|
||
/* Hero 区域 */
|
||
.analytics-hero {
|
||
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 40%, #1e3a5f 100%);
|
||
padding: 28px 28px 24px;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.analytics-hero::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: -80px;
|
||
right: -80px;
|
||
width: 300px;
|
||
height: 300px;
|
||
border-radius: 50%;
|
||
background: rgba(99, 102, 241, 0.2);
|
||
filter: blur(60px);
|
||
}
|
||
|
||
.analytics-hero-content {
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.analytics-hero-title {
|
||
color: #fff;
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
margin: 0 0 6px;
|
||
}
|
||
|
||
.analytics-hero-desc {
|
||
color: rgba(199, 210, 254, 0.8);
|
||
font-size: 14px;
|
||
margin: 0;
|
||
}
|
||
|
||
/* 主体内容 */
|
||
.analytics-body {
|
||
padding: 20px 24px 24px;
|
||
}
|
||
|
||
/* 筛选栏 */
|
||
.filter-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
/* 指标卡片 */
|
||
.metric-card {
|
||
padding: 18px;
|
||
border-radius: 12px;
|
||
border: 1px solid transparent;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.metric-card:hover { transform: translateY(-1px); }
|
||
|
||
.metric-card.blue { background: #eff6ff; border-color: #dbeafe; }
|
||
.metric-card.purple { background: #f5f3ff; border-color: #e9d5ff; }
|
||
.metric-card.green { background: #f0fdf4; border-color: #bbf7d0; }
|
||
.metric-card.orange { background: #fff7ed; border-color: #fed7aa; }
|
||
|
||
.metric-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.metric-icon { font-size: 24px; }
|
||
|
||
.metric-trend {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.metric-trend.up { color: #16a34a; background: #dcfce7; }
|
||
.metric-trend.down { color: #dc2626; background: #fef2f2; }
|
||
|
||
.metric-value {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: rgba(0, 0, 0, 0.85);
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.metric-label {
|
||
font-size: 12px;
|
||
color: rgba(0, 0, 0, 0.45);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
/* 面板通用 */
|
||
.panel {
|
||
background: #fff;
|
||
border: 1px solid #f0f0f0;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.panel-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 14px 18px;
|
||
border-bottom: 1px solid #f5f5f5;
|
||
}
|
||
|
||
.panel-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: rgba(0, 0, 0, 0.85);
|
||
}
|
||
|
||
/* 图表区域 */
|
||
.chart-area {
|
||
padding: 20px;
|
||
}
|
||
|
||
.chart-bar-group {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
gap: 12px;
|
||
height: 200px;
|
||
padding-top: 10px;
|
||
}
|
||
|
||
.chart-bar-col {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
height: 100%;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.chart-bar {
|
||
width: 100%;
|
||
max-width: 48px;
|
||
border-radius: 6px 6px 0 0;
|
||
background: linear-gradient(180deg, #6366f1, #4f46e5);
|
||
min-height: 8px;
|
||
transition: all 0.3s;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.chart-bar:hover,
|
||
.chart-bar.active {
|
||
background: linear-gradient(180deg, #818cf8, #6366f1);
|
||
box-shadow: 0 -4px 12px rgba(99, 102, 241, 0.3);
|
||
}
|
||
|
||
.chart-bar-label {
|
||
font-size: 11px;
|
||
color: rgba(0, 0, 0, 0.4);
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.chart-legend {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
margin-top: 16px;
|
||
font-size: 12px;
|
||
color: rgba(0, 0, 0, 0.5);
|
||
justify-content: center;
|
||
}
|
||
|
||
.legend-dot {
|
||
display: inline-block;
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 2px;
|
||
margin-right: 4px;
|
||
}
|
||
|
||
.legend-dot.green { background: #22c55e; }
|
||
.legend-dot.blue { background: #4f46e5; }
|
||
|
||
/* 收入 */
|
||
.revenue-content {
|
||
padding: 20px;
|
||
}
|
||
|
||
.revenue-total { text-align: center; padding: 4px 0; }
|
||
|
||
.revenue-amount {
|
||
font-size: 32px;
|
||
font-weight: 700;
|
||
color: rgba(0, 0, 0, 0.88);
|
||
}
|
||
|
||
.revenue-label {
|
||
font-size: 13px;
|
||
color: rgba(0, 0, 0, 0.4);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.revenue-items { padding: 0; }
|
||
|
||
.revenue-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 10px 0;
|
||
}
|
||
|
||
.revenue-item-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.revenue-item-icon { font-size: 18px; }
|
||
|
||
.revenue-item-label {
|
||
font-size: 13px;
|
||
color: rgba(0, 0, 0, 0.65);
|
||
}
|
||
|
||
.revenue-item-value {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: rgba(0, 0, 0, 0.85);
|
||
}
|
||
|
||
.revenue-item-trend {
|
||
font-size: 11px;
|
||
margin-left: 6px;
|
||
padding: 1px 4px;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.revenue-item-trend.up { color: #16a34a; background: #dcfce7; }
|
||
.revenue-item-trend.down { color: #dc2626; background: #fef2f2; }
|
||
|
||
/* 排行 */
|
||
.rank-list { padding: 8px 14px; }
|
||
|
||
.rank-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid #f9f9f9;
|
||
}
|
||
|
||
.rank-item:last-child { border-bottom: none; }
|
||
|
||
.rank-pos {
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: rgba(0, 0, 0, 0.4);
|
||
background: #f5f5f5;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.rank-pos.top3 {
|
||
background: linear-gradient(135deg, #f59e0b, #f97316);
|
||
color: #fff;
|
||
}
|
||
|
||
.rank-info {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.rank-icon { font-size: 22px; flex-shrink: 0; }
|
||
|
||
.rank-detail { min-width: 0; }
|
||
|
||
.rank-name {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: rgba(0, 0, 0, 0.85);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.rank-desc {
|
||
font-size: 12px;
|
||
color: rgba(0, 0, 0, 0.4);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.rank-value {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #4f46e5;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* 活跃度 */
|
||
.activity-content { padding: 20px; }
|
||
|
||
.activity-summary {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr 1fr;
|
||
gap: 12px;
|
||
text-align: center;
|
||
}
|
||
|
||
.activity-value {
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.activity-value.blue { color: #3b82f6; }
|
||
.activity-value.green { color: #22c55e; }
|
||
.activity-value.purple { color: #8b5cf6; }
|
||
|
||
.activity-label {
|
||
font-size: 12px;
|
||
color: rgba(0, 0, 0, 0.4);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.activity-dist { padding: 0; }
|
||
|
||
.dist-title {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: rgba(0, 0, 0, 0.65);
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.dist-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.dist-label {
|
||
font-size: 12px;
|
||
color: rgba(0, 0, 0, 0.5);
|
||
width: 100px;
|
||
flex-shrink: 0;
|
||
text-align: right;
|
||
}
|
||
|
||
.dist-bar-wrap {
|
||
flex: 1;
|
||
height: 8px;
|
||
background: #f5f5f5;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.dist-bar {
|
||
height: 100%;
|
||
border-radius: 4px;
|
||
transition: width 0.3s;
|
||
}
|
||
|
||
.dist-value {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: rgba(0, 0, 0, 0.65);
|
||
width: 36px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* API 统计 */
|
||
.api-stats { padding: 16px; }
|
||
|
||
.api-stat-card {
|
||
padding: 16px;
|
||
border-radius: 10px;
|
||
border: 1px solid #f0f0f0;
|
||
background: #fafafa;
|
||
transition: all 0.15s;
|
||
}
|
||
|
||
.api-stat-card:hover {
|
||
border-color: #e0e7ff;
|
||
background: #f5f7ff;
|
||
}
|
||
|
||
.api-stat-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.api-stat-label {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: rgba(0, 0, 0, 0.65);
|
||
}
|
||
|
||
.api-stat-value {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: rgba(0, 0, 0, 0.85);
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.api-stat-bar-wrap {
|
||
height: 6px;
|
||
background: #f0f0f0;
|
||
border-radius: 3px;
|
||
overflow: hidden;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.api-stat-bar {
|
||
height: 100%;
|
||
border-radius: 3px;
|
||
transition: width 0.3s;
|
||
}
|
||
|
||
.api-stat-footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 11px;
|
||
color: rgba(0, 0, 0, 0.4);
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.activity-summary {
|
||
grid-template-columns: 1fr;
|
||
gap: 8px;
|
||
}
|
||
.dist-label { width: 80px; }
|
||
}
|
||
</style>
|