fix(admin): set user image quota by total instead of delta

Operators expect direct total capacity like space quota; the delta-based
grant UI caused wrong results when entering a target amount on production.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-05 16:07:49 +08:00
parent f8317e8312
commit eca1aa635e
+53 -16
View File
@@ -25,7 +25,8 @@ export function UserDetailPage() {
const [spaceQuotaMb, setSpaceQuotaMb] = useState('5'); const [spaceQuotaMb, setSpaceQuotaMb] = useState('5');
const [imageQuota, setImageQuota] = useState<ImageQuotaView | null>(null); const [imageQuota, setImageQuota] = useState<ImageQuotaView | null>(null);
const [imageQuotaLoading, setImageQuotaLoading] = useState(false); const [imageQuotaLoading, setImageQuotaLoading] = useState(false);
const [imageGrant, setImageGrant] = useState({ delta: '', note: '' }); const [imageTotal, setImageTotal] = useState('');
const [imageGrantNote, setImageGrantNote] = useState('');
useEffect(() => { useEffect(() => {
if (!user?.spaceQuotaBytes) return; if (!user?.spaceQuotaBytes) return;
@@ -82,6 +83,14 @@ export function UserDetailPage() {
}; };
}, [user?.id, user?.role]); }, [user?.id, user?.role]);
useEffect(() => {
if (!imageQuota || imageQuota.unlimited || imageQuota.total == null) {
setImageTotal('');
return;
}
setImageTotal(String(imageQuota.total));
}, [imageQuota?.total, imageQuota?.unlimited]);
const handleRecharge = async (e: React.FormEvent) => { const handleRecharge = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!user) return; if (!user) return;
@@ -100,24 +109,42 @@ export function UserDetailPage() {
} }
}; };
const handleImageGrant = async (e: React.FormEvent) => { const handleImageQuotaSave = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!user) return; if (!user) return;
setMessage(null); setMessage(null);
setLocalError(null); setLocalError(null);
setError(null); setError(null);
const delta = Math.floor(Number(imageGrant.delta)); if (!imageQuota) {
if (!Number.isFinite(delta) || delta === 0) { setLocalError('暂无额度信息,用户可能尚无有效订阅');
setLocalError('请输入非零整数额度'); return;
}
if (imageQuota.unlimited) {
setLocalError('当前为无限额度套餐,无法在此调整');
return;
}
const targetTotal = Math.floor(Number(imageTotal));
if (!Number.isFinite(targetTotal) || targetTotal < 0) {
setLocalError('请输入非负整数总额度');
return;
}
if (targetTotal < imageQuota.used) {
setLocalError(`总额度不能低于已用量(${imageQuota.used} 张)`);
return;
}
const targetBonus = targetTotal - imageQuota.limit;
const delta = targetBonus - imageQuota.bonus;
if (delta === 0) {
setMessage('额度未变化');
return; return;
} }
try { try {
const result = await grantUserImageQuota(user.id, delta, imageGrant.note.trim()); const result = await grantUserImageQuota(user.id, delta, imageGrantNote.trim());
setImageQuota(result.quota); setImageQuota(result.quota);
setMessage('图片额度已更新'); setMessage('图片额度已更新');
setImageGrant({ delta: '', note: '' }); setImageGrantNote('');
} catch (err) { } catch (err) {
setLocalError(err instanceof Error ? err.message : '图片额度调整失败'); setLocalError(err instanceof Error ? err.message : '图片额度设置失败');
} }
}; };
@@ -245,21 +272,31 @@ export function UserDetailPage() {
) : ( ) : (
<p className="muted">{imageQuotaSummary || '暂无额度信息(用户可能尚无订阅)'}</p> <p className="muted">{imageQuotaSummary || '暂无额度信息(用户可能尚无订阅)'}</p>
)} )}
<form className="admin-form" onSubmit={handleImageGrant}> <p className="muted">
+
</p>
<form className="admin-form" onSubmit={handleImageQuotaSave}>
<input <input
placeholder="调整额度(张,正数充值、负数扣减" placeholder="图片总额度(张)"
type="number" type="number"
min="0"
step="1" step="1"
value={imageGrant.delta} value={imageTotal}
onChange={(e) => setImageGrant((s) => ({ ...s, delta: e.target.value }))} onChange={(e) => setImageTotal(e.target.value)}
disabled={!imageQuota || imageQuota.unlimited}
/> />
<input <input
placeholder="备注" placeholder="备注"
value={imageGrant.note} value={imageGrantNote}
onChange={(e) => setImageGrant((s) => ({ ...s, note: e.target.value }))} onChange={(e) => setImageGrantNote(e.target.value)}
disabled={!imageQuota || imageQuota.unlimited}
/> />
<button type="submit" className="send-btn"> <button
type="submit"
className="send-btn"
disabled={!imageQuota || imageQuota.unlimited}
>
</button> </button>
</form> </form>
</section> </section>