活动优化
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled

This commit is contained in:
hahwu 2026-04-29 10:38:33 +08:00
parent 3c746fcea9
commit 39195fb2e6
12 changed files with 1231 additions and 6 deletions

View File

@ -0,0 +1,211 @@
import { requestClient } from '#/api/request';
export namespace PermissionApi {
// ─── 用户组 ───────────────────────────────────────────────
export interface UserGroup {
id?: number;
group_code: string;
group_name: string;
status: number;
remark?: string;
createTime?: number;
updateTime?: number;
}
export interface UserGroupListParams {
page?: number;
pageSize?: number;
group_code?: string;
group_name?: string;
}
// ─── 角色(权限组)────────────────────────────────────────
export interface Role {
id?: number;
role_code: string;
role_name: string;
status: number;
is_system?: number;
remark?: string;
createTime?: number;
updateTime?: number;
}
export interface RoleListParams {
page?: number;
pageSize?: number;
role_code?: string;
role_name?: string;
}
// ─── 单点权限 ─────────────────────────────────────────────
export interface Permission {
id?: number;
permission_code: string;
permission_name: string;
permission_group?: string;
api_path?: string;
http_method?: string;
status: number;
remark?: string;
createTime?: number;
updateTime?: number;
}
export interface PermissionListParams {
page?: number;
pageSize?: number;
permission_code?: string;
permission_name?: string;
permission_group?: string;
}
// ─── 关联关系 ─────────────────────────────────────────────
export interface GroupRoleRelItem {
group_id: number;
role_id: number;
}
export interface RolePermissionRelItem {
role_id: number;
permission_id: number;
}
// ─── 用户权限分配 ──────────────────────────────────────────
export interface UserGroupRelItem {
admin_id: number;
group_id: number;
}
export interface UserGroupsAssignParams {
admin_id: number;
group_ids: number[];
}
export interface UserGroupsOfUser {
admin_id: number;
groups: UserGroup[];
}
// 用户直接单点权限admin_user_permission_rel
export interface UserPermissionItem {
permission_id: number;
grant_type: 1 | 2; // 1=允许 2=拒绝
}
export interface UserPermissionsAssignParams {
admin_id: number;
permissions: UserPermissionItem[];
}
// ─── 通用分页响应 ─────────────────────────────────────────
export interface PageResult<T> {
items: T[];
total: number;
}
}
// ═══ 用户组 API ════════════════════════════════════════════
export async function getUserGroupListApi(params: PermissionApi.UserGroupListParams) {
return requestClient.post<PermissionApi.PageResult<PermissionApi.UserGroup>>(
'/admin/usergroup/list',
params,
);
}
export async function addUserGroupApi(params: PermissionApi.UserGroup) {
return requestClient.post('/admin/usergroup/add', params);
}
export async function editUserGroupApi(params: PermissionApi.UserGroup) {
return requestClient.post('/admin/usergroup/edit', params);
}
export async function deleteUserGroupApi(id: number) {
return requestClient.post('/admin/usergroup/delete', { id });
}
// ═══ 角色权限组API ══════════════════════════════════════
export async function getRoleListApi(params: PermissionApi.RoleListParams) {
return requestClient.post<PermissionApi.PageResult<PermissionApi.Role>>(
'/admin/role/list',
params,
);
}
export async function addRoleApi(params: PermissionApi.Role) {
return requestClient.post('/admin/role/add', params);
}
export async function editRoleApi(params: PermissionApi.Role) {
return requestClient.post('/admin/role/edit', params);
}
export async function deleteRoleApi(id: number) {
return requestClient.post('/admin/role/delete', { id });
}
// 用户组绑定角色
export async function setGroupRolesApi(params: { group_id: number; role_ids: number[] }) {
return requestClient.post('/admin/usergroup/role/set', params);
}
export async function getGroupRolesApi(group_id: number) {
return requestClient.post<PermissionApi.Role[]>('/admin/usergroup/role/list', { group_id });
}
// 角色绑定权限
export async function setRolePermissionsApi(params: { role_id: number; permission_ids: number[] }) {
return requestClient.post('/admin/role/permission/set', params);
}
export async function getRolePermissionsApi(role_id: number) {
return requestClient.post<PermissionApi.Permission[]>('/admin/role/permission/list', { role_id });
}
// ═══ 单点权限 API ═══════════════════════════════════════════
export async function getPermissionListApi(params: PermissionApi.PermissionListParams) {
return requestClient.post<PermissionApi.PageResult<PermissionApi.Permission>>(
'/admin/permission/list',
params,
);
}
export async function addPermissionApi(params: PermissionApi.Permission) {
return requestClient.post('/admin/permission/add', params);
}
export async function editPermissionApi(params: PermissionApi.Permission) {
return requestClient.post('/admin/permission/edit', params);
}
export async function deletePermissionApi(id: number) {
return requestClient.post('/admin/permission/delete', { id });
}
// ═══ 用户权限分配 API ═══════════════════════════════════════
export async function getUserGroupsApi(admin_id: number) {
return requestClient.post<PermissionApi.UserGroup[]>('/admin/user/group/list', { admin_id });
}
export async function setUserGroupsApi(params: PermissionApi.UserGroupsAssignParams) {
return requestClient.post('/admin/user/group/set', params);
}
// 用户直接单点权限
export async function getUserPermissionsDirectApi(admin_id: number) {
return requestClient.post<PermissionApi.UserPermissionItem[]>('/admin/user/permission/list', { admin_id });
}
export async function setUserPermissionsDirectApi(params: PermissionApi.UserPermissionsAssignParams) {
return requestClient.post('/admin/user/permission/set', params);
}
// 获取用户通过用户组继承的角色列表
export async function getUserRolesApi(admin_id: number) {
return requestClient.post<PermissionApi.Role[]>('/admin/user/role/list', { admin_id });
}

View File

@ -30,7 +30,14 @@
"title": "Admin", "title": "Admin",
"user": "User", "user": "User",
"setting": "Setting", "setting": "Setting",
"log": "Log" "log": "Log",
"config": "Config",
"permission": {
"userGroup": "User Groups",
"role": "Roles",
"permission": "Permissions",
"userAssign": "User Assignment"
}
}, },
"dashboard": { "dashboard": {
"title": "Dashboard", "title": "Dashboard",

View File

@ -31,7 +31,13 @@
"user": "用户管理", "user": "用户管理",
"setting": "系统设置", "setting": "系统设置",
"log": "操作日志", "log": "操作日志",
"config": "配置管理" "config": "配置管理",
"permission": {
"userGroup": "用户组管理",
"role": "权限组管理",
"permission": "单点权限管理",
"userAssign": "用户权限分配"
}
}, },
"dashboard": { "dashboard": {
"title": "运维管理", "title": "运维管理",

View File

@ -1,4 +1,5 @@
export interface UserInfo { export interface UserInfo {
id?: number;
username: string; username: string;
password?: string; password?: string;
phone: string; phone: string;

View File

@ -48,6 +48,50 @@ const routes: RouteRecordRaw[] = [
title: $t('page.admin.config'), title: $t('page.admin.config'),
}, },
}, },
{
name: 'PermissionUserGroup',
path: '/permission/user-group',
component: () => import('#/views/admin/permission/user-group.vue'),
meta: {
authority: ['super'],
affixTab: false,
icon: 'material-symbols:group',
title: $t('page.admin.permission.userGroup'),
},
},
{
name: 'PermissionRole',
path: '/permission/role',
component: () => import('#/views/admin/permission/role.vue'),
meta: {
authority: ['super'],
affixTab: false,
icon: 'material-symbols:shield-person',
title: $t('page.admin.permission.role'),
},
},
{
name: 'PermissionItem',
path: '/permission/permission',
component: () => import('#/views/admin/permission/permission.vue'),
meta: {
authority: ['super'],
affixTab: false,
icon: 'material-symbols:key',
title: $t('page.admin.permission.permission'),
},
},
{
name: 'PermissionUserAssign',
path: '/permission/user-assign',
component: () => import('#/views/admin/permission/user-assign.vue'),
meta: {
authority: ['super'],
affixTab: false,
icon: 'material-symbols:manage-accounts',
title: $t('page.admin.permission.userAssign'),
},
},
], ],
}, },
]; ];

View File

@ -0,0 +1,228 @@
<script setup lang="ts">
import type { VxeGridProps } from '#/adapter/vxe-table';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { useVbenModal, useVbenForm, Page } from '@vben/common-ui';
import {
getPermissionListApi,
addPermissionApi,
editPermissionApi,
deletePermissionApi,
} from '#/api/core/permission';
import type { PermissionApi } from '#/api/core/permission';
import { Button, Card, Space, Tag, Modal, message } from 'ant-design-vue';
import { ref } from 'vue';
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
const editingId = ref<number | undefined>(undefined);
const [Form, FormApi] = useVbenForm({
layout: 'horizontal',
wrapperClass: 'grid-cols-2',
showDefaultActions: false,
schema: [
{
component: 'Input',
fieldName: 'permission_code',
label: '权限编码',
formItemClass: 'col-span-2',
rules: 'required',
componentProps: { placeholder: '例如AC0004' },
},
{
component: 'Input',
fieldName: 'permission_name',
label: '权限名称',
formItemClass: 'col-span-2',
rules: 'required',
componentProps: { placeholder: '例如:编辑活动' },
},
{
component: 'Input',
fieldName: 'permission_group',
label: '权限分组',
formItemClass: 'col-span-2',
componentProps: { placeholder: '例如activity' },
},
{
component: 'Input',
fieldName: 'api_path',
label: '接口路径',
formItemClass: 'col-span-2',
componentProps: { placeholder: '例如:/api/activity/edit' },
},
{
component: 'Select',
fieldName: 'http_method',
label: '请求方法',
formItemClass: 'col-span-2',
componentProps: {
allowClear: true,
options: HTTP_METHODS.map((m) => ({ label: m, value: m })),
},
},
{
component: 'Select',
fieldName: 'status',
label: '状态',
formItemClass: 'col-span-2',
defaultValue: 1,
componentProps: {
options: [
{ label: '启用', value: 1 },
{ label: '停用', value: 0 },
],
},
},
{
component: 'Textarea',
fieldName: 'remark',
label: '备注',
formItemClass: 'col-span-2',
componentProps: { rows: 3 },
},
],
});
const [FormModal, FormModalApi] = useVbenModal({
confirmText: '保存',
onConfirm: async () => {
const values = await FormApi.getValues();
const params: PermissionApi.Permission = {
id: editingId.value,
permission_code: values.permission_code,
permission_name: values.permission_name,
permission_group: values.permission_group,
api_path: values.api_path,
http_method: values.http_method,
status: values.status,
remark: values.remark,
};
if (editingId.value) {
await editPermissionApi(params);
message.success('更新成功');
} else {
await addPermissionApi(params);
message.success('新增成功');
}
FormModalApi.close();
GridApi.reload();
},
});
const METHOD_COLORS: Record<string, string> = {
GET: 'green',
POST: 'blue',
PUT: 'orange',
DELETE: 'red',
PATCH: 'purple',
};
const gridOptions: VxeGridProps<PermissionApi.Permission> = {
columns: [
{ field: 'id', title: 'ID', width: 80 },
{ field: 'permission_code', title: '权限编码', width: 130 },
{ field: 'permission_name', title: '权限名称', minWidth: 150 },
{ field: 'permission_group', title: '分组', width: 120 },
{ field: 'api_path', title: '接口路径', minWidth: 200 },
{
field: 'http_method',
title: '方法',
width: 90,
slots: { default: 'methodSlot' },
},
{
field: 'status',
title: '状态',
width: 90,
slots: { default: 'statusSlot' },
},
{ fixed: 'right', slots: { default: 'actionSlot' }, title: '操作', width: 160 },
],
height: 'auto',
pagerConfig: {},
proxyConfig: {
response: { result: 'items', total: 'total' },
ajax: {
query: async ({ page }) => {
return await getPermissionListApi({
page: page.currentPage,
pageSize: page.pageSize,
});
},
},
},
rowConfig: { isHover: true },
};
const [Grid, GridApi] = useVbenVxeGrid({ gridOptions });
function openCreate() {
editingId.value = undefined;
FormApi.resetForm();
FormModalApi.setState({ title: '新增单点权限' });
FormModalApi.open();
}
function openEdit(row: PermissionApi.Permission) {
editingId.value = row.id;
FormApi.setValues({
permission_code: row.permission_code,
permission_name: row.permission_name,
permission_group: row.permission_group,
api_path: row.api_path,
http_method: row.http_method,
status: row.status,
remark: row.remark,
});
FormModalApi.setState({ title: '编辑单点权限' });
FormModalApi.open();
}
function handleDelete(row: PermissionApi.Permission) {
Modal.confirm({
title: '删除确认',
content: `确认删除权限「${row.permission_name}${row.permission_code})」吗?`,
async onOk() {
await deletePermissionApi(row.id!);
message.success('删除成功');
GridApi.reload();
},
});
}
</script>
<template>
<Page auto-content-height>
<FormModal>
<Form />
</FormModal>
<Card class="mb-4">
<template #extra>
<Button type="primary" @click="openCreate">新增权限</Button>
</template>
<template #title>单点权限管理</template>
</Card>
<Grid>
<template #methodSlot="{ row }">
<Tag v-if="row.http_method" :color="METHOD_COLORS[row.http_method] ?? 'default'">
{{ row.http_method }}
</Tag>
<span v-else>-</span>
</template>
<template #statusSlot="{ row }">
<Tag :color="row.status === 1 ? 'green' : 'red'">
{{ row.status === 1 ? '启用' : '停用' }}
</Tag>
</template>
<template #actionSlot="{ row }">
<Space>
<Button size="small" type="primary" @click="openEdit(row)">编辑</Button>
<Button danger size="small" @click="handleDelete(row)">删除</Button>
</Space>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,224 @@
<script setup lang="ts">
import type { VxeGridProps } from '#/adapter/vxe-table';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { Page } from '@vben/common-ui';
import {
getRoleListApi,
addRoleApi,
editRoleApi,
deleteRoleApi,
getPermissionListApi,
getRolePermissionsApi,
setRolePermissionsApi,
} from '#/api/core/permission';
import type { PermissionApi } from '#/api/core/permission';
import {
Button, Card, Space, Tag, Modal, message,
Form, FormItem, Input, Textarea, Select, Spin, Tabs, TabPane, Transfer,
} from 'ant-design-vue';
import { ref, computed } from 'vue';
//
const modalOpen = ref(false);
const modalTitle = ref('新增权限组');
const modalLoading = ref(false);
const activeTab = ref('basic');
const editingId = ref<number | undefined>(undefined);
const formState = ref({ role_code: '', role_name: '', status: 1 as 0 | 1, remark: '' });
// Transfer
const allPermissions = ref<PermissionApi.Permission[]>([]);
const selectedPermissionIds = ref<string[]>([]);
const permissionTransferData = computed(() =>
allPermissions.value.map((p) => ({
key: String(p.id),
title: `${p.permission_name}${p.permission_code}`,
description: p.api_path ? `${p.http_method ?? ''} ${p.api_path}` : '',
disabled: p.status !== 1,
})),
);
async function openCreate() {
editingId.value = undefined;
formState.value = { role_code: '', role_name: '', status: 1, remark: '' };
selectedPermissionIds.value = [];
activeTab.value = 'basic';
modalTitle.value = '新增权限组';
modalLoading.value = true;
modalOpen.value = true;
try {
const res = await getPermissionListApi({ page: 1, pageSize: 500 });
allPermissions.value = res.items || [];
} finally {
modalLoading.value = false;
}
}
async function openEdit(row: PermissionApi.Role) {
editingId.value = row.id;
formState.value = { role_code: row.role_code, role_name: row.role_name, status: row.status as 0 | 1, remark: row.remark || '' };
selectedPermissionIds.value = [];
activeTab.value = 'basic';
modalTitle.value = '编辑权限组';
modalLoading.value = true;
modalOpen.value = true;
try {
const [permsRes, rolePerms] = await Promise.all([
getPermissionListApi({ page: 1, pageSize: 500 }),
getRolePermissionsApi(row.id!),
]);
allPermissions.value = permsRes.items || [];
selectedPermissionIds.value = (rolePerms || []).map((p) => String(p.id));
} finally {
modalLoading.value = false;
}
}
async function handleConfirm() {
if (!formState.value.role_code || !formState.value.role_name) {
message.warning('请填写角色编码和名称');
activeTab.value = 'basic';
return;
}
modalLoading.value = true;
try {
const params: PermissionApi.Role = {
id: editingId.value,
role_code: formState.value.role_code,
role_name: formState.value.role_name,
status: formState.value.status,
remark: formState.value.remark,
};
let roleId = editingId.value;
if (editingId.value) {
await editRoleApi(params);
message.success('更新成功');
} else {
const res: any = await addRoleApi(params);
roleId = res?.id ?? res;
message.success('新增成功');
}
if (roleId) {
await setRolePermissionsApi({ role_id: roleId, permission_ids: selectedPermissionIds.value.map(Number) });
}
modalOpen.value = false;
GridApi.reload();
} finally {
modalLoading.value = false;
}
}
//
const gridOptions: VxeGridProps<PermissionApi.Role> = {
columns: [
{ field: 'id', title: 'ID', width: 80 },
{ field: 'role_code', title: '角色编码', width: 160 },
{ field: 'role_name', title: '角色名称', minWidth: 150 },
{ field: 'is_system', title: '类型', width: 100, slots: { default: 'isSystemSlot' } },
{ field: 'status', title: '状态', width: 100, slots: { default: 'statusSlot' } },
{ field: 'remark', title: '备注', minWidth: 200 },
{ fixed: 'right', slots: { default: 'actionSlot' }, title: '操作', width: 160 },
],
height: 'auto',
pagerConfig: {},
proxyConfig: {
response: { result: 'items', total: 'total' },
ajax: {
query: async ({ page }) =>
getRoleListApi({ page: page.currentPage, pageSize: page.pageSize }),
},
},
rowConfig: { isHover: true },
};
const [Grid, GridApi] = useVbenVxeGrid({ gridOptions });
function handleDelete(row: PermissionApi.Role) {
Modal.confirm({
title: '删除确认',
content: `确认删除权限组「${row.role_name}」吗?系统内置角色无法删除。`,
async onOk() {
await deleteRoleApi(row.id!);
message.success('删除成功');
GridApi.reload();
},
});
}
</script>
<template>
<Page auto-content-height>
<!-- 新增/编辑弹窗 -->
<Modal
v-model:open="modalOpen"
:confirm-loading="modalLoading"
:title="modalTitle"
width="760px"
@ok="handleConfirm"
>
<Spin :spinning="modalLoading">
<Tabs v-model:activeKey="activeTab">
<TabPane key="basic" tab="基本信息">
<Form layout="vertical" class="pt-3">
<FormItem label="角色编码" required>
<Input v-model:value="formState.role_code" placeholder="例如R_ACTIVITY" />
</FormItem>
<FormItem label="角色名称" required>
<Input v-model:value="formState.role_name" placeholder="例如:活动管理员" />
</FormItem>
<FormItem label="状态">
<Select
v-model:value="formState.status"
:options="[{ label: '启用', value: 1 }, { label: '停用', value: 0 }]"
class="w-full"
/>
</FormItem>
<FormItem label="备注">
<Textarea v-model:value="formState.remark" :rows="3" />
</FormItem>
</Form>
</TabPane>
<TabPane key="permissions" tab="单点权限配置">
<p class="mb-3 text-gray-500 text-sm">为该权限组角色绑定单点权限</p>
<Transfer
v-model:target-keys="selectedPermissionIds"
:data-source="permissionTransferData"
:render="(item: any) => item.title"
:titles="['可选权限', '已选权限']"
show-search
:list-style="{ width: '100%', height: '320px' }"
/>
</TabPane>
</Tabs>
</Spin>
</Modal>
<Card class="mb-4">
<template #extra>
<Button type="primary" @click="openCreate">新增权限组</Button>
</template>
<template #title>权限组角色管理</template>
</Card>
<Grid>
<template #isSystemSlot="{ row }">
<Tag :color="row.is_system === 1 ? 'blue' : 'default'">
{{ row.is_system === 1 ? '系统内置' : '自定义' }}
</Tag>
</template>
<template #statusSlot="{ row }">
<Tag :color="row.status === 1 ? 'green' : 'red'">
{{ row.status === 1 ? '启用' : '停用' }}
</Tag>
</template>
<template #actionSlot="{ row }">
<Space>
<Button size="small" type="primary" @click="openEdit(row)">编辑</Button>
<Button danger size="small" :disabled="row.is_system === 1" @click="handleDelete(row)">删除</Button>
</Space>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,240 @@
<script setup lang="ts">
import type { VxeGridProps } from '#/adapter/vxe-table';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { Page } from '@vben/common-ui';
import { getAdminListApi } from '#/api/core/admin.user';
import {
getUserGroupListApi,
getUserGroupsApi,
setUserGroupsApi,
getUserRolesApi,
getPermissionListApi,
getUserPermissionsDirectApi,
setUserPermissionsDirectApi,
} from '#/api/core/permission';
import type { PermissionApi } from '#/api/core/permission';
import type { UserInfo } from '#/model/admin.user';
import {
Button, Card, Space, Tag, Spin, message,
Modal, Tabs, TabPane, Transfer,
} from 'ant-design-vue';
import { ref, computed } from 'vue';
//
const userGroupsMap = ref<Map<number, PermissionApi.UserGroup[]>>(new Map());
const userRolesMap = ref<Map<number, PermissionApi.Role[]>>(new Map());
async function loadRowDataForList(users: UserInfo[]) {
const ids = users.map((u) => u.id).filter((id): id is number => id != null);
const [groupResults, roleResults] = await Promise.all([
Promise.allSettled(ids.map((id) => getUserGroupsApi(id))),
Promise.allSettled(ids.map((id) => getUserRolesApi(id))),
]);
const gMap = new Map<number, PermissionApi.UserGroup[]>();
const rMap = new Map<number, PermissionApi.Role[]>();
ids.forEach((id, i) => {
if (groupResults[i]!.status === 'fulfilled') gMap.set(id, (groupResults[i] as any).value || []);
if (roleResults[i]!.status === 'fulfilled') rMap.set(id, (roleResults[i] as any).value || []);
});
userGroupsMap.value = gMap;
userRolesMap.value = rMap;
}
//
const modalOpen = ref(false);
const modalLoading = ref(false);
const activeTab = ref('groups');
const currentAdminId = ref<number | null>(null);
const currentAdminName = ref('');
// Transfer
const allGroups = ref<PermissionApi.UserGroup[]>([]);
const selectedGroupIds = ref<string[]>([]);
const groupTransferData = computed(() =>
allGroups.value.map((g) => ({
key: String(g.id),
title: `${g.group_name}${g.group_code}`,
disabled: g.status !== 1,
})),
);
// Transferallow/deny allow
const allPermissions = ref<PermissionApi.Permission[]>([]);
const selectedAllowPermIds = ref<string[]>([]);
const selectedDenyPermIds = ref<string[]>([]);
const permissionTransferData = computed(() =>
allPermissions.value.map((p) => ({
key: String(p.id),
title: `${p.permission_name}${p.permission_code}`,
description: p.api_path ? `${p.http_method ?? ''} ${p.api_path}` : '',
disabled: p.status !== 1,
})),
);
async function openAssignModal(row: UserInfo) {
if (!row.id) {
message.warning('该用户缺少 ID无法分配权限');
return;
}
currentAdminId.value = row.id;
currentAdminName.value = row.username;
activeTab.value = 'groups';
modalLoading.value = true;
modalOpen.value = true;
try {
const [groupsRes, permsRes, userGroups, userDirectPerms] = await Promise.all([
getUserGroupListApi({ page: 1, pageSize: 500 }),
getPermissionListApi({ page: 1, pageSize: 500 }),
getUserGroupsApi(row.id),
getUserPermissionsDirectApi(row.id),
]);
allGroups.value = groupsRes.items || [];
allPermissions.value = permsRes.items || [];
selectedGroupIds.value = (userGroups || []).map((g) => String(g.id));
const directPerms = userDirectPerms || [];
selectedAllowPermIds.value = directPerms.filter((p) => p.grant_type === 1).map((p) => String(p.permission_id));
selectedDenyPermIds.value = directPerms.filter((p) => p.grant_type === 2).map((p) => String(p.permission_id));
} catch {
message.error('加载权限数据失败');
} finally {
modalLoading.value = false;
}
}
async function handleConfirm() {
if (currentAdminId.value == null) return;
modalLoading.value = true;
try {
const allowPerms = selectedAllowPermIds.value.map((id) => ({ permission_id: Number(id), grant_type: 1 as const }));
const denyPerms = selectedDenyPermIds.value.map((id) => ({ permission_id: Number(id), grant_type: 2 as const }));
await Promise.all([
setUserGroupsApi({ admin_id: currentAdminId.value, group_ids: selectedGroupIds.value.map(Number) }),
setUserPermissionsDirectApi({ admin_id: currentAdminId.value, permissions: [...allowPerms, ...denyPerms] }),
]);
message.success('权限分配成功');
modalOpen.value = false;
//
const [groups, roles] = await Promise.all([
getUserGroupsApi(currentAdminId.value),
getUserRolesApi(currentAdminId.value),
]);
userGroupsMap.value = new Map(userGroupsMap.value).set(currentAdminId.value, groups || []);
userRolesMap.value = new Map(userRolesMap.value).set(currentAdminId.value, roles || []);
} finally {
modalLoading.value = false;
}
}
//
const gridOptions: VxeGridProps<UserInfo> = {
columns: [
{ field: 'id', title: 'ID', width: 80 },
{ field: 'username', title: '用户名', width: 160 },
{ field: 'phone', title: '手机号', width: 140 },
{ field: 'email', title: '邮箱', minWidth: 160 },
{ field: 'userGroups', title: '用户组', minWidth: 200, slots: { default: 'userGroupsSlot' } },
{ field: 'userRoles', title: '权限组(角色)', minWidth: 200, slots: { default: 'userRolesSlot' } },
{ fixed: 'right', slots: { default: 'actionSlot' }, title: '操作', width: 120 },
],
height: '100%',
pagerConfig: {},
proxyConfig: {
response: { total: 'total', result: 'data' },
ajax: {
query: async () => {
const result = await getAdminListApi();
const list: UserInfo[] = Array.isArray(result) ? result : ((result as any)?.data ?? []);
void loadRowDataForList(list);
return result;
},
},
},
rowConfig: { isHover: true },
};
const [Grid, GridApi] = useVbenVxeGrid({ gridOptions });
</script>
<template>
<Page auto-content-height>
<!-- 分配弹窗 -->
<Modal
v-model:open="modalOpen"
:confirm-loading="modalLoading"
:title="`权限分配 — ${currentAdminName}`"
width="800px"
@ok="handleConfirm"
>
<Spin :spinning="modalLoading">
<Tabs v-model:activeKey="activeTab">
<!-- 用户组分配 -->
<TabPane key="groups" tab="用户组分配">
<p class="mb-3 text-gray-500 text-sm">将用户组分配给该管理员继承用户组内的权限组</p>
<Transfer
v-model:target-keys="selectedGroupIds"
:data-source="groupTransferData"
:render="(item: any) => item.title"
:titles="['可选用户组', '已选用户组']"
show-search
:list-style="{ width: '100%', height: '280px' }"
/>
</TabPane>
<!-- 单点权限允许 -->
<TabPane key="allow" tab="单点权限(允许)">
<p class="mb-3 text-gray-500 text-sm">直接授予该用户的单点权限grant_type=1优先级高于角色</p>
<Transfer
v-model:target-keys="selectedAllowPermIds"
:data-source="permissionTransferData"
:render="(item: any) => item.title"
:titles="['可选权限', '已允许']"
show-search
:list-style="{ width: '100%', height: '280px' }"
/>
</TabPane>
<!-- 单点权限拒绝 -->
<TabPane key="deny" tab="单点权限(拒绝)">
<p class="mb-3 text-gray-500 text-sm">明确拒绝该用户的单点权限grant_type=2可覆盖角色授权</p>
<Transfer
v-model:target-keys="selectedDenyPermIds"
:data-source="permissionTransferData"
:render="(item: any) => item.title"
:titles="['可选权限', '已拒绝']"
show-search
:list-style="{ width: '100%', height: '280px' }"
/>
</TabPane>
</Tabs>
</Spin>
</Modal>
<Card class="mb-4" title="用户权限分配">
<template #extra>
<Button @click="GridApi.reload()">刷新</Button>
</template>
</Card>
<Grid>
<template #userGroupsSlot="{ row }">
<Space wrap>
<template v-if="row.id && userGroupsMap.get(row.id)?.length">
<Tag v-for="g in userGroupsMap.get(row.id)" :key="g.id" color="blue">{{ g.group_name }}</Tag>
</template>
<span v-else class="text-gray-400 text-xs">未分配</span>
</Space>
</template>
<template #userRolesSlot="{ row }">
<Space wrap>
<template v-if="row.id && userRolesMap.get(row.id)?.length">
<Tag v-for="r in userRolesMap.get(row.id)" :key="r.id" color="purple">{{ r.role_name }}</Tag>
</template>
<span v-else class="text-gray-400 text-xs"></span>
</Space>
</template>
<template #actionSlot="{ row }">
<Button size="small" type="primary" @click="openAssignModal(row)">分配权限</Button>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,251 @@
<script setup lang="ts">
import type { VxeGridProps } from '#/adapter/vxe-table';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { Page } from '@vben/common-ui';
import {
getUserGroupListApi,
addUserGroupApi,
editUserGroupApi,
deleteUserGroupApi,
getRoleListApi,
getGroupRolesApi,
setGroupRolesApi,
getPermissionListApi,
} from '#/api/core/permission';
import type { PermissionApi } from '#/api/core/permission';
import {
Button, Card, Space, Tag, Modal, message,
Form, FormItem, Input, Textarea, Select, Spin, Tabs, TabPane, Transfer,
} from 'ant-design-vue';
import { ref, computed } from 'vue';
//
const modalOpen = ref(false);
const modalTitle = ref('新增用户组');
const modalLoading = ref(false);
const activeTab = ref('basic');
//
const editingId = ref<number | undefined>(undefined);
const formState = ref({ group_code: '', group_name: '', status: 1 as 0 | 1, remark: '' });
// Transfer
const allRoles = ref<PermissionApi.Role[]>([]);
const selectedRoleIds = ref<string[]>([]);
const roleTransferData = computed(() =>
allRoles.value.map((r) => ({
key: String(r.id),
title: `${r.role_name}${r.role_code}`,
disabled: r.status !== 1,
})),
);
// Transfer
const allPermissions = ref<PermissionApi.Permission[]>([]);
const selectedPermissionIds = ref<string[]>([]);
const permissionTransferData = computed(() =>
allPermissions.value.map((p) => ({
key: String(p.id),
title: `${p.permission_name}${p.permission_code}`,
description: p.api_path ? `${p.http_method ?? ''} ${p.api_path}` : '',
disabled: p.status !== 1,
})),
);
async function openCreate() {
editingId.value = undefined;
formState.value = { group_code: '', group_name: '', status: 1, remark: '' };
selectedRoleIds.value = [];
selectedPermissionIds.value = [];
activeTab.value = 'basic';
modalTitle.value = '新增用户组';
modalLoading.value = true;
modalOpen.value = true;
try {
const [rolesRes, permsRes] = await Promise.all([
getRoleListApi({ page: 1, pageSize: 500 }),
getPermissionListApi({ page: 1, pageSize: 500 }),
]);
allRoles.value = rolesRes.items || [];
allPermissions.value = permsRes.items || [];
} finally {
modalLoading.value = false;
}
}
async function openEdit(row: PermissionApi.UserGroup) {
editingId.value = row.id;
formState.value = { group_code: row.group_code, group_name: row.group_name, status: row.status as 0 | 1, remark: row.remark || '' };
selectedRoleIds.value = [];
selectedPermissionIds.value = [];
activeTab.value = 'basic';
modalTitle.value = '编辑用户组';
modalLoading.value = true;
modalOpen.value = true;
try {
const [rolesRes, permsRes, groupRoles] = await Promise.all([
getRoleListApi({ page: 1, pageSize: 500 }),
getPermissionListApi({ page: 1, pageSize: 500 }),
getGroupRolesApi(row.id!),
]);
allRoles.value = rolesRes.items || [];
allPermissions.value = permsRes.items || [];
selectedRoleIds.value = (groupRoles || []).map((r) => String(r.id));
} finally {
modalLoading.value = false;
}
}
async function handleConfirm() {
if (!formState.value.group_code || !formState.value.group_name) {
message.warning('请填写用户组编码和名称');
activeTab.value = 'basic';
return;
}
modalLoading.value = true;
try {
const params: PermissionApi.UserGroup = {
id: editingId.value,
group_code: formState.value.group_code,
group_name: formState.value.group_name,
status: formState.value.status,
remark: formState.value.remark,
};
let groupId = editingId.value;
if (editingId.value) {
await editUserGroupApi(params);
message.success('更新成功');
} else {
const res: any = await addUserGroupApi(params);
groupId = res?.id ?? res;
message.success('新增成功');
}
if (groupId) {
await setGroupRolesApi({ group_id: groupId, role_ids: selectedRoleIds.value.map(Number) });
}
modalOpen.value = false;
GridApi.reload();
} finally {
modalLoading.value = false;
}
}
//
const gridOptions: VxeGridProps<PermissionApi.UserGroup> = {
columns: [
{ field: 'id', title: 'ID', width: 80 },
{ field: 'group_code', title: '用户组编码', width: 150 },
{ field: 'group_name', title: '用户组名称', minWidth: 150 },
{ field: 'status', title: '状态', width: 100, slots: { default: 'statusSlot' } },
{ field: 'remark', title: '备注', minWidth: 200 },
{ fixed: 'right', slots: { default: 'actionSlot' }, title: '操作', width: 160 },
],
height: 'auto',
pagerConfig: {},
proxyConfig: {
response: { result: 'items', total: 'total' },
ajax: {
query: async ({ page }) =>
getUserGroupListApi({ page: page.currentPage, pageSize: page.pageSize }),
},
},
rowConfig: { isHover: true },
};
const [Grid, GridApi] = useVbenVxeGrid({ gridOptions });
function handleDelete(row: PermissionApi.UserGroup) {
Modal.confirm({
title: '删除确认',
content: `确认删除用户组「${row.group_name}」吗?`,
async onOk() {
await deleteUserGroupApi(row.id!);
message.success('删除成功');
GridApi.reload();
},
});
}
</script>
<template>
<Page auto-content-height>
<!-- 新增/编辑弹窗 -->
<Modal
v-model:open="modalOpen"
:confirm-loading="modalLoading"
:title="modalTitle"
width="760px"
@ok="handleConfirm"
>
<Spin :spinning="modalLoading">
<Tabs v-model:activeKey="activeTab">
<TabPane key="basic" tab="基本信息">
<Form layout="vertical" class="pt-3">
<FormItem label="用户组编码" required>
<Input v-model:value="formState.group_code" placeholder="例如G_OP" />
</FormItem>
<FormItem label="用户组名称" required>
<Input v-model:value="formState.group_name" placeholder="例如:运营组" />
</FormItem>
<FormItem label="状态">
<Select
v-model:value="formState.status"
:options="[{ label: '启用', value: 1 }, { label: '停用', value: 0 }]"
class="w-full"
/>
</FormItem>
<FormItem label="备注">
<Textarea v-model:value="formState.remark" :rows="3" />
</FormItem>
</Form>
</TabPane>
<TabPane key="roles" tab="权限组配置">
<p class="mb-3 text-gray-500 text-sm">将权限组角色绑定到该用户组</p>
<Transfer
v-model:target-keys="selectedRoleIds"
:data-source="roleTransferData"
:render="(item: any) => item.title"
:titles="['可选权限组', '已选权限组']"
show-search
:list-style="{ width: '100%', height: '280px' }"
/>
</TabPane>
<TabPane key="permissions" tab="单点权限配置">
<p class="mb-3 text-gray-500 text-sm">直接为该用户组绑定单点权限优先级高于角色</p>
<Transfer
v-model:target-keys="selectedPermissionIds"
:data-source="permissionTransferData"
:render="(item: any) => item.title"
:titles="['可选权限', '已选权限']"
show-search
:list-style="{ width: '100%', height: '280px' }"
/>
</TabPane>
</Tabs>
</Spin>
</Modal>
<Card class="mb-4">
<template #extra>
<Button type="primary" @click="openCreate">新增用户组</Button>
</template>
<template #title>用户组管理</template>
</Card>
<Grid>
<template #statusSlot="{ row }">
<Tag :color="row.status === 1 ? 'green' : 'red'">
{{ row.status === 1 ? '启用' : '停用' }}
</Tag>
</template>
<template #actionSlot="{ row }">
<Space>
<Button size="small" type="primary" @click="openEdit(row)">编辑</Button>
<Button danger size="small" @click="handleDelete(row)">删除</Button>
</Space>
</template>
</Grid>
</Page>
</template>

View File

@ -62,8 +62,8 @@ const sectionTitles: Record<SectionKey, string> = {
inactiveNotis: 'Inactive Notis', inactiveNotis: 'Inactive Notis',
}; };
const DOTNET_TICKS_AT_UNIX_EPOCH = 621355968000000000n; const DOTNET_TICKS_AT_UNIX_EPOCH = 621355968000000000;
const TICKS_PER_MILLISECOND = 10000n; const TICKS_PER_MILLISECOND = 10000;
function cloneValue<T>(value: T): T { function cloneValue<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T; return JSON.parse(JSON.stringify(value)) as T;
@ -79,8 +79,8 @@ function parseDotNetTicks(value?: number | string) {
} }
try { try {
const ticks = BigInt(String(value)); const ticks = Number(String(value));
const unixMilliseconds = Number((ticks - DOTNET_TICKS_AT_UNIX_EPOCH) / TICKS_PER_MILLISECOND); const unixMilliseconds = (ticks - DOTNET_TICKS_AT_UNIX_EPOCH) / TICKS_PER_MILLISECOND;
const date = dayjs(unixMilliseconds); const date = dayjs(unixMilliseconds);
return date.isValid() ? date : null; return date.isValid() ? date : null;
} catch { } catch {

View File

@ -4,6 +4,9 @@ import { compilerOptions } from 'vue3-pixi';
export default defineConfig(async () => { export default defineConfig(async () => {
return { return {
build:{
target: 'esnext',
},
plugins: [ plugins: [
vue({ vue({
template: { template: {

View File

@ -0,0 +1,10 @@
{
"folders": [
{
"path": "."
},
{
"path": "../../../admin_backend"
}
]
}