diff --git a/frontend/src/components/account/ClaudeResetCreditsCell.vue b/frontend/src/components/account/ClaudeResetCreditsCell.vue index 9f249e0700..34cb9b6b3f 100644 --- a/frontend/src/components/account/ClaudeResetCreditsCell.vue +++ b/frontend/src/components/account/ClaudeResetCreditsCell.vue @@ -191,11 +191,21 @@ const creditTitle = computed(() => { if (!credit) return '' const lines = [credit.label] if (credit.expires_at) lines.push(t('admin.accounts.claudeResetCredits.expiresAtFull', { time: formatTime(credit.expires_at, 'full') })) - if (credit.clears.length) lines.push(t('admin.accounts.claudeResetCredits.clears', { windows: credit.clears.join(', ') })) + if (credit.clears.length) lines.push(t('admin.accounts.claudeResetCredits.clears', { windows: windowLabels(credit.clears) })) if (credit.use_requires_limit) lines.push(t('admin.accounts.claudeResetCredits.requiresLimit')) return lines.join('\n') }) +// 已知窗口显示为可读名称,未知窗口原样显示 +const windowKeys: Record = { + five_hour: 'fiveHour', + seven_day: 'sevenDay', + seven_day_overage_included: 'sevenDayOverage' +} +function windowLabels(windows?: string[]): string { + return (windows ?? []).map(w => windowKeys[w] ? t(`admin.accounts.claudeResetCredits.windows.${windowKeys[w]}`) : w).join(', ') +} + const countButtonTitle = computed(() => { if (!status.value) return t('admin.accounts.claudeResetCredits.countTooltipLoad') return [ @@ -215,8 +225,8 @@ const redeemButtonTitle = computed(() => { const confirmMessage = computed(() => { const next = status.value?.credits.find(c => c.redeemable) return t('admin.accounts.claudeResetCredits.confirmMessage', { - windows: next?.clears.join(', ') || '—', - count: totalResets.value + windows: windowLabels(next?.clears) || '—', + count: Math.max(totalResets.value - 1, 0) }) }) @@ -242,7 +252,7 @@ function outcomeFeedback(result: ClaudeResetOutcome): { kind: 'success' | 'warni const key = 'admin.accounts.claudeResetCredits.outcome' switch (result.outcome) { case 'reset': - return { kind: 'success', text: t(`${key}.reset`, { windows: result.cleared?.join(', ') || '—' }) } + return { kind: 'success', text: t(`${key}.reset`, { windows: windowLabels(result.cleared) || '—' }) } case 'already_used': return { kind: 'warning', text: t(`${key}.alreadyUsed`) } case 'cooldown': diff --git a/frontend/src/components/account/__tests__/ClaudeResetCreditsCell.spec.ts b/frontend/src/components/account/__tests__/ClaudeResetCreditsCell.spec.ts index 1afef83907..3011722373 100644 --- a/frontend/src/components/account/__tests__/ClaudeResetCreditsCell.spec.ts +++ b/frontend/src/components/account/__tests__/ClaudeResetCreditsCell.spec.ts @@ -5,7 +5,8 @@ import type { Account } from '@/types' const getCredits = vi.hoisted(() => vi.fn()) const redeem = vi.hoisted(() => vi.fn()) vi.mock('@/api/admin/claudeResetCredits', () => ({ getClaudeResetCredits: getCredits, redeemClaudeResetCredit: redeem })) -vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key }) })) +const t = vi.hoisted(() => vi.fn((key: string, _params?: Record) => key)) +vi.mock('vue-i18n', () => ({ useI18n: () => ({ t }) })) // Minimal stand-in exposing the dialog's show state and confirm/cancel events. vi.mock('@/components/common/ConfirmDialog.vue', () => ({ default: { @@ -104,6 +105,11 @@ describe('Claude reset credit status', () => { const dialog = wrapper.get('[data-testid="confirm-dialog"]') expect(dialog.attributes('data-danger')).toBe('true') expect(dialog.text()).toContain('admin.accounts.claudeResetCredits.confirmMessage') + // Known windows render as readable labels; count is what remains after this use. + expect(t).toHaveBeenCalledWith('admin.accounts.claudeResetCredits.confirmMessage', { + windows: 'admin.accounts.claudeResetCredits.windows.fiveHour, admin.accounts.claudeResetCredits.windows.sevenDay', + count: 0 + }) await wrapper.get('[data-testid="confirm-cancel"]').trigger('click') await flushPromises() expect(wrapper.find('[data-testid="confirm-dialog"]').exists()).toBe(false) @@ -193,6 +199,15 @@ describe('Claude reset credit status', () => { expect(redeem.mock.calls[1][1]).toBe(redeem.mock.calls[0][1]) }) + it('labels cleared windows and falls back to the raw key for unknown ones', async () => { + const wrapper = await queried() + redeem.mockResolvedValue({ outcome: 'reset', cleared: ['seven_day_overage_included', 'future_window'], replayed: false }) + await confirmReset(wrapper) + expect(t).toHaveBeenCalledWith('admin.accounts.claudeResetCredits.outcome.reset', { + windows: 'admin.accounts.claudeResetCredits.windows.sevenDayOverage, future_window' + }) + }) + it('tells the operator to retry later after an explicit unavailable answer', async () => { const wrapper = await queried() redeem.mockResolvedValue({ outcome: 'unknown', reason: 'upstream_unavailable', replayed: false }) diff --git a/frontend/src/i18n/locales/en/admin/accounts.ts b/frontend/src/i18n/locales/en/admin/accounts.ts index f820f4cab7..8ed2917679 100644 --- a/frontend/src/i18n/locales/en/admin/accounts.ts +++ b/frontend/src/i18n/locales/en/admin/accounts.ts @@ -943,8 +943,13 @@ export default { resetTooltipNeedQuery: 'Check the count first; reset is available once a usable credit is found', resetTooltipNone: 'No reset can be used right now', resetTooltipReady: 'Consume 1 reset to clear limit windows (asks for confirmation)', - confirmTitle: 'Use a Claude reset', - confirmMessage: 'This immediately consumes 1 reset and cannot be undone. Windows cleared: {windows}. Resets held: {count}. Continue?', + confirmTitle: 'Confirm Claude Reset', + confirmMessage: 'This will consume 1 reset credit to immediately restore the {windows} window(s) ({count} remaining). This action cannot be undone. Continue?', + windows: { + fiveHour: '5h', + sevenDay: '7d', + sevenDayOverage: '7d overage' + }, outcome: { reset: 'Reset applied; cleared: {windows}', alreadyUsed: 'This reset was already used; refreshing to confirm', diff --git a/frontend/src/i18n/locales/zh/admin/accounts.ts b/frontend/src/i18n/locales/zh/admin/accounts.ts index 1fc9f3412b..654ba20c3c 100644 --- a/frontend/src/i18n/locales/zh/admin/accounts.ts +++ b/frontend/src/i18n/locales/zh/admin/accounts.ts @@ -1044,8 +1044,13 @@ export default { resetTooltipNeedQuery: '请先点「次数」查询;查询到可用的重置后才能使用', resetTooltipNone: '当前没有可立即使用的重置', resetTooltipReady: '消耗 1 次重置,清除限额窗口(需确认)', - confirmTitle: '使用 Claude 重置', - confirmMessage: '将立即消耗 1 次重置,此操作不可撤销。将清除窗口:{windows}。当前持有 {count} 次。确定继续?', + confirmTitle: '确认使用 Claude 重置', + confirmMessage: '将消耗 1 次重置次数,立即恢复 {windows} 窗口,剩余 {count} 次。此操作不可撤销,确定继续吗?', + windows: { + fiveHour: '5h', + sevenDay: '7d', + sevenDayOverage: '7d 超额' + }, outcome: { reset: '重置成功,已清除:{windows}', alreadyUsed: '该重置已被使用,正在刷新确认',