SubUsageSummary.tsx 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. import { useMemo } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { Progress, Tag } from 'antd';
  4. import { ClockCircleOutlined, ThunderboltOutlined } from '@ant-design/icons';
  5. import './SubUsageSummary.css';
  6. interface SubUsageSummaryProps {
  7. usedByte: number;
  8. totalByte: number;
  9. usedLabel: string;
  10. totalLabel: string;
  11. remainedLabel: string;
  12. expireMs: number;
  13. isActive: boolean;
  14. }
  15. function pickStrokeColor(pct: number): { from: string; to: string } {
  16. if (pct >= 90) return { from: '#ff7875', to: '#ff4d4f' };
  17. if (pct >= 75) return { from: '#ffc53d', to: '#fa8c16' };
  18. return { from: '#5fc983', to: '#36b37e' };
  19. }
  20. function formatExpiryChip(expireMs: number): { label: string; color: string } | null {
  21. if (expireMs <= 0) return null;
  22. const diff = expireMs - Date.now();
  23. if (diff <= 0) return { label: 'Expired', color: 'red' };
  24. const days = Math.floor(diff / 86400000);
  25. if (days >= 1) return { label: `${days}d`, color: days <= 3 ? 'orange' : 'blue' };
  26. const hours = Math.max(1, Math.floor(diff / 3600000));
  27. return { label: `${hours}h`, color: 'orange' };
  28. }
  29. export default function SubUsageSummary({
  30. usedByte,
  31. totalByte,
  32. usedLabel,
  33. totalLabel,
  34. remainedLabel,
  35. expireMs,
  36. isActive,
  37. }: SubUsageSummaryProps) {
  38. const { t } = useTranslation();
  39. const pct = useMemo(() => {
  40. if (totalByte <= 0) return 0;
  41. const v = (usedByte / totalByte) * 100;
  42. if (!Number.isFinite(v)) return 0;
  43. return Math.max(0, Math.min(100, v));
  44. }, [usedByte, totalByte]);
  45. const expiry = formatExpiryChip(expireMs);
  46. const isUnlimited = totalByte <= 0;
  47. const stroke = pickStrokeColor(pct);
  48. return (
  49. <div className={`usage-summary ${!isActive ? 'is-inactive' : ''}`}>
  50. <div className="usage-summary-head">
  51. <div className="usage-summary-labels">
  52. <span className="usage-summary-used">{usedLabel}</span>
  53. <span className="usage-summary-sep">/</span>
  54. <span className="usage-summary-total">{isUnlimited ? '∞' : totalLabel}</span>
  55. </div>
  56. <div className="usage-summary-chips">
  57. {isUnlimited && (
  58. <Tag color="purple" icon={<ThunderboltOutlined />}>
  59. {t('subscription.unlimited')}
  60. </Tag>
  61. )}
  62. {expiry && (
  63. <Tag color={expiry.color} icon={<ClockCircleOutlined />}>
  64. {expiry.label}
  65. </Tag>
  66. )}
  67. </div>
  68. </div>
  69. {!isUnlimited && (
  70. <Progress
  71. percent={pct}
  72. showInfo={false}
  73. strokeColor={{ '0%': stroke.from, '100%': stroke.to }}
  74. trailColor="var(--ant-color-fill-secondary)"
  75. strokeWidth={10}
  76. className="usage-summary-bar"
  77. />
  78. )}
  79. <div className="usage-summary-foot">
  80. {!isUnlimited && (
  81. <>
  82. <span className="usage-summary-remained">{remainedLabel}</span>
  83. <span className="usage-summary-pct">{pct.toFixed(1)}%</span>
  84. </>
  85. )}
  86. </div>
  87. </div>
  88. );
  89. }