父文件选择优化,seo优化

This commit is contained in:
2025-11-29 13:44:18 +08:00
parent df58469d4b
commit f98b368793
4 changed files with 395 additions and 34 deletions
+4 -2
View File
@@ -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>
+14 -32
View File
@@ -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">
+224
View File
@@ -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>