From 594324c4594f5f3f4f9ab1d20b12e0a3bd874d14 Mon Sep 17 00:00:00 2001 From: ZhuRui Date: Wed, 29 Jul 2026 10:15:23 +0800 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=E9=83=A8=E5=88=86=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F=E4=BF=AE=E6=94=B9=20=E6=B7=BB=E5=8A=A0=E8=B5=9B?= =?UTF-8?q?=E4=BA=8B=E8=A7=84=E7=A8=8B=E6=9F=A5=E7=9C=8B=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/events/banner/index.tsx | 7 +------ .../components/EventRegulationModal.module.less | 2 +- .../list/components/EventRegulationModal.tsx | 8 ++++---- src/pages/events/list/index.tsx | 14 ++++++-------- src/pages/events/users/index.tsx | 2 +- src/pages/system/users/index.tsx | 8 +------- 6 files changed, 14 insertions(+), 27 deletions(-) diff --git a/src/pages/events/banner/index.tsx b/src/pages/events/banner/index.tsx index 71865f0..a44beca 100644 --- a/src/pages/events/banner/index.tsx +++ b/src/pages/events/banner/index.tsx @@ -107,12 +107,7 @@ function renderBodyCell({ - - @@ -100,12 +102,7 @@ function renderBodyCell({ - @@ -313,6 +310,7 @@ export default defineComponent({ renderBodyCell({ ...args, onView: handleView, + onViewRegulation: handleViewRegulation, onAudit: handleAudit, onToggleShelf: handleToggleShelf, }), diff --git a/src/pages/events/users/index.tsx b/src/pages/events/users/index.tsx index 3823c06..dcfe083 100644 --- a/src/pages/events/users/index.tsx +++ b/src/pages/events/users/index.tsx @@ -46,7 +46,7 @@ function renderBodyCell({ if (column.key === 'action') { const isActive = record.status === 'active'; return ( - ); diff --git a/src/pages/system/users/index.tsx b/src/pages/system/users/index.tsx index 5150aaf..3879f4a 100644 --- a/src/pages/system/users/index.tsx +++ b/src/pages/system/users/index.tsx @@ -28,13 +28,7 @@ function renderBodyCell({ - -- 2.52.0 From 04ff2fb4df08fe902f0cd37738842daa68c90e86 Mon Sep 17 00:00:00 2001 From: ZhuRui Date: Wed, 29 Jul 2026 10:39:46 +0800 Subject: [PATCH 2/9] =?UTF-8?q?fix:=20=E4=BD=93=E9=AA=8C=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../events/banner/model/useBannerModel.ts | 22 +++++++++++----- .../events/list/model/useEventListModel.ts | 24 ++++++++++++----- .../events/orders/model/useOrderModel.ts | 26 +++++++++++++------ src/pages/events/users/model/useUserModel.ts | 24 ++++++++++++----- src/pages/system/roles/model/useRoleModel.ts | 22 +++++++++++----- src/pages/system/users/model/useUserModel.ts | 22 +++++++++++----- 6 files changed, 100 insertions(+), 40 deletions(-) diff --git a/src/pages/events/banner/model/useBannerModel.ts b/src/pages/events/banner/model/useBannerModel.ts index 9ee00f5..258cb8b 100644 --- a/src/pages/events/banner/model/useBannerModel.ts +++ b/src/pages/events/banner/model/useBannerModel.ts @@ -1,5 +1,6 @@ import { computed, reactive, toRef, Ref, h } from 'vue'; import { message } from 'ant-design-vue'; +import dayjs from 'dayjs'; import { StatusTag, type StatusTagTone } from '@/components'; import { useState, useDebounce, useThrottleFn } from '@/hooks'; @@ -108,6 +109,15 @@ const MOCK_DATA = Array.from({ length: 12 }, (_, i) => { }; }); +/** + * 临时排序:对假数据按创建时间倒序排列。 + * 【注意】实际项目中排序由后端接口负责,对接后端后请删除此函数及相关调用。 + */ +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'); + // ============================================================ // Model // ============================================================ @@ -130,11 +140,11 @@ export function useBannerModel() { // ===== 表格状态 ===== const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState(MOCK_DATA); + const [dataSource, setDataSource] = useState(SORTED_MOCK_DATA); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, - total: MOCK_DATA.length, + total: SORTED_MOCK_DATA.length, }); // ===== 弹窗状态 ===== @@ -180,8 +190,8 @@ export function useBannerModel() { status: filterForm.status, }); // TODO: 替换为真实 API 调用 - setDataSource(MOCK_DATA); - setPagination({ ...pagination.value, total: MOCK_DATA.length }); + setDataSource(SORTED_MOCK_DATA); + setPagination({ ...pagination.value, total: SORTED_MOCK_DATA.length }); message.success('查询成功'); } catch (error: any) { message.error(error.msg || '查询失败'); @@ -194,8 +204,8 @@ export function useBannerModel() { const handleReset = useThrottleFn(() => { filterForm.searchTitle = ''; filterForm.status = ''; - setPagination({ current: 1, pageSize: 10, total: MOCK_DATA.length }); - setDataSource(MOCK_DATA); + setPagination({ current: 1, pageSize: 10, total: SORTED_MOCK_DATA.length }); + setDataSource(SORTED_MOCK_DATA); }, 500); const handlePageChange = (page: number, pageSize: number) => { diff --git a/src/pages/events/list/model/useEventListModel.ts b/src/pages/events/list/model/useEventListModel.ts index c58784c..53b8f5e 100644 --- a/src/pages/events/list/model/useEventListModel.ts +++ b/src/pages/events/list/model/useEventListModel.ts @@ -1,5 +1,6 @@ import { computed, reactive, toRef, Ref, h } from 'vue'; import { message } from 'ant-design-vue'; +import dayjs from 'dayjs'; import { StatusTag, type StatusTagTone } from '@/components'; import { useState, useDebounce, useThrottleFn } from '@/hooks'; @@ -61,12 +62,21 @@ const MOCK_DATA = Array.from({ length: 12 }, (_, i) => ({ needIdCardImage: i % 2 === 0 ? '是' : '否', creatorNickname: ['张运营', '李管理', '王策划', '赵赛事'][i % 4], creatorPhone: ['13800138001', '13900139002', '13700137003', '13600136004'][i % 4], - createTime: '2026-07-01 14:30:00', + createTime: `2026-07-${String((i % 10) + 1).padStart(2, '0')} ${String((i * 3 + 8) % 24).padStart(2, '0')}:${String((i * 17 + 11) % 60).padStart(2, '0')}:${String((i * 13 + 7) % 60).padStart(2, '0')}`, status: (['pending', 'enrolling', 'deleted', 'removed', 'rejected'] as const)[i % 5], enrolledCount: [128, 256, 512, 64, 0][i % 5], cancelledCount: [5, 12, 8, 3, 0][i % 5], })); +/** + * 临时排序:对假数据按创建时间倒序排列。 + * 【注意】实际项目中排序由后端接口负责,对接后端后请删除此函数及相关调用。 + */ +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'); + /** 状态标签映射 */ const STATUS_MAP: Record = { pending: { label: '待审核', tone: 'warning' }, @@ -109,11 +119,11 @@ export function useEventListModel() { // ===== 表格状态 ===== const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState(MOCK_DATA); + const [dataSource, setDataSource] = useState(SORTED_MOCK_DATA); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, - total: MOCK_DATA.length, + total: SORTED_MOCK_DATA.length, }); // ===== 表格列配置 ===== @@ -213,8 +223,8 @@ export function useEventListModel() { needIdCardImage: filterForm.needIdCardImage, }); // TODO: 替换为真实 API 调用 - setDataSource(MOCK_DATA); - setPagination({ ...pagination.value, total: MOCK_DATA.length }); + setDataSource(SORTED_MOCK_DATA); + setPagination({ ...pagination.value, total: SORTED_MOCK_DATA.length }); message.success('查询成功'); } catch (error: any) { message.error(error.msg || '查询失败'); @@ -234,8 +244,8 @@ export function useEventListModel() { filterForm.publicEvent = ''; filterForm.needIdCard = ''; filterForm.needIdCardImage = ''; - setPagination({ current: 1, pageSize: 10, total: MOCK_DATA.length }); - setDataSource(MOCK_DATA); + setPagination({ current: 1, pageSize: 10, total: SORTED_MOCK_DATA.length }); + setDataSource(SORTED_MOCK_DATA); }, 500); const handlePageChange = (page: number, pageSize: number) => { diff --git a/src/pages/events/orders/model/useOrderModel.ts b/src/pages/events/orders/model/useOrderModel.ts index 8385bac..a2298ec 100644 --- a/src/pages/events/orders/model/useOrderModel.ts +++ b/src/pages/events/orders/model/useOrderModel.ts @@ -1,5 +1,6 @@ import { computed, reactive, toRef, Ref, h } from 'vue'; import { message } from 'ant-design-vue'; +import dayjs from 'dayjs'; import { StatusTag, type StatusTagTone } from '@/components'; import { useState, useDebounce, useThrottleFn } from '@/hooks'; @@ -131,8 +132,8 @@ const MOCK_DATA = Array.from({ length: 12 }, (_, i) => { registrant: ['张报名', '李报名', '王报名', '赵报名'][i % 4], registrantPhone: ['13800138001', '13900139002', '13700137003', '13600136004'][i % 4], registrantCount: [1, 2, 1, 3][i % 4], - orderTime: '2026-07-15 10:30:00', - payTime: '2026-07-15 10:32:15', + orderTime: `2026-07-${String((i % 12) + 1).padStart(2, '0')} ${String((i * 2 + 8) % 24).padStart(2, '0')}:${String((i * 13 + 7) % 60).padStart(2, '0')}:${String((i * 11 + 3) % 60).padStart(2, '0')}`, + payTime: `2026-07-${String((i % 12) + 1).padStart(2, '0')} ${String((i * 2 + 8) % 24).padStart(2, '0')}:${String((i * 13 + 9) % 60).padStart(2, '0')}:${String((i * 11 + 5) % 60).padStart(2, '0')}`, totalAmount, discountAmount, paidAmount: totalAmount - discountAmount, @@ -144,6 +145,15 @@ const MOCK_DATA = Array.from({ length: 12 }, (_, i) => { }; }); +/** + * 临时排序:对假数据按订单时间倒序排列。 + * 【注意】实际项目中排序由后端接口负责,对接后端后请删除此函数及相关调用。 + */ +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, 'orderTime'); + // ============================================================ // 金额汇总假数据 // ============================================================ @@ -188,11 +198,11 @@ export function useOrderModel() { // ===== 表格状态 ===== const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState(MOCK_DATA); + const [dataSource, setDataSource] = useState(SORTED_MOCK_DATA); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, - total: MOCK_DATA.length, + total: SORTED_MOCK_DATA.length, }); // ===== 金额汇总 ===== @@ -300,8 +310,8 @@ export function useOrderModel() { refundStatus: filterForm.refundStatus, }); // TODO: 替换为真实 API 调用 - setDataSource(MOCK_DATA); - setPagination({ ...pagination.value, total: MOCK_DATA.length }); + setDataSource(SORTED_MOCK_DATA); + setPagination({ ...pagination.value, total: SORTED_MOCK_DATA.length }); setSummary(MOCK_SUMMARY); message.success('查询成功'); } catch (error: any) { @@ -319,8 +329,8 @@ export function useOrderModel() { filterForm.searchPlayerName = ''; filterForm.orderStatus = ''; filterForm.refundStatus = ''; - setPagination({ current: 1, pageSize: 10, total: MOCK_DATA.length }); - setDataSource(MOCK_DATA); + setPagination({ current: 1, pageSize: 10, total: SORTED_MOCK_DATA.length }); + setDataSource(SORTED_MOCK_DATA); setSummary(MOCK_SUMMARY); }, 500); diff --git a/src/pages/events/users/model/useUserModel.ts b/src/pages/events/users/model/useUserModel.ts index 2b43238..64bca5e 100644 --- a/src/pages/events/users/model/useUserModel.ts +++ b/src/pages/events/users/model/useUserModel.ts @@ -1,5 +1,6 @@ import { computed, reactive, toRef, Ref, h } from 'vue'; import { message } from 'ant-design-vue'; +import dayjs from 'dayjs'; import { StatusTag, type StatusTagTone } from '@/components'; import { useState, useDebounce, useThrottleFn } from '@/hooks'; @@ -38,9 +39,18 @@ const MOCK_DATA = Array.from({ length: 12 }, (_, i) => ({ ? ['https://picsum.photos/seed/idcard-a/120/80', 'https://picsum.photos/seed/idcard-b/120/80'] : [], status: (i % 4 === 3 ? 'disabled' : 'active') as string, - loginTime: '2026-07-20 14:30:00', + loginTime: `2026-07-${String((i % 20) + 1).padStart(2, '0')} ${String((i * 3 + 6) % 24).padStart(2, '0')}:${String((i * 17 + 11) % 60).padStart(2, '0')}:${String((i * 13 + 7) % 60).padStart(2, '0')}`, })); +/** + * 临时排序:对假数据按登录时间倒序排列。 + * 【注意】实际项目中排序由后端接口负责,对接后端后请删除此函数及相关调用。 + */ +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, 'loginTime'); + // ============================================================ // Model // ============================================================ @@ -68,11 +78,11 @@ export function useUserModel() { // ===== 表格状态 ===== const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState(MOCK_DATA); + const [dataSource, setDataSource] = useState(SORTED_MOCK_DATA); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, - total: MOCK_DATA.length, + total: SORTED_MOCK_DATA.length, }); // ===== 表格列配置 ===== @@ -123,8 +133,8 @@ export function useUserModel() { status: filterForm.status, }); // TODO: 替换为真实 API 调用 - setDataSource(MOCK_DATA); - setPagination({ ...pagination.value, total: MOCK_DATA.length }); + setDataSource(SORTED_MOCK_DATA); + setPagination({ ...pagination.value, total: SORTED_MOCK_DATA.length }); message.success('查询成功'); } catch (error: any) { message.error(error.msg || '查询失败'); @@ -138,8 +148,8 @@ export function useUserModel() { filterForm.searchUserName = ''; filterForm.searchPhone = ''; filterForm.status = ''; - setPagination({ current: 1, pageSize: 10, total: MOCK_DATA.length }); - setDataSource(MOCK_DATA); + setPagination({ current: 1, pageSize: 10, total: SORTED_MOCK_DATA.length }); + setDataSource(SORTED_MOCK_DATA); }, 500); const handlePageChange = (page: number, pageSize: number) => { diff --git a/src/pages/system/roles/model/useRoleModel.ts b/src/pages/system/roles/model/useRoleModel.ts index 76693bd..ca3bdb5 100644 --- a/src/pages/system/roles/model/useRoleModel.ts +++ b/src/pages/system/roles/model/useRoleModel.ts @@ -1,5 +1,6 @@ import { computed, reactive, toRef, Ref, h } from 'vue'; import { message } from 'ant-design-vue'; +import dayjs from 'dayjs'; import { StatusTag, type StatusTagTone } from '@/components'; import { useState, useDebounce, useThrottleFn } from '@/hooks'; @@ -38,6 +39,15 @@ const MOCK_DATA = [ }, ]; +/** + * 临时排序:对假数据按创建时间倒序排列。 + * 【注意】实际项目中排序由后端接口负责,对接后端后请删除此函数及相关调用。 + */ +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'); + /** 角色下用户列表 mock */ const MOCK_USERS: Record = { R20250601001: [ @@ -159,11 +169,11 @@ export function useRoleModel() { // ===== 表格状态 ===== const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState(MOCK_DATA); + const [dataSource, setDataSource] = useState(SORTED_MOCK_DATA); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, - total: MOCK_DATA.length, + total: SORTED_MOCK_DATA.length, }); // ===== 弹窗状态 ===== @@ -202,8 +212,8 @@ export function useRoleModel() { try { console.log('搜索条件:', { keyword: debouncedKeyword.value }); // TODO: 替换为真实 API 调用 - setDataSource(MOCK_DATA); - setPagination({ ...pagination.value, total: MOCK_DATA.length }); + setDataSource(SORTED_MOCK_DATA); + setPagination({ ...pagination.value, total: SORTED_MOCK_DATA.length }); message.success('查询成功'); } catch (error: any) { message.error(error.msg || '查询失败'); @@ -215,8 +225,8 @@ export function useRoleModel() { /** 重置 */ const handleReset = useThrottleFn(() => { filterForm.searchKeyword = ''; - setPagination({ current: 1, pageSize: 10, total: MOCK_DATA.length }); - setDataSource(MOCK_DATA); + setPagination({ current: 1, pageSize: 10, total: SORTED_MOCK_DATA.length }); + setDataSource(SORTED_MOCK_DATA); }, 500); const handlePageChange = (page: number, pageSize: number) => { diff --git a/src/pages/system/users/model/useUserModel.ts b/src/pages/system/users/model/useUserModel.ts index e885fe1..cec2fda 100644 --- a/src/pages/system/users/model/useUserModel.ts +++ b/src/pages/system/users/model/useUserModel.ts @@ -1,5 +1,6 @@ import { computed, reactive, toRef, Ref, h } from 'vue'; import { message } from 'ant-design-vue'; +import dayjs from 'dayjs'; import { StatusTag, type StatusTagTone } from '@/components'; import { useState, useDebounce, useThrottleFn } from '@/hooks'; @@ -58,6 +59,15 @@ const MOCK_DATA = [ }, ]; +/** + * 临时排序:对假数据按创建时间倒序排列。 + * 【注意】实际项目中排序由后端接口负责,对接后端后请删除此函数及相关调用。 + */ +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'); + // ============================================================ // Model // ============================================================ @@ -81,11 +91,11 @@ export function useUserModel() { // ===== 表格状态 ===== const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState(MOCK_DATA); + const [dataSource, setDataSource] = useState(SORTED_MOCK_DATA); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, - total: MOCK_DATA.length, + total: SORTED_MOCK_DATA.length, }); // ===== 弹窗状态 ===== @@ -144,8 +154,8 @@ export function useUserModel() { status: filterForm.status, }); // TODO: 替换为真实 API 调用 - setDataSource(MOCK_DATA); - setPagination({ ...pagination.value, total: MOCK_DATA.length }); + setDataSource(SORTED_MOCK_DATA); + setPagination({ ...pagination.value, total: SORTED_MOCK_DATA.length }); message.success('查询成功'); } catch (error: any) { message.error(error.msg || '查询失败'); @@ -159,8 +169,8 @@ export function useUserModel() { filterForm.searchKeyword = ''; filterForm.role = ''; filterForm.status = ''; - setPagination({ current: 1, pageSize: 10, total: MOCK_DATA.length }); - setDataSource(MOCK_DATA); + setPagination({ current: 1, pageSize: 10, total: SORTED_MOCK_DATA.length }); + setDataSource(SORTED_MOCK_DATA); }, 500); const handlePageChange = (page: number, pageSize: number) => { -- 2.52.0 From 1c432ea890d962c00e42583ead2b8d2b2958ef39 Mon Sep 17 00:00:00 2001 From: ZhuRui Date: Wed, 29 Jul 2026 11:03:54 +0800 Subject: [PATCH 3/9] =?UTF-8?q?fix:=20=E5=A4=84=E7=90=86=E5=8A=A8=E7=94=BB?= =?UTF-8?q?=E5=BC=95=E8=B5=B7=E7=9A=84=E8=AD=A6=E6=8A=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/styles/index.less | 4 ++-- src/layouts/BasicLayout.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/assets/styles/index.less b/src/assets/styles/index.less index 2bf63a1..e5c92db 100644 --- a/src/assets/styles/index.less +++ b/src/assets/styles/index.less @@ -123,8 +123,8 @@ a { height: 100%; min-height: 0; transition: - opacity 0.22s cubic-bezier(0.4, 0, 0.2, 1), - transform 0.22s cubic-bezier(0.4, 0, 0.2, 1); + opacity 0.24s cubic-bezier(0.4, 0, 0.2, 1), + transform 0.24s cubic-bezier(0.4, 0, 0.2, 1); } .page-fade-slide-enter-from { diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx index 74fb457..c3ef6b7 100644 --- a/src/layouts/BasicLayout.tsx +++ b/src/layouts/BasicLayout.tsx @@ -249,7 +249,7 @@ export default defineComponent({ v-slots={{ default: ({ Component, route }: any) => ( - + {Component ? h(Component, { key: route.path }) : null} -- 2.52.0 From bf09234c8c3521f73d10a473bf694ec929505333 Mon Sep 17 00:00:00 2001 From: ZhuRui Date: Wed, 29 Jul 2026 15:02:05 +0800 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=E5=B0=8F=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=B8=8E=E9=85=8D=E7=BD=AE=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../events/list/components/EventDetailModal.tsx | 16 ++++++++++++++-- src/pages/events/list/model/useEventListModel.ts | 1 - src/pages/events/orders/model/useOrderModel.ts | 1 - src/types/yp-component.d.ts | 1 + 4 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 src/types/yp-component.d.ts diff --git a/src/pages/events/list/components/EventDetailModal.tsx b/src/pages/events/list/components/EventDetailModal.tsx index 0644989..dfa1457 100644 --- a/src/pages/events/list/components/EventDetailModal.tsx +++ b/src/pages/events/list/components/EventDetailModal.tsx @@ -1,5 +1,6 @@ import { defineComponent } from 'vue'; import { Modal, Descriptions, Image, Table, Pagination, Space } from 'ant-design-vue'; +import { EyeOutlined } from '@ant-design/icons-vue'; import styles from './EventDetailModal.module.less'; interface EventDetailModalProps { @@ -72,6 +73,17 @@ const MOCK_GROUPS = [ // 假分组数据 const MOCK_DIVISIONS = ['分组1:男子小组1(循环赛)', '分组2:女子小组1(淘汰赛)']; +const renderIdImage = (src: string) => ( + , + }} + /> +); + const PLAYER_COLUMNS = [ { title: '选手', dataIndex: 'name', key: 'name', width: 100 }, { title: '性别', dataIndex: 'gender', key: 'gender', width: 80 }, @@ -84,8 +96,8 @@ const PLAYER_COLUMNS = [ width: 180, customRender: () => ( - - + {renderIdImage('https://picsum.photos/seed/idimg1/96/64')} + {renderIdImage('https://picsum.photos/seed/idimg2/96/64')} ), }, diff --git a/src/pages/events/list/model/useEventListModel.ts b/src/pages/events/list/model/useEventListModel.ts index 53b8f5e..a1ca485 100644 --- a/src/pages/events/list/model/useEventListModel.ts +++ b/src/pages/events/list/model/useEventListModel.ts @@ -180,7 +180,6 @@ export function useEventListModel() { dataIndex: 'status', key: 'status', width: 90, - align: 'center' as const, customRender: ({ text }: { text: string }) => { const info = STATUS_MAP[text] || { label: text, tone: 'default' as const }; return h(StatusTag, { label: info.label, tone: info.tone }); diff --git a/src/pages/events/orders/model/useOrderModel.ts b/src/pages/events/orders/model/useOrderModel.ts index a2298ec..9513f95 100644 --- a/src/pages/events/orders/model/useOrderModel.ts +++ b/src/pages/events/orders/model/useOrderModel.ts @@ -263,7 +263,6 @@ export function useOrderModel() { dataIndex: 'orderStatus', key: 'orderStatus', width: 100, - align: 'center' as const, customRender: ({ text }: { text: string }) => { const info = ORDER_STATUS_MAP[text] || { label: text || '-', tone: 'default' as const }; return h(StatusTag, { label: info.label, tone: info.tone }); diff --git a/src/types/yp-component.d.ts b/src/types/yp-component.d.ts new file mode 100644 index 0000000..95e3bbb --- /dev/null +++ b/src/types/yp-component.d.ts @@ -0,0 +1 @@ +declare module '@yp-component/root'; -- 2.52.0 From 444b6f60a547f8a46a64e978d5649c1b1df1fdaa Mon Sep 17 00:00:00 2001 From: ZhuRui Date: Wed, 29 Jul 2026 16:49:33 +0800 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20=E5=AF=B9=E6=8E=A5=E8=B5=9B?= =?UTF-8?q?=E4=BA=8B=E5=88=97=E8=A1=A8=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E6=96=87=E6=A1=A3=20=E6=9B=B4=E6=96=B0=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 3 +- .env.production | 3 +- .env.test | 3 +- src/api/events/index.ts | 100 +++++ src/api/events/types.ts | 199 +++++++++ src/config/mock.ts | 12 + src/config/mock/eventDetail.ts | 39 ++ src/config/mock/eventList.ts | 268 ++++++++++++ src/config/mock/eventSignup.ts | 96 +++++ .../list/components/EventDetailModal.tsx | 389 ++++++++++-------- src/pages/events/list/index.tsx | 97 +++-- .../events/list/model/useEventListModel.ts | 280 +++++++------ 12 files changed, 1146 insertions(+), 343 deletions(-) create mode 100644 src/api/events/index.ts create mode 100644 src/api/events/types.ts create mode 100644 src/config/mock.ts create mode 100644 src/config/mock/eventDetail.ts create mode 100644 src/config/mock/eventList.ts create mode 100644 src/config/mock/eventSignup.ts diff --git a/.env.development b/.env.development index 9cf069a..1a992dd 100644 --- a/.env.development +++ b/.env.development @@ -1,4 +1,3 @@ # 开发环境 VITE_PORT = 5173 -VITE_API_BASE_URL = http://localhost:3000 -VITE_API_PREFIX = /api +VITE_API_BASE_URL = http://localhost:3000/admin diff --git a/.env.production b/.env.production index dc54b4f..34d7624 100644 --- a/.env.production +++ b/.env.production @@ -1,3 +1,2 @@ # 生产环境 -VITE_API_BASE_URL = https://api.cpms.example.com -VITE_API_PREFIX = /api +VITE_API_BASE_URL = https://api.cpms.example.com/admin diff --git a/.env.test b/.env.test index b3621a6..7ca21d6 100644 --- a/.env.test +++ b/.env.test @@ -1,3 +1,2 @@ # 测试环境 -VITE_API_BASE_URL = https://test-api.cpms.example.com -VITE_API_PREFIX = /api +VITE_API_BASE_URL = https://test-api.cpms.example.com/admin diff --git a/src/api/events/index.ts b/src/api/events/index.ts new file mode 100644 index 0000000..4c62196 --- /dev/null +++ b/src/api/events/index.ts @@ -0,0 +1,100 @@ +/** + * 赛事管理 API + * 注意:/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, + ToggleOnlineParams, + ApiResult, + PageData, + TournamentAdminVO, + TournamentAdminInfoVO, + TournamentAdminInfoSignupVO, +} from './types'; + +export type { + TournamentAdminVO, + TournamentAdminInfoVO, + TournamentAdminInfoSignupVO, + InnerCategoryVO, + EventListQueryParams, + SignupListQueryParams, + ToggleOnlineParams, + PageData, + ApiResult, +} from './types'; + +/** 模拟延时 */ +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * 赛事列表 - 列表分页 + * GET /admin/manager/tm/page → /manager/tm/page + */ +export async function getEventList( + params: EventListQueryParams, +): Promise>> { + if (USE_MOCK) { + await delay(MOCK_DELAY); + const page = Number(params.page) || 1; + const limit = Number(params.limit) || 10; + return { + code: 200, + msg: 'success', + data: buildMockEventListPage(page, limit), + }; + } + return get('/manager/tm/page', params as Record); +} + +/** + * 赛事列表 - 赛事详情 + * GET /admin/manager/tm/info → /manager/tm/info + */ +export async function getEventDetail(id: string): Promise> { + if (USE_MOCK) { + await delay(MOCK_DELAY); + return { + code: 200, + msg: 'success', + data: { ...MOCK_EVENT_DETAIL }, + }; + } + return get('/manager/tm/info', { id }); +} + +/** + * 赛事列表 - 赛事详情 - 人员列表 + * GET /admin/manager/tm/info/signupPage → /manager/tm/info/signupPage + */ +export async function getSignupList( + params: SignupListQueryParams, +): Promise>> { + if (USE_MOCK) { + await delay(MOCK_DELAY); + return { + code: 200, + msg: 'success', + data: buildMockSignupListPage(params), + }; + } + return get('/manager/tm/info/signupPage', params as Record); +} + +/** + * 赛事上架 / 下架 + * POST /admin/manager/tm/online → /manager/tm/online + * @param params.id 赛事ID + * @param params.online 0=下架,1=上架 + */ +export function toggleEventOnline( + params: ToggleOnlineParams, +): Promise>> { + return post('/manager/tm/online', params); +} diff --git a/src/api/events/types.ts b/src/api/events/types.ts new file mode 100644 index 0000000..0a562fc --- /dev/null +++ b/src/api/events/types.ts @@ -0,0 +1,199 @@ +/** + * 赛事管理 — 类型定义 + * 对应 OpenAPI: /admin/manager/tm/page + */ + +/** 赛事列表项 */ +export interface TournamentAdminVO { + /** 赛事ID */ + id: string; + /** 赛事名称 */ + name: string; + /** 赛事开始时间 */ + startTimeBegin: string; + /** 赛事结束时间 */ + startTimeEnd: string; + /** 赛事省市区,场地区划名称 */ + venueArea: string; + /** 赛事地点名称 */ + venue: string; + /** 是否公开;0=非公开,1=公开 */ + isPublic: string; + /** 是否要求提供证件图片;0=无需,1=需要 */ + isRequireIdCardImg: string; + /** 是否要求提供证件号码;0=无需,1=需要 */ + isRequireIdCardNo: string; + /** 创建人昵称 */ + createNickname: string; + /** 创建人手机号 */ + createPhone: string; + /** 创建时间 */ + createTime: string; + /** + * 状态 + * 优先级:del_flag==1 → 3(已删除);online==0 → 4(已下架);否则取 status + * 0=报名中,1=进行中,2=已结束,3=已删除,4=已下架 + */ + status: string; + /** 报名人数 */ + signupCount: string; + /** 退赛人数 */ + dropoutCount: string; + /** 赛事规程 */ + rulesDescr: string; + /** 赛事规程图片 */ + rulesDescrImg: string; + /** 赛事规程图片数组 */ + rulesDescrImgList: string[]; +} + +/** 赛事列表查询参数 */ +export interface EventListQueryParams { + page?: string; + limit?: string; + /** 创建时间开始 yyyy-MM-dd */ + createDate?: string; + /** 创建时间结束 yyyy-MM-dd */ + createDateEnd?: string; + /** 赛事时间开始 yyyy-MM-dd */ + startDate?: string; + /** 赛事时间结束 yyyy-MM-dd */ + startDateEnd?: string; + /** 赛事名称 */ + name?: string; + /** 创建人昵称或手机号 */ + createName?: string; + /** + * 赛事状态 + * 不传=全部、0=报名中,1=进行中,2=已结束,3=已删除,4=已下架 + */ + status?: string; + /** 市级别城市名称 */ + areaName?: string; + /** 是否公开;0=非公开,1=公开 */ + isPublic?: string; + /** 是否要求提供证件图片;0=无需,1=需要 */ + idCardImg?: string; + /** 是否要求提供证件号码;0=无需,1=需要 */ + idCardNo?: string; +} + +/** 分页数据 */ +export interface PageData { + total: number; + list: T[]; +} + +/** 统一响应包裹 */ +export interface ApiResult { + /** 编码:200表示成功,其他值表示失败 */ + code: number; + /** 消息内容 */ + msg: string; + /** 响应数据 */ + data: T; +} + +// ============================================================ +// 赛事详情(对应 /manager/tm/info) +// ============================================================ + +/** 组别信息 */ +export interface InnerCategoryVO { + /** 组别ID */ + id: string; + /** 组别名称 */ + name: string; + /** 组别类型;1=单打,2=双打 */ + type: string; +} + +/** 赛事详情 */ +export interface TournamentAdminInfoVO { + /** 赛事名称 */ + name: string; + /** 赛事开始时间 */ + startTimeBegin: string; + /** 赛事结束时间 */ + startTimeEnd: string; + /** 赛事说明 */ + descr: string; + /** 报名开始时间 */ + signupTimeBegin: string; + /** 报名结束时间 */ + signupTimeEnd: string; + /** 赛事省市区 */ + venueArea: string; + /** 赛事地点名称 */ + venue: string; + /** 场地号 */ + venueNo: string; + /** 是否公开;0=非公开,1=公开 */ + isPublic: string; + /** 是否要求提供证件图片;0=无需,1=需要 */ + isRequireIdCardImg: string; + /** 是否要求提供证件号码;0=无需,1=需要 */ + isRequireIdCardNo: string; + /** 创建时间 */ + createTime: string; + /** 创建人昵称 */ + createNickname: string; + /** 报名人数 */ + signupCount: string; + /** 退赛人数 */ + dropoutCount: string; + /** 允许报名最大人数 */ + dtotalLimit: string; + /** 总对阵数 */ + matchTotalCount: string; + /** 已完成对阵数 */ + matchCount: string; + /** 赛事封面url */ + img: string; + /** 活动公告 */ + notice: string; + /** 组别列表,创建时间倒序 */ + categoryList: InnerCategoryVO[]; + /** 分组名称列表,格式 "小组名称(淘汰赛或循环赛)" */ + groupInfoList: string[]; +} + +// ============================================================ +// 人员列表(对应 /manager/tm/info/signupPage) +// ============================================================ + +/** 报名人员 */ +export interface TournamentAdminInfoSignupVO { + /** 选手名称 */ + realName: string; + /** 选手性别;1=男,2=女 */ + gender: string; + /** 选手手机号 */ + phone: string; + /** 选手身份证 */ + idCard: string; + /** 选手身份证图片 */ + idCardImg: string; + /** 选手身份证图片列表 */ + idCardImgList: string[]; +} + +/** 人员列表查询参数 */ +export interface SignupListQueryParams { + page: string; + limit: string; + /** 组别ID(必填) */ + categoryId: string; +} + +// ============================================================ +// 上下架(对应 /manager/tm/online) +// ============================================================ + +/** 上下架请求参数 */ +export interface ToggleOnlineParams { + /** 赛事ID */ + id: string; + /** 0=下架,1=上架 */ + online: string; +} diff --git a/src/config/mock.ts b/src/config/mock.ts new file mode 100644 index 0000000..c2a4655 --- /dev/null +++ b/src/config/mock.ts @@ -0,0 +1,12 @@ +/** + * 全局假数据开关 + * + * true = 所有 API 使用假数据(模拟真实网络延时 2s 后返回假数据) + * false = 走真实后端接口 + * + * 对接后端接口后将此值改为 false 即可切换。 + */ +export const USE_MOCK = true; + +/** 模拟网络延时(毫秒) */ +export const MOCK_DELAY = 850; diff --git a/src/config/mock/eventDetail.ts b/src/config/mock/eventDetail.ts new file mode 100644 index 0000000..87b8329 --- /dev/null +++ b/src/config/mock/eventDetail.ts @@ -0,0 +1,39 @@ +/** + * 赛事详情弹窗假数据 + * + * 使用页面:`src/pages/events/list/components/EventDetailModal.tsx` + * 对应接口:`GET /manager/tm/info` + * 类型:`TournamentAdminInfoVO` + */ +import type { TournamentAdminInfoVO } from '@/api/events'; + +export const MOCK_EVENT_DETAIL: TournamentAdminInfoVO = { + name: '第二届全国地学羽毛球邀请赛', + startTimeBegin: '2026-05-26 08:00', + startTimeEnd: '2026-05-31 22:00', + descr: + '赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍。', + signupTimeBegin: '2026-05-24 08:00', + signupTimeEnd: '2026-05-26 22:00', + venueArea: '北京市 北京市 东城区', + venue: '李宁体育馆', + venueNo: '李宁体育馆', + isPublic: '1', + isRequireIdCardImg: '0', + isRequireIdCardNo: '0', + createTime: '2026-05-26 08:00', + createNickname: '张三', + signupCount: '100', + dropoutCount: '1', + dtotalLimit: '1000', + matchTotalCount: '50', + matchCount: '50', + img: 'https://picsum.photos/seed/eventcover/600/300', + notice: + '赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告。', + categoryList: [ + { id: '1', name: '男子单打', type: '1' }, + { id: '2', name: '女子双打', type: '2' }, + ], + groupInfoList: ['男子小组1(循环赛)', '女子小组1(淘汰赛)'], +}; diff --git a/src/config/mock/eventList.ts b/src/config/mock/eventList.ts new file mode 100644 index 0000000..6681d3c --- /dev/null +++ b/src/config/mock/eventList.ts @@ -0,0 +1,268 @@ +/** + * 赛事列表页假数据 + * + * 使用页面:`src/pages/events/list/index.tsx` + * 对应接口:`GET /manager/tm/page` + * 类型:`TournamentAdminVO[]` + */ +import type { TournamentAdminVO, PageData } from '@/api/events'; + +export const MOCK_EVENT_LIST: TournamentAdminVO[] = [ + { + id: 'EV202607010001', + name: '2026全国青少年羽毛球锦标赛暨体育文化交流大会', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '1', + isRequireIdCardImg: '1', + isRequireIdCardNo: '1', + createNickname: '张运营', + createPhone: '13800138001', + createTime: '2026-07-10 08:11:07', + status: '0', + signupCount: '128', + dropoutCount: '5', + rulesDescr: '一、赛事目的:推动羽毛球运动发展。\n二、参赛资格:身体健康、年龄符合要求。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: [ + 'https://picsum.photos/seed/rule1/300/200', + 'https://picsum.photos/seed/rule2/300/200', + ], + }, + { + id: 'EV202607010002', + name: '国际马拉松城市联赛', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '0', + isRequireIdCardImg: '0', + isRequireIdCardNo: '0', + createNickname: '李管理', + createPhone: '13900139002', + createTime: '2026-07-09 11:28:20', + status: '1', + signupCount: '256', + dropoutCount: '12', + rulesDescr: '一、赛事目的:推动马拉松运动发展。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: ['https://picsum.photos/seed/rule1/300/200'], + }, + { + id: 'EV202607010003', + name: '全民健身运动会', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '1', + isRequireIdCardImg: '1', + isRequireIdCardNo: '1', + createNickname: '王策划', + createPhone: '13700137003', + createTime: '2026-07-08 14:45:33', + status: '2', + signupCount: '512', + dropoutCount: '8', + rulesDescr: '一、赛事目的:促进全民健身。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: ['https://picsum.photos/seed/rule1/300/200'], + }, + { + id: 'EV202607010004', + name: '2026全国青少年羽毛球锦标赛暨体育文化交流大会', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '0', + isRequireIdCardImg: '0', + isRequireIdCardNo: '0', + createNickname: '赵赛事', + createPhone: '13600136004', + createTime: '2026-07-07 18:02:46', + status: '3', + signupCount: '64', + dropoutCount: '3', + rulesDescr: '一、赛事目的:交流体育文化。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: [], + }, + { + id: 'EV202607010005', + name: '国际马拉松城市联赛', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '1', + isRequireIdCardImg: '1', + isRequireIdCardNo: '1', + createNickname: '张运营', + createPhone: '13800138001', + createTime: '2026-07-06 21:19:59', + status: '4', + signupCount: '0', + dropoutCount: '0', + rulesDescr: '一、赛事目的:推动城市体育交流。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: ['https://picsum.photos/seed/rule1/300/200'], + }, + { + id: 'EV202607010006', + name: '全民健身运动会', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '0', + isRequireIdCardImg: '0', + isRequireIdCardNo: '0', + createNickname: '李管理', + createPhone: '13900139002', + createTime: '2026-07-05 00:36:12', + status: '0', + signupCount: '128', + dropoutCount: '5', + rulesDescr: '一、赛事目的:增强全民体质。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: [], + }, + { + id: 'EV202607010007', + name: '2026全国青少年羽毛球锦标赛暨体育文化交流大会', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '1', + isRequireIdCardImg: '1', + isRequireIdCardNo: '1', + createNickname: '王策划', + createPhone: '13700137003', + createTime: '2026-07-04 03:53:25', + status: '1', + signupCount: '256', + dropoutCount: '12', + rulesDescr: '一、赛事目的:选拔青少年人才。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: [ + 'https://picsum.photos/seed/rule1/300/200', + 'https://picsum.photos/seed/rule2/300/200', + ], + }, + { + id: 'EV202607010008', + name: '国际马拉松城市联赛', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '0', + isRequireIdCardImg: '0', + isRequireIdCardNo: '0', + createNickname: '赵赛事', + createPhone: '13600136004', + createTime: '2026-07-03 07:10:38', + status: '2', + signupCount: '512', + dropoutCount: '8', + rulesDescr: '一、赛事目的:连接城市与运动。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: [], + }, + { + id: 'EV202607010009', + name: '全民健身运动会', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '1', + isRequireIdCardImg: '1', + isRequireIdCardNo: '1', + createNickname: '张运营', + createPhone: '13800138001', + createTime: '2026-07-02 10:27:51', + status: '3', + signupCount: '64', + dropoutCount: '3', + rulesDescr: '一、赛事目的:全民参与、共享健康。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: ['https://picsum.photos/seed/rule1/300/200'], + }, + { + id: 'EV202607010010', + name: '2026全国青少年羽毛球锦标赛暨体育文化交流大会', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '0', + isRequireIdCardImg: '0', + isRequireIdCardNo: '0', + createNickname: '李管理', + createPhone: '13900139002', + createTime: '2026-07-01 13:44:04', + status: '4', + signupCount: '0', + dropoutCount: '0', + rulesDescr: '一、赛事目的:培养后备力量。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: [], + }, + { + id: 'EV202607010011', + name: '国际马拉松城市联赛', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '1', + isRequireIdCardImg: '1', + isRequireIdCardNo: '1', + createNickname: '王策划', + createPhone: '13700137003', + createTime: '2026-06-30 17:01:17', + status: '0', + signupCount: '128', + dropoutCount: '5', + rulesDescr: '一、赛事目的:推广城市品牌。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: [ + 'https://picsum.photos/seed/rule1/300/200', + 'https://picsum.photos/seed/rule2/300/200', + 'https://picsum.photos/seed/rule3/300/200', + ], + }, + { + id: 'EV202607010012', + name: '全民健身运动会', + startTimeBegin: '2026-08-12 09:00', + startTimeEnd: '2026-08-15 18:00', + venueArea: '广东省 深圳市 南山区', + venue: '深圳湾体育中心', + isPublic: '0', + isRequireIdCardImg: '0', + isRequireIdCardNo: '0', + createNickname: '赵赛事', + createPhone: '13600136004', + createTime: '2026-06-29 20:18:30', + status: '1', + signupCount: '256', + dropoutCount: '12', + rulesDescr: '一、赛事目的:丰富群众体育生活。', + rulesDescrImg: 'https://picsum.photos/seed/rules/300/200', + rulesDescrImgList: [], + }, +]; + +/** 构建分页响应 */ +export function buildMockEventListPage(page: number, limit: number): PageData { + const start = (page - 1) * limit; + const list = MOCK_EVENT_LIST.slice(start, start + limit); + return { total: MOCK_EVENT_LIST.length, list }; +} diff --git a/src/config/mock/eventSignup.ts b/src/config/mock/eventSignup.ts new file mode 100644 index 0000000..4b8940c --- /dev/null +++ b/src/config/mock/eventSignup.ts @@ -0,0 +1,96 @@ +/** + * 赛事详情 — 人员列表假数据 + * + * 使用页面:`src/pages/events/list/components/EventDetailModal.tsx` + * 对应接口:`GET /manager/tm/info/signupPage` + * 类型:`TournamentAdminInfoSignupVO[]` + */ +import type { TournamentAdminInfoSignupVO, PageData, SignupListQueryParams } from '@/api/events'; + +export const MOCK_SIGNUP_LIST: TournamentAdminInfoSignupVO[] = [ + { + realName: '张三', + gender: '1', + phone: '13800138001', + idCard: '110101199001011234', + idCardImg: 'https://picsum.photos/seed/idcard1/96/64', + idCardImgList: [ + 'https://picsum.photos/seed/idcard1a/96/64', + 'https://picsum.photos/seed/idcard1b/96/64', + ], + }, + { + realName: '李四', + gender: '1', + phone: '13900139002', + idCard: '110101199002022345', + idCardImg: 'https://picsum.photos/seed/idcard2/96/64', + idCardImgList: [ + 'https://picsum.photos/seed/idcard2a/96/64', + 'https://picsum.photos/seed/idcard2b/96/64', + ], + }, + { + realName: '王芳', + gender: '2', + phone: '13700137003', + idCard: '110101199103033456', + idCardImg: '', + idCardImgList: [], + }, + { + realName: '赵六', + gender: '1', + phone: '13600136004', + idCard: '110101199204044567', + idCardImg: 'https://picsum.photos/seed/idcard4/96/64', + idCardImgList: ['https://picsum.photos/seed/idcard4a/96/64'], + }, + { + realName: '孙七', + gender: '2', + phone: '13500135005', + idCard: '110101199305055678', + idCardImg: '', + idCardImgList: [], + }, + { + realName: '周八', + gender: '1', + phone: '13400134006', + idCard: '110101199406066789', + idCardImg: 'https://picsum.photos/seed/idcard6/96/64', + idCardImgList: [ + 'https://picsum.photos/seed/idcard6a/96/64', + 'https://picsum.photos/seed/idcard6b/96/64', + ], + }, + { + realName: '吴九', + gender: '2', + phone: '13300133007', + idCard: '110101199507077890', + idCardImg: '', + idCardImgList: [], + }, + { + realName: '郑十', + gender: '1', + phone: '13200132008', + idCard: '110101199608088901', + idCardImg: 'https://picsum.photos/seed/idcard8/96/64', + idCardImgList: ['https://picsum.photos/seed/idcard8a/96/64'], + }, +]; + +/** 构建分页响应 */ +export function buildMockSignupListPage( + params: SignupListQueryParams, +): PageData { + // mock 模式下忽略 categoryId 过滤,直接分页返回 + const page = Number(params.page) || 1; + const limit = Number(params.limit) || 10; + const start = (page - 1) * limit; + const list = MOCK_SIGNUP_LIST.slice(start, start + limit); + return { total: MOCK_SIGNUP_LIST.length, list }; +} diff --git a/src/pages/events/list/components/EventDetailModal.tsx b/src/pages/events/list/components/EventDetailModal.tsx index dfa1457..6e3edc4 100644 --- a/src/pages/events/list/components/EventDetailModal.tsx +++ b/src/pages/events/list/components/EventDetailModal.tsx @@ -1,6 +1,15 @@ -import { defineComponent } from 'vue'; -import { Modal, Descriptions, Image, Table, Pagination, Space } from 'ant-design-vue'; +import { defineComponent, reactive, onMounted } 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'; +import { + getEventDetail, + getSignupList, + type TournamentAdminInfoVO, + type InnerCategoryVO, + type TournamentAdminInfoSignupVO, +} from '@/api/events'; +import { useState } from '@/hooks'; import styles from './EventDetailModal.module.less'; interface EventDetailModalProps { @@ -9,97 +18,57 @@ interface EventDetailModalProps { onClose: () => void; } -// ===== Mock 数据 ===== -const MOCK_DETAIL = { - name: '第二届全国地学羽毛球邀请赛', - eventTime: '2026.05.26 08:00 ~ 2026.05.31 22:00', - descr: - '赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍。', - registerTime: '2026.05.24 08:00 ~ 2026.05.26 22:00', - cancelDeadline: '2026.05.25 22:00', - region: '北京市 北京市 东城区', - address: '李宁体育馆', - venueNo: '李宁体育馆', - publicEvent: '在首页展示赛事', - needIdCard: '无需提供', - needIdCardImage: '无需提供', - createTime: '2026.05.26 08:00', - creator: '张三', - status: '报名中', - enrolledCount: 100, - cancelledCount: 1, - maxCount: 1000, - crossedCount: 50, - completedCount: 50, - cover: 'https://picsum.photos/seed/eventcover/600/300', - announcement: - '赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告赛事公告。', +// ===== 0/1 显示映射 ===== +const YES_NO_MAP: Record = { '0': '否', '1': '是' }; +const PUBLIC_MAP: Record = { '0': '非公开', '1': '公开' }; +const GENDER_MAP: Record = { '1': '男', '2': '女' }; +const CATEGORY_TYPE_MAP: Record = { '1': '单打', '2': '双打' }; + +const formatYN = (v: string) => YES_NO_MAP[v] ?? v; +const formatPublic = (v: string) => PUBLIC_MAP[v] ?? v; +const formatGender = (v: string) => GENDER_MAP[v] ?? v; + +const PAGE_SIZE = 10; + +/** 渲染身份证图片 */ +const renderIdImages = (imgList: string[], singleImg: string) => { + const urls = imgList.length > 0 ? imgList : singleImg ? [singleImg] : []; + if (urls.length === 0) return -; + return ( + + + {urls.map((url) => ( + }} + /> + ))} + + + ); }; -// 假组别数据 -const MOCK_GROUPS = [ - { - key: 'group1', - title: '组别1:男子单打(单打)', - players: [ - { - key: '1', - name: '张三', - gender: '男', - phone: '12345678995', - idNo: 'xxxxxxxxxxxxxxxx', - idImg: '', - }, - { key: '2', name: '李四', gender: '男', phone: '', idNo: '', idImg: '' }, - ], - }, - { - key: 'group2', - title: '组别2:女子双打(双打)', - players: [ - { - key: '1', - name: '张三', - gender: '女', - phone: '12345678995', - idNo: 'xxxxxxxxxxxxxxxx', - idImg: '', - }, - { key: '2', name: '李四', gender: '女', phone: '', idNo: '', idImg: '' }, - ], - }, -]; - -// 假分组数据 -const MOCK_DIVISIONS = ['分组1:男子小组1(循环赛)', '分组2:女子小组1(淘汰赛)']; - -const renderIdImage = (src: string) => ( - , - }} - /> -); - +/** 选手表格列 */ const PLAYER_COLUMNS = [ - { title: '选手', dataIndex: 'name', key: 'name', width: 100 }, - { title: '性别', dataIndex: 'gender', key: 'gender', width: 80 }, - { title: '手机号', dataIndex: 'phone', key: 'phone', width: 150 }, - { title: '证件号', dataIndex: 'idNo', key: 'idNo', width: 180 }, + { title: '选手', dataIndex: 'realName', key: 'realName', width: 100 }, + { + title: '性别', + dataIndex: 'gender', + key: 'gender', + width: 60, + customRender: ({ text }: { text: string }) => formatGender(text), + }, + { title: '手机号', dataIndex: 'phone', key: 'phone', width: 130 }, + { title: '证件号', dataIndex: 'idCard', key: 'idCard', width: 180 }, { title: '证件图片', - dataIndex: 'idImg', - key: 'idImg', + key: 'idCardImgs', width: 180, - customRender: () => ( - - {renderIdImage('https://picsum.photos/seed/idimg1/96/64')} - {renderIdImage('https://picsum.photos/seed/idimg2/96/64')} - - ), + customRender: ({ record }: { record: TournamentAdminInfoSignupVO }) => + renderIdImages(record.idCardImgList, record.idCardImg), }, ]; @@ -111,7 +80,71 @@ export default defineComponent({ onClose: { type: Function, required: true }, }, setup(props: EventDetailModalProps) { - const detail = MOCK_DETAIL; + // ===== 详情数据 ===== + const [detail, setDetail] = useState(null); + const [detailLoading, setDetailLoading] = useState(true); + + // ===== 每个组别的选手数据(key = categoryId) ===== + const categoryPlayers = reactive>({}); + const categoryPlayerTotal = reactive>({}); + const categoryPlayerLoading = reactive>({}); + const categoryPlayerPage = reactive>({}); + + /** 获取某个组别的选手列表(分页) */ + const fetchPlayers = async (categoryId: string, page: number) => { + categoryPlayerLoading[categoryId] = true; + try { + const res = await getSignupList({ + categoryId, + page: String(page), + limit: String(PAGE_SIZE), + }); + if (res.code === 200) { + categoryPlayers[categoryId] = res.data.list; + categoryPlayerTotal[categoryId] = res.data.total; + categoryPlayerPage[categoryId] = page; + } + } catch { + // mock 模式不会到这里,真实模式下由 API 层处理 + } finally { + categoryPlayerLoading[categoryId] = false; + } + }; + + /** 选手分页变更 */ + const handlePlayerPageChange = (categoryId: string, page: number) => { + fetchPlayers(categoryId, page); + }; + + // ===== 初始化 ===== + onMounted(async () => { + setDetailLoading(true); + try { + const id = props.record?.id; + if (!id) { + message.error('缺少赛事ID'); + return; + } + + const res = await getEventDetail(String(id)); + if (res.code === 200) { + setDetail(res.data); + // 为每个组别加载第一页选手 + for (const cat of res.data.categoryList) { + categoryPlayers[cat.id] = []; + categoryPlayerTotal[cat.id] = 0; + categoryPlayerPage[cat.id] = 1; + fetchPlayers(cat.id, 1); + } + } else { + message.error(res.msg || '获取详情失败'); + } + } catch (e: any) { + message.error(e.msg || '获取详情失败'); + } finally { + setDetailLoading(false); + } + }); return () => ( - {/* ===== 基础信息 ===== */} -
-
基础信息
- - {/* 第一行:赛事名称(1 列) + 赛事时间(占 2 列) */} - {detail.name} - - {detail.eventTime} - + {detailLoading.value ? ( +
+ +
+ ) : detail.value ? ( + <> + {/* ===== 基础信息 ===== */} +
+
基础信息
+ + {detail.value.name} + + {detail.value.startTimeBegin} 至 {detail.value.startTimeEnd} + - {/* 第二行:赛事说明(占 3 列,独占整行) */} - - {detail.descr} - + + {detail.value.descr} + - {detail.registerTime} - - {detail.cancelDeadline} - + + {detail.value.signupTimeBegin} 至 {detail.value.signupTimeEnd} + - {detail.region} - {detail.address} - {detail.venueNo} + {detail.value.venueArea} + {detail.value.venue} + {detail.value.venueNo} - {detail.publicEvent} - {detail.needIdCard} - {detail.needIdCardImage} + + {formatPublic(detail.value.isPublic)} + + + {formatYN(detail.value.isRequireIdCardNo)} + + + {formatYN(detail.value.isRequireIdCardImg)} + - {detail.createTime} - {detail.creator} - {detail.status} - {detail.enrolledCount} - - {detail.cancelledCount} - + {detail.value.createTime} + + {detail.value.createNickname} + - {detail.maxCount} - {detail.crossedCount} - {detail.completedCount} - -
+ {detail.value.signupCount} + {detail.value.dropoutCount} + {detail.value.dtotalLimit} - {/* ===== 封面 ===== */} -
-
封面
- -
+ + {detail.value.matchTotalCount} + + + {detail.value.matchCount} + +
+
- {/* ===== 赛事公告 ===== */} -
-
赛事公告
-
{detail.announcement}
-
+ {/* ===== 封面 ===== */} +
+
封面
+ +
- {/* ===== 组别信息 ===== */} -
-
组别信息
- {MOCK_GROUPS.map((group) => ( -
-
{group.title}
- -
- + {/* ===== 赛事公告 ===== */} +
+
赛事公告
+
{detail.value.notice}
+
+ + {/* ===== 组别信息 ===== */} +
+
组别信息
+ {detail.value.categoryList.map((cat: InnerCategoryVO) => ( +
+
+ {cat.name}({CATEGORY_TYPE_MAP[cat.type] ?? cat.type}) +
+
+
+ handlePlayerPageChange(cat.id, page)} + /> +
+ + ))} + + + {/* ===== 分组信息 ===== */} +
+
分组信息
+
+ {detail.value.groupInfoList.map((d: string) => ( +
+ {d} +
+ ))}
- ))} - - - {/* ===== 分组信息 ===== */} -
-
分组信息
-
- {MOCK_DIVISIONS.map((d) => ( -
- {d} -
- ))} -
-
+ + ) : null} ); }, diff --git a/src/pages/events/list/index.tsx b/src/pages/events/list/index.tsx index ff861e8..ab8b1cb 100644 --- a/src/pages/events/list/index.tsx +++ b/src/pages/events/list/index.tsx @@ -1,4 +1,4 @@ -import { defineComponent } from 'vue'; +import { defineComponent, onMounted } from 'vue'; import { Button, Input, @@ -20,6 +20,7 @@ import { ID_CARD_OPTIONS, ID_CARD_IMAGE_OPTIONS, } from './model/useEventListModel'; +import { toggleEventOnline } from '@/api/events'; import { regionOptions } from '@/utils/areaData'; import { useContainerSize, useState } from '@/hooks'; import EventRegulationModal from './components/EventRegulationModal'; @@ -49,7 +50,7 @@ function renderBodyCell({ onAudit: (record: any) => void; onToggleShelf: (record: any) => void; }) { - if (column.dataIndex === 'eventName') { + if (column.dataIndex === 'name') { const maxLen = 15; const raw = text || ''; const display = raw.length > maxLen ? raw.slice(0, maxLen) + '...' : raw; @@ -74,19 +75,8 @@ function renderBodyCell({ const status = record.status; return ( - // 待审核:查看 + 审核 - (status === 'pending' && ( - - - - - )) || - // 报名中:查看 + 下架 - (status === 'enrolling' && ( + // 报名中(0) / 进行中(1):查看 + 下架 + ((status === '0' || status === '1') && ( )) || - // 已下架:查看 + 上架 - (status === 'removed' && ( + // 已下架(4):查看 + 上架 + (status === '4' && ( )) || ( - // 审核不通过 / 已删除:仅查看 + // 已结束(2) / 已删除(3):仅查看 @@ -144,49 +134,62 @@ export default defineComponent({ const [currentRecord, setCurrentRecord] = useState({}); const handleView = (record: any) => { - console.log('查看赛事:', record.eventId); + console.log('查看赛事:', record.id); setCurrentRecord(record); setDetailModalVisible(true); }; /** 上架 / 下架 通用 */ const handleToggleShelf = (record: any) => { - const isRemoved = record.status === 'removed'; + const isRemoved = record.status === '4'; const actionText = isRemoved ? '上架' : '下架'; Modal.confirm({ title: `${actionText}确认`, - content: `确认${actionText}"${record.eventName}"该赛事吗,下架后用户将无法搜索和报名该赛事,已有报名和订单不受影响。`, + content: `确认${actionText}"${record.name}"该赛事吗,下架后用户将无法搜索和报名该赛事,已有报名和订单不受影响。`, okText: '确认', cancelText: '取消', - onOk: () => { - console.log(`${actionText}赛事:`, record.eventId); - // TODO: 替换为真实 API 调用 - message.success(`${actionText}成功`); + 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) { + message.error(e.msg || `${actionText}失败`); + } }, }); }; /** 打开审核弹窗 */ const handleAudit = (record: any) => { - console.log('审核赛事:', record.eventId); + console.log('审核赛事:', record.id); setCurrentRecord(record); setAuditModalVisible(true); }; /** 提交审核 */ const handleAuditSubmit = (payload: { approved: boolean; comment: string }) => { - console.log('提交审核:', currentRecord.value.eventId, payload); + console.log('提交审核:', currentRecord.value.id, payload); // TODO: 替换为真实 API 调用 message.success('审核提交成功'); setAuditModalVisible(false); }; const handleViewRegulation = (record: any) => { - console.log('查看规程:', record.eventId); + console.log('查看规程:', record.id); setCurrentRecord(record); setRegulationModalVisible(true); }; + onMounted(() => { + handleSearch(); + }); + /** 最终表格列:模型列 + 赛事规程 + 操作 */ const tableColumns = [ ...columns, @@ -205,23 +208,27 @@ export default defineComponent({ {/* ===== 筛选区 ===== */}
- + (filterForm.createTimeRange = val)} + onUpdate:value={(val: any) => (filterForm.createDateRange = val)} /> - + (filterForm.eventTimeRange = val)} + onUpdate:value={(val: any) => (filterForm.startDateRange = val)} /> - + - + (filterForm.status = val || '')} /> - + (filterForm.needIdCard = val || '')} + onUpdate:value={(val: any) => (filterForm.idCardNo = val || '')} /> - + (filterForm.name = val.target?.value ?? val ?? '')} onPressEnter={handleSearch} /> + (filterForm.createName = val.target?.value ?? val ?? '') + } onPressEnter={handleSearch} /> diff --git a/src/pages/events/list/model/useEventListModel.ts b/src/pages/events/list/model/useEventListModel.ts index b867c8b..5c30fa1 100644 --- a/src/pages/events/list/model/useEventListModel.ts +++ b/src/pages/events/list/model/useEventListModel.ts @@ -205,11 +205,11 @@ export function useEventListModel() { if (filterForm.region.length > 0) { params.areaName = filterForm.region[filterForm.region.length - 1]; } - if (debouncedName.value.trim()) { - params.name = debouncedName.value.trim(); + if (filterForm.name.trim()) { + params.name = filterForm.name.trim(); } - if (debouncedCreator.value.trim()) { - params.createName = debouncedCreator.value.trim(); + if (filterForm.createName.trim()) { + params.createName = filterForm.createName.trim(); } if (filterForm.status) { params.status = filterForm.status; @@ -249,6 +249,7 @@ export function useEventListModel() { setLoading(true); try { const queryParams = buildQueryParams(); + console.log('赛事列表查询参数:', queryParams); const res = await getEventList(queryParams); if (res.code === 200) { @@ -258,7 +259,7 @@ export function useEventListModel() { message.error(res.msg || '查询失败'); } } catch (error: any) { - message.error(error.msg || '查询失败'); + console.error('赛事列表查询失败:', error); } finally { setLoading(false); } @@ -277,6 +278,8 @@ export function useEventListModel() { filterForm.idCardImg = ''; setPagination({ current: 1, pageSize: 10, total: 0 }); setDataSource([]); + // 等待 debounce 生效后自动查询 + setTimeout(() => handleSearch(), 350); }, 500); const handlePageChange = (page: number, pageSize: number) => { diff --git a/src/pages/events/orders/components/OrderDetailModal.tsx b/src/pages/events/orders/components/OrderDetailModal.tsx index 66ad6fa..b94ab68 100644 --- a/src/pages/events/orders/components/OrderDetailModal.tsx +++ b/src/pages/events/orders/components/OrderDetailModal.tsx @@ -1,39 +1,74 @@ -import { defineComponent, computed } from 'vue'; -import { Modal, Table, Button, Pagination, Image, Space } from 'ant-design-vue'; +import { defineComponent, computed, onMounted } from 'vue'; +import { Modal, Table, Button, Pagination, Image, Space, Spin } from 'ant-design-vue'; import { StatusTag, type StatusTagTone } from '@/components'; import { useState } from '@/hooks'; +import { + getOrderDetail, + getOrderPlayerList, + type TournamentAdminOrderInfoVO, + type InnerRefundInfo, + type TournamentAdminOrderInfoPageVO, +} from '@/api/orders'; import styles from './OrderDetailModal.module.less'; interface OrderDetailModalProps { visible: boolean; - record: any; + orderNo: string; + uniqueId: string; onClose: () => void; - /** 触发"重新退款"二次确认对话框 */ - onReRefund: (record: any) => void; + onReRefund: () => void; } +// ===== 选手表格列(对齐 TournamentAdminOrderInfoPageVO) ===== +const GENDER_MAP: Record = { '1': '男', '2': '女' }; +const formatGender = (v: string) => GENDER_MAP[v] ?? v; +const formatMoney = (v: string) => { + const n = parseFloat(v); + return isNaN(n) ? '-' : `${n.toFixed(2)}`; +}; + const PLAYER_COLUMNS = [ - { title: '真实姓名', dataIndex: 'name', key: 'name', width: 100, align: 'center' as const }, - { title: '性别', dataIndex: 'gender', key: 'gender', width: 80, align: 'center' as const }, + { + title: '真实姓名', + dataIndex: 'realName', + key: 'realName', + width: 100, + align: 'center' as const, + }, + { + title: '性别', + dataIndex: 'gender', + key: 'gender', + width: 60, + align: 'center' as const, + customRender: ({ text }: { text: string }) => formatGender(text), + }, { title: '手机号', dataIndex: 'phone', key: 'phone', width: 130 }, - { title: '证件号', dataIndex: 'idNo', key: 'idNo', width: 180 }, + { title: '证件号', dataIndex: 'idCard', key: 'idCard', width: 180 }, { title: '证件图片', - dataIndex: 'idImg', - key: 'idImg', + key: 'idCardImg', width: 170, - customRender: ({ text }: { text: string[] }) => ( - - {(text || []).map((src) => ( - - ))} - - ), + customRender: ({ record }: { record: TournamentAdminOrderInfoPageVO }) => { + const urls = record.idCardImgList?.length + ? record.idCardImgList + : record.idCardImg + ? [record.idCardImg] + : []; + if (!urls.length) return -; + return ( + + {urls.map((src) => ( + + ))} + + ); + }, }, { title: '比赛组别', - dataIndex: 'groupName', - key: 'groupName', + dataIndex: 'categoryName', + key: 'categoryName', width: 110, align: 'center' as const, }, @@ -43,7 +78,7 @@ const PLAYER_COLUMNS = [ key: 'orderAmount', width: 100, align: 'right' as const, - customRender: ({ text }: { text: number }) => `${(text || 0).toFixed(2)}`, + customRender: ({ text }: { text: string }) => formatMoney(text), }, { title: '优惠金额', @@ -51,15 +86,15 @@ const PLAYER_COLUMNS = [ key: 'discountAmount', width: 100, align: 'right' as const, - customRender: ({ text }: { text: number }) => `${(text || 0).toFixed(2)}`, + customRender: ({ text }: { text: string }) => formatMoney(text), }, { title: '实付金额', - dataIndex: 'paidAmount', - key: 'paidAmount', + dataIndex: 'payAmount', + key: 'payAmount', width: 100, align: 'right' as const, - customRender: ({ text }: { text: number }) => `${(text || 0).toFixed(2)}`, + customRender: ({ text }: { text: string }) => formatMoney(text), }, { title: '退款金额', @@ -67,7 +102,7 @@ const PLAYER_COLUMNS = [ key: 'refundAmount', width: 100, align: 'right' as const, - customRender: ({ text }: { text: number }) => (text ? text.toFixed(2) : '-'), + customRender: ({ text }: { text: string }) => (text === '-' || !text ? '-' : formatMoney(text)), }, ]; @@ -80,43 +115,79 @@ export default defineComponent({ name: 'OrderDetailModal', props: { visible: { type: Boolean, default: false }, - record: { type: Object, default: () => ({}) }, + orderNo: { type: String, default: '' }, + uniqueId: { type: String, default: '' }, onClose: { type: Function, required: true }, onReRefund: { type: Function, required: true }, }, setup(props: OrderDetailModalProps) { - const [refundPage, setRefundPage] = useState(1); - const [refundPageSize, setRefundPageSize] = useState(2); + // ===== 数据状态 ===== + const [detail, setDetail] = useState(null); + const [detailLoading, setDetailLoading] = useState(true); - /** 退款记录 */ - const refundRecords = computed(() => props.record?.refundRecords || []); + // ===== 选手列表分页状态 ===== + const [players, setPlayers] = useState([]); + const [playersLoading, setPlayersLoading] = useState(true); + const [playerPage, setPlayerPage] = useState(1); + const [playerTotal, setPlayerTotal] = useState(0); + const playerPageSize = 5; + + /** 获取选手列表(分页) */ + const fetchPlayers = async (page: number) => { + setPlayersLoading(true); + try { + const res = await getOrderPlayerList({ + orderNo: props.orderNo, + uniqueId: props.uniqueId, + page: String(page), + limit: String(playerPageSize), + }); + if (res.code === 200) { + setPlayers(res.data.list); + setPlayerTotal(res.data.total); + setPlayerPage(page); + } + } finally { + setPlayersLoading(false); + } + }; + + // ===== 退款记录分页 ===== + const [refundPage, setRefundPage] = useState(1); + const [refundPageSize] = useState(2); + + const refundRecords = computed(() => detail.value?.refundInfoList || []); const hasRefund = computed(() => refundRecords.value.length > 0); - /** 当前页展示的退款记录 */ const pagedRefunds = computed(() => { const start = (refundPage.value - 1) * refundPageSize.value; return refundRecords.value.slice(start, start + refundPageSize.value); }); - /** 已退金额合计 */ - const totalRefunded = computed(() => - refundRecords.value - .filter((r) => r.status === 'success') - .reduce((sum, r) => sum + (r.amount || 0), 0), - ); + const totalRefunded = computed(() => { + const records = refundRecords.value; + if (!records.length) return 0; + return records.reduce((sum, r) => sum + parseFloat(r.refundAmount || '0'), 0); + }); - const handleRefundPageChange = (page: number, pageSize: number) => { + // ===== 初始化:获取详情 + 选手列表第一页 ===== + onMounted(async () => { + const params = { orderNo: props.orderNo, uniqueId: props.uniqueId }; + + const [detailRes] = await Promise.all([getOrderDetail(params), fetchPlayers(1)]); + + if (detailRes.code === 200) { + setDetail(detailRes.data); + } + setDetailLoading(false); + }); + + const handlePlayerPageChange = (page: number) => { + fetchPlayers(page); + }; + + const handleRefundPageChange = (page: number) => { setRefundPage(page); - setRefundPageSize(pageSize); - }; - - const handleConfirm = () => { - // 无退款信息时:仅关闭弹窗 - props.onClose(); - }; - - const handleReRefund = () => { - props.onReRefund(props.record); }; return () => ( @@ -139,130 +210,159 @@ export default defineComponent({
-
- {/* ===== 订单信息 ===== */} -
-
- 赛事名称: - {props.record?.eventName} -
-
- 订单编号: - {props.record?.orderId} -
-
- 订单时间: - {props.record?.orderTime} -
-
- 支付时间: - {props.record?.payTime} -
-
- 订单金额: - {props.record?.totalAmount?.toFixed(2)}元 -
-
- 优惠金额: - {(props.record?.discountAmount || 0).toFixed(2)}元 -
-
- 实付金额: - {props.record?.paidAmount?.toFixed(2)}元 -
-
- 退款金额: - {props.record?.refundAmount?.toFixed(2)}元 -
+ {detailLoading.value ? ( +
+
- - {/* ===== 选手信息 ===== */} -
-
选手信息
-
- - - {/* ===== 退款记录 ===== */} -
-
- 退款记录 + ) : detail.value ? ( +
+ {/* ===== 订单信息 ===== */} +
+
+ 赛事名称: + {detail.value.tournamentName} +
+
+ 订单编号: + {detail.value.orderNumber} +
+
+ 订单时间: + {detail.value.orderTime} +
+
+ 支付时间: + {detail.value.payTime} +
+
+ 订单金额: + {formatMoney(detail.value.orderAmount)}元 +
+
+ 优惠金额: + {formatMoney(detail.value.discountAmount)}元 +
+
+ 实付金额: + {formatMoney(detail.value.payAmount)}元 +
+
+ 退款金额: + {formatMoney(detail.value.refundAmount)}元 +
- {hasRefund.value ? ( - <> -
- 已退金额:{totalRefunded.value.toFixed(2)}元 - 退款次数:{refundRecords.value.length}次 -
- {/* 固定高度 + 内部滚动 */} -
- {pagedRefunds.value.map((r, idx) => { - const statusInfo = REFUND_STATUS_MAP[r.status] || { - label: '-', - tone: 'default' as const, - }; - const realIdx = (refundPage.value - 1) * refundPageSize.value + idx + 1; - return ( -
-
- 第{realIdx}次退款 - -
-
-
-
- 退款金额: - - ¥{r.amount.toFixed(2)} - {r.isPartial && ( - (部分退款) - )} - + + {/* ===== 选手信息 ===== */} +
+
选手信息
+
+
+ +
+ + + {/* ===== 退款记录 ===== */} +
+
+ 退款记录 +
+ {hasRefund.value ? ( + <> +
+ 已退金额:{totalRefunded.value.toFixed(2)}元 + 退款次数:{refundRecords.value.length}次 +
+
+ {pagedRefunds.value.map((r, idx) => { + const realIdx = (refundPage.value - 1) * refundPageSize.value + idx + 1; + const isSuccess = parseFloat(r.refundTime || '') > 0 || !!r.refundTime; + const statusInfo = { + label: isSuccess + ? '退款成功' + : r.isFullRefund === '1' + ? '退款待处理' + : '退款中', + tone: (isSuccess + ? 'success' + : r.isFullRefund === '1' + ? 'warning' + : 'orange') as StatusTagTone, + }; + return ( +
+
+ 第{realIdx}次退款 + +
+
+
+
+ 退款金额: + + ¥{formatMoney(r.refundAmount)} + {r.isFullRefund === '1' && ( + (全部退款) + )} + {r.isFullRefund === '0' && ( + (部分退款) + )} + +
+
申请时间:{r.refundApplyTime}
+
完款时间:{r.refundTime || '-'}
+
+
+
退款方式:{r.refundMethod}
+
第三方退款单号:{r.refundOutTradeNo || '-'}
+
已退金额:¥{formatMoney(r.refundedAmount)}
-
申请时间:{r.applyTime}
-
完款时间:{r.completeTime || '-'}
-
-
-
退款方式:{r.method}
-
第三方退款单号:{r.thirdPartyNo || '-'}
-
- ); - })} -
-
- -
- - ) : ( -
暂无退款记录
- )} + ); + })} + +
+ +
+ + ) : ( +
暂无退款记录
+ )} + - + ) : null} {/* ===== 底部按钮 ===== */}
{hasRefund.value ? ( - ) : ( - )} diff --git a/src/pages/events/orders/index.tsx b/src/pages/events/orders/index.tsx index 12f81be..2e050df 100644 --- a/src/pages/events/orders/index.tsx +++ b/src/pages/events/orders/index.tsx @@ -1,4 +1,4 @@ -import { defineComponent } from 'vue'; +import { defineComponent, onMounted } from 'vue'; import { Button, Input, @@ -10,8 +10,10 @@ import { Tooltip, Pagination, Modal, + message, } from 'ant-design-vue'; import { useOrderModel, ORDER_STATUS_OPTIONS, REFUND_STATUS_OPTIONS } from './model/useOrderModel'; +import { retryRefundOrder } from '@/api/orders'; import { useContainerSize, useState } from '@/hooks'; import OrderDetailModal from './components/OrderDetailModal'; import pageStyles from '@/assets/styles/pageLayout.module.less'; @@ -20,22 +22,29 @@ const { RangePicker } = DatePicker; /** * 触发「重新退款」二次确认对话框 - * 表格中的重新退款按钮和详情弹窗中的重新退款按钮均调用此函数。 */ function promptReRefund(record: any) { Modal.confirm({ title: '确认要重新退款吗?', content: (
-
退款金额:¥{(record.refundAmount || 0).toFixed(2)}
+
退款金额:¥{parseFloat(record.creatorSignupFeeRefund || '0').toFixed(2)}
退款方式:原路退回(微信支付)
), okText: '确认', cancelText: '取消', - onOk: () => { - console.log('重新退款:', record.orderId); - // TODO: 替换为真实 API 调用 + onOk: async () => { + try { + const res = await retryRefundOrder({ payOrderNo: record.orderNo }); + if (res.code === 200) { + message.success('重新退款提交成功'); + } else { + message.error(res.msg || '操作失败'); + } + } catch (e: any) { + console.error('重新退款失败:', e); + } }, }); } @@ -57,7 +66,7 @@ function renderBodyCell({ onReRefund: (record: any) => void; }) { // 赛事名称:最多15字符,超出显示 Tooltip - if (column.dataIndex === 'eventName') { + if (column.dataIndex === 'name') { const maxLen = 15; const raw = text || ''; const display = raw.length > maxLen ? raw.slice(0, maxLen) + '...' : raw; @@ -71,7 +80,7 @@ function renderBodyCell({ } if (column.key === 'action') { - const showReRefund = record.refundStatus === 'refund_failed'; + const showReRefund = record.refundStatus === 3; return (
- {/* 独立分页,右下方 */}
- {/* ===== 新增/编辑弹窗 ===== */} {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} /> - + handleRangeChange(val)} + onUpdate:value={handleRangeChange} options={[...RANGE_OPTIONS]} style={{ width: '120px' }} /> diff --git a/src/pages/finance/reports/model/useReportsModel.ts b/src/pages/finance/reports/model/useReportsModel.ts index 80aae9c..59b4805 100644 --- a/src/pages/finance/reports/model/useReportsModel.ts +++ b/src/pages/finance/reports/model/useReportsModel.ts @@ -1,6 +1,9 @@ import { ref } from 'vue'; +import type { SelectValue } from 'ant-design-vue/es/select'; import { useState } from '@/hooks'; +export type RangeValue = (typeof RANGE_OPTIONS)[number]['value']; + // ============================================================ // 常量 // ============================================================ @@ -13,6 +16,10 @@ export const RANGE_OPTIONS = [ { value: 'year', label: '本年' }, ] as const; +function isRangeValue(val: SelectValue): val is RangeValue { + return typeof val === 'string' && RANGE_OPTIONS.some((item) => item.value === val); +} + // ============================================================ // 假数据(数值自洽:总收入 - 总提现 = 平台余额) // ============================================================ @@ -91,10 +98,11 @@ function formatMoney(n: number): string { */ export function useReportsModel() { // ===== 时间范围 ===== - const [range, setRange] = useState('month'); + const [range, setRange] = useState('month'); /** 切换时间范围(演示用:打日志,不改数据) */ - const handleRangeChange = (val: string) => { + const handleRangeChange = (val: SelectValue) => { + if (!isRangeValue(val)) return; setRange(val); // TODO: 接入 API 时按 range 重新拉取汇总 / 图表 / 明细 }; diff --git a/src/pages/system/roles/components/RoleFormModal.module.less b/src/pages/system/roles/components/RoleFormModal.module.less index a0c05a0..467e9ac 100644 --- a/src/pages/system/roles/components/RoleFormModal.module.less +++ b/src/pages/system/roles/components/RoleFormModal.module.less @@ -24,6 +24,12 @@ background: #fafafa; } + .treeEmpty { + text-align: center; + color: #999; + padding: 24px 0; + } + .footer { display: flex; justify-content: flex-end; diff --git a/src/pages/system/roles/components/RoleFormModal.tsx b/src/pages/system/roles/components/RoleFormModal.tsx index 15c1caf..2b65e7e 100644 --- a/src/pages/system/roles/components/RoleFormModal.tsx +++ b/src/pages/system/roles/components/RoleFormModal.tsx @@ -1,8 +1,13 @@ -import { defineComponent, ref, reactive } from 'vue'; -import { useEffect } from '@/hooks'; -import { Modal, Form, Input, Tree, Button } from 'ant-design-vue'; +import { defineComponent, ref, reactive, watch } from 'vue'; +import { Modal, Form, Input, Tree, Button, Spin, message } from 'ant-design-vue'; import type { TreeProps } from 'ant-design-vue'; -import { roleNameRules, PERMISSION_TREE } from '../controller'; +import { getPermissionTree, type PermissionTreeNode } from '@/api/roles'; +import { + roleNameRules, + buildTreeData, + extractLeafPermissions, + type AntdTreeNode, +} from '../controller'; import styles from './RoleFormModal.module.less'; interface RoleFormModalProps { @@ -17,21 +22,9 @@ interface RoleFormModalProps { const getDefaultForm = () => ({ roleName: '', checkedKeys: [] as string[], + halfCheckedKeys: [] as string[], }); -/** 编辑模式默认勾选:所有权限(演示用) */ -const ALL_KEYS = (() => { - const keys: string[] = []; - const walk = (nodes: any[]) => { - for (const n of nodes) { - keys.push(n.key); - if (n.children?.length) walk(n.children); - } - }; - walk(PERMISSION_TREE); - return keys; -})(); - export default defineComponent({ name: 'RoleFormModal', props: { @@ -45,34 +38,68 @@ export default defineComponent({ const formRef = ref(); const formData = reactive(getDefaultForm()); + // 权限树数据 & 加载状态 + const treeData = ref([]); + const treeLoading = ref(false); + // 保存从 API 获取的原始树,提交时用于提取叶节点 + const rawApiTree = ref([]); + + // ===== 加载权限树 ===== + const loadPermissionTree = async (roleId: number | string) => { + treeLoading.value = true; + try { + const res = await getPermissionTree(roleId); + if (res.code === 200 && res.data) { + rawApiTree.value = res.data; + const result = buildTreeData(res.data); + treeData.value = result.treeData; + // checkedKeys 由叶节点 selected 推导,antd Tree checkStrictly=false 时自动级联 + formData.checkedKeys = result.checkedKeys; + } else { + message.error(res.msg || '获取权限树失败'); + } + } catch { + // 网络层已统一提示,不再重复 message.error + } finally { + treeLoading.value = false; + } + }; + /** 根据 record 初始化表单 */ - const initFormFromRecord = (record: any) => { + const initFormFromRecord = async (record: any) => { const fresh = getDefaultForm(); if (!record) { + // 新增模式:加载权限树(不传 roleId,后端返回全量未选中树) Object.assign(formData, fresh); + await loadPermissionTree(0); return; } + // 编辑模式:回填角色名 + 加载该角色的权限树 Object.assign(formData, { ...fresh, roleName: record.roleName || '', - // 编辑时默认全选(演示用,后续应按 record.permissions 回填) - checkedKeys: [...ALL_KEYS], }); + await loadPermissionTree(record.roleId); }; - /** 监听 visible 重置 */ - useEffect(() => { - if (props.visible) { - initFormFromRecord(props.record); - setTimeout(() => formRef.value?.clearValidate(), 0); - } - }, [() => props.visible]); + // 监听 visible 变化,弹窗打开时重新初始化 + // immediate: true 是因为父组件用 v-if 控制显隐, + // 组件挂载时 visible 就已经是 true,不会触发 watch 变更回调 + watch( + () => props.visible, + async (visible) => { + if (visible) { + await initFormFromRecord(props.record); + setTimeout(() => formRef.value?.clearValidate(), 0); + } + }, + { immediate: true }, + ); /** Tree 勾选回调(受控) */ - const handleCheck: TreeProps['onCheck'] = (checked) => { - // checked 可能是 { checked: string[], halfChecked: string[] } 或 string[] - const keys = Array.isArray(checked) ? checked : (checked as any).checked; - formData.checkedKeys = keys as string[]; + const handleCheck: TreeProps['onCheck'] = (checkedKeys, info) => { + formData.checkedKeys = checkedKeys as string[]; + formData.halfCheckedKeys = (info as any).halfCheckedKeys || []; }; /** 提交 */ @@ -82,9 +109,19 @@ export default defineComponent({ } catch { return; } + + const isEdit = !!props.record; + // 仅提取叶节点 { id, selected } 传给后端 + const leafPermissions = extractLeafPermissions(rawApiTree.value); + props.onSave({ roleName: formData.roleName, + roleId: props.record?.roleId, + // 完整选中 key 列表(含父节点),供向后兼容 permissions: formData.checkedKeys, + // 仅叶节点数据,后端实际需要的格式 + leafPermissions, + isEdit, }); }; @@ -116,15 +153,21 @@ export default defineComponent({ {/* 菜单权限配置 */} -
- -
+ +
+ {treeData.value.length > 0 ? ( + + ) : ( + !treeLoading.value &&
暂无权限数据
+ )} +
+
diff --git a/src/pages/system/roles/components/UserListModal.tsx b/src/pages/system/roles/components/UserListModal.tsx index f75b542..d8172ca 100644 --- a/src/pages/system/roles/components/UserListModal.tsx +++ b/src/pages/system/roles/components/UserListModal.tsx @@ -1,13 +1,21 @@ -import { defineComponent, ref, reactive, computed } from 'vue'; -import { Modal, Form, Input, Table, Button, Space, Pagination } from 'ant-design-vue'; -import { useEffect, useState } from '@/hooks'; +import { defineComponent, reactive } from 'vue'; +import { Modal, Form, Input, Table, Button, Space, Pagination, message } from 'ant-design-vue'; +import { h } from 'vue'; +import { StatusTag, type StatusTagTone } from '@/components'; +import { useState, useThrottleFn, useEffect } from '@/hooks'; +import { getRoleUsageUserList, type RoleUsageUserVO } from '@/api/roles'; import styles from './UserListModal.module.less'; +/** 状态映射 */ +const STATUS_MAP: Record = { + 1: { label: '正常', tone: 'success' }, + 0: { label: '停用', tone: 'danger' }, +}; + interface UserListModalProps { visible: boolean; - role: any; - users: any[]; - renderStatus: (status: string) => any; + roleId: number; + roleName: string; onClose: () => void; } @@ -15,123 +23,155 @@ export default defineComponent({ name: 'RoleUserListModal', props: { visible: { type: Boolean, default: false }, - role: { type: Object, default: null }, - users: { type: Array, default: () => [] }, - renderStatus: { type: Function, required: true }, + roleId: { type: Number, default: 0 }, + roleName: { type: String, default: '' }, onClose: { type: Function, required: true }, }, setup(props: UserListModalProps) { - const filterForm = reactive({ searchKeyword: '' }); - + const filterForm = reactive({ text: '' }); + const [loading, setLoading] = useState(false); + const [dataSource, setDataSource] = useState([]); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }); - /** 过滤后的用户列表 */ - const filteredUsers = computed(() => { - const kw = filterForm.searchKeyword.trim(); - if (!kw) return props.users; - return props.users.filter( - (u: any) => u.realName.includes(kw) || (u.phone && u.phone.includes(kw)), - ); - }); + const columns = [ + { title: '姓名', dataIndex: 'realName', key: 'realName', width: 120 }, + { title: '手机号', dataIndex: 'phone', key: 'phone', width: 160 }, + { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 180 }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 90, + align: 'center' as const, + customRender: ({ text }: { text: number }) => { + const info = STATUS_MAP[text] || { label: String(text ?? '-'), tone: 'default' as const }; + return h(StatusTag, { label: info.label, tone: info.tone }); + }, + }, + ]; - /** 监听 visible/users 变化重置分页 */ + /** 搜索(节流 500ms) */ + const doSearch = useThrottleFn(async () => { + setLoading(true); + try { + const res = await getRoleUsageUserList({ + page: String(pagination.value.current), + limit: String(pagination.value.pageSize), + roleId: String(props.roleId), + ...(filterForm.text.trim() ? { text: filterForm.text.trim() } : {}), + }); + if (res.code === 200) { + setDataSource(res.data.list); + setPagination({ ...pagination.value, total: res.data.total }); + } else { + message.error(res.msg || '查询失败'); + } + } catch (e: any) { + console.error('角色用户列表查询失败:', e); + } finally { + setLoading(false); + } + }, 500); + + /** 弹窗打开时自动加载 */ useEffect(() => { - setPagination({ current: 1, pageSize: 10, total: filteredUsers.value.length }); - }, [() => props.visible, () => props.users.length]); + if (props.visible) { + filterForm.text = ''; + setPagination({ current: 1, pageSize: 10, total: 0 }); + setDataSource([]); + doSearch(); + } + }, [() => props.visible]); + /** 查询(重置到第 1 页) */ const handleSearch = () => { - setPagination({ ...pagination.value, total: filteredUsers.value.length, current: 1 }); + setPagination({ ...pagination.value, current: 1 }); + doSearch(); }; - const handleReset = () => { - filterForm.searchKeyword = ''; - setPagination({ current: 1, pageSize: 10, total: props.users.length }); - }; + /** 重置 */ + const handleReset = useThrottleFn(() => { + filterForm.text = ''; + setPagination({ current: 1, pageSize: 10, total: 0 }); + setDataSource([]); + doSearch(); + }, 500); + /** 分页变更 */ const handlePageChange = (page: number, pageSize: number) => { setPagination({ ...pagination.value, current: page, pageSize }); + doSearch(); }; - return () => { - const roleName = props.role?.roleName || ''; - - const columns = [ - { title: '姓名', dataIndex: 'realName', key: 'realName', width: 120 }, - { title: '手机号', dataIndex: 'phone', key: 'phone', width: 160 }, - { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 200 }, - { - title: '状态', - dataIndex: 'status', - key: 'status', - width: 100, - align: 'center' as const, - customRender: ({ text }: { text: string }) => props.renderStatus(text), - }, - ]; - - return ( - - {/* 筛选 */} -
-
- - - - - - - - - -
-
- - {/* 表格 */} - - - {/* 底部:共 X 人 + 关闭按钮 */} -
-
共 {filteredUsers.value.length} 人
- -
- - {/* 分页(右侧) */} - {filteredUsers.value.length > pagination.value.pageSize && ( -
- ( + + {/* 筛选 */} +
+
+ + (filterForm.text = val?.target?.value ?? val ?? '')} + onPressEnter={handleSearch} /> -
- )} -
- ); - }; + + + + + + + + +
+ + {/* 表格 */} +
+ + {/* 底部 */} +
+
共 {pagination.value.total} 人
+ +
+ + {/* 分页 */} + {pagination.value.total > pagination.value.pageSize && ( +
+ `共 ${total} 条`} + onChange={handlePageChange} + onShowSizeChange={handlePageChange} + /> +
+ )} + + ); }, }); diff --git a/src/pages/system/roles/controller.ts b/src/pages/system/roles/controller.ts index fe1dba0..78e1ed2 100644 --- a/src/pages/system/roles/controller.ts +++ b/src/pages/system/roles/controller.ts @@ -1,9 +1,10 @@ /** * 角色权限 - 表单域控制器 * - * 集中管理角色表单的规则、菜单权限树 mock 数据与提示文案。 + * 集中管理角色表单的规则、权限树转换工具。 */ import type { Rule } from 'ant-design-vue/es/form'; +import type { PermissionTreeNode } from '@/api/roles'; // ============================================================ // 规则 @@ -16,50 +17,82 @@ export const roleNameRules: Rule[] = [ ]; // ============================================================ -// 菜单权限树(mock) +// 权限树 — 类型 & 工具 // ============================================================ -export interface MenuPermNode { - /** 唯一 key,作为 Tree 节点的 key */ +/** Ant Design Tree 组件使用的节点结构 */ +export interface AntdTreeNode { key: string; - /** 显示标题 */ title: string; - /** 子节点 */ - children?: MenuPermNode[]; + children?: AntdTreeNode[]; } /** - * 完整菜单权限树。 - * 后续可改为从后端接口拉取(接口应返回该结构)。 + * 将后端权限树节点递归转为 antd Tree 格式, + * 同时收集叶节点中 selected===true 的 key。 + * + * ## 级联规则 + * - `selected` 仅叶节点有实际值(true / false / null) + * - 父节点的勾选状态由子节点的 selected 推导(全选=勾选,部分选=半选) + * - antd Tree 的 checkedKeys 传所有选中的 key(含父节点),Tree 自动处理级联 */ -export const PERMISSION_TREE: MenuPermNode[] = [ - { - key: 'events', - title: '赛事管理', - children: [ - { - key: 'events.list', - title: '赛事列表', - children: [ - { key: 'events.list.view', title: '赛事列表-查看' }, - { key: 'events.list.on', title: '赛事列表-上架' }, - { key: 'events.list.off', title: '赛事列表-下架' }, - { key: 'events.rules.view', title: '赛事规程-查看' }, - ], - }, - { - key: 'orders', - title: '订单管理', - children: [ - { key: 'orders.view', title: '订单管理-查看' }, - { key: 'orders.refund', title: '订单管理-重新退款' }, - ], - }, - { key: 'users', title: '用户列表' }, - ], - }, - { - key: 'events_logs', - title: '赛事操作日志', - }, -]; +function convertNode(node: PermissionTreeNode, leafKeys: Set): AntdTreeNode { + const key = String(node.id); + const hasChildren = node.children && node.children.length > 0; + + if (!hasChildren) { + // 叶节点:根据 selected 收集 + if (node.selected === true) { + leafKeys.add(key); + } + return { key, title: node.name }; + } + + // 父节点:递归处理子节点 + const children = node.children!.map((child) => convertNode(child, leafKeys)); + return { key, title: node.name, children }; +} + +/** + * 将后端 PermissionTreeNode[] 转为: + * 1. antd Tree 可用的 treeData + * 2. 由叶节点 selected 推导出的完整 checkedKeys(含父节点) + */ +export function buildTreeData(apiTree: PermissionTreeNode[]): { + treeData: AntdTreeNode[]; + checkedKeys: string[]; +} { + const leafKeys = new Set(); + const treeData = apiTree.map((node) => convertNode(node, leafKeys)); + return { treeData, checkedKeys: Array.from(leafKeys) }; +} + +/** + * 从后端权限树中提取叶节点数据,整理为提交格式。 + * + * 只返回叶节点,格式为 `{ id, selected }` 数组。 + * 父节点的勾选状态由后端根据叶节点数据推导,前端不传父节点。 + */ +export function extractLeafPermissions( + apiTree: PermissionTreeNode[], +): { id: number; selected: boolean }[] { + const result: { id: number; selected: boolean }[] = []; + + const walk = (nodes: PermissionTreeNode[]) => { + for (const node of nodes) { + const isLeaf = !node.children || node.children.length === 0; + if (isLeaf) { + result.push({ + id: node.id, + // null 视为 false,即未授权 + selected: node.selected === true, + }); + } else { + walk(node.children!); + } + } + }; + + walk(apiTree); + return result; +} diff --git a/src/pages/system/roles/index.tsx b/src/pages/system/roles/index.tsx index 2153780..9edc3c2 100644 --- a/src/pages/system/roles/index.tsx +++ b/src/pages/system/roles/index.tsx @@ -1,4 +1,4 @@ -import { defineComponent } from 'vue'; +import { defineComponent, onMounted } from 'vue'; import { Button, Input, Table, Form, Space, Select, Modal, Pagination } from 'ant-design-vue'; import type { ModalProps } from 'ant-design-vue'; import { useRoleModel } from './model/useRoleModel'; @@ -97,8 +97,6 @@ export default defineComponent({ handleDelete, handleViewUsers, handleCloseUserList, - getRoleUsers, - renderStatus, } = useRoleModel(); const { containerRef, height } = useContainerSize(); @@ -116,16 +114,23 @@ export default defineComponent({ }, ]; + // ===== 初始化 ===== + onMounted(() => { + handleSearch(); + }); + return () => (
{/* ===== 筛选区 ===== */}
- + (filterForm.name = val?.target?.value ?? val ?? '')} onPressEnter={handleSearch} /> @@ -194,9 +199,8 @@ export default defineComponent({ {userListVisible.value && ( )} diff --git a/src/pages/system/roles/model/useRoleModel.ts b/src/pages/system/roles/model/useRoleModel.ts index ca3bdb5..cad00e3 100644 --- a/src/pages/system/roles/model/useRoleModel.ts +++ b/src/pages/system/roles/model/useRoleModel.ts @@ -1,152 +1,12 @@ -import { computed, reactive, toRef, Ref, h } from 'vue'; +import { computed, reactive, toRef, Ref } from 'vue'; import { message } from 'ant-design-vue'; -import dayjs from 'dayjs'; -import { StatusTag, type StatusTagTone } from '@/components'; import { useState, useDebounce, useThrottleFn } from '@/hooks'; - -// ============================================================ -// 假数据 -// ============================================================ - -const MOCK_DATA = [ - { - key: '1', - roleId: 'R20250601001', - roleName: '超级管理员', - userCount: 1, - createTime: '2025-06-01 09:00:00', - }, - { - key: '2', - roleId: 'R20250815001', - roleName: '运营管理员', - userCount: 3, - createTime: '2025-08-15 14:30:00', - }, - { - key: '3', - roleId: 'R20251010001', - roleName: '财务', - userCount: 2, - createTime: '2025-10-10 10:15:00', - }, - { - key: '4', - roleId: 'R20260120001', - roleName: '客服', - userCount: 4, - createTime: '2026-01-20 16:00:00', - }, -]; - -/** - * 临时排序:对假数据按创建时间倒序排列。 - * 【注意】实际项目中排序由后端接口负责,对接后端后请删除此函数及相关调用。 - */ -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'); - -/** 角色下用户列表 mock */ -const MOCK_USERS: Record = { - R20250601001: [ - { - key: '1', - userId: 'U001', - realName: '张超管', - phone: '13900000001', - createTime: '2025-06-01 09:00:00', - status: 'active', - }, - ], - R20250815001: [ - { - key: '1', - userId: 'U002', - realName: '李运营', - phone: '15900000002', - createTime: '2026-01-10 16:00:00', - status: 'disabled', - }, - { - key: '2', - userId: 'U003', - realName: '王运营', - phone: '15900000001', - createTime: '2025-10-20 10:00:00', - status: 'active', - }, - { - key: '3', - userId: 'U004', - realName: '张运营', - phone: '13900000002', - createTime: '2025-08-15 14:30:00', - status: 'active', - }, - ], - R20251010001: [ - { - key: '1', - userId: 'U005', - realName: '赵财务', - phone: '13800000005', - createTime: '2025-10-10 10:15:00', - status: 'active', - }, - { - key: '2', - userId: 'U006', - realName: '钱财务', - phone: '13800000006', - createTime: '2025-10-12 11:00:00', - status: 'active', - }, - ], - R20260120001: [ - { - key: '1', - userId: 'U007', - realName: '孙客服', - phone: '13700000007', - createTime: '2026-01-20 16:00:00', - status: 'active', - }, - { - key: '2', - userId: 'U008', - realName: '周客服', - phone: '13700000008', - createTime: '2026-01-21 09:30:00', - status: 'active', - }, - { - key: '3', - userId: 'U009', - realName: '吴客服', - phone: '13700000009', - createTime: '2026-01-22 10:00:00', - status: 'active', - }, - { - key: '4', - userId: 'U010', - realName: '郑客服', - phone: '13700000010', - createTime: '2026-01-23 14:00:00', - status: 'active', - }, - ], -}; - -// ============================================================ -// 状态文本/颜色映射 -// ============================================================ -const STATUS_MAP: Record = { - active: { label: '正常', tone: 'success' }, - disabled: { label: '禁用', tone: 'danger' }, -}; +import { + getRoleList, + deleteRole, + type TournamentAdminRolePageVO, + type RoleListQueryParams, +} from '@/api/roles'; // ============================================================ // Model @@ -158,23 +18,17 @@ const STATUS_MAP: Record = { export function useRoleModel() { // ===== 筛选条件 ===== const filterForm = reactive({ - searchKeyword: '', + name: '', // 角色名称 }); - // 防抖 300ms - const { debouncedValue: debouncedKeyword } = useDebounce( - toRef(filterForm, 'searchKeyword') as Ref, - { delay: 300 }, - ); + const { debouncedValue: debouncedName } = useDebounce(toRef(filterForm, 'name') as Ref, { + delay: 300, + }); // ===== 表格状态 ===== const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState(SORTED_MOCK_DATA); - const [pagination, setPagination] = useState({ - current: 1, - pageSize: 10, - total: SORTED_MOCK_DATA.length, - }); + const [dataSource, setDataSource] = useState([]); + const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }); // ===== 弹窗状态 ===== const [formVisible, setFormVisible] = useState(false); @@ -199,34 +53,48 @@ export function useRoleModel() { { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 200 }, ]; - // ===== 计算属性 ===== - const hasFilter = computed(() => debouncedKeyword.value.trim() !== ''); + // ===== 构建 API 查询参数 ===== + const buildQueryParams = (): RoleListQueryParams => { + const params: RoleListQueryParams = { + page: String(pagination.value.current), + limit: String(pagination.value.pageSize), + }; + if (filterForm.name.trim()) params.name = filterForm.name.trim(); + return params; + }; + // ===== 计算属性 ===== + const hasFilter = computed(() => debouncedName.value.trim() !== ''); const isEdit = computed(() => editingRecord.value !== null); // ===== 方法 ===== - /** 查询 */ + /** 查询(节流 500ms) */ const handleSearch = useThrottleFn(async () => { setLoading(true); try { - console.log('搜索条件:', { keyword: debouncedKeyword.value }); - // TODO: 替换为真实 API 调用 - setDataSource(SORTED_MOCK_DATA); - setPagination({ ...pagination.value, total: SORTED_MOCK_DATA.length }); - message.success('查询成功'); + const queryParams = buildQueryParams(); + console.log('角色列表查询参数:', queryParams); + const res = await getRoleList(queryParams); + if (res.code === 200) { + setDataSource(res.data.list); + setPagination({ ...pagination.value, total: res.data.total }); + } else { + message.error(res.msg || '查询失败'); + } } catch (error: any) { - message.error(error.msg || '查询失败'); + console.error('角色列表查询失败:', error); } finally { setLoading(false); } }, 500); - /** 重置 */ + /** 重置(重置后自动查询) */ const handleReset = useThrottleFn(() => { - filterForm.searchKeyword = ''; - setPagination({ current: 1, pageSize: 10, total: SORTED_MOCK_DATA.length }); - setDataSource(SORTED_MOCK_DATA); + filterForm.name = ''; + setPagination({ current: 1, pageSize: 10, total: 0 }); + setDataSource([]); + setTimeout(() => handleSearch(), 350); }, 500); const handlePageChange = (page: number, pageSize: number) => { @@ -258,12 +126,12 @@ export function useRoleModel() { setFormSubmitting(true); try { console.log(isEdit.value ? '编辑角色' : '新增角色', formPayload); - // TODO: 替换为真实 API 调用 + // TODO: 替换为真实 API 调用(/sys/role/save、/sys/role/update) message.success(isEdit.value ? '编辑成功' : '新增成功'); handleCloseForm(); handleSearch(); - } catch (error: any) { - message.error(error.msg || '操作失败'); + } catch { + // 网络层已统一提示,不再重复 message.error } finally { setFormSubmitting(false); } @@ -273,12 +141,15 @@ export function useRoleModel() { const handleDelete = useThrottleFn(async (record: any) => { setLoading(true); try { - console.log('删除角色:', record.roleId); - // TODO: 替换为真实 API 调用 - setDataSource(dataSource.value.filter((item: any) => item.key !== record.key)); - message.success('删除成功'); - } catch (error: any) { - message.error(error.msg || '删除失败'); + const res = await deleteRole(record.roleId); + if (res.code === 200) { + setDataSource(dataSource.value.filter((item: any) => item.roleId !== record.roleId)); + message.success('删除成功'); + } else { + message.error(res.msg || '删除失败'); + } + } catch { + // 网络层已统一提示,不再重复 message.error } finally { setLoading(false); } @@ -296,15 +167,6 @@ export function useRoleModel() { setCurrentRole(null); }; - /** 根据 roleId 获取该角色下的用户列表 */ - const getRoleUsers = (roleId: string) => MOCK_USERS[roleId] || []; - - /** 状态 StatusTag 渲染 */ - const renderStatus = (status: string) => { - const info = STATUS_MAP[status] || { label: status || '-', tone: 'default' as const }; - return h(StatusTag, { label: info.label, tone: info.tone }); - }; - return { filterForm, loading, @@ -328,7 +190,5 @@ export function useRoleModel() { handleDelete, handleViewUsers, handleCloseUserList, - getRoleUsers, - renderStatus, }; } diff --git a/src/pages/system/users/components/UserFormModal.tsx b/src/pages/system/users/components/UserFormModal.tsx index d282a68..5eacad5 100644 --- a/src/pages/system/users/components/UserFormModal.tsx +++ b/src/pages/system/users/components/UserFormModal.tsx @@ -30,7 +30,7 @@ const getDefaultForm = () => ({ }); /** 新增用户默认密码 */ -const DEFAULT_USER_PASSWORD = 'A8888888'; +const DEFAULT_USER_PASSWORD = 'yp888888'; /** 获取新增时的表单初始值(预填默认密码) */ const getAddForm = () => ({ -- 2.52.0