Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions apps/src/app/accounts/accounts-page-helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ import type { Account } from "@/types";
export type StatusFilter = "all" | "available" | "low_quota" | "limited" | "banned";
export type AccountExportMode = "single" | "multiple";
export type AccountSizeSortMode = "large-first" | "small-first";
export type AccountMoveDirection = "up" | "down" | "top" | "bottom";
export type AccountMovePlacement =
| { type: "top" }
| { type: "bottom" }
| { type: "before"; anchorAccountId: string }
| { type: "after"; anchorAccountId: string };

const ACCOUNT_SORT_STEP = 5;

Expand Down Expand Up @@ -596,6 +602,39 @@ export function buildAccountOrderUpdates(orderedAccounts: Account[]) {
);
}

// 按目标位置重排账号;锚点账号不存在时返回 null,由调用方提示刷新。
export function buildAccountsByMovedOrder(
orderedAccounts: Account[],
account: Account,
placement: AccountMovePlacement,
): Account[] | null {
const reorderedAccounts = orderedAccounts.filter(
(item) => item.id !== account.id,
);

if (placement.type === "top") {
reorderedAccounts.unshift(account);
return reorderedAccounts;
}
if (placement.type === "bottom") {
reorderedAccounts.push(account);
return reorderedAccounts;
}

const anchorIndex = reorderedAccounts.findIndex(
(item) => item.id === placement.anchorAccountId,
);
if (anchorIndex === -1) {
return null;
}
reorderedAccounts.splice(
placement.type === "before" ? anchorIndex : anchorIndex + 1,
0,
account,
);
return reorderedAccounts;
}

export function getAccountSizeGroup(
account: Account,
): "large" | "standard" | "small" {
Expand Down
42 changes: 41 additions & 1 deletion apps/src/app/accounts/accounts-page-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import type { Dispatch, SetStateAction } from "react";
import {
ArrowDown,
ArrowDownToLine,
ArrowUp,
ArrowUpDown,
ArrowUpToLine,
BarChart3,
Download,
FileUp,
Expand Down Expand Up @@ -87,6 +89,7 @@ import { AccountProxyStatusHeader } from "@/components/accounts/account-proxy-st
import {
type AccountEditorState,
type AccountExportMode,
type AccountMoveDirection,
type AccountSizeSortMode,
type DeleteDialogState,
type StatusFilter,
Expand Down Expand Up @@ -225,7 +228,7 @@ export interface AccountsPageViewProps {
openAccountEditor: (account: Account) => void;
handleMoveAccount: (
account: Account,
direction: "up" | "down",
direction: AccountMoveDirection,
) => Promise<void>;
handleApplyAccountSizeSort: (mode: AccountSizeSortMode) => Promise<void>;
handleConfirmAccountEditor: () => Promise<void>;
Expand Down Expand Up @@ -899,6 +902,9 @@ export function AccountsPageView(props: AccountsPageViewProps) {
const canMoveDown =
filteredIndex !== -1 &&
filteredIndex < filteredAccounts.length - 1;
const isAtListTop = accounts[0]?.id === account.id;
const isAtListBottom =
accounts[accounts.length - 1]?.id === account.id;
return (
<TableRow key={account.id} className="group">
<TableCell className="text-center">
Expand Down Expand Up @@ -1040,6 +1046,40 @@ export function AccountsPageView(props: AccountsPageViewProps) {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuGroup>
<DropdownMenuLabel className="px-2 py-1 text-[11px] uppercase tracking-[0.16em] text-muted-foreground/80">
{t("排序")}
</DropdownMenuLabel>
<DropdownMenuItem
className="gap-2"
disabled={
!isServiceReady ||
isReorderingAccounts ||
isAtListTop
}
onClick={() =>
void handleMoveAccount(account, "top")
}
>
<ArrowUpToLine className="h-4 w-4" />
{t("移到顶部")}
</DropdownMenuItem>
<DropdownMenuItem
className="gap-2"
disabled={
!isServiceReady ||
isReorderingAccounts ||
isAtListBottom
}
onClick={() =>
void handleMoveAccount(account, "bottom")
}
>
<ArrowDownToLine className="h-4 w-4" />
{t("移到底部")}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
className="gap-2"
Expand Down
58 changes: 43 additions & 15 deletions apps/src/app/accounts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ import {
} from "@/lib/api/account-client";
import { useI18n } from "@/lib/i18n/provider";
import {
buildAccountsByMovedOrder,
buildAccountsBySizeOrder,
buildAccountOrderUpdates,
type AccountEditorState,
type AccountMoveDirection,
type AccountMovePlacement,
type DeleteDialogState,
normalizeAccountPlanKey,
normalizeTagsDraft,
Expand Down Expand Up @@ -603,42 +606,67 @@ const toggleCleanupStatus = (rawStatus: string) => {
);
};

const handleMoveAccount = async (
// 顶部/底部按全量列表定位,上移/下移仍按当前筛选结果取相邻账号。
const resolveAccountMovePlacement = (
account: Account,
direction: "up" | "down",
) => {
direction: AccountMoveDirection,
): AccountMovePlacement | null => {
if (direction === "top" || direction === "bottom") {
const boundaryAccount =
direction === "top" ? accounts[0] : accounts[accounts.length - 1];
if (boundaryAccount?.id === account.id) {
toast.info(
direction === "top"
? t("当前账号已经在最前面")
: t("当前账号已经在最后面"),
);
return null;
}
return { type: direction };
}

const filteredIndex = filteredAccountIndexMap.get(account.id);
if (filteredIndex == null) {
toast.error(t("未找到当前账号,请刷新后重试"));
return;
return null;
}

const targetFilteredIndex =
direction === "up" ? filteredIndex - 1 : filteredIndex + 1;
if (targetFilteredIndex < 0) {
toast.info(t("当前账号已经在最前面"));
return;
return null;
}
if (targetFilteredIndex >= filteredAccounts.length) {
toast.info(t("当前账号已经在最后面"));
return null;
}

return {
type: direction === "up" ? "before" : "after",
anchorAccountId: filteredAccounts[targetFilteredIndex].id,
};
};

const handleMoveAccount = async (
account: Account,
direction: AccountMoveDirection,
) => {
const placement = resolveAccountMovePlacement(account, direction);
if (!placement) {
return;
}

const targetAccount = filteredAccounts[targetFilteredIndex];
const reorderedAccounts = accounts.filter((item) => item.id !== account.id);
const anchorIndex = reorderedAccounts.findIndex(
(item) => item.id === targetAccount.id,
const reorderedAccounts = buildAccountsByMovedOrder(
accounts,
account,
placement,
);
if (anchorIndex === -1) {
if (!reorderedAccounts) {
toast.error(t("未找到目标账号,请刷新后重试"));
return;
}

reorderedAccounts.splice(
direction === "up" ? anchorIndex : anchorIndex + 1,
0,
account,
);
const updates = buildAccountOrderUpdates(reorderedAccounts);
if (!updates.length) {
toast.info(t("账号顺序未变化"));
Expand Down
2 changes: 2 additions & 0 deletions apps/src/lib/i18n/messages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,8 @@ export const EN_MESSAGES: MessageCatalog = {
未提供: "Unavailable",
上移一位: "Move up",
下移一位: "Move down",
移到顶部: "Move to top",
移到底部: "Move to bottom",
编辑账号信息: "Edit account info",
用量详情: "Usage details",
套餐信息: "Subscription info",
Expand Down
2 changes: 2 additions & 0 deletions apps/src/lib/i18n/messages/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@ export const KO_MESSAGES: MessageCatalog = {
未提供: "제공 안 됨",
上移一位: "한 칸 위로",
下移一位: "한 칸 아래로",
移到顶部: "맨 위로 이동",
移到底部: "맨 아래로 이동",
编辑账号信息: "계정 정보 편집",
用量详情: "사용량 상세",
套餐信息: "구독 정보",
Expand Down
2 changes: 2 additions & 0 deletions apps/src/lib/i18n/messages/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,8 @@ export const RU_MESSAGES: MessageCatalog = {
未提供: "Нет данных",
上移一位: "Переместить вверх",
下移一位: "Переместить вниз",
移到顶部: "В начало списка",
移到底部: "В конец списка",
编辑账号信息: "Изменить аккаунт",
用量详情: "Детали использования",
套餐信息: "Информация о подписке",
Expand Down
Loading