增加拖拽快速提示词功能
This commit is contained in:
+157
-29
@@ -13,6 +13,7 @@ const draggingIndex = ref<number | null>(null);
|
|||||||
const overIndex = ref<number | null>(null);
|
const overIndex = ref<number | null>(null);
|
||||||
const dragPreview = ref<HTMLElement | null>(null);
|
const dragPreview = ref<HTMLElement | null>(null);
|
||||||
const isDragging = ref(false);
|
const isDragging = ref(false);
|
||||||
|
const externalDraggingTag = ref<string | null>(null);
|
||||||
// 指针拖拽新增状态
|
// 指针拖拽新增状态
|
||||||
const insertSide = ref<'before' | 'after' | null>(null);
|
const insertSide = ref<'before' | 'after' | null>(null);
|
||||||
const pointerId = ref<number | null>(null);
|
const pointerId = ref<number | null>(null);
|
||||||
@@ -449,6 +450,28 @@ function getTokenWrapperInfo(token: string) {
|
|||||||
return store.getTokenWrapperInfo(token);
|
return store.getTokenWrapperInfo(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cacheTokenRects() {
|
||||||
|
const dragContainer = tokenMappingRef.value?.dragContainer;
|
||||||
|
if (!dragContainer) {
|
||||||
|
cachedTokenRects.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selector = viewMode.value === 'compact' ? '.pe-token-compact' : '.pe-token-detail';
|
||||||
|
const elements = dragContainer.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);
|
||||||
|
}
|
||||||
|
|
||||||
// 指针事件版拖拽:更高性能且可自定义插入指示
|
// 指针事件版拖拽:更高性能且可自定义插入指示
|
||||||
function onPointerDown(index: number, e: PointerEvent) {
|
function onPointerDown(index: number, e: PointerEvent) {
|
||||||
if (editingIndex.value === index) return;
|
if (editingIndex.value === index) return;
|
||||||
@@ -469,23 +492,7 @@ function onPointerDown(index: number, e: PointerEvent) {
|
|||||||
insertSide.value = null;
|
insertSide.value = null;
|
||||||
|
|
||||||
// 缓存所有 Token 的位置信息 (相对于 dragContainer)
|
// 缓存所有 Token 的位置信息 (相对于 dragContainer)
|
||||||
const dragContainer = tokenMappingRef.value?.dragContainer;
|
cacheTokenRects();
|
||||||
if (dragContainer) {
|
|
||||||
const selector = viewMode.value === 'compact' ? '.pe-token-compact' : '.pe-token-detail';
|
|
||||||
const elements = dragContainer.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);
|
||||||
@@ -513,7 +520,7 @@ function handlePointerMove(e: PointerEvent) {
|
|||||||
if (rafId) return;
|
if (rafId) return;
|
||||||
rafId = requestAnimationFrame(() => {
|
rafId = requestAnimationFrame(() => {
|
||||||
positionPreview(lastX.value, lastY.value);
|
positionPreview(lastX.value, lastY.value);
|
||||||
updateOverIndexAndSideFast(lastX.value, lastY.value);
|
updateOverIndexAndSideFast(lastX.value, lastY.value, draggingIndex.value);
|
||||||
rafId = null;
|
rafId = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -587,7 +594,7 @@ function positionPreview(x: number, y: number) {
|
|||||||
dragPreview.value.style.transform = `translate(${x - dragOffsetX.value}px, ${y - dragOffsetY.value}px)`;
|
dragPreview.value.style.transform = `translate(${x - dragOffsetX.value}px, ${y - dragOffsetY.value}px)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateOverIndexAndSideFast(clientX: number, clientY: number) {
|
function updateOverIndexAndSideFast(clientX: number, clientY: number, activeDraggingIndex: number | null = null) {
|
||||||
const dragContainer = tokenMappingRef.value?.dragContainer;
|
const dragContainer = tokenMappingRef.value?.dragContainer;
|
||||||
if (!dragContainer) return;
|
if (!dragContainer) return;
|
||||||
|
|
||||||
@@ -596,21 +603,141 @@ function updateOverIndexAndSideFast(clientX: number, clientY: number) {
|
|||||||
const relX = clientX - containerRect.left;
|
const relX = clientX - containerRect.left;
|
||||||
const relY = clientY - containerRect.top;
|
const relY = clientY - containerRect.top;
|
||||||
|
|
||||||
// 在缓存中查找命中的 Token
|
const candidates = cachedTokenRects.value.filter(item => item.index !== activeDraggingIndex);
|
||||||
// 简单碰撞检测
|
if (!candidates.length) {
|
||||||
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;
|
||||||
insertSide.value = null;
|
insertSide.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 优先使用命中检测,鼠标落在 token 上时更准确
|
||||||
|
const target = candidates.find(item =>
|
||||||
|
relX >= item.left && relX <= item.left + item.width &&
|
||||||
|
relY >= item.top && relY <= item.top + item.height
|
||||||
|
);
|
||||||
|
|
||||||
|
if (target) {
|
||||||
overIndex.value = target.index;
|
overIndex.value = target.index;
|
||||||
insertSide.value = relX < target.midX ? 'before' : 'after';
|
insertSide.value = relX < target.midX ? 'before' : 'after';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 命中空隙时,选择同一行或最近的一项,支持任意位置插入
|
||||||
|
let nearest: typeof candidates[number] | null = null;
|
||||||
|
let bestScore = Number.POSITIVE_INFINITY;
|
||||||
|
for (const item of candidates) {
|
||||||
|
const dx = relX - item.midX;
|
||||||
|
const dy =
|
||||||
|
relY < item.top ? item.top - relY :
|
||||||
|
relY > item.top + item.height ? relY - (item.top + item.height) :
|
||||||
|
0;
|
||||||
|
const score = Math.abs(dx) + dy * 4;
|
||||||
|
if (score < bestScore) {
|
||||||
|
bestScore = score;
|
||||||
|
nearest = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nearest) {
|
||||||
|
overIndex.value = null;
|
||||||
|
insertSide.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
overIndex.value = nearest.index;
|
||||||
|
insertSide.value = relX < nearest.midX ? 'before' : 'after';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isQuickAddDragEvent(event: DragEvent): boolean {
|
||||||
|
const types = Array.from(event.dataTransfer?.types ?? []);
|
||||||
|
return types.includes('application/x-prompt-tag') || types.includes('text/plain');
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupExternalDrag() {
|
||||||
|
externalDraggingTag.value = null;
|
||||||
|
cachedTokenRects.value = [];
|
||||||
|
overIndex.value = null;
|
||||||
|
insertSide.value = null;
|
||||||
|
if (draggingIndex.value == null) {
|
||||||
|
isDragging.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleQuickAddDragStart(tag: string) {
|
||||||
|
externalDraggingTag.value = tag;
|
||||||
|
isDragging.value = true;
|
||||||
|
overIndex.value = null;
|
||||||
|
insertSide.value = null;
|
||||||
|
cacheTokenRects();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleQuickAddDragEnd() {
|
||||||
|
cleanupExternalDrag();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePanelDragOver(event: DragEvent) {
|
||||||
|
if (!isQuickAddDragEvent(event)) return;
|
||||||
|
event.preventDefault();
|
||||||
|
if (event.dataTransfer) {
|
||||||
|
event.dataTransfer.dropEffect = 'copy';
|
||||||
|
}
|
||||||
|
if (!externalDraggingTag.value) {
|
||||||
|
externalDraggingTag.value =
|
||||||
|
event.dataTransfer?.getData('application/x-prompt-tag') ||
|
||||||
|
event.dataTransfer?.getData('text/plain') ||
|
||||||
|
null;
|
||||||
|
}
|
||||||
|
isDragging.value = true;
|
||||||
|
if (!cachedTokenRects.value.length && tokens.value.length) {
|
||||||
|
cacheTokenRects();
|
||||||
|
}
|
||||||
|
updateOverIndexAndSideFast(event.clientX, event.clientY, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePanelDragLeave(event: DragEvent) {
|
||||||
|
if (!externalDraggingTag.value) return;
|
||||||
|
const dragContainer = tokenMappingRef.value?.dragContainer;
|
||||||
|
if (!dragContainer) return;
|
||||||
|
const related = event.relatedTarget as Node | null;
|
||||||
|
if (related && dragContainer.contains(related)) return;
|
||||||
|
const rect = dragContainer.getBoundingClientRect();
|
||||||
|
const inside =
|
||||||
|
event.clientX >= rect.left &&
|
||||||
|
event.clientX <= rect.right &&
|
||||||
|
event.clientY >= rect.top &&
|
||||||
|
event.clientY <= rect.bottom;
|
||||||
|
if (!inside) {
|
||||||
|
overIndex.value = null;
|
||||||
|
insertSide.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertTagIntoTokens(tag: string, targetIndex: number | null, side: 'before' | 'after' | null) {
|
||||||
|
const list = splitTokensLocal(text.value);
|
||||||
|
let insertAt = list.length;
|
||||||
|
if (targetIndex != null && side) {
|
||||||
|
insertAt = targetIndex + (side === 'after' ? 1 : 0);
|
||||||
|
}
|
||||||
|
insertAt = Math.max(0, Math.min(insertAt, list.length));
|
||||||
|
list.splice(insertAt, 0, normalizeToken(tag));
|
||||||
|
applyFullPrompt(list.join(', '));
|
||||||
|
showNotification('已插入提示词', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePanelDropTag(event: DragEvent) {
|
||||||
|
if (!isQuickAddDragEvent(event)) return;
|
||||||
|
event.preventDefault();
|
||||||
|
const tag =
|
||||||
|
externalDraggingTag.value ||
|
||||||
|
event.dataTransfer?.getData('application/x-prompt-tag') ||
|
||||||
|
event.dataTransfer?.getData('text/plain') ||
|
||||||
|
'';
|
||||||
|
if (!tag) {
|
||||||
|
cleanupExternalDrag();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
insertTagIntoTokens(tag, overIndex.value, insertSide.value);
|
||||||
|
cleanupExternalDrag();
|
||||||
}
|
}
|
||||||
|
|
||||||
function commitEdit(value: string) {
|
function commitEdit(value: string) {
|
||||||
@@ -740,14 +867,15 @@ function isRemoveDisabled(token: string): boolean {
|
|||||||
:get-suggestions="(prefix, limit) => store.getSuggestions(prefix, limit)"
|
:get-suggestions="(prefix, limit) => store.getSuggestions(prefix, limit)"
|
||||||
@update-suggestions="updateSuggestionsFromText" @copy="copyLeft" @replace-cn-comma="replaceCnComma"
|
@update-suggestions="updateSuggestionsFromText" @copy="copyLeft" @replace-cn-comma="replaceCnComma"
|
||||||
@format-prompt="formatPrompt" @unify-priority="unifyPriorityStyle" @toggle-underscore="toggleUnderscoreSpace"
|
@format-prompt="formatPrompt" @unify-priority="unifyPriorityStyle" @toggle-underscore="toggleUnderscoreSpace"
|
||||||
@add-tag="handleAddTag" />
|
@add-tag="handleAddTag" @drag-tag-start="handleQuickAddDragStart" @drag-tag-end="handleQuickAddDragEnd" />
|
||||||
|
|
||||||
<TokenMappingPanel ref="tokenMappingRef" :tokens="tokens" :selected-lang="selectedLang"
|
<TokenMappingPanel ref="tokenMappingRef" :tokens="tokens" :selected-lang="selectedLang"
|
||||||
v-model:view-mode="viewMode" :dragging-index="draggingIndex" :over-index="overIndex" :insert-side="insertSide"
|
v-model:view-mode="viewMode" :dragging-index="draggingIndex" :over-index="overIndex" :insert-side="insertSide"
|
||||||
:is-dragging="isDragging" :edit-suggestions="editSuggestions" :priority-style="priorityStyle"
|
:is-dragging="isDragging" :edit-suggestions="editSuggestions" :priority-style="priorityStyle"
|
||||||
:display-trans="displayTrans" :is-unmapped="isUnmapped" :get-token-wrapper-info="getTokenWrapperInfo"
|
:display-trans="displayTrans" :is-unmapped="isUnmapped" :get-token-wrapper-info="getTokenWrapperInfo"
|
||||||
:has-weight-suffix="hasWeightSuffix" :get-suggestions="(prefix, limit) => store.getSuggestions(prefix, limit)"
|
:has-weight-suffix="hasWeightSuffix" :get-suggestions="(prefix, limit) => store.getSuggestions(prefix, limit)"
|
||||||
@pointer-down="onPointerDown" @begin-edit="(i) => editingIndex = i" @commit-edit="commitEdit"
|
@pointer-down="onPointerDown" @panel-dragover="handlePanelDragOver" @panel-dragleave="handlePanelDragLeave"
|
||||||
|
@drop-tag="handlePanelDropTag" @begin-edit="(i) => editingIndex = i" @commit-edit="commitEdit"
|
||||||
@cancel-edit="() => editingIndex = null" @show-add-map="showAddMap" @add-wrapper="addWrapperToToken"
|
@cancel-edit="() => editingIndex = null" @show-add-map="showAddMap" @add-wrapper="addWrapperToToken"
|
||||||
@remove-wrapper="removeWrapperFromToken" @remove-token="removeToken" @add-token-after="addTokenAfter"
|
@remove-wrapper="removeWrapperFromToken" @remove-token="removeToken" @add-token-after="addTokenAfter"
|
||||||
@show-translation-popup="() => { translationTargetToken = null; showTranslationPopup = true; }"
|
@show-translation-popup="() => { translationTargetToken = null; showTranslationPopup = true; }"
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import type { PromptTag } from '../types';
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'add-tag', tag: string): void
|
(e: 'add-tag', tag: string): void
|
||||||
|
(e: 'drag-tag-start', tag: string): void
|
||||||
|
(e: 'drag-tag-end'): void
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const store = usePromptStore();
|
const store = usePromptStore();
|
||||||
@@ -17,6 +19,7 @@ const selectedLang = computed(() => store.selectedLang);
|
|||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
const visibleCount = ref(PAGE_SIZE);
|
const visibleCount = ref(PAGE_SIZE);
|
||||||
const tagsContainer = ref<HTMLElement | null>(null);
|
const tagsContainer = ref<HTMLElement | null>(null);
|
||||||
|
const draggedTagKey = ref<string | null>(null);
|
||||||
|
|
||||||
const visibleTags = computed(() => {
|
const visibleTags = computed(() => {
|
||||||
return filteredTags.value.slice(0, visibleCount.value);
|
return filteredTags.value.slice(0, visibleCount.value);
|
||||||
@@ -49,9 +52,26 @@ function selectGroup(index: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onTagClick(tag: PromptTag) {
|
function onTagClick(tag: PromptTag) {
|
||||||
|
if (draggedTagKey.value === tag.key) return;
|
||||||
emit('add-tag', tag.key);
|
emit('add-tag', tag.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onTagDragStart(tag: PromptTag, event: DragEvent) {
|
||||||
|
if (!event.dataTransfer) return;
|
||||||
|
draggedTagKey.value = tag.key;
|
||||||
|
event.dataTransfer.effectAllowed = 'copy';
|
||||||
|
event.dataTransfer.setData('text/plain', tag.key);
|
||||||
|
event.dataTransfer.setData('application/x-prompt-tag', tag.key);
|
||||||
|
emit('drag-tag-start', tag.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTagDragEnd() {
|
||||||
|
emit('drag-tag-end');
|
||||||
|
window.setTimeout(() => {
|
||||||
|
draggedTagKey.value = null;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
function displayTrans(tag: PromptTag) {
|
function displayTrans(tag: PromptTag) {
|
||||||
return tag.translation?.[selectedLang.value] ?? tag.key;
|
return tag.translation?.[selectedLang.value] ?? tag.key;
|
||||||
}
|
}
|
||||||
@@ -83,7 +103,8 @@ function displayTrans(tag: PromptTag) {
|
|||||||
|
|
||||||
<!-- Tags -->
|
<!-- Tags -->
|
||||||
<div class="pqa-tags" ref="tagsContainer" @scroll="onScroll">
|
<div class="pqa-tags" ref="tagsContainer" @scroll="onScroll">
|
||||||
<button v-for="tag in visibleTags" :key="tag.key" class="pqa-tag" @click="onTagClick(tag)" @mousedown.prevent
|
<button v-for="tag in visibleTags" :key="tag.key" class="pqa-tag" draggable="true" @click="onTagClick(tag)"
|
||||||
|
@dragstart="onTagDragStart(tag, $event)" @dragend="onTagDragEnd"
|
||||||
:title="tag.key">
|
:title="tag.key">
|
||||||
<span class="pqa-tag-text">{{ displayTrans(tag) }}</span>
|
<span class="pqa-tag-text">{{ displayTrans(tag) }}</span>
|
||||||
<span class="pqa-tag-sub" v-if="displayTrans(tag) !== tag.key">{{ tag.key }}</span>
|
<span class="pqa-tag-sub" v-if="displayTrans(tag) !== tag.key">{{ tag.key }}</span>
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ const emit = defineEmits<{
|
|||||||
'unify-priority': [];
|
'unify-priority': [];
|
||||||
'toggle-underscore': [];
|
'toggle-underscore': [];
|
||||||
'add-tag': [tag: string];
|
'add-tag': [tag: string];
|
||||||
|
'drag-tag-start': [tag: string];
|
||||||
|
'drag-tag-end': [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const inputEl = ref<HTMLTextAreaElement | null>(null);
|
const inputEl = ref<HTMLTextAreaElement | null>(null);
|
||||||
@@ -230,7 +232,11 @@ defineExpose({
|
|||||||
@click="applySuggestion(s)"
|
@click="applySuggestion(s)"
|
||||||
>{{ s }}</li>
|
>{{ s }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<PromptQuickAdd @add-tag="(tag) => emit('add-tag', tag)" />
|
<PromptQuickAdd
|
||||||
|
@add-tag="(tag) => emit('add-tag', tag)"
|
||||||
|
@drag-tag-start="(tag) => emit('drag-tag-start', tag)"
|
||||||
|
@drag-tag-end="() => emit('drag-tag-end')"
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ const props = defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:viewMode': [value: 'compact' | 'detail'];
|
'update:viewMode': [value: 'compact' | 'detail'];
|
||||||
'pointer-down': [index: number, event: PointerEvent];
|
'pointer-down': [index: number, event: PointerEvent];
|
||||||
|
'panel-dragover': [event: DragEvent];
|
||||||
|
'panel-dragleave': [event: DragEvent];
|
||||||
|
'drop-tag': [event: DragEvent];
|
||||||
'begin-edit': [index: number];
|
'begin-edit': [index: number];
|
||||||
'commit-edit': [value: string];
|
'commit-edit': [value: string];
|
||||||
'cancel-edit': [];
|
'cancel-edit': [];
|
||||||
@@ -194,7 +197,9 @@ defineExpose({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pe-drag-container" ref="dragContainer" :class="{ 'is-dragging': isDragging }">
|
<div class="pe-drag-container" ref="dragContainer" :class="{ 'is-dragging': isDragging }"
|
||||||
|
@dragover="emit('panel-dragover', $event)" @dragleave="emit('panel-dragleave', $event)"
|
||||||
|
@drop="emit('drop-tag', $event)">
|
||||||
<!-- 精简视图 -->
|
<!-- 精简视图 -->
|
||||||
<div class="pe-tokens-compact" v-if="viewMode === 'compact'">
|
<div class="pe-tokens-compact" v-if="viewMode === 'compact'">
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user