diff --git a/src/pages/events/list/model/useEventListModel.ts b/src/pages/events/list/model/useEventListModel.ts index e3fef00..1e0ceb6 100644 --- a/src/pages/events/list/model/useEventListModel.ts +++ b/src/pages/events/list/model/useEventListModel.ts @@ -4,6 +4,7 @@ import { useState, useDebounce } from '@/hooks'; import { useRequest } from '@/hooks/useRequest'; import { useEffect } from '@/hooks/useEffect'; import { usePagination } from '@/hooks/usePagination'; +import { hasValue, isEmptyValue, safeTransform } from '@/utils'; import type { TournamentAdminVO, EventListQueryParams, PageData, ApiResult } from './services'; import { getEventList, toggleEventOnline } from './services'; import { @@ -193,23 +194,3 @@ export function useEventListModel() { 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; - } -} diff --git a/src/pages/events/logs/index.tsx b/src/pages/events/logs/index.tsx index 6070221..92e9564 100644 --- a/src/pages/events/logs/index.tsx +++ b/src/pages/events/logs/index.tsx @@ -10,21 +10,14 @@ import { Tooltip, Pagination, } from 'ant-design-vue'; -import { - useLogModel, - ACTION_TYPE_OPTIONS, - ACTION_SOURCE_OPTIONS, - ACTION_SOURCE_MAP, -} from './model/useLogModel'; +import { useLogModel, ACTION_TYPE_OPTIONS, ACTION_SOURCE_OPTIONS } from './model/useLogModel'; +import { useLogColumns } from './model/useLogColumns'; import { useState, useContainerSize } from '@/hooks'; import pageStyles from '@/assets/styles/pageLayout.module.less'; import './index.module.less'; const { RangePicker } = DatePicker; -// ============================================================ -// LogContentCell: 操作内容单元格 -// ============================================================ const LogContentCell = defineComponent({ name: 'LogContentCell', 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 || '-'; - if (column.key === 'source') return {ACTION_SOURCE_MAP[text] || value}; - return {value}; -} - +/** + * 操作日志页面(纯视图层) + * + * 架构分层: + * - 数据层:services.ts(API 调用) + * - 逻辑层:useLogModel.ts(状态 + 业务逻辑 + 交互处理) + * - 视图层:useLogColumns.ts(表格列配置)+ 本组件(纯 UI 渲染) + */ export default defineComponent({ name: 'EventLogs', setup() { @@ -79,17 +71,24 @@ export default defineComponent({ filterForm, loading, dataSource, - columns, pagination, handleSearch, handleReset, handlePageChange, } = useLogModel(); + + const { columns } = useLogColumns(); const { containerRef, height } = useContainerSize({ headerOffset: 55 }); - onMounted(() => { - handleSearch(); - }); + /** 渲染 bodyCell */ + const renderBodyCell = ({ column, text }: { column: any; text: any }) => { + if (column.key === 'content') { + const raw = text || ''; + return raw ? : -; + } + // 其他列使用 columns 中的 customRender + return {text || '-'}; + }; return () => (
@@ -182,21 +181,15 @@ export default defineComponent({ pagination={false} > {{ - bodyCell: (args: any) => { - if (args.column.key === 'content') { - const raw = args.text || ''; - return raw ? : -; - } - return renderBodyCell(args); - }, + bodyCell: renderBodyCell, }}
`共 ${total} 条`} onChange={handlePageChange} diff --git a/src/pages/events/logs/model/config.ts b/src/pages/events/logs/model/config.ts new file mode 100644 index 0000000..a6bdaac --- /dev/null +++ b/src/pages/events/logs/model/config.ts @@ -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 = { + '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 = { + '1': 'PC', + '2': '小程序', +}; diff --git a/src/pages/events/logs/model/services.ts b/src/pages/events/logs/model/services.ts index b50ae01..05d0f95 100644 --- a/src/pages/events/logs/model/services.ts +++ b/src/pages/events/logs/model/services.ts @@ -20,7 +20,7 @@ export type { // ============================================================ // URL 常量 // ============================================================ -const operationLogs = '/tournament/operation/page'; +const operationLogs = '/admin/manager/operation/page'; // ============================================================ // API 函数 diff --git a/src/pages/events/logs/model/useLogColumns.ts b/src/pages/events/logs/model/useLogColumns.ts new file mode 100644 index 0000000..2c033e3 --- /dev/null +++ b/src/pages/events/logs/model/useLogColumns.ts @@ -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 }; +} diff --git a/src/pages/events/logs/model/useLogModel.ts b/src/pages/events/logs/model/useLogModel.ts index a6ce42d..8cc73a5 100644 --- a/src/pages/events/logs/model/useLogModel.ts +++ b/src/pages/events/logs/model/useLogModel.ts @@ -1,46 +1,44 @@ -import { computed, reactive, toRef, Ref } from 'vue'; -import { message } from 'ant-design-vue'; -import { useState, useDebounce, useThrottleFn } from '@/hooks'; +import { computed, reactive, toRef, Ref, type UnwrapRef } from 'vue'; +import { useDebounce } 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 { getOperationLogs, type TournamentAdminOperationPageVO, type OperationLogQueryParams, + type PageData, + type ApiResult, } 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 字段待定意,暂时留空) */ -export const ACTION_TYPE_OPTIONS = [{ value: '', label: '全部' }] as const; - -/** 操作来源选项(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 = { - '1': 'PC', - '2': '小程序', -}; - -// ============================================================ -// Model -// ============================================================ +// 提取 Pagination Ref 类型 +type PaginationRef = UnwrapRef; +type PageSizeRef = PaginationRef['pageSize']; +/** + * 操作日志页数据模型 + * 职责:状态管理 + 数据获取 + 业务逻辑 + 交互处理 + * + * 架构分层: + * - 数据层:services.ts(API 调用) + * - 逻辑层:本文件(状态 + 业务逻辑) + * - 视图层:useLogColumns.ts + index.tsx(纯 UI 渲染) + */ export function useLogModel() { - // ===== 筛选条件(key 名对齐 API 查询参数) ===== - const filterForm = reactive({ - dateRange: null as [string, string] | null, - type: '', - source: '', - sourceNickname: '', - sourcePhone: '', - tournamentName: '', - }); + const filterForm = reactive({ ...FILTER_DEFAULTS }); + const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 }); const { debouncedValue: debouncedNickname } = useDebounce( toRef(filterForm, 'sourceNickname') as Ref, @@ -55,42 +53,59 @@ export function useLogModel() { { delay: 300 }, ); - // ===== 表格状态 ===== - const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState([]); - 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 params: OperationLogQueryParams = { - page: String(pagination.value.current), - limit: String(pagination.value.pageSize), - }; - if (filterForm.dateRange) { - params.dateBegin = filterForm.dateRange[0]; - params.dateEnd = filterForm.dateRange[1]; + 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, + ); + + // 时间范围特殊处理 + 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; - 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; + + return { page: String(page), limit: String(pageSize), ...params } as OperationLogQueryParams; }; - // ===== 计算属性 ===== + const { + data, + loading, + run: fetchList, + } = useRequest>>( + () => getOperationLogs(buildQueryParams()), + { + refreshDeps: [], + formatResult: (res) => res, + }, + ); + + // 提取列表数据 + const listData = computed | 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(() => { return ( filterForm.dateRange !== null || @@ -102,49 +117,31 @@ export function useLogModel() { ); }); - // ===== 方法 ===== + const handleSearch = () => fetchList(); - const handleSearch = useThrottleFn(async () => { - 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(() => { + const handleReset = () => { filterForm.dateRange = null; filterForm.type = ''; filterForm.source = ''; filterForm.sourceNickname = ''; filterForm.sourcePhone = ''; filterForm.tournamentName = ''; - setPagination({ current: 1, pageSize: 10, total: 0 }); - setDataSource([]); - setTimeout(() => handleSearch(), 350); - }, 500); + pagination.reset(); + setTimeout(() => fetchList(), 0); + }; const handlePageChange = (page: number, pageSize: number) => { - setPagination({ ...pagination.value, current: page, pageSize }); - handleSearch(); + pagination.setCurrent(page); + if (pageSize !== (pagination.pageSize as PageSizeRef)) { + pagination.setPageSize(pageSize); + } + fetchList(); }; return { filterForm, loading, dataSource, - columns, pagination, hasFilter, handleSearch, diff --git a/src/pages/events/orders/model/useOrderModel.ts b/src/pages/events/orders/model/useOrderModel.ts index 6b14a91..99c3ecc 100644 --- a/src/pages/events/orders/model/useOrderModel.ts +++ b/src/pages/events/orders/model/useOrderModel.ts @@ -1,10 +1,11 @@ import { computed, reactive, toRef, Ref, h } from 'vue'; import Big from 'big.js'; import { StatusTag, type StatusTagTone } from '@/components'; -import { useState, useDebounce } from '@/hooks'; +import { useDebounce } from '@/hooks'; import { useRequest } from '@/hooks/useRequest'; import { useEffect } from '@/hooks/useEffect'; import { usePagination } from '@/hooks/usePagination'; +import { hasValue, isEmptyValue, safeTransform } from '@/utils'; import type { TournamentAdminOrderPageVO, OrderListQueryParams, @@ -239,23 +240,3 @@ export function useOrderModel() { 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; - } -} diff --git a/src/pages/events/users/index.tsx b/src/pages/events/users/index.tsx index f330196..1b20eb5 100644 --- a/src/pages/events/users/index.tsx +++ b/src/pages/events/users/index.tsx @@ -1,20 +1,18 @@ -import { defineComponent, onMounted } from 'vue'; -import { - Button, - Input, - 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 { defineComponent } from 'vue'; +import { Button, Input, Table, Form, Space, Select, Pagination } from 'ant-design-vue'; +import { useUserModel, USER_STATUS_OPTIONS } from './model/useUserModel'; +import { useUserColumns } from './model/useUserColumns'; import { useContainerSize } from '@/hooks'; import pageStyles from '@/assets/styles/pageLayout.module.less'; +/** + * 用户列表页面(纯视图层) + * + * 架构分层: + * - 数据层:services.ts(API 调用) + * - 逻辑层:useUserModel.ts(状态 + 业务逻辑 + 交互处理) + * - 视图层:useUserColumns.ts(表格列配置)+ 本组件(纯 UI 渲染) + */ export default defineComponent({ name: 'EventUsers', setup() { @@ -22,36 +20,33 @@ export default defineComponent({ filterForm, loading, dataSource, - columns, pagination, + togglingId, handleSearch, handleReset, handlePageChange, + handleToggleStatus, } = useUserModel(); + + const { columns } = useUserColumns(); const { containerRef, height } = useContainerSize(); - const handleToggleStatus = (record: any) => { + /** 渲染操作列 */ + const renderAction = (record: any) => { const isActive = record.status === '1'; - const actionText = isActive ? '停用' : '启用'; - Modal.confirm({ - title: `${actionText}确认`, - content: `确认${actionText}用户"${record.realName}"吗?`, - okText: '确认', - cancelText: '取消', - onOk: async () => { - try { - const res = await toggleUserActive({ userId: record.id, status: isActive ? '0' : '1' }); - if (res.code == 200) { - message.success(`${actionText}成���`); - handleSearch(); - } else message.error(res.msg || '操作失败'); - } catch (e: any) { - console.error(`用户${actionText}失败:`, e); - } - }, - }); + return ( + + ); }; + /** 完整表格列 */ const tableColumns = [ ...columns, { @@ -63,10 +58,6 @@ export default defineComponent({ }, ]; - onMounted(() => { - handleSearch(); - }); - return () => (
@@ -81,16 +72,6 @@ export default defineComponent({ onPressEnter={handleSearch} /> - -
+
{ if (args.column.key === 'action') { - const isActive = args.record.status === '1'; - return ( - - ); + return renderAction(args.record); } }, }} @@ -139,9 +112,9 @@ export default defineComponent({
`共 ${total} 条`} onChange={handlePageChange} diff --git a/src/pages/events/users/model/config.ts b/src/pages/events/users/model/config.ts new file mode 100644 index 0000000..421500d --- /dev/null +++ b/src/pages/events/users/model/config.ts @@ -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 = { + '1': { label: '正常', tone: 'success' }, + '0': { label: '停用', tone: 'danger' }, +}; diff --git a/src/pages/events/users/model/services.ts b/src/pages/events/users/model/services.ts index 91f90a4..bfd0d7b 100644 --- a/src/pages/events/users/model/services.ts +++ b/src/pages/events/users/model/services.ts @@ -30,11 +30,11 @@ export type { // ============================================================ // URL 常量 // ============================================================ -const userList = '/sys/user/page'; -const userSave = '/sys/user/save'; -const userUpdate = '/sys/user/update'; -const userActive = '/sys/user/active'; -const userUpdatePwd = '/sys/user/updatepwd'; +const userList = '/admin/manager/user/page'; +const userSave = '/admin/sys/user/save'; +const userUpdate = '/admin/sys/user/update'; +const userActive = '/admin/manager/user/active'; +const userUpdatePwd = '/admin/sys/user/updatepwd'; const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); diff --git a/src/pages/events/users/model/useUserColumns.ts b/src/pages/events/users/model/useUserColumns.ts new file mode 100644 index 0000000..e84635e --- /dev/null +++ b/src/pages/events/users/model/useUserColumns.ts @@ -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 = { + '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(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 }; +} diff --git a/src/pages/events/users/model/useUserModel.ts b/src/pages/events/users/model/useUserModel.ts index dbf1458..f14fd45 100644 --- a/src/pages/events/users/model/useUserModel.ts +++ b/src/pages/events/users/model/useUserModel.ts @@ -1,125 +1,168 @@ -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 { getUserList, type TournamentAdminUserVO, type UserListQueryParams } from './services'; +import { computed, reactive, toRef, Ref, type UnwrapRef } 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, 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 = [ - { 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; - -const STATUS_MAP: Record = { - '1': { label: '正常', tone: 'success' }, - '0': { label: '停用', tone: 'danger' }, -}; - -// ============================================================ -// Model -// ============================================================ +// 提取 Pagination Ref 类型 +type PaginationRef = UnwrapRef; +type PageSizeRef = PaginationRef['pageSize']; +/** + * 用户列表页数据模型 + * 职责:状态管理 + 数据获取 + 业务逻辑 + 交互处理 + * + * 架构分层: + * - 数据层:services.ts(API 调用) + * - 逻辑层:本文件(状态 + 业务逻辑) + * - 视图层:useUserColumns.ts + index.tsx(纯 UI 渲染) + */ export function useUserModel() { - const filterForm = reactive({ - text: '', - roleId: '', - status: '', - }); + // ===== 筛选表单状态 ===== + const filterForm = reactive({ ...FILTER_DEFAULTS }); + // ===== 弹窗状态 ===== + const [togglingId, setTogglingId] = useState(''); + + // ===== 分页管理 ===== + const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 }); + + // ===== 防抖处理 ===== const { debouncedValue: debouncedText } = useDebounce(toRef(filterForm, 'text') as Ref, { delay: 300, }); - const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState([]); - 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 params: UserListQueryParams = { - 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 { 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, + ); + + return { page: String(page), limit: String(pageSize), ...params } as UserListQueryParams; }; - const hasFilter = computed( - () => debouncedText.value.trim() !== '' || filterForm.roleId !== '' || filterForm.status !== '', + // ===== 数据请求 ===== + const { + data, + loading, + run: fetchList, + } = useRequest>>( + () => getUserList(buildQueryParams()), + { + refreshDeps: [], + formatResult: (res) => res, + }, ); - const handleSearch = useThrottleFn(async () => { - 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 listData = computed | undefined>(() => { + const res = data.value; + return res ? (res as any).data : undefined; + }); - const handleReset = useThrottleFn(() => { + 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.roleId = ''; filterForm.status = ''; - setPagination({ current: 1, pageSize: 10, total: 0 }); - setDataSource([]); - setTimeout(() => handleSearch(), 350); - }, 500); + pagination.reset(); + setTimeout(() => fetchList(), 0); + }; const handlePageChange = (page: number, pageSize: number) => { - setPagination({ ...pagination.value, current: page, pageSize }); - handleSearch(); + pagination.setCurrent(page); + 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 { filterForm, loading, dataSource, - columns, pagination, + togglingId, hasFilter, handleSearch, handleReset, handlePageChange, + handleToggleStatus, }; } diff --git a/src/utils/index.ts b/src/utils/index.ts index c97158a..4773303 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -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(value: T, transform?: (v: T) => unknown): T | unknown { + if (!transform) return value; + try { + return transform(value) ?? value; + } catch { + return value; + } +} + /** * 生成唯一 ID */