管理端之审批设置
2023-07-18 18:24:26 145浏览
1. 需求描述 76
公司日常办公,审批是必不可少的一个功能,例如常用的钉钉OA审批,今天我们就要做类似的审批功能。
审批设置模块包含:审批类型与审批模板
审批类型:审批类型即为审批的分类,如:出勤、人事、财务等
审批模板:设置具体审批的基本信息、表单信息与审批流程定义,审批流涉及工作流引擎Activiti,常见的审批模板如:加班、出差、请假、费用报销等,我们可以根据公司具体业务配置具体的审批模板
2. 页面效果 76
2.1 管理端
2.1.1 审批类型
2.1.2 审批模板
2.1.3 在线流程设计
2.2 员工端
2.2.1 审批中心
2.2.2 发起审批,显示动态表单
3. 数据库表设计 76
3.1 审批类型表:oa_process_type
CREATE TABLE `oa_process_type` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '类型名称',
`description` varchar(255) DEFAULT NULL COMMENT '描述',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_deleted` tinyint(3) NOT NULL DEFAULT '0' COMMENT '删除标记(0:不可用 1:可用)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='审批类型';
#
# Data for table "oa_process_type"
#
INSERT INTO `oa_process_type` VALUES (1,'出勤','出勤','2022-12-06 09:35:12','2022-12-06 09:36:13',0),(2,'人事','人事','2022-12-06 09:35:19','2022-12-06 09:36:16',0),(3,'财务','财务','2022-12-06 09:35:29','2022-12-06 09:36:17',0);
3.2 审批模板表:oa_process_template
# Structure for table "oa_process_template"
#
CREATE TABLE `oa_process_template` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色id',
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '模板名称',
`icon_url` varchar(100) DEFAULT NULL COMMENT '图标路径',
`process_type_id` varchar(255) DEFAULT NULL,
`form_props` text COMMENT '表单属性',
`form_options` text COMMENT '表单选项',
`process_definition_key` varchar(20) DEFAULT NULL COMMENT '流程定义key',
`process_definition_path` varchar(255) DEFAULT NULL COMMENT '流程定义上传路径',
`process_model_id` varchar(255) DEFAULT NULL COMMENT '流程定义模型id',
`description` varchar(255) DEFAULT NULL COMMENT '描述',
`status` tinyint(3) NOT NULL DEFAULT '0',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_deleted` tinyint(3) NOT NULL DEFAULT '0' COMMENT '删除标记(0:不可用 1:可用)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='审批模板';
#
# Data for table "oa_process_template"
#
INSERT INTO `oa_process_template` VALUES (1,'加班','https://gw.alicdn.com/tfs/TB1bHOWCSzqK1RjSZFjXXblCFXa-112-112.png','1','[{\"type\":\"datePicker\",\"field\":\"sdate\",\"title\":\"开始时间\",\"info\":\"\",\"_fc_drag_tag\":\"datePicker\",\"hidden\":false,\"display\":true,\"validate\":[{\"trigger\":\"change\",\"mode\":\"required\",\"message\":\"必须输入\",\"required\":true,\"type\":\"string\"}]},{\"type\":\"datePicker\",\"field\":\"edate\",\"title\":\"结束时间\",\"info\":\"\",\"_fc_drag_tag\":\"datePicker\",\"hidden\":false,\"display\":true,\"validate\":[{\"trigger\":\"change\",\"mode\":\"required\",\"message\":\"必须输入\",\"required\":true,\"type\":\"string\"}]},{\"type\":\"input\",\"field\":\"day\",\"title\":\"加班天数\",\"info\":\"\",\"_fc_drag_tag\":\"input\",\"hidden\":false,\"display\":true,\"validate\":[{\"trigger\":\"change\",\"mode\":\"required\",\"message\":\"必须输入\",\"required\":true,\"type\":\"string\"}]},{\"type\":\"input\",\"field\":\"reason\",\"title\":\"加班原因\",\"info\":\"\",\"props\":{\"type\":\"textarea\",\"rows\":4},\"_fc_drag_tag\":\"input\",\"hidden\":false,\"display\":true,\"validate\":[{\"trigger\":\"change\",\"mode\":\"required\",\"message\":\"必须输入\",\"required\":true,\"type\":\"string\"}]},{\"type\":\"select\",\"field\":\"dept\",\"title\":\"所属部门\",\"info\":\"\",\"effect\":{\"fetch\":\"\"},\"options\":[{\"value\":\"技术部\",\"label\":\"技术部\"},{\"value\":\"产品部\",\"label\":\"产品部\"},{\"label\":\"市场部\",\"value\":\"市场部\"},{\"label\":\"人事部\",\"value\":\"人事部\"}],\"_fc_drag_tag\":\"select\",\"hidden\":false,\"display\":true}]','{\n \"form\": {\n \"labelPosition\": \"right\",\n \"size\": \"medium\",\n \"labelWidth\": \"80px\",\n \"hideRequiredAsterisk\": false,\n \"showMessage\": true,\n \"inlineMessage\": false\n },\n \"submitBtn\": {\n \"innerText\":\"提交审批 \",\n \"round\":true,\n \"width\": \"280px\",\n \"type\":\"primary\"\n },\n \"resetBtn\": false\n}\n','jiaban','','','加班',1,'2022-12-07 14:33:51','2022-12-27 09:12:58',0),(2,'请假','https://gw.alicdn.com/imgextra/i3/O1CN01LLn0YV1LhBXs7T2iO_!!6000000001330-2-tps-120-120.png','2','[{\"type\":\"datePicker\",\"field\":\"sdate\",\"title\":\"开始时间\",\"info\":\"\",\"_fc_drag_tag\":\"datePicker\",\"hidden\":false,\"display\":true,\"validate\":[{\"trigger\":\"change\",\"mode\":\"required\",\"message\":\"必须输入\",\"required\":true,\"type\":\"string\"}]},{\"type\":\"datePicker\",\"field\":\"edate\",\"title\":\"结束时间\",\"info\":\"\",\"_fc_drag_tag\":\"datePicker\",\"hidden\":false,\"display\":true,\"validate\":[{\"trigger\":\"change\",\"mode\":\"required\",\"message\":\"必须输入\",\"required\":true,\"type\":\"string\"}]},{\"type\":\"input\",\"field\":\"day\",\"title\":\"请假天数\",\"info\":\"\",\"_fc_drag_tag\":\"input\",\"hidden\":false,\"display\":true,\"validate\":[{\"trigger\":\"change\",\"mode\":\"required\",\"message\":\"必须输入\",\"required\":true,\"type\":\"string\"}]},{\"type\":\"input\",\"field\":\"reason\",\"title\":\"请假原因\",\"info\":\"\",\"props\":{\"type\":\"textarea\",\"rows\":4},\"_fc_drag_tag\":\"input\",\"hidden\":false,\"display\":true,\"validate\":[{\"trigger\":\"change\",\"mode\":\"required\",\"message\":\"必须输入\",\"required\":true,\"type\":\"string\"}]},{\"type\":\"select\",\"field\":\"dept\",\"title\":\"所属部门\",\"info\":\"\",\"effect\":{\"fetch\":\"\"},\"options\":[{\"value\":\"技术部\",\"label\":\"技术部\"},{\"value\":\"产品部\",\"label\":\"产品部\"},{\"label\":\"市场部\",\"value\":\"市场部\"},{\"label\":\"人事部\",\"value\":\"人事部\"}],\"_fc_drag_tag\":\"select\",\"hidden\":false,\"display\":true}]','{\n \"form\": {\n \"labelPosition\": \"right\",\n \"size\": \"medium\",\n \"labelWidth\": \"80px\",\n \"hideRequiredAsterisk\": false,\n \"showMessage\": true,\n \"inlineMessage\": false\n },\n \"submitBtn\": {\n \"innerText\":\"提交审批 \",\n \"round\":true,\n \"width\": \"280px\",\n \"type\":\"primary\"\n },\n \"resetBtn\": false\n}\n','qingjia','','','请假',0,'2022-12-07 15:04:02','2022-12-27 09:13:00',0),(3,'申请费用','https://gw.alicdn.com/tfs/TB1e76lCOLaK1RjSZFxXXamPFXa-112-112.png','3','[{\"type\":\"datePicker\",\"field\":\"sdate\",\"title\":\"使用时间\",\"info\":\"\",\"_fc_drag_tag\":\"datePicker\",\"hidden\":false,\"display\":true,\"validate\":[{\"trigger\":\"change\",\"mode\":\"required\",\"message\":\"必须输入\",\"required\":true,\"type\":\"string\"}],\"props\":{\"format\":\"\"}},{\"type\":\"input\",\"field\":\"amount\",\"title\":\"申请金额\",\"info\":\"\",\"_fc_drag_tag\":\"input\",\"hidden\":false,\"display\":true,\"validate\":[{\"trigger\":\"change\",\"mode\":\"required\",\"message\":\"必须输入\",\"required\":true,\"type\":\"string\"}]},{\"type\":\"select\",\"field\":\"F0ma1n7tec860p\",\"title\":\"费用类别\",\"info\":\"\",\"effect\":{\"fetch\":\"\"},\"options\":[{\"value\":\"房租费\",\"label\":\"房租费\"},{\"value\":\"水费\",\"label\":\"水费\"},{\"label\":\"电费\",\"value\":\"电费\"},{\"label\":\"网络费\",\"value\":\"网络费\"},{\"label\":\"火车票\",\"value\":\"火车票\"},{\"label\":\"飞机票\",\"value\":\"飞机票\"},{\"label\":\"部门团建费\",\"value\":\"部门团建费\"}],\"_fc_drag_tag\":\"select\",\"hidden\":false,\"display\":true,\"validate\":[{\"trigger\":\"change\",\"mode\":\"required\",\"message\":\"必须输入\",\"required\":true,\"type\":\"string\"}]},{\"type\":\"input\",\"field\":\"reason\",\"title\":\"其他补充\",\"info\":\"\",\"props\":{\"type\":\"textarea\",\"rows\":4},\"_fc_drag_tag\":\"input\",\"hidden\":false,\"display\":true},{\"type\":\"select\",\"field\":\"dept\",\"title\":\"所属部门\",\"info\":\"\",\"effect\":{\"fetch\":\"\"},\"options\":[{\"value\":\"技术部\",\"label\":\"技术部\"},{\"value\":\"产品部\",\"label\":\"产品部\"},{\"label\":\"市场部\",\"value\":\"市场部\"},{\"label\":\"人事部\",\"value\":\"人事部\"}],\"_fc_drag_tag\":\"select\",\"hidden\":false,\"display\":true},{\"type\":\"input\",\"field\":\"content\",\"title\":\"申请事由\",\"info\":\"\",\"props\":{\"type\":\"textarea\",\"rows\":4},\"_fc_drag_tag\":\"input\",\"hidden\":false,\"display\":true,\"validate\":[{\"trigger\":\"change\",\"mode\":\"required\",\"message\":\"必须输入\",\"required\":true,\"type\":\"string\"}]},{\"type\":\"switch\",\"field\":\"F9hn1n7twlkcfk\",\"title\":\"消息提示\",\"info\":\"\",\"_fc_drag_tag\":\"switch\",\"hidden\":false,\"display\":true},{\"type\":\"span\",\"title\":\"注意\",\"native\":false,\"children\":[\"有疑问找云尚小秘哦!\"],\"_fc_drag_tag\":\"span\",\"hidden\":false,\"display\":true}]',' ','feiyong','','','申请费用',0,'2022-12-08 10:35:33','2022-12-26 21:00:01',0);
重要字段说明:
form_props:动态表单的表单属性,后续入门form-create组件会详细讲解
form_options:动态表单的表单选项,后续入门form-create组件会详细讲解,这两项就是配置动态表单的
process_definition_key:流程定义key,前面Activiti入门已讲解,我们启动流程实例会使用它
process_definition_path:保存流程定义文件的路径
process_model_id:流程定义模型id,流程定义实现两种方式,一种:上传,第二种:在线制作,分别使用这两个字段保存记录
4. 审批设置 77
4.1审批类型
操作service-oa模块
我们使用之前写好的代码生成器,生成相应代码
修改以下
成功
老规矩删除我们不用的实体类,然后吧持久层和业务层中引用的实体类修改成我们自己的实体类
这里就不做演示了
将启动类放到com.atguigu包下
4.1.1 持久层OaProcessTypeMapper
操作service-oa模块
package com.atguigu.process.mapper;
import com.atguigu.model.process.ProcessType;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 审批类型 Mapper 接口
* </p>
*/
public interface OaProcessTypeMapper extends BaseMapper<ProcessType> {
}
4.1.2 业务层接口OaProcessTypeService
package com.atguigu.process.service;
import com.atguigu.model.process.ProcessType;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 审批类型 服务类 77
* </p>
*/
public interface OaProcessTypeService extends IService<ProcessType> {
}
4.1.3 业务层接口实现类 OaProcessTypeServiceImpl
package com.atguigu.process.service.impl;
import com.atguigu.model.process.ProcessType;
import com.atguigu.process.mapper.OaProcessTypeMapper;
import com.atguigu.process.service.OaProcessTypeService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 审批类型 服务实现类 77
* </p>
*/
@Service
public class OaProcessTypeServiceImpl extends ServiceImpl<OaProcessTypeMapper, ProcessType> implements OaProcessTypeService {
}
4.1.4 控制层 OaProcessTypeController
这里实现审批类型的CRUD操作
package com.atguigu.process.controller;
import com.atguigu.common.result.Result;
import com.atguigu.model.process.ProcessType;
import com.atguigu.process.service.OaProcessTypeService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
/**
* <p>
* 审批类型 前端控制器 77
* </p>
*/
@RestController
@RequestMapping(value = "/admin/process/processType")
public class OaProcessTypeController {
@Autowired
private OaProcessTypeService processTypeService;
@ApiOperation(value = "获取分页列表")
@GetMapping("{page}/{limit}")
public Result index(@PathVariable Long page,
@PathVariable Long limit){
Page<ProcessType> pageParam = new Page<>();
IPage<ProcessType> pageModel = processTypeService.page(pageParam);
return Result.ok(pageModel);
}
@ApiOperation(value = "获取")
@GetMapping("get/{id}")
public Result get(@PathVariable Long id) {
ProcessType processType = processTypeService.getById(id);
return Result.ok(processType);
}
@ApiOperation(value = "新增")
@PostMapping("save")
public Result save(@RequestBody ProcessType processType) {
processTypeService.save(processType);
return Result.ok();
}
@ApiOperation(value = "修改")
@PutMapping("update")
public Result updateById(@RequestBody ProcessType processType) {
processTypeService.updateById(processType);
return Result.ok();
}
@ApiOperation(value = "删除")
@DeleteMapping("remove/{id}")
public Result remove(@PathVariable Long id) {
processTypeService.removeById(id);
return Result.ok();
}
}
4.2 审批类型前端 77
4.2.1 动态添加路由
在“系统管理”->“菜单管理”添加“审批设置”->“审批类型”
对于菜单信息,我们也可以直接导入菜单表初始化数据,后续不用再单独配置
4.2.2 定义api
创建src/api/process/processType.js
import request from '@/utils/request'
const api_name = '/admin/process/processType'
export default {
getPageList(page, limit) {
return request({
url: `${api_name}/${page}/${limit}`,
method: 'get'
})
},
getById(id) {
return request({
url: `${api_name}/get/${id}`,
method: 'get'
})
},
save(role) {
return request({
url: `${api_name}/save`,
method: 'post',
data: role
})
},
updateById(role) {
return request({
url: `${api_name}/update`,
method: 'put',
data: role
})
},
removeById(id) {
return request({
url: `${api_name}/remove/${id}`,
method: 'delete'
})
}
}
4.2.3 创建vue页面
创建views/processSet/processType/list.vue
<template>
<div class="app-container">
<!-- 工具条 -->
<div class="tools-div">
<el-button type="success" icon="el-icon-plus" size="mini" @click="add" :disabled="$hasBP('bnt.processType.add') === false">添 加</el-button>
</div>
<!-- banner列表 -->
<el-table
v-loading="listLoading"
:data="list"
stripe
border
style="width: 100%;margin-top: 10px;"
>
<el-table-column
type="selection"
width="55"
/>
<el-table-column
label="序号"
width="70"
align="center"
>
<template slot-scope="scope">
{{ (page - 1) * limit + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column prop="name" label="类型名称"/>
<el-table-column prop="description" label="描述"/>
<el-table-column prop="createTime" label="创建时间"/>
<el-table-column prop="updateTime" label="更新时间"/>
<el-table-column label="操作" width="200" align="center">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="edit(scope.row.id)" :disabled="$hasBP('bnt.processType.update') === false">修改</el-button>
<el-button type="text" size="mini" @click="removeDataById(scope.row.id)" :disabled="$hasBP('bnt.processType.remove') === false">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<el-pagination
:current-page="page"
:total="total"
:page-size="limit"
:page-sizes="[5, 10, 20, 30, 40, 50, 100]"
style="padding: 30px 0; text-align: center;"
layout="sizes, prev, pager, next, jumper, ->, total, slot"
@current-change="fetchData"
@size-change="changeSize"
/>
<el-dialog title="添加/修改" :visible.sync="dialogVisible" width="40%">
<el-form ref="flashPromotionForm" label-width="150px" size="small" style="padding-right: 40px;">
<el-form-item label="类型名称">
<el-input v-model="processType.name"/>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="processType.description"/>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false" size="small">取 消</el-button>
<el-button type="primary" @click="saveOrUpdate()" size="small">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import api from '@/api/process/processType'
const defaultForm = {
id: '',
name: '',
description: ''
}
export default {
data() {
return {
listLoading: true, // 数据是否正在加载
list: null, // banner列表
total: 0, // 数据库中的总记录数
page: 1, // 默认页码
limit: 10, // 每页记录数
searchObj: {}, // 查询表单对象
dialogVisible: false,
processType: defaultForm,
saveBtnDisabled: false
}
},
// 生命周期函数:内存准备完毕,页面尚未渲染
created() {
this.fetchData()
},
// 生命周期函数:内存准备完毕,页面渲染成功
mounted() {
},
methods: {
// 当页码发生改变的时候
changeSize(size) {
console.log(size)
this.limit = size
this.fetchData(1)
},
// 加载列表数据
fetchData(page = 1) {
this.page = page
api.getPageList(this.page, this.limit, this.searchObj).then(response => {
this.list = response.data.records
this.total = response.data.total
// 数据加载并绑定成功
this.listLoading = false
})
},
// 重置查询表单
resetData() {
console.log('重置查询表单')
this.searchObj = {}
this.fetchData()
},
// 根据id删除数据
removeDataById(id) {
this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => { // promise
// 点击确定,远程调用ajax
return api.removeById(id)
}).then((response) => {
this.fetchData(this.page)
this.$message.success(response.message)
}).catch(() => {
this.$message.info('取消删除')
})
},
add() {
this.dialogVisible = true
this.processType = Object.assign({}, defaultForm)
},
edit(id) {
this.dialogVisible = true
this.fetchDataById(id)
},
fetchDataById(id) {
api.getById(id).then(response => {
this.processType = response.data
})
},
saveOrUpdate() {
this.saveBtnDisabled = true // 防止表单重复提交
if (!this.processType.id) {
this.saveData()
} else {
this.updateData()
}
},
// 新增
saveData() {
api.save(this.processType).then(response => {
this.$message.success(response.message || '操作成功')
this.dialogVisible = false
this.fetchData(this.page)
})
},
// 根据id更新记录
updateData() {
api.updateById(this.processType).then(response => {
this.$message.success(response.message || '操作成功')
this.dialogVisible = false
this.fetchData(this.page)
})
}
}
}
</script>
测试
因为启动类我们移动了位置所以,注解ComponentScan就不再需要扫描包了因为它现在本来就在com.atguigu包下
但是启动还是报错了,说是mapper没找到(因为们用的process包下的mapper,不是之前的auth包了),这是因为我们之前在service-util中配置的分页插件没找到mapper
添加一个新的mapper路径
再次启动
后端启动成功
启动前端成功
浏览器输入http://localhost:9528/
添加
修改
删除
4.3审批模板 78
4.3.1 持久层 OaProcessTemplateService
操作service-oa模块
package com.atguigu.process.service;
import com.atguigu.model.process.ProcessTemplate;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 审批模板 服务类
* </p>
*/
public interface OaProcessTemplateService extends IService<ProcessTemplate> {
}
4.3.2 业务层接口OaProcessTemplateService
package com.atguigu.process.service;
import com.atguigu.model.process.ProcessTemplate;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 审批模板 服务类 77
* </p>
*/
public interface OaProcessTemplateService extends IService<ProcessTemplate> {
}
4.3.3 业务层接口实现类OaProcessTemplateServiceImpl
package com.atguigu.process.service.impl;
import com.atguigu.model.process.ProcessTemplate;
import com.atguigu.process.mapper.OaProcessTemplateMapper;
import com.atguigu.process.service.OaProcessTemplateService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 审批模板 服务实现类 77
* </p>
*/
@Service
public class OaProcessTemplateServiceImpl extends ServiceImpl<OaProcessTemplateMapper, ProcessTemplate> implements OaProcessTemplateService {
}
4.3.4 控制层OaProcessTemplateController 79
package com.atguigu.process.controller;
import com.atguigu.common.result.Result;
import com.atguigu.model.process.ProcessTemplate;
import com.atguigu.process.service.OaProcessTemplateService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* <p>
* 审批模板 前端控制器 78
* </p>
*/
@RestController
@RequestMapping(value = "/admin/process/processTemplate")
public class OaProcessTemplateController {
@Autowired
private OaProcessTemplateService processTemplateService;
//分页查询审批模板
@ApiOperation("获取分页审批模板数据")
@GetMapping("{page}/{limit}")
public Result index(@PathVariable Long page,
@PathVariable Long limit) {
Page<ProcessTemplate> pageParam = new Page<>(page,limit);
//分页查询审批模板,把审批类型对应名称查询 78
IPage<ProcessTemplate> pageModel =
processTemplateService.selectPageProcessTempate(pageParam);
return Result.ok(pageModel);
}
//@PreAuthorize("hasAuthority('bnt.processTemplate.list')")
@ApiOperation(value = "获取")
@GetMapping("get/{id}")
public Result get(@PathVariable Long id) {
ProcessTemplate processTemplate = processTemplateService.getById(id);
return Result.ok(processTemplate);
}
//@PreAuthorize("hasAuthority('bnt.processTemplate.templateSet')")
@ApiOperation(value = "新增")
@PostMapping("save")
public Result save(@RequestBody ProcessTemplate processTemplate) {
processTemplateService.save(processTemplate);
return Result.ok();
}
//@PreAuthorize("hasAuthority('bnt.processTemplate.templateSet')")
@ApiOperation(value = "修改")
@PutMapping("update")
public Result updateById(@RequestBody ProcessTemplate processTemplate) {
processTemplateService.updateById(processTemplate);
return Result.ok();
}
//@PreAuthorize("hasAuthority('bnt.processTemplate.remove')")
@ApiOperation(value = "删除")
@DeleteMapping("remove/{id}")
public Result remove(@PathVariable Long id) {
processTemplateService.removeById(id);
return Result.ok();
}
}
业务层接口
package com.atguigu.process.service;
import com.atguigu.model.process.ProcessTemplate;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 审批模板 服务类 77
* </p>
*/
public interface OaProcessTemplateService extends IService<ProcessTemplate> {
//分页查询审批模板,把审批类型对应名称查询 78
IPage<ProcessTemplate> selectPageProcessTempate(Page<ProcessTemplate> pageParam);
}
业务层接口实现类
package com.atguigu.process.service.impl;
import com.atguigu.model.process.ProcessTemplate;
import com.atguigu.model.process.ProcessType;
import com.atguigu.process.mapper.OaProcessTemplateMapper;
import com.atguigu.process.service.OaProcessTemplateService;
import com.atguigu.process.service.OaProcessTypeService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* <p>
* 审批模板 服务实现类 77
* </p>
*/
@Service
public class OaProcessTemplateServiceImpl extends ServiceImpl<OaProcessTemplateMapper, ProcessTemplate> implements OaProcessTemplateService {
@Autowired
private OaProcessTypeService processTypeService;
//分页查询审批模板,把审批类型对应名称查询 79
@Override
public IPage<ProcessTemplate> selectPageProcessTempate(Page<ProcessTemplate> pageParam) {
//1 调用mapper的方法实现分页查询
Page<ProcessTemplate> processTemplatePage = baseMapper.selectPage(pageParam, null);
//2 第一步分页查询返回分页数据,从分页数据获取列表list集合
List<ProcessTemplate> processTemplateList = processTemplatePage.getRecords();
//3 遍历list集合,得到每个对象的审批类型id
for(ProcessTemplate processTemplate : processTemplateList) {
//得到每个对象的审批类型id
Long processTypeId = processTemplate.getProcessTypeId();
//4 根据审批类型id,查询获取对应名称
LambdaQueryWrapper<ProcessType> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ProcessType::getId,processTypeId);
ProcessType processType = processTypeService.getOne(wrapper);
if(processType == null) {
continue;
}
//5 完成最终封装processTypeName
processTemplate.setProcessTypeName(processType.getName());
}
return processTemplatePage;
}
}
4.4 审批模板前端 79
4.4.1 动态添加路由
在“系统管理”->“菜单管理”添加“审批设置”->“审批类型”
对于菜单信息,我们也可以直接导入菜单表初始化数据,后续不用再单独配置
说明:“审批模板设置”页面内容较多,因此单独打开一个独立页面
4.4.2 定义api
创建src/api/process/processTemplate.js
基本的增删改查接口
import request from '@/utils/request'
const api_name = '/admin/process/processTemplate'
export default {
getPageList(page, limit) {
return request({
url: `${api_name}/${page}/${limit}`,
method: 'get'
})
},
getById(id) {
return request({
url: `${api_name}/get/${id}`,
method: 'get'
})
},
save(role) {
return request({
url: `${api_name}/save`,
method: 'post',
data: role
})
},
updateById(role) {
return request({
url: `${api_name}/update`,
method: 'put',
data: role
})
},
removeById(id) {
return request({
url: `${api_name}/remove/${id}`,
method: 'delete'
})
}
}
4.4.3 列表页面
创建views/processSet/processTemplate/list.vue
<template>
<div class="app-container">
<!-- 工具条 -->
<div class="tools-div">
<el-button type="success" icon="el-icon-plus" size="mini" @click="add()" :disabled="$hasBP('bnt.processTemplate.templateSet') === false">添加审批设置</el-button>
</div>
<!-- 列表 -->
<el-table
v-loading="listLoading"
:data="list"
stripe
border
style="width: 100%;margin-top: 10px;"
>
<el-table-column
label="序号"
width="70"
align="center"
>
<template slot-scope="scope">
{{ (page - 1) * limit + scope.$index + 1 }}
</template>
</el-table-column>iconPath
<el-table-column prop="name" label="审批名称"/>
<el-table-column label="图标">
<template slot-scope="scope">
<img :src="scope.row.iconUrl" style="width: 30px;height: 30px;vertical-align: text-bottom;">
</template>
</el-table-column>
<el-table-column prop="processTypeName" label="审批类型"/>
<el-table-column prop="description" label="描述"/>
<el-table-column prop="createTime" label="创建时间"/>
<el-table-column prop="updateTime" label="更新时间"/>
<el-table-column label="操作" width="250" align="center">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="edit(scope.row.id)" :disabled="$hasBP('bnt.processTemplate.templateSet') === false">修改审批设置</el-button>
<el-button type="text" size="mini" @click="removeDataById(scope.row.id)" :disabled="$hasBP('bnt.processTemplate.remove') === false">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<el-pagination
:current-page="page"
:total="total"
:page-size="limit"
:page-sizes="[5, 10, 20, 30, 40, 50, 100]"
style="padding: 30px 0; text-align: center;"
layout="sizes, prev, pager, next, jumper, ->, total, slot"
@current-change="fetchData"
@size-change="changeSize"
/>
</div>
</template>
<script>
import api from '@/api/process/processTemplate'
export default {
data() {
return {
listLoading: true, // 数据是否正在加载
list: null, // banner列表
total: 0, // 数据库中的总记录数
page: 1, // 默认页码
limit: 10, // 每页记录数
searchObj: {} // 查询表单对象
}
},
// 生命周期函数:内存准备完毕,页面尚未渲染
created() {
this.fetchData()
},
// 生命周期函数:内存准备完毕,页面渲染成功
mounted() {
},
methods: {
// 当页码发生改变的时候
changeSize(size) {
this.limit = size
this.fetchData(1)
},
// 加载banner列表数据
fetchData(page = 1) {
// 异步获取远程数据(ajax)
this.page = page
api.getPageList(this.page, this.limit, this.searchObj).then(
response => {
this.list = response.data.records
this.total = response.data.total
// 数据加载并绑定成功
this.listLoading = false
}
)
},
// 重置查询表单
resetData() {
this.searchObj = {}
this.fetchData()
},
// 根据id删除数据
removeDataById(id) {
this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => { // promise
// 点击确定,远程调用ajax
return api.removeById(id)
}).then((response) => {
this.fetchData(this.page)
this.$message.success(response.message)
}).catch(() => {
this.$message.info('取消删除')
})
},
add() {
this.$router.push('/processSet/templateSet')
},
edit(id) {
this.$router.push('/processSet/templateSet?id=' + id)
}
}
}
</script>
测试
启动前后端,成功
浏览器输入http://localhost:9528/
4.5 添加审批模板 80
效果图
1、基本设置:一些基本信息
2、表单设置:动态表单
3、流程设置:本地设计流程定义,上传流程定义文件及流程定义图片(压缩上传)
涉及未实现接口:
1、获取全部审批分类
2、上传流程定义压缩文件
4.5.1 form-create 80
官网:http://www.form-create.com/v2/guide/
轻松搞定 form 表单,让你不再为表单而烦恼。
form-create 是一个可以通过 JSON 生成具有动态渲染、数据收集、验证和提交功能的表单生成组件。
form-create-designer 是基于 form-create实现的表单设计器组件。可以通过拖拽的方式快速创建表单,提高开发者对表单的开发效率,节省开发者的时间
表单设计器:
http://www.form-create.com/designer/?fr=home
可以通过拖拽的方式快速配置动态表单,配置好的动态表单可以通过:生成JSON与生成Options获取数据,这两数据对于表字段:form_props与form_options,后续我们通过这两字段渲染动态表单。
大家可以根据表单设计器,查看数据格式
4.5.2 集成form-create 80
1、添加依赖
在package.json文件添加依赖,注意版本号,更高的版本号可能与本项目不兼容
下载这两个依赖
"@form-create/element-ui": "^2.5.17",
"@form-create/designer": "^1.0.8",
2、在 main.js 中写入以下内容:
import formCreate from '@form-create/element-ui'
import FcDesigner from '@form-create/designer'
Vue.use(formCreate)
Vue.use(FcDesigner)
3、集成表单设计器
创建views/processSet/processTemplate/templateSet.vue
<template>
<div class="app-container">
<div id="app1">
<fc-designer class="form-build" ref="designer"/>
<el-button @click="save">获取数据</el-button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
}
},
created() {
},
methods: {
save() {
console.log(this.$refs.designer.getRule())
console.log(this.$refs.designer.getOption())
}
}
}
</script>
显示效果:
随便拉几个表单项,点击“获取数据”,就是我们需要的动态表单数据格式了。
4.5.3 获取全部审批分类接口 80
操作service-oa模块
1、在OaProcessTypeController类添加接口
//查询所有审批分类 80
@GetMapping("findAll")
public Result findAll() {
List<ProcessType> list = processTypeService.list();
return Result.ok(list);
}
2、在processType.js添加前端接口
findAll() {
return request({
url: `${api_name}/findAll`,
method: 'get'
})
}
4.5.4 上传流程定义接口 80
上传用activiti做的流程定义接口 (我们上传到target\classes\processes目录下,processes是我们新建的文件夹专门放activiti资源的)
操作service-oa模块
在OaProcessTemplateController类添加接口
//上传用activiti做的流程定义接口 (我们上传到target\classes\processes目录下) 80
@ApiOperation(value = "上传流程定义")
@PostMapping("/uploadProcessDefinition")
public Result uploadProcessDefinition(MultipartFile file) throws FileNotFoundException {
//获取classes目录位置
String path = new File(ResourceUtils.getFile("classpath:")
.getPath()).getAbsolutePath();
//设置上传文件夹processes
File tempFile = new File(path + "/processes/");
if(!tempFile.exists()) {
tempFile.mkdirs();
}
//创建空文件,实现文件写入
String filename = file.getOriginalFilename();
File zipFile = new File(path + "/processes/" + filename);
//保存文件
try {
file.transferTo(zipFile);
} catch (IOException e) {
return Result.fail();
}
Map<String, Object> map = new HashMap<>();
//根据上传地址后续部署流程定义,文件名称为流程定义的默认key
map.put("processDefinitionPath", "processes/" + filename);
map.put("processDefinitionKey", filename.substring(0, filename.lastIndexOf(".")));
return Result.ok(map);
}
4.5.5 模板设置完整代码 80
<template>
<div class="app-container">
<el-steps :active="stepIndex" finish-status="success">
<el-step title="基本设置"></el-step>
<el-step title="表单设置"></el-step>
<el-step title="流程设置"></el-step>
</el-steps>
<div class="tools-div">
<el-button v-if="stepIndex > 1" icon="el-icon-check" type="primary" size="small" @click="pre()" round>上一步
</el-button>
<el-button icon="el-icon-check" type="primary" size="small" @click="next()" round>{{
stepIndex == 3 ? '提交保存' : '下一步'
}}
</el-button>
<el-button type="primary" size="small" @click="back()">返回</el-button>
</div>
<!-- 第一步 -->
<div v-show="stepIndex == 1" style="margin-top: 20px;">
<el-form ref="flashPromotionForm" label-width="150px" size="small" style="padding-right: 40px;">
<el-form-item label="审批类型">
<el-select v-model="processTemplate.processTypeId" placeholder="请选择审批类型">
<el-option :key="item.id" v-for="item in processTypeList" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="审批名称">
<el-input v-model="processTemplate.name"/>
</el-form-item>
<el-form-item label="审批图标">
<el-select v-model="processTemplate.iconUrl" placeholder="请选择审批图标">
<el-option :key="item.id" v-for="item in iconUrlList" :label="item.iconUrl" :value="item.iconUrl">
<img :src="item.iconUrl" style="width: 30px;height: 30px;vertical-align: text-bottom;">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="processTemplate.description"/>
</el-form-item>
</el-form>
</div>
<!-- 第二步 -->
<div v-show="stepIndex == 2" style="margin-top: 20px;">
<!--表单构建器-->
<fc-designer class="form-build" ref="designer"/>
</div>
<!-- 第三步 -->
<div v-show="stepIndex == 3" style="margin-top: 20px;">
<el-upload
class="upload-demo"
drag
action="/dev-api/admin/process/processTemplate/uploadProcessDefinition"
:headers="uploadHeaders"
multiple="false"
:before-upload="beforeUpload"
:on-success="onUploadSuccess"
:file-list="fileList"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将Activiti流程设计文件拖到此处,或<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">只能上传zip压缩文件,且不超过2048kb</div>
</el-upload>
</div>
</div>
</template>
<script>
import api from '@/api/process/processTemplate'
import processTypeApi from '@/api/process/processType'
import store from '@/store'
const defaultForm = {
id: '',
name: '',
iconUrl: '',
formProps: '',
formOptions: '',
processDefinitionKey: '',
processDefinitionPath: '',
description: ''
}
export default {
data() {
return {
stepIndex: 1,
processTypeList: [],
processTemplate: defaultForm,
iconUrlList: [
{ iconUrl: 'https://gw.alicdn.com/tfs/TB1t695CFYqK1RjSZLeXXbXppXa-102-102.png', tag: '请假' },
{ iconUrl: 'https://gw.alicdn.com/tfs/TB1bHOWCSzqK1RjSZFjXXblCFXa-112-112.png', tag: '出差' },
{ iconUrl: 'https://gw.alicdn.com/tfs/TB1cbCYCPTpK1RjSZKPXXa3UpXa-112-112.png', tag: '机票出差' },
{ iconUrl: 'https://gw.alicdn.com/tfs/TB1cbCYCPTpK1RjSZKPXXa3UpXa-112-112.png', tag: '机票改签' },
{ iconUrl: 'https://gw.alicdn.com/tfs/TB1e76lCOLaK1RjSZFxXXamPFXa-112-112.png', tag: '外出' },
{ iconUrl: 'https://gw.alicdn.com/tfs/TB1Yfa0CG6qK1RjSZFmXXX0PFXa-112-112.png', tag: '补卡申请' },
{ iconUrl: 'https://gw.alicdn.com/tfs/TB1Y8PlCNjaK1RjSZKzXXXVwXXa-112-112.png', tag: '加班' },
{ iconUrl: 'https://gw.alicdn.com/tfs/TB11X99CNTpK1RjSZFKXXa2wXXa-102-102.png', tag: '居家隔离' },
{ iconUrl: 'https://gw.alicdn.com/tfs/TB1_YG.COrpK1RjSZFhXXXSdXXa-102-102.png', tag: '请假' },
{ iconUrl: 'https://gw.alicdn.com/tfs/TB13ca1CMDqK1RjSZSyXXaxEVXa-102-102.png', tag: '调岗' },
{ iconUrl: 'https://gw.alicdn.com/tfs/TB1U9iBCSzqK1RjSZPcXXbTepXa-102-102.png', tag: '离职' },
{ iconUrl: 'https://gw.alicdn.com/tfs/TB11pS_CFzqK1RjSZSgXXcpAVXa-102-102.png', tag: '费用申请' },
{ iconUrl: 'https://gw.alicdn.com/tfs/TB1t695CFYqK1RjSZLeXXbXppXa-102-102.png', tag: '用章申请' },
{ iconUrl: 'https://gw.alicdn.com/tfs/TB13f_aCQzoK1RjSZFlXXai4VXa-102-102.png', tag: '携章外出' },
{ iconUrl: 'https://gw.alicdn.com/tfs/TB1_YG.COrpK1RjSZFhXXXSdXXa-102-102.png', tag: '学期内分期' },
{ iconUrl: 'https://gw.alicdn.com/tfs/TB1_YG.COrpK1RjSZFhXXXSdXXa-102-102.png', tag: '特殊学费' },
{ iconUrl: 'https://gw.alicdn.com/tfs/TB1Yfa0CG6qK1RjSZFmXXX0PFXa-112-112.png', tag: '充值卡申领' },
{ iconUrl: 'https://gw.alicdn.com/tfs/TB1e76lCOLaK1RjSZFxXXamPFXa-112-112.png', tag: '礼品申领' },
{ iconUrl: 'https://gw.alicdn.com/tfs/TB1FNG.CMHqK1RjSZFgXXa7JXXa-102-102.png', tag: '邮寄快递申请' },
{ iconUrl: 'https://gw.alicdn.com/imgextra/i3/O1CN01LLn0YV1LhBXs7T2iO_!!6000000001330-2-tps-120-120.png', tag: '合同审批' },
{ iconUrl: 'https://gw.alicdn.com/tfs/TB1e76lCOLaK1RjSZFxXXamPFXa-112-112.png', tag: '合同借阅' },
{ iconUrl: 'https://gw.alicdn.com/tfs/TB1e76lCOLaK1RjSZFxXXamPFXa-112-112.png', tag: '魔点临时开门权限' },
{ iconUrl: 'https://gw.alicdn.com/tfs/TB1bHOWCSzqK1RjSZFjXXblCFXa-112-112.png', tag: '北京科技园车证审批' },
{ iconUrl: 'https://gw.alicdn.com/tfs/TB1e76lCOLaK1RjSZFxXXamPFXa-112-112.png', tag: '魔点访客提前预约审批' }
],
uploadHeaders: {
'token': store.getters.token
},
fileList: []
}
},
created() {
let id = this.$route.query.id
console.log(id)
if (id > 0) {
this.fetchDataById(id)
}
this.fetchProcessTypeData()
},
methods: {
pre() {
this.stepIndex -= 1
},
next() {
if (this.stepIndex === 2) {
this.processTemplate.formProps = JSON.stringify(this.$refs.designer.getRule())
this.processTemplate.formOptions = JSON.stringify(this.$refs.designer.getOption())
console.log(JSON.stringify(this.processTemplate))
}
if (this.stepIndex === 3) {
this.saveOrUpdate()
}
this.stepIndex += 1
},
fetchProcessTypeData() {
processTypeApi.findAll().then(response => {
this.processTypeList = response.data
})
},
fetchDataById(id) {
api.getById(id).then(response => {
this.processTemplate = response.data
// 给表单设计器赋值
this.$refs.designer.setRule(JSON.parse(this.processTemplate.formProps))
this.$refs.designer.setOption(JSON.parse(this.processTemplate.formOptions))
this.fileList = [{
name: this.processTemplate.processDefinitionPath,
url: this.processTemplate.processDefinitionPath
}]
})
},
saveOrUpdate() {
this.saveBtnDisabled = true // 防止表单重复提交
if (!this.processTemplate.id) {
this.saveData()
} else {
this.updateData()
}
},
// 新增
saveData() {
api.save(this.processTemplate).then(response => {
this.$router.push('/processSet/processTemplate')
})
},
// 根据id更新记录
updateData() {
api.updateById(this.processTemplate).then(response => {
this.$router.push('/processSet/processTemplate')
})
},
// 文件上传限制条件
beforeUpload(file) {
const isZip = file.type === 'application/x-zip-compressed'
const isLt2M = file.size / 1024 / 1024 < 2
if (!isZip) {
this.$message.error('文件格式不正确!')
return false
}
if (!isLt2M) {
this.$message.error('上传头像图片大小不能超过 2MB!')
return false
}
return true
},
// 上传图片成功的回调
onUploadSuccess(res, file) {
// 填充上传文件列表
this.processTemplate.processDefinitionPath = res.data.processDefinitionPath
this.processTemplate.processDefinitionKey = res.data.processDefinitionKey
},
back() {
this.$router.push('/processSet/processTemplate')
}
}
}
</script>
测试
后端正常启动
前端启动成功
浏览器输入http://localhost:9528/
可以预览一下
上传文件,这里我们把资源放在了E:\java\yunshang_office_tools\activiti_ziyuan
成功后看后端,正好生成了我们对应的文件
点击保存
数据库中也增加了相应的记录
至此,成功
4.6 查看审批模板 81
查看审批模板基本信息、动态表单信息
4.6.1 添加按钮
<el-button type="text" size="mini" @click="show(scope.row)">查看审批设置</el-button>
4.6.2 定义data 81
rule: [],
option: {},
processTemplate: {},
formDialogVisible: false
4.6.3 定义方法 81
show(row) {
this.rule = JSON.parse(row.formProps)
this.option = JSON.parse(row.formOptions)
this.processTemplate = row
this.formDialogVisible = true
}
4.6.4 定义弹出层 81
<el-dialog title="查看审批设置" :visible.sync="formDialogVisible" width="35%">
<h3>基本信息</h3>
<el-divider/>
<el-form ref="flashPromotionForm" label-width="150px" size="small" style="padding-right: 40px;">
<el-form-item label="审批类型" style="margin-bottom: 0px;">{{ processTemplate.processTypeName }}</el-form-item>
<el-form-item label="名称" style="margin-bottom: 0px;">{{ processTemplate.name }}</el-form-item>
<el-form-item label="创建时间" style="margin-bottom: 0px;">{{ processTemplate.createTime }}</el-form-item>
</el-form>
<h3>表单信息</h3>
<el-divider/>
<div>
<form-create
:rule="rule"
:option="option"
></form-create>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="formDialogVisible = false" size="small">取 消</el-button>
</span>
</el-dialog>
测试
5. 发布 81
发布后审批模板就不可以修改了,然后部署流程定义
5.1 添加controller接口
操作service-oa模块
在OaProcessTemplateController类添加接口
//部署流程定义(发布) 81
@ApiOperation(value = "发布")
@GetMapping("/publish/{id}")
public Result publish(@PathVariable Long id) {
//修改模板发布状态 1 已经发布
//流程定义部署
processTemplateService.publish(id);
return Result.ok();
}
业务层接口OaProcessTemplateService
//部署流程定义(发布) 81
void publish(Long id);
业务层接口实现类OaProcessTemplateServiceImpl
//部署流程定义(发布) 81
@Override
public void publish(Long id) {
//修改模板发布状态 1 已经发布 81
ProcessTemplate processTemplate = baseMapper.selectById(id);
processTemplate.setStatus(1);
baseMapper.updateById(processTemplate);
//后续完成流程定义的部署
}
5.2 前端整合 81
1、添加api接口
publish(id) {
return request({
url: `${api_name}/publish/${id}`,
method: 'get'
})
}
2、添加按钮
<el-button v-if="scope.row.status == 0" type="text" size="mini" @click="publish(scope.row.id)" :disabled="$hasBP('bnt.processTemplate.publish') === false">发布</el-button>
3、添加按钮方法
publish(id) {
api.publish(id).then(response => {
this.$message.success('发布成功')
this.fetchData(this.page)
})
}
测试
浏览器输入http://localhost:9528/
数据库也有相应变化
好博客就要一起分享哦!分享海报
此处可发布评论
评论(0)展开评论
展开评论