修复编辑卡的问题
This commit is contained in:
@@ -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) {
|
|
||||||
|
if (!dragStarted.value) {
|
||||||
|
if (Math.hypot(dx, dy) > DRAG_THRESHOLD) {
|
||||||
dragStarted.value = true;
|
dragStarted.value = true;
|
||||||
isDragging.value = true;
|
isDragging.value = true;
|
||||||
if (draggingIndex.value != null) createPointerPreview(draggingIndex.value);
|
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;
|
||||||
|
|
||||||
|
// 计算鼠标在容器内的相对坐标
|
||||||
|
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;
|
overIndex.value = null;
|
||||||
const el = document.elementFromPoint(x, y) as HTMLElement | null;
|
insertSide.value = null;
|
||||||
if (!el) return;
|
return;
|
||||||
const tokenEl = el.closest('.pe-token-compact, .pe-token-detail') as HTMLElement | null;
|
}
|
||||||
if (!tokenEl) return;
|
|
||||||
const idxAttr = tokenEl.getAttribute('data-index');
|
overIndex.value = target.index;
|
||||||
if (idxAttr == null) return;
|
insertSide.value = relX < target.midX ? 'before' : 'after';
|
||||||
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 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"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
if (!tagIndex && this.dataset) {
|
||||||
|
rebuildTagIndex(this.dataset);
|
||||||
|
}
|
||||||
|
if (!tagIndex || !tagNormIndex) return null;
|
||||||
|
if (tagIndex.has(key)) return tagIndex.get(key)!;
|
||||||
const target = normalizeKeyForMatch(key);
|
const target = normalizeKeyForMatch(key);
|
||||||
for (const cat of this.dataset?.categories || []) {
|
return tagNormIndex.get(target) || null;
|
||||||
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;
|
|
||||||
},
|
},
|
||||||
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';
|
||||||
|
|||||||
Reference in New Issue
Block a user