fix(accounts): readable window labels in Claude reset confirm (review R1-4)

- Map five_hour/seven_day/seven_day_overage_included to 5h/7d/7d overage
  (raw key fallback) in the confirm message, credit tooltip and success message.
- Align zh/en confirm copy with the Codex dialog; {count} is the count
  remaining after this use.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
William Drake
2026-09-29 14:24:19 -04:00
co-authored by Claude Opus 5.5
parent dc2efc6b9a
commit 514ad0aa8e
4 changed files with 44 additions and 9 deletions
@@ -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<string, string> = {
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':
@@ -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<string, unknown>) => 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 })
@@ -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',
@@ -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: '该重置已被使用,正在刷新确认',