feat(ui): move usage display inline next to enable button

- Refactor UsageFooter to support inline mode with two-row layout
  - Row 1: Last refresh time + refresh button (right-aligned)
  - Row 2: Used + Remaining + Unit
- Update ProviderCard layout to show usage inline instead of below
- Improve text spacing: reduce gap from 1 to 0.5 for tighter label-value pairs
- Update Chinese translation: "使用" → "已使用" for better clarity
- Maintain backward compatibility with bottom display mode

This change unifies card heights and improves visual consistency
across providers with and without usage queries enabled.
This commit is contained in:
Jason
2025-11-05 22:46:30 +08:00
parent ce24b37b39
commit 4f4c1e4ed7
3 changed files with 115 additions and 20 deletions

View File

@@ -11,6 +11,7 @@ interface UsageFooterProps {
appId: AppId; appId: AppId;
usageEnabled: boolean; // 是否启用了用量查询 usageEnabled: boolean; // 是否启用了用量查询
isCurrent: boolean; // 是否为当前激活的供应商 isCurrent: boolean; // 是否为当前激活的供应商
inline?: boolean; // 是否内联显示(在按钮左侧)
} }
const UsageFooter: React.FC<UsageFooterProps> = ({ const UsageFooter: React.FC<UsageFooterProps> = ({
@@ -19,6 +20,7 @@ const UsageFooter: React.FC<UsageFooterProps> = ({
appId, appId,
usageEnabled, usageEnabled,
isCurrent, isCurrent,
inline = false,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -56,6 +58,25 @@ const UsageFooter: React.FC<UsageFooterProps> = ({
// 错误状态 // 错误状态
if (!usage.success) { if (!usage.success) {
if (inline) {
return (
<div className="flex items-center gap-2 text-xs">
<div className="flex items-center gap-1.5 text-red-500 dark:text-red-400">
<AlertCircle size={12} />
<span>{t("usage.queryFailed")}</span>
</div>
<button
onClick={() => refetch()}
disabled={loading}
className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors disabled:opacity-50 flex-shrink-0"
title={t("usage.refreshUsage")}
>
<RefreshCw size={12} className={loading ? "animate-spin" : ""} />
</button>
</div>
);
}
return ( return (
<div className="mt-3 pt-3 border-t border-border-default "> <div className="mt-3 pt-3 border-t border-border-default ">
<div className="flex items-center justify-between gap-2 text-xs"> <div className="flex items-center justify-between gap-2 text-xs">
@@ -83,6 +104,80 @@ const UsageFooter: React.FC<UsageFooterProps> = ({
// 无数据时不显示 // 无数据时不显示
if (usageDataList.length === 0) return null; if (usageDataList.length === 0) return null;
// 内联模式:仅显示第一个套餐的核心数据(分上下两行)
if (inline) {
const firstUsage = usageDataList[0];
const isExpired = firstUsage.isValid === false;
return (
<div className="flex flex-col gap-1 text-xs flex-shrink-0">
{/* 第一行:刷新时间 + 刷新按钮 */}
<div className="flex items-center gap-2 justify-end">
{/* 上次查询时间 */}
{lastQueriedAt && (
<span className="text-[10px] text-gray-400 dark:text-gray-500 flex items-center gap-1">
<Clock size={10} />
{formatRelativeTime(lastQueriedAt, now, t)}
</span>
)}
{/* 刷新按钮 */}
<button
onClick={() => refetch()}
disabled={loading}
className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors disabled:opacity-50 flex-shrink-0"
title={t("usage.refreshUsage")}
>
<RefreshCw size={12} className={loading ? "animate-spin" : ""} />
</button>
</div>
{/* 第二行:已用 + 剩余 + 单位 */}
<div className="flex items-center gap-2">
{/* 已用 */}
{firstUsage.used !== undefined && (
<div className="flex items-center gap-0.5">
<span className="text-gray-500 dark:text-gray-400">
{t("usage.used")}
</span>
<span className="tabular-nums text-gray-600 dark:text-gray-400 font-medium">
{firstUsage.used.toFixed(2)}
</span>
</div>
)}
{/* 剩余 */}
{firstUsage.remaining !== undefined && (
<div className="flex items-center gap-0.5">
<span className="text-gray-500 dark:text-gray-400">
{t("usage.remaining")}
</span>
<span
className={`font-semibold tabular-nums ${
isExpired
? "text-red-500 dark:text-red-400"
: firstUsage.remaining <
(firstUsage.total || firstUsage.remaining) * 0.1
? "text-orange-500 dark:text-orange-400"
: "text-green-600 dark:text-green-400"
}`}
>
{firstUsage.remaining.toFixed(2)}
</span>
</div>
)}
{/* 单位 */}
{firstUsage.unit && (
<span className="text-gray-500 dark:text-gray-400">
{firstUsage.unit}
</span>
)}
</div>
</div>
);
}
return ( return (
<div className="mt-3 pt-3 border-t border-border-default "> <div className="mt-3 pt-3 border-t border-border-default ">
{/* 标题行:包含刷新按钮和自动查询时间 */} {/* 标题行:包含刷新按钮和自动查询时间 */}
@@ -104,10 +199,7 @@ const UsageFooter: React.FC<UsageFooterProps> = ({
className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors disabled:opacity-50" className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors disabled:opacity-50"
title={t("usage.refreshUsage")} title={t("usage.refreshUsage")}
> >
<RefreshCw <RefreshCw size={12} className={loading ? "animate-spin" : ""} />
size={12}
className={loading ? "animate-spin" : ""}
/>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -170,22 +170,25 @@ export function ProviderCard({
</div> </div>
</div> </div>
<ProviderActions <div className="flex items-center gap-3">
isCurrent={isCurrent} <UsageFooter
onSwitch={() => onSwitch(provider)} provider={provider}
onEdit={() => onEdit(provider)} providerId={provider.id}
onConfigureUsage={() => onConfigureUsage(provider)} appId={appId}
onDelete={() => onDelete(provider)} usageEnabled={usageEnabled}
/> isCurrent={isCurrent}
</div> inline={true}
/>
<UsageFooter <ProviderActions
provider={provider} isCurrent={isCurrent}
providerId={provider.id} onSwitch={() => onSwitch(provider)}
appId={appId} onEdit={() => onEdit(provider)}
usageEnabled={usageEnabled} onConfigureUsage={() => onConfigureUsage(provider)}
isCurrent={isCurrent} onDelete={() => onDelete(provider)}
/> />
</div>
</div>
</div> </div>
); );
} }

View File

@@ -337,7 +337,7 @@
"planUsage": "套餐用量", "planUsage": "套餐用量",
"invalid": "已失效", "invalid": "已失效",
"total": "总:", "total": "总:",
"used": "使用:", "used": "使用:",
"remaining": "剩余:", "remaining": "剩余:",
"justNow": "刚刚", "justNow": "刚刚",
"minutesAgo": "{{count}} 分钟前", "minutesAgo": "{{count}} 分钟前",