增加预设排序功能
This commit is contained in:
@@ -17,6 +17,8 @@ const selectedType = ref<PresetType | 'all'>('all');
|
|||||||
const searchQuery = ref('');
|
const searchQuery = ref('');
|
||||||
const selectedFolderId = ref<string | null>(null);
|
const selectedFolderId = ref<string | null>(null);
|
||||||
const expandedFolderIds = ref<Set<string>>(new Set());
|
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 presetSidebarRef = ref<InstanceType<typeof PresetSidebar> | null>(null);
|
||||||
const presetListRef = ref<InstanceType<typeof PresetList> | null>(null);
|
const presetListRef = ref<InstanceType<typeof PresetList> | null>(null);
|
||||||
const isRestoringViewState = ref(false);
|
const isRestoringViewState = ref(false);
|
||||||
@@ -91,6 +93,24 @@ const filterOptions = computed<{ value: PresetType | 'all'; label: string }[]>((
|
|||||||
...presetTypes
|
...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(() => {
|
const filteredPresets = computed(() => {
|
||||||
let presets = [...(store.extendedPresets || [])];
|
let presets = [...(store.extendedPresets || [])];
|
||||||
|
|
||||||
@@ -121,18 +141,43 @@ const filteredPresets = computed(() => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return presets.sort((a, b) => {
|
const sorted = [...presets];
|
||||||
const ao = typeof a.sortOrder === 'number' ? a.sortOrder : Number.POSITIVE_INFINITY;
|
if (sortField.value === 'custom') {
|
||||||
const bo = typeof b.sortOrder === 'number' ? b.sortOrder : Number.POSITIVE_INFINITY;
|
sorted.sort((a, b) => {
|
||||||
if (ao !== bo) return ao - bo;
|
const ao = typeof a.sortOrder === 'number' ? a.sortOrder : Number.POSITIVE_INFINITY;
|
||||||
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
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({
|
const presetListResetKey = computed(() => JSON.stringify({
|
||||||
selectedType: selectedType.value,
|
selectedType: selectedType.value,
|
||||||
searchQuery: searchQuery.value,
|
searchQuery: searchQuery.value,
|
||||||
selectedFolderId: selectedFolderId.value,
|
selectedFolderId: selectedFolderId.value,
|
||||||
|
sortField: sortField.value,
|
||||||
|
sortDirection: sortDirection.value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const flattenedFolders = computed(() => {
|
const flattenedFolders = computed(() => {
|
||||||
@@ -170,6 +215,8 @@ function persistPresetManagerViewState() {
|
|||||||
searchQuery: searchQuery.value,
|
searchQuery: searchQuery.value,
|
||||||
selectedFolderId: selectedFolderId.value,
|
selectedFolderId: selectedFolderId.value,
|
||||||
expandedFolderIds: Array.from(expandedFolderIds.value),
|
expandedFolderIds: Array.from(expandedFolderIds.value),
|
||||||
|
sortField: sortField.value,
|
||||||
|
sortDirection: sortDirection.value,
|
||||||
sidebarScrollTop: presetSidebarRef.value?.contentRef?.scrollTop ?? 0,
|
sidebarScrollTop: presetSidebarRef.value?.contentRef?.scrollTop ?? 0,
|
||||||
listScrollTop: presetListRef.value?.containerRef?.scrollTop ?? 0,
|
listScrollTop: presetListRef.value?.containerRef?.scrollTop ?? 0,
|
||||||
currentPage: presetListRef.value?.getCurrentPage?.() ?? 1,
|
currentPage: presetListRef.value?.getCurrentPage?.() ?? 1,
|
||||||
@@ -188,6 +235,8 @@ async function restorePresetManagerViewState() {
|
|||||||
searchQuery?: string;
|
searchQuery?: string;
|
||||||
selectedFolderId?: string | null;
|
selectedFolderId?: string | null;
|
||||||
expandedFolderIds?: string[];
|
expandedFolderIds?: string[];
|
||||||
|
sortField?: 'custom' | 'name' | 'updatedAt';
|
||||||
|
sortDirection?: 'asc' | 'desc';
|
||||||
sidebarScrollTop?: number;
|
sidebarScrollTop?: number;
|
||||||
listScrollTop?: number;
|
listScrollTop?: number;
|
||||||
currentPage?: number;
|
currentPage?: number;
|
||||||
@@ -199,6 +248,8 @@ async function restorePresetManagerViewState() {
|
|||||||
searchQuery.value = state.searchQuery ?? '';
|
searchQuery.value = state.searchQuery ?? '';
|
||||||
selectedFolderId.value = state.selectedFolderId ?? null;
|
selectedFolderId.value = state.selectedFolderId ?? null;
|
||||||
expandedFolderIds.value = new Set(state.expandedFolderIds ?? []);
|
expandedFolderIds.value = new Set(state.expandedFolderIds ?? []);
|
||||||
|
sortField.value = state.sortField ?? 'custom';
|
||||||
|
sortDirection.value = state.sortDirection ?? 'asc';
|
||||||
await nextTick();
|
await nextTick();
|
||||||
if (typeof state.currentPage === 'number') {
|
if (typeof state.currentPage === 'number') {
|
||||||
presetListRef.value?.setCurrentPage?.(state.currentPage, false);
|
presetListRef.value?.setCurrentPage?.(state.currentPage, false);
|
||||||
@@ -229,6 +280,14 @@ function handleToggleExpand(id: string) {
|
|||||||
expandedFolderIds.value = set;
|
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) {
|
function createFolder(parentId?: string) {
|
||||||
resetFolderForm();
|
resetFolderForm();
|
||||||
if (parentId) {
|
if (parentId) {
|
||||||
@@ -350,6 +409,10 @@ function toggleFavorite(preset: ExtendedPreset) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleReorderPresets(payload: { draggedId: string; targetId: string; side: 'before' | 'after' }) {
|
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 visibleIds = filteredPresets.value.map(preset => preset.id);
|
||||||
const from = visibleIds.indexOf(payload.draggedId);
|
const from = visibleIds.indexOf(payload.draggedId);
|
||||||
const target = visibleIds.indexOf(payload.targetId);
|
const target = visibleIds.indexOf(payload.targetId);
|
||||||
@@ -785,6 +848,8 @@ watch(
|
|||||||
selectedType.value,
|
selectedType.value,
|
||||||
searchQuery.value,
|
searchQuery.value,
|
||||||
selectedFolderId.value,
|
selectedFolderId.value,
|
||||||
|
sortField.value,
|
||||||
|
sortDirection.value,
|
||||||
Array.from(expandedFolderIds.value).sort().join('|'),
|
Array.from(expandedFolderIds.value).sort().join('|'),
|
||||||
],
|
],
|
||||||
() => {
|
() => {
|
||||||
@@ -848,6 +913,34 @@ onBeforeUnmount(() => {
|
|||||||
v-model="selectedType"
|
v-model="selectedType"
|
||||||
:options="filterOptions"
|
: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>
|
||||||
|
|
||||||
<div class="action-group">
|
<div class="action-group">
|
||||||
@@ -885,11 +978,15 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pm-content-area">
|
<div class="pm-content-area">
|
||||||
|
<div class="sort-hint">
|
||||||
|
{{ isCustomSort ? '当前为自定义排序,可直接拖拽卡片调整顺序。' : '当前为只读排序视图,切回自定义排序后可继续拖拽。' }}
|
||||||
|
</div>
|
||||||
<PresetList
|
<PresetList
|
||||||
ref="presetListRef"
|
ref="presetListRef"
|
||||||
:presets="filteredPresets"
|
:presets="filteredPresets"
|
||||||
:search-query="searchQuery"
|
:search-query="searchQuery"
|
||||||
:reset-key="presetListResetKey"
|
:reset-key="presetListResetKey"
|
||||||
|
:enable-reorder="isCustomSort"
|
||||||
@apply="applyPreset"
|
@apply="applyPreset"
|
||||||
@edit="editPreset"
|
@edit="editPreset"
|
||||||
@delete="deletePreset"
|
@delete="deletePreset"
|
||||||
@@ -1227,6 +1324,8 @@ onBeforeUnmount(() => {
|
|||||||
.filter-group {
|
.filter-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.type-select {
|
.type-select {
|
||||||
@@ -1237,6 +1336,66 @@ onBeforeUnmount(() => {
|
|||||||
color: var(--color-text-primary);
|
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 {
|
.action-group {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1257,6 +1416,12 @@ onBeforeUnmount(() => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sort-hint {
|
||||||
|
padding: 0.625rem 1rem 0;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const props = defineProps<{
|
|||||||
presets: ExtendedPreset[];
|
presets: ExtendedPreset[];
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
resetKey: string;
|
resetKey: string;
|
||||||
|
enableReorder?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -81,6 +82,10 @@ defineExpose({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function onDragStart(preset: ExtendedPreset, event: DragEvent) {
|
function onDragStart(preset: ExtendedPreset, event: DragEvent) {
|
||||||
|
if (!props.enableReorder) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
draggingPresetId.value = preset.id;
|
draggingPresetId.value = preset.id;
|
||||||
if (event.dataTransfer) {
|
if (event.dataTransfer) {
|
||||||
event.dataTransfer.effectAllowed = 'move';
|
event.dataTransfer.effectAllowed = 'move';
|
||||||
@@ -89,6 +94,7 @@ function onDragStart(preset: ExtendedPreset, event: DragEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onDragOver(preset: ExtendedPreset, event: DragEvent) {
|
function onDragOver(preset: ExtendedPreset, event: DragEvent) {
|
||||||
|
if (!props.enableReorder) return;
|
||||||
if (!draggingPresetId.value || draggingPresetId.value === preset.id) return;
|
if (!draggingPresetId.value || draggingPresetId.value === preset.id) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const target = event.currentTarget as HTMLElement | null;
|
const target = event.currentTarget as HTMLElement | null;
|
||||||
@@ -103,6 +109,10 @@ function onDragOver(preset: ExtendedPreset, event: DragEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onDrop(preset: ExtendedPreset, event: DragEvent) {
|
function onDrop(preset: ExtendedPreset, event: DragEvent) {
|
||||||
|
if (!props.enableReorder) {
|
||||||
|
cleanupDragState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!draggingPresetId.value || draggingPresetId.value === preset.id || !dropSide.value) {
|
if (!draggingPresetId.value || draggingPresetId.value === preset.id || !dropSide.value) {
|
||||||
cleanupDragState();
|
cleanupDragState();
|
||||||
return;
|
return;
|
||||||
@@ -209,11 +219,12 @@ function formatDate(dateStr: string) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="preset-grid">
|
<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="{
|
:class="{
|
||||||
dragging: draggingPresetId === preset.id,
|
dragging: draggingPresetId === preset.id,
|
||||||
'insert-before': overPresetId === preset.id && dropSide === 'before' && 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)"
|
@dragstart="onDragStart(preset, $event)" @dragover="onDragOver(preset, $event)"
|
||||||
@drop="onDrop(preset, $event)" @dragend="cleanupDragState">
|
@drop="onDrop(preset, $event)" @dragend="cleanupDragState">
|
||||||
@@ -379,6 +390,10 @@ function formatDate(dateStr: string) {
|
|||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preset-card.reorder-disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
.preset-card.insert-before {
|
.preset-card.insert-before {
|
||||||
border-top: 3px solid var(--color-accent);
|
border-top: 3px solid var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user