diff --git a/src/api/roles/index.ts b/src/api/roles/index.ts new file mode 100644 index 0000000..9234e90 --- /dev/null +++ b/src/api/roles/index.ts @@ -0,0 +1,72 @@ +/** + * 角色管理 API + */ +import { get, post } from '@/utils/request'; +import { USE_MOCK, MOCK_DELAY } from '@/config/mock'; +import { buildMockRoleListPage } from '@/config/mock/roleList'; +import { buildMockRoleUsageUserPage } from '@/config/mock/roleUsageUser'; +import { buildMockPermissionTree } from '@/config/mock/permissionTree'; +import type { + RoleListQueryParams, + RoleUsageUserQueryParams, + TournamentAdminRolePageVO, + RoleUsageUserVO, + PermissionTreeNode, + PageData, + ApiResult, +} from './types'; + +export type { + TournamentAdminRolePageVO, + RoleUsageUserVO, + RoleListQueryParams, + RoleUsageUserQueryParams, + PermissionTreeNode, + PageData, + ApiResult, +} from './types'; + +const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +/** GET /sys/role/page — 角色列表分页 */ +export async function getRoleList( + params: RoleListQueryParams, +): Promise>> { + if (USE_MOCK) { + await delay(MOCK_DELAY); + const page = Number(params.page) || 1; + const limit = Number(params.limit) || 10; + return { code: 200, msg: 'success', data: buildMockRoleListPage(page, limit) }; + } + return get('/sys/role/page', params as Record); +} + +/** GET /sys/role/usageUserPage — 角色下用户列表分页 */ +export async function getRoleUsageUserList( + params: RoleUsageUserQueryParams, +): Promise>> { + if (USE_MOCK) { + await delay(MOCK_DELAY); + const page = Number(params.page) || 1; + const limit = Number(params.limit) || 10; + return { code: 200, msg: 'success', data: buildMockRoleUsageUserPage(page, limit) }; + } + return get('/sys/role/usageUserPage', params as Record); +} + +/** GET /admin/sys/role/permissionList?roleId=xxx — 角色权限树 */ +export async function getPermissionTree( + roleId: number | string, +): Promise> { + if (USE_MOCK) { + await delay(MOCK_DELAY); + const id = Number(roleId) || 0; + return { code: 200, msg: 'success', data: buildMockPermissionTree(id) }; + } + return get('/admin/sys/role/permissionList', { roleId: String(roleId) }); +} + +/** POST /admin/sys/role/del — 删除角色 */ +export async function deleteRole(id: number): Promise> { + return post('/admin/sys/role/del', { id }); +} diff --git a/src/api/roles/types.ts b/src/api/roles/types.ts new file mode 100644 index 0000000..7dc35f7 --- /dev/null +++ b/src/api/roles/types.ts @@ -0,0 +1,66 @@ +/** + * 角色管理 — 类型定义 + * 对应 OpenAPI: /admin/sys/role/* + */ + +/** 角色列表项 */ +export interface TournamentAdminRolePageVO { + /** 角色ID */ + roleId: number; + /** 角色名称 */ + roleName: string; + /** 角色用户数量 */ + userCount: number; + /** 角色创建时间 */ + createTime: string; +} + +/** 角色列表查询参数 */ +export interface RoleListQueryParams { + page?: string; + limit?: string; + /** 角色名称(模糊搜索) */ + name?: string; +} + +/** 角色下用户列表项 */ +export interface RoleUsageUserVO { + realName: string; + phone: string; + createTime: string; + /** 状态: 0=停用、1=正常 */ + status: number; +} + +/** 角色下用户列表查询参数 */ +export interface RoleUsageUserQueryParams { + page?: string; + limit?: string; + /** 用户姓名或手机号 */ + text?: string; + roleId?: string; +} + +export interface PageData { + total: number; + list: T[]; +} +export interface ApiResult { + code: number; + msg: string; + data: T; +} + +/** 权限树节点(后端 /admin/sys/role/permissionList 返回) */ +export interface PermissionTreeNode { + /** 主键 */ + id: number; + /** 上级ID */ + pid: number; + /** 标题 */ + name: string; + /** 是否选中:true=是,false=否,null=空(仅叶节点有意义,父节点遵循级联逻辑) */ + selected: boolean | null; + /** 子节点 */ + children?: PermissionTreeNode[]; +} diff --git a/src/config/mock/operationLogs.ts b/src/config/mock/operationLogs.ts index 3a08e24..8215b61 100644 --- a/src/config/mock/operationLogs.ts +++ b/src/config/mock/operationLogs.ts @@ -1,181 +1,169 @@ /** - * 操作日志页假数据 - * 类型内联定义,避免循环引用。 + * 操作日志假数据 + * 对应接口:GET /admin/sys/operation/page + * + * 支持按 type、nickname、dateBegin/dateEnd 前端筛选, + * 模拟后端分页行为。 */ +import type { SysOperationLogVO, OperationLogQueryParams, PageData } from '@/api/logs'; -interface MockLogVO { - opName: string; - source: string; - nickname: string; - phone: string; - tournamentName: string; - obj: string; - createDate: string; - /** 操作内容详情(mock 扩展字段,API 暂无) */ - content?: string; -} - -const MOCK_LOGS: MockLogVO[] = [ +const MOCK_LOGS: SysOperationLogVO[] = [ { - opName: '删除赛事', - source: '2', + type: 1, nickname: '张三', - phone: '12345678989', - tournamentName: '第二十一届风林杯羽毛球大赛', - obj: '第二十一届风林杯羽毛球大赛', - createDate: '2026-05-26 08:00:00', - content: '删除赛事', + createDate: '2026-07-30 08:00:00', + content: '下架赛事"第二十一届贝格林羽毛球大赛"', }, { - opName: '取消报名', - source: '1', - nickname: '', - phone: '', - tournamentName: '', - obj: '张三', - createDate: '2026-05-27 10:12:00', - content: '选手"张三"取消报名', + type: 2, + nickname: '张三', + createDate: '2026-07-30 09:30:00', + content: '上架赛事"第二十一届贝格林羽毛球大赛"', }, { - opName: '编辑选手', - source: '1', - nickname: '', - phone: '', - tournamentName: '', - obj: '李四', - createDate: '2026-05-27 10:30:00', - content: '修改姓名,修改前:李四,修改后:张三', + type: 3, + nickname: '李四', + createDate: '2026-07-29 10:12:00', + content: '禁用用户"王五(1003)"', }, { - opName: '编辑选手', - source: '1', - nickname: '', - phone: '', - tournamentName: '', - obj: '李四', - createDate: '2026-05-27 10:35:00', - content: - '修改手机号,修改前:1234567899,修改后:13425678997;修改组别,修改前:男子单打,修改后:男子双打', + type: 4, + nickname: '李四', + createDate: '2026-07-29 10:20:00', + content: '启用用户"王五(1003)"', }, { - opName: '批量生成对阵', - source: '1', - nickname: '', - phone: '', - tournamentName: '', - obj: '男子小组A,男子小组B,男子小组C', - createDate: '2026-05-27 11:00:00', - content: '生成对阵', + type: 1, + nickname: '赵六', + createDate: '2026-07-28 14:00:00', + content: '下架赛事"春季马拉松大赛"', }, { - opName: '生成对阵', - source: '1', - nickname: '', - phone: '', - tournamentName: '', - obj: '男子小组A', - createDate: '2026-05-27 11:05:00', - content: '生成对阵', + type: 2, + nickname: '赵六', + createDate: '2026-07-28 15:00:00', + content: '上架赛事"春季马拉松大赛"', }, { - opName: '批量删除对阵', - source: '1', - nickname: '', - phone: '', - tournamentName: '', - obj: '男子小组A,男子小组B,男子小组C', - createDate: '2026-05-27 11:10:00', - content: '删除对阵', + type: 3, + nickname: '张三', + createDate: '2026-07-27 09:00:00', + content: '禁用用户"钱七(1005)"', }, { - opName: '删除对阵', - source: '1', - nickname: '', - phone: '', - tournamentName: '', - obj: '男子小组A', - createDate: '2026-05-27 11:15:00', - content: '删除对阵', + type: 4, + nickname: '张三', + createDate: '2026-07-27 09:15:00', + content: '启用用户"钱七(1005)"', }, { - opName: '修改比分', - source: '1', - nickname: '', - phone: '', - tournamentName: '', - obj: '男子小组A,张三李四,第一局', - createDate: '2026-05-27 12:00:00', - content: '修改前:比分"21:0",修改后:"0:21"', + type: 1, + nickname: '孙八', + createDate: '2026-07-26 16:30:00', + content: '下架赛事"夏季游泳锦标赛"', }, { - opName: '弃权', - source: '1', - nickname: '', - phone: '', - tournamentName: '', - obj: '男子淘汰1,第一轮,张三李四', - createDate: '2026-05-27 13:00:00', - content: '选手"张三"弃权', + type: 2, + nickname: '孙八', + createDate: '2026-07-26 17:00:00', + content: '上架赛事"夏季游泳锦标赛"', }, { - opName: '弃权', - source: '1', - nickname: '', - phone: '', - tournamentName: '', - obj: '男子小组A,张三李四', - createDate: '2026-05-27 13:10:00', - content: '双方弃权,选手"张三"、"李四"', + type: 1, + nickname: '李四', + createDate: '2026-07-25 11:00:00', + content: '下架赛事"秋季篮球邀请赛"', }, { - opName: '修改对阵', - source: '1', - nickname: '', - phone: '', - tournamentName: '', - obj: '男子小组A,张三李四,1', - createDate: '2026-05-27 14:00:00', - content: '修改前:比分"21:10",修改后:比分"21:0"', + type: 2, + nickname: '李四', + createDate: '2026-07-25 12:00:00', + content: '上架赛事"秋季篮球邀请赛"', }, { - opName: '修改对阵', - source: '1', - nickname: '', - phone: '', - tournamentName: '', - obj: '男子小组A,张三李四,双打名额,选手 场地名额', - createDate: '2026-05-27 14:30:00', - content: - '修改前:比分"21:0",对局状态已完成,对阵时间:08:50-09:00,场地号:羽1,修改后:比分"0:0",对局状态为未开始,对阵时间:08:50-09:00,场地号:羽2', + type: 3, + nickname: '周九', + createDate: '2026-07-24 08:30:00', + content: '禁用用户"吴十(1008)"', }, { - opName: '修改场地', - source: '1', - nickname: '', - phone: '', - tournamentName: '', - obj: '羽1', - createDate: '2026-05-27 15:00:00', - content: '修改前:羽1,修改后:羽3', + type: 4, + nickname: '周九', + createDate: '2026-07-24 08:45:00', + content: '启用用户"吴十(1008)"', }, { - opName: '修改种子', - source: '1', - nickname: '', - phone: '', - tournamentName: '', - obj: '张某某,张三 场地名额', - createDate: '2026-05-27 15:30:00', - content: '修改前:1号种子,修改后:2号种子', + type: 1, + nickname: '张三', + createDate: '2026-07-23 13:00:00', + content: '下架赛事"冬季滑雪挑战赛"', + }, + { + type: 2, + nickname: '张三', + createDate: '2026-07-23 14:00:00', + content: '上架赛事"冬季滑雪挑战赛"', + }, + { + type: 3, + nickname: '郑十一', + createDate: '2026-07-22 10:00:00', + content: '禁用用户"冯十二(1012)"', + }, + { + type: 4, + nickname: '郑十一', + createDate: '2026-07-22 10:10:00', + content: '启用用户"冯十二(1012)"', + }, + { + type: 1, + nickname: '孙八', + createDate: '2026-07-21 15:30:00', + content: '下架赛事"元旦全民健身跑"', + }, + { + type: 2, + nickname: '孙八', + createDate: '2026-07-21 16:00:00', + content: '上架赛事"元旦全民健身跑"', }, ]; -interface MockPageData { - total: number; - list: T[]; +/** 按日期范围过滤(dateBegin / dateEnd 格式 yyyy-MM-dd) */ +function inDateRange(dateStr: string, begin?: string, end?: string): boolean { + if (!begin && !end) return true; + const d = dateStr.slice(0, 10); // yyyy-MM-dd + if (begin && d < begin) return false; + if (end && d > end) return false; + return true; } -export function buildMockOperationLogPage(page: number, limit: number): MockPageData { +export function buildMockOperationLogPage( + page: number, + limit: number, + params: OperationLogQueryParams, +): PageData { + let filtered = [...MOCK_LOGS]; + + // 按操作类型筛选(API 传 '1'/'2'/'3'/'4') + if (params.type) { + filtered = filtered.filter((item) => item.type === Number(params.type)); + } + + // 按操作人昵称模糊筛选 + if (params.nickname) { + filtered = filtered.filter((item) => item.nickname.includes(params.nickname!)); + } + + // 按日期范围筛选 + filtered = filtered.filter((item) => + inDateRange(item.createDate, params.dateBegin, params.dateEnd), + ); + + const total = filtered.length; const start = (page - 1) * limit; - return { total: MOCK_LOGS.length, list: MOCK_LOGS.slice(start, start + limit) }; + const list = filtered.slice(start, start + limit); + + return { total, list }; } diff --git a/src/config/mock/permissionTree.ts b/src/config/mock/permissionTree.ts new file mode 100644 index 0000000..d26d7b4 --- /dev/null +++ b/src/config/mock/permissionTree.ts @@ -0,0 +1,213 @@ +/** + * 角色权限树假数据 + * 对应接口:GET /admin/sys/role/permissionList?roleId=xxx + * + * - 传 roleId=0 时返回全量未选中树(新增角色用) + * - 传已有角色ID时返回带 selected 的树,模拟后端已有权限 + * + * 结构对齐 PermissionTreeNode: { id, pid, name, selected, children } + * selected 仅叶节点有值(true/false),父节点为 null + */ + +import type { PermissionTreeNode } from '@/api/roles'; + +/** + * 全量权限树(所有节点的 selected=null,叶节点 selected=false) + * 作为未授权角色的默认返回 + */ +const FULL_UNSELECTED: PermissionTreeNode[] = [ + { + id: 1, + pid: 0, + name: '赛事管理', + selected: null, + children: [ + { + id: 11, + pid: 1, + name: '赛事列表', + selected: null, + children: [ + { id: 101, pid: 11, name: '赛事列表-查看', selected: false }, + { id: 102, pid: 11, name: '赛事列表-上架', selected: false }, + { id: 103, pid: 11, name: '赛事列表-下架', selected: false }, + ], + }, + { + id: 12, + pid: 1, + name: '赛事规程', + selected: null, + children: [ + { id: 104, pid: 12, name: '赛事规程-查看', selected: false }, + { id: 105, pid: 12, name: '赛事规程-编辑', selected: false }, + ], + }, + { + id: 13, + pid: 1, + name: '订单管理', + selected: null, + children: [ + { id: 106, pid: 13, name: '订单管理-查看', selected: false }, + { id: 107, pid: 13, name: '订单管理-退款', selected: false }, + ], + }, + { + id: 14, + pid: 1, + name: '用户列表', + selected: null, + children: [{ id: 108, pid: 14, name: '用户列表-查看', selected: false }], + }, + { + id: 15, + pid: 1, + name: 'Banner管理', + selected: null, + children: [ + { id: 109, pid: 15, name: 'Banner-查看', selected: false }, + { id: 110, pid: 15, name: 'Banner-新增', selected: false }, + { id: 111, pid: 15, name: 'Banner-编辑', selected: false }, + { id: 112, pid: 15, name: 'Banner-删除', selected: false }, + ], + }, + { + id: 16, + pid: 1, + name: '赛事审核', + selected: null, + children: [ + { id: 113, pid: 16, name: '赛事审核-查看', selected: false }, + { id: 114, pid: 16, name: '赛事审核-通过', selected: false }, + { id: 115, pid: 16, name: '赛事审核-驳回', selected: false }, + ], + }, + ], + }, + { + id: 2, + pid: 0, + name: '账务管理', + selected: null, + children: [ + { + id: 21, + pid: 2, + name: '财务报表', + selected: null, + children: [{ id: 201, pid: 21, name: '财务报表-查看', selected: false }], + }, + { + id: 22, + pid: 2, + name: '提现管理', + selected: null, + children: [ + { id: 202, pid: 22, name: '提现管理-查看', selected: false }, + { id: 203, pid: 22, name: '提现管理-审核', selected: false }, + ], + }, + { + id: 23, + pid: 2, + name: '支付流水', + selected: null, + children: [{ id: 204, pid: 23, name: '支付流水-查看', selected: false }], + }, + { + id: 24, + pid: 2, + name: '钱包管理', + selected: null, + children: [{ id: 205, pid: 24, name: '钱包管理-查看', selected: false }], + }, + ], + }, + { + id: 3, + pid: 0, + name: '系统管理', + selected: null, + children: [ + { + id: 31, + pid: 3, + name: '用户管理', + selected: null, + children: [ + { id: 301, pid: 31, name: '用户管理-查看', selected: false }, + { id: 302, pid: 31, name: '用户管理-新增', selected: false }, + { id: 303, pid: 31, name: '用户管理-编辑', selected: false }, + { id: 304, pid: 31, name: '用户管理-启用/禁用', selected: false }, + { id: 305, pid: 31, name: '用户管理-重置密码', selected: false }, + ], + }, + { + id: 32, + pid: 3, + name: '角色权限', + selected: null, + children: [ + { id: 306, pid: 32, name: '角色权限-查看', selected: false }, + { id: 307, pid: 32, name: '角色权限-新增', selected: false }, + { id: 308, pid: 32, name: '角色权限-编辑', selected: false }, + { id: 309, pid: 32, name: '角色权限-删除', selected: false }, + ], + }, + { + id: 33, + pid: 3, + name: '操作日志', + selected: null, + children: [{ id: 310, pid: 33, name: '操作日志-查看', selected: false }], + }, + ], + }, +]; + +/** + * 根据角色已有权限ID集合,深拷贝全量树并标记叶节点 selected + */ +function applySelected(tree: PermissionTreeNode[], selectedIds: Set): PermissionTreeNode[] { + return tree.map((node) => { + const isLeaf = !node.children || node.children.length === 0; + if (isLeaf) { + return { ...node, selected: selectedIds.has(node.id) }; + } + // 父节点:递归处理子节点,自身 selected 保持 null + return { + ...node, + selected: null, + children: applySelected(node.children!, selectedIds), + }; + }); +} + +/** + * 构建权限树假数据 + * @param roleId 角色ID,0 表示新增(返回全量未选中),其他值模拟该角色已有权限 + */ +export function buildMockPermissionTree(roleId: number): PermissionTreeNode[] { + // roleId=0 → 新增角色,全量未选中 + if (roleId === 0) { + return JSON.parse(JSON.stringify(FULL_UNSELECTED)); + } + + // 模拟不同角色拥有不同权限子集 + const rolePermissions: Record = { + // 超级管理员:全选(所有叶节点) + 1: [ + 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 201, 202, 203, 204, + 205, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, + ], + // 赛事管理员:只有赛事管理模块 + 2: [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112], + // 财务:只有账务管理模块 + 4: [201, 202, 203, 204, 205], + // 其他角色默认仅查看权限 + }; + + const selectedIds = new Set(rolePermissions[roleId] || [101, 201, 301, 306, 310]); + return applySelected(FULL_UNSELECTED, selectedIds); +} diff --git a/src/config/mock/roleList.ts b/src/config/mock/roleList.ts new file mode 100644 index 0000000..1a1ff28 --- /dev/null +++ b/src/config/mock/roleList.ts @@ -0,0 +1,31 @@ +/** + * 角色列表页假数据(类型内联,避免循环引用) + * 对应接口:GET /sys/role/page + */ + +interface MockRoleVO { + roleId: number; + roleName: string; + userCount: number; + createTime: string; +} +interface MockPageData { + total: number; + list: T[]; +} + +const MOCK_ROLES: MockRoleVO[] = [ + { roleId: 1, roleName: '超级管理员', userCount: 2, createTime: '2025-06-01 09:00:00' }, + { roleId: 2, roleName: '赛事管理员', userCount: 5, createTime: '2025-08-15 14:30:00' }, + { roleId: 3, roleName: '裁判', userCount: 8, createTime: '2025-10-10 10:15:00' }, + { roleId: 4, roleName: '财务', userCount: 3, createTime: '2026-01-20 16:00:00' }, + { roleId: 5, roleName: '运营管理员', userCount: 4, createTime: '2026-03-05 11:00:00' }, + { roleId: 6, roleName: '客服', userCount: 6, createTime: '2026-04-10 08:30:00' }, + { roleId: 7, roleName: '审核员', userCount: 3, createTime: '2026-05-18 13:45:00' }, + { roleId: 8, roleName: '内容编辑', userCount: 2, createTime: '2026-06-22 09:20:00' }, +]; + +export function buildMockRoleListPage(page: number, limit: number): MockPageData { + const start = (page - 1) * limit; + return { total: MOCK_ROLES.length, list: MOCK_ROLES.slice(start, start + limit) }; +} diff --git a/src/config/mock/roleUsageUser.ts b/src/config/mock/roleUsageUser.ts new file mode 100644 index 0000000..b7e0299 --- /dev/null +++ b/src/config/mock/roleUsageUser.ts @@ -0,0 +1,33 @@ +/** + * 角色下用户列表假数据 + * 对应接口:GET /sys/role/usageUserPage + */ + +interface MockUserVO { + realName: string; + phone: string; + createTime: string; + status: number; +} +interface MockPageData { + total: number; + list: T[]; +} + +const MOCK_USERS: MockUserVO[] = [ + { realName: '张超管', phone: '13900000001', createTime: '2025-06-01 09:00:00', status: 1 }, + { realName: '李运营', phone: '15900000002', createTime: '2026-01-10 16:00:00', status: 0 }, + { realName: '王运营', phone: '15900000001', createTime: '2025-10-20 10:00:00', status: 1 }, + { realName: '张运营', phone: '13900000002', createTime: '2025-08-15 14:30:00', status: 1 }, + { realName: '赵裁判', phone: '13800000005', createTime: '2025-10-10 10:15:00', status: 1 }, + { realName: '钱裁判', phone: '13800000006', createTime: '2025-10-12 11:00:00', status: 1 }, + { realName: '孙财务', phone: '13700000007', createTime: '2026-01-20 16:00:00', status: 1 }, + { realName: '周客服', phone: '13700000008', createTime: '2026-01-21 09:30:00', status: 1 }, + { realName: '吴客服', phone: '13700000009', createTime: '2026-01-22 10:00:00', status: 0 }, + { realName: '郑客服', phone: '13700000010', createTime: '2026-01-23 14:00:00', status: 1 }, +]; + +export function buildMockRoleUsageUserPage(page: number, limit: number): MockPageData { + const start = (page - 1) * limit; + return { total: MOCK_USERS.length, list: MOCK_USERS.slice(start, start + limit) }; +} diff --git a/src/pages/events/banner/components/BannerFormModal.tsx b/src/pages/events/banner/components/BannerFormModal.tsx index 05db4a1..720971a 100644 --- a/src/pages/events/banner/components/BannerFormModal.tsx +++ b/src/pages/events/banner/components/BannerFormModal.tsx @@ -555,7 +555,7 @@ export default defineComponent({
- - - - - - - - -
- - {/* 表格 */} - - - {/* 底部:共 X 人 + 关闭按钮 */} -
-
共 {filteredUsers.value.length} 人
- -
- - {/* 分页(右侧) */} - {filteredUsers.value.length > pagination.value.pageSize && ( -
- ( + + {/* 筛选 */} +
+
+ + (filterForm.text = val?.target?.value ?? val ?? '')} + onPressEnter={handleSearch} /> -
- )} -
- ); - }; + + + + + + + + +
+ + {/* 表格 */} +
+ + {/* 底部 */} +
+
共 {pagination.value.total} 人
+ +
+ + {/* 分页 */} + {pagination.value.total > pagination.value.pageSize && ( +
+ `共 ${total} 条`} + onChange={handlePageChange} + onShowSizeChange={handlePageChange} + /> +
+ )} + + ); }, }); diff --git a/src/pages/system/roles/controller.ts b/src/pages/system/roles/controller.ts index fe1dba0..78e1ed2 100644 --- a/src/pages/system/roles/controller.ts +++ b/src/pages/system/roles/controller.ts @@ -1,9 +1,10 @@ /** * 角色权限 - 表单域控制器 * - * 集中管理角色表单的规则、菜单权限树 mock 数据与提示文案。 + * 集中管理角色表单的规则、权限树转换工具。 */ import type { Rule } from 'ant-design-vue/es/form'; +import type { PermissionTreeNode } from '@/api/roles'; // ============================================================ // 规则 @@ -16,50 +17,82 @@ export const roleNameRules: Rule[] = [ ]; // ============================================================ -// 菜单权限树(mock) +// 权限树 — 类型 & 工具 // ============================================================ -export interface MenuPermNode { - /** 唯一 key,作为 Tree 节点的 key */ +/** Ant Design Tree 组件使用的节点结构 */ +export interface AntdTreeNode { key: string; - /** 显示标题 */ title: string; - /** 子节点 */ - children?: MenuPermNode[]; + children?: AntdTreeNode[]; } /** - * 完整菜单权限树。 - * 后续可改为从后端接口拉取(接口应返回该结构)。 + * 将后端权限树节点递归转为 antd Tree 格式, + * 同时收集叶节点中 selected===true 的 key。 + * + * ## 级联规则 + * - `selected` 仅叶节点有实际值(true / false / null) + * - 父节点的勾选状态由子节点的 selected 推导(全选=勾选,部分选=半选) + * - antd Tree 的 checkedKeys 传所有选中的 key(含父节点),Tree 自动处理级联 */ -export const PERMISSION_TREE: MenuPermNode[] = [ - { - key: 'events', - title: '赛事管理', - children: [ - { - key: 'events.list', - title: '赛事列表', - children: [ - { key: 'events.list.view', title: '赛事列表-查看' }, - { key: 'events.list.on', title: '赛事列表-上架' }, - { key: 'events.list.off', title: '赛事列表-下架' }, - { key: 'events.rules.view', title: '赛事规程-查看' }, - ], - }, - { - key: 'orders', - title: '订单管理', - children: [ - { key: 'orders.view', title: '订单管理-查看' }, - { key: 'orders.refund', title: '订单管理-重新退款' }, - ], - }, - { key: 'users', title: '用户列表' }, - ], - }, - { - key: 'events_logs', - title: '赛事操作日志', - }, -]; +function convertNode(node: PermissionTreeNode, leafKeys: Set): AntdTreeNode { + const key = String(node.id); + const hasChildren = node.children && node.children.length > 0; + + if (!hasChildren) { + // 叶节点:根据 selected 收集 + if (node.selected === true) { + leafKeys.add(key); + } + return { key, title: node.name }; + } + + // 父节点:递归处理子节点 + const children = node.children!.map((child) => convertNode(child, leafKeys)); + return { key, title: node.name, children }; +} + +/** + * 将后端 PermissionTreeNode[] 转为: + * 1. antd Tree 可用的 treeData + * 2. 由叶节点 selected 推导出的完整 checkedKeys(含父节点) + */ +export function buildTreeData(apiTree: PermissionTreeNode[]): { + treeData: AntdTreeNode[]; + checkedKeys: string[]; +} { + const leafKeys = new Set(); + const treeData = apiTree.map((node) => convertNode(node, leafKeys)); + return { treeData, checkedKeys: Array.from(leafKeys) }; +} + +/** + * 从后端权限树中提取叶节点数据,整理为提交格式。 + * + * 只返回叶节点,格式为 `{ id, selected }` 数组。 + * 父节点的勾选状态由后端根据叶节点数据推导,前端不传父节点。 + */ +export function extractLeafPermissions( + apiTree: PermissionTreeNode[], +): { id: number; selected: boolean }[] { + const result: { id: number; selected: boolean }[] = []; + + const walk = (nodes: PermissionTreeNode[]) => { + for (const node of nodes) { + const isLeaf = !node.children || node.children.length === 0; + if (isLeaf) { + result.push({ + id: node.id, + // null 视为 false,即未授权 + selected: node.selected === true, + }); + } else { + walk(node.children!); + } + } + }; + + walk(apiTree); + return result; +} diff --git a/src/pages/system/roles/index.tsx b/src/pages/system/roles/index.tsx index 2153780..9edc3c2 100644 --- a/src/pages/system/roles/index.tsx +++ b/src/pages/system/roles/index.tsx @@ -1,4 +1,4 @@ -import { defineComponent } from 'vue'; +import { defineComponent, onMounted } from 'vue'; import { Button, Input, Table, Form, Space, Select, Modal, Pagination } from 'ant-design-vue'; import type { ModalProps } from 'ant-design-vue'; import { useRoleModel } from './model/useRoleModel'; @@ -97,8 +97,6 @@ export default defineComponent({ handleDelete, handleViewUsers, handleCloseUserList, - getRoleUsers, - renderStatus, } = useRoleModel(); const { containerRef, height } = useContainerSize(); @@ -116,16 +114,23 @@ export default defineComponent({ }, ]; + // ===== 初始化 ===== + onMounted(() => { + handleSearch(); + }); + return () => (
{/* ===== 筛选区 ===== */}
- + (filterForm.name = val?.target?.value ?? val ?? '')} onPressEnter={handleSearch} /> @@ -194,9 +199,8 @@ export default defineComponent({ {userListVisible.value && ( )} diff --git a/src/pages/system/roles/model/useRoleModel.ts b/src/pages/system/roles/model/useRoleModel.ts index ca3bdb5..cad00e3 100644 --- a/src/pages/system/roles/model/useRoleModel.ts +++ b/src/pages/system/roles/model/useRoleModel.ts @@ -1,152 +1,12 @@ -import { computed, reactive, toRef, Ref, h } from 'vue'; +import { computed, reactive, toRef, Ref } from 'vue'; import { message } from 'ant-design-vue'; -import dayjs from 'dayjs'; -import { StatusTag, type StatusTagTone } from '@/components'; import { useState, useDebounce, useThrottleFn } from '@/hooks'; - -// ============================================================ -// 假数据 -// ============================================================ - -const MOCK_DATA = [ - { - key: '1', - roleId: 'R20250601001', - roleName: '超级管理员', - userCount: 1, - createTime: '2025-06-01 09:00:00', - }, - { - key: '2', - roleId: 'R20250815001', - roleName: '运营管理员', - userCount: 3, - createTime: '2025-08-15 14:30:00', - }, - { - key: '3', - roleId: 'R20251010001', - roleName: '财务', - userCount: 2, - createTime: '2025-10-10 10:15:00', - }, - { - key: '4', - roleId: 'R20260120001', - roleName: '客服', - userCount: 4, - createTime: '2026-01-20 16:00:00', - }, -]; - -/** - * 临时排序:对假数据按创建时间倒序排列。 - * 【注意】实际项目中排序由后端接口负责,对接后端后请删除此函数及相关调用。 - */ -const sortByTimeDesc = (list: any[], field: string) => - [...list].sort((a, b) => dayjs(b[field]).valueOf() - dayjs(a[field]).valueOf()); - -const SORTED_MOCK_DATA = sortByTimeDesc(MOCK_DATA, 'createTime'); - -/** 角色下用户列表 mock */ -const MOCK_USERS: Record = { - R20250601001: [ - { - key: '1', - userId: 'U001', - realName: '张超管', - phone: '13900000001', - createTime: '2025-06-01 09:00:00', - status: 'active', - }, - ], - R20250815001: [ - { - key: '1', - userId: 'U002', - realName: '李运营', - phone: '15900000002', - createTime: '2026-01-10 16:00:00', - status: 'disabled', - }, - { - key: '2', - userId: 'U003', - realName: '王运营', - phone: '15900000001', - createTime: '2025-10-20 10:00:00', - status: 'active', - }, - { - key: '3', - userId: 'U004', - realName: '张运营', - phone: '13900000002', - createTime: '2025-08-15 14:30:00', - status: 'active', - }, - ], - R20251010001: [ - { - key: '1', - userId: 'U005', - realName: '赵财务', - phone: '13800000005', - createTime: '2025-10-10 10:15:00', - status: 'active', - }, - { - key: '2', - userId: 'U006', - realName: '钱财务', - phone: '13800000006', - createTime: '2025-10-12 11:00:00', - status: 'active', - }, - ], - R20260120001: [ - { - key: '1', - userId: 'U007', - realName: '孙客服', - phone: '13700000007', - createTime: '2026-01-20 16:00:00', - status: 'active', - }, - { - key: '2', - userId: 'U008', - realName: '周客服', - phone: '13700000008', - createTime: '2026-01-21 09:30:00', - status: 'active', - }, - { - key: '3', - userId: 'U009', - realName: '吴客服', - phone: '13700000009', - createTime: '2026-01-22 10:00:00', - status: 'active', - }, - { - key: '4', - userId: 'U010', - realName: '郑客服', - phone: '13700000010', - createTime: '2026-01-23 14:00:00', - status: 'active', - }, - ], -}; - -// ============================================================ -// 状态文本/颜色映射 -// ============================================================ -const STATUS_MAP: Record = { - active: { label: '正常', tone: 'success' }, - disabled: { label: '禁用', tone: 'danger' }, -}; +import { + getRoleList, + deleteRole, + type TournamentAdminRolePageVO, + type RoleListQueryParams, +} from '@/api/roles'; // ============================================================ // Model @@ -158,23 +18,17 @@ const STATUS_MAP: Record = { export function useRoleModel() { // ===== 筛选条件 ===== const filterForm = reactive({ - searchKeyword: '', + name: '', // 角色名称 }); - // 防抖 300ms - const { debouncedValue: debouncedKeyword } = useDebounce( - toRef(filterForm, 'searchKeyword') as Ref, - { delay: 300 }, - ); + const { debouncedValue: debouncedName } = useDebounce(toRef(filterForm, 'name') as Ref, { + delay: 300, + }); // ===== 表格状态 ===== const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState(SORTED_MOCK_DATA); - const [pagination, setPagination] = useState({ - current: 1, - pageSize: 10, - total: SORTED_MOCK_DATA.length, - }); + const [dataSource, setDataSource] = useState([]); + const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }); // ===== 弹窗状态 ===== const [formVisible, setFormVisible] = useState(false); @@ -199,34 +53,48 @@ export function useRoleModel() { { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 200 }, ]; - // ===== 计算属性 ===== - const hasFilter = computed(() => debouncedKeyword.value.trim() !== ''); + // ===== 构建 API 查询参数 ===== + const buildQueryParams = (): RoleListQueryParams => { + const params: RoleListQueryParams = { + page: String(pagination.value.current), + limit: String(pagination.value.pageSize), + }; + if (filterForm.name.trim()) params.name = filterForm.name.trim(); + return params; + }; + // ===== 计算属性 ===== + const hasFilter = computed(() => debouncedName.value.trim() !== ''); const isEdit = computed(() => editingRecord.value !== null); // ===== 方法 ===== - /** 查询 */ + /** 查询(节流 500ms) */ const handleSearch = useThrottleFn(async () => { setLoading(true); try { - console.log('搜索条件:', { keyword: debouncedKeyword.value }); - // TODO: 替换为真实 API 调用 - setDataSource(SORTED_MOCK_DATA); - setPagination({ ...pagination.value, total: SORTED_MOCK_DATA.length }); - message.success('查询成功'); + const queryParams = buildQueryParams(); + console.log('角色列表查询参数:', queryParams); + const res = await getRoleList(queryParams); + if (res.code === 200) { + setDataSource(res.data.list); + setPagination({ ...pagination.value, total: res.data.total }); + } else { + message.error(res.msg || '查询失败'); + } } catch (error: any) { - message.error(error.msg || '查询失败'); + console.error('角色列表查询失败:', error); } finally { setLoading(false); } }, 500); - /** 重置 */ + /** 重置(重置后自动查询) */ const handleReset = useThrottleFn(() => { - filterForm.searchKeyword = ''; - setPagination({ current: 1, pageSize: 10, total: SORTED_MOCK_DATA.length }); - setDataSource(SORTED_MOCK_DATA); + filterForm.name = ''; + setPagination({ current: 1, pageSize: 10, total: 0 }); + setDataSource([]); + setTimeout(() => handleSearch(), 350); }, 500); const handlePageChange = (page: number, pageSize: number) => { @@ -258,12 +126,12 @@ export function useRoleModel() { setFormSubmitting(true); try { console.log(isEdit.value ? '编辑角色' : '新增角色', formPayload); - // TODO: 替换为真实 API 调用 + // TODO: 替换为真实 API 调用(/sys/role/save、/sys/role/update) message.success(isEdit.value ? '编辑成功' : '新增成功'); handleCloseForm(); handleSearch(); - } catch (error: any) { - message.error(error.msg || '操作失败'); + } catch { + // 网络层已统一提示,不再重复 message.error } finally { setFormSubmitting(false); } @@ -273,12 +141,15 @@ export function useRoleModel() { const handleDelete = useThrottleFn(async (record: any) => { setLoading(true); try { - console.log('删除角色:', record.roleId); - // TODO: 替换为真实 API 调用 - setDataSource(dataSource.value.filter((item: any) => item.key !== record.key)); - message.success('删除成功'); - } catch (error: any) { - message.error(error.msg || '删除失败'); + const res = await deleteRole(record.roleId); + if (res.code === 200) { + setDataSource(dataSource.value.filter((item: any) => item.roleId !== record.roleId)); + message.success('删除成功'); + } else { + message.error(res.msg || '删除失败'); + } + } catch { + // 网络层已统一提示,不再重复 message.error } finally { setLoading(false); } @@ -296,15 +167,6 @@ export function useRoleModel() { setCurrentRole(null); }; - /** 根据 roleId 获取该角色下的用户列表 */ - const getRoleUsers = (roleId: string) => MOCK_USERS[roleId] || []; - - /** 状态 StatusTag 渲染 */ - const renderStatus = (status: string) => { - const info = STATUS_MAP[status] || { label: status || '-', tone: 'default' as const }; - return h(StatusTag, { label: info.label, tone: info.tone }); - }; - return { filterForm, loading, @@ -328,7 +190,5 @@ export function useRoleModel() { handleDelete, handleViewUsers, handleCloseUserList, - getRoleUsers, - renderStatus, }; } diff --git a/src/pages/system/users/components/UserFormModal.tsx b/src/pages/system/users/components/UserFormModal.tsx index d282a68..5eacad5 100644 --- a/src/pages/system/users/components/UserFormModal.tsx +++ b/src/pages/system/users/components/UserFormModal.tsx @@ -30,7 +30,7 @@ const getDefaultForm = () => ({ }); /** 新增用户默认密码 */ -const DEFAULT_USER_PASSWORD = 'A8888888'; +const DEFAULT_USER_PASSWORD = 'yp888888'; /** 获取新增时的表单初始值(预填默认密码) */ const getAddForm = () => ({