Files
wr.do/components/file/storage-size.tsx
weiruchenai1 509ad15652 feat: 实现分层存储配额管理和UI优化
🆕 新增功能:
- 添加存储桶容量限制配置功能,支持按存储桶设置最大容量
- 新增存储桶使用情况API端点 (/api/storage/bucket-usage)
- 实现Plan配额和存储桶配额的分层显示

🎨 界面优化:
- 存储使用情况面板支持双层配额显示
- 上传错误提示改为右对齐显示
- Toast通知位置改为右下角
- 为存储桶配置添加帮助提示

🔧 功能改进:
- 优化上传前容量检查逻辑
- 改进错误信息显示,使用中文提示
- 智能显示更严格的配额限制
- 完善国际化支持

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-22 12:47:23 +08:00

239 lines
9.1 KiB
TypeScript

import React from "react";
import { AlertTriangle, CheckCircle, HardDrive, Folder } from "lucide-react";
import { formatFileSize } from "@/lib/utils";
export function FileSizeDisplay({ files, plan, bucketInfo, bucketUsage, t }) {
const totalSize = files?.totalSize || 0;
const maxSize = Number(plan?.stMaxTotalSize || 0);
const usagePercentage =
maxSize > 0 ? Math.min((totalSize / maxSize) * 100, 100) : 0;
// 存储桶级别的配额信息
const bucketTotalSize = bucketUsage?.usage?.totalSize || 0;
const bucketMaxSize = bucketUsage?.limits?.maxStorage || 0;
const bucketUsagePercentage =
bucketMaxSize > 0 ? Math.min((bucketTotalSize / bucketMaxSize) * 100, 100) : 0;
const hasBucketLimit = bucketMaxSize > 0;
const getStatusColor = (percentage) => {
if (percentage >= 90) return "text-red-600";
if (percentage >= 70) return "text-yellow-600";
return "text-green-600";
};
const getProgressColor = (percentage) => {
if (percentage >= 90) return "bg-red-500";
if (percentage >= 70) return "bg-yellow-500";
return "bg-blue-500";
};
const getStatusIcon = (percentage) => {
if (percentage >= 90) return <AlertTriangle className="h-4 w-4" />;
if (percentage >= 70) return <AlertTriangle className="h-4 w-4" />;
return <CheckCircle className="h-4 w-4" />;
};
return (
<div className="mx-auto w-full max-w-md p-4">
{/* 标题 */}
<div className="mb-3 flex items-center gap-2">
<HardDrive className="h-5 w-5 text-neutral-600 dark:text-neutral-200" />
<h3 className="text-sm font-medium text-neutral-700 dark:text-neutral-200">
{t("storageUsage")}
</h3>
</div>
{/* 进度条 */}
<div className="mb-3">
<div className="mb-1 flex items-center justify-between">
<span className="text-xs text-neutral-500 dark:text-neutral-300">
{t("used")}
</span>
<span className="text-xs text-neutral-500 dark:text-neutral-300">
{usagePercentage.toFixed(1)}%
</span>
</div>
<div className="h-2 w-full rounded-full bg-neutral-200 dark:bg-neutral-600">
<div
className={`h-2 rounded-full transition-all duration-300 ${getProgressColor(usagePercentage)}`}
style={{ width: `${usagePercentage}%` }}
/>
</div>
</div>
{/* Plan级别详细信息 */}
<div className="space-y-2">
<div className="flex items-center gap-2 text-xs font-medium text-neutral-500 dark:text-neutral-400">
<span>{t("planQuota")}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-neutral-600 dark:text-neutral-300">
{t("usedSpace")}:
</span>
<span className="text-sm font-medium">
{formatFileSize(totalSize, { precision: 2 })}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-neutral-600 dark:text-neutral-300">
{t("totalCapacity")}:
</span>
<span className="text-sm font-medium">
{formatFileSize(maxSize, { precision: 2 })}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-neutral-600 dark:text-neutral-300">
{t("availableSpace")}:
</span>
<span className="text-sm font-medium">
{formatFileSize(maxSize - totalSize, { precision: 2 })}
</span>
</div>
</div>
{/* 存储桶级别信息 */}
{hasBucketLimit && (
<>
<div className="my-3 border-t pt-3">
<div className="mb-2 flex items-center gap-2">
<Folder className="h-4 w-4 text-neutral-600 dark:text-neutral-200" />
<span className="text-xs font-medium text-neutral-500 dark:text-neutral-400">
{t("bucketQuota")} - {bucketInfo?.bucket}
</span>
</div>
<div className="mb-1 flex items-center justify-between">
<span className="text-xs text-neutral-500 dark:text-neutral-300">
{t("used")}
</span>
<span className="text-xs text-neutral-500 dark:text-neutral-300">
{bucketUsagePercentage.toFixed(1)}%
</span>
</div>
<div className="mb-2 h-1.5 w-full rounded-full bg-neutral-200 dark:bg-neutral-600">
<div
className={`h-1.5 rounded-full transition-all duration-300 ${getProgressColor(bucketUsagePercentage)}`}
style={{ width: `${bucketUsagePercentage}%` }}
/>
</div>
<div className="space-y-1">
<div className="flex items-center justify-between">
<span className="text-xs text-neutral-600 dark:text-neutral-300">
{t("usedSpace")}:
</span>
<span className="text-xs font-medium">
{formatFileSize(bucketTotalSize, { precision: 2 })}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-neutral-600 dark:text-neutral-300">
{t("bucketCapacity")}:
</span>
<span className="text-xs font-medium">
{formatFileSize(bucketMaxSize, { precision: 2 })}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-neutral-600 dark:text-neutral-300">
{t("availableSpace")}:
</span>
<span className="text-xs font-medium">
{formatFileSize(bucketMaxSize - bucketTotalSize, { precision: 2 })}
</span>
</div>
</div>
</div>
</>
)}
{/* 状态提示 */}
<div
className={`mt-3 flex items-center gap-2 rounded-md bg-neutral-50 p-2 dark:bg-neutral-800 ${getStatusColor(hasBucketLimit && bucketUsagePercentage > usagePercentage ? bucketUsagePercentage : usagePercentage)}`}
>
{getStatusIcon(hasBucketLimit && bucketUsagePercentage > usagePercentage ? bucketUsagePercentage : usagePercentage)}
<span className="text-xs">
{(() => {
const criticalPercentage = hasBucketLimit && bucketUsagePercentage > usagePercentage ? bucketUsagePercentage : usagePercentage;
if (criticalPercentage >= 90) {
return hasBucketLimit && bucketUsagePercentage >= 90 ? t("bucketStorageFull") : t("storageFull");
} else if (criticalPercentage >= 70) {
return hasBucketLimit && bucketUsagePercentage >= 70 ? t("bucketStorageHigh") : t("storageHigh");
} else {
return t("storageGood");
}
})()}
</span>
</div>
</div>
);
}
export function CircularStorageIndicator({ files, plan, bucketUsage, size = 32 }) {
const totalSize = files?.totalSize || 0;
const maxSize = Number(plan?.stMaxTotalSize || 0);
const usagePercentage =
maxSize > 0 ? Math.min((totalSize / maxSize) * 100, 100) : 0;
// 存储桶级别的配额信息
const bucketTotalSize = bucketUsage?.usage?.totalSize || 0;
const bucketMaxSize = bucketUsage?.limits?.maxStorage || 0;
const bucketUsagePercentage =
bucketMaxSize > 0 ? Math.min((bucketTotalSize / bucketMaxSize) * 100, 100) : 0;
const hasBucketLimit = bucketMaxSize > 0;
// 使用更严格的限制来显示
const displayPercentage = hasBucketLimit && bucketUsagePercentage > usagePercentage ? bucketUsagePercentage : usagePercentage;
// 圆形参数
const radius = (size - 6) / 2;
const circumference = 2 * Math.PI * radius;
const strokeDashoffset =
circumference - (displayPercentage / 100) * circumference;
// 根据使用率确定颜色
const getColor = (percentage) => {
if (percentage >= 90) return "#ef4444"; // red-500
if (percentage >= 70) return "#f59e0b"; // amber-500
return "#3b82f6"; // blue-500
};
return (
<div
className="relative flex cursor-pointer items-center"
style={{ width: size, height: size }}
>
<svg width={size} height={size} className="-rotate-90 transform">
{/* 背景圆圈 */}
<circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke="#e5e7eb"
strokeWidth="3"
fill="none"
/>
{/* 进度圆圈 */}
<circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke={getColor(displayPercentage)}
strokeWidth="3"
fill="none"
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
className="transition-all duration-300 ease-out"
/>
</svg>
<div
className="absolute inset-0 flex scale-[.85] items-center justify-center text-xs font-medium"
style={{ color: getColor(displayPercentage) }}
>
{Math.round(displayPercentage)}%
</div>
</div>
);
}