feat(admin): 添加账号批量调度开关功能
- 后端:支持批量更新账号的 schedulable 字段 - 在 BulkUpdateAccountsRequest 中添加 schedulable 参数 - 在 AccountBulkUpdate 中添加 schedulable 字段支持 - 更新 repository 层批量更新 SQL 逻辑 - 前端:在账号管理页面添加批量调度控制 - 新增"批量启用调度"和"批量停止调度"按钮 - 添加 handleBulkToggleSchedulable 处理函数 - 显示具体的成功提示信息(包含操作账号数量) - 国际化:添加批量调度相关中英文翻译 - 优化:添加 search 参数标准化和验证(account_handler)
This commit is contained in:
@@ -116,6 +116,7 @@ type BulkUpdateAccountsRequest struct {
|
|||||||
Concurrency *int `json:"concurrency"`
|
Concurrency *int `json:"concurrency"`
|
||||||
Priority *int `json:"priority"`
|
Priority *int `json:"priority"`
|
||||||
Status string `json:"status" binding:"omitempty,oneof=active inactive error"`
|
Status string `json:"status" binding:"omitempty,oneof=active inactive error"`
|
||||||
|
Schedulable *bool `json:"schedulable"`
|
||||||
GroupIDs *[]int64 `json:"group_ids"`
|
GroupIDs *[]int64 `json:"group_ids"`
|
||||||
Credentials map[string]any `json:"credentials"`
|
Credentials map[string]any `json:"credentials"`
|
||||||
Extra map[string]any `json:"extra"`
|
Extra map[string]any `json:"extra"`
|
||||||
@@ -136,6 +137,11 @@ func (h *AccountHandler) List(c *gin.Context) {
|
|||||||
accountType := c.Query("type")
|
accountType := c.Query("type")
|
||||||
status := c.Query("status")
|
status := c.Query("status")
|
||||||
search := c.Query("search")
|
search := c.Query("search")
|
||||||
|
// 标准化和验证 search 参数
|
||||||
|
search = strings.TrimSpace(search)
|
||||||
|
if len(search) > 100 {
|
||||||
|
search = search[:100]
|
||||||
|
}
|
||||||
|
|
||||||
accounts, total, err := h.adminService.ListAccounts(c.Request.Context(), page, pageSize, platform, accountType, status, search)
|
accounts, total, err := h.adminService.ListAccounts(c.Request.Context(), page, pageSize, platform, accountType, status, search)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -655,6 +661,7 @@ func (h *AccountHandler) BulkUpdate(c *gin.Context) {
|
|||||||
req.Concurrency != nil ||
|
req.Concurrency != nil ||
|
||||||
req.Priority != nil ||
|
req.Priority != nil ||
|
||||||
req.Status != "" ||
|
req.Status != "" ||
|
||||||
|
req.Schedulable != nil ||
|
||||||
req.GroupIDs != nil ||
|
req.GroupIDs != nil ||
|
||||||
len(req.Credentials) > 0 ||
|
len(req.Credentials) > 0 ||
|
||||||
len(req.Extra) > 0
|
len(req.Extra) > 0
|
||||||
@@ -671,6 +678,7 @@ func (h *AccountHandler) BulkUpdate(c *gin.Context) {
|
|||||||
Concurrency: req.Concurrency,
|
Concurrency: req.Concurrency,
|
||||||
Priority: req.Priority,
|
Priority: req.Priority,
|
||||||
Status: req.Status,
|
Status: req.Status,
|
||||||
|
Schedulable: req.Schedulable,
|
||||||
GroupIDs: req.GroupIDs,
|
GroupIDs: req.GroupIDs,
|
||||||
Credentials: req.Credentials,
|
Credentials: req.Credentials,
|
||||||
Extra: req.Extra,
|
Extra: req.Extra,
|
||||||
|
|||||||
@@ -831,6 +831,11 @@ func (r *accountRepository) BulkUpdate(ctx context.Context, ids []int64, updates
|
|||||||
args = append(args, *updates.Status)
|
args = append(args, *updates.Status)
|
||||||
idx++
|
idx++
|
||||||
}
|
}
|
||||||
|
if updates.Schedulable != nil {
|
||||||
|
setClauses = append(setClauses, "schedulable = $"+itoa(idx))
|
||||||
|
args = append(args, *updates.Schedulable)
|
||||||
|
idx++
|
||||||
|
}
|
||||||
// JSONB 需要合并而非覆盖,使用 raw SQL 保持旧行为。
|
// JSONB 需要合并而非覆盖,使用 raw SQL 保持旧行为。
|
||||||
if len(updates.Credentials) > 0 {
|
if len(updates.Credentials) > 0 {
|
||||||
payload, err := json.Marshal(updates.Credentials)
|
payload, err := json.Marshal(updates.Credentials)
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ type AccountBulkUpdate struct {
|
|||||||
Concurrency *int
|
Concurrency *int
|
||||||
Priority *int
|
Priority *int
|
||||||
Status *string
|
Status *string
|
||||||
|
Schedulable *bool
|
||||||
Credentials map[string]any
|
Credentials map[string]any
|
||||||
Extra map[string]any
|
Extra map[string]any
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ type BulkUpdateAccountsInput struct {
|
|||||||
Concurrency *int
|
Concurrency *int
|
||||||
Priority *int
|
Priority *int
|
||||||
Status string
|
Status string
|
||||||
|
Schedulable *bool
|
||||||
GroupIDs *[]int64
|
GroupIDs *[]int64
|
||||||
Credentials map[string]any
|
Credentials map[string]any
|
||||||
Extra map[string]any
|
Extra map[string]any
|
||||||
@@ -910,6 +911,9 @@ func (s *adminServiceImpl) BulkUpdateAccounts(ctx context.Context, input *BulkUp
|
|||||||
if input.Status != "" {
|
if input.Status != "" {
|
||||||
repoUpdates.Status = &input.Status
|
repoUpdates.Status = &input.Status
|
||||||
}
|
}
|
||||||
|
if input.Schedulable != nil {
|
||||||
|
repoUpdates.Schedulable = input.Schedulable
|
||||||
|
}
|
||||||
|
|
||||||
// Run bulk update for column/jsonb fields first.
|
// Run bulk update for column/jsonb fields first.
|
||||||
if _, err := s.accountRepo.BulkUpdate(ctx, input.AccountIDs, repoUpdates); err != nil {
|
if _, err := s.accountRepo.BulkUpdate(ctx, input.AccountIDs, repoUpdates); err != nil {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button @click="$emit('delete')" class="btn btn-danger btn-sm">{{ t('admin.accounts.bulkActions.delete') }}</button>
|
<button @click="$emit('delete')" class="btn btn-danger btn-sm">{{ t('admin.accounts.bulkActions.delete') }}</button>
|
||||||
|
<button @click="$emit('toggle-schedulable', true)" class="btn btn-success btn-sm">{{ t('admin.accounts.bulkActions.enableScheduling') }}</button>
|
||||||
|
<button @click="$emit('toggle-schedulable', false)" class="btn btn-warning btn-sm">{{ t('admin.accounts.bulkActions.disableScheduling') }}</button>
|
||||||
<button @click="$emit('edit')" class="btn btn-primary btn-sm">{{ t('admin.accounts.bulkActions.edit') }}</button>
|
<button @click="$emit('edit')" class="btn btn-primary btn-sm">{{ t('admin.accounts.bulkActions.edit') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,5 +29,5 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
defineProps(['selectedIds']); defineEmits(['delete', 'edit', 'clear', 'select-page']); const { t } = useI18n()
|
defineProps(['selectedIds']); defineEmits(['delete', 'edit', 'clear', 'select-page', 'toggle-schedulable']); const { t } = useI18n()
|
||||||
</script>
|
</script>
|
||||||
@@ -1076,12 +1076,16 @@ export default {
|
|||||||
tokenRefreshed: 'Token refreshed successfully',
|
tokenRefreshed: 'Token refreshed successfully',
|
||||||
accountDeleted: 'Account deleted successfully',
|
accountDeleted: 'Account deleted successfully',
|
||||||
rateLimitCleared: 'Rate limit cleared successfully',
|
rateLimitCleared: 'Rate limit cleared successfully',
|
||||||
|
bulkSchedulableEnabled: 'Successfully enabled scheduling for {count} account(s)',
|
||||||
|
bulkSchedulableDisabled: 'Successfully disabled scheduling for {count} account(s)',
|
||||||
bulkActions: {
|
bulkActions: {
|
||||||
selected: '{count} account(s) selected',
|
selected: '{count} account(s) selected',
|
||||||
selectCurrentPage: 'Select this page',
|
selectCurrentPage: 'Select this page',
|
||||||
clear: 'Clear selection',
|
clear: 'Clear selection',
|
||||||
edit: 'Bulk Edit',
|
edit: 'Bulk Edit',
|
||||||
delete: 'Bulk Delete'
|
delete: 'Bulk Delete',
|
||||||
|
enableScheduling: 'Enable Scheduling',
|
||||||
|
disableScheduling: 'Disable Scheduling'
|
||||||
},
|
},
|
||||||
bulkEdit: {
|
bulkEdit: {
|
||||||
title: 'Bulk Edit Accounts',
|
title: 'Bulk Edit Accounts',
|
||||||
|
|||||||
@@ -1212,12 +1212,16 @@ export default {
|
|||||||
accountCreatedSuccess: '账号添加成功',
|
accountCreatedSuccess: '账号添加成功',
|
||||||
accountUpdatedSuccess: '账号更新成功',
|
accountUpdatedSuccess: '账号更新成功',
|
||||||
accountDeletedSuccess: '账号删除成功',
|
accountDeletedSuccess: '账号删除成功',
|
||||||
|
bulkSchedulableEnabled: '成功启用 {count} 个账号的调度',
|
||||||
|
bulkSchedulableDisabled: '成功停止 {count} 个账号的调度',
|
||||||
bulkActions: {
|
bulkActions: {
|
||||||
selected: '已选择 {count} 个账号',
|
selected: '已选择 {count} 个账号',
|
||||||
selectCurrentPage: '本页全选',
|
selectCurrentPage: '本页全选',
|
||||||
clear: '清除选择',
|
clear: '清除选择',
|
||||||
edit: '批量编辑账号',
|
edit: '批量编辑账号',
|
||||||
delete: '批量删除'
|
delete: '批量删除',
|
||||||
|
enableScheduling: '批量启用调度',
|
||||||
|
disableScheduling: '批量停止调度'
|
||||||
},
|
},
|
||||||
bulkEdit: {
|
bulkEdit: {
|
||||||
title: '批量编辑账号',
|
title: '批量编辑账号',
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #table>
|
<template #table>
|
||||||
<AccountBulkActionsBar :selected-ids="selIds" @delete="handleBulkDelete" @edit="showBulkEdit = true" @clear="selIds = []" @select-page="selectPage" />
|
<AccountBulkActionsBar :selected-ids="selIds" @delete="handleBulkDelete" @edit="showBulkEdit = true" @clear="selIds = []" @select-page="selectPage" @toggle-schedulable="handleBulkToggleSchedulable" />
|
||||||
<DataTable :columns="cols" :data="accounts" :loading="loading">
|
<DataTable :columns="cols" :data="accounts" :loading="loading">
|
||||||
<template #cell-select="{ row }">
|
<template #cell-select="{ row }">
|
||||||
<input type="checkbox" :checked="selIds.includes(row.id)" @change="toggleSel(row.id)" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" />
|
<input type="checkbox" :checked="selIds.includes(row.id)" @change="toggleSel(row.id)" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" />
|
||||||
@@ -209,6 +209,21 @@ const openMenu = (a: Account, e: MouseEvent) => { menu.acc = a; menu.pos = { top
|
|||||||
const toggleSel = (id: number) => { const i = selIds.value.indexOf(id); if(i === -1) selIds.value.push(id); else selIds.value.splice(i, 1) }
|
const toggleSel = (id: number) => { const i = selIds.value.indexOf(id); if(i === -1) selIds.value.push(id); else selIds.value.splice(i, 1) }
|
||||||
const selectPage = () => { selIds.value = [...new Set([...selIds.value, ...accounts.value.map(a => a.id)])] }
|
const selectPage = () => { selIds.value = [...new Set([...selIds.value, ...accounts.value.map(a => a.id)])] }
|
||||||
const handleBulkDelete = async () => { if(!confirm(t('common.confirm'))) return; try { await Promise.all(selIds.value.map(id => adminAPI.accounts.delete(id))); selIds.value = []; reload() } catch (error) { console.error('Failed to bulk delete accounts:', error) } }
|
const handleBulkDelete = async () => { if(!confirm(t('common.confirm'))) return; try { await Promise.all(selIds.value.map(id => adminAPI.accounts.delete(id))); selIds.value = []; reload() } catch (error) { console.error('Failed to bulk delete accounts:', error) } }
|
||||||
|
const handleBulkToggleSchedulable = async (schedulable: boolean) => {
|
||||||
|
const count = selIds.value.length
|
||||||
|
try {
|
||||||
|
const result = await adminAPI.accounts.bulkUpdate(selIds.value, { schedulable });
|
||||||
|
const message = schedulable
|
||||||
|
? t('admin.accounts.bulkSchedulableEnabled', { count: result.success || count })
|
||||||
|
: t('admin.accounts.bulkSchedulableDisabled', { count: result.success || count });
|
||||||
|
appStore.showSuccess(message);
|
||||||
|
selIds.value = [];
|
||||||
|
reload()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to bulk toggle schedulable:', error);
|
||||||
|
appStore.showError(t('common.error'))
|
||||||
|
}
|
||||||
|
}
|
||||||
const handleBulkUpdated = () => { showBulkEdit.value = false; selIds.value = []; reload() }
|
const handleBulkUpdated = () => { showBulkEdit.value = false; selIds.value = []; reload() }
|
||||||
const closeTestModal = () => { showTest.value = false; testingAcc.value = null }
|
const closeTestModal = () => { showTest.value = false; testingAcc.value = null }
|
||||||
const closeStatsModal = () => { showStats.value = false; statsAcc.value = null }
|
const closeStatsModal = () => { showStats.value = false; statsAcc.value = null }
|
||||||
|
|||||||
Reference in New Issue
Block a user