888 lines
25 KiB
Vue
888 lines
25 KiB
Vue
<template>
|
||
<DefaultLayout>
|
||
<div class="analytics-page">
|
||
<div class="container">
|
||
<div class="header-actions">
|
||
<a-select v-model:value="dateRange" class="date-range-select" @change="loadData">
|
||
<a-select-option value="week">近一周</a-select-option>
|
||
<a-select-option value="month">近一月</a-select-option>
|
||
<a-select-option value="quarter">近三月</a-select-option>
|
||
<a-select-option value="year">近一年</a-select-option>
|
||
<a-select-option value="all">全部</a-select-option>
|
||
</a-select>
|
||
<a-button type="primary" @click="exportData">
|
||
<template #icon><Icon name="download" size="sm" /></template>
|
||
导出报表
|
||
</a-button>
|
||
</div>
|
||
|
||
<a-spin :spinning="loading">
|
||
<!-- 总览卡片 -->
|
||
<div v-if="statistics" class="overview-section">
|
||
<h2 class="section-title">数据总览</h2>
|
||
<a-row :gutter="[16, 16]">
|
||
<a-col :xs="24" :sm="12" :md="6">
|
||
<div class="stat-card">
|
||
<div class="stat-icon">
|
||
<Icon name="document" size="xl" color="#31f08b" />
|
||
</div>
|
||
<div class="stat-content">
|
||
<div class="stat-label">总需求数</div>
|
||
<div class="stat-value">{{ overview.totalDemands }}</div>
|
||
<div class="stat-sub">今日: {{ overview.todayDemands }}</div>
|
||
</div>
|
||
</div>
|
||
</a-col>
|
||
<a-col :xs="24" :sm="12" :md="6">
|
||
<div class="stat-card">
|
||
<div class="stat-icon">
|
||
<Icon name="checkCircle" size="xl" color="#31f08b" />
|
||
</div>
|
||
<div class="stat-content">
|
||
<div class="stat-label">已完成</div>
|
||
<div class="stat-value">{{ overview.completedDemands }}</div>
|
||
<div class="stat-sub">完成率: {{ completionRate }}%</div>
|
||
</div>
|
||
</div>
|
||
</a-col>
|
||
<a-col :xs="24" :sm="12" :md="6">
|
||
<div class="stat-card">
|
||
<div class="stat-icon">
|
||
<Icon name="trophy" size="xl" color="#31f08b" />
|
||
</div>
|
||
<div class="stat-content">
|
||
<div class="stat-label">总积分消耗</div>
|
||
<div class="stat-value">{{ overview.totalScoreUsed }}</div>
|
||
<div class="stat-sub">今日: {{ overview.todayScoreUsed }}</div>
|
||
</div>
|
||
</div>
|
||
</a-col>
|
||
<a-col :xs="24" :sm="12" :md="6">
|
||
<div class="stat-card">
|
||
<div class="stat-icon">
|
||
<Icon name="star" size="xl" color="#31f08b" />
|
||
</div>
|
||
<div class="stat-content">
|
||
<div class="stat-label">平均满意度</div>
|
||
<div class="stat-value">{{ overview.avgSatisfaction?.toFixed(1) || '0.0' }}</div>
|
||
<div class="stat-sub">平均完成时长: {{ overview.avgCompletionTime?.toFixed(1) || '0' }}h</div>
|
||
</div>
|
||
</div>
|
||
</a-col>
|
||
</a-row>
|
||
</div>
|
||
|
||
<!-- 趋势图表 -->
|
||
<div v-if="statistics" class="chart-section">
|
||
<h2 class="section-title">需求趋势</h2>
|
||
<div class="chart-card">
|
||
<div ref="trendChartRef" style="width: 100%; height: 400px;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 类型分布 -->
|
||
<div v-if="statistics" class="chart-section">
|
||
<a-row :gutter="16">
|
||
<a-col :xs="24" :md="12">
|
||
<h2 class="section-title">需求类型分布</h2>
|
||
<div class="chart-card">
|
||
<div ref="typeChartRef" style="width: 100%; height: 350px;"></div>
|
||
</div>
|
||
</a-col>
|
||
<a-col :xs="24" :md="12">
|
||
<h2 class="section-title">部门积分使用情况</h2>
|
||
<div class="chart-card">
|
||
<div ref="deptChartRef" style="width: 100%; height: 350px;"></div>
|
||
</div>
|
||
</a-col>
|
||
</a-row>
|
||
</div>
|
||
|
||
<!-- 用户排行 -->
|
||
<div v-if="statistics && statistics.userStats" class="table-section">
|
||
<h2 class="section-title">用户需求排行</h2>
|
||
<div class="table-card">
|
||
<a-table
|
||
:columns="userColumns"
|
||
:data-source="statistics.userStats || []"
|
||
:pagination="{ pageSize: 10 }"
|
||
row-key="userId"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'rank'">
|
||
<span class="rank-badge" :class="getRankClass(record.rank)">
|
||
{{ record.rank }}
|
||
</span>
|
||
</template>
|
||
<template v-else-if="column.key === 'completionRate'">
|
||
<a-progress
|
||
:percent="Math.round(record.completionRate)"
|
||
:stroke-color="getProgressColor(record.completionRate)"
|
||
size="small"
|
||
/>
|
||
</template>
|
||
<template v-else-if="column.key === 'satisfactionRate'">
|
||
<a-rate :value="record.satisfactionRate / 20" disabled allow-half />
|
||
<span style="margin-left: 8px;">{{ record.satisfactionRate.toFixed(1) }}%</span>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 部门统计 -->
|
||
<div v-if="statistics && statistics.deptStats" class="table-section">
|
||
<h2 class="section-title">部门需求统计</h2>
|
||
<div class="table-card">
|
||
<a-table
|
||
:columns="deptColumns"
|
||
:data-source="statistics.deptStats || []"
|
||
:pagination="{ pageSize: 10 }"
|
||
row-key="deptId"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'alertLevel'">
|
||
<a-tag :color="getAlertColor(record.alertLevel)">
|
||
{{ getAlertText(record.alertLevel) }}
|
||
</a-tag>
|
||
</template>
|
||
<template v-else-if="column.key === 'utilizationRate'">
|
||
<a-progress
|
||
:percent="Math.round(record.utilizationRate)"
|
||
:stroke-color="getUtilizationColor(record.utilizationRate)"
|
||
size="small"
|
||
/>
|
||
</template>
|
||
<template v-else-if="column.key === 'scoreStatus'">
|
||
<div class="score-status">
|
||
<div>剩余: <strong>{{ record.remainingScore }}</strong></div>
|
||
<div class="score-limits">
|
||
<span>日: {{ record.dailyUsed }}/{{ record.dailyLimit }}</span>
|
||
<span>周: {{ record.weeklyUsed }}/{{ record.weeklyLimit }}</span>
|
||
<span>月: {{ record.monthlyUsed }}/{{ record.monthlyLimit }}</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
</div>
|
||
</a-spin>
|
||
</div>
|
||
</div>
|
||
</DefaultLayout>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, onMounted, computed, nextTick } from 'vue'
|
||
import DefaultLayout from '@/layouts/DefaultLayout.vue'
|
||
import { getDemandStatistics, type DemandStatistics } from '@/api/analytics'
|
||
import { message } from 'ant-design-vue'
|
||
import * as echarts from 'echarts'
|
||
import Icon from '@/components/Icon.vue'
|
||
|
||
const loading = ref(false)
|
||
const dateRange = ref('month')
|
||
// 初始化 statistics 为默认值,避免 undefined 错误
|
||
// 使用 reactive 确保响应式对象结构始终完整
|
||
const statistics = reactive<DemandStatistics>({
|
||
overview: {
|
||
totalDemands: 0,
|
||
todayDemands: 0,
|
||
weekDemands: 0,
|
||
monthDemands: 0,
|
||
completedDemands: 0,
|
||
pendingDemands: 0,
|
||
canceledDemands: 0,
|
||
totalScoreUsed: 0,
|
||
todayScoreUsed: 0,
|
||
weekScoreUsed: 0,
|
||
monthScoreUsed: 0,
|
||
avgCompletionTime: 0,
|
||
avgSatisfaction: 0
|
||
},
|
||
trendData: [],
|
||
typeDistribution: [],
|
||
userStats: [],
|
||
deptStats: []
|
||
})
|
||
|
||
// ECharts实例引用
|
||
const trendChartRef = ref<HTMLElement>()
|
||
const typeChartRef = ref<HTMLElement>()
|
||
const deptChartRef = ref<HTMLElement>()
|
||
|
||
// 安全访问 overview 的计算属性
|
||
const overview = computed(() => {
|
||
return statistics?.overview || {
|
||
totalDemands: 0,
|
||
todayDemands: 0,
|
||
weekDemands: 0,
|
||
monthDemands: 0,
|
||
completedDemands: 0,
|
||
pendingDemands: 0,
|
||
canceledDemands: 0,
|
||
totalScoreUsed: 0,
|
||
todayScoreUsed: 0,
|
||
weekScoreUsed: 0,
|
||
monthScoreUsed: 0,
|
||
avgCompletionTime: 0,
|
||
avgSatisfaction: 0
|
||
}
|
||
})
|
||
|
||
// 计算完成率
|
||
const completionRate = computed(() => {
|
||
const ov = overview.value
|
||
const completedDemands = ov.completedDemands || 0
|
||
const totalDemands = ov.totalDemands || 0
|
||
if (totalDemands === 0) return '0.0'
|
||
return ((completedDemands / totalDemands) * 100).toFixed(1)
|
||
})
|
||
|
||
// 用户表格列定义
|
||
const userColumns = [
|
||
{ title: '排名', key: 'rank', width: 80, customRender: (_: any, __: any, index: number) => index + 1 },
|
||
{ title: '用户名', dataIndex: 'userName', key: 'userName' },
|
||
{ title: '部门', dataIndex: 'deptName', key: 'deptName' },
|
||
{ title: '总需求', dataIndex: 'totalDemands', key: 'totalDemands', sorter: (a: any, b: any) => a.totalDemands - b.totalDemands },
|
||
{ title: '已完成', dataIndex: 'completedDemands', key: 'completedDemands' },
|
||
{ title: '进行中', dataIndex: 'pendingDemands', key: 'pendingDemands' },
|
||
{ title: '总积分', dataIndex: 'totalScore', key: 'totalScore', sorter: (a: any, b: any) => a.totalScore - b.totalScore },
|
||
{ title: '完成率', key: 'completionRate' },
|
||
{ title: '满意度', key: 'satisfactionRate' }
|
||
]
|
||
|
||
// 部门表格列定义
|
||
const deptColumns = [
|
||
{ title: '部门名称', dataIndex: 'deptName', key: 'deptName' },
|
||
{ title: '总需求', dataIndex: 'totalDemands', key: 'totalDemands', sorter: (a: any, b: any) => a.totalDemands - b.totalDemands },
|
||
{ title: '已完成', dataIndex: 'completedDemands', key: 'completedDemands' },
|
||
{ title: '进行中', dataIndex: 'pendingDemands', key: 'pendingDemands' },
|
||
{ title: '总积分', dataIndex: 'totalScore', key: 'totalScore' },
|
||
{ title: '积分状态', key: 'scoreStatus', width: 200 },
|
||
{ title: '使用率', key: 'utilizationRate', width: 150 },
|
||
{ title: '预警', key: 'alertLevel', width: 100 }
|
||
]
|
||
|
||
// 加载数据
|
||
const loadData = async () => {
|
||
loading.value = true
|
||
try {
|
||
console.log('Loading analytics data...')
|
||
const data = await getDemandStatistics(dateRange.value)
|
||
console.log('Received data:', data)
|
||
|
||
// 确保数据结构完整,如果缺少字段则使用默认值
|
||
// 使用 Object.assign 更新 reactive 对象,保持响应式
|
||
const defaultOverview = {
|
||
totalDemands: 0,
|
||
todayDemands: 0,
|
||
weekDemands: 0,
|
||
monthDemands: 0,
|
||
completedDemands: 0,
|
||
pendingDemands: 0,
|
||
canceledDemands: 0,
|
||
totalScoreUsed: 0,
|
||
todayScoreUsed: 0,
|
||
weekScoreUsed: 0,
|
||
monthScoreUsed: 0,
|
||
avgCompletionTime: 0,
|
||
avgSatisfaction: 0
|
||
}
|
||
|
||
// 确保 data 和 data.overview 存在
|
||
if (!data) {
|
||
console.warn('Data is null or undefined')
|
||
Object.assign(statistics, {
|
||
overview: defaultOverview,
|
||
trendData: [],
|
||
typeDistribution: [],
|
||
userStats: [],
|
||
deptStats: []
|
||
})
|
||
} else {
|
||
// 合并 overview,确保所有字段都存在
|
||
const mergedOverview = {
|
||
...defaultOverview,
|
||
...(data.overview || {})
|
||
}
|
||
|
||
Object.assign(statistics, {
|
||
overview: mergedOverview,
|
||
trendData: Array.isArray(data.trendData) ? data.trendData : [],
|
||
typeDistribution: Array.isArray(data.typeDistribution) ? data.typeDistribution : [],
|
||
userStats: Array.isArray(data.userStats) ? data.userStats : [],
|
||
deptStats: Array.isArray(data.deptStats) ? data.deptStats : []
|
||
})
|
||
}
|
||
|
||
// 确保 overview 对象始终存在
|
||
if (!statistics.overview) {
|
||
console.warn('Overview is missing, creating default')
|
||
statistics.overview = defaultOverview
|
||
}
|
||
|
||
console.log('Statistics after update:', statistics)
|
||
await nextTick()
|
||
renderCharts()
|
||
} catch (error) {
|
||
message.error('加载数据失败')
|
||
console.error('加载数据分析数据失败:', error)
|
||
// 重置为默认值以避免渲染错误
|
||
Object.assign(statistics, {
|
||
overview: {
|
||
totalDemands: 0,
|
||
todayDemands: 0,
|
||
weekDemands: 0,
|
||
monthDemands: 0,
|
||
completedDemands: 0,
|
||
pendingDemands: 0,
|
||
canceledDemands: 0,
|
||
totalScoreUsed: 0,
|
||
todayScoreUsed: 0,
|
||
weekScoreUsed: 0,
|
||
monthScoreUsed: 0,
|
||
avgCompletionTime: 0,
|
||
avgSatisfaction: 0
|
||
},
|
||
trendData: [],
|
||
typeDistribution: [],
|
||
userStats: [],
|
||
deptStats: []
|
||
})
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 渲染图表
|
||
const renderCharts = () => {
|
||
if (!statistics) return
|
||
|
||
// 趋势图
|
||
if (trendChartRef.value && statistics.trendData && Array.isArray(statistics.trendData)) {
|
||
const trendChart = echarts.init(trendChartRef.value)
|
||
trendChart.setOption({
|
||
backgroundColor: 'transparent',
|
||
textStyle: {
|
||
color: '#ECECEC'
|
||
},
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: '#2A2A2A',
|
||
borderColor: '#3A3A3A',
|
||
textStyle: { color: '#ECECEC' }
|
||
},
|
||
legend: {
|
||
data: ['需求数量', '完成数量', '积分消耗'],
|
||
textStyle: { color: '#ECECEC' }
|
||
},
|
||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: statistics.trendData.map(d => d.date),
|
||
axisLine: { lineStyle: { color: '#3A3A3A' } },
|
||
axisLabel: { color: '#AFAFAF' }
|
||
},
|
||
yAxis: [
|
||
{
|
||
type: 'value',
|
||
name: '数量',
|
||
axisLine: { lineStyle: { color: '#3A3A3A' } },
|
||
axisLabel: { color: '#AFAFAF' },
|
||
splitLine: { lineStyle: { color: '#262626' } }
|
||
},
|
||
{
|
||
type: 'value',
|
||
name: '积分',
|
||
position: 'right',
|
||
axisLine: { lineStyle: { color: '#3A3A3A' } },
|
||
axisLabel: { color: '#AFAFAF' },
|
||
splitLine: { lineStyle: { color: '#262626' } }
|
||
}
|
||
],
|
||
series: [
|
||
{
|
||
name: '需求数量',
|
||
type: 'line',
|
||
data: statistics.trendData.map(d => d.demandCount || 0),
|
||
smooth: true
|
||
},
|
||
{
|
||
name: '完成数量',
|
||
type: 'line',
|
||
data: statistics.trendData.map(d => d.completedCount || 0),
|
||
smooth: true
|
||
},
|
||
{
|
||
name: '积分消耗',
|
||
type: 'bar',
|
||
yAxisIndex: 1,
|
||
data: statistics.trendData.map(d => d.scoreUsed || 0)
|
||
}
|
||
]
|
||
})
|
||
}
|
||
|
||
// 类型分布饼图
|
||
if (typeChartRef.value && statistics.typeDistribution && Array.isArray(statistics.typeDistribution)) {
|
||
const typeChart = echarts.init(typeChartRef.value)
|
||
typeChart.setOption({
|
||
backgroundColor: 'transparent',
|
||
textStyle: {
|
||
color: '#ECECEC'
|
||
},
|
||
tooltip: {
|
||
trigger: 'item',
|
||
backgroundColor: '#2A2A2A',
|
||
borderColor: '#3A3A3A',
|
||
textStyle: { color: '#ECECEC' }
|
||
},
|
||
legend: {
|
||
orient: 'vertical',
|
||
left: 'left',
|
||
textStyle: { color: '#ECECEC' }
|
||
},
|
||
series: [
|
||
{
|
||
type: 'pie',
|
||
radius: '60%',
|
||
data: statistics.typeDistribution.map(d => ({
|
||
name: d.typeName || '未知',
|
||
value: d.count || 0
|
||
})),
|
||
emphasis: {
|
||
itemStyle: {
|
||
shadowBlur: 10,
|
||
shadowOffsetX: 0,
|
||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||
}
|
||
}
|
||
}
|
||
]
|
||
})
|
||
}
|
||
|
||
// 部门积分柱状图
|
||
if (deptChartRef.value && statistics.deptStats && Array.isArray(statistics.deptStats)) {
|
||
const deptChart = echarts.init(deptChartRef.value)
|
||
deptChart.setOption({
|
||
backgroundColor: 'transparent',
|
||
textStyle: {
|
||
color: '#ECECEC'
|
||
},
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: '#2A2A2A',
|
||
borderColor: '#3A3A3A',
|
||
textStyle: { color: '#ECECEC' }
|
||
},
|
||
legend: {
|
||
data: ['总积分', '剩余积分'],
|
||
textStyle: { color: '#ECECEC' }
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: statistics.deptStats.map(d => d.deptName || '未知'),
|
||
axisLabel: { rotate: 30, color: '#AFAFAF' },
|
||
axisLine: { lineStyle: { color: '#3A3A3A' } }
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
axisLine: { lineStyle: { color: '#3A3A3A' } },
|
||
axisLabel: { color: '#AFAFAF' },
|
||
splitLine: { lineStyle: { color: '#262626' } }
|
||
},
|
||
series: [
|
||
{
|
||
name: '总积分',
|
||
type: 'bar',
|
||
data: statistics.deptStats.map(d => d.totalScore || 0)
|
||
},
|
||
{
|
||
name: '剩余积分',
|
||
type: 'bar',
|
||
data: statistics.deptStats.map(d => d.remainingScore || 0)
|
||
}
|
||
]
|
||
})
|
||
}
|
||
}
|
||
|
||
// 工具函数
|
||
const getRankClass = (rank: number) => {
|
||
if (rank === 1) return 'rank-1'
|
||
if (rank === 2) return 'rank-2'
|
||
if (rank === 3) return 'rank-3'
|
||
return ''
|
||
}
|
||
|
||
const getProgressColor = (rate: number) => {
|
||
if (rate >= 80) return '#10A37F' // $color-success
|
||
if (rate >= 60) return '#3B82F6' // $color-info
|
||
if (rate >= 40) return '#FFA500' // $color-warning
|
||
return '#EF4444' // $color-error
|
||
}
|
||
|
||
const getAlertColor = (level: string) => {
|
||
switch (level) {
|
||
case 'safe': return 'success'
|
||
case 'warning': return 'warning'
|
||
case 'danger': return 'error'
|
||
default: return 'default'
|
||
}
|
||
}
|
||
|
||
const getAlertText = (level: string) => {
|
||
switch (level) {
|
||
case 'safe': return '正常'
|
||
case 'warning': return '预警'
|
||
case 'danger': return '紧急'
|
||
default: return '-'
|
||
}
|
||
}
|
||
|
||
const getUtilizationColor = (rate: number) => {
|
||
if (rate >= 90) return '#EF4444' // $color-error
|
||
if (rate >= 70) return '#FFA500' // $color-warning
|
||
return '#10A37F' // $color-success
|
||
}
|
||
|
||
const exportData = () => {
|
||
message.info('导出功能开发中...')
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadData()
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.analytics-page {
|
||
min-height: 100vh;
|
||
padding: $space-xl 0;
|
||
background: $bg-primary;
|
||
}
|
||
|
||
.container {
|
||
max-width: $container-max-width;
|
||
margin: 0 auto;
|
||
padding: 0 $space-xl;
|
||
|
||
@media (max-width: $breakpoint-tablet) {
|
||
padding: 0 $space-lg;
|
||
}
|
||
|
||
@media (max-width: $breakpoint-mobile) {
|
||
padding: 0 $space-md;
|
||
}
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
gap: $space-md;
|
||
margin-bottom: $space-2xl;
|
||
|
||
.date-range-select {
|
||
:deep(.ant-select-selector) {
|
||
height: 40px !important;
|
||
line-height: 40px !important;
|
||
min-width: 120px;
|
||
}
|
||
|
||
:deep(.ant-select-selection-item),
|
||
:deep(.ant-select-selection-placeholder) {
|
||
line-height: 38px !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
.section-title {
|
||
font-size: $font-size-xl;
|
||
font-weight: $font-weight-semibold;
|
||
color: $text-primary;
|
||
margin-bottom: $space-lg;
|
||
}
|
||
|
||
.overview-section {
|
||
margin-bottom: $space-2xl;
|
||
}
|
||
|
||
.stat-card {
|
||
background: $bg-card;
|
||
border: 1px solid $border-primary;
|
||
border-radius: $radius-lg;
|
||
padding: $space-lg;
|
||
display: flex;
|
||
gap: $space-lg;
|
||
transition: all $transition-base $ease-out;
|
||
|
||
&:hover {
|
||
border-color: $border-color;
|
||
background: $bg-card-hover;
|
||
box-shadow: $shadow-base;
|
||
}
|
||
|
||
.stat-icon {
|
||
width: 60px;
|
||
height: 60px;
|
||
border-radius: $radius-md;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: transparent;
|
||
font-size: 28px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.stat-content {
|
||
flex: 1;
|
||
|
||
.stat-label {
|
||
font-size: $font-size-sm;
|
||
color: $text-secondary;
|
||
margin-bottom: $space-xs;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: $font-size-2xl;
|
||
font-weight: $font-weight-bold;
|
||
color: $text-primary;
|
||
margin-bottom: $space-xs;
|
||
}
|
||
|
||
.stat-sub {
|
||
font-size: $font-size-sm;
|
||
color: $text-tertiary;
|
||
}
|
||
}
|
||
}
|
||
|
||
.chart-section {
|
||
margin-bottom: $space-2xl;
|
||
}
|
||
|
||
.chart-card {
|
||
background: $bg-card;
|
||
border: 1px solid $border-primary;
|
||
border-radius: $radius-lg;
|
||
padding: $space-xl;
|
||
transition: all $transition-base $ease-out;
|
||
|
||
&:hover {
|
||
border-color: $border-color;
|
||
background: $bg-card-hover;
|
||
}
|
||
}
|
||
|
||
.table-section {
|
||
margin-bottom: $space-2xl;
|
||
}
|
||
|
||
.table-card {
|
||
background: $bg-card;
|
||
border: 1px solid $border-primary;
|
||
border-radius: $radius-lg;
|
||
overflow: hidden;
|
||
transition: all $transition-base $ease-out;
|
||
|
||
&:hover {
|
||
border-color: $border-color;
|
||
}
|
||
}
|
||
|
||
.rank-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 50%;
|
||
font-weight: $font-weight-bold;
|
||
background: $bg-tertiary;
|
||
color: $text-secondary;
|
||
|
||
&.rank-1 {
|
||
background: rgba(#31f08b, 0.15);
|
||
color: #31f08b;
|
||
border: 1px solid rgba(#31f08b, 0.3);
|
||
}
|
||
|
||
&.rank-2 {
|
||
background: rgba(#31f08b, 0.1);
|
||
color: #31f08b;
|
||
border: 1px solid rgba(#31f08b, 0.2);
|
||
}
|
||
|
||
&.rank-3 {
|
||
background: rgba(#31f08b, 0.08);
|
||
color: #31f08b;
|
||
border: 1px solid rgba(#31f08b, 0.15);
|
||
}
|
||
}
|
||
|
||
.score-status {
|
||
.score-limits {
|
||
display: flex;
|
||
gap: $space-sm;
|
||
font-size: $font-size-xs;
|
||
color: $text-tertiary;
|
||
margin-top: $space-xs;
|
||
|
||
span {
|
||
background: $bg-tertiary;
|
||
padding: 2px 6px;
|
||
border-radius: $radius-sm;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Ant Design 组件深色主题覆盖
|
||
:deep(.ant-select) {
|
||
.ant-select-selector {
|
||
background: $bg-tertiary !important;
|
||
border-color: $border-color !important;
|
||
color: $text-primary !important;
|
||
}
|
||
|
||
.ant-select-selection-item {
|
||
color: $text-primary !important;
|
||
}
|
||
|
||
.ant-select-arrow {
|
||
color: $text-secondary !important;
|
||
}
|
||
}
|
||
|
||
:deep(.ant-select-dropdown) {
|
||
background: $bg-secondary !important;
|
||
border-color: $border-color !important;
|
||
|
||
.ant-select-item {
|
||
color: $text-primary !important;
|
||
|
||
&:hover {
|
||
background: $bg-tertiary !important;
|
||
}
|
||
|
||
&.ant-select-item-option-selected {
|
||
background: rgba($brand-primary, 0.1) !important;
|
||
color: $brand-primary !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
:deep(.ant-table) {
|
||
background: transparent !important;
|
||
color: $text-primary !important;
|
||
|
||
.ant-table-thead > tr > th {
|
||
background: $bg-tertiary !important;
|
||
border-color: $border-color !important;
|
||
color: $text-primary !important;
|
||
font-weight: $font-weight-semibold;
|
||
}
|
||
|
||
.ant-table-tbody > tr {
|
||
background: transparent !important;
|
||
|
||
&:hover > td {
|
||
background: $bg-tertiary !important;
|
||
}
|
||
|
||
> td {
|
||
border-color: $border-color !important;
|
||
color: $text-primary !important;
|
||
}
|
||
}
|
||
|
||
.ant-table-pagination {
|
||
.ant-pagination-item {
|
||
background: $bg-tertiary !important;
|
||
border-color: $border-primary !important;
|
||
|
||
a {
|
||
color: $text-primary !important;
|
||
}
|
||
|
||
&.ant-pagination-item-active {
|
||
background: $brand-primary !important;
|
||
border-color: $brand-primary !important;
|
||
|
||
a {
|
||
color: #000000 !important; // 当前页文字为黑色
|
||
}
|
||
}
|
||
}
|
||
|
||
.ant-pagination-prev,
|
||
.ant-pagination-next {
|
||
.ant-pagination-item-link {
|
||
background: $bg-tertiary !important;
|
||
border-color: $border-primary !important;
|
||
color: $text-primary !important;
|
||
}
|
||
}
|
||
|
||
.ant-pagination-options {
|
||
.ant-select {
|
||
.ant-select-selector {
|
||
background: $bg-tertiary !important;
|
||
border-color: $border-primary !important;
|
||
color: $text-primary !important;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
:deep(.ant-progress) {
|
||
.ant-progress-bg {
|
||
background-color: $brand-primary !important;
|
||
}
|
||
|
||
.ant-progress-text {
|
||
color: $text-primary !important;
|
||
}
|
||
}
|
||
|
||
:deep(.ant-rate) {
|
||
.ant-rate-star {
|
||
color: $text-tertiary !important;
|
||
|
||
&.ant-rate-star-full,
|
||
&.ant-rate-star-half .ant-rate-star-first {
|
||
color: $color-warning !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
:deep(.ant-tag) {
|
||
background: $bg-tertiary !important;
|
||
border-color: $border-color !important;
|
||
color: $text-primary !important;
|
||
|
||
&.ant-tag-success {
|
||
background: rgba($color-success, 0.1) !important;
|
||
border-color: $color-success !important;
|
||
color: $color-success !important;
|
||
}
|
||
|
||
&.ant-tag-warning {
|
||
background: rgba($color-warning, 0.1) !important;
|
||
border-color: $color-warning !important;
|
||
color: $color-warning !important;
|
||
}
|
||
|
||
&.ant-tag-error {
|
||
background: rgba($color-error, 0.1) !important;
|
||
border-color: $color-error !important;
|
||
color: $color-error !important;
|
||
}
|
||
}
|
||
</style>
|
||
|