diff --git a/src/api/banner/index.ts b/src/api/banner/index.ts new file mode 100644 index 0000000..ffbdef1 --- /dev/null +++ b/src/api/banner/index.ts @@ -0,0 +1,58 @@ +/** + * Banner 管理 API + */ +import { get, post } from '@/utils/request'; +import { USE_MOCK, MOCK_DELAY } from '@/config/mock'; +import { MOCK_BANNER_LIST } from '@/config/mock/bannerList'; +import type { + BannerListQueryParams, + BannerSaveParams, + BannerDelParams, + BannerActiveParams, + TournamentAdminBannerListVO, + ApiResult, +} from './types'; + +export type { + TournamentAdminBannerListVO, + BannerListQueryParams, + BannerSaveParams, + BannerDelParams, + BannerActiveParams, + ApiResult, +} from './types'; + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +/** GET /manager/banner/list — Banner 列表 */ +export async function getBannerList( + params: BannerListQueryParams, +): Promise> { + if (USE_MOCK) { + await delay(MOCK_DELAY); + return { code: 200, msg: 'success', data: MOCK_BANNER_LIST }; + } + return get('/manager/banner/list', params as Record); +} + +/** POST /manager/banner/del — 删除 Banner */ +export function delBanner(id: string): Promise>> { + return post('/manager/banner/del', { id }); +} + +/** POST /manager/banner/save — 新增 Banner */ +export function saveBanner(params: BannerSaveParams): Promise>> { + return post('/manager/banner/save', params); +} + +/** POST /manager/banner/update �� 编辑 Banner */ +export function updateBanner(params: BannerSaveParams): Promise>> { + return post('/manager/banner/update', params); +} + +/** POST /manager/banner/active — 启用/禁用 Banner */ +export function toggleBannerActive( + params: BannerActiveParams, +): Promise>> { + return post('/manager/banner/active', params); +} diff --git a/src/api/banner/types.ts b/src/api/banner/types.ts new file mode 100644 index 0000000..b9946de --- /dev/null +++ b/src/api/banner/types.ts @@ -0,0 +1,53 @@ +/** + * Banner 管理 — 类型定义 + */ + +/** Banner 列表项 */ +export interface TournamentAdminBannerListVO { + id: string; + img: string; + title: string; + type: string; + linkUrl: string; + area: string; + showTimeBegin: string; + showTimeEnd: string; + status: string; + createTime: string; +} + +/** Banner 列表查询参数 */ +export interface BannerListQueryParams { + title?: string; + status?: string; +} + +/** Banner 新增/编辑请求 */ +export interface BannerSaveParams { + id?: string; + img: string; + title: string; + type: string; + linkUrl: string; + area: string; + showTimeBegin: string; + showTimeEnd: string; + sort: string; +} + +/** Banner 删除请求 */ +export interface BannerDelParams { + id: string; +} + +/** Banner 启用/禁用请求 */ +export interface BannerActiveParams { + id: string; + status: string; +} + +export interface ApiResult { + code: number; + msg: string; + data: T; +} diff --git a/src/api/users/index.ts b/src/api/users/index.ts index eed37dc..d76ac08 100644 --- a/src/api/users/index.ts +++ b/src/api/users/index.ts @@ -6,48 +6,63 @@ import { USE_MOCK, MOCK_DELAY } from '@/config/mock'; import { buildMockUserListPage } from '@/config/mock/userList'; import type { UserListQueryParams, + UserSaveParams, + UserUpdateParams, UserActiveParams, - TournamentAdminUserPageVO, + UserUpdatePwdParams, + TournamentAdminUserVO, PageData, ApiResult, } from './types'; export type { - TournamentAdminUserPageVO, + TournamentAdminUserVO, UserListQueryParams, + UserSaveParams, + UserUpdateParams, UserActiveParams, + UserUpdatePwdParams, PageData, ApiResult, } from './types'; -const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); -/** - * 用户管理 - 用户分页 - * GET /admin/manager/user/page → /manager/user/page - */ +/** GET /sys/user/page — 用户分页 */ export async function getUserList( params: UserListQueryParams, -): Promise>> { +): 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 { code: 200, msg: 'success', data: buildMockUserListPage(page, limit) }; } - return get('/manager/user/page', params as Record); + return get('/sys/user/page', params as Record); } -/** - * 用户管理 - 启用/禁用 - * POST /admin/manager/user/active → /manager/user/active - */ +/** POST /sys/user/save — 新增用户 */ +export function saveUser(params: UserSaveParams): Promise>> { + if (USE_MOCK) return delay(MOCK_DELAY).then(() => ({ code: 200, msg: 'success', data: {} })); + return post('/sys/user/save', params); +} + +/** POST /sys/user/update — 更新用户 */ +export function updateUser(params: UserUpdateParams): Promise>> { + if (USE_MOCK) return delay(MOCK_DELAY).then(() => ({ code: 200, msg: 'success', data: {} })); + return post('/sys/user/update', params); +} + +/** POST /sys/user/active — 启用/禁用(直接调接口) */ export function toggleUserActive( params: UserActiveParams, ): Promise>> { - return post('/manager/user/active', params); + return post('/sys/user/active', params); +} + +/** POST /sys/user/updatepwd — 修改密码(直接调接口) */ +export function updateUserPwd( + params: UserUpdatePwdParams, +): Promise>> { + return post('/sys/user/updatepwd', params); } diff --git a/src/api/users/types.ts b/src/api/users/types.ts index c30fc2a..0dbcf4a 100644 --- a/src/api/users/types.ts +++ b/src/api/users/types.ts @@ -1,57 +1,69 @@ /** * 用户管理 — 类型定义 - * 对应 OpenAPI: /admin/manager/user/page + * 对应 OpenAPI: /admin/sys/user/* */ -/** 用户列表项 */ -export interface TournamentAdminUserPageVO { - /** 用户ID(接口实际返回 string) */ +export interface TournamentAdminUserVO { id: string; - /** 昵称 */ - nickname: string; - /** 手机号 */ - phone: string; - /** 真实姓名 */ + /** 用户姓名 */ realName: string; - /** 证件号 */ - idCard: string; - /** 证件号图片 */ - idCardImg: string; - /** 证件号图片列表 */ - idCardImgList: string[]; - /** 状态;0=禁用、1=正常(接口实际返回 string) */ + phone: string; + /** 角色名称 */ + roleName: string; + /** 角色ID(编辑时需要回显,列表可能不返回) */ + roleId?: string; + /** 状态;0=停用、1=正常(接口实际返回 string) */ status: string; - /** 创建时间 */ createTime: string; } -/** 用户列表查询参数 */ export interface UserListQueryParams { page?: string; limit?: string; - /** 用户昵称 */ - nickname?: string; - /** 用户手机号 */ - phone?: string; - /** 状态;不传=全部、0=禁用、1=正常 */ + /** 用户姓名或手机号 */ + text?: string; + /** 角色ID */ + roleId?: string; + /** 状态;不传=全部、0=停用、1=正常 */ status?: string; } -/** 启用/禁用请求参数 */ +/** 新增用户 — POST /admin/sys/user/save */ +export interface UserSaveParams { + realName: string; + phone: string; + roleId: string; + password: string; + confirmPassword: string; +} + +/** 更新用户 — POST /admin/sys/user/update */ +export interface UserUpdateParams { + id: string; + realName: string; + phone: string; + roleId: string; + password?: string; + confirmPassword?: string; +} + +/** 启用/禁用 — POST /admin/sys/user/active */ export interface UserActiveParams { - /** 用户ID */ userId: string; - /** 0=禁用、1=正常 */ + /** 0=禁用、1=启用 */ status: string; } -/** 分页数据 */ +/** 修改密码 — POST /admin/sys/user/updatepwd */ +export interface UserUpdatePwdParams { + password: string; + confirmPassword: string; +} + export interface PageData { total: number; list: T[]; } - -/** 统一响应包裹 */ export interface ApiResult { code: number; msg: string; diff --git a/src/config/mock/bannerList.ts b/src/config/mock/bannerList.ts new file mode 100644 index 0000000..ea7a532 --- /dev/null +++ b/src/config/mock/bannerList.ts @@ -0,0 +1,169 @@ +/** + * Banner 列表页假数据 + * + * 使用页面:`src/pages/events/banner/index.tsx` + * 对应接口:`GET /manager/banner/list` + * 类型:`TournamentAdminBannerListVO[]` + * + * 注意:类型内联定义,避免循环引用。 + */ + +export interface MockBannerVO { + id: string; + img: string; + title: string; + type: string; + linkUrl: string; + area: string; + showTimeBegin: string; + showTimeEnd: string; + status: string; + createTime: string; +} + +export const MOCK_BANNER_LIST: MockBannerVO[] = [ + { + id: '1', + img: 'https://picsum.photos/seed/banner1/80/44', + title: '2026全国青少年羽毛球锦标赛火热报名中', + type: '1', + linkUrl: '', + area: '广东省深圳市', + showTimeBegin: '2026-08-01 00:00', + showTimeEnd: '2026-08-31 23:59', + status: '1', + createTime: '2026-07-20 08:00:00', + }, + { + id: '2', + img: 'https://picsum.photos/seed/banner2/80/44', + title: '马拉松城市联赛限时优惠', + type: '2', + linkUrl: 'https://example.com/marathon', + area: '浙江省杭州市', + showTimeBegin: '2026-08-15 00:00', + showTimeEnd: '2026-09-15 23:59', + status: '1', + createTime: '2026-07-19 11:30:00', + }, + { + id: '3', + img: 'https://picsum.photos/seed/banner3/80/44', + title: '健身运动会专属福利', + type: '1', + linkUrl: '', + area: '北京市', + showTimeBegin: '2026-07-01 00:00', + showTimeEnd: '2026-07-30 23:59', + status: '1', + createTime: '2026-07-18 14:20:00', + }, + { + id: '4', + img: 'https://picsum.photos/seed/banner4/80/44', + title: '平台品牌宣传活动', + type: '2', + linkUrl: 'https://example.com/promo', + area: '上海市', + showTimeBegin: '2026-06-01 00:00', + showTimeEnd: '2026-06-30 23:59', + status: '0', + createTime: '2026-07-17 18:05:00', + }, + { + id: '5', + img: 'https://picsum.photos/seed/banner5/80/44', + title: '2026全国青少年羽毛球锦标赛火热报名中', + type: '1', + linkUrl: '', + area: '广东省深圳市', + showTimeBegin: '2026-09-01 00:00', + showTimeEnd: '2026-09-30 23:59', + status: '1', + createTime: '2026-07-16 21:15:00', + }, + { + id: '6', + img: 'https://picsum.photos/seed/banner6/80/44', + title: '马拉松城市联赛限时优惠', + type: '3', + linkUrl: '/pages/activity/detail', + area: '浙江省杭州市', + showTimeBegin: '2026-10-01 00:00', + showTimeEnd: '2026-10-31 23:59', + status: '1', + createTime: '2026-07-15 00:30:00', + }, + { + id: '7', + img: 'https://picsum.photos/seed/banner7/80/44', + title: '全民健身日特别活动', + type: '1', + linkUrl: '', + area: '北京市', + showTimeBegin: '2026-08-08 00:00', + showTimeEnd: '2026-08-08 23:59', + status: '1', + createTime: '2026-07-14 03:50:00', + }, + { + id: '8', + img: 'https://picsum.photos/seed/banner8/80/44', + title: '新春运动季大促销', + type: '2', + linkUrl: 'https://example.com/spring', + area: '上海市', + showTimeBegin: '2026-01-15 00:00', + showTimeEnd: '2026-02-15 23:59', + status: '0', + createTime: '2026-07-13 07:05:00', + }, + { + id: '9', + img: 'https://picsum.photos/seed/banner9/80/44', + title: '2026全国青少年羽毛球锦标赛火热报名中', + type: '1', + linkUrl: '', + area: '广东省深圳市', + showTimeBegin: '2026-08-01 00:00', + showTimeEnd: '2026-08-31 23:59', + status: '1', + createTime: '2026-07-12 10:25:00', + }, + { + id: '10', + img: 'https://picsum.photos/seed/banner10/80/44', + title: '平台品牌宣传活动', + type: '2', + linkUrl: 'https://example.com/brand', + area: '浙江省杭州市', + showTimeBegin: '2026-11-01 00:00', + showTimeEnd: '2026-11-30 23:59', + status: '1', + createTime: '2026-07-11 13:40:00', + }, + { + id: '11', + img: 'https://picsum.photos/seed/banner11/80/44', + title: '秋季健身挑战赛', + type: '1', + linkUrl: '', + area: '北京市', + showTimeBegin: '2026-09-01 00:00', + showTimeEnd: '2026-09-30 23:59', + status: '1', + createTime: '2026-07-10 17:00:00', + }, + { + id: '12', + img: 'https://picsum.photos/seed/banner12/80/44', + title: '双十一运动装备大促', + type: '2', + linkUrl: 'https://example.com/double11', + area: '上海市', + showTimeBegin: '2026-11-01 00:00', + showTimeEnd: '2026-11-11 23:59', + status: '1', + createTime: '2026-07-09 20:15:00', + }, +]; diff --git a/src/config/mock/operationLogs.ts b/src/config/mock/operationLogs.ts new file mode 100644 index 0000000..3a08e24 --- /dev/null +++ b/src/config/mock/operationLogs.ts @@ -0,0 +1,181 @@ +/** + * 操作日志页假数据 + * 类型内联定义,避免循环引用。 + */ + +interface MockLogVO { + opName: string; + source: string; + nickname: string; + phone: string; + tournamentName: string; + obj: string; + createDate: string; + /** 操作内容详情(mock 扩展字段,API 暂无) */ + content?: string; +} + +const MOCK_LOGS: MockLogVO[] = [ + { + opName: '删除赛事', + source: '2', + nickname: '张三', + phone: '12345678989', + tournamentName: '第二十一届风林杯羽毛球大赛', + obj: '第二十一届风林杯羽毛球大赛', + createDate: '2026-05-26 08:00:00', + content: '删除赛事', + }, + { + opName: '取消报名', + source: '1', + nickname: '', + phone: '', + tournamentName: '', + obj: '张三', + createDate: '2026-05-27 10:12:00', + content: '选手"张三"取消报名', + }, + { + opName: '编辑选手', + source: '1', + nickname: '', + phone: '', + tournamentName: '', + obj: '李四', + createDate: '2026-05-27 10:30:00', + content: '修改姓名,修改前:李四,修改后:张三', + }, + { + opName: '编辑选手', + source: '1', + nickname: '', + phone: '', + tournamentName: '', + obj: '李四', + createDate: '2026-05-27 10:35:00', + content: + '修改手机号,修改前:1234567899,修改后:13425678997;修改组别,修改前:男子单打,修改后:男子双打', + }, + { + opName: '批量生成对阵', + source: '1', + nickname: '', + phone: '', + tournamentName: '', + obj: '男子小组A,男子小组B,男子小组C', + createDate: '2026-05-27 11:00:00', + content: '生成对阵', + }, + { + opName: '生成对阵', + source: '1', + nickname: '', + phone: '', + tournamentName: '', + obj: '男子小组A', + createDate: '2026-05-27 11:05:00', + content: '生成对阵', + }, + { + opName: '批量删除对阵', + source: '1', + nickname: '', + phone: '', + tournamentName: '', + obj: '男子小组A,男子小组B,男子小组C', + createDate: '2026-05-27 11:10:00', + content: '删除对阵', + }, + { + opName: '删除对阵', + source: '1', + nickname: '', + phone: '', + tournamentName: '', + obj: '男子小组A', + createDate: '2026-05-27 11:15:00', + content: '删除对阵', + }, + { + opName: '修改比分', + source: '1', + nickname: '', + phone: '', + tournamentName: '', + obj: '男子小组A,张三李四,第一局', + createDate: '2026-05-27 12:00:00', + content: '修改前:比分"21:0",修改后:"0:21"', + }, + { + opName: '弃权', + source: '1', + nickname: '', + phone: '', + tournamentName: '', + obj: '男子淘汰1,第一轮,张三李四', + createDate: '2026-05-27 13:00:00', + content: '选手"张三"弃权', + }, + { + opName: '弃权', + source: '1', + nickname: '', + phone: '', + tournamentName: '', + obj: '男子小组A,张三李四', + createDate: '2026-05-27 13:10:00', + content: '双方弃权,选手"张三"、"李四"', + }, + { + opName: '修改对阵', + source: '1', + nickname: '', + phone: '', + tournamentName: '', + obj: '男子小组A,张三李四,1', + createDate: '2026-05-27 14:00:00', + content: '修改前:比分"21:10",修改后:比分"21:0"', + }, + { + opName: '修改对阵', + source: '1', + nickname: '', + phone: '', + tournamentName: '', + obj: '男子小组A,张三李四,双打名额,选手 场地名额', + createDate: '2026-05-27 14:30:00', + content: + '修改前:比分"21:0",对局状态已完成,对阵时间:08:50-09:00,场地号:羽1,修改后:比分"0:0",对局状态为未开始,对阵时间:08:50-09:00,场地号:羽2', + }, + { + opName: '修改场地', + source: '1', + nickname: '', + phone: '', + tournamentName: '', + obj: '羽1', + createDate: '2026-05-27 15:00:00', + content: '修改前:羽1,修改后:羽3', + }, + { + opName: '修改种子', + source: '1', + nickname: '', + phone: '', + tournamentName: '', + obj: '张某某,张三 场地名额', + createDate: '2026-05-27 15:30:00', + content: '修改前:1号种子,修改后:2号种子', + }, +]; + +interface MockPageData { + total: number; + list: T[]; +} + +export function buildMockOperationLogPage(page: number, limit: number): MockPageData { + const start = (page - 1) * limit; + return { total: MOCK_LOGS.length, list: MOCK_LOGS.slice(start, start + limit) }; +} diff --git a/src/config/mock/userList.ts b/src/config/mock/userList.ts index d51b3be..57fb63b 100644 --- a/src/config/mock/userList.ts +++ b/src/config/mock/userList.ts @@ -1,170 +1,128 @@ /** - * 用户列表页假数据 - * - * 使用页面:`src/pages/events/users/index.tsx` - * 对应接口:`GET /manager/user/page` - * 类型:`TournamentAdminUserPageVO[]` - * - * 注意:为避免循环引用(api/users/index.ts → 此文件 → api/users/types), - * 此文件不 import 任何 api 层类型,接口定义内联。 + * 用户列表页假数据(类型内联,避免循环引用) + * 对应接口:GET /sys/user/page */ interface MockUserVO { id: string; - nickname: string; - phone: string; realName: string; - idCard: string; - idCardImg: string; - idCardImgList: string[]; + phone: string; + roleId: string; + roleName: string; status: string; createTime: string; } - interface MockPageData { total: number; list: T[]; } -export const MOCK_USER_LIST: MockUserVO[] = [ +const MOCK_USERS: MockUserVO[] = [ { id: '1', - nickname: '羽毛球小将', - phone: '13800138001', realName: '张三', - idCard: '320101199001011234', - idCardImg: '', - idCardImgList: [ - 'https://picsum.photos/seed/uid1a/96/64', - 'https://picsum.photos/seed/uid1b/96/64', - ], + phone: '13800138001', + roleId: '1', + roleName: '超级管理员', status: '1', createTime: '2026-07-20 08:11:07', }, { id: '2', - nickname: '马拉松达人', - phone: '13900139002', realName: '李四', - idCard: '320101199002022345', - idCardImg: 'https://picsum.photos/seed/uid2/96/64', - idCardImgList: [], + phone: '13900139002', + roleId: '2', + roleName: '赛事管理员', status: '1', createTime: '2026-07-19 11:28:20', }, { id: '3', - nickname: '健身爱好者', - phone: '13700137003', realName: '王五', - idCard: '320101199103033456', - idCardImg: '', - idCardImgList: ['https://picsum.photos/seed/uid3a/96/64'], + phone: '13700137003', + roleId: '3', + roleName: '裁判', status: '1', createTime: '2026-07-18 14:45:33', }, { id: '4', - nickname: '游泳健将', - phone: '13600136004', realName: '赵六', - idCard: '320101199204044567', - idCardImg: 'https://picsum.photos/seed/uid4/96/64', - idCardImgList: [], + phone: '13600136004', + roleId: '2', + roleName: '赛事管理员', status: '0', createTime: '2026-07-17 18:02:46', }, { id: '5', - nickname: '羽毛球小将', - phone: '13800138005', realName: '孙七', - idCard: '320101199305055678', - idCardImg: '', - idCardImgList: [ - 'https://picsum.photos/seed/uid5a/96/64', - 'https://picsum.photos/seed/uid5b/96/64', - ], + phone: '13800138005', + roleId: '1', + roleName: '超级管理员', status: '1', createTime: '2026-07-16 21:19:59', }, { id: '6', - nickname: '马拉松达人', - phone: '13900139006', realName: '周八', - idCard: '320101199406066789', - idCardImg: 'https://picsum.photos/seed/uid6/96/64', - idCardImgList: [], + phone: '13900139006', + roleId: '3', + roleName: '裁判', status: '1', createTime: '2026-07-15 00:36:12', }, { id: '7', - nickname: '健身爱好者', - phone: '13700137007', realName: '吴九', - idCard: '320101199507077890', - idCardImg: '', - idCardImgList: [], + phone: '13700137007', + roleId: '2', + roleName: '赛事管理员', status: '1', createTime: '2026-07-14 03:53:25', }, { id: '8', - nickname: '游泳健将', - phone: '13600136008', realName: '郑十', - idCard: '320101199608088901', - idCardImg: 'https://picsum.photos/seed/uid8/96/64', - idCardImgList: ['https://picsum.photos/seed/uid8a/96/64'], + phone: '13600136008', + roleId: '4', + roleName: '财务', status: '1', createTime: '2026-07-13 07:10:38', }, { id: '9', - nickname: '羽毛球小将', - phone: '13800138009', realName: '陈一', - idCard: '320101199709099012', - idCardImg: '', - idCardImgList: [], + phone: '13800138009', + roleId: '3', + roleName: '裁判', status: '1', createTime: '2026-07-12 10:27:51', }, { id: '10', - nickname: '马拉松达人', - phone: '13900139010', realName: '刘二', - idCard: '320101199810100123', - idCardImg: 'https://picsum.photos/seed/uid10/96/64', - idCardImgList: ['https://picsum.photos/seed/uid10a/96/64'], + phone: '13900139010', + roleId: '2', + roleName: '赛事管理员', status: '0', createTime: '2026-07-11 13:44:04', }, { id: '11', - nickname: '健身爱好者', - phone: '13700137011', realName: '黄三', - idCard: '320101199911111234', - idCardImg: '', - idCardImgList: [], + phone: '13700137011', + roleId: '1', + roleName: '超级管理员', status: '1', createTime: '2026-07-10 17:01:17', }, { id: '12', - nickname: '游泳健将', - phone: '13600136012', realName: '林四', - idCard: '320101200012122345', - idCardImg: '', - idCardImgList: [ - 'https://picsum.photos/seed/uid12a/96/64', - 'https://picsum.photos/seed/uid12b/96/64', - ], + phone: '13600136012', + roleId: '4', + roleName: '财务', status: '1', createTime: '2026-07-09 20:18:30', }, @@ -172,6 +130,5 @@ export const MOCK_USER_LIST: MockUserVO[] = [ export function buildMockUserListPage(page: number, limit: number): MockPageData { const start = (page - 1) * limit; - const list = MOCK_USER_LIST.slice(start, start + limit); - return { total: MOCK_USER_LIST.length, list }; + return { total: MOCK_USERS.length, list: MOCK_USERS.slice(start, start + limit) }; } diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx index c3ef6b7..2f0693b 100644 --- a/src/layouts/BasicLayout.tsx +++ b/src/layouts/BasicLayout.tsx @@ -14,6 +14,7 @@ import { useUserStore } from '@/stores/userStore'; import { useTabsStore } from '@/stores/tabsStore'; import { useState, useBreadcrumb, auth } from '@/hooks'; import { ChangePasswordModal, PageTabs } from '@/components'; +import { updateUserPwd } from '@/api/users'; import { renderRouteIcon } from '@/utils/routeIcon'; import logoutSvg from '@/assets/img/icon/logout.svg'; import styles from './BasicLayout.module.less'; @@ -122,12 +123,18 @@ export default defineComponent({ if (changePwdSubmitting.value) return; setChangePwdSubmitting(true); try { - // TODO: 替换为真实 API 调用 - console.log('修改密码:', payload); - message.success('修改成功'); - setChangePwdVisible(false); + const res = await updateUserPwd({ + password: payload.newPassword, + confirmPassword: payload.newPassword, + }); + if (res.code === 200) { + message.success('密码修改成功'); + setChangePwdVisible(false); + } else { + message.error(res.msg || '修改失败'); + } } catch (err: any) { - message.error(err?.msg || '修改失败'); + console.error('修改密码失败:', err); } finally { setChangePwdSubmitting(false); } diff --git a/src/pages/events/banner/components/BannerFormModal.tsx b/src/pages/events/banner/components/BannerFormModal.tsx index 89f311e..05db4a1 100644 --- a/src/pages/events/banner/components/BannerFormModal.tsx +++ b/src/pages/events/banner/components/BannerFormModal.tsx @@ -6,28 +6,36 @@ import { Input, Select, DatePicker, + Cascader, InputNumber, Button, message, Spin, } from 'ant-design-vue'; -import { JUMP_TYPE_OPTIONS, EVENT_OPTIONS, REGION_OPTIONS } from '../model/useBannerModel'; +import { JUMP_TYPE_OPTIONS, EVENT_OPTIONS } from '../model/useBannerModel'; +import { + getProvinceCityCascaderOptions, + parseAreaToCascaderPath, + formatCascaderArea, +} from '@/utils/areaData'; import { ImageCropper } from '@yp-component/root'; import { FileImageOutlined } from '@ant-design/icons-vue'; import { uploadFile, OssUploadType } from '@/utils/oss'; import dayjs from 'dayjs'; import styles from './BannerFormModal.module.less'; +/** 省市两级 Cascader 选项 */ +const cascaderAreaOptions = getProvinceCityCascaderOptions(); + /** 默认表单数据 */ const getDefaultForm = () => ({ title: '', imageUrl: '', imageStatus: 'empty' as 'empty' | 'uploading' | 'success' | 'error', - jumpType: 'event_detail' as 'event_detail' | 'miniprogram_page' | 'custom_link' | 'none', - relatedEventId: '', - pagePath: '', - customUrl: '', - region: 'home', + type: '1' as '1' | '2' | '3', + linkUrl: '', + relatedEventId: undefined as string | undefined, + area: '', displayTimeRange: null as [string, string] | null, sort: 1, }); @@ -80,6 +88,10 @@ export default defineComponent({ const formRef = ref(); const fileInputRef = ref(null); const formData = reactive(getDefaultForm()); + /** Cascader 区域的路径数组(与 formData.area 字符串双向同步) */ + const areaPath = ref([]); + /** 是否由用户主动切换了跳转类型(跳过初始加载的 type 联动校验) */ + const typeChanged = ref(false); /** 裁剪弹窗状态 */ const [cropperVisible, setCropperVisible] = useState(false); @@ -91,38 +103,38 @@ export default defineComponent({ const fresh = getDefaultForm(); if (!record) { Object.assign(formData, fresh); + areaPath.value = []; return; } - let pagePath = ''; - let customUrl = ''; - if (record.jumpType === 'miniprogram_page') { - pagePath = record.jumpUrl || ''; - } else if (record.jumpType === 'custom_link') { - customUrl = record.jumpUrl || ''; - } - + const area = record.area || record.region || ''; + areaPath.value = parseAreaToCascaderPath(area); Object.assign(formData, { ...fresh, title: record.title || '', - imageUrl: record.imageUrl || '', - imageStatus: record.imageUrl ? ('success' as const) : ('empty' as const), - jumpType: record.jumpType || 'event_detail', - relatedEventId: record.relatedEventId || '', - pagePath, - customUrl, - region: record.region && record.region !== record.city ? record.region : 'home', + imageUrl: record.img || record.imageUrl || '', + imageStatus: record.img || record.imageUrl ? ('success' as const) : ('empty' as const), + type: String(record.type || record.jumpType || '1'), + linkUrl: record.linkUrl || record.jumpUrl || '', + relatedEventId: record.relatedEventId || undefined, + area, displayTimeRange: - record.displayStartTime && record.displayEndTime - ? ([record.displayStartTime, record.displayEndTime] as [string, string]) + (record.showTimeBegin || record.displayStartTime) && + (record.showTimeEnd || record.displayEndTime) + ? ([ + record.showTimeBegin || record.displayStartTime, + record.showTimeEnd || record.displayEndTime, + ] as [string, string]) : null, - sort: record.sort ?? 1, + sort: Number(record.sort ?? 1), }); }; /** 监听 visible 变化重置表单 */ useEffect(() => { if (props.visible) { + // 标记首次渲染,跳过 type 联动校验 + typeChanged.value = false; initFormFromRecord(props.record); setCropperVisible(false); setUploadImageUrl(''); @@ -130,18 +142,33 @@ export default defineComponent({ } }, [() => props.visible]); - /** 监听 jumpType 变化时清理联动字段 */ + /** 监听 type 变化时清理联动字段值和校验 */ useEffect(() => { - const newType = formData.jumpType; - if (newType !== 'event_detail') formData.relatedEventId = ''; - if (newType !== 'miniprogram_page') formData.pagePath = ''; - if (newType !== 'custom_link') formData.customUrl = ''; - const fieldsToValidate: string[] = []; - if (newType === 'event_detail') fieldsToValidate.push('relatedEventId'); - if (newType === 'miniprogram_page') fieldsToValidate.push('pagePath'); - if (newType === 'custom_link') fieldsToValidate.push('customUrl'); - if (fieldsToValidate.length) formRef.value?.clearValidate(fieldsToValidate); - }, [() => formData.jumpType]); + // 首次加载跳过校验 + if (!typeChanged.value) { + if (formData.type !== '1') formData.relatedEventId = undefined; + if (formData.type !== '2' && formData.type !== '3') formData.linkUrl = ''; + typeChanged.value = true; + return; + } + + if (formData.type !== '1') { + formData.relatedEventId = undefined; + formRef.value?.clearValidate('relatedEventId'); + } + if (formData.type !== '2' && formData.type !== '3') { + formData.linkUrl = ''; + } + const activeNames: string[] = []; + if (formData.type === '1') activeNames.push('relatedEventId'); + if (formData.type === '2' || formData.type === '3') activeNames.push('linkUrl'); + // 清除所有联动字段的校验状态 + formRef.value?.clearValidate(['relatedEventId', 'linkUrl']); + // 如果有已填值,重新校验当前字段 + if (activeNames.length) { + setTimeout(() => formRef.value?.validateFields(activeNames).catch(() => {}), 0); + } + }, [() => formData.type]); /** 触发隐藏的文件选择器 */ const triggerFilePicker = () => { @@ -235,25 +262,28 @@ export default defineComponent({ return; } - // 防御:上传中时禁止提交 if (formData.imageStatus === 'uploading') { message.warning('图片仍在上传中,请稍候'); return; } - // 构造提交数据:根据 jumpType 整理 jumpUrl - const payload: any = { ...formData }; - if (payload.jumpType === 'event_detail') { - payload.jumpUrl = payload.relatedEventId; - } else if (payload.jumpType === 'miniprogram_page') { - payload.jumpUrl = payload.pagePath; - } else if (payload.jumpType === 'custom_link') { - payload.jumpUrl = payload.customUrl; - } else { - payload.jumpUrl = ''; - } - payload.displayStartTime = payload.displayTimeRange?.[0] || ''; - payload.displayEndTime = payload.displayTimeRange?.[1] || ''; + const payload = { + img: formData.imageUrl || '', + title: formData.title || '', + type: formData.type, + linkUrl: + formData.type === '1' + ? formData.relatedEventId || '' + : formData.type === '2' + ? formData.linkUrl || '' + : formData.type === '3' + ? formData.linkUrl || '' + : '', + area: formData.area || formatCascaderArea(areaPath.value), + showTimeBegin: formData.displayTimeRange?.[0] || '', + showTimeEnd: formData.displayTimeRange?.[1] || '', + sort: String(formData.sort ?? 1), + }; props.onSave(payload); }; @@ -344,18 +374,18 @@ export default defineComponent({ {/* ===== 跳转类型 ===== */} )} - {formData.jumpType === 'miniprogram_page' && ( + {formData.type === '2' && ( - - - )} - - {formData.jumpType === 'custom_link' && ( - @@ -409,25 +413,44 @@ export default defineComponent({ placeholder="https://xxx" maxlength={URL_MAX} allowClear - v-model:value={formData.customUrl} + v-model:value={formData.linkUrl} /> )} - {formData.jumpType === 'none' && ( -
选择"无链接"时,点击 Banner 不会跳转。
+ {formData.type === '3' && ( + + + )} {/* ===== 所属区域 ===== */} - (filterForm.title = val.target?.value ?? val ?? '')} onPressEnter={handleSearch} /> @@ -223,7 +212,6 @@ export default defineComponent({ - {/* ===== 表格区 ===== */}
- {/* 独立分页,右下方 */}
- {/* ===== 新增/编辑弹窗 ===== */} {modalVisible.value && ( = { - event_detail: { label: '赛事详情页', tone: 'primary' }, - miniprogram_page: { label: '小程序页面', tone: 'cyan' }, - custom_link: { label: '自定义链接', tone: 'purple' }, - none: { label: '无链接', tone: 'default' }, + '1': { label: '赛事详情页', tone: 'primary' }, + '2': { label: '自定义链接', tone: 'purple' }, + '3': { label: '小程序页面', tone: 'cyan' }, }; /** Banner 状态映射 */ const STATUS_MAP: Record = { - enabled: { label: '启用中', tone: 'success' }, - disabled: { label: '已禁用', tone: 'default', dimmed: true }, + '1': { label: '启用中', tone: 'success' }, + '0': { label: '已禁用', tone: 'default', dimmed: true }, }; -/** 关联赛事选项(mock) */ +/** 关联赛事选项(mock,TODO: 替换为真实 API) */ export const EVENT_OPTIONS = [ - { value: '', label: '不关联' }, { value: 'EV20260701001', label: '2026全国青少年羽毛球锦标赛暨体育文化交流大会' }, { value: 'EV20260701002', label: '国际马拉松城市联赛' }, { value: 'EV20260701003', label: '全民健身运动会' }, @@ -47,131 +53,74 @@ export const EVENT_OPTIONS = [ { value: 'EV20260701005', label: '北京城市定向越野挑战赛' }, ]; -/** 所属区域选项(mock) */ -export const REGION_OPTIONS = [ - { value: 'home', label: '首页' }, - { value: 'guangdong_shenzhen', label: '广东省深圳市' }, - { value: 'zhejiang_hangzhou', label: '浙江省杭州市' }, - { value: 'beijing', label: '北京市' }, - { value: 'shanghai', label: '上海市' }, -]; - -// ============================================================ -// 假数据(表格) -// ============================================================ - -const MOCK_DATA = Array.from({ length: 12 }, (_, i) => { - const jumpType = (['event_detail', 'miniprogram_page', 'custom_link', 'none'] as const)[i % 4]; - const provinces = ['广东省', '浙江省', '北京市', '上海市']; - const cities = ['深圳市', '杭州市', '北京市', '上海市']; - - return { - key: `${i + 1}`, - id: `BANNER${String(i + 1).padStart(4, '0')}`, - sort: i + 1, - imageUrl: `https://picsum.photos/seed/banner${i + 1}/80/44`, - title: - i % 4 === 0 - ? '2026全国青少年羽毛球锦标赛火热报名中' - : i % 4 === 1 - ? '马拉松城市联赛限时优惠' - : i % 4 === 2 - ? '健身运动会专属福利' - : '平台品牌宣传活动', - jumpType, - jumpUrl: - jumpType === 'custom_link' - ? 'https://example.com/activity' - : jumpType === 'miniprogram_page' - ? '/pages/activity/detail' - : '', - relatedEventId: - jumpType === 'event_detail' - ? (['EV20260701001', 'EV20260701002', 'EV20260701003'] as const)[i % 3] - : '', - relatedEvent: - jumpType === 'event_detail' - ? (() => { - const map: Record = { - EV20260701001: '2026全国青少年羽毛球锦标赛暨体育文化交流大会', - EV20260701002: '国际马拉松城市联赛', - EV20260701003: '全民健身运动会', - }; - return map[(['EV20260701001', 'EV20260701002', 'EV20260701003'] as const)[i % 3]] || ''; - })() - : '', - region: provinces[i % 4], - city: cities[i % 4], - displayStartTime: '2026-08-01 00:00', - displayEndTime: '2026-08-31 23:59', - createTime: `2026-07-${String((i % 28) + 1).padStart(2, '0')} ${String((i * 2 + 8) % 24).padStart(2, '0')}:${String((i * 7) % 60).padStart(2, '0')}`, - status: (i % 4 === 0 ? 'disabled' : 'enabled') as 'enabled' | 'disabled', - }; -}); - -/** - * 临时排序:对假数据按创建时间倒序排列。 - * 【注意】实际项目中排序由后端接口负责,对接后端后请删除此函数及相关调用。 - */ -const sortByTimeDesc = (list: any[], field: string) => - [...list].sort((a, b) => dayjs(b[field]).valueOf() - dayjs(a[field]).valueOf()); - -const SORTED_MOCK_DATA = sortByTimeDesc(MOCK_DATA, 'createTime'); +/** 所属区域选项(动态生成,直辖市仅市名,其他省市) */ +export const REGION_OPTIONS = getProvinceCityOptions(); // ============================================================ // Model // ============================================================ -/** - * Banner 配置页数据模型 - */ export function useBannerModel() { - // ===== 筛选条件 ===== + // ===== 筛选条件(key 名对齐 API 查询参数) ===== const filterForm = reactive({ - searchTitle: '', - status: '' as '' | 'enabled' | 'disabled', + title: '', + status: '' as '' | '0' | '1', }); - // 防抖 300ms const { debouncedValue: debouncedTitle } = useDebounce( - toRef(filterForm, 'searchTitle') as Ref, + toRef(filterForm, 'title') as Ref, { delay: 300 }, ); - // ===== 表格状态 ===== + // ===== 表格状态(API 返回全量,前端分页) ===== const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState(SORTED_MOCK_DATA); + const [allData, setAllData] = useState([]); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, - total: SORTED_MOCK_DATA.length, + total: 0, + }); + + /** 当前页数据(客户端切片) */ + const dataSource = computed(() => { + const start = (pagination.value.current - 1) * pagination.value.pageSize; + return allData.value.slice(start, start + pagination.value.pageSize); }); // ===== 弹窗状态 ===== const [modalVisible, setModalVisible] = useState(false); const [editingRecord, setEditingRecord] = useState(null); - - // ===== 提交态(用于表单弹窗的确认按钮 loading)===== const [formSubmitting, setFormSubmitting] = useState(false); - // ===== 表格列配置(纯数据列,复杂渲染在页面 bodyCell 中处理)===== + // ===== 表格列配置(dataIndex 对齐 TournamentAdminBannerListVO) ===== const columns = [ - { title: '排序', dataIndex: 'sort', key: 'sort', width: 70, align: 'center' as const }, { title: '图片', key: 'image', width: 100, align: 'center' as const }, { title: '标题', dataIndex: 'title', key: 'title', width: 220 }, { title: '跳转类型', - dataIndex: 'jumpType', - key: 'jumpType', + dataIndex: 'type', + key: 'type', width: 120, }, - { title: '关联赛事', dataIndex: 'relatedEvent', key: 'relatedEvent', width: 200 }, - { title: '所属区域', key: 'region', width: 140 }, + { title: '跳转地址', dataIndex: 'linkUrl', key: 'linkUrl', width: 200 }, + { title: '所属区域', dataIndex: 'area', key: 'area', width: 140 }, { title: '展示时间', key: 'displayTime', width: 260 }, { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 160 }, { title: '状态', dataIndex: 'status', key: 'status', width: 100, align: 'center' as const }, ]; + // ===== 构建 API 查询参数 ===== + const buildQueryParams = (): BannerListQueryParams => { + const params: BannerListQueryParams = {}; + if (filterForm.title.trim()) { + params.title = filterForm.title.trim(); + } + if (filterForm.status) { + params.status = filterForm.status; + } + return params; + }; + // ===== 计算属性 ===== const hasFilter = computed(() => { return debouncedTitle.value.trim() !== '' || filterForm.status !== ''; @@ -181,18 +130,20 @@ export function useBannerModel() { // ===== 方法 ===== - /** 查询(节流 500ms) */ + /** 查询 */ const handleSearch = useThrottleFn(async () => { setLoading(true); try { - console.log('搜索条件:', { - title: debouncedTitle.value, - status: filterForm.status, - }); - // TODO: 替换为真实 API 调用 - setDataSource(SORTED_MOCK_DATA); - setPagination({ ...pagination.value, total: SORTED_MOCK_DATA.length }); - message.success('查询成功'); + const queryParams = buildQueryParams(); + console.log('Banner列表查询参数:', queryParams); + const res = await getBannerList(queryParams); + + if (res.code === 200) { + setAllData(res.data); + setPagination({ current: 1, pageSize: 10, total: res.data.length }); + } else { + message.error(res.msg || '查询失败'); + } } catch (error: any) { console.error('Banner查询失败:', error); } finally { @@ -200,17 +151,17 @@ export function useBannerModel() { } }, 500); - /** 重置(节流 500ms) */ + /** 重置 */ const handleReset = useThrottleFn(() => { - filterForm.searchTitle = ''; + filterForm.title = ''; filterForm.status = ''; - setPagination({ current: 1, pageSize: 10, total: SORTED_MOCK_DATA.length }); - setDataSource(SORTED_MOCK_DATA); + setPagination({ current: 1, pageSize: 10, total: 0 }); + setAllData([]); + setTimeout(() => handleSearch(), 350); }, 500); const handlePageChange = (page: number, pageSize: number) => { setPagination({ ...pagination.value, current: page, pageSize }); - handleSearch(); }; /** 打开新增弹窗 */ @@ -218,14 +169,10 @@ export function useBannerModel() { setEditingRecord(null); setModalVisible(true); }; - - /** 打开编辑弹窗 */ const handleEdit = (record: any) => { setEditingRecord(record); setModalVisible(true); }; - - /** 关闭弹窗 */ const handleCloseModal = () => { setModalVisible(false); setEditingRecord(null); @@ -236,61 +183,81 @@ export function useBannerModel() { if (formSubmitting.value) return; setFormSubmitting(true); try { - const payload = { - ...formPayload, - displayStartTime: formPayload.displayTimeRange?.[0] || '', - displayEndTime: formPayload.displayTimeRange?.[1] || '', + const params = { + img: formPayload.img || formPayload.imageUrl || '', + title: formPayload.title || '', + type: String(formPayload.type || formPayload.jumpType || '1'), + linkUrl: formPayload.linkUrl || formPayload.jumpUrl || '', + area: formPayload.area || formPayload.region || '', + showTimeBegin: + formPayload.showTimeBegin || + formPayload.displayTimeRange?.[0] || + formPayload.displayStartTime || + '', + showTimeEnd: + formPayload.showTimeEnd || + formPayload.displayTimeRange?.[1] || + formPayload.displayEndTime || + '', + sort: String(formPayload.sort ?? 1), }; - console.log(isEdit.value ? '编辑 Banner' : '新增 Banner', payload); - // TODO: 替换为真实 API 调用 - message.success(isEdit.value ? '编辑成功' : '新增成功'); - handleCloseModal(); - handleSearch(); - } catch (error: any) { - console.error('Banner提交失败:', error); + const isEdit = editingRecord.value !== null; + const res = isEdit + ? await updateBanner({ ...params, id: editingRecord.value.id }) + : await saveBanner(params); + if (res.code === 200) { + message.success(isEdit ? '编辑成功' : '新增成功'); + handleCloseModal(); + handleSearch(); + } else { + message.error(res.msg || '操作失败'); + } + } catch (e: any) { + console.error('Banner提交失败:', e); } finally { setFormSubmitting(false); } }, 500); - /** 删除(节流 500ms) */ + /** 删除 */ const handleDelete = useThrottleFn(async (record: any) => { setLoading(true); try { - console.log('删除 Banner:', record.id); - // TODO: 替换为真实 API 调用 - message.success('删除成功'); - handleSearch(); - } catch (error: any) { - console.error('Banner删除失败:', error); + const res = await delBanner(record.id); + if (res.code === 200) { + message.success('删除成功'); + handleSearch(); + } else { + message.error(res.msg || '删除失败'); + } + } catch (e: any) { + console.error('Banner删除失败:', e); } finally { setLoading(false); } }, 500); - /** 启用/禁用切换(节流 500ms) */ + /** 启用/禁用切换 */ const handleToggleStatus = useThrottleFn(async (record: any) => { setLoading(true); try { - const newStatus = record.status === 'enabled' ? 'disabled' : 'enabled'; - const actionText = newStatus === 'enabled' ? '启用' : '禁用'; - console.log(`${actionText} Banner:`, record.id); - // TODO: 替换为真实 API 调用 - // 更新本地数据 - setDataSource( - dataSource.value.map((item: any) => - item.key === record.key ? { ...item, status: newStatus } : item, - ), - ); - message.success(`${actionText}成功`); - } catch (error: any) { - console.error('Banner状态切换失败:', error); + const newStatus = record.status === '1' ? '0' : '1'; + const actionText = newStatus === '1' ? '启用' : '禁用'; + const res = await toggleBannerActive({ id: record.id, status: newStatus }); + if (res.code === 200) { + message.success(`${actionText}成功`); + handleSearch(); + } else { + message.error(res.msg || '操作失败'); + } + } catch (e: any) { + console.error('Banner状态切换失败:', e); } finally { setLoading(false); } }, 500); - // ===== 供页面 bodyCell 使用的渲染工具 ===== + // ===== 渲染工具 ===== const renderJumpType = (type: string) => { const info = JUMP_TYPE_MAP[type] || { label: type || '-', tone: 'default' as const }; return h(StatusTag, { label: info.label, tone: info.tone }); @@ -298,11 +265,7 @@ export function useBannerModel() { const renderStatus = (status: string) => { const info = STATUS_MAP[status] || { label: status || '-', tone: 'default' as const }; - return h(StatusTag, { - label: info.label, - tone: info.tone, - dimmed: info.dimmed, - }); + return h(StatusTag, { label: info.label, tone: info.tone, dimmed: info.dimmed }); }; return { diff --git a/src/pages/events/users/index.tsx b/src/pages/events/users/index.tsx index c5b9003..f090310 100644 --- a/src/pages/events/users/index.tsx +++ b/src/pages/events/users/index.tsx @@ -6,69 +6,15 @@ import { Form, Space, Select, - Image, Pagination, Modal, message, } from 'ant-design-vue'; -import { EyeOutlined } from '@ant-design/icons-vue'; -import { useUserModel, USER_STATUS_OPTIONS } from './model/useUserModel'; +import { useUserModel, USER_STATUS_OPTIONS, ROLE_OPTIONS } from './model/useUserModel'; import { toggleUserActive } from '@/api/users'; import { useContainerSize } from '@/hooks'; import pageStyles from '@/assets/styles/pageLayout.module.less'; -/** - * bodyCell 渲染函数 - */ -function renderBodyCell({ - column, - text, - record, - onToggleStatus, -}: { - column: any; - text: any; - record: any; - onToggleStatus: (record: any) => void; -}) { - // 证件图片:优先 idCardImgList,回退 idCardImg - if (column.dataIndex === 'idCardImg') { - const urls = record.idCardImgList?.length - ? record.idCardImgList - : record.idCardImg - ? [record.idCardImg] - : []; - if (urls.length === 0) return -; - return ( - - {urls.map((src: string) => ( - }} - /> - ))} - - ); - } - - if (column.key === 'action') { - const isActive = record.status === '1'; - return ( - - ); - } - - return; -} - -/** - * 用户列表 - */ export default defineComponent({ name: 'EventUsers', setup() { @@ -82,30 +28,23 @@ export default defineComponent({ handleReset, handlePageChange, } = useUserModel(); - const { containerRef, height } = useContainerSize(); const handleToggleStatus = (record: any) => { const isActive = record.status === '1'; - const actionText = isActive ? '禁用' : '启用'; - const targetStatus = isActive ? '0' : '1'; + const actionText = isActive ? '停用' : '启用'; Modal.confirm({ title: `${actionText}确认`, - content: `确认${actionText}用户"${record.nickname}"吗?`, + content: `确认${actionText}用户"${record.realName}"吗?`, okText: '确认', cancelText: '取消', onOk: async () => { try { - const res = await toggleUserActive({ - userId: record.id, - status: targetStatus, - }); + const res = await toggleUserActive({ userId: record.id, status: isActive ? '0' : '1' }); if (res.code === 200) { - message.success(`${actionText}成功`); + message.success(`${actionText}成���`); handleSearch(); - } else { - message.error(res.msg || '操作失败'); - } + } else message.error(res.msg || '操作失败'); } catch (e: any) { console.error(`用户${actionText}失败:`, e); } @@ -113,7 +52,6 @@ export default defineComponent({ }); }; - /** 最终表格列:模型列 + 操作 */ const tableColumns = [ ...columns, { @@ -131,29 +69,26 @@ export default defineComponent({ return () => (
- {/* ===== 筛选区 ===== */}
- + - (filterForm.nickname = val.target?.value ?? val ?? '') - } + onUpdate:value={(val: any) => (filterForm.text = val.target?.value ?? val ?? '')} onPressEnter={handleSearch} /> - - +
{{ - bodyCell: (args: any) => - renderBodyCell({ - ...args, - onToggleStatus: handleToggleStatus, - }), + bodyCell: (args: any) => { + if (args.column.key === 'action') { + const isActive = args.record.status === '1'; + return ( + + ); + } + }, }}
- - {/* 独立分页,右下方 */}
= { +/** 角色选项(TODO: 对接角色 API) */ +export const ROLE_OPTIONS = [ + { value: '', label: '全部' }, + { value: '1', label: '超级管理员' }, + { value: '2', label: '赛事管理员' }, + { value: '3', label: '裁判' }, + { value: '4', label: '财务' }, +] as const; + +const STATUS_MAP: Record = { '1': { label: '正常', tone: 'success' }, - '0': { label: '禁用', tone: 'danger' }, + '0': { label: '停用', tone: 'danger' }, }; // ============================================================ // Model // ============================================================ -/** - * 用户列表页数据模型 - */ export function useUserModel() { - // ===== 筛选条件(key 名对齐 API 查询参数) ===== const filterForm = reactive({ - nickname: '', - phone: '', + text: '', + roleId: '', status: '', }); - // 可搜索字段防抖 - const { debouncedValue: debouncedNickname } = useDebounce( - toRef(filterForm, 'nickname') as Ref, - { delay: 300 }, - ); - const { debouncedValue: debouncedPhone } = useDebounce( - toRef(filterForm, 'phone') as Ref, - { delay: 300 }, - ); - - // ===== 表格状态 ===== - const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState([]); - const [pagination, setPagination] = useState({ - current: 1, - pageSize: 10, - total: 0, + const { debouncedValue: debouncedText } = useDebounce(toRef(filterForm, 'text') as Ref, { + delay: 300, }); - // ===== 表格列配置(dataIndex 对齐 TournamentAdminUserPageVO) ===== + const [loading, setLoading] = useState(false); + const [dataSource, setDataSource] = useState([]); + const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }); + const columns = [ - { title: '昵称', dataIndex: 'nickname', key: 'nickname', width: 120 }, + { title: '用户姓名', dataIndex: 'realName', key: 'realName', width: 120 }, { title: '手机号', dataIndex: 'phone', key: 'phone', width: 140 }, - { title: '真实姓名', dataIndex: 'realName', key: 'realName', width: 100 }, - { title: '证件号', dataIndex: 'idCard', key: 'idCard', width: 180 }, - { - title: '证件图片', - dataIndex: 'idCardImg', - key: 'idCardImg', - width: 140, - }, + { title: '角色', dataIndex: 'roleName', key: 'roleName', width: 120 }, + { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 170 }, { title: '状态', dataIndex: 'status', @@ -74,69 +59,47 @@ export function useUserModel() { width: 90, align: 'center' as const, customRender: ({ text }: { text: string }) => { - const info = USER_STATUS_MAP[text] || { label: text, tone: 'default' as const }; + const info = STATUS_MAP[text] || { label: text, tone: 'default' as const }; return h(StatusTag, { label: info.label, tone: info.tone }); }, }, - { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 170 }, ]; - // ===== 构建 API 查询参数 ===== const buildQueryParams = (): UserListQueryParams => { const params: UserListQueryParams = { page: String(pagination.value.current), limit: String(pagination.value.pageSize), }; - - if (filterForm.nickname.trim()) { - params.nickname = filterForm.nickname.trim(); - } - if (filterForm.phone.trim()) { - params.phone = filterForm.phone.trim(); - } - if (filterForm.status) { - params.status = filterForm.status; - } - + if (filterForm.text.trim()) params.text = filterForm.text.trim(); + if (filterForm.roleId) params.roleId = filterForm.roleId; + if (filterForm.status) params.status = filterForm.status; return params; }; - // ===== 计算属性 ===== - const hasFilter = computed(() => { - return ( - debouncedNickname.value.trim() !== '' || - debouncedPhone.value.trim() !== '' || - filterForm.status !== '' - ); - }); + const hasFilter = computed( + () => debouncedText.value.trim() !== '' || filterForm.roleId !== '' || filterForm.status !== '', + ); - // ===== 方法 ===== - - /** 查询 */ const handleSearch = useThrottleFn(async () => { setLoading(true); try { - const queryParams = buildQueryParams(); - console.log('用户列表查询参数:', queryParams); - const res = await getUserList(queryParams); - + const params = buildQueryParams(); + console.log('用户列表查询参数:', params); + const res = await getUserList(params); 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); + } else message.error(res.msg || '查询失败'); + } catch (e: any) { + console.error('用户列表查询失败:', e); } finally { setLoading(false); } }, 500); - /** 重置 */ const handleReset = useThrottleFn(() => { - filterForm.nickname = ''; - filterForm.phone = ''; + filterForm.text = ''; + filterForm.roleId = ''; filterForm.status = ''; setPagination({ current: 1, pageSize: 10, total: 0 }); setDataSource([]); diff --git a/src/pages/system/users/components/UserFormModal.tsx b/src/pages/system/users/components/UserFormModal.tsx index e5f44dd..d282a68 100644 --- a/src/pages/system/users/components/UserFormModal.tsx +++ b/src/pages/system/users/components/UserFormModal.tsx @@ -26,7 +26,17 @@ const getDefaultForm = () => ({ phone: '', password: '', confirmPassword: '', - role: '', + roleId: undefined, +}); + +/** 新增用户默认密码 */ +const DEFAULT_USER_PASSWORD = 'A8888888'; + +/** 获取新增时的表单初始值(预填默认密码) */ +const getAddForm = () => ({ + ...getDefaultForm(), + password: DEFAULT_USER_PASSWORD, + confirmPassword: DEFAULT_USER_PASSWORD, }); export default defineComponent({ @@ -47,17 +57,18 @@ export default defineComponent({ /** 根据 record 初始化表单 */ const initFormFromRecord = (record: any) => { - const fresh = getDefaultForm(); if (!record) { - Object.assign(formData, fresh); + // 新增:预填默认密码 + 确认密码 + Object.assign(formData, getAddForm()); setIsResetting(false); return; } + const fresh = getDefaultForm(); Object.assign(formData, { ...fresh, realName: record.realName || '', phone: record.phone || '', - role: record.role || '', + roleId: record.roleId || undefined, }); setIsResetting(false); }; @@ -90,7 +101,7 @@ export default defineComponent({ const payload: any = { realName: formData.realName, phone: formData.phone, - role: formData.role, + roleId: formData.roleId, }; if (props.record) { if (isResetting.value) { @@ -159,11 +170,11 @@ export default defineComponent({ } /> - + diff --git a/src/pages/system/users/controller.ts b/src/pages/system/users/controller.ts index d87b4f6..c13ca64 100644 --- a/src/pages/system/users/controller.ts +++ b/src/pages/system/users/controller.ts @@ -14,11 +14,12 @@ import { isChinaPhone, isValidPassword, isValidRealName } from '@/utils/form'; /** 密码占位符(写死长度,避免暴露真实密码长度) */ export const PASSWORD_PLACEHOLDER = '********'; -/** 角色下拉选项(表单使用) */ +/** 角色下拉选项(表单使用,value 对应 API roleId) */ export const ROLE_FORM_OPTIONS = [ - { value: 'admin', label: '超级管理员' }, - { value: 'operator', label: '运营人员' }, - { value: 'auditor', label: '审核人员' }, + { value: '1', label: '超级管理员' }, + { value: '2', label: '赛事管理员' }, + { value: '3', label: '裁判' }, + { value: '4', label: '财务' }, ] as const; // ============================================================ diff --git a/src/pages/system/users/index.tsx b/src/pages/system/users/index.tsx index 3879f4a..222e35f 100644 --- a/src/pages/system/users/index.tsx +++ b/src/pages/system/users/index.tsx @@ -1,7 +1,18 @@ -import { defineComponent } from 'vue'; -import { Button, Input, Table, Form, Space, Select, Modal, Pagination } from 'ant-design-vue'; -import { useUserModel, USER_ROLE_OPTIONS, USER_STATUS_OPTIONS } from './model/useUserModel'; -import { useContainerSize } from '@/hooks'; +import { defineComponent, onMounted } from 'vue'; +import { + Button, + Input, + Table, + Form, + Space, + Select, + Modal, + Pagination, + message, +} from 'ant-design-vue'; +import { useUserModel, ROLE_OPTIONS, USER_STATUS_OPTIONS } from './model/useUserModel'; +import { toggleUserActive, saveUser, updateUser } from '@/api/users'; +import { useState, useThrottleFn, useContainerSize } from '@/hooks'; import UserFormModal from './components/UserFormModal'; import pageStyles from '@/assets/styles/pageLayout.module.less'; @@ -22,14 +33,14 @@ function renderBodyCell({ }) { // 操作列 if (column.key === 'action') { - const isActive = record.status === 'active'; + const isActive = record.status === '1'; return ( ); @@ -37,29 +48,6 @@ function renderBodyCell({ return; } -/** - * 禁用/启用 二次确认弹窗 - */ -function confirmToggleStatus({ record, onOk }: { record: any; onOk: (record: any) => void }) { - const isActive = record.status === 'active'; - const actionText = isActive ? '禁用' : '启用'; - Modal.confirm({ - title: `确认${actionText}`, - class: 'toggleConfirmModal', - content: ( -
- 确定要{actionText}用户 - {record.realName} - 吗? -
- ), - okText: '确认', - cancelText: '取消', - okButtonProps: { danger: isActive }, - onOk: () => onOk(record), - }); -} - /** * 系统管理 - 用户管理 */ @@ -72,33 +60,110 @@ export default defineComponent({ dataSource, columns, pagination, - modalVisible, - editingRecord, - isEdit, - formSubmitting, handleSearch, handleReset, handlePageChange, - handleAdd, - handleEdit, - handleCloseModal, - handleSave, - handleToggleStatus, } = useUserModel(); const { containerRef, height } = useContainerSize(); - /** 触发禁用/启用的二次确认 */ - const triggerToggle = (record: any) => { - confirmToggleStatus({ record, onOk: handleToggleStatus }); + // ===== 弹窗状态(组件级) ===== + const [modalVisible, setModalVisible] = useState(false); + const [editingRecord, setEditingRecord] = useState(null); + const [formSubmitting, setFormSubmitting] = useState(false); + + // ===== 启用/禁用(使用真实 API) ===== + const handleToggleStatus = useThrottleFn(async (record: any) => { + const isActive = record.status === '1'; + const actionText = isActive ? '停用' : '启用'; + Modal.confirm({ + title: `${actionText}确认`, + content: ( +
+ 确定要{actionText}用户 + {record.realName} + 吗? +
+ ), + okText: '确认', + cancelText: '取消', + okButtonProps: { danger: isActive }, + onOk: async () => { + try { + const res = await toggleUserActive({ + userId: record.id, + status: isActive ? '0' : '1', + }); + if (res.code === 200) { + message.success(`${actionText}成功`); + handleSearch(); + } else { + message.error(res.msg || '操作失败'); + } + } catch (e: any) { + console.error(`用户${actionText}失败:`, e); + } + }, + }); + }, 500); + + // ===== 新增/编辑(组件级) ===== + const handleAdd = () => { + setEditingRecord(null); + setModalVisible(true); }; - /** 触发编辑 */ - const triggerEdit = (record: any) => { - handleEdit(record); + const handleEdit = (record: any) => { + setEditingRecord(record); + setModalVisible(true); }; - /** 表格最终列:模型列 + 操作 */ + const handleCloseModal = () => { + setModalVisible(false); + setEditingRecord(null); + }; + + const handleSave = useThrottleFn(async (formPayload: any) => { + if (formSubmitting.value) return; + setFormSubmitting(true); + try { + let res: any; + if (editingRecord.value) { + // 编辑:调用更新接口 + res = await updateUser({ + id: editingRecord.value.id, + realName: formPayload.realName, + phone: formPayload.phone, + roleId: formPayload.roleId, + ...(formPayload.password + ? { password: formPayload.password, confirmPassword: formPayload.password } + : {}), + }); + } else { + // 新增:调用保存接口 + res = await saveUser({ + realName: formPayload.realName, + phone: formPayload.phone, + roleId: formPayload.roleId, + password: formPayload.password, + confirmPassword: formPayload.password, + }); + } + if (res.code === 200) { + message.success(editingRecord.value ? '编辑成功' : '新增成功'); + handleCloseModal(); + handleSearch(); + } else { + message.error(res.msg || '操作失败'); + } + } catch (error: any) { + console.error('保存用户失败:', error); + } finally { + setFormSubmitting(false); + } + }, 500); + + // ===== 表格最终列 ===== const tableColumns = [ ...columns, { @@ -110,26 +175,34 @@ export default defineComponent({ }, ]; + // ===== 初始化 ===== + onMounted(() => { + handleSearch(); + }); + return () => (
{/* ===== 筛选区 ===== */}
- + (filterForm.text = val?.target?.value ?? val ?? '')} onPressEnter={handleSearch} /> - +