Commit 8640a623 authored by Ethan0x0000's avatar Ethan0x0000
Browse files

refactor: extract formatCompactNumber util and add last_used_at to refresh key

- Add formatCompactNumber() for consistent large-number formatting (K/M/B)
- Include last_used_at in OpenAI usage refresh key for better change detection
- Add .gitattributes eol=lf rules for frontend source files
parent fa782e70
...@@ -4,6 +4,13 @@ backend/migrations/*.sql text eol=lf ...@@ -4,6 +4,13 @@ backend/migrations/*.sql text eol=lf
# Go 源代码文件 # Go 源代码文件
*.go text eol=lf *.go text eol=lf
# 前端 源代码文件
*.ts text eol=lf
*.tsx text eol=lf
*.js text eol=lf
*.jsx text eol=lf
*.vue text eol=lf
# Shell 脚本 # Shell 脚本
*.sh text eol=lf *.sh text eol=lf
......
...@@ -8,6 +8,7 @@ describe('buildOpenAIUsageRefreshKey', () => { ...@@ -8,6 +8,7 @@ describe('buildOpenAIUsageRefreshKey', () => {
platform: 'openai', platform: 'openai',
type: 'oauth', type: 'oauth',
updated_at: '2026-03-07T10:00:00Z', updated_at: '2026-03-07T10:00:00Z',
last_used_at: '2026-03-07T09:59:00Z',
extra: { extra: {
codex_usage_updated_at: '2026-03-07T10:00:00Z', codex_usage_updated_at: '2026-03-07T10:00:00Z',
codex_5h_used_percent: 0, codex_5h_used_percent: 0,
...@@ -27,12 +28,35 @@ describe('buildOpenAIUsageRefreshKey', () => { ...@@ -27,12 +28,35 @@ describe('buildOpenAIUsageRefreshKey', () => {
expect(buildOpenAIUsageRefreshKey(base)).not.toBe(buildOpenAIUsageRefreshKey(next)) expect(buildOpenAIUsageRefreshKey(base)).not.toBe(buildOpenAIUsageRefreshKey(next))
}) })
it('会在 last_used_at 变化时生成不同 key', () => {
const base = {
id: 3,
platform: 'openai',
type: 'oauth',
updated_at: '2026-03-07T10:00:00Z',
last_used_at: '2026-03-07T10:00:00Z',
extra: {
codex_usage_updated_at: '2026-03-07T10:00:00Z',
codex_5h_used_percent: 12,
codex_7d_used_percent: 24
}
} as any
const next = {
...base,
last_used_at: '2026-03-07T10:02:00Z'
}
expect(buildOpenAIUsageRefreshKey(base)).not.toBe(buildOpenAIUsageRefreshKey(next))
})
it('非 OpenAI OAuth 账号返回空 key', () => { it('非 OpenAI OAuth 账号返回空 key', () => {
expect(buildOpenAIUsageRefreshKey({ expect(buildOpenAIUsageRefreshKey({
id: 2, id: 2,
platform: 'anthropic', platform: 'anthropic',
type: 'oauth', type: 'oauth',
updated_at: '2026-03-07T10:00:00Z', updated_at: '2026-03-07T10:00:00Z',
last_used_at: '2026-03-07T10:00:00Z',
extra: {} extra: {}
} as any)).toBe('') } as any)).toBe('')
}) })
......
import { describe, expect, it } from 'vitest'
import { formatCompactNumber } from '../format'
describe('formatCompactNumber', () => {
it('formats boundary values with K/M/B', () => {
expect(formatCompactNumber(0)).toBe('0')
expect(formatCompactNumber(999)).toBe('999')
expect(formatCompactNumber(1000)).toBe('1.0K')
expect(formatCompactNumber(999999)).toBe('1000.0K')
expect(formatCompactNumber(1000000)).toBe('1.0M')
expect(formatCompactNumber(1000000000)).toBe('1.0B')
})
it('supports disabling billion unit (requests style)', () => {
expect(formatCompactNumber(1000000000, { allowBillions: false })).toBe('1000.0M')
})
it('returns 0 for nullish input', () => {
expect(formatCompactNumber(null)).toBe('0')
expect(formatCompactNumber(undefined)).toBe('0')
})
})
...@@ -5,7 +5,7 @@ const normalizeUsageRefreshValue = (value: unknown): string => { ...@@ -5,7 +5,7 @@ const normalizeUsageRefreshValue = (value: unknown): string => {
return String(value) return String(value)
} }
export const buildOpenAIUsageRefreshKey = (account: Pick<Account, 'id' | 'platform' | 'type' | 'updated_at' | 'rate_limit_reset_at' | 'extra'>): string => { export const buildOpenAIUsageRefreshKey = (account: Pick<Account, 'id' | 'platform' | 'type' | 'updated_at' | 'last_used_at' | 'rate_limit_reset_at' | 'extra'>): string => {
if (account.platform !== 'openai' || account.type !== 'oauth') { if (account.platform !== 'openai' || account.type !== 'oauth') {
return '' return ''
} }
...@@ -14,6 +14,7 @@ export const buildOpenAIUsageRefreshKey = (account: Pick<Account, 'id' | 'platfo ...@@ -14,6 +14,7 @@ export const buildOpenAIUsageRefreshKey = (account: Pick<Account, 'id' | 'platfo
return [ return [
account.id, account.id,
account.updated_at, account.updated_at,
account.last_used_at,
account.rate_limit_reset_at, account.rate_limit_reset_at,
extra.codex_usage_updated_at, extra.codex_usage_updated_at,
extra.codex_5h_used_percent, extra.codex_5h_used_percent,
......
...@@ -247,6 +247,26 @@ export function formatTokensK(tokens: number): string { ...@@ -247,6 +247,26 @@ export function formatTokensK(tokens: number): string {
return tokens.toString() return tokens.toString()
} }
/**
* 格式化大数字(K/M/B,保留 1 位小数)
* @param num 数字
* @param options allowBillions=false 时最高只显示到 M
*/
export function formatCompactNumber(
num: number | null | undefined,
options?: { allowBillions?: boolean }
): string {
if (num === null || num === undefined) return '0'
const abs = Math.abs(num)
const allowBillions = options?.allowBillions !== false
if (allowBillions && abs >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)}B`
if (abs >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M`
if (abs >= 1_000) return `${(num / 1_000).toFixed(1)}K`
return num.toString()
}
/** /**
* 格式化倒计时(从现在到目标时间的剩余时间) * 格式化倒计时(从现在到目标时间的剩余时间)
* @param targetDate 目标日期字符串或 Date 对象 * @param targetDate 目标日期字符串或 Date 对象
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment