gin-vue-admin是一套国人用golang开发的后台管理系统,本文记录插件开发内容。
官网:https://www.gin-vue-admin.com/
学习视频:https://www.bilibili.com/video/BV1kv4y1g7nT/
后端位置:\server\plugin\
前端位置:\web\src\plugin\
建议插件目录采用:家族标识\插件名\
例如test插件目录:abc123\test\
这么安排目录的原因是尽量不要把同一个项目打碎后分配到各个目录去,而是集中在一起,方便未来备份、转移等。
gin-vue-admin的plugin相当于一个五脏俱全的app,所以可以实现上面所建议的插件式目录分配方案。
现在正式开始
\server\plugin\abc123\test\api\
\server\plugin\abc123\test\api\api.go
\server\plugin\abc123\test\api\enter.go
\server\plugin\abc123\test\config\
\server\plugin\abc123\test\config\config.go
\server\plugin\abc123\test\global\
\server\plugin\abc123\test\global\global.go
\server\plugin\abc123\test\model\
\server\plugin\abc123\test\model\request
\server\plugin\abc123\test\model\request\cms.go
\server\plugin\abc123\test\model\model.go
\server\plugin\abc123\test\router\
\server\plugin\abc123\test\router\enter.go
\server\plugin\abc123\test\router\router.go
\server\plugin\abc123\test\service\
\server\plugin\abc123\test\router\enter.go
\server\plugin\abc123\test\router\service.go
\server\plugin\abc123\test\main.go
\web\src\plugin\abc123\test\api\
\web\src\plugin\abc123\test\api\cms.js
\web\src\plugin\abc123\test\view\cms\
\web\src\plugin\abc123\test\view\cms\cms.vue
\web\src\plugin\abc123\test\view\setting\
\web\src\plugin\abc123\test\view\setting\a.vue
\web\src\plugin\abc123\test\view\setting\b.vue
本例中前端实际就涉及:
cms.vue
用于界面设置;cms.js
,用于和后端通讯;详细开发可以看官方文档,也可以用自动化代码管理器生成后拷贝其代码至插件目录。
Golang学习日志 ━━ Gin-Vue-Admin按步骤手动创建api及router、service
后端文件比较多,只举例一部分。
\server\plugin\abc123\test\api\api.go
package api
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/abc123/test/model"
appReq "github.com/flipped-aurora/gin-vue-admin/server/plugin/abc123/test/model/request"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/abc123/test/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type ABC123AppApi struct{}
var appService = service.ServiceGroupApp.ABC123AppService
// @Tags ABC123App
// @Summary 请手动填写接口功能
// @Produce application/json
// @Success 200 {string} string "{"success":true,"data":{},"msg":"发送成功"}"
// @Router /abc123/app/routerName [post]
func (p *ABC123AppApi) ApiName(c *gin.Context) {
var plug model.Request
_ = c.ShouldBindJSON(&plug)
if res, err := service.ServiceGroupApp.PlugService(plug); err != nil {
global.GVA_LOG.Error("失败!", zap.Error(err))
response.FailWithMessage("失败", c)
} else {
response.OkWithDetailed(res, "成功", c)
}
}
// CreateTest 创建Test
// @Tags Test
// @Summary 创建Test
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body model.Test true "创建Test"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /abc123/app/createTest [post]
func (appApi *ABC123AppApi) CreateTest(c *gin.Context) {
var data model.Test
err := c.ShouldBindJSON(&data)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if err := appService.CreateTest(data); err != nil {
global.GVA_LOG.Error("创建失败!", zap.Error(err))
response.FailWithMessage("创建失败", c)
} else {
response.OkWithMessage("创建成功", c)
}
}
// DeleteTest 删除Test
// @Tags Test
// @Summary 删除Test
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body model.Test true "删除Test"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
// @Router /abc123/app/deleteTest [delete]
func (appApi *ABC123AppApi) DeleteTest(c *gin.Context) {
var data model.Test
err := c.ShouldBindJSON(&data)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if err := appService.DeleteTest(data); err != nil {
global.GVA_LOG.Error("删除失败!", zap.Error(err))
response.FailWithMessage("删除失败", c)
} else {
response.OkWithMessage("删除成功", c)
}
}
// DeleteTestByIds 批量删除Test
// @Tags Test
// @Summary 批量删除Test
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.IdsReq true "批量删除Test"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"批量删除成功"}"
// @Router /abc123/app/deleteTestByIds [delete]
func (appApi *ABC123AppApi) DeleteTestByIds(c *gin.Context) {
var IDS request.IdsReq
err := c.ShouldBindJSON(&IDS)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if err := appService.DeleteTestByIds(IDS); err != nil {
global.GVA_LOG.Error("批量删除失败!", zap.Error(err))
response.FailWithMessage("批量删除失败", c)
} else {
response.OkWithMessage("批量删除成功", c)
}
}
// UpdateTest 更新Test
// @Tags Test
// @Summary 更新Test
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body model.Test true "更新Test"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}"
// @Router /abc123/app/updateTest [put]
func (appApi *ABC123AppApi) UpdateTest(c *gin.Context) {
var data model.Test
err := c.ShouldBindJSON(&data)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if err := appService.UpdateTest(data); err != nil {
global.GVA_LOG.Error("更新失败!", zap.Error(err))
response.FailWithMessage("更新失败", c)
} else {
response.OkWithMessage("更新成功", c)
}
}
// FindTest 用id查询Test
// @Tags Test
// @Summary 用id查询Test
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data query model.Test true "用id查询Test"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}"
// @Router /abc123/app/findTest [get]
func (appApi *ABC123AppApi) FindTest(c *gin.Context) {
var data model.Test
err := c.ShouldBindQuery(&data)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if redata, err := appService.GetTest(data.ID); err != nil {
global.GVA_LOG.Error("查询失败!", zap.Error(err))
response.FailWithMessage("查询失败", c)
} else {
response.OkWithData(gin.H{"redata": redata}, c)
}
}
// GetTestList 分页获取Test列表
// @Tags Test
// @Summary 分页获取Test列表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data query appReq.TestSearch true "分页获取Test列表"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /abc123/app/getTestList [get]
func (appApi *ABC123AppApi) GetTestList(c *gin.Context) {
var pageInfo appReq.TestSearch
err := c.ShouldBindQuery(&pageInfo)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if list, total, err := appService.GetTestInfoList(pageInfo); err != nil {
global.GVA_LOG.Error("获取失败!", zap.Error(err))
response.FailWithMessage("获取失败", c)
} else {
response.OkWithDetailed(response.PageResult{
List: list,
Total: total,
Page: pageInfo.Page,
PageSize: pageInfo.PageSize,
}, "获取成功", c)
}
}
\server\plugin\abc123\test\router\router.go
package router
import (
"github.com/flipped-aurora/gin-vue-admin/server/middleware"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/abc123/test/api"
"github.com/gin-gonic/gin"
)
type ABC123AppRouter struct {
}
func (s *ABC123AppRouter) InitABC123AppRouter(Router *gin.RouterGroup) {
plugRouter := Router.Use(middleware.OperationRecord()) // 行为记录 middleware.OperationRecord()
plugRouterWithoutRecord := Router
plugApi := api.ApiGroupApp.ABC123AppApi
{
plugRouter.POST("routerName", plugApi.ApiName)
}
{
plugRouter.POST("createTest", plugApi.CreateTest) // 新建Test
plugRouter.DELETE("deleteTest", plugApi.DeleteTest) // 删除Test
plugRouter.DELETE("deleteTestByIds", plugApi.DeleteTestByIds) // 批量删除Test
plugRouter.PUT("updateTest", plugApi.UpdateTest) // 更新Test
}
{
plugRouterWithoutRecord.GET("findTest", plugApi.FindTest) // 根据ID获取Test
plugRouterWithoutRecord.GET("getTestList", plugApi.GetTestList) // 获取Test列表
}
}
\web\src\plugin\abc123\test\api\cms.vue
<template>
<div>
<div class="gva-search-box">
<el-form :inline="true" :model="searchInfo" class="demo-form-inline">
<el-form-item label="创建时间">
<el-date-picker v-model="searchInfo.startCreatedAt" type="datetime" placeholder="开始时间"></el-date-picker>
—
<el-date-picker v-model="searchInfo.endCreatedAt" type="datetime" placeholder="结束时间"></el-date-picker>
</el-form-item>
<el-form-item>
<el-button size="small" type="primary" icon="search" @click="onSubmit">查询</el-button>
<el-button size="small" icon="refresh" @click="onReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button size="small" type="primary" icon="plus" @click="openDialog">新增</el-button>
<el-popover v-model:visible="deleteVisible" placement="top" width="160">
<p>确定要删除吗?</p>
<div style="text-align: right; margin-top: 8px;">
<el-button size="small" type="primary" link @click="deleteVisible = false">取消</el-button>
<el-button size="small" type="primary" @click="onDelete">确定</el-button>
</div>
<template #reference>
<el-button icon="delete" size="small" style="margin-left: 10px;" :disabled="!multipleSelection.length" @click="deleteVisible = true">删除</el-button>
</template>
</el-popover>
</div>
<el-table
ref="multipleTable"
style="width: 100%"
tooltip-effect="dark"
:data="tableData"
row-key="ID"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column align="left" label="日期" width="180">
<template #default="scope">{{ formatDate(scope.row.CreatedAt) }}</template>
</el-table-column>
<el-table-column align="left" label="title字段" prop="title" width="120" />
<el-table-column align="left" label="content字段" prop="content" width="120" />
<el-table-column align="left" label="showtime字段" width="180">
<template #default="scope">{{ formatDate(scope.row.showtime) }}</template>
</el-table-column>
<el-table-column align="left" label="按钮组">
<template #default="scope">
<el-button type="primary" link icon="edit" size="small" class="table-button" @click="updateCmsFunc(scope.row)">变更</el-button>
<el-button type="primary" link icon="delete" size="small" @click="deleteRow(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="gva-pagination">
<el-pagination
layout="total, sizes, prev, pager, next, jumper"
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
:total="total"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
<el-dialog v-model="dialogFormVisible" :before-close="closeDialog" title="弹窗操作" :destroy-on-close="true">
<el-form :model="formData" label-position="right" ref="elFormRef" :rules="rule" label-width="80px">
<el-form-item label="标题:" prop="title" >
<el-input v-model="formData.title" :clearable="true" placeholder="请输入" />
</el-form-item>
<el-form-item label="内容:" prop="content" >
<!--el-input id="editor" v-model="formData.content" :clearable="true" placeholder="请输入" /-->
<Editor
:api-key="tinymceApiKey"
:init="tinymceInit"
v-model="formData.content"
/>
</el-form-item>
<el-form-item label="显示时间:" prop="showtime" >
<el-date-picker v-model="formData.showtime" type="date" style="width:100%" placeholder="选择日期" :clearable="true" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button size="small" @click="closeDialog">取 消</el-button>
<el-button size="small" type="primary" @click="enterDialog">确 定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'Cms'
}
</script>
<script setup>
import {
createCms,
deleteCms,
deleteCmsByIds,
updateCms,
findCms,
getCmsList
} from '@/plugin/abc123/test/api/cms'
// 全量引入格式化工具 请按需保留
import { getDictFunc, formatDate, formatBoolean, filterDict } from '@/utils/format'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ref, reactive } from 'vue'
const elFormRef = ref()
// =========== 表格控制部分 ===========
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const tableData = ref([])
const searchInfo = ref({})
// 重置
const onReset = () => {
searchInfo.value = {}
getTableData()
}
// 搜索
const onSubmit = () => {
page.value = 1
pageSize.value = 10
getTableData()
}
// 分页
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
// 修改页面容量
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
// 查询
const getTableData = async() => {
const table = await getCmsList({ page: page.value, pageSize: pageSize.value, ...searchInfo.value })
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
}
}
getTableData()
// ============== 表格控制部分结束 ===============
// 获取需要的字典 可能为空 按需保留
const setOptions = async () =>{
}
// 获取需要的字典 可能为空 按需保留
setOptions()
// 多选数据
const multipleSelection = ref([])
// 多选
const handleSelectionChange = (val) => {
multipleSelection.value = val
}
// 删除行
const deleteRow = (row) => {
ElMessageBox.confirm('确定要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteCmsFunc(row)
})
}
// 批量删除控制标记
const deleteVisible = ref(false)
// 多选删除
const onDelete = async() => {
const ids = []
if (multipleSelection.value.length === 0) {
ElMessage({
type: 'warning',
message: '请选择要删除的数据'
})
return
}
multipleSelection.value &&
multipleSelection.value.map(item => {
ids.push(item.ID)
})
const res = await deleteCmsByIds({ ids })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功'
})
if (tableData.value.length === ids.length && page.value > 1) {
page.value--
}
deleteVisible.value = false
getTableData()
}
}
// 行为控制标记(弹窗内部需要增还是改)
const type = ref('')
// 更新行
const updateCmsFunc = async(row) => {
const res = await findCms({ ID: row.ID })
type.value = 'update'
if (res.code === 0) {
formData.value = res.data.redata
dialogFormVisible.value = true
}
}
// 删除行
const deleteCmsFunc = async (row) => {
const res = await deleteCms({ ID: row.ID })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功'
})
if (tableData.value.length === 1 && page.value > 1) {
page.value--
}
getTableData()
}
}
// 弹窗控制标记
const dialogFormVisible = ref(false)
// 打开弹窗
const openDialog = () => {
type.value = 'create'
dialogFormVisible.value = true
}
// 关闭弹窗
const closeDialog = () => {
dialogFormVisible.value = false
formData.value = {
title: '',
content: '',
showtime: new Date(),
}
}
// 弹窗确定
const enterDialog = async () => {
elFormRef.value?.validate( async (valid) => {
if (!valid) return
let res
switch (type.value) {
case 'create':
res = await createCms(formData.value)
break
case 'update':
res = await updateCms(formData.value)
break
default:
res = await createCms(formData.value)
break
}
if (res.code === 0) {
ElMessage({
type: 'success',
message: '创建/更改成功'
})
closeDialog()
getTableData()
}
})
}
</script>
<style>
/* 在el-dialog中tinymce z-index 被太小而被遮挡时要加这两句 */
.tox-tinymce-aux{z-index:99999 !important;}
.tinymce.ui.FloatPanel{z-Index: 99;}
</style>
<style scoped>
@media (min-width: 1024px) {
#sample {
display: flex;
flex-direction: column;
place-items: center;
width: 1000px;
}
}
</style>
\web\src\plugin\abc123\test\api\cms.js
import service from '@/utils/request'
// @Tags Cms
// @Summary 创建Cms
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body model.Cms true "创建Cms"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /abc123/test/createCms [post]
export const createCms = (data) => {
return service({
url: '/abc123/test/createTest',
method: 'post',
data
})
}
// @Tags Cms
// @Summary 删除Cms
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body model.Cms true "删除Cms"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
// @Router /abc123/test/deleteCms [delete]
export const deleteCms = (data) => {
return service({
url: '/abc123/test/deleteTest',
method: 'delete',
data
})
}
// @Tags Cms
// @Summary 删除Cms
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.IdsReq true "批量删除Cms"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
// @Router /abc123/test/deleteCms [delete]
export const deleteCmsByIds = (data) => {
return service({
url: '/abc123/test/deleteTestByIds',
method: 'delete',
data
})
}
// @Tags Cms
// @Summary 更新Cms
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body model.Cms true "更新Cms"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}"
// @Router /abc123/test/updateCms [put]
export const updateCms = (data) => {
return service({
url: '/abc123/test/updateTest',
method: 'put',
data
})
}
// @Tags Cms
// @Summary 用id查询Cms
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data query model.Cms true "用id查询Cms"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}"
// @Router /abc123/test/findCms [get]
export const findCms = (params) => {
return service({
url: '/abc123/test/findTest',
method: 'get',
params
})
}
// @Tags Cms
// @Summary 分页获取Cms列表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data query request.PageInfo true "分页获取Cms列表"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /abc123/test/getCmsList [get]
export const getCmsList = (params) => {
return service({
url: '/abc123/test/getTestList',
method: 'get',
params
})
}
现在前后端都开发好了,那么如何实现前后端沟通呢?
注册插件即可,因为路由等已经在插件内部设置完成。
\server\initialize\plugin.go
package initialize
import (
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/middleware"
...
...
...
"github.com/flipped-aurora/gin-vue-admin/server/plugin/abc123/test" // 调用测试插件
"github.com/flipped-aurora/gin-vue-admin/server/utils/plugin"
"github.com/gin-gonic/gin"
)
func PluginInit(group *gin.RouterGroup, Plugin ...plugin.Plugin) {
for i := range Plugin {
PluginGroup := group.Group(Plugin[i].RouterPath())
Plugin[i].Register(PluginGroup)
}
}
func InstallPlugin(Router *gin.Engine) {
PublicGroup := Router.Group("")
fmt.Println("无鉴权插件安装==》", PublicGroup)
PrivateGroup := Router.Group("")
fmt.Println("鉴权插件安装==》", PrivateGroup)
PrivateGroup.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler())
// 添加跟角色挂钩权限的插件 示例 本地示例模式于在线仓库模式注意上方的import 可以自行切换 效果相同
...
...
...
PluginInit(PrivateGroup, abc123_test.CreateABC123AppPlug("")) // 注册测试插件
}
代码已经不需要修改什么了,只需要在后台中设置好api、页面,再对角色进行授权就行。
这部分了解gin-vue-admin的同学可以直接跳过了,因为这和非插件形式的操作是一致的。现在插件已经注册,路由也已经确定,基本没有新的内容了~~哈哈
测试环境运行如下命令:
go run main.go
npm run serve