Merge pull request #519 from bayma888/feature/group-sort-order

feat(admin): 新增-分组管理自由拖拽排序功能
This commit is contained in:
Wesley Liddick
2026-02-08 18:00:22 +08:00
committed by GitHub
35 changed files with 955 additions and 17 deletions

View File

@@ -153,6 +153,20 @@ export async function getGroupApiKeys(
return data
}
/**
* Update group sort orders
* @param updates - Array of { id, sort_order } objects
* @returns Success confirmation
*/
export async function updateSortOrder(
updates: Array<{ id: number; sort_order: number }>
): Promise<{ message: string }> {
const { data } = await apiClient.put<{ message: string }>('/admin/groups/sort-order', {
updates
})
return data
}
export const groupsAPI = {
list,
getAll,
@@ -163,7 +177,8 @@ export const groupsAPI = {
delete: deleteGroup,
toggleStatus,
getStats,
getGroupApiKeys
getGroupApiKeys,
updateSortOrder
}
export default groupsAPI

View File

@@ -58,6 +58,7 @@ const icons = {
arrowLeft: 'M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18',
arrowUp: 'M5 10l7-7m0 0l7 7m-7-7v18',
arrowDown: 'M19 14l-7 7m0 0l-7-7m7 7V3',
arrowsUpDown: 'M3 7.5L7.5 3m0 0L12 7.5M7.5 3v13.5m13.5 0L16.5 21m0 0L12 16.5m4.5 4.5V7.5',
chevronUp: 'M5 15l7-7 7 7',
externalLink: 'M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14',

View File

@@ -1042,6 +1042,10 @@ export default {
createGroup: 'Create Group',
editGroup: 'Edit Group',
deleteGroup: 'Delete Group',
sortOrder: 'Sort',
sortOrderHint: 'Drag groups to adjust display order, groups at the top will be displayed first',
sortOrderUpdated: 'Sort order updated',
failedToUpdateSortOrder: 'Failed to update sort order',
allPlatforms: 'All Platforms',
allStatus: 'All Status',
allGroups: 'All Groups',

View File

@@ -1099,6 +1099,10 @@ export default {
createGroup: '创建分组',
editGroup: '编辑分组',
deleteGroup: '删除分组',
sortOrder: '排序',
sortOrderHint: '拖拽分组调整显示顺序,排在前面的分组会优先显示',
sortOrderUpdated: '排序已更新',
failedToUpdateSortOrder: '更新排序失败',
deleteConfirm: "确定要删除分组 '{name}' 吗?所有关联的 API 密钥将不再属于任何分组。",
deleteConfirmSubscription:
"确定要删除订阅分组 '{name}' 吗?此操作会让所有绑定此订阅的用户的 API Key 失效,并删除所有相关的订阅记录。此操作无法撤销。",

View File

@@ -379,6 +379,9 @@ export interface AdminGroup extends Group {
// 分组下账号数量(仅管理员可见)
account_count?: number
// 分组排序
sort_order: number
}
export interface ApiKey {

View File

@@ -52,6 +52,14 @@
>
<Icon name="refresh" size="md" :class="loading ? 'animate-spin' : ''" />
</button>
<button
@click="openSortModal"
class="btn btn-secondary"
:title="t('admin.groups.sortOrder')"
>
<Icon name="arrowsUpDown" size="md" class="mr-2" />
{{ t('admin.groups.sortOrder') }}
</button>
<button
@click="showCreateModal = true"
class="btn btn-primary"
@@ -1455,6 +1463,92 @@
@confirm="confirmDelete"
@cancel="showDeleteDialog = false"
/>
<!-- Sort Order Modal -->
<BaseDialog
:show="showSortModal"
:title="t('admin.groups.sortOrder')"
width="normal"
@close="closeSortModal"
>
<div class="space-y-4">
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t('admin.groups.sortOrderHint') }}
</p>
<VueDraggable
v-model="sortableGroups"
:animation="200"
class="space-y-2"
>
<div
v-for="group in sortableGroups"
:key="group.id"
class="flex cursor-grab items-center gap-3 rounded-lg border border-gray-200 bg-white p-3 transition-shadow hover:shadow-md active:cursor-grabbing dark:border-dark-600 dark:bg-dark-700"
>
<div class="text-gray-400">
<Icon name="menu" size="md" />
</div>
<div class="flex-1">
<div class="font-medium text-gray-900 dark:text-white">{{ group.name }}</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
<span
:class="[
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium',
group.platform === 'anthropic'
? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
: group.platform === 'openai'
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
: group.platform === 'antigravity'
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
]"
>
{{ t('admin.groups.platforms.' + group.platform) }}
</span>
</div>
</div>
<div class="text-sm text-gray-400">
#{{ group.id }}
</div>
</div>
</VueDraggable>
</div>
<template #footer>
<div class="flex justify-end gap-3 pt-4">
<button @click="closeSortModal" type="button" class="btn btn-secondary">
{{ t('common.cancel') }}
</button>
<button
@click="saveSortOrder"
:disabled="sortSubmitting"
class="btn btn-primary"
>
<svg
v-if="sortSubmitting"
class="-ml-1 mr-2 h-4 w-4 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{{ sortSubmitting ? t('common.saving') : t('common.save') }}
</button>
</div>
</template>
</BaseDialog>
</AppLayout>
</template>
@@ -1476,6 +1570,7 @@ import EmptyState from '@/components/common/EmptyState.vue'
import Select from '@/components/common/Select.vue'
import PlatformIcon from '@/components/common/PlatformIcon.vue'
import Icon from '@/components/icons/Icon.vue'
import { VueDraggable } from 'vue-draggable-plus'
const { t } = useI18n()
const appStore = useAppStore()
@@ -1640,9 +1735,12 @@ let abortController: AbortController | null = null
const showCreateModal = ref(false)
const showEditModal = ref(false)
const showDeleteDialog = ref(false)
const showSortModal = ref(false)
const submitting = ref(false)
const sortSubmitting = ref(false)
const editingGroup = ref<AdminGroup | null>(null)
const deletingGroup = ref<AdminGroup | null>(null)
const sortableGroups = ref<AdminGroup[]>([])
const createForm = reactive({
name: '',
@@ -2101,6 +2199,46 @@ const handleClickOutside = (event: MouseEvent) => {
}
}
// 打开排序弹窗
const openSortModal = async () => {
try {
// 获取所有分组(不分页)
const allGroups = await adminAPI.groups.getAll()
// 按 sort_order 排序
sortableGroups.value = [...allGroups].sort((a, b) => a.sort_order - b.sort_order)
showSortModal.value = true
} catch (error) {
appStore.showError(t('admin.groups.failedToLoad'))
console.error('Error loading groups for sorting:', error)
}
}
// 关闭排序弹窗
const closeSortModal = () => {
showSortModal.value = false
sortableGroups.value = []
}
// 保存排序
const saveSortOrder = async () => {
sortSubmitting.value = true
try {
const updates = sortableGroups.value.map((g, index) => ({
id: g.id,
sort_order: index * 10
}))
await adminAPI.groups.updateSortOrder(updates)
appStore.showSuccess(t('admin.groups.sortOrderUpdated'))
closeSortModal()
loadGroups()
} catch (error: any) {
appStore.showError(error.response?.data?.detail || t('admin.groups.failedToUpdateSortOrder'))
console.error('Error updating sort order:', error)
} finally {
sortSubmitting.value = false
}
}
onMounted(() => {
loadGroups()
document.addEventListener('click', handleClickOutside)