diff --git a/src/App.vue b/src/App.vue index 17c569a..e8b922b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,9 +2,10 @@ import { ref, onMounted } from 'vue' import PromptEditor from './components/PromptEditor.vue' import PromptManager from './components/PromptManager.vue' +import PresetManager from './components/PresetManager.vue' import { usePromptStore } from './stores/promptStore' -const currentView = ref<'editor' | 'manager'>('editor') +const currentView = ref<'editor' | 'manager' | 'presets'>('editor') const isDark = ref(false) const store = usePromptStore() @@ -27,7 +28,7 @@ function updateTheme() { document.documentElement.classList.toggle('dark', isDark.value) } -function switchView(view: 'editor' | 'manager') { +function switchView(view: 'editor' | 'manager' | 'presets') { currentView.value = view } @@ -63,7 +64,7 @@ function switchView(view: 'editor' | 'manager') { - 编辑器 + 编辑器 + @@ -95,7 +108,8 @@ function switchView(view: 'editor' | 'manager') {
- + +
diff --git a/src/components/PresetDropdown.vue b/src/components/PresetDropdown.vue new file mode 100644 index 0000000..51dd98d --- /dev/null +++ b/src/components/PresetDropdown.vue @@ -0,0 +1,1031 @@ + + + + + diff --git a/src/components/PresetManager.vue b/src/components/PresetManager.vue new file mode 100644 index 0000000..a751ff7 --- /dev/null +++ b/src/components/PresetManager.vue @@ -0,0 +1,1130 @@ + + + + + diff --git a/src/components/PromptEditor.vue b/src/components/PromptEditor.vue index 7878ffc..305efe7 100644 --- a/src/components/PromptEditor.vue +++ b/src/components/PromptEditor.vue @@ -3,6 +3,7 @@ import { onMounted, onUnmounted, ref, computed, nextTick, watch } from 'vue'; import { usePromptStore } from '../stores/promptStore'; import type { LangCode } from '../types'; import NotificationToast from './NotificationToast.vue'; +import PresetDropdown from './PresetDropdown.vue'; const store = usePromptStore(); const draggingIndex = ref(null); @@ -14,9 +15,6 @@ const editingValue = ref(''); const addingMapIndex = ref(null); const addingMapValue = ref(''); const presetName = ref(''); -const presetSearch = ref(''); -const renamingPreset = ref(null); -const renamingValue = ref(''); const viewMode = ref<'compact' | 'detail'>('compact'); const showPresetDropdown = ref(false); const notification = ref<{ message: string; type: 'success' | 'error' | 'info'; show: boolean }>({ @@ -35,7 +33,8 @@ function showNotification(message: string, type: 'success' | 'error' | 'info' = // 点击外部关闭下拉菜单 function handleClickOutside(event: Event) { const target = event.target as HTMLElement; - if (!target.closest('.pe-presets')) { + // 检查点击是否在预设下拉区域内,包括重命名输入框和按钮 + if (!target.closest('.pe-presets') && !target.closest('.pd-dropdown')) { showPresetDropdown.value = false; } } @@ -55,12 +54,6 @@ const selectedLang = computed({ }); const tokens = computed(() => store.tokens); -const filteredPresets = computed(() => { - const q = presetSearch.value.trim().toLowerCase(); - const list = [...store.presets].sort((a, b) => (a.updatedAt < b.updatedAt ? 1 : -1)); - if (!q) return list; - return list.filter((p) => p.name.toLowerCase().includes(q)); -}); const suggestions = ref([]); const inputEl = ref(null); @@ -278,31 +271,52 @@ function savePreset() { showNotification('请输入预设名称', 'error'); return; } - store.savePreset(presetName.value.trim()); - showNotification(`预设「${presetName.value.trim()}」已保存`, 'success'); + + const name = presetName.value.trim(); + + // 只保存到新的扩展预设系统 + const defaultFolder = store.presetManagement?.settings?.defaultFolder; + store.createExtendedPreset({ + name: name, + type: 'positive', + content: store.promptText, + description: '从编辑器快速保存', + folderId: defaultFolder + }); + + showNotification(`预设「${name}」已保存到预设管理`, 'success'); presetName.value = ''; } -function loadPreset(name: string) { - store.loadPreset(name); - text.value = store.promptText; + +// 预设下拉组件的事件处理 +function handlePresetLoad(name: string) { + // 优先从扩展预设中查找 + const extendedPreset = store.extendedPresets.find(p => p.name === name); + if (extendedPreset) { + store.setPromptTextRaw(extendedPreset.content); + text.value = extendedPreset.content; + } else { + // 回退到旧预设系统 + store.loadPreset(name); + text.value = store.promptText; + } showNotification(`已加载预设「${name}」`, 'success'); } -function deletePreset(name: string) { - if (confirm(`确定删除预设「${name}」吗?`)) { - store.deletePreset(name); - showNotification(`预设「${name}」已删除`, 'info'); - } + +function handlePresetSave(name: string) { + store.savePreset(name); + showNotification(`预设「${name}」已保存`, 'success'); } -function beginRename(name: string) { renamingPreset.value = name; renamingValue.value = name; } -function commitRename() { - if (!renamingPreset.value) return; - const oldName = renamingPreset.value; - const newName = renamingValue.value.trim(); - if (!newName) { alert('预设名称不能为空'); return; } + +function handlePresetDelete(name: string) { + store.deletePreset(name); + showNotification(`预设「${name}」已删除`, 'info'); +} + +function handlePresetRename(oldName: string, newName: string) { store.renamePreset(oldName, newName); - renamingPreset.value = null; renamingValue.value = ''; + showNotification(`预设已重命名为「${newName}」`, 'success'); } -function cancelRename() { renamingPreset.value = null; } async function applySuggestion(s: string) { const el = inputEl.value; @@ -355,12 +369,12 @@ function displayTrans(key: string): string { - -
-
- -
-
-
- - -
-
-
- {{ presetSearch ? '未找到匹配的预设' : '暂无预设' }} -
-
-
+ @@ -437,6 +434,7 @@ function displayTrans(key: string): string {
  • {{ s }}
  • +
    提示词映射 @@ -1703,6 +1701,7 @@ function displayTrans(key: string): string { background: var(--color-text-tertiary); } + /* 保证按钮内图标不压缩文本,提升对齐与可读性 */ .pe-left button svg, .pe-right button svg, diff --git a/src/stores/promptStore.ts b/src/stores/promptStore.ts index dc959bc..e1b49d6 100644 --- a/src/stores/promptStore.ts +++ b/src/stores/promptStore.ts @@ -1,6 +1,6 @@ import { defineStore } from 'pinia'; import { loadInitialDataset } from '../utils/yamlLoader'; -import type { PromptDataset, PromptCategory, PromptGroup, PromptTag, LangCode, ExportBundle, CustomDiff, PromptPreset } from '../types'; +import type { PromptDataset, PromptCategory, PromptGroup, PromptTag, LangCode, ExportBundle, CustomDiff, PromptPreset, ExtendedPreset, PresetFolder, PresetManagement, PresetType } from '../types'; const LS_KEY = 'ops.prompt.dataset.v1'; let saveTimer: number | null = null; // 非响应式计时器,避免递归更新 @@ -20,6 +20,17 @@ export const usePromptStore = defineStore('promptStore', { // 编辑器相关 promptText: '', presets: [] as PromptPreset[], + // 扩展预设管理 + extendedPresets: [] as ExtendedPreset[], + presetFolders: [] as PresetFolder[], + presetManagement: { + folders: [], + presets: [], + settings: { + autoBackup: true, + maxPresets: 1000 + } + } as PresetManagement, }), getters: { categories: (s) => s.dataset?.categories ?? [], @@ -68,6 +79,12 @@ export const usePromptStore = defineStore('promptStore', { this.dataset = deepClone(baseline!); } this.presets = bundle.presets || []; + // 恢复扩展预设数据 + this.extendedPresets = bundle.extendedPresets || []; + this.presetFolders = bundle.presetFolders || []; + if (bundle.presetManagement) { + this.presetManagement = bundle.presetManagement; + } // 恢复编辑器内容与语言 if (typeof bundle.promptText === 'string') { this.promptText = bundle.promptText; @@ -86,6 +103,10 @@ export const usePromptStore = defineStore('promptStore', { const guessLang: LangCode = (this.dataset.languages.includes('zh_CN') ? 'zh_CN' : 'en') as LangCode; this.selectedLang = guessLang; } + // 初始化扩展预设管理 + this.initializeExtendedPresets(); + // 自动迁移旧预设到新系统 + this.migrateOldPresets(); this.autoPersist(); }, autoPersist() { @@ -104,6 +125,10 @@ export const usePromptStore = defineStore('promptStore', { savedAt: new Date().toISOString(), dataset: deepClone(this.dataset), presets: deepClone(this.presets), + // 扩展预设数据 + extendedPresets: deepClone(this.extendedPresets), + presetFolders: deepClone(this.presetFolders), + presetManagement: deepClone(this.presetManagement), promptText: this.promptText, selectedLang: this.selectedLang, }; @@ -116,7 +141,13 @@ export const usePromptStore = defineStore('promptStore', { if (!bundle) { if (!baseline) baseline = await loadInitialDataset(); this.dataset = deepClone(baseline!); + this.presets = []; + this.extendedPresets = []; + this.presetFolders = []; } else { + // 确保兼容性 + bundle = this.ensureCompatibility(bundle); + if (bundle.dataset) { this.dataset = bundle.dataset; } else if (bundle.customDiff) { @@ -124,9 +155,17 @@ export const usePromptStore = defineStore('promptStore', { this.dataset = this.applyDiff(deepClone(baseline!), bundle.customDiff); } this.presets = bundle.presets || []; + // 导入扩展预设数据 + this.extendedPresets = bundle.extendedPresets || []; + this.presetFolders = bundle.presetFolders || []; + if (bundle.presetManagement) { + this.presetManagement = bundle.presetManagement; + } } this.selectedCategoryIndex = 0; this.selectedGroupIndex = 0; + // 确保扩展预设管理已初始化 + this.initializeExtendedPresets(); this.save(); }, exportToJson(): string { @@ -137,6 +176,10 @@ export const usePromptStore = defineStore('promptStore', { savedAt: new Date().toISOString(), customDiff: diff, presets: deepClone(this.presets), + // 导出扩展预设数据 + extendedPresets: deepClone(this.extendedPresets), + presetFolders: deepClone(this.presetFolders), + presetManagement: deepClone(this.presetManagement), }; return JSON.stringify(bundle, null, 2); }, @@ -599,6 +642,249 @@ export const usePromptStore = defineStore('promptStore', { } return target; }, + + // 扩展预设管理方法 + initializeExtendedPresets() { + // 如果没有扩展预设数据,初始化默认结构 + if (!this.extendedPresets) { + this.extendedPresets = []; + } + if (!this.presetFolders) { + this.presetFolders = []; + } + if (!this.presetManagement) { + this.presetManagement = { + folders: [], + presets: [], + settings: { + autoBackup: true, + maxPresets: 1000 + } + }; + } + + // 确保有默认文件夹 + if (this.presetFolders.length === 0) { + const defaultFolder = this.createPresetFolder({ + name: '默认文件夹', + description: '系统默认预设文件夹', + color: '#6366f1' + }); + + // 设置为默认文件夹 + if (this.presetManagement.settings) { + this.presetManagement.settings.defaultFolder = defaultFolder.id; + } + } + }, + + createExtendedPreset(data: Omit) { + const now = new Date().toISOString(); + const preset: ExtendedPreset = { + id: `preset_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`, + createdAt: now, + updatedAt: now, + ...data + }; + + this.extendedPresets.push(preset); + this.save(); + return preset; + }, + + updateExtendedPreset(id: string, data: Partial>) { + const preset = this.extendedPresets.find(p => p.id === id); + if (!preset) return false; + + Object.assign(preset, data, { updatedAt: new Date().toISOString() }); + this.save(); + return true; + }, + + deleteExtendedPreset(id: string) { + const index = this.extendedPresets.findIndex(p => p.id === id); + if (index === -1) return false; + + this.extendedPresets.splice(index, 1); + this.save(); + return true; + }, + + createPresetFolder(data: Omit) { + const now = new Date().toISOString(); + const folder: PresetFolder = { + id: `folder_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`, + createdAt: now, + updatedAt: now, + ...data + }; + + this.presetFolders.push(folder); + this.save(); + return folder; + }, + + updatePresetFolder(id: string, data: Partial>) { + const folder = this.presetFolders.find(f => f.id === id); + if (!folder) return false; + + Object.assign(folder, data, { updatedAt: new Date().toISOString() }); + this.save(); + return true; + }, + + deletePresetFolder(id: string) { + const index = this.presetFolders.findIndex(f => f.id === id); + if (index === -1) return false; + + // 将该文件夹下的预设移动到未分类 + this.extendedPresets.forEach(preset => { + if (preset.folderId === id) { + preset.folderId = undefined; + preset.updatedAt = new Date().toISOString(); + } + }); + + // 删除子文件夹或将其移动到父级 + const folder = this.presetFolders[index]; + if (folder) { + this.presetFolders.forEach(f => { + if (f.parentId === id) { + f.parentId = folder.parentId; + f.updatedAt = new Date().toISOString(); + } + }); + } + + this.presetFolders.splice(index, 1); + this.save(); + return true; + }, + + importExtendedPresets(data: { folders?: PresetFolder[]; presets?: ExtendedPreset[] }) { + if (data.folders) { + // 合并文件夹,避免ID冲突 + data.folders.forEach(folder => { + const existingFolder = this.presetFolders.find(f => f.name === folder.name); + if (existingFolder) { + // 更新现有文件夹 + Object.assign(existingFolder, folder, { + id: existingFolder.id, + updatedAt: new Date().toISOString() + }); + } else { + // 创建新文件夹,重新生成ID + const newFolder = { + ...folder, + id: `folder_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }; + this.presetFolders.push(newFolder); + } + }); + } + + if (data.presets) { + // 合并预设,避免ID冲突 + data.presets.forEach(preset => { + const existingPreset = this.extendedPresets.find(p => p.name === preset.name && p.type === preset.type); + if (existingPreset) { + // 更新现有预设 + Object.assign(existingPreset, preset, { + id: existingPreset.id, + updatedAt: new Date().toISOString() + }); + } else { + // 创建新预设,重新生成ID + const newPreset = { + ...preset, + id: `preset_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }; + this.extendedPresets.push(newPreset); + } + }); + } + + this.save(); + }, + + // 兼容性:从旧预设迁移到新预设系统 + migrateOldPresets() { + if (this.presets.length === 0) return; // 没有旧预设需要迁移 + + console.log(`开始迁移 ${this.presets.length} 个旧预设到新系统...`); + + let migratedCount = 0; + const defaultFolder = this.presetManagement?.settings?.defaultFolder; + + this.presets.forEach(oldPreset => { + const existingExtended = this.extendedPresets.find(p => + p.name === oldPreset.name && p.type === 'positive' + ); + + if (!existingExtended) { + // 创建新的扩展预设,但不触发保存(避免递归) + const now = new Date().toISOString(); + const preset: ExtendedPreset = { + id: `migrated_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`, + name: oldPreset.name, + type: 'positive', + content: oldPreset.text, + description: '从旧版快速预设自动迁移', + folderId: defaultFolder, + createdAt: oldPreset.updatedAt, + updatedAt: oldPreset.updatedAt + }; + + this.extendedPresets.push(preset); + migratedCount++; + } + }); + + if (migratedCount > 0) { + console.log(`成功迁移 ${migratedCount} 个预设到新系统`); + // 清空旧预设数组,完成迁移 + this.presets = []; + console.log('旧预设已清空,迁移完成'); + } + }, + + // 检查并处理版本兼容性 + ensureCompatibility(bundle: ExportBundle) { + // 如果是旧版本的导出文件,确保新字段存在 + if (!bundle.extendedPresets && bundle.presets && bundle.presets.length > 0) { + // 自动迁移旧预设到新系统 + bundle.extendedPresets = bundle.presets.map(preset => ({ + id: `migrated_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`, + name: preset.name, + type: 'positive' as PresetType, + content: preset.text, + description: '从旧版本自动迁移', + createdAt: preset.updatedAt, + updatedAt: preset.updatedAt + })); + } + + if (!bundle.presetFolders) { + bundle.presetFolders = []; + } + + if (!bundle.presetManagement) { + bundle.presetManagement = { + folders: [], + presets: [], + settings: { + autoBackup: true, + maxPresets: 1000 + } + }; + } + + return bundle; + }, }, }); // —— 工具方法 —— diff --git a/src/types.ts b/src/types.ts index 7d61592..5d628df 100644 --- a/src/types.ts +++ b/src/types.ts @@ -31,6 +31,10 @@ export interface ExportBundle { dataset?: PromptDataset; // full snapshot (used for localStorage persistence) customDiff?: CustomDiff; // only user-defined changes for export/import presets?: PromptPreset[]; // saved prompt texts by name + // 新的扩展预设管理 + extendedPresets?: ExtendedPreset[]; + presetFolders?: PresetFolder[]; + presetManagement?: PresetManagement; // editor state persistence promptText?: string; selectedLang?: LangCode; @@ -64,4 +68,44 @@ export interface PromptPreset { name: string; text: string; updatedAt: string; +} + +// 新的预设类型枚举 +export type PresetType = 'positive' | 'negative' | 'setting' | 'style' | 'character' | 'scene' | 'custom'; + +// 扩展的预设接口 +export interface ExtendedPreset { + id: string; + name: string; + type: PresetType; + content: string; + description?: string; + tags?: string[]; + folderId?: string; + createdAt: string; + updatedAt: string; + isPublic?: boolean; + author?: string; +} + +// 预设文件夹 +export interface PresetFolder { + id: string; + name: string; + description?: string; + color?: string; + parentId?: string; + createdAt: string; + updatedAt: string; +} + +// 预设管理数据结构 +export interface PresetManagement { + folders: PresetFolder[]; + presets: ExtendedPreset[]; + settings: { + defaultFolder?: string; + autoBackup: boolean; + maxPresets: number; + }; } \ No newline at end of file