更新用户权限模块功能

This commit is contained in:
2026-02-18 16:17:11 +08:00
parent 790b3140a7
commit 5450777bd7
7 changed files with 404 additions and 96 deletions

View File

@@ -324,4 +324,40 @@ class DepartmentService
}
return $this->isDescendant($id, $child->parent_id);
}
/**
* 获取部门及所有子部门的ID列表
*
* @param int $departmentId 部门ID
* @param array $departments 所有部门数据
* @return array
*/
public function getDepartmentAndChildrenIds(int $departmentId, array $departments = null): array
{
if ($departments === null) {
$departments = Department::where('status', 1)->get()->keyBy('id')->toArray();
}
$ids = [$departmentId];
$this->collectChildrenIds($departmentId, $departments, $ids);
return $ids;
}
/**
* 递归收集子部门ID
*
* @param int $parentId 父部门ID
* @param array $departments 所有部门数据
* @param array &$ids ID收集数组
*/
private function collectChildrenIds(int $parentId, array $departments, array &$ids): void
{
foreach ($departments as $department) {
if ($department['parent_id'] == $parentId) {
$ids[] = $department['id'];
$this->collectChildrenIds($department['id'], $departments, $ids);
}
}
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Services\Auth;
use App\Models\Auth\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
@@ -14,6 +15,20 @@ use App\Jobs\Auth\UserExportJob;
class UserService
{
protected $departmentService;
public function __construct(DepartmentService $departmentService)
{
$this->departmentService = $departmentService;
}
/**
* 获取当前登录用户ID
*/
protected function getCurrentUserId(): int
{
return Auth::guard('admin')->id();
}
/**
* 获取用户列表
*/
@@ -32,7 +47,9 @@ class UserService
}
if (!empty($params['department_id'])) {
$query->where('department_id', $params['department_id']);
// 获取部门及所有子部门的ID
$departmentIds = $this->departmentService->getDepartmentAndChildrenIds($params['department_id']);
$query->whereIn('department_id', $departmentIds);
}
if (isset($params['status']) && $params['status'] !== '') {
@@ -226,6 +243,15 @@ class UserService
*/
public function batchDelete(array $ids): int
{
$currentUserId = $this->getCurrentUserId();
// 检查是否包含当前用户
if (in_array($currentUserId, $ids)) {
throw ValidationException::withMessages([
'ids' => ['不能删除当前登录用户'],
]);
}
return User::whereIn('id', $ids)->delete();
}
@@ -234,6 +260,15 @@ class UserService
*/
public function batchUpdateStatus(array $ids, int $status): int
{
$currentUserId = $this->getCurrentUserId();
// 如果是禁用操作,检查是否包含当前用户
if ($status === 0 && in_array($currentUserId, $ids)) {
throw ValidationException::withMessages([
'ids' => ['不能禁用当前登录用户'],
]);
}
return User::whereIn('id', $ids)->update(['status' => $status]);
}

View File

@@ -5,8 +5,8 @@
<a-tree ref="menuTreeRef" v-model:checkedKeys="checkedPermissionIds" :tree-data="permissionTree"
:field-names="fieldNames" :checkable="true" :default-expand-all="true"
:check-strictly="false" :selectable="false">
<template #title="{ name }">
{{ name }}
<template #title="{ title }">
{{ title }}
</template>
</a-tree>
</div>
@@ -37,7 +37,7 @@ const checkedPermissionIds = ref([])
// 树字段映射
const fieldNames = {
title: 'name',
title: 'title',
key: 'id',
children: 'children'
}

View File

@@ -0,0 +1,104 @@
<template>
<a-modal v-model:open="visible" title="批量分配角色" :confirm-loading="loading" @ok="handleOk" @cancel="handleCancel">
<a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<a-form-item label="角色">
<a-select
v-model:value="selectedRoleIds"
mode="multiple"
placeholder="请选择角色"
allow-clear
:options="roleOptions"
:field-names="{ label: 'name', value: 'id' }"
:loading="roleLoading"
show-search
:filter-option="filterOption"
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { ref } from 'vue'
import { message } from 'ant-design-vue'
import authApi from '@/api/auth'
const visible = ref(false)
const loading = ref(false)
const roleLoading = ref(false)
const roleOptions = ref([])
const userIds = ref([])
const selectedRoleIds = ref([])
// 打开弹窗
const open = async (ids) => {
visible.value = true
userIds.value = ids
selectedRoleIds.value = []
await loadRoles()
}
// 加载角色列表
const loadRoles = async () => {
try {
roleLoading.value = true
const res = await authApi.roles.list.get({ page_size: 1000 })
if (res.code === 200) {
roleOptions.value = (res.data.list || []).map(role => ({
id: role.id,
name: role.name,
code: role.code
}))
}
} catch (error) {
console.error('加载角色列表失败:', error)
} finally {
roleLoading.value = false
}
}
// 角色过滤
const filterOption = (input, option) => {
const name = option?.name?.toLowerCase() || ''
const code = option?.code?.toLowerCase() || ''
const keyword = input?.toLowerCase() || ''
return name.includes(keyword) || code.includes(keyword)
}
// 确认
const handleOk = async () => {
try {
loading.value = true
const res = await authApi.users.batchRoles.post({
ids: userIds.value,
role_ids: selectedRoleIds.value
})
if (res.code === 200) {
message.success('分配成功')
emit('success')
handleCancel()
} else {
message.error(res.message || '分配失败')
}
} catch (error) {
console.error('批量分配角色失败:', error)
message.error(error.message || '分配失败')
} finally {
loading.value = false
}
}
// 取消
const handleCancel = () => {
visible.value = false
loading.value = false
selectedRoleIds.value = []
}
const emit = defineEmits(['success'])
defineExpose({
open
})
</script>

View File

@@ -0,0 +1,101 @@
<template>
<a-modal v-model:open="visible" title="批量分配部门" :confirm-loading="loading" @ok="handleOk" @cancel="handleCancel">
<a-form :model="formState" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<a-form-item label="部门">
<a-tree-select
v-model:value="formState.department_id"
:tree-data="departmentTree"
placeholder="请选择部门"
allow-clear
:field-names="{ label: 'name', value: 'id', children: 'children' }"
tree-default-expand-all
show-search
:filter-tree-node="filterTreeNode"
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { message } from 'ant-design-vue'
import authApi from '@/api/auth'
const visible = ref(false)
const loading = ref(false)
const departmentTree = ref([])
const userIds = ref([])
const formState = reactive({
department_id: undefined
})
// 打开弹窗
const open = (ids) => {
visible.value = true
userIds.value = ids
formState.department_id = undefined
loadDepartmentTree()
}
// 加载部门树
const loadDepartmentTree = async () => {
try {
const res = await authApi.departments.tree.get()
if (res.code === 200) {
departmentTree.value = res.data || []
}
} catch (error) {
console.error('加载部门树失败:', error)
}
}
// 树节点过滤
const filterTreeNode = (inputValue, treeNode) => {
const name = treeNode.dataRef.name
return name ? name.toLowerCase().includes(inputValue.toLowerCase()) : false
}
// 确认
const handleOk = async () => {
if (!formState.department_id) {
message.warning('请选择部门')
return
}
try {
loading.value = true
const res = await authApi.users.batchDepartment.post({
ids: userIds.value,
department_id: formState.department_id
})
if (res.code === 200) {
message.success('分配成功')
emit('success')
handleCancel()
} else {
message.error(res.message || '分配失败')
}
} catch (error) {
console.error('批量分配部门失败:', error)
message.error(error.message || '分配失败')
} finally {
loading.value = false
}
}
// 取消
const handleCancel = () => {
visible.value = false
loading.value = false
formState.department_id = undefined
}
const emit = defineEmits(['success'])
defineExpose({
open
})
</script>

View File

@@ -1,18 +1,23 @@
<template>
<a-modal title="角色设置" :open="visible" :width="500" :destroy-on-close="true" :footer="null" @cancel="handleCancel">
<div class="role-content">
<a-checkbox-group v-model:value="checkedRoles" style="width: 100%">
<a-space direction="vertical" style="width: 100%">
<a-checkbox v-for="role in roleList" :key="role.id" :value="role.id" style="width: 100%">
{{ role.name }}
</a-checkbox>
</a-space>
</a-checkbox-group>
</div>
<template #footer>
<a-button @click="handleCancel"> </a-button>
<a-button type="primary" :loading="isSaveing" @click="submit"> </a-button>
</template>
<a-modal v-model:open="visible" title="设置角色" :confirm-loading="loading" @ok="handleOk" @cancel="handleCancel">
<a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<a-form-item label="用户">
<a-input :value="userForm.username" disabled />
</a-form-item>
<a-form-item label="角色">
<a-select
v-model:value="selectedRoleIds"
mode="multiple"
placeholder="请选择角色"
allow-clear
:options="roleOptions"
:field-names="{ label: 'name', value: 'id' }"
:loading="roleLoading"
show-search
:filter-option="filterOption"
/>
</a-form-item>
</a-form>
</a-modal>
</template>
@@ -21,104 +26,100 @@ import { ref, reactive } from 'vue'
import { message } from 'ant-design-vue'
import authApi from '@/api/auth'
const emit = defineEmits(['success', 'closed'])
const visible = ref(false)
const isSaveing = ref(false)
const loading = ref(false)
const roleLoading = ref(false)
const roleOptions = ref([])
const selectedRoleIds = ref([])
const userId = ref(null)
// 表单数据
const form = reactive({
id: '',
role_ids: []
const userForm = reactive({
username: ''
})
// 选中的角色
const checkedRoles = ref([])
// 角色列表
const roleList = ref([])
// 打开对话框
// 打开弹窗
const open = () => {
visible.value = true
return {
open,
setData,
close
}
loadRoles()
}
// 关闭对话框
const close = () => {
visible.value = false
// 设置用户数据
const setData = (user) => {
userId.value = user.id
userForm.username = user.username
// 设置已选择的角色
selectedRoleIds.value = (user.roles || []).map(role => role.id)
}
// 处理取消
const handleCancel = () => {
emit('closed')
visible.value = false
}
// 提交保存
const submit = async () => {
// 加载角色列表
const loadRoles = async () => {
try {
isSaveing.value = true
// 获取选中的角色 ID
form.role_ids = checkedRoles.value || []
const res = await authApi.users.batchRoles.post({
ids: [form.id],
role_ids: form.role_ids
})
isSaveing.value = false
roleLoading.value = true
const res = await authApi.roles.list.get({ page_size: 1000 })
if (res.code === 200) {
emit('success', form)
visible.value = false
message.success('操作成功')
} else {
message.error(res.message || '操作失败')
roleOptions.value = (res.data.list || []).map(role => ({
id: role.id,
name: role.name,
code: role.code
}))
}
} catch (error) {
console.error('保存角色失败:', error)
isSaveing.value = false
message.error('操作失败')
console.error('加载角色列表失败:', error)
} finally {
roleLoading.value = false
}
}
// 获取角色列表
const getRoles = async () => {
// 角色过滤
const filterOption = (input, option) => {
const name = option?.name?.toLowerCase() || ''
const code = option?.code?.toLowerCase() || ''
const keyword = input?.toLowerCase() || ''
return name.includes(keyword) || code.includes(keyword)
}
// 确认
const handleOk = async () => {
if (!userId.value) {
message.warning('用户ID不能为空')
return
}
try {
const res = await authApi.roles.all.get()
roleList.value = res.data || []
loading.value = true
const res = await authApi.users.batchRoles.post({
ids: [userId.value],
role_ids: selectedRoleIds.value
})
if (res.code === 200) {
message.success('设置成功')
emit('success')
handleCancel()
} else {
message.error(res.message || '设置失败')
}
} catch (error) {
console.error('获取角色列表失败:', error)
message.error('获取角色列表失败')
console.error('设置角色失败:', error)
message.error(error.message || '设置失败')
} finally {
loading.value = false
}
}
// 设置数据
const setData = (data) => {
form.id = data.id
checkedRoles.value = data.roles ? data.roles.map(item => item.id) : []
// 取消
const handleCancel = () => {
visible.value = false
loading.value = false
userId.value = null
userForm.username = ''
selectedRoleIds.value = []
}
// 组件挂载时加载数据
getRoles()
const emit = defineEmits(['success'])
// 暴露方法给父组件
defineExpose({
open,
setData,
close
setData
})
</script>
<style scoped>
.role-content {
padding: 20px 0;
max-height: 400px;
overflow-y: auto;
}
</style>

View File

@@ -10,7 +10,7 @@
</div>
<div class="body">
<a-tree v-model:selectedKeys="selectedDeptKeys" :tree-data="filteredDepartmentTree"
:field-names="{ title: 'name', key: 'id', children: 'children' }" show-line @select="onDeptSelect">
:field-names="{ title: 'name', key: 'id', children: 'children' }" show-line default-expand-all @select="onDeptSelect">
<template #icon="{ dataRef }">
<ApartmentOutlined v-if="dataRef.children && dataRef.children.length > 0" />
<UserOutlined v-else />
@@ -127,6 +127,12 @@
<!-- 角色设置弹窗 -->
<role-dialog v-if="dialog.role" ref="roleDialogRef" @success="handleRoleSuccess" @closed="dialog.role = false" />
<!-- 批量分配部门弹窗 -->
<department-dialog v-if="dialog.department" ref="departmentDialogRef" @success="handleDepartmentSuccess" @closed="dialog.department = false" />
<!-- 批量分配角色弹窗 -->
<batch-role-dialog v-if="dialog.batchRole" ref="batchRoleDialogRef" @success="handleBatchRoleSuccess" @closed="dialog.batchRole = false" />
<!-- 导入用户弹窗 -->
<sc-import v-model:open="dialog.import" title="导入用户" :api="authApi.users.import.post"
:template-api="authApi.users.downloadTemplate.get" filename="用户" @success="handleImportSuccess" />
@@ -159,6 +165,8 @@ import scImport from '@/components/scImport/index.vue'
import scExport from '@/components/scExport/index.vue'
import saveDialog from './components/SaveDialog.vue'
import roleDialog from './components/RoleDialog.vue'
import departmentDialog from './components/DepartmentDialog.vue'
import batchRoleDialog from './components/BatchRoleDialog.vue'
import authApi from '@/api/auth'
import { useTable } from '@/hooks/useTable'
@@ -200,6 +208,8 @@ const {
const dialog = reactive({
save: false,
role: false,
department: false,
batchRole: false,
import: false,
export: false
})
@@ -207,6 +217,8 @@ const dialog = reactive({
// 弹窗引用
const saveDialogRef = ref(null)
const roleDialogRef = ref(null)
const departmentDialogRef = ref(null)
const batchRoleDialogRef = ref(null)
// 部门树数据
const departmentTree = ref([])
@@ -366,8 +378,10 @@ const handleBatchDepartment = () => {
message.warning('请选择要分配部门的用户')
return
}
// TODO: 实现批量分配部门弹窗
message.info('批量分配部门功能开发中...')
dialog.department = true
setTimeout(() => {
departmentDialogRef.value?.open(selectedRows.value.map(item => item.id))
}, 0)
}
// 批量分配角色
@@ -376,8 +390,10 @@ const handleBatchRoles = () => {
message.warning('请选择要分配角色的用户')
return
}
// TODO: 实现批量分配角色弹窗
message.info('批量分配角色功能开发中...')
dialog.batchRole = true
setTimeout(() => {
batchRoleDialogRef.value?.open(selectedRows.value.map(item => item.id))
}, 0)
}
// 导出数据
@@ -472,7 +488,10 @@ const handleEdit = (record) => {
const handleRole = (record) => {
dialog.role = true
setTimeout(() => {
roleDialogRef.value?.open().setData(record)
if (roleDialogRef.value) {
roleDialogRef.value.open()
roleDialogRef.value.setData(record)
}
}, 0)
}
@@ -502,6 +521,18 @@ const handleRoleSuccess = () => {
refreshTable()
}
// 批量分配部门成功回调
const handleDepartmentSuccess = () => {
selectedRows.value = []
refreshTable()
}
// 批量分配角色成功回调
const handleBatchRoleSuccess = () => {
selectedRows.value = []
refreshTable()
}
// 初始化
onMounted(() => {
loadDepartmentTree()