在提示词页面也添加翻译支持
This commit is contained in:
@@ -1,11 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, reactive, nextTick, watch } from 'vue';
|
||||
import { usePromptStore } from '../stores/promptStore';
|
||||
import type { LangCode, PromptGroup, PromptTag } from '../types';
|
||||
import type { LangCode, PromptTag } from '../types';
|
||||
|
||||
const store = usePromptStore();
|
||||
const draggingIndex = ref<number | null>(null);
|
||||
const overIndex = ref<number | null>(null);
|
||||
const keyInputRefs = ref<HTMLInputElement[]>([]);
|
||||
const tagIds = new WeakMap<PromptTag, string>();
|
||||
let tagIdSeq = 0;
|
||||
|
||||
type TagDraft = {
|
||||
key: string;
|
||||
translation: string;
|
||||
};
|
||||
|
||||
const drafts = reactive<Record<string, TagDraft>>({});
|
||||
const translating = reactive<Record<string, boolean>>({});
|
||||
const translateErrors = reactive<Record<string, string>>({});
|
||||
|
||||
const languages = computed(() => store.languages);
|
||||
const selectedLang = computed({
|
||||
@@ -18,6 +30,37 @@ const currentGroup = computed(() => store.currentGroup);
|
||||
const filteredTags = computed(() => store.filteredTags);
|
||||
const isSearching = computed(() => store.searchQuery.trim().length > 0);
|
||||
|
||||
watch(selectedLang, () => {
|
||||
for (const tag of filteredTags.value) {
|
||||
const id = getTagId(tag);
|
||||
drafts[id] = {
|
||||
key: tag.key,
|
||||
translation: displayTranslation(tag),
|
||||
};
|
||||
delete translateErrors[id];
|
||||
}
|
||||
});
|
||||
|
||||
function getTagId(tag: PromptTag): string {
|
||||
let id = tagIds.get(tag);
|
||||
if (!id) {
|
||||
id = `tag_${++tagIdSeq}`;
|
||||
tagIds.set(tag, id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
function getDraft(tag: PromptTag): TagDraft {
|
||||
const id = getTagId(tag);
|
||||
if (!drafts[id]) {
|
||||
drafts[id] = {
|
||||
key: tag.key,
|
||||
translation: displayTranslation(tag),
|
||||
};
|
||||
}
|
||||
return drafts[id];
|
||||
}
|
||||
|
||||
function onDragStart(index: number) {
|
||||
if (isSearching.value) return;
|
||||
draggingIndex.value = index;
|
||||
@@ -42,22 +85,40 @@ function displayTranslation(tag: PromptTag): string {
|
||||
return tag.translation?.[selectedLang.value] ?? tag.key;
|
||||
}
|
||||
|
||||
function updateKey(tag: PromptTag, val: string) {
|
||||
function commitKey(tag: PromptTag) {
|
||||
const gid = currentGroup.value?.id;
|
||||
if (!gid) return;
|
||||
store.updateTagKey(gid, tag.key, val);
|
||||
const draft = getDraft(tag);
|
||||
const nextKey = draft.key.trim();
|
||||
if (!nextKey || nextKey === tag.key) {
|
||||
draft.key = tag.key;
|
||||
return;
|
||||
}
|
||||
const oldKey = tag.key;
|
||||
store.updateTagKey(gid, oldKey, nextKey);
|
||||
if (tag.key !== oldKey) {
|
||||
if (draft.translation === oldKey) {
|
||||
draft.translation = nextKey;
|
||||
}
|
||||
draft.key = tag.key;
|
||||
}
|
||||
}
|
||||
|
||||
function updateTrans(tag: PromptTag, val: string) {
|
||||
function commitTrans(tag: PromptTag) {
|
||||
const gid = currentGroup.value?.id;
|
||||
if (!gid) return;
|
||||
store.setTranslation(gid, tag.key, selectedLang.value, val);
|
||||
const draft = getDraft(tag);
|
||||
store.setTranslation(gid, tag.key, selectedLang.value, draft.translation.trim());
|
||||
}
|
||||
|
||||
function addTag() {
|
||||
async function addTag() {
|
||||
const gid = currentGroup.value?.id;
|
||||
if (!gid) return;
|
||||
store.setSearch('');
|
||||
store.addTag(gid);
|
||||
await nextTick();
|
||||
keyInputRefs.value[0]?.focus();
|
||||
keyInputRefs.value[0]?.select();
|
||||
}
|
||||
|
||||
function removeTag(tag: PromptTag) {
|
||||
@@ -75,9 +136,50 @@ function confirmRemoveTag(tag: PromptTag) {
|
||||
function toggleHidden(tag: PromptTag) {
|
||||
const gid = currentGroup.value?.id;
|
||||
if (!gid) return;
|
||||
commitKey(tag);
|
||||
store.toggleHidden(gid, tag.key);
|
||||
}
|
||||
|
||||
function cleanPromptKey(key: string): string {
|
||||
return key
|
||||
.replace(/^[([{<]+/, '')
|
||||
.replace(/[)\]}>]+$/, '')
|
||||
.replace(/_/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
async function translateTag(tag: PromptTag) {
|
||||
const gid = currentGroup.value?.id;
|
||||
if (!gid) return;
|
||||
commitKey(tag);
|
||||
|
||||
const id = getTagId(tag);
|
||||
const key = tag.key.trim();
|
||||
if (!key || translating[id]) return;
|
||||
|
||||
translating[id] = true;
|
||||
delete translateErrors[id];
|
||||
|
||||
try {
|
||||
const target = selectedLang.value === 'zh_CN' ? 'zh' : selectedLang.value;
|
||||
const text = cleanPromptKey(key);
|
||||
const url = `https://sywb.top/api/translate2?text=${encodeURIComponent(text)}&sourceLang=auto&targetLang=${target}`;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
if (!data.success || !data.translation) {
|
||||
throw new Error(data.message || '翻译失败');
|
||||
}
|
||||
const translation = String(data.translation).trim();
|
||||
const draft = getDraft(tag);
|
||||
draft.translation = translation;
|
||||
store.setTranslation(gid, key, selectedLang.value, translation);
|
||||
} catch (error: any) {
|
||||
translateErrors[id] = error?.message || '翻译失败';
|
||||
} finally {
|
||||
translating[id] = false;
|
||||
}
|
||||
}
|
||||
|
||||
function exportAll() {
|
||||
const json = store.exportToJson();
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
@@ -244,7 +346,7 @@ function resetDefault() {
|
||||
|
||||
<div v-else class="pm-tags-wrapper">
|
||||
<TransitionGroup name="list" tag="ul" class="pm-tags-list" :css="!isSearching">
|
||||
<li v-for="(t, ti) in filteredTags" :key="t.key + '_' + ti"
|
||||
<li v-for="(t, ti) in filteredTags" :key="getTagId(t)"
|
||||
class="pm-tag-item"
|
||||
:draggable="!isSearching"
|
||||
@dragstart="onDragStart(ti)"
|
||||
@@ -271,22 +373,47 @@ function resetDefault() {
|
||||
<div class="pm-input-group">
|
||||
<label class="pm-input-label">Key</label>
|
||||
<input class="pm-input pm-key-input"
|
||||
:value="t.key"
|
||||
@input="updateKey(t, ($event.target as HTMLInputElement).value)"
|
||||
ref="keyInputRefs"
|
||||
v-model="getDraft(t).key"
|
||||
@blur="commitKey(t)"
|
||||
@keydown.enter.prevent="commitKey(t)"
|
||||
placeholder="提示词 Key"
|
||||
/>
|
||||
</div>
|
||||
<div class="pm-input-group">
|
||||
<div class="pm-input-label-row">
|
||||
<label class="pm-input-label">Translation</label>
|
||||
<span v-if="translateErrors[getTagId(t)]" class="pm-input-error" :title="translateErrors[getTagId(t)]">
|
||||
{{ translateErrors[getTagId(t)] }}
|
||||
</span>
|
||||
</div>
|
||||
<input class="pm-input pm-trans-input"
|
||||
:value="displayTranslation(t)"
|
||||
@input="updateTrans(t, ($event.target as HTMLInputElement).value)"
|
||||
v-model="getDraft(t).translation"
|
||||
@blur="commitTrans(t)"
|
||||
@keydown.enter.prevent="commitTrans(t)"
|
||||
placeholder="翻译内容"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pm-tag-actions">
|
||||
<button
|
||||
class="pm-icon-btn pm-btn-translate"
|
||||
:class="{ loading: translating[getTagId(t)] }"
|
||||
:disabled="translating[getTagId(t)]"
|
||||
@click="translateTag(t)"
|
||||
title="自动翻译当前提示词"
|
||||
>
|
||||
<svg v-if="!translating[getTagId(t)]" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="m5 8 6 6"></path>
|
||||
<path d="m4 14 6-6 2-3"></path>
|
||||
<path d="M2 5h12"></path>
|
||||
<path d="M7 2h1"></path>
|
||||
<path d="m22 22-5-10-5 10"></path>
|
||||
<path d="M14 18h6"></path>
|
||||
</svg>
|
||||
<span v-else class="pm-mini-spinner"></span>
|
||||
</button>
|
||||
<button class="pm-icon-btn" @click="toggleHidden(t)" :title="t.hidden ? '显示' : '隐藏'">
|
||||
<svg v-if="!t.hidden" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
||||
@@ -698,6 +825,24 @@ function resetDefault() {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.pm-input-label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
min-height: 15px;
|
||||
}
|
||||
|
||||
.pm-input-error {
|
||||
color: var(--color-error);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
max-width: 160px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pm-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
@@ -741,6 +886,35 @@ function resetDefault() {
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.pm-icon-btn:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.pm-btn-translate {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.pm-btn-translate:hover:not(:disabled),
|
||||
.pm-btn-translate.loading {
|
||||
background-color: var(--color-accent-light);
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.pm-mini-spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid currentColor;
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: pm-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes pm-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.pm-btn-delete:hover {
|
||||
background-color: #fef2f2;
|
||||
color: var(--color-error);
|
||||
|
||||
@@ -275,8 +275,15 @@ export const usePromptStore = defineStore('promptStore', {
|
||||
addTag(groupId: string, key = 'new_tag') {
|
||||
const grp = this.findGroupById(groupId);
|
||||
if (!grp) return;
|
||||
const existing = new Set(grp.tags.map((t) => t.key));
|
||||
let nextKey = key;
|
||||
let suffix = 2;
|
||||
while (existing.has(nextKey)) {
|
||||
nextKey = `${key}_${suffix}`;
|
||||
suffix++;
|
||||
}
|
||||
// 新增提示词插入到列表顶部,便于用户立即编辑
|
||||
grp.tags.unshift({ key, translation: { en: key, [this.selectedLang]: key } });
|
||||
grp.tags.unshift({ key: nextKey, translation: { en: nextKey, [this.selectedLang]: nextKey } });
|
||||
rebuildTagIndex(this.dataset);
|
||||
},
|
||||
removeTag(groupId: string, key: string) {
|
||||
@@ -290,9 +297,17 @@ export const usePromptStore = defineStore('promptStore', {
|
||||
if (!grp) return;
|
||||
const tag = grp.tags.find((t) => t.key === oldKey);
|
||||
if (!tag) return;
|
||||
tag.key = newKey;
|
||||
const baseKey = newKey.trim() || oldKey;
|
||||
const existing = new Set(grp.tags.filter((t) => t !== tag).map((t) => t.key));
|
||||
let nextKey = baseKey;
|
||||
let suffix = 2;
|
||||
while (existing.has(nextKey)) {
|
||||
nextKey = `${baseKey}_${suffix}`;
|
||||
suffix++;
|
||||
}
|
||||
tag.key = nextKey;
|
||||
if (!tag.translation) tag.translation = {};
|
||||
tag.translation.en = newKey;
|
||||
tag.translation.en = nextKey;
|
||||
rebuildTagIndex(this.dataset);
|
||||
},
|
||||
setTranslation(groupId: string, key: string, lang: LangCode, val: string) {
|
||||
|
||||
Reference in New Issue
Block a user