初始化
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
message: string
|
||||
type: 'success' | 'error' | 'info'
|
||||
show: boolean
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="notification">
|
||||
<div v-if="show" class="notification-toast" :class="type">
|
||||
<div class="notification-content">
|
||||
<svg v-if="type === 'success'" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polyline points="22,4 12,14.01 9,11.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<svg v-else-if="type === 'error'" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
||||
<line x1="15" y1="9" x2="9" y2="15" stroke="currentColor" stroke-width="2"/>
|
||||
<line x1="9" y1="9" x2="15" y2="15" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
<svg v-else width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="m9 12 2 2 4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>{{ message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.notification-toast {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 1000;
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
padding: 1rem;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.notification-toast.success {
|
||||
background-color: var(--color-success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.notification-toast.error {
|
||||
background-color: var(--color-error);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.notification-toast.info {
|
||||
background-color: var(--color-accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.notification-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 通知动画 */
|
||||
.notification-enter-active {
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.notification-leave-active {
|
||||
transition: all 0.3s ease-in;
|
||||
}
|
||||
|
||||
.notification-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%) scale(0.9);
|
||||
}
|
||||
|
||||
.notification-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(100%) scale(0.9);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.notification-toast {
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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