Merge remote-tracking branch 'refs/remotes/origin/feature_风采墙' into feature_界面UI优化
This commit is contained in:
+26
-5
@@ -79,14 +79,19 @@ public class ProposalDashboardController {
|
||||
// 节点
|
||||
ProcessDefine define = processDefineService.getLastByName("JDHTA_NC");
|
||||
ProcessModel processModel = processDefineService.processDefineToModel(define);
|
||||
List<TaskModel> taskModels = processModel.getTasks();
|
||||
// 按流程图的连线关系遍历任务节点,保证看板展示顺序与流程图一致。
|
||||
List<TaskModel> taskModels = processModel.getModels(TaskModel.class);
|
||||
|
||||
List<String> excludeNodes = new ArrayList<>(3);
|
||||
excludeNodes.add("撰写提案");
|
||||
excludeNodes.add("邀请附议人");
|
||||
excludeNodes.add("提案附议");
|
||||
|
||||
List<NutMap> taskNodes = taskModels.stream().filter(taskModel -> !excludeNodes.contains(taskModel.getDisplayName()))
|
||||
// opinion_unit_reply、two_unit_reply 为临时屏蔽的承办单位答复节点,不在看板展示。
|
||||
List<String> hiddenNodeIds = List.of("opinion_unit_reply", "two_unit_reply");
|
||||
List<NutMap> taskNodes = new ArrayList<>(taskModels.stream()
|
||||
.filter(taskModel -> !excludeNodes.contains(taskModel.getDisplayName()))
|
||||
.filter(taskModel -> !hiddenNodeIds.contains(taskModel.getName()))
|
||||
.map(taskModel -> NutMap.NEW()
|
||||
.addv("name", taskModel.getDisplayName())
|
||||
.addv("id", taskModel.getName())
|
||||
@@ -94,19 +99,35 @@ public class ProposalDashboardController {
|
||||
.addv("count", 0)
|
||||
.addv("todoCount", 0)
|
||||
.addv("doneCount", 0)
|
||||
).toList();
|
||||
nodes.addAll(taskNodes);
|
||||
).toList());
|
||||
|
||||
// 立案结果
|
||||
List<Sys_dict> caseFilingResult = sysDictService.getSubListByCode("PROPOSAL_CASE_FILING_RESULT");
|
||||
List<NutMap> caseFilingResultNodes = new ArrayList<>();
|
||||
for (Sys_dict dict : caseFilingResult) {
|
||||
nodes.add(NutMap.NEW()
|
||||
caseFilingResultNodes.add(NutMap.NEW()
|
||||
.addv("name", dict.getName())
|
||||
.addv("id", dict.getCode())
|
||||
.addv("type", "caseFilingResult")
|
||||
.addv("count", 0));
|
||||
}
|
||||
|
||||
// 立案结果需展示在委员会确认承办单位之前,反馈评价统一固定到看板末尾。
|
||||
List<NutMap> feedbackNodes = taskNodes.stream()
|
||||
.filter(taskNode -> "feedback".equals(taskNode.getString("id")))
|
||||
.toList();
|
||||
taskNodes.removeIf(taskNode -> "feedback".equals(taskNode.getString("id")));
|
||||
int committeeFilingUnitIndex = taskNodes.size();
|
||||
for (int i = 0; i < taskNodes.size(); i++) {
|
||||
if ("committeeFilingUnit".equals(taskNodes.get(i).getString("id"))) {
|
||||
committeeFilingUnitIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
taskNodes.addAll(committeeFilingUnitIndex, caseFilingResultNodes);
|
||||
nodes.addAll(taskNodes);
|
||||
nodes.addAll(feedbackNodes);
|
||||
|
||||
// 查询待办任务
|
||||
Sql todoSql = Sqls.create("""
|
||||
SELECT
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.budwk.app.zhgh.spread.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.budwk.app.base.annotation.SLog;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.zhgh.spread.models.SpreadCategory;
|
||||
import com.budwk.app.zhgh.spread.service.SpreadCategoryService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
|
||||
@IocBean
|
||||
@Ok("json:full")
|
||||
@Api("风采墙-栏目管理")
|
||||
@At("/platform/spread/category")
|
||||
public class SpreadCategoryController {
|
||||
|
||||
@Inject
|
||||
private SpreadCategoryService categoryService;
|
||||
|
||||
@At("")
|
||||
@Ok("beetl:/platform/zhgh/spread/category/index.html")
|
||||
@SaCheckPermission("spread.category")
|
||||
public void index() {
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("栏目列表")
|
||||
@SaCheckPermission("spread.category")
|
||||
public Result listData() {
|
||||
return Result.success(categoryService.listCategories(false));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("栏目树")
|
||||
@SaCheckPermission("spread.category")
|
||||
public Result treeData() {
|
||||
return Result.success(categoryService.categoryTree(false));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("新增栏目")
|
||||
@SaCheckPermission("spread.category")
|
||||
@SLog(tag = "风采墙-栏目管理", msg = "新增栏目:${args[0].name}")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result doAdd(SpreadCategory category) {
|
||||
try {
|
||||
categoryService.saveCategory(category);
|
||||
return Result.success();
|
||||
} catch (RuntimeException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("编辑栏目")
|
||||
@SaCheckPermission("spread.category")
|
||||
@SLog(tag = "风采墙-栏目管理", msg = "编辑栏目:${args[0].id}")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result doEdit(SpreadCategory category) {
|
||||
try {
|
||||
categoryService.updateCategory(category);
|
||||
return Result.success();
|
||||
} catch (RuntimeException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("删除栏目")
|
||||
@SaCheckPermission("spread.category")
|
||||
@SLog(tag = "风采墙-栏目管理", msg = "删除栏目:${args[0]}")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result doDelete(String id) {
|
||||
try {
|
||||
categoryService.deleteCategory(id);
|
||||
return Result.success();
|
||||
} catch (RuntimeException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("设置栏目显示状态")
|
||||
@SaCheckPermission("spread.category")
|
||||
@SLog(tag = "风采墙-栏目管理", msg = "栏目:${args[0]},显示状态:${args[1]}")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result doVisible(String id, Integer visible) {
|
||||
try {
|
||||
categoryService.setVisible(id, visible);
|
||||
return Result.success();
|
||||
} catch (RuntimeException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.budwk.app.zhgh.spread.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.budwk.app.base.annotation.SLog;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.zhgh.spread.models.SpreadContent;
|
||||
import com.budwk.app.zhgh.spread.service.SpreadCategoryService;
|
||||
import com.budwk.app.zhgh.spread.service.SpreadContentService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
|
||||
@IocBean
|
||||
@Ok("json:full")
|
||||
@Api("风采墙-内容发布")
|
||||
@At("/platform/spread/creation")
|
||||
public class SpreadCreationController {
|
||||
|
||||
@Inject
|
||||
private SpreadCategoryService categoryService;
|
||||
|
||||
@Inject
|
||||
private SpreadContentService contentService;
|
||||
|
||||
@At("")
|
||||
@Ok("beetl:/platform/zhgh/spread/creation/index.html")
|
||||
@SaCheckPermission("spread.creation")
|
||||
public void index() {
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("可选栏目树")
|
||||
@SaCheckPermission("spread.creation")
|
||||
public Result categoryTree() {
|
||||
return Result.success(categoryService.categoryTree(true));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("当前登录人员可选择的工会")
|
||||
@SaCheckPermission("spread.creation")
|
||||
public Result unionOptions() {
|
||||
return Result.success(contentService.selectableUnions());
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("内容详情")
|
||||
@SaCheckPermission("spread.creation")
|
||||
public Result detailData(String id) {
|
||||
try {
|
||||
return Result.success(contentService.getContent(id));
|
||||
} catch (RuntimeException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("新增内容")
|
||||
@SaCheckPermission("spread.creation")
|
||||
@SLog(tag = "风采墙-内容发布", msg = "新增内容:${args[0].title}")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result doAdd(SpreadContent content) {
|
||||
try {
|
||||
contentService.saveContent(content);
|
||||
return Result.success();
|
||||
} catch (RuntimeException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("编辑内容")
|
||||
@SaCheckPermission("spread.creation")
|
||||
@SLog(tag = "风采墙-内容发布", msg = "编辑内容:${args[0].id}")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result doEdit(SpreadContent content) {
|
||||
try {
|
||||
contentService.updateContent(content);
|
||||
return Result.success();
|
||||
} catch (RuntimeException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.budwk.app.zhgh.spread.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.budwk.app.base.param.PageForm;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.zhgh.spread.service.SpreadCategoryService;
|
||||
import com.budwk.app.zhgh.spread.service.SpreadContentService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
|
||||
@IocBean
|
||||
@Ok("json:full")
|
||||
@Api("风采墙-展示")
|
||||
@At("/platform/spread/display")
|
||||
public class SpreadDisplayController {
|
||||
|
||||
@Inject
|
||||
private SpreadCategoryService categoryService;
|
||||
|
||||
@Inject
|
||||
private SpreadContentService contentService;
|
||||
|
||||
@At("")
|
||||
@Ok("beetl:/platform/zhgh/spread/display/index.html")
|
||||
@SaCheckPermission("spread.display")
|
||||
public void index() {
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("可见栏目树")
|
||||
@SaCheckPermission("spread.display")
|
||||
public Result categoryTree() {
|
||||
return Result.success(categoryService.categoryTree(true));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("已发布内容分页")
|
||||
@SaCheckPermission("spread.display")
|
||||
public Result pageData(PageForm pageForm, String categoryId) {
|
||||
return Result.success(contentService.pagePublished(pageForm, categoryId));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("内容详情")
|
||||
@SaCheckPermission("spread.display")
|
||||
public Result detailData(String id) {
|
||||
try {
|
||||
return Result.success(contentService.getContent(id));
|
||||
} catch (RuntimeException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("轮播内容")
|
||||
@SaCheckPermission("spread.display")
|
||||
public Result sliderData() {
|
||||
return Result.success(contentService.sliderContent());
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("增加浏览量")
|
||||
@SaCheckPermission("spread.display")
|
||||
public Result addView(String id) {
|
||||
try {
|
||||
contentService.addViewCount(id);
|
||||
return Result.success();
|
||||
} catch (RuntimeException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.budwk.app.zhgh.spread.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.budwk.app.base.annotation.SLog;
|
||||
import com.budwk.app.base.param.PageForm;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.zhgh.spread.param.SpreadContentQuery;
|
||||
import com.budwk.app.zhgh.spread.service.SpreadCategoryService;
|
||||
import com.budwk.app.zhgh.spread.service.SpreadContentService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
|
||||
@IocBean
|
||||
@Ok("json:full")
|
||||
@Api("风采墙-内容管理")
|
||||
@At("/platform/spread/manage")
|
||||
public class SpreadManageController {
|
||||
|
||||
@Inject
|
||||
private SpreadCategoryService categoryService;
|
||||
|
||||
@Inject
|
||||
private SpreadContentService contentService;
|
||||
|
||||
@At("")
|
||||
@Ok("beetl:/platform/zhgh/spread/manage/index.html")
|
||||
@SaCheckPermission("spread.manage")
|
||||
public void index() {
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("内容分页")
|
||||
@SaCheckPermission("spread.manage")
|
||||
public Result pageData(PageForm pageForm, SpreadContentQuery query) {
|
||||
return Result.success(contentService.pageContent(pageForm, query));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("栏目树")
|
||||
@SaCheckPermission("spread.manage")
|
||||
public Result categoryTree() {
|
||||
return Result.success(categoryService.categoryTree(false));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("删除内容")
|
||||
@SaCheckPermission("spread.manage")
|
||||
@SLog(tag = "风采墙-内容管理", msg = "删除内容:${args[0]}")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result doDelete(String id) {
|
||||
try {
|
||||
contentService.deleteContent(id);
|
||||
return Result.success();
|
||||
} catch (RuntimeException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("发布或撤回内容")
|
||||
@SaCheckPermission("spread.manage")
|
||||
@SLog(tag = "风采墙-内容管理", msg = "内容:${args[0]},发布状态:${args[1]}")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result doPublish(String id, Integer status) {
|
||||
try {
|
||||
contentService.publishContent(id, status);
|
||||
return Result.success();
|
||||
} catch (RuntimeException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.budwk.app.zhgh.spread.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.zhgh.spread.param.SpreadStatisticsQuery;
|
||||
import com.budwk.app.zhgh.spread.service.SpreadCategoryService;
|
||||
import com.budwk.app.zhgh.spread.service.SpreadStatisticsService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
|
||||
@IocBean
|
||||
@Ok("json:full")
|
||||
@Api("风采墙-统计分析")
|
||||
@At("/platform/spread/statistics")
|
||||
public class SpreadStatisticsController {
|
||||
|
||||
@Inject
|
||||
private SpreadCategoryService categoryService;
|
||||
|
||||
@Inject
|
||||
private SpreadStatisticsService statisticsService;
|
||||
|
||||
@At("")
|
||||
@Ok("beetl:/platform/zhgh/spread/statistics/index.html")
|
||||
@SaCheckPermission("spread.statistics")
|
||||
public void index() {
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("栏目树")
|
||||
@SaCheckPermission("spread.statistics")
|
||||
public Result categoryTree() {
|
||||
return Result.success(categoryService.categoryTree(false));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("统计卡片")
|
||||
@SaCheckPermission("spread.statistics")
|
||||
public Result summary(SpreadStatisticsQuery query) {
|
||||
return Result.success(statisticsService.summary(query));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("浏览量前十")
|
||||
@SaCheckPermission("spread.statistics")
|
||||
public Result top10(SpreadStatisticsQuery query) {
|
||||
return Result.success(statisticsService.top10(query));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("栏目统计")
|
||||
@SaCheckPermission("spread.statistics")
|
||||
public Result categoryData(SpreadStatisticsQuery query) {
|
||||
return Result.success(statisticsService.categoryStatistics(query));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.budwk.app.zhgh.spread.models;
|
||||
|
||||
import com.budwk.app.base.model.BaseModel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.nutz.dao.entity.annotation.ColDefine;
|
||||
import org.nutz.dao.entity.annotation.ColType;
|
||||
import org.nutz.dao.entity.annotation.Column;
|
||||
import org.nutz.dao.entity.annotation.Comment;
|
||||
import org.nutz.dao.entity.annotation.Name;
|
||||
import org.nutz.dao.entity.annotation.Table;
|
||||
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
||||
|
||||
@Data
|
||||
@Table("spread_category")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SpreadCategory extends BaseModel {
|
||||
|
||||
@Column
|
||||
@Name
|
||||
@Comment("ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
@PrevInsert(uu32 = true)
|
||||
private String id;
|
||||
|
||||
@Column
|
||||
@Comment("父栏目ID,0表示顶级栏目")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String parentId;
|
||||
|
||||
@Column
|
||||
@Comment("栏目名称")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String name;
|
||||
|
||||
@Column
|
||||
@Comment("栏目编码")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 50)
|
||||
private String code;
|
||||
|
||||
@Column
|
||||
@Comment("排序")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer sortOrder;
|
||||
|
||||
@Column
|
||||
@Comment("是否可见")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer visible;
|
||||
|
||||
@Column
|
||||
@Comment("是否参与轮播")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer slider;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.budwk.app.zhgh.spread.models;
|
||||
|
||||
import com.budwk.app.base.model.BaseModel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.nutz.dao.entity.annotation.ColDefine;
|
||||
import org.nutz.dao.entity.annotation.ColType;
|
||||
import org.nutz.dao.entity.annotation.Column;
|
||||
import org.nutz.dao.entity.annotation.Comment;
|
||||
import org.nutz.dao.entity.annotation.Name;
|
||||
import org.nutz.dao.entity.annotation.Table;
|
||||
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
||||
|
||||
@Data
|
||||
@Table("spread_content")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SpreadContent extends BaseModel {
|
||||
|
||||
@Column
|
||||
@Name
|
||||
@Comment("ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
@PrevInsert(uu32 = true)
|
||||
private String id;
|
||||
|
||||
@Column
|
||||
@Comment("栏目ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String categoryId;
|
||||
|
||||
@Column
|
||||
@Comment("栏目名称")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String categoryName;
|
||||
|
||||
@Column
|
||||
@Comment("所属工会ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String unionId;
|
||||
|
||||
@Column
|
||||
@Comment("所属工会名称")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String unionName;
|
||||
|
||||
@Column
|
||||
@Comment("标题")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 300)
|
||||
private String title;
|
||||
|
||||
@Column
|
||||
@Comment("副标题")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 255)
|
||||
private String subTitle;
|
||||
|
||||
@Column
|
||||
@Comment("摘要")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 1000)
|
||||
private String summary;
|
||||
|
||||
@Column
|
||||
@Comment("正文")
|
||||
@ColDefine(customType = "longtext")
|
||||
private String contentBody;
|
||||
|
||||
@Column
|
||||
@Comment("封面图")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 1000)
|
||||
private String thumbUrl;
|
||||
|
||||
@Column
|
||||
@Comment("轮播图片")
|
||||
@ColDefine(customType = "longtext")
|
||||
private String sliderImages;
|
||||
|
||||
@Column
|
||||
@Comment("是否置顶")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer isTop;
|
||||
|
||||
@Column
|
||||
@Comment("是否推荐")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer isRecommend;
|
||||
|
||||
@Column
|
||||
@Comment("是否头条")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer isHeadline;
|
||||
|
||||
@Column
|
||||
@Comment("浏览量")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer viewCount;
|
||||
|
||||
@Column
|
||||
@Comment("排序")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer sortOrder;
|
||||
|
||||
@Column
|
||||
@Comment("是否发布")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer isPublished;
|
||||
|
||||
@Column
|
||||
@Comment("发布时间")
|
||||
private Long publishedAt;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.budwk.app.zhgh.spread.param;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SpreadContentQuery {
|
||||
private String categoryId;
|
||||
private String title;
|
||||
private Integer isPublished;
|
||||
private String beginTime;
|
||||
private String endTime;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.budwk.app.zhgh.spread.param;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SpreadStatisticsQuery {
|
||||
private String categoryId;
|
||||
private Integer isPublished;
|
||||
private String beginTime;
|
||||
private String endTime;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.budwk.app.zhgh.spread.service;
|
||||
|
||||
import com.budwk.app.base.service.BaseService;
|
||||
import com.budwk.app.zhgh.spread.models.SpreadCategory;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SpreadCategoryService extends BaseService<SpreadCategory> {
|
||||
|
||||
List<SpreadCategory> listCategories(boolean visibleOnly);
|
||||
|
||||
List<NutMap> categoryTree(boolean visibleOnly);
|
||||
|
||||
void saveCategory(SpreadCategory category);
|
||||
|
||||
void updateCategory(SpreadCategory category);
|
||||
|
||||
void deleteCategory(String id);
|
||||
|
||||
void setVisible(String id, Integer visible);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.budwk.app.zhgh.spread.service;
|
||||
|
||||
import com.budwk.app.base.page.Pagination;
|
||||
import com.budwk.app.base.param.PageForm;
|
||||
import com.budwk.app.base.service.BaseService;
|
||||
import com.budwk.app.zhgh.spread.models.SpreadContent;
|
||||
import com.budwk.app.zhgh.spread.param.SpreadContentQuery;
|
||||
import com.budwk.app.sys.models.Sys_union;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SpreadContentService extends BaseService<SpreadContent> {
|
||||
|
||||
Pagination<NutMap> pageContent(PageForm pageForm, SpreadContentQuery query);
|
||||
|
||||
Pagination<NutMap> pagePublished(PageForm pageForm, String categoryId);
|
||||
|
||||
SpreadContent getContent(String id);
|
||||
|
||||
void saveContent(SpreadContent content);
|
||||
|
||||
void updateContent(SpreadContent content);
|
||||
|
||||
void deleteContent(String id);
|
||||
|
||||
void publishContent(String id, Integer status);
|
||||
|
||||
void addViewCount(String id);
|
||||
|
||||
List<NutMap> sliderContent();
|
||||
|
||||
/**
|
||||
* 查询当前登录人员可选择的工会。
|
||||
*/
|
||||
List<Sys_union> selectableUnions();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.budwk.app.zhgh.spread.service;
|
||||
|
||||
import com.budwk.app.zhgh.spread.param.SpreadStatisticsQuery;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SpreadStatisticsService {
|
||||
|
||||
NutMap summary(SpreadStatisticsQuery query);
|
||||
|
||||
List<NutMap> top10(SpreadStatisticsQuery query);
|
||||
|
||||
List<NutMap> categoryStatistics(SpreadStatisticsQuery query);
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package com.budwk.app.zhgh.spread.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||
import com.budwk.app.zhgh.spread.models.SpreadCategory;
|
||||
import com.budwk.app.zhgh.spread.models.SpreadContent;
|
||||
import com.budwk.app.zhgh.spread.service.SpreadCategoryService;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@IocBean(args = {"refer:dao"})
|
||||
public class SpreadCategoryServiceImpl extends BaseServiceImpl<SpreadCategory> implements SpreadCategoryService {
|
||||
|
||||
public SpreadCategoryServiceImpl(Dao dao) {
|
||||
super(dao);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SpreadCategory> listCategories(boolean visibleOnly) {
|
||||
Cnd cnd = Cnd.where("delFlag", "=", false);
|
||||
if (visibleOnly) {
|
||||
cnd.and("visible", "=", 1);
|
||||
}
|
||||
cnd.asc("sortOrder").asc("createdAt");
|
||||
return query(cnd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NutMap> categoryTree(boolean visibleOnly) {
|
||||
List<SpreadCategory> categories = listCategories(visibleOnly);
|
||||
Map<String, NutMap> nodeMap = new LinkedHashMap<>();
|
||||
Map<String, List<NutMap>> childrenMap = new LinkedHashMap<>();
|
||||
for (SpreadCategory category : categories) {
|
||||
List<NutMap> children = new ArrayList<>();
|
||||
nodeMap.put(category.getId(), NutMap.NEW()
|
||||
.addv("id", category.getId())
|
||||
.addv("parentId", category.getParentId())
|
||||
.addv("name", category.getName())
|
||||
.addv("code", category.getCode())
|
||||
.addv("sortOrder", category.getSortOrder())
|
||||
.addv("visible", category.getVisible())
|
||||
.addv("slider", category.getSlider())
|
||||
.addv("children", children));
|
||||
childrenMap.put(category.getId(), children);
|
||||
}
|
||||
List<NutMap> roots = new ArrayList<>();
|
||||
for (NutMap node : nodeMap.values()) {
|
||||
String parentId = node.getString("parentId");
|
||||
if ("0".equals(parentId) || !nodeMap.containsKey(parentId)) {
|
||||
roots.add(node);
|
||||
} else {
|
||||
// 直接向节点持有的原始集合追加,避免 NutMap 类型转换后子节点没有写回。
|
||||
childrenMap.get(parentId).add(node);
|
||||
}
|
||||
}
|
||||
// 叶子栏目不返回空 children,避免级联选择器误判为仍有下一级。
|
||||
nodeMap.forEach((id, node) -> {
|
||||
if (childrenMap.get(id).isEmpty()) {
|
||||
node.remove("children");
|
||||
}
|
||||
});
|
||||
return roots;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveCategory(SpreadCategory category) {
|
||||
validateCategory(category, null);
|
||||
applyDefaults(category);
|
||||
insert(category);
|
||||
syncParentSlider(category);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateCategory(SpreadCategory category) {
|
||||
if (category == null || StrUtil.isBlank(category.getId())) {
|
||||
throw new RuntimeException("请选择要编辑的栏目");
|
||||
}
|
||||
SpreadCategory dbCategory = fetch(category.getId());
|
||||
if (dbCategory == null || Boolean.TRUE.equals(dbCategory.getDelFlag())) {
|
||||
throw new RuntimeException("栏目不存在");
|
||||
}
|
||||
validateCategory(category, category.getId());
|
||||
if (!dbCategory.getParentId().equals(category.getParentId())
|
||||
&& count(Cnd.where("parentId", "=", category.getId()).and("delFlag", "=", false)) > 0) {
|
||||
throw new RuntimeException("该栏目下存在子栏目,不能移动");
|
||||
}
|
||||
applyDefaults(category);
|
||||
updateIgnoreNull(category);
|
||||
syncParentSlider(category);
|
||||
if ("0".equals(category.getParentId()) && Integer.valueOf(0).equals(category.getSlider())) {
|
||||
SpreadCategory child = new SpreadCategory();
|
||||
child.setSlider(0);
|
||||
dao().update(SpreadCategory.class, org.nutz.dao.Chain.make("slider", 0),
|
||||
Cnd.where("parentId", "=", category.getId()).and("delFlag", "=", false));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteCategory(String id) {
|
||||
if (StrUtil.isBlank(id)) {
|
||||
throw new RuntimeException("请选择要删除的栏目");
|
||||
}
|
||||
List<SpreadCategory> children = query(Cnd.where("parentId", "=", id).and("delFlag", "=", false));
|
||||
List<String> ids = new ArrayList<>();
|
||||
ids.add(id);
|
||||
children.forEach(item -> ids.add(item.getId()));
|
||||
if (dao().count(SpreadContent.class,
|
||||
Cnd.where("categoryId", "in", ids).and("delFlag", "=", false)) > 0) {
|
||||
throw new RuntimeException("栏目下存在内容,请先删除相关内容");
|
||||
}
|
||||
dao().clear(SpreadCategory.class, Cnd.where("id", "in", ids));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisible(String id, Integer visible) {
|
||||
if (StrUtil.isBlank(id) || (visible == null || (visible != 0 && visible != 1))) {
|
||||
throw new RuntimeException("栏目或显示状态不正确");
|
||||
}
|
||||
dao().update(SpreadCategory.class, org.nutz.dao.Chain.make("visible", visible),
|
||||
Cnd.where("id", "=", id).and("delFlag", "=", false));
|
||||
if (visible == 0) {
|
||||
dao().update(SpreadCategory.class, org.nutz.dao.Chain.make("visible", 0),
|
||||
Cnd.where("parentId", "=", id).and("delFlag", "=", false));
|
||||
}
|
||||
}
|
||||
|
||||
private void validateCategory(SpreadCategory category, String excludeId) {
|
||||
if (category == null || StrUtil.isBlank(category.getName())) {
|
||||
throw new RuntimeException("栏目名称不能为空");
|
||||
}
|
||||
category.setName(category.getName().trim());
|
||||
// 参考模块不要求用户维护栏目编码,空值统一保存为 NULL。
|
||||
category.setCode(StrUtil.isBlank(category.getCode()) ? null : category.getCode().trim());
|
||||
category.setParentId(StrUtil.blankToDefault(category.getParentId(), "0"));
|
||||
if (category.getId() != null && category.getId().equals(category.getParentId())) {
|
||||
throw new RuntimeException("上级栏目不能选择自身");
|
||||
}
|
||||
if (!"0".equals(category.getParentId())) {
|
||||
SpreadCategory parent = fetch(category.getParentId());
|
||||
if (parent == null || Boolean.TRUE.equals(parent.getDelFlag())) {
|
||||
throw new RuntimeException("上级栏目不存在");
|
||||
}
|
||||
if (!"0".equals(parent.getParentId())) {
|
||||
throw new RuntimeException("风采墙栏目最多支持两级");
|
||||
}
|
||||
}
|
||||
Cnd nameCnd = Cnd.where("name", "=", category.getName())
|
||||
.and("parentId", "=", category.getParentId())
|
||||
.and("delFlag", "=", false);
|
||||
if (StrUtil.isNotBlank(excludeId)) {
|
||||
nameCnd.and("id", "!=", excludeId);
|
||||
}
|
||||
if (count(nameCnd) > 0) {
|
||||
throw new RuntimeException("同级栏目名称已存在");
|
||||
}
|
||||
}
|
||||
|
||||
private void applyDefaults(SpreadCategory category) {
|
||||
if (category.getSortOrder() == null) {
|
||||
category.setSortOrder(255);
|
||||
}
|
||||
if (category.getVisible() == null) {
|
||||
category.setVisible(1);
|
||||
}
|
||||
if (category.getSlider() == null) {
|
||||
category.setSlider(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void syncParentSlider(SpreadCategory category) {
|
||||
if (!"0".equals(category.getParentId()) && Integer.valueOf(1).equals(category.getSlider())) {
|
||||
dao().update(SpreadCategory.class, org.nutz.dao.Chain.make("slider", 1),
|
||||
Cnd.where("id", "=", category.getParentId()).and("delFlag", "=", false));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
package com.budwk.app.zhgh.spread.service.impl;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.budwk.app.base.constant.RoleConstant;
|
||||
import com.budwk.app.base.page.Pagination;
|
||||
import com.budwk.app.base.param.PageForm;
|
||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||
import com.budwk.app.sys.models.Sys_union;
|
||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import com.budwk.app.zhgh.spread.models.SpreadCategory;
|
||||
import com.budwk.app.zhgh.spread.models.SpreadContent;
|
||||
import com.budwk.app.zhgh.spread.param.SpreadContentQuery;
|
||||
import com.budwk.app.zhgh.spread.service.SpreadContentService;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@IocBean(args = {"refer:dao"})
|
||||
public class SpreadContentServiceImpl extends BaseServiceImpl<SpreadContent> implements SpreadContentService {
|
||||
|
||||
public SpreadContentServiceImpl(Dao dao) {
|
||||
super(dao);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pagination<NutMap> pageContent(PageForm pageForm, SpreadContentQuery query) {
|
||||
Cnd cnd = buildCondition(query);
|
||||
Sql countSql = Sqls.create("""
|
||||
SELECT COUNT(1)
|
||||
FROM spread_content c
|
||||
$condition
|
||||
""");
|
||||
countSql.setCondition(cnd);
|
||||
countSql.setCallback(Sqls.callback.integer());
|
||||
dao().execute(countSql);
|
||||
|
||||
Sql listSql = Sqls.create("""
|
||||
SELECT c.*
|
||||
FROM spread_content c
|
||||
$condition
|
||||
ORDER BY c.isTop DESC, c.sortOrder ASC, c.createdAt DESC
|
||||
""");
|
||||
listSql.setCondition(cnd);
|
||||
listSql.setPager(dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
|
||||
listSql.setCallback(Sqls.callback.maps());
|
||||
dao().execute(listSql);
|
||||
return new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(),
|
||||
countSql.getInt(), listSql.getList(NutMap.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pagination<NutMap> pagePublished(PageForm pageForm, String categoryId) {
|
||||
SpreadContentQuery query = new SpreadContentQuery();
|
||||
query.setCategoryId(categoryId);
|
||||
query.setIsPublished(1);
|
||||
Cnd cnd = buildCondition(query);
|
||||
Sql countSql = Sqls.create("SELECT COUNT(1) FROM spread_content c $condition");
|
||||
countSql.setCondition(cnd);
|
||||
countSql.setCallback(Sqls.callback.integer());
|
||||
dao().execute(countSql);
|
||||
|
||||
Sql listSql = Sqls.create("""
|
||||
SELECT c.*
|
||||
FROM spread_content c
|
||||
$condition
|
||||
ORDER BY c.isTop DESC, c.publishedAt DESC, c.sortOrder ASC, c.createdAt DESC
|
||||
""");
|
||||
listSql.setCondition(cnd);
|
||||
listSql.setPager(dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
|
||||
listSql.setCallback(Sqls.callback.maps());
|
||||
dao().execute(listSql);
|
||||
return new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(),
|
||||
countSql.getInt(), listSql.getList(NutMap.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpreadContent getContent(String id) {
|
||||
if (StrUtil.isBlank(id)) {
|
||||
throw new RuntimeException("请选择内容");
|
||||
}
|
||||
SpreadContent content = fetch(Cnd.where("id", "=", id).and("delFlag", "=", false));
|
||||
if (content == null) {
|
||||
throw new RuntimeException("内容不存在");
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveContent(SpreadContent content) {
|
||||
validateContent(content);
|
||||
applyDefaults(content);
|
||||
if (Integer.valueOf(1).equals(content.getIsPublished())) {
|
||||
content.setPublishedAt(System.currentTimeMillis());
|
||||
}
|
||||
insert(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateContent(SpreadContent content) {
|
||||
if (content == null || StrUtil.isBlank(content.getId())) {
|
||||
throw new RuntimeException("请选择要编辑的内容");
|
||||
}
|
||||
SpreadContent dbContent = getContent(content.getId());
|
||||
validateContent(content);
|
||||
applyDefaults(content);
|
||||
if (Integer.valueOf(1).equals(content.getIsPublished()) && !Integer.valueOf(1).equals(dbContent.getIsPublished())) {
|
||||
content.setPublishedAt(System.currentTimeMillis());
|
||||
}
|
||||
updateIgnoreNull(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteContent(String id) {
|
||||
getContent(id);
|
||||
dao().clear(SpreadContent.class, Cnd.where("id", "=", id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishContent(String id, Integer status) {
|
||||
getContent(id);
|
||||
if (status == null || (status != 0 && status != 1)) {
|
||||
throw new RuntimeException("发布状态不正确");
|
||||
}
|
||||
org.nutz.dao.Chain chain = org.nutz.dao.Chain.make("isPublished", status);
|
||||
if (status == 1) {
|
||||
chain.add("publishedAt", System.currentTimeMillis());
|
||||
}
|
||||
dao().update(SpreadContent.class, chain, Cnd.where("id", "=", id).and("delFlag", "=", false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addViewCount(String id) {
|
||||
getContent(id);
|
||||
Sql sql = Sqls.create("""
|
||||
UPDATE spread_content
|
||||
SET viewCount = IFNULL(viewCount, 0) + 1,
|
||||
updatedAt = @updatedAt
|
||||
WHERE id = @id AND delFlag = 0
|
||||
""");
|
||||
sql.params().set("id", id);
|
||||
sql.params().set("updatedAt", System.currentTimeMillis());
|
||||
dao().execute(sql);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NutMap> sliderContent() {
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT c.id, c.title, c.summary, c.thumbUrl, c.sliderImages, c.categoryId, c.categoryName, c.unionId, c.unionName
|
||||
FROM spread_content c
|
||||
INNER JOIN spread_category child ON child.id = c.categoryId
|
||||
LEFT JOIN spread_category parent ON parent.id = child.parentId
|
||||
WHERE c.delFlag = 0
|
||||
AND c.isPublished = 1
|
||||
AND child.delFlag = 0
|
||||
AND child.visible = 1
|
||||
AND child.slider = 1
|
||||
AND (child.parentId = '0' OR (parent.delFlag = 0 AND parent.visible = 1 AND parent.slider = 1))
|
||||
ORDER BY c.isTop DESC, c.publishedAt DESC, c.createdAt DESC
|
||||
LIMIT 10
|
||||
""");
|
||||
sql.setCallback(Sqls.callback.maps());
|
||||
dao().execute(sql);
|
||||
return sql.getList(NutMap.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Sys_union> selectableUnions() {
|
||||
Cnd cnd = Cnd.NEW();
|
||||
if (!canSelectAllUnions()) {
|
||||
String currentUnionId = SecurityUtil.getUnionId();
|
||||
if (StrUtil.isBlank(currentUnionId)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
cnd.and("id", "=", currentUnionId);
|
||||
}
|
||||
cnd.asc("unionCode");
|
||||
return dao().query(Sys_union.class, cnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校工会管理员和系统管理员可选择全部工会,其他人员只能选择登录人员自己的分工会。
|
||||
*/
|
||||
private boolean canSelectAllUnions() {
|
||||
return AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name());
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验工会是否存在且在当前登录人员的可选择范围内。
|
||||
*/
|
||||
private Sys_union requireSelectableUnion(String unionId) {
|
||||
if (StrUtil.isBlank(unionId)) {
|
||||
throw new RuntimeException("请选择所属工会");
|
||||
}
|
||||
Sys_union union = dao().fetch(Sys_union.class, unionId);
|
||||
if (union == null) {
|
||||
throw new RuntimeException("所属工会不存在");
|
||||
}
|
||||
if (!canSelectAllUnions() && !unionId.equals(SecurityUtil.getUnionId())) {
|
||||
throw new RuntimeException("无权选择该所属工会");
|
||||
}
|
||||
return union;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并规范每张轮播图的标题,轮播图统一继承内容所属工会。
|
||||
*/
|
||||
private void validateSliderImages(SpreadContent content) {
|
||||
if (StrUtil.isBlank(content.getSliderImages())) {
|
||||
return;
|
||||
}
|
||||
JSONArray sliderArray;
|
||||
try {
|
||||
sliderArray = JSONUtil.parseArray(content.getSliderImages());
|
||||
} catch (RuntimeException e) {
|
||||
throw new RuntimeException("轮播图片数据格式不正确");
|
||||
}
|
||||
JSONArray normalizedArray = new JSONArray();
|
||||
for (int i = 0; i < sliderArray.size(); i++) {
|
||||
Object rawItem = sliderArray.get(i);
|
||||
if (!(rawItem instanceof JSONObject)) {
|
||||
throw new RuntimeException("请完善第" + (i + 1) + "张轮播图标题");
|
||||
}
|
||||
JSONObject item = (JSONObject) rawItem;
|
||||
if (StrUtil.isBlank(item.getStr("src"))) {
|
||||
throw new RuntimeException("第" + (i + 1) + "张轮播图地址不能为空");
|
||||
}
|
||||
if (StrUtil.isBlank(item.getStr("title"))) {
|
||||
throw new RuntimeException("请输入第" + (i + 1) + "张轮播图标题");
|
||||
}
|
||||
JSONObject normalizedItem = new JSONObject();
|
||||
normalizedItem.set("src", item.getStr("src"));
|
||||
normalizedItem.set("title", item.getStr("title").trim());
|
||||
normalizedArray.add(normalizedItem);
|
||||
}
|
||||
content.setSliderImages(JSONUtil.toJsonStr(normalizedArray));
|
||||
}
|
||||
private Cnd buildCondition(SpreadContentQuery query) {
|
||||
Cnd cnd = Cnd.where("c.delFlag", "=", false);
|
||||
if (query == null) {
|
||||
return cnd;
|
||||
}
|
||||
if (StrUtil.isNotBlank(query.getCategoryId())) {
|
||||
List<String> categoryIds = new ArrayList<>();
|
||||
categoryIds.add(query.getCategoryId());
|
||||
List<SpreadCategory> children = dao().query(SpreadCategory.class,
|
||||
Cnd.where("parentId", "=", query.getCategoryId()).and("delFlag", "=", false));
|
||||
children.forEach(item -> categoryIds.add(item.getId()));
|
||||
cnd.and("c.categoryId", "in", categoryIds);
|
||||
}
|
||||
cnd.and(Cnd.likeEX("c.title", query.getTitle()));
|
||||
cnd.andEX("c.isPublished", "=", query.getIsPublished());
|
||||
if (StrUtil.isNotBlank(query.getBeginTime())) {
|
||||
cnd.and("c.createdAt", ">=", DateUtil.parse(query.getBeginTime()).getTime());
|
||||
}
|
||||
if (StrUtil.isNotBlank(query.getEndTime())) {
|
||||
cnd.and("c.createdAt", "<=", DateUtil.parse(query.getEndTime()).getTime());
|
||||
}
|
||||
return cnd;
|
||||
}
|
||||
|
||||
private void validateContent(SpreadContent content) {
|
||||
if (content == null || StrUtil.isBlank(content.getCategoryId())) {
|
||||
throw new RuntimeException("请选择栏目");
|
||||
}
|
||||
SpreadCategory category = dao().fetch(SpreadCategory.class,
|
||||
Cnd.where("id", "=", content.getCategoryId()).and("delFlag", "=", false));
|
||||
if (category == null) {
|
||||
throw new RuntimeException("栏目不存在");
|
||||
}
|
||||
if (dao().count(SpreadCategory.class,
|
||||
Cnd.where("parentId", "=", category.getId()).and("delFlag", "=", false)) > 0) {
|
||||
throw new RuntimeException("内容只能发布到末级栏目");
|
||||
}
|
||||
|
||||
Sys_union union = requireSelectableUnion(content.getUnionId());
|
||||
if (StrUtil.isBlank(content.getTitle())) {
|
||||
throw new RuntimeException("标题不能为空");
|
||||
}
|
||||
content.setTitle(content.getTitle().trim());
|
||||
content.setCategoryName(category.getName());
|
||||
content.setUnionName(union.getName());
|
||||
validateSliderImages(content);
|
||||
|
||||
}
|
||||
|
||||
private void applyDefaults(SpreadContent content) {
|
||||
if (content.getIsTop() == null) {
|
||||
content.setIsTop(0);
|
||||
}
|
||||
if (content.getIsRecommend() == null) {
|
||||
content.setIsRecommend(0);
|
||||
}
|
||||
if (content.getIsHeadline() == null) {
|
||||
content.setIsHeadline(0);
|
||||
}
|
||||
if (content.getViewCount() == null) {
|
||||
content.setViewCount(0);
|
||||
}
|
||||
if (content.getSortOrder() == null) {
|
||||
content.setSortOrder(255);
|
||||
}
|
||||
if (content.getIsPublished() == null) {
|
||||
content.setIsPublished(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package com.budwk.app.zhgh.spread.service.impl;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.budwk.app.zhgh.spread.param.SpreadStatisticsQuery;
|
||||
import com.budwk.app.zhgh.spread.service.SpreadStatisticsService;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@IocBean
|
||||
public class SpreadStatisticsServiceImpl implements SpreadStatisticsService {
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
|
||||
@Override
|
||||
public NutMap summary(SpreadStatisticsQuery query) {
|
||||
String condition = buildContentCondition(query, "c");
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
(SELECT COUNT(1) FROM spread_category cg WHERE cg.delFlag = 0) AS categoryCount,
|
||||
COUNT(1) AS contentCount,
|
||||
IFNULL(SUM(CASE WHEN c.isPublished = 1 THEN 1 ELSE 0 END), 0) AS publishedCount,
|
||||
IFNULL(SUM(c.viewCount), 0) AS viewCount,
|
||||
IFNULL(ROUND(AVG(c.viewCount)), 0) AS avgViewCount
|
||||
FROM spread_content c
|
||||
WHERE c.delFlag = 0
|
||||
$condition
|
||||
""");
|
||||
sql.setVar("condition", condition);
|
||||
setParams(sql, query);
|
||||
sql.setCallback(Sqls.callback.map());
|
||||
dao.execute(sql);
|
||||
NutMap result = sql.getObject(NutMap.class);
|
||||
return result == null ? NutMap.NEW() : result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NutMap> top10(SpreadStatisticsQuery query) {
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT c.id, c.title, c.summary, c.categoryName, c.unionId, c.unionName, c.viewCount, c.isPublished, c.publishedAt
|
||||
FROM spread_content c
|
||||
WHERE c.delFlag = 0
|
||||
AND c.isPublished = 1
|
||||
$condition
|
||||
ORDER BY c.viewCount DESC, c.updatedAt DESC
|
||||
LIMIT 10
|
||||
""");
|
||||
sql.setVar("condition", buildContentCondition(query, "c"));
|
||||
setParams(sql, query);
|
||||
sql.setCallback(Sqls.callback.maps());
|
||||
dao.execute(sql);
|
||||
return sql.getList(NutMap.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NutMap> categoryStatistics(SpreadStatisticsQuery query) {
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT c.categoryId, c.categoryName, COUNT(1) AS contentCount,
|
||||
IFNULL(SUM(c.viewCount), 0) AS viewCount
|
||||
FROM spread_content c
|
||||
WHERE c.delFlag = 0
|
||||
AND c.isPublished = 1
|
||||
$condition
|
||||
GROUP BY c.categoryId, c.categoryName
|
||||
ORDER BY viewCount DESC, contentCount DESC
|
||||
""");
|
||||
sql.setVar("condition", buildContentCondition(query, "c"));
|
||||
setParams(sql, query);
|
||||
sql.setCallback(Sqls.callback.maps());
|
||||
dao.execute(sql);
|
||||
return sql.getList(NutMap.class);
|
||||
}
|
||||
|
||||
private String buildContentCondition(SpreadStatisticsQuery query, String alias) {
|
||||
if (query == null) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder condition = new StringBuilder();
|
||||
if (StrUtil.isNotBlank(query.getCategoryId())) {
|
||||
condition.append(" AND ").append(alias)
|
||||
.append(".categoryId IN (SELECT id FROM spread_category WHERE id = @categoryId OR parentId = @categoryId)");
|
||||
}
|
||||
if (query.getIsPublished() != null) {
|
||||
condition.append(" AND ").append(alias).append(".isPublished = @isPublished");
|
||||
}
|
||||
if (StrUtil.isNotBlank(query.getBeginTime())) {
|
||||
condition.append(" AND ").append(alias).append(".createdAt >= @beginAt");
|
||||
}
|
||||
if (StrUtil.isNotBlank(query.getEndTime())) {
|
||||
condition.append(" AND ").append(alias).append(".createdAt <= @endAt");
|
||||
}
|
||||
return condition.toString();
|
||||
}
|
||||
|
||||
private void setParams(Sql sql, SpreadStatisticsQuery query) {
|
||||
if (query == null) {
|
||||
return;
|
||||
}
|
||||
sql.params().set("categoryId", query.getCategoryId());
|
||||
sql.params().set("isPublished", query.getIsPublished());
|
||||
if (StrUtil.isNotBlank(query.getBeginTime())) {
|
||||
sql.params().set("beginAt", DateUtil.parse(query.getBeginTime()).getTime());
|
||||
}
|
||||
if (StrUtil.isNotBlank(query.getEndTime())) {
|
||||
sql.params().set("endAt", DateUtil.parse(query.getEndTime()).getTime());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
-- 风采墙电脑端菜单。使用 permission 做幂等判断,脚本可重复执行。
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT
|
||||
'9f5c0200000000000000000000000001',
|
||||
'',
|
||||
LPAD(IFNULL(MAX(CAST(path AS UNSIGNED)), 0) + 1, 4, '0'),
|
||||
'风采墙',
|
||||
'Style Wall',
|
||||
'menu',
|
||||
'',
|
||||
'',
|
||||
'ti-medall',
|
||||
1,
|
||||
0,
|
||||
'spread',
|
||||
NULL,
|
||||
992,
|
||||
1,
|
||||
'',
|
||||
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||
'',
|
||||
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||
0,
|
||||
'PC',
|
||||
NULL,
|
||||
NULL,
|
||||
'f',
|
||||
0,
|
||||
0
|
||||
FROM sys_menu
|
||||
WHERE (parentId = '' OR parentId IS NULL)
|
||||
AND CHAR_LENGTH(path) = 4
|
||||
HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'spread') t);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT '9f5c0200000000000000000000000002', p.id, CONCAT(p.path, '0001'), '风采展示', 'Style Display', 'menu', '/platform/spread/display', 'data-pjax', '', 1, 0, 'spread.display', NULL, 1, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'f', 0, 0
|
||||
FROM sys_menu p
|
||||
WHERE p.permission = 'spread'
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'spread.display') t);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT '9f5c0200000000000000000000000003', p.id, CONCAT(p.path, '0002'), '内容发布', 'Content Creation', 'menu', '/platform/spread/creation', 'data-pjax', '', 1, 0, 'spread.creation', NULL, 2, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'n', 0, 0
|
||||
FROM sys_menu p
|
||||
WHERE p.permission = 'spread'
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'spread.creation') t);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT '9f5c0200000000000000000000000004', p.id, CONCAT(p.path, '0003'), '内容管理', 'Content Manage', 'menu', '/platform/spread/manage', 'data-pjax', '', 1, 0, 'spread.manage', NULL, 3, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'n', 0, 0
|
||||
FROM sys_menu p
|
||||
WHERE p.permission = 'spread'
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'spread.manage') t);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT '9f5c0200000000000000000000000005', p.id, CONCAT(p.path, '0004'), '栏目管理', 'Category Manage', 'menu', '/platform/spread/category', 'data-pjax', '', 1, 0, 'spread.category', NULL, 4, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0
|
||||
FROM sys_menu p
|
||||
WHERE p.permission = 'spread'
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'spread.category') t);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT '9f5c0200000000000000000000000006', p.id, CONCAT(p.path, '0005'), '统计分析', 'Statistics', 'menu', '/platform/spread/statistics', 'data-pjax', '', 1, 0, 'spread.statistics', NULL, 5, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 't', 0, 0
|
||||
FROM sys_menu p
|
||||
WHERE p.permission = 'spread'
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'spread.statistics') t);
|
||||
|
||||
-- 系统管理员默认拥有风采墙全部权限,其他角色可在角色管理中按需分配。
|
||||
INSERT INTO sys_role_menu (roleId, menuId)
|
||||
SELECT r.id, m.id
|
||||
FROM sys_role r
|
||||
JOIN sys_menu m ON m.permission IN (
|
||||
'spread',
|
||||
'spread.display',
|
||||
'spread.creation',
|
||||
'spread.manage',
|
||||
'spread.category',
|
||||
'spread.statistics'
|
||||
)
|
||||
LEFT JOIN sys_role_menu rm ON rm.roleId = r.id AND rm.menuId = m.id
|
||||
WHERE r.code = 'SYSADMIN'
|
||||
AND rm.roleId IS NULL;
|
||||
@@ -0,0 +1,79 @@
|
||||
-- 风采墙栏目表,适用于 MySQL 8。
|
||||
CREATE TABLE IF NOT EXISTS `spread_category` (
|
||||
`id` varchar(32) NOT NULL COMMENT 'ID',
|
||||
`parentId` varchar(32) NOT NULL DEFAULT '0' COMMENT '父栏目ID,0表示顶级栏目',
|
||||
`name` varchar(100) NOT NULL COMMENT '栏目名称',
|
||||
`code` varchar(50) DEFAULT NULL COMMENT '栏目编码(兼容预置数据,页面无需维护)',
|
||||
`sortOrder` int NOT NULL DEFAULT 255 COMMENT '排序,数值越小越靠前',
|
||||
`visible` tinyint NOT NULL DEFAULT 1 COMMENT '是否可见:0否,1是',
|
||||
`slider` tinyint NOT NULL DEFAULT 0 COMMENT '是否参与轮播:0否,1是',
|
||||
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
|
||||
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
|
||||
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
|
||||
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
|
||||
`delFlag` tinyint NOT NULL DEFAULT 0 COMMENT '删除标记',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_spread_category_code` (`code`),
|
||||
KEY `idx_spread_category_parent` (`parentId`),
|
||||
KEY `idx_spread_category_visible_sort` (`visible`, `sortOrder`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='风采墙栏目';
|
||||
|
||||
-- 风采墙内容表。
|
||||
CREATE TABLE IF NOT EXISTS `spread_content` (
|
||||
`id` varchar(32) NOT NULL COMMENT 'ID',
|
||||
`categoryId` varchar(32) NOT NULL COMMENT '栏目ID',
|
||||
`categoryName` varchar(100) NOT NULL COMMENT '栏目名称',
|
||||
`unionId` varchar(32) DEFAULT NULL COMMENT '所属工会ID',
|
||||
`unionName` varchar(100) DEFAULT NULL COMMENT '所属工会名称',
|
||||
`title` varchar(300) NOT NULL COMMENT '标题',
|
||||
`subTitle` varchar(255) DEFAULT NULL COMMENT '副标题',
|
||||
`summary` varchar(1000) DEFAULT NULL COMMENT '摘要',
|
||||
`contentBody` longtext COMMENT 'HTML正文',
|
||||
`thumbUrl` varchar(1000) DEFAULT NULL COMMENT '封面图',
|
||||
`sliderImages` longtext COMMENT '轮播图片JSON',
|
||||
`isTop` tinyint NOT NULL DEFAULT 0 COMMENT '是否置顶',
|
||||
`isRecommend` tinyint NOT NULL DEFAULT 0 COMMENT '是否推荐',
|
||||
`isHeadline` tinyint NOT NULL DEFAULT 0 COMMENT '是否头条',
|
||||
`viewCount` int NOT NULL DEFAULT 0 COMMENT '浏览量',
|
||||
`sortOrder` int NOT NULL DEFAULT 255 COMMENT '排序',
|
||||
`isPublished` tinyint NOT NULL DEFAULT 0 COMMENT '是否发布',
|
||||
`publishedAt` bigint DEFAULT NULL COMMENT '发布时间',
|
||||
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
|
||||
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
|
||||
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
|
||||
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
|
||||
`delFlag` tinyint NOT NULL DEFAULT 0 COMMENT '删除标记',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_spread_content_category` (`categoryId`),
|
||||
KEY `idx_spread_content_union` (`unionId`),
|
||||
KEY `idx_spread_content_publish` (`isPublished`, `publishedAt`),
|
||||
KEY `idx_spread_content_top_sort` (`isTop`, `sortOrder`),
|
||||
KEY `idx_spread_content_created` (`createdAt`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='风采墙内容';
|
||||
|
||||
-- 初始化栏目,仅在对应编码不存在时插入,脚本可重复执行。
|
||||
INSERT INTO `spread_category`
|
||||
(`id`, `parentId`, `name`, `code`, `sortOrder`, `visible`, `slider`, `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`)
|
||||
SELECT '9f5c0100000000000000000000000001', '0', '风采展示', 'style_wall', 1, 1, 1, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `spread_category` WHERE `code` = 'style_wall');
|
||||
|
||||
INSERT INTO `spread_category`
|
||||
(`id`, `parentId`, `name`, `code`, `sortOrder`, `visible`, `slider`, `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`)
|
||||
SELECT '9f5c0100000000000000000000000002', p.id, '劳模风采', 'model_worker_style', 1, 1, 1, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
|
||||
FROM `spread_category` p
|
||||
WHERE p.code = 'style_wall'
|
||||
AND NOT EXISTS (SELECT 1 FROM `spread_category` WHERE `code` = 'model_worker_style');
|
||||
|
||||
INSERT INTO `spread_category`
|
||||
(`id`, `parentId`, `name`, `code`, `sortOrder`, `visible`, `slider`, `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`)
|
||||
SELECT '9f5c0100000000000000000000000003', p.id, '巾帼风采', 'women_style', 2, 1, 1, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
|
||||
FROM `spread_category` p
|
||||
WHERE p.code = 'style_wall'
|
||||
AND NOT EXISTS (SELECT 1 FROM `spread_category` WHERE `code` = 'women_style');
|
||||
|
||||
INSERT INTO `spread_category`
|
||||
(`id`, `parentId`, `name`, `code`, `sortOrder`, `visible`, `slider`, `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`)
|
||||
SELECT '9f5c0100000000000000000000000004', p.id, '教工风采', 'faculty_style', 3, 1, 1, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0
|
||||
FROM `spread_category` p
|
||||
WHERE p.code = 'style_wall'
|
||||
AND NOT EXISTS (SELECT 1 FROM `spread_category` WHERE `code` = 'faculty_style');
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 460 KiB |
@@ -6,41 +6,39 @@ const act = {
|
||||
<!-- Swiper容器 -->
|
||||
|
||||
<div v-if="actList && actList.length > 0" class="activity-swiper-container">
|
||||
<div class="activity-swiper-container">
|
||||
<!-- 活动数据 - Swiper -->
|
||||
<div class="swiper activity-swiper">
|
||||
<div class="swiper-wrapper">
|
||||
<div class="swiper-slide" v-for="(activity, index) in actList" :key="activity.id">
|
||||
<div class="item" @click="go(activity)">
|
||||
<div class="img-box">
|
||||
<img :src="activity.cover || 'https://www.ncu.edu.cn/__local/8/52/2C/644986B4C9A030F7B65300178A6_8679CB08_18467.jpg'"
|
||||
alt="">
|
||||
<span class="status">
|
||||
{{getStatusText(activity)}}
|
||||
</span>
|
||||
</div>
|
||||
<h4 class="title">
|
||||
{{ activity.name}}
|
||||
</h4>
|
||||
<!--报名时间-->
|
||||
<div class="time">
|
||||
<p>
|
||||
<i class="fa fa-clock-o"></i>
|
||||
时间:{{ activity.startDate }}至{{ activity.endDate }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- 活动数据 - Swiper -->
|
||||
<div ref="activitySwiper" class="swiper activity-swiper">
|
||||
<div class="swiper-wrapper">
|
||||
<div class="swiper-slide" v-for="(activity, index) in actList" :key="activity.id">
|
||||
<div class="item" @click="go(activity)">
|
||||
<div class="img-box">
|
||||
<img :src="activity.cover || 'https://www.ncu.edu.cn/__local/8/52/2C/644986B4C9A030F7B65300178A6_8679CB08_18467.jpg'"
|
||||
alt="">
|
||||
<span class="status">
|
||||
{{getStatusText(activity)}}
|
||||
</span>
|
||||
</div>
|
||||
<h4 class="title">
|
||||
{{ activity.name}}
|
||||
</h4>
|
||||
<!--报名时间-->
|
||||
<div class="time">
|
||||
<p>
|
||||
<i class="fa fa-clock-o"></i>
|
||||
时间:{{ activity.startDate }}至{{ activity.endDate }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导航按钮 -->
|
||||
<div class="swiper-button-next"></div>
|
||||
<div class="swiper-button-prev"></div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="swiper-pagination"></div>
|
||||
</div>
|
||||
|
||||
<!-- 导航按钮 -->
|
||||
<div ref="nextButton" class="swiper-button-next"></div>
|
||||
<div ref="prevButton" class="swiper-button-prev"></div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div ref="pagination" class="swiper-pagination"></div>
|
||||
</div>
|
||||
|
||||
<div v-else class="no-activity-placeholder">
|
||||
@@ -80,17 +78,33 @@ const act = {
|
||||
this.swiper.destroy(true, true)
|
||||
this.swiper = null
|
||||
}
|
||||
this.swiper = new Swiper('.activity-swiper', {
|
||||
const swiperElement = this.$refs.activitySwiper
|
||||
if (!swiperElement) {
|
||||
return
|
||||
}
|
||||
// 仅操作当前组件的轮播节点,避免页面其他轮播或尺寸观察器影响卡片宽度。
|
||||
this.swiper = new Swiper(swiperElement, {
|
||||
slidesPerView: 1,
|
||||
spaceBetween: 18,
|
||||
centeredSlides: false,
|
||||
loop: false,
|
||||
watchOverflow: true,
|
||||
observer: false,
|
||||
observeParents: false,
|
||||
resizeObserver: false,
|
||||
// 鼠标停留在活动区域时可横向浏览,到首尾后释放页面滚动。
|
||||
mousewheel: {
|
||||
enabled: true,
|
||||
forceToAxis: false,
|
||||
releaseOnEdges: true,
|
||||
sensitivity: 0.8,
|
||||
},
|
||||
navigation: {
|
||||
nextEl: '.swiper-button-next',
|
||||
prevEl: '.swiper-button-prev',
|
||||
nextEl: this.$refs.nextButton,
|
||||
prevEl: this.$refs.prevButton,
|
||||
},
|
||||
pagination: {
|
||||
el: '.swiper-pagination',
|
||||
el: this.$refs.pagination,
|
||||
type: 'progressbar',
|
||||
},
|
||||
breakpoints: {
|
||||
@@ -134,10 +148,25 @@ const act = {
|
||||
mounted() {
|
||||
this.listAct()
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.swiper) {
|
||||
this.swiper.destroy(true, true)
|
||||
this.swiper = null
|
||||
}
|
||||
},
|
||||
style: /*language=CSS*/ `
|
||||
.activity-section-wrapper {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-act {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.section-act .section-act-title span {
|
||||
@@ -182,8 +211,9 @@ const act = {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 9px 14px;
|
||||
padding: 9px 54px 12px;
|
||||
/*margin-top: 30px;*/
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -207,6 +237,8 @@ const act = {
|
||||
background: white;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
top: 72px;
|
||||
margin-top: 0;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
@@ -225,11 +257,9 @@ const act = {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 进度条样式 */
|
||||
/* 进度条不展示,活动通过左右箭头或区域内滚轮浏览。 */
|
||||
.activity-swiper-container .swiper-pagination {
|
||||
position: relative;
|
||||
margin-top: 12px;
|
||||
height: 4px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.activity-swiper-container .swiper-pagination-progressbar {
|
||||
|
||||
@@ -53,16 +53,23 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
.act-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 320px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
padding: 11px;
|
||||
padding: 22px 11px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.home-section {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.home-section-header {
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<el-card shadow="never">
|
||||
<table-tool :app="this" label="风采墙栏目">
|
||||
<el-button size="medium" type="primary" icon="el-icon-plus" @click="openAdd">新增栏目</el-button>
|
||||
<el-button size="medium" icon="el-icon-s-unfold" @click="expandAll">展开全部</el-button>
|
||||
<el-button size="medium" icon="el-icon-s-fold" @click="collapseAll">折叠全部</el-button>
|
||||
</table-tool>
|
||||
<el-table ref="categoryTable" v-loading="loading" :data="tableData" border row-key="id"
|
||||
:tree-props="{children: 'children'}" default-expand-all>
|
||||
<el-table-column prop="name" label="栏目名称" min-width="220" align="left"></el-table-column>
|
||||
<el-table-column prop="sortOrder" label="排序" width="90" align="center"></el-table-column>
|
||||
<el-table-column label="显示" width="100" align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-switch :value="row.visible === 1" @change="changeVisible(row, $event)"></el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="轮播" width="100" align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag :type="row.slider === 1 ? 'success' : 'info'" size="mini">
|
||||
{{row.slider === 1 ? '开启' : '关闭'}}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="190" align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-button type="primary" size="mini" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" size="mini" @click="remove(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</guava>
|
||||
|
||||
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="560px" :close-on-click-modal="false">
|
||||
<el-form ref="categoryForm" :model="formData" :rules="rules" label-width="100px">
|
||||
<el-form-item label="上级栏目" prop="parentId">
|
||||
<el-select v-model="formData.parentId" style="width: 100%" placeholder="请选择上级栏目">
|
||||
<el-option label="顶级栏目" value="0"></el-option>
|
||||
<el-option v-for="item in rootOptions" :key="item.id" :label="item.name" :value="item.id"
|
||||
:disabled="item.id === formData.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="栏目名称" prop="name">
|
||||
<el-input v-model.trim="formData.name" maxlength="100" show-word-limit></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sortOrder">
|
||||
<el-input-number v-model="formData.sortOrder" :min="0" :max="9999" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="显示状态">
|
||||
<el-radio-group v-model="formData.visible">
|
||||
<el-radio :label="1">显示</el-radio>
|
||||
<el-radio :label="0">隐藏</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="参与轮播">
|
||||
<el-radio-group v-model="formData.slider">
|
||||
<el-radio :label="1">开启</el-radio>
|
||||
<el-radio :label="0">关闭</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="el-form-item__tip">子栏目开启轮播后,其上级栏目会自动开启。</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submit">确定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
submitting: false,
|
||||
tableData: [],
|
||||
rootOptions: [],
|
||||
dialogVisible: false,
|
||||
dialogTitle: "",
|
||||
formData: {},
|
||||
rules: {
|
||||
parentId: [{required: true, message: "请选择上级栏目", trigger: "change"}],
|
||||
name: [{required: true, message: "请输入栏目名称", trigger: "blur"}],
|
||||
sortOrder: [{required: true, message: "请输入排序", trigger: "change"}]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 加载栏目树,同时提取顶级栏目作为上级栏目选项。
|
||||
loadData() {
|
||||
this.loading = true
|
||||
this.$axios.post(loc() + "/treeData").then(resp => {
|
||||
if (resp.code === 0) {
|
||||
this.tableData = resp.data || []
|
||||
this.rootOptions = (resp.data || []).slice()
|
||||
// 栏目数据为异步加载,赋值后显式展开,避免组件只展示顶级栏目。
|
||||
this.$nextTick(this.expandAll)
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
}).finally(() => this.loading = false)
|
||||
},
|
||||
// 构造新增栏目默认值。
|
||||
openAdd() {
|
||||
this.dialogTitle = "新增栏目"
|
||||
this.formData = {parentId: "0", name: "", sortOrder: 255, visible: 1, slider: 0}
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => this.$refs.categoryForm && this.$refs.categoryForm.clearValidate())
|
||||
},
|
||||
// 将当前行复制到表单,避免直接修改表格数据。
|
||||
openEdit(row) {
|
||||
this.dialogTitle = "编辑栏目"
|
||||
this.formData = Object.assign({}, row)
|
||||
delete this.formData.children
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => this.$refs.categoryForm && this.$refs.categoryForm.clearValidate())
|
||||
},
|
||||
// 展开当前栏目树中的全部父级栏目。
|
||||
expandAll() {
|
||||
this.setAllExpanded(true)
|
||||
},
|
||||
// 折叠当前栏目树中的全部父级栏目。
|
||||
collapseAll() {
|
||||
this.setAllExpanded(false)
|
||||
},
|
||||
// Element UI 异步树表格需要逐行设置展开状态。
|
||||
setAllExpanded(expanded) {
|
||||
const table = this.$refs.categoryTable
|
||||
if (!table) return
|
||||
const walk = rows => {
|
||||
(rows || []).forEach(row => {
|
||||
if (row.children && row.children.length) {
|
||||
table.toggleRowExpansion(row, expanded)
|
||||
walk(row.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
walk(this.tableData)
|
||||
},
|
||||
// 校验后调用新增或编辑接口。
|
||||
submit() {
|
||||
this.$refs.categoryForm.validate(valid => {
|
||||
if (!valid) return
|
||||
this.submitting = true
|
||||
const api = this.formData.id ? "/doEdit" : "/doAdd"
|
||||
this.$axios.post(loc() + api, this.formData).then(resp => {
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg || "保存成功")
|
||||
this.dialogVisible = false
|
||||
this.loadData()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
}).finally(() => this.submitting = false)
|
||||
})
|
||||
},
|
||||
// 修改栏目显示状态,失败时重新加载以恢复开关。
|
||||
changeVisible(row, checked) {
|
||||
this.$axios.post(loc() + "/doVisible", {id: row.id, visible: checked ? 1 : 0}).then(resp => {
|
||||
if (resp.code === 0) {
|
||||
this.$message.success("设置成功")
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
this.loadData()
|
||||
})
|
||||
},
|
||||
// 删除前进行二次确认;存在内容时后端会阻止删除。
|
||||
remove(row) {
|
||||
this.$confirm("确定删除栏目“" + row.name + "”吗?", "提示", {type: "warning"}).then(() => {
|
||||
this.$axios.post(loc() + "/doDelete", {id: row.id}).then(resp => {
|
||||
if (resp.code === 0) {
|
||||
this.$message.success("删除成功")
|
||||
this.loadData()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,381 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<style>
|
||||
.spread-editor-page { width: 100%; }
|
||||
.spread-editor-page .form-card { width: 100%; max-width: none; margin: 0; box-sizing: border-box; }
|
||||
.spread-editor-page .form-tip { color: #909399; font-size: 12px; line-height: 20px; }
|
||||
.spread-editor-page .footer-actions { padding: 18px 0 6px; text-align: center; }
|
||||
.spread-editor-page .slider-upload-head { width: 100%; margin-bottom: 10px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.spread-editor-page .slider-upload-title { color: #303133; font-weight: 600; }
|
||||
.spread-editor-page .slider-upload-count { color: #909399; font-size: 13px; font-weight: 400; }
|
||||
.spread-editor-page .slider-meta-list { width: 100%; margin-top: 14px; }
|
||||
.spread-editor-page .slider-meta-toolbar { margin-bottom: 10px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.spread-editor-page .slider-meta-item { margin-bottom: 12px; padding: 14px; display: grid; grid-template-columns: 130px minmax(220px, 1fr) 92px; gap: 14px; align-items: center; border: 1px solid #e4e7ed; border-radius: 8px; background: #fafafa; transition: border-color .2s, box-shadow .2s; }
|
||||
.spread-editor-page .slider-meta-item:hover { border-color: #c6e2ff; box-shadow: 0 5px 16px rgba(64,158,255,.10); }
|
||||
.spread-editor-page .slider-preview-wrap { position: relative; }
|
||||
.spread-editor-page .slider-index { min-width: 26px; height: 26px; padding: 0 7px; position: absolute; top: 6px; left: 6px; z-index: 2; box-sizing: border-box; color: #fff; font-size: 12px; line-height: 26px; text-align: center; border-radius: 13px; background: rgba(0,0,0,.58); }
|
||||
.spread-editor-page .slider-meta-image { width: 130px; height: 82px; display: block; object-fit: cover; border: 4px solid #fff; border-radius: 5px; box-sizing: border-box; background: #fff; box-shadow: 0 2px 8px rgba(0,0,0,.12); }
|
||||
.spread-editor-page .slider-field-label { margin-bottom: 7px; display: block; color: #606266; font-size: 13px; }
|
||||
.spread-editor-page .color-danger { color: #f56c6c; }
|
||||
.spread-editor-page .slider-item-actions { display: flex; flex-direction: column; align-items: stretch; }
|
||||
.spread-editor-page .slider-item-actions .el-button + .el-button { margin-top: 6px; margin-left: 0; }
|
||||
@media(max-width: 1050px) {
|
||||
.spread-editor-page .slider-meta-item { grid-template-columns: 110px minmax(0, 1fr) 92px; }
|
||||
.spread-editor-page .slider-meta-image { width: 110px; }
|
||||
.spread-editor-page .slider-item-actions { grid-column: 1 / -1; flex-direction: row; justify-content: flex-end; }
|
||||
.spread-editor-page .slider-item-actions .el-button + .el-button { margin-top: 0; margin-left: 8px; }
|
||||
}
|
||||
@media(max-width: 680px) {
|
||||
.spread-editor-page .slider-meta-item { grid-template-columns: 1fr; }
|
||||
.spread-editor-page .slider-meta-image { width: 100%; height: 180px; }
|
||||
.spread-editor-page .slider-item-actions { grid-column: auto; }
|
||||
}
|
||||
</style>
|
||||
<div id="app" class="spread-editor-page" v-cloak>
|
||||
<el-card class="form-card" shadow="never" v-loading="loading">
|
||||
<div slot="header">
|
||||
<span style="font-size: 17px;font-weight: 600">{{formData.id ? '编辑风采内容' : '发布风采内容'}}</span>
|
||||
</div>
|
||||
<el-form ref="contentForm" :model="formData" :rules="rules" label-width="105px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="所属栏目" prop="categoryId">
|
||||
<el-cascader v-model="formData.categoryId" :options="categoryOptions"
|
||||
:props="{value:'id', label:'name', children:'children', emitPath:false, checkStrictly:false}"
|
||||
filterable clearable style="width: 100%" placeholder="请选择末级栏目"></el-cascader>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="所属工会" prop="unionId">
|
||||
<el-select v-model="formData.unionId" filterable clearable style="width: 100%"
|
||||
placeholder="请选择所属工会" @change="contentUnionChanged">
|
||||
<el-option v-for="item in unionOptions" :key="item.id"
|
||||
:label="item.name" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="排序" prop="sortOrder">
|
||||
<el-input-number v-model="formData.sortOrder" :min="0" :max="9999" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model.trim="formData.title" maxlength="300" show-word-limit></el-input>
|
||||
</el-form-item>
|
||||
<!-- 副标题当前无实际业务用途,暂不开放录入。
|
||||
<el-form-item label="副标题">
|
||||
<el-input v-model.trim="formData.subTitle" maxlength="255" show-word-limit></el-input>
|
||||
</el-form-item>
|
||||
-->
|
||||
|
||||
<el-form-item label="摘要" prop="summary">
|
||||
<el-input v-model="formData.summary" type="textarea" :rows="3" maxlength="1000" show-word-limit
|
||||
placeholder="用于风采墙列表展示"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="封面图" prop="thumbUrl">
|
||||
<file-upload :upload_number="1" :upload_size="10 * 1024 * 1024"
|
||||
:value.sync="formData.thumbUrl" accept=".jpg,.jpeg,.png,.webp"
|
||||
upload_mode="image" upload_result_category="interval"></file-upload>
|
||||
<div class="form-tip">建议使用横版图片,单张不超过 10MB。</div>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="sliderEnabled" label="轮播图片">
|
||||
<div class="slider-upload-head">
|
||||
<span class="slider-upload-title">点击下方“+”新增轮播图片</span>
|
||||
<span class="slider-upload-count">已添加 {{sliderMeta.length}} / 10 张</span>
|
||||
</div>
|
||||
<file-upload :upload_number="10" :upload_size="10 * 1024 * 1024"
|
||||
:value.sync="sliderFiles" accept=".jpg,.jpeg,.png,.webp"
|
||||
upload_mode="image" upload_result_category="array"></file-upload>
|
||||
<div class="form-tip">仅栏目开启“参与轮播”后生效,最多上传 10 张;每张图片只需填写标题,工会名称沿用内容所属工会。</div>
|
||||
<div v-if="sliderMeta.length" class="slider-meta-list">
|
||||
<div class="slider-meta-toolbar">
|
||||
<span class="form-tip">可调整顺序,轮播将按当前顺序展示。</span>
|
||||
<el-button type="text" icon="el-icon-delete" class="color-danger" @click="clearSliderImages">
|
||||
清空全部
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-for="(img, index) in sliderMeta" :key="img.src + '-' + index" class="slider-meta-item">
|
||||
<div class="slider-preview-wrap">
|
||||
<span class="slider-index">{{index + 1}}</span>
|
||||
<img class="slider-meta-image" :src="img.src" :alt="img.title || ('轮播图' + (index + 1))">
|
||||
</div>
|
||||
<div>
|
||||
<span class="slider-field-label"><i class="color-danger">*</i> 图片标题</span>
|
||||
<el-input v-model.trim="img.title" maxlength="100" show-word-limit
|
||||
:placeholder="'请输入第' + (index + 1) + '张图片标题'"></el-input>
|
||||
</div>
|
||||
|
||||
<div class="slider-item-actions">
|
||||
<el-button size="mini" icon="el-icon-top" :disabled="index === 0"
|
||||
@click="moveSliderImage(index, -1)">上移</el-button>
|
||||
<el-button size="mini" icon="el-icon-bottom" :disabled="index === sliderMeta.length - 1"
|
||||
@click="moveSliderImage(index, 1)">下移</el-button>
|
||||
<el-button size="mini" type="danger" plain icon="el-icon-delete"
|
||||
@click="removeSliderImage(index)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="正文" prop="contentBody">
|
||||
<text-editor v-model="formData.contentBody" :height="360" placeholder="请输入风采内容"></text-editor>
|
||||
</el-form-item>
|
||||
<el-form-item label="置顶">
|
||||
<el-switch v-model="formData.isTop" :active-value="1" :inactive-value="0"
|
||||
active-text="置顶" inactive-text="不置顶"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="发布状态">
|
||||
<el-radio-group v-model="formData.isPublished">
|
||||
<el-radio :label="0">保存草稿</el-radio>
|
||||
<el-radio :label="1">立即发布</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<div class="footer-actions">
|
||||
<el-button @click="resetForm">重置</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submit">保存</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
submitting: false,
|
||||
categoryOptions: [],
|
||||
unionOptions: [],
|
||||
sliderFiles: [],
|
||||
sliderMeta: [],
|
||||
formData: {},
|
||||
rules: {
|
||||
categoryId: [{required: true, message: "请选择所属栏目", trigger: "change"}],
|
||||
unionId: [{required: true, message: "请选择所属工会", trigger: "change"}],
|
||||
title: [{required: true, message: "请输入标题", trigger: "blur"}],
|
||||
summary: [{required: true, message: "请输入摘要", trigger: "blur"}],
|
||||
contentBody: [{required: true, message: "请输入正文", trigger: "change"}]
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 仅所选末级栏目及其上级栏目都开启轮播时,显示轮播图片配置。
|
||||
sliderEnabled() {
|
||||
if (!this.formData.categoryId) return false
|
||||
const findPath = (nodes, categoryId, parents) => {
|
||||
for (const node of nodes || []) {
|
||||
const path = parents.concat(node)
|
||||
if (node.id === categoryId) return path
|
||||
const childPath = findPath(node.children, categoryId, path)
|
||||
if (childPath) return childPath
|
||||
}
|
||||
return null
|
||||
}
|
||||
const categoryPath = findPath(this.categoryOptions, this.formData.categoryId, [])
|
||||
return !!categoryPath && categoryPath.every(item => Number(item.slider) === 1)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 生成新增内容时使用的默认表单。
|
||||
defaultForm() {
|
||||
return {
|
||||
categoryId: "", unionId: "", unionName: "", title: "", subTitle: "", summary: "", contentBody: "",
|
||||
thumbUrl: "", sliderImages: "", isTop: 0,
|
||||
viewCount: 0, sortOrder: 255, isPublished: 0
|
||||
}
|
||||
},
|
||||
// 读取地址栏中的编辑内容 ID。
|
||||
getEditId() {
|
||||
return new URLSearchParams(window.location.search).get("id") || ""
|
||||
},
|
||||
// 加载栏目树,内容只能选择末级栏目。
|
||||
loadCategories() {
|
||||
return this.$axios.post(loc() + "/categoryTree").then(resp => {
|
||||
if (resp.code === 0) {
|
||||
this.categoryOptions = resp.data || []
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
// 按当前登录人员权限加载可选择工会。
|
||||
loadUnionOptions() {
|
||||
return this.$axios.post(loc() + "/unionOptions").then(resp => {
|
||||
if (resp.code === 0) {
|
||||
this.unionOptions = resp.data || []
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
// 内容所属工会变化时同步保存工会名称。
|
||||
contentUnionChanged(unionId) {
|
||||
const union = this.unionOptions.find(item => item.id === unionId)
|
||||
this.formData.unionName = union ? union.name : ""
|
||||
},
|
||||
|
||||
// 分工会人员只有一个可选工会时自动带入,校工会管理员仍需主动选择。
|
||||
applyUnionDefaults() {
|
||||
if (this.unionOptions.length !== 1) return
|
||||
const union = this.unionOptions[0]
|
||||
if (!this.formData.unionId) {
|
||||
this.formData.unionId = union.id
|
||||
this.formData.unionName = union.name
|
||||
}
|
||||
|
||||
},
|
||||
// 编辑状态下加载内容详情并还原轮播图片数组。
|
||||
loadDetail(id) {
|
||||
if (!id) return Promise.resolve()
|
||||
this.loading = true
|
||||
return this.$axios.post(loc() + "/detailData", {id}).then(resp => {
|
||||
if (resp.code === 0) {
|
||||
this.formData = Object.assign(this.defaultForm(), resp.data || {})
|
||||
this.sliderMeta = this.parseSliderImages(this.formData.sliderImages)
|
||||
this.sliderFiles = this.sliderMeta.map(item => item.src)
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
}).finally(() => this.loading = false)
|
||||
},
|
||||
// 兼容历史图片地址数组,并转换为只包含图片地址和标题的轮播对象。
|
||||
parseSliderImages(value) {
|
||||
if (!value) return []
|
||||
let items = value
|
||||
if (!Array.isArray(items)) {
|
||||
try {
|
||||
items = JSON.parse(value)
|
||||
} catch (e) {
|
||||
items = String(value).split(",").filter(Boolean)
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(items)) return []
|
||||
return items.map(item => {
|
||||
if (typeof item === "string") {
|
||||
return {src: item, title: ""}
|
||||
}
|
||||
return {
|
||||
src: item.src || item.url || item.data || (item.response && item.response.data) || "",
|
||||
title: item.title || ""
|
||||
}
|
||||
}).filter(item => item.src)
|
||||
},
|
||||
// 取得上传组件返回的图片地址。
|
||||
getSliderFileUrl(item) {
|
||||
if (typeof item === "string") return item
|
||||
return item && (item.url || item.data || (item.response && item.response.data)) || ""
|
||||
},
|
||||
// 上传或删除图片后同步图片元数据,并保留已经填写的标题和工会。
|
||||
syncSliderMeta() {
|
||||
const oldItems = {}
|
||||
this.sliderMeta.forEach(item => oldItems[item.src] = item)
|
||||
this.sliderMeta = (this.sliderFiles || []).map(item => this.getSliderFileUrl(item)).filter(Boolean).map(src => {
|
||||
return oldItems[src] || {src: src, title: ""}
|
||||
})
|
||||
this.applyUnionDefaults()
|
||||
},
|
||||
// 删除单张轮播图片,同时更新上传组件和图片配置。
|
||||
removeSliderImage(index) {
|
||||
const files = (this.sliderFiles || []).slice()
|
||||
files.splice(index, 1)
|
||||
this.sliderFiles = files
|
||||
this.$message.success("已删除第" + (index + 1) + "张轮播图片")
|
||||
},
|
||||
// 调整轮播图片顺序,保存和展示均使用调整后的顺序。
|
||||
moveSliderImage(index, offset) {
|
||||
const targetIndex = index + offset
|
||||
if (targetIndex < 0 || targetIndex >= this.sliderMeta.length) return
|
||||
const files = (this.sliderFiles || []).slice()
|
||||
const file = files[index]
|
||||
files.splice(index, 1)
|
||||
files.splice(targetIndex, 0, file)
|
||||
this.sliderFiles = files
|
||||
},
|
||||
// 清空全部轮播图片,二次确认避免误操作。
|
||||
clearSliderImages() {
|
||||
this.$confirm("确定清空全部轮播图片吗?", "提示", {type: "warning"}).then(() => {
|
||||
this.sliderFiles = []
|
||||
this.sliderMeta = []
|
||||
this.$message.success("轮播图片已清空")
|
||||
}).catch(() => {})
|
||||
},
|
||||
// 将轮播图片地址和标题转换为可持久化 JSON。
|
||||
buildSliderImages() {
|
||||
this.syncSliderMeta()
|
||||
return JSON.stringify(this.sliderMeta.map(item => ({
|
||||
src: item.src,
|
||||
title: item.title
|
||||
})))
|
||||
},
|
||||
// 校验每张轮播图的标题。
|
||||
validateSliderMeta() {
|
||||
this.syncSliderMeta()
|
||||
const invalidIndex = this.sliderMeta.findIndex(item => !item.title)
|
||||
if (invalidIndex < 0) return true
|
||||
this.$message.warning("请输入第" + (invalidIndex + 1) + "张轮播图标题")
|
||||
return false
|
||||
},
|
||||
// 重置新增表单;编辑状态下恢复数据库内容。
|
||||
resetForm() {
|
||||
const id = this.getEditId()
|
||||
if (id) {
|
||||
this.loadDetail(id)
|
||||
} else {
|
||||
this.formData = this.defaultForm()
|
||||
this.sliderFiles = []
|
||||
this.sliderMeta = []
|
||||
this.applyUnionDefaults()
|
||||
}
|
||||
this.$nextTick(() => this.$refs.contentForm && this.$refs.contentForm.clearValidate())
|
||||
},
|
||||
// 校验并提交新增或编辑数据。
|
||||
submit() {
|
||||
this.$refs.contentForm.validate(valid => {
|
||||
if (!valid) return
|
||||
if (this.sliderEnabled && !this.validateSliderMeta()) return
|
||||
const data = Object.assign({}, this.formData)
|
||||
if (this.sliderEnabled) {
|
||||
data.sliderImages = this.buildSliderImages()
|
||||
} else {
|
||||
// 未开启轮播的栏目不提交隐藏字段,编辑时保留已有数据,避免切换栏目造成误删。
|
||||
delete data.sliderImages
|
||||
}
|
||||
// 推荐和头条没有对应业务逻辑,提交时不再携带这两个字段。
|
||||
delete data.isRecommend
|
||||
delete data.isHeadline
|
||||
const api = data.id ? "/doEdit" : "/doAdd"
|
||||
this.submitting = true
|
||||
this.$axios.post(loc() + api, data).then(resp => {
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg || "保存成功")
|
||||
if (!data.id) {
|
||||
this.resetForm()
|
||||
}
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
}).finally(() => this.submitting = false)
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 监听上传组件结果,及时生成对应的标题和工会填写行。
|
||||
sliderFiles: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.syncSliderMeta()
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.formData = this.defaultForm()
|
||||
const id = this.getEditId()
|
||||
Promise.all([this.loadCategories(), this.loadUnionOptions(), this.loadDetail(id)]).then(this.applyUnionDefaults)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,366 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<style>
|
||||
#sub-app-container-main-content { padding: 0 !important; background: #f5f5f5; }
|
||||
.spread-wall { min-height: 100%; overflow-x: hidden; color: #2c3e50; background: #f5f5f5; }
|
||||
.spread-hero { min-height: 450px; position: relative; padding: 18px 0 72px; display: flex; flex-direction: column; justify-content: center; overflow: visible; }
|
||||
.spread-hero::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 320px; background: url("${base!}/assets/platform/img/spread-bg.png?v=20260731") top center / cover no-repeat; z-index: 0; }
|
||||
.spread-hero > * { position: relative; z-index: 1; }
|
||||
.spread-brand { margin: 0 auto 24px; padding: 10px 16px; display: flex; align-items: center; gap: 12px; color: #2c3e50; border-radius: 12px; background: rgba(255,255,255,.76); box-shadow: 0 10px 26px rgba(0,0,0,.12); backdrop-filter: blur(6px); }
|
||||
.spread-brand-icon { width: 42px; height: 42px; display: flex; align-items: center; justify-content: center; color: #c0141a; font-size: 24px; border-radius: 50%; background: #fff0f0; }
|
||||
.spread-brand-title { font-family: "Microsoft YaHei", serif; font-size: 24px; font-weight: 700; letter-spacing: 3px; }
|
||||
.carousel-bg-text { position: absolute; top: 48%; left: 50%; transform: translate(-50%, -50%); color: rgba(255,255,255,.04); font-size: 170px; font-weight: 900; letter-spacing: 24px; pointer-events: none; user-select: none; }
|
||||
.hero-slider { width: calc(100% - 32px); max-width: none; min-height: 380px; height: auto; aspect-ratio: 32 / 9; margin: 0 auto; }
|
||||
.hero-slider .el-carousel, .hero-slider .el-carousel__container { height: 100%; }
|
||||
.hero-slider .el-carousel__arrow { width: 42px; height: 42px; color: #fff; border: 1px solid rgba(255,255,255,.25); background: rgba(44,62,80,.42); backdrop-filter: blur(4px); }
|
||||
.hero-slider .el-carousel__item--card { overflow: visible; display: flex; align-items: center; justify-content: center; }
|
||||
.hero-slider .el-carousel__mask { opacity: 0; background: transparent; }
|
||||
.slider-item { width: 97%; height: auto; aspect-ratio: 16 / 9; margin: 0 auto; position: relative; overflow: hidden; display: flex; align-items: center; justify-content: center; box-sizing: border-box; cursor: pointer; border: 8px solid #fff; background: #fff; box-shadow: 0 12px 30px rgba(0,0,0,.18), 0 2px 5px rgba(0,0,0,.12); }
|
||||
.slider-item img { width: auto; height: auto; max-width: 100%; max-height: 100%; display: block; flex: 0 0 auto; object-fit: contain !important; object-position: center; background: #fff; }
|
||||
.slider-caption { min-height: 25%; padding: 10px 18px 9px; position: absolute; left: 0; right: 0; bottom: 0; display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-end; box-sizing: border-box; color: #fff; background: linear-gradient(to bottom, rgba(82,82,82,.30) 0%, rgba(52,52,52,.52) 45%, rgba(24,24,24,.78) 100%); }
|
||||
.slider-caption-line { width: 100%; }
|
||||
.slider-caption-title { color: #fff; font-size: 15px; font-weight: 600; line-height: 1.45; text-shadow: 0 1px 3px rgba(0,0,0,.42); white-space: normal; word-break: break-all; }
|
||||
.slider-caption-tag { max-width: 32%; margin: 0 8px 0 0; padding: 1px 7px; display: inline-block; overflow: hidden; box-sizing: border-box; color: rgba(255,255,255,.92); font-size: 10px; font-weight: 400; line-height: 18px; vertical-align: middle; text-align: center; text-overflow: ellipsis; white-space: nowrap; border: 1px solid rgba(255,255,255,.42); border-radius: 3px; background: rgba(255,255,255,.10); }
|
||||
.slider-empty { height: 380px; display: flex; align-items: center; justify-content: center; color: #8a6f68; font-size: 22px; letter-spacing: 5px; border-radius: 12px; background: rgba(255,255,255,.86); box-shadow: 0 8px 28px rgba(0,0,0,.08); }
|
||||
.spread-main { width: calc(100% - 32px); max-width: none; margin: -42px auto 40px; padding: 20px 0 38px; position: relative; z-index: 5; display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: 24px; align-items: start; }
|
||||
.category-panel, .content-panel { background: #fff; border-radius: 12px; box-shadow: 0 8px 28px rgba(90,53,34,.09); }
|
||||
.category-panel { position: sticky; top: 20px; overflow: hidden; }
|
||||
.category-header { padding: 18px 15px; color: #fff; font-family: "Microsoft YaHei", serif; font-size: 18px; font-weight: 700; text-align: center; letter-spacing: 2px; background: #c0141a; }
|
||||
.category-group { border-bottom: 1px solid #f0e7e5; }
|
||||
.category-parent { padding: 18px 20px; display: flex; align-items: center; gap: 10px; color: #2c3e50; cursor: pointer; transition: color .2s, background .2s; user-select: none; }
|
||||
.category-parent:hover, .category-parent.open, .category-parent.active { color: #c0141a; background: #fafafa; }
|
||||
.category-parent-name { flex: 1; font-size: 15px; }
|
||||
.category-arrow { font-size: 13px; transition: transform .25s ease; }
|
||||
.category-parent.open .category-arrow { transform: rotate(90deg); }
|
||||
.category-children { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .3s ease; }
|
||||
.category-children.open { grid-template-rows: 1fr; }
|
||||
.category-children-inner { min-height: 0; overflow: hidden; }
|
||||
.category-child { padding: 14px 20px 14px 32px; display: flex; align-items: center; gap: 10px; color: #606266; cursor: pointer; border-left: 3px solid transparent; transition: color .2s, background .2s; }
|
||||
.category-child::before { content: ""; width: 5px; height: 5px; flex: 0 0 5px; border-radius: 50%; background: #c0c4cc; }
|
||||
.category-child:hover, .category-child.active { color: #c0141a; background: #fff5f5; border-left-color: #c0141a; }
|
||||
.category-child:hover::before, .category-child.active::before { background: #c0141a; }
|
||||
.content-panel { min-height: 600px; padding: 30px; }
|
||||
.section-heading { margin-bottom: 22px; padding-bottom: 16px; display: flex; align-items: flex-end; justify-content: space-between; border-bottom: 1px solid #ebeef5; }
|
||||
.section-heading h2 { margin: 0 0 4px; color: #2c3e50; font-family: "Microsoft YaHei", serif; font-size: 24px; }
|
||||
.section-heading span { color: #909399; font-size: 14px; }
|
||||
.content-card { margin-bottom: 16px; padding: 20px; display: grid; grid-template-columns: 80px minmax(0,1fr) 90px; gap: 20px; align-items: center; cursor: pointer; border: 1px solid #ebeef5; border-radius: 8px; transition: transform .2s, border-color .2s, box-shadow .2s; }
|
||||
.content-card:hover { transform: translateY(-2px); border-color: #f3c8c6; box-shadow: 0 8px 20px rgba(192,20,26,.08); }
|
||||
.content-date { color: #909399; text-align: center; border-right: 1px solid #ebeef5; }
|
||||
.content-date strong { display: block; color: #2c3e50; font-size: 25px; }
|
||||
.content-card:hover .content-date strong { color: #c0141a; }
|
||||
.content-title { margin-bottom: 8px; color: #303133; font-size: 17px; font-weight: 700; }
|
||||
.content-summary { overflow: hidden; color: #7c6d67; font-size: 13px; line-height: 21px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.content-view { color: #aa9d96; font-size: 12px; text-align: right; }
|
||||
.wall-pagination { margin-top: 25px; text-align: center; }
|
||||
.detail-title { margin: 8px 0 12px; color: #672025; font-size: 28px; text-align: center; }
|
||||
.detail-meta { padding-bottom: 18px; color: #9c8d86; text-align: center; border-bottom: 1px solid #eee2d9; }
|
||||
.detail-cover { max-width: 100%; max-height: 520px; margin: 24px auto; display: block; object-fit: contain; border-radius: 8px; }
|
||||
.detail-body { width: 100%; overflow: hidden; color: #493b36; font-size: 16px; line-height: 1.9; word-break: break-word; }
|
||||
.detail-body img, .detail-body video { max-width: 100% !important; height: auto !important; }
|
||||
.detail-body table { max-width: 100% !important; }
|
||||
.back-link { margin-bottom: 12px; }
|
||||
@media (max-width: 900px) {
|
||||
.spread-hero { min-height: 360px; padding-bottom: 50px; }
|
||||
.spread-hero::before { height: 260px; }
|
||||
.hero-slider, .hero-slider .el-carousel, .hero-slider .el-carousel__container { height: 280px; }
|
||||
.spread-main { width: calc(100% - 24px); grid-template-columns: 1fr; margin-top: -20px; }
|
||||
.category-panel { position: static; }
|
||||
.content-panel { padding: 20px 15px; }
|
||||
.content-card { grid-template-columns: 64px minmax(0,1fr); }
|
||||
.content-view { display: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" class="spread-wall" v-cloak>
|
||||
<header class="spread-hero">
|
||||
<div class="spread-brand">
|
||||
<span class="spread-brand-icon"><i class="el-icon-trophy"></i></span>
|
||||
<span class="spread-brand-title">风采墙</span>
|
||||
</div>
|
||||
<div class="carousel-bg-text">HONOR</div>
|
||||
<div class="hero-slider">
|
||||
<el-carousel v-if="sliderItems.length" type="card" height="100%" :interval="4000"
|
||||
indicator-position="none" arrow="hover" :autoplay="sliderItems.length > 1" loop>
|
||||
<el-carousel-item v-for="item in sliderItems" :key="item.slideKey">
|
||||
<div class="slider-item" @click="openDetail(item)">
|
||||
<img v-if="item.sliderImage" :src="item.sliderImage" :alt="item.title">
|
||||
<div class="slider-caption">
|
||||
<div class="slider-caption-line">
|
||||
<div class="slider-caption-title" :title="item.title">
|
||||
<span class="slider-caption-tag">{{item.unionName || item.categoryName || '风采展示'}}</span>{{item.title}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
<div v-else class="slider-empty">榜样引领 · 风采绽放</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="spread-main">
|
||||
<aside class="category-panel">
|
||||
<div class="category-header">工会信息传播与文化展示</div>
|
||||
<div v-for="parent in categories" :key="parent.id" class="category-group">
|
||||
<div class="category-parent" :class="{open: isParentOpen(parent), active: isParentActive(parent)}" @click="toggleParent(parent)">
|
||||
<i class="el-icon-tickets"></i>
|
||||
<span class="category-parent-name">{{parent.name}}</span>
|
||||
<i v-if="parent.children && parent.children.length" class="el-icon-arrow-right category-arrow"></i>
|
||||
</div>
|
||||
<div v-if="parent.children && parent.children.length"
|
||||
class="category-children" :class="{open: isParentOpen(parent)}">
|
||||
<div class="category-children-inner">
|
||||
<div v-for="child in parent.children" :key="child.id"
|
||||
class="category-child" :class="{active: currentCategory.id === child.id}"
|
||||
@click="selectCategory(child)">{{child.name}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="content-panel" v-loading="loading">
|
||||
<template v-if="!detail">
|
||||
<div class="section-heading">
|
||||
<h2>{{currentCategory.name || '风采展示'}}</h2>
|
||||
<span>展示教职工的先进事迹与精彩瞬间</span>
|
||||
</div>
|
||||
<article v-for="item in pageData.list" :key="item.id" class="content-card" @click="openDetail(item)">
|
||||
<div class="content-date">
|
||||
<strong>{{datePart(item.publishedAt, 'DD')}}</strong>
|
||||
{{datePart(item.publishedAt, 'YYYY.MM')}}
|
||||
</div>
|
||||
<div>
|
||||
<div class="content-title">
|
||||
<el-tag v-if="item.isTop === 1" type="danger" size="mini">置顶</el-tag>
|
||||
{{item.title}}
|
||||
</div>
|
||||
<div class="content-summary">{{item.summary || item.subTitle || '点击查看详细内容'}}</div>
|
||||
</div>
|
||||
<div class="content-view"><i class="el-icon-view"></i> {{item.viewCount || 0}}</div>
|
||||
</article>
|
||||
<el-empty v-if="!pageData.list.length && !loading" description="暂无风采内容"></el-empty>
|
||||
<el-pagination v-if="pageData.totalCount > pageData.pageSize" class="wall-pagination"
|
||||
background layout="prev, pager, next" :current-page.sync="pageData.pageNumber"
|
||||
:page-size="pageData.pageSize" :total="pageData.totalCount"
|
||||
@current-change="loadPage"></el-pagination>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button class="back-link" icon="el-icon-back" plain size="small" @click="backToList">返回列表</el-button>
|
||||
<h1 class="detail-title">{{detail.title}}</h1>
|
||||
<div class="detail-meta">
|
||||
{{formatTime(detail.publishedAt || detail.createdAt)}}
|
||||
{{detail.categoryName}} <i class="el-icon-view"></i> {{detail.viewCount || 0}}
|
||||
</div>
|
||||
<img v-if="getCoverUrl(detail.thumbUrl)" class="detail-cover" :src="getCoverUrl(detail.thumbUrl)" :alt="detail.title">
|
||||
<div class="detail-body" v-html="detail.contentBody"></div>
|
||||
</template>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
apiBase: "/platform/spread/display",
|
||||
loading: false,
|
||||
categories: [],
|
||||
sliderItems: [],
|
||||
openParents: [],
|
||||
currentCategory: {},
|
||||
detail: null,
|
||||
pageData: {pageNumber: 1, pageSize: 8, totalCount: 0, list: []}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 加载可见栏目并自动选择第一个末级栏目。
|
||||
loadCategories() {
|
||||
return this.$axios.post(this.apiBase + "/categoryTree").then(resp => {
|
||||
if (resp.code !== 0) return
|
||||
this.categories = resp.data || []
|
||||
const firstParent = this.categories[0]
|
||||
this.openParents = firstParent ? [firstParent.id] : []
|
||||
this.currentCategory = firstParent && firstParent.children && firstParent.children.length
|
||||
? firstParent.children[0] : (firstParent || {})
|
||||
})
|
||||
},
|
||||
// 判断父栏目是否处于展开状态。
|
||||
isParentOpen(parent) {
|
||||
return this.openParents.indexOf(parent.id) !== -1
|
||||
},
|
||||
// 当前栏目属于该父栏目时保持父级高亮。
|
||||
isParentActive(parent) {
|
||||
if (this.currentCategory.id === parent.id) return true
|
||||
return (parent.children || []).some(child => child.id === this.currentCategory.id)
|
||||
},
|
||||
// 点击父栏目时展开或关闭子节点;无子节点时直接查询该栏目。
|
||||
toggleParent(parent) {
|
||||
const children = parent.children || []
|
||||
if (!children.length) {
|
||||
this.selectCategory(parent)
|
||||
return
|
||||
}
|
||||
const index = this.openParents.indexOf(parent.id)
|
||||
if (index === -1) {
|
||||
this.openParents.push(parent.id)
|
||||
} else {
|
||||
this.openParents.splice(index, 1)
|
||||
}
|
||||
},
|
||||
// 加载开启轮播栏目的最新发布内容。
|
||||
loadSlider() {
|
||||
return this.$axios.post(this.apiBase + "/sliderData").then(resp => {
|
||||
if (resp.code === 0) {
|
||||
this.sliderItems = this.buildSliderItems(resp.data || [])
|
||||
}
|
||||
})
|
||||
},
|
||||
// 根据当前栏目加载已发布内容。
|
||||
loadPage() {
|
||||
if (!this.currentCategory.id) {
|
||||
this.pageData.list = []
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
this.$axios.post(this.apiBase + "/pageData", {
|
||||
categoryId: this.currentCategory.id,
|
||||
pageNumber: this.pageData.pageNumber,
|
||||
pageSize: this.pageData.pageSize
|
||||
}).then(resp => {
|
||||
if (resp.code === 0) this.pageData = resp.data
|
||||
}).finally(() => this.loading = false)
|
||||
},
|
||||
// 切换栏目时回到第一页和列表状态。
|
||||
selectCategory(category) {
|
||||
this.currentCategory = category
|
||||
this.detail = null
|
||||
this.pageData.pageNumber = 1
|
||||
const parent = this.categories.find(item => (item.children || []).some(child => child.id === category.id))
|
||||
if (parent && !this.isParentOpen(parent)) {
|
||||
this.openParents.push(parent.id)
|
||||
}
|
||||
this.loadPage()
|
||||
},
|
||||
// 根据内容所属栏目同步左侧栏目选中状态,并展开对应父栏目。
|
||||
syncCategorySelection(categoryId) {
|
||||
if (!categoryId) return
|
||||
for (const parent of this.categories) {
|
||||
if (parent.id === categoryId) {
|
||||
this.currentCategory = parent
|
||||
return
|
||||
}
|
||||
const child = (parent.children || []).find(item => item.id === categoryId)
|
||||
if (child) {
|
||||
this.currentCategory = child
|
||||
if (!this.isParentOpen(parent)) this.openParents.push(parent.id)
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
// 从详情返回时,按当前回显栏目重新加载内容列表。
|
||||
backToList() {
|
||||
this.detail = null
|
||||
this.pageData.pageNumber = 1
|
||||
this.loadPage()
|
||||
},
|
||||
// 获取完整详情并登记一次浏览。
|
||||
openDetail(item) {
|
||||
this.loading = true
|
||||
Promise.all([
|
||||
this.$axios.post(this.apiBase + "/detailData", {id: item.id}),
|
||||
this.$axios.post(this.apiBase + "/addView", {id: item.id})
|
||||
]).then(values => {
|
||||
if (values[0].code === 0) {
|
||||
this.detail = values[0].data
|
||||
this.detail.viewCount = Number(this.detail.viewCount || 0) + 1
|
||||
this.syncCategorySelection(this.detail.categoryId || item.categoryId)
|
||||
// 轮播区域较高,打开详情后滚动到正文区域完整展示内容。
|
||||
this.$nextTick(() => {
|
||||
const main = document.querySelector(".spread-main")
|
||||
if (main) main.scrollIntoView({behavior: "smooth", block: "start"})
|
||||
})
|
||||
}
|
||||
}).finally(() => this.loading = false)
|
||||
},
|
||||
// 将上传字段转换为完整图片地址数组。
|
||||
getImageUrls(value) {
|
||||
if (!value) return []
|
||||
let files = value
|
||||
if (typeof value === "string" && value.trim().startsWith("[")) {
|
||||
try {
|
||||
files = JSON.parse(value)
|
||||
} catch (e) {
|
||||
files = []
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(files)) {
|
||||
files = String(files).split(",")
|
||||
}
|
||||
return files.map(file => {
|
||||
if (typeof file === "string") return file.trim()
|
||||
return file && (file.src || file.url || file.data || (file.response && file.response.data)) || ""
|
||||
}).filter(Boolean)
|
||||
},
|
||||
// 从上传字段中取得第一张图片地址。
|
||||
getCoverUrl(value) {
|
||||
const urls = this.getImageUrls(value)
|
||||
return urls.length ? urls[0] : ""
|
||||
},
|
||||
// 将轮播图片解析为图片地址和标题,兼容历史纯地址数组。
|
||||
getSliderEntries(value) {
|
||||
if (!value) return []
|
||||
let items = value
|
||||
if (!Array.isArray(items)) {
|
||||
try {
|
||||
items = JSON.parse(value)
|
||||
} catch (e) {
|
||||
items = String(value).split(",").filter(Boolean)
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(items)) return []
|
||||
return items.map(item => {
|
||||
if (typeof item === "string") return {src: item}
|
||||
return {
|
||||
src: item.src || item.url || item.data || (item.response && item.response.data) || "",
|
||||
title: item.title || ""
|
||||
}
|
||||
}).filter(item => item.src)
|
||||
},
|
||||
// 每篇内容的多张轮播图片分别生成轮播项,图片标题独立,工会名称继承内容。
|
||||
buildSliderItems(contents) {
|
||||
const result = []
|
||||
const source = contents || []
|
||||
source.forEach(item => {
|
||||
let slides = this.getSliderEntries(item.sliderImages)
|
||||
if (!slides.length) {
|
||||
slides = this.getImageUrls(item.thumbUrl).map(url => ({src: url}))
|
||||
}
|
||||
slides.forEach((slide, index) => {
|
||||
result.push(Object.assign({}, item, {
|
||||
title: slide.title || item.title,
|
||||
sliderImage: slide.src,
|
||||
slideKey: item.id + "-" + index
|
||||
}))
|
||||
})
|
||||
})
|
||||
return result
|
||||
},
|
||||
// 格式化列表日期的指定部分。
|
||||
datePart(value, format) {
|
||||
return value ? this.$moment(Number(value)).format(format) : "--"
|
||||
},
|
||||
// 格式化详情发布时间。
|
||||
formatTime(value) {
|
||||
return value ? this.$moment(Number(value)).format("YYYY-MM-DD HH:mm") : ""
|
||||
}
|
||||
},
|
||||
created() {
|
||||
Promise.all([this.loadCategories(), this.loadSlider()]).then(() => this.loadPage())
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,203 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<el-card shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="内容标题">
|
||||
<el-input v-model="pageForm.title" clearable placeholder="请输入标题" @keyup.enter.native="doSearch"></el-input>
|
||||
</search-item>
|
||||
<search-item label="所属栏目">
|
||||
<el-cascader v-model="pageForm.categoryId" :options="categoryOptions"
|
||||
:props="{value:'id',label:'name',children:'children',emitPath:false,checkStrictly:true}"
|
||||
clearable filterable style="width: 100%"></el-cascader>
|
||||
</search-item>
|
||||
<search-item label="发布状态">
|
||||
<el-select v-model="pageForm.isPublished" clearable placeholder="请选择">
|
||||
<el-option label="草稿" :value="0"></el-option>
|
||||
<el-option label="已发布" :value="1"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="创建时间">
|
||||
<el-date-picker v-model="pageForm.createTime" type="datetimerange"
|
||||
start-placeholder="开始时间" end-placeholder="结束时间"
|
||||
value-format="yyyy-MM-dd HH:mm:ss" style="width: 100%"></el-date-picker>
|
||||
</search-item>
|
||||
<div class="search-query">
|
||||
<el-button type="primary" size="medium" icon="el-icon-search" @click="doSearch">搜索</el-button>
|
||||
<el-button size="medium" @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt10" shadow="never">
|
||||
<table-tool :app="this" label="风采内容列表">
|
||||
<el-button type="primary" size="medium" icon="el-icon-plus" @click="goCreate">发布内容</el-button>
|
||||
</table-tool>
|
||||
<el-table v-loading="tableLoading" :data="tableData" :size="tableSize" border>
|
||||
<el-table-column :index="indexMethod" type="index" label="序号" width="70" align="center"></el-table-column>
|
||||
<el-table-column prop="title" label="标题" min-width="240" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="categoryName" label="栏目" min-width="180">
|
||||
<template slot-scope="{row}">
|
||||
<span>{{row.categoryName}}</span>
|
||||
<el-tag v-if="categorySliderEnabled(row.categoryId)" type="success" size="mini"
|
||||
effect="plain" style="margin-left: 6px">轮播</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="unionName" label="所属工会" min-width="150" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="isTop" label="置顶" width="100" align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag :type="row.isTop === 1 ? 'success' : 'info'" size="mini">
|
||||
{{row.isTop === 1 ? '置顶' : '未置顶'}}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="viewCount" label="浏览量" width="100" align="center"></el-table-column>
|
||||
<el-table-column prop="sortOrder" label="排序" width="90" align="center"></el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag :type="row.isPublished === 1 ? 'success' : 'info'" size="mini">
|
||||
{{row.isPublished === 1 ? '已发布' : '草稿'}}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发布时间" width="170" align="center">
|
||||
<template slot-scope="{row}">{{formatTime(row.publishedAt)}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="270" align="center" fixed="right">
|
||||
<template slot-scope="{row}">
|
||||
<el-button size="mini" type="primary" @click="goEdit(row)">编辑</el-button>
|
||||
<el-button size="mini" :type="row.isPublished === 1 ? 'warning' : 'success'"
|
||||
@click="togglePublish(row)">
|
||||
{{row.isPublished === 1 ? '撤回' : '发布'}}
|
||||
</el-button>
|
||||
<el-button size="mini" type="danger" @click="remove(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
</guava>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
apiBase: "/platform/spread/manage",
|
||||
categoryOptions: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 组装分页查询参数,将时间范围拆成后端字段。
|
||||
buildPageParams() {
|
||||
const params = Object.assign({}, this.pageForm)
|
||||
params.beginTime = params.createTime && params.createTime.length ? params.createTime[0] : ""
|
||||
params.endTime = params.createTime && params.createTime.length ? params.createTime[1] : ""
|
||||
delete params.createTime
|
||||
return params
|
||||
},
|
||||
// 加载内容管理分页数据。
|
||||
pageData() {
|
||||
this.tableLoading = true
|
||||
this.$axios.post(this.apiBase + "/pageData", this.buildPageParams()).then(resp => {
|
||||
if (resp.code === 0) {
|
||||
this.tableData = resp.data.list || []
|
||||
this.pageForm.totalCount = resp.data.totalCount || 0
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
}).finally(() => this.tableLoading = false)
|
||||
},
|
||||
// 加载可筛选的栏目树。
|
||||
loadCategories() {
|
||||
return this.$axios.post(this.apiBase + "/categoryTree").then(resp => {
|
||||
if (resp.code === 0) this.categoryOptions = resp.data || []
|
||||
})
|
||||
},
|
||||
// 判断内容所属栏目及其全部上级栏目是否都已开启轮播。
|
||||
categorySliderEnabled(categoryId) {
|
||||
const findPath = (nodes, parents) => {
|
||||
for (const node of nodes || []) {
|
||||
const path = parents.concat(node)
|
||||
if (node.id === categoryId) return path
|
||||
const childPath = findPath(node.children, path)
|
||||
if (childPath) return childPath
|
||||
}
|
||||
return null
|
||||
}
|
||||
const categoryPath = findPath(this.categoryOptions, [])
|
||||
return !!categoryPath && categoryPath.every(item => Number(item.slider) === 1)
|
||||
},
|
||||
// 清空筛选条件并重新查询。
|
||||
resetSearch() {
|
||||
this.pageForm.title = ""
|
||||
this.pageForm.categoryId = ""
|
||||
this.pageForm.isPublished = ""
|
||||
this.pageForm.createTime = []
|
||||
this.doSearch()
|
||||
},
|
||||
// 跳转到独立发布页面。
|
||||
goCreate() {
|
||||
this.navigate("/platform/spread/creation")
|
||||
},
|
||||
// 跳转到发布页面并携带编辑 ID。
|
||||
goEdit(row) {
|
||||
this.navigate("/platform/spread/creation?id=" + encodeURIComponent(row.id))
|
||||
},
|
||||
// 兼容 pjax 与普通页面跳转。
|
||||
navigate(url) {
|
||||
if (window.commonUtil && commonUtil.pjaxPush) {
|
||||
commonUtil.pjaxPush(url)
|
||||
} else {
|
||||
window.location.href = url
|
||||
}
|
||||
},
|
||||
// 发布已保存的草稿,或撤回已发布内容。
|
||||
togglePublish(row) {
|
||||
const status = row.isPublished === 1 ? 0 : 1
|
||||
const action = status === 1 ? "发布" : "撤回"
|
||||
this.$confirm("确定" + action + "“" + row.title + "”吗?", "提示", {type: "warning"}).then(() => {
|
||||
this.$axios.post(this.apiBase + "/doPublish", {id: row.id, status}).then(resp => {
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(action + "成功")
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
// 删除内容前进行二次确认。
|
||||
remove(row) {
|
||||
this.$confirm("删除后无法恢复,确定删除该内容吗?", "提示", {type: "warning"}).then(() => {
|
||||
this.$axios.post(this.apiBase + "/doDelete", {id: row.id}).then(resp => {
|
||||
if (resp.code === 0) {
|
||||
this.$message.success("删除成功")
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
// 将毫秒时间戳格式化为日期时间。
|
||||
formatTime(value) {
|
||||
return value ? this.$moment(Number(value)).format("YYYY-MM-DD HH:mm:ss") : ""
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$set(this.pageForm, "title", "")
|
||||
this.$set(this.pageForm, "categoryId", "")
|
||||
this.$set(this.pageForm, "isPublished", "")
|
||||
this.$set(this.pageForm, "createTime", [])
|
||||
this.loadCategories().then(() => this.pageData())
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,252 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<script src="${base!}/assets/platform/plugins/echarts/echarts.min.js" nonce="${cspNonce!}"></script>
|
||||
<style>
|
||||
#sub-app-container-main-content { background: #f4f6f8; }
|
||||
.spread-statistics { width: 100%; }
|
||||
.spread-statistics .filter-card { margin-bottom: 12px; }
|
||||
.spread-statistics .metrics-card { margin-bottom: 18px; border: 0; border-radius: 12px; }
|
||||
.spread-statistics .metrics-card > .el-card__body { padding: 24px; }
|
||||
.spread-statistics .stat-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 18px; }
|
||||
.spread-statistics .metric-tile { min-width: 0; min-height: 108px; padding: 20px; display: flex; align-items: center; gap: 16px; box-sizing: border-box; border: 1px solid #e8edf3; border-radius: 13px; background: #fff; box-shadow: 0 3px 14px rgba(15,23,42,.06); transition: transform .25s, box-shadow .25s; }
|
||||
.spread-statistics .metric-tile:hover { transform: translateY(-2px); box-shadow: 0 8px 22px rgba(15,23,42,.11); }
|
||||
.spread-statistics .metric-icon { width: 58px; height: 58px; flex: 0 0 58px; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 27px; border-radius: 14px; }
|
||||
.spread-statistics .metric-icon.purple { background: linear-gradient(135deg,#667eea,#764ba2); }
|
||||
.spread-statistics .metric-icon.green { background: linear-gradient(135deg,#43e97b,#38f9d7); }
|
||||
.spread-statistics .metric-icon.orange { background: linear-gradient(135deg,#f7971e,#ffd200); }
|
||||
.spread-statistics .metric-icon.blue { background: linear-gradient(135deg,#4facfe,#00d7ef); }
|
||||
.spread-statistics .metric-icon.pink { background: linear-gradient(135deg,#f093fb,#f5576c); }
|
||||
.spread-statistics .metric-info { min-width: 0; }
|
||||
.spread-statistics .stat-label { margin-bottom: 6px; overflow: hidden; color: #8492a6; font-size: 15px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.spread-statistics .stat-value { color: #17233d; font-size: 31px; font-weight: 700; line-height: 1; }
|
||||
.spread-statistics .chart-grid { display: grid; grid-template-columns: minmax(360px, 1fr) minmax(0, 2fr); gap: 14px; }
|
||||
.spread-statistics .chart-card { min-width: 0; border: 0; border-radius: 11px; }
|
||||
.spread-statistics .chart-card > .el-card__header { padding: 26px 26px 18px; font-size: 18px; border-bottom: 1px solid #e4e7ed; }
|
||||
.spread-statistics .chart-card > .el-card__body { padding: 8px 24px 18px; }
|
||||
.spread-statistics .chart-box { width: 100%; height: 410px; }
|
||||
.spread-statistics .top-table-card { margin-top: 14px; border: 0; border-radius: 11px; }
|
||||
.spread-statistics .top-table-card > .el-card__header { padding: 20px 24px; font-size: 17px; }
|
||||
@media(max-width: 1199px) { .spread-statistics .stat-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
|
||||
@media(max-width: 980px) { .spread-statistics .stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .spread-statistics .chart-grid { grid-template-columns: 1fr; } }
|
||||
@media(max-width: 560px) { .spread-statistics .stat-grid { grid-template-columns: 1fr; } .spread-statistics .metrics-card > .el-card__body { padding: 12px; } }
|
||||
</style>
|
||||
<div id="app" class="spread-statistics" v-cloak>
|
||||
<guava ref="guava">
|
||||
<el-card class="filter-card" shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="所属栏目">
|
||||
<el-cascader v-model="query.categoryId" :options="categoryOptions"
|
||||
:props="{value:'id',label:'name',children:'children',emitPath:false,checkStrictly:true}"
|
||||
clearable filterable style="width: 100%"></el-cascader>
|
||||
</search-item>
|
||||
<search-item label="发布状态">
|
||||
<el-select v-model="query.isPublished" clearable placeholder="全部">
|
||||
<el-option label="草稿" :value="0"></el-option>
|
||||
<el-option label="已发布" :value="1"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="创建时间">
|
||||
<el-date-picker v-model="query.createTime" type="datetimerange"
|
||||
value-format="yyyy-MM-dd HH:mm:ss" start-placeholder="开始时间"
|
||||
end-placeholder="结束时间" style="width: 100%"></el-date-picker>
|
||||
</search-item>
|
||||
<div class="search-query">
|
||||
<el-button type="primary" size="medium" icon="el-icon-search" @click="loadAll">查询</el-button>
|
||||
<el-button size="medium" @click="resetQuery">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card class="metrics-card" shadow="never" v-loading="loading">
|
||||
<div class="stat-grid">
|
||||
<div class="metric-tile">
|
||||
<div class="metric-icon purple"><i class="el-icon-folder-opened"></i></div>
|
||||
<div class="metric-info"><div class="stat-label">栏目总数</div><div class="stat-value">{{summary.categoryCount || 0}}</div></div>
|
||||
</div>
|
||||
<div class="metric-tile">
|
||||
<div class="metric-icon purple"><i class="el-icon-document"></i></div>
|
||||
<div class="metric-info"><div class="stat-label">内容总数</div><div class="stat-value">{{summary.contentCount || 0}}</div></div>
|
||||
</div>
|
||||
<div class="metric-tile">
|
||||
<div class="metric-icon green"><i class="el-icon-circle-check"></i></div>
|
||||
<div class="metric-info"><div class="stat-label">已发布</div><div class="stat-value">{{summary.publishedCount || 0}}</div></div>
|
||||
</div>
|
||||
<div class="metric-tile">
|
||||
<div class="metric-icon orange"><i class="el-icon-time"></i></div>
|
||||
<div class="metric-info"><div class="stat-label">未发布</div><div class="stat-value">{{unpublishedCount}}</div></div>
|
||||
</div>
|
||||
<div class="metric-tile">
|
||||
<div class="metric-icon blue"><i class="el-icon-view"></i></div>
|
||||
<div class="metric-info"><div class="stat-label">总访问量</div><div class="stat-value">{{summary.viewCount || 0}}</div></div>
|
||||
</div>
|
||||
<div class="metric-tile">
|
||||
<div class="metric-icon pink"><i class="el-icon-data-line"></i></div>
|
||||
<div class="metric-info"><div class="stat-label">平均访问量</div><div class="stat-value">{{summary.avgViewCount || 0}}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<div class="chart-grid">
|
||||
<el-card class="chart-card" shadow="never">
|
||||
<div slot="header"><b>栏目访问量占比</b></div>
|
||||
<div ref="categoryChart" class="chart-box"></div>
|
||||
</el-card>
|
||||
<el-card class="chart-card" shadow="never">
|
||||
<div slot="header"><b>新闻内容访问量 Top10</b></div>
|
||||
<div ref="topChart" class="chart-box"></div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<el-card class="top-table-card" shadow="never" v-loading="loading">
|
||||
<div slot="header"><b>新闻内容访问量 Top10</b></div>
|
||||
<el-table :data="topData" stripe border style="width: 100%">
|
||||
<el-table-column type="index" label="序号" width="70" align="center"></el-table-column>
|
||||
<el-table-column prop="categoryName" label="栏目" min-width="120" align="center"></el-table-column>
|
||||
<el-table-column prop="title" label="标题" min-width="260" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="summary" label="摘要" min-width="260" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="unionName" label="所属工会" min-width="150" show-overflow-tooltip align="center"></el-table-column>
|
||||
<el-table-column label="发布时间" width="170" align="center">
|
||||
<template slot-scope="{row}">{{formatTime(row.publishedAt)}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="viewCount" label="访问量" width="100" align="center"></el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</guava>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
apiBase: "/platform/spread/statistics",
|
||||
loading: false,
|
||||
query: {categoryId: "", isPublished: "", createTime: []},
|
||||
categoryOptions: [],
|
||||
summary: {},
|
||||
topData: [],
|
||||
categoryData: [],
|
||||
topChart: null,
|
||||
categoryChart: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 未发布数量由内容总数减去已发布数量得到。
|
||||
unpublishedCount() {
|
||||
return Math.max(Number(this.summary.contentCount || 0) - Number(this.summary.publishedCount || 0), 0)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 将时间范围拆分成统计接口参数。
|
||||
buildParams() {
|
||||
return {
|
||||
categoryId: this.query.categoryId,
|
||||
isPublished: this.query.isPublished,
|
||||
beginTime: this.query.createTime && this.query.createTime.length ? this.query.createTime[0] : "",
|
||||
endTime: this.query.createTime && this.query.createTime.length ? this.query.createTime[1] : ""
|
||||
}
|
||||
},
|
||||
// 加载栏目筛选树。
|
||||
loadCategories() {
|
||||
return this.$axios.post(this.apiBase + "/categoryTree").then(resp => {
|
||||
if (resp.code === 0) this.categoryOptions = resp.data || []
|
||||
})
|
||||
},
|
||||
// 并行加载卡片、排行和栏目统计,减少页面等待时间。
|
||||
loadAll() {
|
||||
this.loading = true
|
||||
const params = this.buildParams()
|
||||
Promise.all([
|
||||
this.$axios.post(this.apiBase + "/summary", params),
|
||||
this.$axios.post(this.apiBase + "/top10", params),
|
||||
this.$axios.post(this.apiBase + "/categoryData", params)
|
||||
]).then(values => {
|
||||
this.summary = values[0].code === 0 ? (values[0].data || {}) : {}
|
||||
this.topData = values[1].code === 0 ? (values[1].data || []) : []
|
||||
this.categoryData = values[2].code === 0 ? (values[2].data || []) : []
|
||||
this.$nextTick(this.renderCharts)
|
||||
}).finally(() => this.loading = false)
|
||||
},
|
||||
// 清空筛选并重新统计。
|
||||
resetQuery() {
|
||||
this.query = {categoryId: "", isPublished: "", createTime: []}
|
||||
this.loadAll()
|
||||
},
|
||||
// 创建或刷新两个统计图表。
|
||||
renderCharts() {
|
||||
if (!window.echarts) return
|
||||
if (!this.topChart) this.topChart = echarts.init(this.$refs.topChart)
|
||||
if (!this.categoryChart) this.categoryChart = echarts.init(this.$refs.categoryChart)
|
||||
this.topChart.setOption({
|
||||
color: ["#5b8ff9"],
|
||||
tooltip: {trigger: "axis", axisPointer: {type: "shadow"}},
|
||||
grid: {left: 58, right: 28, top: 42, bottom: 82},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: this.topData.map(item => item.title),
|
||||
axisTick: {show: false},
|
||||
axisLine: {lineStyle: {color: "#cfd5df"}},
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
rotate: 24,
|
||||
color: "#606266",
|
||||
formatter: value => value.length > 12 ? value.slice(0, 12) + "…" : value
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
minInterval: 1,
|
||||
axisLine: {show: false},
|
||||
axisTick: {show: false},
|
||||
splitLine: {lineStyle: {color: "#dfe3e8", type: "dashed"}}
|
||||
},
|
||||
series: [{
|
||||
type: "bar",
|
||||
barMaxWidth: 44,
|
||||
itemStyle: {color: "#5b8ff9"},
|
||||
data: this.topData.map(item => item.viewCount || 0)
|
||||
}]
|
||||
}, true)
|
||||
this.categoryChart.setOption({
|
||||
color: ["#5b8ff9", "#5ad8a6", "#6f86ad", "#f6bd16", "#e8684a", "#9270ca"],
|
||||
tooltip: {trigger: "item", formatter: "{b}<br/>访问量:{c}<br/>占比:{d}%"},
|
||||
series: [{
|
||||
type: "pie",
|
||||
radius: ["35%", "64%"],
|
||||
center: ["50%", "56%"],
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: {borderColor: "#fff", borderWidth: 3, borderRadius: 10},
|
||||
label: {color: "#303133", formatter: "{b}: {d}%"},
|
||||
labelLine: {length: 16, length2: 18},
|
||||
data: this.categoryData.map(item => ({name: item.categoryName, value: item.viewCount || 0}))
|
||||
}]
|
||||
}, true)
|
||||
},
|
||||
// 格式化 Top10 表格的发布时间。
|
||||
formatTime(value) {
|
||||
return value ? this.$moment(Number(value)).format("YYYY-MM-DD HH:mm:ss") : ""
|
||||
},
|
||||
// 浏览器尺寸变化时重算图表尺寸。
|
||||
resizeCharts() {
|
||||
if (this.topChart) this.topChart.resize()
|
||||
if (this.categoryChart) this.categoryChart.resize()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener("resize", this.resizeCharts)
|
||||
// 图表容器挂载完成后自动执行首次查询,避免必须点击“查询”才显示数据。
|
||||
this.$nextTick(() => {
|
||||
this.loadCategories().then(() => this.loadAll())
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener("resize", this.resizeCharts)
|
||||
if (this.topChart) this.topChart.dispose()
|
||||
if (this.categoryChart) this.categoryChart.dispose()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
Reference in New Issue
Block a user