增加页面切换状态保存
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, nextTick, onBeforeUnmount, onMounted, watch } from 'vue';
|
||||||
import { usePromptStore } from '../stores/promptStore';
|
import { usePromptStore } from '../stores/promptStore';
|
||||||
import type { ExtendedPreset, PresetFolder, PresetType } from '../types';
|
import type { ExtendedPreset, PresetFolder, PresetType } from '../types';
|
||||||
import NotificationToast from './NotificationToast.vue';
|
import NotificationToast from './NotificationToast.vue';
|
||||||
@@ -9,6 +9,7 @@ import FolderSelector from './preset/FolderSelector.vue';
|
|||||||
import TypeSelector from './preset/TypeSelector.vue';
|
import TypeSelector from './preset/TypeSelector.vue';
|
||||||
|
|
||||||
const store = usePromptStore();
|
const store = usePromptStore();
|
||||||
|
const PRESET_MANAGER_VIEW_STATE_KEY = 'preset-manager-view-state';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const activeTab = ref<'presets' | 'folders'>('presets'); // Kept for compatibility if needed, but UI will be unified
|
const activeTab = ref<'presets' | 'folders'>('presets'); // Kept for compatibility if needed, but UI will be unified
|
||||||
@@ -16,6 +17,9 @@ 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 presetSidebarRef = ref<InstanceType<typeof PresetSidebar> | null>(null);
|
||||||
|
const presetListRef = ref<InstanceType<typeof PresetList> | null>(null);
|
||||||
|
const isRestoringViewState = ref(false);
|
||||||
|
|
||||||
// Dialog State
|
// Dialog State
|
||||||
const showMobileSidebar = ref(false);
|
const showMobileSidebar = ref(false);
|
||||||
@@ -88,7 +92,7 @@ const filterOptions = computed<{ value: PresetType | 'all'; label: string }[]>((
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const filteredPresets = computed(() => {
|
const filteredPresets = computed(() => {
|
||||||
let presets = store.extendedPresets || [];
|
let presets = [...(store.extendedPresets || [])];
|
||||||
|
|
||||||
// Filter by Type
|
// Filter by Type
|
||||||
if (selectedType.value !== 'all') {
|
if (selectedType.value !== 'all') {
|
||||||
@@ -117,8 +121,19 @@ const filteredPresets = computed(() => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return presets.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
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 presetListResetKey = computed(() => JSON.stringify({
|
||||||
|
selectedType: selectedType.value,
|
||||||
|
searchQuery: searchQuery.value,
|
||||||
|
selectedFolderId: selectedFolderId.value,
|
||||||
|
}));
|
||||||
|
|
||||||
const flattenedFolders = computed(() => {
|
const flattenedFolders = computed(() => {
|
||||||
type FlatItem = { id: string; name: string; label: string; level: number; presetCount: number; hasChildren: boolean };
|
type FlatItem = { id: string; name: string; label: string; level: number; presetCount: number; hasChildren: boolean };
|
||||||
@@ -147,6 +162,62 @@ const allPresetsCount = computed(() => (store.extendedPresets || []).length);
|
|||||||
const uncategorizedCount = computed(() => (store.extendedPresets || []).filter(p => !p.folderId).length);
|
const uncategorizedCount = computed(() => (store.extendedPresets || []).filter(p => !p.folderId).length);
|
||||||
const favoritesCount = computed(() => (store.extendedPresets || []).filter(p => p.isFavorite).length);
|
const favoritesCount = computed(() => (store.extendedPresets || []).filter(p => p.isFavorite).length);
|
||||||
|
|
||||||
|
function persistPresetManagerViewState() {
|
||||||
|
if (isRestoringViewState.value) return;
|
||||||
|
const payload = {
|
||||||
|
activeTab: activeTab.value,
|
||||||
|
selectedType: selectedType.value,
|
||||||
|
searchQuery: searchQuery.value,
|
||||||
|
selectedFolderId: selectedFolderId.value,
|
||||||
|
expandedFolderIds: Array.from(expandedFolderIds.value),
|
||||||
|
sidebarScrollTop: presetSidebarRef.value?.contentRef?.scrollTop ?? 0,
|
||||||
|
listScrollTop: presetListRef.value?.containerRef?.scrollTop ?? 0,
|
||||||
|
currentPage: presetListRef.value?.getCurrentPage?.() ?? 1,
|
||||||
|
};
|
||||||
|
window.sessionStorage.setItem(PRESET_MANAGER_VIEW_STATE_KEY, JSON.stringify(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restorePresetManagerViewState() {
|
||||||
|
const raw = window.sessionStorage.getItem(PRESET_MANAGER_VIEW_STATE_KEY);
|
||||||
|
if (!raw) return;
|
||||||
|
try {
|
||||||
|
isRestoringViewState.value = true;
|
||||||
|
const state = JSON.parse(raw) as {
|
||||||
|
activeTab?: 'presets' | 'folders';
|
||||||
|
selectedType?: PresetType | 'all';
|
||||||
|
searchQuery?: string;
|
||||||
|
selectedFolderId?: string | null;
|
||||||
|
expandedFolderIds?: string[];
|
||||||
|
sidebarScrollTop?: number;
|
||||||
|
listScrollTop?: number;
|
||||||
|
currentPage?: number;
|
||||||
|
};
|
||||||
|
if (state.activeTab === 'presets' || state.activeTab === 'folders') {
|
||||||
|
activeTab.value = state.activeTab;
|
||||||
|
}
|
||||||
|
selectedType.value = state.selectedType ?? 'all';
|
||||||
|
searchQuery.value = state.searchQuery ?? '';
|
||||||
|
selectedFolderId.value = state.selectedFolderId ?? null;
|
||||||
|
expandedFolderIds.value = new Set(state.expandedFolderIds ?? []);
|
||||||
|
await nextTick();
|
||||||
|
if (typeof state.currentPage === 'number') {
|
||||||
|
presetListRef.value?.setCurrentPage?.(state.currentPage, false);
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
if (presetSidebarRef.value?.contentRef) {
|
||||||
|
presetSidebarRef.value.contentRef.scrollTop = Math.max(0, state.sidebarScrollTop ?? 0);
|
||||||
|
}
|
||||||
|
if (presetListRef.value?.containerRef) {
|
||||||
|
presetListRef.value.containerRef.scrollTop = Math.max(0, state.listScrollTop ?? 0);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore broken persisted state and continue with defaults.
|
||||||
|
} finally {
|
||||||
|
isRestoringViewState.value = false;
|
||||||
|
persistPresetManagerViewState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
function handleFolderSelect(id: string | null) {
|
function handleFolderSelect(id: string | null) {
|
||||||
selectedFolderId.value = id;
|
selectedFolderId.value = id;
|
||||||
@@ -278,6 +349,28 @@ function toggleFavorite(preset: ExtendedPreset) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleReorderPresets(payload: { draggedId: string; targetId: string; side: 'before' | 'after' }) {
|
||||||
|
const visibleIds = filteredPresets.value.map(preset => preset.id);
|
||||||
|
const from = visibleIds.indexOf(payload.draggedId);
|
||||||
|
const target = visibleIds.indexOf(payload.targetId);
|
||||||
|
if (from === -1 || target === -1) return;
|
||||||
|
const reordered = [...visibleIds];
|
||||||
|
const [draggedId] = reordered.splice(from, 1);
|
||||||
|
if (!draggedId) return;
|
||||||
|
let insertAt = target;
|
||||||
|
if (payload.side === 'after') {
|
||||||
|
insertAt += from < target ? 0 : 1;
|
||||||
|
} else if (from < target) {
|
||||||
|
insertAt -= 1;
|
||||||
|
}
|
||||||
|
insertAt = Math.max(0, Math.min(insertAt, reordered.length));
|
||||||
|
reordered.splice(insertAt, 0, draggedId);
|
||||||
|
if (store.reorderExtendedPresets(reordered)) {
|
||||||
|
persistPresetManagerViewState();
|
||||||
|
showNotification('已调整预设顺序', 'success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function copyPresetContent(preset: ExtendedPreset) {
|
async function copyPresetContent(preset: ExtendedPreset) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(preset.content);
|
await navigator.clipboard.writeText(preset.content);
|
||||||
@@ -681,6 +774,26 @@ function closeFolderDialog() {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
store.initializeExtendedPresets();
|
store.initializeExtendedPresets();
|
||||||
|
nextTick(() => {
|
||||||
|
restorePresetManagerViewState();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [
|
||||||
|
activeTab.value,
|
||||||
|
selectedType.value,
|
||||||
|
searchQuery.value,
|
||||||
|
selectedFolderId.value,
|
||||||
|
Array.from(expandedFolderIds.value).sort().join('|'),
|
||||||
|
],
|
||||||
|
() => {
|
||||||
|
persistPresetManagerViewState();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
persistPresetManagerViewState();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -693,6 +806,7 @@ onMounted(() => {
|
|||||||
<div class="pm-sidebar" :class="{ 'mobile-open': showMobileSidebar }">
|
<div class="pm-sidebar" :class="{ 'mobile-open': showMobileSidebar }">
|
||||||
<button v-if="showMobileSidebar" class="mobile-sidebar-close" @click="showMobileSidebar = false">×</button>
|
<button v-if="showMobileSidebar" class="mobile-sidebar-close" @click="showMobileSidebar = false">×</button>
|
||||||
<PresetSidebar
|
<PresetSidebar
|
||||||
|
ref="presetSidebarRef"
|
||||||
:folder-tree="folderTree"
|
:folder-tree="folderTree"
|
||||||
:selected-folder-id="selectedFolderId"
|
:selected-folder-id="selectedFolderId"
|
||||||
:expanded-ids="expandedFolderIds"
|
:expanded-ids="expandedFolderIds"
|
||||||
@@ -706,6 +820,7 @@ onMounted(() => {
|
|||||||
@edit-folder="editFolder"
|
@edit-folder="editFolder"
|
||||||
@delete-folder="deleteFolder"
|
@delete-folder="deleteFolder"
|
||||||
@share-folder="handleShareFolder"
|
@share-folder="handleShareFolder"
|
||||||
|
@view-state-change="persistPresetManagerViewState"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -771,20 +886,24 @@ onMounted(() => {
|
|||||||
|
|
||||||
<div class="pm-content-area">
|
<div class="pm-content-area">
|
||||||
<PresetList
|
<PresetList
|
||||||
|
ref="presetListRef"
|
||||||
:presets="filteredPresets"
|
:presets="filteredPresets"
|
||||||
:search-query="searchQuery"
|
:search-query="searchQuery"
|
||||||
|
:reset-key="presetListResetKey"
|
||||||
@apply="applyPreset"
|
@apply="applyPreset"
|
||||||
@edit="editPreset"
|
@edit="editPreset"
|
||||||
@delete="deletePreset"
|
@delete="deletePreset"
|
||||||
@copy="copyPresetContent"
|
@copy="copyPresetContent"
|
||||||
@share="handleShare"
|
@share="handleShare"
|
||||||
@toggle-favorite="toggleFavorite"
|
@toggle-favorite="toggleFavorite"
|
||||||
|
@reorder="handleReorderPresets"
|
||||||
|
@view-state-change="persistPresetManagerViewState"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Create/Edit Preset Modal -->
|
<!-- Create/Edit Preset Modal -->
|
||||||
<div v-if="showCreateDialog" class="modal-overlay" @click.self="closePresetDialog">
|
<div v-if="showCreateDialog" class="modal-overlay">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3>{{ editingPreset ? '编辑预设' : '新建预设' }}</h3>
|
<h3>{{ editingPreset ? '编辑预设' : '新建预设' }}</h3>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
import { usePromptStore } from '../stores/promptStore';
|
import { usePromptStore } from '../stores/promptStore';
|
||||||
import type { PromptTag } from '../types';
|
import type { PromptTag } from '../types';
|
||||||
|
|
||||||
@@ -17,19 +17,23 @@ const filteredTags = computed(() => store.filteredTags);
|
|||||||
const selectedLang = computed(() => store.selectedLang);
|
const selectedLang = computed(() => store.selectedLang);
|
||||||
|
|
||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
|
const QUICK_ADD_STATE_KEY = 'prompt-quick-add-view-state';
|
||||||
const visibleCount = ref(PAGE_SIZE);
|
const visibleCount = ref(PAGE_SIZE);
|
||||||
const tagsContainer = ref<HTMLElement | null>(null);
|
const tagsContainer = ref<HTMLElement | null>(null);
|
||||||
const draggedTagKey = ref<string | null>(null);
|
const draggedTagKey = ref<string | null>(null);
|
||||||
|
const isRestoringState = ref(false);
|
||||||
|
|
||||||
const visibleTags = computed(() => {
|
const visibleTags = computed(() => {
|
||||||
return filteredTags.value.slice(0, visibleCount.value);
|
return filteredTags.value.slice(0, visibleCount.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => filteredTags.value, () => {
|
watch(() => filteredTags.value, () => {
|
||||||
|
if (isRestoringState.value) return;
|
||||||
visibleCount.value = PAGE_SIZE;
|
visibleCount.value = PAGE_SIZE;
|
||||||
if (tagsContainer.value) {
|
if (tagsContainer.value) {
|
||||||
tagsContainer.value.scrollTop = 0;
|
tagsContainer.value.scrollTop = 0;
|
||||||
}
|
}
|
||||||
|
persistQuickAddState();
|
||||||
});
|
});
|
||||||
|
|
||||||
function onScroll() {
|
function onScroll() {
|
||||||
@@ -41,14 +45,17 @@ function onScroll() {
|
|||||||
visibleCount.value += PAGE_SIZE;
|
visibleCount.value += PAGE_SIZE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
persistQuickAddState();
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectCategory(index: number) {
|
function selectCategory(index: number) {
|
||||||
store.selectCategory(index);
|
store.selectCategory(index);
|
||||||
|
persistQuickAddState();
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectGroup(index: number) {
|
function selectGroup(index: number) {
|
||||||
store.selectGroup(index);
|
store.selectGroup(index);
|
||||||
|
persistQuickAddState();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTagClick(tag: PromptTag) {
|
function onTagClick(tag: PromptTag) {
|
||||||
@@ -75,6 +82,63 @@ function onTagDragEnd() {
|
|||||||
function displayTrans(tag: PromptTag) {
|
function displayTrans(tag: PromptTag) {
|
||||||
return tag.translation?.[selectedLang.value] ?? tag.key;
|
return tag.translation?.[selectedLang.value] ?? tag.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function persistQuickAddState() {
|
||||||
|
const payload = {
|
||||||
|
categoryIndex: store.selectedCategoryIndex,
|
||||||
|
groupIndex: store.selectedGroupIndex,
|
||||||
|
visibleCount: visibleCount.value,
|
||||||
|
scrollTop: tagsContainer.value?.scrollTop ?? 0,
|
||||||
|
};
|
||||||
|
window.sessionStorage.setItem(QUICK_ADD_STATE_KEY, JSON.stringify(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreQuickAddState() {
|
||||||
|
const raw = window.sessionStorage.getItem(QUICK_ADD_STATE_KEY);
|
||||||
|
if (!raw) return;
|
||||||
|
try {
|
||||||
|
const state = JSON.parse(raw) as {
|
||||||
|
categoryIndex?: number;
|
||||||
|
groupIndex?: number;
|
||||||
|
visibleCount?: number;
|
||||||
|
scrollTop?: number;
|
||||||
|
};
|
||||||
|
isRestoringState.value = true;
|
||||||
|
const categoryCount = categories.value.length;
|
||||||
|
const nextCategoryIndex = Math.min(Math.max(state.categoryIndex ?? 0, 0), Math.max(categoryCount - 1, 0));
|
||||||
|
store.selectCategory(nextCategoryIndex);
|
||||||
|
const groupCount = currentCategory.value?.groups.length ?? 0;
|
||||||
|
const nextGroupIndex = Math.min(Math.max(state.groupIndex ?? 0, 0), Math.max(groupCount - 1, 0));
|
||||||
|
store.selectGroup(nextGroupIndex);
|
||||||
|
visibleCount.value = Math.max(PAGE_SIZE, state.visibleCount ?? PAGE_SIZE);
|
||||||
|
await nextTick();
|
||||||
|
if (tagsContainer.value) {
|
||||||
|
tagsContainer.value.scrollTop = Math.max(0, state.scrollTop ?? 0);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore broken persisted state and continue with defaults.
|
||||||
|
} finally {
|
||||||
|
isRestoringState.value = false;
|
||||||
|
persistQuickAddState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [store.selectedCategoryIndex, store.selectedGroupIndex, visibleCount.value],
|
||||||
|
() => {
|
||||||
|
if (!isRestoringState.value) {
|
||||||
|
persistQuickAddState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
restoreQuickAddState();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
persistQuickAddState();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import IconArrowRight from '../icons/IconArrowRight.vue';
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
presets: ExtendedPreset[];
|
presets: ExtendedPreset[];
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
|
resetKey: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -17,12 +18,17 @@ const emit = defineEmits<{
|
|||||||
(e: 'copy', preset: ExtendedPreset): void;
|
(e: 'copy', preset: ExtendedPreset): void;
|
||||||
(e: 'share', preset: ExtendedPreset): void;
|
(e: 'share', preset: ExtendedPreset): void;
|
||||||
(e: 'toggle-favorite', preset: ExtendedPreset): void;
|
(e: 'toggle-favorite', preset: ExtendedPreset): void;
|
||||||
|
(e: 'reorder', payload: { draggedId: string; targetId: string; side: 'before' | 'after' }): void;
|
||||||
|
(e: 'view-state-change'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Pagination Logic
|
// Pagination Logic
|
||||||
const PAGE_SIZE = 24;
|
const PAGE_SIZE = 24;
|
||||||
const currentPage = ref(1);
|
const currentPage = ref(1);
|
||||||
const containerRef = ref<HTMLElement | null>(null);
|
const containerRef = ref<HTMLElement | null>(null);
|
||||||
|
const draggingPresetId = ref<string | null>(null);
|
||||||
|
const overPresetId = ref<string | null>(null);
|
||||||
|
const dropSide = ref<'before' | 'after' | null>(null);
|
||||||
|
|
||||||
const totalPages = computed(() => Math.ceil(props.presets.length / PAGE_SIZE));
|
const totalPages = computed(() => Math.ceil(props.presets.length / PAGE_SIZE));
|
||||||
|
|
||||||
@@ -32,11 +38,19 @@ const displayedPresets = computed(() => {
|
|||||||
return props.presets.slice(start, end);
|
return props.presets.slice(start, end);
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => props.presets, () => {
|
watch(() => props.resetKey, () => {
|
||||||
currentPage.value = 1;
|
currentPage.value = 1;
|
||||||
if (containerRef.value) {
|
if (containerRef.value) {
|
||||||
containerRef.value.scrollTop = 0;
|
containerRef.value.scrollTop = 0;
|
||||||
}
|
}
|
||||||
|
emit('view-state-change');
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.presets.length, () => {
|
||||||
|
const fallbackMax = totalPages.value || 1;
|
||||||
|
if (currentPage.value > fallbackMax) {
|
||||||
|
currentPage.value = fallbackMax;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function changePage(page: number) {
|
function changePage(page: number) {
|
||||||
@@ -45,6 +59,67 @@ function changePage(page: number) {
|
|||||||
if (containerRef.value) {
|
if (containerRef.value) {
|
||||||
containerRef.value.scrollTop = 0;
|
containerRef.value.scrollTop = 0;
|
||||||
}
|
}
|
||||||
|
emit('view-state-change');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCurrentPage(page: number, resetScroll = false) {
|
||||||
|
const fallbackMax = totalPages.value || 1;
|
||||||
|
currentPage.value = Math.min(Math.max(page, 1), fallbackMax);
|
||||||
|
if (resetScroll && containerRef.value) {
|
||||||
|
containerRef.value.scrollTop = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentPage() {
|
||||||
|
return currentPage.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
containerRef,
|
||||||
|
setCurrentPage,
|
||||||
|
getCurrentPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
function onDragStart(preset: ExtendedPreset, event: DragEvent) {
|
||||||
|
draggingPresetId.value = preset.id;
|
||||||
|
if (event.dataTransfer) {
|
||||||
|
event.dataTransfer.effectAllowed = 'move';
|
||||||
|
event.dataTransfer.setData('text/plain', preset.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragOver(preset: ExtendedPreset, event: DragEvent) {
|
||||||
|
if (!draggingPresetId.value || draggingPresetId.value === preset.id) return;
|
||||||
|
event.preventDefault();
|
||||||
|
const target = event.currentTarget as HTMLElement | null;
|
||||||
|
if (!target) return;
|
||||||
|
const rect = target.getBoundingClientRect();
|
||||||
|
const side = event.clientY < rect.top + rect.height / 2 ? 'before' : 'after';
|
||||||
|
overPresetId.value = preset.id;
|
||||||
|
dropSide.value = side;
|
||||||
|
if (event.dataTransfer) {
|
||||||
|
event.dataTransfer.dropEffect = 'move';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrop(preset: ExtendedPreset, event: DragEvent) {
|
||||||
|
if (!draggingPresetId.value || draggingPresetId.value === preset.id || !dropSide.value) {
|
||||||
|
cleanupDragState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
emit('reorder', {
|
||||||
|
draggedId: draggingPresetId.value,
|
||||||
|
targetId: preset.id,
|
||||||
|
side: dropSide.value,
|
||||||
|
});
|
||||||
|
cleanupDragState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupDragState() {
|
||||||
|
draggingPresetId.value = null;
|
||||||
|
overPresetId.value = null;
|
||||||
|
dropSide.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTypeLabel(type: PresetType) {
|
function getTypeLabel(type: PresetType) {
|
||||||
@@ -66,7 +141,7 @@ function formatDate(dateStr: string) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="preset-list-container" ref="containerRef">
|
<div class="preset-list-container" ref="containerRef" @scroll="emit('view-state-change')">
|
||||||
<div v-if="presets.length === 0" class="empty-state">
|
<div v-if="presets.length === 0" class="empty-state">
|
||||||
<div class="empty-icon">📭</div>
|
<div class="empty-icon">📭</div>
|
||||||
<p class="empty-text">暂无预设</p>
|
<p class="empty-text">暂无预设</p>
|
||||||
@@ -74,7 +149,14 @@ function formatDate(dateStr: string) {
|
|||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="preset-grid">
|
<div class="preset-grid">
|
||||||
<div v-for="preset in displayedPresets" :key="preset.id" class="preset-card nav-btn">
|
<div v-for="preset in displayedPresets" :key="preset.id" class="preset-card nav-btn" draggable="true"
|
||||||
|
: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
|
||||||
|
}"
|
||||||
|
@dragstart="onDragStart(preset, $event)" @dragover="onDragOver(preset, $event)"
|
||||||
|
@drop="onDrop(preset, $event)" @dragend="cleanupDragState">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="preset-type" :title="getTypeLabel(preset.type)">
|
<div class="preset-type" :title="getTypeLabel(preset.type)">
|
||||||
<IconPresetType :type="preset.type" width="24" height="24" />
|
<IconPresetType :type="preset.type" width="24" height="24" />
|
||||||
@@ -221,6 +303,7 @@ function formatDate(dateStr: string) {
|
|||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preset-card:hover {
|
.preset-card:hover {
|
||||||
@@ -229,6 +312,19 @@ function formatDate(dateStr: string) {
|
|||||||
border-color: var(--color-border-hover);
|
border-color: var(--color-border-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preset-card.dragging {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-card.insert-before {
|
||||||
|
border-top: 3px solid var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-card.insert-after {
|
||||||
|
border-bottom: 3px solid var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue';
|
import { ref } from 'vue';
|
||||||
import FolderTreeItem from './FolderTreeItem.vue';
|
import FolderTreeItem from './FolderTreeItem.vue';
|
||||||
import type { PresetFolder } from '../../types';
|
import type { PresetFolder } from '../../types';
|
||||||
|
|
||||||
@@ -25,11 +25,18 @@ const emit = defineEmits<{
|
|||||||
(e: 'edit-folder', folder: PresetFolder): void;
|
(e: 'edit-folder', folder: PresetFolder): void;
|
||||||
(e: 'delete-folder', folder: PresetFolder): void;
|
(e: 'delete-folder', folder: PresetFolder): void;
|
||||||
(e: 'share-folder', folder: PresetFolder): void;
|
(e: 'share-folder', folder: PresetFolder): void;
|
||||||
|
(e: 'view-state-change'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
function selectFolder(id: string | null) {
|
function selectFolder(id: string | null) {
|
||||||
emit('update:selectedFolderId', id);
|
emit('update:selectedFolderId', id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contentRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
contentRef,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -45,7 +52,7 @@ function selectFolder(id: string | null) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content" ref="contentRef" @scroll="emit('view-state-change')">
|
||||||
<!-- 固定选项 -->
|
<!-- 固定选项 -->
|
||||||
<div class="system-folders">
|
<div class="system-folders">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -833,6 +833,20 @@ export const usePromptStore = defineStore('promptStore', {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalized = [...this.extendedPresets].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();
|
||||||
|
});
|
||||||
|
let changed = false;
|
||||||
|
normalized.forEach((preset, index) => {
|
||||||
|
if (preset.sortOrder !== index) {
|
||||||
|
preset.sortOrder = index;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 确保有默认文件夹
|
// 确保有默认文件夹
|
||||||
if (this.presetFolders.length === 0) {
|
if (this.presetFolders.length === 0) {
|
||||||
const defaultFolder = this.createPresetFolder({
|
const defaultFolder = this.createPresetFolder({
|
||||||
@@ -845,15 +859,25 @@ export const usePromptStore = defineStore('promptStore', {
|
|||||||
if (this.presetManagement.settings) {
|
if (this.presetManagement.settings) {
|
||||||
this.presetManagement.settings.defaultFolder = defaultFolder.id;
|
this.presetManagement.settings.defaultFolder = defaultFolder.id;
|
||||||
}
|
}
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
this.save();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
createExtendedPreset(data: Omit<ExtendedPreset, 'id' | 'createdAt' | 'updatedAt'>) {
|
createExtendedPreset(data: Omit<ExtendedPreset, 'id' | 'createdAt' | 'updatedAt'>) {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
const nextSortOrder = this.extendedPresets.reduce((max, preset) => {
|
||||||
|
const value = typeof preset.sortOrder === 'number' ? preset.sortOrder : -1;
|
||||||
|
return Math.max(max, value);
|
||||||
|
}, -1) + 1;
|
||||||
const preset: ExtendedPreset = {
|
const preset: ExtendedPreset = {
|
||||||
id: `preset_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
|
id: `preset_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
|
sortOrder: nextSortOrder,
|
||||||
...data
|
...data
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -876,6 +900,41 @@ export const usePromptStore = defineStore('promptStore', {
|
|||||||
if (index === -1) return false;
|
if (index === -1) return false;
|
||||||
|
|
||||||
this.extendedPresets.splice(index, 1);
|
this.extendedPresets.splice(index, 1);
|
||||||
|
this.extendedPresets
|
||||||
|
.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
|
||||||
|
.forEach((preset, idx) => {
|
||||||
|
preset.sortOrder = idx;
|
||||||
|
});
|
||||||
|
this.save();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
reorderExtendedPresets(orderedIds: string[]) {
|
||||||
|
if (!orderedIds.length) return false;
|
||||||
|
const idSet = new Set(orderedIds);
|
||||||
|
const ordered = [...this.extendedPresets].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 replacements = ordered.filter(preset => idSet.has(preset.id));
|
||||||
|
if (replacements.length !== orderedIds.length) return false;
|
||||||
|
const replacementMap = new Map(replacements.map(preset => [preset.id, preset]));
|
||||||
|
const reorderedVisible = orderedIds
|
||||||
|
.map(id => replacementMap.get(id))
|
||||||
|
.filter((preset): preset is ExtendedPreset => !!preset);
|
||||||
|
|
||||||
|
let visibleIndex = 0;
|
||||||
|
const merged = ordered.map(preset => {
|
||||||
|
if (!idSet.has(preset.id)) return preset;
|
||||||
|
const nextPreset = reorderedVisible[visibleIndex];
|
||||||
|
visibleIndex += 1;
|
||||||
|
return nextPreset ?? preset;
|
||||||
|
});
|
||||||
|
merged.forEach((preset, index2) => {
|
||||||
|
preset.sortOrder = index2;
|
||||||
|
});
|
||||||
this.save();
|
this.save();
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export interface ExtendedPreset {
|
|||||||
folderId?: string;
|
folderId?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
sortOrder?: number;
|
||||||
isPublic?: boolean;
|
isPublic?: boolean;
|
||||||
author?: string;
|
author?: string;
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user