mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-06 08:49:24 +00:00
merge: sync batch image branch with origin main
This commit is contained in:
commit
1b07fe821a
107 files changed
+4469
-263
No files matched your search
@@ -679,14 +679,21 @@ const formatStickySchedulerScore = (score: AccountSchedulerGroupScore): string =
|
||||
}
|
||||
|
||||
const getSchedulerScoreRows = (account: Account): AccountSchedulerGroupScore[] => {
|
||||
if (!Array.isArray(account.scheduler_scores)) return []
|
||||
return account.scheduler_scores.filter(score => score.group_id != null)
|
||||
const groupRows = Array.isArray(account.scheduler_scores)
|
||||
? account.scheduler_scores.filter(score => score.group_id != null)
|
||||
: []
|
||||
if (groupRows.length) return groupRows
|
||||
// 未分组账号没有分组维度分数,回退展示后端返回的基础分
|
||||
if (account.scheduler_score) {
|
||||
return [{ group_id: null, ...account.scheduler_score }]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const formatSchedulerScoreGroup = (score: AccountSchedulerGroupScore): string => {
|
||||
if ('group_name' in score && score.group_name) return score.group_name
|
||||
if ('group_id' in score && score.group_id != null) return `#${score.group_id}`
|
||||
return '-'
|
||||
return t('admin.accounts.schedulerScore.ungrouped')
|
||||
}
|
||||
|
||||
const loadSavedColumns = () => {
|
||||
|
||||
@@ -6480,6 +6480,34 @@
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{
|
||||
t("admin.settings.payment.subscriptionUsdToCnyRate")
|
||||
}}</label>
|
||||
<input
|
||||
:value="form.payment_subscription_usd_to_cny_rate || ''"
|
||||
@input="
|
||||
form.payment_subscription_usd_to_cny_rate =
|
||||
parseFloat(
|
||||
($event.target as HTMLInputElement).value,
|
||||
) || 0
|
||||
"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
class="input"
|
||||
:placeholder="
|
||||
t(
|
||||
'admin.settings.payment.subscriptionUsdToCnyRateDisabled',
|
||||
)
|
||||
"
|
||||
/>
|
||||
<p class="mt-0.5 text-xs text-gray-400">
|
||||
{{
|
||||
t("admin.settings.payment.subscriptionUsdToCnyRateHint")
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{
|
||||
t("admin.settings.payment.rechargeFeeRate")
|
||||
@@ -8037,6 +8065,7 @@ const form = reactive<SettingsForm>({
|
||||
payment_order_timeout_minutes: 30,
|
||||
payment_balance_disabled: false,
|
||||
payment_balance_recharge_multiplier: 1,
|
||||
payment_subscription_usd_to_cny_rate: 0,
|
||||
payment_recharge_fee_rate: 0,
|
||||
payment_enabled_types: [],
|
||||
payment_help_image_url: "",
|
||||
@@ -9538,6 +9567,8 @@ async function saveSettings() {
|
||||
payment_balance_disabled: form.payment_balance_disabled,
|
||||
payment_balance_recharge_multiplier:
|
||||
Number(form.payment_balance_recharge_multiplier) || 1,
|
||||
payment_subscription_usd_to_cny_rate:
|
||||
Number(form.payment_subscription_usd_to_cny_rate) || 0,
|
||||
payment_recharge_fee_rate: Number(form.payment_recharge_fee_rate) || 0,
|
||||
payment_enabled_types: form.payment_enabled_types,
|
||||
payment_load_balance_strategy: form.payment_load_balance_strategy,
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
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 scheduler-score cell slot for every row so the fallback logic is observable.
|
||||
const DataTableStub = {
|
||||
props: ['columns', 'data'],
|
||||
template: `
|
||||
<div data-test="data-table">
|
||||
<div v-for="row in data" :key="row.id" :data-test="'scheduler-score-' + row.id">
|
||||
<slot name="cell-scheduler_score" :row="row" />
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
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: true,
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const baseAccount = {
|
||||
platform: 'openai',
|
||||
type: 'apikey',
|
||||
status: 'active',
|
||||
schedulable: true,
|
||||
concurrency: 1,
|
||||
priority: 0,
|
||||
error_message: null,
|
||||
last_used_at: null,
|
||||
expires_at: null,
|
||||
auto_pause_on_expired: false,
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
updated_at: '2026-01-01T00:00:00Z'
|
||||
}
|
||||
|
||||
describe('admin AccountsView scheduler score column', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
|
||||
listAccounts.mockReset()
|
||||
listWithEtag.mockReset()
|
||||
getBatchTodayStats.mockReset()
|
||||
getAllProxies.mockReset()
|
||||
getAllGroups.mockReset()
|
||||
|
||||
listAccounts.mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
...baseAccount,
|
||||
id: 1,
|
||||
name: 'ungrouped-openai',
|
||||
// 未分组账号:后端只返回基础分(scheduler_score),无分组维度分数
|
||||
scheduler_score: {
|
||||
base_score: 1.234567,
|
||||
sticky_score: 0,
|
||||
sticky_weighted_enabled: false
|
||||
}
|
||||
},
|
||||
{
|
||||
...baseAccount,
|
||||
id: 2,
|
||||
name: 'grouped-openai',
|
||||
scheduler_score: {
|
||||
base_score: 2,
|
||||
sticky_score: 3,
|
||||
sticky_weighted_enabled: true
|
||||
},
|
||||
scheduler_scores: [
|
||||
{
|
||||
group_id: 5,
|
||||
group_name: 'group-five',
|
||||
base_score: 2,
|
||||
sticky_score: 3,
|
||||
sticky_weighted_enabled: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
...baseAccount,
|
||||
id: 3,
|
||||
name: 'no-score',
|
||||
platform: 'anthropic'
|
||||
}
|
||||
],
|
||||
total: 3,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
pages: 1
|
||||
})
|
||||
listWithEtag.mockResolvedValue({
|
||||
notModified: true,
|
||||
etag: null,
|
||||
data: null
|
||||
})
|
||||
getBatchTodayStats.mockResolvedValue({ stats: {} })
|
||||
getAllProxies.mockResolvedValue([])
|
||||
getAllGroups.mockResolvedValue([])
|
||||
})
|
||||
|
||||
it('falls back to the base score for ungrouped accounts instead of showing a dash', async () => {
|
||||
const wrapper = mountView()
|
||||
await flushPromises()
|
||||
|
||||
const ungroupedCell = wrapper.find('[data-test="scheduler-score-1"]')
|
||||
expect(ungroupedCell.exists()).toBe(true)
|
||||
expect(ungroupedCell.text()).toContain('1.234567')
|
||||
expect(ungroupedCell.text()).toContain('admin.accounts.schedulerScore.ungrouped')
|
||||
expect(ungroupedCell.text()).not.toBe('-')
|
||||
})
|
||||
|
||||
it('renders per-group scores for grouped accounts', async () => {
|
||||
const wrapper = mountView()
|
||||
await flushPromises()
|
||||
|
||||
const groupedCell = wrapper.find('[data-test="scheduler-score-2"]')
|
||||
expect(groupedCell.exists()).toBe(true)
|
||||
expect(groupedCell.text()).toContain('group-five')
|
||||
expect(groupedCell.text()).toContain('2')
|
||||
})
|
||||
|
||||
it('still shows a dash when no scheduler score is available', async () => {
|
||||
const wrapper = mountView()
|
||||
await flushPromises()
|
||||
|
||||
const emptyCell = wrapper.find('[data-test="scheduler-score-3"]')
|
||||
expect(emptyCell.exists()).toBe(true)
|
||||
expect(emptyCell.text()).toBe('-')
|
||||
})
|
||||
})
|
||||
@@ -412,6 +412,7 @@ const baseSettingsResponse = {
|
||||
payment_enabled_types: [],
|
||||
payment_balance_disabled: false,
|
||||
payment_balance_recharge_multiplier: 1,
|
||||
payment_subscription_usd_to_cny_rate: 0,
|
||||
payment_recharge_fee_rate: 0,
|
||||
payment_load_balance_strategy: "round-robin",
|
||||
payment_product_name_prefix: "",
|
||||
|
||||
Reference in new issue
Block a user