Merge pull request #519 from bayma888/feature/group-sort-order
feat(admin): 新增-分组管理自由拖拽排序功能
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1099,6 +1099,10 @@ export default {
|
||||
createGroup: '创建分组',
|
||||
editGroup: '编辑分组',
|
||||
deleteGroup: '删除分组',
|
||||
sortOrder: '排序',
|
||||
sortOrderHint: '拖拽分组调整显示顺序,排在前面的分组会优先显示',
|
||||
sortOrderUpdated: '排序已更新',
|
||||
failedToUpdateSortOrder: '更新排序失败',
|
||||
deleteConfirm: "确定要删除分组 '{name}' 吗?所有关联的 API 密钥将不再属于任何分组。",
|
||||
deleteConfirmSubscription:
|
||||
"确定要删除订阅分组 '{name}' 吗?此操作会让所有绑定此订阅的用户的 API Key 失效,并删除所有相关的订阅记录。此操作无法撤销。",
|
||||
|
||||
@@ -379,6 +379,9 @@ export interface AdminGroup extends Group {
|
||||
|
||||
// 分组下账号数量(仅管理员可见)
|
||||
account_count?: number
|
||||
|
||||
// 分组排序
|
||||
sort_order: number
|
||||
}
|
||||
|
||||
export interface ApiKey {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user