Merge pull request #2895 from wucm667/feat/usage-window-tooltip

feat(admin): 账号用量窗口 5h/7d 增加说明 tooltip
This commit is contained in:
Wesley Liddick 2026-06-01 09:57:03 +08:00 committed by GitHub
commit ed8f5666c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 173 additions and 0 deletions

View File

@ -3105,6 +3105,7 @@ export default {
expiresAt: 'Expires At', expiresAt: 'Expires At',
actions: 'Actions' 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', allPrivacyModes: 'All Privacy States',
privacyUnset: 'Unset', privacyUnset: 'Unset',
privacyTrainingOff: 'Training data sharing disabled', privacyTrainingOff: 'Training data sharing disabled',

View File

@ -3143,6 +3143,7 @@ export default {
expiresAt: '过期时间', expiresAt: '过期时间',
actions: '操作' actions: '操作'
}, },
usageWindowsHint: '“5h / 7d”是上游账号如 OpenAI ChatGPT、Claude官方的滚动用量窗口限制由上游对账号设定并非 sub2api 配置,也与你映射的模型无关。窗口滚动到期后用量会自动重置,无法在 sub2api 端解除该限制。',
allPrivacyModes: '全部Privacy状态', allPrivacyModes: '全部Privacy状态',
privacyUnset: '未设置', privacyUnset: '未设置',
privacyTrainingOff: '已关闭训练数据共享', privacyTrainingOff: '已关闭训练数据共享',

View File

@ -273,6 +273,12 @@
<template #cell-groups="{ row }"> <template #cell-groups="{ row }">
<AccountGroupsCell :groups="row.groups" :max-display="4" /> <AccountGroupsCell :groups="row.groups" :max-display="4" />
</template> </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 }"> <template #cell-usage="{ row }">
<AccountUsageCell <AccountUsageCell
:account="row" :account="row"
@ -390,6 +396,7 @@ import { useTableSelection } from '@/composables/useTableSelection'
import AppLayout from '@/components/layout/AppLayout.vue' import AppLayout from '@/components/layout/AppLayout.vue'
import TablePageLayout from '@/components/layout/TablePageLayout.vue' import TablePageLayout from '@/components/layout/TablePageLayout.vue'
import DataTable from '@/components/common/DataTable.vue' import DataTable from '@/components/common/DataTable.vue'
import HelpTooltip from '@/components/common/HelpTooltip.vue'
import Pagination from '@/components/common/Pagination.vue' import Pagination from '@/components/common/Pagination.vue'
import ConfirmDialog from '@/components/common/ConfirmDialog.vue' import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
import { CreateAccountModal, EditAccountModal, BulkEditAccountModal, SyncFromCrsModal, TempUnschedStatusModal } from '@/components/account' import { CreateAccountModal, EditAccountModal, BulkEditAccountModal, SyncFromCrsModal, TempUnschedStatusModal } from '@/components/account'

View File

@ -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')
})
})