Files
schoolNews/schoolNewsWeb/src/views/user/study-plan/StudyTasksView.vue
2025-12-04 14:23:49 +08:00

686 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<StudyPlanLayout category-name="学习任务">
<!-- 主要内容区 -->
<div class="main-content">
<div class="container">
<!-- 任务进度 -->
<h2 class="section-title">任务进度</h2>
<div class="progress-card">
<div class="task-stats">
<div class="stat-card pending">
<div class="stat-content">
<div class="stat-title">待完成任务</div>
<div class="stat-number">{{ pendingCount }}</div>
</div>
<div class="stat-icon pending-icon">
<el-icon><img src="@/assets/imgs/learningtask.svg" /></el-icon>
</div>
</div>
<div class="stat-card completed">
<div class="stat-content">
<div class="stat-title">已完成任务</div>
<div class="stat-number">{{ completedCount }}</div>
</div>
<div class="stat-icon completed-icon">
<el-icon><img src="@/assets/imgs/finishtask.svg" /></el-icon>
</div>
</div>
</div>
<div class="progress-info">
<div class="progress-header">
<span class="progress-text">当前学习进度{{ completedCount }}/{{ totalCount }}</span>
<!-- <span class="progress-percent">{{ progressPercent }}%</span> -->
<!-- <span class="progress-level">{{ userLevel }}</span> -->
</div>
<div class="progress-bar-container">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: progressPercent + '%' }"></div>
</div>
</div>
</div>
</div>
<!-- 任务列表 -->
<h2 class="section-title">任务列表</h2>
<div v-if="loading" class="loading-container">
<el-skeleton :rows="6" animated />
</div>
<div v-else-if="taskList.length > 0" class="task-grid">
<div
v-for="task in taskList"
:key="task.taskID"
class="task-card"
>
<!-- 背景装饰 -->
<div class="task-bg-decoration"></div>
<!-- 内容容器 -->
<div class="task-content">
<!-- 顶部信息区 -->
<div class="task-header">
<!-- 状态标签小标签自适应宽度 -->
<div
class="status-tag"
:class="{
'status-pending': task.status === 0,
'status-processing': task.status === 1,
'status-completed': task.status === 2
}"
>
{{ task.status === 0 ? '待完成' : task.status === 1 ? '进行中' : '已完成' }}
</div>
<!-- 任务标题 -->
<h3 class="task-title">{{ task.name }}</h3>
</div>
<!-- 任务描述 -->
<p class="task-desc">{{ task.description || '暂无描述' }}</p>
<!-- 学习进度 -->
<div class="task-progress">
<div class="progress-header">
<span class="progress-label">学习进度</span>
<span class="progress-value">{{ getTaskProgress(task) }}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: getTaskProgress(task) + '%' }"></div>
</div>
</div>
<!-- 任务底部信息 -->
<div class="task-footer">
<div class="task-time">
<el-icon class="calendar-icon"><Calendar /></el-icon>
<span>任务时间{{ formatDate(task.startTime) }}-{{ formatDate(task.endTime) }}</span>
<span
v-if="getDeadlineStatus(task).show"
class="deadline-text"
:class="'deadline-' + getDeadlineStatus(task).type"
>
{{ getDeadlineStatus(task).text }}
</span>
</div>
<button
class="start-btn"
@click.stop="handleTaskClick(task)"
>
{{ task.status === 0 ? '开始学习' : '继续学习' }}
</button>
</div>
</div>
</div>
</div>
<div v-else class="empty-state">
<el-empty description="暂无学习任务" />
</div>
</div>
</div>
</StudyPlanLayout>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { DocumentCopy, DocumentChecked, Calendar } from '@element-plus/icons-vue';
import { useStore } from 'vuex';
import { learningTaskApi } from '@/apis/study';
import type { TaskItemVO } from '@/types';
import { StudyPlanLayout } from '@/views/user/study-plan';
defineOptions({
name: 'StudyTasksView'
});
const router = useRouter();
const store = useStore();
const loading = ref(false);
const taskList = ref<TaskItemVO[]>([]);
// 统计数据
const totalCount = ref(0);
const completedCount = ref(0);
const pendingCount = ref(0);
// const userLevel = "Lv"+useStore().getters['auth/userinfo'].level;
// 计算进度百分比
const progressPercent = computed(() => {
if (totalCount.value === 0) return 0;
return Math.round((completedCount.value / totalCount.value) * 100);
});
onMounted(() => {
loadTaskList();
loadUserProgress();
});
// 获取当前用户ID
function getUserID() {
const userInfo = store.getters['auth/user'];
return userInfo?.id || '';
}
// 加载任务列表(用户视角)
async function loadTaskList() {
loading.value = true;
try {
const userID = getUserID();
if (!userID) {
ElMessage.warning('请先登录');
loading.value = false;
return;
}
// 调用用户任务分页接口
const pageParam = {
pageNumber: 1,
pageSize: 100 // 获取所有任务,不做分页
};
const filter: TaskItemVO = {
userID
};
const res = await learningTaskApi.getUserTaskPage(pageParam, filter);
if (res.success && res.dataList) {
taskList.value = res.dataList;
} else {
ElMessage.error(res.message || '加载学习任务失败');
}
} catch (error: any) {
console.error('加载学习任务失败:', error);
ElMessage.error(error?.message || '加载学习任务失败');
} finally {
loading.value = false;
}
}
// 加载用户进度统计数据
async function loadUserProgress() {
try {
const userID = getUserID();
if (!userID) {
return;
}
const res = await learningTaskApi.getUserProgress(userID);
if (res.success && res.data) {
const progressData = res.data;
const pending = (progressData.notStartTaskNum || 0) + (progressData.learningTaskNum || 0);
// 设置统计数据
totalCount.value = progressData.totalTaskNum || 0;
completedCount.value = progressData.completedTaskNum || 0;
pendingCount.value = pending;
}
} catch (error) {
console.error('加载用户进度失败:', error);
// 不显示错误消息,使用从任务列表计算的统计数据即可
}
}
// 点击任务卡片
function handleTaskClick(task: TaskItemVO) {
if (!task.taskID) {
ElMessage.warning('任务ID不存在');
return;
}
// 使用路由跳转到任务详情页
router.push({
path: '/study-plan/task-detail',
query: {
taskId: task.taskID
}
});
}
// 格式化日期
function formatDate(dateString?: string): string {
if (!dateString) return '';
const date = new Date(dateString);
return `${date.getFullYear()}.${(date.getMonth() + 1).toString().padStart(2, '0')}.${date.getDate().toString().padStart(2, '0')}`;
}
// 获取任务学习进度
function getTaskProgress(task: TaskItemVO): number {
if (!task.progress && task.progress !== 0) return 0;
return Math.round(task.progress);
}
// 获取截止时间状态
function getDeadlineStatus(task: TaskItemVO): { show: boolean; text: string; type: 'danger' | 'info' | 'success' | 'primary' } {
if (!task.endTime) {
return { show: false, text: '', type: 'primary' };
}
const now = new Date();
const endTime = new Date(task.endTime);
const diffTime = endTime.getTime() - now.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (task.status === 2) {
// 已完成
return { show: true, text: '已完成', type: 'success' };
} else if (diffDays < 0) {
// 已过期
return { show: true, text: '已截止', type: 'info' };
} else if (diffDays <= 8) {
// 即将截止
return { show: true, text: `${diffDays}天后截止`, type: 'danger' };
}
return { show: true, text: `${diffDays}天后截止`, type: 'primary' };
}
</script>
<style lang="scss" scoped>
.main-content {
padding: 40px 0 80px;
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
}
}
.section-title {
font-size: 28px;
font-weight: 600;
color: #141F38;
margin: 0 0 20px 0;
}
// 进度卡片
.progress-card {
background: #fff;
border-radius: 10px;
padding: 50px;
margin-bottom: 60px;
box-shadow: 0 17px 22.4px rgba(164, 182, 199, 0.1);
.progress-info {
margin-top: 20px;
border-radius: 10px;
background: #F7F8F9;
padding: 10px;
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
.progress-text {
font-size: 16px;
font-weight: 500;
color: #334155;
}
.progress-percent {
font-size: 16px;
font-weight: 500;
color: #C62828;
}
.progress-level {
font-size: 16px;
font-weight: 500;
color: #334155;
}
}
.progress-bar-container {
// margin: 20px;
.progress-bar {
width: 100%;
height: 12px;
background: #ffffff;
border-radius: 27px;
overflow: hidden;
.progress-fill {
height: 100%;
background: linear-gradient(143deg, #3D3D3D 1%, #3D3D3D 99%);
border-radius: 27px;
transition: width 0.3s ease;
}
}
}
}
.task-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 30px;
.stat-card {
background: #FFFDFD;
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 10px;
padding: 30px 40px;
display: flex;
justify-content: space-between;
align-items: center;
&.pending {
background: linear-gradient(45deg, #ffffff 0%, #FFF1F0 100%);
.stat-icon {
background: #FFF5F4;
:deep(.el-icon) {
font-size: 44px;
background: linear-gradient(143deg, #FD9082 1%, #FD6D78 99%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
}
}
&.completed {
background: linear-gradient(45deg, #ffffff 0%, #F0FAFF 100%);
.stat-icon {
background: #EFF8FF;
:deep(.el-icon) {
font-size: 44px;
background: linear-gradient(143deg, #82B7FD 1%, #2F6AFF 99%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
}
}
.stat-content {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 30px;
.stat-title {
font-size: 20px;
font-weight: 600;
color: #141F38;
text-align: right;
width: 100px;
}
.stat-number {
font-size: 40px;
font-weight: 600;
line-height: 0.95;
color: #334155;
width: 100px;
}
}
.stat-icon {
width: 77px;
height: 77px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
// 任务列表
.loading-container {
padding: 40px 0;
}
.task-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
margin-bottom: 40px;
@media (max-width: 1024px) {
grid-template-columns: 1fr;
}
}
.task-card {
position: relative;
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 10px;
padding: 40px;
transition: all 0.3s;
height: 376px;
overflow: hidden;
&:hover {
box-shadow: 0 8px 20px rgba(164, 182, 199, 0.2);
transform: translateY(-2px);
}
// 背景装饰
.task-bg-decoration {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(253, 144, 130, 0.03) 0%, rgba(253, 109, 120, 0.03) 100%);
pointer-events: none;
z-index: 0;
}
.task-content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
height: 100%;
gap: 0;
// 顶部信息区
.task-header {
display: flex;
flex-direction: column;
gap: 18px;
margin-bottom: 18px;
// 状态标签 - 小标签,自适应宽度
.status-tag {
align-self: flex-start;
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
font-size: 14px;
font-weight: 500;
line-height: 1.5714285714285714;
border-radius: 2px;
// 待完成 - 红色
&.status-pending {
background-color: #FFECE8;
color: #F53F3F;
}
// 进行中 - 蓝色
&.status-processing {
background-color: #E8F7FF;
color: #3491FA;
}
// 已完成 - 绿色
&.status-completed {
background-color: #E8FFEA;
color: #00B42A;
}
}
}
.task-title {
font-size: 20px;
font-weight: 600;
color: #141F38;
margin: 0;
line-height: 1.4;
}
.task-desc {
font-size: 14px;
font-weight: 400;
color: #334155;
line-height: 1.5714285714285714;
margin: 0 0 20px 0;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
line-clamp: 3;
overflow: hidden;
text-overflow: ellipsis;
}
// 学习进度
.task-progress {
margin-top: auto;
padding-bottom: 20px;
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
.progress-label {
font-size: 14px;
font-weight: 400;
color: #1D2129;
line-height: 1.5714285714285714;
}
.progress-value {
font-size: 14px;
font-weight: 400;
color: #1D2129;
line-height: 1.5714285714285714;
}
}
.progress-bar {
width: 100%;
height: 6px;
background: #F7F8F9;
border-radius: 27px;
overflow: hidden;
.progress-fill {
height: 100%;
background: linear-gradient(143deg, #FD9082 1%, #FD6D78 99%);
border-radius: 27px;
transition: width 0.3s ease;
}
}
}
.task-footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
// margin-top: auto;
.task-time {
display: flex;
align-items: center;
gap: 3px;
font-size: 14px;
font-weight: 400;
color: #979797;
line-height: 1.5714285714285714;
flex: 1;
.calendar-icon {
font-size: 16px;
color: #979797;
flex-shrink: 0;
}
// 截止时间文本
.deadline-text {
font-size: 14px;
font-weight: 400;
line-height: 1.5714285714285714;
// 即将截止 - 红色
&.deadline-danger {
color: #F53F3F;
}
// 已截止 - 灰色
&.deadline-info {
color: #979797;
}
// 已完成 - 绿色
&.deadline-success {
color: #00B42A;
}
// 进行中 - 蓝色
&.deadline-primary {
color: #3491FA;
}
}
}
// 开始学习按钮
.start-btn {
flex-shrink: 0;
width: 104px;
height: 42px;
background: #C62828;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
color: #FFFFFF;
line-height: 1.5;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: #D32F2F;
box-shadow: 0 4px 8px rgba(198, 40, 40, 0.3);
}
&:active {
background: #B71C1C;
}
}
}
}
}
.empty-state {
padding: 80px 0;
text-align: center;
}
.pagination-container {
display: flex;
justify-content: center;
margin-top: 40px;
}
</style>