文章、课程标签的默认封面

This commit is contained in:
2025-12-24 15:44:07 +08:00
parent 46464f36a0
commit 878133fb40
22 changed files with 1278 additions and 25 deletions

View File

@@ -31,9 +31,7 @@ CREATE TABLE `tb_learning_task_tag` (
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `uk_task_tag` (`task_id`, `tag_id`), UNIQUE KEY `uk_task_tag` (`task_id`, `tag_id`),
KEY `idx_task_id` (`task_id`), KEY `idx_task_id` (`task_id`),
KEY `idx_tag_id` (`tag_id`), KEY `idx_tag_id` (`tag_id`)
CONSTRAINT `fk_task_tag_task` FOREIGN KEY (`task_id`) REFERENCES `tb_learning_task` (`task_id`) ON DELETE CASCADE,
CONSTRAINT `fk_task_tag_tag` FOREIGN KEY (`tag_id`) REFERENCES `tb_tag` (`tag_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='学习任务标签关联表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='学习任务标签关联表';

View File

@@ -99,6 +99,22 @@ CREATE TABLE `tb_tag` (
KEY `idx_tag_type` (`tag_type`) KEY `idx_tag_type` (`tag_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='标签表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='标签表';
DROP TABLE IF EXISTS `tb_tag_default_cover`;
CREATE TABLE IF NOT EXISTS `tb_tag_default_cover` (
`id` VARCHAR(32) NOT NULL COMMENT '主键ID',
`tag_id` VARCHAR(32) NOT NULL COMMENT '标签ID',
`cover_image` VARCHAR(255) NOT NULL COMMENT '封面图片fileID',
`order_num` INT DEFAULT 0 COMMENT '排序号',
`is_active` TINYINT(1) DEFAULT 1 COMMENT '是否启用0-禁用1-启用',
`creator` VARCHAR(32) DEFAULT NULL COMMENT '创建人',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(32) DEFAULT NULL COMMENT '更新人',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
INDEX `idx_tag_id` (`tag_id`),
INDEX `idx_active` (`is_active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签默认封面配置表';
-- 资源标签关联表 -- 资源标签关联表
DROP TABLE IF EXISTS `tb_resource_tag`; DROP TABLE IF EXISTS `tb_resource_tag`;
CREATE TABLE `tb_resource_tag` ( CREATE TABLE `tb_resource_tag` (

View File

@@ -0,0 +1,65 @@
package org.xyzh.api.news.tag;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.resource.TbTagDefaultCover;
import java.util.List;
/**
* @description 标签默认封面服务接口
* @author system
* @since 2025-12-24
*/
public interface TagDefaultCoverService {
/**
* 根据标签ID获取启用的默认封面列表
* @param tagID 标签ID
* @return 默认封面列表
*/
ResultDomain<TbTagDefaultCover> getDefaultCovers(String tagID);
/**
* 根据标签ID获取所有默认封面列表包括禁用的
* @param tagID 标签ID
* @return 默认封面列表
*/
ResultDomain<TbTagDefaultCover> getAllDefaultCovers(String tagID);
/**
* 添加默认封面
* @param cover 默认封面对象
* @return 操作结果
*/
ResultDomain<String> addDefaultCover(TbTagDefaultCover cover);
/**
* 更新默认封面
* @param cover 默认封面对象
* @return 操作结果
*/
ResultDomain<String> updateDefaultCover(TbTagDefaultCover cover);
/**
* 删除默认封面
* @param id 主键ID
* @return 操作结果
*/
ResultDomain<String> deleteDefaultCover(String id);
/**
* 批量添加默认封面
* @param covers 默认封面列表
* @return 操作结果
*/
ResultDomain<String> batchAddDefaultCovers(List<TbTagDefaultCover> covers);
/**
* @description 根据标签ID删除所有默认封面
* @param tagID 标签ID
* @return ResultDomain<String> 操作结果
* @author system
* @since 2025-12-24
*/
ResultDomain<String> deleteDefaultCoversByTagId(String tagID);
}

View File

@@ -0,0 +1,106 @@
package org.xyzh.common.dto.resource;
import org.xyzh.common.dto.BaseDTO;
/**
* @description 标签默认封面配置表
* @filename TbTagDefaultCover.java
* @author system
* @copyright xyzh
* @since 2025-12-24
*/
public class TbTagDefaultCover extends BaseDTO {
private static final long serialVersionUID = 1L;
/**
* 标签ID
*/
private String tagID;
/**
* 封面图片fileID
*/
private String coverImage;
/**
* 排序号
*/
private Integer orderNum;
/**
* 是否启用0-禁用1-启用
*/
private Integer isActive;
/**
* 创建人
*/
private String creator;
/**
* 更新人
*/
private String updater;
public String getTagID() {
return tagID;
}
public void setTagID(String tagID) {
this.tagID = tagID;
}
public String getCoverImage() {
return coverImage;
}
public void setCoverImage(String coverImage) {
this.coverImage = coverImage;
}
public Integer getOrderNum() {
return orderNum;
}
public void setOrderNum(Integer orderNum) {
this.orderNum = orderNum;
}
public Integer getIsActive() {
return isActive;
}
public void setIsActive(Integer isActive) {
this.isActive = isActive;
}
public String getCreator() {
return creator;
}
public void setCreator(String creator) {
this.creator = creator;
}
public String getUpdater() {
return updater;
}
public void setUpdater(String updater) {
this.updater = updater;
}
@Override
public String toString() {
return "TbTagDefaultCover{" +
"id='" + getId() + '\'' +
", tagID='" + tagID + '\'' +
", coverImage='" + coverImage + '\'' +
", orderNum=" + orderNum +
", isActive=" + isActive +
", creator='" + creator + '\'' +
", createTime=" + getCreateTime() +
'}';
}
}

View File

@@ -69,6 +69,11 @@ public class TbCourse extends BaseDTO {
*/ */
private Integer orderNum; private Integer orderNum;
/**
* @description 标签ID用于获取默认封面
*/
private String tagID;
/** /**
* @description 创建者 * @description 创建者
*/ */
@@ -182,6 +187,14 @@ public class TbCourse extends BaseDTO {
this.orderNum = orderNum; this.orderNum = orderNum;
} }
public String getTagID() {
return tagID;
}
public void setTagID(String tagID) {
this.tagID = tagID;
}
public String getCreator() { public String getCreator() {
return creator; return creator;
} }

View File

@@ -68,6 +68,11 @@ public class CourseItemVO extends BaseDTO {
*/ */
private Integer learnCount; private Integer learnCount;
/**
* @description 标签ID用于获取默认封面和分类
*/
private String tagID;
/** /**
* @description 课程创建时间 * @description 课程创建时间
*/ */
@@ -235,6 +240,14 @@ public class CourseItemVO extends BaseDTO {
this.learnCount = learnCount; this.learnCount = learnCount;
} }
public String getTagID() {
return tagID;
}
public void setTagID(String tagID) {
this.tagID = tagID;
}
public Date getCreateTime() { public Date getCreateTime() {
return createTime; return createTime;
} }
@@ -390,6 +403,7 @@ public class CourseItemVO extends BaseDTO {
course.setId(this.getId()); course.setId(this.getId());
course.setCourseID(this.courseID); course.setCourseID(this.courseID);
course.setName(this.name); course.setName(this.name);
course.setTagID(this.tagID);
course.setCoverImage(this.coverImage); course.setCoverImage(this.coverImage);
course.setDescription(this.description); course.setDescription(this.description);
course.setDuration(this.duration); course.setDuration(this.duration);
@@ -423,6 +437,7 @@ public class CourseItemVO extends BaseDTO {
vo.setViewCount(course.getViewCount()); vo.setViewCount(course.getViewCount());
vo.setLearnCount(course.getLearnCount()); vo.setLearnCount(course.getLearnCount());
vo.setCreateTime(course.getCreateTime()); vo.setCreateTime(course.getCreateTime());
vo.setTagID(course.getTagID());
return vo; return vo;
} }

View File

@@ -5,9 +5,11 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.xyzh.api.news.tag.TagService; import org.xyzh.api.news.tag.TagService;
import org.xyzh.api.news.tag.TagDefaultCoverService;
import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.resource.TbTag; import org.xyzh.common.dto.resource.TbTag;
import org.xyzh.common.dto.resource.TbResourceTag; import org.xyzh.common.dto.resource.TbResourceTag;
import org.xyzh.common.dto.resource.TbTagDefaultCover;
import java.util.List; import java.util.List;
@@ -26,6 +28,9 @@ public class TagController {
@Autowired @Autowired
private TagService tagService; private TagService tagService;
@Autowired
private TagDefaultCoverService tagDefaultCoverService;
/** /**
* 获取标签列表 * 获取标签列表
*/ */
@@ -138,4 +143,62 @@ public class TagController {
public ResultDomain<String> getResourcesByTag(@PathVariable("tagID") String tagID) { public ResultDomain<String> getResourcesByTag(@PathVariable("tagID") String tagID) {
return tagService.getResourcesByTag(tagID); return tagService.getResourcesByTag(tagID);
} }
// ----------------标签默认封面相关--------------------------------
/**
* 获取标签的启用默认封面列表
*/
@GetMapping("/tag/{tagID}/default-covers")
public ResultDomain<TbTagDefaultCover> getDefaultCovers(@PathVariable("tagID") String tagID) {
return tagDefaultCoverService.getDefaultCovers(tagID);
}
/**
* 获取标签的所有默认封面列表(管理端使用)
*/
@GetMapping("/tag/{tagID}/all-default-covers")
public ResultDomain<TbTagDefaultCover> getAllDefaultCovers(@PathVariable("tagID") String tagID) {
return tagDefaultCoverService.getAllDefaultCovers(tagID);
}
/**
* 添加默认封面
*/
@PostMapping("/default-cover")
public ResultDomain<String> addDefaultCover(@RequestBody TbTagDefaultCover cover) {
return tagDefaultCoverService.addDefaultCover(cover);
}
/**
* 更新默认封面
*/
@PutMapping("/default-cover")
public ResultDomain<String> updateDefaultCover(@RequestBody TbTagDefaultCover cover) {
return tagDefaultCoverService.updateDefaultCover(cover);
}
/**
* 删除默认封面
*/
@DeleteMapping("/default-cover/{id}")
public ResultDomain<String> deleteDefaultCover(@PathVariable("id") String id) {
return tagDefaultCoverService.deleteDefaultCover(id);
}
/**
* 批量添加默认封面
*/
@PostMapping("/default-covers/batch")
public ResultDomain<String> batchAddDefaultCovers(@RequestBody List<TbTagDefaultCover> covers) {
return tagDefaultCoverService.batchAddDefaultCovers(covers);
}
/**
* 根据标签ID删除所有默认封面
*/
@DeleteMapping("/tag/{tagID}/default-covers")
public ResultDomain<String> deleteDefaultCoversByTagId(@PathVariable("tagID") String tagID) {
return tagDefaultCoverService.deleteDefaultCoversByTagId(tagID);
}
} }

View File

@@ -0,0 +1,79 @@
package org.xyzh.news.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.common.dto.resource.TbTagDefaultCover;
import java.util.List;
/**
* @description 标签默认封面Mapper接口
* @author system
* @since 2025-12-24
*/
@Mapper
public interface TagDefaultCoverMapper {
/**
* @description 根据标签ID查询默认封面列表
* @param tagId 标签ID
* @return List<TbTagDefaultCover> 默认封面列表
* @author system
* @since 2025-12-24
*/
List<TbTagDefaultCover> selectDefaultCoversByTagId(@Param("tagId") String tagId);
/**
* @description 根据标签ID查询启用的默认封面列表
* @param tagId 标签ID
* @return List<TbTagDefaultCover> 启用的默认封面列表
* @author system
* @since 2025-12-24
*/
List<TbTagDefaultCover> selectActiveDefaultCoversByTagId(@Param("tagId") String tagId);
/**
* @description 插入默认封面
* @param cover 默认封面对象
* @return int 影响行数
* @author system
* @since 2025-12-24
*/
int insertDefaultCover(TbTagDefaultCover cover);
/**
* @description 更新默认封面
* @param cover 默认封面对象
* @return int 影响行数
* @author system
* @since 2025-12-24
*/
int updateDefaultCover(TbTagDefaultCover cover);
/**
* @description 删除默认封面
* @param id 主键ID
* @return int 影响行数
* @author system
* @since 2025-12-24
*/
int deleteDefaultCover(@Param("id") String id);
/**
* @description 根据标签ID删除所有默认封面
* @param tagId 标签ID
* @return int 影响行数
* @author system
* @since 2025-12-24
*/
int deleteDefaultCoversByTagId(@Param("tagId") String tagId);
/**
* @description 根据ID查询默认封面
* @param id 主键ID
* @return TbTagDefaultCover 默认封面对象
* @author system
* @since 2025-12-24
*/
TbTagDefaultCover selectDefaultCoverById(@Param("id") String id);
}

View File

@@ -0,0 +1,217 @@
package org.xyzh.news.service.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.xyzh.api.news.tag.TagDefaultCoverService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.resource.TbTagDefaultCover;
import org.xyzh.news.mapper.TagDefaultCoverMapper;
import java.util.List;
import java.util.UUID;
/**
* @description 标签默认封面服务实现
* @author system
* @since 2025-12-24
*/
@Service
public class NCTagDefaultCoverServiceImpl implements TagDefaultCoverService {
private static final Logger logger = LoggerFactory.getLogger(NCTagDefaultCoverServiceImpl.class);
@Autowired
private TagDefaultCoverMapper tagDefaultCoverMapper;
@Override
public ResultDomain<TbTagDefaultCover> getDefaultCovers(String tagID) {
ResultDomain<TbTagDefaultCover> result = new ResultDomain<>();
try {
if (tagID == null || tagID.trim().isEmpty()) {
result.fail("标签ID不能为空");
return result;
}
List<TbTagDefaultCover> covers = tagDefaultCoverMapper.selectActiveDefaultCoversByTagId(tagID);
logger.info("获取标签[{}]的默认封面列表,数量:{}", tagID, covers.size());
result.success("获取成功", covers);
return result;
} catch (Exception e) {
logger.error("获取标签默认封面列表失败", e);
result.fail("获取默认封面列表失败:" + e.getMessage());
return result;
}
}
@Override
public ResultDomain<TbTagDefaultCover> getAllDefaultCovers(String tagID) {
ResultDomain<TbTagDefaultCover> result = new ResultDomain<>();
try {
if (tagID == null || tagID.trim().isEmpty()) {
result.fail("标签ID不能为空");
return result;
}
List<TbTagDefaultCover> covers = tagDefaultCoverMapper.selectDefaultCoversByTagId(tagID);
logger.info("获取标签[{}]的所有默认封面列表,数量:{}", tagID, covers.size());
result.success("获取成功", covers);
return result;
} catch (Exception e) {
logger.error("获取标签所有默认封面列表失败", e);
result.fail("获取默认封面列表失败:" + e.getMessage());
return result;
}
}
@Override
@Transactional
public ResultDomain<String> addDefaultCover(TbTagDefaultCover cover) {
ResultDomain<String> result = new ResultDomain<>();
try {
if (cover.getTagID() == null || cover.getTagID().trim().isEmpty()) {
result.fail("标签ID不能为空");
return result;
}
if (cover.getCoverImage() == null || cover.getCoverImage().trim().isEmpty()) {
result.fail("封面图片不能为空");
return result;
}
cover.setId(UUID.randomUUID().toString().replace("-", ""));
if (cover.getIsActive() == null) {
cover.setIsActive(1);
}
if (cover.getOrderNum() == null) {
cover.setOrderNum(0);
}
int rows = tagDefaultCoverMapper.insertDefaultCover(cover);
if (rows > 0) {
logger.info("添加标签默认封面成功ID{}", cover.getId());
result.success("添加成功", cover.getId());
return result;
} else {
result.fail("添加失败");
return result;
}
} catch (Exception e) {
logger.error("添加标签默认封面失败", e);
result.fail("添加失败:" + e.getMessage());
return result;
}
}
@Override
@Transactional
public ResultDomain<String> updateDefaultCover(TbTagDefaultCover cover) {
ResultDomain<String> result = new ResultDomain<>();
try {
if (cover.getId() == null || cover.getId().trim().isEmpty()) {
result.fail("ID不能为空");
return result;
}
int rows = tagDefaultCoverMapper.updateDefaultCover(cover);
if (rows > 0) {
logger.info("更新标签默认封面成功ID{}", cover.getId());
result.success("更新成功", cover.getId());
return result;
} else {
result.fail("更新失败,记录不存在");
return result;
}
} catch (Exception e) {
logger.error("更新标签默认封面失败", e);
result.fail("更新失败:" + e.getMessage());
return result;
}
}
@Override
@Transactional
public ResultDomain<String> deleteDefaultCover(String id) {
ResultDomain<String> result = new ResultDomain<>();
try {
if (id == null || id.trim().isEmpty()) {
result.fail("ID不能为空");
return result;
}
int rows = tagDefaultCoverMapper.deleteDefaultCover(id);
if (rows > 0) {
logger.info("删除标签默认封面成功ID{}", id);
result.success("删除成功", id);
return result;
} else {
result.fail("删除失败,记录不存在");
return result;
}
} catch (Exception e) {
logger.error("删除标签默认封面失败", e);
result.fail("删除失败:" + e.getMessage());
return result;
}
}
@Override
@Transactional
public ResultDomain<String> batchAddDefaultCovers(List<TbTagDefaultCover> covers) {
ResultDomain<String> result = new ResultDomain<>();
try {
if (covers == null || covers.isEmpty()) {
result.fail("封面列表不能为空");
return result;
}
int successCount = 0;
for (TbTagDefaultCover cover : covers) {
cover.setId(UUID.randomUUID().toString().replace("-", ""));
if (cover.getIsActive() == null) {
cover.setIsActive(1);
}
if (cover.getOrderNum() == null) {
cover.setOrderNum(0);
}
int rows = tagDefaultCoverMapper.insertDefaultCover(cover);
if (rows > 0) {
successCount++;
}
}
logger.info("批量添加标签默认封面完成,成功:{}/总数:{}", successCount, covers.size());
result.success("批量添加成功,成功数量:" + successCount, String.valueOf(successCount));
return result;
} catch (Exception e) {
logger.error("批量添加标签默认封面失败", e);
result.fail("批量添加失败:" + e.getMessage());
return result;
}
}
@Override
@Transactional
public ResultDomain<String> deleteDefaultCoversByTagId(String tagID) {
ResultDomain<String> result = new ResultDomain<>();
try {
if (tagID == null || tagID.trim().isEmpty()) {
result.fail("标签ID不能为空");
return result;
}
int rows = tagDefaultCoverMapper.deleteDefaultCoversByTagId(tagID);
logger.info("删除标签[{}]的所有默认封面成功,删除数量:{}", tagID, rows);
result.success("删除成功", String.valueOf(rows));
return result;
} catch (Exception e) {
logger.error("删除标签默认封面失败", e);
result.fail("删除失败:" + e.getMessage());
return result;
}
}
}

View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.xyzh.news.mapper.TagDefaultCoverMapper">
<!-- 基础结果映射 -->
<resultMap id="BaseResultMap" type="org.xyzh.common.dto.resource.TbTagDefaultCover">
<id column="id" property="id" jdbcType="VARCHAR"/>
<result column="tag_id" property="tagID" jdbcType="VARCHAR"/>
<result column="cover_image" property="coverImage" jdbcType="VARCHAR"/>
<result column="order_num" property="orderNum" jdbcType="INTEGER"/>
<result column="is_active" property="isActive" jdbcType="TINYINT"/>
<result column="creator" property="creator" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="updater" property="updater" jdbcType="VARCHAR"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- 基础字段 -->
<sql id="Base_Column_List">
id, tag_id, cover_image, order_num, is_active, creator, create_time, updater, update_time
</sql>
<!-- 根据标签ID查询默认封面列表 -->
<select id="selectDefaultCoversByTagId" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM tb_tag_default_cover
WHERE tag_id = #{tagId}
ORDER BY order_num ASC, create_time DESC
</select>
<!-- 根据标签ID查询启用的默认封面列表 -->
<select id="selectActiveDefaultCoversByTagId" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM tb_tag_default_cover
WHERE tag_id = #{tagId} AND is_active = 1
ORDER BY order_num ASC, create_time DESC
</select>
<!-- 根据ID查询默认封面 -->
<select id="selectDefaultCoverById" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM tb_tag_default_cover
WHERE id = #{id}
</select>
<!-- 插入默认封面 -->
<insert id="insertDefaultCover">
INSERT INTO tb_tag_default_cover
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if>
<if test="tagID != null">tag_id,</if>
<if test="coverImage != null">cover_image,</if>
<if test="orderNum != null">order_num,</if>
<if test="isActive != null">is_active,</if>
<if test="creator != null">creator,</if>
create_time,
</trim>
<trim prefix="VALUES (" suffix=")" suffixOverrides=",">
<if test="id != null">#{id},</if>
<if test="tagID != null">#{tagID},</if>
<if test="coverImage != null">#{coverImage},</if>
<if test="orderNum != null">#{orderNum},</if>
<if test="isActive != null">#{isActive},</if>
<if test="creator != null">#{creator},</if>
NOW(),
</trim>
</insert>
<!-- 更新默认封面 -->
<update id="updateDefaultCover">
UPDATE tb_tag_default_cover
<set>
<if test="coverImage != null">cover_image = #{coverImage},</if>
<if test="orderNum != null">order_num = #{orderNum},</if>
<if test="isActive != null">is_active = #{isActive},</if>
<if test="updater != null">updater = #{updater},</if>
update_time = NOW()
</set>
WHERE id = #{id}
</update>
<!-- 删除默认封面 -->
<delete id="deleteDefaultCover">
DELETE FROM tb_tag_default_cover WHERE id = #{id}
</delete>
<!-- 根据标签ID删除所有默认封面 -->
<delete id="deleteDefaultCoversByTagId">
DELETE FROM tb_tag_default_cover WHERE tag_id = #{tagId}
</delete>
</mapper>

View File

@@ -22,13 +22,15 @@ import org.xyzh.common.dto.resource.TbResource;
import org.xyzh.common.dto.study.TbCourse; import org.xyzh.common.dto.study.TbCourse;
import org.xyzh.common.dto.study.TbCourseChapter; import org.xyzh.common.dto.study.TbCourseChapter;
import org.xyzh.common.dto.study.TbCourseNode; import org.xyzh.common.dto.study.TbCourseNode;
import org.xyzh.common.dto.study.TbCourseTag;
import org.xyzh.common.dto.user.TbSysUser; import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.common.utils.IDUtils; import org.xyzh.common.utils.IDUtils;
import org.xyzh.common.vo.CourseItemVO; import org.xyzh.common.vo.CourseItemVO;
import org.xyzh.common.vo.ResourceVO; import org.xyzh.common.vo.ResourceVO;
import org.xyzh.study.mapper.CourseMapper;
import org.xyzh.study.mapper.CourseChapterMapper; import org.xyzh.study.mapper.CourseChapterMapper;
import org.xyzh.study.mapper.CourseMapper;
import org.xyzh.study.mapper.CourseNodeMapper; import org.xyzh.study.mapper.CourseNodeMapper;
import org.xyzh.study.mapper.CourseTagMapper;
import org.xyzh.study.service.SCCourseService; import org.xyzh.study.service.SCCourseService;
import org.xyzh.system.utils.LoginUtil; import org.xyzh.system.utils.LoginUtil;
import org.xyzh.api.news.resource.ResourceAuditService; import org.xyzh.api.news.resource.ResourceAuditService;
@@ -58,6 +60,9 @@ public class SCCourseServiceImpl implements SCCourseService {
@Autowired @Autowired
private CourseNodeMapper courseNodeMapper; private CourseNodeMapper courseNodeMapper;
@Autowired
private CourseTagMapper courseTagMapper;
@Autowired @Autowired
private ResourceAuditService auditService; private ResourceAuditService auditService;
@@ -231,6 +236,22 @@ public class SCCourseServiceImpl implements SCCourseService {
logger.error("创建课程权限异常,但不影响课程创建: {}", e.getMessage(), e); logger.error("创建课程权限异常,但不影响课程创建: {}", e.getMessage(), e);
} }
// 处理课程标签关联
if (courseItemVO.getTagID() != null && !courseItemVO.getTagID().isEmpty()) {
try {
TbCourseTag courseTag = new TbCourseTag();
courseTag.setId(IDUtils.generateID());
courseTag.setCourseID(courseID);
courseTag.setTagID(courseItemVO.getTagID());
courseTag.setCreator(user.getId());
courseTag.setCreateTime(now);
courseTagMapper.insertCourseTag(courseTag);
logger.info("创建课程标签关联成功: courseID={}, tagID={}", courseID, courseItemVO.getTagID());
} catch (Exception e) {
logger.error("创建课程标签关联失败: {}", e.getMessage(), e);
}
}
resultDomain.success("创建课程成功", courseItemVO); resultDomain.success("创建课程成功", courseItemVO);
return resultDomain; return resultDomain;
} }
@@ -259,6 +280,34 @@ public class SCCourseServiceImpl implements SCCourseService {
course.setUpdateTime(now); course.setUpdateTime(now);
courseMapper.updateCourse(course); courseMapper.updateCourse(course);
// 1.5. 处理课程标签关联
if (courseItemVO.getTagID() != null && !courseItemVO.getTagID().isEmpty()) {
try {
// 先删除旧的标签关联
courseTagMapper.deleteByCourseId(courseID);
// 创建新的标签关联
TbCourseTag courseTag = new TbCourseTag();
courseTag.setId(IDUtils.generateID());
courseTag.setCourseID(courseID);
courseTag.setTagID(courseItemVO.getTagID());
courseTag.setCreator(user.getId());
courseTag.setCreateTime(now);
courseTagMapper.insertCourseTag(courseTag);
logger.info("更新课程标签关联成功: courseID={}, tagID={}", courseID, courseItemVO.getTagID());
} catch (Exception e) {
logger.error("更新课程标签关联失败: {}", e.getMessage(), e);
}
} else {
// 如果没有tagID删除所有标签关联
try {
courseTagMapper.deleteByCourseId(courseID);
logger.info("清空课程标签关联: courseID={}", courseID);
} catch (Exception e) {
logger.error("清空课程标签关联失败: {}", e.getMessage(), e);
}
}
// 2. 处理章节和节点 // 2. 处理章节和节点
List<CourseItemVO> newChapterVOs = courseItemVO.getChapters(); List<CourseItemVO> newChapterVOs = courseItemVO.getChapters();
if (newChapterVOs == null) { if (newChapterVOs == null) {

View File

@@ -16,6 +16,7 @@
<result column="view_count" property="viewCount" jdbcType="INTEGER"/> <result column="view_count" property="viewCount" jdbcType="INTEGER"/>
<result column="learn_count" property="learnCount" jdbcType="INTEGER"/> <result column="learn_count" property="learnCount" jdbcType="INTEGER"/>
<result column="order_num" property="orderNum" jdbcType="INTEGER"/> <result column="order_num" property="orderNum" jdbcType="INTEGER"/>
<result column="tag_id" property="tagID" jdbcType="VARCHAR"/>
<result column="creator" property="creator" jdbcType="VARCHAR"/> <result column="creator" property="creator" jdbcType="VARCHAR"/>
<result column="updater" property="updater" jdbcType="VARCHAR"/> <result column="updater" property="updater" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/> <result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
@@ -26,9 +27,9 @@
<!-- 基础字段 --> <!-- 基础字段 -->
<sql id="Base_Column_List"> <sql id="Base_Column_List">
id, course_id, name, cover_image, description, content, duration, c.id, c.course_id, c.name, c.cover_image, c.description, c.content, c.duration,
teacher, status, view_count, learn_count, order_num, creator, updater, c.teacher, c.status, c.view_count, c.learn_count, c.order_num, ct.tag_id, c.creator, c.updater,
create_time, update_time, delete_time, deleted c.create_time, c.update_time, c.delete_time, c.deleted
</sql> </sql>
<!-- 通用条件 --> <!-- 通用条件 -->
@@ -111,8 +112,9 @@
<!-- selectCourses - 添加权限过滤 --> <!-- selectCourses - 添加权限过滤 -->
<select id="selectCourses" resultMap="BaseResultMap"> <select id="selectCourses" resultMap="BaseResultMap">
SELECT DISTINCT c.* SELECT DISTINCT <include refid="Base_Column_List" />
FROM tb_course c FROM tb_course c
LEFT JOIN tb_course_tag ct ON c.course_id = ct.course_id
<include refid="Permission_Filter"/> <include refid="Permission_Filter"/>
WHERE c.deleted = 0 WHERE c.deleted = 0
<if test="filter.courseID != null and filter.courseID != ''"> <if test="filter.courseID != null and filter.courseID != ''">
@@ -134,8 +136,9 @@
<select id="selectByCourseId" resultMap="BaseResultMap"> <select id="selectByCourseId" resultMap="BaseResultMap">
SELECT SELECT
<include refid="Base_Column_List" /> <include refid="Base_Column_List" />
FROM tb_course FROM tb_course c
WHERE course_id = #{courseId} AND deleted = 0 LEFT JOIN tb_course_tag ct ON c.course_id = ct.course_id
WHERE c.course_id = #{courseId} AND c.deleted = 0
</select> </select>
<!-- 根据课程名称查询课程 --> <!-- 根据课程名称查询课程 -->
@@ -316,8 +319,9 @@
<!-- 分页查询课程 --> <!-- 分页查询课程 -->
<!-- selectCoursesPage - 添加权限过滤 --> <!-- selectCoursesPage - 添加权限过滤 -->
<select id="selectCoursesPage" resultMap="BaseResultMap"> <select id="selectCoursesPage" resultMap="BaseResultMap">
SELECT DISTINCT c.* SELECT DISTINCT <include refid="Base_Column_List" />
FROM tb_course c FROM tb_course c
LEFT JOIN tb_course_tag ct ON c.course_id = ct.course_id
<include refid="Permission_Filter"/> <include refid="Permission_Filter"/>
WHERE c.deleted = 0 WHERE c.deleted = 0
<if test="filter != null"> <if test="filter != null">

View File

@@ -147,6 +147,78 @@ export const resourceTagApi = {
async getResourcesByTag(tagID: string): Promise<ResultDomain<string>> { async getResourcesByTag(tagID: string): Promise<ResultDomain<string>> {
const response = await api.get<string>(`/news/tags/tag/${tagID}/resources`); const response = await api.get<string>(`/news/tags/tag/${tagID}/resources`);
return response.data; return response.data;
},
// ==================== 标签默认封面操作 ====================
/**
* 获取标签的启用默认封面列表
* @param tagID 标签ID
* @returns Promise<ResultDomain<any>>
*/
async getDefaultCovers(tagID: string): Promise<ResultDomain<any>> {
const response = await api.get<any>(`/news/tags/tag/${tagID}/default-covers`);
return response.data;
},
/**
* 获取标签的所有默认封面列表(管理端使用)
* @param tagID 标签ID
* @returns Promise<ResultDomain<any>>
*/
async getAllDefaultCovers(tagID: string): Promise<ResultDomain<any>> {
const response = await api.get<any>(`/news/tags/tag/${tagID}/all-default-covers`);
return response.data;
},
/**
* 添加默认封面
* @param cover 默认封面对象
* @returns Promise<ResultDomain<string>>
*/
async addDefaultCover(cover: any): Promise<ResultDomain<string>> {
const response = await api.post<string>('/news/tags/default-cover', cover);
return response.data;
},
/**
* 更新默认封面
* @param cover 默认封面对象
* @returns Promise<ResultDomain<string>>
*/
async updateDefaultCover(cover: any): Promise<ResultDomain<string>> {
const response = await api.put<string>('/news/tags/default-cover', cover);
return response.data;
},
/**
* 删除默认封面
* @param id 主键ID
* @returns Promise<ResultDomain<string>>
*/
async deleteDefaultCover(id: string): Promise<ResultDomain<string>> {
const response = await api.delete<string>(`/news/tags/default-cover/${id}`);
return response.data;
},
/**
* 批量添加默认封面
* @param covers 默认封面列表
* @returns Promise<ResultDomain<string>>
*/
async batchAddDefaultCovers(covers: any[]): Promise<ResultDomain<string>> {
const response = await api.post<string>('/news/tags/default-covers/batch', covers);
return response.data;
},
/**
* 根据标签ID删除所有默认封面
* @param tagID 标签ID
* @returns Promise<ResultDomain<string>>
*/
async deleteDefaultCoversByTagId(tagID: string): Promise<ResultDomain<string>> {
const response = await api.delete<string>(`/news/tags/tag/${tagID}/default-covers`);
return response.data;
} }
}; };

View File

@@ -34,6 +34,8 @@ export interface Course extends BaseDTO {
learnCount?: number; learnCount?: number;
/** 排序号 */ /** 排序号 */
orderNum?: number; orderNum?: number;
/** 标签ID用于获取默认封面 */
tagID?: string;
/** 创建者 */ /** 创建者 */
creator?: string; creator?: string;
/** 更新者 */ /** 更新者 */
@@ -144,6 +146,8 @@ export interface CourseItemVO extends BaseDTO {
viewCount?: number; viewCount?: number;
/** 学习人数 */ /** 学习人数 */
learnCount?: number; learnCount?: number;
/** 标签ID用于获取默认封面和分类 */
tagID?: string;
/** 课程创建时间 */ /** 课程创建时间 */
createTime?: string; createTime?: string;
@@ -342,6 +346,8 @@ export interface TaskItemVO extends LearningTask {
viewCount?: number; viewCount?: number;
/** 发布时间(用于搜索结果展示) */ /** 发布时间(用于搜索结果展示) */
publishTime?: Date | string; publishTime?: Date | string;
/** 标签ID用于获取默认封面 */
tagID?: string;
} }
/** /**

View File

@@ -0,0 +1,82 @@
/**
* @description 默认封面工具函数
* @filename defaultCover.ts
* @author system
* @since 2025-12-24
*/
import { resourceTagApi } from '@/apis/resource/resourceTag';
import { FILE_DOWNLOAD_URL } from '@/config';
import defaultArticleImg from '@/assets/imgs/article-default.png';
// 缓存标签默认封面,避免重复请求
const tagCoverCache: Map<string, string[]> = new Map();
/**
* 根据标签ID获取随机默认封面
* @param tagID 标签ID
* @returns Promise<string> 封面URL
*/
export async function getRandomDefaultCover(tagID: string | undefined): Promise<string> {
// 如果没有tagID返回默认封面
if (!tagID) {
return defaultArticleImg;
}
try {
// 检查缓存
if (tagCoverCache.has(tagID)) {
const covers = tagCoverCache.get(tagID)!;
if (covers.length > 0) {
// 随机选择一个
const randomIndex = Math.floor(Math.random() * covers.length);
return covers[randomIndex];
}
return defaultArticleImg;
}
// 从后端获取
const result = await resourceTagApi.getDefaultCovers(tagID);
if (result.success && result.dataList && result.dataList.length > 0) {
// 构建完整的URL列表
const coverUrls = result.dataList.map((cover: any) =>
`${FILE_DOWNLOAD_URL}${cover.coverImage}`
);
// 存入缓存
tagCoverCache.set(tagID, coverUrls);
// 随机选择一个
const randomIndex = Math.floor(Math.random() * coverUrls.length);
return coverUrls[randomIndex];
}
// 如果没有配置默认封面,返回通用默认封面
return defaultArticleImg;
} catch (error) {
console.error('获取标签默认封面失败:', error);
return defaultArticleImg;
}
}
/**
* 清除标签封面缓存
* @param tagID 可选,指定清除某个标签的缓存,不传则清除所有
*/
export function clearTagCoverCache(tagID?: string) {
if (tagID) {
tagCoverCache.delete(tagID);
} else {
tagCoverCache.clear();
}
}
/**
* 预加载标签默认封面到缓存
* @param tagIDs 标签ID列表
*/
export async function preloadTagCovers(tagIDs: string[]) {
const promises = tagIDs.map(tagID => getRandomDefaultCover(tagID));
await Promise.all(promises);
}

View File

@@ -161,6 +161,48 @@
placeholder="请输入标签描述" placeholder="请输入标签描述"
/> />
</el-form-item> </el-form-item>
<el-form-item label="默认封面">
<div class="default-covers-section">
<div class="covers-tip">为该标签配置多张默认封面资源没有封面时会随机显示其中一张</div>
<!-- 已上传的封面列表 -->
<div class="covers-list" v-if="defaultCovers.length > 0">
<div v-for="(cover, index) in defaultCovers" :key="cover.id || index" class="cover-item">
<img :src="cover.coverImage" alt="默认封面" class="cover-preview" />
<div class="cover-actions">
<el-switch
v-model="cover.isActive"
:active-value="1"
:inactive-value="0"
active-text="启用"
inactive-text="禁用"
/>
<el-button
type="danger"
size="small"
@click="removeCover(index)"
:icon="Delete"
>
删除
</el-button>
</div>
</div>
</div>
<!-- 添加封面 -->
<FileUpload
list-type="cover"
:cover-url="''"
@update:cover-url="handleAddCover"
accept="image/*"
:max-size="5"
module="tag-cover"
:as-dialog="false"
tip="点击上传默认封面图片"
/>
</div>
</el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
@@ -175,10 +217,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, computed } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'; import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
import { Delete } from '@element-plus/icons-vue';
import { resourceTagApi } from '@/apis/resource'; import { resourceTagApi } from '@/apis/resource';
import type { Tag } from '@/types/resource'; import type { Tag } from '@/types/resource';
import { TagType } from '@/types/resource'; import { TagType } from '@/types/resource';
import {AdminLayout} from '@/views/admin'; import { AdminLayout } from '@/views/admin';
import {FileUpload} from '@/components';
import { FILE_DOWNLOAD_URL } from '@/config';
defineOptions({ defineOptions({
name: 'TagManagementView' name: 'TagManagementView'
@@ -198,6 +243,9 @@ const currentTag = ref<Partial<Tag>>({
description: '' description: ''
}); });
// 默认封面列表
const defaultCovers = ref<any[]>([]);
// 按类型分类的标签 // 按类型分类的标签
const articleTags = computed(() => { const articleTags = computed(() => {
return tags.value.filter(tag => tag.tagType === TagType.ARTICLE_CATEGORY); return tags.value.filter(tag => tag.tagType === TagType.ARTICLE_CATEGORY);
@@ -256,16 +304,65 @@ function showCreateDialog() {
color: '#409EFF', color: '#409EFF',
description: '' description: ''
}; };
defaultCovers.value = [];
dialogVisible.value = true; dialogVisible.value = true;
} }
// 编辑标签 // 编辑标签
function editTag(row: Tag) { async function editTag(row: Tag) {
isEdit.value = true; isEdit.value = true;
currentTag.value = { ...row }; currentTag.value = { ...row };
// 加载默认封面
await loadDefaultCovers(row.tagID!);
dialogVisible.value = true; dialogVisible.value = true;
} }
// 加载默认封面
async function loadDefaultCovers(tagID: string) {
try {
const result = await resourceTagApi.getAllDefaultCovers(tagID);
console.log('加载默认封面结果:', result);
if (result.success && result.dataList) {
defaultCovers.value = result.dataList.map((cover: any) => ({
...cover,
coverImage: FILE_DOWNLOAD_URL + cover.coverImage
}));
console.log('处理后的封面列表:', defaultCovers.value);
} else {
defaultCovers.value = [];
}
} catch (error) {
console.error('加载默认封面失败:', error);
defaultCovers.value = [];
}
}
// 添加封面
function handleAddCover(fileIdOrUrl: string) {
if (fileIdOrUrl) {
// 如果是完整URL提取fileId否则直接使用
let fileId = fileIdOrUrl;
// 如果包含FILE_DOWNLOAD_URL说明是完整URL需要提取fileId
if (fileIdOrUrl.includes(FILE_DOWNLOAD_URL)) {
fileId = fileIdOrUrl.replace(FILE_DOWNLOAD_URL, '');
}
defaultCovers.value.push({
coverImage: FILE_DOWNLOAD_URL + fileId,
isActive: 1,
orderNum: defaultCovers.value.length
});
}
}
// 删除封面
function removeCover(index: number) {
defaultCovers.value.splice(index, 1);
}
// 删除标签 // 删除标签
async function deleteTag(row: Tag) { async function deleteTag(row: Tag) {
try { try {
@@ -310,6 +407,17 @@ async function handleSubmit() {
} }
if (result.success) { if (result.success) {
// 保存默认封面
let tagID = currentTag.value.tagID;
if (!tagID && result.data) {
// 新建标签从返回数据中获取tagID
tagID = typeof result.data === 'string' ? result.data : result.data.tagID;
}
if (tagID) {
await saveDefaultCovers(tagID);
}
ElMessage.success(isEdit.value ? '更新成功' : '创建成功'); ElMessage.success(isEdit.value ? '更新成功' : '创建成功');
dialogVisible.value = false; dialogVisible.value = false;
loadTags(); loadTags();
@@ -323,9 +431,36 @@ async function handleSubmit() {
} }
} }
// 保存默认封面
async function saveDefaultCovers(tagID: string) {
try {
// 编辑模式下,先删除旧的默认封面
if (isEdit.value) {
await resourceTagApi.deleteDefaultCoversByTagId(tagID);
}
// 准备要保存的封面数据
const coversToSave = defaultCovers.value.map((cover, index) => ({
tagID: tagID,
coverImage: cover.coverImage.replace(FILE_DOWNLOAD_URL, ''),
isActive: cover.isActive,
orderNum: index
}));
// 批量添加新的默认封面
if (coversToSave.length > 0) {
await resourceTagApi.batchAddDefaultCovers(coversToSave);
}
} catch (error) {
console.error('保存默认封面失败:', error);
ElMessage.warning('默认封面保存失败');
}
}
// 对话框关闭处理 // 对话框关闭处理
function handleDialogClose() { function handleDialogClose() {
formRef.value?.resetFields(); formRef.value?.resetFields();
defaultCovers.value = [];
} }
</script> </script>
@@ -577,4 +712,48 @@ function handleDialogClose() {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
.default-covers-section {
display: flex;
flex-direction: column;
gap: 16px;
width: 100%;
}
.covers-tip {
font-size: 12px;
color: #909399;
line-height: 1.5;
}
.covers-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-bottom: 12px;
}
.cover-item {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background: #f9f9f9;
}
.cover-preview {
width: 100%;
height: 120px;
object-fit: cover;
border-radius: 4px;
}
.cover-actions {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
</style> </style>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="article-card" @click="handleClick"> <div class="article-card" @click="handleClick">
<div class="article-image"> <div class="article-image">
<img :src="resource?.coverImage ? (FILE_DOWNLOAD_URL + resource.coverImage) : defaultArticleImg" :alt="resource.title" /> <img :src="coverUrl" :alt="resource?.title" />
<div class="article-tag">精选文章</div> <div class="article-tag">精选文章</div>
</div> </div>
<div class="article-content"> <div class="article-content">
@@ -21,12 +21,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed, ref, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { Document } from '@element-plus/icons-vue'; import { Document } from '@element-plus/icons-vue';
import type { ResourceRecommendVO } from '@/types'; import type { ResourceRecommendVO } from '@/types';
import { FILE_DOWNLOAD_URL } from '@/config'; import { FILE_DOWNLOAD_URL } from '@/config';
import defaultArticleImg from '@/assets/imgs/article-default.png'; import defaultArticleImg from '@/assets/imgs/article-default.png';
import { getRandomDefaultCover } from '@/utils/defaultCover';
const props = defineProps<{ const props = defineProps<{
resource?: ResourceRecommendVO; resource?: ResourceRecommendVO;
@@ -34,6 +35,28 @@ const props = defineProps<{
const router = useRouter(); const router = useRouter();
// 封面URL响应式
const coverUrl = ref<string>(defaultArticleImg);
// 加载封面
async function loadCover() {
if (props.resource?.coverImage) {
// 有封面直接使用
coverUrl.value = FILE_DOWNLOAD_URL + props.resource.coverImage;
} else if (props.resource?.tagID) {
// 没有封面根据tagID获取默认封面
coverUrl.value = await getRandomDefaultCover(props.resource.tagID);
} else {
// 既没有封面也没有tagID使用全局默认
coverUrl.value = defaultArticleImg;
}
}
// 监听resource变化
watch(() => props.resource, () => {
loadCover();
}, { immediate: true });
// 格式化浏览量 // 格式化浏览量
function formatViewCount(count: number): string { function formatViewCount(count: number): string {
if (count < 1000) { if (count < 1000) {

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="ideological-card" @click="handleClick"> <div class="ideological-card" @click="handleClick">
<div class="card-image"> <div class="card-image">
<img :src="resource?.coverImage ? (FILE_DOWNLOAD_URL + resource.coverImage) : defaultArticleImg" :alt="resource.title" /> <img :src="coverUrl" :alt="resource?.title" />
</div> </div>
<div class="date-box" v-if="publishDate"> <div class="date-box" v-if="publishDate">
<div class="day">{{ publishDate.day }}</div> <div class="day">{{ publishDate.day }}</div>
@@ -17,11 +17,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed, ref, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import type { ResourceRecommendVO } from '@/types'; import type { ResourceRecommendVO } from '@/types';
import { FILE_DOWNLOAD_URL } from '@/config'; import { FILE_DOWNLOAD_URL } from '@/config';
import defaultArticleImg from '@/assets/imgs/article-default.png'; import defaultArticleImg from '@/assets/imgs/article-default.png';
import { getRandomDefaultCover } from '@/utils/defaultCover';
const props = defineProps<{ const props = defineProps<{
resource?: ResourceRecommendVO; resource?: ResourceRecommendVO;
@@ -29,6 +30,25 @@ const props = defineProps<{
const router = useRouter(); const router = useRouter();
// 封面URL响应式
const coverUrl = ref<string>(defaultArticleImg);
// 加载封面
async function loadCover() {
if (props.resource?.coverImage) {
coverUrl.value = FILE_DOWNLOAD_URL + props.resource.coverImage;
} else if (props.resource?.tagID) {
coverUrl.value = await getRandomDefaultCover(props.resource.tagID);
} else {
coverUrl.value = defaultArticleImg;
}
}
// 监听resource变化
watch(() => props.resource, () => {
loadCover();
}, { immediate: true });
// 格式化发布日期 // 格式化发布日期
const publishDate = computed(() => { const publishDate = computed(() => {
if (!props.resource?.publishTime) return null; if (!props.resource?.publishTime) return null;

View File

@@ -50,6 +50,24 @@
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="课程分类" prop="tagID">
<el-select
v-model="currentCourseItemVO.tagID"
placeholder="请选择课程分类"
:loading="tagsLoading"
:disabled="!editMode"
clearable
style="width: 100%"
>
<el-option
v-for="tag in tagList"
:key="tag.tagID || tag.id"
:label="tag.name"
:value="tag.tagID || ''"
/>
</el-select>
</el-form-item>
<el-form-item label="授课老师" prop="teacher"> <el-form-item label="授课老师" prop="teacher">
<div style="display: flex; gap: 8px; align-items: flex-start;"> <div style="display: flex; gap: 8px; align-items: flex-start;">
<el-select <el-select
@@ -396,12 +414,13 @@ import { Plus, ArrowLeft, Refresh } from '@element-plus/icons-vue';
import { FileUpload } from '@/components/file'; import { FileUpload } from '@/components/file';
import { RichTextComponent } from '@/components/text'; import { RichTextComponent } from '@/components/text';
import { courseApi } from '@/apis/study'; import { courseApi } from '@/apis/study';
import { resourceApi } from '@/apis/resource'; import { resourceApi, resourceTagApi } from '@/apis/resource';
import { userApi } from '@/apis/system'; import { userApi } from '@/apis/system';
import type { CourseItemVO } from '@/types/study'; import type { CourseItemVO } from '@/types/study';
import type { Resource } from '@/types/resource'; import type { Resource, Tag, TagType } from '@/types/resource';
import type { SysFile, UserVO } from '@/types'; import type { SysFile, UserVO } from '@/types';
import { FILE_DOWNLOAD_URL } from '@/config'; import { FILE_DOWNLOAD_URL } from '@/config';
import { TagType as TagTypeEnum } from '@/types/resource';
defineOptions({ defineOptions({
name: 'CourseAdd' name: 'CourseAdd'
}); });
@@ -428,6 +447,10 @@ const editMode = ref(true);
const teacherList = ref<UserVO[]>([]); const teacherList = ref<UserVO[]>([]);
const teachersLoading = ref(false); const teachersLoading = ref(false);
// 标签列表
const tagList = ref<Tag[]>([]);
const tagsLoading = ref(false);
// 原始数据(用于比对) // 原始数据(用于比对)
const originalCourseItemVO = ref<CourseItemVO>(); const originalCourseItemVO = ref<CourseItemVO>();
// 当前编辑的数据 // 当前编辑的数据
@@ -445,12 +468,15 @@ const currentCourseItemVO = ref<CourseItemVO>({
// 表单验证规则 // 表单验证规则
const rules = { const rules = {
'name': [{ required: true, message: '请输入课程名称', trigger: 'blur' }], 'name': [{ required: true, message: '请输入课程名称', trigger: 'blur' }],
'tagID': [{ required: true, message: '请选择课程分类', trigger: 'change' }],
'teacher': [{ required: true, message: '请输入授课老师', trigger: 'blur' }] 'teacher': [{ required: true, message: '请输入授课老师', trigger: 'blur' }]
}; };
onMounted(() => { onMounted(() => {
// 加载教师列表 // 加载教师列表
loadTeacherList(); loadTeacherList();
// 加载标签列表
loadTagList();
if (props.courseID) { if (props.courseID) {
loadCourse(); loadCourse();
@@ -641,6 +667,25 @@ async function loadTeacherList() {
} }
} }
// 加载标签列表
async function loadTagList() {
try {
tagsLoading.value = true;
const result = await resourceTagApi.getTagsByType(TagTypeEnum.COURSE_CATEGORY);
if (result.success) {
tagList.value = result.dataList || [];
console.log(`✅ 已加载 ${tagList.value.length} 个课程分类标签`);
} else {
ElMessage.error(result.message || '加载课程分类失败');
}
} catch (error) {
console.error('加载课程分类失败:', error);
ElMessage.error('加载课程分类失败');
} finally {
tagsLoading.value = false;
}
}
// 处理教师选择框获得焦点 // 处理教师选择框获得焦点
function handleTeacherSelectFocus() { function handleTeacherSelectFocus() {
// 如果还没有加载教师列表,则加载 // 如果还没有加载教师列表,则加载

View File

@@ -92,7 +92,7 @@
<!-- 文章封面 --> <!-- 文章封面 -->
<div class="article-cover"> <div class="article-cover">
<img <img
:src="article.coverImage ? (FILE_DOWNLOAD_URL + article.coverImage) : defaultArticleImg" :src="getCoverUrl(article)"
:alt="article.resourceName" :alt="article.resourceName"
/> />
<div class="cover-overlay"> <div class="cover-overlay">
@@ -155,7 +155,7 @@
<!-- 课程封面 --> <!-- 课程封面 -->
<div class="course-cover"> <div class="course-cover">
<img <img
:src="course.coverImage ? (FILE_DOWNLOAD_URL + course.coverImage) : defaultCourseImg" :src="getCoverUrl(course)"
:alt="course.courseName" :alt="course.courseName"
/> />
<div class="cover-overlay"> <div class="cover-overlay">
@@ -226,12 +226,13 @@ import { resourceApi } from '@/apis/resource';
import { FILE_DOWNLOAD_URL } from '@/config'; import { FILE_DOWNLOAD_URL } from '@/config';
import type { TaskItemVO } from '@/types/study'; import type { TaskItemVO } from '@/types/study';
import defaultArticleImg from '@/assets/imgs/article-default.png'; import defaultArticleImg from '@/assets/imgs/article-default.png';
import { getRandomDefaultCover } from '@/utils/defaultCover';
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
// 默认封面(文章和课程使用同一个 // 封面URL映射resourceID -> coverUrl
const defaultCourseImg = defaultArticleImg; const coverUrls = ref<Map<string, string>>(new Map());
// 响应式数据 // 响应式数据
const loading = ref(false); const loading = ref(false);
@@ -275,6 +276,9 @@ async function loadSearchResults() {
if (result.success && result.dataList) { if (result.success && result.dataList) {
searchResults.value = result.dataList; searchResults.value = result.dataList;
total.value = result.dataList.length; total.value = result.dataList.length;
// 加载封面
await loadCovers(result.dataList);
} else { } else {
searchResults.value = []; searchResults.value = [];
total.value = 0; total.value = 0;
@@ -289,6 +293,40 @@ async function loadSearchResults() {
} }
} }
/**
* 加载资源封面
*/
async function loadCovers(items: TaskItemVO[]) {
const newCoverUrls = new Map<string, string>();
for (const item of items) {
const itemId = item.resourceID || item.courseID || '';
if (!itemId) continue;
if (item.coverImage) {
// 有封面直接使用
newCoverUrls.set(itemId, FILE_DOWNLOAD_URL + item.coverImage);
} else if (item.tagID) {
// 没有封面根据tagID获取默认封面
const cover = await getRandomDefaultCover(item.tagID);
newCoverUrls.set(itemId, cover);
} else {
// 使用全局默认
newCoverUrls.set(itemId, defaultArticleImg);
}
}
coverUrls.value = newCoverUrls;
}
/**
* 获取资源封面URL
*/
function getCoverUrl(item: TaskItemVO): string {
const itemId = item.resourceID || item.courseID || '';
return coverUrls.value.get(itemId) || defaultArticleImg;
}
/** /**
* 执行搜索 * 执行搜索
*/ */

View File

@@ -9,7 +9,7 @@
> >
<div class="resource-cover"> <div class="resource-cover">
<img <img
:src="resource.coverImage ? (FILE_DOWNLOAD_URL + resource.coverImage) : staticAssets.defaultArticleImg" :src="getCoverUrl(resource)"
alt="cover" alt="cover"
/> />
</div> </div>
@@ -55,6 +55,7 @@ import type { Resource } from '@/types/resource';
import type { PageParam } from '@/types'; import type { PageParam } from '@/types';
import defaultArticleImgUrl from '@/assets/imgs/article-default.png'; import defaultArticleImgUrl from '@/assets/imgs/article-default.png';
import { useDevice } from '@/utils/deviceUtils'; import { useDevice } from '@/utils/deviceUtils';
import { getRandomDefaultCover } from '@/utils/defaultCover';
// 创建响应式数据对象,包含静态资源 // 创建响应式数据对象,包含静态资源
const staticAssets = reactive({ const staticAssets = reactive({
@@ -82,6 +83,7 @@ const listContainerRef = ref<HTMLElement>();
const hasMoreData = ref(true); const hasMoreData = ref(true);
const isLoadingMore = ref(false); const isLoadingMore = ref(false);
const hasTriggeredLoadMore = ref(false); // 标记是否触发过加载更多 const hasTriggeredLoadMore = ref(false); // 标记是否触发过加载更多
const coverUrls = ref<Map<string, string>>(new Map()); // 存储资源封面URL
// 设备检测 // 设备检测
const { isMobileDevice } = useDevice(); const { isMobileDevice } = useDevice();
@@ -171,6 +173,9 @@ async function loadResources(isAppend = false) {
const totalPages = Math.ceil(total.value / pageSize.value); const totalPages = Math.ceil(total.value / pageSize.value);
hasMoreData.value = currentPage.value < totalPages; hasMoreData.value = currentPage.value < totalPages;
// 加载封面
await loadCovers(newData);
// 通知父组件列表已更新 // 通知父组件列表已更新
emit('list-updated', resources.value); emit('list-updated', resources.value);
} }
@@ -254,6 +259,35 @@ async function loadMoreData() {
await loadResources(true); // true 表示追加模式 await loadResources(true); // true 表示追加模式
} }
/**
* 加载资源封面
*/
async function loadCovers(items: Resource[]) {
for (const item of items) {
const itemId = item.resourceID || '';
if (!itemId) continue;
if (item.coverImage) {
// 有封面直接使用
coverUrls.value.set(itemId, FILE_DOWNLOAD_URL + item.coverImage);
} else if (item.tagID) {
// 没有封面根据tagID获取默认封面
const cover = await getRandomDefaultCover(item.tagID);
coverUrls.value.set(itemId, cover);
} else {
// 使用全局默认
coverUrls.value.set(itemId, defaultArticleImgUrl);
}
}
}
/**
* 获取资源封面URL
*/
function getCoverUrl(resource: Resource): string {
return coverUrls.value.get(resource.resourceID || '') || defaultArticleImgUrl;
}
defineExpose({ defineExpose({
loadResources, loadResources,
getResources, getResources,

View File

@@ -44,7 +44,7 @@
<!-- 课程封面 --> <!-- 课程封面 -->
<div class="course-cover"> <div class="course-cover">
<img <img
:src="course.coverImage ? FILE_DOWNLOAD_URL + course.coverImage : defaultCover" :src="getCoverUrl(course)"
:alt="course.name" :alt="course.name"
class="cover-image" class="cover-image"
/> />
@@ -111,6 +111,7 @@ import type { Course, PageParam } from '@/types';
import { StudyPlanLayout } from '@/views/user/study-plan'; import { StudyPlanLayout } from '@/views/user/study-plan';
import defaultCoverImg from '@/assets/imgs/default-course-bg.png' import defaultCoverImg from '@/assets/imgs/default-course-bg.png'
import { FILE_DOWNLOAD_URL } from '@/config'; import { FILE_DOWNLOAD_URL } from '@/config';
import { getRandomDefaultCover } from '@/utils/defaultCover';
defineOptions({ defineOptions({
name: 'CourseCenterView' name: 'CourseCenterView'
@@ -124,6 +125,7 @@ const courseList = ref<Course[]>([]);
const total = ref(0); const total = ref(0);
const isMobile = ref(false); const isMobile = ref(false);
const hasMore = ref(true); const hasMore = ref(true);
const coverUrls = ref<Map<string, string>>(new Map()); // 存储课程封面URL
// 默认封面图片 // 默认封面图片
const defaultCover = defaultCoverImg; const defaultCover = defaultCoverImg;
@@ -200,6 +202,9 @@ async function loadMoreCourses() {
pageParam.value.pageNumber = nextPage; pageParam.value.pageNumber = nextPage;
total.value = res.pageParam?.totalElements || 0; total.value = res.pageParam?.totalElements || 0;
// 加载封面
await loadCovers(res.dataList);
// 检查是否还有更多数据 // 检查是否还有更多数据
hasMore.value = courseList.value.length < total.value; hasMore.value = courseList.value.length < total.value;
} else { } else {
@@ -236,6 +241,9 @@ async function loadCourseList(isRefresh = false) {
courseList.value = res.dataList || []; courseList.value = res.dataList || [];
total.value = res.pageParam?.totalElements || 0; total.value = res.pageParam?.totalElements || 0;
// 加载封面
await loadCovers(res.dataList || []);
// 移动端下检查是否还有更多数据 // 移动端下检查是否还有更多数据
if (isMobile.value) { if (isMobile.value) {
hasMore.value = courseList.value.length < total.value; hasMore.value = courseList.value.length < total.value;
@@ -292,6 +300,35 @@ function getCategoryName(): string {
// TODO: 从 courseTags 中获取第一个标签作为分类 // TODO: 从 courseTags 中获取第一个标签作为分类
return '党史学习'; return '党史学习';
} }
/**
* 加载课程封面
*/
async function loadCovers(courses: Course[]) {
for (const course of courses) {
const courseId = course.courseID || '';
if (!courseId) continue;
if (course.coverImage) {
// 有封面直接使用
coverUrls.value.set(courseId, FILE_DOWNLOAD_URL + course.coverImage);
} else if (course.tagID) {
// 没有封面根据tagID获取默认封面
const cover = await getRandomDefaultCover(course.tagID);
coverUrls.value.set(courseId, cover);
} else {
// 使用全局默认
coverUrls.value.set(courseId, defaultCover);
}
}
}
/**
* 获取课程封面URL
*/
function getCoverUrl(course: Course): string {
return coverUrls.value.get(course.courseID || '') || defaultCover;
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>