update roles
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoleManageStore } from '@/stores/role-manage'
|
||||
import type { RoleRecord } from '@/stores/role-manage'
|
||||
import RolePermissionModal from '@/components/RolePermissionModal.vue'
|
||||
|
||||
const roleStore = useRoleManageStore()
|
||||
|
||||
const columns = [
|
||||
{ title: '角色名称', dataIndex: 'description', key: 'description' },
|
||||
{ title: '标识', dataIndex: 'name', key: 'name' },
|
||||
{ title: '用户数', dataIndex: 'user_count', key: 'user_count', width: 100 },
|
||||
{ title: '路由组数', key: 'route_group_count', width: 120 },
|
||||
{ title: '操作', key: 'action', width: 120 },
|
||||
]
|
||||
|
||||
const permModalOpen = ref(false)
|
||||
const currentRole = ref<RoleRecord | null>(null)
|
||||
|
||||
function handleConfigPermission(record: RoleRecord) {
|
||||
currentRole.value = record
|
||||
permModalOpen.value = true
|
||||
}
|
||||
|
||||
function handlePermSaved() {
|
||||
roleStore.fetchRoles()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
roleStore.fetchRoles()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-card title="角色管理">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="roleStore.roles"
|
||||
:loading="roleStore.loading"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
size="middle"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'route_group_count'">
|
||||
<a-tag v-if="record.name === 'administrator'" color="blue">全部</a-tag>
|
||||
<span v-else>{{ record.route_group_count }}</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-button
|
||||
v-if="record.name !== 'administrator'"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleConfigPermission(record as RoleRecord)"
|
||||
>
|
||||
配置权限
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<RolePermissionModal
|
||||
v-model:open="permModalOpen"
|
||||
:role="currentRole"
|
||||
@saved="handlePermSaved"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,235 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouteGroupStore } from '@/stores/route-group'
|
||||
import type { RouteGroupRecord } from '@/stores/route-group'
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const groupStore = useRouteGroupStore()
|
||||
|
||||
// 路由组表单
|
||||
const modalOpen = ref(false)
|
||||
const modalTitle = ref('新建路由组')
|
||||
const editingGroupId = ref<number | null>(null)
|
||||
const formState = reactive({
|
||||
name: '',
|
||||
label: '',
|
||||
description: '',
|
||||
sort_order: 0,
|
||||
})
|
||||
const saving = ref(false)
|
||||
|
||||
// method 颜色映射
|
||||
const methodColorMap: Record<string, string> = {
|
||||
GET: 'green',
|
||||
POST: 'blue',
|
||||
PUT: 'orange',
|
||||
DELETE: 'red',
|
||||
PATCH: 'cyan',
|
||||
}
|
||||
|
||||
const routeColumns = [
|
||||
{ title: '方法', key: 'method', width: 100 },
|
||||
{ title: '路径', dataIndex: 'path', key: 'path' },
|
||||
{ title: '描述', dataIndex: 'description', key: 'description', width: 200 },
|
||||
{ title: '所属组', key: 'group', width: 180 },
|
||||
]
|
||||
|
||||
// 组选项(含"无"选项)
|
||||
const groupOptions = computed(() => [
|
||||
{ value: null as number | null, label: '无(未分组)' },
|
||||
...groupStore.groups.map((g) => ({ value: g.id, label: g.label || g.name })),
|
||||
])
|
||||
|
||||
// 筛选路由
|
||||
const filteredRoutes = computed(() => {
|
||||
if (groupStore.selectedGroupId === null) return groupStore.routes
|
||||
if (groupStore.selectedGroupId === 'ungrouped') {
|
||||
return groupStore.routes.filter((r) => r.group_id === null)
|
||||
}
|
||||
return groupStore.routes.filter((r) => r.group_id === groupStore.selectedGroupId)
|
||||
})
|
||||
|
||||
function selectGroup(id: number | null | 'ungrouped') {
|
||||
groupStore.selectedGroupId = id
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
editingGroupId.value = null
|
||||
modalTitle.value = '新建路由组'
|
||||
formState.name = ''
|
||||
formState.label = ''
|
||||
formState.description = ''
|
||||
formState.sort_order = 0
|
||||
modalOpen.value = true
|
||||
}
|
||||
|
||||
function openEditModal(group: RouteGroupRecord) {
|
||||
editingGroupId.value = group.id
|
||||
modalTitle.value = '编辑路由组'
|
||||
formState.name = group.name
|
||||
formState.label = group.label
|
||||
formState.description = group.description
|
||||
formState.sort_order = group.sort_order
|
||||
modalOpen.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
saving.value = true
|
||||
try {
|
||||
if (editingGroupId.value) {
|
||||
await groupStore.updateGroup(editingGroupId.value, { ...formState })
|
||||
message.success('路由组更新成功')
|
||||
} else {
|
||||
await groupStore.createGroup({ ...formState })
|
||||
message.success('路由组创建成功')
|
||||
}
|
||||
modalOpen.value = false
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : '操作失败'
|
||||
message.error(msg)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(group: RouteGroupRecord) {
|
||||
try {
|
||||
await groupStore.deleteGroup(group.id)
|
||||
if (groupStore.selectedGroupId === group.id) {
|
||||
groupStore.selectedGroupId = null
|
||||
}
|
||||
message.success('路由组删除成功')
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : '删除失败'
|
||||
message.error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGroupChange(routeId: number, groupId: number | null) {
|
||||
try {
|
||||
await groupStore.assignRouteToGroup(routeId, groupId)
|
||||
await groupStore.fetchRoutes()
|
||||
await groupStore.fetchGroups()
|
||||
message.success('路由分配成功')
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : '分配失败'
|
||||
message.error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([groupStore.fetchGroups(), groupStore.fetchRoutes()])
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-row :gutter="16">
|
||||
<!-- 左侧:路由组列表 -->
|
||||
<a-col :span="8">
|
||||
<a-card title="路由组" :loading="groupStore.loading">
|
||||
<template #extra>
|
||||
<a-button type="primary" size="small" @click="openCreateModal">
|
||||
<PlusOutlined /> 新建
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 未分组入口 -->
|
||||
<div
|
||||
class="cursor-pointer px-3 py-2 rounded mb-1"
|
||||
:class="groupStore.selectedGroupId === 'ungrouped' ? 'bg-blue-50' : 'hover:bg-gray-50'"
|
||||
@click="selectGroup('ungrouped')"
|
||||
>
|
||||
<div class="font-medium">未分组</div>
|
||||
<div class="text-xs text-gray-400">未归入任何路由组的路由</div>
|
||||
</div>
|
||||
|
||||
<a-divider class="my-2" />
|
||||
|
||||
<!-- 路由组列表 -->
|
||||
<div
|
||||
v-for="group in groupStore.groups"
|
||||
:key="group.id"
|
||||
class="cursor-pointer px-3 py-2 rounded mb-1 flex items-center justify-between"
|
||||
:class="groupStore.selectedGroupId === group.id ? 'bg-blue-50' : 'hover:bg-gray-50'"
|
||||
@click="selectGroup(group.id)"
|
||||
>
|
||||
<div>
|
||||
<div class="font-medium">{{ group.label || group.name }}</div>
|
||||
<div class="text-xs text-gray-400">{{ group.route_count ?? 0 }} 条路由</div>
|
||||
</div>
|
||||
<div class="flex gap-1" @click.stop>
|
||||
<a-button type="text" size="small" @click="openEditModal(group)">
|
||||
<EditOutlined />
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="删除后,关联的角色授权将受影响,确认删除?"
|
||||
@confirm="handleDelete(group)"
|
||||
>
|
||||
<a-button type="text" size="small" danger>
|
||||
<DeleteOutlined />
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<!-- 右侧:路由列表 -->
|
||||
<a-col :span="16">
|
||||
<a-card title="路由列表">
|
||||
<a-table
|
||||
:columns="routeColumns"
|
||||
:data-source="filteredRoutes"
|
||||
:loading="groupStore.routesLoading"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'method'">
|
||||
<a-tag :color="methodColorMap[record.method] || 'default'">
|
||||
{{ record.method }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'group'">
|
||||
<a-select
|
||||
:value="record.group_id"
|
||||
:options="groupOptions"
|
||||
size="small"
|
||||
style="width: 150px"
|
||||
placeholder="选择路由组"
|
||||
@change="(val: unknown) => handleGroupChange(record.id, val as number | null)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 路由组新建/编辑 Modal -->
|
||||
<a-modal
|
||||
v-model:open="modalOpen"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="saving"
|
||||
@ok="handleSubmit"
|
||||
>
|
||||
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }" class="mt-4">
|
||||
<a-form-item label="组标识" required>
|
||||
<a-input v-model:value="formState.name" placeholder="如 user-management" />
|
||||
</a-form-item>
|
||||
<a-form-item label="显示名称" required>
|
||||
<a-input v-model:value="formState.label" placeholder="如 用户管理" />
|
||||
</a-form-item>
|
||||
<a-form-item label="描述">
|
||||
<a-textarea v-model:value="formState.description" :rows="2" />
|
||||
</a-form-item>
|
||||
<a-form-item label="排序">
|
||||
<a-input-number v-model:value="formState.sort_order" :min="0" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
@@ -22,14 +22,21 @@ const modalOpen = ref(false)
|
||||
const modalMode = ref<'create' | 'edit'>('create')
|
||||
const editingUser = ref<UserRecord | null>(null)
|
||||
|
||||
// Role assign modal
|
||||
const roleModalOpen = ref(false)
|
||||
const roleAssigning = ref(false)
|
||||
const assigningUser = ref<UserRecord | null>(null)
|
||||
const selectedRoleId = ref<number | undefined>(undefined)
|
||||
const roleOptions = ref<{ value: number; label: string }[]>([])
|
||||
|
||||
const columns = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 80 },
|
||||
{ title: '用户名', dataIndex: 'username' },
|
||||
{ title: '邮箱', dataIndex: 'email' },
|
||||
{ title: '角色', dataIndex: 'role_name', width: 120 },
|
||||
{ title: '角色', key: 'role', width: 120 },
|
||||
{ title: '状态', dataIndex: 'status', width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'created_at', width: 180 },
|
||||
{ title: '操作', key: 'action', width: 200 },
|
||||
{ title: '操作', key: 'action', width: 280 },
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
@@ -93,6 +100,35 @@ function handleModalSuccess() {
|
||||
store.fetchUsers()
|
||||
}
|
||||
|
||||
async function handleAssignRole(record: UserRecord) {
|
||||
assigningUser.value = record
|
||||
selectedRoleId.value = record.role_id || undefined
|
||||
roleModalOpen.value = true
|
||||
try {
|
||||
const roles = await api.get<{ id: number; name: string; description: string }[]>('/api/v1/roles')
|
||||
roleOptions.value = roles.map((r) => ({ value: r.id, label: r.description || r.name }))
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : '获取角色列表失败'
|
||||
message.error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async function submitRoleAssign() {
|
||||
if (!assigningUser.value || !selectedRoleId.value) return
|
||||
roleAssigning.value = true
|
||||
try {
|
||||
await api.put(`/api/v1/users/${assigningUser.value.id}/role`, { role_id: selectedRoleId.value })
|
||||
message.success('角色分配成功')
|
||||
roleModalOpen.value = false
|
||||
store.fetchUsers()
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : '分配失败'
|
||||
message.error(msg)
|
||||
} finally {
|
||||
roleAssigning.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(time: string) {
|
||||
return time ? time.replace('T', ' ').substring(0, 19) : '-'
|
||||
}
|
||||
@@ -164,8 +200,9 @@ function formatTime(time: string) {
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'role_name'">
|
||||
{{ record.role?.name || '-' }}
|
||||
<template v-if="column.key === 'role'">
|
||||
<a-tag v-if="record.role" color="blue">{{ record.role.name }}</a-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'status'">
|
||||
<a-tag :color="record.status === 1 ? 'green' : 'red'">
|
||||
@@ -183,6 +220,14 @@ function formatTime(time: string) {
|
||||
<a-button v-if="userStore.isAdmin" type="link" size="small" @click="handleEdit(record as UserRecord)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="userStore.isAdmin && record.id !== userStore.user?.id"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleAssignRole(record as UserRecord)"
|
||||
>
|
||||
分配角色
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
v-if="userStore.isAdmin"
|
||||
:title="`确定要${record.status === 1 ? '禁用' : '启用'}该用户吗?`"
|
||||
@@ -243,6 +288,28 @@ function formatTime(time: string) {
|
||||
</a-spin>
|
||||
</a-drawer>
|
||||
|
||||
<!-- Role assign modal -->
|
||||
<a-modal
|
||||
v-model:open="roleModalOpen"
|
||||
title="分配角色"
|
||||
:confirm-loading="roleAssigning"
|
||||
@ok="submitRoleAssign"
|
||||
>
|
||||
<div class="py-2">
|
||||
<p class="mb-3">
|
||||
用户:<strong>{{ assigningUser?.username }}</strong>
|
||||
</p>
|
||||
<a-form-item label="选择角色">
|
||||
<a-select
|
||||
v-model:value="selectedRoleId"
|
||||
:options="roleOptions"
|
||||
placeholder="请选择角色"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- Create/Edit modal -->
|
||||
<UserFormModal
|
||||
v-model:open="modalOpen"
|
||||
|
||||
Reference in New Issue
Block a user