修复编辑卡的问题

This commit is contained in:
2025-11-28 12:44:22 +08:00
parent dc87eb2215
commit f292230c13
2 changed files with 111 additions and 32 deletions
+72 -23
View File
@@ -18,6 +18,17 @@ const startY = ref(0);
const lastX = ref(0); const lastX = ref(0);
const lastY = ref(0); const lastY = ref(0);
const dragStarted = ref(false); const dragStarted = ref(false);
const dragContainer = ref<HTMLElement | null>(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 DRAG_THRESHOLD = 3; // 像素阈值,避免误触
const editingIndex = ref<number | null>(null); const editingIndex = ref<number | null>(null);
const editingValue = ref(''); const editingValue = ref('');
@@ -316,6 +327,24 @@ function onPointerDown(index: number, e: PointerEvent) {
isDragging.value = false; isDragging.value = false;
insertSide.value = null; 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('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp, { once: true }); window.addEventListener('pointerup', handlePointerUp, { once: true });
@@ -326,14 +355,25 @@ function handlePointerMove(e: PointerEvent) {
lastY.value = e.clientY; lastY.value = e.clientY;
const dx = e.clientX - startX.value; const dx = e.clientX - startX.value;
const dy = e.clientY - startY.value; const dy = e.clientY - startY.value;
if (!dragStarted.value && Math.hypot(dx, dy) > DRAG_THRESHOLD) {
dragStarted.value = true; if (!dragStarted.value) {
isDragging.value = true; if (Math.hypot(dx, dy) > DRAG_THRESHOLD) {
if (draggingIndex.value != null) createPointerPreview(draggingIndex.value); dragStarted.value = true;
isDragging.value = true;
if (draggingIndex.value != null) createPointerPreview(draggingIndex.value);
}
return;
} }
if (!isDragging.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) { function handlePointerUp(e: PointerEvent) {
@@ -364,6 +404,8 @@ function handlePointerUp(e: PointerEvent) {
} }
function cleanupDrag() { function cleanupDrag() {
if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
cachedTokenRects.value = [];
draggingIndex.value = null; draggingIndex.value = null;
overIndex.value = null; overIndex.value = null;
isDragging.value = false; isDragging.value = false;
@@ -403,22 +445,29 @@ function positionPreview(x: number, y: number) {
dragPreview.value.style.transform = `translate(${x + 12}px, ${y + 12}px)`; dragPreview.value.style.transform = `translate(${x + 12}px, ${y + 12}px)`;
} }
function updateOverIndexAndSide(x: number, y: number) { function updateOverIndexAndSideFast(clientX: number, clientY: number) {
insertSide.value = null; if (!dragContainer.value) return;
overIndex.value = null;
const el = document.elementFromPoint(x, y) as HTMLElement | null; // 计算鼠标在容器内的相对坐标
if (!el) return; const containerRect = dragContainer.value.getBoundingClientRect();
const tokenEl = el.closest('.pe-token-compact, .pe-token-detail') as HTMLElement | null; const relX = clientX - containerRect.left;
if (!tokenEl) return; const relY = clientY - containerRect.top;
const idxAttr = tokenEl.getAttribute('data-index');
if (idxAttr == null) return; // 在缓存中查找命中的 Token
const idx = parseInt(idxAttr, 10); // 简单碰撞检测
if (Number.isNaN(idx)) return; const target = cachedTokenRects.value.find(item =>
if (idx === draggingIndex.value) { overIndex.value = null; insertSide.value = null; return; } relX >= item.left && relX <= item.left + item.width &&
const rect = tokenEl.getBoundingClientRect(); relY >= item.top && relY <= item.top + item.height
const midX = rect.left + rect.width / 2; );
overIndex.value = idx;
insertSide.value = x < midX ? 'before' : 'after'; 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) { function beginEdit(i: number) {
@@ -717,7 +766,7 @@ function isRemoveDisabled(token: string): boolean {
</div> </div>
</div> </div>
<div class="pe-drag-container" :class="{ 'is-dragging': isDragging }"> <div class="pe-drag-container" ref="dragContainer" :class="{ 'is-dragging': isDragging }">
<div class="pe-tokens-compact" v-if="viewMode === 'compact'"> <div class="pe-tokens-compact" v-if="viewMode === 'compact'">
<div <div
v-for="(k,i) in tokens" v-for="(k,i) in tokens"
+39 -9
View File
@@ -5,6 +5,31 @@ import type { PromptDataset, PromptCategory, PromptGroup, PromptTag, LangCode, E
const LS_KEY = 'ops.prompt.dataset.v1'; const LS_KEY = 'ops.prompt.dataset.v1';
let saveTimer: number | null = null; // 非响应式计时器,避免递归更新 let saveTimer: number | null = null; // 非响应式计时器,避免递归更新
let baseline: PromptDataset | null = null; // 基线词库(从 public/sd 加载) let baseline: PromptDataset | null = null; // 基线词库(从 public/sd 加载)
let tagIndex: Map<string, PromptTag> | null = null;
let tagNormIndex: Map<string, PromptTag> | 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<T>(obj: T): T { function deepClone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj)); return JSON.parse(JSON.stringify(obj));
@@ -115,6 +140,9 @@ export const usePromptStore = defineStore('promptStore', {
this.dataset = deepClone(baseline!); this.dataset = deepClone(baseline!);
this.promptText = '1girl, solo, long hair, blue eyes, smile, looking_at_viewer, upper_body, outdoors, sunset'; this.promptText = '1girl, solo, long hair, blue eyes, smile, looking_at_viewer, upper_body, outdoors, sunset';
} }
// 建立索引
rebuildTagIndex(this.dataset);
// 若无恢复语言,则默认使用 zh_CN // 若无恢复语言,则默认使用 zh_CN
if (!this.selectedLang) { if (!this.selectedLang) {
this.selectedLang = 'zh_CN' as LangCode; this.selectedLang = 'zh_CN' as LangCode;
@@ -172,6 +200,7 @@ export const usePromptStore = defineStore('promptStore', {
console.warn('检测到预设数据,请使用预设管理页面的导入功能来导入预设数据'); console.warn('检测到预设数据,请使用预设管理页面的导入功能来导入预设数据');
} }
} }
rebuildTagIndex(this.dataset);
this.selectedCategoryIndex = 0; this.selectedCategoryIndex = 0;
this.selectedGroupIndex = 0; this.selectedGroupIndex = 0;
this.save(); this.save();
@@ -248,11 +277,13 @@ export const usePromptStore = defineStore('promptStore', {
if (!grp) return; if (!grp) return;
// 新增提示词插入到列表顶部,便于用户立即编辑 // 新增提示词插入到列表顶部,便于用户立即编辑
grp.tags.unshift({ key, translation: { en: key, [this.selectedLang]: key } }); grp.tags.unshift({ key, translation: { en: key, [this.selectedLang]: key } });
rebuildTagIndex(this.dataset);
}, },
removeTag(groupId: string, key: string) { removeTag(groupId: string, key: string) {
const grp = this.findGroupById(groupId); const grp = this.findGroupById(groupId);
if (!grp) return; if (!grp) return;
grp.tags = grp.tags.filter((t) => t.key !== key); grp.tags = grp.tags.filter((t) => t.key !== key);
rebuildTagIndex(this.dataset);
}, },
updateTagKey(groupId: string, oldKey: string, newKey: string) { updateTagKey(groupId: string, oldKey: string, newKey: string) {
const grp = this.findGroupById(groupId); const grp = this.findGroupById(groupId);
@@ -262,6 +293,7 @@ export const usePromptStore = defineStore('promptStore', {
tag.key = newKey; tag.key = newKey;
if (!tag.translation) tag.translation = {}; if (!tag.translation) tag.translation = {};
tag.translation.en = newKey; tag.translation.en = newKey;
rebuildTagIndex(this.dataset);
}, },
setTranslation(groupId: string, key: string, lang: LangCode, val: string) { setTranslation(groupId: string, key: string, lang: LangCode, val: string) {
const grp = this.findGroupById(groupId); const grp = this.findGroupById(groupId);
@@ -432,16 +464,13 @@ export const usePromptStore = defineStore('promptStore', {
this.promptText = tokens.join(', '); this.promptText = tokens.join(', ');
}, },
getTagByKey(key: string): PromptTag | null { getTagByKey(key: string): PromptTag | null {
const target = normalizeKeyForMatch(key); if (!tagIndex && this.dataset) {
for (const cat of this.dataset?.categories || []) { rebuildTagIndex(this.dataset);
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; // 下划线/空格归一化匹配
}
}
} }
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 { getTranslation(key: string, lang: LangCode): string | null {
// 兼容包裹层:如 {aaa}、(aaa) 等 // 兼容包裹层:如 {aaa}、(aaa) 等
@@ -485,6 +514,7 @@ export const usePromptStore = defineStore('promptStore', {
} }
const grp = this.ensureCustomGroup(); const grp = this.ensureCustomGroup();
grp.tags.push({ key, translation: { en: key, [lang]: val } }); grp.tags.push({ key, translation: { en: key, [lang]: val } });
rebuildTagIndex(this.dataset);
}, },
ensureCustomGroup(): PromptGroup { ensureCustomGroup(): PromptGroup {
const catName = 'Custom'; const catName = 'Custom';