From 6b7a0fd284173691e6c0e9185df2a67683672a6b Mon Sep 17 00:00:00 2001 From: ZhuRui Date: Thu, 30 Jul 2026 17:16:29 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=E5=A4=84=E7=90=86=E6=89=93=E5=8C=85?= =?UTF-8?q?=E8=AD=A6=E5=91=8A,=E5=A4=84=E7=90=86=E5=91=BD=E5=90=8D?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/index.ts | 2 +- .../pageTabs/{PageTabs.tsx => index.tsx} | 2 +- ...PageTabs.module.less => style.module.less} | 0 src/config/mock/eventOperationLogs.ts | 57 +++++++++++++++++++ src/config/mock/operationLogs.ts | 2 +- src/router/guard.ts | 16 ++---- 6 files changed, 66 insertions(+), 13 deletions(-) rename src/components/pageTabs/{PageTabs.tsx => index.tsx} (99%) rename src/components/pageTabs/{PageTabs.module.less => style.module.less} (100%) create mode 100644 src/config/mock/eventOperationLogs.ts 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 99% rename from src/components/pageTabs/PageTabs.tsx rename to src/components/pageTabs/index.tsx index ebd4d32..a301785 100644 --- a/src/components/pageTabs/PageTabs.tsx +++ b/src/components/pageTabs/index.tsx @@ -12,7 +12,7 @@ import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue'; 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; diff --git a/src/components/pageTabs/PageTabs.module.less b/src/components/pageTabs/style.module.less similarity index 100% rename from src/components/pageTabs/PageTabs.module.less rename to src/components/pageTabs/style.module.less diff --git a/src/config/mock/eventOperationLogs.ts b/src/config/mock/eventOperationLogs.ts new file mode 100644 index 0000000..aabdc02 --- /dev/null +++ b/src/config/mock/eventOperationLogs.ts @@ -0,0 +1,57 @@ +/** + * 赛事操作日志假数据 + */ +import type { + OperationLogQueryParams, + PageData, + TournamentAdminOperationPageVO, +} from '@/api/logs/types'; + +const MOCK_EVENT_LOGS: TournamentAdminOperationPageVO[] = [ + { + 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: OperationLogQueryParams, +): PageData { + 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..b2ff894 100644 --- a/src/config/mock/operationLogs.ts +++ b/src/config/mock/operationLogs.ts @@ -5,7 +5,7 @@ * 支持按 type、nickname、dateBegin/dateEnd 前端筛选, * 模拟后端分页行为。 */ -import type { SysOperationLogVO, OperationLogQueryParams, PageData } from '@/api/logs'; +import type { SysOperationLogVO, OperationLogQueryParams, PageData } from '@/api/logs/types'; const MOCK_LOGS: SysOperationLogVO[] = [ { diff --git a/src/router/guard.ts b/src/router/guard.ts index 9462d6a..b3a501b 100644 --- a/src/router/guard.ts +++ b/src/router/guard.ts @@ -1,7 +1,8 @@ import router from '@/router'; import { auth } from '@/hooks/useAuth'; -import { useTabsStore } from '@/stores/tabsStore'; import { useMenuStore } from '@/stores/menuStore'; +import { usePermissionStore } from '@/stores/permissionStore'; +import { useTabsStore } from '@/stores/tabsStore'; const appTitle = import.meta.env.VITE_APP_TITLE || 'CPMS 运营平台'; @@ -32,12 +33,9 @@ router.beforeEach(async (to, _from, next) => { // ── /login 特殊处理 ── if (to.path === '/login') { if (auth.isLoggedIn()) { - const { loadMenu, loaded, homePath, isRoutePathAvailable } = - await import('@/stores/menuStore').then((m) => m.useMenuStore()); + const { loadMenu, loaded, homePath, isRoutePathAvailable } = useMenuStore(); if (!loaded.value) { - const { loadPermissions } = await import('@/stores/permissionStore').then((m) => - m.usePermissionStore(), - ); + const { loadPermissions } = usePermissionStore(); await Promise.all([loadMenu(), loadPermissions()]); } @@ -60,11 +58,9 @@ router.beforeEach(async (to, _from, next) => { } // ── 已登录:确保菜单和动态路由已加载 ── - const { loadMenu, loaded } = await import('@/stores/menuStore').then((m) => m.useMenuStore()); + const { loadMenu, loaded } = useMenuStore(); if (!loaded.value) { - const { loadPermissions } = await import('@/stores/permissionStore').then((m) => - m.usePermissionStore(), - ); + const { loadPermissions } = usePermissionStore(); await Promise.all([loadMenu(), loadPermissions()]); // 动态路由刚注册,需要用新路由表重新匹配当前路径 -- 2.52.0 From 7606bb7d8d239482eed16e7ca504bc3b15207055 Mon Sep 17 00:00:00 2001 From: ZhuRui Date: Thu, 30 Jul 2026 17:23:23 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=E5=A4=84=E7=90=86=20ignore=20?= =?UTF-8?q?=E5=B9=B2=E6=89=B0=E9=97=AE=E9=A2=98=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 +- src/api/logs/index.ts | 66 +++++++ src/api/logs/types.ts | 60 ++++++ src/config/mock/eventOperationLogs.ts | 35 +++- src/config/mock/operationLogs.ts | 33 +++- src/pages/events/logs/index.module.less | 36 ++++ src/pages/events/logs/index.tsx | 210 +++++++++++++++++++++ src/pages/events/logs/model/useLogModel.ts | 154 +++++++++++++++ src/pages/system/logs/index.tsx | 118 ++++++++++++ src/pages/system/logs/model/useLogModel.ts | 146 ++++++++++++++ 10 files changed, 845 insertions(+), 19 deletions(-) create mode 100644 src/api/logs/index.ts create mode 100644 src/api/logs/types.ts create mode 100644 src/pages/events/logs/index.module.less create mode 100644 src/pages/events/logs/index.tsx create mode 100644 src/pages/events/logs/model/useLogModel.ts create mode 100644 src/pages/system/logs/index.tsx create mode 100644 src/pages/system/logs/model/useLogModel.ts 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..2f85c25 --- /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('/admin/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('/admin/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/config/mock/eventOperationLogs.ts b/src/config/mock/eventOperationLogs.ts index aabdc02..a068752 100644 --- a/src/config/mock/eventOperationLogs.ts +++ b/src/config/mock/eventOperationLogs.ts @@ -1,13 +1,32 @@ /** * 赛事操作日志假数据 + * + * 类型内联,避免与 @/api/logs 循环引用。 */ -import type { - OperationLogQueryParams, - PageData, - TournamentAdminOperationPageVO, -} from '@/api/logs/types'; +interface MockTournamentAdminOperationPageVO { + opName: string; + source: string; + nickname: string; + phone: string; + tournamentName: string; + obj: string; + content: string; + createDate: string; +} -const MOCK_EVENT_LOGS: TournamentAdminOperationPageVO[] = [ +interface MockEventOperationLogQueryParams { + source?: string; + sourceNickname?: string; + sourcePhone?: string; + tournamentName?: string; +} + +interface MockPageData { + total: number; + list: T[]; +} + +const MOCK_EVENT_LOGS: MockTournamentAdminOperationPageVO[] = [ { opName: '创建赛事', source: 'PC', @@ -33,8 +52,8 @@ const MOCK_EVENT_LOGS: TournamentAdminOperationPageVO[] = [ export function buildMockEventOperationLogPage( page: number, limit: number, - params: OperationLogQueryParams, -): PageData { + params: MockEventOperationLogQueryParams, +): MockPageData { let filtered = [...MOCK_EVENT_LOGS]; if (params.source) { diff --git a/src/config/mock/operationLogs.ts b/src/config/mock/operationLogs.ts index b2ff894..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/types'; +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/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..f57578d --- /dev/null +++ b/src/pages/events/logs/index.tsx @@ -0,0 +1,210 @@ +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} + onShowSizeChange={handlePageChange} + /> +
+
+
+ ); + }, +}); diff --git a/src/pages/events/logs/model/useLogModel.ts b/src/pages/events/logs/model/useLogModel.ts new file mode 100644 index 0000000..efe8bb5 --- /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 '@/api/logs'; + +// ============================================================ +// 常量 +// ============================================================ + +/** 操作类型选项(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/system/logs/index.tsx b/src/pages/system/logs/index.tsx new file mode 100644 index 0000000..e2241d8 --- /dev/null +++ b/src/pages/system/logs/index.tsx @@ -0,0 +1,118 @@ +import { defineComponent, onMounted } from 'vue'; +import { Button, Input, Table, DatePicker, Form, Space, Select, Pagination } from 'ant-design-vue'; +import { useLogModel, ACTION_TYPE_OPTIONS, ACTION_TYPE_MAP } from './model/useLogModel'; +import { useContainerSize } from '@/hooks'; +import pageStyles from '@/assets/styles/pageLayout.module.less'; + +const { RangePicker } = DatePicker; + +/** + * bodyCell 渲染 + */ +function renderBodyCell({ column, text }: { column: any; text: any }) { + if (column.key === 'type') { + return {ACTION_TYPE_MAP[text as number] || text || '-'}; + } + return {text || '-'}; +} + +/** + * 系统操作日志 + */ +export default defineComponent({ + name: 'SystemLogs', + setup() { + const { + filterForm, + loading, + dataSource, + columns, + pagination, + handleSearch, + handleReset, + handlePageChange, + } = useLogModel(); + + const { containerRef, height } = useContainerSize({ headerOffset: 55 }); + + // 首次进入自动加载 + onMounted(() => { + handleSearch(); + }); + + return () => ( +
+ {/* ===== 筛选区 ===== */} +
+
+ + (filterForm.actionTimeRange = val)} + /> + + + + + + + + + + +
+
+ + {/* ===== 表格区 ===== */} +
+
+ + {{ + bodyCell: (args: any) => renderBodyCell(args), + }} +
+
+ + {/* 独立分页 */} +
+ `共 ${total} 条`} + onChange={handlePageChange} + onShowSizeChange={handlePageChange} + /> +
+
+
+ ); + }, +}); diff --git a/src/pages/system/logs/model/useLogModel.ts b/src/pages/system/logs/model/useLogModel.ts new file mode 100644 index 0000000..394216c --- /dev/null +++ b/src/pages/system/logs/model/useLogModel.ts @@ -0,0 +1,146 @@ +import { computed, reactive, toRef, Ref } from 'vue'; +import dayjs from 'dayjs'; +import { useState, useDebounce, useThrottleFn } from '@/hooks'; +import { + getOperationLogList, + type SysOperationLogVO, + type OperationLogQueryParams, +} from '@/api/logs'; + +// ============================================================ +// 常量 +// ============================================================ + +/** 操作类型选项(筛选),value 对应 API type 字段 */ +export const ACTION_TYPE_OPTIONS = [ + { value: '', label: '全部' }, + { value: '1', label: '下架赛事' }, + { value: '2', label: '上架赛事' }, + { value: '3', label: '禁用用户' }, + { value: '4', label: '启用用户' }, +] as const; + +/** 操作类型文案映射 */ +export const ACTION_TYPE_MAP: Record = { + 1: '下架赛事', + 2: '上架赛事', + 3: '禁用用户', + 4: '启用用户', +}; + +// ============================================================ +// Model +// ============================================================ + +/** + * 系统操作日志页数据模型 + */ +export function useLogModel() { + // ===== 筛选条件 ===== + const filterForm = reactive({ + actionTimeRange: null as [string, string] | null, + actionType: '', + searchOperator: '', + }); + + // 操作人字段防抖 300ms + const { debouncedValue: debouncedOperator } = useDebounce( + toRef(filterForm, 'searchOperator') as Ref, + { delay: 300 }, + ); + + // ===== 表格状态 ===== + const [loading, setLoading] = useState(false); + const [dataSource, setDataSource] = useState([]); + const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }); + + // ===== 表格列配置(字段名对齐 API) ===== + const columns = [ + { title: '操作类型', dataIndex: 'type', key: 'type', width: 140 }, + { title: '操作人', dataIndex: 'nickname', key: 'nickname', width: 140 }, + { title: '操作内容', dataIndex: 'content', key: 'content', minWidth: 360 }, + { title: '操作时间', dataIndex: 'createDate', key: 'createDate', width: 180 }, + ]; + + // ===== 计算属性 ===== + const hasFilter = computed(() => { + return ( + filterForm.actionTimeRange !== null || + filterForm.actionType !== '' || + debouncedOperator.value.trim() !== '' + ); + }); + + // ===== 构建 API 查询参数 ===== + const buildQueryParams = (): OperationLogQueryParams => { + const params: OperationLogQueryParams = { + page: String(pagination.value.current), + limit: String(pagination.value.pageSize), + }; + + // 时间范围 → dateBegin / dateEnd + if (filterForm.actionTimeRange) { + const [start, end] = filterForm.actionTimeRange; + if (start) params.dateBegin = dayjs(start).format('YYYY-MM-DD'); + if (end) params.dateEnd = dayjs(end).format('YYYY-MM-DD'); + } + + // 操作类型(仅非空时传参) + if (filterForm.actionType) { + params.type = filterForm.actionType; + } + + // 操作人昵称(防抖后的值) + if (debouncedOperator.value.trim()) { + params.nickname = debouncedOperator.value.trim(); + } + + return params; + }; + + // ===== 方法 ===== + + /** 查询(节流 500ms) */ + const handleSearch = useThrottleFn(async () => { + setLoading(true); + try { + const queryParams = buildQueryParams(); + const res = await getOperationLogList(queryParams); + if (res.code === 200) { + setDataSource(res.data.list); + setPagination({ ...pagination.value, total: res.data.total }); + } + } catch { + // 网络层已统一提示 + } finally { + setLoading(false); + } + }, 500); + + /** 重置(重置后自动查询) */ + const handleReset = useThrottleFn(() => { + filterForm.actionTimeRange = null; + filterForm.actionType = ''; + filterForm.searchOperator = ''; + 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, + }; +} -- 2.52.0 From ea906a0d9243dd206ed9ed67c454ac8d65914049 Mon Sep 17 00:00:00 2001 From: ZhuRui Date: Thu, 30 Jul 2026 18:02:41 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=E9=A1=B5=E9=9D=A2=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E7=BB=84=E4=BB=B6=E6=A0=B7=E5=BC=8F=E4=B8=8E=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/pageTabs/index.tsx | 79 ++++++++++++++++++++++- src/components/pageTabs/style.module.less | 65 ++++++++++++++++++- src/layouts/BasicLayout.tsx | 30 ++++++++- src/stores/tabsStore.ts | 38 +++++++++++ 4 files changed, 208 insertions(+), 4 deletions(-) diff --git a/src/components/pageTabs/index.tsx b/src/components/pageTabs/index.tsx index a301785..ff9c669 100644 --- a/src/components/pageTabs/index.tsx +++ b/src/components/pageTabs/index.tsx @@ -1,14 +1,24 @@ 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'; @@ -17,6 +27,10 @@ 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,34 @@ 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; + + if (key === 'left') { + if (activeIndex === 0) { + message.warning('左侧没有可关闭的标签页'); + return; + } + props.onCloseLeft?.(props.activeKey); + } else if (key === 'right') { + if (activeIndex === props.tabs.length - 1) { + message.warning('右侧没有可关闭的标签页'); + return; + } + props.onCloseRight?.(props.activeKey); + } else if (key === 'other') { + props.onCloseOther?.(props.activeKey); + } else if (key === 'all') { + props.onCloseAll?.(); + } + }; + onMounted(() => { const el = scrollRef.value; if (!el) return; @@ -174,6 +224,15 @@ const PageTabs = defineComponent({ ); }; + const renderBatchMenuItem = (key: string, icon: VNode, label: string) => ( + +
+ {icon} + {label} +
+
+ ); + return () => (
{showNavLeft.value && ( @@ -201,6 +260,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/style.module.less b/src/components/pageTabs/style.module.less index b94d883..4c9957f 100644 --- a/src/components/pageTabs/style.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/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx index 2f0693b..ae6b97b 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]); @@ -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/stores/tabsStore.ts b/src/stores/tabsStore.ts index d9c6bcc..12a548d 100644 --- a/src/stores/tabsStore.ts +++ b/src/stores/tabsStore.ts @@ -69,10 +69,46 @@ export function useTabsStore() { } }; + /** 关闭 path 左侧的所有 tab */ + const removeLeftTabs = (path: string): void => { + const idx = state.tabs.findIndex((t) => t.path === path); + if (idx <= 0) return; + + const currentPath = router.currentRoute.value.path; + const shouldRedirect = state.tabs.slice(0, idx).some((t) => t.path === currentPath); + + state.tabs = state.tabs.slice(idx); + syncCachedViews(); + + if (shouldRedirect) { + router.push(path); + } + }; + + /** 关闭 path 右侧的所有 tab */ + const removeRightTabs = (path: string): void => { + const idx = state.tabs.findIndex((t) => t.path === path); + if (idx === -1 || idx >= state.tabs.length - 1) return; + + const currentPath = router.currentRoute.value.path; + const shouldRedirect = state.tabs.slice(idx + 1).some((t) => t.path === currentPath); + + state.tabs = state.tabs.slice(0, idx + 1); + syncCachedViews(); + + if (shouldRedirect) { + router.push(path); + } + }; + /** 关闭除 path 之外的所有 tab */ const removeOtherTabs = (path: string): void => { + const currentPath = router.currentRoute.value.path; state.tabs = state.tabs.filter((t) => t.path === path); syncCachedViews(); + if (currentPath !== path && state.tabs.length > 0) { + router.push(path); + } }; /** 关闭所有 tab,保留第一个并跳转过去 */ @@ -106,6 +142,8 @@ export function useTabsStore() { cachedViews: computed(() => state.cachedViews), addTab, removeTab, + removeLeftTabs, + removeRightTabs, removeOtherTabs, removeAllTabs, updateTab, -- 2.52.0 From 3b107e8d52833ea52d35e020b1eb92147d6f7107 Mon Sep 17 00:00:00 2001 From: ZhuRui Date: Fri, 31 Jul 2026 17:25:42 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=E6=8C=89=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=AF=B9=E6=8E=A5=E8=B4=A6=E5=8A=A1=E7=AE=A1=E7=90=86=E4=B8=8B?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E9=80=BB=E8=BE=91=E4=B8=8E=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/payments/index.ts | 25 ++ src/api/payments/types.ts | 48 +++ src/api/wallet/index.ts | 53 +++ src/api/wallet/types.ts | 92 +++++ src/api/withdraw/index.ts | 56 +++ src/api/withdraw/types.ts | 104 ++++++ src/config/mock/paymentFlow.ts | 149 ++++++++ src/config/mock/walletList.ts | 185 ++++++++++ src/config/mock/walletTransactions.ts | 187 ++++++++++ src/config/mock/withdrawInfo.ts | 150 ++++++++ src/config/mock/withdrawList.ts | 194 ++++++++++ src/pages/events/banner/index.tsx | 1 - src/pages/events/list/index.tsx | 1 - src/pages/events/logs/index.tsx | 1 - src/pages/events/orders/index.tsx | 1 - src/pages/events/users/index.tsx | 1 - src/pages/finance/payments/index.tsx | 28 +- .../payments/model/usePaymentsModel.ts | 213 +++++------ .../wallet/components/WalletDetailModal.tsx | 257 +++++++------ src/pages/finance/wallet/index.tsx | 41 +-- .../finance/wallet/model/useWalletModel.ts | 343 +++++------------- ...e.less => WithdrawDetailModal.module.less} | 10 + ...AuditModal.tsx => WithdrawDetailModal.tsx} | 201 +++++----- src/pages/finance/withdraw/index.tsx | 58 ++- .../withdraw/model/useWithdrawModel.ts | 318 +++++----------- src/pages/system/logs/index.tsx | 1 - .../system/roles/components/UserListModal.tsx | 1 - src/pages/system/roles/index.tsx | 1 - src/pages/system/users/index.tsx | 1 - 29 files changed, 1833 insertions(+), 888 deletions(-) create mode 100644 src/api/payments/index.ts create mode 100644 src/api/payments/types.ts create mode 100644 src/api/wallet/index.ts create mode 100644 src/api/wallet/types.ts create mode 100644 src/api/withdraw/index.ts create mode 100644 src/api/withdraw/types.ts create mode 100644 src/config/mock/paymentFlow.ts create mode 100644 src/config/mock/walletList.ts create mode 100644 src/config/mock/walletTransactions.ts create mode 100644 src/config/mock/withdrawInfo.ts create mode 100644 src/config/mock/withdrawList.ts rename src/pages/finance/withdraw/components/{WithdrawAuditModal.module.less => WithdrawDetailModal.module.less} (96%) rename src/pages/finance/withdraw/components/{WithdrawAuditModal.tsx => WithdrawDetailModal.tsx} (52%) diff --git a/src/api/payments/index.ts b/src/api/payments/index.ts new file mode 100644 index 0000000..ad6a10c --- /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('/admin/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/wallet/index.ts b/src/api/wallet/index.ts new file mode 100644 index 0000000..39ec46e --- /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('/admin/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('/admin/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..39dd0ab --- /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 /admin/finance/withdraw/page — 提现列表分页 */ +export async function getWithdrawList( + params: WithdrawQueryParams, +): Promise>> { + if (USE_MOCK) { + await delay(MOCK_DELAY); + const page = Number(params.page) || 1; + const limit = Number(params.limit) || 10; + return { + code: 200, + msg: 'success', + data: buildMockWithdrawPage(page, limit, params), + }; + } + return get('/admin/finance/withdraw/page', params as Record); +} + +/** GET /admin/finance/withdraw/info?id=xxx — 提现详情 */ +export async function getWithdrawInfo(id: number): Promise> { + if (USE_MOCK) { + await delay(MOCK_DELAY); + return { code: 200, msg: 'success', data: buildMockWithdrawInfo(id) }; + } + return get('/admin/finance/withdraw/info', { id: String(id) }); +} + +/** 提现审核请求参数 */ +export interface WithdrawAuditParams { + /** 提现ID */ + id: string; + /** 审核结果:1=通过,2=驳回 */ + audit: string; + /** 审核内容 */ + checkMsg: string; + /** 审核图片(逗号拼接的 URL) */ + checkImg: string; +} + +/** POST /admin/finance/withdraw/audit — 审核提现 */ +export async function postWithdrawAudit(params: WithdrawAuditParams): Promise> { + return post('/admin/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/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/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/list/index.tsx b/src/pages/events/list/index.tsx index f0fb526..4819bad 100644 --- a/src/pages/events/list/index.tsx +++ b/src/pages/events/list/index.tsx @@ -340,7 +340,6 @@ export default defineComponent({ showSizeChanger showTotal={(total: number) => `共 ${total} 条`} onChange={handlePageChange} - onShowSizeChange={handlePageChange} /> diff --git a/src/pages/events/logs/index.tsx b/src/pages/events/logs/index.tsx index f57578d..6070221 100644 --- a/src/pages/events/logs/index.tsx +++ b/src/pages/events/logs/index.tsx @@ -200,7 +200,6 @@ export default defineComponent({ showSizeChanger showTotal={(total: number) => `共 ${total} 条`} onChange={handlePageChange} - onShowSizeChange={handlePageChange} /> diff --git a/src/pages/events/orders/index.tsx b/src/pages/events/orders/index.tsx index 2e050df..804d015 100644 --- a/src/pages/events/orders/index.tsx +++ b/src/pages/events/orders/index.tsx @@ -273,7 +273,6 @@ export default defineComponent({ showSizeChanger showTotal={(total: number) => `共 ${total} 条`} onChange={handlePageChange} - onShowSizeChange={handlePageChange} /> diff --git a/src/pages/events/users/index.tsx b/src/pages/events/users/index.tsx index f090310..214199e 100644 --- a/src/pages/events/users/index.tsx +++ b/src/pages/events/users/index.tsx @@ -145,7 +145,6 @@ export default defineComponent({ showSizeChanger showTotal={(total: number) => `共 ${total} 条`} onChange={handlePageChange} - onShowSizeChange={handlePageChange} /> 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} />
- {/* ===== 交易明细弹窗 ===== */} = { - 正常: { 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..faae718 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 '@/api/withdraw'; +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)} > 通过 - 未通过 + 驳回
-
审核内容