mirror of
https://github.com/coaidev/coai.git
synced 2025-05-30 02:10:25 +09:00
feat: optimize pagination
This commit is contained in:
parent
c993fe34e2
commit
98690e093a
@ -8,7 +8,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "chatnio",
|
||||
"version": "3.10.3"
|
||||
"version": "3.10.4"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
@ -337,16 +337,15 @@
|
||||
margin: 0;
|
||||
background: hsl(var(--background));
|
||||
transition: 0.2s ease-in-out;
|
||||
transition-property: width, background, box-shadow, border-right, opacity;
|
||||
transition-property: width, background, box-shadow;
|
||||
border-right: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
overflow-x: hidden;
|
||||
|
||||
&.open {
|
||||
width: 260px;
|
||||
border-right: 1px solid hsl(var(--border));
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
@ -362,10 +361,6 @@
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
&.open .conversation-list {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
height: max-content;
|
||||
width: 100%;
|
||||
@ -409,7 +404,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 6px 0;
|
||||
@ -603,6 +597,7 @@
|
||||
|
||||
button {
|
||||
margin: 0.5rem 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.space-footer {
|
||||
|
@ -131,7 +131,7 @@ function SelectGroupMobile(props: SelectGroupProps) {
|
||||
props.onChange?.(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="select-group mobile">
|
||||
<SelectTrigger className="select-group mobile whitespace-nowrap flex-nowrap">
|
||||
<SelectValue placeholder={props.current?.value || ""} />
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
|
@ -19,19 +19,14 @@ import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
import { InvitationForm, InvitationResponse } from "@/admin/types.ts";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Download,
|
||||
Loader2,
|
||||
RotateCw,
|
||||
} from "lucide-react";
|
||||
import { Download, Loader2, RotateCw } from "lucide-react";
|
||||
import { useEffectAsync } from "@/utils/hook.ts";
|
||||
import { generateInvitation, getInvitationList } from "@/admin/api/chart.ts";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import { useToast } from "@/components/ui/use-toast.ts";
|
||||
import { Textarea } from "@/components/ui/textarea.tsx";
|
||||
import { saveAsFile } from "@/utils/dom.ts";
|
||||
import { PaginationAction } from "@/components/ui/pagination.tsx";
|
||||
|
||||
function GenerateDialog() {
|
||||
const { t } = useTranslation();
|
||||
@ -186,27 +181,12 @@ function InvitationTable() {
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className={`pagination`}>
|
||||
<Button
|
||||
variant={`default`}
|
||||
size={`icon`}
|
||||
onClick={() => setPage(page - 1)}
|
||||
disabled={page === 0}
|
||||
>
|
||||
<ChevronLeft className={`h-4 w-4`} />
|
||||
</Button>
|
||||
<Button variant={`ghost`} size={`icon`}>
|
||||
{page + 1}
|
||||
</Button>
|
||||
<Button
|
||||
variant={`default`}
|
||||
size={`icon`}
|
||||
onClick={() => setPage(page + 1)}
|
||||
disabled={page + 1 === data.total}
|
||||
>
|
||||
<ChevronRight className={`h-4 w-4`} />
|
||||
</Button>
|
||||
</div>
|
||||
<PaginationAction
|
||||
current={page}
|
||||
total={data.total}
|
||||
onPageChange={setPage}
|
||||
offset
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className={`empty`}>
|
||||
|
@ -18,25 +18,28 @@ import { useLocation } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { mobile } from "@/utils/device.ts";
|
||||
import { cn } from "@/components/ui/lib/utils.ts";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
|
||||
type MenuItemProps = {
|
||||
title: string;
|
||||
icon: React.ReactNode;
|
||||
path: string;
|
||||
exit?: boolean;
|
||||
};
|
||||
|
||||
function MenuItem({ title, icon, path }: MenuItemProps) {
|
||||
function MenuItem({ title, icon, path, exit }: MenuItemProps) {
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const active = useMemo(
|
||||
() =>
|
||||
location.pathname === `/admin${path}` ||
|
||||
location.pathname + "/" === `/admin${path}`,
|
||||
!exit &&
|
||||
(location.pathname === `/admin${path}` ||
|
||||
location.pathname + "/" === `/admin${path}`),
|
||||
[location.pathname, path],
|
||||
);
|
||||
|
||||
const redirect = async () => {
|
||||
if (exit) return await router.navigate("/");
|
||||
|
||||
if (mobile) dispatch(closeMenu());
|
||||
await router.navigate(`/admin${path}`);
|
||||
};
|
||||
@ -87,16 +90,7 @@ function MenuBar() {
|
||||
icon={<FileClock />}
|
||||
path={"/logger"}
|
||||
/>
|
||||
|
||||
<div className={`grow mt-6`} />
|
||||
<Button
|
||||
variant={`outline`}
|
||||
size={`icon`}
|
||||
className={`ml-3`}
|
||||
onClick={() => router.navigate("/")}
|
||||
>
|
||||
<LogOut className={`h-4 w-4`} />
|
||||
</Button>
|
||||
<MenuItem title={t("admin.exit")} icon={<LogOut />} path={""} exit />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -38,8 +38,6 @@ import {
|
||||
CalendarCheck2,
|
||||
CalendarClock,
|
||||
CalendarOff,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
CloudCog,
|
||||
CloudFog,
|
||||
KeyRound,
|
||||
@ -59,6 +57,7 @@ import { getNumber, parseNumber } from "@/utils/base.ts";
|
||||
import { useDeeptrain } from "@/conf/env.ts";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectUsername } from "@/store/auth.ts";
|
||||
import { PaginationAction } from "@/components/ui/pagination.tsx";
|
||||
|
||||
type OperationMenuProps = {
|
||||
user: UserData;
|
||||
@ -435,27 +434,12 @@ function UserTable() {
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className={`pagination`}>
|
||||
<Button
|
||||
variant={`default`}
|
||||
size={`icon`}
|
||||
onClick={() => setPage(page - 1)}
|
||||
disabled={page === 0}
|
||||
>
|
||||
<ChevronLeft className={`h-4 w-4`} />
|
||||
</Button>
|
||||
<Button variant={`ghost`} size={`icon`}>
|
||||
{page + 1}
|
||||
</Button>
|
||||
<Button
|
||||
variant={`default`}
|
||||
size={`icon`}
|
||||
onClick={() => setPage(page + 1)}
|
||||
disabled={page + 1 === data.total}
|
||||
>
|
||||
<ChevronRight className={`h-4 w-4`} />
|
||||
</Button>
|
||||
</div>
|
||||
<PaginationAction
|
||||
current={page}
|
||||
total={data.total}
|
||||
onPageChange={setPage}
|
||||
offset
|
||||
/>
|
||||
</>
|
||||
) : loading ? (
|
||||
<div className={`flex flex-col mb-4 mt-12 items-center`}>
|
||||
|
@ -2,7 +2,7 @@ import * as React from "react";
|
||||
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
|
||||
|
||||
import { cn } from "@/components/ui/lib/utils";
|
||||
import { ButtonProps, buttonVariants } from "src/components/ui/button";
|
||||
import { ButtonProps, buttonVariants } from "@/components/ui/button";
|
||||
|
||||
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
|
||||
<nav
|
||||
@ -30,7 +30,11 @@ const PaginationItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<"li">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li ref={ref} className={cn("", className)} {...props} />
|
||||
<li
|
||||
ref={ref}
|
||||
className={cn("cursor-pointer select-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
PaginationItem.displayName = "PaginationItem";
|
||||
|
||||
@ -65,12 +69,11 @@ const PaginationPrevious = ({
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 pl-2.5", className)}
|
||||
size="icon"
|
||||
className={cn("gap-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<span>Previous</span>
|
||||
</PaginationLink>
|
||||
);
|
||||
PaginationPrevious.displayName = "PaginationPrevious";
|
||||
@ -81,11 +84,10 @@ const PaginationNext = ({
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 pr-2.5", className)}
|
||||
size="icon"
|
||||
className={cn("gap-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<span>Next</span>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</PaginationLink>
|
||||
);
|
||||
@ -101,11 +103,94 @@ const PaginationEllipsis = ({
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
);
|
||||
PaginationEllipsis.displayName = "PaginationEllipsis";
|
||||
|
||||
type PaginationActionProps = React.ComponentProps<"div"> & {
|
||||
current: number;
|
||||
total: number;
|
||||
offset?: boolean;
|
||||
onPageChange: (page: number) => void;
|
||||
};
|
||||
|
||||
const PaginationAction = ({
|
||||
current,
|
||||
total,
|
||||
offset = false,
|
||||
className,
|
||||
onPageChange,
|
||||
children,
|
||||
...props
|
||||
}: PaginationActionProps) => {
|
||||
const real = current + (offset ? 1 : 0);
|
||||
const diff = total - real;
|
||||
|
||||
const hasPrev = current > 0;
|
||||
const hasNext = diff > 1;
|
||||
|
||||
const hasStepPrev = current > 1 && !hasNext;
|
||||
const hasStepNext = diff > 0 && !hasPrev;
|
||||
|
||||
const showRightEllipsis = diff > 2;
|
||||
const showLeftEllipsis = real > 2 && !showRightEllipsis;
|
||||
|
||||
return (
|
||||
<Pagination className={cn("py-4", className)} {...props}>
|
||||
<PaginationContent>
|
||||
<PaginationItem onClick={() => hasPrev && onPageChange(current - 1)}>
|
||||
<PaginationPrevious />
|
||||
</PaginationItem>
|
||||
|
||||
{showLeftEllipsis && (
|
||||
<PaginationItem>
|
||||
<PaginationEllipsis />
|
||||
</PaginationItem>
|
||||
)}
|
||||
|
||||
{hasStepPrev && (
|
||||
<PaginationItem onClick={() => onPageChange(current - 2)}>
|
||||
<PaginationLink>{real - 2}</PaginationLink>
|
||||
</PaginationItem>
|
||||
)}
|
||||
|
||||
{hasPrev && (
|
||||
<PaginationItem onClick={() => onPageChange(current - 1)}>
|
||||
<PaginationLink>{real - 1}</PaginationLink>
|
||||
</PaginationItem>
|
||||
)}
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationLink isActive>{real}</PaginationLink>
|
||||
</PaginationItem>
|
||||
|
||||
{hasNext && (
|
||||
<PaginationItem onClick={() => onPageChange(current + 1)}>
|
||||
<PaginationLink>{real + 1}</PaginationLink>
|
||||
</PaginationItem>
|
||||
)}
|
||||
|
||||
{hasStepNext && (
|
||||
<PaginationItem onClick={() => onPageChange(current + 2)}>
|
||||
<PaginationLink>{real + 2}</PaginationLink>
|
||||
</PaginationItem>
|
||||
)}
|
||||
|
||||
{showRightEllipsis && (
|
||||
<PaginationItem>
|
||||
<PaginationEllipsis />
|
||||
</PaginationItem>
|
||||
)}
|
||||
|
||||
<PaginationItem onClick={() => hasNext && onPageChange(current + 1)}>
|
||||
<PaginationNext />
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
);
|
||||
};
|
||||
PaginationAction.displayName = "PaginationAction";
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
@ -114,4 +199,5 @@ export {
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
PaginationAction,
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
import { syncSiteInfo } from "@/admin/api/info.ts";
|
||||
import { setAxiosConfig } from "@/conf/api.ts";
|
||||
|
||||
export const version = "3.10.3"; // version of the current build
|
||||
export const version = "3.10.4"; // version of the current build
|
||||
export const dev: boolean = getDev(); // is in development mode (for debugging, in localhost origin)
|
||||
export const deploy: boolean = true; // is production environment (for api endpoint)
|
||||
export const tokenField = getTokenField(deploy); // token field name for storing token
|
||||
|
@ -116,7 +116,8 @@ export function setAnnouncement(announcement: string): void {
|
||||
*/
|
||||
if (!announcement || announcement.trim() === "") return;
|
||||
|
||||
const firstReceived = getMemory("announcement").trim() !== announcement.trim();
|
||||
const firstReceived =
|
||||
getMemory("announcement").trim() !== announcement.trim();
|
||||
setMemory("announcement", announcement);
|
||||
|
||||
announcementEvent.emit({
|
||||
|
@ -395,11 +395,12 @@
|
||||
"dashboard": "仪表盘",
|
||||
"users": "后台管理",
|
||||
"user": "用户管理",
|
||||
"broadcast": "公告管理",
|
||||
"broadcast": "公告通知",
|
||||
"channel": "渠道设置",
|
||||
"settings": "系统设置",
|
||||
"prize": "价格设定",
|
||||
"subscription": "订阅管理",
|
||||
"exit": "退出后台",
|
||||
"billing": "收入",
|
||||
"billing-today": "今日入账",
|
||||
"billing-month": "本月入账",
|
||||
|
@ -669,7 +669,8 @@
|
||||
"unban-action": " unBlock User",
|
||||
"unban-action-desc": "Are you sure you want to unblock this user?",
|
||||
"billing": "Income",
|
||||
"chatnio-format-only": "This format is unique to Chat Nio"
|
||||
"chatnio-format-only": "This format is unique to Chat Nio",
|
||||
"exit": "Log out of the background"
|
||||
},
|
||||
"mask": {
|
||||
"title": "Mask Settings",
|
||||
|
@ -669,7 +669,8 @@
|
||||
"unban-action": "ユーザーのブロックを解除する",
|
||||
"unban-action-desc": "このユーザーのブロックを解除してもよろしいですか?",
|
||||
"billing": "収入",
|
||||
"chatnio-format-only": "このフォーマットはChat Nioに固有です"
|
||||
"chatnio-format-only": "このフォーマットはChat Nioに固有です",
|
||||
"exit": "バックグラウンドからログアウト"
|
||||
},
|
||||
"mask": {
|
||||
"title": "プリセット設定",
|
||||
|
@ -669,7 +669,8 @@
|
||||
"unban-action": "Разблокировать пользователя",
|
||||
"unban-action-desc": "Вы уверены, что хотите разблокировать этого пользователя?",
|
||||
"billing": "Доходы",
|
||||
"chatnio-format-only": "Этот формат уникален для Chat Nio"
|
||||
"chatnio-format-only": "Этот формат уникален для Chat Nio",
|
||||
"exit": "Выйти из фонового режима"
|
||||
},
|
||||
"mask": {
|
||||
"title": "Настройки маски",
|
||||
|
Loading…
Reference in New Issue
Block a user