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
6 changes: 6 additions & 0 deletions messages/en/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -1385,6 +1385,9 @@
"keyStatus": {
"enabled": "Enabled",
"disabled": "Disabled",
"active": "Active",
"expired": "Expired",
"expiringSoon": "Expiring Soon",
"keyEnabled": "Key enabled",
"keyDisabled": "Key disabled",
"toggleKeyStatus": "Toggle key status",
Expand All @@ -1396,6 +1399,9 @@
"userStatus": {
"enabled": "Enabled",
"disabled": "Disabled",
"active": "Active",
"expired": "Expired",
"expiringSoon": "Expiring Soon",
"userEnabled": "User enabled",
"userDisabled": "User disabled",
"toggleUserStatus": "Toggle user status",
Expand Down
15 changes: 14 additions & 1 deletion messages/ja/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -1344,11 +1344,24 @@
},
"keyStatus": {
"enabled": "有効",
"disabled": "無効"
"disabled": "無効",
"active": "正常",
"expired": "期限切れ",
"expiringSoon": "まもなく期限切れ",
"keyEnabled": "キーが有効になりました",
"keyDisabled": "キーが無効になりました",
"toggleKeyStatus": "キー状態を切り替える",
"clickToDisableKey": "クリックしてキーを無効化",
"clickToEnableKey": "クリックしてキーを有効化",
"operationFailed": "操作に失敗しました",
"clickToQuickRenew": "クリックして更新"
},
"userStatus": {
"enabled": "有効",
"disabled": "無効",
"active": "正常",
"expired": "期限切れ",
"expiringSoon": "まもなく期限切れ",
"userEnabled": "ユーザーが有効になりました",
"userDisabled": "ユーザーが無効になりました",
"toggleUserStatus": "ユーザー状態を切り替える",
Expand Down
15 changes: 14 additions & 1 deletion messages/ru/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -1357,11 +1357,24 @@
},
"keyStatus": {
"enabled": "Включён",
"disabled": "Отключён"
"disabled": "Отключён",
"active": "Активен",
"expired": "Истёк",
"expiringSoon": "Скоро истечёт",
"keyEnabled": "Ключ включён",
"keyDisabled": "Ключ отключён",
"toggleKeyStatus": "Переключить статус ключа",
"clickToDisableKey": "Нажмите, чтобы отключить ключ",
"clickToEnableKey": "Нажмите, чтобы включить ключ",
"operationFailed": "Операция не удалась",
"clickToQuickRenew": "Нажмите для быстрого продления"
},
"userStatus": {
"enabled": "Включён",
"disabled": "Отключён",
"active": "Активен",
"expired": "Истёк",
"expiringSoon": "Скоро истечёт",
"userEnabled": "Пользователь включён",
"userDisabled": "Пользователь отключён",
"toggleUserStatus": "Переключить статус пользователя",
Expand Down
6 changes: 6 additions & 0 deletions messages/zh-CN/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -1384,6 +1384,9 @@
"keyStatus": {
"enabled": "启用",
"disabled": "禁用",
"active": "正常",
"expired": "已过期",
"expiringSoon": "即将过期",
"keyEnabled": "密钥已启用",
"keyDisabled": "密钥已禁用",
"toggleKeyStatus": "切换密钥启用状态",
Expand All @@ -1395,6 +1398,9 @@
"userStatus": {
"enabled": "启用",
"disabled": "禁用",
"active": "正常",
"expired": "已过期",
"expiringSoon": "即将过期",
"userEnabled": "用户已启用",
"userDisabled": "用户已禁用",
"toggleUserStatus": "切换用户启用状态",
Expand Down
15 changes: 14 additions & 1 deletion messages/zh-TW/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -1356,11 +1356,24 @@
},
"keyStatus": {
"enabled": "啟用",
"disabled": "停用"
"disabled": "停用",
"active": "正常",
"expired": "已過期",
"expiringSoon": "即將過期",
"keyEnabled": "密鑰已啟用",
"keyDisabled": "密鑰已停用",
"toggleKeyStatus": "切換密鑰狀態",
"clickToDisableKey": "點擊停用密鑰",
"clickToEnableKey": "點擊啟用密鑰",
"operationFailed": "操作失敗",
"clickToQuickRenew": "點擊快速續期"
},
"userStatus": {
"enabled": "啟用",
"disabled": "停用",
"active": "正常",
"expired": "已過期",
"expiringSoon": "即將過期",
"userEnabled": "使用者已啟用",
"userDisabled": "使用者已停用",
"toggleUserStatus": "切換使用者狀態",
Expand Down
47 changes: 34 additions & 13 deletions src/app/[locale]/dashboard/_components/user/key-row-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,41 @@ export interface KeyRowItemProps {
};
}

const EXPIRING_SOON_MS = 72 * 60 * 60 * 1000; // 72小时
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

常量 EXPIRING_SOON_MSuser-key-table-row.tsx 中也有定义。为了避免重复和确保一致性,建议将其提取到一个共享的工具文件中(例如 src/lib/constants.tssrc/lib/utils/time.ts)。


function splitGroups(value?: string | null): string[] {
return (value ?? "")
.split(",")
.map((g) => g.trim())
.filter(Boolean);
Comment on lines 97 to 101
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

函数 splitGroupsuser-key-table-row.tsx 中也有定义。为了避免重复和确保一致性,建议将其提取到一个共享的工具文件中(例如 src/lib/utils/array.tssrc/lib/utils/string.ts)。

}

function formatExpiry(expiresAt: string | null | undefined, locale: string): string {
if (!expiresAt) return "-";
const date = new Date(expiresAt);
// 如果解析失败(如"永不过期"等翻译文本),直接返回原文本
if (Number.isNaN(date.getTime())) return expiresAt;
return formatDate(date, "yyyy-MM-dd", locale);
}

function getKeyExpiryStatus(
status: "enabled" | "disabled",
expiresAt: string | null | undefined
): { label: string; variant: "default" | "secondary" | "destructive" | "outline" } {
if (status === "disabled") return { label: "disabled", variant: "secondary" };
if (!expiresAt) return { label: "active", variant: "default" };

const date = new Date(expiresAt);
if (Number.isNaN(date.getTime())) return { label: "active", variant: "default" };

const now = Date.now();
const expTs = date.getTime();

if (expTs <= now) return { label: "expired", variant: "destructive" };
if (expTs - now <= EXPIRING_SOON_MS) return { label: "expiringSoon", variant: "outline" };
return { label: "active", variant: "default" };
}

export function KeyRowItem({
keyData,
userProviderGroup: _userProviderGroup,
Expand Down Expand Up @@ -148,6 +176,9 @@ export function KeyRowItem({
const keyGroups = splitGroups(keyData.providerGroup);
const effectiveGroups = keyGroups.length > 0 ? keyGroups : [translations.defaultGroup];
const visibleGroups = effectiveGroups.slice(0, 1);

// 计算 key 过期状态
const keyExpiryStatus = getKeyExpiryStatus(localStatus, localExpiresAt);
const remainingGroups = Math.max(0, effectiveGroups.length - visibleGroups.length);
const effectiveGroupText = effectiveGroups.join(", ");

Expand Down Expand Up @@ -194,13 +225,6 @@ export function KeyRowItem({
}
};

const formatExpiry = (expiresAt: string | null | undefined): string => {
if (!expiresAt) return "-";
const date = new Date(expiresAt);
if (Number.isNaN(date.getTime())) return "-";
return formatDate(date, "yyyy-MM-dd", locale);
};

const handleQuickRenewConfirm = async (
_keyId: number,
expiresAt: Date,
Expand Down Expand Up @@ -297,11 +321,8 @@ export function KeyRowItem({
<div className="min-w-0">
<div className="flex items-center gap-2 min-w-0">
<div className="truncate font-medium">{keyData.name}</div>
<Badge
variant={localStatus === "enabled" ? "default" : "secondary"}
className="text-[10px]"
>
{localStatus === "enabled" ? translations.status.enabled : translations.status.disabled}
<Badge variant={keyExpiryStatus.variant} className="text-[10px] shrink-0">
{tKeyStatus(keyExpiryStatus.label)}
</Badge>
</div>
</div>
Expand Down Expand Up @@ -444,7 +465,7 @@ export function KeyRowItem({
setQuickRenewOpen(true);
}}
>
{formatExpiry(localExpiresAt)}
{formatExpiry(localExpiresAt, locale)}
</div>

{/* 操作 */}
Expand Down
60 changes: 58 additions & 2 deletions src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Switch } from "@/components/ui/switch";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useRouter } from "@/i18n/routing";
import { cn } from "@/lib/utils";
import { getContrastTextColor, getGroupColor } from "@/lib/utils/color";
import { formatDate } from "@/lib/utils/date-format";
import type { UserDisplay } from "@/types/user";
import { KeyRowItem } from "./key-row-item";
Expand Down Expand Up @@ -66,6 +67,30 @@ export interface UserKeyTableRowProps {
}

const DEFAULT_GRID_COLUMNS_CLASS = "grid-cols-[minmax(260px,1fr)_120px_repeat(6,90px)_80px]";
const EXPIRING_SOON_MS = 72 * 60 * 60 * 1000; // 72小时
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

常量 EXPIRING_SOON_MSkey-row-item.tsx 中也有定义。为了避免重复和确保一致性,建议将其提取到一个共享的工具文件中(例如 src/lib/constants.tssrc/lib/utils/time.ts)。

const MAX_VISIBLE_GROUPS = 2; // 最多显示的分组数量

function splitGroups(value?: string | null): string[] {
return (value ?? "")
.split(",")
.map((g) => g.trim())
.filter(Boolean);
Comment on lines +73 to +77
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

函数 splitGroupskey-row-item.tsx 中也有定义。为了避免重复和确保一致性,建议将其提取到一个共享的工具文件中(例如 src/lib/utils/array.tssrc/lib/utils/string.ts)。

}

function getExpiryStatus(
isEnabled: boolean,
expiresAt: Date | null | undefined
): { label: string; variant: "default" | "secondary" | "destructive" | "outline" } {
const now = Date.now();
const expTs = expiresAt?.getTime();
const hasExpiry = typeof expTs === "number" && Number.isFinite(expTs);

if (!isEnabled) return { label: "disabled", variant: "secondary" };
if (hasExpiry && expTs <= now) return { label: "expired", variant: "destructive" };
if (hasExpiry && expTs - now <= EXPIRING_SOON_MS)
return { label: "expiringSoon", variant: "outline" };
return { label: "active", variant: "default" };
}

function normalizeLimitValue(value: unknown): number | null {
const raw = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
Expand Down Expand Up @@ -129,6 +154,14 @@ export function UserKeyTableRow({

const expiresText = formatExpiry(localExpiresAt ?? null, locale);

// 计算用户过期状态
const expiryStatus = getExpiryStatus(localIsEnabled, localExpiresAt ?? null);

// 处理 Provider Group:拆分成数组
const userGroups = splitGroups(user.providerGroup);
const visibleGroups = userGroups.slice(0, MAX_VISIBLE_GROUPS);
const remainingGroupsCount = Math.max(0, userGroups.length - MAX_VISIBLE_GROUPS);

const limit5h = normalizeLimitValue(user.limit5hUsd);
const limitDaily = normalizeLimitValue(user.dailyQuota);
const limitWeekly = normalizeLimitValue(user.limitWeeklyUsd);
Expand Down Expand Up @@ -222,11 +255,34 @@ export function UserKeyTableRow({
{isExpanded ? translations.collapse : translations.expand}
</span>
<span className="font-medium truncate">{user.name}</span>
{!localIsEnabled && (
<Badge variant={expiryStatus.variant} className="text-[10px] shrink-0">
{tUserStatus(expiryStatus.label)}
</Badge>
{visibleGroups.map((group) => {
const bgColor = getGroupColor(group);
return (
<Badge
key={group}
className="text-[10px] shrink-0"
style={{
backgroundColor: bgColor,
color: getContrastTextColor(bgColor),
}}
>
{group}
</Badge>
);
})}
{remainingGroupsCount > 0 && (
<Badge variant="secondary" className="text-[10px] shrink-0">
{translations.userStatus?.disabled || "Disabled"}
+{remainingGroupsCount}
</Badge>
)}
{user.tags && user.tags.length > 0 && (
<span className="text-xs text-muted-foreground truncate">
[{user.tags.join(", ")}]
</span>
)}
{user.note ? (
<span className="text-xs text-muted-foreground truncate">{user.note}</span>
) : null}
Expand Down
Loading
Loading