父文件选择优化,seo优化
This commit is contained in:
+4
-2
@@ -9,7 +9,7 @@
|
|||||||
<meta name="description"
|
<meta name="description"
|
||||||
content="提示词编辑器:用于编辑、管理与翻译 AI 提示词(支持多语言映射、预设保存、文件夹分类、拖拽排序、智能补全、导入导出与本地持久化)。提示词工具:纯前端无需后端与额外配置,点击即用;支持自定义静态部署与本地持久化;适用于 Stable Diffusion、Midjourney 等场景的提示词组织与复用。" />
|
content="提示词编辑器:用于编辑、管理与翻译 AI 提示词(支持多语言映射、预设保存、文件夹分类、拖拽排序、智能补全、导入导出与本地持久化)。提示词工具:纯前端无需后端与额外配置,点击即用;支持自定义静态部署与本地持久化;适用于 Stable Diffusion、Midjourney 等场景的提示词组织与复用。" />
|
||||||
<meta name="keywords"
|
<meta name="keywords"
|
||||||
content="提示词编辑器,点击即用,静态部署, 提示词管理, 词库, 预设, 多语言, 翻译, 拖拽排序, 智能补全, 导入导出, 本地持久化, Stable Diffusion, Midjourney, AI绘图" />
|
content="提示词编辑器,点击即用,静态部署, 提示词管理, 词库, 预设, 多语言, 翻译, 提示词翻译, AI 翻译, Prompt Translation, 拖拽排序, 智能补全, 导入导出, 本地持久化, Stable Diffusion, Midjourney, AI绘图" />
|
||||||
<meta name="robots" content="index, follow" />
|
<meta name="robots" content="index, follow" />
|
||||||
<meta name="googlebot" content="index, follow" />
|
<meta name="googlebot" content="index, follow" />
|
||||||
<link rel="canonical" href="https://prompt.sywb.top/" />
|
<link rel="canonical" href="https://prompt.sywb.top/" />
|
||||||
@@ -132,7 +132,9 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app">
|
||||||
|
<h1>AI 提示词编辑器 - 支持语言翻译与词库管理的智能工具</h1>
|
||||||
|
</div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { ExtendedPreset, PresetFolder, PresetType } from '../types';
|
|||||||
import NotificationToast from './NotificationToast.vue';
|
import NotificationToast from './NotificationToast.vue';
|
||||||
import PresetSidebar from './preset/PresetSidebar.vue';
|
import PresetSidebar from './preset/PresetSidebar.vue';
|
||||||
import PresetList from './preset/PresetList.vue';
|
import PresetList from './preset/PresetList.vue';
|
||||||
|
import FolderSelector from './preset/FolderSelector.vue';
|
||||||
|
|
||||||
const store = usePromptStore();
|
const store = usePromptStore();
|
||||||
|
|
||||||
@@ -133,26 +134,6 @@ const flattenedFolders = computed(() => {
|
|||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Parent options for folder dialog (exclude self and children)
|
|
||||||
const flattenedParentOptions = computed(() => {
|
|
||||||
const exclude = new Set<string>();
|
|
||||||
if (editingFolder.value) {
|
|
||||||
exclude.add(editingFolder.value.id);
|
|
||||||
|
|
||||||
// Helper to find descendants
|
|
||||||
const all = store.presetFolders || [];
|
|
||||||
function walk(id: string) {
|
|
||||||
const children = all.filter(f => f.parentId === id);
|
|
||||||
for (const c of children) {
|
|
||||||
exclude.add(c.id);
|
|
||||||
walk(c.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
walk(editingFolder.value.id);
|
|
||||||
}
|
|
||||||
return flattenedFolders.value.filter(f => !exclude.has(f.id));
|
|
||||||
});
|
|
||||||
|
|
||||||
const allPresetsCount = computed(() => (store.extendedPresets || []).length);
|
const allPresetsCount = computed(() => (store.extendedPresets || []).length);
|
||||||
const uncategorizedCount = computed(() => (store.extendedPresets || []).filter(p => !p.folderId).length);
|
const uncategorizedCount = computed(() => (store.extendedPresets || []).filter(p => !p.folderId).length);
|
||||||
|
|
||||||
@@ -480,12 +461,12 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>文件夹</label>
|
<label>文件夹</label>
|
||||||
<select v-model="presetForm.folderId">
|
<FolderSelector
|
||||||
<option value="">(无 - 未分类)</option>
|
v-model="presetForm.folderId"
|
||||||
<option v-for="f in flattenedFolders" :key="f.id" :value="f.id">
|
:tree="folderTree"
|
||||||
{{ f.label }}
|
:flattened="flattenedFolders"
|
||||||
</option>
|
root-label="(无 - 未分类)"
|
||||||
</select>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -526,12 +507,13 @@ onMounted(() => {
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>父文件夹</label>
|
<label>父文件夹</label>
|
||||||
<select v-model="folderForm.parentId">
|
<FolderSelector
|
||||||
<option value="">(无 - 根文件夹)</option>
|
v-model="folderForm.parentId"
|
||||||
<option v-for="f in flattenedParentOptions" :key="f.id" :value="f.id">
|
:tree="folderTree"
|
||||||
{{ f.label }}
|
:flattened="flattenedFolders"
|
||||||
</option>
|
:exclude-id="editingFolder?.id"
|
||||||
</select>
|
root-label="(无 - 根文件夹)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
|
||||||
|
import FolderSelectorItem from './FolderSelectorItem.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: string | null | undefined;
|
||||||
|
tree: any[];
|
||||||
|
flattened: any[]; // Used for quick name lookup
|
||||||
|
excludeId?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
rootLabel?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', val: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isOpen = ref(false);
|
||||||
|
const expandedIds = ref(new Set<string>());
|
||||||
|
const selectorRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const displayLabel = computed(() => {
|
||||||
|
if (!props.modelValue) return props.rootLabel || '(无)';
|
||||||
|
const found = props.flattened.find(f => f.id === props.modelValue);
|
||||||
|
return found ? found.name : (props.rootLabel || '(未知文件夹)');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-expand to selected folder on mount/change
|
||||||
|
watch(() => props.modelValue, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
expandToId(newVal);
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
function expandToId(id: string) {
|
||||||
|
// Find path to this id
|
||||||
|
// Since we don't have parent pointers easily available without traversal,
|
||||||
|
// we can rely on the fact that we want to ensure parents are expanded.
|
||||||
|
// A simple way is to traverse the tree.
|
||||||
|
// Or, we can just search in flattened list if it had parentId info?
|
||||||
|
// The flattened list in PresetManager has `label` which is path, but maybe not direct parent chain.
|
||||||
|
// Let's just do a simple tree search if needed.
|
||||||
|
|
||||||
|
// Actually, let's just rely on user expanding, OR simple search.
|
||||||
|
// For now, let's not overengineer auto-expansion unless requested.
|
||||||
|
// Wait, if I select a deep folder, I expect to see it if I open the dropdown again?
|
||||||
|
// Maybe just keep `expandedIds` persistent.
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDropdown() {
|
||||||
|
isOpen.value = !isOpen.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDropdown() {
|
||||||
|
isOpen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelect(id: string) {
|
||||||
|
emit('update:modelValue', id);
|
||||||
|
closeDropdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToggle(id: string) {
|
||||||
|
const newSet = new Set(expandedIds.value);
|
||||||
|
if (newSet.has(id)) {
|
||||||
|
newSet.delete(id);
|
||||||
|
} else {
|
||||||
|
newSet.add(id);
|
||||||
|
}
|
||||||
|
expandedIds.value = newSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click outside to close
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (selectorRef.value && !selectorRef.value.contains(event.target as Node)) {
|
||||||
|
closeDropdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', handleClickOutside);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', handleClickOutside);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="folder-selector" ref="selectorRef">
|
||||||
|
<!-- Trigger Button -->
|
||||||
|
<div class="selector-trigger" @click="toggleDropdown" :class="{ active: isOpen }">
|
||||||
|
<span class="selected-label">
|
||||||
|
<span v-if="!modelValue" class="icon-root">📂</span>
|
||||||
|
<span v-else class="icon-folder">📁</span>
|
||||||
|
{{ displayLabel }}
|
||||||
|
</span>
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" class="chevron" :class="{ rotated: isOpen }">
|
||||||
|
<path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown -->
|
||||||
|
<div v-if="isOpen" class="selector-dropdown">
|
||||||
|
<!-- Root Option -->
|
||||||
|
<div
|
||||||
|
class="root-option"
|
||||||
|
:class="{ active: !modelValue }"
|
||||||
|
@click="handleSelect('')"
|
||||||
|
>
|
||||||
|
<span class="icon-root">📂</span>
|
||||||
|
<span>{{ rootLabel || '(无)' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tree -->
|
||||||
|
<div class="tree-container">
|
||||||
|
<FolderSelectorItem
|
||||||
|
v-for="folder in tree"
|
||||||
|
:key="folder.id"
|
||||||
|
:folder="folder"
|
||||||
|
:level="0"
|
||||||
|
:selected-id="modelValue"
|
||||||
|
:exclude-id="excludeId"
|
||||||
|
:expanded-ids="expandedIds"
|
||||||
|
@select="handleSelect"
|
||||||
|
@toggle="handleToggle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.folder-selector {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-trigger:hover {
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-trigger.active {
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
box-shadow: 0 0 0 2px var(--color-accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron {
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron.rotated {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root-option:hover {
|
||||||
|
background-color: var(--color-bg-tertiary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root-option.active {
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-container {
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-root, .icon-folder {
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { PresetFolder } from '../../types';
|
||||||
|
|
||||||
|
interface FolderNode extends PresetFolder {
|
||||||
|
children: FolderNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
folder: FolderNode;
|
||||||
|
level: number;
|
||||||
|
selectedId: string | null | undefined;
|
||||||
|
excludeId?: string;
|
||||||
|
expandedIds: Set<string>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'select', id: string): void;
|
||||||
|
(e: 'toggle', id: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isExpanded = computed(() => props.expandedIds.has(props.folder.id));
|
||||||
|
const isSelected = computed(() => props.selectedId === props.folder.id);
|
||||||
|
const isExcluded = computed(() => props.excludeId && props.folder.id === props.excludeId);
|
||||||
|
|
||||||
|
function handleToggle(e: Event) {
|
||||||
|
e.stopPropagation();
|
||||||
|
emit('toggle', props.folder.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelect() {
|
||||||
|
emit('select', props.folder.id);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="!isExcluded" class="folder-selector-item">
|
||||||
|
<div
|
||||||
|
class="item-row"
|
||||||
|
:class="{ active: isSelected }"
|
||||||
|
:style="{ paddingLeft: `${level * 1.2 + 0.5}rem` }"
|
||||||
|
@click="handleSelect"
|
||||||
|
>
|
||||||
|
<!-- Toggle Arrow -->
|
||||||
|
<button
|
||||||
|
class="toggle-btn"
|
||||||
|
:class="{ invisible: !folder.children?.length }"
|
||||||
|
@click="handleToggle"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
:class="{ rotated: isExpanded }"
|
||||||
|
class="arrow-icon"
|
||||||
|
>
|
||||||
|
<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Folder Icon -->
|
||||||
|
<span class="folder-icon" :style="{ color: folder.color || '#6366f1' }">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" fill="currentColor" fill-opacity="0.2" stroke="currentColor" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="folder-name">{{ folder.name }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Children -->
|
||||||
|
<div v-if="isExpanded && folder.children?.length" class="item-children">
|
||||||
|
<FolderSelectorItem
|
||||||
|
v-for="child in folder.children"
|
||||||
|
:key="child.id"
|
||||||
|
:folder="child"
|
||||||
|
:level="level + 1"
|
||||||
|
:selected-id="selectedId"
|
||||||
|
:exclude-id="excludeId"
|
||||||
|
:expanded-ids="expandedIds"
|
||||||
|
@select="emit('select', $event)"
|
||||||
|
@toggle="emit('toggle', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.item-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
transition: background-color 0.1s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-row:hover {
|
||||||
|
background-color: var(--color-bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-row.active {
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-row.active .folder-icon {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn.invisible {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon.rotated {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-name {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user