# 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

目前我们只支持了mysql,当然sqltoy本身是适配了多数据库的。填写完数据库信息后点确定即可进行数据库的初始化。初始化完成后会在项目目录下生成app.yml文件。
可将app.yml移动到src/main/resources目录下面,这样打包后运行不需要重新配置。也可以将app.yml放置与jar包同一目录(工作目录)
初始化完成后不出意外的化,我们会看到下面的界面

进入系统后我们会看到一个非常干净的首页,是的,什么都没有。你可以通过新建文件 src/main/resources/static/home.vue 来覆盖首页,其他页面也可以通过这种方式覆盖。

到这里基本的admin系统已经跑起来了,其他功能可以自行查看。
### 开始开发
在开始开发前,我们需要进行一点点小小的配置,首先在应用启动配置的应用参数中加入 --debug=1,同时在maven配置文件中勾选 dev(打包时需要把dev取消选中,以下称为dev模式)。
然后启动应用,这时候控制台会输出一些调试信息,比如SLQ。

再次进入浏览器页面,刷新后页面会多出一个开发工具菜单,可以通过导入数据库表的方式新建代码生成模型以生成各种代码,也可查看api接口

#### 代码生成
在数据库中设计好业务表后(表字段需要有注释),在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
把图片文件拖入到这里
```