视图路径修改

This commit is contained in:
2025-10-27 17:29:25 +08:00
parent 5fa4e1cd42
commit 0033ac10ec
69 changed files with 162 additions and 1199 deletions

View File

@@ -0,0 +1,977 @@
<template>
<div class="task-detail">
<!-- 返回按钮可选 -->
<div v-if="showBackButton" class="back-header">
<el-button @click="handleBack" :icon="ArrowLeft">{{ backButtonText }}</el-button>
</div>
<!-- 加载中 -->
<div v-if="loading" class="loading">
<el-skeleton :rows="10" animated />
</div>
<!-- 任务详情 -->
<div v-else-if="taskVO" class="task-content">
<!-- 任务基本信息 -->
<el-card class="task-info-card" shadow="hover">
<template #header>
<div class="card-header">
<div class="header-left">
<el-icon class="header-icon"><Memo /></el-icon>
<span class="header-title">任务信息</span>
</div>
<el-tag
:type="getTaskStatusType(taskVO.learningTask.status)"
size="large"
>
{{ getTaskStatusText(taskVO.learningTask.status) }}
</el-tag>
</div>
</template>
<div class="task-info">
<h1 class="task-title">{{ taskVO.learningTask.name }}</h1>
<div class="task-meta">
<div class="meta-row">
<div class="meta-item">
<el-icon><Calendar /></el-icon>
<span class="meta-label">开始时间</span>
<span class="meta-value">{{ formatDate(taskVO.learningTask.startTime) }}</span>
</div>
<div class="meta-item">
<el-icon><Calendar /></el-icon>
<span class="meta-label">结束时间</span>
<span class="meta-value">{{ formatDate(taskVO.learningTask.endTime) }}</span>
</div>
</div>
<div class="meta-row">
<div class="meta-item">
<el-icon><User /></el-icon>
<span class="meta-label">创建者</span>
<span class="meta-value">{{ taskVO.learningTask.creator || '系统' }}</span>
</div>
<div class="meta-item">
<el-icon><Clock /></el-icon>
<span class="meta-label">创建时间</span>
<span class="meta-value">{{ formatDateTime(taskVO.learningTask.createTime) }}</span>
</div>
</div>
</div>
<div v-if="taskVO.learningTask.description" class="task-description">
<h3 class="section-subtitle">任务描述</h3>
<p>{{ taskVO.learningTask.description }}</p>
</div>
<!-- 任务统计 -->
<div class="task-stats">
<div class="stat-item">
<div class="stat-icon total">
<el-icon><Document /></el-icon>
</div>
<div class="stat-content">
<div class="stat-label">总任务数</div>
<div class="stat-value">{{ taskVO.totalTaskNum || 0 }}</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon completed">
<el-icon><CircleCheck /></el-icon>
</div>
<div class="stat-content">
<div class="stat-label">已完成</div>
<div class="stat-value">{{ taskVO.completedTaskNum || 0 }}</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon learning">
<el-icon><Reading /></el-icon>
</div>
<div class="stat-content">
<div class="stat-label">学习中</div>
<div class="stat-value">{{ taskVO.learningTaskNum || 0 }}</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon pending">
<el-icon><Clock /></el-icon>
</div>
<div class="stat-content">
<div class="stat-label">未开始</div>
<div class="stat-value">{{ taskVO.notStartTaskNum || 0 }}</div>
</div>
</div>
</div>
<!-- 学习进度 -->
<div v-if="taskVO.totalTaskNum && taskVO.totalTaskNum > 0" class="learning-progress">
<div class="progress-header">
<span>学习进度</span>
<span class="progress-text">{{ progressPercent }}%</span>
</div>
<el-progress
:percentage="progressPercent"
:stroke-width="12"
:color="progressColor"
/>
</div>
</div>
</el-card>
<!-- 课程列表 -->
<el-card v-if="taskVO.taskCourses && taskVO.taskCourses.length > 0" class="course-card" shadow="hover">
<template #header>
<div class="card-header">
<div class="header-left">
<el-icon class="header-icon"><VideoPlay /></el-icon>
<span class="header-title">学习课程</span>
</div>
<span class="item-count"> {{ taskVO.taskCourses.length }} 门课程</span>
</div>
</template>
<div class="course-list">
<div
v-for="(course, index) in taskVO.taskCourses"
:key="course.courseID"
class="course-item"
@click="handleCourseClick(course)"
>
<div class="course-index">{{ index + 1 }}</div>
<div class="course-info">
<div class="course-name-row">
<span class="course-name">{{ course.courseName }}</span>
<el-tag
v-if="course.required"
type="danger"
size="small"
effect="plain"
>
必修
</el-tag>
</div>
<div class="course-meta">
<div class="status-info">
<el-tag
:type="getItemStatusType(course.status)"
size="small"
>
{{ getItemStatusText(course.status) }}
</el-tag>
<span v-if="course.status === 2 && course.completeTime" class="complete-time">
<el-icon><CircleCheck /></el-icon>
{{ formatDateTime(course.completeTime) }}
</span>
</div>
<div v-if="course.progress !== null && course.progress !== undefined" class="progress-info">
<span class="progress-text">{{ course.progress }}%</span>
</div>
</div>
</div>
<div class="course-action">
<el-button
type="primary"
:icon="course.status === 2 ? CircleCheck : (course.status === 1 ? VideoPlay : Reading)"
:disabled="course.status === 2"
@click.stop="handleCourseClick(course)"
>
{{ course.status === 2 ? '已完成' : (course.status === 1 ? '继续学习' : '开始学习') }}
</el-button>
</div>
</div>
</div>
</el-card>
<!-- 文章/资源列表 -->
<el-card v-if="taskVO.taskResources && taskVO.taskResources.length > 0" class="resource-card" shadow="hover">
<template #header>
<div class="card-header">
<div class="header-left">
<el-icon class="header-icon"><Document /></el-icon>
<span class="header-title">学习资源</span>
</div>
<span class="item-count"> {{ taskVO.taskResources.length }} 篇文章</span>
</div>
</template>
<div class="resource-list">
<div
v-for="(resource, index) in taskVO.taskResources"
:key="resource.resourceID"
class="resource-item"
@click="handleResourceClick(resource)"
>
<div class="resource-index">{{ index + 1 }}</div>
<div class="resource-info">
<div class="resource-name-row">
<span class="resource-name">{{ resource.resourceName }}</span>
<el-tag
v-if="resource.required"
type="danger"
size="small"
effect="plain"
>
必修
</el-tag>
</div>
<div class="resource-meta">
<div class="status-info">
<el-tag
:type="getItemStatusType(resource.status)"
size="small"
>
{{ getItemStatusText(resource.status) }}
</el-tag>
<span v-if="resource.status === 2 && resource.completeTime" class="complete-time">
<el-icon><CircleCheck /></el-icon>
{{ formatDateTime(resource.completeTime) }}
</span>
</div>
<div v-if="resource.progress !== null && resource.progress !== undefined" class="progress-info">
<span class="progress-text">{{ resource.progress }}%</span>
</div>
</div>
</div>
<div class="resource-action">
<el-button
type="primary"
:icon="resource.status === 2 ? CircleCheck : (resource.status === 1 ? Reading : View)"
:disabled="resource.status === 2"
@click.stop="handleResourceClick(resource)"
>
{{ resource.status === 2 ? '已完成' : (resource.status === 1 ? '继续阅读' : '开始阅读') }}
</el-button>
</div>
</div>
</div>
</el-card>
<!-- 空状态 -->
<el-card
v-if="(!taskVO.taskCourses || taskVO.taskCourses.length === 0) &&
(!taskVO.taskResources || taskVO.taskResources.length === 0)"
class="empty-card"
shadow="never"
>
<el-empty description="暂无学习内容" />
</el-card>
</div>
<!-- 加载失败 -->
<div v-else class="error-tip">
<el-empty description="加载任务失败" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import {
ArrowLeft,
Memo,
Calendar,
User,
Clock,
Document,
CircleCheck,
Reading,
VideoPlay,
View
} from '@element-plus/icons-vue';
import { learningTaskApi } from '@/apis/study';
import type { TaskVO, TaskItemVO } from '@/types';
interface Props {
taskId?: string;
showBackButton?: boolean;
backButtonText?: string;
}
defineOptions({
name: 'LearingTaskDetail'
});
const props = withDefaults(defineProps<Props>(), {
showBackButton: false,
backButtonText: '返回'
});
const emit = defineEmits<{
back: [];
}>();
const router = useRouter();
const loading = ref(false);
const taskVO = ref<TaskVO | null>(null);
const currentTaskId = computed(() => props.taskId || '');
// 计算学习进度百分比
const progressPercent = computed(() => {
if (!taskVO.value || !taskVO.value.totalTaskNum || taskVO.value.totalTaskNum === 0) {
return 0;
}
const completed = taskVO.value.completedTaskNum || 0;
const total = taskVO.value.totalTaskNum;
return Math.round((completed / total) * 100);
});
// 进度条颜色
const progressColor = computed(() => {
const progress = progressPercent.value;
if (progress >= 80) return '#67c23a';
if (progress >= 50) return '#409eff';
return '#e6a23c';
});
watch(() => currentTaskId.value, (newId) => {
if (newId) {
loadTaskDetail();
}
}, { immediate: true });
// 加载任务详情
async function loadTaskDetail() {
if (!currentTaskId.value) {
ElMessage.error('任务ID不存在');
return;
}
loading.value = true;
try {
const res = await learningTaskApi.getUserTask(currentTaskId.value);
if (res.success && res.data) {
taskVO.value = res.data;
// 确保数据结构完整
if (!taskVO.value.taskCourses) {
taskVO.value.taskCourses = [];
}
if (!taskVO.value.taskResources) {
taskVO.value.taskResources = [];
}
if (!taskVO.value.taskUsers) {
taskVO.value.taskUsers = [];
}
} else {
ElMessage.error(res.message || '加载任务失败');
}
} catch (error: any) {
console.error('加载任务失败:', error);
ElMessage.error(error?.message || '加载任务失败');
} finally {
loading.value = false;
}
}
// 处理课程点击
function handleCourseClick(course: TaskItemVO) {
if (!course.courseID) {
ElMessage.warning('课程ID不存在');
return;
}
// 跳转到课程学习路由
router.push({
path: '/study-plan/course-study',
query: {
courseId: course.courseID,
chapterIndex: '0',
nodeIndex: '0',
taskId: currentTaskId.value
}
});
}
// 处理资源点击
function handleResourceClick(resource: TaskItemVO) {
if (!resource.resourceID) {
ElMessage.warning('资源ID不存在');
return;
}
// 跳转到文章查看页面
router.push({
path: '/article/show',
query: {
articleId: resource.resourceID,
taskId: currentTaskId.value // 传递 taskId
}
});
}
// 处理返回
function handleBack() {
emit('back');
}
// 格式化日期
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 formatDateTime(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')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
}
// 获取任务状态文本
function getTaskStatusText(status?: number): string {
switch (status) {
case 0:
return '草稿';
case 1:
return '进行中';
case 2:
return '已结束';
default:
return '未知';
}
}
// 获取任务状态类型
function getTaskStatusType(status?: number): 'info' | 'success' | 'warning' | 'danger' {
switch (status) {
case 0:
return 'info';
case 1:
return 'success';
case 2:
return 'warning';
default:
return 'info';
}
}
// 获取学习项状态文本(课程/资源通用)
function getItemStatusText(status?: number): string {
switch (status) {
case 0:
return '未开始';
case 1:
return '学习中';
case 2:
return '已完成';
default:
return '未知';
}
}
// 获取学习项状态类型(课程/资源通用)
function getItemStatusType(status?: number): 'info' | 'warning' | 'success' {
switch (status) {
case 0:
return 'info';
case 1:
return 'warning';
case 2:
return 'success';
default:
return 'info';
}
}
</script>
<style lang="scss" scoped>
.task-detail {
min-height: 100vh;
background: #f5f7fa;
padding-bottom: 60px;
}
.back-header {
position: sticky;
top: 0;
z-index: 100;
padding: 16px 24px;
background: #fff;
border-bottom: 1px solid #e4e7ed;
margin-bottom: 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.loading {
padding: 24px;
background: #fff;
margin: 16px;
border-radius: 8px;
}
.task-content {
max-width: 1200px;
margin: 0 auto;
padding: 24px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
.header-left {
display: flex;
align-items: center;
gap: 8px;
.header-icon {
font-size: 20px;
color: #409eff;
}
.header-title {
font-size: 18px;
font-weight: 600;
color: #303133;
}
}
.item-count {
font-size: 14px;
color: #909399;
}
}
// 任务信息卡片
.task-info-card {
margin-bottom: 24px;
:deep(.el-card__header) {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
.card-header {
.header-icon {
color: #fff;
}
.header-title {
color: #fff;
}
}
}
}
.task-info {
.task-title {
font-size: 28px;
font-weight: 600;
color: #303133;
margin: 0 0 24px 0;
line-height: 1.4;
}
.task-meta {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 24px;
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
.meta-row {
display: flex;
gap: 40px;
flex-wrap: wrap;
}
.meta-item {
display: flex;
align-items: center;
gap: 8px;
color: #606266;
font-size: 14px;
.el-icon {
color: #909399;
font-size: 16px;
}
.meta-label {
color: #909399;
}
.meta-value {
color: #303133;
font-weight: 500;
}
}
}
.task-description {
margin-bottom: 24px;
.section-subtitle {
font-size: 16px;
font-weight: 600;
color: #303133;
margin: 0 0 12px 0;
}
p {
color: #606266;
font-size: 14px;
line-height: 1.8;
margin: 0;
}
}
.task-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
.stat-item {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
background: #fafafa;
border-radius: 8px;
border: 1px solid #e4e7ed;
transition: all 0.3s;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
.el-icon {
font-size: 24px;
}
&.total {
background: #e3f2fd;
color: #1976d2;
}
&.completed {
background: #e8f5e9;
color: #388e3c;
}
&.learning {
background: #fff3e0;
color: #f57c00;
}
&.pending {
background: #fce4ec;
color: #c2185b;
}
}
.stat-content {
flex: 1;
.stat-label {
font-size: 13px;
color: #909399;
margin-bottom: 4px;
}
.stat-value {
font-size: 24px;
font-weight: 600;
color: #303133;
}
}
}
}
.learning-progress {
padding: 20px;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 8px;
.progress-header {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
font-size: 14px;
font-weight: 500;
.progress-text {
color: #409eff;
font-weight: 600;
font-size: 16px;
}
}
}
}
// 课程卡片
.course-card {
margin-bottom: 24px;
:deep(.el-card__header) {
background: #fff9f0;
}
}
.course-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.course-item {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
background: #fafafa;
border-radius: 8px;
border: 1px solid #e4e7ed;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: #f0f2f5;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transform: translateX(4px);
}
.course-index {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 600;
flex-shrink: 0;
}
.course-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
.course-name-row {
display: flex;
align-items: center;
gap: 12px;
.course-name {
font-size: 16px;
font-weight: 500;
color: #303133;
}
}
.course-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
.status-info {
display: flex;
align-items: center;
gap: 8px;
.complete-time {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: #67c23a;
.el-icon {
font-size: 16px;
}
}
}
.progress-info {
.progress-text {
font-size: 14px;
font-weight: 600;
color: #409eff;
}
}
}
}
.course-action {
flex-shrink: 0;
}
}
// 资源卡片
.resource-card {
margin-bottom: 24px;
:deep(.el-card__header) {
background: #f0f9ff;
}
}
.resource-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.resource-item {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
background: #fafafa;
border-radius: 8px;
border: 1px solid #e4e7ed;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: #f0f2f5;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transform: translateX(4px);
}
.resource-index {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 600;
flex-shrink: 0;
}
.resource-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
.resource-name-row {
display: flex;
align-items: center;
gap: 12px;
.resource-name {
font-size: 16px;
font-weight: 500;
color: #303133;
}
}
.resource-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
.status-info {
display: flex;
align-items: center;
gap: 8px;
.complete-time {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: #67c23a;
.el-icon {
font-size: 16px;
}
}
}
.progress-info {
.progress-text {
font-size: 14px;
font-weight: 600;
color: #409eff;
}
}
}
}
.resource-action {
flex-shrink: 0;
}
}
// 空状态卡片
.empty-card {
margin-bottom: 24px;
}
.error-tip {
padding: 40px;
}
// 响应式设计
@media (max-width: 768px) {
.task-content {
padding: 16px;
}
.task-info {
.task-title {
font-size: 22px;
}
.task-meta {
.meta-row {
flex-direction: column;
gap: 12px;
}
}
.task-stats {
grid-template-columns: repeat(2, 1fr);
}
}
.course-item,
.resource-item {
flex-direction: column;
align-items: flex-start;
.course-action,
.resource-action {
width: 100%;
.el-button {
width: 100%;
}
}
}
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
export { default as LearningTaskAdd } from './LearningTaskAdd.vue';
export { default as LearningTaskList } from './LearningTaskList.vue';
export { default as LearingTaskDetail } from './LearingTaskDetail.vue';