diff --git a/.gitignore b/.gitignore index 715094c..2b3c593 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Logs -logs +/logs *.log npm-debug.log* yarn-debug.log* @@ -40,3 +40,7 @@ coverage .cache .temp .tmp + +# Easy LESS 误生成的编译产物(本项目为 Vue PC 端,样式源文件为 .less,由 Vite 编译) +*.wxss +*.module.css diff --git a/src/api/logs/index.ts b/src/api/logs/index.ts new file mode 100644 index 0000000..ddbf68d --- /dev/null +++ b/src/api/logs/index.ts @@ -0,0 +1,66 @@ +/** + * 操作日志 API + */ +import { get } from '@/utils/request'; +import { USE_MOCK, MOCK_DELAY } from '@/config/mock'; +import { buildMockOperationLogPage } from '@/config/mock/operationLogs'; +import { buildMockEventOperationLogPage } from '@/config/mock/eventOperationLogs'; +import type { + OperationLogQueryParams, + SysOperationLogVO, + TournamentAdminOperationPageVO, + PageData, + ApiResult, +} from './types'; + +export type { + SysOperationLogVO, + TournamentAdminOperationPageVO, + OperationLogQueryParams, + PageData, + 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); +} + +/** GET /admin/tournament/operation/page — 赛事操作日志分页 */ +export async function getOperationLogs( + 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: buildMockEventOperationLogPage(page, limit, pickFilters(params)), + }; + } + return get('/tournament/operation/page', params as Record); +} + +/** @deprecated 使用 getOperationLogList */ +export const getOperationLogPage = getOperationLogList; diff --git a/src/api/logs/types.ts b/src/api/logs/types.ts new file mode 100644 index 0000000..21cefd2 --- /dev/null +++ b/src/api/logs/types.ts @@ -0,0 +1,60 @@ +/** + * 操作日志 — 类型定义 + */ + +/** 系统操作日志列表项(GET /admin/sys/operation/page) */ +export interface SysOperationLogVO { + /** 操作类型:1=下架赛事、2=上架赛事、3=禁用用户、4=启用用户 */ + type: number; + /** 操作人昵称 */ + nickname: string; + /** 操作时间 */ + createDate: string; + /** 操作内容 */ + content: string; +} + +/** 赛事操作日志列表项 */ +export interface TournamentAdminOperationPageVO { + opName: string; + source: string; + nickname: string; + phone: string; + tournamentName: string; + obj: string; + content: string; + createDate: string; +} + +/** 操作日志列表查询参数 */ +export interface OperationLogQueryParams { + page?: string; + limit?: string; + /** 操作类型 */ + type?: string; + /** 系统日志:操作人昵称 */ + nickname?: string; + /** 赛事日志:操作来源 1=PC,2=小程序 */ + source?: string; + /** 赛事日志:操作人昵称 */ + sourceNickname?: string; + /** 赛事日志:操作人手机号 */ + sourcePhone?: string; + /** 赛事日志:赛事名称 */ + tournamentName?: string; + /** 开始日期 yyyy-MM-dd */ + dateBegin?: string; + /** 结束日期 yyyy-MM-dd */ + dateEnd?: string; +} + +export interface PageData { + total: number; + list: T[]; +} + +export interface ApiResult { + code: number; + msg: string; + data: T; +} diff --git a/src/api/payments/index.ts b/src/api/payments/index.ts new file mode 100644 index 0000000..e5daba7 --- /dev/null +++ b/src/api/payments/index.ts @@ -0,0 +1,25 @@ +/** + * 支付流水 API + * 对应 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'; + +export type { PaymentFlowVO, PaymentFlowQueryParams, PageData, ApiResult } from './types'; + +const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +/** 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) }; + } + return get('/finance/paymentFlow/page', params as Record); +} diff --git a/src/api/payments/types.ts b/src/api/payments/types.ts new file mode 100644 index 0000000..4b52998 --- /dev/null +++ b/src/api/payments/types.ts @@ -0,0 +1,48 @@ +/** + * 支付流水 — 类型定义 + * 对应 OpenAPI: GET /admin/finance/paymentFlow/page + */ + +/** 支付流水列表项 */ +export interface PaymentFlowVO { + /** 流水号 */ + serialNo: string; + /** 订单编号 */ + orderNo: string; + /** 类型:1=报名,2=取消报名 */ + type: number; + /** 用户昵称 */ + nickname: string; + /** 用户手机�� */ + phone: string; + /** 订单金额("0.00"格式) */ + amount: string; + /** 订单创建时间 */ + createDate: string; +} + +/** 支付流水查询参数 */ +export interface PaymentFlowQueryParams { + page?: string; + limit?: string; + /** 订单创建开始时间 yyyy-MM-dd */ + createDateBegin?: string; + /** 订单创建结束时间 yyyy-MM-dd */ + createDateEnd?: string; + /** 用户昵称 */ + nickname?: string; + /** 手机号 */ + phone?: string; + /** 类型:1/2 */ + type?: string; +} + +export interface PageData { + total: number; + list: T[]; +} +export interface ApiResult { + code: number; + msg: string; + data: T; +} diff --git a/src/api/roles/index.ts b/src/api/roles/index.ts index 9234e90..fcda64f 100644 --- a/src/api/roles/index.ts +++ b/src/api/roles/index.ts @@ -63,10 +63,10 @@ export async function getPermissionTree( const id = Number(roleId) || 0; return { code: 200, msg: 'success', data: buildMockPermissionTree(id) }; } - return get('/admin/sys/role/permissionList', { roleId: String(roleId) }); + return get('/sys/role/permissionList', { roleId: String(roleId) }); } /** POST /admin/sys/role/del — 删除角色 */ export async function deleteRole(id: number): Promise> { - return post('/admin/sys/role/del', { id }); + return post('/sys/role/del', { id }); } diff --git a/src/api/wallet/index.ts b/src/api/wallet/index.ts new file mode 100644 index 0000000..9d605d5 --- /dev/null +++ b/src/api/wallet/index.ts @@ -0,0 +1,53 @@ +/** + * 用户钱包 API + * 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, + WalletTransactionQueryParams, + WalletTransactionVO, + PageData, + ApiResult, +} from './types'; + +export type { + WalletSummaryVO, + WalletQueryParams, + WalletTransactionVO, + WalletTransactionQueryParams, + PageData, + 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) }; + } + return get('/finance/wallet/summary', params as Record); +} + +/** GET /admin/finance/wallet/page — 交易明细分页 */ +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 new file mode 100644 index 0000000..5f993e9 --- /dev/null +++ b/src/api/wallet/types.ts @@ -0,0 +1,92 @@ +/** + * 用户钱包 — 类型定义 + * 对应 OpenAPI: GET /admin/finance/wallet/summary + */ + +/** 钱包列表项 */ +export interface WalletSummaryVO { + /** 用户ID */ + userId: number; + /** 用户昵称 */ + userNickname: string; + /** 手机号 */ + phone: string; + /** 余额("0.00"格式) */ + amount: string; + /** 冻结金额("0.00"格式) */ + frozenAmount: string; + /** 累计成功提现金额("0.00"格式) */ + withdrawalAmount: string; + /** 汇总行-总余额 */ + totalAmount: string; + /** 汇总行-总冻结金额 */ + totalFrozenAmount: string; + /** 汇总行-总累计提现 */ + totalWithdrawalAmount: string; +} + +/** 钱包列表查询参数 */ +export interface WalletQueryParams { + page?: string; + limit?: string; + /** 用户昵称,模糊搜索 */ + nickname?: string; + /** 手机号 */ + phone?: string; + /** 余额最小值 */ + amountBegin?: string; + /** 余额最大值 */ + amountEnd?: string; +} + +export interface PageData { + total: number; + list: T[]; +} +export interface ApiResult { + code: number; + msg: string; + data: T; +} + +// ============================================================ +// 交易明细(GET /admin/finance/wallet/page) +// ============================================================ + +/** 交易明细列表项 */ +export interface WalletTransactionVO { + /** 类型:1=报名入账,2=提现出账,3=退款出账 */ + type: number; + /** 金额("0.00"格式,入账为+,出账为-) */ + amount: string; + /** 余额("0.00"格式) */ + afterAmount: string; + /** 可提现余额("0.00"格式) */ + withdrawAmount: string; + /** 冻结金额("0.00"格式) */ + freezeAmount: string; + /** 冻结时间 */ + freezeTime: string; + /** 解冻时间 */ + unfreezeTime: string; + /** 关联单号 */ + orderNo: string; + /** 时间 */ + createDate: string; +} + +/** 交易明细查询参数 */ +export interface WalletTransactionQueryParams { + page?: string; + limit?: string; + /** 用户ID */ + userId?: string; + /** 创建开始时间 yyyy-MM-dd */ + createDateBegin?: string; + /** 创建结束时间 yyyy-MM-dd */ + createDateEnd?: string; + /** 类型:1/2/3 */ + type?: string; + /** 状态:1=已解冻,2=冻结中,3=正常 */ + status?: string; +} diff --git a/src/api/withdraw/index.ts b/src/api/withdraw/index.ts new file mode 100644 index 0000000..4c2c20b --- /dev/null +++ b/src/api/withdraw/index.ts @@ -0,0 +1,56 @@ +/** + * 提现管理 API + * 对应 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 — 提现列表分页 */ +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); +} + +/** GET /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) }); +} + +/** 提现审核请求参数 */ +export interface WithdrawAuditParams { + /** 提现ID */ + id: string; + /** 审核结果:1=通过,2=驳回 */ + audit: string; + /** 审核内容 */ + checkMsg: string; + /** 审核图片(逗号拼接的 URL) */ + checkImg: string; +} + +/** POST /finance/withdraw/audit — 审核提现 */ +export async function postWithdrawAudit(params: WithdrawAuditParams): Promise> { + return post('/finance/withdraw/audit', params); +} diff --git a/src/api/withdraw/types.ts b/src/api/withdraw/types.ts new file mode 100644 index 0000000..f8c688e --- /dev/null +++ b/src/api/withdraw/types.ts @@ -0,0 +1,104 @@ +/** + * 提现管理 — 类型定义 + * 对应 OpenAPI: GET /admin/finance/withdraw/page + */ + +/** 提现列表项(对齐 TournamentFinanceWithdrawVO) */ +export interface WithdrawVO { + /** 提现ID */ + id: number; + /** 申请人昵称 */ + nickname: string; + /** 申请人手机号 */ + phone: string; + /** 申请人真实姓名 */ + realName: string; + /** 提现类型(固定"银行卡") */ + withdrawType: string; + /** 提现金额("0.00"格式) */ + withdrawAmount: string; + /** 到账金额("0.00"格式) */ + receivedAmount: string; + /** 审核状态:0=待审核,1=审核通过,2=审核驳回 */ + auditStatus: number; + /** 到账状态:0=未支付,1=支付成功,2=支付失败 */ + payStatus: number; + /** 流水单号 */ + serialNumber: string; + /** 申请时间 */ + createDate: string; +} + +/** 提现列表查询参数 */ +export interface WithdrawQueryParams { + page?: string; + limit?: string; + /** 提现申请人昵称 */ + nickname?: string; + /** 提现申请人手机号 */ + phone?: string; + /** 提现申请人真实姓名 */ + realName?: string; + /** 审核状态:0/1/2 */ + auditStatus?: string; + /** 到账状态:0/1/2 */ + payStatus?: string; +} + +export interface PageData { + total: number; + list: T[]; +} +export interface ApiResult { + code: number; + msg: string; + data: T; +} + +/** 提现详情(对齐 TournamentFinanceWithdrawInfoVO) */ +export interface WithdrawInfoVO { + /** 申请人昵称 */ + nickname: string; + /** 申请人手机号 */ + phone: string; + /** 申请人真实姓名 */ + realName: string; + /** 申请时间 */ + createDate: string; + /** 提现类型(固定"银行卡") */ + withdrawType: string; + /** 提现金额("0.00"格式) */ + withdrawAmount: string; + /** 费率 */ + feeRate: string; + /** 手续费 */ + feeAmount: string; + /** 到账金额("0.00"格式) */ + receivedAmount: string; + /** 审核状态:0=待审核,1=审核通过,2=审核驳回 */ + auditStatus: number; + /** 到账状态:0=未支付,1=支付成功,2=支付失败 */ + payStatus: number; + /** 银行卡类型:1=个人 */ + bankCardType: number; + /** 持卡人真实姓名 */ + cardHolderName: string; + /** 银行卡号 */ + bankCardNumber: string; + /** 开户行名称 */ + bankName: string; + /** 支行名称 */ + branchName: string; + /** 支付时间 */ + payTime: string; + /** 订单号 */ + orderNo: string; + /** 审核时间 */ + auditDate: string; + /** 审核人昵称 */ + auditNickname: string; + /** 审核图片 */ + auditImg: string; + /** 审核图片列表 */ + auditImgList: string[]; +} diff --git a/src/components/index.ts b/src/components/index.ts index fc4b600..2afbde3 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -4,6 +4,6 @@ */ export { default as HelloWorld } from './HelloWorld'; export { default as ChangePasswordModal } from './ChangePasswordModal'; -export { default as PageTabs } from './PageTabs/PageTabs'; +export { default as PageTabs } from './pageTabs'; export { StatusTag } from './StatusTag'; export type { StatusTagTone } from './StatusTag'; diff --git a/src/components/pageTabs/PageTabs.tsx b/src/components/pageTabs/index.tsx similarity index 67% rename from src/components/pageTabs/PageTabs.tsx rename to src/components/pageTabs/index.tsx index ebd4d32..524bc36 100644 --- a/src/components/pageTabs/PageTabs.tsx +++ b/src/components/pageTabs/index.tsx @@ -1,22 +1,36 @@ import { defineComponent, + h, nextTick, onBeforeUnmount, onMounted, ref, type PropType, + type VNode, watch, } from 'vue'; -import { message } from 'ant-design-vue'; +import { Dropdown, Menu, message } from 'ant-design-vue'; import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue'; +import type { IconType } from 'vue-icons-plus/lib'; +import { + LuCopyX, + LuListX, + LuMoreHorizontal, + LuPanelLeftClose, + LuPanelRightClose, +} from 'vue-icons-plus/lu'; import type { TabView } from '@/types'; import { useState } from '@/hooks'; import { renderRouteIcon } from '@/utils/routeIcon'; -import styles from './PageTabs.module.less'; +import styles from './style.module.less'; const SCROLL_STEP = 240; type TabKeyHandler = (key: string) => void; +type TabBatchHandler = (key: string) => void; +type TabAllHandler = () => void; + +const renderAiIcon = (Icon: IconType, size = 16) => h(Icon, { size }); /** * Chrome 风格的页面标签栏组件 @@ -32,6 +46,14 @@ const PageTabs = defineComponent({ activeKey: { type: String, required: true }, /** 关闭标签 */ onClose: { type: Function as PropType, default: undefined }, + /** 关闭当前标签左侧标签 */ + onCloseLeft: { type: Function as PropType, default: undefined }, + /** 关闭当前标签右侧标签 */ + onCloseRight: { type: Function as PropType, default: undefined }, + /** 关闭当前标签以外的标签 */ + onCloseOther: { type: Function as PropType, default: undefined }, + /** 关闭全部标签 */ + onCloseAll: { type: Function as PropType, default: undefined }, /** 切换标签 */ onChange: { type: Function as PropType, default: undefined }, }, @@ -116,6 +138,39 @@ const PageTabs = defineComponent({ } }; + const handleBatchClose = (key: string) => { + if (props.tabs.length <= 1) { + message.warning('至少保留一个标签'); + return; + } + + const activeIndex = props.tabs.findIndex((tab) => tab.path === props.activeKey); + if (activeIndex === -1) return; + + switch (key) { + case 'left': + if (activeIndex === 0) { + message.warning('左侧没有可关闭的标签页'); + return; + } + props.onCloseLeft?.(props.activeKey); + break; + case 'right': + if (activeIndex === props.tabs.length - 1) { + message.warning('右侧没有可关闭的标签页'); + return; + } + props.onCloseRight?.(props.activeKey); + break; + case 'other': + props.onCloseOther?.(props.activeKey); + break; + case 'all': + props.onCloseAll?.(); + break; + } + }; + onMounted(() => { const el = scrollRef.value; if (!el) return; @@ -174,6 +229,15 @@ const PageTabs = defineComponent({ ); }; + const renderBatchMenuItem = (key: string, icon: VNode, label: string) => ( + +
+ {icon} + {label} +
+
+ ); + return () => (
{showNavLeft.value && ( @@ -201,6 +265,24 @@ const PageTabs = defineComponent({ )} + + + {{ + default: () => ( + + ), + overlay: () => ( + handleBatchClose(String(key))}> + {renderBatchMenuItem('left', renderAiIcon(LuPanelLeftClose), '关闭左侧标签页')} + {renderBatchMenuItem('right', renderAiIcon(LuPanelRightClose), '关闭右侧标签页')} + {renderBatchMenuItem('other', renderAiIcon(LuCopyX), '关闭其它标签页')} + {renderBatchMenuItem('all', renderAiIcon(LuListX), '关闭全部标签页')} + + ), + }} +
); }, diff --git a/src/components/pageTabs/PageTabs.module.less b/src/components/pageTabs/style.module.less similarity index 79% rename from src/components/pageTabs/PageTabs.module.less rename to src/components/pageTabs/style.module.less index b94d883..4c9957f 100644 --- a/src/components/pageTabs/PageTabs.module.less +++ b/src/components/pageTabs/style.module.less @@ -2,6 +2,8 @@ .pageTabsWrap { position: relative; + display: flex; + align-items: center; width: 100%; height: 100%; @@ -42,13 +44,46 @@ } .navBtnRight { - right: 0; + right: 34px; + } + + .batchBtn { + display: inline-flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + margin-left: 6px; + padding: 0; + border: none; + border-radius: 6px; + background: #fff; + color: rgba(0, 0, 0, 0.45); + box-shadow: 0 0 4px rgba(214, 214, 214, 0.9); + cursor: pointer; + transition: + color 0.15s, + background-color 0.15s; + + &:hover { + color: rgba(0, 0, 0, 0.88); + background: #f5f5f5; + } + + &:active { + color: rgba(0, 0, 0, 0.88); + background: #ebebeb; + } } .pageTabs { display: flex; + flex: 1; align-items: flex-start; - width: 100%; + box-sizing: border-box; + min-width: 0; + width: auto; height: 100%; overflow-x: auto; overflow-y: hidden; @@ -227,3 +262,29 @@ } } } + +.batchMenuItem { + display: flex; + align-items: center; + padding: 4px 0; + gap: 8px; + line-height: 1; +} + +.batchMenuItem + .batchMenuItem { + margin-top: 4px; +} + +.batchMenuIcon { + display: inline-flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + line-height: 1; + + > svg { + display: block; + } +} diff --git a/src/config/mock/eventOperationLogs.ts b/src/config/mock/eventOperationLogs.ts new file mode 100644 index 0000000..a068752 --- /dev/null +++ b/src/config/mock/eventOperationLogs.ts @@ -0,0 +1,76 @@ +/** + * 赛事操作日志假数据 + * + * 类型内联,避免与 @/api/logs 循环引用。 + */ +interface MockTournamentAdminOperationPageVO { + opName: string; + source: string; + nickname: string; + phone: string; + tournamentName: string; + obj: string; + content: string; + createDate: string; +} + +interface MockEventOperationLogQueryParams { + source?: string; + sourceNickname?: string; + sourcePhone?: string; + tournamentName?: string; +} + +interface MockPageData { + total: number; + list: T[]; +} + +const MOCK_EVENT_LOGS: MockTournamentAdminOperationPageVO[] = [ + { + opName: '创建赛事', + source: 'PC', + nickname: '张三', + phone: '13800000001', + tournamentName: '第二届全国地学羽毛球邀请赛', + obj: '赛事', + content: '创建赛事「第二届全国地学羽毛球邀请赛」', + createDate: '2026-07-30 08:00:00', + }, + { + opName: '修改赛事', + source: '小程序', + nickname: '李四', + phone: '13800000002', + tournamentName: '春季马拉松大赛', + obj: '赛事', + content: '修改赛事基础信息', + createDate: '2026-07-29 10:12:00', + }, +]; + +export function buildMockEventOperationLogPage( + page: number, + limit: number, + params: MockEventOperationLogQueryParams, +): MockPageData { + let filtered = [...MOCK_EVENT_LOGS]; + + if (params.source) { + const sourceLabel = params.source === '1' ? 'PC' : '小程序'; + filtered = filtered.filter((item) => item.source === sourceLabel); + } + if (params.sourceNickname) { + filtered = filtered.filter((item) => item.nickname.includes(params.sourceNickname!)); + } + if (params.sourcePhone) { + filtered = filtered.filter((item) => item.phone.includes(params.sourcePhone!)); + } + if (params.tournamentName) { + filtered = filtered.filter((item) => item.tournamentName.includes(params.tournamentName!)); + } + + const total = filtered.length; + const start = (page - 1) * limit; + return { total, list: filtered.slice(start, start + limit) }; +} diff --git a/src/config/mock/operationLogs.ts b/src/config/mock/operationLogs.ts index 8215b61..397132f 100644 --- a/src/config/mock/operationLogs.ts +++ b/src/config/mock/operationLogs.ts @@ -2,12 +2,28 @@ * 操作日志假数据 * 对应接口:GET /admin/sys/operation/page * - * 支持按 type、nickname、dateBegin/dateEnd 前端筛选, - * 模拟后端分页行为。 + * 类型内联,避免与 @/api/logs 循环引用。 */ -import type { SysOperationLogVO, OperationLogQueryParams, PageData } from '@/api/logs'; +interface MockSysOperationLogVO { + type: number; + nickname: string; + createDate: string; + content: string; +} -const MOCK_LOGS: SysOperationLogVO[] = [ +interface MockOperationLogQueryParams { + type?: string; + nickname?: string; + dateBegin?: string; + dateEnd?: string; +} + +interface MockPageData { + total: number; + list: T[]; +} + +const MOCK_LOGS: MockSysOperationLogVO[] = [ { type: 1, nickname: '张三', @@ -133,7 +149,7 @@ const MOCK_LOGS: SysOperationLogVO[] = [ /** 按日期范围过滤(dateBegin / dateEnd 格式 yyyy-MM-dd) */ function inDateRange(dateStr: string, begin?: string, end?: string): boolean { if (!begin && !end) return true; - const d = dateStr.slice(0, 10); // yyyy-MM-dd + const d = dateStr.slice(0, 10); if (begin && d < begin) return false; if (end && d > end) return false; return true; @@ -142,21 +158,18 @@ function inDateRange(dateStr: string, begin?: string, end?: string): boolean { export function buildMockOperationLogPage( page: number, limit: number, - params: OperationLogQueryParams, -): PageData { + params: MockOperationLogQueryParams, +): MockPageData { let filtered = [...MOCK_LOGS]; - // 按操作类型筛选(API 传 '1'/'2'/'3'/'4') if (params.type) { filtered = filtered.filter((item) => item.type === Number(params.type)); } - // 按操作人昵称模糊筛选 if (params.nickname) { filtered = filtered.filter((item) => item.nickname.includes(params.nickname!)); } - // 按日期范围筛选 filtered = filtered.filter((item) => inDateRange(item.createDate, params.dateBegin, params.dateEnd), ); diff --git a/src/config/mock/paymentFlow.ts b/src/config/mock/paymentFlow.ts new file mode 100644 index 0000000..1ddc00e --- /dev/null +++ b/src/config/mock/paymentFlow.ts @@ -0,0 +1,149 @@ +/** + * 支付流水假数据 + * 对应接口: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/walletList.ts b/src/config/mock/walletList.ts new file mode 100644 index 0000000..afd273c --- /dev/null +++ b/src/config/mock/walletList.ts @@ -0,0 +1,185 @@ +/** + * 钱包列表假数据 + * 对应接口: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 new file mode 100644 index 0000000..e597b1b --- /dev/null +++ b/src/config/mock/walletTransactions.ts @@ -0,0 +1,187 @@ +/** + * 钱包交易明细假数据 + * 对应接口: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 new file mode 100644 index 0000000..feb23e4 --- /dev/null +++ b/src/config/mock/withdrawInfo.ts @@ -0,0 +1,150 @@ +/** + * 提现详情假数据 + * 对应接口: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 new file mode 100644 index 0000000..aa1c268 --- /dev/null +++ b/src/config/mock/withdrawList.ts @@ -0,0 +1,194 @@ +/** + * 提现列表假数据 + * 对应接口: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/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx index 2f0693b..fc6064b 100644 --- a/src/layouts/BasicLayout.tsx +++ b/src/layouts/BasicLayout.tsx @@ -79,7 +79,15 @@ export default defineComponent({ const { menuItems } = useMenuStore(); const { phone } = useUserStore(); - const { tabs, cachedViews, removeTab } = useTabsStore(); + const { + tabs, + cachedViews, + removeTab, + removeLeftTabs, + removeRightTabs, + removeOtherTabs, + removeAllTabs, + } = useTabsStore(); const { breadcrumbs } = useBreadcrumb(); const selectedKeys = computed(() => [route.path]); @@ -127,7 +135,7 @@ export default defineComponent({ password: payload.newPassword, confirmPassword: payload.newPassword, }); - if (res.code === 200) { + if (res.code == 200) { message.success('密码修改成功'); setChangePwdVisible(false); } else { @@ -160,6 +168,22 @@ export default defineComponent({ removeTab(key); }; + const handleCloseLeftTabs = (key: string) => { + removeLeftTabs(key); + }; + + const handleCloseRightTabs = (key: string) => { + removeRightTabs(key); + }; + + const handleCloseOtherTabs = (key: string) => { + removeOtherTabs(key); + }; + + const handleCloseAllTabs = () => { + removeAllTabs(); + }; + return () => ( @@ -241,6 +265,10 @@ export default defineComponent({ activeKey={activeTabKey.value} onChange={handleTabChange} onClose={handleTabClose} + onCloseLeft={handleCloseLeftTabs} + onCloseRight={handleCloseRightTabs} + onCloseOther={handleCloseOtherTabs} + onCloseAll={handleCloseAllTabs} /> diff --git a/src/pages/events/banner/index.tsx b/src/pages/events/banner/index.tsx index e9c7db1..af46bac 100644 --- a/src/pages/events/banner/index.tsx +++ b/src/pages/events/banner/index.tsx @@ -243,7 +243,6 @@ export default defineComponent({ showSizeChanger showTotal={(total: number) => `共 ${total} 条`} onChange={handlePageChange} - onShowSizeChange={handlePageChange} /> diff --git a/src/pages/events/banner/model/services.ts b/src/pages/events/banner/model/services.ts new file mode 100644 index 0000000..e3dec86 --- /dev/null +++ b/src/pages/events/banner/model/services.ts @@ -0,0 +1,72 @@ +/** + * Banner 管理 - 接口服务 + * 该页面使用的所有接口集中管理 + */ +import { get, post } from '@/utils/request'; +import { USE_MOCK, MOCK_DELAY } from '@/config/mock'; +import { MOCK_BANNER_LIST } from '@/config/mock/bannerList'; +import type { + BannerListQueryParams, + BannerSaveParams, + BannerDelParams, + BannerActiveParams, + TournamentAdminBannerListVO, + ApiResult, +} from '@/api/banner/types'; + +export type { + TournamentAdminBannerListVO, + BannerListQueryParams, + BannerSaveParams, + BannerDelParams, + BannerActiveParams, + ApiResult, +} from '@/api/banner/types'; + +// ============================================================ +// 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)); + +// ============================================================ +// API 函数 +// ============================================================ + +/** 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(bannerList, params as Record); +} + +/** 删除 Banner */ +export function delBanner(id: string): Promise>> { + return post(bannerDel, { id }); +} + +/** 新增 Banner */ +export function saveBanner(params: BannerSaveParams): Promise>> { + return post(bannerSave, params); +} + +/** 编辑 Banner */ +export function updateBanner(params: BannerSaveParams): Promise>> { + return post(bannerUpdate, params); +} + +/** 启用/禁用 Banner */ +export function toggleBannerActive( + params: BannerActiveParams, +): Promise>> { + return post(bannerActive, params); +} diff --git a/src/pages/events/banner/model/useBannerModel.ts b/src/pages/events/banner/model/useBannerModel.ts index 99a67e2..85aa6ee 100644 --- a/src/pages/events/banner/model/useBannerModel.ts +++ b/src/pages/events/banner/model/useBannerModel.ts @@ -10,7 +10,7 @@ import { toggleBannerActive, type TournamentAdminBannerListVO, type BannerListQueryParams, -} from '@/api/banner'; +} from './services'; import { getProvinceCityOptions } from '@/utils/areaData'; // ============================================================ @@ -138,7 +138,7 @@ export function useBannerModel() { console.log('Banner列表查询参数:', queryParams); const res = await getBannerList(queryParams); - if (res.code === 200) { + if (res.code == 200) { setAllData(res.data); setPagination({ current: 1, pageSize: 10, total: res.data.length }); } else { @@ -205,7 +205,7 @@ export function useBannerModel() { const res = isEdit ? await updateBanner({ ...params, id: editingRecord.value.id }) : await saveBanner(params); - if (res.code === 200) { + if (res.code == 200) { message.success(isEdit ? '编辑成功' : '新增成功'); handleCloseModal(); handleSearch(); @@ -224,7 +224,7 @@ export function useBannerModel() { setLoading(true); try { const res = await delBanner(record.id); - if (res.code === 200) { + if (res.code == 200) { message.success('删除成功'); handleSearch(); } else { @@ -244,7 +244,7 @@ export function useBannerModel() { const newStatus = record.status === '1' ? '0' : '1'; const actionText = newStatus === '1' ? '启用' : '禁用'; const res = await toggleBannerActive({ id: record.id, status: newStatus }); - if (res.code === 200) { + if (res.code == 200) { message.success(`${actionText}成功`); handleSearch(); } else { diff --git a/src/pages/events/list/components/EventDetailModal.tsx b/src/pages/events/list/components/EventDetailModal.tsx index 10af427..9bc8816 100644 --- a/src/pages/events/list/components/EventDetailModal.tsx +++ b/src/pages/events/list/components/EventDetailModal.tsx @@ -8,7 +8,7 @@ import { type TournamentAdminInfoVO, type InnerCategoryVO, type TournamentAdminInfoSignupVO, -} from '@/api/events'; +} from '../model/services'; import { useState } from '@/hooks'; import styles from './EventDetailModal.module.less'; @@ -99,7 +99,7 @@ export default defineComponent({ page: String(page), limit: String(PAGE_SIZE), }); - if (res.code === 200) { + if (res.code == 200) { categoryPlayers[categoryId] = res.data.list; categoryPlayerTotal[categoryId] = res.data.total; categoryPlayerPage[categoryId] = page; @@ -127,7 +127,7 @@ export default defineComponent({ } const res = await getEventDetail(String(id)); - if (res.code === 200) { + if (res.code == 200) { setDetail(res.data); // 为每个组别加载第一页选手 for (const cat of res.data.categoryList) { diff --git a/src/pages/events/list/index.tsx b/src/pages/events/list/index.tsx index f0fb526..2251798 100644 --- a/src/pages/events/list/index.tsx +++ b/src/pages/events/list/index.tsx @@ -20,7 +20,7 @@ import { ID_CARD_OPTIONS, ID_CARD_IMAGE_OPTIONS, } from './model/useEventListModel'; -import { toggleEventOnline } from '@/api/events'; +import { toggleEventOnline } from './model/services'; import { regionOptions } from '@/utils/areaData'; import { useContainerSize, useState } from '@/hooks'; import EventRegulationModal from './components/EventRegulationModal'; @@ -152,7 +152,7 @@ export default defineComponent({ const online = isRemoved ? '1' : '0'; try { const res = await toggleEventOnline({ id: record.id, online }); - if (res.code === 200) { + if (res.code == 200) { message.success(`${actionText}成功`); handleSearch(); } else { @@ -340,7 +340,6 @@ export default defineComponent({ showSizeChanger showTotal={(total: number) => `共 ${total} 条`} onChange={handlePageChange} - onShowSizeChange={handlePageChange} /> diff --git a/src/pages/events/list/model/services.ts b/src/pages/events/list/model/services.ts new file mode 100644 index 0000000..03e3d22 --- /dev/null +++ b/src/pages/events/list/model/services.ts @@ -0,0 +1,97 @@ +/** + * 赛事列表 - 接口服务 + * 该页面使用的所有接口集中管理 + */ +import { get, post } from '@/utils/request'; +import { USE_MOCK, MOCK_DELAY } from '@/config/mock'; +import { MOCK_EVENT_DETAIL } from '@/config/mock/eventDetail'; +import { buildMockEventListPage } from '@/config/mock/eventList'; +import { buildMockSignupListPage } from '@/config/mock/eventSignup'; +import type { + EventListQueryParams, + SignupListQueryParams, + ToggleOnlineParams, + ApiResult, + PageData, + TournamentAdminVO, + TournamentAdminInfoVO, + TournamentAdminInfoSignupVO, +} from '@/api/events/types'; + +export type { + TournamentAdminVO, + TournamentAdminInfoVO, + TournamentAdminInfoSignupVO, + InnerCategoryVO, + EventListQueryParams, + SignupListQueryParams, + ToggleOnlineParams, + PageData, + ApiResult, +} from '@/api/events/types'; + +// ============================================================ +// URL 常量 +// ============================================================ +const eventList = '/manager/tm/page'; +const eventDetail = '/manager/tm/info'; +const signupList = '/manager/tm/info/signupPage'; +const toggleOnline = '/manager/tm/online'; + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +// ============================================================ +// API 函数 +// ============================================================ + +/** 赛事列表 - 列表分页 */ +export async function getEventList( + params: EventListQueryParams, +): Promise>> { + if (USE_MOCK) { + await delay(MOCK_DELAY); + const page = Number(params.page) || 1; + const limit = Number(params.limit) || 10; + return { + code: 200, + msg: 'success', + data: buildMockEventListPage(page, limit), + }; + } + return get(eventList, params as Record); +} + +/** 赛事列表 - 赛事详情 */ +export async function getEventDetail(id: string): Promise> { + if (USE_MOCK) { + await delay(MOCK_DELAY); + return { + code: 200, + msg: 'success', + data: { ...MOCK_EVENT_DETAIL }, + }; + } + return get(eventDetail, { id }); +} + +/** 赛事列表 - 赛事详情 - 人员列表 */ +export async function getSignupList( + params: SignupListQueryParams, +): Promise>> { + if (USE_MOCK) { + await delay(MOCK_DELAY); + return { + code: 200, + msg: 'success', + data: buildMockSignupListPage(params), + }; + } + return get(signupList, params as Record); +} + +/** 赛事上架 / 下架 */ +export function toggleEventOnline( + params: ToggleOnlineParams, +): Promise>> { + return post(toggleOnline, params); +} diff --git a/src/pages/events/list/model/useEventListModel.ts b/src/pages/events/list/model/useEventListModel.ts index 5c30fa1..10e1da3 100644 --- a/src/pages/events/list/model/useEventListModel.ts +++ b/src/pages/events/list/model/useEventListModel.ts @@ -2,8 +2,8 @@ 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 type { TournamentAdminVO, EventListQueryParams } from '@/api/events'; -import { getEventList } from '@/api/events'; +import type { TournamentAdminVO, EventListQueryParams } from './services'; +import { getEventList } from './services'; // ============================================================ // 常量 @@ -252,7 +252,7 @@ export function useEventListModel() { console.log('赛事列表查询参数:', queryParams); const res = await getEventList(queryParams); - if (res.code === 200) { + if (res.code == 200) { setDataSource(res.data.list); setPagination({ ...pagination.value, total: res.data.total }); } else { diff --git a/src/pages/events/logs/index.module.less b/src/pages/events/logs/index.module.less new file mode 100644 index 0000000..c00070d --- /dev/null +++ b/src/pages/events/logs/index.module.less @@ -0,0 +1,36 @@ +// 操作内容单元格:文字最多 2 行省略,末尾带"查看"链接 +:global { + .log-content-cell { + display: flex; + align-items: center; + gap: 6px; + width: 100%; + cursor: default; + } + + .log-content-text { + flex: 1; + min-width: 0; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + line-height: 1.5; + word-break: break-all; + color: rgba(0, 0, 0, 0.88); + } + + .log-content-view { + flex-shrink: 0; + color: #1677ff; + cursor: pointer; + user-select: none; + line-height: 1.5; + white-space: nowrap; + + &:hover { + opacity: 0.85; + } + } +} diff --git a/src/pages/events/logs/index.tsx b/src/pages/events/logs/index.tsx new file mode 100644 index 0000000..6070221 --- /dev/null +++ b/src/pages/events/logs/index.tsx @@ -0,0 +1,209 @@ +import { defineComponent, ref, onMounted, onUnmounted, nextTick } from 'vue'; +import { + Button, + Input, + Table, + DatePicker, + Form, + Space, + Select, + Tooltip, + Pagination, +} from 'ant-design-vue'; +import { + useLogModel, + ACTION_TYPE_OPTIONS, + ACTION_SOURCE_OPTIONS, + ACTION_SOURCE_MAP, +} from './model/useLogModel'; +import { useState, useContainerSize } from '@/hooks'; +import pageStyles from '@/assets/styles/pageLayout.module.less'; +import './index.module.less'; + +const { RangePicker } = DatePicker; + +// ============================================================ +// LogContentCell: 操作内容单元格 +// ============================================================ +const LogContentCell = defineComponent({ + name: 'LogContentCell', + props: { text: { type: String, required: true } }, + setup(props) { + const textRef = ref(null); + const [overflow, setOverflow] = useState(false); + const checkOverflow = () => { + const el = textRef.value; + if (el) setOverflow(el.scrollHeight > el.clientHeight); + }; + let observer: ResizeObserver | null = null; + onMounted(() => { + nextTick(checkOverflow); + const el = textRef.value; + if (el) { + observer = new ResizeObserver(checkOverflow); + observer.observe(el); + } + }); + onUnmounted(() => observer?.disconnect()); + return () => { + const raw = props.text; + return ( +
+
+ {raw} +
+ {overflow.value ? ( + + 查看 + + ) : null} +
+ ); + }; + }, +}); + +// ============================================================ +// bodyCell 渲染 +// ============================================================ +function renderBodyCell({ column, text }: { column: any; text: any }) { + const value = text || '-'; + if (column.key === 'source') return {ACTION_SOURCE_MAP[text] || value}; + return {value}; +} + +export default defineComponent({ + name: 'EventLogs', + setup() { + const { + filterForm, + loading, + dataSource, + columns, + pagination, + handleSearch, + handleReset, + handlePageChange, + } = useLogModel(); + const { containerRef, height } = useContainerSize({ headerOffset: 55 }); + + onMounted(() => { + handleSearch(); + }); + + return () => ( +
+
+
+ + (filterForm.dateRange = val)} + /> + + + (filterForm.source = val || '')} + /> + + + + (filterForm.sourceNickname = val.target?.value ?? val ?? '') + } + onPressEnter={handleSearch} + /> + + + + (filterForm.sourcePhone = val.target?.value ?? val ?? '') + } + onPressEnter={handleSearch} + /> + + + + (filterForm.tournamentName = val.target?.value ?? val ?? '') + } + onPressEnter={handleSearch} + /> + + + + + + + +
+
+ +
+
+ + {{ + bodyCell: (args: any) => { + if (args.column.key === 'content') { + const raw = args.text || ''; + return raw ? : -; + } + return renderBodyCell(args); + }, + }} +
+
+
+ `共 ${total} 条`} + onChange={handlePageChange} + /> +
+
+
+ ); + }, +}); diff --git a/src/pages/events/logs/model/services.ts b/src/pages/events/logs/model/services.ts new file mode 100644 index 0000000..7102626 --- /dev/null +++ b/src/pages/events/logs/model/services.ts @@ -0,0 +1,53 @@ +/** + * 赛事操作日志 - 接口服务 + * 该页面使用的所有接口集中管理 + */ +import { get } from '@/utils/request'; +import { USE_MOCK, MOCK_DELAY } from '@/config/mock'; +import { buildMockEventOperationLogPage } from '@/config/mock/eventOperationLogs'; +import type { + OperationLogQueryParams, + TournamentAdminOperationPageVO, + PageData, + ApiResult, +} from '@/api/logs/types'; + +export type { + TournamentAdminOperationPageVO, + OperationLogQueryParams, + PageData, + ApiResult, +} from '@/api/logs/types'; + +// ============================================================ +// URL 常量 +// ============================================================ +const operationLogs = '/tournament/operation/page'; + +const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +// ============================================================ +// API 函数 +// ============================================================ + +function pickFilters(params: OperationLogQueryParams): OperationLogQueryParams { + const { page: _page, limit: _limit, ...filters } = params; + return filters; +} + +/** 赛事操作日志分页 */ +export async function getOperationLogs( + 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: buildMockEventOperationLogPage(page, limit, pickFilters(params)), + }; + } + return get(operationLogs, params as Record); +} diff --git a/src/pages/events/logs/model/useLogModel.ts b/src/pages/events/logs/model/useLogModel.ts new file mode 100644 index 0000000..a6ce42d --- /dev/null +++ b/src/pages/events/logs/model/useLogModel.ts @@ -0,0 +1,154 @@ +import { computed, reactive, toRef, Ref } from 'vue'; +import { message } from 'ant-design-vue'; +import { useState, useDebounce, useThrottleFn } from '@/hooks'; +import { + getOperationLogs, + type TournamentAdminOperationPageVO, + type OperationLogQueryParams, +} from './services'; + +// ============================================================ +// 常量 +// ============================================================ + +/** 操作类型选项(TODO: API type 字段待定意,暂时留空) */ +export const ACTION_TYPE_OPTIONS = [{ value: '', label: '全部' }] as const; + +/** 操作来源选项(value 对应 API source: 1=PC,2=小程序) */ +export const ACTION_SOURCE_OPTIONS = [ + { value: '', label: '全部' }, + { value: '1', label: 'PC' }, + { value: '2', label: '小程序' }, +] as const; + +/** 操作来源文案映射 */ +export const ACTION_SOURCE_MAP: Record = { + '1': 'PC', + '2': '小程序', +}; + +// ============================================================ +// Model +// ============================================================ + +export function useLogModel() { + // ===== 筛选条件(key 名对齐 API 查询参数) ===== + const filterForm = reactive({ + dateRange: null as [string, string] | null, + type: '', + source: '', + sourceNickname: '', + sourcePhone: '', + tournamentName: '', + }); + + const { debouncedValue: debouncedNickname } = useDebounce( + toRef(filterForm, 'sourceNickname') as Ref, + { delay: 300 }, + ); + const { debouncedValue: debouncedPhone } = useDebounce( + toRef(filterForm, 'sourcePhone') as Ref, + { delay: 300 }, + ); + const { debouncedValue: debouncedTournamentName } = useDebounce( + toRef(filterForm, 'tournamentName') as Ref, + { delay: 300 }, + ); + + // ===== 表格状态 ===== + const [loading, setLoading] = useState(false); + const [dataSource, setDataSource] = useState([]); + const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }); + + // ===== 表格列配置(dataIndex 对齐 TournamentAdminOperationPageVO) ===== + const columns = [ + { title: '操作类型', dataIndex: 'opName', key: 'opName', width: 140 }, + { title: '操作来源', dataIndex: 'source', key: 'source', width: 100 }, + { title: '操作人昵称', dataIndex: 'nickname', key: 'nickname', width: 120 }, + { title: '操作人手机号', dataIndex: 'phone', key: 'phone', width: 140 }, + { title: '赛事名称', dataIndex: 'tournamentName', key: 'tournamentName', width: 200 }, + { title: '操作对象', dataIndex: 'obj', key: 'obj', width: 240 }, + { title: '操作内容', dataIndex: 'content', key: 'content', width: 360 }, + { title: '操作时间', dataIndex: 'createDate', key: 'createDate', width: 170 }, + ]; + + // ===== 构建 API 查询参数 ===== + const buildQueryParams = (): OperationLogQueryParams => { + const params: OperationLogQueryParams = { + page: String(pagination.value.current), + limit: String(pagination.value.pageSize), + }; + if (filterForm.dateRange) { + params.dateBegin = filterForm.dateRange[0]; + params.dateEnd = filterForm.dateRange[1]; + } + if (filterForm.type) params.type = filterForm.type; + if (filterForm.source) params.source = filterForm.source; + if (filterForm.sourceNickname.trim()) params.sourceNickname = filterForm.sourceNickname.trim(); + if (filterForm.sourcePhone.trim()) params.sourcePhone = filterForm.sourcePhone.trim(); + if (filterForm.tournamentName.trim()) params.tournamentName = filterForm.tournamentName.trim(); + return params; + }; + + // ===== 计算属性 ===== + const hasFilter = computed(() => { + return ( + filterForm.dateRange !== null || + filterForm.type !== '' || + filterForm.source !== '' || + debouncedNickname.value.trim() !== '' || + debouncedPhone.value.trim() !== '' || + debouncedTournamentName.value.trim() !== '' + ); + }); + + // ===== 方法 ===== + + const handleSearch = useThrottleFn(async () => { + setLoading(true); + try { + const params = buildQueryParams(); + console.log('操作日志查询参数:', params); + const res = await getOperationLogs(params); + if (res.code == 200) { + setDataSource(res.data.list); + setPagination({ ...pagination.value, total: res.data.total }); + } else { + message.error(res.msg || '查询失败'); + } + } catch (e: any) { + console.error('操作日志查询失败:', e); + } finally { + setLoading(false); + } + }, 500); + + const handleReset = useThrottleFn(() => { + filterForm.dateRange = null; + filterForm.type = ''; + filterForm.source = ''; + filterForm.sourceNickname = ''; + filterForm.sourcePhone = ''; + filterForm.tournamentName = ''; + setPagination({ current: 1, pageSize: 10, total: 0 }); + setDataSource([]); + setTimeout(() => handleSearch(), 350); + }, 500); + + const handlePageChange = (page: number, pageSize: number) => { + setPagination({ ...pagination.value, current: page, pageSize }); + handleSearch(); + }; + + return { + filterForm, + loading, + dataSource, + columns, + pagination, + hasFilter, + handleSearch, + handleReset, + handlePageChange, + }; +} diff --git a/src/pages/events/orders/components/OrderDetailModal.tsx b/src/pages/events/orders/components/OrderDetailModal.tsx index b94ab68..3c1d666 100644 --- a/src/pages/events/orders/components/OrderDetailModal.tsx +++ b/src/pages/events/orders/components/OrderDetailModal.tsx @@ -8,7 +8,7 @@ import { type TournamentAdminOrderInfoVO, type InnerRefundInfo, type TournamentAdminOrderInfoPageVO, -} from '@/api/orders'; +} from '../model/services'; import styles from './OrderDetailModal.module.less'; interface OrderDetailModalProps { @@ -142,7 +142,7 @@ export default defineComponent({ page: String(page), limit: String(playerPageSize), }); - if (res.code === 200) { + if (res.code == 200) { setPlayers(res.data.list); setPlayerTotal(res.data.total); setPlayerPage(page); @@ -176,7 +176,7 @@ export default defineComponent({ const [detailRes] = await Promise.all([getOrderDetail(params), fetchPlayers(1)]); - if (detailRes.code === 200) { + if (detailRes.code == 200) { setDetail(detailRes.data); } setDetailLoading(false); diff --git a/src/pages/events/orders/index.tsx b/src/pages/events/orders/index.tsx index 2e050df..9188d16 100644 --- a/src/pages/events/orders/index.tsx +++ b/src/pages/events/orders/index.tsx @@ -13,7 +13,7 @@ import { message, } from 'ant-design-vue'; import { useOrderModel, ORDER_STATUS_OPTIONS, REFUND_STATUS_OPTIONS } from './model/useOrderModel'; -import { retryRefundOrder } from '@/api/orders'; +import { retryRefundOrder } from './model/services'; import { useContainerSize, useState } from '@/hooks'; import OrderDetailModal from './components/OrderDetailModal'; import pageStyles from '@/assets/styles/pageLayout.module.less'; @@ -37,7 +37,7 @@ function promptReRefund(record: any) { onOk: async () => { try { const res = await retryRefundOrder({ payOrderNo: record.orderNo }); - if (res.code === 200) { + if (res.code == 200) { message.success('重新退款提交成功'); } else { message.error(res.msg || '操作失败'); @@ -273,7 +273,6 @@ export default defineComponent({ showSizeChanger showTotal={(total: number) => `共 ${total} 条`} onChange={handlePageChange} - onShowSizeChange={handlePageChange} /> diff --git a/src/pages/events/orders/model/services.ts b/src/pages/events/orders/model/services.ts new file mode 100644 index 0000000..80ac800 --- /dev/null +++ b/src/pages/events/orders/model/services.ts @@ -0,0 +1,103 @@ +/** + * 订单管理 - 接口服务 + * 该页面使用的所有接口集中管理 + */ +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 { buildMockOrderPlayerListPage } from '@/config/mock/orderPlayerList'; +import type { + OrderListQueryParams, + OrderDetailQueryParams, + OrderPlayerListQueryParams, + RetryRefundParams, + TournamentAdminOrderPageVO, + TournamentAdminOrderInfoVO, + TournamentAdminOrderInfoPageVO, + PageData, + ApiResult, +} from '@/api/orders/types'; + +export type { + TournamentAdminOrderPageVO, + TournamentAdminOrderInfoVO, + TournamentAdminOrderInfoPageVO, + InnerRefundInfo, + OrderListQueryParams, + OrderDetailQueryParams, + OrderPlayerListQueryParams, + RetryRefundParams, + PageData, + ApiResult, +} from '@/api/orders/types'; + +// ============================================================ +// URL 常量 +// ============================================================ +const orderList = '/manager/order/page'; +const orderDetail = '/manager/order/info'; +const orderPlayerList = '/manager/order/info/page'; +const retryRefund = '/manager/order/retryRefund'; + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +// ============================================================ +// API 函数 +// ============================================================ + +/** 订单管理 - 订单分页 */ +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(orderList, params as Record); +} + +/** 订单管理 - 订单详情 */ +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(orderDetail, params as Record); +} + +/** 订单管理 - 订单详情 - 选手列表 */ +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(orderPlayerList, params as Record); +} + +/** 订单管理 - 重新退款 */ +export function retryRefundOrder( + params: RetryRefundParams, +): Promise>> { + return post(retryRefund, params); +} diff --git a/src/pages/events/orders/model/useOrderModel.ts b/src/pages/events/orders/model/useOrderModel.ts index bf163fb..e3d7dc2 100644 --- a/src/pages/events/orders/model/useOrderModel.ts +++ b/src/pages/events/orders/model/useOrderModel.ts @@ -6,7 +6,7 @@ import { getOrderList, type TournamentAdminOrderPageVO, type OrderListQueryParams, -} from '@/api/orders'; +} from './services'; import { MOCK_ORDER_SUMMARY } from '@/config/mock/orderList'; // ============================================================ @@ -228,7 +228,7 @@ export function useOrderModel() { console.log('订单列表查询参数:', queryParams); const res = await getOrderList(queryParams); - if (res.code === 200) { + if (res.code == 200) { setDataSource(res.data.list); setPagination({ ...pagination.value, total: res.data.total }); } else { diff --git a/src/pages/events/users/index.tsx b/src/pages/events/users/index.tsx index f090310..f330196 100644 --- a/src/pages/events/users/index.tsx +++ b/src/pages/events/users/index.tsx @@ -11,7 +11,7 @@ import { message, } from 'ant-design-vue'; import { useUserModel, USER_STATUS_OPTIONS, ROLE_OPTIONS } from './model/useUserModel'; -import { toggleUserActive } from '@/api/users'; +import { toggleUserActive } from './model/services'; import { useContainerSize } from '@/hooks'; import pageStyles from '@/assets/styles/pageLayout.module.less'; @@ -41,7 +41,7 @@ export default defineComponent({ onOk: async () => { try { const res = await toggleUserActive({ userId: record.id, status: isActive ? '0' : '1' }); - if (res.code === 200) { + if (res.code == 200) { message.success(`${actionText}成���`); handleSearch(); } else message.error(res.msg || '操作失败'); @@ -145,7 +145,6 @@ export default defineComponent({ showSizeChanger showTotal={(total: number) => `共 ${total} 条`} onChange={handlePageChange} - onShowSizeChange={handlePageChange} /> diff --git a/src/pages/events/users/model/services.ts b/src/pages/events/users/model/services.ts new file mode 100644 index 0000000..91f90a4 --- /dev/null +++ b/src/pages/events/users/model/services.ts @@ -0,0 +1,82 @@ +/** + * 用户列表 - 接口服务 + * 该页面使用的所有接口集中管理 + */ +import { get, post } from '@/utils/request'; +import { USE_MOCK, MOCK_DELAY } from '@/config/mock'; +import { buildMockUserListPage } from '@/config/mock/userList'; +import type { + UserListQueryParams, + UserSaveParams, + UserUpdateParams, + UserActiveParams, + UserUpdatePwdParams, + TournamentAdminUserVO, + PageData, + ApiResult, +} from '@/api/users/types'; + +export type { + TournamentAdminUserVO, + UserListQueryParams, + UserSaveParams, + UserUpdateParams, + UserActiveParams, + UserUpdatePwdParams, + PageData, + ApiResult, +} from '@/api/users/types'; + +// ============================================================ +// URL 常量 +// ============================================================ +const userList = '/sys/user/page'; +const userSave = '/sys/user/save'; +const userUpdate = '/sys/user/update'; +const userActive = '/sys/user/active'; +const userUpdatePwd = '/sys/user/updatepwd'; + +const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +// ============================================================ +// API 函数 +// ============================================================ + +/** 用户分页列表 */ +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(userList, params as Record); +} + +/** 新增用户 */ +export function saveUser(params: UserSaveParams): Promise>> { + if (USE_MOCK) return delay(MOCK_DELAY).then(() => ({ code: 200, msg: 'success', data: {} })); + return post(userSave, params); +} + +/** 更新用户 */ +export function updateUser(params: UserUpdateParams): Promise>> { + if (USE_MOCK) return delay(MOCK_DELAY).then(() => ({ code: 200, msg: 'success', data: {} })); + return post(userUpdate, params); +} + +/** 启用/禁用用户 */ +export function toggleUserActive( + params: UserActiveParams, +): Promise>> { + return post(userActive, params); +} + +/** 修改密码 */ +export function updateUserPwd( + params: UserUpdatePwdParams, +): Promise>> { + return post(userUpdatePwd, params); +} diff --git a/src/pages/events/users/model/useUserModel.ts b/src/pages/events/users/model/useUserModel.ts index 189349d..dbf1458 100644 --- a/src/pages/events/users/model/useUserModel.ts +++ b/src/pages/events/users/model/useUserModel.ts @@ -2,7 +2,7 @@ import { computed, reactive, toRef, Ref, h } from 'vue'; import { message } from 'ant-design-vue'; import { StatusTag, type StatusTagTone } from '@/components'; import { useState, useDebounce, useThrottleFn } from '@/hooks'; -import { getUserList, type TournamentAdminUserVO, type UserListQueryParams } from '@/api/users'; +import { getUserList, type TournamentAdminUserVO, type UserListQueryParams } from './services'; // ============================================================ // 常量 @@ -86,7 +86,7 @@ export function useUserModel() { const params = buildQueryParams(); console.log('用户列表查询参数:', params); const res = await getUserList(params); - if (res.code === 200) { + if (res.code == 200) { setDataSource(res.data.list); setPagination({ ...pagination.value, total: res.data.total }); } else message.error(res.msg || '查询失败'); diff --git a/src/pages/finance/payments/index.tsx b/src/pages/finance/payments/index.tsx index 98527c2..f3d2162 100644 --- a/src/pages/finance/payments/index.tsx +++ b/src/pages/finance/payments/index.tsx @@ -1,4 +1,4 @@ -import { defineComponent } from 'vue'; +import { defineComponent, onMounted } from 'vue'; import { Button, Input, Table, Form, Space, Pagination, Select, DatePicker } from 'ant-design-vue'; import { usePaymentsModel } from './model/usePaymentsModel'; import { useContainerSize } from '@/hooks'; @@ -6,9 +6,6 @@ import pageStyles from '@/assets/styles/pageLayout.module.less'; const { RangePicker } = DatePicker; -/** - * 支付流水 - */ export default defineComponent({ name: 'FinancePayments', setup() { @@ -27,9 +24,12 @@ export default defineComponent({ const { containerRef, height } = useContainerSize({ headerOffset: 55 }); + onMounted(() => { + handleSearch(); + }); + return () => (
- {/* ===== 筛选区 ===== */}
@@ -40,16 +40,18 @@ export default defineComponent({ onUpdate:value={(val: any) => (filterForm.timeRange = val)} /> - + - + (detailFilter.type = val || '')} + placeholder="全部" + onUpdate:value={(val: any) => (filterForm.type = val || '')} /> - +
- {/* ===== 下方区域:筛选 + 表格(独立白卡片) ===== */} - {/* ===== 表格区 ===== */}
@@ -203,20 +194,16 @@ export default defineComponent({ columns={tableColumns} dataSource={dataSource.value} loading={loading.value} + rowKey="userId" scroll={{ x: 'max-content', y: height.value }} pagination={false} > {{ bodyCell: (args: any) => - renderBodyCell({ - ...args, - onViewDetail: handleViewDetail, - }), + renderBodyCell({ ...args, onViewDetail: handleViewDetail }), }}
- - {/* 独立分页,右下方 */}
`共 ${total} 条`} onChange={handlePageChange} - onShowSizeChange={handlePageChange} />
- {/* ===== 交易明细弹窗 ===== */} new Promise((r) => setTimeout(r, ms)); + +// ============================================================ +// API 函数 +// ============================================================ + +/** 钱包列表分页 */ +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) }; + } + return get(walletList, params as Record); +} + +/** 交易明细分页 */ +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(walletTransaction, params as Record); +} diff --git a/src/pages/finance/wallet/model/useWalletModel.ts b/src/pages/finance/wallet/model/useWalletModel.ts index e334496..437496a 100644 --- a/src/pages/finance/wallet/model/useWalletModel.ts +++ b/src/pages/finance/wallet/model/useWalletModel.ts @@ -1,292 +1,176 @@ -import { computed, reactive, toRef, Ref, h } from 'vue'; -import { message } from 'ant-design-vue'; -import { StatusTag, type StatusTagTone } from '@/components'; +import { computed, reactive, toRef, Ref } from 'vue'; import { useState, useDebounce, useThrottleFn } from '@/hooks'; +import { getWalletList, type WalletSummaryVO, type WalletQueryParams } from './services'; // ============================================================ -// 常量 +// 常量(value 对齐 API 数字码) // ============================================================ -/** 明细类型选项(弹窗内筛选) */ +/** 交易类型选项:value 对应 API type 字段 */ export const TRANSACTION_TYPE_OPTIONS = [ { value: '', label: '全部' }, - { value: '报名收入', label: '报名收入' }, - { value: '提现', label: '提现' }, - { value: '取消报名退款', label: '取消报名退款' }, + { value: '1', label: '报名收入' }, + { value: '2', label: '提现' }, + { value: '3', label: '取消报名退款' }, ] as const; -/** 明细状态选项(弹窗内筛选) */ +/** 交易状态选项:value 对应 API status 字段 */ export const TRANSACTION_STATUS_OPTIONS = [ { value: '', label: '全部' }, - { value: '正常', label: '正常' }, - { value: '冻结中', label: '冻结中' }, - { value: '已解冻', label: '已解冻' }, + { value: '1', label: '已解冻' }, + { value: '2', label: '冻结中' }, + { value: '3', label: '正常' }, ] as const; -/** 明细状态映射(用于表格 StatusTag 渲染) */ -const TRANSACTION_STATUS_MAP: Record = { - 正常: { label: '正常', tone: 'primary' }, - 冻结中: { label: '冻结中', tone: 'orange' }, - 已解冻: { label: '已解冻', tone: 'success' }, +/** 交易类型文案映射 */ +export const TRANSACTION_TYPE_MAP: Record = { + 1: '报名收入', + 2: '提现', + 3: '取消报名退款', }; -// ============================================================ -// 顶部统计卡假数据 -// ============================================================ - -const MOCK_SUMMARY = { - totalBalance: 86520.0, - frozenAmount: 3200.0, - totalWithdraw: 63080.0, +/** 交易状态文案映射(弹窗内表格渲染用) */ +export const TRANSACTION_STATUS_LABEL_MAP: Record = { + 1: '已解冻', + 2: '冻结中', + 3: '正常', }; -// ============================================================ -// 假数据(钱包列表) -// ============================================================ - -const MOCK_DATA = [ - { - key: '1', - userId: 'U20260701001', - nickName: '张三', - phone: '12345678997', - balance: 0, - frozen: 0, - totalWithdraw: 0, - }, - { - key: '2', - userId: '', - nickName: '李四', - phone: '12345678998', - balance: 350, - frozen: 200, - totalWithdraw: 1450, - }, - { - key: '3', - userId: '', - nickName: '王五', - phone: '12345678999', - balance: 1280, - frozen: 0, - totalWithdraw: 800, - }, - { - key: '4', - userId: 'U20260701004', - nickName: '赵六', - phone: '13800138000', - balance: 560, - frozen: 100, - totalWithdraw: 200, - }, - { - key: '5', - userId: '', - nickName: '钱七', - phone: '13900139001', - balance: 4200, - frozen: 1500, - totalWithdraw: 5600, - }, - { - key: '6', - userId: '', - nickName: '孙八', - phone: '12345678997', - balance: 880, - frozen: 0, - totalWithdraw: 300, - }, -]; - -// ============================================================ -// 交易明细假数据(所有用户共用,演示用) -// ============================================================ - -const MOCK_TRANSACTIONS: any[] = [ - { - key: 't-1', - type: '报名收入', - amount: 100, - balance: 600, - withdrawable: 500, - frozen: 100, - status: '已解冻', - frozenTime: '2026-06-10 08:00', - unfreezeTime: '2026-06-12 08:00', - relatedNo: 'xxxxxxxx', - time: '2026-06-10 08:00', - }, - { - key: 't-2', - type: '提现', - amount: -500, - balance: 1100, - withdrawable: 500, - frozen: 600, - status: '冻结中', - frozenTime: '2026-06-10 06:00', - unfreezeTime: '-', - relatedNo: 'xxxxxxxx', - time: '2026-06-10 06:00', - }, - { - key: 't-3', - type: '取消报名退款', - amount: -100, - balance: 500, - withdrawable: 500, - frozen: 0, - status: '正常', - frozenTime: '-', - unfreezeTime: '-', - relatedNo: 'xxxxxxxx', - time: '2026-06-10 05:00', - }, - { - key: 't-4', - type: '报名收入', - amount: 350, - balance: 850, - withdrawable: 750, - frozen: 100, - status: '正常', - frozenTime: '-', - unfreezeTime: '-', - relatedNo: 'xxxxxxxx', - time: '2026-06-09 17:30', - }, - { - key: 't-5', - type: '提现', - amount: -250, - balance: 600, - withdrawable: 500, - frozen: 100, - status: '已解冻', - frozenTime: '2026-06-08 10:00', - unfreezeTime: '2026-06-09 08:00', - relatedNo: 'xxxxxxxx', - time: '2026-06-08 10:00', - }, -]; - // ============================================================ // Model // ============================================================ -/** - * 用户钱包页数据模型 - */ export function useWalletModel() { - // ===== 筛选条件 ===== + // ===== 筛选条件(字段名对齐 API) ===== const filterForm = reactive({ - searchUserName: '', - searchPhone: '', - minBalance: '' as string | number, - maxBalance: '' as string | number, + nickname: '', + phone: '', + minBalance: '', + maxBalance: '', }); - // 可搜索字段防抖 - const { debouncedValue: debouncedUserName } = useDebounce( - toRef(filterForm, 'searchUserName') as Ref, + const { debouncedValue: debouncedNickname } = useDebounce( + toRef(filterForm, 'nickname') as Ref, { delay: 300 }, ); const { debouncedValue: debouncedPhone } = useDebounce( - toRef(filterForm, 'searchPhone') as Ref, + toRef(filterForm, 'phone') as Ref, { delay: 300 }, ); - // ===== 顶部统计 ===== - const [summary, setSummary] = useState(MOCK_SUMMARY); + // ===== 顶部统计(汇总值从接口返回的第一条记录中提取) ===== + const [summary, setSummary] = useState({ + totalBalance: '0.00', + frozenAmount: '0.00', + totalWithdraw: '0.00', + }); // ===== 表格状态 ===== const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState(MOCK_DATA); - const [pagination, setPagination] = useState({ - current: 1, - pageSize: 10, - total: MOCK_DATA.length, - }); + const [dataSource, setDataSource] = useState([]); + const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }); // ===== 详情弹窗状态 ===== const [detailVisible, setDetailVisible] = useState(false); const [currentWallet, setCurrentWallet] = useState({}); - const [detailLoading, setDetailLoading] = useState(false); - // ===== 表格列配置 ===== + // ===== 表格列配置(dataIndex 对齐 API WalletSummaryVO) ===== const columns = [ - { title: '用户ID', dataIndex: 'userId', key: 'userId', width: 160 }, - { title: '用户昵称', dataIndex: 'nickName', key: 'nickName', width: 120 }, + { title: '用户ID', dataIndex: 'userId', key: 'userId', width: 120 }, + { title: '用户昵称', dataIndex: 'userNickname', key: 'userNickname', width: 120 }, { title: '手机号', dataIndex: 'phone', key: 'phone', width: 140 }, { title: '余额', - dataIndex: 'balance', - key: 'balance', - width: 120, - customRender: ({ text }: { text: number }) => `${(text || 0).toFixed(2)}`, + dataIndex: 'amount', + key: 'amount', + width: 130, + align: 'left' as const, }, { title: '冻结金额', - dataIndex: 'frozen', - key: 'frozen', - width: 120, - customRender: ({ text }: { text: number }) => `${(text || 0).toFixed(2)}`, + dataIndex: 'frozenAmount', + key: 'frozenAmount', + width: 130, + align: 'left' as const, }, { title: '累计提现', - dataIndex: 'totalWithdraw', - key: 'totalWithdraw', - width: 120, - customRender: ({ text }: { text: number }) => `${(text || 0).toFixed(2)}`, + dataIndex: 'withdrawalAmount', + key: 'withdrawalAmount', + width: 130, + align: 'left' as const, }, ]; // ===== 计算属性 ===== const hasFilter = computed(() => { return ( - debouncedUserName.value.trim() !== '' || + debouncedNickname.value.trim() !== '' || debouncedPhone.value.trim() !== '' || filterForm.minBalance !== '' || filterForm.maxBalance !== '' ); }); + // ===== 构建 API 查询参数 ===== + const buildQueryParams = (): WalletQueryParams => { + const params: WalletQueryParams = { + page: String(pagination.value.current), + limit: String(pagination.value.pageSize), + }; + if (debouncedNickname.value.trim()) params.nickname = debouncedNickname.value.trim(); + if (debouncedPhone.value.trim()) params.phone = debouncedPhone.value.trim(); + if (filterForm.minBalance) params.amountBegin = filterForm.minBalance; + if (filterForm.maxBalance) params.amountEnd = filterForm.maxBalance; + return params; + }; + // ===== 方法 ===== - /** 查询(节流 500ms) */ const handleSearch = useThrottleFn(async () => { setLoading(true); try { - console.log('搜索条件:', { - nickName: debouncedUserName.value, - phone: debouncedPhone.value, - minBalance: filterForm.minBalance, - maxBalance: filterForm.maxBalance, - }); - // TODO: 替换为真实 API 调用 - setDataSource(MOCK_DATA); - setPagination({ ...pagination.value, total: MOCK_DATA.length }); - setSummary(MOCK_SUMMARY); - message.success('查询成功'); - } catch (error: any) { - message.error(error.msg || '查询失败'); + // 余额筛选:若起始 > 截止,互换两值 + const min = Number(filterForm.minBalance); + const max = Number(filterForm.maxBalance); + if (filterForm.minBalance && filterForm.maxBalance && min > max) { + filterForm.minBalance = filterForm.maxBalance; + filterForm.maxBalance = String(min); + } + + const params = buildQueryParams(); + console.log('钱包列表查询参数:', params); + const res = await getWalletList(params); + if (res.code == 200) { + console.log('钱包列表查询结果:', { total: res.data.total, count: res.data.list.length }); + setDataSource(res.data.list); + setPagination({ ...pagination.value, total: res.data.total }); + // 从第一条记录提取汇总值 + if (res.data.list.length > 0) { + const first = res.data.list[0]; + setSummary({ + totalBalance: first.totalAmount || '0.00', + frozenAmount: first.totalFrozenAmount || '0.00', + totalWithdraw: first.totalWithdrawalAmount || '0.00', + }); + } + } + } catch { + // 网络层已统一提示 } finally { setLoading(false); } }, 500); - /** 重置(节流 500ms) */ const handleReset = useThrottleFn(() => { - filterForm.searchUserName = ''; - filterForm.searchPhone = ''; + filterForm.nickname = ''; + filterForm.phone = ''; filterForm.minBalance = ''; filterForm.maxBalance = ''; - setPagination({ current: 1, pageSize: 10, total: MOCK_DATA.length }); - setDataSource(MOCK_DATA); - setSummary(MOCK_SUMMARY); + setPagination({ current: 1, pageSize: 10, total: 0 }); + setDataSource([]); + setSummary({ totalBalance: '0.00', frozenAmount: '0.00', totalWithdraw: '0.00' }); + setTimeout(() => handleSearch(), 350); }, 500); const handlePageChange = (page: number, pageSize: number) => { @@ -294,41 +178,15 @@ export function useWalletModel() { handleSearch(); }; - /** 打开明细弹窗 */ const handleViewDetail = (record: any) => { setCurrentWallet(record); setDetailVisible(true); }; - /** 关闭明细弹窗 */ const handleCloseDetail = () => { setDetailVisible(false); }; - /** - * 加载某用户的交易明细 - * 真实场景应调用 API;演示阶段所有用户共用同一份假数据 - */ - const loadTransactions = async (_record: any): Promise => { - setDetailLoading(true); - try { - // 模拟接口延迟 - await new Promise((resolve) => setTimeout(resolve, 200)); - return [...MOCK_TRANSACTIONS]; - } finally { - setDetailLoading(false); - } - }; - - /** 渲染交易状态 StatusTag */ - const renderTransactionStatus = (status: string) => { - const info = TRANSACTION_STATUS_MAP[status] || { - label: status || '-', - tone: 'default' as const, - }; - return h(StatusTag, { label: info.label, tone: info.tone }); - }; - return { filterForm, loading, @@ -339,14 +197,11 @@ export function useWalletModel() { hasFilter, detailVisible, currentWallet, - detailLoading, handleSearch, handleReset, handlePageChange, handleViewDetail, handleCloseDetail, - loadTransactions, - renderTransactionStatus, TRANSACTION_TYPE_OPTIONS: TRANSACTION_TYPE_OPTIONS as any, TRANSACTION_STATUS_OPTIONS: TRANSACTION_STATUS_OPTIONS as any, }; diff --git a/src/pages/finance/withdraw/components/WithdrawAuditModal.module.less b/src/pages/finance/withdraw/components/WithdrawDetailModal.module.less similarity index 96% rename from src/pages/finance/withdraw/components/WithdrawAuditModal.module.less rename to src/pages/finance/withdraw/components/WithdrawDetailModal.module.less index e2e2d47..a114085 100644 --- a/src/pages/finance/withdraw/components/WithdrawAuditModal.module.less +++ b/src/pages/finance/withdraw/components/WithdrawDetailModal.module.less @@ -106,6 +106,12 @@ color: rgba(0, 0, 0, 0.85); } + .auditImgList { + display: flex; + gap: 5px; + align-items: center; + } + // ===== 审核表单 ===== .formRow { display: flex; @@ -189,6 +195,10 @@ margin-right: 8px; } + .auditImg + .auditImg { + margin-left: 5px; + } + .noImage { color: rgba(0, 0, 0, 0.45); } diff --git a/src/pages/finance/withdraw/components/WithdrawAuditModal.tsx b/src/pages/finance/withdraw/components/WithdrawDetailModal.tsx similarity index 52% rename from src/pages/finance/withdraw/components/WithdrawAuditModal.tsx rename to src/pages/finance/withdraw/components/WithdrawDetailModal.tsx index c22a010..9e83023 100644 --- a/src/pages/finance/withdraw/components/WithdrawAuditModal.tsx +++ b/src/pages/finance/withdraw/components/WithdrawDetailModal.tsx @@ -1,72 +1,80 @@ import { defineComponent, reactive, ref, watch } from 'vue'; -import { Modal, Radio, Input, Button, Upload, Image, message } from 'ant-design-vue'; +import { Modal, Radio, Input, Button, Upload, Image, Spin, message } from 'ant-design-vue'; import { PlusOutlined } from '@ant-design/icons-vue'; -import { useWithdrawModel } from '../model/useWithdrawModel'; -import styles from './WithdrawAuditModal.module.less'; +import { getWithdrawInfo, postWithdrawAudit, type WithdrawInfoVO } from '../model/services'; +import { AUDIT_STATUS_MAP, PAY_STATUS_MAP } from '../model/useWithdrawModel'; +import styles from './WithdrawDetailModal.module.less'; const { TextArea } = Input; -interface WithdrawAuditModalProps { +interface WithdrawDetailModalProps { visible: boolean; record: any; onClose: () => void; + onAudited: () => void; } /** - * 提现申请详情 / 审核弹窗 - * - * 结构: - * - 基本信息 - * - 银行卡信息 - * - 支付信息 - * - 审核操作 + * 提现详情 / 审核弹窗(查看与审核两用) */ export default defineComponent({ - name: 'WithdrawAuditModal', + name: 'WithdrawDetailModal', props: { visible: { type: Boolean, default: false }, record: { type: Object, default: () => ({}) }, onClose: { type: Function, required: true }, + onAudited: { type: Function, default: undefined }, }, - setup(props: WithdrawAuditModalProps) { - const { submitAudit } = useWithdrawModel(); + setup(props: WithdrawDetailModalProps) { + const detail = ref(null); + const detailLoading = ref(false); - // ===== 表单状态 ===== - const auditForm = reactive({ - pass: true, - remark: '', - }); + const auditForm = reactive({ pass: true, remark: '' }); const imageList = ref([]); - const submitting = ref(false); + const submitting = ref(false); + + // ===== 拉取详情 ===== + const fetchDetail = async (id: number) => { + detailLoading.value = true; + detail.value = null; + try { + const res = await getWithdrawInfo(id); + if (res.code == 200) detail.value = res.data; + } catch { + // 网络层已统一提示 + } finally { + detailLoading.value = false; + } + }; - /** 监听 visible:每次打开都重置表单 */ watch( () => props.visible, - (val) => { - if (val) { + async (val) => { + if (val && props.record?.id) { auditForm.pass = true; auditForm.remark = ''; imageList.value = []; submitting.value = false; + await fetchDetail(props.record.id); } }, + { immediate: true }, ); - /** 上传前校验(演示用:限制 5 张 & 5MB) */ + // ===== 上传 ===== const beforeUpload = (file: any) => { - const isLt5M = file.size / 1024 / 1024 < 5; - if (!isLt5M) { + if (file.size / 1024 / 1024 >= 5) { message.error('图片大小不能超过 5MB'); return false; } imageList.value = [...imageList.value, file]; - return false; // 阻止自动上传,由"提交审核"按钮统一处理 + return false; }; - const handleRemove = (file: any) => { imageList.value = imageList.value.filter((f) => f.uid !== file.uid); }; + // ===== 提交审核 ===== const handleSubmit = async () => { if (!auditForm.remark.trim()) { message.warning('请输入审核内容'); @@ -74,17 +82,30 @@ export default defineComponent({ } submitting.value = true; try { - // 真实场景应上传图片并调用审核 API - const urls = imageList.value.map((f) => f.name || ''); - submitAudit(auditForm.pass, auditForm.remark.trim(), urls); + const checkImg = imageList.value + .map((f) => f.name || f.url || '') + .filter(Boolean) + .join(','); + const res = await postWithdrawAudit({ + id: String(props.record.id), + audit: auditForm.pass ? '1' : '2', + checkMsg: auditForm.remark.trim(), + checkImg, + }); + if (res.code == 200) { + message.success(auditForm.pass ? '已审核通过' : '已审核驳回'); + props.onAudited?.(); + props.onClose(); + } else { + message.error(res.msg || '审核失败'); + } + } catch { + // 网络层已统一提示 } finally { submitting.value = false; } }; - /** - * 通用描述项渲染 - */ const renderItem = (label: string, value: any, isBold = false) => (
{label} @@ -92,47 +113,30 @@ export default defineComponent({
); - return () => { - const record = props.record || {}; - const bank = record.bank || {}; - const pay = record.pay || {}; - const isPending = record.auditStatus === '待审核'; + /** 渲染内容区(加载完成后) */ + const renderContent = () => { + const d = detail.value!; + const bankCardTypeLabel = d.bankCardType === 1 ? '个人' : d.bankCardType || '-'; + const isPending = d.auditStatus === 0; return ( - - {/* 自定义标题栏 */} -
- 提现申请详情 - -
- + <> {/* ===== 基本信息 ===== */}
基本信息
- {renderItem('申请人昵称:', record.nickName)} - {renderItem('申请人手机号:', record.phone)} - {renderItem('真实姓名:', record.realName)} - {renderItem('申请时间:', record.applyTime)} - {renderItem('打款类型:', record.withdrawType)} + {renderItem('申请人昵称:', d.nickname)} + {renderItem('申请人手机号:', d.phone)} + {renderItem('真实姓名:', d.realName)} + {renderItem('申请时间:', d.createDate)} + {renderItem('打款类型:', d.withdrawType)}
- {renderItem('提现金额:', `¥${(record.withdrawAmount || 0).toFixed(2)}`, true)} - {renderItem('费率:', `${((record.feeRate || 0) * 100).toFixed(2)}%`)} - {renderItem('手续费:', `¥${(record.feeAmount || 0).toFixed(2)}`)} - {renderItem('到账金额:', `¥${(record.transferAmount || 0).toFixed(2)}`, true)} - {renderItem('审核状态:', record.auditStatus, true)} - {renderItem('列账状态:', record.transferStatus || '-')} + {renderItem('提现金额:', `¥${d.withdrawAmount}`, true)} + {renderItem('费率:', `${(Number(d.feeRate) * 100).toFixed(2)}%`)} + {renderItem('手续费:', `¥${d.feeAmount}`)} + {renderItem('到账金额:', `¥${d.receivedAmount}`, true)} + {renderItem('审核状态:', AUDIT_STATUS_MAP[d.auditStatus] ?? '-', true)} + {renderItem('列账状态:', PAY_STATUS_MAP[d.payStatus] ?? '-')}
@@ -140,11 +144,11 @@ export default defineComponent({
银行卡信息
- {renderItem('银行卡类型:', bank.cardType)} - {renderItem('持卡人:', bank.holder)} - {renderItem('银行卡号:', bank.cardNo)} - {renderItem('开户行:', bank.bankName)} - {renderItem('开户支行:', bank.branchName)} + {renderItem('银行卡类型:', bankCardTypeLabel)} + {renderItem('持卡人:', d.cardHolderName)} + {renderItem('银行卡号:', d.bankCardNumber)} + {renderItem('开户行:', d.bankName)} + {renderItem('开户支行:', d.branchName)}
@@ -152,8 +156,8 @@ export default defineComponent({
支付信息
- {renderItem('到账支付时间:', pay.transferTime || '-')} - {renderItem('商户订单号:', pay.merchantNo)} + {renderItem('到账支付时间:', d.payTime || '-')} + {renderItem('订单号:', d.orderNo)}
@@ -169,10 +173,9 @@ export default defineComponent({ onUpdate:value={(val: boolean) => (auditForm.pass = val)} > 通过 - 未通过 + 驳回
-
审核内容