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