feat: 接口联调

This commit is contained in:
2026-08-03 19:12:33 +08:00
parent 3e94ce60ec
commit 2497e6aabb
29 changed files with 941 additions and 1283 deletions
+6 -3
View File
@@ -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
+6 -2
View File
@@ -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
+60
View File
@@ -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"
}
}
}
}
-33
View File
@@ -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<void>((resolve) => setTimeout(resolve, ms));
/**
* 赛事列表 - 列表分页
* GET /admin/manager/tm/page → /manager/tm/page
@@ -40,16 +33,6 @@ const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve,
export async function getEventList(
params: EventListQueryParams,
): Promise<ApiResult<PageData<TournamentAdminVO>>> {
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<string, any>);
}
@@ -58,14 +41,6 @@ export async function getEventList(
* GET /admin/manager/tm/info → /manager/tm/info
*/
export async function getEventDetail(id: string): Promise<ApiResult<TournamentAdminInfoVO>> {
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<ApiResult<TournamentAd
export async function getSignupList(
params: SignupListQueryParams,
): Promise<ApiResult<PageData<TournamentAdminInfoSignupVO>>> {
if (USE_MOCK) {
await delay(MOCK_DELAY);
return {
code: 200,
msg: 'success',
data: buildMockSignupListPage(params),
};
}
return get('/manager/tm/info/signupPage', params as Record<string, any>);
}
+14
View File
@@ -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
// ============================================================
+5
View File
@@ -0,0 +1,5 @@
import { post } from '@/utils/request';
export const login = (params: { phone: string; password: string }) => {
return post(`/op/login`, params);
};
-11
View File
@@ -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<ApiResult<PageData<TournamentAdminOperationPageVO>>> {
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<string, any>);
}
+2 -2
View File
@@ -5,12 +5,12 @@ import type { MenuNode, PermissionCode } from '@/types';
* 获取当前用户的菜单树(后端下发)
*/
export function fetchMenuTree(): Promise<MenuNode[]> {
return get('/menu/tree');
return get('/op/permission');
}
/**
* 获取当前用户的权限编码列表
*/
export function fetchPermissions(): Promise<PermissionCode[]> {
return get('/permission/list');
return get('/admin/sys/role/permissionList');
}
+1 -1
View File
@@ -6,7 +6,7 @@
*
* 对接后端接口后将此值改为 false 即可切换。
*/
export const USE_MOCK = true;
export const USE_MOCK = false;
/** 模拟网络延时(毫秒) */
export const MOCK_DELAY = 850;
-39
View File
@@ -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(淘汰赛)'],
};
-268
View File
@@ -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<TournamentAdminVO> {
const start = (page - 1) * limit;
const list = MOCK_EVENT_LIST.slice(start, start + limit);
return { total: MOCK_EVENT_LIST.length, list };
}
-76
View File
@@ -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<T> {
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<MockTournamentAdminOperationPageVO> {
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) };
}
-100
View File
@@ -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<TournamentAdminInfoSignupVO> {
// 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 };
}
@@ -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 || '') : <span>-</span>,
},
];
@@ -82,17 +82,19 @@ export default defineComponent({
setup(props: EventDetailModalProps) {
// ===== 详情数据 =====
const [detail, setDetail] = useState<TournamentAdminInfoVO | null>(null);
const [detailLoading, setDetailLoading] = useState(true);
const [detailLoading, setDetailLoading] = useState(false);
// ===== 每个组别的选手数据(key = categoryId =====
const categoryPlayers = reactive<Record<string, TournamentAdminInfoSignupVO[]>>({});
const categoryPlayerTotal = reactive<Record<string, number>>({});
const categoryPlayerLoading = reactive<Record<string, boolean>>({});
const categoryPlayerPage = reactive<Record<string, number>>({});
const categoryPlayers = ref<Record<string, TournamentAdminInfoSignupVO[]>>({});
const categoryPlayerTotal = ref<Record<string, number>>({});
const categoryPlayerLoading = ref<Record<string, boolean>>({});
const categoryPlayerPage = ref<Record<string, number>>({});
/** 获取某个组别的选手列表(分页) */
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 () => (
<Modal
@@ -234,8 +242,8 @@ export default defineComponent({
</div>
<Table
columns={PLAYER_COLUMNS}
dataSource={categoryPlayers[cat.id] || []}
loading={categoryPlayerLoading[cat.id]}
dataSource={categoryPlayers.value[cat.id] || []}
loading={categoryPlayerLoading.value[cat.id]}
scroll={{ x: 'max-content', y: 200 }}
size="small"
pagination={false}
@@ -243,9 +251,9 @@ export default defineComponent({
/>
<div class={styles.paginationRow}>
<Pagination
current={categoryPlayerPage[cat.id] || 1}
current={categoryPlayerPage.value[cat.id] || 1}
pageSize={PAGE_SIZE}
total={categoryPlayerTotal[cat.id] || 0}
total={categoryPlayerTotal.value[cat.id] || 0}
showSizeChanger={false}
size="small"
onChange={(page: number) => handlePlayerPageChange(cat.id, page)}
@@ -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<string, any>;
onClose: () => void;
}
/** 规程内容 */
const REGULATION_TEXT = [
'一、赛事目的:推动羽毛球运动发展,提高运动员技术水平,增进体育文化交流。',
'二、参赛资格:凡身体健康、年龄符合要求的羽毛球爱好者均可报名参加。',
'三、比赛规则:采用中国羽毛球协会审定的最新《羽毛球竞赛规则》。',
'四、赛制安排:比赛分小组赛和淘汰赛两个阶段,小组赛采用单循环,淘汰赛采用单败淘汰。',
'五、器材要求:参赛运动员自备球拍,比赛用球由组委会统一提供。',
'六、裁判设置:每场比赛设主裁判1名、副裁判2名,确保比赛公平公正。',
].join('\n\n');
/** 规程图片(mock8 张示例图,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<TournamentAdminRuleInfoVO | null>(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 () => (
<Modal
visible={props.visible}
@@ -42,25 +60,48 @@ export default defineComponent({
destroyOnClose
wrapClassName={styles.eventRegulationModalMain}
>
{/* 规程内容 */}
<div class={styles.section}>
<div class={styles.sectionTitle}></div>
<div class={styles.textBlock}>{REGULATION_TEXT}</div>
</div>
{/* 规程图片:4 列 grid,点击可浏览 */}
<div class={styles.section}>
<div class={styles.sectionTitle}></div>
<Image.PreviewGroup>
<div class={styles.imageGrid}>
{REGULATION_IMAGES.map((img) => (
<div key={img.id} class={styles.imageCell}>
<Image src={img.src} />
<Spin spinning={loading.value}>
{ruleData.value ? (
<>
{/* 规程内容 */}
{ruleData.value.rulesDescr && (
<div class={styles.section}>
<div class={styles.sectionTitle}></div>
<div class={styles.textBlock}>{ruleData.value.rulesDescr}</div>
</div>
))}
</div>
</Image.PreviewGroup>
</div>
)}
{/* 规程图片 */}
{(ruleData.value.rulesDescrImgList?.length ?? 0) > 0 && (
<div class={styles.section}>
<div class={styles.sectionTitle}></div>
<Image.PreviewGroup>
<div class={styles.imageGrid}>
{ruleData.value.rulesDescrImgList!.map((src: string, idx: number) => (
<div key={idx} class={styles.imageCell}>
<Image src={src} />
</div>
))}
</div>
</Image.PreviewGroup>
</div>
)}
{/* 无数据提示 */}
{!ruleData.value.rulesDescr && !ruleData.value.rulesDescrImgList?.length && (
<div style={{ textAlign: 'center', color: 'rgba(0,0,0,0.45)', padding: '40px 0' }}>
</div>
)}
</>
) : (
!loading.value && (
<div style={{ textAlign: 'center', color: 'rgba(0,0,0,0.45)', padding: '40px 0' }}>
</div>
)
)}
</Spin>
</Modal>
);
},
+110 -170
View File
@@ -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 ? (
<Tooltip title={raw}>
<span>{display}</span>
</Tooltip>
) : (
<span>{display}</span>
);
}
if (column.key === 'regulation') {
return (
<a style={{ cursor: 'pointer' }} onClick={() => onViewRegulation(record)}>
</a>
);
}
if (column.key === 'action') {
const status = record.status;
return (
// 报名中(0) / 进行中(1):查看 + 下架
((status === '0' || status === '1') && (
<Space>
<Button type="link" size="small" onClick={() => onView(record)}>
</Button>
<Button type="link" size="small" onClick={() => onToggleShelf(record)}>
</Button>
</Space>
)) ||
// 已下架(4):查看 + 上架
(status === '4' && (
<Space>
<Button type="link" size="small" onClick={() => onView(record)}>
</Button>
<Button type="link" size="small" onClick={() => onToggleShelf(record)}>
</Button>
</Space>
)) || (
// 已结束(2) / 已删除(3):仅查看
<Button type="link" size="small" onClick={() => onView(record)}>
</Button>
)
);
}
return;
}
/**
* 赛事列表
* 赛事列表页面(纯视图层)
*
* 架构分层:
* - 数据层:services.tsAPI 调用)
* - 逻辑层: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<boolean>(false);
const [detailModalVisible, setDetailModalVisible] = useState<boolean>(false);
const [auditModalVisible, setAuditModalVisible] = useState<boolean>(false);
const [currentRecord, setCurrentRecord] = useState<any>({});
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 ? (
<Tooltip title={raw}>
<span>{display}</span>
</Tooltip>
) : (
<span>{display}</span>
);
};
/** 上架 / 下架 通用 */
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 (
<Space>
<Button type="link" size="small" onClick={() => openDetailModal(record)}>
</Button>
<Button
type="link"
size="small"
loading={togglingId.value === record.id}
onClick={() => handleToggleShelf(record)}
>
</Button>
</Space>
);
}
if (status === '4') {
return (
<Space>
<Button type="link" size="small" onClick={() => openDetailModal(record)}>
</Button>
<Button
type="link"
size="small"
loading={togglingId.value === record.id}
onClick={() => handleToggleShelf(record)}
>
</Button>
</Space>
);
}
return (
<Button type="link" size="small" onClick={() => openDetailModal(record)}>
</Button>
);
};
/** 打开审核弹窗 */
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 }) => (
<a style={{ cursor: 'pointer' }} onClick={() => openRegulationModal(record)}>
</a>
),
},
{
title: '操作',
key: 'action',
width: 160,
fixed: 'right' as const,
align: 'center' as const,
customRender: ({ record }: { record: any }) => renderAction(record),
},
];
return () => (
<div class={pageStyles.containerMain}>
{/* ===== 筛选区 ===== */}
<div class={pageStyles.filter}>
<Form style={{ rowGap: '10px' }} layout="inline" model={filterForm}>
<Form.Item label="创建时间" name="createDateRange">
@@ -308,7 +261,6 @@ export default defineComponent({
</Form>
</div>
{/* ===== 表格区 ===== */}
<div class={pageStyles.table}>
<div ref={containerRef} class={pageStyles.tableBody}>
<Table
@@ -317,26 +269,14 @@ export default defineComponent({
loading={loading.value}
scroll={{ x: 'max-content', y: height.value }}
pagination={false}
>
{{
bodyCell: (args: any) =>
renderBodyCell({
...args,
onView: handleView,
onViewRegulation: handleViewRegulation,
onAudit: handleAudit,
onToggleShelf: handleToggleShelf,
}),
}}
</Table>
/>
</div>
{/* 独立分页,右下方 */}
<div class={pageStyles.pagination}>
<Pagination
current={pagination.value.current}
pageSize={pagination.value.pageSize}
total={pagination.value.total}
current={(pagination as any).current.value}
pageSize={(pagination as any).pageSize.value}
total={(pagination as any).total.value}
showSizeChanger
showTotal={(total: number) => `${total}`}
onChange={handlePageChange}
@@ -344,26 +284,26 @@ export default defineComponent({
</div>
</div>
{/* ===== 弹窗区 ===== */}
{regulationModalVisible.value && (
<EventRegulationModal
visible={regulationModalVisible.value}
onClose={() => setRegulationModalVisible(false)}
record={currentRecord.value}
onClose={closeRegulationModal}
/>
)}
{detailModalVisible.value && (
<EventDetailModal
visible={detailModalVisible.value}
record={currentRecord.value}
onClose={() => setDetailModalVisible(false)}
onClose={closeDetailModal}
/>
)}
{auditModalVisible.value && (
<EventAuditModal
visible={auditModalVisible.value}
record={currentRecord.value}
onClose={() => setAuditModalVisible(false)}
onSubmit={handleAuditSubmit}
onClose={closeAuditModal}
onSubmit={submitAudit}
/>
)}
</div>
+69
View File
@@ -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;
+12 -43
View File
@@ -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<void>((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<ApiResult<PageData<TournamentAdminVO>>> {
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<string, any>);
}
/** 赛事列表 - 赛事详情 */
export async function getEventDetail(id: string): Promise<ApiResult<TournamentAdminInfoVO>> {
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<ApiResult<TournamentAdminRuleInfoVO>> {
return get(ruleInfo, { id });
}
/** 赛事列表 - 赛事详情 - 人员列表 */
export async function getSignupList(
params: SignupListQueryParams,
): Promise<ApiResult<PageData<TournamentAdminInfoSignupVO>>> {
if (USE_MOCK) {
await delay(MOCK_DELAY);
return {
code: 200,
msg: 'success',
data: buildMockSignupListPage(params),
};
}
return get(signupList, params as Record<string, any>);
}
@@ -0,0 +1,116 @@
import { h } from 'vue';
import { StatusTag, type StatusTagTone } from '@/components';
import type { TournamentAdminVO } from './services';
/** 状态标签映射 */
const STATUS_MAP: Record<string, { label: string; tone: StatusTagTone }> = {
'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<string, string>) => map[value] ?? value;
const ID_CARD_DISPLAY_MAP: Record<string, string> = { '0': '否', '1': '是' };
const IS_PUBLIC_DISPLAY_MAP: Record<string, string> = { '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 };
}
+174 -260
View File
@@ -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<string, { label: string; tone: StatusTagTone }> = {
'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<string, string>) => map[value] ?? value;
const ID_CARD_DISPLAY_MAP: Record<string, string> = { '0': '否', '1': '是' };
const IS_PUBLIC_DISPLAY_MAP: Record<string, string> = { '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<any>({});
const [togglingId, setTogglingId] = useState('');
const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
// 可搜索字段防抖
const { debouncedValue: debouncedName } = useDebounce(toRef(filterForm, 'name') as Ref<string>, {
delay: 300,
});
@@ -87,215 +39,177 @@ export function useEventListModel() {
{ delay: 300 },
);
// ===== 表格状态 =====
const [loading, setLoading] = useState<boolean>(false);
const [dataSource, setDataSource] = useState<TournamentAdminVO[]>([]);
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<string, any>,
);
return { page: String(page), limit: String(pageSize), ...params } as EventListQueryParams;
};
// ===== 计算属性 =====
const {
data,
loading,
run: fetchList,
} = useRequest<ApiResult<PageData<TournamentAdminVO>>>(() => getEventList(buildQueryParams()), {
refreshDeps: [],
formatResult: (res) => res,
});
// 提取列表数据
const listData = computed<PageData<TournamentAdminVO> | 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;
}
}
-19
View File
@@ -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<void>((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<ApiResult<PageData<TournamentAdminOperationPageVO>>> {
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<string, any>);
}
+5 -9
View File
@@ -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 () => (
<div class={pageStyles.containerMain}>
{/* ===== 筛选区 ===== */}
@@ -264,12 +260,12 @@ export default defineComponent({
</Table>
</div>
{/* 独立分页,右下方 */}
{/* 独立分页 */}
<div class={pageStyles.pagination}>
<Pagination
current={pagination.value.current}
pageSize={pagination.value.pageSize}
total={pagination.value.total}
current={(pagination as any).current.value}
pageSize={(pagination as any).pageSize.value}
total={(pagination as any).total.value}
showSizeChanger
showTotal={(total: number) => `${total}`}
onChange={handlePageChange}
+57
View File
@@ -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;
+4 -4
View File
@@ -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<void>((resolve) => setTimeout(resolve, ms));
+118 -135
View File
@@ -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<number, { label: string; tone: StatusTagTone }> = {
@@ -44,26 +35,15 @@ const REFUND_STATUS_MAP: Record<number, { label: string; tone: StatusTagTone }>
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<string>,
{ delay: 300 },
@@ -77,19 +57,70 @@ export function useOrderModel() {
{ delay: 300 },
);
// ===== 表格状态 =====
const [loading, setLoading] = useState<boolean>(false);
const [dataSource, setDataSource] = useState<TournamentAdminOrderPageVO[]>([]);
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<string, any>,
);
return { page: String(page), limit: String(pageSize), ...params } as OrderListQueryParams;
};
// ===== 列表数据请求 =====
const {
data,
loading,
run: fetchList,
} = useRequest<ApiResult<PageData<TournamentAdminOrderPageVO>>>(
() => getOrderList(buildQueryParams()),
{ refreshDeps: [], formatResult: (res) => res },
);
// 提取列表数据
const listData = computed<PageData<TournamentAdminOrderPageVO> | 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;
}
}
+17 -19
View File
@@ -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<LoginForm>({
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}
>
<FormItem name="username">
<FormItem name="phone">
<Input
v-model={[form.username, 'value']}
v-model={[form.phone, 'value']}
placeholder="请输入用户名"
size="large"
v-slots={{ prefix: () => <UserOutlined /> }}
+41 -16
View File
@@ -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<void>((r) => setTimeout(r, ms));
// ============================================================
// API 函数
// ============================================================
/** 用户分页列表 */
/**
* 用户列表 - 通过 fetchPermissions 接口获取用户权限列表
* 接口: GET /admin/sys/role/permissionList
* 返回权限编码列表,前端做分页处理
*/
export async function getUserList(
params: UserListQueryParams,
_params: UserListQueryParams,
): Promise<ApiResult<PageData<TournamentAdminUserVO>>> {
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<string, any>);
// 调用 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<ApiResult<Record<string, never>>> {
if (USE_MOCK) return delay(MOCK_DELAY).then(() => ({ code: 200, msg: 'success', data: {} }));
return post(userSave, params);
}
/** 更新用户 */
export function updateUser(params: UserUpdateParams): Promise<ApiResult<Record<string, never>>> {
if (USE_MOCK) return delay(MOCK_DELAY).then(() => ({ code: 200, msg: 'success', data: {} }));
return post(userUpdate, params);
}
+5 -3
View File
@@ -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();
+1 -1
View File
@@ -10,7 +10,7 @@ interface UserState {
}
const state = reactive<UserState>({
phone: '13377981125',
phone: '',
nickname: '',
token: '',
});