修复编辑卡的问题
This commit is contained in:
@@ -18,6 +18,17 @@ const startY = ref(0);
|
||||
const lastX = ref(0);
|
||||
const lastY = ref(0);
|
||||
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 editingIndex = ref<number | null>(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 {
|
||||
</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
|
||||
v-for="(k,i) in tokens"
|
||||
|
||||
@@ -5,6 +5,31 @@ import type { PromptDataset, PromptCategory, PromptGroup, PromptTag, LangCode, E
|
||||
const LS_KEY = 'ops.prompt.dataset.v1';
|
||||
let saveTimer: number | null = null; // 非响应式计时器,避免递归更新
|
||||
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 {
|
||||
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';
|
||||
|
||||
Reference in New Issue
Block a user