新增预设管理功能
This commit is contained in:
+19
-5
@@ -2,9 +2,10 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import PromptEditor from './components/PromptEditor.vue'
|
import PromptEditor from './components/PromptEditor.vue'
|
||||||
import PromptManager from './components/PromptManager.vue'
|
import PromptManager from './components/PromptManager.vue'
|
||||||
|
import PresetManager from './components/PresetManager.vue'
|
||||||
import { usePromptStore } from './stores/promptStore'
|
import { usePromptStore } from './stores/promptStore'
|
||||||
|
|
||||||
const currentView = ref<'editor' | 'manager'>('editor')
|
const currentView = ref<'editor' | 'manager' | 'presets'>('editor')
|
||||||
const isDark = ref(false)
|
const isDark = ref(false)
|
||||||
const store = usePromptStore()
|
const store = usePromptStore()
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ function updateTheme() {
|
|||||||
document.documentElement.classList.toggle('dark', isDark.value)
|
document.documentElement.classList.toggle('dark', isDark.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchView(view: 'editor' | 'manager') {
|
function switchView(view: 'editor' | 'manager' | 'presets') {
|
||||||
currentView.value = view
|
currentView.value = view
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -63,7 +64,7 @@ function switchView(view: 'editor' | 'manager') {
|
|||||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="m18.5 2.5 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="m18.5 2.5 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
编辑器
|
<span>编辑器</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="nav-btn"
|
class="nav-btn"
|
||||||
@@ -73,7 +74,19 @@ function switchView(view: 'editor' | 'manager') {
|
|||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M3 3h18v18H3zM9 9h6v6H9z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M3 3h18v18H3zM9 9h6v6H9z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
词库管理
|
<span>词库管理</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="nav-btn"
|
||||||
|
:class="{ active: currentView === 'presets' }"
|
||||||
|
@click="switchView('presets')"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<polyline points="17,21 17,13 7,13 7,21" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<polyline points="7,3 7,8 15,8" stroke="currentColor" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
<span>预设管理</span>
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -95,7 +108,8 @@ function switchView(view: 'editor' | 'manager') {
|
|||||||
<main class="app-main">
|
<main class="app-main">
|
||||||
<Transition name="view-transition" mode="out-in">
|
<Transition name="view-transition" mode="out-in">
|
||||||
<PromptEditor v-if="currentView === 'editor'" key="editor" />
|
<PromptEditor v-if="currentView === 'editor'" key="editor" />
|
||||||
<PromptManager v-else key="manager" />
|
<PromptManager v-else-if="currentView === 'manager'" key="manager" />
|
||||||
|
<PresetManager v-else-if="currentView === 'presets'" key="presets" />
|
||||||
</Transition>
|
</Transition>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ import { onMounted, onUnmounted, ref, computed, nextTick, watch } from 'vue';
|
|||||||
import { usePromptStore } from '../stores/promptStore';
|
import { usePromptStore } from '../stores/promptStore';
|
||||||
import type { LangCode } from '../types';
|
import type { LangCode } from '../types';
|
||||||
import NotificationToast from './NotificationToast.vue';
|
import NotificationToast from './NotificationToast.vue';
|
||||||
|
import PresetDropdown from './PresetDropdown.vue';
|
||||||
|
|
||||||
const store = usePromptStore();
|
const store = usePromptStore();
|
||||||
const draggingIndex = ref<number | null>(null);
|
const draggingIndex = ref<number | null>(null);
|
||||||
@@ -14,9 +15,6 @@ const editingValue = ref('');
|
|||||||
const addingMapIndex = ref<number | null>(null);
|
const addingMapIndex = ref<number | null>(null);
|
||||||
const addingMapValue = ref('');
|
const addingMapValue = ref('');
|
||||||
const presetName = ref('');
|
const presetName = ref('');
|
||||||
const presetSearch = ref('');
|
|
||||||
const renamingPreset = ref<string | null>(null);
|
|
||||||
const renamingValue = ref('');
|
|
||||||
const viewMode = ref<'compact' | 'detail'>('compact');
|
const viewMode = ref<'compact' | 'detail'>('compact');
|
||||||
const showPresetDropdown = ref(false);
|
const showPresetDropdown = ref(false);
|
||||||
const notification = ref<{ message: string; type: 'success' | 'error' | 'info'; show: boolean }>({
|
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) {
|
function handleClickOutside(event: Event) {
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
if (!target.closest('.pe-presets')) {
|
// 检查点击是否在预设下拉区域内,包括重命名输入框和按钮
|
||||||
|
if (!target.closest('.pe-presets') && !target.closest('.pd-dropdown')) {
|
||||||
showPresetDropdown.value = false;
|
showPresetDropdown.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,12 +54,6 @@ const selectedLang = computed({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const tokens = computed(() => store.tokens);
|
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<string[]>([]);
|
const suggestions = ref<string[]>([]);
|
||||||
const inputEl = ref<HTMLTextAreaElement | null>(null);
|
const inputEl = ref<HTMLTextAreaElement | null>(null);
|
||||||
@@ -278,31 +271,52 @@ function savePreset() {
|
|||||||
showNotification('请输入预设名称', 'error');
|
showNotification('请输入预设名称', 'error');
|
||||||
return;
|
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 = '';
|
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');
|
showNotification(`已加载预设「${name}」`, 'success');
|
||||||
}
|
}
|
||||||
function deletePreset(name: string) {
|
|
||||||
if (confirm(`确定删除预设「${name}」吗?`)) {
|
function handlePresetSave(name: string) {
|
||||||
store.deletePreset(name);
|
store.savePreset(name);
|
||||||
showNotification(`预设「${name}」已删除`, 'info');
|
showNotification(`预设「${name}」已保存`, 'success');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
function beginRename(name: string) { renamingPreset.value = name; renamingValue.value = name; }
|
|
||||||
function commitRename() {
|
function handlePresetDelete(name: string) {
|
||||||
if (!renamingPreset.value) return;
|
store.deletePreset(name);
|
||||||
const oldName = renamingPreset.value;
|
showNotification(`预设「${name}」已删除`, 'info');
|
||||||
const newName = renamingValue.value.trim();
|
}
|
||||||
if (!newName) { alert('预设名称不能为空'); return; }
|
|
||||||
|
function handlePresetRename(oldName: string, newName: string) {
|
||||||
store.renamePreset(oldName, newName);
|
store.renamePreset(oldName, newName);
|
||||||
renamingPreset.value = null; renamingValue.value = '';
|
showNotification(`预设已重命名为「${newName}」`, 'success');
|
||||||
}
|
}
|
||||||
function cancelRename() { renamingPreset.value = null; }
|
|
||||||
|
|
||||||
async function applySuggestion(s: string) {
|
async function applySuggestion(s: string) {
|
||||||
const el = inputEl.value;
|
const el = inputEl.value;
|
||||||
@@ -355,12 +369,12 @@ function displayTrans(key: string): string {
|
|||||||
<button
|
<button
|
||||||
class="pe-preset-toggle"
|
class="pe-preset-toggle"
|
||||||
@click="showPresetDropdown = !showPresetDropdown"
|
@click="showPresetDropdown = !showPresetDropdown"
|
||||||
title="管理预设"
|
title="快速预设"
|
||||||
>
|
>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z" fill="currentColor"/>
|
<path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z" fill="currentColor"/>
|
||||||
</svg>
|
</svg>
|
||||||
预设管理
|
快速预设
|
||||||
<svg
|
<svg
|
||||||
width="12" height="12"
|
width="12" height="12"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -373,31 +387,14 @@ function displayTrans(key: string): string {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Transition name="dropdown">
|
<PresetDropdown
|
||||||
<div v-if="showPresetDropdown" class="pe-preset-dropdown">
|
:show="showPresetDropdown"
|
||||||
<div class="pe-preset-search-wrapper">
|
@close="showPresetDropdown = false"
|
||||||
<input class="pe-preset-search" placeholder="搜索预设..." v-model="presetSearch" />
|
@load="handlePresetLoad"
|
||||||
</div>
|
@save="handlePresetSave"
|
||||||
<div class="pe-preset-list" v-if="filteredPresets.length">
|
@delete="handlePresetDelete"
|
||||||
<div v-for="p in filteredPresets" :key="p.name" class="pe-preset-item">
|
@rename="handlePresetRename"
|
||||||
<template v-if="renamingPreset !== p.name">
|
/>
|
||||||
<button class="pe-preset-load" title="加载" @click="loadPreset(p.name)">{{ p.name }}</button>
|
|
||||||
<span class="pe-preset-meta">{{ new Date(p.updatedAt).toLocaleString() }}</span>
|
|
||||||
<button class="pe-preset-rename" title="重命名" @click="beginRename(p.name)">✎</button>
|
|
||||||
<button class="pe-preset-delete" title="删除" @click="deletePreset(p.name)">🗑</button>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<input class="pe-preset-rename-input" v-model="renamingValue" @keyup.enter="commitRename" />
|
|
||||||
<button class="pe-preset-rename-ok" @click="commitRename">确定</button>
|
|
||||||
<button class="pe-preset-rename-cancel" @click="cancelRename">取消</button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="pe-preset-empty">
|
|
||||||
<span>{{ presetSearch ? '未找到匹配的预设' : '暂无预设' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -437,6 +434,7 @@ function displayTrans(key: string): string {
|
|||||||
<li v-for="s in suggestions" :key="s" @click="applySuggestion(s)">{{ s }}</li>
|
<li v-for="s in suggestions" :key="s" @click="applySuggestion(s)">{{ s }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="pe-right-pane">
|
<section class="pe-right-pane">
|
||||||
<div class="pe-section-title mode">
|
<div class="pe-section-title mode">
|
||||||
<span>提示词映射</span>
|
<span>提示词映射</span>
|
||||||
@@ -1703,6 +1701,7 @@ function displayTrans(key: string): string {
|
|||||||
background: var(--color-text-tertiary);
|
background: var(--color-text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 保证按钮内图标不压缩文本,提升对齐与可读性 */
|
/* 保证按钮内图标不压缩文本,提升对齐与可读性 */
|
||||||
.pe-left button svg,
|
.pe-left button svg,
|
||||||
.pe-right button svg,
|
.pe-right button svg,
|
||||||
|
|||||||
+287
-1
@@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { loadInitialDataset } from '../utils/yamlLoader';
|
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';
|
const LS_KEY = 'ops.prompt.dataset.v1';
|
||||||
let saveTimer: number | null = null; // 非响应式计时器,避免递归更新
|
let saveTimer: number | null = null; // 非响应式计时器,避免递归更新
|
||||||
@@ -20,6 +20,17 @@ export const usePromptStore = defineStore('promptStore', {
|
|||||||
// 编辑器相关
|
// 编辑器相关
|
||||||
promptText: '',
|
promptText: '',
|
||||||
presets: [] as PromptPreset[],
|
presets: [] as PromptPreset[],
|
||||||
|
// 扩展预设管理
|
||||||
|
extendedPresets: [] as ExtendedPreset[],
|
||||||
|
presetFolders: [] as PresetFolder[],
|
||||||
|
presetManagement: {
|
||||||
|
folders: [],
|
||||||
|
presets: [],
|
||||||
|
settings: {
|
||||||
|
autoBackup: true,
|
||||||
|
maxPresets: 1000
|
||||||
|
}
|
||||||
|
} as PresetManagement,
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
categories: (s) => s.dataset?.categories ?? [],
|
categories: (s) => s.dataset?.categories ?? [],
|
||||||
@@ -68,6 +79,12 @@ export const usePromptStore = defineStore('promptStore', {
|
|||||||
this.dataset = deepClone(baseline!);
|
this.dataset = deepClone(baseline!);
|
||||||
}
|
}
|
||||||
this.presets = bundle.presets || [];
|
this.presets = bundle.presets || [];
|
||||||
|
// 恢复扩展预设数据
|
||||||
|
this.extendedPresets = bundle.extendedPresets || [];
|
||||||
|
this.presetFolders = bundle.presetFolders || [];
|
||||||
|
if (bundle.presetManagement) {
|
||||||
|
this.presetManagement = bundle.presetManagement;
|
||||||
|
}
|
||||||
// 恢复编辑器内容与语言
|
// 恢复编辑器内容与语言
|
||||||
if (typeof bundle.promptText === 'string') {
|
if (typeof bundle.promptText === 'string') {
|
||||||
this.promptText = bundle.promptText;
|
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;
|
const guessLang: LangCode = (this.dataset.languages.includes('zh_CN') ? 'zh_CN' : 'en') as LangCode;
|
||||||
this.selectedLang = guessLang;
|
this.selectedLang = guessLang;
|
||||||
}
|
}
|
||||||
|
// 初始化扩展预设管理
|
||||||
|
this.initializeExtendedPresets();
|
||||||
|
// 自动迁移旧预设到新系统
|
||||||
|
this.migrateOldPresets();
|
||||||
this.autoPersist();
|
this.autoPersist();
|
||||||
},
|
},
|
||||||
autoPersist() {
|
autoPersist() {
|
||||||
@@ -104,6 +125,10 @@ export const usePromptStore = defineStore('promptStore', {
|
|||||||
savedAt: new Date().toISOString(),
|
savedAt: new Date().toISOString(),
|
||||||
dataset: deepClone(this.dataset),
|
dataset: deepClone(this.dataset),
|
||||||
presets: deepClone(this.presets),
|
presets: deepClone(this.presets),
|
||||||
|
// 扩展预设数据
|
||||||
|
extendedPresets: deepClone(this.extendedPresets),
|
||||||
|
presetFolders: deepClone(this.presetFolders),
|
||||||
|
presetManagement: deepClone(this.presetManagement),
|
||||||
promptText: this.promptText,
|
promptText: this.promptText,
|
||||||
selectedLang: this.selectedLang,
|
selectedLang: this.selectedLang,
|
||||||
};
|
};
|
||||||
@@ -116,7 +141,13 @@ export const usePromptStore = defineStore('promptStore', {
|
|||||||
if (!bundle) {
|
if (!bundle) {
|
||||||
if (!baseline) baseline = await loadInitialDataset();
|
if (!baseline) baseline = await loadInitialDataset();
|
||||||
this.dataset = deepClone(baseline!);
|
this.dataset = deepClone(baseline!);
|
||||||
|
this.presets = [];
|
||||||
|
this.extendedPresets = [];
|
||||||
|
this.presetFolders = [];
|
||||||
} else {
|
} else {
|
||||||
|
// 确保兼容性
|
||||||
|
bundle = this.ensureCompatibility(bundle);
|
||||||
|
|
||||||
if (bundle.dataset) {
|
if (bundle.dataset) {
|
||||||
this.dataset = bundle.dataset;
|
this.dataset = bundle.dataset;
|
||||||
} else if (bundle.customDiff) {
|
} else if (bundle.customDiff) {
|
||||||
@@ -124,9 +155,17 @@ export const usePromptStore = defineStore('promptStore', {
|
|||||||
this.dataset = this.applyDiff(deepClone(baseline!), bundle.customDiff);
|
this.dataset = this.applyDiff(deepClone(baseline!), bundle.customDiff);
|
||||||
}
|
}
|
||||||
this.presets = bundle.presets || [];
|
this.presets = bundle.presets || [];
|
||||||
|
// 导入扩展预设数据
|
||||||
|
this.extendedPresets = bundle.extendedPresets || [];
|
||||||
|
this.presetFolders = bundle.presetFolders || [];
|
||||||
|
if (bundle.presetManagement) {
|
||||||
|
this.presetManagement = bundle.presetManagement;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.selectedCategoryIndex = 0;
|
this.selectedCategoryIndex = 0;
|
||||||
this.selectedGroupIndex = 0;
|
this.selectedGroupIndex = 0;
|
||||||
|
// 确保扩展预设管理已初始化
|
||||||
|
this.initializeExtendedPresets();
|
||||||
this.save();
|
this.save();
|
||||||
},
|
},
|
||||||
exportToJson(): string {
|
exportToJson(): string {
|
||||||
@@ -137,6 +176,10 @@ export const usePromptStore = defineStore('promptStore', {
|
|||||||
savedAt: new Date().toISOString(),
|
savedAt: new Date().toISOString(),
|
||||||
customDiff: diff,
|
customDiff: diff,
|
||||||
presets: deepClone(this.presets),
|
presets: deepClone(this.presets),
|
||||||
|
// 导出扩展预设数据
|
||||||
|
extendedPresets: deepClone(this.extendedPresets),
|
||||||
|
presetFolders: deepClone(this.presetFolders),
|
||||||
|
presetManagement: deepClone(this.presetManagement),
|
||||||
};
|
};
|
||||||
return JSON.stringify(bundle, null, 2);
|
return JSON.stringify(bundle, null, 2);
|
||||||
},
|
},
|
||||||
@@ -599,6 +642,249 @@ export const usePromptStore = defineStore('promptStore', {
|
|||||||
}
|
}
|
||||||
return target;
|
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<ExtendedPreset, 'id' | 'createdAt' | 'updatedAt'>) {
|
||||||
|
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<Omit<ExtendedPreset, 'id' | 'createdAt'>>) {
|
||||||
|
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<PresetFolder, 'id' | 'createdAt' | 'updatedAt'>) {
|
||||||
|
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<Omit<PresetFolder, 'id' | 'createdAt'>>) {
|
||||||
|
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;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// —— 工具方法 ——
|
// —— 工具方法 ——
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ export interface ExportBundle {
|
|||||||
dataset?: PromptDataset; // full snapshot (used for localStorage persistence)
|
dataset?: PromptDataset; // full snapshot (used for localStorage persistence)
|
||||||
customDiff?: CustomDiff; // only user-defined changes for export/import
|
customDiff?: CustomDiff; // only user-defined changes for export/import
|
||||||
presets?: PromptPreset[]; // saved prompt texts by name
|
presets?: PromptPreset[]; // saved prompt texts by name
|
||||||
|
// 新的扩展预设管理
|
||||||
|
extendedPresets?: ExtendedPreset[];
|
||||||
|
presetFolders?: PresetFolder[];
|
||||||
|
presetManagement?: PresetManagement;
|
||||||
// editor state persistence
|
// editor state persistence
|
||||||
promptText?: string;
|
promptText?: string;
|
||||||
selectedLang?: LangCode;
|
selectedLang?: LangCode;
|
||||||
@@ -65,3 +69,43 @@ export interface PromptPreset {
|
|||||||
text: string;
|
text: string;
|
||||||
updatedAt: 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user