[BD1.0] 自定义上下级用户体系

119 阅读3分钟
一.说明
  • 1.这里主要是将之前改造的用户管理的实现代码贴一下
  • 2.目前这里使用的大量的后端接口来实现下拉列表, 可以考虑优化
二.步骤
  • 1.新建一个一级菜单, 使用生成CURD的方式, 表格就选择用户表ba_admin(也可以添加一个菜单目录, 之前没做, 可以扩展)
  • 2.之前的路由是admin/adminUser, 也就是adminUser
三.后端代码
  • 1.控制器app\admin\controller\AdminUser.php
namespace app\admin\controller;

use app\common\controller\Backend;
use ba\Random;
use think\db\exception\PDOException;
use think\exception\ValidateException;
use Exception;
use think\facade\Config;
use think\facade\Db;
use app\admin\model\AdminUser as AdminUserModel;

class AdminUser extends Backend
{

    protected $model = null;
    protected $preExcludeFields = ['id', 'username', 'avatar', 'createtime', 'status', 'pid'];
    protected $quickSearchField = ['id', 'username', 'nickname'];

    public function initialize()
    {
        parent::initialize();
        $this->model = new \app\admin\model\AdminUser;
    }

    public function index()
    {

        $this->request->filter(['strip_tags', 'trim']);
        if ($this->request->param('select')) {
            $this->select();
        }

        $field = ['id', 'username', 'nickname', 'avatar', 'mobile', 'createtime', 'status', 'pid'];
        list($where, $alias, $limit, $order) = $this->queryBuilder();
        $query = $this->model
            ->field($field)
            ->withJoin($this->withJoinTable, $this->withJoinType)
            ->alias($alias)
            ->where($where);
        if (! $this->auth->isSuperAdmin()) {
            $uidArr = $this->model->getChildArr($this->auth->getAdmin()->id);
            $query->whereIn('id', $uidArr);
        }
        $res = $query->order($order)->paginate($limit);
        $list = $res->items();
        
        foreach ($list as &$user) {
            $user['pid'] = $this->model->getNickname($user['pid']);
        }

        $this->success('', [
            'list'   => $list,
            'total'  => $res->total(),
            'remark' => get_route_remark(),
        ]);
    }


    public function CreateUser()
    {
        if ($this->request->isPost()) {
            $data = $this->request->post();

            if (!$data) {
                $this->error(__('Parameter %s can not be empty', ['']));
            }

            /**
             * 由于有密码字段-对方法进行重写
             * 数据验证
             */
            if ($this->modelValidate) {
                try {
                    $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
                    $validate = new $validate;
                    $validate->scene('add')->check($data);
                } catch (ValidateException $e) {
                    $this->error($e->getMessage());
                }
            }

            $salt   = Random::build('alnum', 16);
            $passwd = encrypt_password($data['password'], $salt);

            // 上级用户
            if ($this->auth->isSuperAdmin()) {
                $pid = trim($data['pid']);
            } else {
                $pid = $this->auth->getAdmin()->id; // 上级用户
            }

            $username = trim($data['username']);
            $data   = $this->excludeFields($data);
            $result = false;

            Db::startTrans();
            try {
                $data['salt']     = $salt;
                $data['password'] = $passwd;
                $data['group_arr'] = [$data['group_id']]; // 格式化成权限需要的模式
                $data['username'] = $username;
                $data['createtime'] = time();
                $data['pid'] = $pid;
                /**
                 * array:7 [
                 *  "status" => "1"
                 *  "username" => "zhangsan"
                 *  "nickname" => "张三"
                 *  "group_arr" => array:1 [
                 *      0 => "3"
                 *   ]
                 *   "mobile" => "13800138000"
                 *   "salt" => "RpUoGcwnvTl68myO"
                 *   "password" => "32488ff98fcdae4d3a125490d1c81146"
                 *  ]
                 */
                $result  = $this->model->save($data); // 1.添加用户
                if ($data['group_arr']) {
                    $groupAccess = [];
                    foreach ($data['group_arr'] as $datum) {
                        $groupAccess[] = [
                            'uid'      => $this->model->id,
                            'group_id' => $datum,
                        ];
                    }
                    /**
                     * array:1 [
                     *    0 => array:2 ["uid" => 6, "group_id" => "3"]
                     *  ]
                     */
                    Db::name('admin_group_access')->insertAll($groupAccess); // 2.添加用户权限关联
                }
                Db::commit();
            } catch (ValidateException|PDOException|Exception $e) {
                Db::rollback();
                $this->error($e->getMessage());
            }
            if ($result !== false) {
                $this->success("添加用户成功");
            } else {
                $this->error("添加失败");
            }

        } else {
            $this->error("ERROR");
        }
    }


    /**
     * 根据登录用户的角色, 展示添加列表时的可选角色
     */
   public function getUserGroup()
   {
        $group_id = $this->auth->getAdminGroupId(); //获取用户组ID
        $group = $this->model->getGroupSelect($group_id);
        return $this->success('', $group);
   }

   /**
    * 获取上级用户列表
    */
   public function getPidUser()
   {
        $isSuper = $this->auth->isSuperAdmin();
        if ($isSuper) {
            $list = $this->model->getPidUser();
        } else {
            $list = [];
        }
        $this->success('', $list);
   }

}
  • 2.模型 app\admin\model\AdminUser.php
<?php

namespace app\admin\model;

use think\Model;
use app\admin\library\Auth;

/**
 * AdminUser
 */
class AdminUser extends Model
{
    // 表名
    protected $name = 'admin';

    // 自动写入时间戳字段
    protected $autoWriteTimestamp = false;

    const GROUP_SUPER = 1;
    const GROUP_LEVEL1 = 2;
    const GROUP_LEVEL2 = 3;
    const GROUP_LEVEL3 = 4;
    public $group = [
        self::GROUP_SUPER => '超级管理员',
        self::GROUP_LEVEL1 => '一级管理员',
        self::GROUP_LEVEL2 => '二级管理员',
        self::GROUP_LEVEL3 => '三级管理员',
    ];
    
    // 超管可选的用户组, 但是如果不是选择的L1, 需要指定上级
    public $group_super = [
        self::GROUP_LEVEL1,
        self::GROUP_LEVEL2,
        self::GROUP_LEVEL3,
    ];

    // 一级可选的用户组
    public $group_level1 = [
        self::GROUP_LEVEL2,
        self::GROUP_LEVEL3,
    ];

    // 二级可选的用户组
    public $group_level2 = [
        self::GROUP_LEVEL2,
        self::GROUP_LEVEL3,
    ];

    /**
     * 获取指定角色可获取角色组
     */
    public function getGroupSelect($group_id)
    {
        switch($group_id) {
            case self::GROUP_SUPER:
                $group = $this->group_super;
                break;
            case self::GROUP_LEVEL1:
                $group = $this->group_level1;
                break;
            case self::GROUP_LEVEL2:
                $group = $this->group_level2;
                break;
            default:
                $group = []; 
                break;
        }

        $result = [];
        foreach ($group as $gid) {
           $result[] = [
                'id'    => $gid,
                'title' => $this->group[$gid]
           ]; 
        }
        return $result;
    }

    
    /**
     * 获取上级用户列表
     */
    public function getPidUser() 
    {
        $auth = new Auth();
        $uidArr = $auth->getGroupAdmins([self::GROUP_LEVEL1, self::GROUP_LEVEL2]);
        
        $list = $this->field(['id','nickname as title'])->whereIn('id', $uidArr)->select()->toArray();
        return $list;
    }


    // 获取用户名
    public function getNickname($uid)
    {
        return $this->where("id", $uid)->value('nickname');
    }

    /**
     * 获取所有下级用户ID
     */
    function getChildArr($id) {
        $ids = [$id];
        $subIds = self::where('pid', $id)->column('id');
        foreach ($subIds as $subId) {
            $ids = array_merge($ids, $this->getChildArr($subId));
        }
        return $ids;
    }

     /********************** 从Admin模型中转移过来的方法 ************************ */

    // 获取树形用户
    public function getUserTree($isSuper, $uid)
    {
        if ($isSuper) {
            $res = $this->field(['id','username label','pid'])->where("pid", 0)->select()->toArray();
        } else {
            $res = $this->field(['id','username label','pid'])->where("id", $uid)->select()->toArray();
        }
        $list = [];
        foreach ($res as $item) {
            $list[] = [
                'id'    => $item['id'],
                'pid'   => $item['pid'],
                'label' => $item['label'],
            ];
        }
        foreach ($list as &$user) {
            $child = $this->getChildren($user['id']);
            $user['children'] = $child;
        }
        return $list;
    }

    // 无限极方法
    public function getChildren($id)
    {
        $children = $this->hidden(['group_arr'])->field(['id','username label','pid'])->where('pid', $id)->select()->toArray();
        $list = [];
        foreach ($children as $item) {
            $list[] = [
                'id'    => $item['id'],
                'pid'   => $item['pid'],
                'label' => $item['label'],
            ];
        }
        foreach ($list as &$child) {
            $child['children'] = $this->getChildren($child['id']);
        }
        return $list;
    }


    // 获取用户列表
    public function getUserList()
    {
        $list = $this->hidden(['group_arr'])->field(['id','nickname title'])->select()->toArray();
        return $list;
    }
}
  • 3.扩展权限类app\admin\library\Auth.php, 增加自定义方法(看看能不能迁移到其他地方,不修改系统源码)
    /**************************** 自定义方法 *******************************/
    /**
     * 获取管理员组ID(角色ID)
     */
    public function getAdminGroupId()
    {
        $groupId = $this->getAdmin()->group_arr;
        if ($groupId && count($groupId) == 1) {
            return $groupId[0];
        } else {
            return 0;
        }
    }

     /**
     * 获取管理员UID
     */
    public function getUid()
    {
        return $this->getAdmin()->id;
    }
  • 4.后端验证器
namespace app\admin\validate;
use think\Validate;

class AdminUser extends Validate
{
    protected $failException = true;

    protected $rule = [
        'username'  => 'require|regex:^[a-zA-Z][a-zA-Z0-9_]{2,15}$|unique:admin',
        'nickname'  => 'require',
        'password'  => 'require|regex:^(?!.*[&<>"\'\n\r]).{6,32}$',
        'group_id' => 'require',
    ];

    /**
     * 验证提示信息
     * @var array
     */
    protected $message = [];

    /**
     * 字段描述
     */
    protected $field = [
    ];

    /**
     * 验证场景
     */
    protected $scene = [
        'add' => ['username', 'nickname', 'password', 'group_id'],
    ];


    public function __construct()
    {
        $this->field   = [
            'username'  => __('Username'),
            'nickname'  => __('Nickname'),
            'password'  => __('Password'),
            'group_id' => __('Group Name Arr'),
        ];
        $this->message = array_merge($this->message, [
            'username.regex' => __('Please input correct username'),
            'password.regex' => __('Please input correct password'),
        ]);
        parent::__construct();
    }

}
四.前端代码
  • 前端主要的代码都在: web\src\views\backend\adminUser

    1. index.vue 列表页面
    2. popupForm.vue 弹窗和编辑页面
    3. addUser.vue 添加用户页面
    4. roleEdit.vue 这个是用户自定义权限的, 暂时忽略
  • 另外还有一些接口文件之类的

  1. web\src\api\backend\adminapi.ts

    1. index.vue
<template>
    <div class="default-main ba-table-box">
        <el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />

        <!-- 表格顶部菜单 -->
        <TableHeader
            :buttons="['refresh', 'comSearch', 'quickSearch', 'columnDisplay']"
            :quick-search-placeholder="t('quick Search Placeholder', { fields: t('adminUser.quick Search Fields') })"
        >
            <el-button type="primal" @click="isOpen = true">添加用户</el-button>
        </TableHeader>

        <!-- 表格 -->
        <!-- 要使用`el-table`组件原有的属性,直接加在Table标签上即可 -->
        <Table ref="tableRef" />

        <addUser/>
        <roleEdit/>
    </div>
</template>

<script setup lang="ts">
import { ref, provide, onMounted } from 'vue'
import baTableClass from '/@/utils/baTable'
import { defaultOptButtons } from '/@/components/table'
import { baTableApi } from '/@/api/common'
import { useI18n } from 'vue-i18n'
import Table from '/@/components/table/index.vue'
import TableHeader from '/@/components/table/header/index.vue'
import addUser from './addUser.vue'
import roleEdit from './roleEdit.vue'

const { t } = useI18n()
const tableRef = ref()
let optButtons = defaultOptButtons([])

// 自定义一个新的按钮
let newButton: OptButton[] = [
    {
        render: 'tipButton',
        text: '权限',
        type: 'primary',
        icon: 'fa fa-search-plus',
        disabledTip: false,
        // 自定义点击事件
        click: (row: TableRow, field: TableColumn) => {
            console.log('禁用'+row.id);
            isOpenRole.value = true
        },
    },
]
// 新按钮合入到默认的按钮数组
optButtons = newButton.concat(optButtons)

const baTable = new baTableClass(
    new baTableApi('/admin/AdminUser/'),
    {
        pk: 'id',
        column: [
            { type: 'selection', align: 'center', operator: false },
            { label: t('adminUser.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
            { label: t('adminUser.username'), prop: 'username', align: 'center', operatorPlaceholder: t('Fuzzy query'), operator: 'LIKE', sortable: false },
            { label: t('adminUser.nickname'), prop: 'nickname', align: 'center', operatorPlaceholder: t('Fuzzy query'), operator: 'LIKE', sortable: false },
            { label: t('adminUser.avatar'), prop: 'avatar', align: 'center', render: 'image', operator: false },
            { label: t('adminUser.createtime'), prop: 'createtime', align: 'center', render: 'datetime', operator: 'RANGE', sortable: 'custom', width: 160, timeFormat: 'yyyy-mm-dd hh:MM:ss' },
            { label: t('adminUser.status'), prop: 'status', align: 'center', render: 'tag', operator: '=', sortable: false, replaceValue: { 1: t('adminUser.status 1'), 0: t('adminUser.status 0') } },
            { label: '上级', prop: 'pid', align: 'center', operator: 'RANGE', sortable: false },
            { 
                label: t('operate'), 
                align: 'center', 
                width: 100, 
                render: 'buttons', 
                buttons: optButtons, 
                operator: false 
            },
        ],
        dblClickNotEditColumn: [undefined],
    },
    {
        defaultItems: {},
    }
)

provide('baTable', baTable)

onMounted(() => {
    baTable.table.ref = tableRef.value
    baTable.mount()
    baTable.getIndex()?.then(() => {
        baTable.initSort()
        baTable.dragSort()
    })
})

// 弹窗开关
const isOpen = ref(false)
provide('isOpen', isOpen)

// 设置权限弹窗开关
const isOpenRole = ref(false)
provide('isOpenRole', isOpenRole)
</script>

<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
    name: 'adminUser',
})
</script>

<style scoped lang="scss"></style>
  • 2.popupForm.vue 弹窗和编辑页面
<template>
    <!-- 对话框表单 -->
    <el-dialog
        class="ba-operate-dialog"
        :close-on-click-modal="false"
        :model-value="['add', 'edit'].includes(baTable.form.operate!)"
        @close="baTable.toggleForm"
        width="50%"
    >
        <template #header>
            <div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
                {{ baTable.form.operate ? t(baTable.form.operate) : '' }}
            </div>
        </template>
        <el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
            <div
                class="ba-operate-form"
                :class="'ba-' + baTable.form.operate + '-form'"
                :style="'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'"
            >
                <el-form
                    v-if="!baTable.form.loading"
                    ref="formRef"
                    @submit.prevent=""
                    @keyup.enter="baTable.onSubmit(formRef)"
                    :model="baTable.form.items"
                    label-position="right"
                    :label-width="baTable.form.labelWidth + 'px'"
                    :rules="rules"
                >
                    <FormItem :label="t('adminUser.nickname')" type="string" v-model="baTable.form.items!.nickname" prop="nickname" :placeholder="t('Please input field', { field: t('adminUser.nickname') })" />
                </el-form>
            </div>
        </el-scrollbar>
        <template #footer>
            <div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
                <el-button @click="baTable.toggleForm('')">{{ t('Cancel') }}</el-button>
                <el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
                    {{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
                </el-button>
            </div>
        </template>
    </el-dialog>
</template>

<script setup lang="ts">
import { reactive, ref, inject } from 'vue'
import { useI18n } from 'vue-i18n'
import type baTableClass from '/@/utils/baTable'
import FormItem from '/@/components/formItem/index.vue'
import type { ElForm, FormItemRule } from 'element-plus'
import { buildValidatorData } from '/@/utils/validate'

const formRef = ref<InstanceType<typeof ElForm>>()
const baTable = inject('baTable') as baTableClass

const { t } = useI18n()

const rules: Partial<Record<string, FormItemRule[]>> = reactive({
    createtime: [buildValidatorData({ name: 'date', title: t('adminUser.createtime') })],
    pid: [buildValidatorData({ name: 'number', title: t('adminUser.pid') })],
})
</script>

<style scoped lang="scss"></style>
  • 3.addUser.vue 添加用户界面
<template>
    <div>
        <el-dialog v-model="isOpen" title="添加用户">
            <el-form :model="form" :rules="rules">
                <el-form-item label="登录名" prop="username">
                    <el-input v-model="form.username" autocomplete="off" placeholder="由英文和数字组成,不可输入中文" />
                </el-form-item>
                <el-form-item label="昵称" prop="nickname">
                    <el-input v-model="form.nickname" autocomplete="off" />
                </el-form-item>
                <el-form-item label="密码"  prop="password">
                    <el-input v-model="form.password"  type="password" autocomplete="off" />
                </el-form-item>
                <el-form-item label="用户组"  prop="group_id">
                    <el-select v-model="form.group_id" placeholder="请选择用户组">
                        <el-option :label="item.title" :value="item.id" :key="item.id" v-for="item in group" />
                    </el-select>
                </el-form-item>
                <el-form-item label="上级用户" prop="pid" v-show="userLiserShow">
                    <el-select v-model="form.pid" placeholder="上级用户">
                        <el-option :label="item.title" :value="item.id" :key="item.id" v-for="item in pidList" />
                    </el-select>
                </el-form-item>
            </el-form>
            <template #footer>
                <span class="dialog-footer">
                    <el-button @click="isOpen = false">取消</el-button>
                    <el-button type="primary" @click="onSubmit"> 确认</el-button>
                </span>
            </template>
        </el-dialog>
    </div>
</template>

<script setup lang='ts'>
    import { useAdminInfo } from '/@/stores/adminInfo'
    import { reactive, ref, watchEffect, inject } from 'vue'
    import { ElMessage, FormRules } from 'element-plus'
    import { CreateUser, getUserGroup, getPidUser } from '/@/api/backend/adminapi'

    // el-upload中的属性data, 用来传值, 这里名称要对应
    const form = reactive({
        username: '',
        nickname:'',
        password:'',
        group_id:'',
        pid:'',
    })

    const isOpen = inject('isOpen')
    // const adminInfo = useAdminInfo()

    // 表单验证
    const rules = reactive<FormRules>({
        username: [
            { required: true, message: '请输入用户名', trigger: 'blur' },
            { min: 4, max: 12, message: '请输入4-12个字符', trigger: 'blur' },
        ],
        nickname: [
            { required: true, message: '请输入昵称', trigger: 'blur' },
            { min: 2, max: 12, message: '请输入2-12个字符', trigger: 'blur' },
        ],
        password: [
            { required: true, message: '请输入密码', trigger: 'blur' },
            { min: 6, max: 12, message: '请输入6-12个字符', trigger: 'blur' },
        ],
        group_id: [
            { required: true, message: '请选择用户组', trigger: 'blur' }
        ]
    })

    // 获取下拉用户列表
    const group = reactive([]);
    getUserGroup().then((res)=> {
       group.push(...res.data)
    })

    // 获取上级用户列表
    const pidList = reactive([]);
    let userLiserShow = ref(false);
    getPidUser().then((res)=> {
       console.log(res);
       pidList.push(...res.data)
       if (res.data.length > 0) {
         userLiserShow.value = true;
       }
    })

    // 表单提交
    const onSubmit = () => {
        console.log('submit!', form)
        CreateUser(form).then((res)=> {
            console.log(res);
            if (res.code === 1) {
                  ElMessage({message: '添加成功',type: 'success', onClose: () => {
                        isOpen.value = false
                  }})
            } else {
                ElMessage({message: '添加失败',type: 'warning'})
            }
        })
    }
</script>
  • 4.roleEdit.vue 角色编辑界面? 忽略

  • adminapi.ts

import { getUrl} from '/@/utils/axios'
import { useAdminInfo } from '/@/stores/adminInfo'
import createAxios from '/@/utils/axios'


// 获取导入的URL, 记得调用时要写()
// 引用: import { getUserTree } from '/@/api/backend/adminapi'
// 使用: getUserTree()

export function getUserTree() {
    let url = getUrl() + '/admin/api/getUserTree' + '?server=1'
    return createAxios({
        url:url,
        method: 'get',
    })
}


// 添加用户
export function CreateUser(data: anyObj) {
    let url = getUrl() + '/admin/AdminUser/CreateUser' + '?server=1'
    return createAxios(
        {
            url: url,
            method: 'post',
            data: data,
        }
    )
}

// 获取可选用户组
export function getUserGroup() {
    let url = getUrl() + '/admin/AdminUser/getUserGroup' + '?server=1'
    return createAxios({
        url:url,
        method: 'get',
    })
}

// 获取用户列表(用于选择上级用户)
export function getPidUser() {
    let url = getUrl() + '/admin/AdminUser/getPidUser' + '?server=1'
    return createAxios({
        url:url,
        method: 'get',
    })
}

// 获取用户列表(用于选择上级用户)
export function download(type: any) {
    const adminInfo = useAdminInfo()
    let url = getUrl() + '/admin/api/download' + '?server=1' + '&type=' + type + '&batoken=' +  adminInfo.getToken()
    return url;
}

// remoteMenuUrl
export function remoteMenuUrl() {
    const adminInfo = useAdminInfo()
    let url = getUrl() + '/admin/api/remoteMenuUrl'
    return url;
}

// 获取远程用户
export function remoteUrlUser() {
    const adminInfo = useAdminInfo()
    let url = getUrl() + '/admin/api/remoteUrlUser'
    return url;
}

// 获取远程URL: 用户级菜单列表
export function remoteUrlUserMenu() {
    const adminInfo = useAdminInfo()
    let url = getUrl() + '/admin/api/remoteUrlUserMenu'
    return url;
}