Files
websopy-mp/src/developer/app/[id]/analytics.tsx
赵忠林 ffab0ec25c feat(developer): 完成小程序开发者中心和企业控制台改造
- 设计并实现了开发者中心与企业控制台两大模块
- 按用户角色区分开发者和企业客户,支持多项目类型及成员管理
- 新增项目管理、应用管理、API Key管理及成员邀请等多功能页面
- 实现应用版本发布、消息通知中心、权限审批与开发者申请流程
- 完成CI/CD流水线、运营监控、发票管理、SSO单点登录功能
- 搭建SDK下载中心、工单系统、FAQ系统、数据导入导出等模块
- 优化后端API,支持已登录和未注册用户不同加入应用流程
- 前端按钮统一采用微信手机号授权,完善用户授权体验
- 修复多个页面的JSX语法错误及依赖导入问题,替换部分组件库
- 增加详细的类型定义文件,提升项目类型安全
- 新增超过55个页面及60个API接口,扩展应用功能和服务体系
- 完成全面的样式设计,实现一致的视觉风格和交互体验
2026-04-13 02:26:46 +08:00

426 lines
15 KiB
TypeScript

/**
* 运营监控页面 - 数据分析看板
*/
import { useState, useEffect } from 'react'
import { View, Text, ScrollView } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { AtTabs, AtTabsPane, AtTag, AtActivityIndicator } from 'taro-ui'
import {
getOverviewStats,
getRealtimeData,
getPerformanceMetrics,
getDeviceStats,
getSourceStats,
getPageStats,
getRegionStats,
} from '../../../../api/analytics'
import type { OverviewStats, PerformanceMetric, DeviceStat, SourceStat, PageStat, RegionStat } from '../../../../types/analytics'
import './analytics.scss'
// 格式化数字
const formatNumber = (num?: number) => {
if (!num) return '0'
if (num >= 100000000) return (num / 100000000).toFixed(1) + '亿'
if (num >= 10000) return (num / 10000).toFixed(1) + '万'
return num.toLocaleString()
}
// 格式化百分比
const formatPercent = (num?: number) => {
if (!num) return '0%'
return num.toFixed(1) + '%'
}
export default function Analytics() {
const [loading, setLoading] = useState(true)
const [realtime, setRealtime] = useState({
uv: 0,
pv: 0,
apiCalls: 0,
errors: 0,
activeUsers: 0,
})
const [overview, setOverview] = useState<OverviewStats>({})
const [metrics, setMetrics] = useState<PerformanceMetric[]>([])
const [devices, setDevices] = useState<DeviceStat[]>([])
const [sources, setSources] = useState<SourceStat[]>([])
const [pages, setPages] = useState<PageStat[]>([])
const [regions, setRegions] = useState<RegionStat[]>([])
const [activeTab, setActiveTab] = useState(0)
const [appId, setAppId] = useState('')
useEffect(() => {
const pages = Taro.getCurrentPages()
const current = pages[pages.length - 1]
if (current?.options?.appId) {
setAppId(current.options.appId)
}
}, [])
useEffect(() => {
if (appId) {
fetchData()
// 实时数据每 30 秒刷新
const timer = setInterval(() => {
fetchRealtimeData()
}, 30000)
return () => clearInterval(timer)
}
}, [appId])
// 获取所有数据
const fetchData = async () => {
try {
setLoading(true)
await Promise.all([
fetchRealtimeData(),
fetchOverviewData(),
fetchMetricsData(),
fetchDeviceData(),
fetchSourceData(),
fetchPageData(),
fetchRegionData(),
])
} catch (err) {
console.error('获取数据失败', err)
} finally {
setLoading(false)
}
}
// 获取实时数据
const fetchRealtimeData = async () => {
try {
const res = await getRealtimeData(Number(appId))
if (res.data) {
setRealtime(res.data)
}
} catch (err) {
console.error('获取实时数据失败', err)
}
}
// 获取概览数据
const fetchOverviewData = async () => {
try {
const res = await getOverviewStats(Number(appId))
if (res.data) {
setOverview(res.data)
}
} catch (err) {
console.error('获取概览数据失败', err)
}
}
// 获取性能指标
const fetchMetricsData = async () => {
try {
const res = await getPerformanceMetrics(Number(appId))
if (res.data) {
setMetrics(res.data.list || [])
}
} catch (err) {
console.error('获取性能指标失败', err)
}
}
// 获取设备分布
const fetchDeviceData = async () => {
try {
const res = await getDeviceStats(Number(appId))
if (res.data) {
setDevices(res.data.list || [])
}
} catch (err) {
console.error('获取设备数据失败', err)
}
}
// 获取来源分布
const fetchSourceData = async () => {
try {
const res = await getSourceStats(Number(appId))
if (res.data) {
setSources(res.data.list || [])
}
} catch (err) {
console.error('获取来源数据失败', err)
}
}
// 获取页面访问
const fetchPageData = async () => {
try {
const res = await getPageStats({
websiteId: Number(appId),
limit: 10,
})
if (res.data) {
setPages(res.data.list || [])
}
} catch (err) {
console.error('获取页面数据失败', err)
}
}
// 获取区域分布
const fetchRegionData = async () => {
try {
const res = await getRegionStats(Number(appId))
if (res.data) {
setRegions(res.data.list || [])
}
} catch (err) {
console.error('获取区域数据失败', err)
}
}
const tabs = [
{ title: '概览' },
{ title: '性能' },
{ title: '用户' },
{ title: '页面' },
]
return (
<View className="analytics-page">
{loading && realtime.uv === 0 ? (
<View className="loading-wrap">
<AtActivityIndicator size={32} />
<Text className="loading-text">...</Text>
</View>
) : (
<>
{/* 实时数据 */}
<View className="realtime-card">
<View className="realtime-header">
<Text className="title"></Text>
<View className="live-dot">
<View className="dot" />
<Text>LIVE</Text>
</View>
</View>
<View className="realtime-stats">
<View className="stat-item">
<Text className="stat-value">{formatNumber(realtime.uv)}</Text>
<Text className="stat-label"> UV</Text>
</View>
<View className="stat-item">
<Text className="stat-value">{formatNumber(realtime.pv)}</Text>
<Text className="stat-label"> PV</Text>
</View>
<View className="stat-item">
<Text className="stat-value">{formatNumber(realtime.apiCalls)}</Text>
<Text className="stat-label">API </Text>
</View>
<View className="stat-item">
<Text className={`stat-value ${realtime.errors > 0 ? 'error' : ''}`}>
{formatNumber(realtime.errors)}
</Text>
<Text className="stat-label"></Text>
</View>
</View>
</View>
{/* 今日概览 */}
<View className="overview-stats">
<View className="stat-card">
<Text className="stat-value">{formatNumber(overview.todayUv)}</Text>
<Text className="stat-label"> UV</Text>
</View>
<View className="stat-card">
<Text className="stat-value">{formatNumber(overview.todayPv)}</Text>
<Text className="stat-label"> PV</Text>
</View>
<View className="stat-card">
<Text className="stat-value">{formatNumber(overview.todayApiCalls)}</Text>
<Text className="stat-label">API </Text>
</View>
</View>
{/* Tab 切换 */}
<AtTabs
current={activeTab}
tabList={tabs}
scroll
onClick={(index) => setActiveTab(index)}
>
<AtTabsPane current={activeTab} index={0}>
<ScrollView scrollY className="tab-content">
{/* 趋势图表区 */}
<View className="chart-section">
<Text className="section-title"></Text>
<View className="chart-placeholder">
<Text className="iconfont icon-chart" />
<Text className="placeholder-text"></Text>
<Text className="placeholder-hint"> ECharts </Text>
</View>
</View>
{/* 设备分布 */}
<View className="chart-section">
<Text className="section-title"></Text>
{devices.length > 0 ? (
<View className="pie-chart">
{devices.map((device, index) => (
<View className="pie-item" key={index}>
<View
className="pie-color"
style={{
background: ['#1890ff', '#52c41a', '#faad14', '#f5222d'][index % 4],
}}
/>
<Text className="pie-label">{device.device}</Text>
<Text className="pie-value">{formatPercent(device.percentage)}</Text>
</View>
))}
</View>
) : (
<View className="empty-data"></View>
)}
</View>
</ScrollView>
</AtTabsPane>
<AtTabsPane current={activeTab} index={1}>
<ScrollView scrollY className="tab-content">
{/* 性能指标 */}
<View className="metrics-section">
<Text className="section-title"></Text>
{metrics.length > 0 ? (
metrics.map((metric, index) => (
<View className="metric-item" key={index}>
<View className="metric-info">
<Text className="metric-name">{metric.metric}</Text>
<View className="metric-value-row">
<Text className="metric-value">{metric.value?.toFixed(2)}</Text>
<Text className="metric-unit">{metric.unit}</Text>
<AtTag
size="small"
type={metric.trend === 'up' ? 'success' : metric.trend === 'down' ? 'error' : 'primary'}
>
{metric.trend === 'up' ? '↑' : metric.trend === 'down' ? '↓' : '→'}
{Math.abs(metric.change || 0)}%
</AtTag>
</View>
</View>
</View>
))
) : (
<View className="empty-data"></View>
)}
</View>
{/* 错误统计 */}
<View className="error-section">
<Text className="section-title"></Text>
<View className="error-summary">
<View className="error-item">
<Text className="error-value error">{overview.todayErrors || 0}</Text>
<Text className="error-label"></Text>
</View>
<View className="error-item">
<Text className="error-value warning">0</Text>
<Text className="error-label"></Text>
</View>
</View>
</View>
</ScrollView>
</AtTabsPane>
<AtTabsPane current={activeTab} index={2}>
<ScrollView scrollY className="tab-content">
{/* 用户统计 */}
<View className="user-stats">
<View className="user-card">
<Text className="user-value">{formatNumber(overview.activeUsers)}</Text>
<Text className="user-label"></Text>
</View>
<View className="user-card">
<Text className="user-value">{formatNumber(overview.newUsers)}</Text>
<Text className="user-label"></Text>
</View>
<View className="user-card">
<Text className="user-value">{formatPercent(overview.retention)}</Text>
<Text className="user-label"></Text>
</View>
</View>
{/* 区域分布 */}
<View className="region-section">
<Text className="section-title"></Text>
{regions.length > 0 ? (
regions.slice(0, 5).map((region, index) => (
<View className="region-item" key={index}>
<View className="region-rank">{index + 1}</View>
<Text className="region-name">{region.region}</Text>
<View className="region-bar">
<View
className="bar-fill"
style={{ width: (region.percentage || 0) + '%' }}
/>
</View>
<Text className="region-value">{formatPercent(region.percentage)}</Text>
</View>
))
) : (
<View className="empty-data"></View>
)}
</View>
{/* 来源分布 */}
<View className="source-section">
<Text className="section-title"></Text>
{sources.length > 0 ? (
sources.map((source, index) => (
<View className="source-item" key={index}>
<View className="source-rank">{index + 1}</View>
<Text className="source-name">{source.source}</Text>
<Text className="source-value">{formatPercent(source.percentage)}</Text>
</View>
))
) : (
<View className="empty-data"></View>
)}
</View>
</ScrollView>
</AtTabsPane>
<AtTabsPane current={activeTab} index={3}>
<ScrollView scrollY className="tab-content">
{/* 页面排行 */}
<View className="page-section">
<Text className="section-title">访</Text>
{pages.length > 0 ? (
pages.map((page, index) => (
<View className="page-item" key={index}>
<View className="page-rank">{index + 1}</View>
<View className="page-info">
<Text className="page-title">{page.title || page.path}</Text>
<Text className="page-path">{page.path}</Text>
</View>
<View className="page-stats">
<View className="page-stat">
<Text className="stat-num">{formatNumber(page.pv)}</Text>
<Text className="stat-label">PV</Text>
</View>
<View className="page-stat">
<Text className="stat-num">{formatNumber(page.uv)}</Text>
<Text className="stat-label">UV</Text>
</View>
</View>
</View>
))
) : (
<View className="empty-data"></View>
)}
</View>
</ScrollView>
</AtTabsPane>
</AtTabs>
</>
)}
</View>
)
}