211 lines
8.0 KiB
Vue
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> |