diff --git a/.env.development b/.env.development index 9cf069a..1a992dd 100644 --- a/.env.development +++ b/.env.development @@ -1,4 +1,3 @@ # 开发环境 VITE_PORT = 5173 -VITE_API_BASE_URL = http://localhost:3000 -VITE_API_PREFIX = /api +VITE_API_BASE_URL = http://localhost:3000/admin diff --git a/.env.production b/.env.production index dc54b4f..34d7624 100644 --- a/.env.production +++ b/.env.production @@ -1,3 +1,2 @@ # 生产环境 -VITE_API_BASE_URL = https://api.cpms.example.com -VITE_API_PREFIX = /api +VITE_API_BASE_URL = https://api.cpms.example.com/admin diff --git a/.env.test b/.env.test index b3621a6..7ca21d6 100644 --- a/.env.test +++ b/.env.test @@ -1,3 +1,2 @@ # 测试环境 -VITE_API_BASE_URL = https://test-api.cpms.example.com -VITE_API_PREFIX = /api +VITE_API_BASE_URL = https://test-api.cpms.example.com/admin diff --git a/src/api/banner/index.ts b/src/api/banner/index.ts new file mode 100644 index 0000000..ffbdef1 --- /dev/null +++ b/src/api/banner/index.ts @@ -0,0 +1,58 @@ +/** + * Banner 管理 API + */ +import { get, post } from '@/utils/request'; +import { USE_MOCK, MOCK_DELAY } from '@/config/mock'; +import { MOCK_BANNER_LIST } from '@/config/mock/bannerList'; +import type { + BannerListQueryParams, + BannerSaveParams, + BannerDelParams, + BannerActiveParams, + TournamentAdminBannerListVO, + ApiResult, +} from './types'; + +export type { + TournamentAdminBannerListVO, + BannerListQueryParams, + BannerSaveParams, + BannerDelParams, + BannerActiveParams, + ApiResult, +} from './types'; + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +/** GET /manager/banner/list — Banner 列表 */ +export async function getBannerList( + params: BannerListQueryParams, +): Promise> { + if (USE_MOCK) { + await delay(MOCK_DELAY); + return { code: 200, msg: 'success', data: MOCK_BANNER_LIST }; + } + return get('/manager/banner/list', params as Record); +} + +/** POST /manager/banner/del — 删除 Banner */ +export function delBanner(id: string): Promise>> { + return post('/manager/banner/del', { id }); +} + +/** POST /manager/banner/save — 新增 Banner */ +export function saveBanner(params: BannerSaveParams): Promise>> { + return post('/manager/banner/save', params); +} + +/** POST /manager/banner/update �� 编辑 Banner */ +export function updateBanner(params: BannerSaveParams): Promise>> { + return post('/manager/banner/update', params); +} + +/** POST /manager/banner/active — 启用/禁用 Banner */ +export function toggleBannerActive( + params: BannerActiveParams, +): Promise>> { + return post('/manager/banner/active', params); +} diff --git a/src/api/banner/types.ts b/src/api/banner/types.ts new file mode 100644 index 0000000..b9946de --- /dev/null +++ b/src/api/banner/types.ts @@ -0,0 +1,53 @@ +/** + * Banner 管理 — 类型定义 + */ + +/** Banner 列表项 */ +export interface TournamentAdminBannerListVO { + id: string; + img: string; + title: string; + type: string; + linkUrl: string; + area: string; + showTimeBegin: string; + showTimeEnd: string; + status: string; + createTime: string; +} + +/** Banner 列表查询参数 */ +export interface BannerListQueryParams { + title?: string; + status?: string; +} + +/** Banner 新增/编辑请求 */ +export interface BannerSaveParams { + id?: string; + img: string; + title: string; + type: string; + linkUrl: string; + area: string; + showTimeBegin: string; + showTimeEnd: string; + sort: string; +} + +/** Banner 删除请求 */ +export interface BannerDelParams { + id: string; +} + +/** Banner 启用/禁用请求 */ +export interface BannerActiveParams { + id: string; + status: string; +} + +export interface ApiResult { + code: number; + msg: string; + data: T; +} diff --git a/src/api/events/index.ts b/src/api/events/index.ts new file mode 100644 index 0000000..4c62196 --- /dev/null +++ b/src/api/events/index.ts @@ -0,0 +1,100 @@ +/** + * 赛事管理 API + * 注意:/admin 前缀已通过 VITE_API_BASE_URL 统一注入(见 .env.*),此文件中只写业务路径。 + */ +import { get, post } from '@/utils/request'; +import { USE_MOCK, MOCK_DELAY } from '@/config/mock'; +import { MOCK_EVENT_DETAIL } from '@/config/mock/eventDetail'; +import { buildMockEventListPage } from '@/config/mock/eventList'; +import { buildMockSignupListPage } from '@/config/mock/eventSignup'; +import type { + EventListQueryParams, + SignupListQueryParams, + ToggleOnlineParams, + ApiResult, + PageData, + TournamentAdminVO, + TournamentAdminInfoVO, + TournamentAdminInfoSignupVO, +} from './types'; + +export type { + TournamentAdminVO, + TournamentAdminInfoVO, + TournamentAdminInfoSignupVO, + InnerCategoryVO, + EventListQueryParams, + SignupListQueryParams, + ToggleOnlineParams, + PageData, + ApiResult, +} from './types'; + +/** 模拟延时 */ +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * 赛事列表 - 列表分页 + * GET /admin/manager/tm/page → /manager/tm/page + */ +export async function getEventList( + params: EventListQueryParams, +): 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: buildMockEventListPage(page, limit), + }; + } + return get('/manager/tm/page', params as Record); +} + +/** + * 赛事列表 - 赛事详情 + * GET /admin/manager/tm/info → /manager/tm/info + */ +export async function getEventDetail(id: string): Promise> { + if (USE_MOCK) { + await delay(MOCK_DELAY); + return { + code: 200, + msg: 'success', + data: { ...MOCK_EVENT_DETAIL }, + }; + } + return get('/manager/tm/info', { id }); +} + +/** + * 赛事列表 - 赛事详情 - 人员列表 + * GET /admin/manager/tm/info/signupPage → /manager/tm/info/signupPage + */ +export async function getSignupList( + params: SignupListQueryParams, +): Promise>> { + if (USE_MOCK) { + await delay(MOCK_DELAY); + return { + code: 200, + msg: 'success', + data: buildMockSignupListPage(params), + }; + } + return get('/manager/tm/info/signupPage', params as Record); +} + +/** + * 赛事上架 / 下架 + * POST /admin/manager/tm/online → /manager/tm/online + * @param params.id 赛事ID + * @param params.online 0=下架,1=上架 + */ +export function toggleEventOnline( + params: ToggleOnlineParams, +): Promise>> { + return post('/manager/tm/online', params); +} diff --git a/src/api/events/types.ts b/src/api/events/types.ts new file mode 100644 index 0000000..0a562fc --- /dev/null +++ b/src/api/events/types.ts @@ -0,0 +1,199 @@ +/** + * 赛事管理 — 类型定义 + * 对应 OpenAPI: /admin/manager/tm/page + */ + +/** 赛事列表项 */ +export interface TournamentAdminVO { + /** 赛事ID */ + id: string; + /** 赛事名称 */ + name: string; + /** 赛事开始时间 */ + startTimeBegin: string; + /** 赛事结束时间 */ + startTimeEnd: string; + /** 赛事省市区,场地区划名称 */ + venueArea: string; + /** 赛事地点名称 */ + venue: string; + /** 是否公开;0=非公开,1=公开 */ + isPublic: string; + /** 是否要求提供证件图片;0=无需,1=需要 */ + isRequireIdCardImg: string; + /** 是否要求提供证件号码;0=无需,1=需要 */ + isRequireIdCardNo: string; + /** 创建人昵称 */ + createNickname: string; + /** 创建人手机号 */ + createPhone: string; + /** 创建时间 */ + createTime: string; + /** + * 状态 + * 优先级:del_flag==1 → 3(已删除);online==0 → 4(已下架);否则取 status + * 0=报名中,1=进行中,2=已结束,3=已删除,4=已下架 + */ + status: string; + /** 报名人数 */ + signupCount: string; + /** 退赛人数 */ + dropoutCount: string; + /** 赛事规程 */ + rulesDescr: string; + /** 赛事规程图片 */ + rulesDescrImg: string; + /** 赛事规程图片数组 */ + rulesDescrImgList: string[]; +} + +/** 赛事列表查询参数 */ +export interface EventListQueryParams { + page?: string; + limit?: string; + /** 创建时间开始 yyyy-MM-dd */ + createDate?: string; + /** 创建时间结束 yyyy-MM-dd */ + createDateEnd?: string; + /** 赛事时间开始 yyyy-MM-dd */ + startDate?: string; + /** 赛事时间结束 yyyy-MM-dd */ + startDateEnd?: string; + /** 赛事名称 */ + name?: string; + /** 创建人昵称或手机号 */ + createName?: string; + /** + * 赛事状态 + * 不传=全部、0=报名中,1=进行中,2=已结束,3=已删除,4=已下架 + */ + status?: string; + /** 市级别城市名称 */ + areaName?: string; + /** 是否公开;0=非公开,1=公开 */ + isPublic?: string; + /** 是否要求提供证件图片;0=无需,1=需要 */ + idCardImg?: string; + /** 是否要求提供证件号码;0=无需,1=需要 */ + idCardNo?: string; +} + +/** 分页数据 */ +export interface PageData { + total: number; + list: T[]; +} + +/** 统一响应包裹 */ +export interface ApiResult { + /** 编码:200表示成功,其他值表示失败 */ + code: number; + /** 消息内容 */ + msg: string; + /** 响应数据 */ + data: T; +} + +// ============================================================ +// 赛事详情(对应 /manager/tm/info) +// ============================================================ + +/** 组别信息 */ +export interface InnerCategoryVO { + /** 组别ID */ + id: string; + /** 组别名称 */ + name: string; + /** 组别类型;1=单打,2=双打 */ + type: string; +} + +/** 赛事详情 */ +export interface TournamentAdminInfoVO { + /** 赛事名称 */ + name: string; + /** 赛事开始时间 */ + startTimeBegin: string; + /** 赛事结束时间 */ + startTimeEnd: string; + /** 赛事说明 */ + descr: string; + /** 报名开始时间 */ + signupTimeBegin: string; + /** 报名结束时间 */ + signupTimeEnd: string; + /** 赛事省市区 */ + venueArea: string; + /** 赛事地点名称 */ + venue: string; + /** 场地号 */ + venueNo: string; + /** 是否公开;0=非公开,1=公开 */ + isPublic: string; + /** 是否要求提供证件图片;0=无需,1=需要 */ + isRequireIdCardImg: string; + /** 是否要求提供证件号码;0=无需,1=需要 */ + isRequireIdCardNo: string; + /** 创建时间 */ + createTime: string; + /** 创建人昵称 */ + createNickname: string; + /** 报名人数 */ + signupCount: string; + /** 退赛人数 */ + dropoutCount: string; + /** 允许报名最大人数 */ + dtotalLimit: string; + /** 总对阵数 */ + matchTotalCount: string; + /** 已完成对阵数 */ + matchCount: string; + /** 赛事封面url */ + img: string; + /** 活动公告 */ + notice: string; + /** 组别列表,创建时间倒序 */ + categoryList: InnerCategoryVO[]; + /** 分组名称列表,格式 "小组名称(淘汰赛或循环赛)" */ + groupInfoList: string[]; +} + +// ============================================================ +// 人员列表(对应 /manager/tm/info/signupPage) +// ============================================================ + +/** 报名人员 */ +export interface TournamentAdminInfoSignupVO { + /** 选手名称 */ + realName: string; + /** 选手性别;1=男,2=女 */ + gender: string; + /** 选手手机号 */ + phone: string; + /** 选手身份证 */ + idCard: string; + /** 选手身份证图片 */ + idCardImg: string; + /** 选手身份证图片列表 */ + idCardImgList: string[]; +} + +/** 人员列表查询参数 */ +export interface SignupListQueryParams { + page: string; + limit: string; + /** 组别ID(必填) */ + categoryId: string; +} + +// ============================================================ +// 上下架(对应 /manager/tm/online) +// ============================================================ + +/** 上下架请求参数 */ +export interface ToggleOnlineParams { + /** 赛事ID */ + id: string; + /** 0=下架,1=上架 */ + online: string; +} diff --git a/src/api/orders/index.ts b/src/api/orders/index.ts new file mode 100644 index 0000000..4a38ef5 --- /dev/null +++ b/src/api/orders/index.ts @@ -0,0 +1,106 @@ +/** + * 订单管理 API + * 注意:/admin 前缀已通过 VITE_API_BASE_URL 统一注入(见 .env.*),此文件中只写业务路径。 + */ +import { get, post } from '@/utils/request'; +import { USE_MOCK, MOCK_DELAY } from '@/config/mock'; +import { buildMockOrderListPage } from '@/config/mock/orderList'; +import { MOCK_ORDER_DETAIL } from '@/config/mock/orderDetail'; +import { + MOCK_ORDER_PLAYER_LIST, + buildMockOrderPlayerListPage, +} from '@/config/mock/orderPlayerList'; +import type { + OrderListQueryParams, + OrderDetailQueryParams, + OrderPlayerListQueryParams, + RetryRefundParams, + TournamentAdminOrderPageVO, + TournamentAdminOrderInfoVO, + TournamentAdminOrderInfoPageVO, + PageData, + ApiResult, +} from './types'; + +export type { + TournamentAdminOrderPageVO, + TournamentAdminOrderInfoVO, + TournamentAdminOrderInfoPageVO, + InnerRefundInfo, + OrderListQueryParams, + OrderDetailQueryParams, + OrderPlayerListQueryParams, + RetryRefundParams, + PageData, + ApiResult, +} from './types'; + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * 订单管理 - 订单分页 + * GET /admin/manager/order/page → /manager/order/page + */ +export async function getOrderList( + params: OrderListQueryParams, +): 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: buildMockOrderListPage(page, limit), + }; + } + return get('/manager/order/page', params as Record); +} + +/** + * 订单管理 - 订单详情 + * GET /admin/manager/order/info → /manager/order/info + */ +export async function getOrderDetail( + params: OrderDetailQueryParams, +): Promise> { + if (USE_MOCK) { + await delay(MOCK_DELAY); + return { + code: 200, + msg: 'success', + data: { ...MOCK_ORDER_DETAIL }, + }; + } + return get('/manager/order/info', params as Record); +} + +/** + * 订单管理 - 订单详情 - 选手列表 + * GET /admin/manager/order/info/page → /manager/order/info/page + */ +export async function getOrderPlayerList( + params: OrderPlayerListQueryParams, +): 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: buildMockOrderPlayerListPage(page, limit), + }; + } + return get('/manager/order/info/page', params as Record); +} + +/** + * 订单管理 - 重新退款 + * POST /admin/manager/order/retryRefund → /manager/order/retryRefund + */ +export function retryRefundOrder( + params: RetryRefundParams, +): Promise>> { + return post('/manager/order/retryRefund', params); +} diff --git a/src/api/orders/types.ts b/src/api/orders/types.ts new file mode 100644 index 0000000..540f3db --- /dev/null +++ b/src/api/orders/types.ts @@ -0,0 +1,185 @@ +/** + * 订单管理 — 类型定义 + * 对应 OpenAPI: /admin/manager/order/page + */ + +/** 订单列表项 */ +export interface TournamentAdminOrderPageVO { + /** 订单编号 */ + orderNo: string; + /** 选手ID */ + uniqueId: number; + /** 赛事名称 */ + name: string; + /** 选手名称 */ + realName: string; + /** 手机号 */ + phone: string; + /** 报名比赛组别名称 */ + categoryName: string; + /** 报名人名称 */ + creatorName: string; + /** 报名人手机号 */ + creatorPhone: string; + /** 报名人一共报名人数 */ + creatorSignupCount: number; + /** 订单金额;0.00格式 */ + creatorSignupFee: string; + /** 优惠金额;0.00格式 */ + creatorSignupFeeDiscount: string; + /** 实付金额;0.00格式 */ + creatorSignupFeeActual: string; + /** 退款金额;0.00格式 */ + creatorSignupFeeRefund: string; + /** 订单时间;yyyy.MM.dd HH:mm:ss */ + orderTime: string; + /** 支付时间;yyyy.MM.dd HH:mm:ss */ + payTime: string; + /** 订单状态;1=已支付,2=部分退款,3=全部退款,4=已关闭,-1=其他 */ + orderStatus: number; + /** 退款状态;1=无退款,2=退款成功,3=退款失败,-1=其他 */ + refundStatus: number; +} + +/** 订单列表查询参数 */ +export interface OrderListQueryParams { + page?: string; + limit?: string; + /** 订单时间开始 yyyy-MM-dd */ + orderDate?: string; + /** 订单时间结束 yyyy-MM-dd */ + orderDateEnd?: string; + /** 订单编号 */ + orderNo?: string; + /** 赛事名称 */ + tName?: string; + /** 选手名称 */ + sName?: string; + /** + * 订单状态 + * 不传=全部、1=支付成功,2=部分退款,3=全部退款,4=超时关闭 + */ + status?: string; + /** + * 退款状态 + * 不传=全部、0=退款失败,1=退款成功 + */ + refundStatus?: string; +} + +/** 分页数据 */ +export interface PageData { + total: number; + list: T[]; +} + +/** 统一响应包裹 */ +export interface ApiResult { + code: number; + msg: string; + data: T; +} + +// ============================================================ +// 订单详情(对应 /manager/order/info) +// ============================================================ + +/** 退款记录 */ +export interface InnerRefundInfo { + /** 已退金额;0.00格式 */ + refundedAmount: string; + /** 总退款次数(接口实际返回 string) */ + totalRefundCount: string; + /** 退款金额;0.00格式 */ + refundAmount: string; + /** 是否全部退款;0=否,1=是(接口实际返回 string) */ + isFullRefund: string; + /** 退款方式 */ + refundMethod: string; + /** 退款申请时间 */ + refundApplyTime: string; + /** 第三方退款单号 */ + refundOutTradeNo: string; + /** 退款完成时间 */ + refundTime: string; +} + +/** 订单详情 */ +export interface TournamentAdminOrderInfoVO { + /** 赛事名称 */ + tournamentName: string; + /** 订单编号 */ + orderNumber: string; + /** 订单时间 */ + orderTime: string; + /** 支付时间 */ + payTime: string; + /** 订单金额;0.00格式 */ + orderAmount: string; + /** 优惠金额;0.00格式 */ + discountAmount: string; + /** 实付金额;0.00格式 */ + payAmount: string; + /** 退款金额;0.00格式 */ + refundAmount: string; + /** 退款信息list */ + refundInfoList: InnerRefundInfo[]; +} + +/** 订单详情查询参数 */ +export interface OrderDetailQueryParams { + /** 订单编号 */ + orderNo: string; + /** 选手ID */ + uniqueId: string; +} + +// ============================================================ +// 订单详情 — 选手列表(对应 /manager/order/info/page) +// ============================================================ + +/** 订单下选手信息 */ +export interface TournamentAdminOrderInfoPageVO { + /** 姓名 */ + realName: string; + /** 性别;1=男,2=女(接口实际返回 string) */ + gender: string; + /** 手机号 */ + phone: string; + /** 证件号 */ + idCard: string; + /** 证件号图片 */ + idCardImg: string; + /** 选手身份证图片列表 */ + idCardImgList: string[]; + /** 选手组别名称 */ + categoryName: string; + /** 订单金额;0.00 */ + orderAmount: string; + /** 优惠金额;0.00 */ + discountAmount: string; + /** 实付金额;0.00 */ + payAmount: string; + /** 退款金额;0.00 */ + refundAmount: string; +} + +/** 选手列表查询参数 */ +export interface OrderPlayerListQueryParams { + /** 订单编号 */ + orderNo: string; + /** 选手ID */ + uniqueId: string; + page: string; + limit: string; +} + +// ============================================================ +// 重新退款(对应 /manager/order/retryRefund) +// ============================================================ + +/** 重新退款请求参数 */ +export interface RetryRefundParams { + /** 支付单号 */ + payOrderNo: string; +} 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/api/users/index.ts b/src/api/users/index.ts new file mode 100644 index 0000000..d76ac08 --- /dev/null +++ b/src/api/users/index.ts @@ -0,0 +1,68 @@ +/** + * 用户管理 API + */ +import { get, post } from '@/utils/request'; +import { USE_MOCK, MOCK_DELAY } from '@/config/mock'; +import { buildMockUserListPage } from '@/config/mock/userList'; +import type { + UserListQueryParams, + UserSaveParams, + UserUpdateParams, + UserActiveParams, + UserUpdatePwdParams, + TournamentAdminUserVO, + PageData, + ApiResult, +} from './types'; + +export type { + TournamentAdminUserVO, + UserListQueryParams, + UserSaveParams, + UserUpdateParams, + UserActiveParams, + UserUpdatePwdParams, + PageData, + ApiResult, +} from './types'; + +const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +/** GET /sys/user/page — 用户分页 */ +export async function getUserList( + params: UserListQueryParams, +): 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: buildMockUserListPage(page, limit) }; + } + return get('/sys/user/page', params as Record); +} + +/** POST /sys/user/save — 新增用户 */ +export function saveUser(params: UserSaveParams): Promise>> { + if (USE_MOCK) return delay(MOCK_DELAY).then(() => ({ code: 200, msg: 'success', data: {} })); + return post('/sys/user/save', params); +} + +/** POST /sys/user/update — 更新用户 */ +export function updateUser(params: UserUpdateParams): Promise>> { + if (USE_MOCK) return delay(MOCK_DELAY).then(() => ({ code: 200, msg: 'success', data: {} })); + return post('/sys/user/update', params); +} + +/** POST /sys/user/active — 启用/禁用(直接调接口) */ +export function toggleUserActive( + params: UserActiveParams, +): Promise>> { + return post('/sys/user/active', params); +} + +/** POST /sys/user/updatepwd — 修改密码(直接调接口) */ +export function updateUserPwd( + params: UserUpdatePwdParams, +): Promise>> { + return post('/sys/user/updatepwd', params); +} diff --git a/src/api/users/types.ts b/src/api/users/types.ts new file mode 100644 index 0000000..0dbcf4a --- /dev/null +++ b/src/api/users/types.ts @@ -0,0 +1,71 @@ +/** + * 用户管理 — 类型定义 + * 对应 OpenAPI: /admin/sys/user/* + */ + +export interface TournamentAdminUserVO { + id: string; + /** 用户姓名 */ + realName: string; + phone: string; + /** 角色名称 */ + roleName: string; + /** 角色ID(编辑时需要回显,列表可能不返回) */ + roleId?: string; + /** 状态;0=停用、1=正常(接口实际返回 string) */ + status: string; + createTime: string; +} + +export interface UserListQueryParams { + page?: string; + limit?: string; + /** 用户姓名或手机号 */ + text?: string; + /** 角色ID */ + roleId?: string; + /** 状态;不传=全部、0=停用、1=正常 */ + status?: string; +} + +/** 新增用户 — POST /admin/sys/user/save */ +export interface UserSaveParams { + realName: string; + phone: string; + roleId: string; + password: string; + confirmPassword: string; +} + +/** 更新用户 — POST /admin/sys/user/update */ +export interface UserUpdateParams { + id: string; + realName: string; + phone: string; + roleId: string; + password?: string; + confirmPassword?: string; +} + +/** 启用/禁用 — POST /admin/sys/user/active */ +export interface UserActiveParams { + userId: string; + /** 0=禁用、1=启用 */ + status: string; +} + +/** 修改密码 — POST /admin/sys/user/updatepwd */ +export interface UserUpdatePwdParams { + password: string; + confirmPassword: string; +} + +export interface PageData { + total: number; + list: T[]; +} +export interface ApiResult { + code: number; + msg: string; + data: T; +} diff --git a/src/assets/styles/index.less b/src/assets/styles/index.less index 2bf63a1..e5c92db 100644 --- a/src/assets/styles/index.less +++ b/src/assets/styles/index.less @@ -123,8 +123,8 @@ a { height: 100%; min-height: 0; transition: - opacity 0.22s cubic-bezier(0.4, 0, 0.2, 1), - transform 0.22s cubic-bezier(0.4, 0, 0.2, 1); + opacity 0.24s cubic-bezier(0.4, 0, 0.2, 1), + transform 0.24s cubic-bezier(0.4, 0, 0.2, 1); } .page-fade-slide-enter-from { diff --git a/src/config/mock.ts b/src/config/mock.ts new file mode 100644 index 0000000..c2a4655 --- /dev/null +++ b/src/config/mock.ts @@ -0,0 +1,12 @@ +/** + * 全局假数据开关 + * + * true = 所有 API 使用假数据(模拟真实网络延时 2s 后返回假数据) + * false = 走真实后端接口 + * + * 对接后端接口后将此值改为 false 即可切换。 + */ +export const USE_MOCK = true; + +/** 模拟网络延时(毫秒) */ +export const MOCK_DELAY = 850; diff --git a/src/config/mock/bannerList.ts b/src/config/mock/bannerList.ts new file mode 100644 index 0000000..ea7a532 --- /dev/null +++ b/src/config/mock/bannerList.ts @@ -0,0 +1,169 @@ +/** + * Banner 列表页假数据 + * + * 使用页面:`src/pages/events/banner/index.tsx` + * 对应接口:`GET /manager/banner/list` + * 类型:`TournamentAdminBannerListVO[]` + * + * 注意:类型内联定义,避免循环引用。 + */ + +export interface MockBannerVO { + id: string; + img: string; + title: string; + type: string; + linkUrl: string; + area: string; + showTimeBegin: string; + showTimeEnd: string; + status: string; + createTime: string; +} + +export const MOCK_BANNER_LIST: MockBannerVO[] = [ + { + id: '1', + img: 'https://picsum.photos/seed/banner1/80/44', + title: '2026全国青少年羽毛球锦标赛火热报名中', + type: '1', + linkUrl: '', + area: '广东省深圳市', + showTimeBegin: '2026-08-01 00:00', + showTimeEnd: '2026-08-31 23:59', + status: '1', + createTime: '2026-07-20 08:00:00', + }, + { + id: '2', + img: 'https://picsum.photos/seed/banner2/80/44', + title: '马拉松城市联赛限时优惠', + type: '2', + linkUrl: 'https://example.com/marathon', + area: '浙江省杭州市', + showTimeBegin: '2026-08-15 00:00', + showTimeEnd: '2026-09-15 23:59', + status: '1', + createTime: '2026-07-19 11:30:00', + }, + { + id: '3', + img: 'https://picsum.photos/seed/banner3/80/44', + title: '健身运动会专属福利', + type: '1', + linkUrl: '', + area: '北京市', + showTimeBegin: '2026-07-01 00:00', + showTimeEnd: '2026-07-30 23:59', + status: '1', + createTime: '2026-07-18 14:20:00', + }, + { + id: '4', + img: 'https://picsum.photos/seed/banner4/80/44', + title: '平台品牌宣传活动', + type: '2', + linkUrl: 'https://example.com/promo', + area: '上海市', + showTimeBegin: '2026-06-01 00:00', + showTimeEnd: '2026-06-30 23:59', + status: '0', + createTime: '2026-07-17 18:05:00', + }, + { + id: '5', + img: 'https://picsum.photos/seed/banner5/80/44', + title: '2026全国青少年羽毛球锦标赛火热报名中', + type: '1', + linkUrl: '', + area: '广东省深圳市', + showTimeBegin: '2026-09-01 00:00', + showTimeEnd: '2026-09-30 23:59', + status: '1', + createTime: '2026-07-16 21:15:00', + }, + { + id: '6', + img: 'https://picsum.photos/seed/banner6/80/44', + title: '马拉松城市联赛限时优惠', + type: '3', + linkUrl: '/pages/activity/detail', + area: '浙江省杭州市', + showTimeBegin: '2026-10-01 00:00', + showTimeEnd: '2026-10-31 23:59', + status: '1', + createTime: '2026-07-15 00:30:00', + }, + { + id: '7', + img: 'https://picsum.photos/seed/banner7/80/44', + title: '全民健身日特别活动', + type: '1', + linkUrl: '', + area: '北京市', + showTimeBegin: '2026-08-08 00:00', + showTimeEnd: '2026-08-08 23:59', + status: '1', + createTime: '2026-07-14 03:50:00', + }, + { + id: '8', + img: 'https://picsum.photos/seed/banner8/80/44', + title: '新春运动季大促销', + type: '2', + linkUrl: 'https://example.com/spring', + area: '上海市', + showTimeBegin: '2026-01-15 00:00', + showTimeEnd: '2026-02-15 23:59', + status: '0', + createTime: '2026-07-13 07:05:00', + }, + { + id: '9', + img: 'https://picsum.photos/seed/banner9/80/44', + title: '2026全国青少年羽毛球锦标赛火热报名中', + type: '1', + linkUrl: '', + area: '广东省深圳市', + showTimeBegin: '2026-08-01 00:00', + showTimeEnd: '2026-08-31 23:59', + status: '1', + createTime: '2026-07-12 10:25:00', + }, + { + id: '10', + img: 'https://picsum.photos/seed/banner10/80/44', + title: '平台品牌宣传活动', + type: '2', + linkUrl: 'https://example.com/brand', + area: '浙江省杭州市', + showTimeBegin: '2026-11-01 00:00', + showTimeEnd: '2026-11-30 23:59', + status: '1', + createTime: '2026-07-11 13:40:00', + }, + { + id: '11', + img: 'https://picsum.photos/seed/banner11/80/44', + title: '秋季健身挑战赛', + type: '1', + linkUrl: '', + area: '北京市', + showTimeBegin: '2026-09-01 00:00', + showTimeEnd: '2026-09-30 23:59', + status: '1', + createTime: '2026-07-10 17:00:00', + }, + { + id: '12', + img: 'https://picsum.photos/seed/banner12/80/44', + title: '双十一运动装备大促', + type: '2', + linkUrl: 'https://example.com/double11', + area: '上海市', + showTimeBegin: '2026-11-01 00:00', + showTimeEnd: '2026-11-11 23:59', + status: '1', + createTime: '2026-07-09 20:15:00', + }, +]; diff --git a/src/config/mock/eventDetail.ts b/src/config/mock/eventDetail.ts new file mode 100644 index 0000000..5dd51c8 --- /dev/null +++ b/src/config/mock/eventDetail.ts @@ -0,0 +1,39 @@ +/** + * 赛事详情弹窗假数据 + * + * 使用页面:`src/pages/events/list/components/EventDetailModal.tsx` + * 对应接口:`GET /manager/tm/info` + * 类型:`TournamentAdminInfoVO` + */ +import type { TournamentAdminInfoVO } from '@/api/events/types'; + +export const MOCK_EVENT_DETAIL: TournamentAdminInfoVO = { + name: '第二届全国地学羽毛球邀请赛', + startTimeBegin: '2026-05-26 08:00', + startTimeEnd: '2026-05-31 22:00', + descr: + '赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍。', + signupTimeBegin: '2026-05-24 08:00', + signupTimeEnd: '2026-05-26 22:00', + venueArea: '北京市 北京市 东城区', + venue: '李宁体育馆', + venueNo: '李宁体育馆', + isPublic: '1', + isRequireIdCardImg: '0', + isRequireIdCardNo: '0', + createTime: '2026-05-26 08:00', + createNickname: '张三', + signupCount: '100', + dropoutCount: '1', + dtotalLimit: '1000', + matchTotalCount: '50', + matchCount: '50', + img: 'https://picsum.photos/seed/eventcover/600/300', + notice: + '赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告。', + categoryList: [ + { id: '1', name: '男子单打', type: '1' }, + { id: '2', name: '女子双打', type: '2' }, + ], + groupInfoList: ['男子小组1(循环赛)', '女子小组1(淘汰赛)'], +}; diff --git a/src/config/mock/eventList.ts b/src/config/mock/eventList.ts new file mode 100644 index 0000000..55ac581 --- /dev/null +++ b/src/config/mock/eventList.ts @@ -0,0 +1,268 @@ +/** + * 赛事列表页假数据 + * + * 使用页面:`src/pages/events/list/index.tsx` + * 对应接口:`GET /manager/tm/page` + * 类型:`TournamentAdminVO[]` + */ +import type { TournamentAdminVO, PageData } from '@/api/events/types'; + +export const MOCK_EVENT_LIST: TournamentAdminVO[] = [ + { + id: 'EV202607010001', + name: '2026全国青少年羽毛球锦标赛暨体育文化交流大会', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '1', + isRequireIdCardImg: '1', + isRequireIdCardNo: '1', + createNickname: '张运营', + createPhone: '13800138001', + createTime: '2026-07-10 08:11:07', + status: '0', + signupCount: '128', + dropoutCount: '5', + rulesDescr: '一、赛事目的:推动羽毛球运动发展。\n二、参赛资格:身体健康、年龄符合要求。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: [ + 'https://picsum.photos/seed/rule1/300/200', + 'https://picsum.photos/seed/rule2/300/200', + ], + }, + { + id: 'EV202607010002', + name: '国际马拉松城市联赛', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '0', + isRequireIdCardImg: '0', + isRequireIdCardNo: '0', + createNickname: '李管理', + createPhone: '13900139002', + createTime: '2026-07-09 11:28:20', + status: '1', + signupCount: '256', + dropoutCount: '12', + rulesDescr: '一、赛事目的:推动马拉松运动发展。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: ['https://picsum.photos/seed/rule1/300/200'], + }, + { + id: 'EV202607010003', + name: '全民健身运动会', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '1', + isRequireIdCardImg: '1', + isRequireIdCardNo: '1', + createNickname: '王策划', + createPhone: '13700137003', + createTime: '2026-07-08 14:45:33', + status: '2', + signupCount: '512', + dropoutCount: '8', + rulesDescr: '一、赛事目的:促进全民健身。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: ['https://picsum.photos/seed/rule1/300/200'], + }, + { + id: 'EV202607010004', + name: '2026全国青少年羽毛球锦标赛暨体育文化交流大会', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '0', + isRequireIdCardImg: '0', + isRequireIdCardNo: '0', + createNickname: '赵赛事', + createPhone: '13600136004', + createTime: '2026-07-07 18:02:46', + status: '3', + signupCount: '64', + dropoutCount: '3', + rulesDescr: '一、赛事目的:交流体育文化。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: [], + }, + { + id: 'EV202607010005', + name: '国际马拉松城市联赛', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '1', + isRequireIdCardImg: '1', + isRequireIdCardNo: '1', + createNickname: '张运营', + createPhone: '13800138001', + createTime: '2026-07-06 21:19:59', + status: '4', + signupCount: '0', + dropoutCount: '0', + rulesDescr: '一、赛事目的:推动城市体育交流。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: ['https://picsum.photos/seed/rule1/300/200'], + }, + { + id: 'EV202607010006', + name: '全民健身运动会', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '0', + isRequireIdCardImg: '0', + isRequireIdCardNo: '0', + createNickname: '李管理', + createPhone: '13900139002', + createTime: '2026-07-05 00:36:12', + status: '0', + signupCount: '128', + dropoutCount: '5', + rulesDescr: '一、赛事目的:增强全民体质。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: [], + }, + { + id: 'EV202607010007', + name: '2026全国青少年羽毛球锦标赛暨体育文化交流大会', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '1', + isRequireIdCardImg: '1', + isRequireIdCardNo: '1', + createNickname: '王策划', + createPhone: '13700137003', + createTime: '2026-07-04 03:53:25', + status: '1', + signupCount: '256', + dropoutCount: '12', + rulesDescr: '一、赛事目的:选拔青少年人才。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: [ + 'https://picsum.photos/seed/rule1/300/200', + 'https://picsum.photos/seed/rule2/300/200', + ], + }, + { + id: 'EV202607010008', + name: '国际马拉松城市联赛', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '0', + isRequireIdCardImg: '0', + isRequireIdCardNo: '0', + createNickname: '赵赛事', + createPhone: '13600136004', + createTime: '2026-07-03 07:10:38', + status: '2', + signupCount: '512', + dropoutCount: '8', + rulesDescr: '一、赛事目的:连接城市与运动。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: [], + }, + { + id: 'EV202607010009', + name: '全民健身运动会', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '1', + isRequireIdCardImg: '1', + isRequireIdCardNo: '1', + createNickname: '张运营', + createPhone: '13800138001', + createTime: '2026-07-02 10:27:51', + status: '3', + signupCount: '64', + dropoutCount: '3', + rulesDescr: '一、赛事目的:全民参与、共享健康。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: ['https://picsum.photos/seed/rule1/300/200'], + }, + { + id: 'EV202607010010', + name: '2026全国青少年羽毛球锦标赛暨体育文化交流大会', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '0', + isRequireIdCardImg: '0', + isRequireIdCardNo: '0', + createNickname: '李管理', + createPhone: '13900139002', + createTime: '2026-07-01 13:44:04', + status: '4', + signupCount: '0', + dropoutCount: '0', + rulesDescr: '一、赛事目的:培养后备力量。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: [], + }, + { + id: 'EV202607010011', + name: '国际马拉松城市联赛', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '1', + isRequireIdCardImg: '1', + isRequireIdCardNo: '1', + createNickname: '王策划', + createPhone: '13700137003', + createTime: '2026-06-30 17:01:17', + status: '0', + signupCount: '128', + dropoutCount: '5', + rulesDescr: '一、赛事目的:推广城市品牌。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: [ + 'https://picsum.photos/seed/rule1/300/200', + 'https://picsum.photos/seed/rule2/300/200', + 'https://picsum.photos/seed/rule3/300/200', + ], + }, + { + id: 'EV202607010012', + name: '全民健身运动会', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '0', + isRequireIdCardImg: '0', + isRequireIdCardNo: '0', + createNickname: '赵赛事', + createPhone: '13600136004', + createTime: '2026-06-29 20:18:30', + status: '1', + signupCount: '256', + dropoutCount: '12', + rulesDescr: '一、赛事目的:丰富群众体育生活。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: [], + }, +]; + +/** 构建分页响应 */ +export function buildMockEventListPage(page: number, limit: number): PageData { + const start = (page - 1) * limit; + const list = MOCK_EVENT_LIST.slice(start, start + limit); + return { total: MOCK_EVENT_LIST.length, list }; +} diff --git a/src/config/mock/eventSignup.ts b/src/config/mock/eventSignup.ts new file mode 100644 index 0000000..20ffce5 --- /dev/null +++ b/src/config/mock/eventSignup.ts @@ -0,0 +1,100 @@ +/** + * 赛事详情 — 人员列表假数据 + * + * 使用页面:`src/pages/events/list/components/EventDetailModal.tsx` + * 对应接口:`GET /manager/tm/info/signupPage` + * 类型:`TournamentAdminInfoSignupVO[]` + */ +import type { + TournamentAdminInfoSignupVO, + PageData, + SignupListQueryParams, +} from '@/api/events/types'; + +export const MOCK_SIGNUP_LIST: TournamentAdminInfoSignupVO[] = [ + { + realName: '张三', + gender: '1', + phone: '13800138001', + idCard: '110101199001011234', + idCardImg: 'https://picsum.photos/seed/idcard1/96/64', + idCardImgList: [ + 'https://picsum.photos/seed/idcard1a/96/64', + 'https://picsum.photos/seed/idcard1b/96/64', + ], + }, + { + realName: '李四', + gender: '1', + phone: '13900139002', + idCard: '110101199002022345', + idCardImg: 'https://picsum.photos/seed/idcard2/96/64', + idCardImgList: [ + 'https://picsum.photos/seed/idcard2a/96/64', + 'https://picsum.photos/seed/idcard2b/96/64', + ], + }, + { + realName: '王芳', + gender: '2', + phone: '13700137003', + idCard: '110101199103033456', + idCardImg: '', + idCardImgList: [], + }, + { + realName: '赵六', + gender: '1', + phone: '13600136004', + idCard: '110101199204044567', + idCardImg: 'https://picsum.photos/seed/idcard4/96/64', + idCardImgList: ['https://picsum.photos/seed/idcard4a/96/64'], + }, + { + realName: '孙七', + gender: '2', + phone: '13500135005', + idCard: '110101199305055678', + idCardImg: '', + idCardImgList: [], + }, + { + realName: '周八', + gender: '1', + phone: '13400134006', + idCard: '110101199406066789', + idCardImg: 'https://picsum.photos/seed/idcard6/96/64', + idCardImgList: [ + 'https://picsum.photos/seed/idcard6a/96/64', + 'https://picsum.photos/seed/idcard6b/96/64', + ], + }, + { + realName: '吴九', + gender: '2', + phone: '13300133007', + idCard: '110101199507077890', + idCardImg: '', + idCardImgList: [], + }, + { + realName: '郑十', + gender: '1', + phone: '13200132008', + idCard: '110101199608088901', + idCardImg: 'https://picsum.photos/seed/idcard8/96/64', + idCardImgList: ['https://picsum.photos/seed/idcard8a/96/64'], + }, +]; + +/** 构建分页响应 */ +export function buildMockSignupListPage( + params: SignupListQueryParams, +): PageData { + // mock 模式下忽略 categoryId 过滤,直接分页返回 + const page = Number(params.page) || 1; + const limit = Number(params.limit) || 10; + const start = (page - 1) * limit; + const list = MOCK_SIGNUP_LIST.slice(start, start + limit); + return { total: MOCK_SIGNUP_LIST.length, list }; +} diff --git a/src/config/mock/operationLogs.ts b/src/config/mock/operationLogs.ts new file mode 100644 index 0000000..8215b61 --- /dev/null +++ b/src/config/mock/operationLogs.ts @@ -0,0 +1,169 @@ +/** + * 操作日志假数据 + * 对应接口:GET /admin/sys/operation/page + * + * 支持按 type、nickname、dateBegin/dateEnd 前端筛选, + * 模拟后端分页行为。 + */ +import type { SysOperationLogVO, OperationLogQueryParams, PageData } from '@/api/logs'; + +const MOCK_LOGS: SysOperationLogVO[] = [ + { + type: 1, + nickname: '张三', + createDate: '2026-07-30 08:00:00', + content: '下架赛事"第二十一届贝格林羽毛球大赛"', + }, + { + type: 2, + nickname: '张三', + createDate: '2026-07-30 09:30:00', + content: '上架赛事"第二十一届贝格林羽毛球大赛"', + }, + { + type: 3, + nickname: '李四', + createDate: '2026-07-29 10:12:00', + content: '禁用用户"王五(1003)"', + }, + { + type: 4, + nickname: '李四', + createDate: '2026-07-29 10:20:00', + content: '启用用户"王五(1003)"', + }, + { + type: 1, + nickname: '赵六', + createDate: '2026-07-28 14:00:00', + content: '下架赛事"春季马拉松大赛"', + }, + { + type: 2, + nickname: '赵六', + createDate: '2026-07-28 15:00:00', + content: '上架赛事"春季马拉松大赛"', + }, + { + type: 3, + nickname: '张三', + createDate: '2026-07-27 09:00:00', + content: '禁用用户"钱七(1005)"', + }, + { + type: 4, + nickname: '张三', + createDate: '2026-07-27 09:15:00', + content: '启用用户"钱七(1005)"', + }, + { + type: 1, + nickname: '孙八', + createDate: '2026-07-26 16:30:00', + content: '下架赛事"夏季游泳锦标赛"', + }, + { + type: 2, + nickname: '孙八', + createDate: '2026-07-26 17:00:00', + content: '上架赛事"夏季游泳锦标赛"', + }, + { + type: 1, + nickname: '李四', + createDate: '2026-07-25 11:00:00', + content: '下架赛事"秋季篮球邀请赛"', + }, + { + type: 2, + nickname: '李四', + createDate: '2026-07-25 12:00:00', + content: '上架赛事"秋季篮球邀请赛"', + }, + { + type: 3, + nickname: '周九', + createDate: '2026-07-24 08:30:00', + content: '禁用用户"吴十(1008)"', + }, + { + type: 4, + nickname: '周九', + createDate: '2026-07-24 08:45:00', + content: '启用用户"吴十(1008)"', + }, + { + 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: '上架赛事"元旦全民健身跑"', + }, +]; + +/** 按日期范围过滤(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, + 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; + const list = filtered.slice(start, start + limit); + + return { total, list }; +} diff --git a/src/config/mock/orderDetail.ts b/src/config/mock/orderDetail.ts new file mode 100644 index 0000000..24dae23 --- /dev/null +++ b/src/config/mock/orderDetail.ts @@ -0,0 +1,41 @@ +/** + * 订单详情弹窗假数据 + * + * 使用页面:`src/pages/events/orders/components/OrderDetailModal.tsx` + * 对应接口:`GET /manager/order/info` + * 类型:`TournamentAdminOrderInfoVO` + */ +import type { TournamentAdminOrderInfoVO } from '@/api/orders/types'; + +export const MOCK_ORDER_DETAIL: TournamentAdminOrderInfoVO = { + tournamentName: '2026全国青少年羽毛球锦标赛暨体育文化交流大会', + orderNumber: 'ORD20260700001', + orderTime: '2026.07.12 08:11:07', + payTime: '2026.07.12 08:12:30', + orderAmount: '188.00', + discountAmount: '20.00', + payAmount: '168.00', + refundAmount: '50.00', + refundInfoList: [ + { + refundedAmount: '30.00', + totalRefundCount: '2', + refundAmount: '30.00', + isFullRefund: '0', + refundMethod: '原路退回(微信支付)', + refundApplyTime: '2026.07.15 09:30:00', + refundOutTradeNo: '5030000789202607150930000123', + refundTime: '2026.07.15 09:35:00', + }, + { + refundedAmount: '20.00', + totalRefundCount: '2', + refundAmount: '20.00', + isFullRefund: '1', + refundMethod: '原路退回(微信支付)', + refundApplyTime: '2026.07.16 14:00:00', + refundOutTradeNo: '5030000789202607161400000456', + refundTime: '', + }, + ], +}; diff --git a/src/config/mock/orderList.ts b/src/config/mock/orderList.ts new file mode 100644 index 0000000..8e66a44 --- /dev/null +++ b/src/config/mock/orderList.ts @@ -0,0 +1,256 @@ +/** + * 订单列表页假数据 + * + * 使用页面:`src/pages/events/orders/index.tsx` + * 对应接口:`GET /manager/order/page` + * 类型:`TournamentAdminOrderPageVO[]` + */ +import type { TournamentAdminOrderPageVO, PageData } from '@/api/orders/types'; + +export const MOCK_ORDER_LIST: TournamentAdminOrderPageVO[] = [ + { + orderNo: 'ORD20260700001', + uniqueId: 101, + name: '2026全国青少年羽毛球锦标赛暨体育文化交流大会', + realName: '张选手', + phone: '13800138001', + categoryName: '男子青年组', + creatorName: '张报名', + creatorPhone: '13800138001', + creatorSignupCount: 2, + creatorSignupFee: '188.00', + creatorSignupFeeDiscount: '20.00', + creatorSignupFeeActual: '168.00', + creatorSignupFeeRefund: '0.00', + orderTime: '2026.07.12 08:11:07', + payTime: '2026.07.12 08:12:30', + orderStatus: 1, + refundStatus: 1, + }, + { + orderNo: 'ORD20260700002', + uniqueId: 102, + name: '国际马拉松城市联赛', + realName: '李选手', + phone: '13900139002', + categoryName: '女子青年组', + creatorName: '李报名', + creatorPhone: '13900139002', + creatorSignupCount: 1, + creatorSignupFee: '256.50', + creatorSignupFeeDiscount: '10.00', + creatorSignupFeeActual: '246.50', + creatorSignupFeeRefund: '50.00', + orderTime: '2026.07.11 11:28:20', + payTime: '2026.07.11 11:30:05', + orderStatus: 2, + refundStatus: 2, + }, + { + orderNo: 'ORD20260700003', + uniqueId: 103, + name: '全民健身运动会', + realName: '王选手', + phone: '13700137003', + categoryName: '男子中年组', + creatorName: '王报名', + creatorPhone: '13700137003', + creatorSignupCount: 1, + creatorSignupFee: '399.00', + creatorSignupFeeDiscount: '0.00', + creatorSignupFeeActual: '399.00', + creatorSignupFeeRefund: '399.00', + orderTime: '2026.07.10 14:45:33', + payTime: '2026.07.10 14:46:10', + orderStatus: 3, + refundStatus: 2, + }, + { + orderNo: 'ORD20260700004', + uniqueId: 104, + name: '2026全国青少年羽毛球锦标赛暨体育文化交流大会', + realName: '赵选手', + phone: '13600136004', + categoryName: '女子中年组', + creatorName: '赵报名', + creatorPhone: '13600136004', + creatorSignupCount: 3, + creatorSignupFee: '128.00', + creatorSignupFeeDiscount: '20.00', + creatorSignupFeeActual: '108.00', + creatorSignupFeeRefund: '0.00', + orderTime: '2026.07.09 18:02:46', + payTime: '2026.07.09 18:03:15', + orderStatus: 4, + refundStatus: 1, + }, + { + orderNo: 'ORD20260700005', + uniqueId: 105, + name: '国际马拉松城市联赛', + realName: '张选手', + phone: '13800138001', + categoryName: '男子青年组', + creatorName: '张报名', + creatorPhone: '13800138001', + creatorSignupCount: 2, + creatorSignupFee: '520.00', + creatorSignupFeeDiscount: '10.00', + creatorSignupFeeActual: '510.00', + creatorSignupFeeRefund: '0.00', + orderTime: '2026.07.08 21:19:59', + payTime: '2026.07.08 21:21:20', + orderStatus: 1, + refundStatus: 1, + }, + { + orderNo: 'ORD20260700006', + uniqueId: 106, + name: '全民健身运动会', + realName: '李选手', + phone: '13900139002', + categoryName: '女子青年组', + creatorName: '李报名', + creatorPhone: '13900139002', + creatorSignupCount: 1, + creatorSignupFee: '188.00', + creatorSignupFeeDiscount: '0.00', + creatorSignupFeeActual: '188.00', + creatorSignupFeeRefund: '0.00', + orderTime: '2026.07.07 00:36:12', + payTime: '2026.07.07 00:37:00', + orderStatus: 1, + refundStatus: 1, + }, + { + orderNo: 'ORD20260700007', + uniqueId: 107, + name: '2026全国青少年羽毛球锦标赛暨体育文化交流大会', + realName: '王选手', + phone: '13700137003', + categoryName: '男子中年组', + creatorName: '王报名', + creatorPhone: '13700137003', + creatorSignupCount: 1, + creatorSignupFee: '256.50', + creatorSignupFeeDiscount: '20.00', + creatorSignupFeeActual: '236.50', + creatorSignupFeeRefund: '236.50', + orderTime: '2026.07.06 03:53:25', + payTime: '2026.07.06 03:54:00', + orderStatus: 3, + refundStatus: 3, + }, + { + orderNo: 'ORD20260700008', + uniqueId: 108, + name: '国际马拉松城市联赛', + realName: '赵选手', + phone: '13600136004', + categoryName: '女子中年组', + creatorName: '赵报名', + creatorPhone: '13600136004', + creatorSignupCount: 3, + creatorSignupFee: '399.00', + creatorSignupFeeDiscount: '10.00', + creatorSignupFeeActual: '389.00', + creatorSignupFeeRefund: '30.00', + orderTime: '2026.07.05 07:10:38', + payTime: '2026.07.05 07:11:00', + orderStatus: 2, + refundStatus: 2, + }, + { + orderNo: 'ORD20260700009', + uniqueId: 109, + name: '全民健身运动会', + realName: '张选手', + phone: '13800138001', + categoryName: '男子青年组', + creatorName: '张报名', + creatorPhone: '13800138001', + creatorSignupCount: 2, + creatorSignupFee: '128.00', + creatorSignupFeeDiscount: '0.00', + creatorSignupFeeActual: '128.00', + creatorSignupFeeRefund: '0.00', + orderTime: '2026.07.04 10:27:51', + payTime: '2026.07.04 10:28:30', + orderStatus: 4, + refundStatus: 1, + }, + { + orderNo: 'ORD20260700010', + uniqueId: 110, + name: '2026全国青少年羽毛球锦标赛暨体育文化交流大会', + realName: '李选手', + phone: '13900139002', + categoryName: '女子青年组', + creatorName: '李报名', + creatorPhone: '13900139002', + creatorSignupCount: 1, + creatorSignupFee: '520.00', + creatorSignupFeeDiscount: '20.00', + creatorSignupFeeActual: '500.00', + creatorSignupFeeRefund: '0.00', + orderTime: '2026.07.03 13:44:04', + payTime: '2026.07.03 13:45:00', + orderStatus: 1, + refundStatus: 1, + }, + { + orderNo: 'ORD20260700011', + uniqueId: 111, + name: '国际马拉松城市联赛', + realName: '王选手', + phone: '13700137003', + categoryName: '男子中年组', + creatorName: '王报名', + creatorPhone: '13700137003', + creatorSignupCount: 1, + creatorSignupFee: '188.00', + creatorSignupFeeDiscount: '10.00', + creatorSignupFeeActual: '178.00', + creatorSignupFeeRefund: '50.00', + orderTime: '2026.07.02 17:01:17', + payTime: '2026.07.02 17:02:00', + orderStatus: 2, + refundStatus: 3, + }, + { + orderNo: 'ORD20260700012', + uniqueId: 112, + name: '全民健身运动会', + realName: '赵选手', + phone: '13600136004', + categoryName: '女子中年组', + creatorName: '赵报名', + creatorPhone: '13600136004', + creatorSignupCount: 3, + creatorSignupFee: '256.50', + creatorSignupFeeDiscount: '0.00', + creatorSignupFeeActual: '256.50', + creatorSignupFeeRefund: '0.00', + orderTime: '2026.07.01 20:18:30', + payTime: '2026.07.01 20:19:00', + orderStatus: 1, + refundStatus: 1, + }, +]; + +/** 金额汇总假数据(汇总接口独立,此为 mock 占位) */ +export const MOCK_ORDER_SUMMARY = { + totalOrderAmount: 113255.0, + totalPaidAmount: 113255.0, + totalRefundAmount: 1255.0, +}; + +/** 构建分页响应 */ +export function buildMockOrderListPage( + page: number, + limit: number, +): PageData { + const start = (page - 1) * limit; + const list = MOCK_ORDER_LIST.slice(start, start + limit); + return { total: MOCK_ORDER_LIST.length, list }; +} diff --git a/src/config/mock/orderPlayerList.ts b/src/config/mock/orderPlayerList.ts new file mode 100644 index 0000000..287e20a --- /dev/null +++ b/src/config/mock/orderPlayerList.ts @@ -0,0 +1,104 @@ +/** + * 订单详情 — 选手列表假数据 + * + * 使用页面:`src/pages/events/orders/components/OrderDetailModal.tsx` + * 对应接口:`GET /manager/order/info/page` + * 类型:`TournamentAdminOrderInfoPageVO[]` + */ +import type { TournamentAdminOrderInfoPageVO } from '@/api/orders/types'; + +export const MOCK_ORDER_PLAYER_LIST: TournamentAdminOrderInfoPageVO[] = [ + { + realName: '张三', + gender: '1', + phone: '13800138001', + idCard: '110101199001011234', + idCardImg: 'https://picsum.photos/seed/idcard1/96/64', + idCardImgList: [ + 'https://picsum.photos/seed/idcard1a/96/64', + 'https://picsum.photos/seed/idcard1b/96/64', + ], + categoryName: '男子青年组', + orderAmount: '100.00', + discountAmount: '10.00', + payAmount: '90.00', + refundAmount: '0.00', + }, + { + realName: '李四', + gender: '1', + phone: '13900139002', + idCard: '110101199002022345', + idCardImg: '', + idCardImgList: [], + categoryName: '男子青年组', + orderAmount: '88.00', + discountAmount: '10.00', + payAmount: '78.00', + refundAmount: '50.00', + }, + { + realName: '王芳', + gender: '2', + phone: '13700137003', + idCard: '110101199103033456', + idCardImg: 'https://picsum.photos/seed/idcard3/96/64', + idCardImgList: ['https://picsum.photos/seed/idcard3a/96/64'], + categoryName: '女子双打', + orderAmount: '0.00', + discountAmount: '0.00', + payAmount: '0.00', + refundAmount: '-', + }, + { + realName: '赵六', + gender: '1', + phone: '13600136004', + idCard: '110101199204044567', + idCardImg: '', + idCardImgList: [], + categoryName: '女子双打', + orderAmount: '50.00', + discountAmount: '0.00', + payAmount: '50.00', + refundAmount: '-', + }, + { + realName: '孙七', + gender: '2', + phone: '13500135005', + idCard: '110101199305055678', + idCardImg: 'https://picsum.photos/seed/idcard5/96/64', + idCardImgList: [ + 'https://picsum.photos/seed/idcard5a/96/64', + 'https://picsum.photos/seed/idcard5b/96/64', + ], + categoryName: '男子青年组', + orderAmount: '60.00', + discountAmount: '5.00', + payAmount: '55.00', + refundAmount: '0.00', + }, + { + realName: '周八', + gender: '1', + phone: '13400134006', + idCard: '110101199406066789', + idCardImg: 'https://picsum.photos/seed/idcard6/96/64', + idCardImgList: ['https://picsum.photos/seed/idcard6a/96/64'], + categoryName: '男子中年组', + orderAmount: '120.00', + discountAmount: '20.00', + payAmount: '100.00', + refundAmount: '-', + }, +]; + +export function buildMockOrderPlayerListPage( + page: number, + limit: number, +): { total: number; list: TournamentAdminOrderInfoPageVO[] } { + const start = (page - 1) * limit; + const list = MOCK_ORDER_PLAYER_LIST.slice(start, start + limit); + return { total: MOCK_ORDER_PLAYER_LIST.length, 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/config/mock/userList.ts b/src/config/mock/userList.ts new file mode 100644 index 0000000..57fb63b --- /dev/null +++ b/src/config/mock/userList.ts @@ -0,0 +1,134 @@ +/** + * 用户列表页假数据(类型内联,避免循环引用) + * 对应接口:GET /sys/user/page + */ + +interface MockUserVO { + id: string; + realName: string; + phone: string; + roleId: string; + roleName: string; + status: string; + createTime: string; +} +interface MockPageData { + total: number; + list: T[]; +} + +const MOCK_USERS: MockUserVO[] = [ + { + id: '1', + realName: '张三', + phone: '13800138001', + roleId: '1', + roleName: '超级管理员', + status: '1', + createTime: '2026-07-20 08:11:07', + }, + { + id: '2', + realName: '李四', + phone: '13900139002', + roleId: '2', + roleName: '赛事管理员', + status: '1', + createTime: '2026-07-19 11:28:20', + }, + { + id: '3', + realName: '王五', + phone: '13700137003', + roleId: '3', + roleName: '裁判', + status: '1', + createTime: '2026-07-18 14:45:33', + }, + { + id: '4', + realName: '赵六', + phone: '13600136004', + roleId: '2', + roleName: '赛事管理员', + status: '0', + createTime: '2026-07-17 18:02:46', + }, + { + id: '5', + realName: '孙七', + phone: '13800138005', + roleId: '1', + roleName: '超级管理员', + status: '1', + createTime: '2026-07-16 21:19:59', + }, + { + id: '6', + realName: '周八', + phone: '13900139006', + roleId: '3', + roleName: '裁判', + status: '1', + createTime: '2026-07-15 00:36:12', + }, + { + id: '7', + realName: '吴九', + phone: '13700137007', + roleId: '2', + roleName: '赛事管理员', + status: '1', + createTime: '2026-07-14 03:53:25', + }, + { + id: '8', + realName: '郑十', + phone: '13600136008', + roleId: '4', + roleName: '财务', + status: '1', + createTime: '2026-07-13 07:10:38', + }, + { + id: '9', + realName: '陈一', + phone: '13800138009', + roleId: '3', + roleName: '裁判', + status: '1', + createTime: '2026-07-12 10:27:51', + }, + { + id: '10', + realName: '刘二', + phone: '13900139010', + roleId: '2', + roleName: '赛事管理员', + status: '0', + createTime: '2026-07-11 13:44:04', + }, + { + id: '11', + realName: '黄三', + phone: '13700137011', + roleId: '1', + roleName: '超级管理员', + status: '1', + createTime: '2026-07-10 17:01:17', + }, + { + id: '12', + realName: '林四', + phone: '13600136012', + roleId: '4', + roleName: '财务', + status: '1', + createTime: '2026-07-09 20:18:30', + }, +]; + +export function buildMockUserListPage(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/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx index 74fb457..2f0693b 100644 --- a/src/layouts/BasicLayout.tsx +++ b/src/layouts/BasicLayout.tsx @@ -14,6 +14,7 @@ import { useUserStore } from '@/stores/userStore'; import { useTabsStore } from '@/stores/tabsStore'; import { useState, useBreadcrumb, auth } from '@/hooks'; import { ChangePasswordModal, PageTabs } from '@/components'; +import { updateUserPwd } from '@/api/users'; import { renderRouteIcon } from '@/utils/routeIcon'; import logoutSvg from '@/assets/img/icon/logout.svg'; import styles from './BasicLayout.module.less'; @@ -122,12 +123,18 @@ export default defineComponent({ if (changePwdSubmitting.value) return; setChangePwdSubmitting(true); try { - // TODO: 替换为真实 API 调用 - console.log('修改密码:', payload); - message.success('修改成功'); - setChangePwdVisible(false); + const res = await updateUserPwd({ + password: payload.newPassword, + confirmPassword: payload.newPassword, + }); + if (res.code === 200) { + message.success('密码修改成功'); + setChangePwdVisible(false); + } else { + message.error(res.msg || '修改失败'); + } } catch (err: any) { - message.error(err?.msg || '修改失败'); + console.error('修改密码失败:', err); } finally { setChangePwdSubmitting(false); } @@ -249,7 +256,7 @@ export default defineComponent({ v-slots={{ default: ({ Component, route }: any) => ( - + {Component ? h(Component, { key: route.path }) : null} diff --git a/src/pages/events/banner/components/BannerFormModal.tsx b/src/pages/events/banner/components/BannerFormModal.tsx index 89f311e..720971a 100644 --- a/src/pages/events/banner/components/BannerFormModal.tsx +++ b/src/pages/events/banner/components/BannerFormModal.tsx @@ -6,28 +6,36 @@ import { Input, Select, DatePicker, + Cascader, InputNumber, Button, message, Spin, } from 'ant-design-vue'; -import { JUMP_TYPE_OPTIONS, EVENT_OPTIONS, REGION_OPTIONS } from '../model/useBannerModel'; +import { JUMP_TYPE_OPTIONS, EVENT_OPTIONS } from '../model/useBannerModel'; +import { + getProvinceCityCascaderOptions, + parseAreaToCascaderPath, + formatCascaderArea, +} from '@/utils/areaData'; import { ImageCropper } from '@yp-component/root'; import { FileImageOutlined } from '@ant-design/icons-vue'; import { uploadFile, OssUploadType } from '@/utils/oss'; import dayjs from 'dayjs'; import styles from './BannerFormModal.module.less'; +/** 省市两级 Cascader 选项 */ +const cascaderAreaOptions = getProvinceCityCascaderOptions(); + /** 默认表单数据 */ const getDefaultForm = () => ({ title: '', imageUrl: '', imageStatus: 'empty' as 'empty' | 'uploading' | 'success' | 'error', - jumpType: 'event_detail' as 'event_detail' | 'miniprogram_page' | 'custom_link' | 'none', - relatedEventId: '', - pagePath: '', - customUrl: '', - region: 'home', + type: '1' as '1' | '2' | '3', + linkUrl: '', + relatedEventId: undefined as string | undefined, + area: '', displayTimeRange: null as [string, string] | null, sort: 1, }); @@ -80,6 +88,10 @@ export default defineComponent({ const formRef = ref(); const fileInputRef = ref(null); const formData = reactive(getDefaultForm()); + /** Cascader 区域的路径数组(与 formData.area 字符串双向同步) */ + const areaPath = ref([]); + /** 是否由用户主动切换了跳转类型(跳过初始加载的 type 联动校验) */ + const typeChanged = ref(false); /** 裁剪弹窗状态 */ const [cropperVisible, setCropperVisible] = useState(false); @@ -91,38 +103,38 @@ export default defineComponent({ const fresh = getDefaultForm(); if (!record) { Object.assign(formData, fresh); + areaPath.value = []; return; } - let pagePath = ''; - let customUrl = ''; - if (record.jumpType === 'miniprogram_page') { - pagePath = record.jumpUrl || ''; - } else if (record.jumpType === 'custom_link') { - customUrl = record.jumpUrl || ''; - } - + const area = record.area || record.region || ''; + areaPath.value = parseAreaToCascaderPath(area); Object.assign(formData, { ...fresh, title: record.title || '', - imageUrl: record.imageUrl || '', - imageStatus: record.imageUrl ? ('success' as const) : ('empty' as const), - jumpType: record.jumpType || 'event_detail', - relatedEventId: record.relatedEventId || '', - pagePath, - customUrl, - region: record.region && record.region !== record.city ? record.region : 'home', + imageUrl: record.img || record.imageUrl || '', + imageStatus: record.img || record.imageUrl ? ('success' as const) : ('empty' as const), + type: String(record.type || record.jumpType || '1'), + linkUrl: record.linkUrl || record.jumpUrl || '', + relatedEventId: record.relatedEventId || undefined, + area, displayTimeRange: - record.displayStartTime && record.displayEndTime - ? ([record.displayStartTime, record.displayEndTime] as [string, string]) + (record.showTimeBegin || record.displayStartTime) && + (record.showTimeEnd || record.displayEndTime) + ? ([ + record.showTimeBegin || record.displayStartTime, + record.showTimeEnd || record.displayEndTime, + ] as [string, string]) : null, - sort: record.sort ?? 1, + sort: Number(record.sort ?? 1), }); }; /** 监听 visible 变化重置表单 */ useEffect(() => { if (props.visible) { + // 标记首次渲染,跳过 type 联动校验 + typeChanged.value = false; initFormFromRecord(props.record); setCropperVisible(false); setUploadImageUrl(''); @@ -130,18 +142,33 @@ export default defineComponent({ } }, [() => props.visible]); - /** 监听 jumpType 变化时清理联动字段 */ + /** 监听 type 变化时清理联动字段值和校验 */ useEffect(() => { - const newType = formData.jumpType; - if (newType !== 'event_detail') formData.relatedEventId = ''; - if (newType !== 'miniprogram_page') formData.pagePath = ''; - if (newType !== 'custom_link') formData.customUrl = ''; - const fieldsToValidate: string[] = []; - if (newType === 'event_detail') fieldsToValidate.push('relatedEventId'); - if (newType === 'miniprogram_page') fieldsToValidate.push('pagePath'); - if (newType === 'custom_link') fieldsToValidate.push('customUrl'); - if (fieldsToValidate.length) formRef.value?.clearValidate(fieldsToValidate); - }, [() => formData.jumpType]); + // 首次加载跳过校验 + if (!typeChanged.value) { + if (formData.type !== '1') formData.relatedEventId = undefined; + if (formData.type !== '2' && formData.type !== '3') formData.linkUrl = ''; + typeChanged.value = true; + return; + } + + if (formData.type !== '1') { + formData.relatedEventId = undefined; + formRef.value?.clearValidate('relatedEventId'); + } + if (formData.type !== '2' && formData.type !== '3') { + formData.linkUrl = ''; + } + const activeNames: string[] = []; + if (formData.type === '1') activeNames.push('relatedEventId'); + if (formData.type === '2' || formData.type === '3') activeNames.push('linkUrl'); + // 清除所有联动字段的校验状态 + formRef.value?.clearValidate(['relatedEventId', 'linkUrl']); + // 如果有已填值,重新校验当前字段 + if (activeNames.length) { + setTimeout(() => formRef.value?.validateFields(activeNames).catch(() => {}), 0); + } + }, [() => formData.type]); /** 触发隐藏的文件选择器 */ const triggerFilePicker = () => { @@ -235,25 +262,28 @@ export default defineComponent({ return; } - // 防御:上传中时禁止提交 if (formData.imageStatus === 'uploading') { message.warning('图片仍在上传中,请稍候'); return; } - // 构造提交数据:根据 jumpType 整理 jumpUrl - const payload: any = { ...formData }; - if (payload.jumpType === 'event_detail') { - payload.jumpUrl = payload.relatedEventId; - } else if (payload.jumpType === 'miniprogram_page') { - payload.jumpUrl = payload.pagePath; - } else if (payload.jumpType === 'custom_link') { - payload.jumpUrl = payload.customUrl; - } else { - payload.jumpUrl = ''; - } - payload.displayStartTime = payload.displayTimeRange?.[0] || ''; - payload.displayEndTime = payload.displayTimeRange?.[1] || ''; + const payload = { + img: formData.imageUrl || '', + title: formData.title || '', + type: formData.type, + linkUrl: + formData.type === '1' + ? formData.relatedEventId || '' + : formData.type === '2' + ? formData.linkUrl || '' + : formData.type === '3' + ? formData.linkUrl || '' + : '', + area: formData.area || formatCascaderArea(areaPath.value), + showTimeBegin: formData.displayTimeRange?.[0] || '', + showTimeEnd: formData.displayTimeRange?.[1] || '', + sort: String(formData.sort ?? 1), + }; props.onSave(payload); }; @@ -344,18 +374,18 @@ export default defineComponent({ {/* ===== 跳转类型 ===== */} )} - {formData.jumpType === 'miniprogram_page' && ( + {formData.type === '2' && ( - - - )} - - {formData.jumpType === 'custom_link' && ( - @@ -409,25 +413,44 @@ export default defineComponent({ placeholder="https://xxx" maxlength={URL_MAX} allowClear - v-model:value={formData.customUrl} + v-model:value={formData.linkUrl} /> )} - {formData.jumpType === 'none' && ( -
选择"无链接"时,点击 Banner 不会跳转。
+ {formData.type === '3' && ( + + + )} {/* ===== 所属区域 ===== */} - (filterForm.title = val.target?.value ?? val ?? '')} onPressEnter={handleSearch} /> @@ -228,7 +212,6 @@ export default defineComponent({ - {/* ===== 表格区 ===== */}
- {/* 独立分页,右下方 */}
- {/* ===== 新增/编辑弹窗 ===== */} {modalVisible.value && ( = { - event_detail: { label: '赛事详情页', tone: 'primary' }, - miniprogram_page: { label: '小程序页面', tone: 'cyan' }, - custom_link: { label: '自定义链接', tone: 'purple' }, - none: { label: '无链接', tone: 'default' }, + '1': { label: '赛事详情页', tone: 'primary' }, + '2': { label: '自定义链接', tone: 'purple' }, + '3': { label: '小程序页面', tone: 'cyan' }, }; /** Banner 状态映射 */ const STATUS_MAP: Record = { - enabled: { label: '启用中', tone: 'success' }, - disabled: { label: '已禁用', tone: 'default', dimmed: true }, + '1': { label: '启用中', tone: 'success' }, + '0': { label: '已禁用', tone: 'default', dimmed: true }, }; -/** 关联赛事选项(mock) */ +/** 关联赛事选项(mock,TODO: 替换为真实 API) */ export const EVENT_OPTIONS = [ - { value: '', label: '不关联' }, { value: 'EV20260701001', label: '2026全国青少年羽毛球锦标赛暨体育文化交流大会' }, { value: 'EV20260701002', label: '国际马拉松城市联赛' }, { value: 'EV20260701003', label: '全民健身运动会' }, @@ -46,122 +53,74 @@ export const EVENT_OPTIONS = [ { value: 'EV20260701005', label: '北京城市定向越野挑战赛' }, ]; -/** 所属区域选项(mock) */ -export const REGION_OPTIONS = [ - { value: 'home', label: '首页' }, - { value: 'guangdong_shenzhen', label: '广东省深圳市' }, - { value: 'zhejiang_hangzhou', label: '浙江省杭州市' }, - { value: 'beijing', label: '北京市' }, - { value: 'shanghai', label: '上海市' }, -]; - -// ============================================================ -// 假数据(表格) -// ============================================================ - -const MOCK_DATA = Array.from({ length: 12 }, (_, i) => { - const jumpType = (['event_detail', 'miniprogram_page', 'custom_link', 'none'] as const)[i % 4]; - const provinces = ['广东省', '浙江省', '北京市', '上海市']; - const cities = ['深圳市', '杭州市', '北京市', '上海市']; - - return { - key: `${i + 1}`, - id: `BANNER${String(i + 1).padStart(4, '0')}`, - sort: i + 1, - imageUrl: `https://picsum.photos/seed/banner${i + 1}/80/44`, - title: - i % 4 === 0 - ? '2026全国青少年羽毛球锦标赛火热报名中' - : i % 4 === 1 - ? '马拉松城市联赛限时优惠' - : i % 4 === 2 - ? '健身运动会专属福利' - : '平台品牌宣传活动', - jumpType, - jumpUrl: - jumpType === 'custom_link' - ? 'https://example.com/activity' - : jumpType === 'miniprogram_page' - ? '/pages/activity/detail' - : '', - relatedEventId: - jumpType === 'event_detail' - ? (['EV20260701001', 'EV20260701002', 'EV20260701003'] as const)[i % 3] - : '', - relatedEvent: - jumpType === 'event_detail' - ? (() => { - const map: Record = { - EV20260701001: '2026全国青少年羽毛球锦标赛暨体育文化交流大会', - EV20260701002: '国际马拉松城市联赛', - EV20260701003: '全民健身运动会', - }; - return map[(['EV20260701001', 'EV20260701002', 'EV20260701003'] as const)[i % 3]] || ''; - })() - : '', - region: provinces[i % 4], - city: cities[i % 4], - displayStartTime: '2026-08-01 00:00', - displayEndTime: '2026-08-31 23:59', - createTime: `2026-07-${String((i % 28) + 1).padStart(2, '0')} ${String((i * 2 + 8) % 24).padStart(2, '0')}:${String((i * 7) % 60).padStart(2, '0')}`, - status: (i % 4 === 0 ? 'disabled' : 'enabled') as 'enabled' | 'disabled', - }; -}); +/** 所属区域选项(动态生成,直辖市仅市名,其他省市) */ +export const REGION_OPTIONS = getProvinceCityOptions(); // ============================================================ // Model // ============================================================ -/** - * Banner 配置页数据模型 - */ export function useBannerModel() { - // ===== 筛选条件 ===== + // ===== 筛选条件(key 名对齐 API 查询参数) ===== const filterForm = reactive({ - searchTitle: '', - status: '' as '' | 'enabled' | 'disabled', + title: '', + status: '' as '' | '0' | '1', }); - // 防抖 300ms const { debouncedValue: debouncedTitle } = useDebounce( - toRef(filterForm, 'searchTitle') as Ref, + toRef(filterForm, 'title') as Ref, { delay: 300 }, ); - // ===== 表格状态 ===== + // ===== 表格状态(API 返回全量,前端分页) ===== const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState(MOCK_DATA); + const [allData, setAllData] = useState([]); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, - total: MOCK_DATA.length, + total: 0, + }); + + /** 当前页数据(客户端切片) */ + const dataSource = computed(() => { + const start = (pagination.value.current - 1) * pagination.value.pageSize; + return allData.value.slice(start, start + pagination.value.pageSize); }); // ===== 弹窗状态 ===== const [modalVisible, setModalVisible] = useState(false); const [editingRecord, setEditingRecord] = useState(null); - - // ===== 提交态(用于表单弹窗的确认按钮 loading)===== const [formSubmitting, setFormSubmitting] = useState(false); - // ===== 表格列配置(纯数据列,复杂渲染在页面 bodyCell 中处理)===== + // ===== 表格列配置(dataIndex 对齐 TournamentAdminBannerListVO) ===== const columns = [ - { title: '排序', dataIndex: 'sort', key: 'sort', width: 70, align: 'center' as const }, { title: '图片', key: 'image', width: 100, align: 'center' as const }, { title: '标题', dataIndex: 'title', key: 'title', width: 220 }, { title: '跳转类型', - dataIndex: 'jumpType', - key: 'jumpType', + dataIndex: 'type', + key: 'type', width: 120, }, - { title: '关联赛事', dataIndex: 'relatedEvent', key: 'relatedEvent', width: 200 }, - { title: '所属区域', key: 'region', width: 140 }, + { title: '跳转地址', dataIndex: 'linkUrl', key: 'linkUrl', width: 200 }, + { title: '所属区域', dataIndex: 'area', key: 'area', width: 140 }, { title: '展示时间', key: 'displayTime', width: 260 }, { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 160 }, { title: '状态', dataIndex: 'status', key: 'status', width: 100, align: 'center' as const }, ]; + // ===== 构建 API 查询参数 ===== + const buildQueryParams = (): BannerListQueryParams => { + const params: BannerListQueryParams = {}; + if (filterForm.title.trim()) { + params.title = filterForm.title.trim(); + } + if (filterForm.status) { + params.status = filterForm.status; + } + return params; + }; + // ===== 计算属性 ===== const hasFilter = computed(() => { return debouncedTitle.value.trim() !== '' || filterForm.status !== ''; @@ -171,36 +130,38 @@ export function useBannerModel() { // ===== 方法 ===== - /** 查询(节流 500ms) */ + /** 查询 */ const handleSearch = useThrottleFn(async () => { setLoading(true); try { - console.log('搜索条件:', { - title: debouncedTitle.value, - status: filterForm.status, - }); - // TODO: 替换为真实 API 调用 - setDataSource(MOCK_DATA); - setPagination({ ...pagination.value, total: MOCK_DATA.length }); - message.success('查询成功'); + const queryParams = buildQueryParams(); + console.log('Banner列表查询参数:', queryParams); + const res = await getBannerList(queryParams); + + if (res.code === 200) { + setAllData(res.data); + setPagination({ current: 1, pageSize: 10, total: res.data.length }); + } else { + message.error(res.msg || '查询失败'); + } } catch (error: any) { - message.error(error.msg || '查询失败'); + console.error('Banner查询失败:', error); } finally { setLoading(false); } }, 500); - /** 重置(节流 500ms) */ + /** 重置 */ const handleReset = useThrottleFn(() => { - filterForm.searchTitle = ''; + filterForm.title = ''; filterForm.status = ''; - setPagination({ current: 1, pageSize: 10, total: MOCK_DATA.length }); - setDataSource(MOCK_DATA); + setPagination({ current: 1, pageSize: 10, total: 0 }); + setAllData([]); + setTimeout(() => handleSearch(), 350); }, 500); const handlePageChange = (page: number, pageSize: number) => { setPagination({ ...pagination.value, current: page, pageSize }); - handleSearch(); }; /** 打开新增弹窗 */ @@ -208,14 +169,10 @@ export function useBannerModel() { setEditingRecord(null); setModalVisible(true); }; - - /** 打开编辑弹窗 */ const handleEdit = (record: any) => { setEditingRecord(record); setModalVisible(true); }; - - /** 关闭弹窗 */ const handleCloseModal = () => { setModalVisible(false); setEditingRecord(null); @@ -226,61 +183,81 @@ export function useBannerModel() { if (formSubmitting.value) return; setFormSubmitting(true); try { - const payload = { - ...formPayload, - displayStartTime: formPayload.displayTimeRange?.[0] || '', - displayEndTime: formPayload.displayTimeRange?.[1] || '', + const params = { + img: formPayload.img || formPayload.imageUrl || '', + title: formPayload.title || '', + type: String(formPayload.type || formPayload.jumpType || '1'), + linkUrl: formPayload.linkUrl || formPayload.jumpUrl || '', + area: formPayload.area || formPayload.region || '', + showTimeBegin: + formPayload.showTimeBegin || + formPayload.displayTimeRange?.[0] || + formPayload.displayStartTime || + '', + showTimeEnd: + formPayload.showTimeEnd || + formPayload.displayTimeRange?.[1] || + formPayload.displayEndTime || + '', + sort: String(formPayload.sort ?? 1), }; - console.log(isEdit.value ? '编辑 Banner' : '新增 Banner', payload); - // TODO: 替换为真实 API 调用 - message.success(isEdit.value ? '编辑成功' : '新增成功'); - handleCloseModal(); - handleSearch(); - } catch (error: any) { - message.error(error.msg || '操作失败'); + const isEdit = editingRecord.value !== null; + const res = isEdit + ? await updateBanner({ ...params, id: editingRecord.value.id }) + : await saveBanner(params); + if (res.code === 200) { + message.success(isEdit ? '编辑成功' : '新增成功'); + handleCloseModal(); + handleSearch(); + } else { + message.error(res.msg || '操作失败'); + } + } catch (e: any) { + console.error('Banner提交失败:', e); } finally { setFormSubmitting(false); } }, 500); - /** 删除(节流 500ms) */ + /** 删除 */ const handleDelete = useThrottleFn(async (record: any) => { setLoading(true); try { - console.log('删除 Banner:', record.id); - // TODO: 替换为真实 API 调用 - message.success('删除成功'); - handleSearch(); - } catch (error: any) { - message.error(error.msg || '删除失败'); + const res = await delBanner(record.id); + if (res.code === 200) { + message.success('删除成功'); + handleSearch(); + } else { + message.error(res.msg || '删除失败'); + } + } catch (e: any) { + console.error('Banner删除失败:', e); } finally { setLoading(false); } }, 500); - /** 启用/禁用切换(节流 500ms) */ + /** 启用/禁用切换 */ const handleToggleStatus = useThrottleFn(async (record: any) => { setLoading(true); try { - const newStatus = record.status === 'enabled' ? 'disabled' : 'enabled'; - const actionText = newStatus === 'enabled' ? '启用' : '禁用'; - console.log(`${actionText} Banner:`, record.id); - // TODO: 替换为真实 API 调用 - // 更新本地数据 - setDataSource( - dataSource.value.map((item: any) => - item.key === record.key ? { ...item, status: newStatus } : item, - ), - ); - message.success(`${actionText}成功`); - } catch (error: any) { - message.error(error.msg || '操作失败'); + const newStatus = record.status === '1' ? '0' : '1'; + const actionText = newStatus === '1' ? '启用' : '禁用'; + const res = await toggleBannerActive({ id: record.id, status: newStatus }); + if (res.code === 200) { + message.success(`${actionText}成功`); + handleSearch(); + } else { + message.error(res.msg || '操作失败'); + } + } catch (e: any) { + console.error('Banner状态切换失败:', e); } finally { setLoading(false); } }, 500); - // ===== 供页面 bodyCell 使用的渲染工具 ===== + // ===== 渲染工具 ===== const renderJumpType = (type: string) => { const info = JUMP_TYPE_MAP[type] || { label: type || '-', tone: 'default' as const }; return h(StatusTag, { label: info.label, tone: info.tone }); @@ -288,11 +265,7 @@ export function useBannerModel() { const renderStatus = (status: string) => { const info = STATUS_MAP[status] || { label: status || '-', tone: 'default' as const }; - return h(StatusTag, { - label: info.label, - tone: info.tone, - dimmed: info.dimmed, - }); + return h(StatusTag, { label: info.label, tone: info.tone, dimmed: info.dimmed }); }; return { diff --git a/src/pages/events/list/components/EventDetailModal.tsx b/src/pages/events/list/components/EventDetailModal.tsx index 0644989..10af427 100644 --- a/src/pages/events/list/components/EventDetailModal.tsx +++ b/src/pages/events/list/components/EventDetailModal.tsx @@ -1,5 +1,15 @@ -import { defineComponent } from 'vue'; -import { Modal, Descriptions, Image, Table, Pagination, Space } from 'ant-design-vue'; +import { defineComponent, reactive, onMounted } from 'vue'; +import { Modal, Descriptions, Image, Table, Pagination, Space, Spin } from 'ant-design-vue'; +import { EyeOutlined } from '@ant-design/icons-vue'; +import { message } from 'ant-design-vue'; +import { + getEventDetail, + getSignupList, + type TournamentAdminInfoVO, + type InnerCategoryVO, + type TournamentAdminInfoSignupVO, +} from '@/api/events'; +import { useState } from '@/hooks'; import styles from './EventDetailModal.module.less'; interface EventDetailModalProps { @@ -8,86 +18,57 @@ interface EventDetailModalProps { onClose: () => void; } -// ===== Mock 数据 ===== -const MOCK_DETAIL = { - name: '第二届全国地学羽毛球邀请赛', - eventTime: '2026.05.26 08:00 ~ 2026.05.31 22:00', - descr: - '赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍。', - registerTime: '2026.05.24 08:00 ~ 2026.05.26 22:00', - cancelDeadline: '2026.05.25 22:00', - region: '北京市 北京市 东城区', - address: '李宁体育馆', - venueNo: '李宁体育馆', - publicEvent: '在首页展示赛事', - needIdCard: '无需提供', - needIdCardImage: '无需提供', - createTime: '2026.05.26 08:00', - creator: '张三', - status: '报名中', - enrolledCount: 100, - cancelledCount: 1, - maxCount: 1000, - crossedCount: 50, - completedCount: 50, - cover: 'https://picsum.photos/seed/eventcover/600/300', - announcement: - '赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告。', +// ===== 0/1 显示映射 ===== +const YES_NO_MAP: Record = { '0': '否', '1': '是' }; +const PUBLIC_MAP: Record = { '0': '非公开', '1': '公开' }; +const GENDER_MAP: Record = { '1': '男', '2': '女' }; +const CATEGORY_TYPE_MAP: Record = { '1': '单打', '2': '双打' }; + +const formatYN = (v: string) => YES_NO_MAP[v] ?? v; +const formatPublic = (v: string) => PUBLIC_MAP[v] ?? v; +const formatGender = (v: string) => GENDER_MAP[v] ?? v; + +const PAGE_SIZE = 10; + +/** 渲染身份证图片 */ +const renderIdImages = (imgList: string[], singleImg: string) => { + const urls = imgList.length > 0 ? imgList : singleImg ? [singleImg] : []; + if (urls.length === 0) return -; + return ( + + + {urls.map((url) => ( + }} + /> + ))} + + + ); }; -// 假组别数据 -const MOCK_GROUPS = [ - { - key: 'group1', - title: '组别1:男子单打(单打)', - players: [ - { - key: '1', - name: '张三', - gender: '男', - phone: '12345678995', - idNo: 'xxxxxxxxxxxxxxxx', - idImg: '', - }, - { key: '2', name: '李四', gender: '男', phone: '', idNo: '', idImg: '' }, - ], - }, - { - key: 'group2', - title: '组别2:女子双打(双打)', - players: [ - { - key: '1', - name: '张三', - gender: '女', - phone: '12345678995', - idNo: 'xxxxxxxxxxxxxxxx', - idImg: '', - }, - { key: '2', name: '李四', gender: '女', phone: '', idNo: '', idImg: '' }, - ], - }, -]; - -// 假分组数据 -const MOCK_DIVISIONS = ['分组1:男子小组1(循环赛)', '分组2:女子小组1(淘汰赛)']; - +/** 选手表格列 */ const PLAYER_COLUMNS = [ - { title: '选手', dataIndex: 'name', key: 'name', width: 100 }, - { title: '性别', dataIndex: 'gender', key: 'gender', width: 80 }, - { title: '手机号', dataIndex: 'phone', key: 'phone', width: 150 }, - { title: '证件号', dataIndex: 'idNo', key: 'idNo', width: 180 }, + { title: '选手', dataIndex: 'realName', key: 'realName', width: 100 }, + { + title: '性别', + dataIndex: 'gender', + key: 'gender', + width: 60, + customRender: ({ text }: { text: string }) => formatGender(text), + }, + { title: '手机号', dataIndex: 'phone', key: 'phone', width: 130 }, + { title: '证件号', dataIndex: 'idCard', key: 'idCard', width: 180 }, { title: '证件图片', - dataIndex: 'idImg', - key: 'idImg', + key: 'idCardImgs', width: 180, - customRender: () => ( - - - - - ), + customRender: ({ record }: { record: TournamentAdminInfoSignupVO }) => + renderIdImages(record.idCardImgList, record.idCardImg), }, ]; @@ -99,7 +80,71 @@ export default defineComponent({ onClose: { type: Function, required: true }, }, setup(props: EventDetailModalProps) { - const detail = MOCK_DETAIL; + // ===== 详情数据 ===== + const [detail, setDetail] = useState(null); + const [detailLoading, setDetailLoading] = useState(true); + + // ===== 每个组别的选手数据(key = categoryId) ===== + const categoryPlayers = reactive>({}); + const categoryPlayerTotal = reactive>({}); + const categoryPlayerLoading = reactive>({}); + const categoryPlayerPage = reactive>({}); + + /** 获取某个组别的选手列表(分页) */ + const fetchPlayers = async (categoryId: string, page: number) => { + categoryPlayerLoading[categoryId] = true; + try { + const res = await getSignupList({ + categoryId, + page: String(page), + limit: String(PAGE_SIZE), + }); + if (res.code === 200) { + categoryPlayers[categoryId] = res.data.list; + categoryPlayerTotal[categoryId] = res.data.total; + categoryPlayerPage[categoryId] = page; + } + } catch { + // mock 模式不会到这里,真实模式下由 API 层处理 + } finally { + categoryPlayerLoading[categoryId] = false; + } + }; + + /** 选手分页变更 */ + const handlePlayerPageChange = (categoryId: string, page: number) => { + fetchPlayers(categoryId, page); + }; + + // ===== 初始化 ===== + onMounted(async () => { + setDetailLoading(true); + try { + const id = props.record?.id; + if (!id) { + message.error('缺少赛事ID'); + return; + } + + const res = await getEventDetail(String(id)); + if (res.code === 200) { + setDetail(res.data); + // 为每个组别加载第一页选手 + for (const cat of res.data.categoryList) { + categoryPlayers[cat.id] = []; + categoryPlayerTotal[cat.id] = 0; + categoryPlayerPage[cat.id] = 1; + fetchPlayers(cat.id, 1); + } + } else { + message.error(res.msg || '获取详情失败'); + } + } catch (e: any) { + console.error('获取赛事详情失败:', e); + } finally { + setDetailLoading(false); + } + }); return () => ( - {/* ===== 基础信息 ===== */} -
-
基础信息
- - {/* 第一行:赛事名称(1 列) + 赛事时间(占 2 列) */} - {detail.name} - - {detail.eventTime} - + {detailLoading.value ? ( +
+ +
+ ) : detail.value ? ( + <> + {/* ===== 基础信息 ===== */} +
+
基础信息
+ + {detail.value.name} + + {detail.value.startTimeBegin} 至 {detail.value.startTimeEnd} + - {/* 第二行:赛事说明(占 3 列,独占整行) */} - - {detail.descr} - + + {detail.value.descr} + - {detail.registerTime} - - {detail.cancelDeadline} - + + {detail.value.signupTimeBegin} 至 {detail.value.signupTimeEnd} + - {detail.region} - {detail.address} - {detail.venueNo} + {detail.value.venueArea} + {detail.value.venue} + {detail.value.venueNo} - {detail.publicEvent} - {detail.needIdCard} - {detail.needIdCardImage} + + {formatPublic(detail.value.isPublic)} + + + {formatYN(detail.value.isRequireIdCardNo)} + + + {formatYN(detail.value.isRequireIdCardImg)} + - {detail.createTime} - {detail.creator} - {detail.status} - {detail.enrolledCount} - - {detail.cancelledCount} - + {detail.value.createTime} + + {detail.value.createNickname} + - {detail.maxCount} - {detail.crossedCount} - {detail.completedCount} - -
+ {detail.value.signupCount} + {detail.value.dropoutCount} + {detail.value.dtotalLimit} - {/* ===== 封面 ===== */} -
-
封面
- -
+ + {detail.value.matchTotalCount} + + + {detail.value.matchCount} + +
+
- {/* ===== 赛事公告 ===== */} -
-
赛事公告
-
{detail.announcement}
-
+ {/* ===== 封面 ===== */} +
+
封面
+ +
- {/* ===== 组别信息 ===== */} -
-
组别信息
- {MOCK_GROUPS.map((group) => ( -
-
{group.title}
-
-
- + {/* ===== 赛事公告 ===== */} +
+
赛事公告
+
{detail.value.notice}
+
+ + {/* ===== 组别信息 ===== */} +
+
组别信息
+ {detail.value.categoryList.map((cat: InnerCategoryVO) => ( +
+
+ {cat.name}({CATEGORY_TYPE_MAP[cat.type] ?? cat.type}) +
+
+
+ handlePlayerPageChange(cat.id, page)} + /> +
+ + ))} + + + {/* ===== 分组信息 ===== */} +
+
分组信息
+
+ {detail.value.groupInfoList.map((d: string) => ( +
+ {d} +
+ ))}
- ))} - - - {/* ===== 分组信息 ===== */} -
-
分组信息
-
- {MOCK_DIVISIONS.map((d) => ( -
- {d} -
- ))} -
-
+ + ) : null} ); }, diff --git a/src/pages/events/list/components/EventRegulationModal.module.less b/src/pages/events/list/components/EventRegulationModal.module.less index e8b5048..9db2931 100644 --- a/src/pages/events/list/components/EventRegulationModal.module.less +++ b/src/pages/events/list/components/EventRegulationModal.module.less @@ -29,7 +29,7 @@ .imageGrid { display: grid; - grid-template-columns: repeat(4, 1fr); + grid-template-columns: repeat(5, 1fr); gap: 12px; } diff --git a/src/pages/events/list/components/EventRegulationModal.tsx b/src/pages/events/list/components/EventRegulationModal.tsx index 304bc76..f0230ce 100644 --- a/src/pages/events/list/components/EventRegulationModal.tsx +++ b/src/pages/events/list/components/EventRegulationModal.tsx @@ -7,7 +7,7 @@ interface EventRegulationModalProps { onClose: () => void; } -/** 规程文字 */ +/** 规程内容 */ const REGULATION_TEXT = [ '一、赛事目的:推动羽毛球运动发展,提高运动员技术水平,增进体育文化交流。', '二、参赛资格:凡身体健康、年龄符合要求的羽毛球爱好者均可报名参加。', @@ -35,16 +35,16 @@ export default defineComponent({ - {/* 规程文字 */} + {/* 规程内容 */}
-
规程文字
+
规程内容
{REGULATION_TEXT}
diff --git a/src/pages/events/list/index.tsx b/src/pages/events/list/index.tsx index 682785f..f0fb526 100644 --- a/src/pages/events/list/index.tsx +++ b/src/pages/events/list/index.tsx @@ -1,4 +1,4 @@ -import { defineComponent } from 'vue'; +import { defineComponent, onMounted } from 'vue'; import { Button, Input, @@ -20,6 +20,7 @@ import { ID_CARD_OPTIONS, ID_CARD_IMAGE_OPTIONS, } from './model/useEventListModel'; +import { toggleEventOnline } from '@/api/events'; import { regionOptions } from '@/utils/areaData'; import { useContainerSize, useState } from '@/hooks'; import EventRegulationModal from './components/EventRegulationModal'; @@ -37,6 +38,7 @@ function renderBodyCell({ text, record, onView, + onViewRegulation, onAudit, onToggleShelf, }: { @@ -44,10 +46,11 @@ function renderBodyCell({ text: any; record: any; onView: (record: any) => void; + onViewRegulation: (record: any) => void; onAudit: (record: any) => void; onToggleShelf: (record: any) => void; }) { - if (column.dataIndex === 'eventName') { + if (column.dataIndex === 'name') { const maxLen = 15; const raw = text || ''; const display = raw.length > maxLen ? raw.slice(0, maxLen) + '...' : raw; @@ -62,7 +65,7 @@ function renderBodyCell({ if (column.key === 'regulation') { return ( - onView(record)}> + onViewRegulation(record)}> 查看 ); @@ -72,45 +75,29 @@ function renderBodyCell({ const status = record.status; return ( - // 待审核:查看 + 审核 - (status === 'pending' && ( + // 报名中(0) / 进行中(1):查看 + 下架 + ((status === '0' || status === '1') && ( - - - )) || - // 报名中:查看 + 下架 - (status === 'enrolling' && ( - - - )) || - // 已下架:查看 + 上架 - (status === 'removed' && ( + // 已下架(4):查看 + 上架 + (status === '4' && ( - )) || ( - // 审核不通过 / 已删除:仅查看 + // 已结束(2) / 已删除(3):仅查看 @@ -147,49 +134,62 @@ export default defineComponent({ const [currentRecord, setCurrentRecord] = useState({}); const handleView = (record: any) => { - console.log('查看赛事:', record.eventId); + console.log('查看赛事:', record.id); setCurrentRecord(record); setDetailModalVisible(true); }; /** 上架 / 下架 通用 */ const handleToggleShelf = (record: any) => { - const isRemoved = record.status === 'removed'; + const isRemoved = record.status === '4'; const actionText = isRemoved ? '上架' : '下架'; Modal.confirm({ title: `${actionText}确认`, - content: `确认${actionText}"${record.eventName}"该赛事吗,下架后用户将无法搜索和报名该赛事,已有报名和订单不受影响。`, + content: `确认${actionText}"${record.name}"该赛事吗,下架后用户将无法搜索和报名该赛事,已有报名和订单不受影响。`, okText: '确认', cancelText: '取消', - onOk: () => { - console.log(`${actionText}赛事:`, record.eventId); - // TODO: 替换为真实 API 调用 - message.success(`${actionText}成功`); + onOk: async () => { + const online = isRemoved ? '1' : '0'; + try { + const res = await toggleEventOnline({ id: record.id, online }); + if (res.code === 200) { + message.success(`${actionText}成功`); + handleSearch(); + } else { + message.error(res.msg || `${actionText}失败`); + } + } catch (e: any) { + console.error(`${actionText}失败:`, e); + } }, }); }; /** 打开审核弹窗 */ const handleAudit = (record: any) => { - console.log('审核赛事:', record.eventId); + console.log('审核赛事:', record.id); setCurrentRecord(record); setAuditModalVisible(true); }; /** 提交审核 */ const handleAuditSubmit = (payload: { approved: boolean; comment: string }) => { - console.log('提交审核:', currentRecord.value.eventId, payload); + console.log('提交审核:', currentRecord.value.id, payload); // TODO: 替换为真实 API 调用 message.success('审核提交成功'); setAuditModalVisible(false); }; const handleViewRegulation = (record: any) => { - console.log('查看规程:', record.eventId); + console.log('查看规程:', record.id); setCurrentRecord(record); setRegulationModalVisible(true); }; + onMounted(() => { + handleSearch(); + }); + /** 最终表格列:模型列 + 赛事规程 + 操作 */ const tableColumns = [ ...columns, @@ -208,35 +208,45 @@ export default defineComponent({ {/* ===== 筛选区 ===== */}
- + (filterForm.createTimeRange = val)} + onUpdate:value={(val: any) => (filterForm.createDateRange = val)} /> - + (filterForm.eventTimeRange = val)} + onUpdate:value={(val: any) => (filterForm.startDateRange = val)} /> - + (filterForm.name = val.target?.value ?? val ?? '')} onPressEnter={handleSearch} /> - + + (filterForm.createName = val.target?.value ?? val ?? '') + } onPressEnter={handleSearch} /> @@ -249,31 +259,31 @@ export default defineComponent({ onUpdate:value={(val: any) => (filterForm.status = val || '')} /> - + (filterForm.needIdCard = val || '')} + onUpdate:value={(val: any) => (filterForm.idCardNo = val || '')} /> - +
- - - {/* ===== 退款记录 ===== */} -
-
- 退款记录 + ) : detail.value ? ( +
+ {/* ===== 订单信息 ===== */} +
+
+ 赛事名称: + {detail.value.tournamentName} +
+
+ 订单编号: + {detail.value.orderNumber} +
+
+ 订单时间: + {detail.value.orderTime} +
+
+ 支付时间: + {detail.value.payTime} +
+
+ 订单金额: + {formatMoney(detail.value.orderAmount)}元 +
+
+ 优惠金额: + {formatMoney(detail.value.discountAmount)}元 +
+
+ 实付金额: + {formatMoney(detail.value.payAmount)}元 +
+
+ 退款金额: + {formatMoney(detail.value.refundAmount)}元 +
- {hasRefund.value ? ( - <> -
- 已退金额:{totalRefunded.value.toFixed(2)}元 - 退款次数:{refundRecords.value.length}次 -
- {/* 固定高度 + 内部滚动 */} -
- {pagedRefunds.value.map((r, idx) => { - const statusInfo = REFUND_STATUS_MAP[r.status] || { - label: '-', - tone: 'default' as const, - }; - const realIdx = (refundPage.value - 1) * refundPageSize.value + idx + 1; - return ( -
-
- 第{realIdx}次退款 - -
-
-
-
- 退款金额: - - ¥{r.amount.toFixed(2)} - {r.isPartial && ( - (部分退款) - )} - + + {/* ===== 选手信息 ===== */} +
+
选手信息
+
+
+ +
+ + + {/* ===== 退款记录 ===== */} +
+
+ 退款记录 +
+ {hasRefund.value ? ( + <> +
+ 已退金额:{totalRefunded.value.toFixed(2)}元 + 退款次数:{refundRecords.value.length}次 +
+
+ {pagedRefunds.value.map((r, idx) => { + const realIdx = (refundPage.value - 1) * refundPageSize.value + idx + 1; + const isSuccess = parseFloat(r.refundTime || '') > 0 || !!r.refundTime; + const statusInfo = { + label: isSuccess + ? '退款成功' + : r.isFullRefund === '1' + ? '退款待处理' + : '退款中', + tone: (isSuccess + ? 'success' + : r.isFullRefund === '1' + ? 'warning' + : 'orange') as StatusTagTone, + }; + return ( +
+
+ 第{realIdx}次退款 + +
+
+
+
+ 退款金额: + + ¥{formatMoney(r.refundAmount)} + {r.isFullRefund === '1' && ( + (全部退款) + )} + {r.isFullRefund === '0' && ( + (部分退款) + )} + +
+
申请时间:{r.refundApplyTime}
+
完款时间:{r.refundTime || '-'}
+
+
+
退款方式:{r.refundMethod}
+
第三方退款单号:{r.refundOutTradeNo || '-'}
+
已退金额:¥{formatMoney(r.refundedAmount)}
-
申请时间:{r.applyTime}
-
完款时间:{r.completeTime || '-'}
-
-
-
退款方式:{r.method}
-
第三方退款单号:{r.thirdPartyNo || '-'}
-
- ); - })} -
-
- -
- - ) : ( -
暂无退款记录
- )} + ); + })} + +
+ +
+ + ) : ( +
暂无退款记录
+ )} + - + ) : null} {/* ===== 底部按钮 ===== */}
{hasRefund.value ? ( - ) : ( - )} diff --git a/src/pages/events/orders/index.tsx b/src/pages/events/orders/index.tsx index 12f81be..2e050df 100644 --- a/src/pages/events/orders/index.tsx +++ b/src/pages/events/orders/index.tsx @@ -1,4 +1,4 @@ -import { defineComponent } from 'vue'; +import { defineComponent, onMounted } from 'vue'; import { Button, Input, @@ -10,8 +10,10 @@ import { Tooltip, Pagination, Modal, + message, } from 'ant-design-vue'; import { useOrderModel, ORDER_STATUS_OPTIONS, REFUND_STATUS_OPTIONS } from './model/useOrderModel'; +import { retryRefundOrder } from '@/api/orders'; import { useContainerSize, useState } from '@/hooks'; import OrderDetailModal from './components/OrderDetailModal'; import pageStyles from '@/assets/styles/pageLayout.module.less'; @@ -20,22 +22,29 @@ const { RangePicker } = DatePicker; /** * 触发「重新退款」二次确认对话框 - * 表格中的重新退款按钮和详情弹窗中的重新退款按钮均调用此函数。 */ function promptReRefund(record: any) { Modal.confirm({ title: '确认要重新退款吗?', content: (
-
退款金额:¥{(record.refundAmount || 0).toFixed(2)}
+
退款金额:¥{parseFloat(record.creatorSignupFeeRefund || '0').toFixed(2)}
退款方式:原路退回(微信支付)
), okText: '确认', cancelText: '取消', - onOk: () => { - console.log('重新退款:', record.orderId); - // TODO: 替换为真实 API 调用 + onOk: async () => { + try { + const res = await retryRefundOrder({ payOrderNo: record.orderNo }); + if (res.code === 200) { + message.success('重新退款提交成功'); + } else { + message.error(res.msg || '操作失败'); + } + } catch (e: any) { + console.error('重新退款失败:', e); + } }, }); } @@ -57,7 +66,7 @@ function renderBodyCell({ onReRefund: (record: any) => void; }) { // 赛事名称:最多15字符,超出显示 Tooltip - if (column.dataIndex === 'eventName') { + if (column.dataIndex === 'name') { const maxLen = 15; const raw = text || ''; const display = raw.length > maxLen ? raw.slice(0, maxLen) + '...' : raw; @@ -71,7 +80,7 @@ function renderBodyCell({ } if (column.key === 'action') { - const showReRefund = record.refundStatus === 'refund_failed'; + const showReRefund = record.refundStatus === 3; return (
{{ - bodyCell: (args: any) => - renderBodyCell({ - ...args, - onToggleStatus: handleToggleStatus, - }), + bodyCell: (args: any) => { + if (args.column.key === 'action') { + const isActive = args.record.status === '1'; + return ( + + ); + } + }, }}
- - {/* 独立分页,右下方 */}
= { - active: { label: '正常', tone: 'success' }, - disabled: { label: '禁用', tone: 'danger' }, +/** 角色选项(TODO: 对接角色 API) */ +export const ROLE_OPTIONS = [ + { value: '', label: '全部' }, + { value: '1', label: '超级管理员' }, + { value: '2', label: '赛事管理员' }, + { value: '3', label: '裁判' }, + { value: '4', label: '财务' }, +] as const; + +const STATUS_MAP: Record = { + '1': { label: '正常', tone: 'success' }, + '0': { label: '停用', tone: 'danger' }, }; -// ============================================================ -// 假数据 -// ============================================================ - -const MOCK_DATA = Array.from({ length: 12 }, (_, i) => ({ - key: `${i + 1}`, - userId: `U202607${String(i + 1).padStart(5, '0')}`, - nickName: ['羽毛球小将', '马拉松达人', '健身爱好者', '游泳健将'][i % 4], - phone: ['13800138001', '13900139002', '13700137003', '13600136004'][i % 4], - realName: ['张三', '李四', '王五', '赵六'][i % 4], - gender: i % 2 === 0 ? '男' : '女', - idNo: '320101199001011234', - // 证图片来源:奇数索引有,偶数无 - idImg: - i % 2 === 1 - ? ['https://picsum.photos/seed/idcard-a/120/80', 'https://picsum.photos/seed/idcard-b/120/80'] - : [], - status: (i % 4 === 3 ? 'disabled' : 'active') as string, - loginTime: '2026-07-20 14:30:00', -})); - // ============================================================ // Model // ============================================================ -/** - * 用户列表页数据模型 - */ export function useUserModel() { - // ===== 筛选条件 ===== const filterForm = reactive({ - searchUserName: '', - searchPhone: '', + text: '', + roleId: '', status: '', }); - // 各字段防抖 300ms - const { debouncedValue: debouncedUserName } = useDebounce( - toRef(filterForm, 'searchUserName') as Ref, - { delay: 300 }, - ); - const { debouncedValue: debouncedPhone } = useDebounce( - toRef(filterForm, 'searchPhone') as Ref, - { delay: 300 }, - ); - - // ===== 表格状态 ===== - const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState(MOCK_DATA); - const [pagination, setPagination] = useState({ - current: 1, - pageSize: 10, - total: MOCK_DATA.length, + const { debouncedValue: debouncedText } = useDebounce(toRef(filterForm, 'text') as Ref, { + delay: 300, }); - // ===== 表格列配置 ===== + const [loading, setLoading] = useState(false); + const [dataSource, setDataSource] = useState([]); + const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }); + const columns = [ - { title: '用户昵称', dataIndex: 'nickName', key: 'nickName', width: 120 }, + { title: '用户姓名', dataIndex: 'realName', key: 'realName', width: 120 }, { title: '手机号', dataIndex: 'phone', key: 'phone', width: 140 }, - { title: '真实姓名', dataIndex: 'realName', key: 'realName', width: 100 }, - { title: '性别', dataIndex: 'gender', key: 'gender', width: 80 }, - { title: '证件号', dataIndex: 'idNo', key: 'idNo', width: 180 }, - { - title: '证件图片', - dataIndex: 'idImg', - key: 'idImg', - width: 140, - }, + { title: '角色', dataIndex: 'roleName', key: 'roleName', width: 120 }, + { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 170 }, { title: '状态', dataIndex: 'status', @@ -95,51 +59,51 @@ export function useUserModel() { width: 90, align: 'center' as const, customRender: ({ text }: { text: string }) => { - const info = USER_STATUS_MAP[text] || { label: text, tone: 'default' as const }; + const info = STATUS_MAP[text] || { label: text, tone: 'default' as const }; return h(StatusTag, { label: info.label, tone: info.tone }); }, }, - { title: '登录时间', dataIndex: 'loginTime', key: 'loginTime', width: 170 }, ]; - // ===== 计算属性 ===== - const hasFilter = computed(() => { - return ( - debouncedUserName.value.trim() !== '' || - debouncedPhone.value.trim() !== '' || - filterForm.status !== '' - ); - }); + const buildQueryParams = (): UserListQueryParams => { + const params: UserListQueryParams = { + page: String(pagination.value.current), + limit: String(pagination.value.pageSize), + }; + if (filterForm.text.trim()) params.text = filterForm.text.trim(); + if (filterForm.roleId) params.roleId = filterForm.roleId; + if (filterForm.status) params.status = filterForm.status; + return params; + }; - // ===== 方法 ===== + const hasFilter = computed( + () => debouncedText.value.trim() !== '' || filterForm.roleId !== '' || filterForm.status !== '', + ); - /** 查询(节流 500ms) */ const handleSearch = useThrottleFn(async () => { setLoading(true); try { - console.log('搜索条件:', { - userName: debouncedUserName.value, - phone: debouncedPhone.value, - status: filterForm.status, - }); - // TODO: 替换为真实 API 调用 - setDataSource(MOCK_DATA); - setPagination({ ...pagination.value, total: MOCK_DATA.length }); - message.success('查询成功'); - } catch (error: any) { - message.error(error.msg || '查询失败'); + const params = buildQueryParams(); + console.log('用户列表查询参数:', params); + const res = await getUserList(params); + if (res.code === 200) { + setDataSource(res.data.list); + setPagination({ ...pagination.value, total: res.data.total }); + } else message.error(res.msg || '查询失败'); + } catch (e: any) { + console.error('用户列表查询失败:', e); } finally { setLoading(false); } }, 500); - /** 重置(节流 500ms) */ const handleReset = useThrottleFn(() => { - filterForm.searchUserName = ''; - filterForm.searchPhone = ''; + filterForm.text = ''; + filterForm.roleId = ''; filterForm.status = ''; - setPagination({ current: 1, pageSize: 10, total: MOCK_DATA.length }); - setDataSource(MOCK_DATA); + setPagination({ current: 1, pageSize: 10, total: 0 }); + setDataSource([]); + setTimeout(() => handleSearch(), 350); }, 500); const handlePageChange = (page: number, pageSize: number) => { diff --git a/src/pages/finance/reports/index.tsx b/src/pages/finance/reports/index.tsx index 1df044b..af30a0a 100644 --- a/src/pages/finance/reports/index.tsx +++ b/src/pages/finance/reports/index.tsx @@ -210,7 +210,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 76693bd..cad00e3 100644 --- a/src/pages/system/roles/model/useRoleModel.ts +++ b/src/pages/system/roles/model/useRoleModel.ts @@ -1,142 +1,12 @@ -import { computed, reactive, toRef, Ref, h } from 'vue'; +import { computed, reactive, toRef, Ref } from 'vue'; import { message } from 'ant-design-vue'; -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', - }, -]; - -/** 角色下用户列表 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 @@ -148,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(MOCK_DATA); - const [pagination, setPagination] = useState({ - current: 1, - pageSize: 10, - total: MOCK_DATA.length, - }); + const [dataSource, setDataSource] = useState([]); + const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }); // ===== 弹窗状态 ===== const [formVisible, setFormVisible] = useState(false); @@ -189,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(MOCK_DATA); - setPagination({ ...pagination.value, total: 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: MOCK_DATA.length }); - setDataSource(MOCK_DATA); + filterForm.name = ''; + setPagination({ current: 1, pageSize: 10, total: 0 }); + setDataSource([]); + setTimeout(() => handleSearch(), 350); }, 500); const handlePageChange = (page: number, pageSize: number) => { @@ -248,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); } @@ -263,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); } @@ -286,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, @@ -318,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 e5f44dd..5eacad5 100644 --- a/src/pages/system/users/components/UserFormModal.tsx +++ b/src/pages/system/users/components/UserFormModal.tsx @@ -26,7 +26,17 @@ const getDefaultForm = () => ({ phone: '', password: '', confirmPassword: '', - role: '', + roleId: undefined, +}); + +/** 新增用户默认密码 */ +const DEFAULT_USER_PASSWORD = 'yp888888'; + +/** 获取新增时的表单初始值(预填默认密码) */ +const getAddForm = () => ({ + ...getDefaultForm(), + password: DEFAULT_USER_PASSWORD, + confirmPassword: DEFAULT_USER_PASSWORD, }); export default defineComponent({ @@ -47,17 +57,18 @@ export default defineComponent({ /** 根据 record 初始化表单 */ const initFormFromRecord = (record: any) => { - const fresh = getDefaultForm(); if (!record) { - Object.assign(formData, fresh); + // 新增:预填默认密码 + 确认密码 + Object.assign(formData, getAddForm()); setIsResetting(false); return; } + const fresh = getDefaultForm(); Object.assign(formData, { ...fresh, realName: record.realName || '', phone: record.phone || '', - role: record.role || '', + roleId: record.roleId || undefined, }); setIsResetting(false); }; @@ -90,7 +101,7 @@ export default defineComponent({ const payload: any = { realName: formData.realName, phone: formData.phone, - role: formData.role, + roleId: formData.roleId, }; if (props.record) { if (isResetting.value) { @@ -159,11 +170,11 @@ export default defineComponent({ } /> - + diff --git a/src/pages/system/users/controller.ts b/src/pages/system/users/controller.ts index d87b4f6..c13ca64 100644 --- a/src/pages/system/users/controller.ts +++ b/src/pages/system/users/controller.ts @@ -14,11 +14,12 @@ import { isChinaPhone, isValidPassword, isValidRealName } from '@/utils/form'; /** 密码占位符(写死长度,避免暴露真实密码长度) */ export const PASSWORD_PLACEHOLDER = '********'; -/** 角色下拉选项(表单使用) */ +/** 角色下拉选项(表单使用,value 对应 API roleId) */ export const ROLE_FORM_OPTIONS = [ - { value: 'admin', label: '超级管理员' }, - { value: 'operator', label: '运营人员' }, - { value: 'auditor', label: '审核人员' }, + { value: '1', label: '超级管理员' }, + { value: '2', label: '赛事管理员' }, + { value: '3', label: '裁判' }, + { value: '4', label: '财务' }, ] as const; // ============================================================ diff --git a/src/pages/system/users/index.tsx b/src/pages/system/users/index.tsx index 5150aaf..222e35f 100644 --- a/src/pages/system/users/index.tsx +++ b/src/pages/system/users/index.tsx @@ -1,7 +1,18 @@ -import { defineComponent } from 'vue'; -import { Button, Input, Table, Form, Space, Select, Modal, Pagination } from 'ant-design-vue'; -import { useUserModel, USER_ROLE_OPTIONS, USER_STATUS_OPTIONS } from './model/useUserModel'; -import { useContainerSize } from '@/hooks'; +import { defineComponent, onMounted } from 'vue'; +import { + Button, + Input, + Table, + Form, + Space, + Select, + Modal, + Pagination, + message, +} from 'ant-design-vue'; +import { useUserModel, ROLE_OPTIONS, USER_STATUS_OPTIONS } from './model/useUserModel'; +import { toggleUserActive, saveUser, updateUser } from '@/api/users'; +import { useState, useThrottleFn, useContainerSize } from '@/hooks'; import UserFormModal from './components/UserFormModal'; import pageStyles from '@/assets/styles/pageLayout.module.less'; @@ -22,20 +33,14 @@ function renderBodyCell({ }) { // 操作列 if (column.key === 'action') { - const isActive = record.status === 'active'; + const isActive = record.status === '1'; return ( - ); @@ -43,29 +48,6 @@ function renderBodyCell({ return; } -/** - * 禁用/启用 二次确认弹窗 - */ -function confirmToggleStatus({ record, onOk }: { record: any; onOk: (record: any) => void }) { - const isActive = record.status === 'active'; - const actionText = isActive ? '禁用' : '启用'; - Modal.confirm({ - title: `确认${actionText}`, - class: 'toggleConfirmModal', - content: ( -
- 确定要{actionText}用户 - {record.realName} - 吗? -
- ), - okText: '确认', - cancelText: '取消', - okButtonProps: { danger: isActive }, - onOk: () => onOk(record), - }); -} - /** * 系统管理 - 用户管理 */ @@ -78,33 +60,110 @@ export default defineComponent({ dataSource, columns, pagination, - modalVisible, - editingRecord, - isEdit, - formSubmitting, handleSearch, handleReset, handlePageChange, - handleAdd, - handleEdit, - handleCloseModal, - handleSave, - handleToggleStatus, } = useUserModel(); const { containerRef, height } = useContainerSize(); - /** 触发禁用/启用的二次确认 */ - const triggerToggle = (record: any) => { - confirmToggleStatus({ record, onOk: handleToggleStatus }); + // ===== 弹窗状态(组件级) ===== + const [modalVisible, setModalVisible] = useState(false); + const [editingRecord, setEditingRecord] = useState(null); + const [formSubmitting, setFormSubmitting] = useState(false); + + // ===== 启用/禁用(使用真实 API) ===== + const handleToggleStatus = useThrottleFn(async (record: any) => { + const isActive = record.status === '1'; + const actionText = isActive ? '停用' : '启用'; + Modal.confirm({ + title: `${actionText}确认`, + content: ( +
+ 确定要{actionText}用户 + {record.realName} + 吗? +
+ ), + okText: '确认', + cancelText: '取消', + okButtonProps: { danger: isActive }, + onOk: async () => { + try { + const res = await toggleUserActive({ + userId: record.id, + status: isActive ? '0' : '1', + }); + if (res.code === 200) { + message.success(`${actionText}成功`); + handleSearch(); + } else { + message.error(res.msg || '操作失败'); + } + } catch (e: any) { + console.error(`用户${actionText}失败:`, e); + } + }, + }); + }, 500); + + // ===== 新增/编辑(组件级) ===== + const handleAdd = () => { + setEditingRecord(null); + setModalVisible(true); }; - /** 触发编辑 */ - const triggerEdit = (record: any) => { - handleEdit(record); + const handleEdit = (record: any) => { + setEditingRecord(record); + setModalVisible(true); }; - /** 表格最终列:模型列 + 操作 */ + const handleCloseModal = () => { + setModalVisible(false); + setEditingRecord(null); + }; + + const handleSave = useThrottleFn(async (formPayload: any) => { + if (formSubmitting.value) return; + setFormSubmitting(true); + try { + let res: any; + if (editingRecord.value) { + // 编辑:调用更新接口 + res = await updateUser({ + id: editingRecord.value.id, + realName: formPayload.realName, + phone: formPayload.phone, + roleId: formPayload.roleId, + ...(formPayload.password + ? { password: formPayload.password, confirmPassword: formPayload.password } + : {}), + }); + } else { + // 新增:调用保存接口 + res = await saveUser({ + realName: formPayload.realName, + phone: formPayload.phone, + roleId: formPayload.roleId, + password: formPayload.password, + confirmPassword: formPayload.password, + }); + } + if (res.code === 200) { + message.success(editingRecord.value ? '编辑成功' : '新增成功'); + handleCloseModal(); + handleSearch(); + } else { + message.error(res.msg || '操作失败'); + } + } catch (error: any) { + console.error('保存用户失败:', error); + } finally { + setFormSubmitting(false); + } + }, 500); + + // ===== 表格最终列 ===== const tableColumns = [ ...columns, { @@ -116,26 +175,34 @@ export default defineComponent({ }, ]; + // ===== 初始化 ===== + onMounted(() => { + handleSearch(); + }); + return () => (
{/* ===== 筛选区 ===== */}
- + (filterForm.text = val?.target?.value ?? val ?? '')} onPressEnter={handleSearch} /> - +
diff --git a/src/pages/system/users/model/useUserModel.ts b/src/pages/system/users/model/useUserModel.ts index e885fe1..9087c09 100644 --- a/src/pages/system/users/model/useUserModel.ts +++ b/src/pages/system/users/model/useUserModel.ts @@ -2,62 +2,34 @@ import { computed, reactive, toRef, Ref, h } from 'vue'; import { message } from 'ant-design-vue'; import { StatusTag, type StatusTagTone } from '@/components'; import { useState, useDebounce, useThrottleFn } from '@/hooks'; +import { getUserList, type TournamentAdminUserVO, type UserListQueryParams } from '@/api/users'; // ============================================================ // 常量 // ============================================================ /** 角色选项(筛选) */ -export const USER_ROLE_OPTIONS = [ +export const ROLE_OPTIONS = [ { value: '', label: '全部' }, - { value: 'admin', label: '管理员' }, + { value: '1', label: '超级管理员' }, + { value: '2', label: '赛事管理员' }, + { value: '3', label: '裁判' }, + { value: '4', label: '财务' }, ] as const; /** 状态选项(筛选) */ export const USER_STATUS_OPTIONS = [ { value: '', label: '全部' }, - { value: 'active', label: '正常' }, - { value: 'disabled', label: '禁用' }, + { value: '0', label: '停用' }, + { value: '1', label: '正常' }, ] as const; -/** 角色文本/色调映射(表格渲染) */ -const ROLE_MAP: Record = { - admin: { label: '管理员', tone: 'primary' }, - operator: { label: '运营人员', tone: 'cyan' }, - auditor: { label: '审核人员', tone: 'purple' }, -}; - -/** 状态文本/色调映射 */ +/** 状态文本/色调映射(表格渲染),对应 API status 字段:0=停用、1=正常 */ const STATUS_MAP: Record = { - active: { label: '正常', tone: 'success' }, - disabled: { label: '禁用', tone: 'danger' }, + '1': { label: '正常', tone: 'success' }, + '0': { label: '停用', tone: 'danger' }, }; -// ============================================================ -// 假数据 -// ============================================================ - -const MOCK_DATA = [ - { - key: '1', - userId: 'U20260526001', - realName: '张三', - phone: '15585658975', - role: 'admin', - status: 'active' as 'active' | 'disabled', - createTime: '2026.05.26 08:00:00', - }, - { - key: '2', - userId: 'U20260526002', - realName: '王五', - phone: '13800138000', - role: 'admin', - status: 'disabled' as 'active' | 'disabled', - createTime: '2026.05.26 14:00:00', - }, -]; - // ============================================================ // Model // ============================================================ @@ -68,47 +40,30 @@ const MOCK_DATA = [ export function useUserModel() { // ===== 筛选条件 ===== const filterForm = reactive({ - searchKeyword: '', // 用户名/手机号 - role: '' as '' | 'admin', - status: '' as '' | 'active' | 'disabled', + text: '', // 用户姓名/手机号 + roleId: '', // 角色ID + status: '' as '' | '0' | '1', }); - // 防抖 300ms - const { debouncedValue: debouncedKeyword } = useDebounce( - toRef(filterForm, 'searchKeyword') as Ref, - { delay: 300 }, - ); + // 文本筛选防抖 300ms + const { debouncedValue: debouncedText } = useDebounce(toRef(filterForm, 'text') as Ref, { + delay: 300, + }); // ===== 表格状态 ===== const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState(MOCK_DATA); + const [dataSource, setDataSource] = useState([]); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, - total: MOCK_DATA.length, + total: 0, }); - // ===== 弹窗状态 ===== - const [modalVisible, setModalVisible] = useState(false); - const [editingRecord, setEditingRecord] = useState(null); - - // ===== 提交态 ===== - const [formSubmitting, setFormSubmitting] = useState(false); - // ===== 表格列配置 ===== const columns = [ { title: '姓名', dataIndex: 'realName', key: 'realName', width: 120 }, { title: '手机号', dataIndex: 'phone', key: 'phone', width: 140 }, - { - title: '角色', - dataIndex: 'role', - key: 'role', - width: 120, - customRender: ({ text }: { text: string }) => { - const info = ROLE_MAP[text] || { label: text || '-', tone: 'default' as const }; - return h(StatusTag, { label: info.label, tone: info.tone }); - }, - }, + { title: '角色', dataIndex: 'roleName', key: 'roleName', width: 120 }, { title: '状态', dataIndex: 'status', @@ -123,107 +78,64 @@ export function useUserModel() { { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 180 }, ]; + // ===== 构建 API 查询参数 ===== + const buildQueryParams = (): UserListQueryParams => { + const params: UserListQueryParams = { + page: String(pagination.value.current), + limit: String(pagination.value.pageSize), + }; + if (filterForm.text.trim()) params.text = filterForm.text.trim(); + if (filterForm.roleId) params.roleId = filterForm.roleId; + if (filterForm.status) params.status = filterForm.status; + return params; + }; + // ===== 计算属性 ===== const hasFilter = computed(() => { return ( - debouncedKeyword.value.trim() !== '' || filterForm.role !== '' || filterForm.status !== '' + debouncedText.value.trim() !== '' || filterForm.roleId !== '' || filterForm.status !== '' ); }); - const isEdit = computed(() => editingRecord.value !== null); - // ===== 方法 ===== /** 查询(节流 500ms) */ const handleSearch = useThrottleFn(async () => { setLoading(true); try { - console.log('搜索条件:', { - keyword: debouncedKeyword.value, - role: filterForm.role, - status: filterForm.status, - }); - // TODO: 替换为真实 API 调用 - setDataSource(MOCK_DATA); - setPagination({ ...pagination.value, total: MOCK_DATA.length }); - message.success('查询成功'); + const queryParams = buildQueryParams(); + console.log('用户列表查询参数:', queryParams); + const res = await getUserList(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); - /** 重置(节流 500ms) */ + /** 重置(节流 500ms,重置后自动查询) */ const handleReset = useThrottleFn(() => { - filterForm.searchKeyword = ''; - filterForm.role = ''; + filterForm.text = ''; + filterForm.roleId = ''; filterForm.status = ''; - setPagination({ current: 1, pageSize: 10, total: MOCK_DATA.length }); - setDataSource(MOCK_DATA); + setPagination({ current: 1, pageSize: 10, total: 0 }); + setDataSource([]); + // 等待 debounce(300ms) 生效后自动查询 + setTimeout(() => handleSearch(), 350); }, 500); + /** 分页变更 */ const handlePageChange = (page: number, pageSize: number) => { setPagination({ ...pagination.value, current: page, pageSize }); handleSearch(); }; - /** 打开新增弹窗 */ - const handleAdd = () => { - setEditingRecord(null); - setModalVisible(true); - }; - - /** 打开编辑弹窗 */ - const handleEdit = (record: any) => { - setEditingRecord(record); - setModalVisible(true); - }; - - /** 关闭弹窗 */ - const handleCloseModal = () => { - setModalVisible(false); - setEditingRecord(null); - }; - - /** 提交表单(新增/编辑) */ - const handleSave = useThrottleFn(async (formPayload: any) => { - if (formSubmitting.value) return; - setFormSubmitting(true); - try { - console.log(isEdit.value ? '编辑用户' : '新增用户', formPayload); - // TODO: 替换为真实 API 调用 - message.success(isEdit.value ? '编辑成功' : '新增成功'); - handleCloseModal(); - handleSearch(); - } catch (error: any) { - message.error(error.msg || '操作失败'); - } finally { - setFormSubmitting(false); - } - }, 500); - - /** 启用/禁用切换(节流 500ms) */ - const handleToggleStatus = useThrottleFn(async (record: any) => { - setLoading(true); - try { - const newStatus = record.status === 'active' ? 'disabled' : 'active'; - const actionText = newStatus === 'active' ? '启用' : '禁用'; - console.log(`${actionText}用户:`, record.userId); - // TODO: 替换为真实 API 调用 - setDataSource( - dataSource.value.map((item: any) => - item.key === record.key ? { ...item, status: newStatus } : item, - ), - ); - message.success(`${actionText}成功`); - } catch (error: any) { - message.error(error.msg || '操作失败'); - } finally { - setLoading(false); - } - }, 500); - return { filterForm, loading, @@ -231,18 +143,8 @@ export function useUserModel() { columns, pagination, hasFilter, - modalVisible, - setModalVisible, - editingRecord, - isEdit, - formSubmitting, handleSearch, handleReset, handlePageChange, - handleAdd, - handleEdit, - handleCloseModal, - handleSave, - handleToggleStatus, }; } diff --git a/src/types/yp-component.d.ts b/src/types/yp-component.d.ts new file mode 100644 index 0000000..95e3bbb --- /dev/null +++ b/src/types/yp-component.d.ts @@ -0,0 +1 @@ +declare module '@yp-component/root'; diff --git a/src/utils/areaData.ts b/src/utils/areaData.ts index 76baa6a..91d28e8 100644 --- a/src/utils/areaData.ts +++ b/src/utils/areaData.ts @@ -31,3 +31,103 @@ function buildTree(parentCode: string): CascadeOption[] { /** 省市区三级联动数据 */ export const regionOptions: CascadeOption[] = buildTree('86'); + +// ============================================================ +// 省市选项(精确到市,直辖市仅市名) +// ============================================================ + +/** 直辖市代码 */ +const MUNICIPALITY_CODES = ['110000', '120000', '310000', '500000']; + +/** 省市选项 */ +export interface ProvinceCityOption { + value: string; + label: string; +} + +/** + * 获取"省-市"格式的选项列表 + * - 普通省份:格式为 "省名-市名",如 "广东省-深圳市" + * - 直辖市:直接使用市名,如 "北京市" + */ +export function getProvinceCityOptions(): ProvinceCityOption[] { + const options: ProvinceCityOption[] = []; + const provinces = areaData['86']; + if (!provinces) return options; + + for (const [provinceCode, provinceName] of Object.entries(provinces)) { + const pname = provinceName as string; + + if (MUNICIPALITY_CODES.includes(provinceCode)) { + options.push({ value: pname, label: pname }); + } else { + const cities = areaData[provinceCode]; + if (cities) { + for (const [, cityName] of Object.entries(cities)) { + const fullName = `${pname}-${cityName}`; + options.push({ value: fullName, label: fullName }); + } + } + } + } + + return options; +} + +/** Cascader 节点 */ +export interface CascaderNode { + value: string; + label: string; + children?: CascaderNode[]; +} + +/** + * 获取省市两级 Cascader 选项 + * - 普通省份:省 → 市,选中值 ["省名", "市名"] + * - 直辖市:直接叶子节点,选中值 ["市名"] + */ +export function getProvinceCityCascaderOptions(): CascaderNode[] { + const nodes: CascaderNode[] = []; + const provinces = areaData['86']; + if (!provinces) return nodes; + + for (const [provinceCode, provinceName] of Object.entries(provinces)) { + const pname = provinceName as string; + + if (MUNICIPALITY_CODES.includes(provinceCode)) { + // 直辖市:直接作为叶子节点 + nodes.push({ value: pname, label: pname }); + } else { + const cities = areaData[provinceCode]; + if (cities) { + const cityNodes: CascaderNode[] = Object.entries(cities).map(([, cname]) => ({ + value: cname as string, + label: cname as string, + })); + nodes.push({ value: pname, label: pname, children: cityNodes }); + } + } + } + + return nodes; +} + +/** + * 将 Cascader 选中的路径数组转为存储用的区域字符串 + * ["广东省", "深圳市"] → "广东省-深圳市" + * ["北京市"] → "北京市" + */ +export function formatCascaderArea(path: string[]): string { + if (!path || path.length === 0) return ''; + return path.join('-'); +} + +/** + * 将存储的区域字符串反向解析为 Cascader 路径数组 + * "广东省-深圳市" → ["广东省", "深圳市"] + * "北京市" → ["北京市"] + */ +export function parseAreaToCascaderPath(area: string): string[] { + if (!area) return []; + return area.split('-'); +}