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