样式修改

This commit is contained in:
2025-10-31 13:17:57 +08:00
parent a881f57e30
commit 9ad9507a72
19 changed files with 693 additions and 971 deletions

View File

@@ -96,7 +96,7 @@ public interface LearningTaskService {
// ----------------任务用户相关-------------------------------- // ----------------任务用户相关--------------------------------
ResultDomain<TbLearningTask> getUserTaskPage(TaskItemVO filter, PageParam pageParam); ResultDomain<TaskItemVO> getUserTaskPage(TaskItemVO filter, PageParam pageParam);
/** /**
* @description 获取任务用户列表 * @description 获取任务用户列表

View File

@@ -37,7 +37,7 @@ public class CourseTagController {
* 根据ID获取标签详情 * 根据ID获取标签详情
*/ */
@GetMapping("/{tagID}") @GetMapping("/{tagID}")
public ResultDomain<TbCourseTag> getTagById(@PathVariable String tagID) { public ResultDomain<TbCourseTag> getTagById(@PathVariable("tagID") String tagID) {
return courseTagService.getTagById(tagID); return courseTagService.getTagById(tagID);
} }
@@ -45,7 +45,7 @@ public class CourseTagController {
* 根据标签获取课程列表 * 根据标签获取课程列表
*/ */
@GetMapping("/{tagID}/courses") @GetMapping("/{tagID}/courses")
public ResultDomain<TbCourse> getCoursesByTag(@PathVariable String tagID) { public ResultDomain<TbCourse> getCoursesByTag(@PathVariable("tagID") String tagID) {
return null; return null;
// return courseTagService.getCoursesByTag(tagID); // return courseTagService.getCoursesByTag(tagID);
} }
@@ -55,8 +55,8 @@ public class CourseTagController {
*/ */
@PostMapping("/course/{courseID}/tag") @PostMapping("/course/{courseID}/tag")
public ResultDomain<TbCourseTag> addTagToCourse( public ResultDomain<TbCourseTag> addTagToCourse(
@PathVariable String courseID, @PathVariable("courseID") String courseID,
@RequestParam String tagID) { @RequestParam("tagID") String tagID) {
return null; return null;
// return courseTagService.addTagToCourse(courseID, tagID); // return courseTagService.addTagToCourse(courseID, tagID);
} }
@@ -66,8 +66,8 @@ public class CourseTagController {
*/ */
@DeleteMapping("/course/{courseID}/tag/{tagID}") @DeleteMapping("/course/{courseID}/tag/{tagID}")
public ResultDomain<Boolean> removeTagFromCourse( public ResultDomain<Boolean> removeTagFromCourse(
@PathVariable String courseID, @PathVariable("courseID") String courseID,
@PathVariable String tagID) { @PathVariable("tagID") String tagID) {
return null; return null;
// return courseTagService.removeTagFromCourse(courseID, tagID); // return courseTagService.removeTagFromCourse(courseID, tagID);
} }
@@ -76,7 +76,7 @@ public class CourseTagController {
* 获取课程标签列表 * 获取课程标签列表
*/ */
@GetMapping("/course/{courseID}/tags") @GetMapping("/course/{courseID}/tags")
public ResultDomain<TbCourseTag> getCourseTags(@PathVariable String courseID) { public ResultDomain<TbCourseTag> getCourseTags(@PathVariable("courseID") String courseID) {
return courseTagService.getCourseTags(courseID); return courseTagService.getCourseTags(courseID);
} }
} }

View File

@@ -1,192 +0,0 @@
package org.xyzh.study.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.study.TbLearningTask;
import org.xyzh.common.dto.study.TbTaskUser;
import java.util.List;
import java.util.Map;
/**
* @description 学习管理控制器
* @filename LearningManagementController.java
* @author yslg
* @copyright xyzh
* @since 2025-10-15
*/
@RestController
@RequestMapping("/study/management")
public class LearningManagementController {
private static final Logger logger = LoggerFactory.getLogger(LearningManagementController.class);
// ==================== 任务发布管理 ====================
/**
* 发布学习任务
*/
@PostMapping("/task/publish")
public ResultDomain<TbLearningTask> publishLearningTask(@RequestBody Map<String, Object> taskData) {
// TODO: 实现发布学习任务(编辑任务名称、任务描述、任务周期、关联资源/课程)
return null;
}
/**
* 编辑学习任务
*/
@PutMapping("/task/edit")
public ResultDomain<TbLearningTask> editLearningTask(@RequestBody TbLearningTask task) {
// TODO: 实现编辑学习任务
return null;
}
/**
* 获取任务发布列表
*/
@GetMapping("/task/published-list")
public ResultDomain<TbLearningTask> getPublishedTaskList(
@RequestParam(required = false) String taskName,
@RequestParam(required = false) Integer status) {
// TODO: 实现获取任务发布列表
return null;
}
/**
* 关联资源到任务
*/
@PostMapping("/task/link-resource")
public ResultDomain<Boolean> linkResourceToTask(@RequestBody Map<String, Object> params) {
// TODO: 实现关联资源/课程到任务
return null;
}
/**
* 关联课程到任务
*/
@PostMapping("/task/link-course")
public ResultDomain<Boolean> linkCourseToTask(@RequestBody Map<String, Object> params) {
// TODO: 实现关联课程到任务
return null;
}
// ==================== 人员选定管理 ====================
/**
* 按部门筛选人员
*/
@GetMapping("/personnel/dept-filter")
public ResultDomain<Map<String, Object>> filterPersonnelByDept(@RequestParam String deptID) {
// TODO: 实现按部门层级筛选人员
return null;
}
/**
* 手动勾选人员
*/
@PostMapping("/personnel/select")
public ResultDomain<List<Map<String, Object>>> selectPersonnel(@RequestBody List<String> userIDs) {
// TODO: 实现手动勾选人员
return null;
}
/**
* 生成任务接收名单
*/
@PostMapping("/personnel/generate-list")
public ResultDomain<List<TbTaskUser>> generateTaskReceiverList(@RequestBody Map<String, Object> params) {
// TODO: 实现生成任务接收名单
return null;
}
/**
* 批量分配任务给用户
*/
@PostMapping("/personnel/batch-assign")
public ResultDomain<Boolean> batchAssignTaskToUsers(@RequestBody Map<String, Object> params) {
// TODO: 实现批量分配任务给用户
return null;
}
/**
* 获取部门人员树形结构
*/
@GetMapping("/personnel/dept-tree")
public ResultDomain<Map<String, Object>> getDeptPersonnelTree() {
// TODO: 实现获取部门人员树形结构
return null;
}
// ==================== 学习记录统计 ====================
/**
* 获取学习记录统计
*/
@GetMapping("/record/statistics")
public ResultDomain<Map<String, Object>> getLearningRecordStatistics(
@RequestParam(required = false) String deptID,
@RequestParam(required = false) String userID,
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate) {
// TODO: 实现获取学习记录统计(支持按部门/个人/时间筛选查询)
return null;
}
/**
* 获取用户学习时长统计
*/
@GetMapping("/record/learning-duration")
public ResultDomain<Map<String, Object>> getLearningDurationStatistics(@RequestParam String userID) {
// TODO: 实现统计用户学习时长
return null;
}
/**
* 获取学习资源数量统计
*/
@GetMapping("/record/resource-count")
public ResultDomain<Map<String, Object>> getResourceCountStatistics(@RequestParam String userID) {
// TODO: 实现统计学习资源数量
return null;
}
/**
* 获取任务完成情况统计
*/
@GetMapping("/record/task-completion")
public ResultDomain<Map<String, Object>> getTaskCompletionStatistics(@RequestParam String userID) {
// TODO: 实现统计任务完成情况
return null;
}
/**
* 生成可视化数据图表
*/
@GetMapping("/record/chart-data")
public ResultDomain<Map<String, Object>> getChartData(
@RequestParam String chartType,
@RequestParam(required = false) String userID,
@RequestParam(required = false) String deptID) {
// TODO: 实现生成可视化数据(图表接口)
return null;
}
/**
* 获取部门学习统计
*/
@GetMapping("/record/dept-statistics")
public ResultDomain<Map<String, Object>> getDeptLearningStatistics(@RequestParam String deptID) {
// TODO: 实现获取部门学习统计
return null;
}
/**
* 导出学习记录报告
*/
@PostMapping("/record/export-report")
public ResultDomain<String> exportLearningReport(@RequestBody Map<String, Object> params) {
// TODO: 实现导出学习记录报告
return null;
}
}

View File

@@ -1,204 +0,0 @@
package org.xyzh.study.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.study.TbLearningTask;
import org.xyzh.common.dto.study.TbTaskUser;
import java.util.Map;
/**
* @description 学习计划控制器
* @filename LearningPlanController.java
* @author yslg
* @copyright xyzh
* @since 2025-10-15
*/
@RestController
@RequestMapping("/study/plan")
public class LearningPlanController {
private static final Logger logger = LoggerFactory.getLogger(LearningPlanController.class);
// ==================== 计划列表管理 ====================
/**
* 获取学习计划列表
*/
@GetMapping("/list")
public ResultDomain<TbLearningTask> getLearningPlanList(
@RequestParam(required = false) String planName,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize) {
// TODO: 实现展示校方统一发布的学习计划(名称、起止时间、任务描述)
return null;
}
/**
* 获取计划详情
*/
@GetMapping("/{planID}")
public ResultDomain<TbLearningTask> getPlanDetail(@PathVariable String planID) {
// TODO: 实现获取计划详情
return null;
}
/**
* 获取进行中的计划
*/
@GetMapping("/active")
public ResultDomain<TbLearningTask> getActivePlans() {
// TODO: 实现获取进行中的计划
return null;
}
/**
* 获取已完成的计划
*/
@GetMapping("/completed")
public ResultDomain<TbLearningTask> getCompletedPlans() {
// TODO: 实现获取已完成的计划
return null;
}
/**
* 获取即将开始的计划
*/
@GetMapping("/upcoming")
public ResultDomain<TbLearningTask> getUpcomingPlans() {
// TODO: 实现获取即将开始的计划
return null;
}
/**
* 按时间范围筛选计划
*/
@GetMapping("/filter/date-range")
public ResultDomain<TbLearningTask> filterPlansByDateRange(
@RequestParam String startDate,
@RequestParam String endDate) {
// TODO: 实现按时间范围筛选计划
return null;
}
// ==================== 任务进度管理 ====================
/**
* 获取当前用户的学习任务
*/
@GetMapping("/user/{userID}/tasks")
public ResultDomain<TbTaskUser> getUserLearningTasks(@PathVariable String userID) {
// TODO: 实现展示当前用户的学习任务(已完成/未完成)
return null;
}
/**
* 获取任务进度详情
*/
@GetMapping("/task/{taskID}/progress")
public ResultDomain<Map<String, Object>> getTaskProgress(@PathVariable String taskID) {
// TODO: 实现获取任务进度详情
return null;
}
/**
* 获取用户任务进度
*/
@GetMapping("/user/{userID}/task/{taskID}/progress")
public ResultDomain<Map<String, Object>> getUserTaskProgress(
@PathVariable String userID,
@PathVariable String taskID) {
// TODO: 实现获取用户任务进度
return null;
}
/**
* 更新任务进度
*/
@PutMapping("/task/{taskID}/progress")
public ResultDomain<Boolean> updateTaskProgress(
@PathVariable String taskID,
@RequestParam String userID,
@RequestParam Integer progress) {
// TODO: 实现更新任务进度
return null;
}
/**
* 完成任务
*/
@PostMapping("/task/{taskID}/complete")
public ResultDomain<Boolean> completeTask(
@PathVariable String taskID,
@RequestParam String userID) {
// TODO: 实现完成任务
return null;
}
/**
* 获取进度条数据
*/
@GetMapping("/user/{userID}/progress-bar")
public ResultDomain<Map<String, Object>> getProgressBarData(@PathVariable String userID) {
// TODO: 实现用进度条显示完成百分比
return null;
}
/**
* 获取已完成任务列表
*/
@GetMapping("/user/{userID}/completed-tasks")
public ResultDomain<TbTaskUser> getCompletedTasks(@PathVariable String userID) {
// TODO: 实现获取已完成任务列表
return null;
}
/**
* 获取未完成任务列表
*/
@GetMapping("/user/{userID}/pending-tasks")
public ResultDomain<TbTaskUser> getPendingTasks(@PathVariable String userID) {
// TODO: 实现获取未完成任务列表
return null;
}
/**
* 获取任务统计信息
*/
@GetMapping("/user/{userID}/task-statistics")
public ResultDomain<Map<String, Object>> getTaskStatistics(@PathVariable String userID) {
// TODO: 实现获取任务统计信息
return null;
}
/**
* 获取学习计划统计
*/
@GetMapping("/statistics")
public ResultDomain<Map<String, Object>> getLearningPlanStatistics() {
// TODO: 实现获取学习计划统计
return null;
}
/**
* 获取用户学习计划参与情况
*/
@GetMapping("/user/{userID}/participation")
public ResultDomain<Map<String, Object>> getUserParticipation(@PathVariable String userID) {
// TODO: 实现获取用户学习计划参与情况
return null;
}
/**
* 获取计划排行榜
*/
@GetMapping("/ranking")
public ResultDomain<Map<String, Object>> getPlanRanking(
@RequestParam(required = false) String planID,
@RequestParam(required = false) Integer limit) {
// TODO: 实现获取计划排行榜
return null;
}
}

View File

@@ -145,7 +145,7 @@ public class LearningTaskController {
* 用户获取个人任务列表(用户视角) * 用户获取个人任务列表(用户视角)
*/ */
@PostMapping("/users/page") @PostMapping("/users/page")
public ResultDomain<TbLearningTask> getUserTaskPage(@RequestBody PageRequest<TaskItemVO> pageRequest) { public ResultDomain<TaskItemVO> getUserTaskPage(@RequestBody PageRequest<TaskItemVO> pageRequest) {
TaskItemVO filter = pageRequest.getFilter(); TaskItemVO filter = pageRequest.getFilter();
PageParam pageParam = pageRequest.getPageParam(); PageParam pageParam = pageRequest.getPageParam();
return learningTaskService.getUserTaskPage(filter, pageParam); return learningTaskService.getUserTaskPage(filter, pageParam);

View File

@@ -162,7 +162,16 @@ public interface LearningTaskMapper extends BaseMapper<TbLearningTask> {
*/ */
List<TbLearningTask> selectLearningTasksPage(@Param("filter") TbLearningTask filter, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles); List<TbLearningTask> selectLearningTasksPage(@Param("filter") TbLearningTask filter, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
List<TbLearningTask> selectUserLearningTasksPage(@Param("filter") TaskItemVO filter, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles); /**
* @description 查询用户学习任务分页(包含权限过滤)
* @param filter 过滤条件
* @param pageParam 分页参数
* @param userDeptRoles 用户部门角色列表
* @return List<TaskItemVO> 学习任务列表(包含用户状态信息)
* @author yslg
* @since 2025-10-15
*/
List<TaskItemVO> selectUserLearningTasksPage(@Param("filter") TaskItemVO filter, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
/** /**
* @description 统计学习任务总数 * @description 统计学习任务总数

View File

@@ -162,8 +162,8 @@ public class SCLearningTaskServiceImpl implements LearningTaskService {
} }
@Override @Override
public ResultDomain<TbLearningTask> getUserTaskPage(TaskItemVO filter, PageParam pageParam) { public ResultDomain<TaskItemVO> getUserTaskPage(TaskItemVO filter, PageParam pageParam) {
ResultDomain<TbLearningTask> resultDomain = new ResultDomain<>(); ResultDomain<TaskItemVO> resultDomain = new ResultDomain<>();
TbSysUser user = LoginUtil.getCurrentUser(); TbSysUser user = LoginUtil.getCurrentUser();
if (user == null) { if (user == null) {
resultDomain.fail("请先登录"); resultDomain.fail("请先登录");
@@ -172,14 +172,14 @@ public class SCLearningTaskServiceImpl implements LearningTaskService {
filter.setUserID(user.getID()); filter.setUserID(user.getID());
// 获取当前用户的部门角色 // 获取当前用户的部门角色
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole(); List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<TbLearningTask> taskList = learningTaskMapper.selectUserLearningTasksPage(filter, pageParam, userDeptRoles); List<TaskItemVO> taskList = learningTaskMapper.selectUserLearningTasksPage(filter, pageParam, userDeptRoles);
long total = learningTaskMapper.countLearningTasks(filter, userDeptRoles); long total = learningTaskMapper.countLearningTasks(filter, userDeptRoles);
pageParam.setTotalElements(total); pageParam.setTotalElements(total);
pageParam.setTotalPages((int) Math.ceil((double) total / pageParam.getPageSize())); pageParam.setTotalPages((int) Math.ceil((double) total / pageParam.getPageSize()));
PageDomain<TbLearningTask> pageDomain = new PageDomain<>(); PageDomain<TaskItemVO> pageDomain = new PageDomain<>();
pageDomain.setDataList(taskList); pageDomain.setDataList(taskList);
pageDomain.setPageParam(pageParam); pageDomain.setPageParam(pageParam);
resultDomain.success("获取任务列表分页成功", pageDomain); resultDomain.success("获取用户任务列表分页成功", pageDomain);
return resultDomain; return resultDomain;
} }

View File

@@ -19,6 +19,18 @@
<result column="deleted" property="deleted" jdbcType="BOOLEAN"/> <result column="deleted" property="deleted" jdbcType="BOOLEAN"/>
</resultMap> </resultMap>
<!-- TaskItemVO 结果映射 -->
<resultMap id="TaskItemResultMap" type="org.xyzh.common.vo.TaskItemVO" extends="BaseResultMap">
<result column="user_id" property="userID" jdbcType="VARCHAR"/>
<result column="username" property="username" jdbcType="VARCHAR"/>
<result column="dept_id" property="deptID" jdbcType="VARCHAR"/>
<result column="dept_name" property="deptName" jdbcType="VARCHAR"/>
<result column="parent_id" property="parentID" jdbcType="VARCHAR"/>
<result column="user_status" property="status" jdbcType="INTEGER"/>
<result column="progress" property="progress" jdbcType="DECIMAL"/>
<result column="complete_time" property="completeTime" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- 基础字段 --> <!-- 基础字段 -->
<sql id="Base_Column_List"> <sql id="Base_Column_List">
id, task_id, name, description, start_time, end_time, status, id, task_id, name, description, start_time, end_time, status,
@@ -282,12 +294,15 @@
</select> </select>
<!-- selectUserLearningTasksPage - 添加权限过滤 --> <!-- selectUserLearningTasksPage - 添加权限过滤 -->
<select id="selectUserLearningTasksPage" resultMap="BaseResultMap"> <select id="selectUserLearningTasksPage" resultMap="TaskItemResultMap">
SELECT DISTINCT SELECT DISTINCT
tlt.id, tlt.task_id, tlt.name, tlt.description, tlt.start_time, tlt.end_time, ttu.status, tlt.id, tlt.task_id, tlt.name, tlt.description, tlt.start_time, tlt.end_time,
tlt.creator, tlt.updater, tlt.create_time, tlt.update_time tlt.creator, tlt.updater, tlt.create_time, tlt.update_time,
ttu.user_id, ttu.status AS user_status, ttu.progress, ttu.complete_time,
ttu.dept_id, d.name AS dept_name, d.parent_id
FROM tb_task_user ttu FROM tb_task_user ttu
INNER JOIN tb_learning_task tlt ON ttu.task_id = tlt.task_id INNER JOIN tb_learning_task tlt ON ttu.task_id = tlt.task_id
LEFT JOIN tb_sys_dept d ON ttu.dept_id = d.dept_id AND d.deleted = 0
INNER JOIN tb_resource_permission rp ON tlt.task_id = rp.resource_id INNER JOIN tb_resource_permission rp ON tlt.task_id = rp.resource_id
AND rp.resource_type = 3 AND rp.resource_type = 3
AND rp.deleted = 0 AND rp.deleted = 0
@@ -311,6 +326,9 @@
WHERE ttu.user_id = #{filter.userID} WHERE ttu.user_id = #{filter.userID}
AND tlt.deleted = 0 AND tlt.deleted = 0
AND ttu.deleted = 0 AND ttu.deleted = 0
<if test="filter.status != null">
AND ttu.status = #{filter.status}
</if>
ORDER BY tlt.create_time DESC ORDER BY tlt.create_time DESC
LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset} LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset}
</select> </select>

View File

@@ -0,0 +1,5 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="60" height="60" rx="4.67532" fill="#F3FFF0"/>
<path d="M42.0005 23.3318V42.0105C42.0005 42.7409 41.4076 43.3327 40.6763 43.3328H19.3257C18.5942 43.3328 18.0005 42.7258 18.0005 42.0105V17.989C18.0005 17.2587 18.5944 16.6667 19.3257 16.6667H35.3345L42.0005 23.3318ZM29.3813 29.9324L26.5532 27.1042L24.6675 28.989L29.3813 33.7039L35.0386 28.0466L33.1528 26.1609L29.3813 29.9324Z" fill="#44BB17"/>
</svg>

After

Width:  |  Height:  |  Size: 518 B

View File

@@ -0,0 +1,7 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="60" height="60" rx="4.67532" fill="#FFFBEB"/>
<path d="M35.3338 16.6667L42.0005 23.3334V42.011C42.0005 42.7414 41.4073 43.3334 40.676 43.3334H19.325C18.5935 43.3334 18.0005 42.7263 18.0005 42.011V17.9891C18.0005 17.2588 18.5938 16.6667 19.325 16.6667H35.3338ZM30.0005 24.6667C27.055 24.6667 24.6672 27.0546 24.6672 30.0001C24.6672 32.9455 27.055 35.3334 30.0005 35.3334C32.946 35.3334 35.3338 32.9455 35.3338 30.0001H30.0005V24.6667Z" fill="#F6A723"/>
<path d="M30.0005 24.6667C27.055 24.6667 24.6672 27.0546 24.6672 30.0001C24.6672 32.9455 27.055 35.3334 30.0005 35.3334C32.946 35.3334 35.3338 32.9455 35.3338 30.0001H30.0005V24.6667Z" fill="#F6A723"/>
<path d="M36.949 27.6489L34.6331 25.333L24.6704 33.0174L24.667 35.3298L26.9863 35.3333L36.949 27.6489Z" fill="#F9F9F9"/>
</svg>

After

Width:  |  Height:  |  Size: 903 B

View File

@@ -0,0 +1,5 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="60" height="60" rx="4.67532" fill="#F8FBFC"/>
<path d="M35.3338 16.6667L42.0005 23.3321V42.011C42.0005 42.7414 41.4073 43.3334 40.676 43.3334H19.325C18.5935 43.3334 18.0005 42.7263 18.0005 42.011V17.9891C18.0005 17.2588 18.5938 16.6667 19.325 16.6667H35.3338ZM31.3338 26.0001H28.6672V34.0001H35.3338V31.3334H31.3338V26.0001Z" fill="#38BDF8"/>
</svg>

After

Width:  |  Height:  |  Size: 467 B

View File

@@ -0,0 +1,5 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="60" height="60" rx="4.67532" fill="#F4FAFF"/>
<path d="M42.0015 23.334V42.0127C42.0012 42.7429 41.4075 43.335 40.6763 43.335H19.3247C18.5935 43.3348 18.0007 42.7277 18.0005 42.0127V17.9902C18.0005 17.26 18.5936 16.6672 19.3247 16.667H35.3345L42.0015 23.334ZM24.6675 32.668V35.334H35.3345V32.668H24.6675ZM24.6675 27.334V30.001H35.3345V27.334H24.6675ZM24.6675 22.001V24.667H28.6675V22.001H24.6675Z" fill="#4DAEFF"/>
</svg>

After

Width:  |  Height:  |  Size: 538 B

View File

@@ -1,6 +1,5 @@
<template> <template>
<ArticleAdd <ArticleAdd
v-if="articleId !== undefined"
:article-id="articleId" :article-id="articleId"
:show-back-button="true" :show-back-button="true"
back-button-text="返回" back-button-text="返回"

View File

@@ -12,123 +12,106 @@
<!-- 任务详情 --> <!-- 任务详情 -->
<div v-else-if="taskVO" class="task-content"> <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"> <!-- 任务基本信息卡片 -->
<div class="task-info-card">
<div class="section-header">
<div class="header-bar"></div>
<span class="header-text">任务信息</span>
</div>
<div class="task-info-section">
<!-- 任务信息标题 -->
<!-- 任务标题 -->
<h1 class="task-title">{{ taskVO.learningTask.name }}</h1> <h1 class="task-title">{{ taskVO.learningTask.name }}</h1>
<!-- 任务描述 -->
<p v-if="taskVO.learningTask.description" class="task-description">
{{ taskVO.learningTask.description }}
</p>
<!-- 任务元信息 -->
<div class="task-meta"> <div class="task-meta">
<div class="meta-row">
<div class="meta-item"> <div class="meta-item">
<el-icon><Calendar /></el-icon> <img src="@/assets/imgs/default-avatar.png" alt="创建者" class="creator-avatar" />
<span class="meta-label">开始时间</span> <span>{{ taskVO.learningTask.creator || '系统' }}</span>
<span class="meta-value">{{ formatDate(taskVO.learningTask.startTime) }}</span>
</div> </div>
<div class="meta-divider"></div>
<div class="meta-item"> <div class="meta-item">
<el-icon><Calendar /></el-icon> <img src="@/assets/imgs/clock.svg" alt="时间" class="meta-icon" />
<span class="meta-label">结束时间</span> <span>截止时间{{ formatDate(taskVO.learningTask.endTime) }}{{ getDeadlineText() }}</span>
<span class="meta-value">{{ formatDate(taskVO.learningTask.endTime) }}</span> </div>
<div class="meta-divider"></div>
<div class="meta-item">
<img src="@/assets/imgs/book-read.svg" alt="学习" class="meta-icon" />
<span>{{ taskVO.taskUsers?.length || 0 }}人学习</span>
</div> </div>
</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="task-stats">
<div class="stat-item"> <div class="stat-card">
<div class="stat-icon total">
<el-icon><Document /></el-icon>
</div>
<div class="stat-content"> <div class="stat-content">
<div class="stat-label">总任务数</div> <div class="stat-label">总任务数</div>
<div class="stat-value">{{ taskVO.totalTaskNum || 0 }}</div> <div class="stat-value">{{ taskVO.totalTaskNum || 0 }}</div>
</div> </div>
<img src="@/assets/imgs/task-total.svg" alt="总任务数" class="meta-icon" />
</div> </div>
<div class="stat-item"> <div class="stat-card">
<div class="stat-icon completed">
<el-icon><CircleCheck /></el-icon>
</div>
<div class="stat-content"> <div class="stat-content">
<div class="stat-label">已完成</div> <div class="stat-label">已完成</div>
<div class="stat-value">{{ taskVO.completedTaskNum || 0 }}</div> <div class="stat-value">{{ taskVO.completedTaskNum || 0 }}</div>
</div> </div>
<img src="@/assets/imgs/task-finish.svg" alt="已完成" class="meta-icon" />
</div> </div>
<div class="stat-item"> <div class="stat-card">
<div class="stat-icon learning">
<el-icon><Reading /></el-icon>
</div>
<div class="stat-content"> <div class="stat-content">
<div class="stat-label">学习中</div> <div class="stat-label">学习中</div>
<div class="stat-value">{{ taskVO.learningTaskNum || 0 }}</div> <div class="stat-value">{{ taskVO.learningTaskNum || 0 }}</div>
</div> </div>
<img src="@/assets/imgs/task-learning.svg" alt="学习中" class="meta-icon" />
</div> </div>
<div class="stat-item"> <div class="stat-card">
<div class="stat-icon pending">
<el-icon><Clock /></el-icon>
</div>
<div class="stat-content"> <div class="stat-content">
<div class="stat-label">未开始</div> <div class="stat-label">未开始</div>
<div class="stat-value">{{ taskVO.notStartTaskNum || 0 }}</div> <div class="stat-value">{{ taskVO.notStartTaskNum || 0 }}</div>
</div> </div>
<img src="@/assets/imgs/task-notstart.svg" alt="未开始" class="meta-icon" />
</div> </div>
</div> </div>
<!-- 学习进度 --> <!-- 进度条区域 -->
<div v-if="taskVO.totalTaskNum && taskVO.totalTaskNum > 0" class="learning-progress"> <div v-if="taskVO.totalTaskNum && taskVO.totalTaskNum > 0" class="progress-section">
<div class="progress-header"> <div class="progress-header">
<span>学习进度</span> <span class="progress-label">当前任务进度</span>
<span class="progress-text">{{ progressPercent }}%</span> <span class="progress-value">已完成{{ progressPercent }}%</span>
</div> </div>
<el-progress <div class="progress-bar-container">
:percentage="progressPercent" <div class="progress-bar">
:stroke-width="12" <div class="progress-fill" :style="{ width: progressPercent + '%' }"></div>
:color="progressColor"
/>
</div> </div>
</div> </div>
</el-card> </div>
</div>
</div>
<div class="task-content-section">
<div class="section-header">
<div class="header-bar"></div>
<span class="header-text">任务信息</span>
</div>
<!-- 课程列表 --> <!-- 课程列表 -->
<el-card v-if="taskVO.taskCourses && taskVO.taskCourses.length > 0" class="course-card" shadow="hover"> <el-card v-if="taskVO.taskCourses && taskVO.taskCourses.length > 0" class="course-card" shadow="hover">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<div class="header-left"> <div class="header-left">
<el-icon class="header-icon"><VideoPlay /></el-icon> <el-icon class="header-icon">
<VideoPlay />
</el-icon>
<span class="header-title">学习课程</span> <span class="header-title">学习课程</span>
</div> </div>
<span class="item-count"> {{ taskVO.taskCourses.length }} 门课程</span> <span class="item-count"> {{ taskVO.taskCourses.length }} 门课程</span>
@@ -136,35 +119,25 @@
</template> </template>
<div class="course-list"> <div class="course-list">
<div <div v-for="(course, index) in taskVO.taskCourses" :key="course.courseID" class="course-item"
v-for="(course, index) in taskVO.taskCourses" @click="handleCourseClick(course)">
:key="course.courseID"
class="course-item"
@click="handleCourseClick(course)"
>
<div class="course-index">{{ index + 1 }}</div> <div class="course-index">{{ index + 1 }}</div>
<div class="course-info"> <div class="course-info">
<div class="course-name-row"> <div class="course-name-row">
<span class="course-name">{{ course.courseName }}</span> <span class="course-name">{{ course.courseName }}</span>
<el-tag <el-tag v-if="course.required" type="danger" size="small" effect="plain">
v-if="course.required"
type="danger"
size="small"
effect="plain"
>
必修 必修
</el-tag> </el-tag>
</div> </div>
<div class="course-meta"> <div class="course-meta">
<div class="status-info"> <div class="status-info">
<el-tag <el-tag :type="getItemStatusType(course.status)" size="small">
:type="getItemStatusType(course.status)"
size="small"
>
{{ getItemStatusText(course.status) }} {{ getItemStatusText(course.status) }}
</el-tag> </el-tag>
<span v-if="course.status === 2 && course.completeTime" class="complete-time"> <span v-if="course.status === 2 && course.completeTime" class="complete-time">
<el-icon><CircleCheck /></el-icon> <el-icon>
<CircleCheck />
</el-icon>
{{ formatDateTime(course.completeTime) }} {{ formatDateTime(course.completeTime) }}
</span> </span>
</div> </div>
@@ -174,12 +147,9 @@
</div> </div>
</div> </div>
<div class="course-action"> <div class="course-action">
<el-button <el-button type="primary"
type="primary"
:icon="course.status === 2 ? CircleCheck : (course.status === 1 ? VideoPlay : Reading)" :icon="course.status === 2 ? CircleCheck : (course.status === 1 ? VideoPlay : Reading)"
:disabled="course.status === 2" :disabled="course.status === 2" @click.stop="handleCourseClick(course)">
@click.stop="handleCourseClick(course)"
>
{{ course.status === 2 ? '已完成' : (course.status === 1 ? '继续学习' : '开始学习') }} {{ course.status === 2 ? '已完成' : (course.status === 1 ? '继续学习' : '开始学习') }}
</el-button> </el-button>
</div> </div>
@@ -192,7 +162,9 @@
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<div class="header-left"> <div class="header-left">
<el-icon class="header-icon"><Document /></el-icon> <el-icon class="header-icon">
<Document />
</el-icon>
<span class="header-title">学习资源</span> <span class="header-title">学习资源</span>
</div> </div>
<span class="item-count"> {{ taskVO.taskResources.length }} 篇文章</span> <span class="item-count"> {{ taskVO.taskResources.length }} 篇文章</span>
@@ -200,35 +172,25 @@
</template> </template>
<div class="resource-list"> <div class="resource-list">
<div <div v-for="(resource, index) in taskVO.taskResources" :key="resource.resourceID" class="resource-item"
v-for="(resource, index) in taskVO.taskResources" @click="handleResourceClick(resource)">
:key="resource.resourceID"
class="resource-item"
@click="handleResourceClick(resource)"
>
<div class="resource-index">{{ index + 1 }}</div> <div class="resource-index">{{ index + 1 }}</div>
<div class="resource-info"> <div class="resource-info">
<div class="resource-name-row"> <div class="resource-name-row">
<span class="resource-name">{{ resource.resourceName }}</span> <span class="resource-name">{{ resource.resourceName }}</span>
<el-tag <el-tag v-if="resource.required" type="danger" size="small" effect="plain">
v-if="resource.required"
type="danger"
size="small"
effect="plain"
>
必修 必修
</el-tag> </el-tag>
</div> </div>
<div class="resource-meta"> <div class="resource-meta">
<div class="status-info"> <div class="status-info">
<el-tag <el-tag :type="getItemStatusType(resource.status)" size="small">
:type="getItemStatusType(resource.status)"
size="small"
>
{{ getItemStatusText(resource.status) }} {{ getItemStatusText(resource.status) }}
</el-tag> </el-tag>
<span v-if="resource.status === 2 && resource.completeTime" class="complete-time"> <span v-if="resource.status === 2 && resource.completeTime" class="complete-time">
<el-icon><CircleCheck /></el-icon> <el-icon>
<CircleCheck />
</el-icon>
{{ formatDateTime(resource.completeTime) }} {{ formatDateTime(resource.completeTime) }}
</span> </span>
</div> </div>
@@ -238,12 +200,9 @@
</div> </div>
</div> </div>
<div class="resource-action"> <div class="resource-action">
<el-button <el-button type="primary"
type="primary"
:icon="resource.status === 2 ? CircleCheck : (resource.status === 1 ? Reading : View)" :icon="resource.status === 2 ? CircleCheck : (resource.status === 1 ? Reading : View)"
:disabled="resource.status === 2" :disabled="resource.status === 2" @click.stop="handleResourceClick(resource)">
@click.stop="handleResourceClick(resource)"
>
{{ resource.status === 2 ? '已完成' : (resource.status === 1 ? '继续阅读' : '开始阅读') }} {{ resource.status === 2 ? '已完成' : (resource.status === 1 ? '继续阅读' : '开始阅读') }}
</el-button> </el-button>
</div> </div>
@@ -252,16 +211,14 @@
</el-card> </el-card>
<!-- 空状态 --> <!-- 空状态 -->
<el-card <el-card v-if="(!taskVO.taskCourses || taskVO.taskCourses.length === 0) &&
v-if="(!taskVO.taskCourses || taskVO.taskCourses.length === 0) && (!taskVO.taskResources || taskVO.taskResources.length === 0)" class="empty-card" shadow="never">
(!taskVO.taskResources || taskVO.taskResources.length === 0)"
class="empty-card"
shadow="never"
>
<el-empty description="暂无学习内容" /> <el-empty description="暂无学习内容" />
</el-card> </el-card>
</div> </div>
</div>
<!-- 加载失败 --> <!-- 加载失败 -->
<div v-else class="error-tip"> <div v-else class="error-tip">
<el-empty description="加载任务失败" /> <el-empty description="加载任务失败" />
@@ -275,10 +232,6 @@ import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { import {
ArrowLeft, ArrowLeft,
Memo,
Calendar,
User,
Clock,
Document, Document,
CircleCheck, CircleCheck,
Reading, Reading,
@@ -323,13 +276,6 @@ const progressPercent = computed(() => {
return Math.round((completed / total) * 100); 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) => { watch(() => currentTaskId.value, (newId) => {
if (newId) { if (newId) {
@@ -416,7 +362,25 @@ function handleBack() {
function formatDate(dateString?: string): string { function formatDate(dateString?: string): string {
if (!dateString) return '--'; if (!dateString) return '--';
const date = new Date(dateString); const date = new Date(dateString);
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`; return `${date.getFullYear()}.${(date.getMonth() + 1).toString().padStart(2, '0')}.${date.getDate().toString().padStart(2, '0')}`;
}
// 获取截止时间提示文本
function getDeadlineText(): string {
if (!taskVO.value?.learningTask?.endTime) return '';
const now = new Date();
const endTime = new Date(taskVO.value.learningTask.endTime);
const diffTime = endTime.getTime() - now.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays < 0) {
return '(已截止)';
} else if (diffDays === 0) {
return '(今天截止)';
} else {
return `${diffDays}天后截止)`;
}
} }
// 格式化日期时间 // 格式化日期时间
@@ -426,34 +390,6 @@ function formatDateTime(dateString?: string): string {
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')}`; 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 { function getItemStatusText(status?: number): string {
switch (status) { switch (status) {
@@ -485,7 +421,7 @@ function getItemStatusType(status?: number): 'info' | 'warning' | 'success' {
<style lang="scss" scoped> <style lang="scss" scoped>
.task-detail { .task-detail {
min-height: 100vh; height: 100%;
background: #f5f7fa; background: #f5f7fa;
padding-bottom: 60px; padding-bottom: 60px;
} }
@@ -514,6 +450,13 @@ function getItemStatusType(status?: number): 'info' | 'warning' | 'success' {
padding: 24px; padding: 24px;
} }
.task-content-section {
padding: 20px;
background: #FFFFFF;
border-radius: 10px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
}
.card-header { .card-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -542,179 +485,199 @@ function getItemStatusType(status?: number): 'info' | 'warning' | 'success' {
} }
} }
// 任务信息卡片 // 任务信息卡片容器
.task-info-card { .task-info-card {
background: #FFFFFF;
border-radius: 10px;
padding: 20px;
margin-bottom: 24px; margin-bottom: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
: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 { .section-header {
.task-title { display: flex;
font-size: 28px;
align-items: center;
gap: 4px;
height: 25px;
}
.header-bar {
width: 3px;
height: 14px;
background: #C62828;
border-radius: 2px;
}
.header-text {
font-family: 'PingFang SC';
font-weight: 600; font-weight: 600;
color: #303133; font-size: 16px;
margin: 0 0 24px 0; line-height: 1.5;
color: #1E293B;
}
// 任务信息区域
.task-info-section {
display: flex;
flex-direction: column;
gap: 30px;
padding: 10px;
.task-title {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 20px;
line-height: 1.4; line-height: 1.4;
color: #141F38;
// margin: -5px 0 0 0;
}
.task-description {
font-family: 'PingFang SC';
font-weight: 400;
font-size: 14px;
line-height: 1.5714285714285714;
color: rgba(0, 0, 0, 0.3);
margin: -20px 0 0 0;
max-height: 66px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
} }
.task-meta { .task-meta {
display: flex; display: flex;
flex-direction: column; align-items: center;
gap: 16px; gap: 7px;
margin-bottom: 24px; margin-top: -10px;
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
.meta-row {
display: flex;
gap: 40px;
flex-wrap: wrap;
}
.meta-item { .meta-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 7px;
color: #606266; font-family: 'PingFang SC';
font-weight: 400;
font-size: 14px; font-size: 14px;
line-height: 1.5714285714285714;
color: #4E5969;
.el-icon { .creator-avatar {
color: #909399; width: 20px;
font-size: 16px; height: 20px;
border-radius: 10px;
object-fit: cover;
} }
.meta-label { .meta-icon {
color: #909399; width: 18px;
} height: 18px;
.meta-value {
color: #303133;
font-weight: 500;
}
} }
} }
.task-description { .meta-divider {
margin-bottom: 24px; width: 0;
height: 14px;
.section-subtitle { border-left: 1px solid #F1F5F9;
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 { .task-stats {
display: grid; display: flex;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 32px;
gap: 16px; flex-wrap: wrap;
margin-bottom: 24px;
.stat-item { .stat-card {
width: calc(25% - 24px);
height: 95px;
padding: 7px 20px;
background: #FEFEFF;
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 10px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; justify-content: space-between;
padding: 20px; gap: 10px;
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 { .stat-content {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
flex: 1; flex: 1;
gap: 8px;
.stat-label { .stat-label {
font-size: 13px; font-family: 'PingFang SC';
color: #909399; font-weight: 600;
margin-bottom: 4px; font-size: 16px;
line-height: 1.5714285714285714;
color: #4E5969;
} }
.stat-value { .stat-value {
font-size: 24px; font-family: 'PingFang SC';
font-weight: 600; font-weight: 600;
color: #303133; font-size: 32px;
} line-height: 1;
} color: #141F38;
} }
} }
.learning-progress { .meta-icon {
padding: 20px; width: 66px;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); height: 66px;
border-radius: 8px; flex-shrink: 0;
}
}
}
.progress-section {
background: #F7F8F9;
border-radius: 10px;
padding: 31px 36px;
height: 100px;
display: flex;
flex-direction: column;
justify-content: center;
gap: 6px;
.progress-header { .progress-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: 12px; align-items: center;
font-size: 14px;
font-weight: 500;
.progress-text { .progress-label,
color: #409eff; .progress-value {
font-weight: 600; font-family: 'PingFang SC';
font-weight: 500;
font-size: 16px; font-size: 16px;
line-height: 1.5;
color: #334155;
}
}
.progress-bar-container {
width: 100%;
.progress-bar {
width: 100%;
height: 8px;
background: #FFFFFF;
border-radius: 27px;
overflow: hidden;
.progress-fill {
height: 100%;
background: #3D3D3D;
border-radius: 27px;
transition: width 0.3s ease;
}
} }
} }
} }
@@ -722,8 +685,8 @@ function getItemStatusType(status?: number): 'info' | 'warning' | 'success' {
// 课程卡片 // 课程卡片
.course-card { .course-card {
margin-bottom: 24px; margin-top: 24px;
// padding: 10px;
:deep(.el-card__header) { :deep(.el-card__header) {
background: #fff9f0; background: #fff9f0;
} }
@@ -825,7 +788,7 @@ function getItemStatusType(status?: number): 'info' | 'warning' | 'success' {
// 资源卡片 // 资源卡片
.resource-card { .resource-card {
margin-bottom: 24px; margin-top: 24px;
:deep(.el-card__header) { :deep(.el-card__header) {
background: #f0f9ff; background: #f0f9ff;
@@ -974,4 +937,3 @@ function getItemStatusType(status?: number): 'info' | 'warning' | 'success' {
} }
} }
</style> </style>

View File

@@ -43,7 +43,7 @@ import { Search, CenterHead } from '@/components/base';
import type { Resource, Tag } from '@/types/resource'; import type { Resource, Tag } from '@/types/resource';
const showArticle = ref(false); const showArticle = ref(false);
const currentCategoryId = ref('party_history'); const currentCategoryId = ref('tag_article_001');
const currentCategoryName = ref('党史学习'); const currentCategoryName = ref('党史学习');
const currentResourceId = ref(''); const currentResourceId = ref('');
const searchKeyword = ref(''); const searchKeyword = ref('');
@@ -99,6 +99,7 @@ async function handleArticleNavigate(direction: 'prev' | 'next', resourceId: str
<style scoped lang="scss"> <style scoped lang="scss">
.resource-center-view { .resource-center-view {
background: #F9F9F9; background: #F9F9F9;
height: 100%;
} }
.search-wrapper { .search-wrapper {

View File

@@ -275,12 +275,5 @@ defineExpose({
color: #979797; color: #979797;
} }
.pagination-wrapper {
display: flex;
justify-content: center;
padding: 30px 0;
background: #FFFFFF;
border-top: 1px solid #EEEEEE;
}
</style> </style>

View File

@@ -13,7 +13,6 @@
v-model="searchKeyword" v-model="searchKeyword"
placeholder="搜索思政资源" placeholder="搜索思政资源"
class="search-input" class="search-input"
clearable
@keyup.enter="handleSearch" @keyup.enter="handleSearch"
> >
<template #suffix> <template #suffix>
@@ -220,6 +219,7 @@ function getCategoryName(): string {
max-width: 400px; max-width: 400px;
.search-input { .search-input {
border: none;
:deep(.el-input__wrapper) { :deep(.el-input__wrapper) {
border-radius: 30px; border-radius: 30px;
padding-right: 4px; padding-right: 4px;

View File

@@ -2,7 +2,7 @@
<LearingTaskDetail <LearingTaskDetail
:task-id="taskId" :task-id="taskId"
:show-back-button="true" :show-back-button="true"
back-button-text="返回任务列表" back-button-text="返回"
@back="handleBack" @back="handleBack"
/> />
</template> </template>

View File

@@ -54,10 +54,14 @@
v-for="task in taskList" v-for="task in taskList"
:key="task.taskID" :key="task.taskID"
class="task-card" class="task-card"
@click="handleTaskClick(task)"
> >
<!-- 背景装饰 -->
<div class="task-bg-decoration"></div>
<!-- 内容容器 --> <!-- 内容容器 -->
<div class="task-content"> <div class="task-content">
<!-- 顶部信息区 -->
<div class="task-header">
<!-- 状态标签小标签自适应宽度 --> <!-- 状态标签小标签自适应宽度 -->
<div <div
class="status-tag" class="status-tag"
@@ -72,27 +76,41 @@
<!-- 任务标题 --> <!-- 任务标题 -->
<h3 class="task-title">{{ task.name }}</h3> <h3 class="task-title">{{ task.name }}</h3>
</div>
<!-- 任务描述 --> <!-- 任务描述 -->
<p class="task-desc">{{ task.description || '暂无描述' }}</p> <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-footer">
<span class="task-time"> <div class="task-time">
任务时间{{ formatDate(task.startTime) }}-{{ formatDate(task.endTime) }} <el-icon class="calendar-icon"><Calendar /></el-icon>
</span> <span>任务时间{{ formatDate(task.startTime) }}-{{ formatDate(task.endTime) }}</span>
<div <span
v-if="getDeadlineStatus(task).show" v-if="getDeadlineStatus(task).show"
class="deadline-tag" class="deadline-text"
:class="{ :class="'deadline-' + getDeadlineStatus(task).type"
'deadline-danger': getDeadlineStatus(task).type === 'danger',
'deadline-info': getDeadlineStatus(task).type === 'info',
'deadline-success': getDeadlineStatus(task).type === 'success',
'deadline-primary': getDeadlineStatus(task).type === 'primary',
}"
> >
{{ getDeadlineStatus(task).text }} {{ getDeadlineStatus(task).text }}
</span>
</div> </div>
<button
class="start-btn"
@click.stop="handleTaskClick(task)"
>
{{ task.status === 0 ? '开始学习' : '继续学习' }}
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -110,10 +128,10 @@
import { ref, onMounted, computed } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { DocumentCopy, DocumentChecked } from '@element-plus/icons-vue'; import { DocumentCopy, DocumentChecked, Calendar } from '@element-plus/icons-vue';
import { useStore } from 'vuex'; import { useStore } from 'vuex';
import { learningTaskApi } from '@/apis/study'; import { learningTaskApi } from '@/apis/study';
import type { LearningTask, TaskItemVO } from '@/types'; import type { TaskItemVO } from '@/types';
import { StudyPlanLayout } from '@/views/user/study-plan'; import { StudyPlanLayout } from '@/views/user/study-plan';
defineOptions({ defineOptions({
@@ -123,7 +141,7 @@ defineOptions({
const router = useRouter(); const router = useRouter();
const store = useStore(); const store = useStore();
const loading = ref(false); const loading = ref(false);
const taskList = ref<LearningTask[]>([]); const taskList = ref<TaskItemVO[]>([]);
// 统计数据 // 统计数据
const totalCount = ref(0); const totalCount = ref(0);
@@ -210,7 +228,7 @@ async function loadUserProgress() {
// 点击任务卡片 // 点击任务卡片
function handleTaskClick(task: LearningTask) { function handleTaskClick(task: TaskItemVO) {
if (!task.taskID) { if (!task.taskID) {
ElMessage.warning('任务ID不存在'); ElMessage.warning('任务ID不存在');
return; return;
@@ -232,8 +250,14 @@ function formatDate(dateString?: string): string {
return `${date.getFullYear()}.${(date.getMonth() + 1).toString().padStart(2, '0')}.${date.getDate().toString().padStart(2, '0')}`; 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: LearningTask): { show: boolean; text: string; type: 'danger' | 'info' | 'success' | 'primary' } { function getDeadlineStatus(task: TaskItemVO): { show: boolean; text: string; type: 'danger' | 'info' | 'success' | 'primary' } {
if (!task.endTime) { if (!task.endTime) {
return { show: false, text: '', type: 'primary' }; return { show: false, text: '', type: 'primary' };
} }
@@ -426,23 +450,46 @@ function getDeadlineStatus(task: LearningTask): { show: boolean; text: string; t
} }
.task-card { .task-card {
position: relative;
background: #FFFFFF; background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.05); border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 10px; border-radius: 10px;
padding: 40px; padding: 40px;
cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
min-height: 278px; height: 376px;
overflow: hidden;
&:hover { &:hover {
box-shadow: 0 8px 20px rgba(164, 182, 199, 0.2); box-shadow: 0 8px 20px rgba(164, 182, 199, 0.2);
transform: translateY(-2px); 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 { .task-content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
height: 100%;
gap: 0;
// 顶部信息区
.task-header {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 18px; gap: 18px;
margin-bottom: 18px;
// 状态标签 - 小标签,自适应宽度 // 状态标签 - 小标签,自适应宽度
.status-tag { .status-tag {
@@ -489,67 +536,134 @@ function getDeadlineStatus(task: LearningTask): { show: boolean; text: string; t
font-weight: 400; font-weight: 400;
color: #334155; color: #334155;
line-height: 1.5714285714285714; line-height: 1.5714285714285714;
margin: 0; margin: 0 0 20px 0;
flex: 1;
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
-webkit-line-clamp: 2; -webkit-line-clamp: 3;
line-clamp: 2; line-clamp: 3;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
// 学习进度
.task-progress {
margin-bottom: 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 { .task-footer {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: 20px; gap: 20px;
margin-top: auto;
.task-time { .task-time {
display: flex;
align-items: center;
gap: 3px;
font-size: 14px; font-size: 14px;
font-weight: 400; font-weight: 400;
color: #979797; color: #979797;
line-height: 1.5714285714285714; line-height: 1.5714285714285714;
flex: 1; flex: 1;
.calendar-icon {
font-size: 16px;
color: #979797;
flex-shrink: 0;
} }
// 截止时间标签 // 截止时间文本
.deadline-tag { .deadline-text {
flex-shrink: 0; font-size: 14px;
display: inline-flex; font-weight: 400;
align-items: center; line-height: 1.5714285714285714;
gap: 4px;
padding: 0 8px;
font-size: 12px;
font-weight: 500;
line-height: 1.6666666666666667;
border-radius: 2px;
border: 1px solid;
background-color: transparent;
// 即将截止 - 红色边框 // 即将截止 - 红色
&.deadline-danger { &.deadline-danger {
border-color: #F53F3F;
color: #F53F3F; color: #F53F3F;
} }
// 已截止 - 灰色边框 // 已截止 - 灰色
&.deadline-info { &.deadline-info {
border-color: #979797;
color: #979797; color: #979797;
} }
// 已完成 - 绿色边框
// 已完成 - 绿色
&.deadline-success { &.deadline-success {
border-color: #00B42A;
color: #00B42A; color: #00B42A;
} }
// 进行中 - 蓝色边框
// 进行中 - 蓝色
&.deadline-primary { &.deadline-primary {
border-color: #3491FA;
color: #3491FA; 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 { .empty-state {