diff --git a/src/components/PromptEditor.vue b/src/components/PromptEditor.vue index a71902c..374caa0 100644 --- a/src/components/PromptEditor.vue +++ b/src/components/PromptEditor.vue @@ -65,8 +65,16 @@ const selectedLang = computed({ const tokens = computed(() => store.tokens); const suggestions = ref([]); +const editSuggestions = ref([]); const inputEl = ref(null); -const editEl = ref(null); +// 注意:ref 在 v-for 中可能成为数组,这里做统一归一化处理 +const editEl = ref(null); + +function currentEditEl(): HTMLInputElement | null { + const raw = editEl.value as any; + if (!raw) return null; + return Array.isArray(raw) ? (raw[0] ?? null) : raw; +} const text = ref(''); watch(text, (val) => { @@ -80,10 +88,52 @@ watch(() => store.promptText, (v) => { }, { immediate: true }); function updateSuggestions() { - const text = store.promptText; - const last = text.split(',').pop() || ''; - const p = last.trim(); - suggestions.value = store.getSuggestions(p, 8); + const el = inputEl.value; + const txt = store.promptText; + let pos = txt.length; + if (el && typeof el.selectionStart === 'number') { + pos = el.selectionStart ?? txt.length; + } + // 计算当前光标所在片段:左右最近的逗号之间 + const leftCommaEn = txt.lastIndexOf(',', pos - 1); + const leftCommaCn = txt.lastIndexOf(',', pos - 1); + const left = Math.max(leftCommaEn, leftCommaCn); + const rightCommaEn = txt.indexOf(',', pos); + const rightCommaCn = txt.indexOf(',', pos); + const rightCandidates = [rightCommaEn, rightCommaCn].filter(i => i !== -1); + const right = rightCandidates.length ? Math.min(...rightCandidates) : txt.length; + const segment = txt.slice(left < 0 ? 0 : left + 1, right).trim(); + suggestions.value = store.getSuggestions(segment, 8); +} + +function updateEditSuggestions() { + const el = currentEditEl(); + const val = editingValue.value || ''; + let pos = val.length; + if (el && typeof el.selectionStart === 'number') { + pos = el.selectionStart ?? val.length; + } + // 对编辑输入,使用光标左侧内容作为前缀 + const before = val.slice(0, pos); + const match = before.match(/[^,,]*$/); + const prefix = (match ? match[0] : before).trim(); + editSuggestions.value = store.getSuggestions(prefix, 8); +} + +// 计算左侧输入(textarea)基于光标位置的片段替换范围(修剪前后空格) +function getTextSegmentBounds(txt: string, pos: number) { + const leftCommaEn = txt.lastIndexOf(',', pos - 1); + const leftCommaCn = txt.lastIndexOf(',', pos - 1); + const left = Math.max(leftCommaEn, leftCommaCn); + const rightCommaEn = txt.indexOf(',', pos); + const rightCommaCn = txt.indexOf(',', pos); + const rightCandidates = [rightCommaEn, rightCommaCn].filter(i => i !== -1); + const right = rightCandidates.length ? Math.min(...rightCandidates) : txt.length; + let start = left < 0 ? 0 : left + 1; + let end = right; + while (start < end && txt[start] && /\s/.test(txt[start]!)) start++; + while (end > start && txt[end - 1] && /\s/.test(txt[end - 1]!)) end--; + return { start, end }; } async function onKeyDown(e: KeyboardEvent) { @@ -95,19 +145,17 @@ async function onKeyDown(e: KeyboardEvent) { const before = store.promptText.slice(0, pos); const match = before.match(/[^,,]*$/); const prefix = (match ? match[0] : '').trim(); - const start = pos - (match ? match[0].length : 0); + const { start, end } = getTextSegmentBounds(store.promptText, pos); const list = store.getSuggestions(prefix, 8); if (list.length > 0) { e.preventDefault(); const s = list[0]; if (!s) return; - const after = store.promptText.slice(pos); - const nextText = store.promptText.slice(0, start) + s + after; - text.value = nextText; + // 通过 setRangeText 模拟用户输入,保留撤回/前进栈 + el.setRangeText(s, start, end, 'end'); + // 确保触发 v-model 同步 + el.dispatchEvent(new Event('input', { bubbles: true })); await nextTick(); - // 将光标移动到补全结尾 - const newPos = start + s.length; - el.setSelectionRange(newPos, newPos); updateSuggestions(); } } @@ -267,9 +315,10 @@ function beginEdit(i: number) { editingValue.value = tokens.value[i] ?? ''; addingMapIndex.value = null; nextTick(() => { - if (editEl.value) { - editEl.value.focus(); - try { editEl.value.setSelectionRange(0, editingValue.value.length); } catch {} + const el = currentEditEl(); + if (el) { + el.focus(); + try { el.setSelectionRange(0, editingValue.value.length); } catch {} } }); } @@ -350,19 +399,48 @@ function handlePresetRename(oldName: string, newName: string) { async function applySuggestion(s: string) { const el = inputEl.value; if (!el) return; + // 确保输入框获得焦点,避免点击建议导致焦点丢失 + el.focus(); const pos = el.selectionStart ?? store.promptText.length; - const before = store.promptText.slice(0, pos); - const match = before.match(/[^,,]*$/); - const start = pos - (match ? match[0].length : 0); - const after = store.promptText.slice(pos); - const nextText = store.promptText.slice(0, start) + s + after; - text.value = nextText; + const { start, end } = getTextSegmentBounds(store.promptText, pos); + // 使用 setRangeText 替换整个片段以支持撤回 + el.setRangeText(s, start, end, 'end'); + el.dispatchEvent(new Event('input', { bubbles: true })); await nextTick(); - const newPos = start + s.length; - el.setSelectionRange(newPos, newPos); updateSuggestions(); } +function onEditKeyDown(e: KeyboardEvent) { + if (e.key !== 'Tab') return; + const el = currentEditEl(); + if (!el) return; + const val = editingValue.value || ''; + const pos = el.selectionStart ?? val.length; + const before = val.slice(0, pos); + const match = before.match(/[^,,]*$/); + const prefix = (match ? match[0] : '').trim(); + const list = store.getSuggestions(prefix, 8); + if (list.length > 0) { + e.preventDefault(); + const s = list[0]; + if (s) el.setRangeText(s, 0, val.length, 'end'); + el.dispatchEvent(new Event('input', { bubbles: true })); + updateEditSuggestions(); + } +} + +function applyEditSuggestion(s: string) { + const el = currentEditEl(); + if (!el) return; + // 保持焦点在编辑输入上 + el.focus(); + const val = editingValue.value || ''; + // 直接替换整个输入为建议 + el.setRangeText(s, 0, val.length, 'end'); + el.dispatchEvent(new Event('input', { bubbles: true })); + updateEditSuggestions(); +} + function displayTrans(key: string): string { return store.getTranslation(key, selectedLang.value) ?? key; } @@ -431,7 +509,15 @@ function displayTrans(key: string): string {
提示词输入(逗号分隔)
- +
    -
  • {{ s }}
  • +
  • {{ s }}
@@ -496,10 +587,21 @@ function displayTrans(key: string): string { ref="editEl" class="pe-edit-input" v-model="editingValue" + @keydown="onEditKeyDown" @keydown.enter.stop.prevent="commitEdit" @keydown.esc.stop.prevent="cancelEdit" + @click="updateEditSuggestions" + @keyup="updateEditSuggestions" placeholder="编辑提示词" /> +
    +
  • {{ s }}
  • +
- + +
    +
  • {{ s }}
  • +
@@ -1033,6 +1151,33 @@ function displayTrans(key: string): string { box-shadow: var(--shadow-sm); } +/* 编辑输入建议列表(紧凑尺寸) */ +.pe-edit-suggest { + list-style: none; + margin: 0.25rem 0 0; + padding: 0; + display: flex; + flex-wrap: wrap; + gap: 0.25rem; +} + +.pe-edit-suggest li { + padding: 0.25rem 0.5rem; + background-color: var(--color-bg-secondary); + color: var(--color-text-primary); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + font-size: 0.75rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.pe-edit-suggest li:hover { + background-color: var(--color-accent); + color: white; + border-color: var(--color-accent); +} + /* 紧凑行视图 */ .pe-tokens-compact { display: flex;