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:
vin authored and GitHub committed 2026-02-01 18:03:27 +08:00
1 parent 6328378da5
commit f6f8069ce4
5 files changed
+98 -19

No files matched your search

+1
View File
@@ -3,6 +3,7 @@
/coverage
/.vite
README.md
CHANGELOG.md
/src/routeTree.gen.ts
.agents/
openspec/
+5 -5
View File
@@ -40,11 +40,11 @@ export const AccountCard: React.FC<AccountCardProps> = ({
const initials = account.name
? account.name
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()
.slice(0, 2)
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()
.slice(0, 2)
: account.email[0].toUpperCase();
// 处理鼠标移动,计算 3D 旋转角度
+77 -9
View File
@@ -19,7 +19,7 @@ import {
import { Checkbox } from '@/components/ui/checkbox';
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 { useTranslation } from 'react-i18next';
@@ -50,11 +50,64 @@ export function CloudAccountCard({
// Helpers to get quota color
const getQuotaColor = (percentage: number) => {
if (percentage > 80) return 'text-green-500';
if (percentage > 20) return 'text-yellow-500';
if (percentage > 80) {
return 'text-green-500';
}
if (percentage > 20) {
return 'text-yellow-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 || {});
return (
@@ -169,15 +222,30 @@ export function CloudAccountCard({
<div className="space-y-2">
{modelQuotas.length > 0 ? (
modelQuotas.map(([modelName, info]) => (
<div key={modelName} className="flex items-center justify-between text-sm">
<span className="text-muted-foreground max-w-[120px] truncate" title={modelName}>
<div
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/', '')}
</span>
<div className="flex items-center gap-2">
<span className={`font-mono font-bold ${getQuotaColor(info.percentage)}`}>
{info.percentage}%
<div className="flex flex-col items-end gap-1">
<span
className="text-muted-foreground text-[10px] leading-none"
title={getResetTimeTitle(info.resetTime)}
>
{getResetTimeLabel(info.resetTime)}
</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>
))
+1 -1
View File
@@ -394,7 +394,7 @@ export function CloudAccountList() {
</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) => (
<CloudAccountCard
key={account.id}
+14 -4
View File
@@ -241,6 +241,9 @@ i18n
refresh: 'Refresh Quota',
delete: 'Delete Account',
noQuota: 'No quota data',
resetPrefix: 'reset',
resetTime: 'Reset time',
resetUnknown: 'Unknown',
},
list: {
noAccounts: 'No cloud accounts added yet.',
@@ -428,8 +431,7 @@ i18n
title: '隐私',
description: '控制你的数据如何用于改进应用。',
error_reporting: '错误上报',
error_reporting_desc:
'发送匿名错误报告以帮助我们改进应用。不收集个人数据。',
error_reporting_desc: '发送匿名错误报告以帮助我们改进应用。不收集个人数据。',
restart_note: '错误上报设置会在重启应用后生效。',
},
proxy: {
@@ -553,6 +555,9 @@ i18n
refresh: '刷新配额',
delete: '删除账号',
noQuota: '无配额数据',
resetPrefix: '重置',
resetTime: '重置时间',
resetUnknown: '未知',
},
list: {
noAccounts: '暂无云账号。',
@@ -688,7 +693,8 @@ i18n
description: 'Управление вашими аккаунтами Antigravity Google Gemini.',
noBackups: {
title: 'Бэкапы не найдены',
description: 'Создайте резервную копию вашего текущего аккаунта Antigravity, чтобы начать.',
description:
'Создайте резервную копию вашего текущего аккаунта Antigravity, чтобы начать.',
action: 'Создать бэкап текущего аккаунта',
},
},
@@ -741,7 +747,8 @@ i18n
error_reporting: 'Отчеты об ошибках',
error_reporting_desc:
'Отправляйте анонимные отчеты об ошибках, чтобы помочь нам улучшить приложение. Личные данные не собираются.',
restart_note: 'Изменения в отчетах об ошибках вступят в силу после перезапуска приложения.',
restart_note:
'Изменения в отчетах об ошибках вступят в силу после перезапуска приложения.',
},
proxy: {
title: 'Верхнеуровневый прокси',
@@ -865,6 +872,9 @@ i18n
refresh: 'Обновить квоту',
delete: 'Удалить аккаунт',
noQuota: 'Нет данных',
resetPrefix: 'Сброс',
resetTime: 'Время сброса',
resetUnknown: 'Неизвестно',
},
list: {
noAccounts: 'Нет добавленных облачных аккаунтов.',