初始化
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user