增加预设排序功能

This commit is contained in:
2026-05-09 11:01:53 +08:00
parent 01ca52cfc7
commit 021be8f22b
2 changed files with 187 additions and 7 deletions
+170 -5
View File
@@ -17,6 +17,8 @@ const selectedType = ref<PresetType | 'all'>('all');
const searchQuery = ref('');
const selectedFolderId = ref<string | null>(null);
const expandedFolderIds = ref<Set<string>>(new Set());
const sortField = ref<'custom' | 'name' | 'updatedAt'>('custom');
const sortDirection = ref<'asc' | 'desc'>('asc');
const presetSidebarRef = ref<InstanceType<typeof PresetSidebar> | null>(null);
const presetListRef = ref<InstanceType<typeof PresetList> | null>(null);
const isRestoringViewState = ref(false);
@@ -91,6 +93,24 @@ const filterOptions = computed<{ value: PresetType | 'all'; label: string }[]>((
...presetTypes
]);
const sortOptions = [
{ value: 'custom', label: '自定义排序' },
{ value: 'name', label: '按名称' },
{ value: 'updatedAt', label: '按时间' }
] as const;
const isCustomSort = computed(() => sortField.value === 'custom');
const sortDirectionLabel = computed(() => {
if (sortField.value === 'name') {
return sortDirection.value === 'asc' ? '名称 A-Z' : '名称 Z-A';
}
if (sortField.value === 'updatedAt') {
return sortDirection.value === 'asc' ? '时间旧到新' : '时间新到旧';
}
return sortDirection.value === 'asc' ? '正序' : '倒序';
});
const filteredPresets = computed(() => {
let presets = [...(store.extendedPresets || [])];
@@ -121,18 +141,43 @@ const filteredPresets = computed(() => {
);
}
return presets.sort((a, b) => {
const ao = typeof a.sortOrder === 'number' ? a.sortOrder : Number.POSITIVE_INFINITY;
const bo = typeof b.sortOrder === 'number' ? b.sortOrder : Number.POSITIVE_INFINITY;
if (ao !== bo) return ao - bo;
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
const sorted = [...presets];
if (sortField.value === 'custom') {
sorted.sort((a, b) => {
const ao = typeof a.sortOrder === 'number' ? a.sortOrder : Number.POSITIVE_INFINITY;
const bo = typeof b.sortOrder === 'number' ? b.sortOrder : Number.POSITIVE_INFINITY;
if (ao !== bo) return sortDirection.value === 'asc' ? ao - bo : bo - ao;
const timeDiff = new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
return sortDirection.value === 'asc' ? timeDiff : -timeDiff;
});
return sorted;
}
if (sortField.value === 'name') {
sorted.sort((a, b) => {
const compare = a.name.localeCompare(b.name, 'zh-CN', { numeric: true, sensitivity: 'base' });
if (compare !== 0) {
return sortDirection.value === 'asc' ? compare : -compare;
}
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
});
return sorted;
}
sorted.sort((a, b) => {
const compare = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime();
if (compare !== 0) {
return sortDirection.value === 'asc' ? compare : -compare;
}
return a.name.localeCompare(b.name, 'zh-CN', { numeric: true, sensitivity: 'base' });
});
return sorted;
});
const presetListResetKey = computed(() => JSON.stringify({
selectedType: selectedType.value,
searchQuery: searchQuery.value,
selectedFolderId: selectedFolderId.value,
sortField: sortField.value,
sortDirection: sortDirection.value,
}));
const flattenedFolders = computed(() => {
@@ -170,6 +215,8 @@ function persistPresetManagerViewState() {
searchQuery: searchQuery.value,
selectedFolderId: selectedFolderId.value,
expandedFolderIds: Array.from(expandedFolderIds.value),
sortField: sortField.value,
sortDirection: sortDirection.value,
sidebarScrollTop: presetSidebarRef.value?.contentRef?.scrollTop ?? 0,
listScrollTop: presetListRef.value?.containerRef?.scrollTop ?? 0,
currentPage: presetListRef.value?.getCurrentPage?.() ?? 1,
@@ -188,6 +235,8 @@ async function restorePresetManagerViewState() {
searchQuery?: string;
selectedFolderId?: string | null;
expandedFolderIds?: string[];
sortField?: 'custom' | 'name' | 'updatedAt';
sortDirection?: 'asc' | 'desc';
sidebarScrollTop?: number;
listScrollTop?: number;
currentPage?: number;
@@ -199,6 +248,8 @@ async function restorePresetManagerViewState() {
searchQuery.value = state.searchQuery ?? '';
selectedFolderId.value = state.selectedFolderId ?? null;
expandedFolderIds.value = new Set(state.expandedFolderIds ?? []);
sortField.value = state.sortField ?? 'custom';
sortDirection.value = state.sortDirection ?? 'asc';
await nextTick();
if (typeof state.currentPage === 'number') {
presetListRef.value?.setCurrentPage?.(state.currentPage, false);
@@ -229,6 +280,14 @@ function handleToggleExpand(id: string) {
expandedFolderIds.value = set;
}
function setSortField(field: 'custom' | 'name' | 'updatedAt') {
sortField.value = field;
}
function toggleSortDirection() {
sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc';
}
function createFolder(parentId?: string) {
resetFolderForm();
if (parentId) {
@@ -350,6 +409,10 @@ function toggleFavorite(preset: ExtendedPreset) {
}
function handleReorderPresets(payload: { draggedId: string; targetId: string; side: 'before' | 'after' }) {
if (!isCustomSort.value) {
showNotification('请先切换到自定义排序后再拖拽', 'info');
return;
}
const visibleIds = filteredPresets.value.map(preset => preset.id);
const from = visibleIds.indexOf(payload.draggedId);
const target = visibleIds.indexOf(payload.targetId);
@@ -785,6 +848,8 @@ watch(
selectedType.value,
searchQuery.value,
selectedFolderId.value,
sortField.value,
sortDirection.value,
Array.from(expandedFolderIds.value).sort().join('|'),
],
() => {
@@ -848,6 +913,34 @@ onBeforeUnmount(() => {
v-model="selectedType"
:options="filterOptions"
/>
<div class="sort-controls">
<div class="sort-field-group">
<button
v-for="option in sortOptions"
:key="option.value"
class="sort-chip nav-btn"
:class="{ active: sortField === option.value }"
@click="setSortField(option.value)"
:title="option.label"
>
{{ option.label }}
</button>
</div>
<button
class="sort-direction-btn nav-btn"
@click="toggleSortDirection"
:title="`切换为${sortDirection === 'asc' ? '倒序' : '正序'}`"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M7 4v16"></path>
<path d="M4 7l3-3 3 3"></path>
<path d="M13 8h7"></path>
<path d="M13 12h5"></path>
<path d="M13 16h3"></path>
</svg>
<span>{{ sortDirectionLabel }}</span>
</button>
</div>
</div>
<div class="action-group">
@@ -885,11 +978,15 @@ onBeforeUnmount(() => {
</div>
<div class="pm-content-area">
<div class="sort-hint">
{{ isCustomSort ? '当前为自定义排序,可直接拖拽卡片调整顺序。' : '当前为只读排序视图,切回自定义排序后可继续拖拽。' }}
</div>
<PresetList
ref="presetListRef"
:presets="filteredPresets"
:search-query="searchQuery"
:reset-key="presetListResetKey"
:enable-reorder="isCustomSort"
@apply="applyPreset"
@edit="editPreset"
@delete="deletePreset"
@@ -1227,6 +1324,8 @@ onBeforeUnmount(() => {
.filter-group {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
}
.type-select {
@@ -1237,6 +1336,66 @@ onBeforeUnmount(() => {
color: var(--color-text-primary);
}
.sort-controls {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.sort-field-group {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background-color: var(--color-bg-primary);
}
.sort-chip {
padding: 0.45rem 0.75rem;
border: none;
border-radius: calc(var(--radius-md) - 4px);
background: transparent;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.875rem;
white-space: nowrap;
}
.sort-chip:hover {
background-color: var(--color-bg-secondary);
color: var(--color-text-primary);
}
.sort-chip.active {
background-color: var(--color-accent);
color: white;
box-shadow: var(--shadow-sm);
}
.sort-direction-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background-color: var(--color-bg-primary);
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.sort-direction-btn:hover {
border-color: var(--color-border-hover);
background-color: var(--color-bg-secondary);
color: var(--color-text-primary);
}
.action-group {
margin-left: auto;
display: flex;
@@ -1257,6 +1416,12 @@ onBeforeUnmount(() => {
position: relative;
}
.sort-hint {
padding: 0.625rem 1rem 0;
font-size: 0.8125rem;
color: var(--color-text-tertiary);
}
/* Buttons */
.btn-primary {
display: flex;
+17 -2
View File
@@ -9,6 +9,7 @@ const props = defineProps<{
presets: ExtendedPreset[];
searchQuery: string;
resetKey: string;
enableReorder?: boolean;
}>();
const emit = defineEmits<{
@@ -81,6 +82,10 @@ defineExpose({
});
function onDragStart(preset: ExtendedPreset, event: DragEvent) {
if (!props.enableReorder) {
event.preventDefault();
return;
}
draggingPresetId.value = preset.id;
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';
@@ -89,6 +94,7 @@ function onDragStart(preset: ExtendedPreset, event: DragEvent) {
}
function onDragOver(preset: ExtendedPreset, event: DragEvent) {
if (!props.enableReorder) return;
if (!draggingPresetId.value || draggingPresetId.value === preset.id) return;
event.preventDefault();
const target = event.currentTarget as HTMLElement | null;
@@ -103,6 +109,10 @@ function onDragOver(preset: ExtendedPreset, event: DragEvent) {
}
function onDrop(preset: ExtendedPreset, event: DragEvent) {
if (!props.enableReorder) {
cleanupDragState();
return;
}
if (!draggingPresetId.value || draggingPresetId.value === preset.id || !dropSide.value) {
cleanupDragState();
return;
@@ -209,11 +219,12 @@ function formatDate(dateStr: string) {
</div>
<div class="preset-grid">
<div v-for="preset in displayedPresets" :key="preset.id" class="preset-card nav-btn" draggable="true"
<div v-for="preset in displayedPresets" :key="preset.id" class="preset-card nav-btn" :draggable="!!enableReorder"
:class="{
dragging: draggingPresetId === preset.id,
'insert-before': overPresetId === preset.id && dropSide === 'before' && draggingPresetId !== preset.id,
'insert-after': overPresetId === preset.id && dropSide === 'after' && draggingPresetId !== preset.id
'insert-after': overPresetId === preset.id && dropSide === 'after' && draggingPresetId !== preset.id,
'reorder-disabled': !enableReorder
}"
@dragstart="onDragStart(preset, $event)" @dragover="onDragOver(preset, $event)"
@drop="onDrop(preset, $event)" @dragend="cleanupDragState">
@@ -379,6 +390,10 @@ function formatDate(dateStr: string) {
cursor: grabbing;
}
.preset-card.reorder-disabled {
cursor: default;
}
.preset-card.insert-before {
border-top: 3px solid var(--color-accent);
}