298 lines
9.9 KiB
Vue
298 lines
9.9 KiB
Vue
|
|
<template>
|
||
|
|
<section class="space-y-6">
|
||
|
|
<header>
|
||
|
|
<h1 class="mos-title text-2xl font-semibold">用户权限管理</h1>
|
||
|
|
<p class="mos-muted mt-2 text-sm">管理用户权限、数据范围和角色分配。</p>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
<div class="mos-card p-4">
|
||
|
|
<div class="flex flex-wrap gap-4 mb-4">
|
||
|
|
<input v-model="searchQuery" class="mos-input w-64" placeholder="搜索用户姓名或ID..." />
|
||
|
|
<select v-model="filterRole" class="mos-input w-40">
|
||
|
|
<option value="">全部角色</option>
|
||
|
|
<option v-for="role in roles" :key="role.code" :value="role.code">
|
||
|
|
{{ role.name }}
|
||
|
|
</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="overflow-x-auto">
|
||
|
|
<table class="w-full">
|
||
|
|
<thead>
|
||
|
|
<tr class="border-b border-mosquito-line text-left text-sm text-mosquito-ink/70">
|
||
|
|
<th class="pb-3 font-medium">用户</th>
|
||
|
|
<th class="pb-3 font-medium">角色</th>
|
||
|
|
<th class="pb-3 font-medium">数据范围</th>
|
||
|
|
<th class="pb-3 font-medium">部门</th>
|
||
|
|
<th class="pb-3 font-medium">状态</th>
|
||
|
|
<th class="pb-3 font-medium">操作</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<tr v-for="user in filteredUsers" :key="user.id" class="border-b border-mosquito-line/50">
|
||
|
|
<td class="py-3">
|
||
|
|
<div class="text-sm font-medium text-mosquito-ink">{{ user.realName || user.username }}</div>
|
||
|
|
<div class="text-xs text-mosquito-ink/60">{{ user.username }} (ID: {{ user.id }})</div>
|
||
|
|
</td>
|
||
|
|
<td class="py-3 text-sm text-mosquito-ink">{{ user.roleName }}</td>
|
||
|
|
<td class="py-3 text-sm text-mosquito-ink">
|
||
|
|
<span :class="dataScopeClass(user.dataScope)" class="rounded-full px-2 py-1 text-xs font-semibold">
|
||
|
|
{{ user.dataScopeName }}
|
||
|
|
</span>
|
||
|
|
</td>
|
||
|
|
<td class="py-3 text-sm text-mosquito-ink">{{ user.departmentName || '-' }}</td>
|
||
|
|
<td class="py-3">
|
||
|
|
<span
|
||
|
|
:class="user.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-700'"
|
||
|
|
class="rounded-full px-2 py-1 text-xs font-semibold"
|
||
|
|
>
|
||
|
|
{{ user.status === 'active' ? '正常' : '禁用' }}
|
||
|
|
</span>
|
||
|
|
</td>
|
||
|
|
<td class="py-3">
|
||
|
|
<div class="flex gap-2">
|
||
|
|
<PermissionButton permission="user.index.update.ALL" variant="secondary" @click="editUser(user)">
|
||
|
|
<span class="text-sm text-mosquito-accent hover:underline">编辑</span>
|
||
|
|
</PermissionButton>
|
||
|
|
<PermissionButton permission="role.index.manage.ALL" variant="secondary" @click="assignRole(user)">
|
||
|
|
<span class="text-sm text-mosquito-accent hover:underline">分配角色</span>
|
||
|
|
</PermissionButton>
|
||
|
|
<PermissionButton permission="user.index.delete.ALL" variant="danger" @click="removeUser(user)">
|
||
|
|
<span class="text-sm text-rose-600 hover:underline">移除</span>
|
||
|
|
</PermissionButton>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div v-if="!filteredUsers.length" class="py-8 text-center text-mosquito-ink/60">
|
||
|
|
暂无用户数据
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 分配角色弹窗 -->
|
||
|
|
<div v-if="showRoleModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||
|
|
<div class="mos-card p-6 w-full max-w-md">
|
||
|
|
<h3 class="text-lg font-semibold mb-4">分配角色 - {{ selectedUser?.username }}</h3>
|
||
|
|
<div class="space-y-3">
|
||
|
|
<label v-for="role in roles" :key="role.code" class="flex items-center gap-3 p-3 rounded-lg border border-mosquito-line cursor-pointer hover:bg-mosquito-bg/50">
|
||
|
|
<input type="radio" v-model="selectedRoleId" :value="role.id" class="h-4 w-4" />
|
||
|
|
<div>
|
||
|
|
<div class="text-sm font-medium text-mosquito-ink">{{ role.name }}</div>
|
||
|
|
<div class="text-xs text-mosquito-ink/60">{{ role.description }}</div>
|
||
|
|
</div>
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
<div class="flex gap-3 mt-6">
|
||
|
|
<button class="mos-btn mos-btn-primary flex-1" @click="confirmAssignRole">确认</button>
|
||
|
|
<button class="mos-btn mos-btn-secondary flex-1" @click="showRoleModal = false">取消</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
import { ref, computed, onMounted } from 'vue'
|
||
|
|
import { userService } from '@/services/userManage'
|
||
|
|
import { permissionService } from '@/services/permission'
|
||
|
|
import PermissionButton from '../components/PermissionButton.vue'
|
||
|
|
|
||
|
|
const showMessage = (msg: string, type: 'success' | 'error' = 'success') => {
|
||
|
|
if (type === 'error') {
|
||
|
|
alert(msg)
|
||
|
|
} else {
|
||
|
|
console.log(msg)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
interface User {
|
||
|
|
id: number
|
||
|
|
username: string
|
||
|
|
realName?: string
|
||
|
|
roleCode: string
|
||
|
|
roleName: string
|
||
|
|
dataScope: string
|
||
|
|
dataScopeName: string
|
||
|
|
departmentId?: number
|
||
|
|
departmentName?: string
|
||
|
|
status: string
|
||
|
|
}
|
||
|
|
|
||
|
|
interface Role {
|
||
|
|
id: number
|
||
|
|
code: string
|
||
|
|
name: string
|
||
|
|
description?: string
|
||
|
|
}
|
||
|
|
|
||
|
|
const searchQuery = ref('')
|
||
|
|
const filterRole = ref('')
|
||
|
|
const showRoleModal = ref(false)
|
||
|
|
const selectedUser = ref<User | null>(null)
|
||
|
|
const selectedRoleId = ref<number | null>(null)
|
||
|
|
const loading = ref(false)
|
||
|
|
|
||
|
|
const roles = ref<Role[]>([])
|
||
|
|
const users = ref<User[]>([])
|
||
|
|
const totalUsers = ref(0)
|
||
|
|
const currentPage = ref(1)
|
||
|
|
const pageSize = ref(10)
|
||
|
|
|
||
|
|
// 角色code到ID的映射
|
||
|
|
const roleCodeToId = ref<Record<string, number>>({})
|
||
|
|
|
||
|
|
// 加载角色列表
|
||
|
|
const loadRoles = async () => {
|
||
|
|
try {
|
||
|
|
const roleList = await permissionService.getRoles()
|
||
|
|
roles.value = roleList.map((r: any) => ({
|
||
|
|
id: r.id,
|
||
|
|
code: r.roleCode,
|
||
|
|
name: r.roleName,
|
||
|
|
description: r.description
|
||
|
|
}))
|
||
|
|
// 建立映射
|
||
|
|
roles.value.forEach(role => {
|
||
|
|
roleCodeToId.value[role.code] = role.id
|
||
|
|
})
|
||
|
|
} catch (error: any) {
|
||
|
|
console.error('加载角色列表失败:', error)
|
||
|
|
showMessage(error.message || '加载角色列表失败', 'error')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 加载用户列表
|
||
|
|
const loadUsers = async () => {
|
||
|
|
try {
|
||
|
|
loading.value = true
|
||
|
|
const result = await userService.getUsers({
|
||
|
|
page: currentPage.value,
|
||
|
|
size: pageSize.value,
|
||
|
|
keyword: searchQuery.value || undefined
|
||
|
|
})
|
||
|
|
// 获取每个用户的角色
|
||
|
|
const userList: User[] = []
|
||
|
|
for (const user of result.items) {
|
||
|
|
try {
|
||
|
|
const roleCodes = await userService.getUserRoles(user.id)
|
||
|
|
const roleCode = roleCodes[0] || ''
|
||
|
|
const roleInfo = roles.value.find(r => r.code === roleCode)
|
||
|
|
userList.push({
|
||
|
|
id: user.id,
|
||
|
|
username: user.username,
|
||
|
|
realName: user.realName,
|
||
|
|
roleCode: roleCode,
|
||
|
|
roleName: roleInfo?.name || roleCode || '未分配',
|
||
|
|
dataScope: 'ALL', // 默认值
|
||
|
|
dataScopeName: '全部数据',
|
||
|
|
departmentId: user.departmentId,
|
||
|
|
departmentName: user.departmentName,
|
||
|
|
status: user.status?.toLowerCase() || 'active'
|
||
|
|
})
|
||
|
|
} catch (e) {
|
||
|
|
// 如果获取用户角色失败,使用默认信息
|
||
|
|
userList.push({
|
||
|
|
id: user.id,
|
||
|
|
username: user.username,
|
||
|
|
realName: user.realName,
|
||
|
|
roleCode: '',
|
||
|
|
roleName: '未分配',
|
||
|
|
dataScope: 'ALL',
|
||
|
|
dataScopeName: '全部数据',
|
||
|
|
departmentId: user.departmentId,
|
||
|
|
departmentName: user.departmentName,
|
||
|
|
status: user.status?.toLowerCase() || 'active'
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
users.value = userList
|
||
|
|
totalUsers.value = result.total
|
||
|
|
} catch (error: any) {
|
||
|
|
console.error('加载用户列表失败:', error)
|
||
|
|
showMessage(error.message || '加载用户列表失败', 'error')
|
||
|
|
} finally {
|
||
|
|
loading.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const filteredUsers = computed(() => {
|
||
|
|
return users.value.filter((user) => {
|
||
|
|
const matchesSearch = !searchQuery.value ||
|
||
|
|
user.username.includes(searchQuery.value) ||
|
||
|
|
user.realName?.includes(searchQuery.value) ||
|
||
|
|
String(user.id).includes(searchQuery.value)
|
||
|
|
const matchesRole = !filterRole.value || user.roleCode === filterRole.value
|
||
|
|
return matchesSearch && matchesRole
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
const dataScopeClass = (scope: string) => {
|
||
|
|
switch (scope) {
|
||
|
|
case 'ALL':
|
||
|
|
return 'bg-purple-100 text-purple-700'
|
||
|
|
case 'DEPARTMENT':
|
||
|
|
return 'bg-blue-100 text-blue-700'
|
||
|
|
case 'OWN':
|
||
|
|
return 'bg-gray-100 text-gray-700'
|
||
|
|
default:
|
||
|
|
return 'bg-gray-100 text-gray-700'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const editUser = (user: User) => {
|
||
|
|
alert(`编辑用户: ${user.username}`)
|
||
|
|
}
|
||
|
|
|
||
|
|
const assignRole = (user: User) => {
|
||
|
|
selectedUser.value = user
|
||
|
|
selectedRoleId.value = roleCodeToId.value[user.roleCode] || null
|
||
|
|
showRoleModal.value = true
|
||
|
|
}
|
||
|
|
|
||
|
|
const confirmAssignRole = async () => {
|
||
|
|
if (selectedUser.value && selectedRoleId.value) {
|
||
|
|
try {
|
||
|
|
loading.value = true
|
||
|
|
await userService.assignRoles(selectedUser.value.id, [selectedRoleId.value], '管理员分配角色')
|
||
|
|
showMessage('角色分配成功,请等待审批')
|
||
|
|
await loadUsers()
|
||
|
|
} catch (error: any) {
|
||
|
|
showMessage(error.message || '分配角色失败', 'error')
|
||
|
|
} finally {
|
||
|
|
loading.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
showRoleModal.value = false
|
||
|
|
}
|
||
|
|
|
||
|
|
const removeUser = async (user: User) => {
|
||
|
|
if (confirm(`确定移除用户"${user.username}"吗?`)) {
|
||
|
|
try {
|
||
|
|
loading.value = true
|
||
|
|
// 使用紧急模式删除
|
||
|
|
await userService.deleteUser(user.id)
|
||
|
|
showMessage('用户删除成功')
|
||
|
|
await loadUsers()
|
||
|
|
} catch (error: any) {
|
||
|
|
showMessage(error.message || '删除用户失败', 'error')
|
||
|
|
} finally {
|
||
|
|
loading.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 搜索用户
|
||
|
|
const handleSearch = () => {
|
||
|
|
currentPage.value = 1
|
||
|
|
loadUsers()
|
||
|
|
}
|
||
|
|
|
||
|
|
onMounted(async () => {
|
||
|
|
await loadRoles()
|
||
|
|
await loadUsers()
|
||
|
|
})
|
||
|
|
</script>
|