# easyadmin **Repository Path**: nmnl/easyadmin ## Basic Information - **Project Name**: easyadmin - **Description**: 项目基于 Solon 、 Sqltoy、Vue的半前后端分离(前后端分离,但不需要node前端环境直接加载vue模板)的后台管理系统 - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: V2 - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2024-11-05 - **Last Updated**: 2024-11-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # EASY-ADMIN 开发框架 #### 介绍 项目基于 Solon 、 SqlToy、Vue的半前后端分离(前后端分离架构,但不需要node前端环境。直接写vue模板)的后台管理系统 ### 主要特性 - 使用[Solon](https://solon.noear.org/),体积超小,同时还有超多的插件集成,缓存,cloud,定时任务等 - 使用[SqlToy](https://gitee.com/sagacity/sagacity-sqltoy) ,CRUD超级方便 - 高效率开发,代码生成器可一键生成前后端代码 - 支持数据字典,可方便地对一些状态进行管理 - 前端使用[HeyUI](https://www.heyui.top/) ,集成后端数据超级方便,表格数据请求、数据字典、表单下拉,多选选等控件可直接用字典或后端地址。 - 独创菜单权限控制方式通过接口声明菜单项,权限注解直接引用 - 独创Vue模版/js双加载模式,可以不用部署node环境使用vue了,支持大部分的vue库。双模式可以直接加载vue模板方便调试, 也可以加载插件编译后的vjs(vue编译后的js,自动混淆代码,去除调试信息)减少代码体积。 - 快捷的Excel导入导出(后端处理) - Pdf打印报表(HiPrint) #### 系统功能 - 用户管理:提供用户的相关配置,新增用户后,默认密码为123456 - 角色管理:对权限与菜单进行分配,角色可绑定用户和菜单/按钮权限 - 部门管理:可配置系统组织架构,树形表格展示。部门有一个带级连关系的内部ID。 - 字典管理:可维护常用一些固定的数据,如:状态,性别等 - 配置项管理:管理各种常用可变配置 - 用户日志:记录用户操作日志,可自定义日志 - 慢SQL日志:记录运行慢的日志,方便维护和调优,阈值可设置 - 定时任务:方便定时任务,有任务执行记录 - 报表管理:HiPrint集成,方便导出pdf报表 - 代码生成:高灵活度生成前后端代码,减少大量重复的工作任务 - API接口:可以导出api.json和在线调试 #### 快速开始 pom.xml ```xml 4.0.0 com.palm easy-admin 2.1.8 11 11 dev com.palm easy-dev mysql mysql-connector-java org.projectlombok lombok provided true com.palm easy-platform palmxj-framewrok-central central https://palmxj-maven.pkg.coding.net/repository/framewrok/central/ true org.springframework.boot spring-boot-maven-plugin ``` 从这里开始 src/main/java/com/easy/demo/DemoApp.java ``` package com.easy.demo; import org.noear.solon.Solon; public class DemoApp { public static void main(String[] args) { Solon.start(DemoApp.class,args); } } ``` ### 第一次 什么都有第一次,使用EasyAdmin也不例外。 现在可以执行DemoApp的Main方法启动应用了,启动后控制台输出类似下面字样时表示启动成功,这里我们用了长达1s多的时间来第一次启动应用。 ```shell [Solon] View: load: EnjoyRender [Solon] View: load: org.noear.solon.view.enjoy.EnjoyRender [Solon] View: mapping: .shtm=EnjoyRender [Solon] View: mapping: @json=StringSerializerRender#FastjsonSerializer [Solon] View: mapping: @type_json=StringSerializerRender#FastjsonSerializer 总计将加载.sql.xml文件数量为:1 如果.sql.xml文件不在下列清单中,很可能是文件没有在编译路径下(bin、classes等),请仔细检查! 正在解析第:[0]个sql文件:mapper/platform.sql.xml [Solon] Server:main: Jetty 9.4(jetty) solon.connector:main: jetty: Started ServerConnector@{HTTP/1.1,[http/1.1]}{http://localhost:8080} [Solon] Server:main: jetty: Started @155ms [Solon] App: End loading elapsed=1084ms pid=55858 ``` 根据控制台输出,我们打开浏览器进入 http://localhost:8080 ![安装](doc/install.png) 目前我们只支持了mysql,当然sqltoy本身是适配了多数据库的。填写完数据库信息后点确定即可进行数据库的初始化。初始化完成后会在项目目录下生成app.yml文件。 可将app.yml移动到src/main/resources目录下面,这样打包后运行不需要重新配置。也可以将app.yml放置与jar包同一目录(工作目录) 初始化完成后不出意外的化,我们会看到下面的界面 ![安装](doc/installed.png) 进入系统后我们会看到一个非常干净的首页,是的,什么都没有。你可以通过新建文件 src/main/resources/static/home.vue 来覆盖首页,其他页面也可以通过这种方式覆盖。 ![首页](doc/index.png) 到这里基本的admin系统已经跑起来了,其他功能可以自行查看。 ### 开始开发 在开始开发前,我们需要进行一点点小小的配置,首先在应用启动配置的应用参数中加入 --debug=1,同时在maven配置文件中勾选 dev(打包时需要把dev取消选中,以下称为dev模式)。 然后启动应用,这时候控制台会输出一些调试信息,比如SLQ。 ![调试控制台](doc/console_debug.png) 再次进入浏览器页面,刷新后页面会多出一个开发工具菜单,可以通过导入数据库表的方式新建代码生成模型以生成各种代码,也可查看api接口 ![代码生成器](doc/designer.png) #### 代码生成 在数据库中设计好业务表后(表字段需要有注释),在dev模式下启动应用,进入后在在菜单项中进入开发工具->代码生成。点击导入,可选择通过数据库导入生成模型,通过勾选配置后。点击生成代码,可以生成对应代码 ### 后端开发 Easy Framework后端采用Solon和sqltoy进行开发 #### 模块 Easy Framework按照业务划分包模块,可在模块中配置模块的业务菜单,固定配置项,字典项等 ```java import com.palm.core.Module; import org.noear.solon.annotation.Configuration; import org.noear.solon.core.AopContext; @Configuration public class DemoModule implements Module { public void start(AopContext context) { //注册菜单 //regMenu(TestMenu.class); } } ``` ### 业务菜单和权限 Easy Framework使用注解方式来声明权限 ```java import com.palm.core.anno.Menu; import com.palm.core.anno.Permission; //这里声明了菜单以及权限 @Menu(value = "测试", icon = "icon-file", id = TestMenu.ID) public interface TestMenu { String ID = "Demo"; @Menu(value = "示例", path = "test/demo.vue", icon = "icon-file") String DEMO = ID + ".demo"; // demo增删改权限 @Permission(value = "新增", parent = DEMO) String DEMO_ADD = DEMO + ".add"; @Permission(value = "修改", parent = DEMO) String DEMO_EDIT = DEMO + ".edit"; @Permission(value = "删除", parent = DEMO) String DEMO_DELETE = DEMO + ".delete"; } ``` ```java import com.palm.core.data.response.Resp; import com.palm.demo.test.domain.Demo; import org.noear.solon.core.handle.MethodType; import com.palm.core.anno.Auth; import org.noear.solon.annotation.*; import org.sagacity.sqltoy.dao.SqlToyLazyDao; import org.sagacity.sqltoy.model.Page; import org.noear.solon.extend.sqltoy.annotation.Db; import com.palm.core.anno.doc.Notes; import com.palm.demo.test.TestMenu; import java.util.List; import java.util.Map; /** * 示例 @Auth 表示需要登录,@Auth(string)表示调用该方法需要的权限,也可指定所需角色(role) * 当传入多个权限或者角色时,表示有当中任意一个条件满足即可调用该方法 */ @Notes("示例") @Auth @Controller @Mapping("demo") public class DemoController { @Db SqlToyLazyDao dao; /** * 新增示例 */ @Notes("新增示例") @Auth(TestMenu.DEMO_ADD) @Mapping(value = "add", method = MethodType.POST) public Resp add(Demo demo) { dao.save(demo); return Resp.ok(); } //@DataScope(branch = "sn")实现了通过branch表的sn字段, //按照用户分配数据权限范围,进行自动过滤 @Notes("获取组织机构列表") @Mapping("list") @DataScope(branch = "sn") public Resp> list(Branch query) { //过滤权限 List result = dao.findBySql("branch_list",query); return Resp.of(result); } //Excel流式导入导出,可处理大量数据,根据JavaBean对于表头,如需通过模板导出,可考虑jxls @Notes("产品经纪人") @Data @Entity(tableName=Broker.TABLE) public class Broker implements Serializable { public static final String TABLE = "a_broker"; /** * id */ @Notes("id") @Id(strategy = "identity") @Column(name = "id", length = 10L, type = Types.INTEGER, nullable = false) private Integer id; /** * 地区 */ @Notes("地区") @Length(max = 25) @Excel("地区") @Column(name = "area", length = 25L, type = Types.VARCHAR) private String area; /** * 单位名称 */ @Notes("单位名称") @Length(max = 100) @Excel("单位名称") @Column(name = "place", length = 100L, type = Types.VARCHAR) private String place; //... } /** * 导出产品经纪人 */ @Auth(ListMenu.BROKER) @Mapping(path = "export",method = MethodType.POST) public void export(Broker query, Context ctx){ //通过sql直接导出数据 String sql="SELECT * FROM a_broker WHERE #[ and area like :area]#[ and place like :place]#[ and type like :type]"; ExcelStreamExporter exporter=new ExcelStreamExporter(ctx,"产品经纪人.xlsx"); dao.fetchStream(new QueryExecutor(sql,query),exporter); //或者手动导出 ExcelBatchExport be=new ExcelBatchExport(ctx,"测试.xlsx"); //可设置表头,和单sheet最大行数 be.setHeaderRow("姓名","性别"); be.export(bw->{ //按写入数据 bw.writeRow("cell11","cell12"); bw.writeRow("cell21","cell22"); //或者写入带格式的数据 List cells=new ArrayList(); bw.writeRow(cells); }); /** * 可根据模板导出 * 模板中#(xx)表示取ds中变量xx,#[xx]表示循环取ds中xx列表中的值 * 其中 列表数据类型为,数组,List,StreamDataSource * StreamDataSource可直接绑定sql,次数 * 当导出的数据量较大时,可以用StreamDataSource,减少内存溢出风险 */ Map ds=new HashMap<>(); ds.put("name","测试Name"); ds.put("id",new StreamDataSource<>(20)); ds.put("id1",new StreamDataSource<>(40)); XlsxTemplate.render(new FileInputStream("test.xlsx"),ds,"测试导出.xlsx",ctx); } /** * 导入产品经纪人 */ @Auth(ListMenu.BROKER_ADD) @Mapping(path = "upload",method = MethodType.POST) public Resp upload(UploadedFile file) throws IOException { if(file==null){ Resp.fail("文件不能为空"); } ExcelBatchImporter importer=new ExcelBatchImporter(file.content,Broker.class); importer.start(((data, lastIndex) -> { dao.saveAll(data); })); return Resp.ok(); } } ``` #### 固定配置项 ```java import com.palm.core.config.IConfigItem; import com.palm.core.config.Item; /** * 系统配置项,通过enum+@Item注解的方式声明配置项,配置项的key值为enum类名+条目名, * 比如下面这个DemoName的Key值为DemoConfig.DemoName。该可以值作为数据库中t_config表的sn列 * 或者前端调用时使用,如: _config[key] * 当front=true时,表示可以在前端页面中直接使用_config[key]获取对应key的配置值 * 固定配置项需要在模块配置中注册 */ public enum DemoConfig implements IConfigItem { @Item(title="测试配置",defaultValue = "Hello",front = true) DemoName; //后端获取配置项的值 public static void demo(){ String str= DemoConfig.DemoName.value().asString(); Date date= DemoConfig.DemoName.value().asDate(); // ... } } ``` #### 固定字典项 ```java import com.palm.core.dict.IFixedDict; import org.noear.solon.annotation.Note; @Note("性别") public enum Gender implements IFixedDict { @Note("男") //Male M, @Note("女")//Female F } ``` ### 定时任务: ```xml cn.hutool hutool-cron ``` ```java package com.palm.test; import com.palm.core.anno.Job; import org.noear.solon.annotation.Component; import java.util.Date; @Component public class TestJobService { @Job(cron = "* * * * *",desc = "测试任务") public void testJob(){ System.out.println("test job at:"+new Date()); } } ``` ### 前端指南 EasyAdmin 前端开发框架基于Vue3,采用RequireJS结合自定义loader做vue文件加载。在调试模式下,loader直接加载.vue模板文件,通过loader将模板动态编译为js模块。打包发布时,vue模板文件会被打包为压缩混淆的js文件,loader直接加载js文件,减少响应的数据量,加快响应速度。需要注意的是,前端所有文件应放在文件夹resources/static/下面,该目录对应了web请求的根目录。 本文默认前端掌握ES6及Vue3基础知识 #### 前端框架支持 EasyAdmin 完全支持[RequireJS](https://requirejs.org/),可以按照RequireJS文档配置自定义的三方库。Vue UI库方面,EasyAdmin 集成了好用的[HeyUI](https://www.heyui.top/),同时也集成了其他常用的Vue库,如果要引入自定义三方库,三方库需要支持Vue3及RequireJS(AMD规范,可通过配置shim方式配置常用库,如:Jquery) ,另外,EasyAdmin支持加载以.mjs结尾的js模块文件,和以.less结尾的less文件,mjs文件为满足[esm](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules)模块需求的js文件,EasyAdmin将其转换为了CMD模块,支持大多数的esm语法,支持部分ES6语法,支持程度视浏览器兼容性而定,EasyAdmin没有除了做压缩混淆外,没有做额外的语法支持 ```javascript //hello.mjs 满足esm语法 function hello(){ } export default{ hello } //引用 import {hello} from './hello' ``` ```less /**demo.less**/ .some{ .color{ color:red; } } /** * html中引用 * */ /** * js文件中引用 * require('./demo.css') * mjs或者vue模块中引用 * require('./demo.css') 或 import './demo.css' */ ``` #### framework.js framework中封装了常用的一些方法 ```javascript //ajax 相关,ajax相关操作除特殊说明外,均返回promise import {ajax} from 'framework' //get 请求 let promise=ajax.get(url,{...params}) //post 请求 let promise=ajax.post(url,{...params}) //put 请求 let promise=ajax.put(url,{...params}) //delete 请求 let promise=ajax.delete(url,{...params}) //post 以表单方式提交 ajax.formPost(url,{...params}) //post 上传单个, let params={ file:File } //或者多个文件 let params={ file:[...files] } //或者 命名的多个文件,需要后端对应属性名 let params={ file:file, file1:file1 } ajax.upload(url,{...params}) /** * 文件下载,也可以用window.open(url)等方式下载文件, * 若需要登录用户授权,可以添加url参数 _token=sessionStorage._token */ ajax.download(url,params={},method="POST") //ajax方式的文件下载 downloadA(path, fileName = "", params = {}, method = 'GET') /** * 加载带缓存数据,可直接用于vue数据绑定, * @param url * @param defaultValue * @returns VueRef */ ajax.data(url, defaultValue) /** * 通过url创建数据集,可用于datatable,select等数据绑定 */ ajax.createDs(uri, pageAble = false) //数据集使用 const ds=ajax.createDs('someurl',true) //数据集中的数据,可以直接绑定到datatable上 ds.data //设置附加查询参数,也可直接绑定到vue查询控件上 ds.params.param1='value1' //加载数据,也可通过该方法重新加载数据 ds.load() //设置数据转换,每次数据加载后会通过这方法转换数据 ds.transformer((v)=>v); //清空数据 ds.clean() //设置自定义数据 ds.putData([...data]) //可在数据加载前修改查询参数 ds.on('beforeLoad',(params)=>undefined) //可在数据加载后修改响应数据 ds.on('load',(data)=>undefined) //数据加载错误时调用 ds.on('error',(error)=>undefined) ``` ```javascript //其他用法 import {uuid,format,hasPermission,hasRole,mapState,mapActions} from 'framework' //生成一个随机id 如:'1gk2o6462g94p92' uuid() //数据格式化,两种调用方式 '1.00' format(1,"####.00") const fmt=format("####.00"); fmt(1) //日期格式化,两种调用方式 '2022-12-12' format(Date.now(),"yyyy-MM-dd") const fmt=format("yyyy-MM-dd"); fmt(Date.now()) //判断用户是否有某个权限/角色 hasPermission('some.priv') hasRole('some.role') //引用vue store,也可直接使用 vuex const [user,theme]=mapState(['user','theme']) const loadUserInfo = mapActions(['loadUserInfo']) ``` ```javascript //framework util中包含了一些数据操作的方法 import {util} from 'framework' /** * 数组转换为map * @param list 数组 * @param getId * @param getValue * @returns {{}} */ util.asMap(list, getId = (it, idx) => idx, getValue = it => it) /** * 数组转换为树,parentId为0,false,null的或者获取不到parent的节点转为根节点 * @param list * @param getId * @param getParentId * @param getValue * @returns {*[]} */ util.asTree(list, getId = it => it, getParentId = it => null, getValue = it => it) /** * 获取一个对象中的多个值 */ util.fields(obj, fields) let obja={a:1,b:2,c:3} //obj2={a:1,b:2} let obj2=util.fields(obj,"a,b") //折叠/展开字段。由于现在后台本身支持类似的解码,所以本方法使用较少 // 输出 {a:"1,2,3",b:"1,2,3"} util.fold({a:[1,2,3],b:[1,2,3]}, "a,b") // 输出 {a:[1,2,3],b:[1,2,3]} util.unfold({a:"1,2,3",b:"1,2,3"}, "a,b") //清除对象的数据,输出: {a:null,b:null} util.clean({a:1,b:1}) ``` #### 扩展组件 EasyAdmin添加了部分Vue组件 ```vue 把图片文件拖入到这里 ```