搜索、小助手推荐位
This commit is contained in:
@@ -131,6 +131,7 @@ INSERT INTO `tb_sys_menu` VALUES
|
|||||||
('500', 'menu_profile', '账号中心', 'menu_user_dropdown', '/profile', 'user/profile/ProfileView', NULL, 5, 1, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
|
('500', 'menu_profile', '账号中心', 'menu_user_dropdown', '/profile', 'user/profile/ProfileView', NULL, 5, 1, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
|
||||||
('501', 'menu_personal_info', '个人信息', 'menu_profile', '/profile/personal-info', 'user/profile/PersonalInfoView', NULL, 1, 0, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
|
('501', 'menu_personal_info', '个人信息', 'menu_profile', '/profile/personal-info', 'user/profile/PersonalInfoView', NULL, 1, 0, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
|
||||||
('502', 'menu_account_settings', '账号设置', 'menu_profile', '/profile/account-settings', 'user/profile/AccountSettingsView', NULL, 2, 0, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
|
('502', 'menu_account_settings', '账号设置', 'menu_profile', '/profile/account-settings', 'user/profile/AccountSettingsView', NULL, 2, 0, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
|
||||||
|
('503', 'menu_search', '搜索', NULL, '/search', 'user/resource-center/SearchView', NULL, 3, 0, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
|
||||||
-- 管理后台菜单 (1000-8999)
|
-- 管理后台菜单 (1000-8999)
|
||||||
('1000', 'menu_admin_overview', '系统总览', NULL, '/admin/overview', 'admin/overview/SystemOverviewView', 'admin/overview.svg', 1, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:52:32', NULL, 0),
|
('1000', 'menu_admin_overview', '系统总览', NULL, '/admin/overview', 'admin/overview/SystemOverviewView', 'admin/overview.svg', 1, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:52:32', NULL, 0),
|
||||||
('2000', 'menu_sys_manage', '系统管理', NULL, '', '', 'admin/settings.svg', 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:52:35', NULL, 0),
|
('2000', 'menu_sys_manage', '系统管理', NULL, '', '', 'admin/settings.svg', 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:52:35', NULL, 0),
|
||||||
@@ -191,7 +192,7 @@ INSERT INTO `tb_sys_menu_permission` (id, permission_id, menu_id, creator, creat
|
|||||||
('122', 'perm_default', 'menu_course_detail', '1', now()),
|
('122', 'perm_default', 'menu_course_detail', '1', now()),
|
||||||
('123', 'perm_default', 'menu_course_study', '1', now()),
|
('123', 'perm_default', 'menu_course_study', '1', now()),
|
||||||
('124', 'perm_default', 'menu_article_show', '1', now()),
|
('124', 'perm_default', 'menu_article_show', '1', now()),
|
||||||
|
('125', 'perm_default', 'menu_search', '1', now()),
|
||||||
|
|
||||||
-- 后端管理菜单权限关联
|
-- 后端管理菜单权限关联
|
||||||
('200', 'perm_system_manage', 'menu_admin_overview', '1', now()),
|
('200', 'perm_system_manage', 'menu_admin_overview', '1', now()),
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package org.xyzh.api.news.resource;
|
|||||||
|
|
||||||
import org.xyzh.common.core.domain.ResultDomain;
|
import org.xyzh.common.core.domain.ResultDomain;
|
||||||
import org.xyzh.common.core.page.PageParam;
|
import org.xyzh.common.core.page.PageParam;
|
||||||
|
import org.xyzh.common.core.page.PageRequest;
|
||||||
import org.xyzh.common.dto.resource.TbResource;
|
import org.xyzh.common.dto.resource.TbResource;
|
||||||
import org.xyzh.common.dto.usercenter.TbUserCollection;
|
import org.xyzh.common.dto.usercenter.TbUserCollection;
|
||||||
import org.xyzh.common.vo.ResourceVO;
|
import org.xyzh.common.vo.ResourceVO;
|
||||||
|
import org.xyzh.common.vo.TaskItemVO;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -186,4 +188,12 @@ public interface ResourceService {
|
|||||||
|
|
||||||
ResultDomain<Integer> getResourceCount(TbResource filter);
|
ResultDomain<Integer> getResourceCount(TbResource filter);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 搜索资源
|
||||||
|
* @param filter 过滤条件
|
||||||
|
* @return ResultDomain<TaskItemVO> 搜索结果
|
||||||
|
* @author yslg
|
||||||
|
* @since 2025-10-15
|
||||||
|
*/
|
||||||
|
ResultDomain<TaskItemVO> searchItem(PageRequest<TbResource> filter);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ public class TaskItemVO extends TbLearningTask {
|
|||||||
private BigDecimal progress;
|
private BigDecimal progress;
|
||||||
private Date completeTime;
|
private Date completeTime;
|
||||||
|
|
||||||
|
// 搜索结果展示字段
|
||||||
|
private String coverImage;
|
||||||
|
private String summary;
|
||||||
|
private String author;
|
||||||
|
private Integer viewCount;
|
||||||
|
private Date publishTime;
|
||||||
|
|
||||||
public String getDeptID() {
|
public String getDeptID() {
|
||||||
return deptID;
|
return deptID;
|
||||||
}
|
}
|
||||||
@@ -183,5 +190,35 @@ public class TaskItemVO extends TbLearningTask {
|
|||||||
this.completeTime = completeTime;
|
this.completeTime = completeTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 搜索结果展示字段的 getter 和 setter
|
||||||
|
public String getCoverImage() {
|
||||||
|
return coverImage;
|
||||||
|
}
|
||||||
|
public void setCoverImage(String coverImage) {
|
||||||
|
this.coverImage = coverImage;
|
||||||
|
}
|
||||||
|
public String getSummary() {
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
public void setSummary(String summary) {
|
||||||
|
this.summary = summary;
|
||||||
|
}
|
||||||
|
public String getAuthor() {
|
||||||
|
return author;
|
||||||
|
}
|
||||||
|
public void setAuthor(String author) {
|
||||||
|
this.author = author;
|
||||||
|
}
|
||||||
|
public Integer getViewCount() {
|
||||||
|
return viewCount;
|
||||||
|
}
|
||||||
|
public void setViewCount(Integer viewCount) {
|
||||||
|
this.viewCount = viewCount;
|
||||||
|
}
|
||||||
|
public Date getPublishTime() {
|
||||||
|
return publishTime;
|
||||||
|
}
|
||||||
|
public void setPublishTime(Date publishTime) {
|
||||||
|
this.publishTime = publishTime;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import org.xyzh.common.dto.user.TbSysUser;
|
|||||||
import org.xyzh.common.dto.usercenter.TbUserCollection;
|
import org.xyzh.common.dto.usercenter.TbUserCollection;
|
||||||
import org.xyzh.common.utils.TimeUtils;
|
import org.xyzh.common.utils.TimeUtils;
|
||||||
import org.xyzh.common.vo.ResourceVO;
|
import org.xyzh.common.vo.ResourceVO;
|
||||||
|
import org.xyzh.common.vo.TaskItemVO;
|
||||||
import org.xyzh.system.utils.LoginUtil;
|
import org.xyzh.system.utils.LoginUtil;
|
||||||
/**
|
/**
|
||||||
* @description 资源控制器
|
* @description 资源控制器
|
||||||
@@ -192,4 +193,9 @@ public class ResourceController {
|
|||||||
@RequestParam(value = "status", required = false) Integer status) {
|
@RequestParam(value = "status", required = false) Integer status) {
|
||||||
return resourceService.searchResources(keyword, tagID, status);
|
return resourceService.searchResources(keyword, tagID, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/search")
|
||||||
|
public ResultDomain<TaskItemVO> searchResources(@RequestBody PageRequest<TbResource> filter) {
|
||||||
|
return resourceService.searchItem(filter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import org.apache.ibatis.annotations.Param;
|
|||||||
import org.xyzh.common.core.page.PageParam;
|
import org.xyzh.common.core.page.PageParam;
|
||||||
import org.xyzh.common.dto.resource.TbResource;
|
import org.xyzh.common.dto.resource.TbResource;
|
||||||
import org.xyzh.common.vo.ResourceVO;
|
import org.xyzh.common.vo.ResourceVO;
|
||||||
|
import org.xyzh.common.vo.TaskItemVO;
|
||||||
import org.xyzh.common.vo.UserDeptRoleVO;
|
import org.xyzh.common.vo.UserDeptRoleVO;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -205,4 +206,6 @@ public interface ResourceMapper extends BaseMapper<TbResource> {
|
|||||||
* @since 2025-10-15
|
* @since 2025-10-15
|
||||||
*/
|
*/
|
||||||
int incrementViewCount(@Param("resourceID") String resourceID);
|
int incrementViewCount(@Param("resourceID") String resourceID);
|
||||||
|
|
||||||
|
List<TaskItemVO> selectItem(@Param("filter") TbResource filter, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.springframework.util.StringUtils;
|
|||||||
import org.xyzh.common.core.domain.ResultDomain;
|
import org.xyzh.common.core.domain.ResultDomain;
|
||||||
import org.xyzh.common.core.page.PageDomain;
|
import org.xyzh.common.core.page.PageDomain;
|
||||||
import org.xyzh.common.core.page.PageParam;
|
import org.xyzh.common.core.page.PageParam;
|
||||||
|
import org.xyzh.common.core.page.PageRequest;
|
||||||
import org.xyzh.common.dto.resource.TbResource;
|
import org.xyzh.common.dto.resource.TbResource;
|
||||||
import org.xyzh.common.dto.resource.TbResourceTag;
|
import org.xyzh.common.dto.resource.TbResourceTag;
|
||||||
import org.xyzh.common.dto.resource.TbTag;
|
import org.xyzh.common.dto.resource.TbTag;
|
||||||
@@ -17,6 +18,7 @@ import org.xyzh.common.dto.usercenter.TbUserCollection;
|
|||||||
import org.xyzh.common.utils.IDUtils;
|
import org.xyzh.common.utils.IDUtils;
|
||||||
import org.xyzh.common.vo.ResourceVO;
|
import org.xyzh.common.vo.ResourceVO;
|
||||||
import org.xyzh.common.vo.TagVO;
|
import org.xyzh.common.vo.TagVO;
|
||||||
|
import org.xyzh.common.vo.TaskItemVO;
|
||||||
import org.xyzh.news.mapper.ResourceMapper;
|
import org.xyzh.news.mapper.ResourceMapper;
|
||||||
import org.xyzh.news.mapper.ResourceTagMapper;
|
import org.xyzh.news.mapper.ResourceTagMapper;
|
||||||
import org.xyzh.system.utils.LoginUtil;
|
import org.xyzh.system.utils.LoginUtil;
|
||||||
@@ -771,4 +773,23 @@ public class NCResourceServiceImpl implements ResourceService {
|
|||||||
return resultDomain;
|
return resultDomain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResultDomain<TaskItemVO> searchItem(PageRequest<TbResource> filter) {
|
||||||
|
ResultDomain<TaskItemVO> resultDomain = new ResultDomain<>();
|
||||||
|
try {
|
||||||
|
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
|
||||||
|
List<TaskItemVO> list = resourceMapper.selectItem(filter.getFilter(), filter.getPageParam(), userDeptRoles);
|
||||||
|
if (list == null || list.isEmpty()) {
|
||||||
|
resultDomain.fail("搜索资源失败: 没有找到资源");
|
||||||
|
return resultDomain;
|
||||||
|
}
|
||||||
|
resultDomain.success("搜索资源成功", list);
|
||||||
|
return resultDomain;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("搜索资源异常: {}", e.getMessage(), e);
|
||||||
|
resultDomain.fail("搜索资源失败: " + e.getMessage());
|
||||||
|
return resultDomain;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -323,6 +323,9 @@
|
|||||||
<if test="filter.author != null and filter.author != ''">
|
<if test="filter.author != null and filter.author != ''">
|
||||||
AND r.author LIKE CONCAT('%', #{filter.author}, '%')
|
AND r.author LIKE CONCAT('%', #{filter.author}, '%')
|
||||||
</if>
|
</if>
|
||||||
|
<if test="filter.title != null and filter.title != ''">
|
||||||
|
AND r.title LIKE CONCAT('%', #{filter.title}, '%')
|
||||||
|
</if>
|
||||||
<if test="filter.status != null">
|
<if test="filter.status != null">
|
||||||
AND r.status = #{filter.status}
|
AND r.status = #{filter.status}
|
||||||
</if>
|
</if>
|
||||||
@@ -451,4 +454,116 @@
|
|||||||
</update>
|
</update>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- selectItem -->
|
||||||
|
<!-- 联合模糊查询文章和课程,时间排序 -->
|
||||||
|
<select id="selectItem" resultType="org.xyzh.common.vo.TaskItemVO">
|
||||||
|
SELECT * FROM (
|
||||||
|
-- 查询文章资源
|
||||||
|
SELECT DISTINCT
|
||||||
|
1 AS itemType,
|
||||||
|
r.resource_id AS resourceID,
|
||||||
|
r.title AS resourceName,
|
||||||
|
NULL AS courseID,
|
||||||
|
NULL AS courseName,
|
||||||
|
r.author,
|
||||||
|
r.cover_image AS coverImage,
|
||||||
|
r.summary,
|
||||||
|
r.view_count AS viewCount,
|
||||||
|
r.publish_time AS publishTime,
|
||||||
|
r.create_time AS createTime
|
||||||
|
FROM tb_resource r
|
||||||
|
INNER JOIN tb_resource_permission rp ON r.resource_id = rp.resource_id
|
||||||
|
AND rp.resource_type = 1
|
||||||
|
AND rp.deleted = 0
|
||||||
|
AND rp.can_read = 1
|
||||||
|
AND (
|
||||||
|
-- 全局权限:所有用户可访问
|
||||||
|
(rp.dept_id IS NULL AND rp.role_id IS NULL)
|
||||||
|
<if test="userDeptRoles != null and userDeptRoles.size() > 0">
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM (
|
||||||
|
<foreach collection="userDeptRoles" item="udr" separator=" UNION ALL ">
|
||||||
|
SELECT #{udr.deptID} AS dept_id, #{udr.deptPath} AS dept_path, #{udr.roleID} AS role_id
|
||||||
|
</foreach>
|
||||||
|
) user_roles
|
||||||
|
LEFT JOIN tb_sys_dept perm_dept ON perm_dept.dept_id = rp.dept_id AND perm_dept.deleted = 0
|
||||||
|
WHERE
|
||||||
|
-- 部门级权限:当前部门或父部门(通过dept_path判断继承关系)
|
||||||
|
(rp.role_id IS NULL AND rp.dept_id IS NOT NULL
|
||||||
|
AND user_roles.dept_path LIKE CONCAT(perm_dept.dept_path, '%'))
|
||||||
|
-- 角色级权限:跨部门的角色权限
|
||||||
|
OR (rp.dept_id IS NULL AND rp.role_id = user_roles.role_id)
|
||||||
|
-- 精确权限:特定部门的特定角色
|
||||||
|
OR (rp.dept_id = user_roles.dept_id AND rp.role_id = user_roles.role_id)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
)
|
||||||
|
WHERE r.deleted = 0
|
||||||
|
<if test="filter.status != null">
|
||||||
|
AND r.status = #{filter.status}
|
||||||
|
</if>
|
||||||
|
<if test="filter.title != null and filter.title != ''">
|
||||||
|
AND (r.title LIKE CONCAT('%', #{filter.title}, '%')
|
||||||
|
OR r.summary LIKE CONCAT('%', #{filter.title}, '%'))
|
||||||
|
</if>
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- 查询课程资源
|
||||||
|
SELECT DISTINCT
|
||||||
|
2 AS itemType,
|
||||||
|
NULL AS resourceID,
|
||||||
|
NULL AS resourceName,
|
||||||
|
c.course_id AS courseID,
|
||||||
|
c.name AS courseName,
|
||||||
|
c.teacher AS author,
|
||||||
|
c.cover_image AS coverImage,
|
||||||
|
c.description AS summary,
|
||||||
|
c.view_count AS viewCount,
|
||||||
|
NULL AS publishTime,
|
||||||
|
c.create_time AS createTime
|
||||||
|
FROM tb_course c
|
||||||
|
INNER JOIN tb_resource_permission rp ON c.course_id = rp.resource_id
|
||||||
|
AND rp.resource_type = 2
|
||||||
|
AND rp.deleted = 0
|
||||||
|
AND rp.can_read = 1
|
||||||
|
AND (
|
||||||
|
-- 全局权限:所有用户可访问
|
||||||
|
(rp.dept_id IS NULL AND rp.role_id IS NULL)
|
||||||
|
<if test="userDeptRoles != null and userDeptRoles.size() > 0">
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM (
|
||||||
|
<foreach collection="userDeptRoles" item="udr" separator=" UNION ALL ">
|
||||||
|
SELECT #{udr.deptID} AS dept_id, #{udr.deptPath} AS dept_path, #{udr.roleID} AS role_id
|
||||||
|
</foreach>
|
||||||
|
) user_roles
|
||||||
|
LEFT JOIN tb_sys_dept perm_dept ON perm_dept.dept_id = rp.dept_id AND perm_dept.deleted = 0
|
||||||
|
WHERE
|
||||||
|
-- 部门级权限:当前部门或父部门(通过dept_path判断继承关系)
|
||||||
|
(rp.role_id IS NULL AND rp.dept_id IS NOT NULL
|
||||||
|
AND user_roles.dept_path LIKE CONCAT(perm_dept.dept_path, '%'))
|
||||||
|
-- 角色级权限:跨部门的角色权限
|
||||||
|
OR (rp.dept_id IS NULL AND rp.role_id = user_roles.role_id)
|
||||||
|
-- 精确权限:特定部门的特定角色
|
||||||
|
OR (rp.dept_id = user_roles.dept_id AND rp.role_id = user_roles.role_id)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
)
|
||||||
|
WHERE c.deleted = 0
|
||||||
|
<if test="filter.status != null">
|
||||||
|
AND c.status = #{filter.status}
|
||||||
|
</if>
|
||||||
|
<if test="filter.title != null and filter.title != ''">
|
||||||
|
AND (c.name LIKE CONCAT('%', #{filter.title}, '%')
|
||||||
|
OR c.description LIKE CONCAT('%', #{filter.title}, '%'))
|
||||||
|
</if>
|
||||||
|
) AS combined_results
|
||||||
|
-- 按时间倒序排列,优先使用发布时间,其次使用创建时间
|
||||||
|
ORDER BY COALESCE(publishTime, createTime) DESC
|
||||||
|
<if test="pageParam != null">
|
||||||
|
LIMIT #{pageParam.offset}, #{pageParam.pageSize}
|
||||||
|
</if>
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -237,8 +237,8 @@
|
|||||||
lr.complete_time AS complete_time
|
lr.complete_time AS complete_time
|
||||||
FROM tb_learning_record lr
|
FROM tb_learning_record lr
|
||||||
LEFT JOIN tb_resource r ON lr.resource_type = 1 AND lr.resource_id = r.resource_id
|
LEFT JOIN tb_resource r ON lr.resource_type = 1 AND lr.resource_id = r.resource_id
|
||||||
LEFT JOIN tb_course c ON lr.resource_type = 2 AND lr.course_id = c.id
|
LEFT JOIN tb_course c ON lr.resource_type = 2 AND lr.course_id = c.course_id
|
||||||
LEFT JOIN tb_course_chapter ch ON lr.resource_type = 3 AND lr.chapter_id = ch.id
|
LEFT JOIN tb_course_chapter ch ON lr.resource_type = 3 AND lr.chapter_id = ch.chapter_id
|
||||||
<where>
|
<where>
|
||||||
<if test="filter.userID != null and filter.userID != ''">
|
<if test="filter.userID != null and filter.userID != ''">
|
||||||
AND lr.user_id = #{filter.userID}
|
AND lr.user_id = #{filter.userID}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { api } from '@/apis';
|
import { api } from '@/apis';
|
||||||
import type { ResultDomain, Resource, ResourceSearchParams, PageParam, ResourceVO, UserCollection } from '@/types';
|
import type { ResultDomain, Resource, PageParam, ResourceVO, UserCollection, TaskItemVO } from '@/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 资源API服务
|
* 资源API服务
|
||||||
@@ -20,7 +20,7 @@ export const resourceApi = {
|
|||||||
* @param filter 筛选条件
|
* @param filter 筛选条件
|
||||||
* @returns Promise<ResultDomain<Resource>>
|
* @returns Promise<ResultDomain<Resource>>
|
||||||
*/
|
*/
|
||||||
async getResourceList(filter?: ResourceSearchParams): Promise<ResultDomain<Resource>> {
|
async getResourceList(filter?: Resource): Promise<ResultDomain<Resource>> {
|
||||||
const response = await api.get<Resource>('/news/resources/list', filter);
|
const response = await api.get<Resource>('/news/resources/list', filter);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
@@ -31,7 +31,7 @@ export const resourceApi = {
|
|||||||
* @param pageParam 分页参数
|
* @param pageParam 分页参数
|
||||||
* @returns Promise<ResultDomain<Resource>>
|
* @returns Promise<ResultDomain<Resource>>
|
||||||
*/
|
*/
|
||||||
async getResourcePage(pageParam: PageParam, filter?: ResourceSearchParams): Promise<ResultDomain<Resource>> {
|
async getResourcePage(pageParam: PageParam, filter?: Resource): Promise<ResultDomain<Resource>> {
|
||||||
const response = await api.post<Resource>('/news/resources/page', {
|
const response = await api.post<Resource>('/news/resources/page', {
|
||||||
pageParam,
|
pageParam,
|
||||||
filter,
|
filter,
|
||||||
@@ -45,7 +45,7 @@ export const resourceApi = {
|
|||||||
* @param pageParam 分页参数
|
* @param pageParam 分页参数
|
||||||
* @returns Promise<ResultDomain<ResourceVO>>
|
* @returns Promise<ResultDomain<ResourceVO>>
|
||||||
*/
|
*/
|
||||||
async getResourcePageOrderByViewCount(pageParam: PageParam, filter?: ResourceSearchParams): Promise<ResultDomain<ResourceVO>> {
|
async getResourcePageOrderByViewCount(pageParam: PageParam, filter?: Resource): Promise<ResultDomain<ResourceVO>> {
|
||||||
const response = await api.post<ResourceVO>('/news/resources/page/view-count', {
|
const response = await api.post<ResourceVO>('/news/resources/page/view-count', {
|
||||||
pageParam,
|
pageParam,
|
||||||
filter,
|
filter,
|
||||||
@@ -218,6 +218,16 @@ export const resourceApi = {
|
|||||||
|
|
||||||
const response = await api.get<Resource>('/news/resources/search', params);
|
const response = await api.get<Resource>('/news/resources/search', params);
|
||||||
return response.data;
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 联合搜索文章和课程
|
||||||
|
* @param request 搜索请求参数(包含pageParam和filter)
|
||||||
|
* @returns Promise<ResultDomain<TaskItemVO>>
|
||||||
|
*/
|
||||||
|
async searchItems(request: { pageParam: PageParam; filter: Resource }): Promise<ResultDomain<TaskItemVO>> {
|
||||||
|
const response = await api.post<TaskItemVO>('/news/resources/search', request);
|
||||||
|
return response.data;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -208,10 +208,10 @@ function handleNavClick(menu: SysMenu) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 处理搜索
|
// 处理搜索
|
||||||
function handleSearch() {
|
function handleSearch(keyword: string) {
|
||||||
if (searchKeyword.value.trim()) {
|
if (keyword && keyword.trim()) {
|
||||||
// 这里可以跳转到搜索页面或触发搜索功能
|
// 跳转到搜索页面
|
||||||
router.push(`/search?keyword=${encodeURIComponent(searchKeyword.value.trim())}`);
|
router.push(`/search?keyword=${encodeURIComponent(keyword.trim())}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,5 +52,6 @@ export const APP_CONFIG = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
export const PUBLIC_IMG_PATH = 'http://localhost:8080/schoolNewsWeb/img';
|
export const PUBLIC_IMG_PATH = 'http://localhost:8080/schoolNewsWeb/img';
|
||||||
|
export const PUBLIC_WEB_PATH = 'http://localhost:8080/schoolNewsWeb';
|
||||||
export default APP_CONFIG;
|
export default APP_CONFIG;
|
||||||
|
|
||||||
|
|||||||
@@ -331,6 +331,16 @@ export interface TaskItemVO extends LearningTask {
|
|||||||
progress?: number;
|
progress?: number;
|
||||||
/** 完成时间 */
|
/** 完成时间 */
|
||||||
completeTime?: string;
|
completeTime?: string;
|
||||||
|
/** 封面图片(用于搜索结果展示) */
|
||||||
|
coverImage?: string;
|
||||||
|
/** 简介(用于搜索结果展示) */
|
||||||
|
summary?: string;
|
||||||
|
/** 作者(用于搜索结果展示) */
|
||||||
|
author?: string;
|
||||||
|
/** 浏览次数(用于搜索结果展示) */
|
||||||
|
viewCount?: number;
|
||||||
|
/** 发布时间(用于搜索结果展示) */
|
||||||
|
publishTime?: Date | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
<img v-else src="@/assets/imgs/assistant.svg" alt="AI助手" class="welcome-avatar" />
|
<img v-else src="@/assets/imgs/assistant.svg" alt="AI助手" class="welcome-avatar" />
|
||||||
</div>
|
</div>
|
||||||
<h2>你好!我是{{ agentConfig?.name || 'AI助手' }}</h2>
|
<h2>你好!我是{{ agentConfig?.name || 'AI助手' }}</h2>
|
||||||
<p>{{ agentConfig?.systemPrompt || '有什么可以帮助你的吗?' }}</p>
|
<AIRecommend />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 消息列表 -->
|
<!-- 消息列表 -->
|
||||||
@@ -189,6 +189,7 @@ import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue';
|
|||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { chatApi, chatHistoryApi, aiAgentConfigApi, fileUploadApi } from '@/apis/ai';
|
import { chatApi, chatHistoryApi, aiAgentConfigApi, fileUploadApi } from '@/apis/ai';
|
||||||
import type { AiConversation, AiMessage, AiAgentConfig, AiUploadFile } from '@/types/ai';
|
import type { AiConversation, AiMessage, AiAgentConfig, AiUploadFile } from '@/types/ai';
|
||||||
|
import { AIRecommend } from '@/views/public/ai';
|
||||||
|
|
||||||
interface AIAgentProps {
|
interface AIAgentProps {
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
|
|||||||
271
schoolNewsWeb/src/views/public/ai/AIRecommend.vue
Normal file
271
schoolNewsWeb/src/views/public/ai/AIRecommend.vue
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ai-recommend">
|
||||||
|
<div class="recommend-left">
|
||||||
|
<div class="recommend-title">
|
||||||
|
<span class="title-icon">✨</span>
|
||||||
|
<span class="title-text">智能推荐</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="recommend-right">
|
||||||
|
<!-- Tab选项卡,推荐top资源和思政资源5个 -->
|
||||||
|
<el-tabs v-model="activeName" class="recommend-tabs" @tab-click="handleClick">
|
||||||
|
<el-tab-pane label="推荐top资源" name="top"></el-tab-pane>
|
||||||
|
<el-tab-pane label="思政资源" name="ideological"></el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
<div class="recommend-list">
|
||||||
|
<div v-if="showData.length === 0" class="empty-state">
|
||||||
|
<span class="empty-icon">📚</span>
|
||||||
|
<p>暂无推荐内容</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="list-items">
|
||||||
|
<div v-for="(item, index) in showData" :key="item.id" class="recommend-item">
|
||||||
|
<span class="item-number">{{ index + 1 }}</span>
|
||||||
|
<a :href="buildUrl(item)" class="item-link" target="_blank">
|
||||||
|
<span class="item-title">{{ item.title }}</span>
|
||||||
|
<span class="item-arrow">→</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { PageParam, type ResourceRecommendVO } from '@/types'
|
||||||
|
import { resourceRecommendApi} from '@/apis/resource';
|
||||||
|
import { PUBLIC_WEB_PATH } from '@/config'
|
||||||
|
type TabName = 'top' | 'ideological'
|
||||||
|
|
||||||
|
const activeName = ref<TabName>('top')
|
||||||
|
const handleClick = (tab: any, event: Event) => {
|
||||||
|
console.log(tab, event)
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
const showData = ref<ResourceRecommendVO[]>([])
|
||||||
|
|
||||||
|
const limit = 5
|
||||||
|
const tabMap: Record<TabName, number> = {top: 1, ideological: 2}
|
||||||
|
|
||||||
|
function buildUrl(item: ResourceRecommendVO) {
|
||||||
|
return `${PUBLIC_WEB_PATH}/article/show?articleId=${item.resourceID}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadData(){
|
||||||
|
resourceRecommendApi.getRecommendsByType(tabMap[activeName.value], limit).then((res) => {
|
||||||
|
showData.value = res.dataList || []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadData().then(() => {
|
||||||
|
console.log()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.ai-recommend {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.recommend-left {
|
||||||
|
min-width: 80px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.recommend-title {
|
||||||
|
writing-mode: vertical-lr;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
|
||||||
|
.title-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
animation: sparkle 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-right {
|
||||||
|
flex: 1;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
:deep(.recommend-tabs) {
|
||||||
|
.el-tabs__header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__nav-wrap::after {
|
||||||
|
background-color: #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__item {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #606266;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__active-bar {
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(90deg, #409eff 0%, #66b1ff 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-list {
|
||||||
|
min-height: 180px;
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
color: #909399;
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 15px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.recommend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(90deg, #e3f2fd 0%, #f5f5f5 100%);
|
||||||
|
border-left-color: #409eff;
|
||||||
|
transform: translateX(4px);
|
||||||
|
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
|
||||||
|
|
||||||
|
.item-arrow {
|
||||||
|
transform: translateX(4px);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-number {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #909399;
|
||||||
|
background: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-link {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #303133;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.item-title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-arrow {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #409eff;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sparkle {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1) rotate(0deg);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2) rotate(180deg);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.ai-recommend {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.recommend-left {
|
||||||
|
min-width: auto;
|
||||||
|
|
||||||
|
.recommend-title {
|
||||||
|
writing-mode: horizontal-tb;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1 +1,2 @@
|
|||||||
export { default as AIAgent} from './AIAgent.vue';
|
export { default as AIAgent} from './AIAgent.vue';
|
||||||
|
export { default as AIRecommend} from './AIRecommend.vue';
|
||||||
769
schoolNewsWeb/src/views/user/resource-center/SearchView.vue
Normal file
769
schoolNewsWeb/src/views/user/resource-center/SearchView.vue
Normal file
@@ -0,0 +1,769 @@
|
|||||||
|
<template>
|
||||||
|
<div class="search-view">
|
||||||
|
<!-- 页面头部 -->
|
||||||
|
<div class="search-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="header-left">
|
||||||
|
<button class="back-button" @click="goBack">
|
||||||
|
<el-icon>
|
||||||
|
<ArrowLeft />
|
||||||
|
</el-icon>
|
||||||
|
<span>返回</span>
|
||||||
|
</button>
|
||||||
|
<div class="header-info">
|
||||||
|
<h1 class="page-title">
|
||||||
|
<el-icon class="title-icon">
|
||||||
|
<Search />
|
||||||
|
</el-icon>
|
||||||
|
搜索结果
|
||||||
|
</h1>
|
||||||
|
<p class="page-desc" v-if="searchKeyword">
|
||||||
|
搜索关键词: <span class="keyword-text">{{ searchKeyword }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<div class="header-search">
|
||||||
|
<el-input
|
||||||
|
v-model="localSearchKeyword"
|
||||||
|
placeholder="搜索文章和课程内容"
|
||||||
|
class="search-input"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="Search"
|
||||||
|
@click="handleSearch"
|
||||||
|
class="search-button"
|
||||||
|
>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计信息 -->
|
||||||
|
<div class="stats-bar" v-if="!loading && (articles.length > 0 || courses.length > 0)">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">共找到</span>
|
||||||
|
<span class="stat-value">{{ total }}</span>
|
||||||
|
<span class="stat-label">条结果</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-divider"></div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">文章</span>
|
||||||
|
<span class="stat-value">{{ articles.length }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-divider"></div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">课程</span>
|
||||||
|
<span class="stat-value">{{ courses.length }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索结果 -->
|
||||||
|
<div class="search-results" v-loading="loading">
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-if="!loading && total === 0" class="empty-state">
|
||||||
|
<el-icon class="empty-icon">
|
||||||
|
<DocumentDelete />
|
||||||
|
</el-icon>
|
||||||
|
<h3>未找到相关内容</h3>
|
||||||
|
<p>换个关键词试试吧</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文章结果 -->
|
||||||
|
<div v-if="articles.length > 0" class="result-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">
|
||||||
|
<el-icon>
|
||||||
|
<Document />
|
||||||
|
</el-icon>
|
||||||
|
文章 ({{ articles.length }})
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="articles-grid">
|
||||||
|
<div
|
||||||
|
v-for="article in articles"
|
||||||
|
:key="article.resourceID"
|
||||||
|
class="article-card"
|
||||||
|
@click="handleArticleClick(article)"
|
||||||
|
>
|
||||||
|
<!-- 文章封面 -->
|
||||||
|
<div class="article-cover">
|
||||||
|
<img
|
||||||
|
v-if="article.coverImage"
|
||||||
|
:src="FILE_DOWNLOAD_URL + article.coverImage"
|
||||||
|
:alt="article.resourceName"
|
||||||
|
/>
|
||||||
|
<div v-else class="cover-placeholder">
|
||||||
|
<el-icon>
|
||||||
|
<Document />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="cover-overlay">
|
||||||
|
<span class="view-button">查看详情</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文章信息 -->
|
||||||
|
<div class="article-info">
|
||||||
|
<h3 class="article-title" :title="article.resourceName">
|
||||||
|
{{ article.resourceName }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- 简介 -->
|
||||||
|
<p class="article-summary">{{ article.summary || '暂无简介' }}</p>
|
||||||
|
|
||||||
|
<!-- 底部元信息 -->
|
||||||
|
<div class="article-meta">
|
||||||
|
<div class="meta-item">
|
||||||
|
<el-icon class="meta-icon">
|
||||||
|
<User />
|
||||||
|
</el-icon>
|
||||||
|
<span>{{ article.author || '未知' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<el-icon class="meta-icon">
|
||||||
|
<View />
|
||||||
|
</el-icon>
|
||||||
|
<span>{{ formatNumber(article.viewCount) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item" v-if="article.publishTime">
|
||||||
|
<el-icon class="meta-icon">
|
||||||
|
<Clock />
|
||||||
|
</el-icon>
|
||||||
|
<span>{{ formatDate(article.publishTime) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 课程结果 -->
|
||||||
|
<div v-if="courses.length > 0" class="result-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">
|
||||||
|
<el-icon>
|
||||||
|
<Reading />
|
||||||
|
</el-icon>
|
||||||
|
课程 ({{ courses.length }})
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="courses-grid">
|
||||||
|
<div
|
||||||
|
v-for="course in courses"
|
||||||
|
:key="course.courseID"
|
||||||
|
class="course-card"
|
||||||
|
@click="handleCourseClick(course)"
|
||||||
|
>
|
||||||
|
<!-- 课程封面 -->
|
||||||
|
<div class="course-cover">
|
||||||
|
<img
|
||||||
|
v-if="course.coverImage"
|
||||||
|
:src="FILE_DOWNLOAD_URL + course.coverImage"
|
||||||
|
:alt="course.courseName"
|
||||||
|
/>
|
||||||
|
<div v-else class="cover-placeholder">
|
||||||
|
<el-icon>
|
||||||
|
<Reading />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="cover-overlay">
|
||||||
|
<span class="view-button">查看课程</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 课程信息 -->
|
||||||
|
<div class="course-info">
|
||||||
|
<h3 class="course-title" :title="course.courseName">
|
||||||
|
{{ course.courseName }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- 简介 -->
|
||||||
|
<p class="course-summary">{{ course.summary || '暂无简介' }}</p>
|
||||||
|
|
||||||
|
<!-- 底部元信息 -->
|
||||||
|
<div class="course-meta">
|
||||||
|
<div class="meta-item">
|
||||||
|
<el-icon class="meta-icon">
|
||||||
|
<User />
|
||||||
|
</el-icon>
|
||||||
|
<span>{{ course.author || '未知' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<el-icon class="meta-icon">
|
||||||
|
<View />
|
||||||
|
</el-icon>
|
||||||
|
<span>{{ formatNumber(course.viewCount) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div v-if="total > pageSize" class="pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[10, 20, 30, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed, watch } from 'vue';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
Search,
|
||||||
|
Document,
|
||||||
|
Reading,
|
||||||
|
User,
|
||||||
|
View,
|
||||||
|
Clock,
|
||||||
|
DocumentDelete,
|
||||||
|
} from '@element-plus/icons-vue';
|
||||||
|
import { resourceApi } from '@/apis/resource';
|
||||||
|
import { FILE_DOWNLOAD_URL } from '@/config';
|
||||||
|
import type { TaskItemVO } from '@/types/study';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(false);
|
||||||
|
const searchKeyword = ref('');
|
||||||
|
const localSearchKeyword = ref('');
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const pageSize = ref(20);
|
||||||
|
const total = ref(0);
|
||||||
|
const searchResults = ref<TaskItemVO[]>([]);
|
||||||
|
|
||||||
|
// 计算属性:分离文章和课程
|
||||||
|
const articles = computed(() => {
|
||||||
|
return searchResults.value.filter(item => item.itemType === 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const courses = computed(() => {
|
||||||
|
return searchResults.value.filter(item => item.itemType === 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载搜索结果
|
||||||
|
*/
|
||||||
|
async function loadSearchResults() {
|
||||||
|
if (!searchKeyword.value.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await resourceApi.searchItems({
|
||||||
|
pageParam: {
|
||||||
|
pageNumber: currentPage.value,
|
||||||
|
pageSize: pageSize.value,
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
title: searchKeyword.value.trim(),
|
||||||
|
status: 1, // 只查询已发布的内容
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success && result.dataList) {
|
||||||
|
searchResults.value = result.dataList;
|
||||||
|
total.value = result.dataList.length;
|
||||||
|
} else {
|
||||||
|
searchResults.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('搜索失败:', error);
|
||||||
|
ElMessage.error('搜索失败,请稍后重试');
|
||||||
|
searchResults.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行搜索
|
||||||
|
*/
|
||||||
|
function handleSearch() {
|
||||||
|
if (!localSearchKeyword.value.trim()) {
|
||||||
|
ElMessage.warning('请输入搜索关键词');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchKeyword.value = localSearchKeyword.value;
|
||||||
|
currentPage.value = 1;
|
||||||
|
|
||||||
|
// 更新URL
|
||||||
|
router.push({
|
||||||
|
path: '/search',
|
||||||
|
query: { keyword: searchKeyword.value },
|
||||||
|
});
|
||||||
|
|
||||||
|
loadSearchResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页码变化
|
||||||
|
*/
|
||||||
|
function handlePageChange(page: number) {
|
||||||
|
currentPage.value = page;
|
||||||
|
loadSearchResults();
|
||||||
|
// 滚动到顶部
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每页数量变化
|
||||||
|
*/
|
||||||
|
function handleSizeChange(size: number) {
|
||||||
|
pageSize.value = size;
|
||||||
|
currentPage.value = 1;
|
||||||
|
loadSearchResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点击文章
|
||||||
|
*/
|
||||||
|
function handleArticleClick(article: TaskItemVO) {
|
||||||
|
router.push(`/article/show?articleId=${article.resourceID}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点击课程
|
||||||
|
*/
|
||||||
|
function handleCourseClick(course: TaskItemVO) {
|
||||||
|
router.push(`/study-plan/course-detail?courseId=${course.courseID}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回上一页
|
||||||
|
*/
|
||||||
|
function goBack() {
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化数字
|
||||||
|
*/
|
||||||
|
function formatNumber(num?: number): string {
|
||||||
|
if (!num) return '0';
|
||||||
|
if (num >= 10000) {
|
||||||
|
return (num / 10000).toFixed(1) + 'w';
|
||||||
|
}
|
||||||
|
return num.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期
|
||||||
|
*/
|
||||||
|
function formatDate(date?: Date | string): string {
|
||||||
|
if (!date) return '';
|
||||||
|
const d = new Date(date);
|
||||||
|
const year = d.getFullYear();
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(d.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听路由变化
|
||||||
|
watch(
|
||||||
|
() => route.query.keyword,
|
||||||
|
(newKeyword) => {
|
||||||
|
if (newKeyword && typeof newKeyword === 'string') {
|
||||||
|
searchKeyword.value = newKeyword;
|
||||||
|
localSearchKeyword.value = newKeyword;
|
||||||
|
loadSearchResults();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 组件挂载
|
||||||
|
onMounted(() => {
|
||||||
|
const keyword = route.query.keyword;
|
||||||
|
if (keyword && typeof keyword === 'string') {
|
||||||
|
searchKeyword.value = keyword;
|
||||||
|
localSearchKeyword.value = keyword;
|
||||||
|
loadSearchResults();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.search-view {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-header {
|
||||||
|
background: white;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
padding: 24px 32px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #606266;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e4e7ed;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
|
||||||
|
.title-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.keyword-text {
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-search {
|
||||||
|
width: 400px;
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
:deep(.el-input__wrapper) {
|
||||||
|
border-radius: 24px;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__suffix) {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button {
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 8px 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px 32px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-top: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 16px;
|
||||||
|
background: #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results {
|
||||||
|
padding: 0 32px 32px;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 80px 20px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 80px;
|
||||||
|
color: #dcdfe6;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #606266;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section {
|
||||||
|
margin-bottom: 48px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.articles-grid,
|
||||||
|
.courses-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-card,
|
||||||
|
.course-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 21, 41, 0.12);
|
||||||
|
|
||||||
|
.cover-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-cover,
|
||||||
|
.course-cover {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 180px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f5f7fa;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 64px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
|
||||||
|
.view-button {
|
||||||
|
padding: 8px 24px;
|
||||||
|
background: white;
|
||||||
|
color: #409eff;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-info,
|
||||||
|
.course-info {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-title,
|
||||||
|
.course-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
line-height: 1.4;
|
||||||
|
min-height: 44.8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-summary,
|
||||||
|
.course-summary {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
line-height: 1.5;
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-meta,
|
||||||
|
.course-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
|
||||||
|
.meta-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 32px;
|
||||||
|
padding: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.header-content {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-search {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.articles-grid,
|
||||||
|
.courses-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header-content {
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results {
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.articles-grid,
|
||||||
|
.courses-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
import { ref, watch, onMounted, nextTick } from 'vue';
|
import { ref, watch, onMounted, nextTick } from 'vue';
|
||||||
import { resourceApi } from '@/apis/resource';
|
import { resourceApi } from '@/apis/resource';
|
||||||
import { FILE_DOWNLOAD_URL } from '@/config';
|
import { FILE_DOWNLOAD_URL } from '@/config';
|
||||||
import type { Resource, ResourceSearchParams } from '@/types/resource';
|
import type { Resource } from '@/types/resource';
|
||||||
import type { PageParam } from '@/types';
|
import type { PageParam } from '@/types';
|
||||||
import defaultArticleImg from '@/assets/imgs/article-default.png';
|
import defaultArticleImg from '@/assets/imgs/article-default.png';
|
||||||
|
|
||||||
@@ -83,9 +83,9 @@ async function loadResources() {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const filter: ResourceSearchParams = {
|
const filter: Resource = {
|
||||||
tagID: props.tagID,
|
tagID: props.tagID,
|
||||||
keyword: props.searchKeyword,
|
title: props.searchKeyword,
|
||||||
status: 1 // 只加载已发布的
|
status: 1 // 只加载已发布的
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,20 +17,20 @@
|
|||||||
<div class="records-table" v-loading="tableLoading">
|
<div class="records-table" v-loading="tableLoading">
|
||||||
<h3>学习记录明细</h3>
|
<h3>学习记录明细</h3>
|
||||||
<el-table :data="tableData" stripe style="width: 100%">
|
<el-table :data="tableData" stripe style="width: 100%">
|
||||||
<el-table-column prop="resourceTitle" label="资源标题" width="200" />
|
<el-table-column prop="resourceTitle" label="资源标题"/>
|
||||||
<el-table-column prop="resourceTypeName" label="类型" width="100" />
|
<el-table-column prop="resourceTypeName" label="类型" />
|
||||||
<el-table-column label="学习时长" width="120">
|
<el-table-column label="学习时长">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatDuration(row.totalDuration) }}
|
{{ formatDuration(row.totalDuration) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="learnCount" label="学习次数" width="100" />
|
<el-table-column prop="learnCount" label="学习次数" />
|
||||||
<el-table-column prop="statDate" label="统计日期" width="150">
|
<el-table-column prop="statDate" label="统计日期">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.statDate ? new Date(row.statDate).toLocaleDateString('zh-CN') : '' }}
|
{{ row.statDate ? new Date(row.statDate).toLocaleDateString('zh-CN') : '' }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="完成状态" width="100">
|
<el-table-column label="完成状态">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="row.isComplete ? 'success' : 'info'">{{ row.isComplete ? '已完成' : '学习中' }}</el-tag>
|
<el-tag :type="row.isComplete ? 'success' : 'info'">{{ row.isComplete ? '已完成' : '学习中' }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<el-pagination
|
<el-pagination
|
||||||
|
class="pagination-container"
|
||||||
v-model:current-page="currentPage"
|
v-model:current-page="currentPage"
|
||||||
v-model:page-size="pageSize"
|
v-model:page-size="pageSize"
|
||||||
:total="totalElements"
|
:total="totalElements"
|
||||||
|
|||||||
Reference in New Issue
Block a user