feat: 对接用户列表字段数据

This commit is contained in:
ZhuRui
2026-07-30 11:16:40 +08:00
parent fee0af174e
commit 14f6af35d8
5 changed files with 399 additions and 85 deletions
+51 -17
View File
@@ -1,4 +1,4 @@
import { defineComponent } from 'vue';
import { defineComponent, onMounted } from 'vue';
import {
Button,
Input,
@@ -9,8 +9,11 @@ import {
Image,
Pagination,
Modal,
message,
} from 'ant-design-vue';
import { EyeOutlined } from '@ant-design/icons-vue';
import { useUserModel, USER_STATUS_OPTIONS } from './model/useUserModel';
import { toggleUserActive } from '@/api/users';
import { useContainerSize } from '@/hooks';
import pageStyles from '@/assets/styles/pageLayout.module.less';
@@ -28,23 +31,31 @@ function renderBodyCell({
record: any;
onToggleStatus: (record: any) => void;
}) {
// 证件图片:有则显示,无则不显示
if (column.dataIndex === 'idImg') {
const images = record.idImg || [];
if (images.length === 0) {
return '-';
}
// 证件图片:优先 idCardImgList,回退 idCardImg
if (column.dataIndex === 'idCardImg') {
const urls = record.idCardImgList?.length
? record.idCardImgList
: record.idCardImg
? [record.idCardImg]
: [];
if (urls.length === 0) return <span>-</span>;
return (
<Space>
{images.map((src: string) => (
<Image key={src} width={48} height={32} src={src} />
{urls.map((src: string) => (
<Image
key={src}
width={48}
height={32}
src={src}
v-slots={{ previewMask: () => <EyeOutlined /> }}
/>
))}
</Space>
);
}
if (column.key === 'action') {
const isActive = record.status === 'active';
const isActive = record.status === '1';
return (
<Button type="link" size="small" onClick={() => onToggleStatus(record)}>
{isActive ? '禁用' : '启用'}
@@ -75,16 +86,29 @@ export default defineComponent({
const { containerRef, height } = useContainerSize();
const handleToggleStatus = (record: any) => {
const isActive = record.status === 'active';
const isActive = record.status === '1';
const actionText = isActive ? '禁用' : '启用';
const targetStatus = isActive ? '0' : '1';
Modal.confirm({
title: `${actionText}确认`,
content: `确认${actionText}用户"${record.nickName}"吗?`,
content: `确认${actionText}用户"${record.nickname}"吗?`,
okText: '确认',
cancelText: '取消',
onOk: () => {
console.log(`${actionText}用户:`, record.userId);
// TODO: 替换为真实 API 调用
onOk: async () => {
try {
const res = await toggleUserActive({
userId: record.id,
status: targetStatus,
});
if (res.code === 200) {
message.success(`${actionText}成功`);
handleSearch();
} else {
message.error(res.msg || '操作失败');
}
} catch (e: any) {
console.error(`用户${actionText}失败:`, e);
}
},
});
};
@@ -101,24 +125,34 @@ export default defineComponent({
},
];
onMounted(() => {
handleSearch();
});
return () => (
<div class={pageStyles.containerMain}>
{/* ===== 筛选区 ===== */}
<div class={pageStyles.filter}>
<Form style={{ rowGap: '10px' }} layout="inline" model={filterForm}>
<Form.Item label="用户昵称" name="searchUserName">
<Form.Item label="用户昵称" name="nickname">
<Input
value={filterForm.nickname}
placeholder="请输入用户昵称"
style={{ width: '200px' }}
allowClear
onUpdate:value={(val: any) =>
(filterForm.nickname = val.target?.value ?? val ?? '')
}
onPressEnter={handleSearch}
/>
</Form.Item>
<Form.Item label="手机号" name="searchPhone">
<Form.Item label="手机号" name="phone">
<Input
value={filterForm.phone}
placeholder="请输入手机号"
style={{ width: '200px' }}
allowClear
onUpdate:value={(val: any) => (filterForm.phone = val.target?.value ?? val ?? '')}
onPressEnter={handleSearch}
/>
</Form.Item>
+59 -68
View File
@@ -1,56 +1,26 @@
import { computed, reactive, toRef, Ref, h } from 'vue';
import { message } from 'ant-design-vue';
import dayjs from 'dayjs';
import { StatusTag, type StatusTagTone } from '@/components';
import { useState, useDebounce, useThrottleFn } from '@/hooks';
import { getUserList, type TournamentAdminUserPageVO, type UserListQueryParams } from '@/api/users';
// ============================================================
// 常量
// ============================================================
/** 用户状态选项 */
/** 用户状态选项value 对应 API status: 不传=全部、0=禁用、1=正常) */
export const USER_STATUS_OPTIONS = [
{ value: '', label: '全部' },
{ value: 'active', label: '正常' },
{ value: 'disabled', label: '禁用' },
{ value: '1', label: '正常' },
{ value: '0', label: '禁用' },
] as const;
/** 用户状态 StatusTag 映射 */
const USER_STATUS_MAP: Record<string, { label: string; tone: StatusTagTone }> = {
active: { label: '正常', tone: 'success' },
disabled: { label: '禁用', tone: 'danger' },
'1': { label: '正常', tone: 'success' },
'0': { label: '禁用', tone: 'danger' },
};
// ============================================================
// 假数据
// ============================================================
const MOCK_DATA = Array.from({ length: 12 }, (_, i) => ({
key: `${i + 1}`,
userId: `U202607${String(i + 1).padStart(5, '0')}`,
nickName: ['羽毛球小将', '马拉松达人', '健身爱好者', '游泳健将'][i % 4],
phone: ['13800138001', '13900139002', '13700137003', '13600136004'][i % 4],
realName: ['张三', '李四', '王五', '赵六'][i % 4],
gender: i % 2 === 0 ? '男' : '女',
idNo: '320101199001011234',
// 证图片来源:奇数索引有,偶数无
idImg:
i % 2 === 1
? ['https://picsum.photos/seed/idcard-a/120/80', 'https://picsum.photos/seed/idcard-b/120/80']
: [],
status: (i % 4 === 3 ? 'disabled' : 'active') as string,
loginTime: `2026-07-${String((i % 20) + 1).padStart(2, '0')} ${String((i * 3 + 6) % 24).padStart(2, '0')}:${String((i * 17 + 11) % 60).padStart(2, '0')}:${String((i * 13 + 7) % 60).padStart(2, '0')}`,
}));
/**
* 临时排序:对假数据按登录时间倒序排列。
* 【注意】实际项目中排序由后端接口负责,对接后端后请删除此函数及相关调用。
*/
const sortByTimeDesc = (list: any[], field: string) =>
[...list].sort((a, b) => dayjs(b[field]).valueOf() - dayjs(a[field]).valueOf());
const SORTED_MOCK_DATA = sortByTimeDesc(MOCK_DATA, 'loginTime');
// ============================================================
// Model
// ============================================================
@@ -59,43 +29,42 @@ const SORTED_MOCK_DATA = sortByTimeDesc(MOCK_DATA, 'loginTime');
* 用户列表页数据模型
*/
export function useUserModel() {
// ===== 筛选条件 =====
// ===== 筛选条件key 名对齐 API 查询参数) =====
const filterForm = reactive({
searchUserName: '',
searchPhone: '',
nickname: '',
phone: '',
status: '',
});
// 字段防抖 300ms
const { debouncedValue: debouncedUserName } = useDebounce(
toRef(filterForm, 'searchUserName') as Ref<string>,
// 可搜索字段防抖
const { debouncedValue: debouncedNickname } = useDebounce(
toRef(filterForm, 'nickname') as Ref<string>,
{ delay: 300 },
);
const { debouncedValue: debouncedPhone } = useDebounce(
toRef(filterForm, 'searchPhone') as Ref<string>,
toRef(filterForm, 'phone') as Ref<string>,
{ delay: 300 },
);
// ===== 表格状态 =====
const [loading, setLoading] = useState<boolean>(false);
const [dataSource, setDataSource] = useState<any[]>(SORTED_MOCK_DATA);
const [dataSource, setDataSource] = useState<TournamentAdminUserPageVO[]>([]);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: SORTED_MOCK_DATA.length,
total: 0,
});
// ===== 表格列配置 =====
// ===== 表格列配置dataIndex 对齐 TournamentAdminUserPageVO =====
const columns = [
{ title: '用户昵称', dataIndex: 'nickName', key: 'nickName', width: 120 },
{ title: '昵称', dataIndex: 'nickname', key: 'nickname', width: 120 },
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 140 },
{ title: '真实姓名', dataIndex: 'realName', key: 'realName', width: 100 },
{ title: '性别', dataIndex: 'gender', key: 'gender', width: 80 },
{ title: '证件号', dataIndex: 'idNo', key: 'idNo', width: 180 },
{ title: '证件号', dataIndex: 'idCard', key: 'idCard', width: 180 },
{
title: '证件图片',
dataIndex: 'idImg',
key: 'idImg',
dataIndex: 'idCardImg',
key: 'idCardImg',
width: 140,
},
{
@@ -109,13 +78,33 @@ export function useUserModel() {
return h(StatusTag, { label: info.label, tone: info.tone });
},
},
{ title: '登录时间', dataIndex: 'loginTime', key: 'loginTime', width: 170 },
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 170 },
];
// ===== 构建 API 查询参数 =====
const buildQueryParams = (): UserListQueryParams => {
const params: UserListQueryParams = {
page: String(pagination.value.current),
limit: String(pagination.value.pageSize),
};
if (filterForm.nickname.trim()) {
params.nickname = filterForm.nickname.trim();
}
if (filterForm.phone.trim()) {
params.phone = filterForm.phone.trim();
}
if (filterForm.status) {
params.status = filterForm.status;
}
return params;
};
// ===== 计算属性 =====
const hasFilter = computed(() => {
return (
debouncedUserName.value.trim() !== '' ||
debouncedNickname.value.trim() !== '' ||
debouncedPhone.value.trim() !== '' ||
filterForm.status !== ''
);
@@ -123,19 +112,20 @@ export function useUserModel() {
// ===== 方法 =====
/** 查询(节流 500ms */
/** 查询 */
const handleSearch = useThrottleFn(async () => {
setLoading(true);
try {
console.log('搜索条件:', {
userName: debouncedUserName.value,
phone: debouncedPhone.value,
status: filterForm.status,
});
// TODO: 替换为真实 API 调用
setDataSource(SORTED_MOCK_DATA);
setPagination({ ...pagination.value, total: SORTED_MOCK_DATA.length });
message.success('查询成功');
const queryParams = buildQueryParams();
console.log('用户列表查询参数:', queryParams);
const res = await getUserList(queryParams);
if (res.code === 200) {
setDataSource(res.data.list);
setPagination({ ...pagination.value, total: res.data.total });
} else {
message.error(res.msg || '查询失败');
}
} catch (error: any) {
console.error('用户列表查询失败:', error);
} finally {
@@ -143,13 +133,14 @@ export function useUserModel() {
}
}, 500);
/** 重置(节流 500ms */
/** 重置 */
const handleReset = useThrottleFn(() => {
filterForm.searchUserName = '';
filterForm.searchPhone = '';
filterForm.nickname = '';
filterForm.phone = '';
filterForm.status = '';
setPagination({ current: 1, pageSize: 10, total: SORTED_MOCK_DATA.length });
setDataSource(SORTED_MOCK_DATA);
setPagination({ current: 1, pageSize: 10, total: 0 });
setDataSource([]);
setTimeout(() => handleSearch(), 350);
}, 500);
const handlePageChange = (page: number, pageSize: number) => {