用户封禁、后台筛选排序

This commit is contained in:
Yeomai committed 2026-10-01 09:12:39 +08:00
1 parent 6b263af74f
commit c45025fc86
14 files changed
+293 -30

No files matched your search

@@ -3,7 +3,7 @@
import { useCallback, useEffect, useState } from 'react';
import { api } from '@/lib/api';
import { Button, Card, Empty, Input, Modal, Notice, Select } from '@/components/ui';
import { formatDate } from '@/lib/datetime';
import { formatDateTime } from '@/lib/datetime';
interface UserRow {
id: number;
@@ -44,6 +44,11 @@ export default function UsersPage() {
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [keyword, setKeyword] = useState('');
/** 筛选与排序:'-1' / '' 表示全部 */
const [statusFilter, setStatusFilter] = useState('-1');
const [entitlementFilter, setEntitlementFilter] = useState('');
const [sortKey, setSortKey] = useState('id');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const [error, setError] = useState('');
const [editing, setEditing] = useState<UserRow | null>(null);
const [form, setForm] = useState({ nickname: '', headicon_id: '0', status: '1' });
@@ -54,22 +59,36 @@ export default function UsersPage() {
const load = useCallback((nextPage: number, nextKeyword: string) => {
setError('');
api
.listUsers(nextPage, PAGE_SIZE, nextKeyword)
.listUsers({
page: nextPage,
pageSize: PAGE_SIZE,
keyword: nextKeyword,
status: statusFilter === '-1' ? undefined : Number(statusFilter),
entitlement: entitlementFilter === 'has' || entitlementFilter === 'none' ? entitlementFilter : undefined,
sort: sortKey,
order: sortOrder,
})
.then((res) => {
setItems(res.items as unknown as UserRow[]);
setTotal(res.total);
setPage(res.page);
})
.catch((err: { message: string }) => setError(err.message));
}, []);
}, [statusFilter, entitlementFilter, sortKey, sortOrder]);
// 筛选 / 排序一变,load 的引用就变,这里会带着当前关键词回到第 1 页重拉
useEffect(() => {
load(1, keyword);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [load]);
// 发放权益时用的课程下拉,只在首次进页面拉一次
useEffect(() => {
load(1, '');
api
.listResource('courses', { page: 1, pageSize: 100 })
.then((res) => setCourses(res.items as unknown as CourseRow[]))
.catch(() => setCourses([]));
}, [load]);
}, []);
const loadEntitlements = (userId: number) => {
api
@@ -141,8 +160,48 @@ export default function UsersPage() {
<Card
title={`共 ${total} 位用户`}
action={
<div className="flex gap-2">
<Input value={keyword} onChange={setKeyword} placeholder="昵称或 openid…" />
/* 筛选、排序、搜索排在一行;改任意一项都会从第 1 页重新拉 */
<div className="flex flex-nowrap items-center gap-2">
<div className="w-32">
<Select
value={statusFilter}
onChange={setStatusFilter}
options={[
{ value: -1, label: '全部状态' },
{ value: 1, label: '正常' },
{ value: 0, label: '封禁' },
]}
/>
</div>
<div className="w-36">
<Select
value={entitlementFilter}
onChange={setEntitlementFilter}
options={[
{ value: '', label: '全部用户' },
{ value: 'has', label: '有已购课程' },
{ value: 'none', label: '无已购课程' },
]}
/>
</div>
<div className="w-40">
<Select
value={sortKey}
onChange={setSortKey}
options={[
{ value: 'id', label: '按 ID' },
{ value: 'lastLogin', label: '按最后登录时间' },
{ value: 'registered', label: '按注册时间' },
{ value: 'entitlements', label: '按已购课程数' },
]}
/>
</div>
<Button variant="ghost" onClick={() => setSortOrder(sortOrder === 'desc' ? 'asc' : 'desc')}>
{sortOrder === 'desc' ? '降序 ↓' : '升序 ↑'}
</Button>
<div className="w-52">
<Input value={keyword} onChange={setKeyword} placeholder="昵称或 openid…" />
</div>
<Button variant="ghost" onClick={() => load(1, keyword)}>
搜索
</Button>
@@ -176,7 +235,7 @@ export default function UsersPage() {
{u.status === 1 ? <span className="text-green-600">正常</span> : <span className="text-red-600">封禁</span>}
</td>
<td className="px-2 py-2 text-gray-500">
{formatDate(u.last_login_at)}
{formatDateTime(u.last_login_at)}
</td>
<td className="px-2 py-2 text-right">
<Button variant="ghost" onClick={() => openEdit(u)}>