用户管理增加用户名搜索
This commit is contained in:
+132
-36
@@ -41,12 +41,14 @@ import {
|
||||
Mail,
|
||||
Palette,
|
||||
Plus,
|
||||
Search,
|
||||
Settings,
|
||||
Trash2,
|
||||
Tv,
|
||||
UserPlus,
|
||||
Users,
|
||||
Video,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { GripVertical } from 'lucide-react';
|
||||
import {
|
||||
@@ -512,8 +514,10 @@ interface UserConfigProps {
|
||||
userPage: number;
|
||||
userTotalPages: number;
|
||||
userTotal: number;
|
||||
fetchUsersV2: (page: number) => Promise<void>;
|
||||
fetchUsersV2: (page: number, search?: string) => Promise<void>;
|
||||
userListLoading: boolean;
|
||||
userSearch: string;
|
||||
setUserSearch: (value: string) => void;
|
||||
}
|
||||
|
||||
const UserConfig = ({
|
||||
@@ -526,6 +530,8 @@ const UserConfig = ({
|
||||
userTotal,
|
||||
fetchUsersV2,
|
||||
userListLoading,
|
||||
userSearch,
|
||||
setUserSearch,
|
||||
}: UserConfigProps) => {
|
||||
const { alertModal, showAlert, hideAlert } = useAlertModal();
|
||||
const { isLoading, withLoading } = useLoadingState();
|
||||
@@ -582,6 +588,7 @@ const UserConfig = ({
|
||||
} | null>(null);
|
||||
const [showDeleteUserModal, setShowDeleteUserModal] = useState(false);
|
||||
const [deletingUser, setDeletingUser] = useState<string | null>(null);
|
||||
const trimmedUserSearch = userSearch.trim();
|
||||
|
||||
// 当前登录用户名
|
||||
const currentUsername = getAuthInfoFromBrowserCookie()?.username || null;
|
||||
@@ -887,7 +894,7 @@ const UserConfig = ({
|
||||
if (checked) {
|
||||
// 只选择自己有权限操作的用户
|
||||
const selectableUsernames =
|
||||
config?.UserConfig?.Users?.filter(
|
||||
displayUsers?.filter(
|
||||
(user) =>
|
||||
role === 'owner' ||
|
||||
(role === 'admin' &&
|
||||
@@ -898,7 +905,7 @@ const UserConfig = ({
|
||||
setSelectedUsers(new Set());
|
||||
}
|
||||
},
|
||||
[config?.UserConfig?.Users, role, currentUsername]
|
||||
[displayUsers, role, currentUsername]
|
||||
);
|
||||
|
||||
// 批量设置用户组
|
||||
@@ -1240,15 +1247,87 @@ const UserConfig = ({
|
||||
|
||||
{/* 用户列表 */}
|
||||
<div>
|
||||
<div className='flex items-center justify-between mb-3'>
|
||||
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
|
||||
用户列表
|
||||
</h4>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<div className='mb-3 space-y-3'>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
|
||||
用户列表
|
||||
</h4>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowAddUserForm(!showAddUserForm);
|
||||
if (showChangePasswordForm) {
|
||||
setShowChangePasswordForm(false);
|
||||
setChangePasswordUser({ username: '', password: '' });
|
||||
}
|
||||
}}
|
||||
className={
|
||||
showAddUserForm
|
||||
? buttonStyles.secondary
|
||||
: buttonStyles.success
|
||||
}
|
||||
>
|
||||
{showAddUserForm ? '取消' : '添加用户'}
|
||||
</button>
|
||||
</div>
|
||||
<div className='flex w-full flex-wrap items-center justify-end gap-2'>
|
||||
{!hasOldUserData && usersV2 && (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
setSelectedUsers(new Set());
|
||||
fetchUsersV2(1, trimmedUserSearch);
|
||||
}}
|
||||
className='ml-auto flex min-w-0 items-center gap-2'
|
||||
>
|
||||
<div className='relative w-44 sm:w-56'>
|
||||
<Search className='absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400' />
|
||||
<input
|
||||
type='text'
|
||||
value={userSearch}
|
||||
onChange={(e) => setUserSearch(e.target.value)}
|
||||
placeholder='按用户名搜索'
|
||||
className='w-full pl-9 pr-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent'
|
||||
/>
|
||||
</div>
|
||||
{trimmedUserSearch && (
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => {
|
||||
setUserSearch('');
|
||||
setSelectedUsers(new Set());
|
||||
fetchUsersV2(1, '');
|
||||
}}
|
||||
disabled={userListLoading}
|
||||
aria-label='清空搜索'
|
||||
title='清空'
|
||||
className={`inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg transition-colors ${
|
||||
userListLoading
|
||||
? 'bg-gray-400 dark:bg-gray-600 cursor-not-allowed text-white'
|
||||
: 'bg-gray-600 hover:bg-gray-700 dark:bg-gray-600 dark:hover:bg-gray-700 text-white'
|
||||
}`}
|
||||
>
|
||||
<X className='h-4 w-4' />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type='submit'
|
||||
disabled={userListLoading}
|
||||
aria-label='搜索用户'
|
||||
title='搜索'
|
||||
className={`inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg transition-colors ${
|
||||
userListLoading
|
||||
? 'bg-gray-400 dark:bg-gray-600 cursor-not-allowed text-white'
|
||||
: 'bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white'
|
||||
}`}
|
||||
>
|
||||
<Search className='h-4 w-4' />
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
{/* 批量操作按钮 */}
|
||||
{selectedUsers.size > 0 && (
|
||||
<>
|
||||
<div className='flex items-center space-x-3'>
|
||||
<div className='flex flex-col gap-2 sm:flex-row sm:items-center sm:space-x-3'>
|
||||
<span className='text-sm text-gray-600 dark:text-gray-400'>
|
||||
已选择 {selectedUsers.size} 个用户
|
||||
</span>
|
||||
@@ -1259,23 +1338,9 @@ const UserConfig = ({
|
||||
批量设置用户组
|
||||
</button>
|
||||
</div>
|
||||
<div className='w-px h-6 bg-gray-300 dark:bg-gray-600'></div>
|
||||
<div className='hidden sm:block w-px h-6 bg-gray-300 dark:bg-gray-600'></div>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowAddUserForm(!showAddUserForm);
|
||||
if (showChangePasswordForm) {
|
||||
setShowChangePasswordForm(false);
|
||||
setChangePasswordUser({ username: '', password: '' });
|
||||
}
|
||||
}}
|
||||
className={
|
||||
showAddUserForm ? buttonStyles.secondary : buttonStyles.success
|
||||
}
|
||||
>
|
||||
{showAddUserForm ? '取消' : '添加用户'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1448,7 +1513,7 @@ const UserConfig = ({
|
||||
<th className='w-10 px-1 py-3 text-center'>
|
||||
{(() => {
|
||||
// 检查是否有权限操作任何用户
|
||||
const hasAnyPermission = config?.UserConfig?.Users?.some(
|
||||
const hasAnyPermission = displayUsers?.some(
|
||||
(user) =>
|
||||
role === 'owner' ||
|
||||
(role === 'admin' &&
|
||||
@@ -1516,7 +1581,7 @@ const UserConfig = ({
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
colSpan={7}
|
||||
colSpan={8}
|
||||
className='px-6 py-8 text-center text-gray-500 dark:text-gray-400'
|
||||
>
|
||||
加载中...
|
||||
@@ -1536,6 +1601,23 @@ const UserConfig = ({
|
||||
};
|
||||
return priority(a) - priority(b);
|
||||
});
|
||||
if (sortedUsers.length === 0) {
|
||||
return (
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
colSpan={8}
|
||||
className='px-6 py-8 text-center text-gray-500 dark:text-gray-400'
|
||||
>
|
||||
{trimmedUserSearch
|
||||
? `未找到用户名包含“${trimmedUserSearch}”的用户`
|
||||
: '暂无用户'}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<tbody className='divide-y divide-gray-200 dark:divide-gray-700'>
|
||||
{sortedUsers.map((user) => {
|
||||
@@ -1778,11 +1860,14 @@ const UserConfig = ({
|
||||
{!hasOldUserData && usersV2 && userTotalPages > 1 && (
|
||||
<div className='mt-4 flex items-center justify-between px-4'>
|
||||
<div className='text-sm text-gray-600 dark:text-gray-400'>
|
||||
共 {userTotal} 个用户,第 {userPage} / {userTotalPages} 页
|
||||
{trimmedUserSearch
|
||||
? `搜索结果 ${userTotal} 个用户`
|
||||
: `共 ${userTotal} 个用户`}
|
||||
,第 {userPage} / {userTotalPages} 页
|
||||
</div>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<button
|
||||
onClick={() => fetchUsersV2(1)}
|
||||
onClick={() => fetchUsersV2(1, trimmedUserSearch)}
|
||||
disabled={userPage === 1}
|
||||
className={`px-3 py-1 text-sm rounded ${
|
||||
userPage === 1
|
||||
@@ -1793,7 +1878,7 @@ const UserConfig = ({
|
||||
首页
|
||||
</button>
|
||||
<button
|
||||
onClick={() => fetchUsersV2(userPage - 1)}
|
||||
onClick={() => fetchUsersV2(userPage - 1, trimmedUserSearch)}
|
||||
disabled={userPage === 1}
|
||||
className={`px-3 py-1 text-sm rounded ${
|
||||
userPage === 1
|
||||
@@ -1804,7 +1889,7 @@ const UserConfig = ({
|
||||
上一页
|
||||
</button>
|
||||
<button
|
||||
onClick={() => fetchUsersV2(userPage + 1)}
|
||||
onClick={() => fetchUsersV2(userPage + 1, trimmedUserSearch)}
|
||||
disabled={userPage === userTotalPages}
|
||||
className={`px-3 py-1 text-sm rounded ${
|
||||
userPage === userTotalPages
|
||||
@@ -1815,7 +1900,7 @@ const UserConfig = ({
|
||||
下一页
|
||||
</button>
|
||||
<button
|
||||
onClick={() => fetchUsersV2(userTotalPages)}
|
||||
onClick={() => fetchUsersV2(userTotalPages, trimmedUserSearch)}
|
||||
disabled={userPage === userTotalPages}
|
||||
className={`px-3 py-1 text-sm rounded ${
|
||||
userPage === userTotalPages
|
||||
@@ -16125,14 +16210,23 @@ function AdminPageClient() {
|
||||
const [userTotalPages, setUserTotalPages] = useState(1);
|
||||
const [userTotal, setUserTotal] = useState(0);
|
||||
const [userListLoading, setUserListLoading] = useState(false);
|
||||
const [userSearch, setUserSearch] = useState('');
|
||||
const userLimit = 10;
|
||||
|
||||
// 获取新版本用户列表
|
||||
const fetchUsersV2 = useCallback(async (page = 1) => {
|
||||
const fetchUsersV2 = useCallback(async (page = 1, search = userSearch) => {
|
||||
try {
|
||||
setUserListLoading(true);
|
||||
const params = new URLSearchParams({
|
||||
page: String(page),
|
||||
limit: String(userLimit),
|
||||
});
|
||||
const trimmedSearch = search.trim();
|
||||
if (trimmedSearch) {
|
||||
params.set('search', trimmedSearch);
|
||||
}
|
||||
const response = await fetch(
|
||||
`/api/admin/users?page=${page}&limit=${userLimit}`
|
||||
`/api/admin/users?${params.toString()}`
|
||||
);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
@@ -16146,7 +16240,7 @@ function AdminPageClient() {
|
||||
} finally {
|
||||
setUserListLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [userSearch]);
|
||||
|
||||
// 刷新配置和用户列表
|
||||
const refreshConfigAndUsers = useCallback(async () => {
|
||||
@@ -16418,9 +16512,9 @@ function AdminPageClient() {
|
||||
</CollapsibleTab>
|
||||
|
||||
<div className='space-y-4'>
|
||||
{/* 用户配置标签 */}
|
||||
{/* 用户管理标签 */}
|
||||
<CollapsibleTab
|
||||
title='用户配置'
|
||||
title='用户管理'
|
||||
icon={
|
||||
<Users size={20} className='text-gray-600 dark:text-gray-400' />
|
||||
}
|
||||
@@ -16437,6 +16531,8 @@ function AdminPageClient() {
|
||||
userTotal={userTotal}
|
||||
fetchUsersV2={fetchUsersV2}
|
||||
userListLoading={userListLoading}
|
||||
userSearch={userSearch}
|
||||
setUserSearch={setUserSearch}
|
||||
/>
|
||||
</CollapsibleTab>
|
||||
|
||||
|
||||
@@ -44,10 +44,11 @@ export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const page = parseInt(searchParams.get('page') || '1', 10);
|
||||
const limit = parseInt(searchParams.get('limit') || '10', 10);
|
||||
const search = (searchParams.get('search') || '').trim();
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// 获取用户列表(优先使用新版本)
|
||||
const result = await db.getUserListV2(offset, limit, process.env.USERNAME);
|
||||
const result = await db.getUserListV2(offset, limit, process.env.USERNAME, search);
|
||||
|
||||
if (result.users.length > 0) {
|
||||
// 使用新版本数据
|
||||
|
||||
+45
-17
@@ -1306,7 +1306,8 @@ export class D1Storage implements IStorage {
|
||||
async getUserListV2(
|
||||
offset = 0,
|
||||
limit = 20,
|
||||
ownerUsername?: string
|
||||
ownerUsername?: string,
|
||||
search?: string
|
||||
): Promise<{
|
||||
users: Array<{
|
||||
username: string;
|
||||
@@ -1320,10 +1321,16 @@ export class D1Storage implements IStorage {
|
||||
total: number;
|
||||
}> {
|
||||
try {
|
||||
const trimmedSearch = search?.trim() || '';
|
||||
const searchPattern = `%${trimmedSearch}%`;
|
||||
|
||||
// 获取总数
|
||||
const countResult = await this.db
|
||||
.prepare('SELECT COUNT(*) as total FROM users')
|
||||
.first();
|
||||
const countQuery = trimmedSearch
|
||||
? this.db
|
||||
.prepare('SELECT COUNT(*) as total FROM users WHERE username LIKE ?')
|
||||
.bind(searchPattern)
|
||||
: this.db.prepare('SELECT COUNT(*) as total FROM users');
|
||||
const countResult = await countQuery.first();
|
||||
let total = (countResult?.total as number) || 0;
|
||||
|
||||
// 检查站长是否在数据库中
|
||||
@@ -1333,8 +1340,11 @@ export class D1Storage implements IStorage {
|
||||
ownerInfo = await this.getUserInfoV2(ownerUsername);
|
||||
ownerInDatabase = !!ownerInfo && ownerInfo.created_at !== 0;
|
||||
|
||||
// 如果站长不在数据库中,总数+1
|
||||
if (!ownerInDatabase) {
|
||||
// 如果站长不在数据库中且匹配搜索条件,总数+1
|
||||
if (
|
||||
!ownerInDatabase &&
|
||||
(!trimmedSearch || ownerUsername.includes(trimmedSearch))
|
||||
) {
|
||||
total += 1;
|
||||
}
|
||||
}
|
||||
@@ -1343,7 +1353,11 @@ export class D1Storage implements IStorage {
|
||||
let actualOffset = offset;
|
||||
let actualLimit = limit;
|
||||
|
||||
if (ownerUsername && !ownerInDatabase) {
|
||||
if (
|
||||
ownerUsername &&
|
||||
!ownerInDatabase &&
|
||||
(!trimmedSearch || ownerUsername.includes(trimmedSearch))
|
||||
) {
|
||||
if (offset === 0) {
|
||||
// 第一页:只获取 limit-1 个用户,为站长留出位置
|
||||
actualLimit = limit - 1;
|
||||
@@ -1354,20 +1368,34 @@ export class D1Storage implements IStorage {
|
||||
}
|
||||
|
||||
// 获取用户列表(按创建时间降序)
|
||||
const result = await this.db
|
||||
.prepare(`
|
||||
SELECT username, role, banned, tags, oidc_sub, enabled_apis, created_at
|
||||
FROM users
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`)
|
||||
.bind(actualLimit, actualOffset)
|
||||
.all();
|
||||
const listQuery = trimmedSearch
|
||||
? this.db
|
||||
.prepare(`
|
||||
SELECT username, role, banned, tags, oidc_sub, enabled_apis, created_at
|
||||
FROM users
|
||||
WHERE username LIKE ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`)
|
||||
.bind(searchPattern, actualLimit, actualOffset)
|
||||
: this.db
|
||||
.prepare(`
|
||||
SELECT username, role, banned, tags, oidc_sub, enabled_apis, created_at
|
||||
FROM users
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`)
|
||||
.bind(actualLimit, actualOffset);
|
||||
const result = await listQuery.all();
|
||||
|
||||
const users = [];
|
||||
|
||||
// 如果有站长且在第一页,确保站长始终在第一位
|
||||
if (ownerUsername && offset === 0) {
|
||||
if (
|
||||
ownerUsername &&
|
||||
offset === 0 &&
|
||||
(!trimmedSearch || ownerUsername.includes(trimmedSearch))
|
||||
) {
|
||||
users.push({
|
||||
username: ownerUsername,
|
||||
role: 'owner' as const,
|
||||
|
||||
+3
-2
@@ -555,7 +555,8 @@ export class DbManager {
|
||||
async getUserListV2(
|
||||
offset = 0,
|
||||
limit = 20,
|
||||
ownerUsername?: string
|
||||
ownerUsername?: string,
|
||||
search?: string
|
||||
): Promise<{
|
||||
users: Array<{
|
||||
username: string;
|
||||
@@ -569,7 +570,7 @@ export class DbManager {
|
||||
total: number;
|
||||
}> {
|
||||
if (typeof (this.storage as any).getUserListV2 === 'function') {
|
||||
return (this.storage as any).getUserListV2(offset, limit, ownerUsername);
|
||||
return (this.storage as any).getUserListV2(offset, limit, ownerUsername, search);
|
||||
}
|
||||
return { users: [], total: 0 };
|
||||
}
|
||||
|
||||
+45
-17
@@ -563,7 +563,8 @@ export class PostgresStorage implements IStorage {
|
||||
async getUserListV2(
|
||||
offset = 0,
|
||||
limit = 20,
|
||||
ownerUsername?: string
|
||||
ownerUsername?: string,
|
||||
search?: string
|
||||
): Promise<{
|
||||
users: Array<{
|
||||
username: string;
|
||||
@@ -577,10 +578,16 @@ export class PostgresStorage implements IStorage {
|
||||
total: number;
|
||||
}> {
|
||||
try {
|
||||
const trimmedSearch = search?.trim() || '';
|
||||
const searchPattern = `%${trimmedSearch}%`;
|
||||
|
||||
// 获取总数
|
||||
const countResult = await this.db
|
||||
.prepare('SELECT COUNT(*) as total FROM users')
|
||||
.first();
|
||||
const countQuery = trimmedSearch
|
||||
? this.db
|
||||
.prepare('SELECT COUNT(*) as total FROM users WHERE username LIKE $1')
|
||||
.bind(searchPattern)
|
||||
: this.db.prepare('SELECT COUNT(*) as total FROM users');
|
||||
const countResult = await countQuery.first();
|
||||
let total = (countResult?.total as number) || 0;
|
||||
|
||||
// 检查站长是否在数据库中
|
||||
@@ -590,8 +597,11 @@ export class PostgresStorage implements IStorage {
|
||||
ownerInfo = await this.getUserInfoV2(ownerUsername);
|
||||
ownerInDatabase = !!ownerInfo && ownerInfo.created_at !== 0;
|
||||
|
||||
// 如果站长不在数据库中,总数+1
|
||||
if (!ownerInDatabase) {
|
||||
// 如果站长不在数据库中且匹配搜索条件,总数+1
|
||||
if (
|
||||
!ownerInDatabase &&
|
||||
(!trimmedSearch || ownerUsername.includes(trimmedSearch))
|
||||
) {
|
||||
total += 1;
|
||||
}
|
||||
}
|
||||
@@ -600,7 +610,11 @@ export class PostgresStorage implements IStorage {
|
||||
let actualOffset = offset;
|
||||
let actualLimit = limit;
|
||||
|
||||
if (ownerUsername && !ownerInDatabase) {
|
||||
if (
|
||||
ownerUsername &&
|
||||
!ownerInDatabase &&
|
||||
(!trimmedSearch || ownerUsername.includes(trimmedSearch))
|
||||
) {
|
||||
if (offset === 0) {
|
||||
// 第一页:只获取 limit-1 个用户,为站长留出位置
|
||||
actualLimit = limit - 1;
|
||||
@@ -611,20 +625,34 @@ export class PostgresStorage implements IStorage {
|
||||
}
|
||||
|
||||
// 获取用户列表(按创建时间降序)
|
||||
const result = await this.db
|
||||
.prepare(`
|
||||
SELECT username, role, banned, tags, oidc_sub, enabled_apis, created_at
|
||||
FROM users
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1 OFFSET $2
|
||||
`)
|
||||
.bind(actualLimit, actualOffset)
|
||||
.all();
|
||||
const listQuery = trimmedSearch
|
||||
? this.db
|
||||
.prepare(`
|
||||
SELECT username, role, banned, tags, oidc_sub, enabled_apis, created_at
|
||||
FROM users
|
||||
WHERE username LIKE $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
`)
|
||||
.bind(searchPattern, actualLimit, actualOffset)
|
||||
: this.db
|
||||
.prepare(`
|
||||
SELECT username, role, banned, tags, oidc_sub, enabled_apis, created_at
|
||||
FROM users
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1 OFFSET $2
|
||||
`)
|
||||
.bind(actualLimit, actualOffset);
|
||||
const result = await listQuery.all();
|
||||
|
||||
const users = [];
|
||||
|
||||
// 如果有站长且在第一页,确保站长始终在第一位
|
||||
if (ownerUsername && offset === 0) {
|
||||
if (
|
||||
ownerUsername &&
|
||||
offset === 0 &&
|
||||
(!trimmedSearch || ownerUsername.includes(trimmedSearch))
|
||||
) {
|
||||
users.push({
|
||||
username: ownerUsername,
|
||||
role: 'owner' as const,
|
||||
|
||||
@@ -1347,7 +1347,8 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
async getUserListV2(
|
||||
offset = 0,
|
||||
limit = 20,
|
||||
ownerUsername?: string
|
||||
ownerUsername?: string,
|
||||
search?: string
|
||||
): Promise<{
|
||||
users: Array<{
|
||||
username: string;
|
||||
@@ -1360,6 +1361,8 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
}>;
|
||||
total: number;
|
||||
}> {
|
||||
const trimmedSearch = search?.trim() || '';
|
||||
|
||||
// 获取总数
|
||||
let total = await this.withRetry(() => this.adapter.zCard(this.userListKey()));
|
||||
|
||||
@@ -1386,8 +1389,11 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
ownerExistenceCache.set(ownerUsername, ownerInDatabase);
|
||||
}
|
||||
|
||||
// 如果站长不在数据库中,总数+1(无论在哪一页都要加)
|
||||
if (!ownerInDatabase) {
|
||||
// 如果站长不在数据库中且匹配搜索条件,总数+1(无论在哪一页都要加)
|
||||
if (
|
||||
!ownerInDatabase &&
|
||||
(!trimmedSearch || ownerUsername.includes(trimmedSearch))
|
||||
) {
|
||||
total += 1;
|
||||
}
|
||||
}
|
||||
@@ -1396,7 +1402,11 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
let actualOffset = offset;
|
||||
let actualLimit = limit;
|
||||
|
||||
if (ownerUsername && !ownerInDatabase) {
|
||||
if (
|
||||
ownerUsername &&
|
||||
!ownerInDatabase &&
|
||||
(!trimmedSearch || ownerUsername.includes(trimmedSearch))
|
||||
) {
|
||||
if (offset === 0) {
|
||||
// 第一页:只获取 limit-1 个用户,为站长留出位置
|
||||
actualLimit = limit - 1;
|
||||
@@ -1407,14 +1417,38 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
}
|
||||
|
||||
// 获取用户列表(按注册时间升序)
|
||||
const usernames = await this.withRetry(() =>
|
||||
this.adapter.zRange(this.userListKey(), actualOffset, actualOffset + actualLimit - 1)
|
||||
let usernames = await this.withRetry(() =>
|
||||
trimmedSearch
|
||||
? this.adapter.zRange(this.userListKey(), 0, -1)
|
||||
: this.adapter.zRange(
|
||||
this.userListKey(),
|
||||
actualOffset,
|
||||
actualOffset + actualLimit - 1
|
||||
)
|
||||
);
|
||||
if (trimmedSearch) {
|
||||
usernames = usernames.filter((username) =>
|
||||
ensureString(username).includes(trimmedSearch)
|
||||
);
|
||||
total = usernames.length;
|
||||
if (
|
||||
ownerUsername &&
|
||||
!ownerInDatabase &&
|
||||
ownerUsername.includes(trimmedSearch)
|
||||
) {
|
||||
total += 1;
|
||||
}
|
||||
usernames = usernames.slice(actualOffset, actualOffset + actualLimit);
|
||||
}
|
||||
|
||||
const users = [];
|
||||
|
||||
// 如果有站长且在第一页,确保站长始终在第一位
|
||||
if (ownerUsername && offset === 0) {
|
||||
if (
|
||||
ownerUsername &&
|
||||
offset === 0 &&
|
||||
(!trimmedSearch || ownerUsername.includes(trimmedSearch))
|
||||
) {
|
||||
// 即使站长不在数据库中,也要添加站长(站长使用环境变量认证)
|
||||
users.push({
|
||||
username: ownerUsername,
|
||||
|
||||
Reference in New Issue
Block a user