一.注意事项
- 1.用户组的远程下拉,是通过在后台添加一个按钮菜单来实现权限
- 2.添加和编辑分开了页面
二.步骤
- 1.先使用快速CURD生成基础代码, 使用系统用户表
-
- 3.修改控制器代码
app\admin\controller\Member.php
<?php
namespace app\admin\controller;
use app\admin\library\Auth;
use app\admin\model\Admin;
use ba\Random;
use Throwable;
use app\common\controller\Backend;
use think\facade\Db;
/**
* 用户管理管理
*/
class Member extends Backend
{
/**
* Member模型对象
* @var object
* @phpstan-var \app\admin\model\Member
*/
protected object $model;
protected array|string $preExcludeFields = ['id', 'email', 'mobile', 'login_failure', 'last_login_time', 'last_login_ip', 'salt', 'motto', 'update_time', 'create_time', 'user_path'];
protected array $withJoinTable = ['pidTable'];
protected string|array $quickSearchField = ['username', 'nickname', 'id'];
protected $uid = null;
public function initialize(): void
{
parent::initialize();
$this->model = new \app\admin\model\Member;
$this->uid = $this->auth->getAdmin()->id;
}
/**
* 查看
* @throws Throwable
*/
public function index(): void
{
// 如果是 select 则转发到 select 方法,若未重写该方法,其实还是继续执行 index
if ($this->request->param('select')) {
$this->select();
}
/**
* 1. withJoin 不可使用 alias 方法设置表别名,别名将自动使用关联模型名称(小写下划线命名规则)
* 2. 以下的别名设置了主表别名,同时便于拼接查询参数等
* 3. paginate 数据集可使用链式操作 each(function($item, $key) {}) 遍历处理
*/
list($where, $alias, $limit, $order) = $this->queryBuilder();
$res = $this->model
->withJoin($this->withJoinTable, $this->withJoinType)
->alias($alias)
->where($where)
->order($order)
->paginate($limit);
$res->visible(['pidTable' => ['username']]);
$this->success('', [
'list' => $res->items(),
'total' => $res->total(),
'remark' => get_route_remark(),
]);
}
/**
* 若需重写查看、编辑、删除等方法,请复制 @see \app\admin\library\traits\Backend 中对应的方法至此进行重写
*/
/**
* 重写添加和编辑
*/
public function add(): void
{
if ($this->request->isPost()) {
$data = $this->request->post();
if (!$data) {
$this->error(__('Parameter %s can not be empty', ['']));
}
$data = $this->excludeFields($data);
if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
$data[$this->dataLimitField] = $this->auth->id;
}
$result = false;
$this->model->startTrans();
try {
// 模型验证
if ($this->modelValidate) {
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
if (class_exists($validate)) {
$validate = new $validate;
if ($this->modelSceneValidate) $validate->scene('add');
$validate->check($data);
}
}
$salt = Random::build('alnum', 16);
$passwd = encrypt_password($data['password'], $salt);
$insert = [];
$insert['salt'] = $salt;
$insert['password'] = $passwd;
$insert['nickname'] = trim($data['nickname']);
$insert['username'] = trim($data['username']);
$insert['createtime'] = time();
if ($this->auth->isSuperAdmin()) {
$insert['pid'] = 0;
} else {
$insert['pid'] = $this->uid;
}
$result = $this->model->save($insert); // 1.添加用户
$group_arr = [$data['group_id']]; // 格式化成权限需要的模式
if ($group_arr) {
$groupAccess = [];
foreach ($group_arr as $data) {
$groupAccess[] = [
'uid' => $this->model->id, // 新建用户ID
'group_id' => $data,
];
}
// [0 => ["uid" => 6, "group_id" => "3"] ]
Db::name('admin_group_access')->insertAll($groupAccess); // 2.添加用户权限关联
}
$this->model->commit();
} catch (Throwable $e) {
$this->model->rollback();
$this->error($e->getMessage());
}
if ($result !== false) {
$this->success(__('Added successfully'));
} else {
$this->error(__('No rows were added'));
}
}
$this->error(__('Parameter error'));
}
/**
* 编辑
* @throws Throwable
*/
public function edit($id = null): void
{
$row = $this->model->find($id);
if (!$row) {
$this->error(__('Record not found'));
}
$dataLimitAdminIds = $this->getDataLimitAdminIds();
if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) {
$this->error(__('You have no permission'));
}
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('edit')->check($data);
} catch (Throwable $e) {
$this->error($e->getMessage());
}
}
if ($this->auth->id == $data['id'] && $data['status'] == '0') {
$this->error(__('Please use another administrator account to disable the current account!'));
}
// 更新密码
if (isset($data['password']) && $data['password']) {
$adminModel = new Admin();
$res = $adminModel->resetPassword($data['id'], $data['password']);
if (! $res) {
$this->error("更新密码失败");
}
}
$data = $this->excludeFields($data);
$result = false;
$this->model->startTrans();
try {
$update = [];
$update['nickname'] = $data['nickname'];
$update['avatar'] = $data['avatar'];
$result = $row->save($update);
$this->model->commit();
} catch (Throwable $e) {
$this->model->rollback();
$this->error($e->getMessage());
}
if ($result !== false) {
$this->success(__('Update successful'));
} else {
$this->error(__('No rows updated'));
}
}
unset($row['salt'], $row['login_failure']);
$row['password'] = '';
$this->success('', [
'row' => $row
]);
}
/**
* 获取用户可选择下级用户的权限组
*/
public function groupSelect()
{
$groupInfo = $this->auth->getGroups();
$data = [];
if (count($groupInfo) === 1) {
if (isset($groupInfo[0]['group_id'])) {
$groupId = $groupInfo[0]['group_id'];
$data = config('group.select.'.$groupId);
}
}
$this->success("", [ 'options' => $data]);
}
/**
* 获取用户可见用户列表 (这个不用在添加用户中, 添加用户的逻辑是当前用户id就是pid)
*/
public function memberSelect()
{
}
}
- 2.模型
<?php
namespace app\admin\model;
use think\Model;
/**
* Member
*/
class Member extends Model
{
// 表名
protected $name = 'admin';
// 自动写入时间戳字段
protected $autoWriteTimestamp = true;
public function pidTable(): \think\model\relation\BelongsTo
{
return $this->belongsTo(\app\admin\model\Admin::class, 'pid', 'id');
}
}
- 3.验证器
<?php
namespace app\admin\validate;
use think\Validate;
class Member 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 = [
'username.require' => '用户名不能为空',
'username.regex' => '用户名必须以字母开头,只能包含字母、数字和下划线,长度在3-16之间',
'username.unique' => '用户名已存在',
'nickname.require' => '昵称不能为空',
'password.require' => '密码不能为空',
'password.regex' => '密码必须在6-32位之间,且不能包含特殊字符',
'group_id.require' => '角色ID不能为空',
];
/**
* 字段描述
*/
protected $field = [
'username' => '用户名',
'nickname' => '昵称',
'password' => '密码',
'group_id' => '角色ID',
];
/**
* 验证场景
*/
protected $scene = [
'add' => ['username', 'nickname', 'password', 'group_id'],
'edit' => ['nickname'],
];
public function __construct()
{
parent::__construct();
}
}
4.用户组配置 config\group.php
<?php
return [
'name' => [
1 => '超级管理组',
2 => '一级管理员',
3 => '二级管理员',
4 => '三级管理员',
],
'list' => [
['id'=>2, 'title'=>'一级管理员'],
['id'=>3, 'title'=>'二级管理员'],
['id'=>4, 'title'=>'三级管理员'],
],
'select' => [
// 1.超管可添加
1 => [
['id'=>2, 'title'=>'一级管理员'],
['id'=>3, 'title'=>'二级管理员'],
['id'=>4, 'title'=>'三级管理员'],
],
// 2.一级管理员可添加
2 => [
['id'=>3, 'title'=>'二级管理员'],
['id'=>4, 'title'=>'三级管理员'],
],
// 3.二级管理员可添加
3 => [
['id'=>3, 'title'=>'二级管理员'],
['id'=>4, 'title'=>'三级管理员'],
],
4 => [],
],
];
前端代码
1.列表页面
<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', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('member.quick Search Fields') })"
></TableHeader>
<!-- 表格 -->
<!-- 表格列有多种自定义渲染方式,比如自定义组件、具名插槽等,参见文档 -->
<!-- 要使用 el-table 组件原有的属性,直接加在 Table 标签上即可 -->
<Table ref="tableRef"></Table>
<!-- 表单 -->
<PopupForm />
<EditForm />
</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 PopupForm from './popupForm.vue'
import EditForm from './editForm.vue'
import Table from '/@/components/table/index.vue'
import TableHeader from '/@/components/table/header/index.vue'
defineOptions({
name: 'member',
})
const { t } = useI18n()
const tableRef = ref()
const optButtons: OptButton[] = defaultOptButtons(['edit'])
/**
* baTable 内包含了表格的所有数据且数据具备响应性,然后通过 provide 注入给了后代组件
*/
const baTable = new baTableClass(
new baTableApi('/admin/Member/'),
{
pk: 'id',
column: [
{ type: 'selection', align: 'center', operator: false },
// { label: t('member.id'), prop: 'id', align: 'center', width: 70, operator: false, sortable: 'custom' },
{ label: t('member.username'), prop: 'username', align: 'center', operatorPlaceholder: t('Fuzzy query'), operator: 'LIKE', sortable: false },
{ label: t('member.nickname'), prop: 'nickname', align: 'center', operatorPlaceholder: t('Fuzzy query'), operator: 'LIKE', sortable: false },
{ label: t('member.avatar'), prop: 'avatar', align: 'center', render: 'image', operator: false },
{ label: t('member.status'), prop: 'status', align: 'center', render: 'tag', operator: false, sortable: false, replaceValue: { '0': t('member.status 0'), '1': t('member.status 1') } },
{ label: "上级用户", prop: 'pidTable.username', align: 'center', operatorPlaceholder: t('Fuzzy query'), render: 'tags', operator: 'LIKE' },
{ label: t('member.pid'), prop: 'pid', align: 'center', operator: 'eq', show:false },
{ label: t('member.create_time'), prop: 'create_time', align: 'center', render: 'datetime', operator: false, sortable: 'custom', width: 160, timeFormat: 'yyyy-mm-dd hh:MM:ss' },
{ label: t('Operate'), align: 'center', width: 100, render: 'buttons', buttons: optButtons, operator: false },
],
dblClickNotEditColumn: ['all'],
},
{
defaultItems: { status: '1' },
}
)
provide('baTable', baTable)
onMounted(() => {
baTable.table.ref = tableRef.value
baTable.mount()
baTable.getIndex()?.then(() => {
baTable.initSort()
baTable.dragSort()
})
})
</script>
<style scoped lang="scss"></style>
- 2.添加页面(这里将add和edit拆分, 并且重写了后台的控制器方法)
<template>
<!-- 对话框表单 -->
<!-- 建议使用 Prettier 格式化代码 -->
<!-- el-form 内可以混用 el-form-item、FormItem、ba-input 等输入组件 -->
<el-dialog
class="ba-operate-dialog"
:close-on-click-modal="false"
:model-value="['Add'].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('member.username')" type="string" v-model="baTable.form.items!.username" prop="username" placeholder="由英文和大小写字母组成, 最少6位" />
<FormItem :label="t('member.nickname')" type="string" v-model="baTable.form.items!.nickname" prop="nickname" placeholder="可输入中文和英文,数字" />
<FormItem :label="t('member.avatar')" type="image" v-model="baTable.form.items!.avatar" prop="avatar" />
<FormItem :label="t('member.password')" type="string" v-model="baTable.form.items!.password" prop="password" :placeholder="t('Please input field', { field: t('member.password') })" />
<FormItem :label="t('member.status')" type="radio" v-model="baTable.form.items!.status" prop="status" :data="{ content: { '0': t('member.status 0'), '1': t('member.status 1') } }" :placeholder="t('Please select field', { field: t('member.status') })" />
<FormItem label="角色"
type="remoteSelect"
v-model="baTable.form.items!.group_id"
prop="group_id"
:input-attr="{ pk: 'id', field: 'title', 'remote-url': groupSelect }"
/>
</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 { FormInstance, FormItemRule } from 'element-plus'
import { buildValidatorData } from '/@/utils/validate'
import roleConfig from '/@/config/system/roleConfig'
import { useAdminInfo } from '/@/stores/adminInfo'
import { groupSelect } from '/@/api/controllerUrls'
const formRef = ref<FormInstance>()
const baTable = inject('baTable') as baTableClass
const { t } = useI18n()
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
last_login_time: [buildValidatorData({ name: 'number', title: t('member.last_login_time') })],
update_time: [buildValidatorData({ name: 'date', title: t('member.update_time') })],
create_time: [buildValidatorData({ name: 'date', title: t('member.create_time') })],
// pid: [buildValidatorData({ name: 'number', title: t('member.pid') })],
})
</script>
<style scoped lang="scss"></style>
- 3.编辑页面
<template>
<!-- 对话框表单 -->
<!-- 建议使用 Prettier 格式化代码 -->
<!-- el-form 内可以混用 el-form-item、FormItem、ba-input 等输入组件 -->
<el-dialog
class="ba-operate-dialog"
:close-on-click-modal="false"
:model-value="['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('member.nickname')" type="string" v-model="baTable.form.items!.nickname" prop="nickname" placeholder="可输入中文和英文,数字" />
<FormItem :label="t('member.avatar')" type="image" v-model="baTable.form.items!.avatar" prop="avatar" />
<FormItem :label="t('member.password')" type="string" v-model="baTable.form.items!.password" prop="password" :placeholder="t('Please input field', { field: t('member.password') })" />
<FormItem :label="t('member.status')" type="radio" v-model="baTable.form.items!.status" prop="status" :data="{ content: { '0': t('member.status 0'), '1': t('member.status 1') } }" :placeholder="t('Please select field', { field: t('member.status') })" />
</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 { FormInstance, FormItemRule } from 'element-plus'
import { buildValidatorData } from '/@/utils/validate'
import roleConfig from '/@/config/system/roleConfig'
import { useAdminInfo } from '/@/stores/adminInfo'
import { groupSelect } from '/@/api/controllerUrls'
const formRef = ref<FormInstance>()
const baTable = inject('baTable') as baTableClass
const { t } = useI18n()
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
last_login_time: [buildValidatorData({ name: 'number', title: t('member.last_login_time') })],
update_time: [buildValidatorData({ name: 'date', title: t('member.update_time') })],
create_time: [buildValidatorData({ name: 'date', title: t('member.create_time') })],
// pid: [buildValidatorData({ name: 'number', title: t('member.pid') })],
})
</script>
<style scoped lang="scss"></style>
- 4.前端路由组件
web\src\api\controllerUrls.ts
/**
* 通常将表格的控制器URL定义在此处
此文件是 1.0 版本的, 2.0版本不见此文件, 但是文档中还有
export const authMenu = '/admin/auth.menu/'
export const authAdmin = '/admin/auth.admin/'
export const authAdminLog = '/admin/auth.adminLog/'
export const authGroup = '/admin/auth.group/'
export const routineAttachment = '/admin/routine.attachment/'
export const userUser = '/admin/user.user/'
export const userGroup = '/admin/user.group/'
export const userRule = '/admin/user.rule/'
export const userScoreLog = '/admin/user.scoreLog/'
export const userMoneyLog = '/admin/user.moneyLog/'
export const securityDataRecycle = '/admin/security.dataRecycle/'
export const securityDataRecycleLog = '/admin/security.dataRecycleLog/'
export const securitySensitiveData = '/admin/security.sensitiveData/'
export const securitySensitiveDataLog = '/admin/security.sensitiveDataLog/'
*/
import { useAdminInfo } from "../stores/adminInfo"
export const goodsCategory = '/admin/goodsCategory/'
export const userUser = '/admin/user.user/'
// 用户组远程下拉
export const groupSelect = '/admin/member/groupSelect/'