Feature/0723/zr #9
@@ -12,7 +12,7 @@ import pageStyles from '@/assets/styles/pageLayout.module.less';
|
||||
* 架构分层:
|
||||
* - 数据层:services.ts(API 调用)
|
||||
* - 逻辑层:useUserModel.ts(状态 + 业务逻辑 + 交互处理)
|
||||
* - 视图层:useUserColumns.ts(表格列配置)+ 本组件(纯 UI 渲染)
|
||||
* - 视图层:useUserColumns.tsx(表格列配置)+ 本组件(纯 UI 渲染)
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'EventUsers',
|
||||
@@ -46,7 +46,7 @@ export default defineComponent({
|
||||
loading={togglingId.value === record.id}
|
||||
onClick={() => handleToggleStatus(record)}
|
||||
>
|
||||
{isActive ? '停用' : '启用'}
|
||||
{isActive ? '禁用' : '启用'}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -79,6 +79,16 @@ export default defineComponent({
|
||||
onPressEnter={handleSearch}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="手机号" name="phone">
|
||||
<Input
|
||||
value={filterForm.phone}
|
||||
placeholder="请输入手机号"
|
||||
style={{ width: '180px' }}
|
||||
allowClear
|
||||
onUpdate:value={(val: any) => (filterForm.phone = val.target?.value ?? val ?? '')}
|
||||
onPressEnter={handleSearch}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="状态" name="status">
|
||||
<Select
|
||||
value={filterForm.status}
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
* 集中管理筛选默认值、字段映射、选项配置
|
||||
*/
|
||||
|
||||
import type { StatusTagTone } from '@/components';
|
||||
|
||||
// ============================================================
|
||||
// 筛选默认值
|
||||
// ============================================================
|
||||
export const FILTER_DEFAULTS = {
|
||||
text: '',
|
||||
roleId: '',
|
||||
nickname: '',
|
||||
phone: '',
|
||||
status: '',
|
||||
} as const;
|
||||
|
||||
@@ -19,8 +21,8 @@ export const FILTER_DEFAULTS = {
|
||||
type FieldMapping = [keyof typeof FILTER_DEFAULTS, string, ((v: any) => any)?];
|
||||
|
||||
export const FIELD_MAPPINGS: FieldMapping[] = [
|
||||
['text', 'text', (v) => v?.trim?.() ?? v],
|
||||
['roleId', 'roleId'],
|
||||
['nickname', 'nickname', (v) => v?.trim?.() ?? v],
|
||||
['phone', 'phone'],
|
||||
['status', 'status'],
|
||||
];
|
||||
|
||||
@@ -29,25 +31,15 @@ export const FIELD_MAPPINGS: FieldMapping[] = [
|
||||
// ============================================================
|
||||
export const USER_STATUS_OPTIONS = [
|
||||
{ value: '', label: '全部' },
|
||||
{ value: '0', 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' },
|
||||
'0': { label: '禁用', tone: 'danger' },
|
||||
};
|
||||
|
||||
@@ -18,14 +18,15 @@ import type {
|
||||
|
||||
/** 赛事用户列表项 */
|
||||
export interface EventUserVO {
|
||||
id: number;
|
||||
id: string;
|
||||
nickname: string;
|
||||
phone: string;
|
||||
realName: string;
|
||||
idCard: string;
|
||||
idCardImg: string;
|
||||
idCardImgList: string[];
|
||||
status: number;
|
||||
gender: string;
|
||||
status: string;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
|
||||
+25
-24
@@ -1,6 +1,4 @@
|
||||
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';
|
||||
|
||||
@@ -10,6 +8,9 @@ const GENDER_MAP: Record<string, string> = {
|
||||
'2': '女',
|
||||
};
|
||||
|
||||
/** 空值占位 */
|
||||
const E = (v: string | undefined | null) => v || '--';
|
||||
|
||||
/**
|
||||
* 用户列表页 - 表格列配置(视图层)
|
||||
*
|
||||
@@ -24,6 +25,7 @@ export function useUserColumns() {
|
||||
key: 'nickname',
|
||||
width: 140,
|
||||
align: 'center' as const,
|
||||
customRender: ({ text }: { text: string }) => E(text),
|
||||
},
|
||||
{
|
||||
title: '用户姓名',
|
||||
@@ -31,6 +33,7 @@ export function useUserColumns() {
|
||||
key: 'realName',
|
||||
width: 110,
|
||||
align: 'center' as const,
|
||||
customRender: ({ text }: { text: string }) => E(text),
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
@@ -38,6 +41,7 @@ export function useUserColumns() {
|
||||
key: 'phone',
|
||||
width: 140,
|
||||
align: 'center' as const,
|
||||
customRender: ({ text }: { text: string }) => E(text),
|
||||
},
|
||||
{
|
||||
title: '性别',
|
||||
@@ -45,7 +49,7 @@ export function useUserColumns() {
|
||||
key: 'gender',
|
||||
width: 80,
|
||||
align: 'center' as const,
|
||||
customRender: ({ text }: { text: string }) => GENDER_MAP[text] || '',
|
||||
customRender: ({ text }: { text: string }) => GENDER_MAP[text] || '--',
|
||||
},
|
||||
{
|
||||
title: '证件号',
|
||||
@@ -53,6 +57,7 @@ export function useUserColumns() {
|
||||
key: 'idCard',
|
||||
width: 120,
|
||||
align: 'center' as const,
|
||||
customRender: ({ text }: { text: string }) => E(text),
|
||||
},
|
||||
{
|
||||
title: '证件图片',
|
||||
@@ -60,26 +65,21 @@ export function useUserColumns() {
|
||||
key: 'idCardImgList',
|
||||
width: 120,
|
||||
align: 'center' as const,
|
||||
customRender: ({ text }: { text: string[] }): VNodeChild => {
|
||||
customRender: ({ text }: { text: string[] }) => {
|
||||
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,
|
||||
}),
|
||||
),
|
||||
},
|
||||
if (imgList.length === 0) return <span>--</span>;
|
||||
return (
|
||||
<Space size={4}>
|
||||
{imgList.map((url, index) => (
|
||||
<Image
|
||||
key={index}
|
||||
src={url}
|
||||
width={40}
|
||||
height={40}
|
||||
style={{ objectFit: 'cover', borderRadius: '4px', cursor: 'pointer' }}
|
||||
/>
|
||||
))}
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -89,6 +89,7 @@ export function useUserColumns() {
|
||||
key: 'createTime',
|
||||
width: 170,
|
||||
align: 'center' as const,
|
||||
customRender: ({ text }: { text: string }) => E(text),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
@@ -97,8 +98,8 @@ export function useUserColumns() {
|
||||
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 info = STATUS_MAP[text] || { label: text || '--', tone: 'default' as const };
|
||||
return <StatusTag label={info.label} tone={info.tone} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,13 +1,9 @@
|
||||
import { computed, reactive, toRef, Ref, type UnwrapRef } from 'vue';
|
||||
import { computed, reactive } from 'vue';
|
||||
import { Modal, message } from 'ant-design-vue';
|
||||
import { useState, useDebounce } from '@/hooks';
|
||||
import { useState } from '@/hooks';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
import { useEffect } from '@/hooks/useEffect';
|
||||
import {
|
||||
usePagination,
|
||||
syncPaginationTotal,
|
||||
type UsePaginationReturn,
|
||||
} from '@/hooks/usePagination';
|
||||
import { usePagination, syncPaginationTotal } from '@/hooks/usePagination';
|
||||
import { hasValue, isEmptyValue, safeTransform } from '@/utils';
|
||||
import {
|
||||
getUserList,
|
||||
@@ -17,19 +13,9 @@ import {
|
||||
type PageData,
|
||||
type ApiResult,
|
||||
} from './services';
|
||||
import {
|
||||
FILTER_DEFAULTS,
|
||||
FIELD_MAPPINGS,
|
||||
USER_STATUS_OPTIONS,
|
||||
ROLE_OPTIONS,
|
||||
STATUS_MAP,
|
||||
} from './config';
|
||||
import { FILTER_DEFAULTS, FIELD_MAPPINGS, USER_STATUS_OPTIONS, STATUS_MAP } from './config';
|
||||
|
||||
export { USER_STATUS_OPTIONS, ROLE_OPTIONS, STATUS_MAP };
|
||||
|
||||
// 提取 Pagination Ref 类型
|
||||
type PaginationRef = UnwrapRef<UsePaginationReturn>;
|
||||
type PageSizeRef = PaginationRef['pageSize'];
|
||||
export { USER_STATUS_OPTIONS, STATUS_MAP };
|
||||
|
||||
/**
|
||||
* 用户列表页数据模型
|
||||
@@ -38,7 +24,7 @@ type PageSizeRef = PaginationRef['pageSize'];
|
||||
* 架构分层:
|
||||
* - 数据层:services.ts(API 调用)
|
||||
* - 逻辑层:本文件(状态 + 业务逻辑)
|
||||
* - 视图层:useUserColumns.ts + index.tsx(纯 UI 渲染)
|
||||
* - 视图层:useUserColumns.tsx + index.tsx(纯 UI 渲染)
|
||||
*/
|
||||
export function useUserModel() {
|
||||
// ===== 筛选表单状态 =====
|
||||
@@ -50,11 +36,6 @@ export function useUserModel() {
|
||||
// ===== 分页管理 =====
|
||||
const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
|
||||
|
||||
// ===== 防抖处理 =====
|
||||
const { debouncedValue: debouncedText } = useDebounce(toRef(filterForm, 'text') as Ref<string>, {
|
||||
delay: 300,
|
||||
});
|
||||
|
||||
// ===== 构建查询参数 =====
|
||||
const buildQueryParams = (): UserListQueryParams => {
|
||||
const { page, pageSize } = pagination.params.value;
|
||||
@@ -104,15 +85,15 @@ export function useUserModel() {
|
||||
// ===== 计算属性 =====
|
||||
const hasFilter = computed(() => {
|
||||
const f = filterForm;
|
||||
return !!debouncedText.value?.trim() || !!f.roleId || !!f.status;
|
||||
return !!f.nickname?.trim() || !!f.phone?.trim() || !!f.status;
|
||||
});
|
||||
|
||||
// ===== 事件处理 =====
|
||||
const handleSearch = () => fetchList();
|
||||
|
||||
const handleReset = async () => {
|
||||
filterForm.text = '';
|
||||
filterForm.roleId = '';
|
||||
filterForm.nickname = '';
|
||||
filterForm.phone = '';
|
||||
filterForm.status = '';
|
||||
pagination.reset();
|
||||
await fetchList();
|
||||
@@ -120,16 +101,16 @@ export function useUserModel() {
|
||||
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
pagination.setCurrent(page);
|
||||
if (pageSize !== (pagination.pageSize as PageSizeRef)) {
|
||||
if (pageSize !== pagination.pageSize) {
|
||||
pagination.setPageSize(pageSize);
|
||||
}
|
||||
fetchList();
|
||||
};
|
||||
|
||||
// ===== 启用/停用用户 =====
|
||||
// ===== 启用/禁用用户 =====
|
||||
const handleToggleStatus = async (record: TournamentAdminUserVO) => {
|
||||
const isActive = record.status === '1';
|
||||
const actionText = isActive ? '停用' : '启用';
|
||||
const actionText = isActive ? '禁用' : '启用';
|
||||
|
||||
Modal.confirm({
|
||||
title: `${actionText}确认`,
|
||||
|
||||
Reference in New Issue
Block a user