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
|
||||
/.vite
|
||||
README.md
|
||||
CHANGELOG.md
|
||||
/src/routeTree.gen.ts
|
||||
.agents/
|
||||
openspec/
|
||||
@@ -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 旋转角度
|
||||
|
||||
@@ -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>
|
||||
))
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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: 'Нет добавленных облачных аккаунтов.',
|
||||
|
||||
Reference in new issue
Block a user