diff --git a/src/api/authCache.ts b/src/api/authCache.ts new file mode 100644 index 0000000..590fe53 --- /dev/null +++ b/src/api/authCache.ts @@ -0,0 +1,17 @@ +import type { AuthData } from './menu'; + +/** Promise 级缓存:并发调用共享同一个请求,失败时清除以便重试 */ +let authDataPromise: Promise | null = null; + +export function getAuthDataPromise() { + return authDataPromise; +} + +export function setAuthDataPromise(promise: Promise | null) { + authDataPromise = promise; +} + +/** 清除权限数据缓存(退出登录时调用,确保下次登录不读到旧数据) */ +export function clearAuthDataCache() { + authDataPromise = null; +} diff --git a/src/api/banner/index.ts b/src/api/banner/index.ts index ffbdef1..61c7918 100644 --- a/src/api/banner/index.ts +++ b/src/api/banner/index.ts @@ -2,8 +2,6 @@ * 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, @@ -22,16 +20,10 @@ export type { 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); } diff --git a/src/api/banner/types.ts b/src/api/banner/types.ts index b9946de..d95d867 100644 --- a/src/api/banner/types.ts +++ b/src/api/banner/types.ts @@ -8,12 +8,12 @@ export interface TournamentAdminBannerListVO { img: string; title: string; type: string; - linkUrl: string; + params: string; area: string; showTimeBegin: string; showTimeEnd: string; status: string; - createTime: string; + createDate: string; } /** Banner 列表查询参数 */ @@ -28,7 +28,7 @@ export interface BannerSaveParams { img: string; title: string; type: string; - linkUrl: string; + params: string; area: string; showTimeBegin: string; showTimeEnd: string; diff --git a/src/api/events/types.ts b/src/api/events/types.ts index b68276b..c2c53a6 100644 --- a/src/api/events/types.ts +++ b/src/api/events/types.ts @@ -143,7 +143,7 @@ export interface TournamentAdminInfoVO { /** 退赛人数 */ dropoutCount: string; /** 允许报名最大人数 */ - dtotalLimit: string; + totalLimit: string; /** 总对阵数 */ matchTotalCount: string; /** 已完成对阵数 */ diff --git a/src/api/login/index.ts b/src/api/login/index.ts index ab552ec..1acfc8a 100644 --- a/src/api/login/index.ts +++ b/src/api/login/index.ts @@ -1,5 +1,12 @@ import { post } from '@/utils/request'; +import type { LoginResult } from './types'; + +export type { LoginResult } from './types'; export const login = (params: { phone: string; password: string }) => { - return post(`/op/login`, params); + return post(`/op/login`, params) as Promise<{ + code: number; + msg: string; + data: LoginResult; + }>; }; diff --git a/src/api/login/types.ts b/src/api/login/types.ts new file mode 100644 index 0000000..d152867 --- /dev/null +++ b/src/api/login/types.ts @@ -0,0 +1,8 @@ +export interface LoginResult { + token: string; + phone?: string; + nickName?: string; + realName?: string; + userName?: string; + name?: string; +} diff --git a/src/api/logs/index.ts b/src/api/logs/index.ts index 9fe9ce6..9b02c83 100644 --- a/src/api/logs/index.ts +++ b/src/api/logs/index.ts @@ -2,8 +2,6 @@ * 操作日志 API */ import { get } from '@/utils/request'; -import { USE_MOCK, MOCK_DELAY } from '@/config/mock'; -import { buildMockOperationLogPage } from '@/config/mock/operationLogs'; import type { OperationLogQueryParams, SysOperationLogVO, @@ -20,27 +18,10 @@ export type { ApiResult, } from './types'; -const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); - -function pickFilters(params: OperationLogQueryParams): OperationLogQueryParams { - const { page: _page, limit: _limit, ...filters } = params; - return filters; -} - /** GET /admin/sys/operation/page — 系统操作日志分页 */ export async function getOperationLogList( params: OperationLogQueryParams, ): 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: buildMockOperationLogPage(page, limit, pickFilters(params)), - }; - } return get('/sys/operation/page', params as Record); } diff --git a/src/api/logs/types.ts b/src/api/logs/types.ts index 21cefd2..87cdf03 100644 --- a/src/api/logs/types.ts +++ b/src/api/logs/types.ts @@ -16,8 +16,11 @@ export interface SysOperationLogVO { /** 赛事操作日志列表项 */ export interface TournamentAdminOperationPageVO { - opName: string; + /** 操作类型:1=创建赛事、2=删除赛事、3=取消报名、4=编辑选手、5=对阵管理、6=批量修改组别 */ + type: string; + /** 操作来源:1=PC、2=小程序 */ source: string; + opName: string; nickname: string; phone: string; tournamentName: string; diff --git a/src/api/menu.ts b/src/api/menu.ts index 5256af1..f9be5b1 100644 --- a/src/api/menu.ts +++ b/src/api/menu.ts @@ -1,15 +1,11 @@ import { get } from '@/utils/request'; import type { MenuNode, PermissionCode } from '@/types'; import { FALLBACK_MENU_NODES } from '@/config/fallbackRoutes'; +import { getAuthDataPromise, setAuthDataPromise, clearAuthDataCache } from '@/api/authCache'; +import { forceReLogin } from '@/hooks/useAuth'; +import { useUserStore } from '@/stores/userStore'; -// ============================================================ -// 权限数据缓存(/op/permission 只请求一次) -// ============================================================ - -/** - * Promise 级缓存:并发调用共享同一个请求,失败时清除以便重试 - */ -let authDataPromise: Promise | null = null; +export { clearAuthDataCache } from '@/api/authCache'; /** * 权限数据(角色 + 权限码) @@ -17,6 +13,42 @@ let authDataPromise: Promise | null = null; export interface AuthData { roleList: string[]; permsList: string[]; + nickname: string; + phone: string; +} + +const AUTH_FETCH_FAIL_MSG = '账户权限获取失败,请重新登录'; + +function normalizeAuthData(res: any): AuthData { + if (res?.code != null && res.code != 200) { + throw new Error(res.msg || AUTH_FETCH_FAIL_MSG); + } + + const data = res?.data || {}; + const roleList = data.roleList || []; + const permsList = data.permsList || []; + + if (permsList.length === 0) { + throw new Error(AUTH_FETCH_FAIL_MSG); + } + + const nickname = data.nickName || ''; + const phone = typeof data.phone === 'string' ? data.phone : ''; + + if (nickname || phone) { + const { setUser } = useUserStore(); + setUser({ + ...(nickname ? { nickname } : {}), + ...(phone ? { phone } : {}), + }); + } + + return { roleList, permsList, nickname, phone }; +} + +function handleAuthFetchFailure() { + clearAuthDataCache(); + forceReLogin(AUTH_FETCH_FAIL_MSG); } /** @@ -25,24 +57,18 @@ export interface AuthData { * Promise 级缓存:并发调用共享同一个请求,失败时清除缓存以允许重试 */ export function fetchAuthData(): Promise { - if (authDataPromise) return authDataPromise; - authDataPromise = get('/op/permission') - .then((res: any) => ({ - roleList: res.data?.roleList || [], - permsList: res.data?.permsList || [], - })) + const cached = getAuthDataPromise(); + if (cached) return cached; + + const promise = get('/op/permission') + .then((res) => normalizeAuthData(res)) .catch((err) => { - authDataPromise = null; + handleAuthFetchFailure(); throw err; }); - return authDataPromise; -} -/** - * 清除权限数据缓存(退出登录时调用,确保下次登录不读到旧数据) - */ -export function clearAuthDataCache() { - authDataPromise = null; + setAuthDataPromise(promise); + return promise; } /** @@ -95,7 +121,7 @@ function filterMenuNodesByPermission(nodes: MenuNode[], permsSet: Set): * 2. 用权限码过滤 FALLBACK_MENU_NODES * 3. 返回过滤后的菜单树 * - * 异常时抛出,由调用方(menuStore.loadMenu)降级使用未过滤的兜底路由 + * 权限获取失败时由 fetchAuthData 统一强制重新登录 */ export function fetchMenuTree(): Promise { return fetchAuthData().then(({ permsList }) => { diff --git a/src/api/orders/index.ts b/src/api/orders/index.ts index 4a38ef5..9bc3953 100644 --- a/src/api/orders/index.ts +++ b/src/api/orders/index.ts @@ -3,13 +3,6 @@ * 注意:/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, @@ -35,8 +28,6 @@ export type { ApiResult, } from './types'; -const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - /** * 订单管理 - 订单分页 * GET /admin/manager/order/page → /manager/order/page @@ -44,16 +35,6 @@ const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, 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); } @@ -64,14 +45,6 @@ export async function getOrderList( 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); } @@ -82,16 +55,6 @@ export async function getOrderDetail( 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); } diff --git a/src/api/orders/types.ts b/src/api/orders/types.ts index 540f3db..5040641 100644 --- a/src/api/orders/types.ts +++ b/src/api/orders/types.ts @@ -8,7 +8,7 @@ export interface TournamentAdminOrderPageVO { /** 订单编号 */ orderNo: string; /** 选手ID */ - uniqueId: number; + uniqueId: string; /** 赛事名称 */ name: string; /** 选手名称 */ @@ -36,9 +36,9 @@ export interface TournamentAdminOrderPageVO { /** 支付时间;yyyy.MM.dd HH:mm:ss */ payTime: string; /** 订单状态;1=已支付,2=部分退款,3=全部退款,4=已关闭,-1=其他 */ - orderStatus: number; + orderStatus: string; /** 退款状态;1=无退款,2=退款成功,3=退款失败,-1=其他 */ - refundStatus: number; + refundStatus: string; } /** 订单列表查询参数 */ @@ -62,11 +62,24 @@ export interface OrderListQueryParams { status?: string; /** * 退款状态 - * 不传=全部、0=退款失败,1=退款成功 + * 不传=全部、2=退款成功,3=退款失败 */ refundStatus?: string; } +/** 订单分页数据(含列表统计字段) */ +export interface OrderPageData extends PageData { + /** 扩展数据 */ + exData: { + /** 总订单金额(已支付) */ + totalOrderAmount: string; + /** 总实付金额 */ + totalActualAmount: string; + /** 总退款金额 */ + totalRefundAmount: string; + }; +} + /** 分页数据 */ export interface PageData { total: number; diff --git a/src/api/payments/index.ts b/src/api/payments/index.ts index e5daba7..ba0c61d 100644 --- a/src/api/payments/index.ts +++ b/src/api/payments/index.ts @@ -3,23 +3,24 @@ * 对应 OpenAPI: GET /admin/finance/paymentFlow/page */ import { get } from '@/utils/request'; -import { USE_MOCK, MOCK_DELAY } from '@/config/mock'; -import { buildMockPaymentFlowPage } from '@/config/mock/paymentFlow'; -import type { PaymentFlowQueryParams, PaymentFlowVO, PageData, ApiResult } from './types'; +import type { + PaymentFlowQueryParams, + PaymentFlowVO, + PaymentFlowPageData, + ApiResult, +} from './types'; -export type { PaymentFlowVO, PaymentFlowQueryParams, PageData, ApiResult } from './types'; - -const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); +export type { + PaymentFlowVO, + PaymentFlowQueryParams, + PaymentFlowPageData, + PaymentFlowExData, + ApiResult, +} from './types'; /** GET /admin/finance/paymentFlow/page — 支付流水分页 */ export async function getPaymentFlowList( params: PaymentFlowQueryParams, -): 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: buildMockPaymentFlowPage(page, limit, params) }; - } +): Promise> { return get('/finance/paymentFlow/page', params as Record); } diff --git a/src/api/payments/types.ts b/src/api/payments/types.ts index 4b52998..48b433f 100644 --- a/src/api/payments/types.ts +++ b/src/api/payments/types.ts @@ -41,6 +41,22 @@ export interface PageData { total: number; list: T[]; } + +/** 支付流水分页扩展汇总(与 total 同级) */ +export interface PaymentFlowExData { + /** 总支付金额 */ + totalAmount: string; + /** 总退款金额 */ + totalRefundAmount: string; +} + +/** 支付流水分页响应 */ +export interface PaymentFlowPageData { + total: number; + list: PaymentFlowVO[]; + exData: PaymentFlowExData; +} + export interface ApiResult { code: number; msg: string; diff --git a/src/api/reports/index.ts b/src/api/reports/index.ts new file mode 100644 index 0000000..16f5415 --- /dev/null +++ b/src/api/reports/index.ts @@ -0,0 +1,13 @@ +/** + * 财务报表 API + * 对应 OpenAPI: GET /admin/finance/report/list + */ +import { get } from '@/utils/request'; +import type { ReportQueryParams, FinanceReportVO, ApiResult } from './types'; + +export type { ReportQueryParams, FinanceReportVO, ReportMonthItem, ApiResult } from './types'; + +/** GET /admin/finance/report/list — 财务报表 */ +export function getFinanceReport(params: ReportQueryParams): Promise> { + return get('/admin/finance/report/list', params as Record); +} diff --git a/src/api/reports/types.ts b/src/api/reports/types.ts new file mode 100644 index 0000000..150473a --- /dev/null +++ b/src/api/reports/types.ts @@ -0,0 +1,48 @@ +/** + * 财务报表 — 类型定义 + * 对应 OpenAPI: GET /admin/finance/report/list + */ + +/** 月度明细项(对应 InnerMonthVO) */ +export interface ReportMonthItem { + /** 月份 yyyy-MM */ + month: string; + /** 订单金额,0.00 格式 */ + orderAmount: string; + /** 退款金额,0.00 格式 */ + refundAmount: string; + /** 净收入 = orderAmount - refundAmount,0.00 格式 */ + netIncome: string; + /** 提现金额,0.00 格式 */ + withdrawAmount: string; +} + +/** 财务报表汇总(对应 TournamentFinanceReportVO) */ +export interface FinanceReportVO { + /** 总收入(扣除退款后),0.00 格式 */ + totalIncome: string; + /** 总收入较上月增长率 */ + totalIncomeTendency: string; + /** 总提现金额,0.00 格式 */ + totalWithdrawIncome: string; + /** 总提现金额较上月增长率 */ + totalWithdrawIncomeTendency: string; + /** 平台余额 = totalIncome - totalWithdrawIncome,0.00 格式 */ + totalBalanceAmount: string; + /** 月度收支明细 */ + monthList: ReportMonthItem[]; +} + +/** 财务报表查询参数 */ +export interface ReportQueryParams { + /** 起始月份 yyyy-MM */ + beginDate: string; + /** 结束月份 yyyy-MM */ + endDate: string; +} + +export interface ApiResult { + code: number; + msg: string; + data: T; +} diff --git a/src/api/roles/index.ts b/src/api/roles/index.ts index fcda64f..cf092e5 100644 --- a/src/api/roles/index.ts +++ b/src/api/roles/index.ts @@ -2,10 +2,6 @@ * 角色管理 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, @@ -26,31 +22,17 @@ export type { 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); + return get('/admin/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); } @@ -58,11 +40,6 @@ export async function getRoleUsageUserList( 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('/sys/role/permissionList', { roleId: String(roleId) }); } diff --git a/src/api/roles/types.ts b/src/api/roles/types.ts index 7dc35f7..b4b6219 100644 --- a/src/api/roles/types.ts +++ b/src/api/roles/types.ts @@ -41,6 +41,19 @@ export interface RoleUsageUserQueryParams { roleId?: string; } +/** 保存角色 — POST /admin/sys/role/save */ +export interface RoleSaveParams { + name: string; + menuIds: string[]; +} + +/** 更新角色 — POST /admin/sys/role/update */ +export interface RoleUpdateParams { + id: string; + name: string; + menuIds: string[]; +} + export interface PageData { total: number; list: T[]; diff --git a/src/api/users/index.ts b/src/api/users/index.ts index d76ac08..73efc60 100644 --- a/src/api/users/index.ts +++ b/src/api/users/index.ts @@ -2,8 +2,6 @@ * 用户管理 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, @@ -26,43 +24,33 @@ export type { ApiResult, } from './types'; -const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); - -/** GET /sys/user/page — 用户分页 */ +/** GET /admin/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); + return get('/admin/sys/user/page', params as Record); } -/** POST /sys/user/save — 新增用户 */ +/** POST /admin/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); + return post('/admin/sys/user/save', params); } -/** POST /sys/user/update — 更新用户 */ +/** POST /admin/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); + return post('/admin/sys/user/update', params); } /** POST /sys/user/active — 启用/禁用(直接调接口) */ export function toggleUserActive( params: UserActiveParams, ): Promise>> { - return post('/sys/user/active', params); + return post('/admin/sys/user/active', params); } /** POST /sys/user/updatepwd — 修改密码(直接调接口) */ export function updateUserPwd( params: UserUpdatePwdParams, ): Promise>> { - return post('/sys/user/updatepwd', params); + return post('/admin/sys/user/updatepwd', params); } diff --git a/src/api/wallet/index.ts b/src/api/wallet/index.ts index 9d605d5..9b323b3 100644 --- a/src/api/wallet/index.ts +++ b/src/api/wallet/index.ts @@ -3,12 +3,10 @@ * OpenAPI: GET /admin/finance/wallet/summary, GET /admin/finance/wallet/page */ import { get } from '@/utils/request'; -import { USE_MOCK, MOCK_DELAY } from '@/config/mock'; -import { buildMockWalletPage } from '@/config/mock/walletList'; -import { buildMockWalletTransactionPage } from '@/config/mock/walletTransactions'; import type { WalletQueryParams, WalletSummaryVO, + WalletSummaryPageData, WalletTransactionQueryParams, WalletTransactionVO, PageData, @@ -17,6 +15,7 @@ import type { export type { WalletSummaryVO, + WalletSummaryPageData, WalletQueryParams, WalletTransactionVO, WalletTransactionQueryParams, @@ -24,18 +23,10 @@ export type { ApiResult, } from './types'; -const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); - /** GET /admin/finance/wallet/summary — 钱包列表分页 */ export async function getWalletList( params: WalletQueryParams, -): 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: buildMockWalletPage(page, limit, params) }; - } +): Promise> { return get('/finance/wallet/summary', params as Record); } @@ -43,11 +34,5 @@ export async function getWalletList( export async function getWalletTransactionList( params: WalletTransactionQueryParams, ): 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: buildMockWalletTransactionPage(page, limit, params) }; - } return get('/finance/wallet/page', params as Record); } diff --git a/src/api/wallet/types.ts b/src/api/wallet/types.ts index 5f993e9..a0dc2aa 100644 --- a/src/api/wallet/types.ts +++ b/src/api/wallet/types.ts @@ -17,14 +17,25 @@ export interface WalletSummaryVO { frozenAmount: string; /** 累计成功提现金额("0.00"格式) */ withdrawalAmount: string; - /** 汇总行-总余额 */ +} + +/** 钱包汇总扩展数据(与 total 同级) */ +export interface WalletSummaryExData { + /** 总余额 */ totalAmount: string; - /** 汇总行-总冻结金额 */ + /** 总冻结金额 */ totalFrozenAmount: string; - /** 汇总行-总累计提现 */ + /** 总累计提现 */ totalWithdrawalAmount: string; } +/** 钱包列表分页响应 */ +export interface WalletSummaryPageData { + total: number; + list: WalletSummaryVO[]; + exData: WalletSummaryExData; +} + /** 钱包列表查询参数 */ export interface WalletQueryParams { page?: string; diff --git a/src/api/withdraw/index.ts b/src/api/withdraw/index.ts index 4c2c20b..13f7d54 100644 --- a/src/api/withdraw/index.ts +++ b/src/api/withdraw/index.ts @@ -3,39 +3,20 @@ * 对应 OpenAPI: GET /admin/finance/withdraw/page, GET /admin/finance/withdraw/info */ import { get, post } from '@/utils/request'; -import { USE_MOCK, MOCK_DELAY } from '@/config/mock'; -import { buildMockWithdrawPage } from '@/config/mock/withdrawList'; -import { buildMockWithdrawInfo } from '@/config/mock/withdrawInfo'; import type { WithdrawQueryParams, WithdrawVO, WithdrawInfoVO, PageData, ApiResult } from './types'; export type { WithdrawVO, WithdrawInfoVO, WithdrawQueryParams, PageData, ApiResult } from './types'; -const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); - -/** GET /finance/withdraw/page — 提现列表分页 */ +/** GET /admin/finance/withdraw/page — 提现列表分页 */ export async function getWithdrawList( params: WithdrawQueryParams, ): 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: buildMockWithdrawPage(page, limit, params), - }; - } - return get('/finance/withdraw/page', params as Record); + return get('/admin/finance/withdraw/page', params as Record); } -/** GET /finance/withdraw/info?id=xxx — 提现详情 */ +/** GET /admin/finance/withdraw/info?id=xxx — 提现详情 */ export async function getWithdrawInfo(id: number): Promise> { - if (USE_MOCK) { - await delay(MOCK_DELAY); - return { code: 200, msg: 'success', data: buildMockWithdrawInfo(id) }; - } - return get('/finance/withdraw/info', { id: String(id) }); + return get('/admin/finance/withdraw/info', { id: String(id) }); } /** 提现审核请求参数 */ @@ -50,7 +31,7 @@ export interface WithdrawAuditParams { checkImg: string; } -/** POST /finance/withdraw/audit — 审核提现 */ +/** POST /admin/finance/withdraw/audit — 审核提现 */ export async function postWithdrawAudit(params: WithdrawAuditParams): Promise> { - return post('/finance/withdraw/audit', params); + return post('/admin/finance/withdraw/audit', params); } diff --git a/src/components/ChangePasswordModal/index.tsx b/src/components/ChangePasswordModal/index.tsx index 115a973..faef592 100644 --- a/src/components/ChangePasswordModal/index.tsx +++ b/src/components/ChangePasswordModal/index.tsx @@ -1,4 +1,4 @@ -import { defineComponent, ref, reactive } from 'vue'; +import { defineComponent, ref, reactive, nextTick } from 'vue'; import { useEffect } from '@/hooks'; import { Modal, Form, Input, Button, message } from 'ant-design-vue'; import { TIPS_TEXT, newPasswordRules, confirmNewPasswordRules } from './controller'; @@ -34,7 +34,7 @@ export default defineComponent({ useEffect(() => { if (props.visible) { Object.assign(formData, getDefaultForm()); - setTimeout(() => formRef.value?.clearValidate(), 0); + nextTick().then(() => formRef.value?.clearValidate()); } }, [() => props.visible]); @@ -67,9 +67,12 @@ export default defineComponent({
{ + formData.newPassword = val; + }} placeholder="至少8位,含字母+数字" allowClear - v-model:value={formData.newPassword} /> @@ -79,9 +82,12 @@ export default defineComponent({ rules={confirmNewPasswordRules(() => formData.newPassword)} > { + formData.confirmPassword = val; + }} placeholder="再次输入新密码" allowClear - v-model:value={formData.confirmPassword} /> diff --git a/src/config/mock/bannerList.ts b/src/config/mock/bannerList.ts deleted file mode 100644 index ea7a532..0000000 --- a/src/config/mock/bannerList.ts +++ /dev/null @@ -1,169 +0,0 @@ -/** - * 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/operationLogs.ts b/src/config/mock/operationLogs.ts deleted file mode 100644 index 397132f..0000000 --- a/src/config/mock/operationLogs.ts +++ /dev/null @@ -1,182 +0,0 @@ -/** - * 操作日志假数据 - * 对应接口:GET /admin/sys/operation/page - * - * 类型内联,避免与 @/api/logs 循环引用。 - */ -interface MockSysOperationLogVO { - type: number; - nickname: string; - createDate: string; - content: string; -} - -interface MockOperationLogQueryParams { - type?: string; - nickname?: string; - dateBegin?: string; - dateEnd?: string; -} - -interface MockPageData { - total: number; - list: T[]; -} - -const MOCK_LOGS: MockSysOperationLogVO[] = [ - { - 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); - if (begin && d < begin) return false; - if (end && d > end) return false; - return true; -} - -export function buildMockOperationLogPage( - page: number, - limit: number, - params: MockOperationLogQueryParams, -): MockPageData { - let filtered = [...MOCK_LOGS]; - - 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 deleted file mode 100644 index 24dae23..0000000 --- a/src/config/mock/orderDetail.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * 订单详情弹窗假数据 - * - * 使用页面:`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 deleted file mode 100644 index 8e66a44..0000000 --- a/src/config/mock/orderList.ts +++ /dev/null @@ -1,256 +0,0 @@ -/** - * 订单列表页假数据 - * - * 使用页面:`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 deleted file mode 100644 index 287e20a..0000000 --- a/src/config/mock/orderPlayerList.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * 订单详情 — 选手列表假数据 - * - * 使用页面:`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/paymentFlow.ts b/src/config/mock/paymentFlow.ts deleted file mode 100644 index 1ddc00e..0000000 --- a/src/config/mock/paymentFlow.ts +++ /dev/null @@ -1,149 +0,0 @@ -/** - * 支付流水假数据 - * 对应接口:GET /admin/finance/paymentFlow/page - */ -import type { PaymentFlowVO, PaymentFlowQueryParams, PageData } from '@/api/payments'; - -const MOCK_LIST: PaymentFlowVO[] = [ - { - serialNo: 'FL20260730001', - orderNo: 'OD20260730001', - type: 1, - nickname: '张三', - phone: '12345678997', - amount: '99.00', - createDate: '2026-07-30 08:00:00', - }, - { - serialNo: '', - orderNo: '', - type: 2, - nickname: '李四', - phone: '12345678998', - amount: '100.00', - createDate: '2026-07-30 07:00:00', - }, - { - serialNo: 'FL20260729002', - orderNo: 'OD20260729002', - type: 1, - nickname: '王五', - phone: '12345678998', - amount: '199.00', - createDate: '2026-07-29 16:00:00', - }, - { - serialNo: '', - orderNo: '', - type: 2, - nickname: '赵六', - phone: '13800138000', - amount: '50.00', - createDate: '2026-07-29 15:20:00', - }, - { - serialNo: 'FL20260728003', - orderNo: 'OD20260728003', - type: 1, - nickname: '钱七', - phone: '13900139001', - amount: '350.00', - createDate: '2026-07-28 14:00:00', - }, - { - serialNo: '', - orderNo: '', - type: 2, - nickname: '孙八', - phone: '18600186001', - amount: '120.00', - createDate: '2026-07-28 10:30:00', - }, - { - serialNo: 'FL20260727004', - orderNo: 'OD20260727004', - type: 1, - nickname: '周九', - phone: '15200152000', - amount: '88.00', - createDate: '2026-07-27 09:00:00', - }, - { - serialNo: 'FL20260727005', - orderNo: 'OD20260727005', - type: 1, - nickname: '吴十', - phone: '17000170001', - amount: '520.00', - createDate: '2026-07-27 08:00:00', - }, - { - serialNo: '', - orderNo: '', - type: 2, - nickname: '李四', - phone: '12345678998', - amount: '200.00', - createDate: '2026-07-26 17:00:00', - }, - { - serialNo: 'FL20260726006', - orderNo: 'OD20260726006', - type: 1, - nickname: '张三', - phone: '12345678997', - amount: '128.00', - createDate: '2026-07-26 11:00:00', - }, - { - serialNo: '', - orderNo: '', - type: 2, - nickname: '王五', - phone: '12345678998', - amount: '80.00', - createDate: '2026-07-25 13:00:00', - }, - { - serialNo: 'FL20260725007', - orderNo: 'OD20260725007', - type: 1, - nickname: '赵六', - phone: '13800138000', - amount: '680.00', - createDate: '2026-07-25 10:00:00', - }, -]; - -function inDateRange(date: string, begin?: string, end?: string): boolean { - if (!begin && !end) return true; - const d = date.slice(0, 10); - if (begin && d < begin) return false; - if (end && d > end) return false; - return true; -} - -export function buildMockPaymentFlowPage( - page: number, - limit: number, - params: PaymentFlowQueryParams, -): PageData { - let filtered = [...MOCK_LIST]; - - if (params.nickname) { - filtered = filtered.filter((item) => item.nickname.includes(params.nickname!)); - } - if (params.phone) { - filtered = filtered.filter((item) => item.phone.includes(params.phone!)); - } - if (params.type) { - filtered = filtered.filter((item) => item.type === Number(params.type)); - } - filtered = filtered.filter((item) => - inDateRange(item.createDate, params.createDateBegin, params.createDateEnd), - ); - - const total = filtered.length; - const start = (page - 1) * limit; - return { total, list: filtered.slice(start, start + limit) }; -} diff --git a/src/config/mock/permissionTree.ts b/src/config/mock/permissionTree.ts deleted file mode 100644 index d26d7b4..0000000 --- a/src/config/mock/permissionTree.ts +++ /dev/null @@ -1,213 +0,0 @@ -/** - * 角色权限树假数据 - * 对应接口: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 deleted file mode 100644 index 1a1ff28..0000000 --- a/src/config/mock/roleList.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * 角色列表页假数据(类型内联,避免循环引用) - * 对应接口: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 deleted file mode 100644 index b7e0299..0000000 --- a/src/config/mock/roleUsageUser.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * 角色下用户列表假数据 - * 对应接口: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 deleted file mode 100644 index 57fb63b..0000000 --- a/src/config/mock/userList.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** - * 用户列表页假数据(类型内联,避免循环引用) - * 对应接口: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/config/mock/walletList.ts b/src/config/mock/walletList.ts deleted file mode 100644 index afd273c..0000000 --- a/src/config/mock/walletList.ts +++ /dev/null @@ -1,185 +0,0 @@ -/** - * 钱包列表假数据 - * 对应接口:GET /admin/finance/wallet/summary - */ -import type { WalletSummaryVO, WalletQueryParams, PageData } from '@/api/wallet'; - -const MOCK_LIST: WalletSummaryVO[] = [ - { - userId: 1001, - userNickname: '张三', - phone: '12345678997', - amount: '0.00', - frozenAmount: '0.00', - withdrawalAmount: '0.00', - totalAmount: '0.00', - totalFrozenAmount: '0.00', - totalWithdrawalAmount: '0.00', - }, - { - userId: 1002, - userNickname: '李四', - phone: '12345678998', - amount: '350.00', - frozenAmount: '200.00', - withdrawalAmount: '1450.00', - totalAmount: '0.00', - totalFrozenAmount: '0.00', - totalWithdrawalAmount: '0.00', - }, - { - userId: 1003, - userNickname: '王五', - phone: '12345678999', - amount: '1280.00', - frozenAmount: '0.00', - withdrawalAmount: '800.00', - totalAmount: '0.00', - totalFrozenAmount: '0.00', - totalWithdrawalAmount: '0.00', - }, - { - userId: 1004, - userNickname: '赵六', - phone: '13800138000', - amount: '560.00', - frozenAmount: '100.00', - withdrawalAmount: '200.00', - totalAmount: '0.00', - totalFrozenAmount: '0.00', - totalWithdrawalAmount: '0.00', - }, - { - userId: 1005, - userNickname: '钱七', - phone: '13900139001', - amount: '4200.00', - frozenAmount: '1500.00', - withdrawalAmount: '5600.00', - totalAmount: '0.00', - totalFrozenAmount: '0.00', - totalWithdrawalAmount: '0.00', - }, - { - userId: 1006, - userNickname: '孙八', - phone: '12345678997', - amount: '880.00', - frozenAmount: '0.00', - withdrawalAmount: '300.00', - totalAmount: '0.00', - totalFrozenAmount: '0.00', - totalWithdrawalAmount: '0.00', - }, - { - userId: 1007, - userNickname: '周九', - phone: '17000170001', - amount: '2100.00', - frozenAmount: '800.00', - withdrawalAmount: '3200.00', - totalAmount: '0.00', - totalFrozenAmount: '0.00', - totalWithdrawalAmount: '0.00', - }, - { - userId: 1008, - userNickname: '吴十', - phone: '18900189000', - amount: '650.00', - frozenAmount: '0.00', - withdrawalAmount: '450.00', - totalAmount: '0.00', - totalFrozenAmount: '0.00', - totalWithdrawalAmount: '0.00', - }, - { - userId: 1009, - userNickname: '郑十一', - phone: '15200152000', - amount: '980.00', - frozenAmount: '200.00', - withdrawalAmount: '1200.00', - totalAmount: '0.00', - totalFrozenAmount: '0.00', - totalWithdrawalAmount: '0.00', - }, - { - userId: 1010, - userNickname: '冯十二', - phone: '18600186001', - amount: '320.00', - frozenAmount: '50.00', - withdrawalAmount: '100.00', - totalAmount: '0.00', - totalFrozenAmount: '0.00', - totalWithdrawalAmount: '0.00', - }, - { - userId: 1011, - userNickname: '褚十三', - phone: '13500135000', - amount: '5800.00', - frozenAmount: '0.00', - withdrawalAmount: '2000.00', - totalAmount: '0.00', - totalFrozenAmount: '0.00', - totalWithdrawalAmount: '0.00', - }, - { - userId: 1012, - userNickname: '卫十四', - phone: '13700137000', - amount: '75.00', - frozenAmount: '0.00', - withdrawalAmount: '75.00', - totalAmount: '0.00', - totalFrozenAmount: '0.00', - totalWithdrawalAmount: '0.00', - }, -]; - -/** 计算汇总值 */ -function computeSummary(list: WalletSummaryVO[]) { - const totalAmount = list.reduce((s, i) => s + Number(i.amount || 0), 0).toFixed(2); - const totalFrozenAmount = list.reduce((s, i) => s + Number(i.frozenAmount || 0), 0).toFixed(2); - const totalWithdrawalAmount = list - .reduce((s, i) => s + Number(i.withdrawalAmount || 0), 0) - .toFixed(2); - return { totalAmount, totalFrozenAmount, totalWithdrawalAmount }; -} - -export function buildMockWalletPage( - page: number, - limit: number, - params: WalletQueryParams, -): PageData { - let filtered = [...MOCK_LIST]; - - if (params.nickname) { - filtered = filtered.filter((item) => item.userNickname.includes(params.nickname!)); - } - if (params.phone) { - filtered = filtered.filter((item) => item.phone.includes(params.phone!)); - } - if (params.amountBegin) { - filtered = filtered.filter((item) => Number(item.amount) >= Number(params.amountBegin)); - } - if (params.amountEnd) { - filtered = filtered.filter((item) => Number(item.amount) <= Number(params.amountEnd)); - } - - const total = filtered.length; - const start = (page - 1) * limit; - const pageList = filtered.slice(start, start + limit); - - // 将汇总值写入每条记录(方便前端提取统计卡数据) - const summary = computeSummary(filtered); - for (const item of pageList) { - item.totalAmount = summary.totalAmount; - item.totalFrozenAmount = summary.totalFrozenAmount; - item.totalWithdrawalAmount = summary.totalWithdrawalAmount; - } - - return { total, list: pageList }; -} diff --git a/src/config/mock/walletTransactions.ts b/src/config/mock/walletTransactions.ts deleted file mode 100644 index e597b1b..0000000 --- a/src/config/mock/walletTransactions.ts +++ /dev/null @@ -1,187 +0,0 @@ -/** - * 钱包交易明细假数据 - * 对应接口:GET /admin/finance/wallet/page - * - * type: 1=报名入账, 2=提现出账, 3=退款出账 - * status(由 freezeAmount/freezeTime 推导): 1=已解冻, 2=冻结中, 3=正常 - */ -import type { WalletTransactionVO, WalletTransactionQueryParams, PageData } from '@/api/wallet'; - -const MOCK_TRANSACTIONS: (WalletTransactionVO & { status: number })[] = [ - { - type: 1, - amount: '+100.00', - afterAmount: '600.00', - withdrawAmount: '500.00', - freezeAmount: '100.00', - status: 1, - freezeTime: '2026-06-10 08:00', - unfreezeTime: '2026-06-12 08:00', - orderNo: 'SN20260610001', - createDate: '2026-06-10 08:00', - }, - { - type: 2, - amount: '-500.00', - afterAmount: '1100.00', - withdrawAmount: '500.00', - freezeAmount: '600.00', - status: 2, - freezeTime: '2026-06-10 06:00', - unfreezeTime: '', - orderNo: 'SN20260610002', - createDate: '2026-06-10 06:00', - }, - { - type: 3, - amount: '-100.00', - afterAmount: '500.00', - withdrawAmount: '500.00', - freezeAmount: '0.00', - status: 3, - freezeTime: '', - unfreezeTime: '', - orderNo: 'SN20260610003', - createDate: '2026-06-10 05:00', - }, - { - type: 1, - amount: '+350.00', - afterAmount: '850.00', - withdrawAmount: '750.00', - freezeAmount: '100.00', - status: 3, - freezeTime: '', - unfreezeTime: '', - orderNo: 'SN20260609001', - createDate: '2026-06-09 17:30', - }, - { - type: 2, - amount: '-250.00', - afterAmount: '600.00', - withdrawAmount: '500.00', - freezeAmount: '100.00', - status: 1, - freezeTime: '2026-06-08 10:00', - unfreezeTime: '2026-06-09 08:00', - orderNo: 'SN20260608001', - createDate: '2026-06-08 10:00', - }, - { - type: 1, - amount: '+800.00', - afterAmount: '2000.00', - withdrawAmount: '1800.00', - freezeAmount: '200.00', - status: 3, - freezeTime: '', - unfreezeTime: '', - orderNo: 'SN20260607001', - createDate: '2026-06-07 14:00', - }, - { - type: 2, - amount: '-300.00', - afterAmount: '1200.00', - withdrawAmount: '1000.00', - freezeAmount: '200.00', - status: 2, - freezeTime: '2026-06-06 09:00', - unfreezeTime: '', - orderNo: 'SN20260606001', - createDate: '2026-06-06 09:00', - }, - { - type: 3, - amount: '-50.00', - afterAmount: '1500.00', - withdrawAmount: '1300.00', - freezeAmount: '200.00', - status: 1, - freezeTime: '2026-06-05 10:00', - unfreezeTime: '2026-06-07 10:00', - orderNo: 'SN20260605001', - createDate: '2026-06-05 10:00', - }, - { - type: 1, - amount: '+200.00', - afterAmount: '1550.00', - withdrawAmount: '1350.00', - freezeAmount: '200.00', - status: 3, - freezeTime: '', - unfreezeTime: '', - orderNo: 'SN20260604001', - createDate: '2026-06-04 08:30', - }, - { - type: 2, - amount: '-600.00', - afterAmount: '950.00', - withdrawAmount: '750.00', - freezeAmount: '200.00', - status: 1, - freezeTime: '2026-06-03 16:00', - unfreezeTime: '2026-06-05 16:00', - orderNo: 'SN20260603001', - createDate: '2026-06-03 16:00', - }, - { - type: 1, - amount: '+450.00', - afterAmount: '1050.00', - withdrawAmount: '850.00', - freezeAmount: '200.00', - status: 1, - freezeTime: '2026-06-02 11:00', - unfreezeTime: '2026-06-04 11:00', - orderNo: 'SN20260602001', - createDate: '2026-06-02 11:00', - }, - { - type: 3, - amount: '-120.00', - afterAmount: '600.00', - withdrawAmount: '400.00', - freezeAmount: '200.00', - status: 3, - freezeTime: '', - unfreezeTime: '', - orderNo: 'SN20260601001', - createDate: '2026-06-01 15:00', - }, -]; - -function inDateRange(date: string, begin?: string, end?: string): boolean { - if (!begin && !end) return true; - const d = date.slice(0, 10); - if (begin && d < begin) return false; - if (end && d > end) return false; - return true; -} - -export function buildMockWalletTransactionPage( - page: number, - limit: number, - params: WalletTransactionQueryParams, -): PageData { - let filtered = [...MOCK_TRANSACTIONS]; - - if (params.type) { - filtered = filtered.filter((item) => item.type === Number(params.type)); - } - if (params.status) { - filtered = filtered.filter((item) => item.status === Number(params.status)); - } - filtered = filtered.filter((item) => - inDateRange(item.createDate, params.createDateBegin, params.createDateEnd), - ); - - const total = filtered.length; - const start = (page - 1) * limit; - const list = filtered.slice(start, start + limit).map(({ status, ...rest }) => rest); - - return { total, list }; -} diff --git a/src/config/mock/withdrawInfo.ts b/src/config/mock/withdrawInfo.ts deleted file mode 100644 index feb23e4..0000000 --- a/src/config/mock/withdrawInfo.ts +++ /dev/null @@ -1,150 +0,0 @@ -/** - * 提现详情假数据 - * 对应接口:GET /admin/finance/withdraw/info?id=xxx - * - * 字段对齐 TournamentFinanceWithdrawInfoVO。 - */ -import type { WithdrawInfoVO } from '@/api/withdraw'; - -/** 银行卡类型映射 */ -const BANK_CARD_TYPE_MAP: Record = { 1: '个人' }; - -const MOCK_DETAILS: Record = { - 1: { - nickname: '可乐', - phone: '17762466262', - realName: '可乐', - createDate: '2026-07-30 09:53:00', - withdrawType: '银行卡', - withdrawAmount: '1000.00', - feeRate: '0.006', - feeAmount: '6.00', - receivedAmount: '994.00', - auditStatus: 0, - payStatus: 0, - bankCardType: 1, - cardHolderName: '龚', - bankCardNumber: '6215581807006475126', - bankName: '交通银行', - branchName: '交通银行宜昌西坝支行', - payTime: '', - orderNo: 'WD04111775872421935100', - auditDate: '', - auditNickname: '', - auditImg: '', - auditImgList: [], - }, - 2: { - nickname: '李四', - phone: '12345678998', - realName: '李四', - createDate: '2026-07-29 14:30:00', - withdrawType: '银行卡', - withdrawAmount: '200.00', - feeRate: '0.006', - feeAmount: '1.20', - receivedAmount: '198.80', - auditStatus: 1, - payStatus: 1, - bankCardType: 1, - cardHolderName: '李四', - bankCardNumber: '6222021234567890123', - bankName: '工商银行', - branchName: '工商银行北京中关村支行', - payTime: '2026-07-29 14:35:00', - orderNo: 'SN20260729001', - auditDate: '2026-07-29 14:32:00', - auditNickname: '管理员A', - auditImg: '', - auditImgList: [ - 'https://via.placeholder.com/200x200?text=Audit1', - 'https://via.placeholder.com/200x200?text=Audit2', - ], - }, - 3: { - nickname: '王五', - phone: '13800138000', - realName: '王五', - createDate: '2026-07-28 11:15:00', - withdrawType: '银行卡', - withdrawAmount: '500.00', - feeRate: '0.006', - feeAmount: '3.00', - receivedAmount: '497.00', - auditStatus: 1, - payStatus: 2, - bankCardType: 1, - cardHolderName: '王五', - bankCardNumber: '微信钱包', - bankName: '微信支付', - branchName: '-', - payTime: '', - orderNo: 'SN20260728002', - auditDate: '2026-07-28 11:20:00', - auditNickname: '管理员B', - auditImg: '', - auditImgList: [], - }, - 4: { - nickname: '赵六', - phone: '13900139001', - realName: '赵六', - createDate: '2026-07-27 09:45:00', - withdrawType: '银行卡', - withdrawAmount: '80.00', - feeRate: '0.006', - feeAmount: '0.48', - receivedAmount: '79.52', - auditStatus: 2, - payStatus: 0, - bankCardType: 1, - cardHolderName: '赵六', - bankCardNumber: '6217858000123456789', - bankName: '建设银行', - branchName: '建设银行上海陆家嘴支行', - payTime: '', - orderNo: '', - auditDate: '2026-07-27 10:00:00', - auditNickname: '管理��A', - auditImg: '', - auditImgList: ['https://via.placeholder.com/200x200?text=Reject'], - }, - 5: { - nickname: '孙七', - phone: '18600186001', - realName: '孙七', - createDate: '2026-07-26 16:00:00', - withdrawType: '银行卡', - withdrawAmount: '3000.00', - feeRate: '0.006', - feeAmount: '18.00', - receivedAmount: '2982.00', - auditStatus: 0, - payStatus: 0, - bankCardType: 1, - cardHolderName: '孙七', - bankCardNumber: '6228480012345678910', - bankName: '农业银行', - branchName: '农业银行深圳福田支行', - payTime: '', - orderNo: 'WD05261775872421935200', - auditDate: '', - auditNickname: '', - auditImg: '', - auditImgList: [], - }, -}; - -export function buildMockWithdrawInfo(id: number): WithdrawInfoVO { - const detail = MOCK_DETAILS[id]; - if (detail) return { ...detail }; - - // 找不到对应 ID 时返回一个默认详情(基于 ID 4 的结构) - return { - ...MOCK_DETAILS[4], - nickname: `用户${id}`, - phone: `1380000${String(id).padStart(4, '0')}`, - realName: `用户${id}`, - createDate: '2026-07-27 09:45:00', - }; -} diff --git a/src/config/mock/withdrawList.ts b/src/config/mock/withdrawList.ts deleted file mode 100644 index aa1c268..0000000 --- a/src/config/mock/withdrawList.ts +++ /dev/null @@ -1,194 +0,0 @@ -/** - * 提现列表假数据 - * 对应接口:GET /admin/finance/withdraw/page - * - * 前端筛选 + 分页,模拟后端行为。 - */ -import type { WithdrawVO, WithdrawQueryParams, PageData } from '@/api/withdraw'; - -const MOCK_LIST: WithdrawVO[] = [ - { - id: 1, - nickname: '可乐', - phone: '17762466262', - realName: '可乐', - withdrawType: '银行卡', - withdrawAmount: '1000.00', - receivedAmount: '994.00', - auditStatus: 0, - payStatus: 0, - serialNumber: '', - createDate: '2026-07-30 09:53:00', - }, - { - id: 2, - nickname: '李四', - phone: '12345678998', - realName: '李四', - withdrawType: '银行卡', - withdrawAmount: '200.00', - receivedAmount: '198.80', - auditStatus: 1, - payStatus: 1, - serialNumber: 'SN20260729001', - createDate: '2026-07-29 14:30:00', - }, - { - id: 3, - nickname: '王五', - phone: '13800138000', - realName: '王五', - withdrawType: '银行卡', - withdrawAmount: '500.00', - receivedAmount: '497.00', - auditStatus: 1, - payStatus: 2, - serialNumber: 'SN20260728002', - createDate: '2026-07-28 11:15:00', - }, - { - id: 4, - nickname: '赵六', - phone: '13900139001', - realName: '赵六', - withdrawType: '银行卡', - withdrawAmount: '80.00', - receivedAmount: '79.52', - auditStatus: 2, - payStatus: 0, - serialNumber: '', - createDate: '2026-07-27 09:45:00', - }, - { - id: 5, - nickname: '孙七', - phone: '18600186001', - realName: '孙七', - withdrawType: '银行卡', - withdrawAmount: '3000.00', - receivedAmount: '2982.00', - auditStatus: 0, - payStatus: 0, - serialNumber: '', - createDate: '2026-07-26 16:00:00', - }, - { - id: 6, - nickname: '周八', - phone: '15200152000', - realName: '周八', - withdrawType: '银行卡', - withdrawAmount: '150.00', - receivedAmount: '149.10', - auditStatus: 1, - payStatus: 1, - serialNumber: 'SN20260725003', - createDate: '2026-07-25 10:20:00', - }, - { - id: 7, - nickname: '吴九', - phone: '17000170001', - realName: '吴九', - withdrawType: '银行卡', - withdrawAmount: '600.00', - receivedAmount: '596.40', - auditStatus: 2, - payStatus: 0, - serialNumber: '', - createDate: '2026-07-24 08:30:00', - }, - { - id: 8, - nickname: '郑十', - phone: '18900189000', - realName: '郑十', - withdrawType: '银行卡', - withdrawAmount: '2200.00', - receivedAmount: '2186.80', - auditStatus: 0, - payStatus: 0, - serialNumber: '', - createDate: '2026-07-23 13:10:00', - }, - { - id: 9, - nickname: '可乐', - phone: '17762466262', - realName: '可乐', - withdrawType: '银行卡', - withdrawAmount: '450.00', - receivedAmount: '447.30', - auditStatus: 1, - payStatus: 2, - serialNumber: 'SN20260722004', - createDate: '2026-07-22 15:45:00', - }, - { - id: 10, - nickname: '李四', - phone: '12345678998', - realName: '李四', - withdrawType: '银行卡', - withdrawAmount: '1800.00', - receivedAmount: '1789.20', - auditStatus: 0, - payStatus: 0, - serialNumber: '', - createDate: '2026-07-21 09:00:00', - }, - { - id: 11, - nickname: '王五', - phone: '13800138000', - realName: '王五', - withdrawType: '银行卡', - withdrawAmount: '320.00', - receivedAmount: '318.08', - auditStatus: 1, - payStatus: 1, - serialNumber: 'SN20260720005', - createDate: '2026-07-20 17:30:00', - }, - { - id: 12, - nickname: '孙七', - phone: '18600186001', - realName: '孙七', - withdrawType: '银行卡', - withdrawAmount: '750.00', - receivedAmount: '745.50', - auditStatus: 2, - payStatus: 0, - serialNumber: '', - createDate: '2026-07-19 11:00:00', - }, -]; - -export function buildMockWithdrawPage( - page: number, - limit: number, - params: WithdrawQueryParams, -): PageData { - let filtered = [...MOCK_LIST]; - - if (params.nickname) { - filtered = filtered.filter((item) => item.nickname.includes(params.nickname!)); - } - if (params.phone) { - filtered = filtered.filter((item) => item.phone.includes(params.phone!)); - } - if (params.realName) { - filtered = filtered.filter((item) => item.realName.includes(params.realName!)); - } - if (params.auditStatus) { - filtered = filtered.filter((item) => item.auditStatus === Number(params.auditStatus)); - } - if (params.payStatus) { - filtered = filtered.filter((item) => item.payStatus === Number(params.payStatus)); - } - - const total = filtered.length; - const start = (page - 1) * limit; - return { total, list: filtered.slice(start, start + limit) }; -} diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 677b41e..8a81d87 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -1,14 +1,18 @@ // src/hooks/useAuth.ts +import { notification } from 'ant-design-vue'; import { useState } from './useState'; import { useEffect } from './useEffect'; import { useMenuStore } from '@/stores/menuStore'; import { usePermissionStore } from '@/stores/permissionStore'; import { useTabsStore } from '@/stores/tabsStore'; -import { clearAuthDataCache } from '@/api/menu'; +import { useUserStore } from '@/stores/userStore'; +import { clearAuthDataCache } from '@/api/authCache'; import router from '@/router'; const TOKEN_KEY = 'MY_APP_AUTH_TOKEN'; +let isForceLoggingOut = false; + function createAuth() { const [token, setToken] = useState(window.localStorage.getItem(TOKEN_KEY)); @@ -23,10 +27,26 @@ function createAuth() { const isLoggedIn = () => !!token.value; + const clearSession = () => { + setToken(null); + + const { clearUser } = useUserStore(); + const { clearMenu } = useMenuStore(); + const { clearPermissions } = usePermissionStore(); + const { clearTabs } = useTabsStore(); + + clearUser(); + clearTabs(); + clearMenu(); + clearPermissions(); + clearAuthDataCache(); + }; + /** * 登录:保存 token → 拉取菜单+权限 → 动态注册路由 → 跳转首页 */ const login = async (newToken: string, targetPath?: string) => { + isForceLoggingOut = false; setToken(newToken); const { loadMenu, homePath, isRoutePathAvailable } = useMenuStore(); @@ -35,6 +55,9 @@ function createAuth() { // 并行拉取菜单和权限 await Promise.all([loadMenu(), loadPermissions()]); + // 权限获取失败时会清空会话,此时不再跳转首页 + if (!isLoggedIn()) return; + const nextPath = targetPath && isRoutePathAvailable(targetPath) ? targetPath : homePath.value || '/404'; @@ -46,17 +69,7 @@ function createAuth() { * 退出:清除 token → 清除菜单+权限 → 清除动态路由 → 跳转登录页 */ const logout = () => { - setToken(null); - - const { clearMenu } = useMenuStore(); - const { clearPermissions } = usePermissionStore(); - const { clearTabs } = useTabsStore(); - - clearTabs(); - clearMenu(); - clearPermissions(); - clearAuthDataCache(); - + clearSession(); router.push('/login'); }; @@ -65,7 +78,25 @@ function createAuth() { isLoggedIn, login, logout, + clearSession, }; } export const auth = createAuth(); + +/** + * 强制重新登录:提示错误 → 清空会话 → 跳转登录页 + * 用于权限获取失败、登录态失效等需要彻底重置会话的场景 + */ +export function forceReLogin(msg = '账户权限获取失败,请重新登录') { + if (isForceLoggingOut) return; + isForceLoggingOut = true; + + notification.error({ + message: '提示', + description: msg, + duration: 2600, + }); + auth.clearSession(); + router.push('/login'); +} diff --git a/src/hooks/usePagination.ts b/src/hooks/usePagination.ts index d3a0a13..c44340a 100644 --- a/src/hooks/usePagination.ts +++ b/src/hooks/usePagination.ts @@ -180,3 +180,13 @@ export function usePagination(options: UsePaginationOptions = {}): UsePagination refresh, }; } + +/** 将接口返回的分页 total 同步到 pagination(支持 number / 数字字符串,含 0) */ +export function syncPaginationTotal( + pagination: Pick, + total: unknown, +): void { + if (total == null || total === '') return; + const n = Number(total); + if (Number.isFinite(n)) pagination.setTotal(n); +} diff --git a/src/layouts/BasicLayout.module.less b/src/layouts/BasicLayout.module.less index 2a25135..bf59f0e 100644 --- a/src/layouts/BasicLayout.module.less +++ b/src/layouts/BasicLayout.module.less @@ -62,7 +62,7 @@ align-items: center; } - .userPhone { + .displayName { font-size: 14px; color: rgba(0, 0, 0, 0.65); } diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx index fc6064b..bcbd633 100644 --- a/src/layouts/BasicLayout.tsx +++ b/src/layouts/BasicLayout.tsx @@ -78,7 +78,8 @@ export default defineComponent({ const [collapsed, setCollapsed] = useState(false); const { menuItems } = useMenuStore(); - const { phone } = useUserStore(); + const { phone, nickname } = useUserStore(); + const displayName = computed(() => nickname.value || phone.value); const { tabs, cachedViews, @@ -138,6 +139,9 @@ export default defineComponent({ if (res.code == 200) { message.success('密码修改成功'); setChangePwdVisible(false); + window.setTimeout(() => { + auth.logout(); + }, 1500); } else { message.error(res.msg || '修改失败'); } @@ -229,7 +233,7 @@ export default defineComponent({ {/* 右侧: 账号信息 + 退出 */}
- {phone.value} + {displayName.value} {{ default: () => , diff --git a/src/pages/events/banner/components/BannerFormModal.tsx b/src/pages/events/banner/components/BannerFormModal.tsx index 720971a..e662968 100644 --- a/src/pages/events/banner/components/BannerFormModal.tsx +++ b/src/pages/events/banner/components/BannerFormModal.tsx @@ -1,4 +1,4 @@ -import { defineComponent, ref, reactive } from 'vue'; +import { defineComponent, ref, reactive, computed, nextTick } from 'vue'; import { useEffect, useState } from '@/hooks'; import { Modal, @@ -12,43 +12,51 @@ import { message, Spin, } from 'ant-design-vue'; -import { JUMP_TYPE_OPTIONS, EVENT_OPTIONS } from '../model/useBannerModel'; +import { JUMP_TYPE_OPTIONS } from '../model/useBannerModel'; import { - getProvinceCityCascaderOptions, - parseAreaToCascaderPath, - formatCascaderArea, -} from '@/utils/areaData'; + TITLE_MAX, + PATH_MAX, + URL_MAX, + titleRules, + imageUrlRules, + typeRules, + linkUrlRules, + miniProgramPathRules, + areaRules, + startTimeRules, + endTimeRules, + sortRules, +} from '../model/form'; +import { getProvinceCityCascaderOptions, cityNameToCascaderPath } from '@/utils/areaData'; import { ImageCropper } from '@yp-component/root'; import { FileImageOutlined } from '@ant-design/icons-vue'; import { uploadFile, OssUploadType } from '@/utils/oss'; +import { isImageFile } from '@/utils/form'; +import { getEventList, getEventDetail, type TournamentAdminVO } from '../../list/model/services'; import dayjs from 'dayjs'; import styles from './BannerFormModal.module.less'; -/** 省市两级 Cascader 选项 */ -const cascaderAreaOptions = getProvinceCityCascaderOptions(); +/** 所属区域 Cascader 选项:全国 + 省市两级 */ +const areaCascaderOptions: any[] = [ + { value: '全国', label: '全国' }, + ...getProvinceCityCascaderOptions(), +]; /** 默认表单数据 */ const getDefaultForm = () => ({ title: '', imageUrl: '', imageStatus: 'empty' as 'empty' | 'uploading' | 'success' | 'error', - type: '1' as '1' | '2' | '3', + type: '1' as '1' | '2' | '3' | '0', linkUrl: '', relatedEventId: undefined as string | undefined, - area: '', + area: '全国', displayTimeRange: null as [string, string] | null, sort: 1, }); -/** 表单字段最大长度限制 */ -const TITLE_MAX = 50; -const PATH_MAX = 200; -const URL_MAX = 500; - /** 图片上传限制:最大 10MB */ const IMAGE_MAX_SIZE = 10 * 1024 * 1024; -/** 裁剪比例:1053:351 = 3:1 */ -const CROP_ASPECT_RATIO = 1053 / 351; interface BannerFormModalProps { visible: boolean; @@ -72,6 +80,37 @@ function base64ToFile(dataUrl: string, fileName: string): File { return new File([u8arr], fileName, { type: mime }); } +// ============================================================ +// 工具函数 +// ============================================================ + +/** 从 Cascader 路径中提取城市名(最后一级),用于区域筛选 */ +function extractCityName(path: string[]): string { + if (!path || path.length === 0 || path[0] === '全国') return ''; + return path[path.length - 1]; +} + +/** 从 Cascader 路径中提取存储用的区域字符串 */ +function extractAreaValue(path: string[]): string { + if (!path || path.length === 0) return '全国'; + if (path[0] === '全国') return '全国'; + // 直辖市路径如 ["北京市"],普通城市如 ["广东省", "深圳市"] + return path[path.length - 1]; +} + +/** 根据跳转类型计算 parms 字段的值 */ +function buildParams(type: string, relatedEventId: string | undefined, linkUrl: string): string { + if (type === '1') return relatedEventId ?? ''; + if (type === '2') return linkUrl || ''; + if (type === '3') return linkUrl || ''; + // type === '0' 无链接 + return ''; +} + +// ============================================================ +// 组件 +// ============================================================ + /** * Banner 新增/编辑表单弹窗 */ @@ -88,36 +127,165 @@ export default defineComponent({ const formRef = ref(); const fileInputRef = ref(null); const formData = reactive(getDefaultForm()); - /** Cascader 区域的路径数组(与 formData.area 字符串双向同步) */ - const areaPath = ref([]); - /** 是否由用户主动切换了跳转类型(跳过初始加载的 type 联动校验) */ + + /** Cascader 区域的路径数组:如 ["全国"]、["广东省", "深圳市"] */ + const areaPath = ref(['全国']); + + /** 是否由用户主动切换了跳转类型 */ const typeChanged = ref(false); - /** 裁剪弹窗状态 */ + // ---- 裁剪弹窗状态 ---- const [cropperVisible, setCropperVisible] = useState(false); - /** 待裁剪图片的本地预览 URL */ const [uploadImageUrl, setUploadImageUrl] = useState(''); - /** 根据 record 初始化表单 */ + // ---- 赛事搜索状态 ---- + const eventKeyword = ref(''); + const [eventOptions, setEventOptions] = useState<{ value: string; label: string }[]>([]); + const [eventSearchLoading, setEventSearchLoading] = useState(false); + /** 编辑时加载赛事详情的独立 loading */ + const [editLoading, setEditLoading] = useState(false); + /** 手动防抖计时器 */ + let searchTimer: ReturnType | null = null; + + /** 赛事下拉选项:第一项固定为"不关联" */ + const eventSelectOptions = computed(() => { + return [{ value: '', label: '不关联' }, ...eventOptions.value]; + }); + + // ---- 赛事搜索 API 调用 ---- + const doSearch = async (keyword: string) => { + if (!keyword.trim()) { + setEventOptions([]); + return; + } + setEventSearchLoading(true); + try { + const cityName = extractCityName(areaPath.value); + const params: Record = { + page: '1', + limit: '999', + name: keyword.trim(), + }; + if (cityName) { + params.areaName = cityName; + } + const res = await getEventList(params as any); + if (res.code == 200) { + const list: TournamentAdminVO[] = res.data?.list || []; + setEventOptions( + list.map((item) => ({ + value: item.id, + label: item.name, + })), + ); + } else { + setEventOptions([]); + } + } catch (e) { + console.error('[Banner] 搜索赛事失败:', e); + setEventOptions([]); + } finally { + setEventSearchLoading(false); + } + }; + + /** 赛事搜索输入(带 350ms 手动防抖) */ + const onEventSearch = (value: string) => { + eventKeyword.value = value; + if (searchTimer) clearTimeout(searchTimer); + if (!value.trim()) { + setEventOptions([]); + return; + } + searchTimer = setTimeout(() => { + doSearch(value); + }, 350); + }; + + /** 所属区域 Cascader 变化 */ + const onAreaChange = (val: any) => { + const path: string[] = val || []; + const newArea = extractAreaValue(path); + areaPath.value = path; + formData.area = newArea; + + // 切换到非"全国"区域时清空已关联赛事 + if (newArea !== '全国') { + formData.relatedEventId = undefined; + formRef.value?.clearValidate('relatedEventId'); + } + + // 区域变化时重新搜索(如果当前有关键词) + if (eventKeyword.value.trim()) { + if (searchTimer) clearTimeout(searchTimer); + searchTimer = setTimeout(() => { + doSearch(eventKeyword.value); + }, 0); + } + }; + + // ---- 根据 record 初始化表单 ---- const initFormFromRecord = (record: any) => { const fresh = getDefaultForm(); if (!record) { Object.assign(formData, fresh); - areaPath.value = []; + areaPath.value = ['全国']; + eventKeyword.value = ''; + setEventOptions([]); return; } - const area = record.area || record.region || ''; - areaPath.value = parseAreaToCascaderPath(area); + // 区域:兼容旧格式 "广东省-深圳市"、新格式 "深圳市"、"全国" + const rawArea = record.area || record.region || ''; + let cityName: string; + if (rawArea.includes('-')) { + cityName = rawArea.split('-').pop() || '全国'; + } else { + cityName = rawArea || '全国'; + } + areaPath.value = cityNameToCascaderPath(cityName); + + // 跳转类型 + const type = String(record.type || record.jumpType || '1'); + + // 关联赛事ID(编辑时从 params/linkUrl 读取) + let relatedEventId: string | undefined; + if (type === '1') { + const eventId = record.params || record.linkUrl || record.jumpUrl || ''; + if (eventId) { + relatedEventId = eventId; + // 异步获取赛事名称用于回显 + setEditLoading(true); + getEventDetail(eventId) + .then((detailRes) => { + if (detailRes.code == 200) { + const eventName = detailRes.data?.name || eventId; + setEventOptions([{ value: eventId, label: eventName }]); + } + }) + .catch(() => { + setEventOptions([{ value: eventId, label: eventId }]); + }) + .finally(() => { + setEditLoading(false); + }); + } + } + + eventKeyword.value = ''; + if (type !== '1' || !relatedEventId) { + setEventOptions([]); + } + Object.assign(formData, { ...fresh, title: record.title || '', 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, + type, + linkUrl: record.params || record.linkUrl || record.jumpUrl || '', + relatedEventId, + area: cityName, displayTimeRange: (record.showTimeBegin || record.displayStartTime) && (record.showTimeEnd || record.displayEndTime) @@ -130,61 +298,72 @@ export default defineComponent({ }); }; - /** 监听 visible 变化重置表单 */ + // ---- 监听 visible 变化重置表单 ---- useEffect(() => { if (props.visible) { - // 标记首次渲染,跳过 type 联动校验 typeChanged.value = false; initFormFromRecord(props.record); setCropperVisible(false); setUploadImageUrl(''); - setTimeout(() => formRef.value?.clearValidate(), 0); + nextTick().then(() => formRef.value?.clearValidate()); } }, [() => props.visible]); - /** 监听 type 变化时清理联动字段值和校验 */ + // ---- 清理防抖计时器 ---- useEffect(() => { - // 首次加载跳过校验 - if (!typeChanged.value) { - if (formData.type !== '1') formData.relatedEventId = undefined; - if (formData.type !== '2' && formData.type !== '3') formData.linkUrl = ''; - typeChanged.value = true; - return; + if (!props.visible && searchTimer) { + clearTimeout(searchTimer); + searchTimer = null; } + }, [() => props.visible]); - 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); - } + // ---- 监听 type 变化,清理联动字段 ---- + useEffect(() => { + const syncTypeFields = async () => { + 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) { + await formRef.value?.validateFields(activeNames).catch(() => {}); + } + }; + + syncTypeFields(); }, [() => formData.type]); - /** 触发隐藏的文件选择器 */ + /** 重新校验 Banner 图片字段,上传完成后清除或更新错误提示 */ + const revalidateImageUrl = async () => { + await formRef.value?.validateFields(['imageUrl']).catch(() => {}); + }; + + // ---- 图片上传相关 ---- const triggerFilePicker = () => { if (formData.imageStatus === 'uploading') return; fileInputRef.value?.click(); }; - /** 处理文件选择 → 打开裁剪弹窗 */ const handleFileChange = (e: Event) => { const target = e.target as HTMLInputElement; const file = target.files?.[0]; - // 重置 input 以便同一文件可再次选择 target.value = ''; if (!file) return; - if (!/^image\//.test(file.type)) { + if (!isImageFile(file)) { message.error('请选择图片文件'); return; } @@ -193,50 +372,45 @@ export default defineComponent({ return; } - // 生成本地预览 URL,打开裁剪弹窗 setUploadImageUrl(URL.createObjectURL(file)); setCropperVisible(true); }; - /** 裁剪确认回调:将裁剪后的图片上传到 OSS */ const handleCropConfirm = async (dataUrl: string) => { setCropperVisible(false); - // 释放旧的本地预览 if (formData.imageUrl && formData.imageUrl.startsWith('blob:')) { URL.revokeObjectURL(formData.imageUrl); } - // 先用 base64 做本地预览 const localUrl = dataUrl; formData.imageUrl = localUrl; formData.imageStatus = 'uploading'; try { const file = base64ToFile(dataUrl, `banner_${Date.now()}.jpg`); - const result = await uploadFile(file, OssUploadType.Banner as any); + const result = await uploadFile(file, OssUploadType.EventCover); - // 释放本地预览 URL.revokeObjectURL(localUrl); formData.imageUrl = result.url; formData.imageStatus = 'success'; message.success('图片上传成功'); + revalidateImageUrl(); } catch (err: any) { console.error('[Banner] 上传失败:', err); URL.revokeObjectURL(localUrl); formData.imageUrl = ''; formData.imageStatus = 'error'; message.error(err?.message || '图片上传失败'); + revalidateImageUrl(); } - // 清理裁剪资源 if (uploadImageUrl.value) { URL.revokeObjectURL(uploadImageUrl.value); } setUploadImageUrl(''); }; - /** 裁剪弹窗取消 */ const onCropperCancel = () => { setCropperVisible(false); if (uploadImageUrl.value) { @@ -245,16 +419,16 @@ export default defineComponent({ setUploadImageUrl(''); }; - /** 删除已上传图片 */ const handleRemoveImage = () => { if (formData.imageUrl && formData.imageUrl.startsWith('blob:')) { URL.revokeObjectURL(formData.imageUrl); } formData.imageUrl = ''; formData.imageStatus = 'empty'; + revalidateImageUrl(); }; - /** 提交表单 */ + // ---- 提交表单 ---- const handleSubmit = async () => { try { await formRef.value?.validate(); @@ -271,15 +445,8 @@ export default defineComponent({ 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), + params: buildParams(formData.type, formData.relatedEventId, formData.linkUrl), + area: formData.area, showTimeBegin: formData.displayTimeRange?.[0] || '', showTimeEnd: formData.displayTimeRange?.[1] || '', sort: String(formData.sort ?? 1), @@ -299,246 +466,246 @@ export default defineComponent({ centered wrapClassName={styles.bannerFormModalMain} > - - {/* ===== Banner 标题 ===== */} - - - - - {/* ===== 上传图片(必填,带裁剪) ===== */} - { - if (!formData.imageUrl || formData.imageUrl.startsWith('blob:')) { - return Promise.reject(new Error('请上传 Banner 图片')); - } - return Promise.resolve(); - }, - }, - ]} - > -
- - {formData.imageStatus === 'empty' || formData.imageStatus === 'error' ? ( -
- - - - 点击上传 - - 建议尺寸 1053*351 像素 -
- 支持 JPG/PNG,最大 10MB -
-
- ) : ( -
- - banner preview - -
- 重新上传 -
-
- )} -
-
- - {/* ===== 跳转类型 ===== */} - - - - )} - - {formData.type === '2' && ( - - - - )} - - {formData.type === '3' && ( - - - - )} - - {/* ===== 所属区域 ===== */} - - { - formData.area = formatCascaderArea(val || []); - }} - /> - - - {/* ===== 展示时间(开始/结束) ===== */} -
- - { - formData.displayTimeRange = [ - v ? v.format('YYYY-MM-DD') : '', - formData.displayTimeRange?.[1] || '', - ]; - }} - style={{ width: '100%' }} - /> - - - { - const [s, e] = formData.displayTimeRange || []; - if (s && e && new Date(e) < new Date(s)) { - return Promise.reject(new Error('结束时间不能早于开始时间')); - } - return Promise.resolve(); - }, - }, - ]} - > - { - formData.displayTimeRange = [ - formData.displayTimeRange?.[0] || '', - v ? v.format('YYYY-MM-DD') : '', - ]; - }} - style={{ width: '100%' }} - /> - + {editLoading.value ? ( +
+
+ ) : ( + <> + + {/* ===== Banner 标题 ===== */} + + { + formData.title = val; + }} + placeholder="请输入标题" + maxlength={TITLE_MAX} + allowClear + /> + - {/* ===== 排序 ===== */} - - - - + {/* ===== 上传图片(必填,带裁剪) ===== */} + formData.imageStatus, + () => formData.imageUrl, + )} + > +
+ + {formData.imageStatus === 'empty' || formData.imageStatus === 'error' ? ( +
+ + + + 点击上传 + + 建议尺寸 1053*351 像素 +
+ 支持 JPG/PNG,最大 10MB +
+
+ ) : ( +
+ + banner preview + +
+ 重新上传 +
+
+ )} +
+
- {/* ===== 底部按钮 ===== */} -
- - -
+ {/* ===== 跳转类型 ===== */} + + { + formData.relatedEventId = val; + }} + showSearch + filterOption={false} + allowClear={!eventSearchLoading.value} + loading={eventSearchLoading.value} + placeholder="请输入关键字搜索目标赛事" + options={eventSelectOptions.value as any} + onSearch={onEventSearch} + onFocus={() => { + if (formData.relatedEventId && eventOptions.value.length === 0) { + doSearch(formData.relatedEventId); + } + }} + /> + + )} + + {/* ===== 联动字段:自定义链接 ===== */} + {formData.type === '2' && ( + + { + formData.linkUrl = val; + }} + placeholder="https://xxx" + maxlength={URL_MAX} + allowClear + /> + + )} + + {/* ===== 联动字段:小程序页面 ===== */} + {formData.type === '3' && ( + + { + formData.linkUrl = val; + }} + placeholder="/pages/index/index" + maxlength={PATH_MAX} + allowClear + /> + + )} + + {/* ===== 所属区域(省市树种选择 + 全国) ===== */} + + + + + {/* ===== 展示时间(开始/结束) ===== */} +
+ formData.displayTimeRange)} + > + { + const newStart = v ? v.format('YYYY-MM-DD HH:mm:ss') : ''; + const currentEnd = formData.displayTimeRange?.[1] || ''; + if (newStart && currentEnd && dayjs(newStart).isAfter(dayjs(currentEnd))) { + formData.displayTimeRange = [currentEnd, newStart]; + } else { + formData.displayTimeRange = [newStart, currentEnd]; + } + formRef.value?.clearValidate(['displayStartTime', 'displayEndTime']); + }} + style={{ width: '100%' }} + /> + + + formData.displayTimeRange)} + > + { + const newEnd = v ? v.format('YYYY-MM-DD HH:mm:ss') : ''; + const currentStart = formData.displayTimeRange?.[0] || ''; + if (newEnd && currentStart && dayjs(newEnd).isBefore(dayjs(currentStart))) { + formData.displayTimeRange = [newEnd, currentStart]; + } else { + formData.displayTimeRange = [currentStart, newEnd]; + } + formRef.value?.clearValidate(['displayStartTime', 'displayEndTime']); + }} + style={{ width: '100%' }} + /> + +
+ + {/* ===== 排序 ===== */} + + { + formData.sort = val; + }} + placeholder="数字越小越靠前" + min={1} + max={999} + precision={0} + style={{ width: '200px' }} + /> + + + + {/* ===== 底部按钮 ===== */} +
+ + +
+ + )} {/* ===== 图片裁剪弹窗 ===== */} @@ -555,7 +722,6 @@ export default defineComponent({
void; onToggleStatus: (record: any) => void; onDelete: (record: any) => void; + permissions: { + canEdit: boolean; + canToggleStatus: boolean; + canDelete: boolean; + }; }) { // 图片列 if (column.key === 'image') { @@ -72,7 +79,7 @@ function renderBodyCell({ // 跳转地址列 if (column.dataIndex === 'linkUrl') { - return {text || '-'}; + return {text || '--'}; } // 展示时间列 @@ -92,17 +99,26 @@ function renderBodyCell({ // 操作列 if (column.key === 'action') { const isEnabled = record.status === '1'; + const { canEdit, canToggleStatus, canDelete } = permissions; + if (!canEdit && !canToggleStatus && !canDelete) return --; + return ( - - - + {canEdit && ( + + )} + {canToggleStatus && ( + + )} + {canDelete && ( + + )} ); } @@ -136,6 +152,13 @@ export default defineComponent({ } = useBannerModel(); const { containerRef, height } = useContainerSize(); + const { hasPermission } = usePermissionStore(); + const canAdd = hasPermission('events.banner.add'); + const operationPermissions = { + canEdit: hasPermission('events.banner.edit'), + canToggleStatus: hasPermission('events.banner.toggle_status'), + canDelete: hasPermission('events.banner.delete'), + }; const confirmDelete = (record: any) => { Modal.confirm({ @@ -171,10 +194,6 @@ export default defineComponent({ }, ]; - onMounted(() => { - handleSearch(); - }); - return () => (
@@ -204,9 +223,11 @@ export default defineComponent({ 查询 - + {canAdd && ( + + )} @@ -230,6 +251,7 @@ export default defineComponent({ onEdit: handleEdit, onToggleStatus: confirmToggleStatus, onDelete: confirmDelete, + permissions: operationPermissions, }), }} @@ -237,9 +259,9 @@ export default defineComponent({
`共 ${total} 条`} onChange={handlePageChange} diff --git a/src/pages/events/banner/model/form.ts b/src/pages/events/banner/model/form.ts new file mode 100644 index 0000000..49b808e --- /dev/null +++ b/src/pages/events/banner/model/form.ts @@ -0,0 +1,114 @@ +/** + * Banner 表单域控制器 + * + * 集中管理 Banner 新增/编辑弹窗的所有表单规则校验与常量。 + * 各规则数组 / 工厂函数可直接传给 antd Form.Item 的 rules prop。 + */ +import type { Rule } from 'ant-design-vue/es/form'; + +// ============================================================ +// 常量 +// ============================================================ + +/** 标题最大长度 */ +export const TITLE_MAX = 50; +/** 小程序路径最大长度 */ +export const PATH_MAX = 200; +/** 跳转链接最大长度 */ +export const URL_MAX = 500; + +// ============================================================ +// 表单规则 +// ============================================================ + +/** Banner 标题:必填 + 50 字符上限 */ +export const titleRules: Rule[] = [ + { required: true, message: '请输入' }, + { max: TITLE_MAX, message: `标题最多 ${TITLE_MAX} 个字符` }, +]; + +/** + * Banner 图片:必填 + 上传状态校验 + * + * @param getImageStatus 获取当前上传状态的 getter + * @param getImageUrl 获取当前图片 URL 的 getter + */ +export function imageUrlRules(getImageStatus: () => string, getImageUrl: () => string): Rule[] { + return [ + { required: true, message: '请上传 Banner 图片' }, + { + validator: async () => { + if (getImageStatus() === 'uploading') { + return Promise.reject(new Error('图片仍在上传中,请稍候')); + } + const url = getImageUrl(); + if (url.startsWith('blob:') || url.startsWith('data:')) { + return Promise.reject(new Error('请等待图片上传完成')); + } + return Promise.resolve(); + }, + }, + ]; +} + +/** 跳转类型:必选 */ +export const typeRules: Rule[] = [{ required: true, message: '请选择跳转类型' }]; + +/** 跳转地址(自定义链接 scene=2):必填 + 最大长度 */ +export const linkUrlRules: Rule[] = [ + { required: true, message: '请输入跳转地址' }, + { max: URL_MAX, message: `链接最多 ${URL_MAX} 个字符` }, +]; + +/** 小程序页面路径(scene=3):必填 + 最大长度 */ +export const miniProgramPathRules: Rule[] = [ + { required: true, message: '请输入小程序页面路径' }, + { max: PATH_MAX, message: `路径最多 ${PATH_MAX} 个字符` }, +]; + +/** 所属区域:必选 */ +export const areaRules: Rule[] = [{ required: true, message: '请选择所属区域' }]; + +/** 排序:必填 + 1-999 整数 */ +export const sortRules: Rule[] = [ + { required: true, message: '请输入排序' }, + { type: 'number', min: 1, max: 999, message: '排序范围 1-999' }, +]; + +/** + * 展示开始时间规则:校验 displayTimeRange[0] 非空 + * + * @param getDisplayTimeRange 获取展示时间范围的 getter + */ +export function startTimeRules(getDisplayTimeRange: () => [string, string] | null): Rule[] { + return [ + { + validator: async () => { + const range = getDisplayTimeRange(); + if (!range?.[0]) { + return Promise.reject(new Error('请选择开始时间')); + } + return Promise.resolve(); + }, + }, + ]; +} + +/** + * 展示结束时间规则:校验 displayTimeRange[1] 非空 + * + * @param getDisplayTimeRange 获取展示时间范围的 getter + */ +export function endTimeRules(getDisplayTimeRange: () => [string, string] | null): Rule[] { + return [ + { + validator: async () => { + const range = getDisplayTimeRange(); + if (!range?.[1]) { + return Promise.reject(new Error('请选择结束时间')); + } + return Promise.resolve(); + }, + }, + ]; +} diff --git a/src/pages/events/banner/model/services.ts b/src/pages/events/banner/model/services.ts index e3dec86..0c3f54d 100644 --- a/src/pages/events/banner/model/services.ts +++ b/src/pages/events/banner/model/services.ts @@ -3,8 +3,6 @@ * 该页面使用的所有接口集中管理 */ 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, @@ -26,13 +24,11 @@ export type { // ============================================================ // URL 常量 // ============================================================ -const bannerList = '/manager/banner/list'; -const bannerDel = '/manager/banner/del'; -const bannerSave = '/manager/banner/save'; -const bannerUpdate = '/manager/banner/update'; -const bannerActive = '/manager/banner/active'; - -const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +const bannerList = '/admin/manager/banner/list'; +const bannerDel = '/admin/manager/banner/del'; +const bannerSave = '/admin/manager/banner/save'; +const bannerUpdate = '/admin/manager/banner/update'; +const bannerActive = '/admin/manager/banner/active'; // ============================================================ // API 函数 @@ -42,10 +38,6 @@ const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, 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(bannerList, params as Record); } diff --git a/src/pages/events/banner/model/useBannerModel.ts b/src/pages/events/banner/model/useBannerModel.ts index 85aa6ee..0357a5f 100644 --- a/src/pages/events/banner/model/useBannerModel.ts +++ b/src/pages/events/banner/model/useBannerModel.ts @@ -2,6 +2,9 @@ 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 { useRequest } from '@/hooks/useRequest'; +import { useEffect } from '@/hooks/useEffect'; +import { usePagination } from '@/hooks/usePagination'; import { getBannerList, delBanner, @@ -11,7 +14,6 @@ import { type TournamentAdminBannerListVO, type BannerListQueryParams, } from './services'; -import { getProvinceCityOptions } from '@/utils/areaData'; // ============================================================ // 常量 @@ -24,15 +26,17 @@ export const BANNER_STATUS_OPTIONS = [ { value: '0', label: '禁用' }, ] as const; -/** 跳转类型选项(value 对应 API type: 1=赛事详情,2=自定义连接,3=小程序页面) */ +/** 跳转类型选项(value 对应 API type: 0=无链接,1=赛事详情,2=自定义连接,3=小程序页面) */ export const JUMP_TYPE_OPTIONS = [ { value: '1', label: '赛事详情页' }, { value: '2', label: '自定义链接' }, { value: '3', label: '小程序页面' }, + { value: '0', label: '无链接' }, ] as const; /** 跳转类型映射 */ const JUMP_TYPE_MAP: Record = { + '0': { label: '无链接', tone: 'default' }, '1': { label: '赛事详情页', tone: 'primary' }, '2': { label: '自定义链接', tone: 'purple' }, '3': { label: '小程序页面', tone: 'cyan' }, @@ -44,18 +48,6 @@ const STATUS_MAP: Record(false); - const [allData, setAllData] = useState([]); - const [pagination, setPagination] = useState({ - current: 1, - pageSize: 10, - total: 0, + // ===== 构建 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; + }; + + // ===== 数据请求(API 返回全量,前端分页) ===== + const { + data, + loading, + run: fetchList, + } = useRequest(() => getBannerList(buildQueryParams()), { + formatResult: (res) => (res.code == 200 ? res.data : []), }); + const allData = computed(() => data.value || []); + + // ===== 前端分页 ===== + const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 }); + /** 当前页数据(客户端切片) */ const dataSource = computed(() => { - const start = (pagination.value.current - 1) * pagination.value.pageSize; - return allData.value.slice(start, start + pagination.value.pageSize); + const start = ((pagination as any).current.value - 1) * (pagination as any).pageSize.value; + return allData.value.slice(start, start + (pagination as any).pageSize.value); }); + // 同步前端分页 total + useEffect(() => { + pagination.setTotal(allData.value.length); + }, [allData]); + // ===== 弹窗状态 ===== const [modalVisible, setModalVisible] = useState(false); const [editingRecord, setEditingRecord] = useState(null); @@ -102,25 +116,12 @@ export function useBannerModel() { key: 'type', width: 120, }, - { 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: 'createDate', key: 'createDate', 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 !== ''; @@ -131,37 +132,21 @@ export function useBannerModel() { // ===== 方法 ===== /** 查询 */ - const handleSearch = useThrottleFn(async () => { - setLoading(true); - try { - 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) { - console.error('Banner查询失败:', error); - } finally { - setLoading(false); - } - }, 500); + const handleSearch = () => fetchList(); /** 重置 */ - const handleReset = useThrottleFn(() => { + const handleReset = useThrottleFn(async () => { filterForm.title = ''; filterForm.status = ''; - setPagination({ current: 1, pageSize: 10, total: 0 }); - setAllData([]); - setTimeout(() => handleSearch(), 350); + pagination.reset(); + await fetchList(); }, 500); const handlePageChange = (page: number, pageSize: number) => { - setPagination({ ...pagination.value, current: page, pageSize }); + pagination.setCurrent(page); + if (pageSize !== (pagination as any).pageSize.value) { + pagination.setPageSize(pageSize); + } }; /** 打开新增弹窗 */ @@ -183,11 +168,16 @@ export function useBannerModel() { if (formSubmitting.value) return; setFormSubmitting(true); try { - const params = { + const payload = { img: formPayload.img || formPayload.imageUrl || '', title: formPayload.title || '', type: String(formPayload.type || formPayload.jumpType || '1'), - linkUrl: formPayload.linkUrl || formPayload.jumpUrl || '', + params: + formPayload.params ?? + formPayload.parms ?? + formPayload.linkUrl ?? + formPayload.jumpUrl ?? + '', area: formPayload.area || formPayload.region || '', showTimeBegin: formPayload.showTimeBegin || @@ -203,8 +193,8 @@ export function useBannerModel() { }; const isEdit = editingRecord.value !== null; const res = isEdit - ? await updateBanner({ ...params, id: editingRecord.value.id }) - : await saveBanner(params); + ? await updateBanner({ ...payload, id: editingRecord.value.id }) + : await saveBanner(payload); if (res.code == 200) { message.success(isEdit ? '编辑成功' : '新增成功'); handleCloseModal(); @@ -221,50 +211,44 @@ export function useBannerModel() { /** 删除 */ const handleDelete = useThrottleFn(async (record: any) => { - setLoading(true); try { const res = await delBanner(record.id); if (res.code == 200) { message.success('删除成功'); - handleSearch(); + fetchList(); } else { message.error(res.msg || '删除失败'); } } catch (e: any) { console.error('Banner删除失败:', e); - } finally { - setLoading(false); } }, 500); /** 启用/禁用切换 */ const handleToggleStatus = useThrottleFn(async (record: any) => { - setLoading(true); try { 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(); + fetchList(); } else { message.error(res.msg || '操作失败'); } } catch (e: any) { console.error('Banner状态切换失败:', e); - } finally { - setLoading(false); } }, 500); // ===== 渲染工具 ===== const renderJumpType = (type: string) => { - const info = JUMP_TYPE_MAP[type] || { label: type || '-', tone: 'default' as const }; + const info = JUMP_TYPE_MAP[type] || { label: type || '--', tone: 'default' as const }; return h(StatusTag, { label: info.label, tone: info.tone }); }; const renderStatus = (status: string) => { - const info = STATUS_MAP[status] || { label: status || '-', tone: 'default' as const }; + const info = STATUS_MAP[status] || { label: status || '--', tone: 'default' as const }; return h(StatusTag, { label: info.label, tone: info.tone, dimmed: info.dimmed }); }; diff --git a/src/pages/events/list/components/EventAuditModal.tsx b/src/pages/events/list/components/EventAuditModal.tsx index 0803e0e..892f4d8 100644 --- a/src/pages/events/list/components/EventAuditModal.tsx +++ b/src/pages/events/list/components/EventAuditModal.tsx @@ -1,5 +1,8 @@ -import { defineComponent, reactive } from 'vue'; -import { Modal, Descriptions, Image, Radio, Input, Form } from 'ant-design-vue'; +import { defineComponent, reactive, ref } from 'vue'; +import { Modal, Descriptions, Image, Radio, Input, Form, Spin, message } from 'ant-design-vue'; +import { useEffect, useState } from '@/hooks'; +import { getEventDetail, type TournamentAdminInfoVO } from '../model/services'; +import { validateEventAuditSubmit } from '../model/form'; import styles from './EventAuditModal.module.less'; const { TextArea } = Input; @@ -11,31 +14,6 @@ interface EventAuditModalProps { onSubmit?: (payload: { approved: boolean; comment: string }) => 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', -}; - /** * 赛事审核弹窗 * 包含:基础信息 + 封面 + 审核操作(通过/未通过 + 审核内容) @@ -49,7 +27,8 @@ export default defineComponent({ onSubmit: { type: Function, default: undefined }, }, setup(props: EventAuditModalProps) { - const detail = MOCK_DETAIL; + const detail = ref(null); + const [loading, setLoading] = useState(false); // 审核表单 const auditForm = reactive({ @@ -57,18 +36,145 @@ export default defineComponent({ comment: '', }); - const handleOk = () => { - if (!auditForm.comment.trim()) { - // 不通过时强制要求填写审核内容 - if (!auditForm.approved) { - // 这里交给校验,简化版本不做强制校验 + /** 加载赛事详情 */ + const fetchDetail = async (id: string) => { + setLoading(true); + detail.value = null; + try { + const res = await getEventDetail(id); + if (res.code == 200) { + detail.value = res.data; + } else { + message.error(res.msg || '获取赛事详情失败'); } + } catch { + // 网络层已统一提示 + } finally { + setLoading(false); + } + }; + + /** 弹窗打开时重新拉取详情 */ + useEffect(() => { + if (props.visible && props.record?.id) { + auditForm.approved = true; + auditForm.comment = ''; + fetchDetail(props.record.id); + } + }, [() => props.visible]); + + const handleOk = () => { + const check = validateEventAuditSubmit({ + approved: auditForm.approved, + comment: auditForm.comment, + }); + if (!check.valid) { + message.warning(check.error!); + return; } if (props.onSubmit) { props.onSubmit({ approved: auditForm.approved, comment: auditForm.comment }); } }; + /** 格式化时间范围 */ + const formatRange = (start: string, end: string) => { + if (!start && !end) return '--'; + return `${start || '?'} ~ ${end || '?'}`; + }; + + /** 渲染 0/1 开关文本 */ + const boolText = (val: string, trueText: string, falseText: string) => { + return val === '1' ? trueText : falseText; + }; + + /** 渲染详情内容 */ + const renderContent = () => { + const d = detail.value!; + + return ( + <> + {/* ===== 基础信息 ===== */} +
+
基础信息
+ + {d.name || '--'} + + {formatRange(d.startTimeBegin, d.startTimeEnd)} + + + + {d.descr || '--'} + + + + {formatRange(d.signupTimeBegin, d.signupTimeEnd)} + + + {d.signupTimeEnd || '--'} + + + {d.venueArea || '--'} + {d.venue || '--'} + {d.venueNo || '--'} + + + {boolText(d.isPublic, '在首页展示赛事', '非公开')} + + + {boolText(d.isRequireIdCardNo, '需要提供', '无需提供')} + + + {boolText(d.isRequireIdCardImg, '需要提供', '无需提供')} + + + {d.createTime || '--'} + {d.createNickname || '--'} + 待审核 + {d.signupCount || '0'} + + {d.dropoutCount || '0'} + + + {d.totalLimit || '--'} + {d.matchTotalCount || '0'} + {d.matchCount || '0'} + +
+ + {/* ===== 封面 ===== */} +
+
封面
+ {d.img ? : 暂无封面} +
+ + {/* ===== 审核操作 ===== */} +
+
+ 审核操作 +
+
+ + + 通过 + 未通过 + + + +