feat: add cloud reset time UI (#56)
* feat: show quota reset time on cloud account card * chore: ignore changelog file in prettier * style: format account card
This commit is contained in:
5 files changed
+98
-19
No files matched your search
@@ -3,6 +3,7 @@
|
|||||||
/coverage
|
/coverage
|
||||||
/.vite
|
/.vite
|
||||||
README.md
|
README.md
|
||||||
|
CHANGELOG.md
|
||||||
/src/routeTree.gen.ts
|
/src/routeTree.gen.ts
|
||||||
.agents/
|
.agents/
|
||||||
openspec/
|
openspec/
|
||||||
@@ -40,11 +40,11 @@ export const AccountCard: React.FC<AccountCardProps> = ({
|
|||||||
|
|
||||||
const initials = account.name
|
const initials = account.name
|
||||||
? account.name
|
? account.name
|
||||||
.split(' ')
|
.split(' ')
|
||||||
.map((n) => n[0])
|
.map((n) => n[0])
|
||||||
.join('')
|
.join('')
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
.slice(0, 2)
|
.slice(0, 2)
|
||||||
: account.email[0].toUpperCase();
|
: account.email[0].toUpperCase();
|
||||||
|
|
||||||
// 处理鼠标移动,计算 3D 旋转角度
|
// 处理鼠标移动,计算 3D 旋转角度
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
|
||||||
import { MoreVertical, Trash, RefreshCw, Box, Power } from 'lucide-react';
|
import { MoreVertical, Trash, RefreshCw, Box, Power } from 'lucide-react';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow, differenceInMinutes, differenceInHours, isBefore } from 'date-fns';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@@ -50,11 +50,64 @@ export function CloudAccountCard({
|
|||||||
|
|
||||||
// Helpers to get quota color
|
// Helpers to get quota color
|
||||||
const getQuotaColor = (percentage: number) => {
|
const getQuotaColor = (percentage: number) => {
|
||||||
if (percentage > 80) return 'text-green-500';
|
if (percentage > 80) {
|
||||||
if (percentage > 20) return 'text-yellow-500';
|
return 'text-green-500';
|
||||||
|
}
|
||||||
|
if (percentage > 20) {
|
||||||
|
return 'text-yellow-500';
|
||||||
|
}
|
||||||
return 'text-red-500';
|
return 'text-red-500';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatTimeRemaining = (dateStr: string) => {
|
||||||
|
const targetDate = new Date(dateStr);
|
||||||
|
if (Number.isNaN(targetDate.getTime())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
if (isBefore(targetDate, now)) {
|
||||||
|
return '0h 0m';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate difference in hours and minutes
|
||||||
|
const diffHrs = differenceInHours(targetDate, now);
|
||||||
|
const diffMins = differenceInMinutes(targetDate, now) - diffHrs * 60;
|
||||||
|
if (diffHrs >= 24) {
|
||||||
|
const diffDays = Math.floor(diffHrs / 24);
|
||||||
|
const remainingHrs = diffHrs % 24;
|
||||||
|
return `${diffDays}d ${remainingHrs}h`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${diffHrs}h ${diffMins}m`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getResetTimeLabel = (resetTime?: string) => {
|
||||||
|
if (!resetTime) {
|
||||||
|
return t('cloud.card.resetUnknown');
|
||||||
|
}
|
||||||
|
|
||||||
|
const remaining = formatTimeRemaining(resetTime);
|
||||||
|
if (!remaining) {
|
||||||
|
return t('cloud.card.resetUnknown');
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${t('cloud.card.resetPrefix')}: ${remaining}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getResetTimeTitle = (resetTime?: string) => {
|
||||||
|
if (!resetTime) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetDate = new Date(resetTime);
|
||||||
|
if (Number.isNaN(resetDate.getTime())) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${t('cloud.card.resetTime')}: ${resetDate.toLocaleString()}`;
|
||||||
|
};
|
||||||
|
|
||||||
const modelQuotas = Object.entries(account.quota?.models || {});
|
const modelQuotas = Object.entries(account.quota?.models || {});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -169,15 +222,30 @@ export function CloudAccountCard({
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{modelQuotas.length > 0 ? (
|
{modelQuotas.length > 0 ? (
|
||||||
modelQuotas.map(([modelName, info]) => (
|
modelQuotas.map(([modelName, info]) => (
|
||||||
<div key={modelName} className="flex items-center justify-between text-sm">
|
<div
|
||||||
<span className="text-muted-foreground max-w-[120px] truncate" title={modelName}>
|
key={modelName}
|
||||||
|
className="hover:bg-muted/40 grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3 rounded-md px-1.5 py-1 text-sm transition-colors"
|
||||||
|
>
|
||||||
|
<span className="text-muted-foreground min-w-0 truncate" title={modelName}>
|
||||||
{modelName.replace('models/', '')}
|
{modelName.replace('models/', '')}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-col items-end gap-1">
|
||||||
<span className={`font-mono font-bold ${getQuotaColor(info.percentage)}`}>
|
<span
|
||||||
{info.percentage}%
|
className="text-muted-foreground text-[10px] leading-none"
|
||||||
|
title={getResetTimeTitle(info.resetTime)}
|
||||||
|
>
|
||||||
|
{getResetTimeLabel(info.resetTime)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-muted-foreground text-xs">{t('cloud.card.left')}</span>
|
<div className="flex items-baseline gap-1.5">
|
||||||
|
<span
|
||||||
|
className={`font-mono leading-none font-bold ${getQuotaColor(info.percentage)}`}
|
||||||
|
>
|
||||||
|
{info.percentage}%
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground text-[10px]">
|
||||||
|
{t('cloud.card.left')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -394,7 +394,7 @@ export function CloudAccountList() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||||
{accounts?.map((account) => (
|
{accounts?.map((account) => (
|
||||||
<CloudAccountCard
|
<CloudAccountCard
|
||||||
key={account.id}
|
key={account.id}
|
||||||
|
|||||||
@@ -241,6 +241,9 @@ i18n
|
|||||||
refresh: 'Refresh Quota',
|
refresh: 'Refresh Quota',
|
||||||
delete: 'Delete Account',
|
delete: 'Delete Account',
|
||||||
noQuota: 'No quota data',
|
noQuota: 'No quota data',
|
||||||
|
resetPrefix: 'reset',
|
||||||
|
resetTime: 'Reset time',
|
||||||
|
resetUnknown: 'Unknown',
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
noAccounts: 'No cloud accounts added yet.',
|
noAccounts: 'No cloud accounts added yet.',
|
||||||
@@ -428,8 +431,7 @@ i18n
|
|||||||
title: '隐私',
|
title: '隐私',
|
||||||
description: '控制你的数据如何用于改进应用。',
|
description: '控制你的数据如何用于改进应用。',
|
||||||
error_reporting: '错误上报',
|
error_reporting: '错误上报',
|
||||||
error_reporting_desc:
|
error_reporting_desc: '发送匿名错误报告以帮助我们改进应用。不收集个人数据。',
|
||||||
'发送匿名错误报告以帮助我们改进应用。不收集个人数据。',
|
|
||||||
restart_note: '错误上报设置会在重启应用后生效。',
|
restart_note: '错误上报设置会在重启应用后生效。',
|
||||||
},
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
@@ -553,6 +555,9 @@ i18n
|
|||||||
refresh: '刷新配额',
|
refresh: '刷新配额',
|
||||||
delete: '删除账号',
|
delete: '删除账号',
|
||||||
noQuota: '无配额数据',
|
noQuota: '无配额数据',
|
||||||
|
resetPrefix: '重置',
|
||||||
|
resetTime: '重置时间',
|
||||||
|
resetUnknown: '未知',
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
noAccounts: '暂无云账号。',
|
noAccounts: '暂无云账号。',
|
||||||
@@ -688,7 +693,8 @@ i18n
|
|||||||
description: 'Управление вашими аккаунтами Antigravity Google Gemini.',
|
description: 'Управление вашими аккаунтами Antigravity Google Gemini.',
|
||||||
noBackups: {
|
noBackups: {
|
||||||
title: 'Бэкапы не найдены',
|
title: 'Бэкапы не найдены',
|
||||||
description: 'Создайте резервную копию вашего текущего аккаунта Antigravity, чтобы начать.',
|
description:
|
||||||
|
'Создайте резервную копию вашего текущего аккаунта Antigravity, чтобы начать.',
|
||||||
action: 'Создать бэкап текущего аккаунта',
|
action: 'Создать бэкап текущего аккаунта',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -741,7 +747,8 @@ i18n
|
|||||||
error_reporting: 'Отчеты об ошибках',
|
error_reporting: 'Отчеты об ошибках',
|
||||||
error_reporting_desc:
|
error_reporting_desc:
|
||||||
'Отправляйте анонимные отчеты об ошибках, чтобы помочь нам улучшить приложение. Личные данные не собираются.',
|
'Отправляйте анонимные отчеты об ошибках, чтобы помочь нам улучшить приложение. Личные данные не собираются.',
|
||||||
restart_note: 'Изменения в отчетах об ошибках вступят в силу после перезапуска приложения.',
|
restart_note:
|
||||||
|
'Изменения в отчетах об ошибках вступят в силу после перезапуска приложения.',
|
||||||
},
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
title: 'Верхнеуровневый прокси',
|
title: 'Верхнеуровневый прокси',
|
||||||
@@ -865,6 +872,9 @@ i18n
|
|||||||
refresh: 'Обновить квоту',
|
refresh: 'Обновить квоту',
|
||||||
delete: 'Удалить аккаунт',
|
delete: 'Удалить аккаунт',
|
||||||
noQuota: 'Нет данных',
|
noQuota: 'Нет данных',
|
||||||
|
resetPrefix: 'Сброс',
|
||||||
|
resetTime: 'Время сброса',
|
||||||
|
resetUnknown: 'Неизвестно',
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
noAccounts: 'Нет добавленных облачных аккаунтов.',
|
noAccounts: 'Нет добавленных облачных аккаунтов.',
|
||||||
|
|||||||
Reference in new issue
Block a user