父文件选择优化,seo优化
This commit is contained in:
+4
-2
@@ -9,7 +9,7 @@
|
||||
<meta name="description"
|
||||
content="提示词编辑器:用于编辑、管理与翻译 AI 提示词(支持多语言映射、预设保存、文件夹分类、拖拽排序、智能补全、导入导出与本地持久化)。提示词工具:纯前端无需后端与额外配置,点击即用;支持自定义静态部署与本地持久化;适用于 Stable Diffusion、Midjourney 等场景的提示词组织与复用。" />
|
||||
<meta name="keywords"
|
||||
content="提示词编辑器,点击即用,静态部署, 提示词管理, 词库, 预设, 多语言, 翻译, 拖拽排序, 智能补全, 导入导出, 本地持久化, Stable Diffusion, Midjourney, AI绘图" />
|
||||
content="提示词编辑器,点击即用,静态部署, 提示词管理, 词库, 预设, 多语言, 翻译, 提示词翻译, AI 翻译, Prompt Translation, 拖拽排序, 智能补全, 导入导出, 本地持久化, Stable Diffusion, Midjourney, AI绘图" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<meta name="googlebot" content="index, follow" />
|
||||
<link rel="canonical" href="https://prompt.sywb.top/" />
|
||||
@@ -132,7 +132,9 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="app">
|
||||
<h1>AI 提示词编辑器 - 支持语言翻译与词库管理的智能工具</h1>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { ExtendedPreset, PresetFolder, PresetType } from '../types';
|
||||
import NotificationToast from './NotificationToast.vue';
|
||||
import PresetSidebar from './preset/PresetSidebar.vue';
|
||||
import PresetList from './preset/PresetList.vue';
|
||||
import FolderSelector from './preset/FolderSelector.vue';
|
||||
|
||||
const store = usePromptStore();
|
||||
|
||||
@@ -133,26 +134,6 @@ const flattenedFolders = computed(() => {
|
||||
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 uncategorizedCount = computed(() => (store.extendedPresets || []).filter(p => !p.folderId).length);
|
||||
|
||||
@@ -480,12 +461,12 @@ onMounted(() => {
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>文件夹</label>
|
||||
<select v-model="presetForm.folderId">
|
||||
<option value="">(无 - 未分类)</option>
|
||||
<option v-for="f in flattenedFolders" :key="f.id" :value="f.id">
|
||||
{{ f.label }}
|
||||
</option>
|
||||
</select>
|
||||
<FolderSelector
|
||||
v-model="presetForm.folderId"
|
||||
:tree="folderTree"
|
||||
:flattened="flattenedFolders"
|
||||
root-label="(无 - 未分类)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -526,12 +507,13 @@ onMounted(() => {
|
||||
|
||||
<div class="form-group">
|
||||
<label>父文件夹</label>
|
||||
<select v-model="folderForm.parentId">
|
||||
<option value="">(无 - 根文件夹)</option>
|
||||
<option v-for="f in flattenedParentOptions" :key="f.id" :value="f.id">
|
||||
{{ f.label }}
|
||||
</option>
|
||||
</select>
|
||||
<FolderSelector
|
||||
v-model="folderForm.parentId"
|
||||
:tree="folderTree"
|
||||
:flattened="flattenedFolders"
|
||||
:exclude-id="editingFolder?.id"
|
||||
root-label="(无 - 根文件夹)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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