mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-31 16:26:30 +00:00
Merge pull request #4317 from yan9651688/feat/account-one-click-copy
feat(accounts): add safe one-click account duplication
This commit is contained in:
@@ -399,7 +399,7 @@
|
||||
<AccountTestModal :show="showTest" :account="testingAcc" @close="closeTestModal" />
|
||||
<AccountStatsModal :show="showStats" :account="statsAcc" @close="closeStatsModal" />
|
||||
<ScheduledTestsPanel :show="showSchedulePanel" :account-id="scheduleAcc?.id ?? null" :model-options="scheduleModelOptions" @close="closeSchedulePanel" />
|
||||
<AccountActionMenu :show="menu.show" :account="menu.acc" :position="menu.pos" @close="menu.show = false" @test="handleTest" @stats="handleViewStats" @schedule="handleSchedule" @reauth="handleReAuth" @refresh-token="handleRefresh" @recover-state="handleRecoverState" @reset-quota="handleResetQuota" @set-privacy="handleSetPrivacy" @create-spark-shadow="handleCreateSparkShadow" />
|
||||
<AccountActionMenu :show="menu.show" :account="menu.acc" :position="menu.pos" @close="menu.show = false" @test="handleTest" @stats="handleViewStats" @schedule="handleSchedule" @duplicate="handleDuplicateAccount" @reauth="handleReAuth" @refresh-token="handleRefresh" @recover-state="handleRecoverState" @reset-quota="handleResetQuota" @set-privacy="handleSetPrivacy" @create-spark-shadow="handleCreateSparkShadow" />
|
||||
<SyncFromCrsModal :show="showSync" @close="showSync = false" @synced="reload" />
|
||||
<ImportDataModal :show="showImportData" @close="showImportData = false" @imported="handleDataImported" />
|
||||
<BulkEditAccountModal
|
||||
@@ -1707,6 +1707,21 @@ const handleSchedule = async (a: Account) => {
|
||||
}
|
||||
const closeSchedulePanel = () => { showSchedulePanel.value = false; scheduleAcc.value = null; scheduleModelOptions.value = [] }
|
||||
const handleReAuth = (a: Account) => { reAuthAcc.value = a; showReAuth.value = true }
|
||||
const duplicatingAccountIDs = new Set<number>()
|
||||
const handleDuplicateAccount = async (a: Account) => {
|
||||
if (duplicatingAccountIDs.has(a.id)) return
|
||||
duplicatingAccountIDs.add(a.id)
|
||||
try {
|
||||
const duplicate = await adminAPI.accounts.duplicate(a.id)
|
||||
appStore.showSuccess(t('admin.accounts.duplicateSuccess', { name: duplicate.name }))
|
||||
reload()
|
||||
} catch (error: any) {
|
||||
console.error('Failed to duplicate account:', error)
|
||||
appStore.showError(error?.message || t('admin.accounts.duplicateFailed'))
|
||||
} finally {
|
||||
duplicatingAccountIDs.delete(a.id)
|
||||
}
|
||||
}
|
||||
const handleRefresh = async (a: Account) => {
|
||||
try {
|
||||
const updated = await adminAPI.accounts.refreshCredentials(a.id)
|
||||
|
||||
@@ -14,6 +14,7 @@ const {
|
||||
getBatchTodayStats,
|
||||
getAllProxies,
|
||||
getAllGroups,
|
||||
duplicateAccount,
|
||||
createSparkShadow,
|
||||
showSuccess,
|
||||
showError
|
||||
@@ -23,6 +24,7 @@ const {
|
||||
getBatchTodayStats: vi.fn(),
|
||||
getAllProxies: vi.fn(),
|
||||
getAllGroups: vi.fn(),
|
||||
duplicateAccount: vi.fn(),
|
||||
createSparkShadow: vi.fn(),
|
||||
showSuccess: vi.fn(),
|
||||
showError: vi.fn()
|
||||
@@ -34,6 +36,7 @@ vi.mock('@/api/admin', () => ({
|
||||
list: listAccounts,
|
||||
listWithEtag,
|
||||
getBatchTodayStats,
|
||||
duplicate: duplicateAccount,
|
||||
createSparkShadow,
|
||||
delete: vi.fn(),
|
||||
batchClearError: vi.fn(),
|
||||
@@ -102,7 +105,7 @@ const mountView = () =>
|
||||
describe('admin AccountsView — 外审 F2:spark 影子创建接线', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
for (const fn of [listAccounts, listWithEtag, getBatchTodayStats, getAllProxies, getAllGroups, createSparkShadow, showSuccess, showError]) {
|
||||
for (const fn of [listAccounts, listWithEtag, getBatchTodayStats, getAllProxies, getAllGroups, duplicateAccount, createSparkShadow, showSuccess, showError]) {
|
||||
fn.mockReset()
|
||||
}
|
||||
listAccounts.mockResolvedValue({ items: [], total: 0, page: 1, page_size: 20, pages: 0 })
|
||||
@@ -110,6 +113,7 @@ describe('admin AccountsView — 外审 F2:spark 影子创建接线', () => {
|
||||
getBatchTodayStats.mockResolvedValue({ stats: {} })
|
||||
getAllProxies.mockResolvedValue([])
|
||||
getAllGroups.mockResolvedValue([])
|
||||
duplicateAccount.mockResolvedValue({ id: 998, name: 'parent-acc (Copy)' })
|
||||
createSparkShadow.mockResolvedValue({ id: 999, name: 'parent-acc (Spark)' })
|
||||
})
|
||||
|
||||
@@ -117,6 +121,51 @@ describe('admin AccountsView — 外审 F2:spark 影子创建接线', () => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('AccountActionMenu 的 duplicate 事件一键复制账号并刷新列表', async () => {
|
||||
const wrapper = mountView()
|
||||
await flushPromises()
|
||||
|
||||
wrapper.findComponent(AccountActionMenu).vm.$emit('duplicate', { id: 42, name: 'parent-acc' })
|
||||
await flushPromises()
|
||||
|
||||
expect(duplicateAccount).toHaveBeenCalledTimes(1)
|
||||
expect(duplicateAccount).toHaveBeenCalledWith(42)
|
||||
expect(showSuccess).toHaveBeenCalledWith('admin.accounts.duplicateSuccess')
|
||||
expect(listAccounts.mock.calls.length).toBeGreaterThan(1)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('同一账号复制请求未完成时忽略重复点击', async () => {
|
||||
let resolveDuplicate!: (account: { id: number; name: string }) => void
|
||||
duplicateAccount.mockImplementationOnce(() => new Promise(resolve => { resolveDuplicate = resolve }))
|
||||
const wrapper = mountView()
|
||||
await flushPromises()
|
||||
|
||||
const menu = wrapper.findComponent(AccountActionMenu)
|
||||
menu.vm.$emit('duplicate', { id: 42, name: 'parent-acc' })
|
||||
menu.vm.$emit('duplicate', { id: 42, name: 'parent-acc' })
|
||||
await flushPromises()
|
||||
|
||||
expect(duplicateAccount).toHaveBeenCalledTimes(1)
|
||||
resolveDuplicate({ id: 998, name: 'parent-acc (Copy)' })
|
||||
await flushPromises()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('复制失败时显示后端错误', async () => {
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
duplicateAccount.mockRejectedValueOnce(new Error('duplicate failed'))
|
||||
const wrapper = mountView()
|
||||
await flushPromises()
|
||||
|
||||
wrapper.findComponent(AccountActionMenu).vm.$emit('duplicate', { id: 42, name: 'parent-acc' })
|
||||
await flushPromises()
|
||||
|
||||
expect(showError).toHaveBeenCalledWith('duplicate failed')
|
||||
consoleError.mockRestore()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('AccountActionMenu 的 create-spark-shadow 事件触发 createSparkShadow API + 成功提示', async () => {
|
||||
const wrapper = mountView()
|
||||
await flushPromises()
|
||||
@@ -208,7 +257,7 @@ const mountViewWithRow = () =>
|
||||
describe('admin AccountsView — 影子行 parent_* OR 兜底展示', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
for (const fn of [listAccounts, listWithEtag, getBatchTodayStats, getAllProxies, getAllGroups, createSparkShadow, showSuccess, showError]) {
|
||||
for (const fn of [listAccounts, listWithEtag, getBatchTodayStats, getAllProxies, getAllGroups, duplicateAccount, createSparkShadow, showSuccess, showError]) {
|
||||
fn.mockReset()
|
||||
}
|
||||
listWithEtag.mockResolvedValue({ notModified: true, etag: null, data: null })
|
||||
|
||||
Reference in New Issue
Block a user