feat:浦发项目模块迁移-重大事项初始化

This commit is contained in:
2026-07-28 10:54:28 +08:00
parent 785b9a4b80
commit 5df36acc22
14 changed files with 1520 additions and 0 deletions
@@ -0,0 +1,208 @@
package com.budwk.app.zhgh.dayofficework.material.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.zhgh.dayofficework.material.model.MaterialItem;
import com.budwk.app.zhgh.dayofficework.material.model.MaterialType;
import com.budwk.app.zhgh.dayofficework.material.service.MaterialItemService;
import com.budwk.app.zhgh.dayofficework.material.service.MaterialTypeService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@IocBean
@Ok("json:full")
@Api(tags = "重大事项")
@At("/platform/material/item")
public class MaterialItemController {
@Inject
private MaterialItemService itemService;
@Inject
private MaterialTypeService typeService;
@At("")
@SaCheckPermission("material.item")
@Ok("beetl:/platform/zhgh/dayofficework/material/item/index.html")
public void index() {
}
@At
@ApiOperation("重大事项展示")
@SaCheckPermission("material.display")
@Ok("beetl:/platform/zhgh/dayofficework/material/display/index.html")
public void displayIndex() {
}
@At
@ApiOperation("分页查询")
@SaCheckPermission("material.item")
public Result pageData(PageForm pageForm, @Param("typeId") String typeId, @Param("visible") String visible) {
Sql sql = Sqls.create("select mi.id, mi.typeId, mi.title, mi.remark, mi.visible, mi.createdAt, mi.updatedAt, " +
"mt.name as typeName " +
"from material_item mi left join material_type mt on mi.typeId = mt.id $condition");
Cnd cnd = Cnd.where("mi.delFlag", "=", false).and("mt.delFlag", "=", false);
cnd.andEX("mi.typeId", "=", typeId);
if (StrUtil.isNotBlank(visible)) {
cnd.and("mi.visible", "=", Boolean.parseBoolean(visible));
}
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or("mi.title", "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or("mi.remark", "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
cnd.asc("mt.sort").desc("mi.createdAt");
} else {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
}
sql.setCondition(cnd);
Pagination pagination = itemService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
}
@At
@ApiOperation("新增/修改重大事项")
@SaCheckPermission("material.item")
@SLog(tag = "重大事项-事项管理", msg = "新增/修改重大事项")
public Object submit(@Param("data") MaterialItem item) {
if (item.getVisible() == null) {
item.setVisible(true);
}
itemService.insertOrUpdate(item);
return Result.success();
}
@At
@ApiOperation("更新重大事项公开状态")
@SaCheckPermission("material.item")
@SLog(tag = "重大事项-事项管理", msg = "更新公开状态")
public Object updateVisible(String id, @Param("visible") String visible) {
if (StrUtil.isBlank(id) || StrUtil.isBlank(visible)) {
return Result.error("参数不能为空");
}
if (!StrUtil.equalsAnyIgnoreCase(visible, "true", "false")) {
return Result.error("公开状态不正确");
}
itemService.update(
Chain.make("visible", Boolean.parseBoolean(visible)).add("updatedAt", System.currentTimeMillis()),
Cnd.where(MaterialItem::getId, "=", id).and(MaterialItem::getDelFlag, "=", false)
);
return Result.success();
}
@At
@ApiOperation("删除重大事项")
@SaCheckPermission("material.item")
@SLog(tag = "重大事项-事项管理", msg = "删除重大事项")
public Object delete(String id) {
itemService.vDelete(id);
return Result.success();
}
@At
@ApiOperation("查询单个重大事项")
@SaCheckLogin
public Result info(String id) {
MaterialItem item = itemService.fetch(id);
if (item == null || Boolean.TRUE.equals(item.getDelFlag())) {
return Result.error("数据不存在");
}
NutMap map = org.nutz.lang.Lang.obj2nutmap(item);
MaterialType type = typeService.fetch(item.getTypeId());
if (type != null) {
map.put("typeName", type.getName());
}
return Result.success(map);
}
@At
@ApiOperation("公开展示分组查询")
@SaCheckLogin
public Result display(PageForm pageForm, @Param("typeIds") String typeIds) {
List<String> typeIdList = parseTypeIds(typeIds);
Cnd typeCnd = Cnd.where(MaterialType::getDelFlag, "=", false);
if (!typeIdList.isEmpty()) {
typeCnd.and(MaterialType::getId, "in", typeIdList);
}
typeCnd.asc(MaterialType::getSort).asc(MaterialType::getCreatedAt);
Pagination pagination = typeService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), typeCnd);
List<NutMap> types = pagination.getList(NutMap.class);
if (types.isEmpty()) {
return Result.success(pagination);
}
List<String> pageTypeIds = types.stream().map(type -> type.getString("id")).collect(Collectors.toList());
List<MaterialItem> items = itemService.query(Cnd.where(MaterialItem::getDelFlag, "=", false)
.and(MaterialItem::getVisible, "=", true)
.and(MaterialItem::getTypeId, "in", pageTypeIds)
.desc(MaterialItem::getUpdatedAt)
.desc(MaterialItem::getCreatedAt));
Map<String, List<MaterialItem>> itemMap = items.stream().collect(Collectors.groupingBy(MaterialItem::getTypeId));
for (NutMap type : types) {
List<NutMap> details = itemMap.getOrDefault(type.getString("id"), Collections.emptyList())
.stream()
.limit(5)
.map(org.nutz.lang.Lang::obj2nutmap)
.collect(Collectors.toList());
type.put("details", details);
}
return Result.success(pagination);
}
@At
@ApiOperation("公开事项列表")
@SaCheckLogin
public Result detailList(PageForm pageForm, @Param("typeId") String typeId) {
Cnd cnd = Cnd.where(MaterialItem::getDelFlag, "=", false)
.and(MaterialItem::getVisible, "=", true)
.and(MaterialItem::getTypeId, "=", typeId);
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(MaterialItem::getTitle, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or(MaterialItem::getRemark, "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
cnd.desc(MaterialItem::getUpdatedAt);
cnd.desc(MaterialItem::getCreatedAt);
Pagination pagination = itemService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
return Result.success(pagination);
}
private List<String> parseTypeIds(String typeIds) {
if (StrUtil.isBlank(typeIds)) {
return new ArrayList<>();
}
try {
return Json.fromJsonAsList(String.class, typeIds);
} catch (Exception e) {
return List.of(typeIds);
}
}
}
@@ -0,0 +1,84 @@
package com.budwk.app.zhgh.dayofficework.material.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.zhgh.dayofficework.material.model.MaterialType;
import com.budwk.app.zhgh.dayofficework.material.service.MaterialTypeService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
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;
import java.util.List;
@Slf4j
@IocBean
@Ok("json:full")
@Api(tags = "重大事项类型")
@At("/platform/material/type")
public class MaterialTypeController {
@Inject
private MaterialTypeService typeService;
@At("")
@SaCheckPermission("material.type")
@Ok("beetl:/platform/zhgh/dayofficework/material/type/index.html")
public void index() {
}
@At
@ApiOperation("分页查询")
@SaCheckPermission("material.type")
public Result pageData(PageForm pageForm) {
Cnd cnd = Cnd.where(MaterialType::getDelFlag, "=", false);
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
cnd.and(MaterialType::getName, "like", "%" + pageForm.getSearchKeyword() + "%");
}
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
cnd.asc(MaterialType::getSort).asc(MaterialType::getCreatedAt);
} else {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
}
Pagination pagination = typeService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
return Result.success(pagination);
}
@At
@ApiOperation("新增/修改重大事项类型")
@SaCheckPermission("material.type")
@SLog(tag = "重大事项-类型管理", msg = "新增/修改重大事项类型")
public Object submit(MaterialType type) {
typeService.insertOrUpdate(type);
return Result.success();
}
@At
@ApiOperation("删除重大事项类型")
@SaCheckPermission("material.type")
@SLog(tag = "重大事项-类型管理", msg = "删除重大事项类型")
public Object delete(String id) {
typeService.vDelete(id);
return Result.success();
}
@At
@ApiOperation("查询重大事项类型")
@SaCheckLogin
public Result list() {
List<MaterialType> list = typeService.query(Cnd.where(MaterialType::getDelFlag, "=", false)
.asc(MaterialType::getSort)
.asc(MaterialType::getCreatedAt));
return Result.success(list);
}
}
@@ -0,0 +1,54 @@
package com.budwk.app.zhgh.dayofficework.material.model;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
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.Default;
import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.interceptor.annotation.PrevInsert;
@Data
@Comment("重大事项")
@Accessors(chain = true)
@Table("material_item")
@EqualsAndHashCode(callSuper = true)
public class MaterialItem extends BaseModel {
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 64)
@PrevInsert(uu32 = true)
private String id;
@Column
@ColDefine(type = ColType.VARCHAR, width = 64)
@Comment("重大事项类型ID")
private String typeId;
@Column
@ColDefine(type = ColType.VARCHAR, width = 255)
@Comment("标题")
private String title;
@Column
@ColDefine(type = ColType.TEXT)
@Comment("内容")
private String content;
@Column
@ColDefine(type = ColType.VARCHAR, width = 255)
@Comment("备注")
private String remark;
@Column
@ColDefine(type = ColType.BOOLEAN)
@Comment("是否公开")
@Default("1")
private Boolean visible;
}
@@ -0,0 +1,37 @@
package com.budwk.app.zhgh.dayofficework.material.model;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
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
@Comment("重大事项类型")
@Accessors(chain = true)
@Table("material_type")
@EqualsAndHashCode(callSuper = true)
public class MaterialType extends BaseModel {
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 64)
@PrevInsert(uu32 = true)
private String id;
@Column
@ColDefine(type = ColType.VARCHAR, width = 255)
@Comment("类型名称")
private String name;
@Column
@ColDefine(type = ColType.INT)
@Comment("排序")
private Integer sort;
}
@@ -0,0 +1,7 @@
package com.budwk.app.zhgh.dayofficework.material.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.material.model.MaterialItem;
public interface MaterialItemService extends BaseService<MaterialItem> {
}
@@ -0,0 +1,7 @@
package com.budwk.app.zhgh.dayofficework.material.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.material.model.MaterialType;
public interface MaterialTypeService extends BaseService<MaterialType> {
}
@@ -0,0 +1,17 @@
package com.budwk.app.zhgh.dayofficework.material.service.impl;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.dayofficework.material.model.MaterialItem;
import com.budwk.app.zhgh.dayofficework.material.service.MaterialItemService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
@Slf4j
@IocBean(args = {"refer:dao"})
public class MaterialItemServiceImpl extends BaseServiceImpl<MaterialItem> implements MaterialItemService {
public MaterialItemServiceImpl(Dao dao) {
super(dao);
}
}
@@ -0,0 +1,17 @@
package com.budwk.app.zhgh.dayofficework.material.service.impl;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.dayofficework.material.model.MaterialType;
import com.budwk.app.zhgh.dayofficework.material.service.MaterialTypeService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
@Slf4j
@IocBean(args = {"refer:dao"})
public class MaterialTypeServiceImpl extends BaseServiceImpl<MaterialType> implements MaterialTypeService {
public MaterialTypeServiceImpl(Dao dao) {
super(dao);
}
}
+37
View File
@@ -0,0 +1,37 @@
CREATE TABLE IF NOT EXISTS `material_type` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`name` varchar(255) DEFAULT NULL COMMENT '类型名称',
`sort` int DEFAULT 0 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(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_material_type_sort` (`sort`, `createdAt`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='重大事项-事项类型';
CREATE TABLE IF NOT EXISTS `material_item` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`typeId` varchar(64) DEFAULT NULL COMMENT '重大事项类型ID',
`title` varchar(255) DEFAULT NULL COMMENT '标题',
`content` text COMMENT '内容',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`visible` tinyint(1) DEFAULT 1 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(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_material_item_type` (`typeId`),
KEY `idx_material_item_visible` (`visible`, `delFlag`, `updatedAt`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='重大事项-事项';
-- 浦发 PostgreSQL 数据迁移到本项目 MySQL 时的字段映射:
-- material_type: id -> id, name -> name, sort -> sort,
-- create_time/update_time 转 13 位时间戳写入 createdAt/updatedAt,
-- deleted=0/1 转 delFlag=0/1。
-- material_item: type_id -> typeId, visible=0/1 可直接写入 visible,
-- create_time/update_time 转 13 位时间戳写入 createdAt/updatedAt,
-- deleted=0/1 转 delFlag=0/1。
@@ -0,0 +1,66 @@
-- 重大事项电脑端菜单。
-- 使用 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
'f6d70b1a40b64d0f98684426a6200001',
'',
LPAD(IFNULL(MAX(CAST(path AS UNSIGNED)), 0) + 1, 4, '0'),
'重大事项',
'Material',
'menu',
'',
'',
'ti-announcement',
1,
0,
'material',
NULL,
992,
1,
'',
UNIX_TIMESTAMP(NOW()) * 1000,
'',
UNIX_TIMESTAMP(NOW()) * 1000,
0,
'PC',
NULL,
NULL,
'z',
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 = 'material') 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 'f6d70b1a40b64d0f98684426a6200002', p.id, CONCAT(p.path, '0001'), '类型管理', 'Material Type', 'menu', '/platform/material/type', 'data-pjax', '', 1, 0, 'material.type', NULL, 1, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0
FROM sys_menu p
WHERE p.permission = 'material'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'material.type') 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 'f6d70b1a40b64d0f98684426a6200003', p.id, CONCAT(p.path, '0002'), '事项管理', 'Material Item', 'menu', '/platform/material/item', 'data-pjax', '', 1, 0, 'material.item', NULL, 2, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 's', 0, 0
FROM sys_menu p
WHERE p.permission = 'material'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'material.item') 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 'f6d70b1a40b64d0f98684426a6200004', p.id, CONCAT(p.path, '0003'), '事项展示', 'Material Display', 'menu', '/platform/material/item/displayIndex', 'data-pjax', '', 1, 0, 'material.display', NULL, 3, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 's', 0, 0
FROM sys_menu p
WHERE p.permission = 'material'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'material.display') 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 (
'material',
'material.type',
'material.item',
'material.display'
)
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,635 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.material-display-page {
padding: 20px;
box-sizing: border-box;
}
.material-home {
height: 65vh;
}
.material-header {
display: flex;
align-items: center;
width: 100%;
margin-bottom: 20px;
}
.material-title {
margin: 0;
font-size: 24px;
color: #303133;
}
.material-subtitle {
margin: 8px 0 0;
color: #909399;
font-size: 14px;
}
.material-title-refresh {
margin-left: 8px;
vertical-align: middle;
}
.material-type-filter {
min-width: 20%;
}
.material-home-scroll {
height: 65vh;
overflow-y: auto;
}
.material-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
grid-auto-rows: 360px;
align-items: start;
gap: 50px;
padding: 14px 15px 10px;
}
.material-card-slot {
height: 360px;
min-width: 0;
}
.material-card {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
border-radius: 8px;
transition: all 0.3s ease;
}
.material-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.material-card-head {
display: flex;
align-items: center;
gap: 12px;
padding-bottom: 10px;
cursor: pointer;
}
.material-card-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 18px;
font-weight: 600;
color: #303133;
}
.material-card .el-card__body {
flex: 1;
display: flex;
flex-direction: column;
}
.material-card-body {
height: 270px;
padding: 10px 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.material-list {
margin: 0;
padding: 0;
width: 100%;
list-style: none;
}
.material-list-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: color 0.2s;
}
.material-list-item:last-child {
border-bottom: none;
}
.material-list-item:only-child {
border-bottom: 1px solid #f0f0f0;
}
.material-list-item:hover {
color: #409eff;
}
.material-list-item:hover .material-dot {
background: #409eff;
}
.material-list-item:hover .material-list-title {
color: #409eff;
}
.material-list-item:hover .material-list-time {
color: #409eff;
background: #ecf5ff;
}
.material-dot {
width: 6px;
height: 6px;
margin-right: 12px;
border-radius: 50%;
background: #dcdfe6;
flex-shrink: 0;
}
.material-list-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #606266;
font-size: 14px;
}
.material-list-time {
margin-left: 12px;
padding: 2px 6px;
border-radius: 4px;
background: #f5f7fa;
color: #999;
font-size: 12px;
flex-shrink: 0;
}
.material-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 20px 0;
color: #b4b4b4;
}
.material-home-empty {
height: 65vh;
display: flex;
align-items: center;
justify-content: center;
}
.material-detail-title {
margin: 0;
font-size: 24px;
font-weight: 700;
color: #303133;
}
.material-detail-meta {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-top: 10px;
color: #606266;
font-size: 14px;
}
.material-detail-panel {
margin: 0;
padding: 0;
background: #f0f2f5;
min-height: 65vh;
box-sizing: border-box;
}
.material-detail-card {
min-height: 65vh;
padding: 40px;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
box-sizing: border-box;
}
.material-detail-head {
display: grid;
grid-template-columns: 90px 1fr 90px;
align-items: start;
gap: 12px;
}
.material-detail-back {
justify-self: start;
padding: 0;
}
.material-detail-heading {
min-width: 0;
text-align: center;
}
.material-detail-divider {
height: 1px;
margin: 22px 0;
background: #ebeef5;
}
.material-detail-body {
padding: 20px 0;
font-size: 16px;
line-height: 1.8;
color: #303133;
}
.material-detail-scroll {
max-height: 50vh;
overflow-y: auto;
padding: 0 20px;
box-sizing: border-box;
}
.material-detail-content {
line-height: 1.8;
color: #303133;
overflow: auto;
}
.material-detail-content img,
.material-detail-content video,
.material-detail-content iframe {
max-width: 100%;
}
.material-detail-remark {
margin-top: 16px;
color: #999;
font-size: 12px;
text-align: right;
word-break: break-all;
}
.material-list-panel {
margin: 0;
padding: 0;
background: #f0f2f5;
min-height: 65vh;
box-sizing: border-box;
}
.material-list-card {
min-height: 65vh;
padding: 40px;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
box-sizing: border-box;
}
.material-list-head {
display: grid;
grid-template-columns: 90px 1fr 260px;
align-items: center;
gap: 16px;
}
.material-list-back {
justify-self: start;
padding: 0;
}
.material-list-heading {
min-width: 0;
text-align: center;
}
.material-list-title-large {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 20px;
font-weight: 700;
color: #303133;
}
.material-list-search {
justify-self: end;
width: 240px;
}
.material-list-divider {
height: 1px;
margin: 22px 0;
background: #ebeef5;
}
.material-full-list {
height: calc(65vh - 210px);
min-height: 260px;
display: flex;
align-items: stretch;
justify-content: center;
}
.material-full-list-scroll {
width: 100%;
overflow-y: auto;
padding-right: 30px;
box-sizing: border-box;
}
.material-full-list ul {
margin: 0;
padding: 0;
list-style: none;
}
.material-full-list-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: color 0.2s;
}
.material-full-list-item:hover {
color: #409eff;
}
.material-full-list-item:hover .material-bullet {
background: #409eff;
}
.material-bullet {
width: 6px;
height: 6px;
margin-right: 12px;
border-radius: 50%;
background: #dcdfe6;
flex-shrink: 0;
}
.material-full-list-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #606266;
font-size: 15px;
}
.material-full-list-item:hover .material-full-list-title {
color: #409eff;
}
.material-full-list-date {
margin-left: 12px;
padding: 2px 6px;
border-radius: 4px;
background: #f5f7fa;
color: #999;
font-size: 14px;
flex-shrink: 0;
}
.material-full-list-item:hover .material-full-list-date {
color: #409eff;
background: #ecf5ff;
}
.material-list-pagination {
display: flex;
justify-content: center;
margin-top: 20px;
}
</style>
<div id="app" class="material-display-page" v-cloak>
<div v-if="viewMode === 'home'" class="material-home">
<div class="material-header">
<div style="flex: 1">
<h2 class="material-title">
重大事项
<el-link class="material-title-refresh"
underline="never"
icon="el-icon-refresh"
@click="filterPage"></el-link>
</h2>
<p class="material-subtitle">根据相关规定,现将各类信息公开如下:</p>
</div>
<el-select class="material-type-filter"
v-model="query.typeIds"
multiple
collapse-tags
clearable
placeholder="筛选重大事项类型"
@change="filterPage">
<el-option v-for="item in typeOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</div>
<div class="material-home-scroll" v-loading="loading" @scroll="onHomeScroll">
<div class="material-grid" v-if="groups.length">
<div class="material-card-slot" v-for="item in groups" :key="item.id">
<el-card class="material-card" shadow="hover">
<div slot="header" class="material-card-head" @click="openList(item)">
<span class="material-card-name">{{ item.name }}</span>
</div>
<div class="material-card-body">
<ul class="material-list" v-if="item.details && item.details.length">
<li class="material-list-item" v-for="detail in item.details" :key="detail.id"
@click="openDetail(detail, item.name)">
<span class="material-dot"></span>
<span class="material-list-title">{{ detail.title }}</span>
<span class="material-list-time">{{ formatDate(detail.updatedAt || detail.createdAt) }}</span>
</li>
</ul>
<div class="material-empty" v-else>暂无数据</div>
</div>
</el-card>
</div>
</div>
<div class="material-home-empty" v-else>
<el-empty v-if="!loading"></el-empty>
</div>
</div>
</div>
<div v-else-if="viewMode === 'list'" class="material-list-panel">
<div class="material-list-card">
<div class="material-list-head">
<el-button type="text" class="material-list-back" icon="el-icon-back" @click="backHome">返回</el-button>
<div class="material-list-heading">
<span class="material-list-title-large">{{ currentType.name }}</span>
</div>
<el-input class="material-list-search"
v-model="listKeyword"
placeholder="搜索标题..."
prefix-icon="el-icon-search"
clearable
@keyup.enter.native="searchList"
@clear="searchList"
@blur="searchList"></el-input>
</div>
<div class="material-list-divider"></div>
<div class="material-full-list" v-loading="loading">
<div class="material-full-list-scroll" v-if="listData.length">
<ul>
<li class="material-full-list-item"
v-for="detail in listData"
:key="detail.id"
@click="openDetail(detail, currentType.name)">
<span class="material-bullet"></span>
<span class="material-full-list-title">{{ detail.title }}</span>
<span class="material-full-list-date">{{ formatDate(detail.updatedAt || detail.createdAt) }}</span>
</li>
</ul>
</div>
<el-empty v-else-if="!loading"></el-empty>
</div>
<div class="material-list-pagination" v-if="listPage.totalCount > 0">
<el-pagination
small
layout="total, sizes, prev, pager, next, jumper"
:current-page.sync="listPage.pageNumber"
:page-size="listPage.pageSize"
:page-sizes="[10, 20, 30, 40, 50, 100]"
:total="listPage.totalCount"
@size-change="onListSizeChange"
@current-change="loadList">
</el-pagination>
</div>
</div>
</div>
<div v-else class="material-detail-panel">
<div class="material-detail-card" v-loading="loading">
<div class="material-detail-head">
<el-button type="text" class="material-detail-back" icon="el-icon-back" @click="backFromDetail">返回</el-button>
<div class="material-detail-heading">
<h2 class="material-detail-title">{{ detailData.title }}</h2>
<div class="material-detail-meta">
<el-tag size="small" effect="plain">{{ detailData.typeName || currentType.name }}</el-tag>
<span>发布时间:{{ formatDateTime(detailData.updatedAt || detailData.createdAt) }}</span>
</div>
</div>
<span></span>
</div>
<div class="material-detail-divider"></div>
<article class="material-detail-body">
<div class="material-detail-scroll">
<div class="material-detail-content" v-html="detailData.content"></div>
</div>
<div class="material-detail-remark" v-if="detailData.remark">
备注:{{ detailData.remark }}
</div>
</article>
</div>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
viewMode: "home",
previousMode: "home",
loading: false,
groups: [],
listData: [],
detailData: {},
currentType: {},
typeOptions: [],
listKeyword: '',
query: {
typeIds: []
},
pageForm: {
pageNumber: 1,
pageSize: 8,
totalCount: 0
},
listPage: {
pageNumber: 1,
pageSize: 10,
totalCount: 0
}
}
},
methods: {
formatDate(value) {
if (!value) return ''
const date = new Date(Number(value))
if (isNaN(date.getTime())) return ''
const pad = (num) => String(num).padStart(2, '0')
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate())
},
formatDateTime(value) {
if (!value) return '-'
const date = new Date(Number(value))
if (isNaN(date.getTime())) return '-'
const pad = (num) => String(num).padStart(2, '0')
return date.getFullYear() + '-' +
pad(date.getMonth() + 1) + '-' +
pad(date.getDate()) + ' ' +
pad(date.getHours()) + ':' +
pad(date.getMinutes())
},
loadTypes() {
this.$axios.post("/platform/material/type/list").then((res) => {
if (res.code === 0) {
this.typeOptions = res.data || []
}
})
},
filterPage() {
this.pageForm.pageNumber = 1
this.groups = []
this.pageData()
},
onHomeScroll(event) {
const el = event.target
const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 20
if (!nearBottom || this.loading || this.groups.length >= this.pageForm.totalCount) {
return
}
this.pageForm.pageNumber += 1
this.pageData(true)
},
pageData(append) {
this.loading = true
this.$axios.post("/platform/material/item/display", {
pageNumber: this.pageForm.pageNumber,
pageSize: this.pageForm.pageSize,
typeIds: JSON.stringify(this.query.typeIds || [])
}).then((res) => {
if (res.code === 0) {
const list = res.data.list || []
this.groups = append ? this.groups.concat(list) : list
this.pageForm.totalCount = res.data.totalCount || 0
}
}).finally(() => {
this.loading = false
})
},
openList(type) {
this.currentType = type
this.listKeyword = ''
this.listPage.pageNumber = 1
this.viewMode = "list"
this.loadList()
},
searchList() {
this.listPage.pageNumber = 1
this.loadList()
},
onListSizeChange(size) {
this.listPage.pageSize = size
this.listPage.pageNumber = 1
this.loadList()
},
loadList() {
this.loading = true
this.$axios.post("/platform/material/item/detailList", {
pageNumber: this.listPage.pageNumber,
pageSize: this.listPage.pageSize,
typeId: this.currentType.id,
searchKeyword: this.listKeyword
}).then((res) => {
if (res.code === 0) {
this.listData = res.data.list || []
this.listPage.totalCount = res.data.totalCount || 0
}
}).finally(() => {
this.loading = false
})
},
openDetail(row, typeName) {
this.previousMode = this.viewMode
this.loading = true
this.$axios.post("/platform/material/item/info", {id: row.id}).then((res) => {
if (res.code === 0) {
this.detailData = res.data || {}
if (typeName && !this.detailData.typeName) {
this.detailData.typeName = typeName
}
this.viewMode = "detail"
}
}).finally(() => {
this.loading = false
})
},
backHome() {
this.viewMode = "home"
this.currentType = {}
},
backFromDetail() {
this.viewMode = this.previousMode || "home"
}
},
created() {
this.loadTypes()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,200 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="标题/备注">
<el-input placeholder="请输入标题或备注" clearable v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch"></el-input>
</search-item>
<search-item label="类型">
<el-select v-model="query.typeId" clearable placeholder="请选择类型" @change="doSearch">
<el-option v-for="item in typeOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="是否公开">
<el-select v-model="query.visible" clearable placeholder="请选择" @change="doSearch">
<el-option label="公开" :value="true"></el-option>
<el-option label="不公开" :value="false"></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt20">
<table-tool label="重大事项">
<el-button type="primary" size="small" @click="onAdd">
<i class="ti-plus"></i>
新增事项
</el-button>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" :size="tableSize">
<el-table-column label="序号" type="index" :index="indexMethod" width="60"></el-table-column>
<el-table-column label="类型" prop="typeName" align="center" width="160" show-overflow-tooltip></el-table-column>
<el-table-column label="标题" prop="title" align="center" min-width="220" show-overflow-tooltip></el-table-column>
<el-table-column label="备注" prop="remark" align="center" min-width="180" show-overflow-tooltip></el-table-column>
<el-table-column label="是否公开" prop="visible" align="center" width="110">
<template v-slot="{ row }">
<el-switch v-model="row.visible"
@change="onVisibleChange(row)"></el-switch>
</template>
</el-table-column>
<el-table-column label="创建时间" prop="createdAt" align="center" width="180">
<template v-slot="{ row }">{{ formatTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="更新时间" prop="updatedAt" align="center" width="180">
<template v-slot="{ row }">{{ formatTime(row.updatedAt || row.createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="200">
<template v-slot="{ row }">
<el-button @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
<el-button @click="onDelete(row)" size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog :title="formData.id ? '编辑事项' : '新增事项'" :visible.sync="dialogVisible" width="72%" top="5vh">
<el-form :model="formData" ref="formRef" label-width="100px">
<el-form-item label="类型" prop="typeId" :rules="{ required: true, message: '请选择类型', trigger: 'change' }">
<el-select v-model="formData.typeId" filterable placeholder="请选择类型" style="width: 100%">
<el-option v-for="item in typeOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="标题" prop="title" :rules="{ required: true, message: '请输入标题', trigger: 'blur' }">
<el-input v-model="formData.title" maxlength="255" placeholder="请输入标题"></el-input>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" maxlength="255" placeholder="请输入备注"></el-input>
</el-form-item>
<el-form-item label="是否公开" prop="visible">
<el-switch v-model="formData.visible" active-text="公开" inactive-text="不公开"></el-switch>
</el-form-item>
<el-form-item label="内容" prop="content" :rules="{ required: true, message: '请输入内容', trigger: 'blur' }">
<text-editor v-model="formData.content"></text-editor>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="onSubmit">确定</el-button>
</template>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
store,
mixins: [initTableMixins],
data() {
return {
dialogVisible: false,
formData: {},
typeOptions: [],
query: {
typeId: '',
visible: ''
}
}
},
methods: {
formatTime(value) {
if (!value) return '-'
const date = new Date(Number(value))
if (isNaN(date.getTime())) return '-'
const pad = (num) => String(num).padStart(2, '0')
return date.getFullYear() + '-' +
pad(date.getMonth() + 1) + '-' +
pad(date.getDate()) + ' ' +
pad(date.getHours()) + ':' +
pad(date.getMinutes())
},
loadTypes() {
this.$axios.post("/platform/material/type/list").then((res) => {
if (res.code === 0) {
this.typeOptions = res.data || []
}
})
},
doSearch() {
this.pageForm.pageNumber = 1
this.pageData()
},
onAdd() {
this.formData = {visible: true}
this.dialogVisible = true
},
onEdit(row) {
this.$axios.post("/platform/material/item/info", {id: row.id}).then((res) => {
if (res.code === 0) {
this.formData = Object.assign({visible: true}, res.data)
this.dialogVisible = true
}
})
},
onDelete(row) {
this.$confirm("您确定要删除该事项吗?", "提示", {
type: "warning",
confirmButtonText: "确定",
cancelButtonText: "取消"
}).then(() => {
this.$axios.post("/platform/material/item/delete", {id: row.id}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
}).catch(() => {})
},
onVisibleChange(row) {
this.$axios.post("/platform/material/item/updateVisible", {
id: row.id,
visible: row.visible
}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
} else {
this.pageData()
}
}).catch(() => {
this.pageData()
})
},
onSubmit() {
this.$refs.formRef.validate((valid) => {
if (!valid) return
this.$axios.post("/platform/material/item/submit", {data: JSON.stringify(this.formData)}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.dialogVisible = false
this.pageData()
}
})
})
},
pageData() {
const params = Object.assign({}, this.pageForm, this.query)
this.$axios.post("/platform/material/item/pageData", params).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
})
},
},
created() {
this.loadTypes()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,53 @@
const basicForm = {
template: /*language=HTML*/ `
<div>
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="110px" label-position="left">
<el-form-item label="类型名称" prop="name">
<el-input v-model="formData.name" maxlength="100" placeholder="请输入类型名称"></el-input>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="formData.sort" :min="0" :step="1"></el-input-number>
</el-form-item>
</el-form>
<el-row class="mt10" justify="end" type="flex">
<el-button @click="$emit('refresh')">取消</el-button>
<el-button @click="onSubmit" type="primary">提交</el-button>
</el-row>
</div>
`,
data() {
return {
formData: {
sort: 0
},
formRules: {
name: [{required: true, message: '必填', trigger: ['blur', 'change']}],
sort: [{required: true, message: '必填', trigger: ['blur', 'change']}],
},
}
},
methods: {
onOpen(row) {
this.formData = row && row.id ? clone(row) : {sort: 0}
},
onSubmit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
const resp = await this.$axios.post("/platform/material/type/submit", this.formData)
if (resp.code === 0) {
this.$message.success(resp.msg)
this.$emit('refresh')
} else {
this.$message.warning(resp.msg)
}
})
}
})
},
}
}
@@ -0,0 +1,98 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="类型名称">
<el-input placeholder="请输入类型名称" clearable v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt20">
<table-tool label="重大事项类型">
<el-button type="primary" size="small" @click="onAdd">
<i class="ti-plus"></i>
新增类型
</el-button>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" :size="tableSize">
<el-table-column label="序号" type="index" :index="indexMethod" width="60"></el-table-column>
<el-table-column label="类型名称" prop="name" align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="排序" prop="sort" align="center" sortable="custom" width="120"></el-table-column>
<el-table-column label="操作" width="180">
<template v-slot="{ row }">
<el-button @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
<el-button @click="onDelete(row)" size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #edit>
<basic-form ref="basicFormRef" @refresh="refresh"></basic-form>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('basicForm.js'){}#-->
const vue = new Vue({
el: "#app",
mixins: [initTableMixins],
components: {
"basic-form": basicForm,
},
methods: {
refresh() {
this.doSearch()
this.$refs.guava.index()
},
onAdd() {
this.$refs.guava.edit(() => {
this.$refs.basicFormRef.onOpen()
})
},
onEdit(row) {
this.$refs.guava.edit(() => {
this.$refs.basicFormRef.onOpen(row)
})
},
onDelete(row) {
this.$confirm("您确定要删除吗, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/material/type/delete", {id: row.id}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
}).catch(() => {})
},
pageData() {
this.$axios.post(loc() + "/pageData", this.pageForm).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
})
},
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->