修复学习流程路由切换与计时展示稳定性
This commit is contained in:
+252
-123
@@ -1,28 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import request from '@/utils/request';
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import request from "@/utils/request";
|
||||
import router from "@/router";
|
||||
import {ElMessage} from "element-plus";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
const tasks = reactive([] as any[]);
|
||||
interface TaskItem {
|
||||
title: string;
|
||||
description: string;
|
||||
materialURL: string;
|
||||
lastLearningStatus: string;
|
||||
priority: number | string;
|
||||
taskNum: string;
|
||||
taskId: number;
|
||||
}
|
||||
|
||||
const selectedTask = ref<any>(null);
|
||||
const tasks = ref<TaskItem[]>([]);
|
||||
const selectedTaskId = ref<number | null>(null);
|
||||
const loading = ref(false);
|
||||
|
||||
const fetchTasks = () => {
|
||||
request.get('/tasks', { pageSize: 100, pageNum: 1 })
|
||||
.then((res) => {
|
||||
/* 此处暂时不开发分页模式,数据量应该不会很大,但仍然保留了分页形式的接口 */
|
||||
tasks.splice(0, tasks.length, ...res.data.records.map((record: any) => ({
|
||||
title: record.taskName,
|
||||
description: record.taskDescription || '',
|
||||
materialURL: record.materialURL || record.materialUrl || "",
|
||||
lastLearningStatus: record.lastLearningStatus || '未开始',
|
||||
priority: Number.isFinite(record.taskPriority) ? Math.round(record.taskPriority) : "-",
|
||||
taskNum: record.taskNum,
|
||||
taskId: record.id,
|
||||
})));
|
||||
selectedTask.value = tasks[0] || null;
|
||||
});
|
||||
const selectedTask = computed(() =>
|
||||
tasks.value.find((item) => item.taskId === selectedTaskId.value) || null
|
||||
);
|
||||
|
||||
const fetchTasks = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await request.get("/tasks", { pageSize: 100, pageNum: 1 });
|
||||
tasks.value = (res?.data?.records || []).map((record: any) => ({
|
||||
title: record.taskName,
|
||||
description: record.taskDescription || "",
|
||||
materialURL: record.materialURL || record.materialUrl || "",
|
||||
lastLearningStatus: record.lastLearningStatus || "未开始",
|
||||
priority: Number.isFinite(record.taskPriority) ? Math.round(record.taskPriority) : "-",
|
||||
taskNum: record.taskNum,
|
||||
taskId: record.id,
|
||||
}));
|
||||
selectedTaskId.value = tasks.value[0]?.taskId ?? null;
|
||||
} catch (error: any) {
|
||||
tasks.value = [];
|
||||
selectedTaskId.value = null;
|
||||
ElMessage.error(error?.message || "任务列表加载失败,请重试");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const startTask = () => {
|
||||
@@ -30,40 +50,33 @@ const startTask = () => {
|
||||
ElMessage.warning("请先选择一个任务");
|
||||
return;
|
||||
}
|
||||
router.push({name:'start-task',params:{taskNum:selectedTask.value.taskNum}});
|
||||
window.location.assign(`/start-task/${encodeURIComponent(selectedTask.value.taskNum)}`);
|
||||
};
|
||||
|
||||
const addTask = () => {
|
||||
router.push({name:"add-task"});
|
||||
router.push({ name: "add-task" });
|
||||
};
|
||||
|
||||
const changeTask = () =>{
|
||||
const changeTask = () => {
|
||||
if (!selectedTask.value) {
|
||||
ElMessage.warning("请先选择一个任务");
|
||||
return;
|
||||
}
|
||||
router.push({name:"update-task",params:{"taskId":selectedTask.value.taskId}})
|
||||
router.push({ name: "update-task", params: { taskId: selectedTask.value.taskId } });
|
||||
};
|
||||
|
||||
const removeTask = () => {
|
||||
const removeTask = async () => {
|
||||
if (!selectedTask.value) {
|
||||
ElMessage.warning("请先选择一个任务");
|
||||
return;
|
||||
}
|
||||
request.del(`/tasks/${selectedTask.value.taskId}`,{}).then(res=>{
|
||||
if (res.code === 200){
|
||||
ElMessage.success(`任务${selectedTask.value.title}删除成功`)
|
||||
const index = tasks.indexOf(selectedTask.value);
|
||||
if (index > -1) {
|
||||
tasks.splice(index, 1);
|
||||
selectedTask.value = tasks[0] || null;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const selectTask = (task: any) => {
|
||||
selectedTask.value = task;
|
||||
const target = selectedTask.value;
|
||||
const res = await request.del(`/tasks/${target.taskId}`, {});
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(`任务 ${target.title} 删除成功`);
|
||||
tasks.value = tasks.value.filter((item) => item.taskId !== target.taskId);
|
||||
selectedTaskId.value = tasks.value[0]?.taskId ?? null;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
@@ -72,99 +85,215 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-row :gutter="5" class="task-layout">
|
||||
<el-col :span="6">
|
||||
<el-card class="task-card" :body-style="{ padding: '0' }">
|
||||
<div class="task-header">任务列表</div>
|
||||
<el-scrollbar height="300px">
|
||||
<el-menu class="task-menu" @select="(key:any) => selectTask(tasks[key])">
|
||||
<el-menu-item
|
||||
v-for="(task, index) in tasks"
|
||||
:key="index"
|
||||
:index="index.toString()"
|
||||
class="task-item"
|
||||
>
|
||||
{{ task.title }}
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
<section class="study-page">
|
||||
<header class="surface-card page-head">
|
||||
<div>
|
||||
<h1 class="page-title">学习任务</h1>
|
||||
<p class="page-subtitle">先选任务,再确认材料与优先级,然后开始一次学习会话</p>
|
||||
</div>
|
||||
<el-button type="success" plain @click="fetchTasks" :loading="loading">刷新列表</el-button>
|
||||
</header>
|
||||
|
||||
<div class="layout-grid">
|
||||
<aside class="surface-card task-list">
|
||||
<div class="block-title">任务清单</div>
|
||||
<el-scrollbar height="420px">
|
||||
<div
|
||||
v-for="item in tasks"
|
||||
:key="item.taskId"
|
||||
class="task-list-item"
|
||||
:class="{ active: selectedTaskId === item.taskId }"
|
||||
@click="selectedTaskId = item.taskId"
|
||||
>
|
||||
<p class="task-name">{{ item.title }}</p>
|
||||
<p class="task-meta">状态:{{ item.lastLearningStatus }}</p>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</aside>
|
||||
|
||||
<el-col :span="13">
|
||||
<el-card class="task-card">
|
||||
<div v-if="selectedTask">
|
||||
<h3>任务描述</h3>
|
||||
<el-input type="textarea" :model-value="selectedTask.description" :rows="4" readonly />
|
||||
<main class="content-zone">
|
||||
<article class="surface-card detail-card" v-loading="loading">
|
||||
<template v-if="selectedTask">
|
||||
<div class="detail-head">
|
||||
<h3>{{ selectedTask.title }}</h3>
|
||||
<el-tag type="success" effect="plain">优先级 {{ selectedTask.priority }}</el-tag>
|
||||
</div>
|
||||
|
||||
<h3 class="section-title">学习材料</h3>
|
||||
<el-link v-if="selectedTask.materialURL" :href="selectedTask.materialURL" type="primary" target="_blank">
|
||||
{{ selectedTask.materialURL }}
|
||||
</el-link>
|
||||
<span v-else>未填写材料地址</span>
|
||||
<div class="detail-block">
|
||||
<p class="label">任务描述</p>
|
||||
<el-input type="textarea" :model-value="selectedTask.description" :rows="4" readonly />
|
||||
</div>
|
||||
|
||||
<h3 class="section-title">任务优先级</h3>
|
||||
<el-tag type="info">综合优先级:{{ selectedTask.priority }}</el-tag>
|
||||
<div class="detail-block">
|
||||
<p class="label">学习材料</p>
|
||||
<el-link
|
||||
v-if="selectedTask.materialURL"
|
||||
:href="selectedTask.materialURL"
|
||||
target="_blank"
|
||||
type="primary"
|
||||
>
|
||||
{{ selectedTask.materialURL }}
|
||||
</el-link>
|
||||
<p v-else class="empty-text">暂未填写材料地址</p>
|
||||
</div>
|
||||
</template>
|
||||
<el-empty v-else description="暂无任务,请先创建任务" />
|
||||
</article>
|
||||
|
||||
<h3 class="section-title">上次该任务学习情况</h3>
|
||||
<el-descriptions border column="1">
|
||||
<el-descriptions-item label="有效时间比">0.5</el-descriptions-item>
|
||||
<el-descriptions-item label="实际用时">2小时</el-descriptions-item>
|
||||
<el-descriptions-item label="有效学习时间">1小时</el-descriptions-item>
|
||||
<el-descriptions-item label="备注">学习了“动态面板”的基本使用方式</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>请选择任务或等待任务加载完成。</p>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="5">
|
||||
<el-space class="action-buttons">
|
||||
<el-button type="primary" @click="startTask">开始任务</el-button>
|
||||
<el-button @click="addTask">添加任务</el-button>
|
||||
<el-button @click="changeTask">更新任务</el-button>
|
||||
<el-button type="danger" @click="removeTask">删除任务</el-button>
|
||||
<el-button type="info" @click="fetchTasks"> 刷新 </el-button>
|
||||
</el-space>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<aside class="surface-card action-card">
|
||||
<div class="block-title">常用操作</div>
|
||||
<el-button type="success" size="large" @click="startTask">开始任务</el-button>
|
||||
<el-button size="large" @click="addTask">添加任务</el-button>
|
||||
<el-button size="large" @click="changeTask">更新任务</el-button>
|
||||
<el-button type="danger" size="large" @click="removeTask">删除任务</el-button>
|
||||
</aside>
|
||||
</main>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.task-layout {
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.task-card {
|
||||
padding: 0;
|
||||
}
|
||||
.task-header {
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
background-color: #f5f7fa;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
.task-menu {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
.task-item {
|
||||
padding: 10px 12px;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
.section-title {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.action-buttons {
|
||||
.study-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 20px;
|
||||
gap: 14px;
|
||||
}
|
||||
.action-buttons .el-button {
|
||||
width: 8vw;
|
||||
min-width: 70px;
|
||||
margin-top: 5px;
|
||||
|
||||
.page-head {
|
||||
padding: 18px 20px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.layout-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.block-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--green-900);
|
||||
}
|
||||
|
||||
.task-list-item {
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
background: #fbfefc;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.task-list-item + .task-list-item {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.task-list-item:hover {
|
||||
border-color: #b9d9c8;
|
||||
}
|
||||
|
||||
.task-list-item.active {
|
||||
border-color: var(--green-600);
|
||||
background: var(--green-100);
|
||||
box-shadow: inset 0 0 0 1px rgba(47, 143, 104, 0.2);
|
||||
}
|
||||
|
||||
.task-name {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.task-meta {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.content-zone {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 220px;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
.detail-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.detail-head h3 {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.detail-block {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin: 0 0 8px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.action-card {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.action-card .el-button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.layout-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.content-zone {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.action-card .block-title {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-head {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user