mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-03 06:52:13 +08:00
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
This commit is contained in:
7
.gitattributes
vendored
7
.gitattributes
vendored
@@ -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', () => {
|
|||||||
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', () => {
|
|||||||
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('')
|
||||||
})
|
})
|
||||||
|
|||||||
22
frontend/src/utils/__tests__/formatCompactNumber.spec.ts
Normal file
22
frontend/src/utils/__tests__/formatCompactNumber.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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 => {
|
|||||||
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
|
|||||||
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 {
|
|||||||
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 对象
|
||||||
|
|||||||
Reference in New Issue
Block a user