|
| 1 | +import { WarningOutlined } from '@ant-design/icons'; |
| 2 | +import { Alert, Progress, Tag, Tooltip, Typography } from 'antd'; |
| 3 | +import dayjs from 'dayjs'; |
| 4 | +import { quotas } from '@/constants/quotas'; |
| 5 | +import { useUserInfo } from '@/utils/hooks'; |
| 6 | + |
| 7 | +const { Text } = Typography; |
| 8 | + |
| 9 | +interface DailyCheckQuotaProps { |
| 10 | + variant: 'account'; |
| 11 | +} |
| 12 | + |
| 13 | +const useDailyCheckQuotaState = () => { |
| 14 | + const { user } = useUserInfo(); |
| 15 | + const quota = user |
| 16 | + ? (user.quota ?? quotas[user.tier as keyof typeof quotas]) |
| 17 | + : undefined; |
| 18 | + const dailyQuota = quota?.pv; |
| 19 | + const remaining = user?.checkQuota; |
| 20 | + const hasData = !!dailyQuota && typeof remaining === 'number'; |
| 21 | + |
| 22 | + const remainingRatio = hasData ? remaining / dailyQuota : 0; |
| 23 | + const percent = Math.max(0, Math.min(100, remainingRatio * 100)); |
| 24 | + const isExceeded = hasData && remaining <= 0; |
| 25 | + const isLow = !isExceeded && remainingRatio <= 0.2; |
| 26 | + const status: 'exception' | 'normal' = |
| 27 | + isExceeded || isLow ? 'exception' : 'normal'; |
| 28 | + const tooltip = ( |
| 29 | + <div className="text-xs"> |
| 30 | + {user && ( |
| 31 | + <> |
| 32 | + <div> |
| 33 | + 套餐: |
| 34 | + {quota?.title ?? |
| 35 | + quotas[user.tier as keyof typeof quotas]?.title ?? |
| 36 | + user.tier} |
| 37 | + </div> |
| 38 | + <div> |
| 39 | + 到期: |
| 40 | + {user.tierExpiresAt |
| 41 | + ? dayjs(user.tierExpiresAt).format('YYYY-MM-DD') |
| 42 | + : '无'} |
| 43 | + </div> |
| 44 | + </> |
| 45 | + )} |
| 46 | + {hasData && ( |
| 47 | + <> |
| 48 | + <div> |
| 49 | + 今日剩余额度:{Math.max(0, remaining).toLocaleString()} /{' '} |
| 50 | + {dailyQuota.toLocaleString()} 次 |
| 51 | + </div> |
| 52 | + {remaining < 0 && ( |
| 53 | + <div>已超出:{Math.abs(remaining).toLocaleString()} 次</div> |
| 54 | + )} |
| 55 | + </> |
| 56 | + )} |
| 57 | + {user?.last7dAvg !== undefined && ( |
| 58 | + <div>7 日平均剩余额度:{user.last7dAvg.toLocaleString()} 次</div> |
| 59 | + )} |
| 60 | + </div> |
| 61 | + ); |
| 62 | + |
| 63 | + return { |
| 64 | + hasData, |
| 65 | + isExceeded, |
| 66 | + isLow, |
| 67 | + percent, |
| 68 | + status, |
| 69 | + tooltip, |
| 70 | + user, |
| 71 | + }; |
| 72 | +}; |
| 73 | + |
| 74 | +export function DailyCheckQuotaUserTrigger({ |
| 75 | + compact = false, |
| 76 | + showPlanDetails = false, |
| 77 | + userName, |
| 78 | +}: { |
| 79 | + compact?: boolean; |
| 80 | + showPlanDetails?: boolean; |
| 81 | + userName: string; |
| 82 | +}) { |
| 83 | + const quotaState = useDailyCheckQuotaState(); |
| 84 | + const { user } = quotaState; |
| 85 | + const strokeColor = quotaState.isExceeded |
| 86 | + ? '#ef4444' |
| 87 | + : quotaState.isLow |
| 88 | + ? '#f59e0b' |
| 89 | + : '#2563eb'; |
| 90 | + const tierTitle = user |
| 91 | + ? (user.quota?.title ?? |
| 92 | + quotas[user.tier as keyof typeof quotas]?.title ?? |
| 93 | + user.tier) |
| 94 | + : ''; |
| 95 | + const expireLabel = user?.tierExpiresAt |
| 96 | + ? `${dayjs(user.tierExpiresAt).format('YYYY-MM-DD')} 到期` |
| 97 | + : '无到期'; |
| 98 | + const warningIcon = (quotaState.isExceeded || quotaState.isLow) && ( |
| 99 | + <WarningOutlined |
| 100 | + className={`shrink-0 ${ |
| 101 | + quotaState.isExceeded ? 'text-red-500' : 'text-amber-500' |
| 102 | + }`} |
| 103 | + /> |
| 104 | + ); |
| 105 | + const progress = quotaState.hasData && ( |
| 106 | + <Progress |
| 107 | + className="mt-0.5" |
| 108 | + percent={quotaState.percent} |
| 109 | + showInfo={false} |
| 110 | + size="small" |
| 111 | + status={quotaState.status} |
| 112 | + strokeColor={strokeColor} |
| 113 | + trailColor="#e5e7eb" |
| 114 | + /> |
| 115 | + ); |
| 116 | + const content = compact ? ( |
| 117 | + <span className="flex h-10 w-16 min-w-0 flex-col justify-center text-left"> |
| 118 | + <span className="flex min-w-0 items-center gap-1"> |
| 119 | + <span className="truncate font-medium text-[11px] text-slate-600 leading-4"> |
| 120 | + {userName} |
| 121 | + </span> |
| 122 | + {warningIcon} |
| 123 | + </span> |
| 124 | + {progress} |
| 125 | + </span> |
| 126 | + ) : ( |
| 127 | + <span className="flex min-w-0 items-center gap-2.5 text-left"> |
| 128 | + <span className="min-w-0 flex-1"> |
| 129 | + <span className="block truncate font-medium text-slate-800 text-sm leading-5"> |
| 130 | + {userName} |
| 131 | + </span> |
| 132 | + {showPlanDetails && user && ( |
| 133 | + <span className="block truncate text-[11px] text-slate-500 leading-4"> |
| 134 | + {tierTitle} · {expireLabel} |
| 135 | + </span> |
| 136 | + )} |
| 137 | + {progress} |
| 138 | + </span> |
| 139 | + {warningIcon} |
| 140 | + </span> |
| 141 | + ); |
| 142 | + |
| 143 | + if (!quotaState.hasData) { |
| 144 | + return content; |
| 145 | + } |
| 146 | + |
| 147 | + return <Tooltip title={quotaState.tooltip}>{content}</Tooltip>; |
| 148 | +} |
| 149 | + |
| 150 | +export default function DailyCheckQuota(_props: DailyCheckQuotaProps) { |
| 151 | + const quotaState = useDailyCheckQuotaState(); |
| 152 | + const { user } = quotaState; |
| 153 | + if (!user) { |
| 154 | + return null; |
| 155 | + } |
| 156 | + |
| 157 | + if (!quotaState.hasData) { |
| 158 | + return ( |
| 159 | + <Text type="secondary"> |
| 160 | + 暂无今日检查额度数据。额度用于客户端查询是否有可用热更新,按账户下所有应用汇总。 |
| 161 | + </Text> |
| 162 | + ); |
| 163 | + } |
| 164 | + |
| 165 | + const message = quotaState.isExceeded |
| 166 | + ? '今日检查额度已用尽,请升级套餐或等待每日额度重置。' |
| 167 | + : quotaState.isLow |
| 168 | + ? '今日检查额度偏低,继续发布或大量客户端查询时需要留意。' |
| 169 | + : '今日检查额度状态正常。'; |
| 170 | + |
| 171 | + return ( |
| 172 | + <div className="space-y-3"> |
| 173 | + <div className="rounded-lg border border-slate-200 bg-slate-50 p-4"> |
| 174 | + <div className="mb-2 flex flex-wrap items-center justify-between gap-2"> |
| 175 | + <div> |
| 176 | + <div className="font-medium">每日检查额度</div> |
| 177 | + <div className="text-gray-500 text-sm"> |
| 178 | + 客户端检查热更新时消耗,按账户下所有应用合并计算,每日重置。 |
| 179 | + </div> |
| 180 | + </div> |
| 181 | + <Tag |
| 182 | + color={ |
| 183 | + quotaState.isExceeded |
| 184 | + ? 'red' |
| 185 | + : quotaState.isLow |
| 186 | + ? 'orange' |
| 187 | + : 'green' |
| 188 | + } |
| 189 | + > |
| 190 | + {quotaState.isExceeded |
| 191 | + ? '已用尽' |
| 192 | + : quotaState.isLow |
| 193 | + ? '偏低' |
| 194 | + : '正常'} |
| 195 | + </Tag> |
| 196 | + </div> |
| 197 | + <Tooltip title={quotaState.tooltip}> |
| 198 | + <Progress |
| 199 | + percent={quotaState.percent} |
| 200 | + showInfo={false} |
| 201 | + status={quotaState.status} |
| 202 | + strokeLinecap="round" |
| 203 | + /> |
| 204 | + </Tooltip> |
| 205 | + <div className="mt-2 text-gray-500 text-xs"> |
| 206 | + 页面不直接展示具体次数;鼠标悬停进度条可查看剩余额度、套餐上限和 7 |
| 207 | + 日平均。 |
| 208 | + </div> |
| 209 | + </div> |
| 210 | + {(quotaState.isExceeded || quotaState.isLow) && ( |
| 211 | + <Alert |
| 212 | + showIcon |
| 213 | + type={quotaState.isExceeded ? 'error' : 'warning'} |
| 214 | + message={message} |
| 215 | + /> |
| 216 | + )} |
| 217 | + </div> |
| 218 | + ); |
| 219 | +} |
0 commit comments