From f292230c13d37ddb0255447654a11eb0b0c885cd Mon Sep 17 00:00:00 2001 From: kjqwer <2990346238@qq.com> Date: Fri, 28 Nov 2025 12:44:22 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BC=96=E8=BE=91=E5=8D=A1?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/PromptEditor.vue | 95 +++++++++++++++++++++++++-------- src/stores/promptStore.ts | 48 +++++++++++++---- 2 files changed, 111 insertions(+), 32 deletions(-) diff --git a/src/components/PromptEditor.vue b/src/components/PromptEditor.vue index d5a24e4..72d185d 100644 --- a/src/components/PromptEditor.vue +++ b/src/components/PromptEditor.vue @@ -18,6 +18,17 @@ const startY = ref(0); const lastX = ref(0); const lastY = ref(0); const dragStarted = ref(false); +const dragContainer = ref(null); +const cachedTokenRects = ref<{ + index: number; + left: number; + top: number; + width: number; + height: number; + midX: number; +}[]>([]); +let rafId: number | null = null; + const DRAG_THRESHOLD = 3; // 像素阈值,避免误触 const editingIndex = ref(null); const editingValue = ref(''); @@ -316,6 +327,24 @@ function onPointerDown(index: number, e: PointerEvent) { isDragging.value = false; insertSide.value = null; + // 缓存所有 Token 的位置信息 (相对于 dragContainer) + if (dragContainer.value) { + const selector = viewMode.value === 'compact' ? '.pe-token-compact' : '.pe-token-detail'; + const elements = dragContainer.value.querySelectorAll(selector); + cachedTokenRects.value = Array.from(elements).map(el => { + const htmlEl = el as HTMLElement; + const idx = parseInt(htmlEl.getAttribute('data-index') || '-1', 10); + return { + index: idx, + left: htmlEl.offsetLeft, + top: htmlEl.offsetTop, + width: htmlEl.offsetWidth, + height: htmlEl.offsetHeight, + midX: htmlEl.offsetLeft + htmlEl.offsetWidth / 2 + }; + }).filter(item => item.index !== -1); + } + // 监听全局移动与抬起 window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointerup', handlePointerUp, { once: true }); @@ -326,14 +355,25 @@ function handlePointerMove(e: PointerEvent) { lastY.value = e.clientY; const dx = e.clientX - startX.value; const dy = e.clientY - startY.value; - if (!dragStarted.value && Math.hypot(dx, dy) > DRAG_THRESHOLD) { - dragStarted.value = true; - isDragging.value = true; - if (draggingIndex.value != null) createPointerPreview(draggingIndex.value); + + if (!dragStarted.value) { + if (Math.hypot(dx, dy) > DRAG_THRESHOLD) { + dragStarted.value = true; + isDragging.value = true; + if (draggingIndex.value != null) createPointerPreview(draggingIndex.value); + } + return; } + if (!isDragging.value) return; - positionPreview(e.clientX, e.clientY); - updateOverIndexAndSide(e.clientX, e.clientY); + + // 使用 requestAnimationFrame 节流渲染 + if (rafId) return; + rafId = requestAnimationFrame(() => { + positionPreview(lastX.value, lastY.value); + updateOverIndexAndSideFast(lastX.value, lastY.value); + rafId = null; + }); } function handlePointerUp(e: PointerEvent) { @@ -364,6 +404,8 @@ function handlePointerUp(e: PointerEvent) { } function cleanupDrag() { + if (rafId) { cancelAnimationFrame(rafId); rafId = null; } + cachedTokenRects.value = []; draggingIndex.value = null; overIndex.value = null; isDragging.value = false; @@ -403,22 +445,29 @@ function positionPreview(x: number, y: number) { dragPreview.value.style.transform = `translate(${x + 12}px, ${y + 12}px)`; } -function updateOverIndexAndSide(x: number, y: number) { - insertSide.value = null; - overIndex.value = null; - const el = document.elementFromPoint(x, y) as HTMLElement | null; - if (!el) return; - const tokenEl = el.closest('.pe-token-compact, .pe-token-detail') as HTMLElement | null; - if (!tokenEl) return; - const idxAttr = tokenEl.getAttribute('data-index'); - if (idxAttr == null) return; - const idx = parseInt(idxAttr, 10); - if (Number.isNaN(idx)) return; - if (idx === draggingIndex.value) { overIndex.value = null; insertSide.value = null; return; } - const rect = tokenEl.getBoundingClientRect(); - const midX = rect.left + rect.width / 2; - overIndex.value = idx; - insertSide.value = x < midX ? 'before' : 'after'; +function updateOverIndexAndSideFast(clientX: number, clientY: number) { + if (!dragContainer.value) return; + + // 计算鼠标在容器内的相对坐标 + const containerRect = dragContainer.value.getBoundingClientRect(); + const relX = clientX - containerRect.left; + const relY = clientY - containerRect.top; + + // 在缓存中查找命中的 Token + // 简单碰撞检测 + const target = cachedTokenRects.value.find(item => + relX >= item.left && relX <= item.left + item.width && + relY >= item.top && relY <= item.top + item.height + ); + + if (!target || target.index === draggingIndex.value) { + overIndex.value = null; + insertSide.value = null; + return; + } + + overIndex.value = target.index; + insertSide.value = relX < target.midX ? 'before' : 'after'; } function beginEdit(i: number) { @@ -717,7 +766,7 @@ function isRemoveDisabled(token: string): boolean { -
+
| null = null; +let tagNormIndex: Map | null = null; + +function rebuildTagIndex(dataset: PromptDataset | null) { + if (!dataset) { + tagIndex = null; + tagNormIndex = null; + return; + } + tagIndex = new Map(); + tagNormIndex = new Map(); + for (const cat of dataset.categories) { + for (const g of cat.groups) { + for (const t of g.tags) { + if (!tagIndex.has(t.key)) { + tagIndex.set(t.key, t); + } + const norm = normalizeKeyForMatch(t.key); + if (!tagNormIndex.has(norm)) { + tagNormIndex.set(norm, t); + } + } + } + } +} function deepClone(obj: T): T { return JSON.parse(JSON.stringify(obj)); @@ -115,6 +140,9 @@ export const usePromptStore = defineStore('promptStore', { this.dataset = deepClone(baseline!); this.promptText = '1girl, solo, long hair, blue eyes, smile, looking_at_viewer, upper_body, outdoors, sunset'; } + // 建立索引 + rebuildTagIndex(this.dataset); + // 若无恢复语言,则默认使用 zh_CN if (!this.selectedLang) { this.selectedLang = 'zh_CN' as LangCode; @@ -172,6 +200,7 @@ export const usePromptStore = defineStore('promptStore', { console.warn('检测到预设数据,请使用预设管理页面的导入功能来导入预设数据'); } } + rebuildTagIndex(this.dataset); this.selectedCategoryIndex = 0; this.selectedGroupIndex = 0; this.save(); @@ -248,11 +277,13 @@ export const usePromptStore = defineStore('promptStore', { if (!grp) return; // 新增提示词插入到列表顶部,便于用户立即编辑 grp.tags.unshift({ key, translation: { en: key, [this.selectedLang]: key } }); + rebuildTagIndex(this.dataset); }, removeTag(groupId: string, key: string) { const grp = this.findGroupById(groupId); if (!grp) return; grp.tags = grp.tags.filter((t) => t.key !== key); + rebuildTagIndex(this.dataset); }, updateTagKey(groupId: string, oldKey: string, newKey: string) { const grp = this.findGroupById(groupId); @@ -262,6 +293,7 @@ export const usePromptStore = defineStore('promptStore', { tag.key = newKey; if (!tag.translation) tag.translation = {}; tag.translation.en = newKey; + rebuildTagIndex(this.dataset); }, setTranslation(groupId: string, key: string, lang: LangCode, val: string) { const grp = this.findGroupById(groupId); @@ -432,16 +464,13 @@ export const usePromptStore = defineStore('promptStore', { this.promptText = tokens.join(', '); }, getTagByKey(key: string): PromptTag | null { - const target = normalizeKeyForMatch(key); - for (const cat of this.dataset?.categories || []) { - for (const g of cat.groups) { - for (const t of g.tags) { - if (t.key === key) return t; // 精确匹配优先 - if (normalizeKeyForMatch(t.key) === target) return t; // 下划线/空格归一化匹配 - } - } + if (!tagIndex && this.dataset) { + rebuildTagIndex(this.dataset); } - return null; + if (!tagIndex || !tagNormIndex) return null; + if (tagIndex.has(key)) return tagIndex.get(key)!; + const target = normalizeKeyForMatch(key); + return tagNormIndex.get(target) || null; }, getTranslation(key: string, lang: LangCode): string | null { // 兼容包裹层:如 {aaa}、(aaa) 等 @@ -485,6 +514,7 @@ export const usePromptStore = defineStore('promptStore', { } const grp = this.ensureCustomGroup(); grp.tags.push({ key, translation: { en: key, [lang]: val } }); + rebuildTagIndex(this.dataset); }, ensureCustomGroup(): PromptGroup { const catName = 'Custom';