数据统计
This commit is contained in:
@@ -0,0 +1,354 @@
|
||||
<template>
|
||||
<div class="task-card">
|
||||
<!-- 卡片头部 -->
|
||||
<div class="card-header">
|
||||
<div class="header-content">
|
||||
<h3 class="task-title">{{ task.learningTask.name }}</h3>
|
||||
<div class="task-meta">
|
||||
<!-- <div class="meta-item">
|
||||
<img class="icon" src="@/assets/imgs/usermange.svg" alt="部门" />
|
||||
<span class="meta-text">{{ getDeptName }}</span>
|
||||
</div> -->
|
||||
<div class="meta-item">
|
||||
<img class="icon" src="@/assets/imgs/calendar-icon.svg" alt="日期" />
|
||||
<span class="meta-text">截止: {{ formatDate(task.learningTask.endTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-badge" :class="getStatusClass(task.learningTask.status)">
|
||||
{{ getStatusText(task.learningTask.status, task.learningTask.startTime, task.learningTask.endTime) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="card-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">分配人数</div>
|
||||
<div class="stat-value">{{ task.totalTaskNum || 0 }}</div>
|
||||
</div>
|
||||
<div class="stat-item stat-completed">
|
||||
<div class="stat-label">已完成</div>
|
||||
<div class="stat-value stat-value-completed">{{ task.completedTaskNum || 0 }}</div>
|
||||
</div>
|
||||
<div class="stat-item stat-rate">
|
||||
<div class="stat-label">完成率</div>
|
||||
<div class="stat-value stat-value-rate">{{ completionRate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="card-progress">
|
||||
<div class="progress-header">
|
||||
<span class="progress-label">完成进度</span>
|
||||
<span class="progress-value">{{ completionRate }}</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: completionRate }"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="card-footer">
|
||||
<div class="card-actions">
|
||||
<button class="btn-link btn-primary" @click="$emit('view', task)">查看</button>
|
||||
<button class="btn-link btn-warning" @click="$emit('edit', task)" v-if="task.learningTask.status !== 1">编辑</button>
|
||||
<button class="btn-link btn-success" @click="$emit('publish', task)" v-if="task.learningTask.status !== 1">发布</button>
|
||||
<button class="btn-link btn-warning" @click="$emit('unpublish', task)" v-if="task.learningTask.status === 1">下架</button>
|
||||
<button class="btn-link btn-primary" @click="$emit('statistics', task)" v-if="task.learningTask.status !== 0">统计</button>
|
||||
<button class="btn-link btn-warning" @click="$emit('update-user', task)">修改人员</button>
|
||||
<button class="btn-link btn-danger" @click="$emit('delete', task)" v-if="task.learningTask.status === 0">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { TaskVO } from '@/types/study';
|
||||
|
||||
interface Props {
|
||||
task: TaskVO;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
defineEmits<{
|
||||
view: [task: TaskVO];
|
||||
edit: [task: TaskVO];
|
||||
publish: [task: TaskVO];
|
||||
unpublish: [task: TaskVO];
|
||||
statistics: [task: TaskVO];
|
||||
'update-user': [task: TaskVO];
|
||||
delete: [task: TaskVO];
|
||||
}>();
|
||||
|
||||
/**
|
||||
* 获取部门名称
|
||||
*/
|
||||
// const getDeptName = computed(() => {
|
||||
// if (props.task.taskUsers && props.task.taskUsers.length > 0) {
|
||||
// const deptNames = new Set(props.task.taskUsers.map(u => u.deptName).filter(Boolean));
|
||||
// if (deptNames.size === 0) return '全校';
|
||||
// if (deptNames.size === 1) return Array.from(deptNames)[0] || '全校';
|
||||
// return '多个部门';
|
||||
// }
|
||||
// return '全校';
|
||||
// });
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
function formatDate(dateStr?: string): string {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
}).replace(/\//g, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态样式类
|
||||
*/
|
||||
function getStatusClass(status?: number): string {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return 'status-draft';
|
||||
case 1:
|
||||
return 'status-ongoing';
|
||||
case 2:
|
||||
return 'status-ended';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态文本
|
||||
*/
|
||||
function getStatusText(status?: number, startTime?: string, endTime?: string): string {
|
||||
if (status === 0) return '草稿';
|
||||
if (status === 1) {
|
||||
const now = new Date();
|
||||
const start = new Date(startTime!);
|
||||
const end = new Date(endTime!);
|
||||
if (now >= start && now <= end) {
|
||||
return '进行中';
|
||||
} else if (now < start) {
|
||||
return '未开始';
|
||||
} else {
|
||||
return '已结束';
|
||||
}
|
||||
}
|
||||
if (status === 2) return '下架';
|
||||
return '未知';
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算完成率
|
||||
*/
|
||||
const completionRate = computed(() => {
|
||||
const total = props.task.totalTaskNum || 0;
|
||||
const completed = props.task.completedTaskNum || 0;
|
||||
if (total === 0) return '0%';
|
||||
const rate = Math.round((completed / total) * 100);
|
||||
return `${rate}%`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.task-card {
|
||||
background: #F9FAFB;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 14px;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片头部
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.task-title {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5em;
|
||||
letter-spacing: -1.953125%;
|
||||
color: #101828;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.task-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.meta-text {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 1.428571em;
|
||||
letter-spacing: -1.07421875%;
|
||||
color: #4A5565;
|
||||
}
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px 8px;
|
||||
height: 22px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.333333em;
|
||||
white-space: nowrap;
|
||||
|
||||
&.status-draft {
|
||||
background: #F4F4F5;
|
||||
color: #909399;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
&.status-ongoing {
|
||||
background: #DBEAFE;
|
||||
color: #1447E6;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
&.status-ended {
|
||||
background: #F0F9FF;
|
||||
color: #409EFF;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
// 统计信息
|
||||
.card-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: #FFFFFF;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 1.428571em;
|
||||
letter-spacing: -1.07421875%;
|
||||
color: #4A5565;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
line-height: 1.4em;
|
||||
letter-spacing: -2.24609375%;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.stat-value-completed {
|
||||
color: #00A63E;
|
||||
}
|
||||
|
||||
.stat-value-rate {
|
||||
color: #155DFC;
|
||||
}
|
||||
|
||||
// 进度条
|
||||
.card-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 1.428571em;
|
||||
letter-spacing: -1.07421875%;
|
||||
color: #4A5565;
|
||||
}
|
||||
|
||||
.progress-value {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 1.428571em;
|
||||
letter-spacing: -1.07421875%;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: rgba(3, 2, 19, 0.2);
|
||||
border-radius: 16777200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: #030213;
|
||||
border-radius: 16777200px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
// 卡片底部
|
||||
.card-footer {
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
// 操作按钮
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,631 @@
|
||||
<template>
|
||||
<div class="task-statistics">
|
||||
<!-- 总体学习进度 -->
|
||||
<div class="statistics-summary">
|
||||
<el-card shadow="hover" class="summary-card">
|
||||
<div class="summary-content">
|
||||
<div class="summary-item">
|
||||
<div class="summary-label">总学习人数</div>
|
||||
<div class="summary-value">{{ taskInfo.totalTaskNum || 0 }}</div>
|
||||
</div>
|
||||
<div class="summary-divider"></div>
|
||||
<div class="summary-item">
|
||||
<div class="summary-label">已完成人数</div>
|
||||
<div class="summary-value completed">{{ taskInfo.completedTaskNum || 0 }}</div>
|
||||
</div>
|
||||
<div class="summary-divider"></div>
|
||||
<div class="summary-item">
|
||||
<div class="summary-label">完成率</div>
|
||||
<div class="summary-value rate">
|
||||
{{ completionRate }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<div class="charts-container">
|
||||
<el-row :gutter="20">
|
||||
<!-- 学习时长分布 -->
|
||||
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
|
||||
<el-card shadow="hover" class="chart-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="chart-title">学习时长分布</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="durationChartRef" class="chart" v-loading="loading"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 学习进度分布 -->
|
||||
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
|
||||
<el-card shadow="hover" class="chart-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="chart-title">学习进度分布</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="progressChartRef" class="chart" v-loading="loading"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 排行榜区域 -->
|
||||
<div class="rankings-container">
|
||||
<el-row :gutter="20">
|
||||
<!-- 完成时间排行榜 -->
|
||||
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
|
||||
<el-card shadow="hover" class="ranking-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="chart-title">完成时间排行榜(前10名)</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="ranking-list" v-loading="loading">
|
||||
<div
|
||||
v-for="(item, index) in completionRanking"
|
||||
:key="item.userId"
|
||||
class="ranking-item"
|
||||
:class="{ 'top-three': index < 3 }"
|
||||
>
|
||||
<div class="ranking-number" :class="getRankClass(index)">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
<div class="ranking-info">
|
||||
<div class="ranking-name">{{ item.username }}</div>
|
||||
<div class="ranking-detail">
|
||||
完成时间: {{ formatDateTime(item.completeTime) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ranking-value">
|
||||
{{ formatDuration(item.completionDuration) }}
|
||||
</div>
|
||||
</div>
|
||||
<el-empty
|
||||
v-if="!loading && completionRanking.length === 0"
|
||||
description="暂无数据"
|
||||
:image-size="100"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 学习时长排行榜 -->
|
||||
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
|
||||
<el-card shadow="hover" class="ranking-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="chart-title">学习时长排行榜(前10名)</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="ranking-list" v-loading="loading">
|
||||
<div
|
||||
v-for="(item, index) in durationRanking"
|
||||
:key="item.userId"
|
||||
class="ranking-item"
|
||||
:class="{ 'top-three': index < 3 }"
|
||||
>
|
||||
<div class="ranking-number" :class="getRankClass(index)">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
<div class="ranking-info">
|
||||
<div class="ranking-name">{{ item.username }}</div>
|
||||
<div class="ranking-detail">累计学习时长</div>
|
||||
</div>
|
||||
<div class="ranking-value">
|
||||
{{ formatStudyDuration(item.totalDuration) }}
|
||||
</div>
|
||||
</div>
|
||||
<el-empty
|
||||
v-if="!loading && durationRanking.length === 0"
|
||||
description="暂无数据"
|
||||
:image-size="100"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import type { ECharts } from 'echarts';
|
||||
import { learningTaskApi } from '@/apis/study/learning-task';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
taskId: string;
|
||||
}>();
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false);
|
||||
const taskInfo = ref<any>({});
|
||||
const durationDistribution = ref<any[]>([]);
|
||||
const progressDistribution = ref<any[]>([]);
|
||||
const completionRanking = ref<any[]>([]);
|
||||
const durationRanking = ref<any[]>([]);
|
||||
|
||||
// 图表实例
|
||||
const durationChartRef = ref<HTMLElement>();
|
||||
const progressChartRef = ref<HTMLElement>();
|
||||
let durationChart: ECharts | null = null;
|
||||
let progressChart: ECharts | null = null;
|
||||
|
||||
// 计算完成率
|
||||
const completionRate = computed(() => {
|
||||
const total = taskInfo.value.totalTaskNum || 0;
|
||||
const completed = taskInfo.value.completedTaskNum || 0;
|
||||
if (total === 0) return 0;
|
||||
return ((completed / total) * 100).toFixed(1);
|
||||
});
|
||||
|
||||
// 获取排名样式类
|
||||
const getRankClass = (index: number) => {
|
||||
if (index === 0) return 'rank-first';
|
||||
if (index === 1) return 'rank-second';
|
||||
if (index === 2) return 'rank-third';
|
||||
return '';
|
||||
};
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDateTime = (dateTime: string) => {
|
||||
if (!dateTime) return '-';
|
||||
const date = new Date(dateTime);
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(
|
||||
date.getDate()
|
||||
).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// 格式化时长(秒转为易读格式)
|
||||
const formatDuration = (seconds: number) => {
|
||||
if (!seconds || seconds === 0) return '-';
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
const parts = [];
|
||||
if (days > 0) parts.push(`${days}天`);
|
||||
if (hours > 0) parts.push(`${hours}小时`);
|
||||
if (minutes > 0) parts.push(`${minutes}分钟`);
|
||||
|
||||
return parts.length > 0 ? parts.join('') : '0分钟';
|
||||
};
|
||||
|
||||
// 格式化学习时长
|
||||
const formatStudyDuration = (seconds: number) => {
|
||||
if (!seconds || seconds === 0) return '0分钟';
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
const parts = [];
|
||||
if (hours > 0) parts.push(`${hours}小时`);
|
||||
if (minutes > 0) parts.push(`${minutes}分钟`);
|
||||
|
||||
return parts.length > 0 ? parts.join('') : '0分钟';
|
||||
};
|
||||
|
||||
// 初始化学习时长分布图表
|
||||
const initDurationChart = () => {
|
||||
if (!durationChartRef.value) return;
|
||||
|
||||
durationChart = echarts.init(durationChartRef.value);
|
||||
|
||||
const categories = durationDistribution.value.map((item: any) => item.durationRange);
|
||||
const data = durationDistribution.value.map((item: any) => item.userCount);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
formatter: (params: any) => {
|
||||
const param = params[0];
|
||||
return `${param.name}<br/>人数: ${param.value}人`;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '15%',
|
||||
top: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: categories,
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
rotate: 30,
|
||||
fontSize: 12
|
||||
},
|
||||
name: '学习时长'
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '人数',
|
||||
minInterval: 1,
|
||||
axisLabel: {
|
||||
formatter: '{value}人'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '人数',
|
||||
type: 'bar',
|
||||
data: data,
|
||||
barMaxWidth: 60,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
|
||||
{ offset: 0, color: '#4facfe' },
|
||||
{ offset: 1, color: '#00f2fe' }
|
||||
]),
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
formatter: '{c}人'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
durationChart.setOption(option);
|
||||
};
|
||||
|
||||
// 初始化学习进度分布图表
|
||||
const initProgressChart = () => {
|
||||
if (!progressChartRef.value) return;
|
||||
|
||||
progressChart = echarts.init(progressChartRef.value);
|
||||
|
||||
const categories = progressDistribution.value.map((item: any) => item.progressRange);
|
||||
const data = progressDistribution.value.map((item: any) => item.userCount);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
formatter: (params: any) => {
|
||||
const param = params[0];
|
||||
return `${param.name}<br/>人数: ${param.value}人`;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '12%',
|
||||
top: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: categories,
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
rotate: 30,
|
||||
fontSize: 12
|
||||
},
|
||||
name: '学习进度'
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '人数',
|
||||
minInterval: 1,
|
||||
axisLabel: {
|
||||
formatter: '{value}人'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '人数',
|
||||
type: 'bar',
|
||||
data: data,
|
||||
barMaxWidth: 60,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
|
||||
{ offset: 0, color: '#a18cd1' },
|
||||
{ offset: 1, color: '#fbc2eb' }
|
||||
]),
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
formatter: '{c}人'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
progressChart.setOption(option);
|
||||
};
|
||||
|
||||
// 调整图表大小
|
||||
const resizeCharts = () => {
|
||||
durationChart?.resize();
|
||||
progressChart?.resize();
|
||||
};
|
||||
|
||||
// 获取任务基本信息
|
||||
const fetchTaskInfo = async () => {
|
||||
try {
|
||||
const result = await learningTaskApi.getTaskById(props.taskId);
|
||||
if (result.code === 200 && result.data) {
|
||||
taskInfo.value = result.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取任务信息失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取图表数据
|
||||
const fetchChartsData = async () => {
|
||||
try {
|
||||
const result = await learningTaskApi.getTaskStatisticsCharts(props.taskId);
|
||||
if (result.code === 200 && result.data) {
|
||||
durationDistribution.value = result.data.durationDistribution || [];
|
||||
progressDistribution.value = result.data.progressDistribution || [];
|
||||
|
||||
// 初始化图表
|
||||
setTimeout(() => {
|
||||
initDurationChart();
|
||||
initProgressChart();
|
||||
}, 100);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取图表数据失败:', error);
|
||||
ElMessage.error('获取图表数据失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 获取排行榜数据
|
||||
const fetchRankingsData = async () => {
|
||||
try {
|
||||
const result = await learningTaskApi.getTaskStatisticsRankings(props.taskId);
|
||||
if (result.code === 200 && result.data) {
|
||||
completionRanking.value = result.data.completionTimeRanking || [];
|
||||
durationRanking.value = result.data.durationRanking || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取排行榜数据失败:', error);
|
||||
ElMessage.error('获取排行榜数据失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 加载所有数据
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
await Promise.all([
|
||||
fetchTaskInfo(),
|
||||
fetchChartsData(),
|
||||
fetchRankingsData()
|
||||
]);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 监听taskId变化
|
||||
watch(() => props.taskId, () => {
|
||||
if (props.taskId) {
|
||||
loadData();
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', resizeCharts);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', resizeCharts);
|
||||
durationChart?.dispose();
|
||||
progressChart?.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.task-statistics {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.statistics-summary {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.summary-content {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.summary-value.completed {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.summary-value.rate {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.summary-divider {
|
||||
width: 1px;
|
||||
height: 60px;
|
||||
background: #dcdfe6;
|
||||
}
|
||||
|
||||
.charts-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.chart {
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rankings-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.ranking-card {
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ranking-list {
|
||||
min-height: 400px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.ranking-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.ranking-item:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.ranking-item.top-three {
|
||||
background: linear-gradient(to right, #fff9e6, #ffffff);
|
||||
}
|
||||
|
||||
.ranking-number {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: #e4e7ed;
|
||||
color: #606266;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
margin-right: 15px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ranking-number.rank-first {
|
||||
background: linear-gradient(135deg, #ffd700, #ffed4e);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(255, 215, 0, 0.4);
|
||||
}
|
||||
|
||||
.ranking-number.rank-second {
|
||||
background: linear-gradient(135deg, #c0c0c0, #e8e8e8);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(192, 192, 192, 0.4);
|
||||
}
|
||||
|
||||
.ranking-number.rank-third {
|
||||
background: linear-gradient(135deg, #cd7f32, #daa972);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(205, 127, 50, 0.4);
|
||||
}
|
||||
|
||||
.ranking-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ranking-name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ranking-detail {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ranking-value {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
margin-left: 15px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.summary-content {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.summary-divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.chart {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.ranking-list {
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as TaskCard } from './TaskCard.vue';
|
||||
export { default as TaskStatics } from './TaskStatics.vue';
|
||||
Reference in New Issue
Block a user