900 lines
22 KiB
Plaintext
900 lines
22 KiB
Plaintext
<template>
|
||
<scroll-view direction="vertical" class="analytics-container" :scroll-y="true" :enable-back-to-top="true">
|
||
<!-- Header -->
|
||
<view class="header">
|
||
<text class="title">数据分析</text>
|
||
<view class="filter-bar">
|
||
<input v-model="startDate" placeholder="开始日期 (YYYY-MM-DD)" type="date" class="date-input" @input="onStartDateChange" />
|
||
<input v-model="endDate" placeholder="结束日期 (YYYY-MM-DD)" type="date" class="date-input" @input="onEndDateChange" />
|
||
<button @click="refreshData" class="refresh-btn">
|
||
<text>刷新</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Content -->
|
||
<view v-if="error !== null" class="error-container">
|
||
<text class="error-text">{{ error }}</text>
|
||
<button class="retry-btn" @click="retryLoad">重试</button>
|
||
</view>
|
||
<scroll-view v-else class="content" :class="{ 'large-screen': isLargeScreen }" scroll-y="true">
|
||
<!-- Overview Cards -->
|
||
<view class="overview-section">
|
||
<text class="section-title">概览统计</text> <view class="cards-grid" :class="{ 'large-grid': isLargeScreen }">
|
||
<view v-for="(card, index) in overviewCards" :key="index" class="overview-card">
|
||
<text class="card-value">{{ card.value ?? '0' }}</text>
|
||
<text class="card-label">{{ card.label ?? '' }}</text>
|
||
<text class="card-change" :class="card.changeClass ?? ''">
|
||
{{ card.change ?? '' }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Charts Section -->
|
||
<view class="charts-section">
|
||
<text class="section-title">趋势分析</text>
|
||
|
||
<!-- Assignment Completion Chart -->
|
||
<view class="chart-container">
|
||
<text class="chart-title">作业完成率趋势</text>
|
||
<view class="chart-content">
|
||
<ak-charts v-if="completionRateData.length > 0"
|
||
:option="completionRateChartOption"
|
||
canvas-id="completion-rate-chart"
|
||
class="chart-canvas" />
|
||
<view v-else class="chart-placeholder">
|
||
<text class="chart-text">暂无数据</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Performance Distribution -->
|
||
<view class="chart-container">
|
||
<text class="chart-title">成绩分布</text>
|
||
<view class="chart-content">
|
||
<ak-charts v-if="performanceData.length > 0"
|
||
:option="performanceChartOption"
|
||
canvas-id="performance-chart"
|
||
class="chart-canvas" />
|
||
<view v-else class="chart-placeholder">
|
||
<text class="chart-text">暂无数据</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Activity Distribution -->
|
||
<view class="chart-container">
|
||
<text class="chart-title">学生活跃度分布</text>
|
||
<view class="chart-content">
|
||
<ak-charts v-if="activityDistributionData.length > 0"
|
||
:option="activityDistributionChartOption"
|
||
canvas-id="activity-distribution-chart"
|
||
class="chart-canvas" />
|
||
<view v-else class="chart-placeholder">
|
||
<text class="chart-text">暂无数据</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Top Performers Section -->
|
||
<view class="performers-section">
|
||
<text class="section-title">优秀学员</text>
|
||
<view class="performers-list"> <view v-for="(performer, index) in topPerformers" :key="index" class="performer-card">
|
||
<view class="performer-rank">
|
||
<text class="rank-text">{{ index + 1 }}</text>
|
||
</view>
|
||
<view class="performer-info">
|
||
<text class="performer-name">{{ getPerformerName(performer) }}</text>
|
||
<text class="performer-score">得分: {{ getPerformerScore(performer) }}</text>
|
||
</view>
|
||
<view class="performer-badge" :class="getBadgeClass(index)">
|
||
<text class="badge-text">{{ getBadgeText(index) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Recent Activities -->
|
||
<view class="activities-section">
|
||
<text class="section-title">近期活动</text>
|
||
<view class="activities-list">
|
||
<view v-for="(activity, index) in recentActivities" :key="index" class="activity-item">
|
||
<view class="activity-icon">
|
||
<simple-icon :type="getActivityIcon(activity)" :size="20" color="#6366F1" />
|
||
</view>
|
||
<view class="activity-content">
|
||
<text class="activity-title">{{ getActivityTitle(activity) }}</text>
|
||
<text class="activity-time">{{ getActivityTime(activity) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</scroll-view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, computed, onMounted, nextTick } from 'vue'
|
||
import { onResize } from '@dcloudio/uni-app'
|
||
import supa from '@/components/supadb/aksupainstance.uts'
|
||
import AkCharts from '@/uni_modules/ak-charts/components/ak-charts.uvue'
|
||
|
||
// Responsive state - using onResize for dynamic updates
|
||
const screenWidth = ref<number>(uni.getSystemInfoSync().windowWidth)
|
||
|
||
// Computed properties for responsive design
|
||
const isLargeScreen = computed(() : boolean => {
|
||
return screenWidth.value >= 768
|
||
})
|
||
|
||
// Reactive state
|
||
const loading = ref(false)
|
||
const error = ref<string | null>(null)
|
||
|
||
// Analytics parameters
|
||
const teacherId = ref('current_teacher_id') // 从用户状态获取
|
||
const startDate = ref('')
|
||
const endDate = ref('')
|
||
|
||
// Data arrays using UTSJSONObject
|
||
const statisticsData = ref<UTSJSONObject[]>([])
|
||
const topPerformers = ref<UTSJSONObject[]>([])
|
||
const chartData = ref<UTSJSONObject[]>([])
|
||
const performanceData = ref<UTSJSONObject[]>([])
|
||
const recentActivities = ref<UTSJSONObject[]>([])
|
||
|
||
// Chart specific data
|
||
const completionRateData = ref<number[]>([])
|
||
const completionRateLabels = ref<string[]>([])
|
||
const activityDistributionData = ref<number[]>([])
|
||
const activityDistributionLabels = ref<string[]>([])
|
||
|
||
// Computed properties data
|
||
const overviewCards = ref<UTSJSONObject[]>([])
|
||
const updateOverviewCards = () => {
|
||
if (statisticsData.value.length > 0) {
|
||
const stats = statisticsData.value[0]
|
||
|
||
const totalStudents = stats.get('total_students')
|
||
const totalAssignments = stats.get('total_assignments')
|
||
const completionRate = stats.get('completion_rate')
|
||
const averageScore = stats.get('average_score')
|
||
overviewCards.value = [
|
||
{
|
||
key: 'students',
|
||
value: (totalStudents != null ? parseFloat(totalStudents.toString()) : 0).toString(),
|
||
label: '学员总数',
|
||
change: '+12%',
|
||
changeClass: 'positive'
|
||
},
|
||
{
|
||
key: 'assignments',
|
||
value: (totalAssignments != null ? parseFloat(totalAssignments.toString()) : 0).toString(),
|
||
label: '作业总数',
|
||
change: '+8%',
|
||
changeClass: 'positive'
|
||
},
|
||
{
|
||
key: 'completion',
|
||
value: (completionRate != null ? parseFloat(completionRate.toString()) : 0).toFixed(1) + '%',
|
||
label: '完成率',
|
||
change: '+5%',
|
||
changeClass: 'positive'
|
||
},
|
||
{
|
||
key: 'score',
|
||
value: (averageScore != null ? parseFloat(averageScore.toString()) : 0).toFixed(1),
|
||
label: '平均分',
|
||
change: '+2.1',
|
||
changeClass: 'positive'
|
||
}
|
||
]
|
||
}
|
||
}
|
||
|
||
// Helper function to format date labels - moved before usage
|
||
const formatDateLabel = (dateStr: string): string => {
|
||
try {
|
||
const date = new Date(dateStr)
|
||
return `${date.getMonth() + 1}/${date.getDate()}`
|
||
} catch (e) {
|
||
return dateStr
|
||
}
|
||
}
|
||
// Chart data processing functions - defined before use
|
||
const processChartData = (data: UTSJSONObject[]) => {
|
||
const rates: number[] = []
|
||
const labels: string[] = []
|
||
|
||
data.forEach((item: UTSJSONObject) => {
|
||
const dateKeyValue = item.get('date_key')
|
||
const dateKey = dateKeyValue != null ? dateKeyValue.toString() : ''
|
||
const valueValue = item.get('value')
|
||
const value = valueValue != null ? parseFloat(valueValue.toString()) : 0
|
||
|
||
// 格式化日期标签
|
||
const formattedDate = formatDateLabel(dateKey)
|
||
labels.push(formattedDate)
|
||
rates.push(value)
|
||
})
|
||
|
||
completionRateData.value = rates
|
||
completionRateLabels.value = labels
|
||
}
|
||
|
||
const generateMockChartData = () => {
|
||
// 生成模拟的完成率趋势数据(最近7天)
|
||
const rates: number[] = []
|
||
const labels: string[] = []
|
||
|
||
for (let i = 6; i >= 0; i--) {
|
||
const date = new Date()
|
||
date.setDate(date.getDate() - i)
|
||
|
||
const label = `${date.getMonth() + 1}/${date.getDate()}`
|
||
const rate = Math.round(75 + Math.random() * 20) // 75-95%的随机完成率
|
||
|
||
labels.push(label)
|
||
rates.push(rate)
|
||
}
|
||
|
||
completionRateData.value = rates
|
||
completionRateLabels.value = labels
|
||
}
|
||
const generateMockActivities = () => {
|
||
// 生成模拟近期活动
|
||
recentActivities.value = [
|
||
{
|
||
type: 'assignment_submitted',
|
||
title: '张三提交了跑步训练作业',
|
||
time: '2小时前'
|
||
} as UTSJSONObject,
|
||
{
|
||
type: 'project_completed',
|
||
title: '李四完成了力量训练项目',
|
||
time: '4小时前'
|
||
} as UTSJSONObject,
|
||
{
|
||
type: 'new_record',
|
||
title: '王五创造了新的个人记录',
|
||
time: '6小时前'
|
||
} as UTSJSONObject
|
||
]
|
||
}
|
||
// Chart options for ak-charts
|
||
const completionRateChartOption = computed(() => {
|
||
return {
|
||
type: 'area',
|
||
data: completionRateData.value,
|
||
labels: completionRateLabels.value,
|
||
color: '#6366F1'
|
||
}
|
||
})
|
||
|
||
const performanceChartOption = computed(() => {
|
||
if (performanceData.value.length === 0) {
|
||
return {
|
||
type: 'horizontalBar',
|
||
data: [] as number[],
|
||
labels: [] as string[],
|
||
color: '#10B981'
|
||
}
|
||
}
|
||
const data: number[] = []
|
||
const labels: string[] = []
|
||
|
||
performanceData.value.forEach((item: UTSJSONObject) => {
|
||
const rangeValue = item.get('range')
|
||
const range = rangeValue != null ? rangeValue.toString() : ''
|
||
const countValue = item.get('count')
|
||
const count = countValue != null ? parseFloat(countValue.toString()) : 0
|
||
labels.push(range)
|
||
data.push(count)
|
||
})
|
||
|
||
return {
|
||
type: 'horizontalBar',
|
||
data: data,
|
||
labels: labels,
|
||
color: '#10B981'
|
||
}
|
||
})
|
||
const activityDistributionChartOption = computed(() => {
|
||
return {
|
||
type: 'doughnut',
|
||
data: activityDistributionData.value,
|
||
labels: activityDistributionLabels.value,
|
||
color: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF']
|
||
}
|
||
})
|
||
|
||
// Expose reactive state for template
|
||
const loadTeacherAnalytics = async () => {
|
||
try {
|
||
loading.value = true
|
||
error.value = null
|
||
|
||
const params = new UTSJSONObject()
|
||
params.set('teacher_id', teacherId.value == 'current_teacher_id' ? null : teacherId.value)
|
||
params.set('start_date', startDate.value ?? null)
|
||
params.set('end_date', endDate.value ?? null)
|
||
|
||
const result = await supa.from('').rpc('get_teacher_analytics', params).execute()
|
||
|
||
if (result.error != null) {
|
||
throw new Error(result.error.toString())
|
||
}
|
||
|
||
if (result.data != null) {
|
||
statisticsData.value = [result.data as UTSJSONObject]
|
||
updateOverviewCards()
|
||
}
|
||
} catch (err: any) {
|
||
console.error('获取教师统计数据失败:', err)
|
||
error.value = `获取统计数据失败: ${err.message ?? err.toString()}`
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const loadTopPerformers = async () => {
|
||
try {
|
||
const params = new UTSJSONObject()
|
||
params.set('teacher_id', teacherId.value == 'current_teacher_id' ? null : teacherId.value)
|
||
params.set('start_date', startDate.value ?? null)
|
||
params.set('end_date', endDate.value ?? null)
|
||
params.set('limit', 10)
|
||
|
||
const result = await supa.from('').rpc('get_top_performers', params).execute()
|
||
|
||
if (result.error != null) {
|
||
throw new Error(result.error.toString())
|
||
}
|
||
|
||
if (Array.isArray(result.data)) {
|
||
topPerformers.value = result.data as UTSJSONObject[]
|
||
} else {
|
||
topPerformers.value = []
|
||
}
|
||
} catch (err: any) {
|
||
console.error('获取优秀学员数据失败:', err)
|
||
// 不设置全局错误,避免覆盖主要数据错误
|
||
}
|
||
}
|
||
|
||
const loadChartData = async () => {
|
||
try {
|
||
const params = new UTSJSONObject()
|
||
params.set('teacher_id', teacherId.value == 'current_teacher_id' ? null : teacherId.value)
|
||
params.set('start_date', startDate.value ?? null)
|
||
params.set('end_date', endDate.value ?? null)
|
||
params.set('type', 'completion_rate')
|
||
|
||
const result = await supa.from('').rpc('get_chart_data', params).execute()
|
||
|
||
if (result.error != null) {
|
||
throw new Error(result.error.toString())
|
||
}
|
||
if (Array.isArray(result.data)) {
|
||
chartData.value = result.data as UTSJSONObject[]
|
||
processChartData(result.data as UTSJSONObject[])
|
||
} else {
|
||
chartData.value = []
|
||
generateMockChartData()
|
||
}
|
||
} catch (err: any) {
|
||
console.error('获取图表数据失败:', err)
|
||
generateMockChartData() // 生成模拟数据
|
||
}
|
||
}
|
||
|
||
const loadRecentActivities = async () => {
|
||
try {
|
||
const params = new UTSJSONObject()
|
||
params.set('teacher_id', teacherId.value == 'current_teacher_id' ? null : teacherId.value)
|
||
params.set('limit', 20)
|
||
|
||
const result = await supa.from('').rpc('get_recent_activities', params).execute()
|
||
|
||
if (result.error != null) {
|
||
throw new Error(result.error.toString())
|
||
}
|
||
|
||
if (Array.isArray(result.data)) {
|
||
recentActivities.value = result.data as UTSJSONObject[]
|
||
} else {
|
||
recentActivities.value = []
|
||
generateMockActivities() // 如果 RPC 失败,生成模拟数据
|
||
}
|
||
} catch (err: any) {
|
||
console.error('获取近期活动数据失败:', err)
|
||
generateMockActivities() // 生成模拟数据
|
||
}
|
||
}
|
||
|
||
const initializeDates = () => {
|
||
const now = new Date()
|
||
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
|
||
endDate.value = now.toISOString().split('T')[0]
|
||
startDate.value = thirtyDaysAgo.toISOString().split('T')[0]
|
||
}
|
||
|
||
const generateMockData = () => {
|
||
// 生成模拟图表数据
|
||
performanceData.value = [
|
||
{ range: '90-100分', count: 15 },
|
||
{ range: '80-89分', count: 25 },
|
||
{ range: '70-79分', count: 30 },
|
||
{ range: '60-69分', count: 20 },
|
||
{ range: '60分以下', count: 10 }
|
||
]
|
||
|
||
// 生成学生活跃度分布数据
|
||
activityDistributionData.value = [45, 25, 20, 8, 2]
|
||
activityDistributionLabels.value = ['高度活跃', '活跃', '一般', '较少活跃', '不活跃']
|
||
}
|
||
|
||
const loadAnalyticsData = async () => {
|
||
loading.value = true
|
||
error.value = null
|
||
|
||
// 并发加载所有数据
|
||
await Promise.all([
|
||
loadTeacherAnalytics(),
|
||
loadTopPerformers(),
|
||
loadChartData(),
|
||
loadRecentActivities()
|
||
])
|
||
// 生成模拟性能分布数据
|
||
generateMockData()
|
||
loading.value = false
|
||
}
|
||
|
||
const retryLoad = () => {
|
||
loadAnalyticsData()
|
||
}
|
||
|
||
const refreshData = () => {
|
||
loadAnalyticsData()
|
||
}
|
||
|
||
const onStartDateChange = (event: any) => {
|
||
// Since we're using v-model, the value is already updated
|
||
// Just trigger data reload
|
||
loadAnalyticsData()
|
||
}
|
||
const onEndDateChange = (event: any) => {
|
||
// Since we're using v-model, the value is already updated
|
||
// Just trigger data reload
|
||
loadAnalyticsData()
|
||
}
|
||
|
||
// UTSJSONObject safe access methods
|
||
const getPerformerName = (performer: UTSJSONObject): string => {
|
||
const name = performer.get('name')
|
||
return name != null ? name.toString() : '未知学员'
|
||
}
|
||
|
||
const getPerformerScore = (performer: UTSJSONObject): string => {
|
||
const score = performer.get('score')
|
||
const scoreNumber = score != null ? parseFloat(score.toString()) : 0
|
||
return scoreNumber.toFixed(1)
|
||
}
|
||
const getBadgeClass = (index: number): string => {
|
||
if (index === 0) return 'gold'
|
||
if (index === 1) return 'silver'
|
||
if (index === 2) return 'bronze'
|
||
return 'default'
|
||
}
|
||
|
||
const getBadgeText = (index: number): string => {
|
||
if (index === 0) return '金牌'
|
||
if (index === 1) return '银牌'
|
||
if (index === 2) return '铜牌'
|
||
return '优秀'
|
||
}
|
||
|
||
const getActivityIcon = (activity: UTSJSONObject): string => {
|
||
const type = activity.get('type')
|
||
const typeString = type != null ? type.toString() : ''
|
||
switch (typeString) {
|
||
case 'assignment_submitted': return 'file'
|
||
case 'project_completed': return 'trophy'
|
||
case 'new_record': return 'star'
|
||
default: return 'bell'
|
||
}
|
||
}
|
||
|
||
const getActivityTitle = (activity: UTSJSONObject): string => {
|
||
const title = activity.get('title')
|
||
return title != null ? title.toString() : ''
|
||
}
|
||
|
||
const getActivityTime = (activity: UTSJSONObject): string => {
|
||
const time = activity.get('time')
|
||
return time != null ? time.toString() : ''
|
||
}
|
||
|
||
// Lifecycle hooks
|
||
onMounted(() => {
|
||
initializeDates()
|
||
loadAnalyticsData()
|
||
// 如果没有真实数据,生成模拟图表数据
|
||
if (completionRateData.value.length === 0) {
|
||
generateMockChartData()
|
||
}
|
||
// Initialize screen width
|
||
screenWidth.value = uni.getSystemInfoSync().windowWidth
|
||
})
|
||
|
||
onResize((size) => {
|
||
screenWidth.value = size.size.windowWidth
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.analytics-container {
|
||
display: flex;
|
||
flex:1;
|
||
background-image: linear-gradient(to bottom right, #667eea, #764ba2);
|
||
padding-bottom: 40rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.header {
|
||
padding: 40rpx 30rpx 30rpx;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.title {
|
||
font-size: 48rpx;
|
||
font-weight: bold;
|
||
color: #FFFFFF;
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
.filter-bar {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.filter-bar .date-input {
|
||
margin-right: 20rpx;
|
||
}
|
||
|
||
.filter-bar .filter-btn {
|
||
margin-left: 0;
|
||
}
|
||
|
||
.date-input {
|
||
flex: 1;
|
||
min-width: 200rpx;
|
||
height: 70rpx;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border-radius: 15rpx;
|
||
padding: 0 20rpx;
|
||
color: #FFFFFF;
|
||
border: none;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.date-input::placeholder {
|
||
color: rgba(255, 255, 255, 0.7);
|
||
}
|
||
|
||
.refresh-btn {
|
||
height: 70rpx;
|
||
padding: 0 30rpx;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border-radius: 15rpx;
|
||
border: none;
|
||
color: #FFFFFF;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.refresh-btn:active {
|
||
background: rgba(255, 255, 255, 0.3);
|
||
}
|
||
|
||
.content {
|
||
flex: 1;
|
||
padding: 30rpx;
|
||
background: #F8FAFC;
|
||
border-radius: 30rpx 30rpx 0 0;
|
||
margin-top: 20rpx;
|
||
}
|
||
|
||
.content.large-screen {
|
||
padding: 40rpx;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
color: #1E293B;
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
/* Overview Cards */
|
||
.overview-section {
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.cards-grid {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
margin: 0 -10rpx;
|
||
}
|
||
|
||
.cards-grid .overview-card {
|
||
width: 44%;
|
||
flex: 0 0 44%;
|
||
margin: 0 10rpx 20rpx;
|
||
}
|
||
|
||
.cards-grid.large-grid .overview-card:nth-child(2n) {
|
||
margin-right: 20rpx;
|
||
}
|
||
|
||
.cards-grid.large-grid .overview-card:nth-child(4n) {
|
||
margin-right: 0;
|
||
}
|
||
|
||
.overview-card {
|
||
background: #FFFFFF;
|
||
padding: 30rpx;
|
||
border-radius: 20rpx;
|
||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||
align-items: center;
|
||
}
|
||
|
||
.card-value {
|
||
font-size: 42rpx;
|
||
font-weight: bold;
|
||
color: #6366F1;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
|
||
.card-label {
|
||
font-size: 26rpx;
|
||
color: #64748B;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
|
||
.card-change {
|
||
font-size: 24rpx;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.card-change.positive {
|
||
color: #10B981;
|
||
}
|
||
|
||
.card-change.negative {
|
||
color: #EF4444;
|
||
}
|
||
|
||
/* Charts Section */
|
||
.charts-section {
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.chart-container {
|
||
background: #FFFFFF;
|
||
padding: 30rpx;
|
||
border-radius: 20rpx;
|
||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.chart-title {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #1E293B;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.chart-content {
|
||
min-height: 300rpx;
|
||
}
|
||
|
||
.chart-canvas {
|
||
width: 100%;
|
||
height: 300rpx;
|
||
}
|
||
|
||
.chart-placeholder {
|
||
height: 300rpx;
|
||
background: #F1F5F9;
|
||
border-radius: 15rpx;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.chart-text {
|
||
color: #64748B;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
/* Performers Section */
|
||
.performers-section {
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.performers-list {
|
||
}
|
||
|
||
.performers-list .performer-card {
|
||
margin-bottom: 15rpx;
|
||
}
|
||
|
||
.performers-list .performer-card:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.performer-card {
|
||
background: #FFFFFF;
|
||
padding: 25rpx;
|
||
border-radius: 15rpx;
|
||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.performer-card .performer-avatar {
|
||
margin-right: 20rpx;
|
||
} .performer-rank {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
background: #F1F5F9;
|
||
border-radius: 30rpx;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.rank-text {
|
||
font-size: 24rpx;
|
||
font-weight: bold;
|
||
color: #64748B;
|
||
}
|
||
|
||
.performer-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.performer-name {
|
||
font-size: 30rpx;
|
||
font-weight: 400;
|
||
color: #1E293B;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.performer-score {
|
||
font-size: 26rpx;
|
||
color: #64748B;
|
||
}
|
||
.performer-badge {
|
||
padding: 8rpx 16rpx;
|
||
border-radius: 20rpx;
|
||
}
|
||
|
||
.performer-badge.gold {
|
||
background: #FEF3C7;
|
||
}
|
||
|
||
.performer-badge.silver {
|
||
background: #F3F4F6;
|
||
}
|
||
|
||
.performer-badge.bronze {
|
||
background: #FED7AA;
|
||
}
|
||
|
||
.performer-badge.default {
|
||
background: #EDE9FE;
|
||
}
|
||
|
||
.badge-text {
|
||
font-size: 22rpx;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.performer-badge.gold .badge-text {
|
||
color: #D97706;
|
||
}
|
||
|
||
.performer-badge.silver .badge-text {
|
||
color: #6B7280;
|
||
}
|
||
|
||
.performer-badge.bronze .badge-text {
|
||
color: #EA580C;
|
||
}
|
||
|
||
.performer-badge.default .badge-text {
|
||
color: #7C3AED;
|
||
}
|
||
|
||
/* Activities Section */
|
||
.activities-section {
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.activities-list {
|
||
}
|
||
|
||
.activities-list .activity-item {
|
||
margin-bottom: 15rpx;
|
||
}
|
||
|
||
.activities-list .activity-item:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.activity-item {
|
||
background: #FFFFFF;
|
||
padding: 25rpx;
|
||
border-radius: 15rpx;
|
||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.activity-item .activity-icon {
|
||
margin-right: 20rpx;
|
||
}
|
||
|
||
.activity-icon {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
background: #EDE9FE;
|
||
border-radius: 40rpx;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.activity-content {
|
||
flex: 1;
|
||
}
|
||
|
||
.activity-title {
|
||
font-size: 28rpx;
|
||
color: #1E293B;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.activity-time {
|
||
font-size: 24rpx;
|
||
color: #64748B;
|
||
}
|
||
|
||
/* Error State */
|
||
.error-container {
|
||
flex: 1;
|
||
justify-content: center;
|
||
align-items: center;
|
||
padding: 60rpx;
|
||
}
|
||
|
||
.error-text {
|
||
font-size: 32rpx;
|
||
color: #EF4444;
|
||
margin-bottom: 30rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.retry-btn {
|
||
padding: 20rpx 40rpx;
|
||
background: #6366F1;
|
||
color: #FFFFFF;
|
||
border: none;
|
||
border-radius: 15rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
</style>
|