Files
prompt/src/components/PromptManager.vue
T
2025-11-11 07:17:46 +08:00

211 lines
8.0 KiB
Vue

<script setup lang="ts">
import { onMounted, ref, computed } from 'vue';
import { usePromptStore } from '../stores/promptStore';
import type { LangCode, PromptGroup, PromptTag } from '../types';
const store = usePromptStore();
const draggingIndex = ref<number | null>(null);
const overIndex = ref<number | null>(null);
onMounted(() => {
store.initialize();
});
const languages = computed(() => store.languages);
const selectedLang = computed({
get: () => store.selectedLang,
set: (v: LangCode) => store.setLanguage(v),
});
const categories = computed(() => store.categories);
const currentCategory = computed(() => store.currentCategory);
const currentGroup = computed(() => store.currentGroup);
const filteredTags = computed(() => store.filteredTags);
function onDragStart(index: number) {
draggingIndex.value = index;
}
function onDragOver(index: number, e: DragEvent) {
e.preventDefault();
overIndex.value = index;
}
function onDrop(index: number) {
if (draggingIndex.value == null) return;
const from = draggingIndex.value;
const to = index;
const grpId = currentGroup.value?.id;
if (!grpId) return;
store.reorderTags(grpId, from, to);
draggingIndex.value = null;
overIndex.value = null;
}
function displayTranslation(tag: PromptTag): string {
return tag.translation?.[selectedLang.value] ?? tag.key;
}
function updateKey(tag: PromptTag, val: string) {
const gid = currentGroup.value?.id;
if (!gid) return;
store.updateTagKey(gid, tag.key, val);
}
function updateTrans(tag: PromptTag, val: string) {
const gid = currentGroup.value?.id;
if (!gid) return;
store.setTranslation(gid, tag.key, selectedLang.value, val);
}
function addTag() {
const gid = currentGroup.value?.id;
if (!gid) return;
store.addTag(gid);
}
function removeTag(tag: PromptTag) {
const gid = currentGroup.value?.id;
if (!gid) return;
store.removeTag(gid, tag.key);
}
function toggleHidden(tag: PromptTag) {
const gid = currentGroup.value?.id;
if (!gid) return;
store.toggleHidden(gid, tag.key);
}
function exportAll() {
const json = store.exportToJson();
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `prompt_dataset_${new Date().toISOString().slice(0,19)}.json`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
function importAll(ev: Event) {
const input = ev.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
try {
const text = String(reader.result);
const bundle = JSON.parse(text);
store.importFromBundle(bundle);
} catch (e) {
alert('导入失败:JSON 格式不正确');
} finally {
input.value = '';
}
};
reader.readAsText(file);
}
function resetDefault() {
store.resetToDefault();
}
</script>
<template>
<div class="pm-root">
<header class="pm-toolbar">
<div class="pm-left">
<label>语言</label>
<select v-model="selectedLang">
<option v-for="l in languages" :key="l" :value="l">{{ l }}</option>
</select>
<input
class="pm-search"
type="search"
placeholder="搜索关键字/翻译"
:value="store.searchQuery"
@input="store.setSearch(($event.target as HTMLInputElement).value)"
/>
</div>
<div class="pm-right">
<button class="pm-btn" @click="exportAll">导出 JSON</button>
<span class="pm-tip">导出 JSON 会同时包含你的预设</span>
<label class="pm-import pm-btn">导入 JSON
<input type="file" accept="application/json" @change="importAll" />
</label>
<button class="pm-btn" @click="resetDefault">重置为内置词库</button>
</div>
</header>
<main class="pm-main">
<aside class="pm-cats">
<div class="pm-section-title">分类</div>
<ul>
<li v-for="(c,ci) in categories" :key="c.id" :class="{ active: ci===store.selectedCategoryIndex }" @click="store.selectCategory(ci)">
{{ c.name }}
</li>
</ul>
<div class="pm-section-title">分组</div>
<ul>
<li v-for="(g,gi) in currentCategory?.groups" :key="g.id" :class="{ active: gi===store.selectedGroupIndex }" @click="store.selectGroup(gi)">
<span class="pm-color" :style="{ background: g.color || 'transparent' }"></span>
{{ g.name }}
</li>
</ul>
</aside>
<section class="pm-list">
<div class="pm-list-toolbar">
<button @click="addTag">新增提示词</button>
</div>
<div v-if="!currentGroup" class="pm-empty">请选择一个分组</div>
<ul v-else class="pm-tags">
<li
v-for="(t,ti) in filteredTags"
:key="t.key + '_' + ti"
:draggable="true"
@dragstart="onDragStart(ti)"
@dragover="onDragOver(ti, $event)"
@drop="onDrop(ti)"
:class="{ hidden: t.hidden }"
>
<span class="pm-handle">⋮⋮</span>
<input class="pm-key" :value="t.key" @input="updateKey(t, ($event.target as HTMLInputElement).value)" />
<input class="pm-trans" :value="displayTranslation(t)" @input="updateTrans(t, ($event.target as HTMLInputElement).value)" />
<button class="pm-hide" @click="toggleHidden(t)">{{ t.hidden ? '显示' : '隐藏' }}</button>
<button class="pm-del" @click="removeTag(t)">删除</button>
</li>
</ul>
</section>
</main>
</div>
</template>
<style scoped>
.pm-root { display: flex; flex-direction: column; height: 100vh; font-family: system-ui, -apple-system, Segoe UI, Roboto, 'Noto Sans', 'PingFang SC', 'Microsoft YaHei', sans-serif; }
.pm-toolbar { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; border-bottom: 1px solid #e5e7eb; gap: 12px; }
.pm-left { display: flex; align-items: center; gap: 8px; }
.pm-right { display: flex; align-items: center; gap: 8px; }
.pm-tip { font-size: 12px; color: #6b7280; }
.pm-btn { display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; border: 1px solid #d1d5db; border-radius: 6px; background: white; cursor: pointer; line-height: 1; font-size: 14px; }
.pm-import { display: inline-flex; align-items: center; gap: 6px; }
.pm-import input { display: none; }
.pm-search { width: 240px; padding: 6px 8px; border: 1px solid #d1d5db; border-radius: 6px; }
.pm-main { display: grid; grid-template-columns: 280px 1fr; height: calc(100vh - 50px); }
.pm-cats { border-right: 1px solid #e5e7eb; overflow: auto; padding: 8px; }
.pm-section-title { font-size: 12px; color: #6b7280; margin: 8px 0; }
.pm-cats ul { list-style: none; margin: 0; padding: 0; }
.pm-cats li { padding: 6px 8px; border-radius: 6px; cursor: pointer; }
.pm-cats li.active { background: #eef2ff; }
.pm-color { display: inline-block; width: 12px; height: 12px; border-radius: 2px; margin-right: 6px; vertical-align: middle; }
.pm-list { padding: 8px; overflow: auto; }
.pm-list-toolbar { display: flex; justify-content: flex-end; margin-bottom: 8px; }
.pm-empty { color: #6b7280; padding: 20px; }
.pm-tags { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.pm-tags li { display: grid; grid-template-columns: 24px 1fr 1fr auto auto; align-items: center; gap: 6px; padding: 6px; border: 1px solid #e5e7eb; border-radius: 6px; }
.pm-tags li.hidden { opacity: 0.5; }
.pm-handle { cursor: grab; user-select: none; color: #6b7280; text-align: center; }
.pm-key, .pm-trans { padding: 6px 8px; border: 1px solid #d1d5db; border-radius: 6px; }
.pm-hide, .pm-del, .pm-list-toolbar button, .pm-right button { padding: 6px 10px; border: 1px solid #d1d5db; border-radius: 6px; background: white; cursor: pointer; line-height: 1; }
.pm-hide:hover, .pm-del:hover, .pm-list-toolbar button:hover, .pm-right button:hover, .pm-btn:hover { background: #f3f4f6; }
</style>