定时任务修正
This commit is contained in:
@@ -148,7 +148,6 @@ INSERT INTO `tb_sys_menu` VALUES
|
|||||||
('4001', 'menu_admin_banner', 'Banner管理', 'menu_admin_content_manage', '/admin/manage/content/banner', 'admin/manage/content/BannerManagementView', NULL, 1, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
|
('4001', 'menu_admin_banner', 'Banner管理', 'menu_admin_content_manage', '/admin/manage/content/banner', 'admin/manage/content/BannerManagementView', NULL, 1, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
|
||||||
('4002', 'menu_admin_tag', '标签管理', 'menu_admin_content_manage', '/admin/manage/content/tag', 'admin/manage/content/TagManagementView', NULL, 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
|
('4002', 'menu_admin_tag', '标签管理', 'menu_admin_content_manage', '/admin/manage/content/tag', 'admin/manage/content/TagManagementView', NULL, 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
|
||||||
('4003', 'menu_admin_column', '栏目管理', 'menu_admin_content_manage', '/admin/manage/content/column', 'admin/manage/content/ColumnManagementView', NULL, 3, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
|
('4003', 'menu_admin_column', '栏目管理', 'menu_admin_content_manage', '/admin/manage/content/column', 'admin/manage/content/ColumnManagementView', NULL, 3, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
|
||||||
('4004', 'menu_admin_content', '内容管理', 'menu_admin_content_manage', '/admin/manage/content/content', 'admin/manage/content/ContentManagementView', NULL, 4, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
|
|
||||||
('5000', 'menu_admin_study_manage', '学习管理', NULL, '', '', 'admin/study.svg', 5, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:52:46', NULL, 0),
|
('5000', 'menu_admin_study_manage', '学习管理', NULL, '', '', 'admin/study.svg', 5, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:52:46', NULL, 0),
|
||||||
('5002', 'menu_admin_task_manage', '任务管理', 'menu_admin_study_manage', '/admin/manage/study/task-manage', 'admin/manage/study/TaskManageView', NULL, 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
|
('5002', 'menu_admin_task_manage', '任务管理', 'menu_admin_study_manage', '/admin/manage/study/task-manage', 'admin/manage/study/TaskManageView', NULL, 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
|
||||||
('5003', 'menu_admin_study_records', '学习记录', 'menu_admin_study_manage', '/admin/manage/study/study-records', 'admin/manage/study/StudyRecordsView', NULL, 3, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
|
('5003', 'menu_admin_study_records', '学习记录', 'menu_admin_study_manage', '/admin/manage/study/study-records', 'admin/manage/study/StudyRecordsView', NULL, 3, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
|
||||||
@@ -211,7 +210,6 @@ INSERT INTO `tb_sys_menu_permission` (id, permission_id, menu_id, creator, creat
|
|||||||
('214', 'perm_news_manage', 'menu_admin_banner', '1', now()),
|
('214', 'perm_news_manage', 'menu_admin_banner', '1', now()),
|
||||||
('215', 'perm_news_manage', 'menu_admin_tag', '1', now()),
|
('215', 'perm_news_manage', 'menu_admin_tag', '1', now()),
|
||||||
('216', 'perm_news_manage', 'menu_admin_column', '1', now()),
|
('216', 'perm_news_manage', 'menu_admin_column', '1', now()),
|
||||||
('217', 'perm_news_manage', 'menu_admin_content', '1', now()),
|
|
||||||
('218', 'perm_study_manage', 'menu_admin_study_manage', '1', now()),
|
('218', 'perm_study_manage', 'menu_admin_study_manage', '1', now()),
|
||||||
('220', 'perm_study_manage', 'menu_admin_task_manage', '1', now()),
|
('220', 'perm_study_manage', 'menu_admin_task_manage', '1', now()),
|
||||||
('221', 'perm_study_manage', 'menu_admin_study_records', '1', now()),
|
('221', 'perm_study_manage', 'menu_admin_study_records', '1', now()),
|
||||||
|
|||||||
@@ -39,11 +39,11 @@ public interface ResourceService {
|
|||||||
* @description 获取资源分页(按浏览次数排序)
|
* @description 获取资源分页(按浏览次数排序)
|
||||||
* @param filter 过滤条件
|
* @param filter 过滤条件
|
||||||
* @param pageParam 分页参数
|
* @param pageParam 分页参数
|
||||||
* @return ResultDomain<TbResource> 资源分页
|
* @return ResultDomain<ResourceVO> 资源分页(包含推荐信息)
|
||||||
* @author yslg
|
* @author yslg
|
||||||
* @since 2025-10-15
|
* @since 2025-10-15
|
||||||
*/
|
*/
|
||||||
ResultDomain<TbResource> getResourcePageOrderByViewCount(TbResource filter, PageParam pageParam);
|
ResultDomain<ResourceVO> getResourcePageOrderByViewCount(TbResource filter, PageParam pageParam);
|
||||||
/**
|
/**
|
||||||
* @description 根据ID获取资源详情
|
* @description 根据ID获取资源详情
|
||||||
* @param resourceID 资源ID
|
* @param resourceID 资源ID
|
||||||
|
|||||||
@@ -97,8 +97,6 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
|
||||||
logger.debug("用户认证成功(从缓存),userId: {}", userId);
|
|
||||||
|
|
||||||
// 记录UV:以用户ID为维度,按天去重
|
// 记录UV:以用户ID为维度,按天去重
|
||||||
try {
|
try {
|
||||||
String today = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
|
String today = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
|
||||||
|
|||||||
@@ -25,14 +25,14 @@ public class ResourceVO implements Serializable{
|
|||||||
private List<TbTag> tags;
|
private List<TbTag> tags;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否推荐(用于首页展示)
|
* 是否为TOP推荐
|
||||||
*/
|
*/
|
||||||
private Boolean isRecommended;
|
private Boolean isTopRecommend;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 推荐类型(1-热门资源,2-思政资源)
|
* 是否为思政推荐
|
||||||
*/
|
*/
|
||||||
private Integer recommendType;
|
private Boolean isIdeologicalRecommend;
|
||||||
|
|
||||||
public TbResource getResource() {
|
public TbResource getResource() {
|
||||||
return resource;
|
return resource;
|
||||||
@@ -50,19 +50,19 @@ public class ResourceVO implements Serializable{
|
|||||||
this.tags = tags;
|
this.tags = tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean getIsRecommended() {
|
public Boolean getIsTopRecommend() {
|
||||||
return isRecommended;
|
return isTopRecommend;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setIsRecommended(Boolean isRecommended) {
|
public void setIsTopRecommend(Boolean isTopRecommend) {
|
||||||
this.isRecommended = isRecommended;
|
this.isTopRecommend = isTopRecommend;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Integer getRecommendType() {
|
public Boolean getIsIdeologicalRecommend() {
|
||||||
return recommendType;
|
return isIdeologicalRecommend;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRecommendType(Integer recommendType) {
|
public void setIsIdeologicalRecommend(Boolean isIdeologicalRecommend) {
|
||||||
this.recommendType = recommendType;
|
this.isIdeologicalRecommend = isIdeologicalRecommend;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import org.xyzh.common.core.page.PageParam;
|
|||||||
import org.xyzh.common.core.page.PageRequest;
|
import org.xyzh.common.core.page.PageRequest;
|
||||||
import org.xyzh.common.dto.crontab.TbCrontabTask;
|
import org.xyzh.common.dto.crontab.TbCrontabTask;
|
||||||
import org.xyzh.common.dto.crontab.TbCrontabLog;
|
import org.xyzh.common.dto.crontab.TbCrontabLog;
|
||||||
import org.xyzh.common.utils.IDUtils;
|
|
||||||
import org.xyzh.crontab.pojo.CrontabItem;
|
import org.xyzh.crontab.pojo.CrontabItem;
|
||||||
|
|
||||||
import com.alibaba.fastjson2.JSON;
|
import com.alibaba.fastjson2.JSON;
|
||||||
@@ -19,7 +18,6 @@ import com.alibaba.fastjson2.JSONObject;
|
|||||||
import org.xyzh.common.utils.spring.SpringContextUtil;
|
import org.xyzh.common.utils.spring.SpringContextUtil;
|
||||||
import org.xyzh.crontab.config.CrontabProperties;
|
import org.xyzh.crontab.config.CrontabProperties;
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
@@ -57,7 +55,6 @@ public class CrontabController {
|
|||||||
method->{
|
method->{
|
||||||
method.setClazz(null);
|
method.setClazz(null);
|
||||||
method.setExcuete_method(null);
|
method.setExcuete_method(null);
|
||||||
method.setPath(null);
|
|
||||||
}));
|
}));
|
||||||
rd.success("ok", props.getItems());
|
rd.success("ok", props.getItems());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -75,25 +72,41 @@ public class CrontabController {
|
|||||||
public ResultDomain<TbCrontabTask> createCrontab(@RequestBody TbCrontabTask crontabItem) {
|
public ResultDomain<TbCrontabTask> createCrontab(@RequestBody TbCrontabTask crontabItem) {
|
||||||
ResultDomain<TbCrontabTask> rd = new ResultDomain<>();
|
ResultDomain<TbCrontabTask> rd = new ResultDomain<>();
|
||||||
try {
|
try {
|
||||||
// 根据taskGroup和methodName查找配置并填充beanName和methodName
|
// 根据前端传入的taskGroup和methodName(都是中文显示名)查找配置
|
||||||
if (crontabItem.getBeanName() == null || crontabItem.getBeanName().isEmpty()) {
|
if (crontabItem.getTaskGroup() == null || crontabItem.getTaskGroup().isEmpty()) {
|
||||||
|
rd.fail("任务分组不能为空");
|
||||||
|
return rd;
|
||||||
|
}
|
||||||
|
if (crontabItem.getMethodName() == null || crontabItem.getMethodName().isEmpty()) {
|
||||||
|
rd.fail("方法名称不能为空");
|
||||||
|
return rd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据taskGroup和methodName查找配置
|
||||||
CrontabItem.CrontabMethod method = findMethodByTaskGroupAndMethodName(
|
CrontabItem.CrontabMethod method = findMethodByTaskGroupAndMethodName(
|
||||||
crontabItem.getTaskGroup(),
|
crontabItem.getTaskGroup(),
|
||||||
crontabItem.getMethodName()
|
crontabItem.getMethodName()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (method != null) {
|
if (method != null) {
|
||||||
crontabItem.setBeanName(method.getClazz()); // 设置Bean名称
|
// 填充beanName和实际的Java方法名
|
||||||
crontabItem.setMethodName(method.getExcuete_method()); // 设置执行方法名
|
crontabItem.setBeanName(method.getClazz());
|
||||||
|
crontabItem.setMethodName(method.getExcuete_method());
|
||||||
|
|
||||||
|
// 将scriptPath添加到methodParams中
|
||||||
JSONObject methodParams = JSON.parseObject(crontabItem.getMethodParams());
|
JSONObject methodParams = JSON.parseObject(crontabItem.getMethodParams());
|
||||||
methodParams.put("scriptPath", method.getPath());
|
methodParams.put("scriptPath", method.getPath());
|
||||||
crontabItem.setMethodParams(methodParams.toJSONString());
|
crontabItem.setMethodParams(methodParams.toJSONString());
|
||||||
|
|
||||||
|
logger.info("创建任务 - taskGroup: {}, methodName: {}, beanName: {}, excuete_method: {}, scriptPath: {}",
|
||||||
|
crontabItem.getTaskGroup(), method.getName(), method.getClazz(),
|
||||||
|
method.getExcuete_method(), method.getPath());
|
||||||
} else {
|
} else {
|
||||||
rd.fail("未找到对应的配置: taskGroup=" + crontabItem.getTaskGroup()
|
rd.fail("未找到对应的配置: taskGroup=" + crontabItem.getTaskGroup()
|
||||||
+ ", methodName=" + crontabItem.getMethodName());
|
+ ", methodName=" + crontabItem.getMethodName());
|
||||||
return rd;
|
return rd;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return crontabService.createTask(crontabItem);
|
return crontabService.createTask(crontabItem);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("创建定时任务失败", e);
|
logger.error("创建定时任务失败", e);
|
||||||
@@ -132,21 +145,46 @@ public class CrontabController {
|
|||||||
public ResultDomain<TbCrontabTask> updateCrontab(@RequestBody TbCrontabTask crontabItem) {
|
public ResultDomain<TbCrontabTask> updateCrontab(@RequestBody TbCrontabTask crontabItem) {
|
||||||
ResultDomain<TbCrontabTask> rd = new ResultDomain<>();
|
ResultDomain<TbCrontabTask> rd = new ResultDomain<>();
|
||||||
try {
|
try {
|
||||||
// 根据taskGroup和methodName查找配置并填充beanName和methodName
|
// 确保id字段正确传递(用于数据库更新)
|
||||||
if (crontabItem.getBeanName() == null || crontabItem.getBeanName().isEmpty()) {
|
if (crontabItem.getTaskId() != null && crontabItem.getID() == null) {
|
||||||
|
crontabItem.setID(crontabItem.getTaskId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据前端传入的taskGroup和methodName(都是中文显示名)查找配置
|
||||||
|
if (crontabItem.getTaskGroup() == null || crontabItem.getTaskGroup().isEmpty()) {
|
||||||
|
rd.fail("任务分组不能为空");
|
||||||
|
return rd;
|
||||||
|
}
|
||||||
|
if (crontabItem.getMethodName() == null || crontabItem.getMethodName().isEmpty()) {
|
||||||
|
rd.fail("方法名称不能为空");
|
||||||
|
return rd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据taskGroup和methodName查找配置
|
||||||
CrontabItem.CrontabMethod method = findMethodByTaskGroupAndMethodName(
|
CrontabItem.CrontabMethod method = findMethodByTaskGroupAndMethodName(
|
||||||
crontabItem.getTaskGroup(),
|
crontabItem.getTaskGroup(),
|
||||||
crontabItem.getMethodName()
|
crontabItem.getMethodName()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (method != null) {
|
if (method != null) {
|
||||||
crontabItem.setBeanName(method.getClazz()); // 设置Bean名称
|
// 填充beanName和实际的Java方法名
|
||||||
crontabItem.setMethodName(method.getExcuete_method()); // 设置执行方法名
|
crontabItem.setBeanName(method.getClazz());
|
||||||
|
crontabItem.setMethodName(method.getExcuete_method());
|
||||||
|
|
||||||
|
// 将scriptPath添加到methodParams中
|
||||||
|
JSONObject methodParams = JSON.parseObject(crontabItem.getMethodParams());
|
||||||
|
methodParams.put("scriptPath", method.getPath());
|
||||||
|
crontabItem.setMethodParams(methodParams.toJSONString());
|
||||||
|
|
||||||
|
logger.info("更新任务 - id: {}, taskGroup: {}, methodName: {}, beanName: {}, excuete_method: {}, scriptPath: {}",
|
||||||
|
crontabItem.getID(), crontabItem.getTaskGroup(), method.getName(), method.getClazz(),
|
||||||
|
method.getExcuete_method(), method.getPath());
|
||||||
} else {
|
} else {
|
||||||
rd.fail("未找到对应的配置: taskGroup=" + crontabItem.getTaskGroup()
|
rd.fail("未找到对应的配置: taskGroup=" + crontabItem.getTaskGroup()
|
||||||
+ ", methodName=" + crontabItem.getMethodName());
|
+ ", methodName=" + crontabItem.getMethodName());
|
||||||
return rd;
|
return rd;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return crontabService.updateTask(crontabItem);
|
return crontabService.updateTask(crontabItem);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("更新定时任务失败", e);
|
logger.error("更新定时任务失败", e);
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ import org.xyzh.crontab.mapper.CrontabTaskMapper;
|
|||||||
import org.xyzh.crontab.mapper.CrontabLogMapper;
|
import org.xyzh.crontab.mapper.CrontabLogMapper;
|
||||||
import org.xyzh.crontab.scheduler.SchedulerManager;
|
import org.xyzh.crontab.scheduler.SchedulerManager;
|
||||||
import org.xyzh.api.system.permission.ResourcePermissionService;
|
import org.xyzh.api.system.permission.ResourcePermissionService;
|
||||||
import org.xyzh.common.dto.user.TbSysUser;
|
|
||||||
import org.xyzh.common.dto.user.TbSysUserDeptRole;
|
|
||||||
import org.xyzh.common.core.enums.ResourceType;
|
import org.xyzh.common.core.enums.ResourceType;
|
||||||
import org.xyzh.system.utils.LoginUtil;
|
import org.xyzh.system.utils.LoginUtil;
|
||||||
|
|
||||||
@@ -122,7 +120,7 @@ public class CrontabServiceImpl implements CrontabService {
|
|||||||
public ResultDomain<TbCrontabTask> updateTask(TbCrontabTask task) {
|
public ResultDomain<TbCrontabTask> updateTask(TbCrontabTask task) {
|
||||||
ResultDomain<TbCrontabTask> resultDomain = new ResultDomain<>();
|
ResultDomain<TbCrontabTask> resultDomain = new ResultDomain<>();
|
||||||
try {
|
try {
|
||||||
if (task.getID() == null) {
|
if (task.getTaskId() == null) {
|
||||||
resultDomain.fail("任务ID不能为空");
|
resultDomain.fail("任务ID不能为空");
|
||||||
return resultDomain;
|
return resultDomain;
|
||||||
}
|
}
|
||||||
@@ -140,7 +138,7 @@ public class CrontabServiceImpl implements CrontabService {
|
|||||||
logger.info("更新定时任务成功: {}", task.getTaskName());
|
logger.info("更新定时任务成功: {}", task.getTaskName());
|
||||||
|
|
||||||
// 重新调度任务
|
// 重新调度任务
|
||||||
TbCrontabTask dbTask = taskMapper.selectTaskById(task.getID());
|
TbCrontabTask dbTask = taskMapper.selectTaskById(task.getTaskId());
|
||||||
if (dbTask != null && dbTask.getStatus() == 1) {
|
if (dbTask != null && dbTask.getStatus() == 1) {
|
||||||
schedulerManager.rescheduleTask(dbTask);
|
schedulerManager.rescheduleTask(dbTask);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,10 +52,10 @@ public class ResourceController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取资源分页(按浏览次数排序)
|
* 获取资源分页(按浏览次数排序,包含推荐信息)
|
||||||
*/
|
*/
|
||||||
@PostMapping("/page/view-count")
|
@PostMapping("/page/view-count")
|
||||||
public ResultDomain<TbResource> getResourcePageOrderByViewCount(@RequestBody PageRequest<TbResource> request) {
|
public ResultDomain<ResourceVO> getResourcePageOrderByViewCount(@RequestBody PageRequest<TbResource> request) {
|
||||||
TbResource filter = request.getFilter();
|
TbResource filter = request.getFilter();
|
||||||
PageParam pageParam = request.getPageParam();
|
PageParam pageParam = request.getPageParam();
|
||||||
return resourceService.getResourcePageOrderByViewCount(filter, pageParam);
|
return resourceService.getResourcePageOrderByViewCount(filter, pageParam);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import org.apache.ibatis.annotations.Mapper;
|
|||||||
import org.apache.ibatis.annotations.Param;
|
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.UserDeptRoleVO;
|
import org.xyzh.common.vo.UserDeptRoleVO;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -166,16 +167,15 @@ public interface ResourceMapper extends BaseMapper<TbResource> {
|
|||||||
List<TbResource> selectResourcesPage(@Param("filter") TbResource filter, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
|
List<TbResource> selectResourcesPage(@Param("filter") TbResource filter, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 分页查询资源
|
* @description 获取资源分页(按浏览次数排序,包含推荐信息)
|
||||||
* @param filter 过滤条件
|
* @param filter 过滤条件
|
||||||
* @param pageParam 分页参数
|
* @param pageParam 分页参数
|
||||||
* @param userDeptRoles 用户部门角色列表
|
* @param userDeptRoles 用户部门角色列表
|
||||||
* @return List<TbResource> 资源列表
|
* @return List<ResourceVO> 资源VO列表(包含推荐信息)
|
||||||
* @author yslg
|
* @author yslg
|
||||||
* @since 2025-10-15
|
* @since 2025-10-15
|
||||||
*/
|
*/
|
||||||
List<TbResource> selectResourcesPageOrderByViewCount(@Param("filter") TbResource filter, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
|
List<ResourceVO> selectResourcesPageOrderByViewCount(@Param("filter") TbResource filter, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 统计资源总数
|
* @description 统计资源总数
|
||||||
|
|||||||
@@ -96,19 +96,21 @@ public class NCResourceServiceImpl implements ResourceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResultDomain<TbResource> getResourcePageOrderByViewCount(TbResource filter, PageParam pageParam) {
|
public ResultDomain<ResourceVO> getResourcePageOrderByViewCount(TbResource filter, PageParam pageParam) {
|
||||||
ResultDomain<TbResource> resultDomain = new ResultDomain<>();
|
ResultDomain<ResourceVO> resultDomain = new ResultDomain<>();
|
||||||
try {
|
try {
|
||||||
if (filter == null) {
|
if (filter == null) {
|
||||||
filter = new TbResource();
|
filter = new TbResource();
|
||||||
}
|
}
|
||||||
// 获取当前用户的部门角色
|
// 获取当前用户的部门角色
|
||||||
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
|
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
|
||||||
List<TbResource> list = resourceMapper.selectResourcesPageOrderByViewCount(filter, pageParam, userDeptRoles);
|
// 直接查询ResourceVO列表,SQL已经LEFT JOIN了recommend表
|
||||||
|
List<ResourceVO> resourceVOList = resourceMapper.selectResourcesPageOrderByViewCount(filter, pageParam, userDeptRoles);
|
||||||
long total = resourceMapper.countResources(filter, userDeptRoles);
|
long total = resourceMapper.countResources(filter, userDeptRoles);
|
||||||
pageParam.setTotalElements(total);
|
pageParam.setTotalElements(total);
|
||||||
pageParam.setTotalPages((int) Math.ceil((double) total / pageParam.getPageSize()));
|
pageParam.setTotalPages((int) Math.ceil((double) total / pageParam.getPageSize()));
|
||||||
resultDomain.success("获取资源分页(按浏览次数排序)成功", new PageDomain<TbResource>(pageParam, list));
|
|
||||||
|
resultDomain.success("获取资源分页(按浏览次数排序)成功", new PageDomain<ResourceVO>(pageParam, resourceVOList));
|
||||||
return resultDomain;
|
return resultDomain;
|
||||||
}
|
}
|
||||||
catch (Exception e) {
|
catch (Exception e) {
|
||||||
|
|||||||
@@ -336,9 +336,44 @@
|
|||||||
LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset}
|
LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectResourcesPageOrderByViewCount" resultMap="BaseResultMap">
|
<!-- ResourceVO结果映射(包含推荐信息) -->
|
||||||
SELECT DISTINCT r.*
|
<resultMap id="ResourceVOResultMap" type="org.xyzh.common.vo.ResourceVO">
|
||||||
|
<result column="is_top_recommend" property="isTopRecommend" jdbcType="BOOLEAN"/>
|
||||||
|
<result column="is_ideological_recommend" property="isIdeologicalRecommend" jdbcType="BOOLEAN"/>
|
||||||
|
<association property="resource" javaType="org.xyzh.common.dto.resource.TbResource">
|
||||||
|
<id column="id" property="id" jdbcType="VARCHAR"/>
|
||||||
|
<result column="resource_id" property="resourceID" jdbcType="VARCHAR"/>
|
||||||
|
<result column="title" property="title" jdbcType="VARCHAR"/>
|
||||||
|
<result column="content" property="content" jdbcType="LONGVARCHAR"/>
|
||||||
|
<result column="summary" property="summary" jdbcType="VARCHAR"/>
|
||||||
|
<result column="cover_image" property="coverImage" jdbcType="VARCHAR"/>
|
||||||
|
<result column="tag_id" property="tagID" jdbcType="VARCHAR"/>
|
||||||
|
<result column="author" property="author" jdbcType="VARCHAR"/>
|
||||||
|
<result column="source" property="source" jdbcType="VARCHAR"/>
|
||||||
|
<result column="source_url" property="sourceUrl" jdbcType="VARCHAR"/>
|
||||||
|
<result column="view_count" property="viewCount" jdbcType="INTEGER"/>
|
||||||
|
<result column="like_count" property="likeCount" jdbcType="INTEGER"/>
|
||||||
|
<result column="collect_count" property="collectCount" jdbcType="INTEGER"/>
|
||||||
|
<result column="status" property="status" jdbcType="INTEGER"/>
|
||||||
|
<result column="is_recommend" property="isRecommend" jdbcType="BOOLEAN"/>
|
||||||
|
<result column="is_banner" property="isBanner" jdbcType="BOOLEAN"/>
|
||||||
|
<result column="publish_time" property="publishTime" jdbcType="TIMESTAMP"/>
|
||||||
|
<result column="creator" property="creator" jdbcType="VARCHAR"/>
|
||||||
|
<result column="updater" property="updater" jdbcType="VARCHAR"/>
|
||||||
|
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
|
||||||
|
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
|
||||||
|
<result column="delete_time" property="deleteTime" jdbcType="TIMESTAMP"/>
|
||||||
|
<result column="deleted" property="deleted" jdbcType="BOOLEAN"/>
|
||||||
|
</association>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<select id="selectResourcesPageOrderByViewCount" resultMap="ResourceVOResultMap">
|
||||||
|
SELECT
|
||||||
|
r.*,
|
||||||
|
MAX(CASE WHEN rec.recommend_type = 1 THEN 1 ELSE 0 END) AS is_top_recommend,
|
||||||
|
MAX(CASE WHEN rec.recommend_type = 2 THEN 1 ELSE 0 END) AS is_ideological_recommend
|
||||||
FROM tb_resource r
|
FROM tb_resource r
|
||||||
|
LEFT JOIN tb_resource_recommend rec ON r.resource_id = rec.resource_id AND rec.deleted = 0
|
||||||
<include refid="Permission_Filter"/>
|
<include refid="Permission_Filter"/>
|
||||||
WHERE r.deleted = 0
|
WHERE r.deleted = 0
|
||||||
<if test="filter.title != null and filter.title != ''">
|
<if test="filter.title != null and filter.title != ''">
|
||||||
@@ -359,6 +394,10 @@
|
|||||||
<if test="filter.isBanner != null">
|
<if test="filter.isBanner != null">
|
||||||
AND r.is_banner = #{filter.isBanner}
|
AND r.is_banner = #{filter.isBanner}
|
||||||
</if>
|
</if>
|
||||||
|
GROUP BY r.id, r.resource_id, r.title, r.content, r.summary, r.cover_image, r.tag_id,
|
||||||
|
r.author, r.source, r.source_url, r.view_count, r.like_count, r.collect_count,
|
||||||
|
r.status, r.is_recommend, r.is_banner, r.publish_time, r.creator, r.updater,
|
||||||
|
r.create_time, r.update_time, r.delete_time, r.deleted
|
||||||
ORDER BY r.view_count DESC, r.publish_time DESC, r.create_time DESC
|
ORDER BY r.view_count DESC, r.publish_time DESC, r.create_time DESC
|
||||||
LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset}
|
LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -39,6 +39,20 @@ export const resourceApi = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取资源分页列表(按浏览次数排序,包含推荐信息)
|
||||||
|
* @param filter 筛选条件
|
||||||
|
* @param pageParam 分页参数
|
||||||
|
* @returns Promise<ResultDomain<ResourceVO>>
|
||||||
|
*/
|
||||||
|
async getResourcePageOrderByViewCount(pageParam: PageParam, filter?: ResourceSearchParams): Promise<ResultDomain<ResourceVO>> {
|
||||||
|
const response = await api.post<ResourceVO>('/news/resources/page/view-count', {
|
||||||
|
pageParam,
|
||||||
|
filter,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据ID获取资源详情
|
* 根据ID获取资源详情
|
||||||
* @param resourceID 资源ID
|
* @param resourceID 资源ID
|
||||||
|
|||||||
@@ -151,6 +151,10 @@ export interface Tag extends BaseDTO {
|
|||||||
export interface ResourceVO extends BaseDTO{
|
export interface ResourceVO extends BaseDTO{
|
||||||
resource: Resource;
|
resource: Resource;
|
||||||
tags: Tag[];
|
tags: Tag[];
|
||||||
|
/** 是否为TOP推荐 */
|
||||||
|
isTopRecommend?: boolean;
|
||||||
|
/** 是否为思政推荐 */
|
||||||
|
isIdeologicalRecommend?: boolean;
|
||||||
}
|
}
|
||||||
export interface tagVO extends BaseDTO{
|
export interface tagVO extends BaseDTO{
|
||||||
/** 标签ID */
|
/** 标签ID */
|
||||||
|
|||||||
@@ -59,11 +59,12 @@
|
|||||||
<div class="stat-views">浏览量</div>
|
<div class="stat-views">浏览量</div>
|
||||||
<div class="stat-likes">点赞数</div>
|
<div class="stat-likes">点赞数</div>
|
||||||
<div class="stat-time">发布时间</div>
|
<div class="stat-time">发布时间</div>
|
||||||
|
<div class="stat-actions">操作</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="(article, index) in hotArticles"
|
v-for="(article, index) in hotArticles"
|
||||||
:key="article.resourceID"
|
:key="article.resource?.resourceID"
|
||||||
class="hot-article-row"
|
class="hot-article-row"
|
||||||
:class="{ 'top-three': index < 3 }"
|
:class="{ 'top-three': index < 3 }"
|
||||||
>
|
>
|
||||||
@@ -73,28 +74,50 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-title">
|
<div class="stat-title">
|
||||||
<span class="article-title" :title="article.title">{{ article.title }}</span>
|
<span class="article-title" :title="article.resource?.title">{{ article.resource?.title }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-tag">
|
<div class="stat-tag">
|
||||||
<span v-if="article.tagID" class="tag-name">{{ getTagName(article.tagID) }}</span>
|
<span v-if="article.resource?.tagID" class="tag-name">{{ getTagName(article.resource.tagID) }}</span>
|
||||||
<span v-else class="tag-empty">-</span>
|
<span v-else class="tag-empty">-</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-author">
|
<div class="stat-author">
|
||||||
<span>{{ article.author || '-' }}</span>
|
<span>{{ article.resource?.author || '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-views">
|
<div class="stat-views">
|
||||||
<div class="stat-number hot">
|
<div class="stat-number hot">
|
||||||
<img src="@/assets/imgs/hot.svg" alt="浏览" class="stat-icon" />
|
<img src="@/assets/imgs/hot.svg" alt="浏览" class="stat-icon" />
|
||||||
{{ formatNumber(article.viewCount) }}
|
{{ formatNumber(article.resource?.viewCount) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-likes">
|
<div class="stat-likes">
|
||||||
<div class="stat-number">
|
<div class="stat-number">
|
||||||
{{ formatNumber(article.likeCount) }}
|
{{ formatNumber(article.resource?.likeCount) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-time">
|
<div class="stat-time">
|
||||||
<span class="time-text">{{ formatDate(article.publishTime) }}</span>
|
<span class="time-text">{{ formatDate(article.resource?.publishTime) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-actions">
|
||||||
|
<button
|
||||||
|
v-if="article.resource?.resourceID && !article.isTopRecommend"
|
||||||
|
class="add-to-top-btn"
|
||||||
|
@click="addToTopRecommends(article.resource.resourceID)"
|
||||||
|
:disabled="!!article.resource?.resourceID && addingToTop.has(article.resource.resourceID)"
|
||||||
|
>
|
||||||
|
{{ article.resource?.resourceID && addingToTop.has(article.resource.resourceID) ? '添加中...' : '添加至TOP' }}
|
||||||
|
</button>
|
||||||
|
<span v-else-if="article.isTopRecommend" class="already-top">已在TOP</span>
|
||||||
|
|
||||||
|
<!-- 添加至思政 -->
|
||||||
|
<button
|
||||||
|
v-if="article.resource?.resourceID && !article.isIdeologicalRecommend"
|
||||||
|
class="add-to-ideological-btn"
|
||||||
|
@click="addToIdeologicalRecommends(article.resource.resourceID)"
|
||||||
|
:disabled="!!article.resource?.resourceID && addingToIdeological.has(article.resource.resourceID)"
|
||||||
|
>
|
||||||
|
{{ article.resource?.resourceID && addingToIdeological.has(article.resource.resourceID) ? '添加中...' : '添加至思政' }}
|
||||||
|
</button>
|
||||||
|
<span v-else-if="article.isIdeologicalRecommend" class="already-ideological">已在思政</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -338,7 +361,7 @@ import { ElDialog, ElInput, ElButton, ElCheckbox, ElIcon, ElForm, ElFormItem, El
|
|||||||
import { Search } from '@element-plus/icons-vue';
|
import { Search } from '@element-plus/icons-vue';
|
||||||
import { AdminLayout } from '@/views/admin';
|
import { AdminLayout } from '@/views/admin';
|
||||||
import { resourceRecommendApi, resourceApi, resourceTagApi } from '@/apis/resource';
|
import { resourceRecommendApi, resourceApi, resourceTagApi } from '@/apis/resource';
|
||||||
import type { ResourceRecommendVO, Resource, Tag } from '@/types';
|
import type { ResourceRecommendVO, Resource, ResourceVO, Tag } from '@/types';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'ColumnManagementView'
|
name: 'ColumnManagementView'
|
||||||
@@ -356,9 +379,15 @@ const topRecommends = ref<ResourceRecommendVO[]>([]);
|
|||||||
const ideologicalRecommends = ref<ResourceRecommendVO[]>([]);
|
const ideologicalRecommends = ref<ResourceRecommendVO[]>([]);
|
||||||
|
|
||||||
// 热门文章列表
|
// 热门文章列表
|
||||||
const hotArticles = ref<Resource[]>([]);
|
const hotArticles = ref<ResourceVO[]>([]);
|
||||||
const hotArticleLimit = ref<number>(20);
|
const hotArticleLimit = ref<number>(20);
|
||||||
|
|
||||||
|
// 添加至TOP的加载状态
|
||||||
|
const addingToTop = ref<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// 添加至思政的加载状态
|
||||||
|
const addingToIdeological = ref<Set<string>>(new Set());
|
||||||
|
|
||||||
// 标签列表
|
// 标签列表
|
||||||
const tags = ref<Tag[]>([]);
|
const tags = ref<Tag[]>([]);
|
||||||
|
|
||||||
@@ -381,7 +410,7 @@ const editForm = ref({
|
|||||||
orderNum: 1
|
orderNum: 1
|
||||||
});
|
});
|
||||||
|
|
||||||
// 挂载时加载标签和当前tab的数据
|
// 挂载时加载标签数据和默认tab的数据
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadTags();
|
loadTags();
|
||||||
loadTabData(activeTab.value);
|
loadTabData(activeTab.value);
|
||||||
@@ -445,7 +474,7 @@ async function loadHotArticles() {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
// 获取已发布的文章,按浏览量排序
|
// 获取已发布的文章,按浏览量排序
|
||||||
const result = await resourceApi.getResourcePage(
|
const result = await resourceApi.getResourcePageOrderByViewCount(
|
||||||
{
|
{
|
||||||
pageNumber: 1,
|
pageNumber: 1,
|
||||||
pageSize: hotArticleLimit.value
|
pageSize: hotArticleLimit.value
|
||||||
@@ -458,7 +487,7 @@ async function loadHotArticles() {
|
|||||||
if (result.success && result.pageDomain?.dataList) {
|
if (result.success && result.pageDomain?.dataList) {
|
||||||
// 按浏览量降序排序
|
// 按浏览量降序排序
|
||||||
hotArticles.value = result.pageDomain.dataList
|
hotArticles.value = result.pageDomain.dataList
|
||||||
.sort((a, b) => (b.viewCount || 0) - (a.viewCount || 0));
|
.sort((a, b) => (b.resource?.viewCount || 0) - (a.resource?.viewCount || 0));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载热门文章失败:', error);
|
console.error('加载热门文章失败:', error);
|
||||||
@@ -510,7 +539,7 @@ async function showAddResourceDialog(recommendType: 1 | 2) {
|
|||||||
async function searchResources() {
|
async function searchResources() {
|
||||||
searchLoading.value = true;
|
searchLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await resourceApi.getResourcePage(
|
const result = await resourceApi.getResourcePageOrderByViewCount(
|
||||||
{ pageNumber: 1, pageSize: 50 },
|
{ pageNumber: 1, pageSize: 50 },
|
||||||
{
|
{
|
||||||
keyword: searchKeyword.value,
|
keyword: searchKeyword.value,
|
||||||
@@ -524,9 +553,9 @@ async function searchResources() {
|
|||||||
? topRecommends.value.map(r => r.resourceID)
|
? topRecommends.value.map(r => r.resourceID)
|
||||||
: ideologicalRecommends.value.map(r => r.resourceID);
|
: ideologicalRecommends.value.map(r => r.resourceID);
|
||||||
|
|
||||||
availableResources.value = result.pageDomain.dataList.filter(
|
availableResources.value = result.pageDomain.dataList
|
||||||
r => !existingIds.includes(r.resourceID)
|
.map(vo => vo.resource)
|
||||||
);
|
.filter(r => r && !existingIds.includes(r.resourceID)) as Resource[];
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('搜索资源失败:', error);
|
console.error('搜索资源失败:', error);
|
||||||
@@ -655,6 +684,79 @@ async function deleteRecommend(item: ResourceRecommendVO) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查资源是否已在TOP推荐中
|
||||||
|
function isInTopRecommends(resourceID?: string): boolean {
|
||||||
|
if (!resourceID) return false;
|
||||||
|
return topRecommends.value.some(r => r.resourceID === resourceID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查资源是否已在思政推荐中
|
||||||
|
function isInIdeologicalRecommends(resourceID?: string): boolean {
|
||||||
|
if (!resourceID) return false;
|
||||||
|
return ideologicalRecommends.value.some(r => r.resourceID === resourceID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据ResourceVO获取resourceID
|
||||||
|
function getResourceID(articleVO: ResourceVO): string | undefined {
|
||||||
|
return articleVO.resource?.resourceID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加至TOP推荐
|
||||||
|
async function addToTopRecommends(resourceID?: string) {
|
||||||
|
if (!resourceID) return;
|
||||||
|
|
||||||
|
addingToTop.value.add(resourceID);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await resourceRecommendApi.batchAddRecommends([resourceID], 1);
|
||||||
|
ElMessage.success('添加至TOP推荐成功');
|
||||||
|
|
||||||
|
// 重新加载TOP推荐列表
|
||||||
|
loadedTabs.value.delete('top-resources');
|
||||||
|
const topResult = await resourceRecommendApi.getRecommendsByType(1);
|
||||||
|
if (topResult.success && topResult.dataList) {
|
||||||
|
topRecommends.value = topResult.dataList.sort((a, b) => (a.orderNum || 0) - (b.orderNum || 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新加载热门文章统计以更新按钮状态
|
||||||
|
loadedTabs.value.delete('hot-articles');
|
||||||
|
await loadHotArticles();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('添加至TOP推荐失败:', error);
|
||||||
|
ElMessage.error('添加至TOP推荐失败');
|
||||||
|
} finally {
|
||||||
|
addingToTop.value.delete(resourceID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加至思政推荐
|
||||||
|
async function addToIdeologicalRecommends(resourceID?: string) {
|
||||||
|
if (!resourceID) return;
|
||||||
|
|
||||||
|
addingToIdeological.value.add(resourceID);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await resourceRecommendApi.batchAddRecommends([resourceID], 2);
|
||||||
|
ElMessage.success('添加至思政推荐成功');
|
||||||
|
|
||||||
|
// 重新加载思政推荐列表
|
||||||
|
loadedTabs.value.delete('ideological');
|
||||||
|
const ideologicalResult = await resourceRecommendApi.getRecommendsByType(2);
|
||||||
|
if (ideologicalResult.success && ideologicalResult.dataList) {
|
||||||
|
ideologicalRecommends.value = ideologicalResult.dataList.sort((a, b) => (a.orderNum || 0) - (b.orderNum || 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新加载热门文章统计以更新按钮状态
|
||||||
|
loadedTabs.value.delete('hot-articles');
|
||||||
|
await loadHotArticles();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('添加至思政推荐失败:', error);
|
||||||
|
ElMessage.error('添加至思政推荐失败');
|
||||||
|
} finally {
|
||||||
|
addingToIdeological.value.delete(resourceID);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -906,6 +1008,7 @@ async function deleteRecommend(item: ResourceRecommendVO) {
|
|||||||
.filter-controls {
|
.filter-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 热门文章列表
|
// 热门文章列表
|
||||||
@@ -918,7 +1021,7 @@ async function deleteRecommend(item: ResourceRecommendVO) {
|
|||||||
|
|
||||||
.statistics-header {
|
.statistics-header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 80px 1fr 120px 120px 120px 100px 120px;
|
grid-template-columns: 80px 1fr 120px 120px 120px 100px 120px 150px;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
background: #F9FAFB;
|
background: #F9FAFB;
|
||||||
@@ -931,7 +1034,7 @@ async function deleteRecommend(item: ResourceRecommendVO) {
|
|||||||
|
|
||||||
.hot-article-row {
|
.hot-article-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 80px 1fr 120px 120px 120px 100px 120px;
|
grid-template-columns: 80px 1fr 120px 120px 120px 100px 120px 150px;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
@@ -952,6 +1055,13 @@ async function deleteRecommend(item: ResourceRecommendVO) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stat-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.rank-badge {
|
.rank-badge {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1035,6 +1145,62 @@ async function deleteRecommend(item: ResourceRecommendVO) {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #6B7280;
|
color: #6B7280;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.add-to-top-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: #E7000B;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #FFFFFF;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: #c00009;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.already-top {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #10B981;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-to-ideological-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: #E7000B;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #FFFFFF;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: #c00009;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.already-ideological {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #10B981;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对话框样式
|
// 对话框样式
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
<template>
|
|
||||||
<AdminLayout
|
|
||||||
title="内容管理"
|
|
||||||
subtitle="管理网站横幅、栏目、标签等内容信息"
|
|
||||||
>
|
|
||||||
<div class="content-management">
|
|
||||||
<el-empty description="请使用顶部标签页切换到Banner管理、标签管理或栏目管理" />
|
|
||||||
</div>
|
|
||||||
</AdminLayout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { AdminLayout } from '@/views/admin';
|
|
||||||
|
|
||||||
defineOptions({
|
|
||||||
name: 'ContentManagementView'
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.content-management {
|
|
||||||
background: #FFFFFF;
|
|
||||||
padding: 24px;
|
|
||||||
border-radius: 14px;
|
|
||||||
min-height: 400px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -362,8 +362,10 @@ const formData = reactive<Partial<CrontabTask>>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 监听模板选择变化
|
// 监听模板选择变化
|
||||||
watch(selectedTemplate, (newTemplate) => {
|
watch(selectedTemplate, (newTemplate, oldTemplate) => {
|
||||||
if (newTemplate) {
|
// 只在用户手动切换模板时重置(oldTemplate存在且不为null时才重置)
|
||||||
|
// 编辑回填时oldTemplate为null,不会触发重置
|
||||||
|
if (newTemplate && oldTemplate) {
|
||||||
selectedMethod.value = null;
|
selectedMethod.value = null;
|
||||||
dynamicParams.value = {};
|
dynamicParams.value = {};
|
||||||
}
|
}
|
||||||
@@ -476,6 +478,11 @@ function handleEdit(row: CrontabTask) {
|
|||||||
isEdit.value = true;
|
isEdit.value = true;
|
||||||
Object.assign(formData, row);
|
Object.assign(formData, row);
|
||||||
|
|
||||||
|
// 重置选择
|
||||||
|
selectedTemplate.value = null;
|
||||||
|
selectedMethod.value = null;
|
||||||
|
dynamicParams.value = {};
|
||||||
|
|
||||||
// 尝试解析methodParams来回填表单
|
// 尝试解析methodParams来回填表单
|
||||||
if (row.methodParams) {
|
if (row.methodParams) {
|
||||||
try {
|
try {
|
||||||
@@ -486,13 +493,20 @@ function handleEdit(row: CrontabTask) {
|
|||||||
t.methods.some(m => m.path === params.scriptPath)
|
t.methods.some(m => m.path === params.scriptPath)
|
||||||
);
|
);
|
||||||
if (template) {
|
if (template) {
|
||||||
selectedTemplate.value = template;
|
|
||||||
const method = template.methods.find(m => m.path === params.scriptPath);
|
const method = template.methods.find(m => m.path === params.scriptPath);
|
||||||
if (method) {
|
if (method) {
|
||||||
|
// 先设置template和method,触发watch填充默认值
|
||||||
|
selectedTemplate.value = template;
|
||||||
selectedMethod.value = method;
|
selectedMethod.value = method;
|
||||||
// 回填动态参数
|
|
||||||
|
// 然后使用nextTick确保watch执行完后再覆盖为实际值
|
||||||
|
// 回填动态参数(排除scriptPath)
|
||||||
const { scriptPath, ...restParams } = params;
|
const { scriptPath, ...restParams } = params;
|
||||||
|
// 延迟设置,确保watch先执行完
|
||||||
|
setTimeout(() => {
|
||||||
dynamicParams.value = restParams;
|
dynamicParams.value = restParams;
|
||||||
|
console.log('📝 编辑回填 - template:', template.name, 'method:', method.name, 'params:', restParams);
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -645,19 +659,20 @@ async function handleSubmit() {
|
|||||||
|
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
|
// 传递taskGroup和methodName(中文名),后端根据这两个name查找配置并填充beanName、methodName和scriptPath
|
||||||
const data = {
|
const data = {
|
||||||
...formData,
|
...formData,
|
||||||
taskGroup: selectedTemplate.value.name, // 第一层name作为taskGroup
|
taskGroup: selectedTemplate.value.name, // 模板名称(中文)
|
||||||
methodName: selectedMethod.value.name, // 第二层name作为methodName
|
methodName: selectedMethod.value.name, // 方法名称(中文)
|
||||||
methodParams: JSON.stringify({
|
methodParams: JSON.stringify({
|
||||||
scriptPath: selectedMethod.value.path,
|
...dynamicParams.value // 只传用户输入的参数,scriptPath由后端填充
|
||||||
...dynamicParams.value
|
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('📤 准备提交的数据:', data);
|
console.log('📤 准备提交的数据:', data);
|
||||||
console.log('📤 taskGroup (模板名称):', data.taskGroup);
|
console.log('📤 taskGroup:', selectedTemplate.value.name);
|
||||||
console.log('📤 methodName (方法名称):', data.methodName);
|
console.log('📤 methodName:', selectedMethod.value.name);
|
||||||
|
console.log('📤 动态参数:', dynamicParams.value);
|
||||||
|
|
||||||
let result;
|
let result;
|
||||||
if (isEdit.value) {
|
if (isEdit.value) {
|
||||||
|
|||||||
@@ -70,133 +70,75 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 日志表格 -->
|
<!-- 日志表格 -->
|
||||||
<div class="log-table-wrapper">
|
<el-table
|
||||||
<table class="log-table">
|
:data="logs"
|
||||||
<thead>
|
v-loading="loading"
|
||||||
<tr>
|
style="width: 100%"
|
||||||
<th width="10%">操作人</th>
|
height="calc(100vh - 340px)"
|
||||||
<th width="10%">操作模块</th>
|
stripe
|
||||||
<th width="10%">操作类型</th>
|
>
|
||||||
<th width="10%">请求链接</th>
|
<el-table-column prop="username" label="操作人" width="120" show-overflow-tooltip>
|
||||||
<th width="10%">IP地址</th>
|
<template #default="{ row }">
|
||||||
<th width="10%">操作描述</th>
|
{{ row.username || '-' }}
|
||||||
<!-- <th width="100">耗时(ms)</th> -->
|
</template>
|
||||||
<!-- <th width="100">状态</th> -->
|
</el-table-column>
|
||||||
<th width="10%">操作时间</th>
|
|
||||||
<th width="10%">操作</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody v-if="loading">
|
|
||||||
<tr>
|
|
||||||
<td colspan="9" class="loading-cell">
|
|
||||||
<div class="loading-spinner"></div>
|
|
||||||
<span>加载中...</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
<tbody v-else-if="logs.length === 0">
|
|
||||||
<tr>
|
|
||||||
<td colspan="9" class="empty-cell">
|
|
||||||
<div class="empty-icon">📋</div>
|
|
||||||
<p>暂无操作日志</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
<tbody v-else>
|
|
||||||
<tr v-for="log in logs" :key="log.id" class="table-row">
|
|
||||||
<td>{{ log.username || '-' }}</td>
|
|
||||||
<td>{{ log.module || '-' }}</td>
|
|
||||||
<td>
|
|
||||||
<span class="status-tag" :class="getOperationClass(log.operation)">
|
|
||||||
{{ getOperationText(log.operation) }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="log-desc">
|
|
||||||
<div class="desc-text">{{ log.requestUrl || '-' }}</div>
|
|
||||||
</td>
|
|
||||||
<td>{{ log.ipAddress || '-' }}</td>
|
|
||||||
<td class="log-desc">
|
|
||||||
<div class="desc-text">{{ truncateText(log.responseData, 50) }}</div>
|
|
||||||
</td>
|
|
||||||
<!-- <td>
|
|
||||||
<span class="status-tag" :class="log.status === 'success' ? 'status-success' : 'status-failed'">
|
|
||||||
{{ log.status === 'success' ? '成功' : '失败' }}
|
|
||||||
</span>
|
|
||||||
</td> -->
|
|
||||||
<td>{{ formatTime(log?.createTime || '') || '-' }}</td>
|
|
||||||
|
|
||||||
<td class="action-cell">
|
<el-table-column prop="module" label="操作模块" width="150" show-overflow-tooltip>
|
||||||
<button class="btn-link btn-primary" @click="viewDetail(log)">详情</button>
|
<template #default="{ row }">
|
||||||
</td>
|
{{ row.module || '-' }}
|
||||||
</tr>
|
</template>
|
||||||
</tbody>
|
</el-table-column>
|
||||||
</table>
|
|
||||||
</div>
|
<el-table-column prop="operation" label="操作类型" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="status-tag" :class="getOperationClass(row.operation)">
|
||||||
|
{{ getOperationText(row.operation) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="requestUrl" label="请求链接" min-width="200" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.requestUrl || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="ipAddress" label="IP地址" width="150" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.ipAddress || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="responseData" label="操作描述" min-width="200" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ truncateText(row.responseData, 50) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="createTime" label="操作时间" width="180" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatTime(row?.createTime || '') || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="100" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link @click="viewDetail(row)">详情</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<div v-if="!loading && logs.length > 0" class="pagination">
|
<div class="pagination-container">
|
||||||
<div class="pagination-info">
|
<el-pagination
|
||||||
共 {{ total }} 条数据,每页 {{ pageSize }} 条
|
v-model:current-page="currentPage"
|
||||||
</div>
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
<div class="pagination-controls">
|
:total="total"
|
||||||
<button
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
class="page-btn"
|
@size-change="handleSizeChange"
|
||||||
:disabled="currentPage === 1"
|
@current-change="handlePageChange"
|
||||||
@click="handlePageChange(1)"
|
|
||||||
>
|
|
||||||
首页
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="page-btn"
|
|
||||||
:disabled="currentPage === 1"
|
|
||||||
@click="handlePageChange(currentPage - 1)"
|
|
||||||
>
|
|
||||||
上一页
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="page-numbers">
|
|
||||||
<button
|
|
||||||
v-for="page in displayPages"
|
|
||||||
:key="page"
|
|
||||||
class="page-number"
|
|
||||||
:class="{ active: page === currentPage }"
|
|
||||||
@click="handlePageChange(page)"
|
|
||||||
:disabled="page === -1"
|
|
||||||
>
|
|
||||||
{{ page === -1 ? '...' : page }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="page-btn"
|
|
||||||
:disabled="currentPage === totalPages"
|
|
||||||
@click="handlePageChange(currentPage + 1)"
|
|
||||||
>
|
|
||||||
下一页
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="page-btn"
|
|
||||||
:disabled="currentPage === totalPages"
|
|
||||||
@click="handlePageChange(totalPages)"
|
|
||||||
>
|
|
||||||
末页
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pagination-jump">
|
|
||||||
<span>跳转到</span>
|
|
||||||
<input
|
|
||||||
v-model.number="jumpPage"
|
|
||||||
type="number"
|
|
||||||
class="jump-input"
|
|
||||||
@keyup.enter="handleJumpPage"
|
|
||||||
/>
|
/>
|
||||||
<span>页</span>
|
|
||||||
<button class="jump-btn" @click="handleJumpPage">跳转</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -314,49 +256,9 @@ const currentPage = ref(1);
|
|||||||
const pageSize = ref(20);
|
const pageSize = ref(20);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
const logs = ref<OperationLog[]>([]);
|
const logs = ref<OperationLog[]>([]);
|
||||||
const jumpPage = ref<number>();
|
|
||||||
const detailVisible = ref(false);
|
const detailVisible = ref(false);
|
||||||
const currentLog = ref<OperationLog | null>(null);
|
const currentLog = ref<OperationLog | null>(null);
|
||||||
|
|
||||||
// 计算总页数
|
|
||||||
const totalPages = computed(() => Math.ceil(total.value / pageSize.value) || 1);
|
|
||||||
|
|
||||||
// 计算显示的页码
|
|
||||||
const displayPages = computed(() => {
|
|
||||||
const pages: number[] = [];
|
|
||||||
const maxDisplay = 7;
|
|
||||||
|
|
||||||
if (totalPages.value <= maxDisplay) {
|
|
||||||
for (let i = 1; i <= totalPages.value; i++) {
|
|
||||||
pages.push(i);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (currentPage.value <= 4) {
|
|
||||||
for (let i = 1; i <= 5; i++) {
|
|
||||||
pages.push(i);
|
|
||||||
}
|
|
||||||
pages.push(-1);
|
|
||||||
pages.push(totalPages.value);
|
|
||||||
} else if (currentPage.value >= totalPages.value - 3) {
|
|
||||||
pages.push(1);
|
|
||||||
pages.push(-1);
|
|
||||||
for (let i = totalPages.value - 4; i <= totalPages.value; i++) {
|
|
||||||
pages.push(i);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pages.push(1);
|
|
||||||
pages.push(-1);
|
|
||||||
for (let i = currentPage.value - 1; i <= currentPage.value + 1; i++) {
|
|
||||||
pages.push(i);
|
|
||||||
}
|
|
||||||
pages.push(-1);
|
|
||||||
pages.push(totalPages.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pages;
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadLogs();
|
loadLogs();
|
||||||
});
|
});
|
||||||
@@ -467,20 +369,16 @@ function viewDetail(log: OperationLog) {
|
|||||||
* 页码改变
|
* 页码改变
|
||||||
*/
|
*/
|
||||||
function handlePageChange(page: number) {
|
function handlePageChange(page: number) {
|
||||||
if (page < 1 || page > totalPages.value || page === -1) return;
|
|
||||||
currentPage.value = page;
|
currentPage.value = page;
|
||||||
loadLogs();
|
loadLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 跳转页面
|
* 每页条数改变
|
||||||
*/
|
*/
|
||||||
function handleJumpPage() {
|
function handleSizeChange(size: number) {
|
||||||
if (!jumpPage.value || jumpPage.value < 1 || jumpPage.value > totalPages.value) {
|
pageSize.value = size;
|
||||||
alert('请输入有效的页码');
|
currentPage.value = 1; // 重置到第一页
|
||||||
return;
|
|
||||||
}
|
|
||||||
currentPage.value = jumpPage.value;
|
|
||||||
loadLogs();
|
loadLogs();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -585,60 +483,7 @@ function handleJumpPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-table-wrapper {
|
// el-table会自动处理样式,不需要自定义table样式
|
||||||
background: #fff;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
|
|
||||||
th {
|
|
||||||
background: #f5f7fa;
|
|
||||||
color: #606266;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
text-align: left;
|
|
||||||
border-bottom: 2px solid #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #606266;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-row {
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #f5f7fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-desc {
|
|
||||||
max-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desc-text {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-right {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-tag {
|
.status-tag {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -678,154 +523,15 @@ function handleJumpPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-cell {
|
// el-table和el-button会自动处理样式和交互
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-link {
|
|
||||||
border: none;
|
|
||||||
padding: 6px 12px;
|
|
||||||
font-size: 13px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
border-radius: 4px;
|
|
||||||
min-width: 64px;
|
|
||||||
text-align: center;
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.btn-primary {
|
|
||||||
background: #409eff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-cell,
|
|
||||||
.empty-cell {
|
|
||||||
text-align: center;
|
|
||||||
padding: 60px 20px !important;
|
|
||||||
color: #909399;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
display: inline-block;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border: 4px solid #f3f3f3;
|
|
||||||
border-top: 4px solid #409eff;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-icon {
|
|
||||||
font-size: 48px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
padding: 20px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-info {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #606266;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-btn,
|
|
||||||
.page-number {
|
|
||||||
padding: 6px 12px;
|
|
||||||
border: 1px solid #dcdfe6;
|
|
||||||
background: #fff;
|
|
||||||
color: #606266;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
|
||||||
color: #409eff;
|
|
||||||
border-color: #409eff;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: #409eff;
|
|
||||||
color: #fff;
|
|
||||||
border-color: #409eff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-numbers {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-jump {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #606266;
|
|
||||||
|
|
||||||
.jump-input {
|
|
||||||
width: 60px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
border: 1px solid #dcdfe6;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #409eff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.jump-btn {
|
|
||||||
padding: 6px 12px;
|
|
||||||
border: 1px solid #dcdfe6;
|
|
||||||
background: #fff;
|
|
||||||
color: #606266;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #409eff;
|
|
||||||
border-color: #409eff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模态框样式
|
// 模态框样式
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
|
|||||||
Reference in New Issue
Block a user