前章
1.开发环境搭建
2.项目环境搭建
3.golang项目基础框架-前篇
4.golang项目基础框架-后篇
5.管理后台vue项目搭建
引言
经过前面几章的积累,已经形成前后端可对接的环境;本篇则是基于前期搭建的环境,进行纯粹的CRUD,将管理后台(菜单管理、用户管理、角色管理)作为示例,该项目如何进行开发。
项目地址
本次的代码仓库已更新到完整后台管理功能,api接口与vue后台页面都已上传。
# 管理后台前端
https://github.com/ZTLiao/liaz-admin-web.git
# 服务端
https://github.com/ZTLiao/liaz-server.git
设计
管理后台的基础:用户、菜单、角色,就这三者形成套简易的权限控制。
基于mysql输出数据库表设计:
drop table if exists admin_user;
create table admin_user(
admin_id bigint not null auto_increment comment '主键',
name varchar(64) not null comment '姓名',
username varchar(64) not null comment '账号',
password varchar(128) not null comment '密码',
phone varchar(64) comment '手机号码',
salt varchar(64) comment '盐',
avatar varchar(128) comment '头像',
email varchar(64) comment '邮箱',
introduction varchar(128) comment '描述',
status tinyint(4) default 0 comment '状态',
created_at timestamp(3) default current_timestamp(3) comment '创建时间',
updated_at timestamp(3) default current_timestamp(3) on update current_timestamp(3) comment '更新时间',
primary key(admin_id)
) comment '系统用户表';
drop table if exists admin_menu;
create table admin_menu(
menu_id bigint not null auto_increment comment '主键',
parent_id bigint default 0 comment '父级ID',
name varchar(64) comment '名称',
path varchar(64) comment '路径',
icon varchar(64) comment '图标',
status tinyint(4) default 0 comment '状态',
show_order int(11) default 0 comment '排序',
description varchar(128) comment '描述',
created_at timestamp(3) default current_timestamp(3) comment '创建时间',
updated_at timestamp(3) default current_timestamp(3) on update current_timestamp(3) comment '更新时间',
primary key(menu_id)
) comment '系统菜单表';
drop table if exists admin_role;
create table admin_role(
role_id bigint not null auto_increment comment '主键',
name varchar(64) comment '名称',
created_at timestamp(3) default current_timestamp(3) comment '创建时间',
updated_at timestamp(3) default current_timestamp(3) on update current_timestamp(3) comment '更新时间',
primary key(role_id)
) comment '系统角色表';
drop table if exists admin_role_menu;
create table admin_role_menu(
role_id bigint comment '角色ID',
menu_id bigint comment '菜单ID',
created_at timestamp(3) default current_timestamp(3) comment '创建时间',
key(role_id, menu_id)
) comment '系统角色菜单表';
drop table if exists admin_user_role;
create table admin_user_role(
admin_id bigint comment '用户ID',
role_id bigint comment '角色ID',
created_at timestamp(3) default current_timestamp(3) comment '创建时间',
key(admin_id, role_id)
) comment '系统用户角色表';
开发
先从服务端api开始,因本项目已有nacos、xorm、gin、go-redis,配置将放于nacos上;在golang项目基础框架-前篇中讲述nacos的加载过程。
application.yml
# 日志配置
logger:
type: text
path: ./logs/web_info.log
level: info
# stdout 控制台 file 文件
stdout: stdout
database.yaml
# mysql配置
database:
driver: mysql
url: tcp(127.0.0.1:3306)/liaz?charset=utf8&parseTime=True&loc=Local
username: root
password: 密码
maxIdleConns: 10
maxOpenConns: 100
showSQL: true
# redis配置
redis:
host: 127.0.0.1
port: 6379
password: ""
db: 0
minIdleConns: 0
maxIdleConns: 50
maxActiveConns: 100
# 对象存储配置
minio:
endpoint: 127.0.0.1:9000
accessKeyId: admin
secretAccessKey: admin123
secure: false
# 阿里云oss对象存储配置
oss:
accessKeyId:
accessKeySecret:
bucketName:
endpoint:
# 腾讯云cos对象存储配置
cos:
secretId:
secretKey:
serviceUrl:
bucketUrl:
liaz-admin.yml
相对于不同模块定义名称不一样,主要取自于main.go
package main
import (
_ "admin/router"
"core/cmd"
)
func main() {
cmd.Execute("liaz-admin")
}
# 服务端口
server:
port: 8080
# 权限控制配置
security:
excludes:
- "/admin/login"
以用户管理作为示例
admin_user.go
定义AdminUser用户实体类
package model
import (
"core/model"
"core/types"
)
type AdminUser struct {
AdminId int64 `json:"adminId" xorm:"admin_id pk autoincr BIGINT"`
Name string `json:"name" xorm:"name"`
Username string `json:"username" xorm:"username"`
Password string `json:"password" xorm:"password"`
Salt string `json:"salt" xorm:"salt"`
Phone string `json:"phone" xorm:"phone"`
Avatar string `json:"avatar" xorm:"avatar"`
Email string `json:"email" xorm:"email"`
Introduction string `json:"introduction" xorm:"introduction"`
Status int8 `json:"status" xorm:"status"`
CreatedAt types.Time `json:"createdAt" xorm:"created_at timestamp created"`
UpdatedAt types.Time `json:"updatedAt" xorm:"updated_at timestamp updated"`
}
var _ model.BaseModel = &AdminUser{}
func (e *AdminUser) TableName() string {
return "admin_user"
}
core/model是实体类的基础接口,先设计预留(暂未用上)
package model
type BaseModel interface {
TableName() string
}
core/types将golang的时间类json序列化为毫秒级时间戳,用于各个端接入通用结构;这么定义也是有原因的,golang的json序列化并没有提供时间类型time.Time转时间戳的工具类
package types
import (
"core/utils"
"strconv"
"strings"
"time"
)
type Time time.Time
func (t *Time) UnmarshalJSON(data []byte) (err error) {
if len(data) == 2 {
*t = Time(time.Time{})
return
}
if strings.Contains(string(data), utils.DASHED) && strings.Contains(string(data), utils.DOT) {
now, err := time.ParseInLocation(`"`+utils.NORM_DATETIME_MS_PATTERN+`"`, string(data), time.Local)
*t = Time(now)
return err
} else if strings.Contains(string(data), utils.DASHED) {
now, err := time.ParseInLocation(`"`+utils.NORM_DATETIME_PATTERN+`"`, string(data), time.Local)
*t = Time(now)
return err
} else {
timestamp, err := strconv.ParseInt(string(data), 10, 64)
*t = Time(time.Unix(timestamp/1000, (timestamp%1000)*int64(time.Millisecond)))
return err
}
}
func (t Time) MarshalJSON() ([]byte, error) {
return []byte(strconv.FormatInt(int64(time.Time(t).UnixMilli()), 10)), nil
}
在AdminUser类中,反单引号**`相当于java中的注解功能,扩展属性的作用,以下这行的定义就是json序列化后返回属性名与xorm**数据库表映射对应字段(pk:主键,autoincr:自增,BIGINT:数据类型)
`json:"adminId" xorm:"admin_id pk autoincr BIGINT"`
admin_user_db.go
xorm单表增删改查功能
package storage
import (
"admin/enums"
"admin/model"
"core/types"
"time"
"github.com/go-xorm/xorm"
)
type AdminUserDb struct {
db *xorm.Engine
}
func NewAdminUserDb(db *xorm.Engine) *AdminUserDb {
return &AdminUserDb{db}
}
// 获取登录用户信息
func (e *AdminUserDb) GetLoginUser(username string, password string) (*model.AdminUser, error) {
var adminUsers []model.AdminUser
err := e.db.Where("(username = ? or phone = ? or email = ?) and password = ? and status = ?", username, username, username, password, enums.USER_STATUS_OF_ENABLE).Find(&adminUsers)
if err != nil {
return nil, err
}
if len(adminUsers) == 0 {
return nil, nil
}
return &adminUsers[0], nil
}
func (e *AdminUserDb) GetAdminUserList() ([]model.AdminUser, error) {
var adminUsers []model.AdminUser
err := e.db.OrderBy("created_at desc").Find(&adminUsers)
if err != nil {
return nil, err
}
return adminUsers, nil
}
func (e *AdminUserDb) SaveOrUpdateAdminUser(adminUser *model.AdminUser) error {
var now = types.Time(time.Now())
adminId := adminUser.AdminId
if adminId == 0 {
adminUser.CreatedAt = now
_, err := e.db.Insert(adminUser)
if err != nil {
return err
}
} else {
adminUser.UpdatedAt = now
_, err := e.db.ID(adminId).Update(adminUser)
if err != nil {
return err
}
}
return nil
}
func (e *AdminUserDb) DelAdminUser(adminId int64) error {
_, err := e.db.ID(adminId).Delete(&model.AdminUser{})
if err != nil {
return err
}
return nil
}
func (e *AdminUserDb) ThawAdminUser(adminId int64) error {
_, err := e.db.ID(adminId).Update(&model.AdminUser{
Status: 1,
})
if err != nil {
return err
}
return nil
}
golang在数据库orm框架选用时,笔者纠结了好久,一直在xorm/gorm
/sqlx徘徊着,经过我一番调研,最终决定使用xorm;
用java的orm框架来解释:
gorm就相当于hibernate,xorm就是mybatis,而sqlx为spring-jdbc
admin_user_handler.go
接口业务逻辑层
package handler
import (
"admin/model"
"admin/resp"
"admin/storage"
"core/constant"
"core/response"
"core/web"
"fmt"
"net/http"
"strconv"
)
type AdminUserHandler struct {
AdminUserDb *storage.AdminUserDb
AdminLoginRecordDb *storage.AdminLoginRecordDb
AdminUserCache *storage.AdminUserCache
AccessTokenCache *storage.AccessTokenCache
}
func (e *AdminUserHandler) GetAdminUser(wc *web.WebContext) interface{} {
accessToken := wc.GetHeader(constant.AUTHORIZATION)
adminUser, err := e.AdminUserCache.Get(accessToken)
if err != nil {
wc.AbortWithError(err)
}
if adminUser == nil {
return response.ReturnError(http.StatusForbidden, constant.ILLEGAL_REQUEST)
}
lastTime, err := e.AdminLoginRecordDb.GetLastTime(adminUser.AdminId)
if err != nil {
wc.AbortWithError(err)
}
return response.ReturnOK(&resp.AdminUserResp{
AdminId: adminUser.AdminId,
Name: adminUser.Name,
Username: adminUser.Username,
Avatar: adminUser.Avatar,
LastTime: lastTime,
})
}
func (e *AdminUserHandler) GetAdminUserList(wc *web.WebContext) interface{} {
adminUsers, err := e.AdminUserDb.GetAdminUserList()
if err != nil {
wc.AbortWithError(err)
}
return response.ReturnOK(adminUsers)
}
func (e *AdminUserHandler) SaveAdminUser(wc *web.WebContext) interface{} {
e.saveOrUpdateAdminUser(wc)
return response.Success()
}
func (e *AdminUserHandler) UpdateAdminUser(wc *web.WebContext) interface{} {
e.saveOrUpdateAdminUser(wc)
return response.Success()
}
func (e *AdminUserHandler) saveOrUpdateAdminUser(wc *web.WebContext) {
var params map[string]any
if err := wc.ShouldBindJSON(¶ms); err != nil {
wc.AbortWithError(err)
}
adminIdStr := fmt.Sprint(params["adminId"])
name := fmt.Sprint(params["name"])
username := fmt.Sprint(params["username"])
password := fmt.Sprint(params["password"])
phone := fmt.Sprint(params["phone"])
avatar := fmt.Sprint(params["avatar"])
email := fmt.Sprint(params["email"])
introduction := fmt.Sprint(params["introduction"])
statusStr := fmt.Sprint(params["status"])
var adminUser = new(model.AdminUser)
if len(adminIdStr) > 0 {
adminId, err := strconv.ParseInt(adminIdStr, 10, 64)
if err != nil {
wc.AbortWithError(err)
}
adminUser.AdminId = adminId
}
adminUser.Name = name
adminUser.Username = username
adminUser.Password = password
adminUser.Phone = phone
adminUser.Avatar = avatar
adminUser.Email = email
adminUser.Introduction = introduction
status, err := strconv.ParseInt(statusStr, 10, 64)
if err != nil {
wc.AbortWithError(err)
}
adminUser.Status = int8(status)
e.AdminUserDb.SaveOrUpdateAdminUser(adminUser)
}
func (e *AdminUserHandler) DelAdminUser(wc *web.WebContext) interface{} {
adminIdStr := wc.Param("adminId")
if len(adminIdStr) > 0 {
adminId, err := strconv.ParseInt(adminIdStr, 10, 64)
if err != nil {
wc.AbortWithError(err)
}
e.AdminUserDb.DelAdminUser(adminId)
accessToken, err := e.AccessTokenCache.Get(adminId)
if err != nil {
wc.AbortWithError(err)
}
if len(accessToken) > 0 {
e.AdminUserCache.Del(accessToken)
}
}
return response.Success()
}
func (e *AdminUserHandler) ThawAdminUser(wc *web.WebContext) interface{} {
adminIdStr := wc.PostForm("adminId")
if len(adminIdStr) > 0 {
adminId, err := strconv.ParseInt(adminIdStr, 10, 64)
if err != nil {
wc.AbortWithError(err)
}
e.AdminUserDb.ThawAdminUser(adminId)
accessToken, err := e.AccessTokenCache.Get(adminId)
if err != nil {
wc.AbortWithError(err)
}
if len(accessToken) > 0 {
e.AdminUserCache.Del(accessToken)
}
}
return response.Success()
}
admin_user_controller.go
api路由层
package controller
import (
"admin/handler"
"admin/storage"
"core/redis"
"core/system"
"core/web"
)
type AdminUserController struct {
}
var _ web.IWebController = &AdminUserController{}
func (e *AdminUserController) Router(iWebRoutes web.IWebRoutes) {
db := system.GetXormEngine()
var redis = redis.NewRedisUtil(system.GetRedisClient())
var adminUserHandler = handler.AdminUserHandler{
AdminUserDb: storage.NewAdminUserDb(db),
AdminLoginRecordDb: storage.NewAdminLoginRecordDb(db),
AdminUserCache: storage.NewAdminUserCache(redis),
AccessTokenCache: storage.NewAccessTokenCache(redis),
}
iWebRoutes.GET("/user/get", adminUserHandler.GetAdminUser)
iWebRoutes.GET("/user", adminUserHandler.GetAdminUserList)
iWebRoutes.POST("/user", adminUserHandler.SaveAdminUser)
iWebRoutes.PUT("/user", adminUserHandler.UpdateAdminUser)
iWebRoutes.DELETE("/user/:adminId", adminUserHandler.DelAdminUser)
iWebRoutes.PUT("/user/thaw", adminUserHandler.ThawAdminUser)
}
router.go
gin添加路由
package router
import (
"admin/controller"
"core/web"
)
func init() {
//添加路由
web.AddRouter(func(wrg *web.WebRouterGroup) {
r := wrg.Group("/admin")
{
new(controller.AdminLoginController).Router(r)
new(controller.AdminLogoutController).Router(r)
new(controller.AdminUserController).Router(r)
new(controller.AdminMenuController).Router(r)
new(controller.AdminRoleController).Router(r)
new(controller.AdminRoleMenuController).Router(r)
new(controller.AdminUserRoleController).Router(r)
}
})
}
用户登录权限校验
func AdminSecurityHandler(security *config.Security, db *xorm.Engine, redis *redis.RedisUtil) gin.HandlerFunc {
return func(c *gin.Context) {
if !security.Encrypt {
c.Next()
return
}
requestUri := c.Request.RequestURI
excludes := security.Excludes
for _, v := range excludes {
if requestUri == v {
c.Next()
return
}
}
accessToken := c.Request.Header.Get(constant.AUTHORIZATION)
if len(accessToken) == 0 {
c.JSON(http.StatusUnauthorized, response.ReturnError(http.StatusUnauthorized, constant.UNAUTHORIZED))
c.Abort()
return
}
adminUser, err := storage.NewAdminUserCache(redis).Get(accessToken)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
}
if adminUser == nil {
c.JSON(http.StatusUnauthorized, response.ReturnError(http.StatusUnauthorized, constant.UNAUTHORIZED))
c.Abort()
return
}
headers := request.GetHeaders(c)
formParams, err := request.GetPostFormParams(c)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
}
queryParams := request.GetQueryParams(c)
bodyParams, err := request.GetBodyParams(c)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
}
storage.NewAdminLogDb(db).AddLog(adminUser.AdminId, c.Request.RequestURI, headers, queryParams, formParams, bodyParams)
c.Next()
}
}
添加到gin的拦截函数中
func init() {
//添加路由
web.AddRouter(func(wrg *web.WebRouterGroup) {
db := system.GetXormEngine()
redis := redis.NewRedisUtil(system.GetRedisClient())
wrg.Use(AdminSecurityHandler(config.SystemConfig.Security, db, redis))
var success = func(wc *web.WebContext) interface{} {
return response.Success()
}
wrg.Group("/").GET("/", success).HEAD("/", success)
r := wrg.Group("/admin")
{
new(controller.AdminLoginController).Router(r)
new(controller.AdminLogoutController).Router(r)
new(controller.AdminUserController).Router(r)
new(controller.AdminMenuController).Router(r)
new(controller.AdminRoleController).Router(r)
new(controller.AdminRoleMenuController).Router(r)
new(controller.AdminUserRoleController).Router(r)
}
})
}
以上是用户管理CRUD功能
前端页面
<template>
<section class="content">
<div class="box box-danger">
<div class="box-body">
<!-- Content Header (Page header) -->
<section class="content-header">
<h1 id="itemTitle"></h1>
</section>
<!-- .content -->
<section class="content">
<div id="table"></div>
<div id="toolbar">
<button id="addBtn" class="btn btn-default">
<i class="glyphicon glyphicon-plus"></i>增加
</button>
</div>
</section><!-- .content -->
</div>
</div>
</section>
<!-- .content -->
<div class="modal fade" id="userModal" tabindex="-1" role="dialog" aria-labelledby="modalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<h4 class="modal-title" id="modalLabel">用户信息</h4>
</div>
<div class="modal-body">
<form class="form-horizontal">
<input type="hidden" name="adminId" v-model="adminUser.adminId" />
<div class="form-group">
<label for="name" class="col-sm-3 control-label">
<span style="color: red; ">*</span>姓名:
</label>
<div class="col-sm-8">
<input type="text" class="form-control validate[required]" name="name" id="name"
v-model="adminUser.name" />
</div>
</div>
<div class="form-group">
<label for="username" class="col-sm-3 control-label">
<span style="color: red; ">*</span>账号:
</label>
<div class="col-sm-8">
<input type="text" class="form-control validate[required]" name="username" id="username"
v-model="adminUser.username" />
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">头像</label>
<div class="col-sm-8">
<input type="hidden" class="form-control" name="avatar" v-model="adminUser.avatar" />
<img :src="imgFormatter(adminUser.avatar)" style="width:120px;"/>
<input type="file" class="form-control" id="file" name="file">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label"></label>
<div class="col-sm-8">
<button class="btn btn-success col-sm-4" type="button" id="uploadBtn">上传</button>
<span class="btn col-sm-4" id="uploadInfo" style="color:red;"></span>
</div>
</div>
<div class="form-group">
<label for="email" class="col-sm-3 control-label">邮箱:</label>
<div class="col-sm-8">
<input type="text" class="form-control" name="email" id="email" v-model="adminUser.email" />
</div>
</div>
<div class="form-group">
<label for="phone" class="col-sm-3 control-label">
<span style="color: red; ">*</span>手机号码:
</label>
<div class="col-sm-8">
<input type="text" class="form-control validate[required]" name="phone" id="phone"
v-model="adminUser.phone">
</div>
</div>
<div class="form-group" id="passwordDiv">
<label for="password" class="col-sm-3 control-label">
<span style="color: red; ">*</span>密码:
</label>
<div class="col-sm-8">
<input type="text" class="form-control validate[required]" name="password" id="password"
v-model="adminUser.password">
</div>
</div>
<div class="form-group">
<label for="status" class="col-sm-3 control-label">状态:</label>
<div class="col-sm-8">
<select name="status" id="status" data-btn-class="btn-warning">
<option value="1">有效</option>
<option value="0">无效</option>
</select>
</div>
</div>
<div class="form-group">
<label for="introduction" class="col-sm-3 control-label">描述:</label>
<div class="col-sm-8">
<textarea class="form-control" name="introduction" id="introduction"
v-model="adminUser.introduction">
</textarea>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" id="save">保存</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="roleModal" tabindex="-1" role="dialog" aria-labelledby="roleModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<h4 class="modal-title" id="roleModalLabel">授权角色</h4>
</div>
<div class="modal-body">
<form class="form-horizontal">
<input type="hidden" name="adminId" v-model="adminUser.adminId" />
<div class="form-group">
<label for="adminUserName" class="col-sm-3 control-label">姓名:</label>
<div class="col-sm-8">
<input type="text" class="form-control" name="adminUserName" id="adminUserName"
disabled="disabled" v-model="adminUser.name" />
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">角色选项</label>
<div class="col-sm-8" id="roles">
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" id="roleSave">保存</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="roleMenuModal" tabindex="-1" role="dialog" aria-labelledby="roleMenuModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content" style="width: 700px;">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<h4 class="modal-title" id="roleMenuModalLabel">授权菜单</h4>
</div>
<div class="modal-body">
<form class="form-horizontal">
<input type="hidden" name="roleIds" id="roleIds" />
<div class="form-group">
<label for="roleNames" class="col-sm-3 control-label">名称:</label>
<div class="col-sm-8">
<input type="text" class="form-control" name="roleNames" disabled="disabled"
id="roleNames" />
</div>
</div>
<div id="menuTree"></div>
</form>
</div>
</div>
</div>
</div>
</template>
<script>
import TableHelper from '@/utils/bootstrap-table-helper';
import ComboboxHelper from '@/utils/bootstrap-combobox-helper';
import { getAdminUserList, saveAdminUser, updateAdminUser, delAdminUser, thawAdminUser } from '@/api/system/user';
import { getAdminUserRole, saveAdminUserRole } from '@/api/system/userRole';
import { getAdminRoleMenu } from '@/api/system/roleMenu';
import { uploadFile } from '@/api/common/upload';
import { alertMsg, getFileUrl } from '@/utils/system-helper';
import avatar from '@/assets/images/man.jpg';
import global from '@/constants/global';
export default {
name: 'AdminUserView',
data() {
return {
columns: [
{ field: 'adminId', title: '用户ID', align: 'center', width: '5%' },
{ field: 'name', title: '姓名', align: 'center', width: '5%' },
{ field: 'username', title: '账号', align: 'center', width: '5%' },
{
field: 'avatar',
title: '头像',
align: 'center',
width: '5%',
formatter: function (val, row, index) {
let value = (val ? getFileUrl(val) : avatar);
return '<img src="' + value + '" width="70" height="60"/>';
}
},
{
field: 'status',
title: '状态',
align: 'center',
width: '5%',
formatter: function (val, row, index) {
return (val ? '有效' : '无效');
}
},
{ field: 'email', title: '邮箱', align: 'center', width: '5%' },
{ field: 'phone', title: '手机号码', align: 'center', width: '5%' },
{
field: 'createdAt',
title: '创建时间',
align: 'center',
width: '5%',
formatter: function (val, row, index) {
let value = '';
if (val) {
value = new Date(val).format("yyyy-MM-dd hh:mm:ss");
}
return value;
}
},
{
field: 'adminId',
title: '操作',
align: 'center',
width: '10%',
formatter: function (val, row, index) {
let value = '<button class="btn btn-sm btn-success opt-edit" data-id="' + val + '" data-index="' + index + '"><i class="glyphicon glyphicon-edit"></i>编辑</button>';
value += '<button class="btn btn-sm btn-warning opt-role" data-id="' + val + '" data-index="' + index + '"><i class="glyphicon glyphicon-user"></i>授权角色</button>';
value += '<button class="btn btn-sm btn-info opt-auth" data-id="' + val + '" data-index="' + index + '"><i class="glyphicon glyphicon-user"></i>查看授权</button>';
value += '<button class="btn btn-sm btn-primary opt-thaw" data-id="' + val + '" data-index="' + index + '"><i class="glyphicon glyphicon-remove"></i>解冻</button>';
value += '<button class="btn btn-sm btn-danger opt-del" data-id="' + val + '" data-index="' + index + '"><i class="glyphicon glyphicon-remove"></i>删除</button>';
return value;
}
}
],
adminUsers: [],
adminUser: {
adminId: 0,
name: '',
username: '',
password: '',
phone: '',
avatar: '',
email: '',
introduction: '',
status: 1,
},
};
},
created() {
this.getUser();
this.init();
},
methods: {
imgFormatter(path) {
return getFileUrl(path);
},
init() {
this.$nextTick(function () {
let $this = this;
$this.initTable();
$('#addBtn').click(function () {
$this.add();
});
$('#uploadBtn').click(function () {
uploadFile({
id: 'file',
bucketName: global.BUCKET.AVATAR
}).then(res => {
$this.adminUser.avatar = res.data.path;
});
});
$('#table').on('click', '.opt-edit', function () {
$this.edit(this);
});
$('#table').on('click', '.opt-role', function () {
$this.getUserRole(this);
});
$('#table').on('click', '.opt-auth', function () {
$this.authRoleMenu(this);
});
$('#table').on('click', '.opt-thaw', function () {
$this.thawUser(this);
});
$('#table').on('click', '.opt-del', function () {
$this.del(this);
});
$('#save').click(function () {
$this.save();
});
$('#roleSave').click(function () {
$this.saveUserRole();
});
});
},
initTable() {
let $this = this;
TableHelper.destroy('#table');
$('#table').bootstrapTable({
columns: $this.columns,
cache: false,
striped: true,
showRefresh: true,
search: true,
pageSize: 10,
pagination: true,
pageList: [1, 10, 20, 30, 50],
sidePagination: "client",
queryParamsType: "undefined",
toolbar: '#toolbar',
});
},
getUser() {
getAdminUserList().then(res => {
this.adminUsers = res.data;
TableHelper.load('#table', this.adminUsers);
});
},
add() {
this.adminUser.adminId = 0;
this.adminUser.name = '';
this.adminUser.username = '';
this.adminUser.password = '';
this.adminUser.phone = '';
this.adminUser.avatar = '';
this.adminUser.email = '';
this.adminUser.introduction = '';
this.adminUser.status = 1;
this.show();
},
edit(obj) {
const index = $(obj).data('index');
const record = TableHelper.getData('#table')[index];
this.adminUser.adminId = record.adminId;
this.adminUser.name = record.name;
this.adminUser.username = record.username;
this.adminUser.password = '';
this.adminUser.phone = record.phone;
this.adminUser.avatar = record.avatar;
this.adminUser.email = record.email;
this.adminUser.introduction = record.introduction;
this.adminUser.status = record.status;
this.show();
},
show() {
let uploadInfo = '未上传文件';
if (this.adminUser.avatar) {
uploadInfo = '已上传';
}
$('#uploadInfo').html(uploadInfo);
ComboboxHelper.build('#status', this.adminUser.status);
$('#userModal').modal('show');
},
save() {
let data = this.adminUser;
if (data.password) {
data.password = $.md5(data.password);
}
data.status = ComboboxHelper.getSelected('#status');
(data.adminId != 0 ? updateAdminUser(data) : saveAdminUser(data)).then(res => {
console.log(res);
this.getUser();
$('#userModal').modal('hide');
});
},
del(obj) {
const index = $(obj).data('index');
const record = TableHelper.getData('#table')[index];
const adminId = record.adminId;
if (!confirm('是否确定要删除?')) {
return;
}
delAdminUser(adminId).then(res => {
console.log(res);
this.getUser();
});
},
thawUser(obj) {
const index = $(obj).data('index');
const record = TableHelper.getData('#table')[index];
const adminId = record.adminId;
thawAdminUser(adminId).then(res => {
alertMsg('解冻成功');
this.getUser();
});
},
getUserRole(obj) {
const index = $(obj).data('index');
const record = TableHelper.getData('#table')[index];
this.adminUser.adminId = record.adminId;
this.adminUser.name = record.name;
const adminId = record.adminId;
getAdminUserRole(adminId).then(res => {
let adminRoles = res.data;
let html = '';
for (let i = 0, len = adminRoles.length; i < len; i++) {
let adminRole = adminRoles[i];
html += '<input type="checkbox" name="roles" value="' + adminRole.roleId + '" ' + (adminRole.checked ? 'checked' : '') + '/>' + adminRole.roleName + ' ';
}
$('#roles').html(html);
$("#roleModal").modal('show');
});
},
saveUserRole() {
const adminId = this.adminUser.adminId;
const roleArray = $("input:checkbox[name='roles']:checked").serializeArray();
let roleIds = [];
for (let i = 0, len = roleArray.length; i < len; i++) {
console.log(roleArray[i]);
roleIds[i] = roleArray[i].value;
}
saveAdminUserRole({
adminId: adminId,
roleIds: roleIds.join(',')
});
$("#roleModal").modal('hide');
},
async authRoleMenu(obj) {
const index = $(obj).data('index');
const record = TableHelper.getData('#table')[index];
const adminId = record.adminId;
getAdminUserRole(adminId).then(res => {
let adminRoles = res.data;
let promiseArray = [];
for (let i = 0, len = adminRoles.length; i < len; i++) {
let adminRole = adminRoles[i];
promiseArray[i] = new Promise((resolve, reject) => {
getAdminRoleMenu(adminRole.roleId).then(res => {
resolve(res.data);
}).catch(() => {
reject();
});
});
}
Promise.all(promiseArray).then(item => {
let menus = [];
for (let i = 0; i < item.length; i++) {
let adminMenus = item[i];
if (menus.length == 0) {
for (let j = 0; j < adminMenus.length; j++) {
menus[j] = adminMenus[j];
}
} else {
for (let j = 0; j < menus.length; j++) {
let parentMenu = menus[j];
let adminMenu = adminMenus.filter(v => v.menuId == parentMenu.menuId)[0];
if (adminMenu.checked) {
parentMenu.checked = true;
}
let childs = parentMenu.childs;
for (let k = 0; k < childs.length; k++) {
let childMenu = childs[k];
adminMenu = adminMenus.filter(v => v.menuId == parentMenu.menuId)[0];
if (adminMenu.checked) {
childMenu.checked = true;
}
}
}
}
}
this.menuTree(menus);
$('#roleIds').val(adminRoles.map(v => v.roleId).join(','));
$('#roleNames').val(adminRoles.map(v => v.roleName).join(','));
$('#roleMenuModal').modal('show');
});
});
},
menuTree(menus) {
let $this = this;
let data = this.getMenuData(menus);
console.log(data);
$("#menuTree").treeview({
data: data,// 数据源
levels: 1, //设置继承树默认展开的级别
showTags: true, //是否在每个节点右边显示tags标签。tag值必须在每个列表树的data结构中给出
showCheckbox: true,
onNodeChecked: function (event, node) { //选中节点
var selectNodes = $this.getChildNodeIdArr(node); //获取所有子节点
if (selectNodes) { //子节点不为空,则选中所有子节点
$('#menuTree').treeview('checkNode', [selectNodes, { silent: true }]);
}
var parentNode = $("#menuTree").treeview("getNode", node.parentId);
if (parentNode) {
$('#menuTree').treeview('checkNode', [parentNode, { silent: true }]);
}
},
onNodeUnchecked: function (event, node) { //取消选中节点
var selectNodes = $this.getChildNodeIdArr(node); //获取所有子节点
if (selectNodes) { //子节点不为空,则取消选中所有子节点
$('#menuTree').treeview('uncheckNode', [selectNodes, { silent: true }]);
}
}
});
},
getMenuData(menus) {
let parentMenus = [];
for (let i = 0; i < menus.length; i++) {
let parentMenu = menus[i];
let childs = parentMenu.childs;
let childMenus = [];
if (childs && childs.length > 0) {
for (let j = 0; j < childs.length; j++) {
let childMenu = childs[j];
childMenus[j] = {
menuId: childMenu.menuId,
text: childMenu.menuName,
color: '#2f2424;display:inline-table;',
state: {
checked: childMenu.checked
}
};
}
}
parentMenus[i] = {
menuId: parentMenu.menuId,
text: parentMenu.menuName,
state: {
checked: parentMenu.checked,
expanded: parentMenu.checked,
},
nodes: childMenus
};
}
return parentMenus;
},
getChildNodeIdArr(node) {
var ts = [];
if (node.nodes) {
let x, j;
for (x in node.nodes) {
ts.push(node.nodes[x].nodeId);
if (node.nodes[x].nodes) {
var getNodeDieDai = this.getChildNodeIdArr(node.nodes[x]);
for (j in getNodeDieDai) {
ts.push(getNodeDieDai[j]);
}
}
}
} else {
ts.push(node.nodeId);
}
return ts;
},
},
}
</script>
<style scoped></style>
总结
剩下的功能全都放入git仓库中,怎么说的,本篇很水的!!!CRUD真不知道要讲啥好,大伙都会的活,只能当作代码分享吧。
照旧附上app的gitee地址(漫画/轻小说app):
https://gitee.com/liaz-app/liaz-android/releases/download/1.0.0/app-arm64-v8a-release.apk