feat: 接口联调

This commit is contained in:
2026-08-03 19:12:33 +08:00
parent 3e94ce60ec
commit 2497e6aabb
29 changed files with 941 additions and 1283 deletions
@@ -1,4 +1,4 @@
import { defineComponent, reactive, onMounted } from 'vue';
import { defineComponent, reactive, ref } from 'vue';
import { Modal, Descriptions, Image, Table, Pagination, Space, Spin } from 'ant-design-vue';
import { EyeOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
@@ -9,7 +9,7 @@ import {
type InnerCategoryVO,
type TournamentAdminInfoSignupVO,
} from '../model/services';
import { useState } from '@/hooks';
import { useState, useEffect } from '@/hooks';
import styles from './EventDetailModal.module.less';
interface EventDetailModalProps {
@@ -67,8 +67,8 @@ const PLAYER_COLUMNS = [
title: '证件图片',
key: 'idCardImgs',
width: 180,
customRender: ({ record }: { record: TournamentAdminInfoSignupVO }) =>
renderIdImages(record.idCardImgList, record.idCardImg),
customRender: ({ record }: { record?: TournamentAdminInfoSignupVO }) =>
record ? renderIdImages(record.idCardImgList || [], record.idCardImg || '') : <span>-</span>,
},
];
@@ -82,17 +82,19 @@ export default defineComponent({
setup(props: EventDetailModalProps) {
// ===== 详情数据 =====
const [detail, setDetail] = useState<TournamentAdminInfoVO | null>(null);
const [detailLoading, setDetailLoading] = useState(true);
const [detailLoading, setDetailLoading] = useState(false);
// ===== 每个组别的选手数据(key = categoryId =====
const categoryPlayers = reactive<Record<string, TournamentAdminInfoSignupVO[]>>({});
const categoryPlayerTotal = reactive<Record<string, number>>({});
const categoryPlayerLoading = reactive<Record<string, boolean>>({});
const categoryPlayerPage = reactive<Record<string, number>>({});
const categoryPlayers = ref<Record<string, TournamentAdminInfoSignupVO[]>>({});
const categoryPlayerTotal = ref<Record<string, number>>({});
const categoryPlayerLoading = ref<Record<string, boolean>>({});
const categoryPlayerPage = ref<Record<string, number>>({});
/** 获取某个组别的选手列表(分页) */
const fetchPlayers = async (categoryId: string, page: number) => {
categoryPlayerLoading[categoryId] = true;
categoryPlayerLoading.value[categoryId] = true;
// 触发响应式更新
categoryPlayerLoading.value = { ...categoryPlayerLoading.value };
try {
const res = await getSignupList({
categoryId,
@@ -100,14 +102,18 @@ export default defineComponent({
limit: String(PAGE_SIZE),
});
if (res.code == 200) {
categoryPlayers[categoryId] = res.data.list;
categoryPlayerTotal[categoryId] = res.data.total;
categoryPlayerPage[categoryId] = page;
categoryPlayers.value[categoryId] = res.data.list;
categoryPlayers.value = { ...categoryPlayers.value };
categoryPlayerTotal.value[categoryId] = res.data.total;
categoryPlayerTotal.value = { ...categoryPlayerTotal.value };
categoryPlayerPage.value[categoryId] = page;
categoryPlayerPage.value = { ...categoryPlayerPage.value };
}
} catch {
// mock 模式不会到这里,真实模式下由 API 层处理
} finally {
categoryPlayerLoading[categoryId] = false;
categoryPlayerLoading.value[categoryId] = false;
categoryPlayerLoading.value = { ...categoryPlayerLoading.value };
}
};
@@ -116,24 +122,19 @@ export default defineComponent({
fetchPlayers(categoryId, page);
};
// ===== 初始化 =====
onMounted(async () => {
/** 获取赛事详情 */
const fetchDetail = async (id: string) => {
setDetailLoading(true);
setDetail(null);
try {
const id = props.record?.id;
if (!id) {
message.error('缺少赛事ID');
return;
}
const res = await getEventDetail(String(id));
if (res.code == 200) {
const res = await getEventDetail(id);
if (res.code == 200 && res.data) {
setDetail(res.data);
// 为每个组别加载第一页选手
for (const cat of res.data.categoryList) {
categoryPlayers[cat.id] = [];
categoryPlayerTotal[cat.id] = 0;
categoryPlayerPage[cat.id] = 1;
categoryPlayers.value[cat.id] = [];
categoryPlayerTotal.value[cat.id] = 0;
categoryPlayerPage.value[cat.id] = 1;
fetchPlayers(cat.id, 1);
}
} else {
@@ -144,7 +145,14 @@ export default defineComponent({
} finally {
setDetailLoading(false);
}
});
};
// 弹窗打开且 有 record.id 时自动请求(useEffect 带 immediate: true,兼容 destroyOnClose 重建场景)
useEffect(() => {
if (props.visible && props.record?.id) {
fetchDetail(String(props.record.id));
}
}, [() => props.visible, () => props.record?.id]);
return () => (
<Modal
@@ -234,8 +242,8 @@ export default defineComponent({
</div>
<Table
columns={PLAYER_COLUMNS}
dataSource={categoryPlayers[cat.id] || []}
loading={categoryPlayerLoading[cat.id]}
dataSource={categoryPlayers.value[cat.id] || []}
loading={categoryPlayerLoading.value[cat.id]}
scroll={{ x: 'max-content', y: 200 }}
size="small"
pagination={false}
@@ -243,9 +251,9 @@ export default defineComponent({
/>
<div class={styles.paginationRow}>
<Pagination
current={categoryPlayerPage[cat.id] || 1}
current={categoryPlayerPage.value[cat.id] || 1}
pageSize={PAGE_SIZE}
total={categoryPlayerTotal[cat.id] || 0}
total={categoryPlayerTotal.value[cat.id] || 0}
showSizeChanger={false}
size="small"
onChange={(page: number) => handlePlayerPageChange(cat.id, page)}
@@ -1,36 +1,54 @@
import { defineComponent } from 'vue';
import { Modal, Image } from 'ant-design-vue';
import { defineComponent, ref } from 'vue';
import { Modal, Image, Spin } from 'ant-design-vue';
import { getRuleInfo } from '../model/services';
import type { TournamentAdminRuleInfoVO } from '@/api/events/types';
import { useEffect } from '@/hooks';
import styles from './EventRegulationModal.module.less';
interface EventRegulationModalProps {
visible: boolean;
/** 当前赛事记录(需包含 id 字段) */
record?: Record<string, any>;
onClose: () => void;
}
/** 规程内容 */
const REGULATION_TEXT = [
'一、赛事目的:推动羽毛球运动发展,提高运动员技术水平,增进体育文化交流。',
'二、参赛资格:凡身体健康、年龄符合要求的羽毛球爱好者均可报名参加。',
'三、比赛规则:采用中国羽毛球协会审定的最新《羽毛球竞赛规则》。',
'四、赛制安排:比赛分小组赛和淘汰赛两个阶段,小组赛采用单循环,淘汰赛采用单败淘汰。',
'五、器材要求:参赛运动员自备球拍,比赛用球由组委会统一提供。',
'六、裁判设置:每场比赛设主裁判1名、副裁判2名,确保比赛公平公正。',
].join('\n\n');
/** 规程图片(mock8 张示例图,4 列 grid */
const REGULATION_IMAGES = Array.from({ length: 8 }, (_, i) => ({
id: i + 1,
src: `https://picsum.photos/seed/reg${i + 1}/300/200`,
alt: `规程图片 ${i + 1}`,
}));
export default defineComponent({
name: 'EventRegulationModal',
props: {
visible: { type: Boolean, default: false },
record: { type: Object, default: () => ({}) },
onClose: { type: Function, required: true },
},
setup(props: EventRegulationModalProps) {
const loading = ref(false);
const ruleData = ref<TournamentAdminRuleInfoVO | null>(null);
/** 调用 getRuleInfo 接口获取规程数据 */
const fetchRuleInfo = async (id: string) => {
loading.value = true;
ruleData.value = null;
try {
const res = await getRuleInfo(id);
if (res.code == 200 && res.data) {
ruleData.value = res.data;
} else {
ruleData.value = null;
}
} catch (e) {
console.error('获取赛事规程失我败:', e);
ruleData.value = null;
} finally {
loading.value = false;
}
};
// 弹窗打开且 有 record.id 时自动请求(useEffect 带 immediate: true,兼容 destroyOnClose 重建场景)
useEffect(() => {
if (props.visible && props.record?.id) {
fetchRuleInfo(props.record.id);
}
}, [() => props.visible, () => props.record?.id]);
return () => (
<Modal
visible={props.visible}
@@ -42,25 +60,48 @@ export default defineComponent({
destroyOnClose
wrapClassName={styles.eventRegulationModalMain}
>
{/* 规程内容 */}
<div class={styles.section}>
<div class={styles.sectionTitle}></div>
<div class={styles.textBlock}>{REGULATION_TEXT}</div>
</div>
{/* 规程图片:4 列 grid,点击可浏览 */}
<div class={styles.section}>
<div class={styles.sectionTitle}></div>
<Image.PreviewGroup>
<div class={styles.imageGrid}>
{REGULATION_IMAGES.map((img) => (
<div key={img.id} class={styles.imageCell}>
<Image src={img.src} />
<Spin spinning={loading.value}>
{ruleData.value ? (
<>
{/* 规程内容 */}
{ruleData.value.rulesDescr && (
<div class={styles.section}>
<div class={styles.sectionTitle}></div>
<div class={styles.textBlock}>{ruleData.value.rulesDescr}</div>
</div>
))}
</div>
</Image.PreviewGroup>
</div>
)}
{/* 规程图片 */}
{(ruleData.value.rulesDescrImgList?.length ?? 0) > 0 && (
<div class={styles.section}>
<div class={styles.sectionTitle}></div>
<Image.PreviewGroup>
<div class={styles.imageGrid}>
{ruleData.value.rulesDescrImgList!.map((src: string, idx: number) => (
<div key={idx} class={styles.imageCell}>
<Image src={src} />
</div>
))}
</div>
</Image.PreviewGroup>
</div>
)}
{/* 无数据提示 */}
{!ruleData.value.rulesDescr && !ruleData.value.rulesDescrImgList?.length && (
<div style={{ textAlign: 'center', color: 'rgba(0,0,0,0.45)', padding: '40px 0' }}>
</div>
)}
</>
) : (
!loading.value && (
<div style={{ textAlign: 'center', color: 'rgba(0,0,0,0.45)', padding: '40px 0' }}>
</div>
)
)}
</Spin>
</Modal>
);
},
+110 -170
View File
@@ -1,4 +1,4 @@
import { defineComponent, onMounted } from 'vue';
import { defineComponent } from 'vue';
import {
Button,
Input,
@@ -10,8 +10,6 @@ import {
Cascader,
Tooltip,
Pagination,
Modal,
message,
} from 'ant-design-vue';
import {
useEventListModel,
@@ -20,9 +18,9 @@ import {
ID_CARD_OPTIONS,
ID_CARD_IMAGE_OPTIONS,
} from './model/useEventListModel';
import { toggleEventOnline } from './model/services';
import { useEventListColumns } from './model/useEventListColumns';
import { regionOptions } from '@/utils/areaData';
import { useContainerSize, useState } from '@/hooks';
import { useContainerSize } from '@/hooks';
import EventRegulationModal from './components/EventRegulationModal';
import EventDetailModal from './components/EventDetailModal';
import EventAuditModal from './components/EventAuditModal';
@@ -31,85 +29,12 @@ import pageStyles from '@/assets/styles/pageLayout.module.less';
const { RangePicker } = DatePicker;
/**
* bodyCell 渲染函数
*/
function renderBodyCell({
column,
text,
record,
onView,
onViewRegulation,
onAudit,
onToggleShelf,
}: {
column: any;
text: any;
record: any;
onView: (record: any) => void;
onViewRegulation: (record: any) => void;
onAudit: (record: any) => void;
onToggleShelf: (record: any) => void;
}) {
if (column.dataIndex === 'name') {
const maxLen = 15;
const raw = text || '';
const display = raw.length > maxLen ? raw.slice(0, maxLen) + '...' : raw;
return raw.length > maxLen ? (
<Tooltip title={raw}>
<span>{display}</span>
</Tooltip>
) : (
<span>{display}</span>
);
}
if (column.key === 'regulation') {
return (
<a style={{ cursor: 'pointer' }} onClick={() => onViewRegulation(record)}>
</a>
);
}
if (column.key === 'action') {
const status = record.status;
return (
// 报名中(0) / 进行中(1):查看 + 下架
((status === '0' || status === '1') && (
<Space>
<Button type="link" size="small" onClick={() => onView(record)}>
</Button>
<Button type="link" size="small" onClick={() => onToggleShelf(record)}>
</Button>
</Space>
)) ||
// 已下架(4):查看 + 上架
(status === '4' && (
<Space>
<Button type="link" size="small" onClick={() => onView(record)}>
</Button>
<Button type="link" size="small" onClick={() => onToggleShelf(record)}>
</Button>
</Space>
)) || (
// 已结束(2) / 已删除(3):仅查看
<Button type="link" size="small" onClick={() => onView(record)}>
</Button>
)
);
}
return;
}
/**
* 赛事列表
* 赛事列表页面(纯视图层)
*
* 架构分层:
* - 数据层:services.tsAPI 调用)
* - 逻辑层:useEventListModel.ts(状态 + 业务逻辑 + 交互处理)
* - 视图层:useEventListColumns.ts(表格列配置)+ 本组件(纯 UI 渲染)
*/
export default defineComponent({
name: 'EventList',
@@ -118,94 +43,122 @@ export default defineComponent({
filterForm,
loading,
dataSource,
columns,
pagination,
regulationModalVisible,
detailModalVisible,
auditModalVisible,
currentRecord,
togglingId,
handleSearch,
handleReset,
handlePageChange,
openDetailModal,
openRegulationModal,
closeRegulationModal,
closeDetailModal,
closeAuditModal,
submitAudit,
handleToggleShelf,
} = useEventListModel();
const { columns } = useEventListColumns();
const { containerRef, height } = useContainerSize();
// 弹窗状态
const [regulationModalVisible, setRegulationModalVisible] = useState<boolean>(false);
const [detailModalVisible, setDetailModalVisible] = useState<boolean>(false);
const [auditModalVisible, setAuditModalVisible] = useState<boolean>(false);
const [currentRecord, setCurrentRecord] = useState<any>({});
const handleView = (record: any) => {
console.log('查看赛事:', record.id);
setCurrentRecord(record);
setDetailModalVisible(true);
/** 渲染赛事名称(超长省略 + Tooltip) */
const renderName = (text: string) => {
const maxLen = 15;
const raw = text || '';
const display = raw.length > maxLen ? raw.slice(0, maxLen) + '...' : raw;
return raw.length > maxLen ? (
<Tooltip title={raw}>
<span>{display}</span>
</Tooltip>
) : (
<span>{display}</span>
);
};
/** 上架 / 下架 通用 */
const handleToggleShelf = (record: any) => {
const isRemoved = record.status === '4';
const actionText = isRemoved ? '上架' : '下架';
Modal.confirm({
title: `${actionText}确认`,
content: `确认${actionText}"${record.name}"该赛事吗,下架后用户将无法搜索和报名该赛事,已有报名和订单不受影响。`,
okText: '确认',
cancelText: '取消',
onOk: async () => {
const online = isRemoved ? '1' : '0';
try {
const res = await toggleEventOnline({ id: record.id, online });
if (res.code == 200) {
message.success(`${actionText}成功`);
handleSearch();
} else {
message.error(res.msg || `${actionText}失败`);
}
} catch (e: any) {
console.error(`${actionText}失败:`, e);
}
},
});
/** 渲染操作列(纯视图,逻辑在 model 的 handleToggleShelf */
const renderAction = (record: any) => {
const status = record.status;
if (status === '0' || status === '1') {
return (
<Space>
<Button type="link" size="small" onClick={() => openDetailModal(record)}>
</Button>
<Button
type="link"
size="small"
loading={togglingId.value === record.id}
onClick={() => handleToggleShelf(record)}
>
</Button>
</Space>
);
}
if (status === '4') {
return (
<Space>
<Button type="link" size="small" onClick={() => openDetailModal(record)}>
</Button>
<Button
type="link"
size="small"
loading={togglingId.value === record.id}
onClick={() => handleToggleShelf(record)}
>
</Button>
</Space>
);
}
return (
<Button type="link" size="small" onClick={() => openDetailModal(record)}>
</Button>
);
};
/** 打开审核弹窗 */
const handleAudit = (record: any) => {
console.log('审核赛事:', record.id);
setCurrentRecord(record);
setAuditModalVisible(true);
};
/** 提交审核 */
const handleAuditSubmit = (payload: { approved: boolean; comment: string }) => {
console.log('提交审核:', currentRecord.value.id, payload);
// TODO: 替换为真实 API 调用
message.success('审核提交成功');
setAuditModalVisible(false);
};
const handleViewRegulation = (record: any) => {
console.log('查看规程:', record.id);
setCurrentRecord(record);
setRegulationModalVisible(true);
};
onMounted(() => {
handleSearch();
});
/** 最终表格列:模型列 + 赛事规程 + 操作 */
/** 完整表格列 */
const tableColumns = [
...columns,
{ title: '赛事规程', key: 'regulation', width: 100, align: 'center' as const },
...columns.map((col: any) => {
if (col.dataIndex === 'name') {
return {
...col,
customRender: ({ text }: { text: string }) => renderName(text),
};
}
return col;
}),
{
title: '赛事规程',
key: 'regulation',
width: 100,
align: 'center' as const,
customRender: ({ record }: { record: any }) => (
<a style={{ cursor: 'pointer' }} onClick={() => openRegulationModal(record)}>
</a>
),
},
{
title: '操作',
key: 'action',
width: 160,
fixed: 'right' as const,
align: 'center' as const,
customRender: ({ record }: { record: any }) => renderAction(record),
},
];
return () => (
<div class={pageStyles.containerMain}>
{/* ===== 筛选区 ===== */}
<div class={pageStyles.filter}>
<Form style={{ rowGap: '10px' }} layout="inline" model={filterForm}>
<Form.Item label="创建时间" name="createDateRange">
@@ -308,7 +261,6 @@ export default defineComponent({
</Form>
</div>
{/* ===== 表格区 ===== */}
<div class={pageStyles.table}>
<div ref={containerRef} class={pageStyles.tableBody}>
<Table
@@ -317,26 +269,14 @@ export default defineComponent({
loading={loading.value}
scroll={{ x: 'max-content', y: height.value }}
pagination={false}
>
{{
bodyCell: (args: any) =>
renderBodyCell({
...args,
onView: handleView,
onViewRegulation: handleViewRegulation,
onAudit: handleAudit,
onToggleShelf: handleToggleShelf,
}),
}}
</Table>
/>
</div>
{/* 独立分页,右下方 */}
<div class={pageStyles.pagination}>
<Pagination
current={pagination.value.current}
pageSize={pagination.value.pageSize}
total={pagination.value.total}
current={(pagination as any).current.value}
pageSize={(pagination as any).pageSize.value}
total={(pagination as any).total.value}
showSizeChanger
showTotal={(total: number) => `${total}`}
onChange={handlePageChange}
@@ -344,26 +284,26 @@ export default defineComponent({
</div>
</div>
{/* ===== 弹窗区 ===== */}
{regulationModalVisible.value && (
<EventRegulationModal
visible={regulationModalVisible.value}
onClose={() => setRegulationModalVisible(false)}
record={currentRecord.value}
onClose={closeRegulationModal}
/>
)}
{detailModalVisible.value && (
<EventDetailModal
visible={detailModalVisible.value}
record={currentRecord.value}
onClose={() => setDetailModalVisible(false)}
onClose={closeDetailModal}
/>
)}
{auditModalVisible.value && (
<EventAuditModal
visible={auditModalVisible.value}
record={currentRecord.value}
onClose={() => setAuditModalVisible(false)}
onSubmit={handleAuditSubmit}
onClose={closeAuditModal}
onSubmit={submitAudit}
/>
)}
</div>
+69
View File
@@ -0,0 +1,69 @@
/**
* 赛事列表页配置
* 职责:集中管理字段映射、选项配置等常量
*/
import type { EventListQueryParams } from './services';
export const FILTER_DEFAULTS = {
createDateRange: null as [string, string] | null,
startDateRange: null as [string, string] | null,
name: '',
createName: '',
status: '',
region: [] as string[],
isPublic: '',
idCardNo: '',
idCardImg: '',
};
export type FieldMapping = [
keyof typeof FILTER_DEFAULTS,
keyof EventListQueryParams,
((v: any) => any)?,
];
export const FIELD_MAPPINGS: FieldMapping[] = [
['name', 'name', (v) => v?.trim?.() ?? v],
['createName', 'createName', (v) => v?.trim?.() ?? v],
['status', 'status'],
['isPublic', 'isPublic'],
['idCardNo', 'idCardNo'],
['idCardImg', 'idCardImg'],
['region', 'areaName', (v) => v?.[v.length - 1]],
['createDateRange', 'createDate', (v) => v?.[0]],
['createDateRange', 'createDateEnd', (v) => v?.[1]],
['startDateRange', 'startDate', (v) => v?.[0]],
['startDateRange', 'startDateEnd', (v) => v?.[1]],
];
/** 赛事状态选项(value 对应 API status */
export const EVENT_STATUS_OPTIONS = [
{ value: '', label: '全部' },
{ value: '0', label: '报名中' },
{ value: '1', label: '进行中' },
{ value: '2', label: '已结束' },
{ value: '3', label: '已删除' },
{ value: '4', label: '已下架' },
] as const;
/** 公开活动选项(value 对应 API isPublic: 0=非公开,1=公开) */
export const PUBLIC_EVENT_OPTIONS = [
{ value: '', label: '全部' },
{ value: '1', label: '公开' },
{ value: '0', label: '非公开' },
] as const;
/** 证件号选项(value 对应 API idCardNo: 0=无需,1=需要) */
export const ID_CARD_OPTIONS = [
{ value: '', label: '全部' },
{ value: '0', label: '无需提供' },
{ value: '1', label: '需提供' },
] as const;
/** 证件图片选项(value 对应 API idCardImg: 0=无需,1=需要) */
export const ID_CARD_IMAGE_OPTIONS = [
{ value: '', label: '全部' },
{ value: '0', label: '无需提供' },
{ value: '1', label: '需提供' },
] as const;
+12 -43
View File
@@ -3,10 +3,6 @@
* 该页面使用的所有接口集中管理
*/
import { get, post } from '@/utils/request';
import { USE_MOCK, MOCK_DELAY } from '@/config/mock';
import { MOCK_EVENT_DETAIL } from '@/config/mock/eventDetail';
import { buildMockEventListPage } from '@/config/mock/eventList';
import { buildMockSignupListPage } from '@/config/mock/eventSignup';
import type {
EventListQueryParams,
SignupListQueryParams,
@@ -16,12 +12,14 @@ import type {
TournamentAdminVO,
TournamentAdminInfoVO,
TournamentAdminInfoSignupVO,
TournamentAdminRuleInfoVO,
} from '@/api/events/types';
export type {
TournamentAdminVO,
TournamentAdminInfoVO,
TournamentAdminInfoSignupVO,
TournamentAdminRuleInfoVO,
InnerCategoryVO,
EventListQueryParams,
SignupListQueryParams,
@@ -30,62 +28,33 @@ export type {
ApiResult,
} from '@/api/events/types';
// ============================================================
// URL 常量
// ============================================================
const eventList = '/manager/tm/page';
const eventDetail = '/manager/tm/info';
const signupList = '/manager/tm/info/signupPage';
const toggleOnline = '/manager/tm/online';
const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
// ============================================================
// API 函数
// ============================================================
const eventList = '/admin/manager/tm/page';
const eventDetail = '/admin/manager/tm/info';
const signupList = '/admin/manager/tm/info/signupPage';
const toggleOnline = '/admin/manager/tm/online';
const ruleInfo = `/admin/manager/tm/ruleInfo`;
/** 赛事列表 - 列表分页 */
export async function getEventList(
params: EventListQueryParams,
): Promise<ApiResult<PageData<TournamentAdminVO>>> {
if (USE_MOCK) {
await delay(MOCK_DELAY);
const page = Number(params.page) || 1;
const limit = Number(params.limit) || 10;
return {
code: 200,
msg: 'success',
data: buildMockEventListPage(page, limit),
};
}
return get(eventList, params as Record<string, any>);
}
/** 赛事列表 - 赛事详情 */
export async function getEventDetail(id: string): Promise<ApiResult<TournamentAdminInfoVO>> {
if (USE_MOCK) {
await delay(MOCK_DELAY);
return {
code: 200,
msg: 'success',
data: { ...MOCK_EVENT_DETAIL },
};
}
return get(eventDetail, { id });
}
/** 赛事列表 - 规则详情 */
export async function getRuleInfo(id: string): Promise<ApiResult<TournamentAdminRuleInfoVO>> {
return get(ruleInfo, { id });
}
/** 赛事列表 - 赛事详情 - 人员列表 */
export async function getSignupList(
params: SignupListQueryParams,
): Promise<ApiResult<PageData<TournamentAdminInfoSignupVO>>> {
if (USE_MOCK) {
await delay(MOCK_DELAY);
return {
code: 200,
msg: 'success',
data: buildMockSignupListPage(params),
};
}
return get(signupList, params as Record<string, any>);
}
@@ -0,0 +1,116 @@
import { h } from 'vue';
import { StatusTag, type StatusTagTone } from '@/components';
import type { TournamentAdminVO } from './services';
/** 状态标签映射 */
const STATUS_MAP: Record<string, { label: string; tone: StatusTagTone }> = {
'0': { label: '报名中', tone: 'primary' },
'1': { label: '进行中', tone: 'cyan' },
'2': { label: '已结束', tone: 'neutral' },
'3': { label: '已删除', tone: 'neutral' },
'4': { label: '已下架', tone: 'orange' },
};
/** 0/1 → 显示文字 */
const formatDisplayValue = (value: string, map: Record<string, string>) => map[value] ?? value;
const ID_CARD_DISPLAY_MAP: Record<string, string> = { '0': '否', '1': '是' };
const IS_PUBLIC_DISPLAY_MAP: Record<string, string> = { '0': '非公开', '1': '公开' };
/**
* 赛事列表 - 表格列配置(视图层)
* 纯展示逻辑,不包含状态管理
*/
export function useEventListColumns() {
const columns = [
{ title: '赛事ID', dataIndex: 'id', key: 'id', width: 180 },
{
title: '赛事名称',
dataIndex: 'name',
key: 'name',
width: 160,
},
{
title: '赛事时间',
key: 'eventTime',
width: 260,
customRender: ({ record }: { record: TournamentAdminVO }) =>
`${record.startTimeBegin}${record.startTimeEnd}`,
},
{
title: '省市区',
dataIndex: 'venueArea',
key: 'venueArea',
width: 200,
},
{
title: '地点',
dataIndex: 'venue',
key: 'venue',
width: 180,
},
{
title: '是否公开',
dataIndex: 'isPublic',
key: 'isPublic',
width: 100,
customRender: ({ text }: { text: string }) => formatDisplayValue(text, IS_PUBLIC_DISPLAY_MAP),
},
{
title: '是否提供证件号',
dataIndex: 'isRequireIdCardNo',
key: 'isRequireIdCardNo',
width: 160,
customRender: ({ text }: { text: string }) => formatDisplayValue(text, ID_CARD_DISPLAY_MAP),
},
{
title: '是否提供证件图片',
dataIndex: 'isRequireIdCardImg',
key: 'isRequireIdCardImg',
width: 160,
customRender: ({ text }: { text: string }) => formatDisplayValue(text, ID_CARD_DISPLAY_MAP),
},
{
title: '创建人昵称',
dataIndex: 'createNickname',
key: 'createNickname',
width: 110,
},
{
title: '创建人手机号',
dataIndex: 'createPhone',
key: 'createPhone',
width: 130,
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 170,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 90,
customRender: ({ text }: { text: string }) => {
const info = STATUS_MAP[text] || { label: text, tone: 'default' as const };
return h(StatusTag, { label: info.label, tone: info.tone });
},
},
{
title: '报名人数',
dataIndex: 'signupCount',
key: 'signupCount',
width: 90,
},
{
title: '取消报名人数',
dataIndex: 'dropoutCount',
key: 'dropoutCount',
width: 140,
},
];
return { columns };
}
+174 -260
View File
@@ -1,84 +1,36 @@
import { computed, reactive, toRef, Ref, h } from 'vue';
import { message } from 'ant-design-vue';
import { StatusTag, type StatusTagTone } from '@/components';
import { useState, useDebounce, useThrottleFn } from '@/hooks';
import type { TournamentAdminVO, EventListQueryParams } from './services';
import { getEventList } from './services';
import { computed, reactive, toRef, Ref } from 'vue';
import { Modal, message } from 'ant-design-vue';
import { useState, useDebounce } from '@/hooks';
import { useRequest } from '@/hooks/useRequest';
import { useEffect } from '@/hooks/useEffect';
import { usePagination } from '@/hooks/usePagination';
import type { TournamentAdminVO, EventListQueryParams, PageData, ApiResult } from './services';
import { getEventList, toggleEventOnline } from './services';
import {
FILTER_DEFAULTS,
FIELD_MAPPINGS,
EVENT_STATUS_OPTIONS,
PUBLIC_EVENT_OPTIONS,
ID_CARD_OPTIONS,
ID_CARD_IMAGE_OPTIONS,
} from './config';
// ============================================================
// 常量
// ============================================================
/** 赛事状态选项(value 对应 API status: 不传=全部、0=报名中,1=进行中,2=已结束,3=已删除,4=已下架) */
export const EVENT_STATUS_OPTIONS = [
{ value: '', label: '全部' },
{ value: '0', label: '报名中' },
{ value: '1', label: '进行中' },
{ value: '2', label: '已结束' },
{ value: '3', label: '已删除' },
{ value: '4', label: '已下架' },
] as const;
/** 公开活动选项(value 对应 API isPublic: 0=非公开,1=公开) */
export const PUBLIC_EVENT_OPTIONS = [
{ value: '', label: '全部' },
{ value: '1', label: '公开' },
{ value: '0', label: '非公开' },
] as const;
/** 证件号选项(value 对应 API idCardNo: 0=无需,1=需要) */
export const ID_CARD_OPTIONS = [
{ value: '', label: '全部' },
{ value: '0', label: '无需提供' },
{ value: '1', label: '需提供' },
] as const;
/** 证件图片选项(value 对应 API idCardImg: 0=无需,1=需要) */
export const ID_CARD_IMAGE_OPTIONS = [
{ value: '', label: '全部' },
{ value: '0', label: '无需提供' },
{ value: '1', label: '需提供' },
] as const;
/** 状态标签映射 */
const STATUS_MAP: Record<string, { label: string; tone: StatusTagTone }> = {
'0': { label: '报名中', tone: 'primary' },
'1': { label: '进行中', tone: 'cyan' },
'2': { label: '已结束', tone: 'neutral' },
'3': { label: '已删除', tone: 'neutral' },
'4': { label: '已下架', tone: 'orange' },
};
/** 0/1 → 显示文字 */
const formatDisplayValue = (value: string, map: Record<string, string>) => map[value] ?? value;
const ID_CARD_DISPLAY_MAP: Record<string, string> = { '0': '否', '1': '是' };
const IS_PUBLIC_DISPLAY_MAP: Record<string, string> = { '0': '非公开', '1': '公开' };
// ============================================================
// Model
// ============================================================
export { EVENT_STATUS_OPTIONS, PUBLIC_EVENT_OPTIONS, ID_CARD_OPTIONS, ID_CARD_IMAGE_OPTIONS };
/**
* 赛事列表页数据模型
* 筛选字段名与 API query 参数(EventListQueryParams)保持一致,
* 仅日期区间 RangePicker 拆分为 start/end 两个字段、省市区 Cascader 取末级为 areaName。
* 职责:状态管理 + 数据获取 + 业务逻辑 + 交互处理
*/
export function useEventListModel() {
// ===== 筛选条件(key 名对齐 API 查询参数) =====
const filterForm = reactive({
createDateRange: null as [string, string] | null,
startDateRange: null as [string, string] | null,
name: '',
createName: '',
status: '',
region: [] as string[],
isPublic: '',
idCardNo: '',
idCardImg: '',
});
const filterForm = reactive({ ...FILTER_DEFAULTS });
const [regulationModalVisible, setRegulationModalVisible] = useState(false);
const [detailModalVisible, setDetailModalVisible] = useState(false);
const [auditModalVisible, setAuditModalVisible] = useState(false);
const [currentRecord, setCurrentRecord] = useState<any>({});
const [togglingId, setTogglingId] = useState('');
const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
// 可搜索字段防抖
const { debouncedValue: debouncedName } = useDebounce(toRef(filterForm, 'name') as Ref<string>, {
delay: 300,
});
@@ -87,215 +39,177 @@ export function useEventListModel() {
{ delay: 300 },
);
// ===== 表格状态 =====
const [loading, setLoading] = useState<boolean>(false);
const [dataSource, setDataSource] = useState<TournamentAdminVO[]>([]);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0,
});
// ===== 表格列配置(dataIndex 对齐 TournamentAdminVO 字段) =====
const columns = [
{ title: '赛事ID', dataIndex: 'id', key: 'id', width: 180 },
{
title: '赛事名称',
dataIndex: 'name',
key: 'name',
width: 160,
},
{
title: '赛事时间',
key: 'eventTime',
width: 260,
customRender: ({ record }: { record: TournamentAdminVO }) =>
`${record.startTimeBegin}${record.startTimeEnd}`,
},
{
title: '省市区',
dataIndex: 'venueArea',
key: 'venueArea',
width: 200,
},
{
title: '地点',
dataIndex: 'venue',
key: 'venue',
width: 180,
},
{
title: '是否公开',
dataIndex: 'isPublic',
key: 'isPublic',
width: 100,
customRender: ({ text }: { text: string }) => formatDisplayValue(text, IS_PUBLIC_DISPLAY_MAP),
},
{
title: '是否提供证件号',
dataIndex: 'isRequireIdCardNo',
key: 'isRequireIdCardNo',
width: 160,
customRender: ({ text }: { text: string }) => formatDisplayValue(text, ID_CARD_DISPLAY_MAP),
},
{
title: '是否提供证件图片',
dataIndex: 'isRequireIdCardImg',
key: 'isRequireIdCardImg',
width: 160,
customRender: ({ text }: { text: string }) => formatDisplayValue(text, ID_CARD_DISPLAY_MAP),
},
{
title: '创建人昵称',
dataIndex: 'createNickname',
key: 'createNickname',
width: 110,
},
{
title: '创建人手机号',
dataIndex: 'createPhone',
key: 'createPhone',
width: 130,
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 170,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 90,
customRender: ({ text }: { text: string }) => {
const info = STATUS_MAP[text] || { label: text, tone: 'default' as const };
return h(StatusTag, { label: info.label, tone: info.tone });
},
},
{
title: '报名人数',
dataIndex: 'signupCount',
key: 'signupCount',
width: 90,
},
{
title: '取消报名人数',
dataIndex: 'dropoutCount',
key: 'dropoutCount',
width: 140,
},
];
// ===== 构建 API 查询参数 =====
const buildQueryParams = (): EventListQueryParams => {
const params: EventListQueryParams = {
page: String(pagination.value.current),
limit: String(pagination.value.pageSize),
};
const { page, pageSize } = pagination.params.value;
if (filterForm.createDateRange) {
params.createDate = filterForm.createDateRange[0];
params.createDateEnd = filterForm.createDateRange[1];
}
if (filterForm.startDateRange) {
params.startDate = filterForm.startDateRange[0];
params.startDateEnd = filterForm.startDateRange[1];
}
if (filterForm.region.length > 0) {
params.areaName = filterForm.region[filterForm.region.length - 1];
}
if (filterForm.name.trim()) {
params.name = filterForm.name.trim();
}
if (filterForm.createName.trim()) {
params.createName = filterForm.createName.trim();
}
if (filterForm.status) {
params.status = filterForm.status;
}
if (filterForm.isPublic) {
params.isPublic = filterForm.isPublic;
}
if (filterForm.idCardNo) {
params.idCardNo = filterForm.idCardNo;
}
if (filterForm.idCardImg) {
params.idCardImg = filterForm.idCardImg;
}
const params = FIELD_MAPPINGS.reduce(
(acc, [formKey, paramKey, transform]) => {
const rawValue = filterForm[formKey];
if (!hasValue(rawValue)) return acc;
return params;
const value = safeTransform(rawValue, transform);
if (isEmptyValue(value)) return acc;
return { ...acc, [paramKey]: value };
},
{} as Record<string, any>,
);
return { page: String(page), limit: String(pageSize), ...params } as EventListQueryParams;
};
// ===== 计算属性 =====
const {
data,
loading,
run: fetchList,
} = useRequest<ApiResult<PageData<TournamentAdminVO>>>(() => getEventList(buildQueryParams()), {
refreshDeps: [],
formatResult: (res) => res,
});
// 提取列表数据
const listData = computed<PageData<TournamentAdminVO> | undefined>(() => {
const res = data.value;
return res ? (res as any).data : undefined;
});
const dataSource = computed(() => listData.value?.list || []);
// 同步分页总条数
useEffect(() => {
const total = (data.value as any)?.data?.total;
if (total !== undefined) pagination.setTotal(total);
}, [data]);
const hasFilter = computed(() => {
const f = filterForm;
return (
filterForm.createDateRange !== null ||
filterForm.startDateRange !== null ||
debouncedName.value.trim() !== '' ||
debouncedCreator.value.trim() !== '' ||
filterForm.status !== '' ||
filterForm.region.length > 0 ||
filterForm.isPublic !== '' ||
filterForm.idCardNo !== '' ||
filterForm.idCardImg !== ''
!!f.createDateRange ||
!!f.startDateRange ||
!!debouncedName.value?.trim() ||
!!debouncedCreator.value?.trim() ||
!!f.status ||
f.region.length > 0 ||
!!f.isPublic ||
!!f.idCardNo ||
!!f.idCardImg
);
});
// ===== 方法 =====
const handleSearch = () => fetchList();
/** 查询 */
const handleSearch = useThrottleFn(async () => {
setLoading(true);
try {
const queryParams = buildQueryParams();
console.log('赛事列表查询参数:', queryParams);
const res = await getEventList(queryParams);
if (res.code == 200) {
setDataSource(res.data.list);
setPagination({ ...pagination.value, total: res.data.total });
} else {
message.error(res.msg || '查询失败');
}
} catch (error: any) {
console.error('赛事列表查询失败:', error);
} finally {
setLoading(false);
}
}, 500);
/** 重置 */
const handleReset = useThrottleFn(() => {
filterForm.createDateRange = null;
filterForm.startDateRange = null;
filterForm.name = '';
filterForm.createName = '';
filterForm.status = '';
filterForm.region = [];
filterForm.isPublic = '';
filterForm.idCardNo = '';
filterForm.idCardImg = '';
setPagination({ current: 1, pageSize: 10, total: 0 });
setDataSource([]);
// 等待 debounce 生效后自动查询
setTimeout(() => handleSearch(), 350);
}, 500);
const handleReset = () => {
Object.assign(filterForm, FILTER_DEFAULTS);
pagination.reset();
setTimeout(fetchList, 350);
};
const handlePageChange = (page: number, pageSize: number) => {
setPagination({ ...pagination.value, current: page, pageSize });
handleSearch();
pagination.setCurrent(page);
if (pageSize !== (pagination as any).pageSize.value) {
pagination.setPageSize(pageSize);
}
setTimeout(fetchList, 0);
};
const openDetailModal = (record: any) => {
setCurrentRecord(record);
setDetailModalVisible(true);
};
const openRegulationModal = (record: any) => {
setCurrentRecord(record);
setRegulationModalVisible(true);
};
const openAuditModal = (record: any) => {
setCurrentRecord(record);
setAuditModalVisible(true);
};
const closeRegulationModal = () => setRegulationModalVisible(false);
const closeDetailModal = () => setDetailModalVisible(false);
const closeAuditModal = () => setAuditModalVisible(false);
// TODO
const submitAudit = (payload: { approved: boolean; comment: string }) => {
console.log('提交审核:', currentRecord.value.id, payload);
message.success('审核提交成功');
setAuditModalVisible(false);
};
const handleToggleShelf = (record: any) => {
const isRemoved = record.status === '4';
const actionText = isRemoved ? '上架' : '下架';
const descText = isRemoved
? `确认上架"${record.name}"吗?上架后用户可搜索和报名该赛事。`
: `确认下架"${record.name}"吗?下架后用户将无法搜索和报名该赛事。`;
Modal.confirm({
title: `${actionText}确认`,
content: descText,
okText: '确认',
cancelText: '取消',
onOk: async () => {
setTogglingId(record.id);
try {
const res = await toggleEventOnline({ id: record.id, online: isRemoved ? '1' : '0' });
if (res.code === 200) {
message.success(`${actionText}成功`);
fetchList();
} else {
message.error(res.msg || `${actionText}失败`);
}
} catch (e: any) {
message.error(e?.message || `${actionText}失败`);
} finally {
setTogglingId('');
}
},
});
};
return {
filterForm,
loading,
dataSource,
columns,
pagination,
hasFilter,
regulationModalVisible,
detailModalVisible,
auditModalVisible,
currentRecord,
togglingId,
handleSearch,
handleReset,
handlePageChange,
openDetailModal,
openRegulationModal,
openAuditModal,
closeRegulationModal,
closeDetailModal,
closeAuditModal,
submitAudit,
handleToggleShelf,
};
}
function hasValue(v: any): boolean {
if (v == null) return false;
if (Array.isArray(v)) return v.length > 0 && v.some((item) => item != null);
if (typeof v === 'string') return v.trim() !== '';
return true;
}
function isEmptyValue(v: any): boolean {
return v == null || (typeof v === 'string' && v.trim() === '');
}
function safeTransform(value: any, transform?: (v: any) => any): any {
if (!transform) return value;
try {
return transform(value) ?? value;
} catch {
return value;
}
}
-19
View File
@@ -3,8 +3,6 @@
* 该页面使用的所有接口集中管理
*/
import { get } from '@/utils/request';
import { USE_MOCK, MOCK_DELAY } from '@/config/mock';
import { buildMockEventOperationLogPage } from '@/config/mock/eventOperationLogs';
import type {
OperationLogQueryParams,
TournamentAdminOperationPageVO,
@@ -24,30 +22,13 @@ export type {
// ============================================================
const operationLogs = '/tournament/operation/page';
const delay = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
// ============================================================
// API 函数
// ============================================================
function pickFilters(params: OperationLogQueryParams): OperationLogQueryParams {
const { page: _page, limit: _limit, ...filters } = params;
return filters;
}
/** 赛事操作日志分页 */
export async function getOperationLogs(
params: OperationLogQueryParams,
): Promise<ApiResult<PageData<TournamentAdminOperationPageVO>>> {
if (USE_MOCK) {
await delay(MOCK_DELAY);
const page = Number(params.page) || 1;
const limit = Number(params.limit) || 10;
return {
code: 200,
msg: 'success',
data: buildMockEventOperationLogPage(page, limit, pickFilters(params)),
};
}
return get(operationLogs, params as Record<string, any>);
}
+5 -9
View File
@@ -1,4 +1,4 @@
import { defineComponent, onMounted } from 'vue';
import { defineComponent } from 'vue';
import {
Button,
Input,
@@ -143,10 +143,6 @@ export default defineComponent({
},
];
onMounted(() => {
handleSearch();
});
return () => (
<div class={pageStyles.containerMain}>
{/* ===== 筛选区 ===== */}
@@ -264,12 +260,12 @@ export default defineComponent({
</Table>
</div>
{/* 独立分页,右下方 */}
{/* 独立分页 */}
<div class={pageStyles.pagination}>
<Pagination
current={pagination.value.current}
pageSize={pagination.value.pageSize}
total={pagination.value.total}
current={(pagination as any).current.value}
pageSize={(pagination as any).pageSize.value}
total={(pagination as any).total.value}
showSizeChanger
showTotal={(total: number) => `${total}`}
onChange={handlePageChange}
+57
View File
@@ -0,0 +1,57 @@
/**
* 订单管理页配置
* 职责:集中管理字段映射、选项配置等常量
*/
import type { OrderListQueryParams } from './services';
// ============================================================
// 筛选字段默认值
// ============================================================
export const FILTER_DEFAULTS = {
orderDateRange: null as [string, string] | null,
orderNo: '',
tName: '',
sName: '',
status: '',
refundStatus: '',
};
// ============================================================
// 字段映射配置
// ============================================================
export type FieldMapping = [
keyof typeof FILTER_DEFAULTS,
keyof OrderListQueryParams,
((v: any) => any)?,
];
export const FIELD_MAPPINGS: FieldMapping[] = [
['orderNo', 'orderNo', (v) => v?.trim?.() ?? v],
['tName', 'tName', (v) => v?.trim?.() ?? v],
['sName', 'sName', (v) => v?.trim?.() ?? v],
['status', 'status'],
['refundStatus', 'refundStatus'],
['orderDateRange', 'orderDate', (v) => v?.[0]],
['orderDateRange', 'orderDateEnd', (v) => v?.[1]],
];
// ============================================================
// 下拉选项配置
// ============================================================
/** 订单状态选项(value 对应 API status: 不传=全部、1=支付成功,2=部分退款,3=全部退款,4=超时关闭) */
export const ORDER_STATUS_OPTIONS = [
{ value: '', label: '全部' },
{ value: '1', label: '支付成功' },
{ value: '2', label: '部分退款' },
{ value: '3', label: '全部退款' },
{ value: '4', label: '超时关闭' },
] as const;
/** 退款状态选项(value 对应 API refundStatus: 不传=全部、0=退款失败,1=退款成功) */
export const REFUND_STATUS_OPTIONS = [
{ value: '', label: '全部' },
{ value: '1', label: '退款成功' },
{ value: '0', label: '退款失败' },
] as const;
+4 -4
View File
@@ -35,10 +35,10 @@ export type {
// ============================================================
// URL 常量
// ============================================================
const orderList = '/manager/order/page';
const orderDetail = '/manager/order/info';
const orderPlayerList = '/manager/order/info/page';
const retryRefund = '/manager/order/retryRefund';
const orderList = '/admin/manager/order/page';
const orderDetail = '/admin/manager/order/info';
const orderPlayerList = '/admin/manager/order/info/userPage';
const retryRefund = '/admin/manager/order/retryRefund';
const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
+118 -135
View File
@@ -1,33 +1,24 @@
import { computed, reactive, toRef, Ref, h } from 'vue';
import { message } from 'ant-design-vue';
import { StatusTag, type StatusTagTone } from '@/components';
import { useState, useDebounce, useThrottleFn } from '@/hooks';
import {
getOrderList,
type TournamentAdminOrderPageVO,
type OrderListQueryParams,
import { useState, useDebounce } from '@/hooks';
import { useRequest } from '@/hooks/useRequest';
import { useEffect } from '@/hooks/useEffect';
import { usePagination } from '@/hooks/usePagination';
import type {
TournamentAdminOrderPageVO,
OrderListQueryParams,
PageData,
ApiResult,
} from './services';
import { MOCK_ORDER_SUMMARY } from '@/config/mock/orderList';
import { getOrderList } from './services';
import {
FILTER_DEFAULTS,
FIELD_MAPPINGS,
ORDER_STATUS_OPTIONS,
REFUND_STATUS_OPTIONS,
} from './config';
// ============================================================
// 常量
// ============================================================
/** 订单状态选项(value 对应 API status: 不传=全部、1=支付成功,2=部分退款,3=全部退款,4=超时关闭) */
export const ORDER_STATUS_OPTIONS = [
{ value: '', label: '全部' },
{ value: '1', label: '支付成功' },
{ value: '2', label: '部分退款' },
{ value: '3', label: '全部退款' },
{ value: '4', label: '超时关闭' },
] as const;
/** 退款状态选项(value 对应 API refundStatus: 不传=全部、0=退款失败,1=退款成功) */
export const REFUND_STATUS_OPTIONS = [
{ value: '', label: '全部' },
{ value: '1', label: '退款成功' },
{ value: '0', label: '退款失败' },
] as const;
export { ORDER_STATUS_OPTIONS, REFUND_STATUS_OPTIONS };
/** 订单状态 StatusTag 映射 */
const ORDER_STATUS_MAP: Record<number, { label: string; tone: StatusTagTone }> = {
@@ -44,26 +35,15 @@ const REFUND_STATUS_MAP: Record<number, { label: string; tone: StatusTagTone }>
3: { label: '退款失败', tone: 'danger' },
};
// ============================================================
// Model
// ============================================================
/**
* 订单列表页数据模型
* 筛选字段名与 API query 参数(OrderListQueryParams)保持一致。
* 职责:状态管理 + 数据获取 + 业务逻辑 + 交互处理
*/
export function useOrderModel() {
// ===== 筛选条件(key 名对齐 API 查询参数) =====
const filterForm = reactive({
orderDateRange: null as [string, string] | null,
orderNo: '',
tName: '',
sName: '',
status: '',
refundStatus: '',
});
const filterForm = reactive({ ...FILTER_DEFAULTS });
const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
// 可搜索字段防抖
const { debouncedValue: debouncedOrderNo } = useDebounce(
toRef(filterForm, 'orderNo') as Ref<string>,
{ delay: 300 },
@@ -77,19 +57,70 @@ export function useOrderModel() {
{ delay: 300 },
);
// ===== 表格状态 =====
const [loading, setLoading] = useState<boolean>(false);
const [dataSource, setDataSource] = useState<TournamentAdminOrderPageVO[]>([]);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0,
const buildQueryParams = (): OrderListQueryParams => {
const { page, pageSize } = pagination.params.value;
const params = FIELD_MAPPINGS.reduce(
(acc, [formKey, paramKey, transform]) => {
const rawValue = filterForm[formKey];
if (!hasValue(rawValue)) return acc;
const value = safeTransform(rawValue, transform);
if (isEmptyValue(value)) return acc;
return { ...acc, [paramKey]: value };
},
{} as Record<string, any>,
);
return { page: String(page), limit: String(pageSize), ...params } as OrderListQueryParams;
};
// ===== 列表数据请求 =====
const {
data,
loading,
run: fetchList,
} = useRequest<ApiResult<PageData<TournamentAdminOrderPageVO>>>(
() => getOrderList(buildQueryParams()),
{ refreshDeps: [], formatResult: (res) => res },
);
// 提取列表数据
const listData = computed<PageData<TournamentAdminOrderPageVO> | undefined>(() => {
const res = data.value;
return res ? (res as any).data : undefined;
});
// ===== 金额汇总(TODO: 汇总应来自独立接口) =====
const [summary, setSummary] = useState(MOCK_ORDER_SUMMARY);
const dataSource = computed(() => listData.value?.list || []);
// ===== 表格列配置(dataIndex 对齐 TournamentAdminOrderPageVO =====
// 同步分页总条数
useEffect(() => {
const total = (data.value as any)?.data?.total;
if (total !== undefined) pagination.setTotal(total);
}, [data]);
// ===== 金额汇总(TODO: 汇总应来自独立接口) =====
const [summary] = useState({
totalOrderAmount: 0,
totalPaidAmount: 0,
totalRefundAmount: 0,
});
// ===== 计算属性 =====
const hasFilter = computed(() => {
const f = filterForm;
return (
!!f.orderDateRange ||
!!debouncedOrderNo.value?.trim() ||
!!debouncedTName.value?.trim() ||
!!debouncedSName.value?.trim() ||
!!f.status ||
!!f.refundStatus
);
});
// ===== 表格列配置 =====
const formatMoney = (text: string) => {
const n = parseFloat(text);
return isNaN(n) ? '0.00元' : `${n.toFixed(2)}`;
@@ -97,12 +128,7 @@ export function useOrderModel() {
const columns = [
{ title: '订单编号', dataIndex: 'orderNo', key: 'orderNo', width: 200 },
{
title: '赛事名称',
dataIndex: 'name',
key: 'name',
width: 180,
},
{ title: '赛事名称', dataIndex: 'name', key: 'name', width: 180 },
{ title: '选手名称', dataIndex: 'realName', key: 'realName', width: 100 },
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 130 },
{ title: '比赛组别', dataIndex: 'categoryName', key: 'categoryName', width: 120 },
@@ -176,91 +202,25 @@ export function useOrderModel() {
},
];
// ===== 构建 API 查询参数 =====
const buildQueryParams = (): OrderListQueryParams => {
const params: OrderListQueryParams = {
page: String(pagination.value.current),
limit: String(pagination.value.pageSize),
};
// ===== 列表操作 =====
const handleSearch = () => fetchList();
if (filterForm.orderDateRange) {
params.orderDate = filterForm.orderDateRange[0];
params.orderDateEnd = filterForm.orderDateRange[1];
}
if (filterForm.orderNo.trim()) {
params.orderNo = filterForm.orderNo.trim();
}
if (filterForm.tName.trim()) {
params.tName = filterForm.tName.trim();
}
if (filterForm.sName.trim()) {
params.sName = filterForm.sName.trim();
}
if (filterForm.status) {
params.status = filterForm.status;
}
if (filterForm.refundStatus) {
params.refundStatus = filterForm.refundStatus;
}
return params;
const handleReset = () => {
Object.assign(filterForm, FILTER_DEFAULTS);
pagination.reset();
setTimeout(fetchList, 350);
};
// ===== 计算属性 =====
const hasFilter = computed(() => {
return (
filterForm.orderDateRange !== null ||
debouncedOrderNo.value.trim() !== '' ||
debouncedTName.value.trim() !== '' ||
debouncedSName.value.trim() !== '' ||
filterForm.status !== '' ||
filterForm.refundStatus !== ''
);
});
// ===== 方法 =====
/** 查询 */
const handleSearch = useThrottleFn(async () => {
setLoading(true);
try {
const queryParams = buildQueryParams();
console.log('订单列表查询参数:', queryParams);
const res = await getOrderList(queryParams);
if (res.code == 200) {
setDataSource(res.data.list);
setPagination({ ...pagination.value, total: res.data.total });
} else {
message.error(res.msg || '查询失败');
}
} catch (error: any) {
console.error('订单列表查询失败:', error);
} finally {
setLoading(false);
}
}, 500);
/** 重置 */
const handleReset = useThrottleFn(() => {
filterForm.orderDateRange = null;
filterForm.orderNo = '';
filterForm.tName = '';
filterForm.sName = '';
filterForm.status = '';
filterForm.refundStatus = '';
setPagination({ current: 1, pageSize: 10, total: 0 });
setDataSource([]);
// 等待 debounce 生效后自动查询
setTimeout(() => handleSearch(), 350);
}, 500);
const handlePageChange = (page: number, pageSize: number) => {
setPagination({ ...pagination.value, current: page, pageSize });
handleSearch();
pagination.setCurrent(page);
if (pageSize !== (pagination as any).pageSize.value) {
pagination.setPageSize(pageSize);
}
setTimeout(fetchList, 0);
};
return {
// 数据
filterForm,
loading,
dataSource,
@@ -268,8 +228,31 @@ export function useOrderModel() {
summary,
pagination,
hasFilter,
// 方法
handleSearch,
handleReset,
handlePageChange,
};
}
// ===== 工具函数 =====
function hasValue(v: any): boolean {
if (v == null) return false;
if (Array.isArray(v)) return v.length > 0 && v.some((item) => item != null);
if (typeof v === 'string') return v.trim() !== '';
return true;
}
function isEmptyValue(v: any): boolean {
return v == null || (typeof v === 'string' && v.trim() === '');
}
function safeTransform(value: any, transform?: (v: any) => any): any {
if (!transform) return value;
try {
return transform(value) ?? value;
} catch {
return value;
}
}