feat: 实例分组增强前端(拖拽分组 UI)
Sync source to CNB / Sync Git ref (push) Skipped

移植 Axolotl 的拖拽式实例分组界面到 Starlight,接入阶段1的规范化分组后端:

- 新增 helpers/analytics.ts(trackEvent 遥测桩)
- 补全 helpers/instance-groups.ts(6 个 invoke 包装)
- 新增 composables/useInstanceGroups.ts(分组状态 + 事件监听)
- 升级 composables/useGridGrouping.ts(排序方向 isSortAscending/toggleSortDirection)
- helpers/events.js 补 instance_groups_listener(对接 instance_groups_changed 事件)
- 新增 components/ui/library/DraggableInstanceCard.vue
- 新增 components/ui/library/InstanceGroup.vue(并补 group-drag-handle 修复组拖拽重排)
- 新增 components/ui/modal/InstanceGroupModal.vue
- GridDisplay.vue 替换为 Axolotl 拖拽版(DragDropProvider + vuedraggable)
- 补 en-US/zh-CN i18n 键各 39 条
This commit is contained in:
AxTps committed 2026-10-11 07:54:48 +08:00
1 parent 253141b6ff
commit f253d5529b
11 files changed
+2214 -297

No files matched your search

@@ -7,15 +7,22 @@ export const UNGROUPED_GROUP_KEY = '__ungrouped__'
export type GridGroupingOption = 'Group' | 'Loader' | 'Game version' | 'None'
export type GridSortOption =
| 'Name'
| 'Last played'
| 'Date created'
| 'Date modified'
| 'Game version'
'Name' | 'Last played' | 'Date created' | 'Date modified' | 'Game version'
export type GridSortDirection = 'asc' | 'desc'
export const defaultSortDirections: Record<GridSortOption, GridSortDirection> = {
Name: 'asc',
'Last played': 'desc',
'Date created': 'desc',
'Date modified': 'desc',
'Game version': 'asc',
}
export interface GridDisplayState {
group: GridGroupingOption
sortBy: GridSortOption
sortDirections: Partial<Record<GridSortOption, GridSortDirection>>
collapsedGroups: string[]
}
@@ -38,6 +45,7 @@ export function useGridGrouping<T extends Record<string, unknown>>(
{
group: 'Group',
sortBy: 'Name',
sortDirections: {},
collapsedGroups: [],
},
localStorage,
@@ -66,7 +74,7 @@ export function useGridGrouping<T extends Record<string, unknown>>(
state.value.collapsedGroups = [...collapsedSections]
}
function sortInstances(instances: T[], sortBy: GridSortOption): T[] {
function sortInstances(instances: T[], sortBy: GridSortOption, ascending: boolean): T[] {
const sorted = [...instances]
const getGameVersion = options.getGameVersion ?? ((i: T) => i.game_version ?? '')
const getLastPlayed = options.getLastPlayed ?? ((i: T) => i.last_played ?? 0)
@@ -83,16 +91,19 @@ export function useGridGrouping<T extends Record<string, unknown>>(
)
break
case 'Last played':
sorted.sort((a, b) => dayjs(getLastPlayed(b)).diff(dayjs(getLastPlayed(a))))
sorted.sort((a, b) => dayjs(getLastPlayed(a)).diff(dayjs(getLastPlayed(b))))
break
case 'Date created':
sorted.sort((a, b) => dayjs(getDateCreated(b)).diff(dayjs(getDateCreated(a))))
sorted.sort((a, b) => dayjs(getDateCreated(a)).diff(dayjs(getDateCreated(b))))
break
case 'Date modified':
sorted.sort((a, b) => dayjs(getDateModified(b)).diff(dayjs(getDateModified(a))))
sorted.sort((a, b) => dayjs(getDateModified(a)).diff(dayjs(getDateModified(b))))
break
}
if (!ascending) {
sorted.reverse()
}
return sorted
}
@@ -148,11 +159,25 @@ export function useGridGrouping<T extends Record<string, unknown>>(
function sortSections(
instanceMap: Map<string, T[]>,
group: GridGroupingOption,
sortBy: GridSortOption,
): Map<string, T[]> {
if (group !== 'Group' && group !== 'Game version') {
return instanceMap
}
if (group === 'Group' && sortBy !== 'Name') {
if (instanceMap.has(UNGROUPED_GROUP_KEY) && instanceMap.size > 1) {
const ungroupedInstances = instanceMap.get(UNGROUPED_GROUP_KEY)!
instanceMap.delete(UNGROUPED_GROUP_KEY)
const newMap = new Map<string, T[]>([[UNGROUPED_GROUP_KEY, ungroupedInstances]])
for (const [key, instances] of instanceMap) {
newMap.set(key, instances)
}
return newMap
}
return instanceMap
}
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
if (group === 'Group') {
if (a[0] === UNGROUPED_GROUP_KEY && b[0] !== UNGROUPED_GROUP_KEY) return -1
@@ -166,12 +191,30 @@ export function useGridGrouping<T extends Record<string, unknown>>(
return instanceMap
}
const sortDirection = computed<GridSortDirection>(() => {
return (
state.value.sortDirections[state.value.sortBy] ?? defaultSortDirections[state.value.sortBy]
)
})
const isSortAscending = computed(() => sortDirection.value === 'asc')
const toggleSortDirection = () => {
const current = sortDirection.value
const next: GridSortDirection = current === 'asc' ? 'desc' : 'asc'
state.value.sortDirections = {
...state.value.sortDirections,
[state.value.sortBy]: next,
}
}
const filteredResults = computed(() => {
const { group = 'Group', sortBy = 'Name' } = state.value
const ascending = isSortAscending.value
const sorted = sortInstances(instances.value, sortBy)
const sorted = sortInstances(instances.value, sortBy, ascending)
const grouped = groupInstances(sorted, group)
return sortSections(grouped, group)
return sortSections(grouped, group, sortBy)
})
return {
@@ -180,6 +223,9 @@ export function useGridGrouping<T extends Record<string, unknown>>(
filteredResults,
isSectionCollapsed,
setSectionCollapsed,
sortDirection,
isSortAscending,
toggleSortDirection,
UNGROUPED_GROUP_KEY,
}
}
}