Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 49263792bc | |||
| 0b06cdca69 | |||
| 3330c38092 |
@@ -4,6 +4,7 @@ import { useState, useDebounce } from '@/hooks';
|
|||||||
import { useRequest } from '@/hooks/useRequest';
|
import { useRequest } from '@/hooks/useRequest';
|
||||||
import { useEffect } from '@/hooks/useEffect';
|
import { useEffect } from '@/hooks/useEffect';
|
||||||
import { usePagination } from '@/hooks/usePagination';
|
import { usePagination } from '@/hooks/usePagination';
|
||||||
|
import { hasValue, isEmptyValue, safeTransform } from '@/utils';
|
||||||
import type { TournamentAdminVO, EventListQueryParams, PageData, ApiResult } from './services';
|
import type { TournamentAdminVO, EventListQueryParams, PageData, ApiResult } from './services';
|
||||||
import { getEventList, toggleEventOnline } from './services';
|
import { getEventList, toggleEventOnline } from './services';
|
||||||
import {
|
import {
|
||||||
@@ -193,23 +194,3 @@ export function useEventListModel() {
|
|||||||
handleToggleShelf,
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,21 +10,14 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Pagination,
|
Pagination,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
import {
|
import { useLogModel, ACTION_TYPE_OPTIONS, ACTION_SOURCE_OPTIONS } from './model/useLogModel';
|
||||||
useLogModel,
|
import { useLogColumns } from './model/useLogColumns';
|
||||||
ACTION_TYPE_OPTIONS,
|
|
||||||
ACTION_SOURCE_OPTIONS,
|
|
||||||
ACTION_SOURCE_MAP,
|
|
||||||
} from './model/useLogModel';
|
|
||||||
import { useState, useContainerSize } from '@/hooks';
|
import { useState, useContainerSize } from '@/hooks';
|
||||||
import pageStyles from '@/assets/styles/pageLayout.module.less';
|
import pageStyles from '@/assets/styles/pageLayout.module.less';
|
||||||
import './index.module.less';
|
import './index.module.less';
|
||||||
|
|
||||||
const { RangePicker } = DatePicker;
|
const { RangePicker } = DatePicker;
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// LogContentCell: 操作内容单元格
|
|
||||||
// ============================================================
|
|
||||||
const LogContentCell = defineComponent({
|
const LogContentCell = defineComponent({
|
||||||
name: 'LogContentCell',
|
name: 'LogContentCell',
|
||||||
props: { text: { type: String, required: true } },
|
props: { text: { type: String, required: true } },
|
||||||
@@ -63,15 +56,14 @@ const LogContentCell = defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================================
|
/**
|
||||||
// bodyCell 渲染
|
* 操作日志页面(纯视图层)
|
||||||
// ============================================================
|
*
|
||||||
function renderBodyCell({ column, text }: { column: any; text: any }) {
|
* 架构分层:
|
||||||
const value = text || '-';
|
* - 数据层:services.ts(API 调用)
|
||||||
if (column.key === 'source') return <span>{ACTION_SOURCE_MAP[text] || value}</span>;
|
* - 逻辑层:useLogModel.ts(状态 + 业务逻辑 + 交互处理)
|
||||||
return <span>{value}</span>;
|
* - 视图层:useLogColumns.ts(表格列配置)+ 本组件(纯 UI 渲染)
|
||||||
}
|
*/
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'EventLogs',
|
name: 'EventLogs',
|
||||||
setup() {
|
setup() {
|
||||||
@@ -79,17 +71,24 @@ export default defineComponent({
|
|||||||
filterForm,
|
filterForm,
|
||||||
loading,
|
loading,
|
||||||
dataSource,
|
dataSource,
|
||||||
columns,
|
|
||||||
pagination,
|
pagination,
|
||||||
handleSearch,
|
handleSearch,
|
||||||
handleReset,
|
handleReset,
|
||||||
handlePageChange,
|
handlePageChange,
|
||||||
} = useLogModel();
|
} = useLogModel();
|
||||||
|
|
||||||
|
const { columns } = useLogColumns();
|
||||||
const { containerRef, height } = useContainerSize({ headerOffset: 55 });
|
const { containerRef, height } = useContainerSize({ headerOffset: 55 });
|
||||||
|
|
||||||
onMounted(() => {
|
/** 渲染 bodyCell */
|
||||||
handleSearch();
|
const renderBodyCell = ({ column, text }: { column: any; text: any }) => {
|
||||||
});
|
if (column.key === 'content') {
|
||||||
|
const raw = text || '';
|
||||||
|
return raw ? <LogContentCell text={raw} /> : <span>-</span>;
|
||||||
|
}
|
||||||
|
// 其他列使用 columns 中的 customRender
|
||||||
|
return <span>{text || '-'}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
return () => (
|
return () => (
|
||||||
<div class={pageStyles.containerMain}>
|
<div class={pageStyles.containerMain}>
|
||||||
@@ -182,21 +181,15 @@ export default defineComponent({
|
|||||||
pagination={false}
|
pagination={false}
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
bodyCell: (args: any) => {
|
bodyCell: renderBodyCell,
|
||||||
if (args.column.key === 'content') {
|
|
||||||
const raw = args.text || '';
|
|
||||||
return raw ? <LogContentCell text={raw} /> : <span>-</span>;
|
|
||||||
}
|
|
||||||
return renderBodyCell(args);
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
<div class={pageStyles.pagination}>
|
<div class={pageStyles.pagination}>
|
||||||
<Pagination
|
<Pagination
|
||||||
current={pagination.value.current}
|
current={(pagination as any).current.value}
|
||||||
pageSize={pagination.value.pageSize}
|
pageSize={(pagination as any).pageSize.value}
|
||||||
total={pagination.value.total}
|
total={(pagination as any).total.value}
|
||||||
showSizeChanger
|
showSizeChanger
|
||||||
showTotal={(total: number) => `共 ${total} 条`}
|
showTotal={(total: number) => `共 ${total} 条`}
|
||||||
onChange={handlePageChange}
|
onChange={handlePageChange}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* 操作日志页 - 配置文件
|
||||||
|
* 集中管理筛选默认值、字段映射、选项配置
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 筛选默认值
|
||||||
|
// ============================================================
|
||||||
|
export const FILTER_DEFAULTS = {
|
||||||
|
dateRange: null as [string, string] | null,
|
||||||
|
type: '',
|
||||||
|
source: '',
|
||||||
|
sourceNickname: '',
|
||||||
|
sourcePhone: '',
|
||||||
|
tournamentName: '',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 字段映射配置
|
||||||
|
// 用于 buildQueryParams 函数,实现配置化参数转换
|
||||||
|
// ============================================================
|
||||||
|
type FieldMapping = [keyof typeof FILTER_DEFAULTS, string, ((v: any) => any)?];
|
||||||
|
|
||||||
|
export const FIELD_MAPPINGS: FieldMapping[] = [
|
||||||
|
['type', 'type'],
|
||||||
|
['source', 'source'],
|
||||||
|
['sourceNickname', 'sourceNickname', (v) => v?.trim?.() ?? v],
|
||||||
|
['sourcePhone', 'sourcePhone', (v) => v?.trim?.() ?? v],
|
||||||
|
['tournamentName', 'tournamentName', (v) => v?.trim?.() ?? v],
|
||||||
|
];
|
||||||
|
|
||||||
|
// 特殊字段映射(不在 FIELD_MAPPINGS 中)
|
||||||
|
export const DATE_RANGE_MAPPING = {
|
||||||
|
formKey: 'dateRange' as const,
|
||||||
|
beginKey: 'dateBegin' as const,
|
||||||
|
endKey: 'dateEnd' as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 选项配置
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/** 操作类型选项(type: 1=创建赛事,2=删除赛事,3=取消报名,4=编辑选手,5=对阵管理,6=批量修改组别) */
|
||||||
|
export const ACTION_TYPE_OPTIONS = [
|
||||||
|
{ value: '', label: '全部' },
|
||||||
|
{ value: '1', label: '创建赛事' },
|
||||||
|
{ value: '2', label: '删除赛事' },
|
||||||
|
{ value: '3', label: '取消报名' },
|
||||||
|
{ value: '4', label: '编辑选手' },
|
||||||
|
{ value: '5', label: '对阵管理' },
|
||||||
|
{ value: '6', label: '批量修改组别' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** 操作类型文案映射 */
|
||||||
|
export const ACTION_TYPE_MAP: Record<string, string> = {
|
||||||
|
'1': '创建赛事',
|
||||||
|
'2': '删除赛事',
|
||||||
|
'3': '取消报名',
|
||||||
|
'4': '编辑选手',
|
||||||
|
'5': '对阵管理',
|
||||||
|
'6': '批量修改组别',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 操作来源选项(value 对应 API source: 1=PC,2=小程序) */
|
||||||
|
export const ACTION_SOURCE_OPTIONS = [
|
||||||
|
{ value: '', label: '全部' },
|
||||||
|
{ value: '1', label: 'PC' },
|
||||||
|
{ value: '2', label: '小程序' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** 操作来源文案映射 */
|
||||||
|
export const ACTION_SOURCE_MAP: Record<string, string> = {
|
||||||
|
'1': 'PC',
|
||||||
|
'2': '小程序',
|
||||||
|
};
|
||||||
@@ -20,7 +20,7 @@ export type {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// URL 常量
|
// URL 常量
|
||||||
// ============================================================
|
// ============================================================
|
||||||
const operationLogs = '/tournament/operation/page';
|
const operationLogs = '/admin/manager/operation/page';
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// API 函数
|
// API 函数
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { type VNodeChild } from 'vue';
|
||||||
|
import { ACTION_TYPE_MAP, ACTION_SOURCE_MAP } from './config';
|
||||||
|
|
||||||
|
const renderType = (text: string): string => ACTION_TYPE_MAP[text] || text || '-';
|
||||||
|
|
||||||
|
const renderSource = (text: string): string => ACTION_SOURCE_MAP[text] || text || '-';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作日志页 - 表格列配置(视图层)
|
||||||
|
*
|
||||||
|
* 职责:纯表格列配置,不包含业务逻辑
|
||||||
|
* 操作内容列通过 customRender 占位,在 index.tsx 中通过 bodyCell slot 渲染
|
||||||
|
*/
|
||||||
|
export function useLogColumns() {
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '操作类型',
|
||||||
|
dataIndex: 'type',
|
||||||
|
key: 'type',
|
||||||
|
width: 140,
|
||||||
|
align: 'center' as const,
|
||||||
|
customRender: ({ text }: { text: string }): VNodeChild => renderType(text),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作来源',
|
||||||
|
dataIndex: 'source',
|
||||||
|
key: 'source',
|
||||||
|
width: 100,
|
||||||
|
align: 'center' as const,
|
||||||
|
customRender: ({ text }: { text: string }): VNodeChild => renderSource(text),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作人昵称',
|
||||||
|
dataIndex: 'nickname',
|
||||||
|
key: 'nickname',
|
||||||
|
width: 120,
|
||||||
|
align: 'center' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作人手机号',
|
||||||
|
dataIndex: 'phone',
|
||||||
|
key: 'phone',
|
||||||
|
width: 140,
|
||||||
|
align: 'center' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '赛事名称',
|
||||||
|
dataIndex: 'tournamentName',
|
||||||
|
key: 'tournamentName',
|
||||||
|
width: 200,
|
||||||
|
align: 'center' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作对象',
|
||||||
|
dataIndex: 'obj',
|
||||||
|
key: 'obj',
|
||||||
|
width: 240,
|
||||||
|
align: 'center' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作内容',
|
||||||
|
dataIndex: 'content',
|
||||||
|
key: 'content',
|
||||||
|
width: 360,
|
||||||
|
align: 'center' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作时间',
|
||||||
|
dataIndex: 'createDate',
|
||||||
|
key: 'createDate',
|
||||||
|
width: 170,
|
||||||
|
align: 'center' as const,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return { columns };
|
||||||
|
}
|
||||||
@@ -1,46 +1,44 @@
|
|||||||
import { computed, reactive, toRef, Ref } from 'vue';
|
import { computed, reactive, toRef, Ref, type UnwrapRef } from 'vue';
|
||||||
import { message } from 'ant-design-vue';
|
import { useDebounce } from '@/hooks';
|
||||||
import { useState, useDebounce, useThrottleFn } from '@/hooks';
|
import { useRequest } from '@/hooks/useRequest';
|
||||||
|
import { useEffect } from '@/hooks/useEffect';
|
||||||
|
import { usePagination, type UsePaginationReturn } from '@/hooks/usePagination';
|
||||||
|
import { hasValue, isEmptyValue, safeTransform } from '@/utils';
|
||||||
import {
|
import {
|
||||||
getOperationLogs,
|
getOperationLogs,
|
||||||
type TournamentAdminOperationPageVO,
|
type TournamentAdminOperationPageVO,
|
||||||
type OperationLogQueryParams,
|
type OperationLogQueryParams,
|
||||||
|
type PageData,
|
||||||
|
type ApiResult,
|
||||||
} from './services';
|
} from './services';
|
||||||
|
import {
|
||||||
|
FILTER_DEFAULTS,
|
||||||
|
FIELD_MAPPINGS,
|
||||||
|
DATE_RANGE_MAPPING,
|
||||||
|
ACTION_TYPE_OPTIONS,
|
||||||
|
ACTION_TYPE_MAP,
|
||||||
|
ACTION_SOURCE_OPTIONS,
|
||||||
|
ACTION_SOURCE_MAP,
|
||||||
|
} from './config';
|
||||||
|
|
||||||
// ============================================================
|
export { ACTION_TYPE_OPTIONS, ACTION_TYPE_MAP, ACTION_SOURCE_OPTIONS, ACTION_SOURCE_MAP };
|
||||||
// 常量
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
/** 操作类型选项(TODO: API type 字段待定意,暂时留空) */
|
// 提取 Pagination Ref 类型
|
||||||
export const ACTION_TYPE_OPTIONS = [{ value: '', label: '全部' }] as const;
|
type PaginationRef = UnwrapRef<UsePaginationReturn>;
|
||||||
|
type PageSizeRef = PaginationRef['pageSize'];
|
||||||
/** 操作来源选项(value 对应 API source: 1=PC,2=小程序) */
|
|
||||||
export const ACTION_SOURCE_OPTIONS = [
|
|
||||||
{ value: '', label: '全部' },
|
|
||||||
{ value: '1', label: 'PC' },
|
|
||||||
{ value: '2', label: '小程序' },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
/** 操作来源文案映射 */
|
|
||||||
export const ACTION_SOURCE_MAP: Record<string, string> = {
|
|
||||||
'1': 'PC',
|
|
||||||
'2': '小程序',
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Model
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作日志页数据模型
|
||||||
|
* 职责:状态管理 + 数据获取 + 业务逻辑 + 交互处理
|
||||||
|
*
|
||||||
|
* 架构分层:
|
||||||
|
* - 数据层:services.ts(API 调用)
|
||||||
|
* - 逻辑层:本文件(状态 + 业务逻辑)
|
||||||
|
* - 视图层:useLogColumns.ts + index.tsx(纯 UI 渲染)
|
||||||
|
*/
|
||||||
export function useLogModel() {
|
export function useLogModel() {
|
||||||
// ===== 筛选条件(key 名对齐 API 查询参数) =====
|
const filterForm = reactive({ ...FILTER_DEFAULTS });
|
||||||
const filterForm = reactive({
|
const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
|
||||||
dateRange: null as [string, string] | null,
|
|
||||||
type: '',
|
|
||||||
source: '',
|
|
||||||
sourceNickname: '',
|
|
||||||
sourcePhone: '',
|
|
||||||
tournamentName: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const { debouncedValue: debouncedNickname } = useDebounce(
|
const { debouncedValue: debouncedNickname } = useDebounce(
|
||||||
toRef(filterForm, 'sourceNickname') as Ref<string>,
|
toRef(filterForm, 'sourceNickname') as Ref<string>,
|
||||||
@@ -55,42 +53,59 @@ export function useLogModel() {
|
|||||||
{ delay: 300 },
|
{ delay: 300 },
|
||||||
);
|
);
|
||||||
|
|
||||||
// ===== 表格状态 =====
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
const [dataSource, setDataSource] = useState<TournamentAdminOperationPageVO[]>([]);
|
|
||||||
const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
|
|
||||||
|
|
||||||
// ===== 表格列配置(dataIndex 对齐 TournamentAdminOperationPageVO) =====
|
|
||||||
const columns = [
|
|
||||||
{ title: '操作类型', dataIndex: 'opName', key: 'opName', width: 140 },
|
|
||||||
{ title: '操作来源', dataIndex: 'source', key: 'source', width: 100 },
|
|
||||||
{ title: '操作人昵称', dataIndex: 'nickname', key: 'nickname', width: 120 },
|
|
||||||
{ title: '操作人手机号', dataIndex: 'phone', key: 'phone', width: 140 },
|
|
||||||
{ title: '赛事名称', dataIndex: 'tournamentName', key: 'tournamentName', width: 200 },
|
|
||||||
{ title: '操作对象', dataIndex: 'obj', key: 'obj', width: 240 },
|
|
||||||
{ title: '操作内容', dataIndex: 'content', key: 'content', width: 360 },
|
|
||||||
{ title: '操作时间', dataIndex: 'createDate', key: 'createDate', width: 170 },
|
|
||||||
];
|
|
||||||
|
|
||||||
// ===== 构建 API 查询参数 =====
|
|
||||||
const buildQueryParams = (): OperationLogQueryParams => {
|
const buildQueryParams = (): OperationLogQueryParams => {
|
||||||
const params: OperationLogQueryParams = {
|
const { page, pageSize } = pagination.params.value;
|
||||||
page: String(pagination.value.current),
|
|
||||||
limit: String(pagination.value.pageSize),
|
// 基础参数
|
||||||
};
|
const params = FIELD_MAPPINGS.reduce(
|
||||||
if (filterForm.dateRange) {
|
(acc, [formKey, paramKey, transform]) => {
|
||||||
params.dateBegin = filterForm.dateRange[0];
|
const rawValue = filterForm[formKey];
|
||||||
params.dateEnd = filterForm.dateRange[1];
|
if (!hasValue(rawValue)) return acc;
|
||||||
|
|
||||||
|
const value = safeTransform(rawValue, transform);
|
||||||
|
if (isEmptyValue(value)) return acc;
|
||||||
|
|
||||||
|
return { ...acc, [paramKey]: value };
|
||||||
|
},
|
||||||
|
{} as Record<string, any>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 时间范围特殊处理
|
||||||
|
const dateRange = filterForm[DATE_RANGE_MAPPING.formKey];
|
||||||
|
if (Array.isArray(dateRange) && dateRange.length === 2) {
|
||||||
|
params[DATE_RANGE_MAPPING.beginKey] = dateRange[0];
|
||||||
|
params[DATE_RANGE_MAPPING.endKey] = dateRange[1];
|
||||||
}
|
}
|
||||||
if (filterForm.type) params.type = filterForm.type;
|
|
||||||
if (filterForm.source) params.source = filterForm.source;
|
return { page: String(page), limit: String(pageSize), ...params } as OperationLogQueryParams;
|
||||||
if (filterForm.sourceNickname.trim()) params.sourceNickname = filterForm.sourceNickname.trim();
|
|
||||||
if (filterForm.sourcePhone.trim()) params.sourcePhone = filterForm.sourcePhone.trim();
|
|
||||||
if (filterForm.tournamentName.trim()) params.tournamentName = filterForm.tournamentName.trim();
|
|
||||||
return params;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== 计算属性 =====
|
const {
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
run: fetchList,
|
||||||
|
} = useRequest<ApiResult<PageData<TournamentAdminOperationPageVO>>>(
|
||||||
|
() => getOperationLogs(buildQueryParams()),
|
||||||
|
{
|
||||||
|
refreshDeps: [],
|
||||||
|
formatResult: (res) => res,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 提取列表数据
|
||||||
|
const listData = computed<PageData<TournamentAdminOperationPageVO> | 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 hasFilter = computed(() => {
|
||||||
return (
|
return (
|
||||||
filterForm.dateRange !== null ||
|
filterForm.dateRange !== null ||
|
||||||
@@ -102,49 +117,31 @@ export function useLogModel() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== 方法 =====
|
const handleSearch = () => fetchList();
|
||||||
|
|
||||||
const handleSearch = useThrottleFn(async () => {
|
const handleReset = () => {
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const params = buildQueryParams();
|
|
||||||
console.log('操作日志查询参数:', params);
|
|
||||||
const res = await getOperationLogs(params);
|
|
||||||
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);
|
|
||||||
|
|
||||||
const handleReset = useThrottleFn(() => {
|
|
||||||
filterForm.dateRange = null;
|
filterForm.dateRange = null;
|
||||||
filterForm.type = '';
|
filterForm.type = '';
|
||||||
filterForm.source = '';
|
filterForm.source = '';
|
||||||
filterForm.sourceNickname = '';
|
filterForm.sourceNickname = '';
|
||||||
filterForm.sourcePhone = '';
|
filterForm.sourcePhone = '';
|
||||||
filterForm.tournamentName = '';
|
filterForm.tournamentName = '';
|
||||||
setPagination({ current: 1, pageSize: 10, total: 0 });
|
pagination.reset();
|
||||||
setDataSource([]);
|
setTimeout(() => fetchList(), 0);
|
||||||
setTimeout(() => handleSearch(), 350);
|
};
|
||||||
}, 500);
|
|
||||||
|
|
||||||
const handlePageChange = (page: number, pageSize: number) => {
|
const handlePageChange = (page: number, pageSize: number) => {
|
||||||
setPagination({ ...pagination.value, current: page, pageSize });
|
pagination.setCurrent(page);
|
||||||
handleSearch();
|
if (pageSize !== (pagination.pageSize as PageSizeRef)) {
|
||||||
|
pagination.setPageSize(pageSize);
|
||||||
|
}
|
||||||
|
fetchList();
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filterForm,
|
filterForm,
|
||||||
loading,
|
loading,
|
||||||
dataSource,
|
dataSource,
|
||||||
columns,
|
|
||||||
pagination,
|
pagination,
|
||||||
hasFilter,
|
hasFilter,
|
||||||
handleSearch,
|
handleSearch,
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { computed, reactive, toRef, Ref, h } from 'vue';
|
import { computed, reactive, toRef, Ref, h } from 'vue';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import { StatusTag, type StatusTagTone } from '@/components';
|
import { StatusTag, type StatusTagTone } from '@/components';
|
||||||
import { useState, useDebounce } from '@/hooks';
|
import { useDebounce } from '@/hooks';
|
||||||
import { useRequest } from '@/hooks/useRequest';
|
import { useRequest } from '@/hooks/useRequest';
|
||||||
import { useEffect } from '@/hooks/useEffect';
|
import { useEffect } from '@/hooks/useEffect';
|
||||||
import { usePagination } from '@/hooks/usePagination';
|
import { usePagination } from '@/hooks/usePagination';
|
||||||
|
import { hasValue, isEmptyValue, safeTransform } from '@/utils';
|
||||||
import type {
|
import type {
|
||||||
TournamentAdminOrderPageVO,
|
TournamentAdminOrderPageVO,
|
||||||
OrderListQueryParams,
|
OrderListQueryParams,
|
||||||
@@ -239,23 +240,3 @@ export function useOrderModel() {
|
|||||||
handlePageChange,
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import { defineComponent, onMounted } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import {
|
import { Button, Input, Table, Form, Space, Select, Pagination } from 'ant-design-vue';
|
||||||
Button,
|
import { useUserModel, USER_STATUS_OPTIONS } from './model/useUserModel';
|
||||||
Input,
|
import { useUserColumns } from './model/useUserColumns';
|
||||||
Table,
|
|
||||||
Form,
|
|
||||||
Space,
|
|
||||||
Select,
|
|
||||||
Pagination,
|
|
||||||
Modal,
|
|
||||||
message,
|
|
||||||
} from 'ant-design-vue';
|
|
||||||
import { useUserModel, USER_STATUS_OPTIONS, ROLE_OPTIONS } from './model/useUserModel';
|
|
||||||
import { toggleUserActive } from './model/services';
|
|
||||||
import { useContainerSize } from '@/hooks';
|
import { useContainerSize } from '@/hooks';
|
||||||
import pageStyles from '@/assets/styles/pageLayout.module.less';
|
import pageStyles from '@/assets/styles/pageLayout.module.less';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户列表页面(纯视图层)
|
||||||
|
*
|
||||||
|
* 架构分层:
|
||||||
|
* - 数据层:services.ts(API 调用)
|
||||||
|
* - 逻辑层:useUserModel.ts(状态 + 业务逻辑 + 交互处理)
|
||||||
|
* - 视图层:useUserColumns.ts(表格列配置)+ 本组件(纯 UI 渲染)
|
||||||
|
*/
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'EventUsers',
|
name: 'EventUsers',
|
||||||
setup() {
|
setup() {
|
||||||
@@ -22,36 +20,33 @@ export default defineComponent({
|
|||||||
filterForm,
|
filterForm,
|
||||||
loading,
|
loading,
|
||||||
dataSource,
|
dataSource,
|
||||||
columns,
|
|
||||||
pagination,
|
pagination,
|
||||||
|
togglingId,
|
||||||
handleSearch,
|
handleSearch,
|
||||||
handleReset,
|
handleReset,
|
||||||
handlePageChange,
|
handlePageChange,
|
||||||
|
handleToggleStatus,
|
||||||
} = useUserModel();
|
} = useUserModel();
|
||||||
|
|
||||||
|
const { columns } = useUserColumns();
|
||||||
const { containerRef, height } = useContainerSize();
|
const { containerRef, height } = useContainerSize();
|
||||||
|
|
||||||
const handleToggleStatus = (record: any) => {
|
/** 渲染操作列 */
|
||||||
|
const renderAction = (record: any) => {
|
||||||
const isActive = record.status === '1';
|
const isActive = record.status === '1';
|
||||||
const actionText = isActive ? '停用' : '启用';
|
return (
|
||||||
Modal.confirm({
|
<Button
|
||||||
title: `${actionText}确认`,
|
type="link"
|
||||||
content: `确认${actionText}用户"${record.realName}"吗?`,
|
size="small"
|
||||||
okText: '确认',
|
loading={togglingId.value === record.id}
|
||||||
cancelText: '取消',
|
onClick={() => handleToggleStatus(record)}
|
||||||
onOk: async () => {
|
>
|
||||||
try {
|
{isActive ? '停用' : '启用'}
|
||||||
const res = await toggleUserActive({ userId: record.id, status: isActive ? '0' : '1' });
|
</Button>
|
||||||
if (res.code == 200) {
|
);
|
||||||
message.success(`${actionText}成���`);
|
|
||||||
handleSearch();
|
|
||||||
} else message.error(res.msg || '操作失败');
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(`用户${actionText}失败:`, e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 完整表格列 */
|
||||||
const tableColumns = [
|
const tableColumns = [
|
||||||
...columns,
|
...columns,
|
||||||
{
|
{
|
||||||
@@ -63,10 +58,6 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
handleSearch();
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => (
|
return () => (
|
||||||
<div class={pageStyles.containerMain}>
|
<div class={pageStyles.containerMain}>
|
||||||
<div class={pageStyles.filter}>
|
<div class={pageStyles.filter}>
|
||||||
@@ -81,16 +72,6 @@ export default defineComponent({
|
|||||||
onPressEnter={handleSearch}
|
onPressEnter={handleSearch}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="角色" name="roleId">
|
|
||||||
<Select
|
|
||||||
value={filterForm.roleId}
|
|
||||||
options={ROLE_OPTIONS as any}
|
|
||||||
style={{ width: '140px' }}
|
|
||||||
allowClear
|
|
||||||
placeholder="全部"
|
|
||||||
onUpdate:value={(val: any) => (filterForm.roleId = val || '')}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="状态" name="status">
|
<Form.Item label="状态" name="status">
|
||||||
<Select
|
<Select
|
||||||
value={filterForm.status}
|
value={filterForm.status}
|
||||||
@@ -110,6 +91,7 @@ export default defineComponent({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class={pageStyles.table}>
|
<div class={pageStyles.table}>
|
||||||
<div ref={containerRef} class={pageStyles.tableBody}>
|
<div ref={containerRef} class={pageStyles.tableBody}>
|
||||||
<Table
|
<Table
|
||||||
@@ -122,16 +104,7 @@ export default defineComponent({
|
|||||||
{{
|
{{
|
||||||
bodyCell: (args: any) => {
|
bodyCell: (args: any) => {
|
||||||
if (args.column.key === 'action') {
|
if (args.column.key === 'action') {
|
||||||
const isActive = args.record.status === '1';
|
return renderAction(args.record);
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
size="small"
|
|
||||||
onClick={() => handleToggleStatus(args.record)}
|
|
||||||
>
|
|
||||||
{isActive ? '停用' : '启用'}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -139,9 +112,9 @@ export default defineComponent({
|
|||||||
</div>
|
</div>
|
||||||
<div class={pageStyles.pagination}>
|
<div class={pageStyles.pagination}>
|
||||||
<Pagination
|
<Pagination
|
||||||
current={pagination.value.current}
|
current={(pagination as any).current.value}
|
||||||
pageSize={pagination.value.pageSize}
|
pageSize={(pagination as any).pageSize.value}
|
||||||
total={pagination.value.total}
|
total={(pagination as any).total.value}
|
||||||
showSizeChanger
|
showSizeChanger
|
||||||
showTotal={(total: number) => `共 ${total} 条`}
|
showTotal={(total: number) => `共 ${total} 条`}
|
||||||
onChange={handlePageChange}
|
onChange={handlePageChange}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* 用户列表页 - 配置文件
|
||||||
|
* 集中管理筛选默认值、字段映射、选项配置
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 筛选默认值
|
||||||
|
// ============================================================
|
||||||
|
export const FILTER_DEFAULTS = {
|
||||||
|
text: '',
|
||||||
|
roleId: '',
|
||||||
|
status: '',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 字段映射配置
|
||||||
|
// 用于 buildQueryParams 函数,实现配置化参数转换
|
||||||
|
// ============================================================
|
||||||
|
type FieldMapping = [keyof typeof FILTER_DEFAULTS, string, ((v: any) => any)?];
|
||||||
|
|
||||||
|
export const FIELD_MAPPINGS: FieldMapping[] = [
|
||||||
|
['text', 'text', (v) => v?.trim?.() ?? v],
|
||||||
|
['roleId', 'roleId'],
|
||||||
|
['status', 'status'],
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 选项配置
|
||||||
|
// ============================================================
|
||||||
|
export const USER_STATUS_OPTIONS = [
|
||||||
|
{ value: '', label: '全部' },
|
||||||
|
{ value: '0', label: '停用' },
|
||||||
|
{ value: '1', label: '正常' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** 角色选项(TODO: 对接角色 API) */
|
||||||
|
export const ROLE_OPTIONS = [
|
||||||
|
{ value: '', label: '全部' },
|
||||||
|
{ value: '1', label: '超级管理员' },
|
||||||
|
{ value: '2', label: '赛事管理员' },
|
||||||
|
{ value: '3', label: '裁判' },
|
||||||
|
{ value: '4', label: '财务' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 状态映射(用于 StatusTag 渲染)
|
||||||
|
// ============================================================
|
||||||
|
import type { StatusTagTone } from '@/components';
|
||||||
|
|
||||||
|
export const STATUS_MAP: Record<string, { label: string; tone: StatusTagTone }> = {
|
||||||
|
'1': { label: '正常', tone: 'success' },
|
||||||
|
'0': { label: '停用', tone: 'danger' },
|
||||||
|
};
|
||||||
@@ -30,11 +30,11 @@ export type {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// URL 常量
|
// URL 常量
|
||||||
// ============================================================
|
// ============================================================
|
||||||
const userList = '/sys/user/page';
|
const userList = '/admin/manager/user/page';
|
||||||
const userSave = '/sys/user/save';
|
const userSave = '/admin/sys/user/save';
|
||||||
const userUpdate = '/sys/user/update';
|
const userUpdate = '/admin/sys/user/update';
|
||||||
const userActive = '/sys/user/active';
|
const userActive = '/admin/manager/user/active';
|
||||||
const userUpdatePwd = '/sys/user/updatepwd';
|
const userUpdatePwd = '/admin/sys/user/updatepwd';
|
||||||
|
|
||||||
const delay = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
const delay = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { h, type VNodeChild } from 'vue';
|
||||||
|
import { Image, Space } from 'ant-design-vue';
|
||||||
|
import type { ImageProps } from 'ant-design-vue/es/image';
|
||||||
|
import { StatusTag } from '@/components';
|
||||||
|
import { STATUS_MAP } from './config';
|
||||||
|
|
||||||
|
// 性别映射
|
||||||
|
const GENDER_MAP: Record<string, string> = {
|
||||||
|
'1': '男',
|
||||||
|
'2': '女',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户列表页 - 表格列配置(视图层)
|
||||||
|
*
|
||||||
|
* 职责:纯表格列配置,不包含业务逻辑
|
||||||
|
* 操作列通过 customRender 占位,在 index.tsx 中通过 bodyCell slot 渲染
|
||||||
|
*/
|
||||||
|
export function useUserColumns() {
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '用户昵称',
|
||||||
|
dataIndex: 'nickname',
|
||||||
|
key: 'nickname',
|
||||||
|
width: 140,
|
||||||
|
align: 'center' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '用户姓名',
|
||||||
|
dataIndex: 'realName',
|
||||||
|
key: 'realName',
|
||||||
|
width: 110,
|
||||||
|
align: 'center' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '手机号',
|
||||||
|
dataIndex: 'phone',
|
||||||
|
key: 'phone',
|
||||||
|
width: 140,
|
||||||
|
align: 'center' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '性别',
|
||||||
|
dataIndex: 'gender',
|
||||||
|
key: 'gender',
|
||||||
|
width: 80,
|
||||||
|
align: 'center' as const,
|
||||||
|
customRender: ({ text }: { text: string }) => GENDER_MAP[text] || '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '证件号',
|
||||||
|
dataIndex: 'idCard',
|
||||||
|
key: 'idCard',
|
||||||
|
width: 120,
|
||||||
|
align: 'center' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '证件图片',
|
||||||
|
dataIndex: 'idCardImgList',
|
||||||
|
key: 'idCardImgList',
|
||||||
|
width: 120,
|
||||||
|
align: 'center' as const,
|
||||||
|
customRender: ({ text }: { text: string[] }): VNodeChild => {
|
||||||
|
const imgList = Array.isArray(text) ? text : [];
|
||||||
|
if (imgList.length === 0) return '-';
|
||||||
|
return h(
|
||||||
|
Space,
|
||||||
|
{ size: 4 },
|
||||||
|
{
|
||||||
|
default: () =>
|
||||||
|
imgList.map((url, index) =>
|
||||||
|
h<ImageProps>(Image, {
|
||||||
|
key: index,
|
||||||
|
src: url,
|
||||||
|
alt: `证件图${index + 1}`,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
style: { objectFit: 'cover', borderRadius: '4px', cursor: 'pointer' },
|
||||||
|
preview: true,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'createTime',
|
||||||
|
key: 'createTime',
|
||||||
|
width: 170,
|
||||||
|
align: 'center' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
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 });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return { columns };
|
||||||
|
}
|
||||||
@@ -1,125 +1,168 @@
|
|||||||
import { computed, reactive, toRef, Ref, h } from 'vue';
|
import { computed, reactive, toRef, Ref, type UnwrapRef } from 'vue';
|
||||||
import { message } from 'ant-design-vue';
|
import { Modal, message } from 'ant-design-vue';
|
||||||
import { StatusTag, type StatusTagTone } from '@/components';
|
import { useState, useDebounce } from '@/hooks';
|
||||||
import { useState, useDebounce, useThrottleFn } from '@/hooks';
|
import { useRequest } from '@/hooks/useRequest';
|
||||||
import { getUserList, type TournamentAdminUserVO, type UserListQueryParams } from './services';
|
import { useEffect } from '@/hooks/useEffect';
|
||||||
|
import { usePagination, type UsePaginationReturn } from '@/hooks/usePagination';
|
||||||
|
import { hasValue, isEmptyValue, safeTransform } from '@/utils';
|
||||||
|
import {
|
||||||
|
getUserList,
|
||||||
|
toggleUserActive,
|
||||||
|
type TournamentAdminUserVO,
|
||||||
|
type UserListQueryParams,
|
||||||
|
type PageData,
|
||||||
|
type ApiResult,
|
||||||
|
} from './services';
|
||||||
|
import {
|
||||||
|
FILTER_DEFAULTS,
|
||||||
|
FIELD_MAPPINGS,
|
||||||
|
USER_STATUS_OPTIONS,
|
||||||
|
ROLE_OPTIONS,
|
||||||
|
STATUS_MAP,
|
||||||
|
} from './config';
|
||||||
|
|
||||||
// ============================================================
|
export { USER_STATUS_OPTIONS, ROLE_OPTIONS, STATUS_MAP };
|
||||||
// 常量
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
export const USER_STATUS_OPTIONS = [
|
// 提取 Pagination Ref 类型
|
||||||
{ value: '', label: '全部' },
|
type PaginationRef = UnwrapRef<UsePaginationReturn>;
|
||||||
{ value: '0', label: '停用' },
|
type PageSizeRef = PaginationRef['pageSize'];
|
||||||
{ value: '1', label: '正常' },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
/** 角色选项(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<string, { label: string; tone: StatusTagTone }> = {
|
|
||||||
'1': { label: '正常', tone: 'success' },
|
|
||||||
'0': { label: '停用', tone: 'danger' },
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Model
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户列表页数据模型
|
||||||
|
* 职责:状态管理 + 数据获取 + 业务逻辑 + 交互处理
|
||||||
|
*
|
||||||
|
* 架构分层:
|
||||||
|
* - 数据层:services.ts(API 调用)
|
||||||
|
* - 逻辑层:本文件(状态 + 业务逻辑)
|
||||||
|
* - 视图层:useUserColumns.ts + index.tsx(纯 UI 渲染)
|
||||||
|
*/
|
||||||
export function useUserModel() {
|
export function useUserModel() {
|
||||||
const filterForm = reactive({
|
// ===== 筛选表单状态 =====
|
||||||
text: '',
|
const filterForm = reactive({ ...FILTER_DEFAULTS });
|
||||||
roleId: '',
|
|
||||||
status: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// ===== 弹窗状态 =====
|
||||||
|
const [togglingId, setTogglingId] = useState('');
|
||||||
|
|
||||||
|
// ===== 分页管理 =====
|
||||||
|
const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
|
||||||
|
|
||||||
|
// ===== 防抖处理 =====
|
||||||
const { debouncedValue: debouncedText } = useDebounce(toRef(filterForm, 'text') as Ref<string>, {
|
const { debouncedValue: debouncedText } = useDebounce(toRef(filterForm, 'text') as Ref<string>, {
|
||||||
delay: 300,
|
delay: 300,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
// ===== 构建查询参数 =====
|
||||||
const [dataSource, setDataSource] = useState<TournamentAdminUserVO[]>([]);
|
|
||||||
const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{ title: '用户姓名', dataIndex: 'realName', key: 'realName', width: 120 },
|
|
||||||
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 140 },
|
|
||||||
{ title: '角色', dataIndex: 'roleName', key: 'roleName', width: 120 },
|
|
||||||
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 170 },
|
|
||||||
{
|
|
||||||
title: '状态',
|
|
||||||
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 });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const buildQueryParams = (): UserListQueryParams => {
|
const buildQueryParams = (): UserListQueryParams => {
|
||||||
const params: UserListQueryParams = {
|
const { page, pageSize } = pagination.params.value;
|
||||||
page: String(pagination.value.current),
|
|
||||||
limit: String(pagination.value.pageSize),
|
|
||||||
};
|
|
||||||
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(
|
const params = FIELD_MAPPINGS.reduce(
|
||||||
() => debouncedText.value.trim() !== '' || filterForm.roleId !== '' || filterForm.status !== '',
|
(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>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSearch = useThrottleFn(async () => {
|
return { page: String(page), limit: String(pageSize), ...params } as UserListQueryParams;
|
||||||
setLoading(true);
|
};
|
||||||
try {
|
|
||||||
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 (e: any) {
|
|
||||||
console.error('用户列表查询失败:', e);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
const handleReset = useThrottleFn(() => {
|
// ===== 数据请求 =====
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
run: fetchList,
|
||||||
|
} = useRequest<ApiResult<PageData<TournamentAdminUserVO>>>(
|
||||||
|
() => getUserList(buildQueryParams()),
|
||||||
|
{
|
||||||
|
refreshDeps: [],
|
||||||
|
formatResult: (res) => res,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 提取列表数据
|
||||||
|
const listData = computed<PageData<TournamentAdminUserVO> | 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 !!debouncedText.value?.trim() || !!f.roleId || !!f.status;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 事件处理 =====
|
||||||
|
const handleSearch = () => fetchList();
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
filterForm.text = '';
|
filterForm.text = '';
|
||||||
filterForm.roleId = '';
|
filterForm.roleId = '';
|
||||||
filterForm.status = '';
|
filterForm.status = '';
|
||||||
setPagination({ current: 1, pageSize: 10, total: 0 });
|
pagination.reset();
|
||||||
setDataSource([]);
|
setTimeout(() => fetchList(), 0);
|
||||||
setTimeout(() => handleSearch(), 350);
|
};
|
||||||
}, 500);
|
|
||||||
|
|
||||||
const handlePageChange = (page: number, pageSize: number) => {
|
const handlePageChange = (page: number, pageSize: number) => {
|
||||||
setPagination({ ...pagination.value, current: page, pageSize });
|
pagination.setCurrent(page);
|
||||||
handleSearch();
|
if (pageSize !== (pagination.pageSize as PageSizeRef)) {
|
||||||
|
pagination.setPageSize(pageSize);
|
||||||
|
}
|
||||||
|
fetchList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== 启用/停用用户 =====
|
||||||
|
const handleToggleStatus = async (record: TournamentAdminUserVO) => {
|
||||||
|
const isActive = record.status === '1';
|
||||||
|
const actionText = isActive ? '停用' : '启用';
|
||||||
|
|
||||||
|
Modal.confirm({
|
||||||
|
title: `${actionText}确认`,
|
||||||
|
content: `确认${actionText}用户"${record.realName}"吗?`,
|
||||||
|
okText: '确认',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: async () => {
|
||||||
|
setTogglingId(record.id);
|
||||||
|
try {
|
||||||
|
const res = await toggleUserActive({ userId: record.id, status: isActive ? '0' : '1' });
|
||||||
|
if (res.code == 200) {
|
||||||
|
message.success(`${actionText}成功`);
|
||||||
|
fetchList();
|
||||||
|
} else {
|
||||||
|
message.error(res.msg || '操作失败');
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(`用户${actionText}失败:`, e);
|
||||||
|
message.error(`用户${actionText}失败`);
|
||||||
|
} finally {
|
||||||
|
setTogglingId('');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filterForm,
|
filterForm,
|
||||||
loading,
|
loading,
|
||||||
dataSource,
|
dataSource,
|
||||||
columns,
|
|
||||||
pagination,
|
pagination,
|
||||||
|
togglingId,
|
||||||
hasFilter,
|
hasFilter,
|
||||||
handleSearch,
|
handleSearch,
|
||||||
handleReset,
|
handleReset,
|
||||||
handlePageChange,
|
handlePageChange,
|
||||||
|
handleToggleStatus,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,35 @@
|
|||||||
* 通用工具函数
|
* 通用工具函数
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断值是否有意义(非空、非空字符串、非空数组)
|
||||||
|
*/
|
||||||
|
export function hasValue(v: unknown): 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断值是否为空(null、undefined、空字符串)
|
||||||
|
*/
|
||||||
|
export function isEmptyValue(v: unknown): boolean {
|
||||||
|
return v == null || (typeof v === 'string' && v.trim() === '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全转换值(带异常处理和默认值)
|
||||||
|
*/
|
||||||
|
export function safeTransform<T>(value: T, transform?: (v: T) => unknown): T | unknown {
|
||||||
|
if (!transform) return value;
|
||||||
|
try {
|
||||||
|
return transform(value) ?? value;
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成唯一 ID
|
* 生成唯一 ID
|
||||||
*/
|
*/
|
||||||
|
|||||||
+8
-22
@@ -6,9 +6,6 @@ const DEFAULT_TIMEOUT = 10000;
|
|||||||
|
|
||||||
const activeControllers = new Set<AbortController>();
|
const activeControllers = new Set<AbortController>();
|
||||||
|
|
||||||
const getResponseErrorMsg = (data: any, fallback = '请求失败') =>
|
|
||||||
data?.data?.msg || data?.msg || data?.message || fallback;
|
|
||||||
|
|
||||||
/** 统一处理 401 未授权逻辑 */
|
/** 统一处理 401 未授权逻辑 */
|
||||||
const handleUnauthorized = (msg?: string) => {
|
const handleUnauthorized = (msg?: string) => {
|
||||||
auth.logout();
|
auth.logout();
|
||||||
@@ -77,24 +74,20 @@ const request = (
|
|||||||
activeControllers.delete(controller);
|
activeControllers.delete(controller);
|
||||||
|
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
const errorData = await res.json().catch(() => ({}));
|
handleUnauthorized();
|
||||||
const errorMsg = getResponseErrorMsg(errorData, '登录状态已过期,请重新登录');
|
return Promise.reject(new Error('Unauthorized'));
|
||||||
handleUnauthorized(errorMsg);
|
|
||||||
return Promise.reject(new Error(errorMsg));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.status === 500) {
|
if (res.status === 500) {
|
||||||
|
message.error('服务器开小差了,请稍后再试');
|
||||||
const errorData = await res.json().catch(() => ({}));
|
const errorData = await res.json().catch(() => ({}));
|
||||||
const errorMsg = getResponseErrorMsg(errorData, '服务器开小差了,请稍后再试');
|
return Promise.reject(new Error(errorData.message || '服务器内部错误'));
|
||||||
message.error(errorMsg);
|
|
||||||
return Promise.reject(new Error(errorMsg));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errorData = await res.json().catch(() => ({}));
|
const errorData = await res.json().catch(() => ({}));
|
||||||
const errorMsg = getResponseErrorMsg(errorData);
|
message.error(errorData.message || '请求失败');
|
||||||
message.error(errorMsg);
|
return Promise.reject(new Error(errorData.message || '请求失败'));
|
||||||
return Promise.reject(new Error(errorMsg));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.responseType === 'blob') {
|
if (options.responseType === 'blob') {
|
||||||
@@ -104,15 +97,8 @@ const request = (
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.code == 401) {
|
if (data.code == 401) {
|
||||||
const errorMsg = getResponseErrorMsg(data, '登录状态已过期,请重新登录');
|
handleUnauthorized(data.msg);
|
||||||
handleUnauthorized(errorMsg);
|
return Promise.reject(new Error(data.msg || 'Unauthorized'));
|
||||||
return Promise.reject(new Error(errorMsg));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.code && data.code != '200') {
|
|
||||||
const errorMsg = getResponseErrorMsg(data);
|
|
||||||
message.error(errorMsg);
|
|
||||||
return Promise.reject(new Error(errorMsg));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
Reference in New Issue
Block a user