Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3105,6 +3105,7 @@ export default {
expiresAt: 'Expires At',
actions: 'Actions'
},
usageWindowsHint: '"5h / 7d" are the upstream account\'s official rolling usage windows (e.g. OpenAI ChatGPT, Claude). They are imposed by the upstream provider on the account itself — not configured by sub2api, and unrelated to the models you map. Usage resets automatically once each window rolls over, and the limit cannot be lifted from within sub2api.',
allPrivacyModes: 'All Privacy States',
privacyUnset: 'Unset',
privacyTrainingOff: 'Training data sharing disabled',
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3143,6 +3143,7 @@ export default {
expiresAt: '过期时间',
actions: '操作'
},
usageWindowsHint: '“5h / 7d”是上游账号(如 OpenAI ChatGPT、Claude)官方的滚动用量窗口限制,由上游对账号设定,并非 sub2api 配置,也与你映射的模型无关。窗口滚动到期后用量会自动重置,无法在 sub2api 端解除该限制。',
allPrivacyModes: '全部Privacy状态',
privacyUnset: '未设置',
privacyTrainingOff: '已关闭训练数据共享',
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/views/admin/AccountsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,12 @@
<template #cell-groups="{ row }">
<AccountGroupsCell :groups="row.groups" :max-display="4" />
</template>
<template #header-usage="{ column }">
<div class="flex items-center">
<span>{{ column.label }}</span>
<HelpTooltip :content="t('admin.accounts.usageWindowsHint')" width-class="w-72" />
</div>
</template>
<template #cell-usage="{ row }">
<AccountUsageCell
:account="row"
Expand Down Expand Up @@ -390,6 +396,7 @@ import { useTableSelection } from '@/composables/useTableSelection'
import AppLayout from '@/components/layout/AppLayout.vue'
import TablePageLayout from '@/components/layout/TablePageLayout.vue'
import DataTable from '@/components/common/DataTable.vue'
import HelpTooltip from '@/components/common/HelpTooltip.vue'
import Pagination from '@/components/common/Pagination.vue'
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
import { CreateAccountModal, EditAccountModal, BulkEditAccountModal, SyncFromCrsModal, TempUnschedStatusModal } from '@/components/account'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'

import AccountsView from '../AccountsView.vue'

const {
listAccounts,
listWithEtag,
getBatchTodayStats,
getAllProxies,
getAllGroups
} = vi.hoisted(() => ({
listAccounts: vi.fn(),
listWithEtag: vi.fn(),
getBatchTodayStats: vi.fn(),
getAllProxies: vi.fn(),
getAllGroups: vi.fn()
}))

vi.mock('@/api/admin', () => ({
adminAPI: {
accounts: {
list: listAccounts,
listWithEtag,
getBatchTodayStats,
delete: vi.fn(),
batchClearError: vi.fn(),
batchRefresh: vi.fn(),
toggleSchedulable: vi.fn()
},
proxies: {
getAll: getAllProxies
},
groups: {
getAll: getAllGroups
}
}
}))

vi.mock('@/stores/app', () => ({
useAppStore: () => ({
showError: vi.fn(),
showSuccess: vi.fn(),
showInfo: vi.fn()
})
}))

vi.mock('@/stores/auth', () => ({
useAuthStore: () => ({
token: 'test-token'
})
}))

vi.mock('vue-i18n', async () => {
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
return {
...actual,
useI18n: () => ({
t: (key: string) => key
})
}
})

// Render the per-column header slots so we can assert the usage-window header hint.
const DataTableStub = {
props: ['columns', 'data'],
template: `
<div data-test="data-table">
<template v-for="column in columns" :key="column.key">
<div v-if="column.key === 'usage'" data-test="usage-header">
<slot :name="'header-' + column.key" :column="column" />
</div>
</template>
</div>
`
}

// Expose the content passed to HelpTooltip without dealing with its <Teleport>.
const HelpTooltipStub = {
props: ['content', 'widthClass'],
template: '<span data-test="usage-windows-hint">{{ content }}</span>'
}

function mountView() {
return mount(AccountsView, {
global: {
stubs: {
AppLayout: { template: '<div><slot /></div>' },
TablePageLayout: {
template: '<div><slot name="filters" /><slot name="table" /><slot name="pagination" /></div>'
},
DataTable: DataTableStub,
HelpTooltip: HelpTooltipStub,
Pagination: true,
ConfirmDialog: true,
AccountTableActions: { template: '<div><slot name="beforeCreate" /><slot name="after" /></div>' },
AccountTableFilters: { template: '<div></div>' },
AccountBulkActionsBar: true,
AccountActionMenu: true,
ImportDataModal: true,
ReAuthAccountModal: true,
AccountTestModal: true,
AccountStatsModal: true,
ScheduledTestsPanel: true,
SyncFromCrsModal: true,
TempUnschedStatusModal: true,
ErrorPassthroughRulesModal: true,
TLSFingerprintProfilesModal: true,
CreateAccountModal: true,
EditAccountModal: true,
BulkEditAccountModal: true,
PlatformTypeBadge: true,
AccountCapacityCell: true,
AccountStatusIndicator: true,
AccountTodayStatsCell: true,
AccountGroupsCell: true,
AccountUsageCell: true,
Icon: true
}
}
})
}

describe('admin AccountsView usage windows hint', () => {
beforeEach(() => {
localStorage.clear()

listAccounts.mockReset()
listWithEtag.mockReset()
getBatchTodayStats.mockReset()
getAllProxies.mockReset()
getAllGroups.mockReset()

listAccounts.mockResolvedValue({
items: [],
total: 0,
page: 1,
page_size: 20,
pages: 0
})
listWithEtag.mockResolvedValue({
notModified: true,
etag: null,
data: null
})
getBatchTodayStats.mockResolvedValue({ stats: {} })
getAllProxies.mockResolvedValue([])
getAllGroups.mockResolvedValue([])
})

it('renders an explanatory tooltip next to the usage windows column header', async () => {
const wrapper = mountView()
await flushPromises()

const header = wrapper.find('[data-test="usage-header"]')
expect(header.exists()).toBe(true)
// Column label is still shown alongside the help icon.
expect(header.text()).toContain('admin.accounts.columns.usageWindows')

const hint = wrapper.find('[data-test="usage-windows-hint"]')
expect(hint.exists()).toBe(true)
expect(hint.text()).toBe('admin.accounts.usageWindowsHint')
})
})
Loading