From 2497e6aabb56584c0d179643bc7260633b6f7370 Mon Sep 17 00:00:00 2001 From: Disciple_C Date: Mon, 3 Aug 2026 19:12:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8E=A5=E5=8F=A3=E8=81=94=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 9 +- .env.test | 8 +- package-lock.json | 60 +++ src/api/events/index.ts | 33 -- src/api/events/types.ts | 14 + src/api/login/index.ts | 5 + src/api/logs/index.ts | 11 - src/api/menu.ts | 4 +- src/config/mock.ts | 2 +- src/config/mock/eventDetail.ts | 39 -- src/config/mock/eventList.ts | 268 ----------- src/config/mock/eventOperationLogs.ts | 76 --- src/config/mock/eventSignup.ts | 100 ---- .../list/components/EventDetailModal.tsx | 72 +-- .../list/components/EventRegulationModal.tsx | 115 +++-- src/pages/events/list/index.tsx | 280 +++++------ src/pages/events/list/model/config.ts | 69 +++ src/pages/events/list/model/services.ts | 55 +-- .../events/list/model/useEventListColumns.ts | 116 +++++ .../events/list/model/useEventListModel.ts | 434 +++++++----------- src/pages/events/logs/model/services.ts | 19 - src/pages/events/orders/index.tsx | 14 +- src/pages/events/orders/model/config.ts | 57 +++ src/pages/events/orders/model/services.ts | 8 +- .../events/orders/model/useOrderModel.ts | 253 +++++----- src/pages/login/index.tsx | 36 +- src/pages/system/users/model/services.ts | 57 ++- src/stores/menuStore.ts | 8 +- src/stores/userStore.ts | 2 +- 29 files changed, 941 insertions(+), 1283 deletions(-) create mode 100644 src/api/login/index.ts delete mode 100644 src/config/mock/eventDetail.ts delete mode 100644 src/config/mock/eventList.ts delete mode 100644 src/config/mock/eventOperationLogs.ts delete mode 100644 src/config/mock/eventSignup.ts create mode 100644 src/pages/events/list/model/config.ts create mode 100644 src/pages/events/list/model/useEventListColumns.ts create mode 100644 src/pages/events/orders/model/config.ts diff --git a/.env.development b/.env.development index 1a992dd..03e2369 100644 --- a/.env.development +++ b/.env.development @@ -1,3 +1,6 @@ -# 开发环境 -VITE_PORT = 5173 -VITE_API_BASE_URL = http://localhost:3000/admin +# 开发环境配置 +# 基础API地址 +VITE_API_BASE_URL=http://cpms.op.nps.yunvip123.cn/cpms + +# 是否开启本地调试 +LOCAL_DEBUG=true diff --git a/.env.test b/.env.test index 7ca21d6..889ad12 100644 --- a/.env.test +++ b/.env.test @@ -1,2 +1,6 @@ -# 测试环境 -VITE_API_BASE_URL = https://test-api.cpms.example.com/admin +# 测试环境配置 +# 基础API地址 +VITE_API_BASE_URL=http://cpms.op.nps.yunvip123.cn/cpms + +# 是否开启本地调试 +LOCAL_DEBUG=false \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d6e5eff..53c3ffa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,16 @@ "version": "0.0.0", "dependencies": { "@ant-design/icons-vue": "^7.0.1", + "@yp-component/root": "^0.0.48", "ant-design-vue": "^4.0.0", + "china-area-data": "^5.0.1", + "dayjs": "^1.11.21", + "echarts": "^5.6.0", "express": "^5.2.1", "html2canvas": "^1.4.1", "qs": "^6.15.0", "vue": "^3.3.0", + "vue-icons-plus": "^0.1.8", "vue-router": "^4.2.0" }, "devDependencies": { @@ -1488,6 +1493,15 @@ "version": "3.5.39", "license": "MIT" }, + "node_modules/@yp-component/root": { + "version": "0.0.48", + "resolved": "http://192.168.2.127:4873/@yp-component/root/-/root-0.0.48.tgz", + "integrity": "sha512-TOp8adAob3pMSfecQjEC4uVNCGqNWcShDjSQysd4pYY34i5ldFLRGSiJdpLsFG1xKkcSudiqEc/Si/ZfYoDSqA==", + "dependencies": { + "cropperjs": "^1.6.2", + "vue": "^3.5.25" + } + }, "node_modules/accepts": { "version": "2.0.0", "license": "MIT", @@ -1826,6 +1840,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/china-area-data": { + "version": "5.0.1", + "resolved": "http://192.168.2.127:4873/china-area-data/-/china-area-data-5.0.1.tgz", + "integrity": "sha512-BQDPpiv5Nn+018ekcJK2oSD9PAD+E1bvXB0wgabc//dFVS/KvRqCgg0QOEUt3vBkx9XzB5a9BmkJCEZDBxVjVw==", + "license": "MIT" + }, "node_modules/cli-cursor": { "version": "5.0.0", "dev": true, @@ -2095,6 +2115,12 @@ "typescript": ">=5" } }, + "node_modules/cropperjs": { + "version": "1.6.2", + "resolved": "http://192.168.2.127:4873/cropperjs/-/cropperjs-1.6.2.tgz", + "integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "dev": true, @@ -2221,6 +2247,16 @@ "node": ">= 0.4" } }, + "node_modules/echarts": { + "version": "5.6.0", + "resolved": "http://192.168.2.127:4873/echarts/-/echarts-5.6.0.tgz", + "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.6.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "license": "MIT" @@ -5597,6 +5633,12 @@ "typescript": ">=4.2.0" } }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "http://192.168.2.127:4873/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "dev": true, @@ -5840,6 +5882,15 @@ "eslint": ">=6.0.0" } }, + "node_modules/vue-icons-plus": { + "version": "0.1.8", + "resolved": "http://192.168.2.127:4873/vue-icons-plus/-/vue-icons-plus-0.1.8.tgz", + "integrity": "sha512-Xc4hDsD/oP9waSUf44nSaFBhUPo+QkpKclx0S7//5BRACpXymctbit02epek0VRW6nb81pR486XmxPP/ofm2yQ==", + "license": "MIT", + "peerDependencies": { + "vue": ">=2.7.0" + } + }, "node_modules/vue-router": { "version": "4.6.4", "license": "MIT", @@ -6064,6 +6115,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zrender": { + "version": "5.6.1", + "resolved": "http://192.168.2.127:4873/zrender/-/zrender-5.6.1.tgz", + "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } } } } diff --git a/src/api/events/index.ts b/src/api/events/index.ts index 4c62196..67f3ab5 100644 --- a/src/api/events/index.ts +++ b/src/api/events/index.ts @@ -3,10 +3,6 @@ * 注意:/admin 前缀已通过 VITE_API_BASE_URL 统一注入(见 .env.*),此文件中只写业务路径。 */ import { get, post } from '@/utils/request'; -import { USE_MOCK, MOCK_DELAY } from '@/config/mock'; -import { MOCK_EVENT_DETAIL } from '@/config/mock/eventDetail'; -import { buildMockEventListPage } from '@/config/mock/eventList'; -import { buildMockSignupListPage } from '@/config/mock/eventSignup'; import type { EventListQueryParams, SignupListQueryParams, @@ -30,9 +26,6 @@ export type { ApiResult, } from './types'; -/** 模拟延时 */ -const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - /** * 赛事列表 - 列表分页 * GET /admin/manager/tm/page → /manager/tm/page @@ -40,16 +33,6 @@ const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, export async function getEventList( params: EventListQueryParams, ): Promise>> { - if (USE_MOCK) { - await delay(MOCK_DELAY); - const page = Number(params.page) || 1; - const limit = Number(params.limit) || 10; - return { - code: 200, - msg: 'success', - data: buildMockEventListPage(page, limit), - }; - } return get('/manager/tm/page', params as Record); } @@ -58,14 +41,6 @@ export async function getEventList( * GET /admin/manager/tm/info → /manager/tm/info */ export async function getEventDetail(id: string): Promise> { - if (USE_MOCK) { - await delay(MOCK_DELAY); - return { - code: 200, - msg: 'success', - data: { ...MOCK_EVENT_DETAIL }, - }; - } return get('/manager/tm/info', { id }); } @@ -76,14 +51,6 @@ export async function getEventDetail(id: string): Promise>> { - if (USE_MOCK) { - await delay(MOCK_DELAY); - return { - code: 200, - msg: 'success', - data: buildMockSignupListPage(params), - }; - } return get('/manager/tm/info/signupPage', params as Record); } diff --git a/src/api/events/types.ts b/src/api/events/types.ts index 0a562fc..b68276b 100644 --- a/src/api/events/types.ts +++ b/src/api/events/types.ts @@ -186,6 +186,20 @@ export interface SignupListQueryParams { categoryId: string; } +// ============================================================ +// 赛事规程(对应 /admin/manager/tm/ruleInfo) +// ============================================================ + +/** 赛事规程详情 */ +export interface TournamentAdminRuleInfoVO { + /** 赛事规程文字内容 */ + rulesDescr?: string; + /** 赛事规程图片 */ + rulesDescrImg?: string; + /** 赛事规程图片列表 */ + rulesDescrImgList?: string[]; +} + // ============================================================ // 上下架(对应 /manager/tm/online) // ============================================================ diff --git a/src/api/login/index.ts b/src/api/login/index.ts new file mode 100644 index 0000000..ab552ec --- /dev/null +++ b/src/api/login/index.ts @@ -0,0 +1,5 @@ +import { post } from '@/utils/request'; + +export const login = (params: { phone: string; password: string }) => { + return post(`/op/login`, params); +}; diff --git a/src/api/logs/index.ts b/src/api/logs/index.ts index ddbf68d..9fe9ce6 100644 --- a/src/api/logs/index.ts +++ b/src/api/logs/index.ts @@ -4,7 +4,6 @@ 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, @@ -49,16 +48,6 @@ export async function getOperationLogList( 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); } diff --git a/src/api/menu.ts b/src/api/menu.ts index feaac2e..ab1d3f1 100644 --- a/src/api/menu.ts +++ b/src/api/menu.ts @@ -5,12 +5,12 @@ import type { MenuNode, PermissionCode } from '@/types'; * 获取当前用户的菜单树(后端下发) */ export function fetchMenuTree(): Promise { - return get('/menu/tree'); + return get('/op/permission'); } /** * 获取当前用户的权限编码列表 */ export function fetchPermissions(): Promise { - return get('/permission/list'); + return get('/admin/sys/role/permissionList'); } diff --git a/src/config/mock.ts b/src/config/mock.ts index c2a4655..200af80 100644 --- a/src/config/mock.ts +++ b/src/config/mock.ts @@ -6,7 +6,7 @@ * * 对接后端接口后将此值改为 false 即可切换。 */ -export const USE_MOCK = true; +export const USE_MOCK = false; /** 模拟网络延时(毫秒) */ export const MOCK_DELAY = 850; diff --git a/src/config/mock/eventDetail.ts b/src/config/mock/eventDetail.ts deleted file mode 100644 index 5dd51c8..0000000 --- a/src/config/mock/eventDetail.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * 赛事详情弹窗假数据 - * - * 使用页面:`src/pages/events/list/components/EventDetailModal.tsx` - * 对应接口:`GET /manager/tm/info` - * 类型:`TournamentAdminInfoVO` - */ -import type { TournamentAdminInfoVO } from '@/api/events/types'; - -export const MOCK_EVENT_DETAIL: TournamentAdminInfoVO = { - name: '第二届全国地学羽毛球邀请赛', - startTimeBegin: '2026-05-26 08:00', - startTimeEnd: '2026-05-31 22:00', - descr: - '赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍。', - signupTimeBegin: '2026-05-24 08:00', - signupTimeEnd: '2026-05-26 22:00', - venueArea: '北京市 北京市 东城区', - venue: '李宁体育馆', - venueNo: '李宁体育馆', - isPublic: '1', - isRequireIdCardImg: '0', - isRequireIdCardNo: '0', - createTime: '2026-05-26 08:00', - createNickname: '张三', - signupCount: '100', - dropoutCount: '1', - dtotalLimit: '1000', - matchTotalCount: '50', - matchCount: '50', - img: 'https://picsum.photos/seed/eventcover/600/300', - notice: - '赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告。', - categoryList: [ - { id: '1', name: '男子单打', type: '1' }, - { id: '2', name: '女子双打', type: '2' }, - ], - groupInfoList: ['男子小组1(循环赛)', '女子小组1(淘汰赛)'], -}; diff --git a/src/config/mock/eventList.ts b/src/config/mock/eventList.ts deleted file mode 100644 index 55ac581..0000000 --- a/src/config/mock/eventList.ts +++ /dev/null @@ -1,268 +0,0 @@ -/** - * 赛事列表页假数据 - * - * 使用页面:`src/pages/events/list/index.tsx` - * 对应接口:`GET /manager/tm/page` - * 类型:`TournamentAdminVO[]` - */ -import type { TournamentAdminVO, PageData } from '@/api/events/types'; - -export const MOCK_EVENT_LIST: TournamentAdminVO[] = [ - { - id: 'EV202607010001', - name: '2026全国青少年羽毛球锦标赛暨体育文化交流大会', - startTimeBegin: '2026-08-12 09:00', - startTimeEnd: '2026-08-15 18:00', - venueArea: '广东省 深圳市 南山区', - venue: '深圳湾体育中心', - isPublic: '1', - isRequireIdCardImg: '1', - isRequireIdCardNo: '1', - createNickname: '张运营', - createPhone: '13800138001', - createTime: '2026-07-10 08:11:07', - status: '0', - signupCount: '128', - dropoutCount: '5', - rulesDescr: '一、赛事目的:推动羽毛球运动发展。\n二、参赛资格:身体健康、年龄符合要求。', - rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', - rulesDescrImgList: [ - 'https://picsum.photos/seed/rule1/300/200', - 'https://picsum.photos/seed/rule2/300/200', - ], - }, - { - id: 'EV202607010002', - name: '国际马拉松城市联赛', - startTimeBegin: '2026-08-12 09:00', - startTimeEnd: '2026-08-15 18:00', - venueArea: '广东省 深圳市 南山区', - venue: '深圳湾体育中心', - isPublic: '0', - isRequireIdCardImg: '0', - isRequireIdCardNo: '0', - createNickname: '李管理', - createPhone: '13900139002', - createTime: '2026-07-09 11:28:20', - status: '1', - signupCount: '256', - dropoutCount: '12', - rulesDescr: '一、赛事目的:推动马拉松运动发展。', - rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', - rulesDescrImgList: ['https://picsum.photos/seed/rule1/300/200'], - }, - { - id: 'EV202607010003', - name: '全民健身运动会', - startTimeBegin: '2026-08-12 09:00', - startTimeEnd: '2026-08-15 18:00', - venueArea: '广东省 深圳市 南山区', - venue: '深圳湾体育中心', - isPublic: '1', - isRequireIdCardImg: '1', - isRequireIdCardNo: '1', - createNickname: '王策划', - createPhone: '13700137003', - createTime: '2026-07-08 14:45:33', - status: '2', - signupCount: '512', - dropoutCount: '8', - rulesDescr: '一、赛事目的:促进全民健身。', - rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', - rulesDescrImgList: ['https://picsum.photos/seed/rule1/300/200'], - }, - { - id: 'EV202607010004', - name: '2026全国青少年羽毛球锦标赛暨体育文化交流大会', - startTimeBegin: '2026-08-12 09:00', - startTimeEnd: '2026-08-15 18:00', - venueArea: '广东省 深圳市 南山区', - venue: '深圳湾体育中心', - isPublic: '0', - isRequireIdCardImg: '0', - isRequireIdCardNo: '0', - createNickname: '赵赛事', - createPhone: '13600136004', - createTime: '2026-07-07 18:02:46', - status: '3', - signupCount: '64', - dropoutCount: '3', - rulesDescr: '一、赛事目的:交流体育文化。', - rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', - rulesDescrImgList: [], - }, - { - id: 'EV202607010005', - name: '国际马拉松城市联赛', - startTimeBegin: '2026-08-12 09:00', - startTimeEnd: '2026-08-15 18:00', - venueArea: '广东省 深圳市 南山区', - venue: '深圳湾体育中心', - isPublic: '1', - isRequireIdCardImg: '1', - isRequireIdCardNo: '1', - createNickname: '张运营', - createPhone: '13800138001', - createTime: '2026-07-06 21:19:59', - status: '4', - signupCount: '0', - dropoutCount: '0', - rulesDescr: '一、赛事目的:推动城市体育交流。', - rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', - rulesDescrImgList: ['https://picsum.photos/seed/rule1/300/200'], - }, - { - id: 'EV202607010006', - name: '全民健身运动会', - startTimeBegin: '2026-08-12 09:00', - startTimeEnd: '2026-08-15 18:00', - venueArea: '广东省 深圳市 南山区', - venue: '深圳湾体育中心', - isPublic: '0', - isRequireIdCardImg: '0', - isRequireIdCardNo: '0', - createNickname: '李管理', - createPhone: '13900139002', - createTime: '2026-07-05 00:36:12', - status: '0', - signupCount: '128', - dropoutCount: '5', - rulesDescr: '一、赛事目的:增强全民体质。', - rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', - rulesDescrImgList: [], - }, - { - id: 'EV202607010007', - name: '2026全国青少年羽毛球锦标赛暨体育文化交流大会', - startTimeBegin: '2026-08-12 09:00', - startTimeEnd: '2026-08-15 18:00', - venueArea: '广东省 深圳市 南山区', - venue: '深圳湾体育中心', - isPublic: '1', - isRequireIdCardImg: '1', - isRequireIdCardNo: '1', - createNickname: '王策划', - createPhone: '13700137003', - createTime: '2026-07-04 03:53:25', - status: '1', - signupCount: '256', - dropoutCount: '12', - rulesDescr: '一、赛事目的:选拔青少年人才。', - rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', - rulesDescrImgList: [ - 'https://picsum.photos/seed/rule1/300/200', - 'https://picsum.photos/seed/rule2/300/200', - ], - }, - { - id: 'EV202607010008', - name: '国际马拉松城市联赛', - startTimeBegin: '2026-08-12 09:00', - startTimeEnd: '2026-08-15 18:00', - venueArea: '广东省 深圳市 南山区', - venue: '深圳湾体育中心', - isPublic: '0', - isRequireIdCardImg: '0', - isRequireIdCardNo: '0', - createNickname: '赵赛事', - createPhone: '13600136004', - createTime: '2026-07-03 07:10:38', - status: '2', - signupCount: '512', - dropoutCount: '8', - rulesDescr: '一、赛事目的:连接城市与运动。', - rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', - rulesDescrImgList: [], - }, - { - id: 'EV202607010009', - name: '全民健身运动会', - startTimeBegin: '2026-08-12 09:00', - startTimeEnd: '2026-08-15 18:00', - venueArea: '广东省 深圳市 南山区', - venue: '深圳湾体育中心', - isPublic: '1', - isRequireIdCardImg: '1', - isRequireIdCardNo: '1', - createNickname: '张运营', - createPhone: '13800138001', - createTime: '2026-07-02 10:27:51', - status: '3', - signupCount: '64', - dropoutCount: '3', - rulesDescr: '一、赛事目的:全民参与、共享健康。', - rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', - rulesDescrImgList: ['https://picsum.photos/seed/rule1/300/200'], - }, - { - id: 'EV202607010010', - name: '2026全国青少年羽毛球锦标赛暨体育文化交流大会', - startTimeBegin: '2026-08-12 09:00', - startTimeEnd: '2026-08-15 18:00', - venueArea: '广东省 深圳市 南山区', - venue: '深圳湾体育中心', - isPublic: '0', - isRequireIdCardImg: '0', - isRequireIdCardNo: '0', - createNickname: '李管理', - createPhone: '13900139002', - createTime: '2026-07-01 13:44:04', - status: '4', - signupCount: '0', - dropoutCount: '0', - rulesDescr: '一、赛事目的:培养后备力量。', - rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', - rulesDescrImgList: [], - }, - { - id: 'EV202607010011', - name: '国际马拉松城市联赛', - startTimeBegin: '2026-08-12 09:00', - startTimeEnd: '2026-08-15 18:00', - venueArea: '广东省 深圳市 南山区', - venue: '深圳湾体育中心', - isPublic: '1', - isRequireIdCardImg: '1', - isRequireIdCardNo: '1', - createNickname: '王策划', - createPhone: '13700137003', - createTime: '2026-06-30 17:01:17', - status: '0', - signupCount: '128', - dropoutCount: '5', - rulesDescr: '一、赛事目的:推广城市品牌。', - rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', - rulesDescrImgList: [ - 'https://picsum.photos/seed/rule1/300/200', - 'https://picsum.photos/seed/rule2/300/200', - 'https://picsum.photos/seed/rule3/300/200', - ], - }, - { - id: 'EV202607010012', - name: '全民健身运动会', - startTimeBegin: '2026-08-12 09:00', - startTimeEnd: '2026-08-15 18:00', - venueArea: '广东省 深圳市 南山区', - venue: '深圳湾体育中心', - isPublic: '0', - isRequireIdCardImg: '0', - isRequireIdCardNo: '0', - createNickname: '赵赛事', - createPhone: '13600136004', - createTime: '2026-06-29 20:18:30', - status: '1', - signupCount: '256', - dropoutCount: '12', - rulesDescr: '一、赛事目的:丰富群众体育生活。', - rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', - rulesDescrImgList: [], - }, -]; - -/** 构建分页响应 */ -export function buildMockEventListPage(page: number, limit: number): PageData { - const start = (page - 1) * limit; - const list = MOCK_EVENT_LIST.slice(start, start + limit); - return { total: MOCK_EVENT_LIST.length, list }; -} diff --git a/src/config/mock/eventOperationLogs.ts b/src/config/mock/eventOperationLogs.ts deleted file mode 100644 index a068752..0000000 --- a/src/config/mock/eventOperationLogs.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * 赛事操作日志假数据 - * - * 类型内联,避免与 @/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/eventSignup.ts b/src/config/mock/eventSignup.ts deleted file mode 100644 index 20ffce5..0000000 --- a/src/config/mock/eventSignup.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * 赛事详情 — 人员列表假数据 - * - * 使用页面:`src/pages/events/list/components/EventDetailModal.tsx` - * 对应接口:`GET /manager/tm/info/signupPage` - * 类型:`TournamentAdminInfoSignupVO[]` - */ -import type { - TournamentAdminInfoSignupVO, - PageData, - SignupListQueryParams, -} from '@/api/events/types'; - -export const MOCK_SIGNUP_LIST: TournamentAdminInfoSignupVO[] = [ - { - realName: '张三', - gender: '1', - phone: '13800138001', - idCard: '110101199001011234', - idCardImg: 'https://picsum.photos/seed/idcard1/96/64', - idCardImgList: [ - 'https://picsum.photos/seed/idcard1a/96/64', - 'https://picsum.photos/seed/idcard1b/96/64', - ], - }, - { - realName: '李四', - gender: '1', - phone: '13900139002', - idCard: '110101199002022345', - idCardImg: 'https://picsum.photos/seed/idcard2/96/64', - idCardImgList: [ - 'https://picsum.photos/seed/idcard2a/96/64', - 'https://picsum.photos/seed/idcard2b/96/64', - ], - }, - { - realName: '王芳', - gender: '2', - phone: '13700137003', - idCard: '110101199103033456', - idCardImg: '', - idCardImgList: [], - }, - { - realName: '赵六', - gender: '1', - phone: '13600136004', - idCard: '110101199204044567', - idCardImg: 'https://picsum.photos/seed/idcard4/96/64', - idCardImgList: ['https://picsum.photos/seed/idcard4a/96/64'], - }, - { - realName: '孙七', - gender: '2', - phone: '13500135005', - idCard: '110101199305055678', - idCardImg: '', - idCardImgList: [], - }, - { - realName: '周八', - gender: '1', - phone: '13400134006', - idCard: '110101199406066789', - idCardImg: 'https://picsum.photos/seed/idcard6/96/64', - idCardImgList: [ - 'https://picsum.photos/seed/idcard6a/96/64', - 'https://picsum.photos/seed/idcard6b/96/64', - ], - }, - { - realName: '吴九', - gender: '2', - phone: '13300133007', - idCard: '110101199507077890', - idCardImg: '', - idCardImgList: [], - }, - { - realName: '郑十', - gender: '1', - phone: '13200132008', - idCard: '110101199608088901', - idCardImg: 'https://picsum.photos/seed/idcard8/96/64', - idCardImgList: ['https://picsum.photos/seed/idcard8a/96/64'], - }, -]; - -/** 构建分页响应 */ -export function buildMockSignupListPage( - params: SignupListQueryParams, -): PageData { - // mock 模式下忽略 categoryId 过滤,直接分页返回 - const page = Number(params.page) || 1; - const limit = Number(params.limit) || 10; - const start = (page - 1) * limit; - const list = MOCK_SIGNUP_LIST.slice(start, start + limit); - return { total: MOCK_SIGNUP_LIST.length, list }; -} diff --git a/src/pages/events/list/components/EventDetailModal.tsx b/src/pages/events/list/components/EventDetailModal.tsx index 9bc8816..12c4f43 100644 --- a/src/pages/events/list/components/EventDetailModal.tsx +++ b/src/pages/events/list/components/EventDetailModal.tsx @@ -1,4 +1,4 @@ -import { defineComponent, reactive, onMounted } from 'vue'; +import { defineComponent, reactive, ref } from 'vue'; import { Modal, Descriptions, Image, Table, Pagination, Space, Spin } from 'ant-design-vue'; import { EyeOutlined } from '@ant-design/icons-vue'; import { message } from 'ant-design-vue'; @@ -9,7 +9,7 @@ import { type InnerCategoryVO, type TournamentAdminInfoSignupVO, } from '../model/services'; -import { useState } from '@/hooks'; +import { useState, useEffect } from '@/hooks'; import styles from './EventDetailModal.module.less'; interface EventDetailModalProps { @@ -67,8 +67,8 @@ const PLAYER_COLUMNS = [ title: '证件图片', key: 'idCardImgs', width: 180, - customRender: ({ record }: { record: TournamentAdminInfoSignupVO }) => - renderIdImages(record.idCardImgList, record.idCardImg), + customRender: ({ record }: { record?: TournamentAdminInfoSignupVO }) => + record ? renderIdImages(record.idCardImgList || [], record.idCardImg || '') : -, }, ]; @@ -82,17 +82,19 @@ export default defineComponent({ setup(props: EventDetailModalProps) { // ===== 详情数据 ===== const [detail, setDetail] = useState(null); - const [detailLoading, setDetailLoading] = useState(true); + const [detailLoading, setDetailLoading] = useState(false); // ===== 每个组别的选手数据(key = categoryId) ===== - const categoryPlayers = reactive>({}); - const categoryPlayerTotal = reactive>({}); - const categoryPlayerLoading = reactive>({}); - const categoryPlayerPage = reactive>({}); + const categoryPlayers = ref>({}); + const categoryPlayerTotal = ref>({}); + const categoryPlayerLoading = ref>({}); + const categoryPlayerPage = ref>({}); /** 获取某个组别的选手列表(分页) */ const fetchPlayers = async (categoryId: string, page: number) => { - categoryPlayerLoading[categoryId] = true; + categoryPlayerLoading.value[categoryId] = true; + // 触发响应式更新 + categoryPlayerLoading.value = { ...categoryPlayerLoading.value }; try { const res = await getSignupList({ categoryId, @@ -100,14 +102,18 @@ export default defineComponent({ limit: String(PAGE_SIZE), }); if (res.code == 200) { - categoryPlayers[categoryId] = res.data.list; - categoryPlayerTotal[categoryId] = res.data.total; - categoryPlayerPage[categoryId] = page; + categoryPlayers.value[categoryId] = res.data.list; + categoryPlayers.value = { ...categoryPlayers.value }; + categoryPlayerTotal.value[categoryId] = res.data.total; + categoryPlayerTotal.value = { ...categoryPlayerTotal.value }; + categoryPlayerPage.value[categoryId] = page; + categoryPlayerPage.value = { ...categoryPlayerPage.value }; } } catch { // mock 模式不会到这里,真实模式下由 API 层处理 } finally { - categoryPlayerLoading[categoryId] = false; + categoryPlayerLoading.value[categoryId] = false; + categoryPlayerLoading.value = { ...categoryPlayerLoading.value }; } }; @@ -116,24 +122,19 @@ export default defineComponent({ fetchPlayers(categoryId, page); }; - // ===== 初始化 ===== - onMounted(async () => { + /** 获取赛事详情 */ + const fetchDetail = async (id: string) => { setDetailLoading(true); + setDetail(null); try { - const id = props.record?.id; - if (!id) { - message.error('缺少赛事ID'); - return; - } - - const res = await getEventDetail(String(id)); - if (res.code == 200) { + const res = await getEventDetail(id); + if (res.code == 200 && res.data) { setDetail(res.data); // 为每个组别加载第一页选手 for (const cat of res.data.categoryList) { - categoryPlayers[cat.id] = []; - categoryPlayerTotal[cat.id] = 0; - categoryPlayerPage[cat.id] = 1; + categoryPlayers.value[cat.id] = []; + categoryPlayerTotal.value[cat.id] = 0; + categoryPlayerPage.value[cat.id] = 1; fetchPlayers(cat.id, 1); } } else { @@ -144,7 +145,14 @@ export default defineComponent({ } finally { setDetailLoading(false); } - }); + }; + + // 弹窗打开且 有 record.id 时自动请求(useEffect 带 immediate: true,兼容 destroyOnClose 重建场景) + useEffect(() => { + if (props.visible && props.record?.id) { + fetchDetail(String(props.record.id)); + } + }, [() => props.visible, () => props.record?.id]); return () => (
handlePlayerPageChange(cat.id, page)} diff --git a/src/pages/events/list/components/EventRegulationModal.tsx b/src/pages/events/list/components/EventRegulationModal.tsx index f0230ce..4790b7a 100644 --- a/src/pages/events/list/components/EventRegulationModal.tsx +++ b/src/pages/events/list/components/EventRegulationModal.tsx @@ -1,36 +1,54 @@ -import { defineComponent } from 'vue'; -import { Modal, Image } from 'ant-design-vue'; +import { defineComponent, ref } from 'vue'; +import { Modal, Image, Spin } from 'ant-design-vue'; +import { getRuleInfo } from '../model/services'; +import type { TournamentAdminRuleInfoVO } from '@/api/events/types'; +import { useEffect } from '@/hooks'; import styles from './EventRegulationModal.module.less'; interface EventRegulationModalProps { visible: boolean; + /** 当前赛事记录(需包含 id 字段) */ + record?: Record; onClose: () => void; } -/** 规程内容 */ -const REGULATION_TEXT = [ - '一、赛事目的:推动羽毛球运动发展,提高运动员技术水平,增进体育文化交流。', - '二、参赛资格:凡身体健康、年龄符合要求的羽毛球爱好者均可报名参加。', - '三、比赛规则:采用中国羽毛球协会审定的最新《羽毛球竞赛规则》。', - '四、赛制安排:比赛分小组赛和淘汰赛两个阶段,小组赛采用单循环,淘汰赛采用单败淘汰。', - '五、器材要求:参赛运动员自备球拍,比赛用球由组委会统一提供。', - '六、裁判设置:每场比赛设主裁判1名、副裁判2名,确保比赛公平公正。', -].join('\n\n'); - -/** 规程图片(mock:8 张示例图,4 列 grid) */ -const REGULATION_IMAGES = Array.from({ length: 8 }, (_, i) => ({ - id: i + 1, - src: `https://picsum.photos/seed/reg${i + 1}/300/200`, - alt: `规程图片 ${i + 1}`, -})); - export default defineComponent({ name: 'EventRegulationModal', props: { visible: { type: Boolean, default: false }, + record: { type: Object, default: () => ({}) }, onClose: { type: Function, required: true }, }, setup(props: EventRegulationModalProps) { + const loading = ref(false); + const ruleData = ref(null); + + /** 调用 getRuleInfo 接口获取规程数据 */ + const fetchRuleInfo = async (id: string) => { + loading.value = true; + ruleData.value = null; + try { + const res = await getRuleInfo(id); + if (res.code == 200 && res.data) { + ruleData.value = res.data; + } else { + ruleData.value = null; + } + } catch (e) { + console.error('获取赛事规程失我败:', e); + ruleData.value = null; + } finally { + loading.value = false; + } + }; + + // 弹窗打开且 有 record.id 时自动请求(useEffect 带 immediate: true,兼容 destroyOnClose 重建场景) + useEffect(() => { + if (props.visible && props.record?.id) { + fetchRuleInfo(props.record.id); + } + }, [() => props.visible, () => props.record?.id]); + return () => ( - {/* 规程内容 */} -
-
规程内容
-
{REGULATION_TEXT}
-
- - {/* 规程图片:4 列 grid,点击可浏览 */} -
-
规程图片
- -
- {REGULATION_IMAGES.map((img) => ( -
- + + {ruleData.value ? ( + <> + {/* 规程内容 */} + {ruleData.value.rulesDescr && ( +
+
规程内容
+
{ruleData.value.rulesDescr}
- ))} -
- -
+ )} + + {/* 规程图片 */} + {(ruleData.value.rulesDescrImgList?.length ?? 0) > 0 && ( +
+
规程图片
+ +
+ {ruleData.value.rulesDescrImgList!.map((src: string, idx: number) => ( +
+ +
+ ))} +
+
+
+ )} + + {/* 无数据提示 */} + {!ruleData.value.rulesDescr && !ruleData.value.rulesDescrImgList?.length && ( +
+ 暂无规程信息 +
+ )} + + ) : ( + !loading.value && ( +
+ 暂无规程信息 +
+ ) + )} + ); }, diff --git a/src/pages/events/list/index.tsx b/src/pages/events/list/index.tsx index 2251798..22d49b0 100644 --- a/src/pages/events/list/index.tsx +++ b/src/pages/events/list/index.tsx @@ -1,4 +1,4 @@ -import { defineComponent, onMounted } from 'vue'; +import { defineComponent } from 'vue'; import { Button, Input, @@ -10,8 +10,6 @@ import { Cascader, Tooltip, Pagination, - Modal, - message, } from 'ant-design-vue'; import { useEventListModel, @@ -20,9 +18,9 @@ import { ID_CARD_OPTIONS, ID_CARD_IMAGE_OPTIONS, } from './model/useEventListModel'; -import { toggleEventOnline } from './model/services'; +import { useEventListColumns } from './model/useEventListColumns'; import { regionOptions } from '@/utils/areaData'; -import { useContainerSize, useState } from '@/hooks'; +import { useContainerSize } from '@/hooks'; import EventRegulationModal from './components/EventRegulationModal'; import EventDetailModal from './components/EventDetailModal'; import EventAuditModal from './components/EventAuditModal'; @@ -31,85 +29,12 @@ import pageStyles from '@/assets/styles/pageLayout.module.less'; const { RangePicker } = DatePicker; /** - * bodyCell 渲染函数 - */ -function renderBodyCell({ - column, - text, - record, - onView, - onViewRegulation, - onAudit, - onToggleShelf, -}: { - column: any; - text: any; - record: any; - onView: (record: any) => void; - onViewRegulation: (record: any) => void; - onAudit: (record: any) => void; - onToggleShelf: (record: any) => void; -}) { - if (column.dataIndex === 'name') { - const maxLen = 15; - const raw = text || ''; - const display = raw.length > maxLen ? raw.slice(0, maxLen) + '...' : raw; - return raw.length > maxLen ? ( - - {display} - - ) : ( - {display} - ); - } - - if (column.key === 'regulation') { - return ( - onViewRegulation(record)}> - 查看 - - ); - } - - if (column.key === 'action') { - const status = record.status; - - return ( - // 报名中(0) / 进行中(1):查看 + 下架 - ((status === '0' || status === '1') && ( - - - - - )) || - // 已下架(4):查看 + 上架 - (status === '4' && ( - - - - - )) || ( - // 已结束(2) / 已删除(3):仅查看 - - ) - ); - } - - return; -} - -/** - * 赛事列表 + * 赛事列表页面(纯视图层) + * + * 架构分层: + * - 数据层:services.ts(API 调用) + * - 逻辑层:useEventListModel.ts(状态 + 业务逻辑 + 交互处理) + * - 视图层:useEventListColumns.ts(表格列配置)+ 本组件(纯 UI 渲染) */ export default defineComponent({ name: 'EventList', @@ -118,94 +43,122 @@ export default defineComponent({ filterForm, loading, dataSource, - columns, pagination, + regulationModalVisible, + detailModalVisible, + auditModalVisible, + currentRecord, + togglingId, handleSearch, handleReset, handlePageChange, + openDetailModal, + openRegulationModal, + closeRegulationModal, + closeDetailModal, + closeAuditModal, + submitAudit, + handleToggleShelf, } = useEventListModel(); + const { columns } = useEventListColumns(); const { containerRef, height } = useContainerSize(); - // 弹窗状态 - const [regulationModalVisible, setRegulationModalVisible] = useState(false); - const [detailModalVisible, setDetailModalVisible] = useState(false); - const [auditModalVisible, setAuditModalVisible] = useState(false); - const [currentRecord, setCurrentRecord] = useState({}); - - const handleView = (record: any) => { - console.log('查看赛事:', record.id); - setCurrentRecord(record); - setDetailModalVisible(true); + /** 渲染赛事名称(超长省略 + Tooltip) */ + const renderName = (text: string) => { + const maxLen = 15; + const raw = text || ''; + const display = raw.length > maxLen ? raw.slice(0, maxLen) + '...' : raw; + return raw.length > maxLen ? ( + + {display} + + ) : ( + {display} + ); }; - /** 上架 / 下架 通用 */ - const handleToggleShelf = (record: any) => { - const isRemoved = record.status === '4'; - const actionText = isRemoved ? '上架' : '下架'; - Modal.confirm({ - title: `${actionText}确认`, - content: `确认${actionText}"${record.name}"该赛事吗,下架后用户将无法搜索和报名该赛事,已有报名和订单不受影响。`, - okText: '确认', - cancelText: '取消', - onOk: async () => { - const online = isRemoved ? '1' : '0'; - try { - const res = await toggleEventOnline({ id: record.id, online }); - if (res.code == 200) { - message.success(`${actionText}成功`); - handleSearch(); - } else { - message.error(res.msg || `${actionText}失败`); - } - } catch (e: any) { - console.error(`${actionText}失败:`, e); - } - }, - }); + /** 渲染操作列(纯视图,逻辑在 model 的 handleToggleShelf) */ + const renderAction = (record: any) => { + const status = record.status; + + if (status === '0' || status === '1') { + return ( + + + + + ); + } + + if (status === '4') { + return ( + + + + + ); + } + + return ( + + ); }; - /** 打开审核弹窗 */ - const handleAudit = (record: any) => { - console.log('审核赛事:', record.id); - setCurrentRecord(record); - setAuditModalVisible(true); - }; - - /** 提交审核 */ - const handleAuditSubmit = (payload: { approved: boolean; comment: string }) => { - console.log('提交审核:', currentRecord.value.id, payload); - // TODO: 替换为真实 API 调用 - message.success('审核提交成功'); - setAuditModalVisible(false); - }; - - const handleViewRegulation = (record: any) => { - console.log('查看规程:', record.id); - setCurrentRecord(record); - setRegulationModalVisible(true); - }; - - onMounted(() => { - handleSearch(); - }); - - /** 最终表格列:模型列 + 赛事规程 + 操作 */ + /** 完整表格列 */ const tableColumns = [ - ...columns, - { title: '赛事规程', key: 'regulation', width: 100, align: 'center' as const }, + ...columns.map((col: any) => { + if (col.dataIndex === 'name') { + return { + ...col, + customRender: ({ text }: { text: string }) => renderName(text), + }; + } + return col; + }), + { + title: '赛事规程', + key: 'regulation', + width: 100, + align: 'center' as const, + customRender: ({ record }: { record: any }) => ( + openRegulationModal(record)}> + 查看 + + ), + }, { title: '操作', key: 'action', width: 160, fixed: 'right' as const, align: 'center' as const, + customRender: ({ record }: { record: any }) => renderAction(record), }, ]; return () => (
- {/* ===== 筛选区 ===== */}
@@ -308,7 +261,6 @@ export default defineComponent({
- {/* ===== 表格区 ===== */}
- {{ - bodyCell: (args: any) => - renderBodyCell({ - ...args, - onView: handleView, - onViewRegulation: handleViewRegulation, - onAudit: handleAudit, - onToggleShelf: handleToggleShelf, - }), - }} -
+ /> - {/* 独立分页,右下方 */}
`共 ${total} 条`} onChange={handlePageChange} @@ -344,26 +284,26 @@ export default defineComponent({
- {/* ===== 弹窗区 ===== */} {regulationModalVisible.value && ( setRegulationModalVisible(false)} + record={currentRecord.value} + onClose={closeRegulationModal} /> )} {detailModalVisible.value && ( setDetailModalVisible(false)} + onClose={closeDetailModal} /> )} {auditModalVisible.value && ( setAuditModalVisible(false)} - onSubmit={handleAuditSubmit} + onClose={closeAuditModal} + onSubmit={submitAudit} /> )} diff --git a/src/pages/events/list/model/config.ts b/src/pages/events/list/model/config.ts new file mode 100644 index 0000000..9d6548b --- /dev/null +++ b/src/pages/events/list/model/config.ts @@ -0,0 +1,69 @@ +/** + * 赛事列表页配置 + * 职责:集中管理字段映射、选项配置等常量 + */ + +import type { EventListQueryParams } from './services'; + +export const FILTER_DEFAULTS = { + createDateRange: null as [string, string] | null, + startDateRange: null as [string, string] | null, + name: '', + createName: '', + status: '', + region: [] as string[], + isPublic: '', + idCardNo: '', + idCardImg: '', +}; + +export type FieldMapping = [ + keyof typeof FILTER_DEFAULTS, + keyof EventListQueryParams, + ((v: any) => any)?, +]; + +export const FIELD_MAPPINGS: FieldMapping[] = [ + ['name', 'name', (v) => v?.trim?.() ?? v], + ['createName', 'createName', (v) => v?.trim?.() ?? v], + ['status', 'status'], + ['isPublic', 'isPublic'], + ['idCardNo', 'idCardNo'], + ['idCardImg', 'idCardImg'], + ['region', 'areaName', (v) => v?.[v.length - 1]], + ['createDateRange', 'createDate', (v) => v?.[0]], + ['createDateRange', 'createDateEnd', (v) => v?.[1]], + ['startDateRange', 'startDate', (v) => v?.[0]], + ['startDateRange', 'startDateEnd', (v) => v?.[1]], +]; + +/** 赛事状态选项(value 对应 API status) */ +export const EVENT_STATUS_OPTIONS = [ + { value: '', label: '全部' }, + { value: '0', label: '报名中' }, + { value: '1', label: '进行中' }, + { value: '2', label: '已结束' }, + { value: '3', label: '已删除' }, + { value: '4', label: '已下架' }, +] as const; + +/** 公开活动选项(value 对应 API isPublic: 0=非公开,1=公开) */ +export const PUBLIC_EVENT_OPTIONS = [ + { value: '', label: '全部' }, + { value: '1', label: '公开' }, + { value: '0', label: '非公开' }, +] as const; + +/** 证件号选项(value 对应 API idCardNo: 0=无需,1=需要) */ +export const ID_CARD_OPTIONS = [ + { value: '', label: '全部' }, + { value: '0', label: '无需提供' }, + { value: '1', label: '需提供' }, +] as const; + +/** 证件图片选项(value 对应 API idCardImg: 0=无需,1=需要) */ +export const ID_CARD_IMAGE_OPTIONS = [ + { value: '', label: '全部' }, + { value: '0', label: '无需提供' }, + { value: '1', label: '需提供' }, +] as const; diff --git a/src/pages/events/list/model/services.ts b/src/pages/events/list/model/services.ts index 03e3d22..bb82025 100644 --- a/src/pages/events/list/model/services.ts +++ b/src/pages/events/list/model/services.ts @@ -3,10 +3,6 @@ * 该页面使用的所有接口集中管理 */ 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, @@ -16,12 +12,14 @@ import type { TournamentAdminVO, TournamentAdminInfoVO, TournamentAdminInfoSignupVO, + TournamentAdminRuleInfoVO, } from '@/api/events/types'; export type { TournamentAdminVO, TournamentAdminInfoVO, TournamentAdminInfoSignupVO, + TournamentAdminRuleInfoVO, InnerCategoryVO, EventListQueryParams, SignupListQueryParams, @@ -30,62 +28,33 @@ export type { 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 函数 -// ============================================================ +const eventList = '/admin/manager/tm/page'; +const eventDetail = '/admin/manager/tm/info'; +const signupList = '/admin/manager/tm/info/signupPage'; +const toggleOnline = '/admin/manager/tm/online'; +const ruleInfo = `/admin/manager/tm/ruleInfo`; /** 赛事列表 - 列表分页 */ 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 getRuleInfo(id: string): Promise> { + return get(ruleInfo, { 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); } diff --git a/src/pages/events/list/model/useEventListColumns.ts b/src/pages/events/list/model/useEventListColumns.ts new file mode 100644 index 0000000..4c9e6fe --- /dev/null +++ b/src/pages/events/list/model/useEventListColumns.ts @@ -0,0 +1,116 @@ +import { h } from 'vue'; +import { StatusTag, type StatusTagTone } from '@/components'; +import type { TournamentAdminVO } from './services'; + +/** 状态标签映射 */ +const STATUS_MAP: Record = { + '0': { label: '报名中', tone: 'primary' }, + '1': { label: '进行中', tone: 'cyan' }, + '2': { label: '已结束', tone: 'neutral' }, + '3': { label: '已删除', tone: 'neutral' }, + '4': { label: '已下架', tone: 'orange' }, +}; + +/** 0/1 → 显示文字 */ +const formatDisplayValue = (value: string, map: Record) => map[value] ?? value; + +const ID_CARD_DISPLAY_MAP: Record = { '0': '否', '1': '是' }; +const IS_PUBLIC_DISPLAY_MAP: Record = { '0': '非公开', '1': '公开' }; + +/** + * 赛事列表 - 表格列配置(视图层) + * 纯展示逻辑,不包含状态管理 + */ +export function useEventListColumns() { + const columns = [ + { title: '赛事ID', dataIndex: 'id', key: 'id', width: 180 }, + { + title: '赛事名称', + dataIndex: 'name', + key: 'name', + width: 160, + }, + { + title: '赛事时间', + key: 'eventTime', + width: 260, + customRender: ({ record }: { record: TournamentAdminVO }) => + `${record.startTimeBegin} 至 ${record.startTimeEnd}`, + }, + { + title: '省市区', + dataIndex: 'venueArea', + key: 'venueArea', + width: 200, + }, + { + title: '地点', + dataIndex: 'venue', + key: 'venue', + width: 180, + }, + { + title: '是否公开', + dataIndex: 'isPublic', + key: 'isPublic', + width: 100, + customRender: ({ text }: { text: string }) => formatDisplayValue(text, IS_PUBLIC_DISPLAY_MAP), + }, + { + title: '是否提供证件号', + dataIndex: 'isRequireIdCardNo', + key: 'isRequireIdCardNo', + width: 160, + customRender: ({ text }: { text: string }) => formatDisplayValue(text, ID_CARD_DISPLAY_MAP), + }, + { + title: '是否提供证件图片', + dataIndex: 'isRequireIdCardImg', + key: 'isRequireIdCardImg', + width: 160, + customRender: ({ text }: { text: string }) => formatDisplayValue(text, ID_CARD_DISPLAY_MAP), + }, + { + title: '创建人昵称', + dataIndex: 'createNickname', + key: 'createNickname', + width: 110, + }, + { + title: '创建人手机号', + dataIndex: 'createPhone', + key: 'createPhone', + width: 130, + }, + { + title: '创建时间', + dataIndex: 'createTime', + key: 'createTime', + width: 170, + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 90, + customRender: ({ text }: { text: string }) => { + const info = STATUS_MAP[text] || { label: text, tone: 'default' as const }; + return h(StatusTag, { label: info.label, tone: info.tone }); + }, + }, + { + title: '报名人数', + dataIndex: 'signupCount', + key: 'signupCount', + width: 90, + }, + { + title: '取消报名人数', + dataIndex: 'dropoutCount', + key: 'dropoutCount', + width: 140, + }, + ]; + + return { columns }; +} diff --git a/src/pages/events/list/model/useEventListModel.ts b/src/pages/events/list/model/useEventListModel.ts index 10e1da3..e3fef00 100644 --- a/src/pages/events/list/model/useEventListModel.ts +++ b/src/pages/events/list/model/useEventListModel.ts @@ -1,84 +1,36 @@ -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 './services'; -import { getEventList } from './services'; +import { computed, reactive, toRef, Ref } from 'vue'; +import { Modal, message } from 'ant-design-vue'; +import { useState, useDebounce } from '@/hooks'; +import { useRequest } from '@/hooks/useRequest'; +import { useEffect } from '@/hooks/useEffect'; +import { usePagination } from '@/hooks/usePagination'; +import type { TournamentAdminVO, EventListQueryParams, PageData, ApiResult } from './services'; +import { getEventList, toggleEventOnline } from './services'; +import { + FILTER_DEFAULTS, + FIELD_MAPPINGS, + EVENT_STATUS_OPTIONS, + PUBLIC_EVENT_OPTIONS, + ID_CARD_OPTIONS, + ID_CARD_IMAGE_OPTIONS, +} from './config'; -// ============================================================ -// 常量 -// ============================================================ - -/** 赛事状态选项(value 对应 API status: 不传=全部、0=报名中,1=进行中,2=已结束,3=已删除,4=已下架) */ -export const EVENT_STATUS_OPTIONS = [ - { value: '', label: '全部' }, - { value: '0', label: '报名中' }, - { value: '1', label: '进行中' }, - { value: '2', label: '已结束' }, - { value: '3', label: '已删除' }, - { value: '4', label: '已下架' }, -] as const; - -/** 公开活动选项(value 对应 API isPublic: 0=非公开,1=公开) */ -export const PUBLIC_EVENT_OPTIONS = [ - { value: '', label: '全部' }, - { value: '1', label: '公开' }, - { value: '0', label: '非公开' }, -] as const; - -/** 证件号选项(value 对应 API idCardNo: 0=无需,1=需要) */ -export const ID_CARD_OPTIONS = [ - { value: '', label: '全部' }, - { value: '0', label: '无需提供' }, - { value: '1', label: '需提供' }, -] as const; - -/** 证件图片选项(value 对应 API idCardImg: 0=无需,1=需要) */ -export const ID_CARD_IMAGE_OPTIONS = [ - { value: '', label: '全部' }, - { value: '0', label: '无需提供' }, - { value: '1', label: '需提供' }, -] as const; - -/** 状态标签映射 */ -const STATUS_MAP: Record = { - '0': { label: '报名中', tone: 'primary' }, - '1': { label: '进行中', tone: 'cyan' }, - '2': { label: '已结束', tone: 'neutral' }, - '3': { label: '已删除', tone: 'neutral' }, - '4': { label: '已下架', tone: 'orange' }, -}; - -/** 0/1 → 显示文字 */ -const formatDisplayValue = (value: string, map: Record) => map[value] ?? value; - -const ID_CARD_DISPLAY_MAP: Record = { '0': '否', '1': '是' }; -const IS_PUBLIC_DISPLAY_MAP: Record = { '0': '非公开', '1': '公开' }; - -// ============================================================ -// Model -// ============================================================ +export { EVENT_STATUS_OPTIONS, PUBLIC_EVENT_OPTIONS, ID_CARD_OPTIONS, ID_CARD_IMAGE_OPTIONS }; /** * 赛事列表页数据模型 - * 筛选字段名与 API query 参数(EventListQueryParams)保持一致, - * 仅日期区间 RangePicker 拆分为 start/end 两个字段、省市区 Cascader 取末级为 areaName。 + * 职责:状态管理 + 数据获取 + 业务逻辑 + 交互处理 */ export function useEventListModel() { - // ===== 筛选条件(key 名对齐 API 查询参数) ===== - const filterForm = reactive({ - createDateRange: null as [string, string] | null, - startDateRange: null as [string, string] | null, - name: '', - createName: '', - status: '', - region: [] as string[], - isPublic: '', - idCardNo: '', - idCardImg: '', - }); + const filterForm = reactive({ ...FILTER_DEFAULTS }); + const [regulationModalVisible, setRegulationModalVisible] = useState(false); + const [detailModalVisible, setDetailModalVisible] = useState(false); + const [auditModalVisible, setAuditModalVisible] = useState(false); + const [currentRecord, setCurrentRecord] = useState({}); + const [togglingId, setTogglingId] = useState(''); + + const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 }); - // 可搜索字段防抖 const { debouncedValue: debouncedName } = useDebounce(toRef(filterForm, 'name') as Ref, { delay: 300, }); @@ -87,215 +39,177 @@ export function useEventListModel() { { delay: 300 }, ); - // ===== 表格状态 ===== - const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState([]); - const [pagination, setPagination] = useState({ - current: 1, - pageSize: 10, - total: 0, - }); - - // ===== 表格列配置(dataIndex 对齐 TournamentAdminVO 字段) ===== - const columns = [ - { title: '赛事ID', dataIndex: 'id', key: 'id', width: 180 }, - { - title: '赛事名称', - dataIndex: 'name', - key: 'name', - width: 160, - }, - { - title: '赛事时间', - key: 'eventTime', - width: 260, - customRender: ({ record }: { record: TournamentAdminVO }) => - `${record.startTimeBegin} 至 ${record.startTimeEnd}`, - }, - { - title: '省市区', - dataIndex: 'venueArea', - key: 'venueArea', - width: 200, - }, - { - title: '地点', - dataIndex: 'venue', - key: 'venue', - width: 180, - }, - { - title: '是否公开', - dataIndex: 'isPublic', - key: 'isPublic', - width: 100, - customRender: ({ text }: { text: string }) => formatDisplayValue(text, IS_PUBLIC_DISPLAY_MAP), - }, - { - title: '是否提供证件号', - dataIndex: 'isRequireIdCardNo', - key: 'isRequireIdCardNo', - width: 160, - customRender: ({ text }: { text: string }) => formatDisplayValue(text, ID_CARD_DISPLAY_MAP), - }, - { - title: '是否提供证件图片', - dataIndex: 'isRequireIdCardImg', - key: 'isRequireIdCardImg', - width: 160, - customRender: ({ text }: { text: string }) => formatDisplayValue(text, ID_CARD_DISPLAY_MAP), - }, - { - title: '创建人昵称', - dataIndex: 'createNickname', - key: 'createNickname', - width: 110, - }, - { - title: '创建人手机号', - dataIndex: 'createPhone', - key: 'createPhone', - width: 130, - }, - { - title: '创建时间', - dataIndex: 'createTime', - key: 'createTime', - width: 170, - }, - { - title: '状态', - dataIndex: 'status', - key: 'status', - width: 90, - customRender: ({ text }: { text: string }) => { - const info = STATUS_MAP[text] || { label: text, tone: 'default' as const }; - return h(StatusTag, { label: info.label, tone: info.tone }); - }, - }, - { - title: '报名人数', - dataIndex: 'signupCount', - key: 'signupCount', - width: 90, - }, - { - title: '取消报名人数', - dataIndex: 'dropoutCount', - key: 'dropoutCount', - width: 140, - }, - ]; - - // ===== 构建 API 查询参数 ===== const buildQueryParams = (): EventListQueryParams => { - const params: EventListQueryParams = { - page: String(pagination.value.current), - limit: String(pagination.value.pageSize), - }; + const { page, pageSize } = pagination.params.value; - if (filterForm.createDateRange) { - params.createDate = filterForm.createDateRange[0]; - params.createDateEnd = filterForm.createDateRange[1]; - } - if (filterForm.startDateRange) { - params.startDate = filterForm.startDateRange[0]; - params.startDateEnd = filterForm.startDateRange[1]; - } - if (filterForm.region.length > 0) { - params.areaName = filterForm.region[filterForm.region.length - 1]; - } - if (filterForm.name.trim()) { - params.name = filterForm.name.trim(); - } - if (filterForm.createName.trim()) { - params.createName = filterForm.createName.trim(); - } - if (filterForm.status) { - params.status = filterForm.status; - } - if (filterForm.isPublic) { - params.isPublic = filterForm.isPublic; - } - if (filterForm.idCardNo) { - params.idCardNo = filterForm.idCardNo; - } - if (filterForm.idCardImg) { - params.idCardImg = filterForm.idCardImg; - } + const params = FIELD_MAPPINGS.reduce( + (acc, [formKey, paramKey, transform]) => { + const rawValue = filterForm[formKey]; + if (!hasValue(rawValue)) return acc; - return params; + const value = safeTransform(rawValue, transform); + if (isEmptyValue(value)) return acc; + + return { ...acc, [paramKey]: value }; + }, + {} as Record, + ); + + return { page: String(page), limit: String(pageSize), ...params } as EventListQueryParams; }; - // ===== 计算属性 ===== + const { + data, + loading, + run: fetchList, + } = useRequest>>(() => getEventList(buildQueryParams()), { + refreshDeps: [], + formatResult: (res) => res, + }); + + // 提取列表数据 + const listData = computed | undefined>(() => { + const res = data.value; + return res ? (res as any).data : undefined; + }); + + const dataSource = computed(() => listData.value?.list || []); + + // 同步分页总条数 + useEffect(() => { + const total = (data.value as any)?.data?.total; + if (total !== undefined) pagination.setTotal(total); + }, [data]); + const hasFilter = computed(() => { + const f = filterForm; return ( - filterForm.createDateRange !== null || - filterForm.startDateRange !== null || - debouncedName.value.trim() !== '' || - debouncedCreator.value.trim() !== '' || - filterForm.status !== '' || - filterForm.region.length > 0 || - filterForm.isPublic !== '' || - filterForm.idCardNo !== '' || - filterForm.idCardImg !== '' + !!f.createDateRange || + !!f.startDateRange || + !!debouncedName.value?.trim() || + !!debouncedCreator.value?.trim() || + !!f.status || + f.region.length > 0 || + !!f.isPublic || + !!f.idCardNo || + !!f.idCardImg ); }); - // ===== 方法 ===== + const handleSearch = () => fetchList(); - /** 查询 */ - const handleSearch = useThrottleFn(async () => { - setLoading(true); - try { - const queryParams = buildQueryParams(); - console.log('赛事列表查询参数:', queryParams); - const res = await getEventList(queryParams); - - if (res.code == 200) { - setDataSource(res.data.list); - setPagination({ ...pagination.value, total: res.data.total }); - } else { - message.error(res.msg || '查询失败'); - } - } catch (error: any) { - console.error('赛事列表查询失败:', error); - } finally { - setLoading(false); - } - }, 500); - - /** 重置 */ - const handleReset = useThrottleFn(() => { - filterForm.createDateRange = null; - filterForm.startDateRange = null; - filterForm.name = ''; - filterForm.createName = ''; - filterForm.status = ''; - filterForm.region = []; - filterForm.isPublic = ''; - filterForm.idCardNo = ''; - filterForm.idCardImg = ''; - setPagination({ current: 1, pageSize: 10, total: 0 }); - setDataSource([]); - // 等待 debounce 生效后自动查询 - setTimeout(() => handleSearch(), 350); - }, 500); + const handleReset = () => { + Object.assign(filterForm, FILTER_DEFAULTS); + pagination.reset(); + setTimeout(fetchList, 350); + }; const handlePageChange = (page: number, pageSize: number) => { - setPagination({ ...pagination.value, current: page, pageSize }); - handleSearch(); + pagination.setCurrent(page); + if (pageSize !== (pagination as any).pageSize.value) { + pagination.setPageSize(pageSize); + } + setTimeout(fetchList, 0); + }; + + const openDetailModal = (record: any) => { + setCurrentRecord(record); + setDetailModalVisible(true); + }; + + const openRegulationModal = (record: any) => { + setCurrentRecord(record); + setRegulationModalVisible(true); + }; + + const openAuditModal = (record: any) => { + setCurrentRecord(record); + setAuditModalVisible(true); + }; + + const closeRegulationModal = () => setRegulationModalVisible(false); + const closeDetailModal = () => setDetailModalVisible(false); + const closeAuditModal = () => setAuditModalVisible(false); + + // TODO + const submitAudit = (payload: { approved: boolean; comment: string }) => { + console.log('提交审核:', currentRecord.value.id, payload); + message.success('审核提交成功'); + setAuditModalVisible(false); + }; + + const handleToggleShelf = (record: any) => { + const isRemoved = record.status === '4'; + const actionText = isRemoved ? '上架' : '下架'; + const descText = isRemoved + ? `确认上架"${record.name}"吗?上架后用户可搜索和报名该赛事。` + : `确认下架"${record.name}"吗?下架后用户将无法搜索和报名该赛事。`; + + Modal.confirm({ + title: `${actionText}确认`, + content: descText, + okText: '确认', + cancelText: '取消', + onOk: async () => { + setTogglingId(record.id); + try { + const res = await toggleEventOnline({ id: record.id, online: isRemoved ? '1' : '0' }); + if (res.code === 200) { + message.success(`${actionText}成功`); + fetchList(); + } else { + message.error(res.msg || `${actionText}失败`); + } + } catch (e: any) { + message.error(e?.message || `${actionText}失败`); + } finally { + setTogglingId(''); + } + }, + }); }; return { filterForm, loading, dataSource, - columns, pagination, hasFilter, + regulationModalVisible, + detailModalVisible, + auditModalVisible, + currentRecord, + togglingId, handleSearch, handleReset, handlePageChange, + openDetailModal, + openRegulationModal, + openAuditModal, + closeRegulationModal, + closeDetailModal, + closeAuditModal, + submitAudit, + handleToggleShelf, }; } + +function hasValue(v: any): boolean { + if (v == null) return false; + if (Array.isArray(v)) return v.length > 0 && v.some((item) => item != null); + if (typeof v === 'string') return v.trim() !== ''; + return true; +} + +function isEmptyValue(v: any): boolean { + return v == null || (typeof v === 'string' && v.trim() === ''); +} + +function safeTransform(value: any, transform?: (v: any) => any): any { + if (!transform) return value; + try { + return transform(value) ?? value; + } catch { + return value; + } +} diff --git a/src/pages/events/logs/model/services.ts b/src/pages/events/logs/model/services.ts index 7102626..b50ae01 100644 --- a/src/pages/events/logs/model/services.ts +++ b/src/pages/events/logs/model/services.ts @@ -3,8 +3,6 @@ * 该页面使用的所有接口集中管理 */ import { get } from '@/utils/request'; -import { USE_MOCK, MOCK_DELAY } from '@/config/mock'; -import { buildMockEventOperationLogPage } from '@/config/mock/eventOperationLogs'; import type { OperationLogQueryParams, TournamentAdminOperationPageVO, @@ -24,30 +22,13 @@ export type { // ============================================================ 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/orders/index.tsx b/src/pages/events/orders/index.tsx index 9188d16..5f5de26 100644 --- a/src/pages/events/orders/index.tsx +++ b/src/pages/events/orders/index.tsx @@ -1,4 +1,4 @@ -import { defineComponent, onMounted } from 'vue'; +import { defineComponent } from 'vue'; import { Button, Input, @@ -143,10 +143,6 @@ export default defineComponent({ }, ]; - onMounted(() => { - handleSearch(); - }); - return () => (
{/* ===== 筛选区 ===== */} @@ -264,12 +260,12 @@ export default defineComponent({
- {/* 独立分页,右下方 */} + {/* 独立分页 */}
`共 ${total} 条`} onChange={handlePageChange} diff --git a/src/pages/events/orders/model/config.ts b/src/pages/events/orders/model/config.ts new file mode 100644 index 0000000..e87347c --- /dev/null +++ b/src/pages/events/orders/model/config.ts @@ -0,0 +1,57 @@ +/** + * 订单管理页配置 + * 职责:集中管理字段映射、选项配置等常量 + */ + +import type { OrderListQueryParams } from './services'; + +// ============================================================ +// 筛选字段默认值 +// ============================================================ +export const FILTER_DEFAULTS = { + orderDateRange: null as [string, string] | null, + orderNo: '', + tName: '', + sName: '', + status: '', + refundStatus: '', +}; + +// ============================================================ +// 字段映射配置 +// ============================================================ +export type FieldMapping = [ + keyof typeof FILTER_DEFAULTS, + keyof OrderListQueryParams, + ((v: any) => any)?, +]; + +export const FIELD_MAPPINGS: FieldMapping[] = [ + ['orderNo', 'orderNo', (v) => v?.trim?.() ?? v], + ['tName', 'tName', (v) => v?.trim?.() ?? v], + ['sName', 'sName', (v) => v?.trim?.() ?? v], + ['status', 'status'], + ['refundStatus', 'refundStatus'], + ['orderDateRange', 'orderDate', (v) => v?.[0]], + ['orderDateRange', 'orderDateEnd', (v) => v?.[1]], +]; + +// ============================================================ +// 下拉选项配置 +// ============================================================ + +/** 订单状态选项(value 对应 API status: 不传=全部、1=支付成功,2=部分退款,3=全部退款,4=超时关闭) */ +export const ORDER_STATUS_OPTIONS = [ + { value: '', label: '全部' }, + { value: '1', label: '支付成功' }, + { value: '2', label: '部分退款' }, + { value: '3', label: '全部退款' }, + { value: '4', label: '超时关闭' }, +] as const; + +/** 退款状态选项(value 对应 API refundStatus: 不传=全部、0=退款失败,1=退款成功) */ +export const REFUND_STATUS_OPTIONS = [ + { value: '', label: '全部' }, + { value: '1', label: '退款成功' }, + { value: '0', label: '退款失败' }, +] as const; diff --git a/src/pages/events/orders/model/services.ts b/src/pages/events/orders/model/services.ts index 80ac800..1f341b2 100644 --- a/src/pages/events/orders/model/services.ts +++ b/src/pages/events/orders/model/services.ts @@ -35,10 +35,10 @@ export type { // ============================================================ // URL 常量 // ============================================================ -const orderList = '/manager/order/page'; -const orderDetail = '/manager/order/info'; -const orderPlayerList = '/manager/order/info/page'; -const retryRefund = '/manager/order/retryRefund'; +const orderList = '/admin/manager/order/page'; +const orderDetail = '/admin/manager/order/info'; +const orderPlayerList = '/admin/manager/order/info/userPage'; +const retryRefund = '/admin/manager/order/retryRefund'; const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/src/pages/events/orders/model/useOrderModel.ts b/src/pages/events/orders/model/useOrderModel.ts index e3d7dc2..16da8d8 100644 --- a/src/pages/events/orders/model/useOrderModel.ts +++ b/src/pages/events/orders/model/useOrderModel.ts @@ -1,33 +1,24 @@ 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 { - getOrderList, - type TournamentAdminOrderPageVO, - type OrderListQueryParams, +import { useState, useDebounce } from '@/hooks'; +import { useRequest } from '@/hooks/useRequest'; +import { useEffect } from '@/hooks/useEffect'; +import { usePagination } from '@/hooks/usePagination'; +import type { + TournamentAdminOrderPageVO, + OrderListQueryParams, + PageData, + ApiResult, } from './services'; -import { MOCK_ORDER_SUMMARY } from '@/config/mock/orderList'; +import { getOrderList } from './services'; +import { + FILTER_DEFAULTS, + FIELD_MAPPINGS, + ORDER_STATUS_OPTIONS, + REFUND_STATUS_OPTIONS, +} from './config'; -// ============================================================ -// 常量 -// ============================================================ - -/** 订单状态选项(value 对应 API status: 不传=全部、1=支付成功,2=部分退款,3=全部退款,4=超时关闭) */ -export const ORDER_STATUS_OPTIONS = [ - { value: '', label: '全部' }, - { value: '1', label: '支付成功' }, - { value: '2', label: '部分退款' }, - { value: '3', label: '全部退款' }, - { value: '4', label: '超时关闭' }, -] as const; - -/** 退款状态选项(value 对应 API refundStatus: 不传=全部、0=退款失败,1=退款成功) */ -export const REFUND_STATUS_OPTIONS = [ - { value: '', label: '全部' }, - { value: '1', label: '退款成功' }, - { value: '0', label: '退款失败' }, -] as const; +export { ORDER_STATUS_OPTIONS, REFUND_STATUS_OPTIONS }; /** 订单状态 StatusTag 映射 */ const ORDER_STATUS_MAP: Record = { @@ -44,26 +35,15 @@ const REFUND_STATUS_MAP: Record 3: { label: '退款失败', tone: 'danger' }, }; -// ============================================================ -// Model -// ============================================================ - /** * 订单列表页数据模型 - * 筛选字段名与 API query 参数(OrderListQueryParams)保持一致。 + * 职责:状态管理 + 数据获取 + 业务逻辑 + 交互处理 */ export function useOrderModel() { - // ===== 筛选条件(key 名对齐 API 查询参数) ===== - const filterForm = reactive({ - orderDateRange: null as [string, string] | null, - orderNo: '', - tName: '', - sName: '', - status: '', - refundStatus: '', - }); + const filterForm = reactive({ ...FILTER_DEFAULTS }); + + const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 }); - // 可搜索字段防抖 const { debouncedValue: debouncedOrderNo } = useDebounce( toRef(filterForm, 'orderNo') as Ref, { delay: 300 }, @@ -77,19 +57,70 @@ export function useOrderModel() { { delay: 300 }, ); - // ===== 表格状态 ===== - const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState([]); - const [pagination, setPagination] = useState({ - current: 1, - pageSize: 10, - total: 0, + const buildQueryParams = (): OrderListQueryParams => { + const { page, pageSize } = pagination.params.value; + + const params = FIELD_MAPPINGS.reduce( + (acc, [formKey, paramKey, transform]) => { + const rawValue = filterForm[formKey]; + if (!hasValue(rawValue)) return acc; + + const value = safeTransform(rawValue, transform); + if (isEmptyValue(value)) return acc; + + return { ...acc, [paramKey]: value }; + }, + {} as Record, + ); + + return { page: String(page), limit: String(pageSize), ...params } as OrderListQueryParams; + }; + + // ===== 列表数据请求 ===== + const { + data, + loading, + run: fetchList, + } = useRequest>>( + () => getOrderList(buildQueryParams()), + { refreshDeps: [], formatResult: (res) => res }, + ); + + // 提取列表数据 + const listData = computed | undefined>(() => { + const res = data.value; + return res ? (res as any).data : undefined; }); - // ===== 金额汇总(TODO: 汇总应来自独立接口) ===== - const [summary, setSummary] = useState(MOCK_ORDER_SUMMARY); + const dataSource = computed(() => listData.value?.list || []); - // ===== 表格列配置(dataIndex 对齐 TournamentAdminOrderPageVO) ===== + // 同步分页总条数 + useEffect(() => { + const total = (data.value as any)?.data?.total; + if (total !== undefined) pagination.setTotal(total); + }, [data]); + + // ===== 金额汇总(TODO: 汇总应来自独立接口) ===== + const [summary] = useState({ + totalOrderAmount: 0, + totalPaidAmount: 0, + totalRefundAmount: 0, + }); + + // ===== 计算属性 ===== + const hasFilter = computed(() => { + const f = filterForm; + return ( + !!f.orderDateRange || + !!debouncedOrderNo.value?.trim() || + !!debouncedTName.value?.trim() || + !!debouncedSName.value?.trim() || + !!f.status || + !!f.refundStatus + ); + }); + + // ===== 表格列配置 ===== const formatMoney = (text: string) => { const n = parseFloat(text); return isNaN(n) ? '0.00元' : `${n.toFixed(2)}元`; @@ -97,12 +128,7 @@ export function useOrderModel() { const columns = [ { title: '订单编号', dataIndex: 'orderNo', key: 'orderNo', width: 200 }, - { - title: '赛事名称', - dataIndex: 'name', - key: 'name', - width: 180, - }, + { title: '赛事名称', dataIndex: 'name', key: 'name', width: 180 }, { title: '选手名称', dataIndex: 'realName', key: 'realName', width: 100 }, { title: '手机号', dataIndex: 'phone', key: 'phone', width: 130 }, { title: '比赛组别', dataIndex: 'categoryName', key: 'categoryName', width: 120 }, @@ -176,91 +202,25 @@ export function useOrderModel() { }, ]; - // ===== 构建 API 查询参数 ===== - const buildQueryParams = (): OrderListQueryParams => { - const params: OrderListQueryParams = { - page: String(pagination.value.current), - limit: String(pagination.value.pageSize), - }; + // ===== 列表操作 ===== + const handleSearch = () => fetchList(); - if (filterForm.orderDateRange) { - params.orderDate = filterForm.orderDateRange[0]; - params.orderDateEnd = filterForm.orderDateRange[1]; - } - if (filterForm.orderNo.trim()) { - params.orderNo = filterForm.orderNo.trim(); - } - if (filterForm.tName.trim()) { - params.tName = filterForm.tName.trim(); - } - if (filterForm.sName.trim()) { - params.sName = filterForm.sName.trim(); - } - if (filterForm.status) { - params.status = filterForm.status; - } - if (filterForm.refundStatus) { - params.refundStatus = filterForm.refundStatus; - } - - return params; + const handleReset = () => { + Object.assign(filterForm, FILTER_DEFAULTS); + pagination.reset(); + setTimeout(fetchList, 350); }; - // ===== 计算属性 ===== - const hasFilter = computed(() => { - return ( - filterForm.orderDateRange !== null || - debouncedOrderNo.value.trim() !== '' || - debouncedTName.value.trim() !== '' || - debouncedSName.value.trim() !== '' || - filterForm.status !== '' || - filterForm.refundStatus !== '' - ); - }); - - // ===== 方法 ===== - - /** 查询 */ - const handleSearch = useThrottleFn(async () => { - setLoading(true); - try { - const queryParams = buildQueryParams(); - console.log('订单列表查询参数:', queryParams); - const res = await getOrderList(queryParams); - - if (res.code == 200) { - setDataSource(res.data.list); - setPagination({ ...pagination.value, total: res.data.total }); - } else { - message.error(res.msg || '查询失败'); - } - } catch (error: any) { - console.error('订单列表查询失败:', error); - } finally { - setLoading(false); - } - }, 500); - - /** 重置 */ - const handleReset = useThrottleFn(() => { - filterForm.orderDateRange = null; - filterForm.orderNo = ''; - filterForm.tName = ''; - filterForm.sName = ''; - filterForm.status = ''; - filterForm.refundStatus = ''; - setPagination({ current: 1, pageSize: 10, total: 0 }); - setDataSource([]); - // 等待 debounce 生效后自动查询 - setTimeout(() => handleSearch(), 350); - }, 500); - const handlePageChange = (page: number, pageSize: number) => { - setPagination({ ...pagination.value, current: page, pageSize }); - handleSearch(); + pagination.setCurrent(page); + if (pageSize !== (pagination as any).pageSize.value) { + pagination.setPageSize(pageSize); + } + setTimeout(fetchList, 0); }; return { + // 数据 filterForm, loading, dataSource, @@ -268,8 +228,31 @@ export function useOrderModel() { summary, pagination, hasFilter, + // 方法 handleSearch, handleReset, handlePageChange, }; } + +// ===== 工具函数 ===== + +function hasValue(v: any): boolean { + if (v == null) return false; + if (Array.isArray(v)) return v.length > 0 && v.some((item) => item != null); + if (typeof v === 'string') return v.trim() !== ''; + return true; +} + +function isEmptyValue(v: any): boolean { + return v == null || (typeof v === 'string' && v.trim() === ''); +} + +function safeTransform(value: any, transform?: (v: any) => any): any { + if (!transform) return value; + try { + return transform(value) ?? value; + } catch { + return value; + } +} diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index 86049e8..176767a 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -5,10 +5,11 @@ import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'; import { auth } from '@/hooks/useAuth'; import { useEffect, useState } from '@/hooks'; import { useUserStore } from '@/stores/userStore'; +import { login as loginApi } from '@/api/login'; import styles from './index.module.less'; interface LoginForm { - username: string; + phone: string; password: string; } @@ -18,9 +19,10 @@ export default defineComponent({ const route = useRoute(); const [loading, setLoading] = useState(false); const [rememberMe, setRememberMe] = useState(false); + const { setUser } = useUserStore(); const form = reactive({ - username: '', + phone: '', password: '', }); @@ -29,7 +31,7 @@ export default defineComponent({ if (saved) { try { const data = JSON.parse(saved); - form.username = data.username || ''; + form.phone = data.phone || ''; form.password = data.password || ''; setRememberMe(true); } catch { @@ -44,7 +46,7 @@ export default defineComponent({ }; const handleSubmit = async () => { - if (!form.username || !form.password) { + if (!form.phone || !form.password) { message.warning('请输入用户名和密码'); return; } @@ -52,30 +54,26 @@ export default defineComponent({ setLoading(true); try { - await new Promise((r) => setTimeout(r, 600)); - const token = 'mock_token_' + Date.now(); - - // 写入账号信息到 userStore - const { setUser } = useUserStore(); - setUser({ phone: form.username, nickname: form.username, token }); - - const redirect = Array.isArray(route.query.redirect) - ? route.query.redirect[0] - : route.query.redirect; - await auth.login(token, redirect || undefined); - message.success('登录成功'); + const res = await loginApi({ phone: form.phone, password: form.password }); + setUser({ phone: form.phone, nickname: res.data.nickname, token: res.data.token }); if (rememberMe.value) { localStorage.setItem( 'cpms_login_remember', JSON.stringify({ - username: form.username, + phone: form.phone, password: form.password, }), ); } else { localStorage.removeItem('cpms_login_remember'); } + + const redirect = Array.isArray(route.query.redirect) + ? route.query.redirect[0] + : route.query.redirect; + await auth.login(res.data.token, redirect || undefined); + message.success('登录成功'); } catch (err) { message.error('登录失败,请重试'); } finally { @@ -211,9 +209,9 @@ export default defineComponent({ layout="vertical" onFinish={handleSubmit} > - + }} diff --git a/src/pages/system/users/model/services.ts b/src/pages/system/users/model/services.ts index eef5609..d850d85 100644 --- a/src/pages/system/users/model/services.ts +++ b/src/pages/system/users/model/services.ts @@ -3,8 +3,7 @@ * 该页面使用的所有接口集中管理 */ import { get, post } from '@/utils/request'; -import { USE_MOCK, MOCK_DELAY } from '@/config/mock'; -import { buildMockUserListPage } from '@/config/mock/userList'; +import { fetchPermissions } from '@/api/menu'; import type { UserListQueryParams, UserSaveParams, @@ -30,40 +29,66 @@ export type { // ============================================================ // 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 函数 // ============================================================ -/** 用户分页列表 */ +/** + * 用户列表 - 通过 fetchPermissions 接口获取用户权限列表 + * 接口: GET /admin/sys/role/permissionList + * 返回权限编码列表,前端做分页处理 + */ export async function getUserList( - params: UserListQueryParams, + _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); + // 调用 fetchPermissions 获取用户权限数据 + const list = await fetchPermissions(); + + // 将权限编码字符串转为类用户列表格式,支持分页 + const page = Number(_params.page) || 1; + const limit = Number(_params.limit) || 10; + const start = (page - 1) * limit; + const end = start + limit; + const pagedList = list.slice(start, end); + + // 将权限编码映射为用户 VO 格式(后端返回的权限码可能包含用户信息) + const userListData: TournamentAdminUserVO[] = pagedList.map((item, index) => { + // 如果 item 是对象则直接使用,如果是字符串则构造基本结构 + if (typeof item === 'object' && item !== null) { + return item as unknown as TournamentAdminUserVO; + } + return { + id: String(start + index + 1), + realName: String(item), + phone: '', + roleName: '', + status: '1', + createTime: '', + } as TournamentAdminUserVO; + }); + + return { + code: 200, + msg: 'success', + data: { + total: list.length, + list: userListData, + }, + }; } /** 新增用户 */ 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); } diff --git a/src/stores/menuStore.ts b/src/stores/menuStore.ts index 8431370..5c29a3f 100644 --- a/src/stores/menuStore.ts +++ b/src/stores/menuStore.ts @@ -245,9 +245,11 @@ function ensureLayoutRoute() { * 无论是后端数据还是兜底数据,都走同一套 logic */ function applyMenuTree(menuTree: MenuNode[]) { - state.menuTree = menuTree; - state.menuItems = transformMenuNode(menuTree); - state.homePath = getFirstMenuPath(menuTree); + // 防御:确保是数组,避免后端返回 null/undefined 导致 \"nodes is not iterable\" + const safeMenuTree = Array.isArray(menuTree) ? menuTree : []; + state.menuTree = safeMenuTree; + state.menuItems = transformMenuNode(safeMenuTree); + state.homePath = getFirstMenuPath(safeMenuTree); ensureLayoutRoute(); diff --git a/src/stores/userStore.ts b/src/stores/userStore.ts index e524f82..1fc5fc1 100644 --- a/src/stores/userStore.ts +++ b/src/stores/userStore.ts @@ -10,7 +10,7 @@ interface UserState { } const state = reactive({ - phone: '13377981125', + phone: '', nickname: '', token: '', });