Files
prompt/src/components/PresetDropdown.vue
T
2025-11-11 15:21:13 +08:00

1094 lines
29 KiB
Vue

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { usePromptStore } from '../stores/promptStore';
import type { PromptPreset } from '../types';
import NotificationToast from './NotificationToast.vue';
const store = usePromptStore();
// Props
const props = defineProps<{
show: boolean;
}>();
// Emits
const emit = defineEmits<{
close: [];
load: [name: string];
save: [name: string];
delete: [name: string];
rename: [oldName: string, newName: string];
}>();
// 组件状态
const presetSearch = ref('');
const renamingPreset = ref<string | null>(null);
const renamingValue = ref('');
const newPresetName = ref('');
const showCreateForm = ref(false);
const sortBy = ref<'name' | 'date'>('date');
const sortOrder = ref<'asc' | 'desc'>('desc');
// 通知状态与方法(复用现有提示框架)
const notification = ref<{ message: string; type: 'success' | 'error' | 'info'; show: boolean }>({
message: '',
type: 'info',
show: false
});
function showNotification(message: string, type: 'success' | 'error' | 'info' = 'info') {
notification.value = { message, type, show: true };
setTimeout(() => {
notification.value.show = false;
}, 3000);
}
// 计算属性
const filteredPresets = computed(() => {
const q = presetSearch.value.trim().toLowerCase();
// 合并旧预设和新预设,优先显示新预设
let list = [
...store.extendedPresets.map(p => ({
name: p.name,
text: p.content,
updatedAt: p.updatedAt,
type: p.type,
description: p.description,
isExtended: true
})),
...store.presets.map(p => ({
name: p.name,
text: p.text,
updatedAt: p.updatedAt,
type: 'positive' as const,
description: undefined,
isExtended: false
}))
];
// 去重(如果新旧预设有同名的,优先保留新预设)
const seen = new Set();
list = list.filter(p => {
const key = `${p.name}_${p.type}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
// 搜索过滤 - 支持名称、内容、描述、标签、文件夹搜索
if (q) {
list = list.filter((p) => {
// 基本搜索:名称、内容、描述
const basicMatch = p.name.toLowerCase().includes(q) ||
p.text.toLowerCase().includes(q) ||
p.description?.toLowerCase().includes(q);
// 标签搜索(如果是扩展预设)
let tagMatch = false;
if (p.isExtended) {
const extendedPreset = store.extendedPresets.find(ep => ep.name === p.name && ep.type === p.type);
if (extendedPreset?.tags) {
tagMatch = extendedPreset.tags.some(tag => tag.toLowerCase().includes(q));
}
}
// 文件夹搜索(如果是扩展预设)
let folderMatch = false;
if (p.isExtended) {
const extendedPreset = store.extendedPresets.find(ep => ep.name === p.name && ep.type === p.type);
if (extendedPreset?.folderId) {
const folder = store.presetFolders.find(f => f.id === extendedPreset.folderId);
if (folder) {
folderMatch = folder.name.toLowerCase().includes(q);
}
}
}
// 类型搜索
const typeMatch = getTypeLabel(p.type).toLowerCase().includes(q);
return basicMatch || tagMatch || folderMatch || typeMatch;
});
}
// 排序
list.sort((a, b) => {
let comparison = 0;
if (sortBy.value === 'name') {
comparison = a.name.localeCompare(b.name);
} else {
comparison = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime();
}
return sortOrder.value === 'asc' ? comparison : -comparison;
});
return list;
});
const presetStats = computed(() => {
const totalOld = store.presets.length;
const totalExtended = store.extendedPresets.length;
const dayAgo = new Date();
dayAgo.setDate(dayAgo.getDate() - 1);
const recentOld = store.presets.filter(p => new Date(p.updatedAt) > dayAgo).length;
const recentExtended = store.extendedPresets.filter(p => new Date(p.updatedAt) > dayAgo).length;
return {
total: totalOld + totalExtended,
recent: recentOld + recentExtended
};
});
// 方法
function loadPreset(name: string) {
emit('load', name);
emit('close');
}
function saveNewPreset() {
const name = newPresetName.value.trim();
if (!name) return;
// 只保存到新的扩展预设系统
const defaultFolder = store.presetManagement?.settings?.defaultFolder;
store.createExtendedPreset({
name: name,
type: 'positive',
content: store.promptText,
description: '从快速预设创建',
folderId: defaultFolder
});
newPresetName.value = '';
showCreateForm.value = false;
}
function deletePreset(preset: any) {
if (confirm(`确定删除预设「${preset.name}」吗?`)) {
// 删除旧系统中的预设
if (!preset.isExtended) {
emit('delete', preset.name);
}
// 删除扩展预设系统中的预设
if (preset.isExtended) {
const extendedPreset = store.extendedPresets.find(p => p.name === preset.name && p.type === preset.type);
if (extendedPreset) {
store.deleteExtendedPreset(extendedPreset.id);
}
}
}
}
function beginRename(name: string) {
renamingPreset.value = name;
renamingValue.value = name;
}
function commitRename() {
if (!renamingPreset.value) return;
const oldName = renamingPreset.value;
const newName = renamingValue.value.trim();
if (!newName) {
alert('预设名称不能为空');
return;
}
if (newName !== oldName) {
emit('rename', oldName, newName);
}
cancelRename();
}
function cancelRename() {
renamingPreset.value = null;
renamingValue.value = '';
}
async function copyPresetToClipboard(preset: any) {
const text = preset?.text || '';
if (!text) return;
try {
await navigator.clipboard.writeText(text);
showNotification('已复制到剪贴板', 'success');
} catch (err) {
try {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.focus();
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
showNotification('已复制到剪贴板', 'success');
} catch (e) {
console.error('复制到剪贴板失败:', e);
showNotification('复制失败,请手动复制', 'error');
}
}
}
function exportPreset(preset: any) {
const data = {
name: preset.name,
text: preset.text,
type: preset.type,
description: preset.description,
updatedAt: preset.updatedAt,
exportedAt: new Date().toISOString()
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `preset-${preset.name}.json`;
a.click();
URL.revokeObjectURL(url);
}
function importPreset(event: Event) {
const file = (event.target as HTMLInputElement).files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target?.result as string);
if (data.name && data.text) {
const existingIndex = store.presets.findIndex(p => p.name === data.name);
if (existingIndex >= 0) {
if (confirm(`预设「${data.name}」已存在,是否覆盖?`)) {
store.presets[existingIndex] = {
name: data.name,
text: data.text,
updatedAt: new Date().toISOString()
};
}
} else {
store.presets.push({
name: data.name,
text: data.text,
updatedAt: new Date().toISOString()
});
}
store.save();
}
} catch (error) {
alert('导入失败,请检查文件格式');
}
};
reader.readAsText(file);
// 重置文件输入
(event.target as HTMLInputElement).value = '';
}
function getTypeLabel(type: string) {
const typeMap: Record<string, string> = {
'positive': '正面',
'negative': '负面',
'setting': '设定',
'style': '风格',
'character': '角色',
'scene': '场景',
'custom': '自定义'
};
return typeMap[type] || type;
}
function formatDate(dateStr: string) {
const date = new Date(dateStr);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
return '今天 ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
} else if (diffDays === 1) {
return '昨天 ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
} else if (diffDays < 7) {
return `${diffDays}天前`;
} else {
return date.toLocaleDateString('zh-CN');
}
}
function getPresetPreview(text: string, maxLength = 50) {
return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
}
function toggleSort(field: 'name' | 'date') {
if (sortBy.value === field) {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
} else {
sortBy.value = field;
sortOrder.value = field === 'name' ? 'asc' : 'desc';
}
}
// 键盘事件处理
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
emit('close');
}
}
onMounted(() => {
document.addEventListener('keydown', handleKeydown);
});
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown);
});
</script>
<template>
<Transition name="dropdown">
<div v-if="show" class="pd-dropdown">
<!-- 头部统计和操作 -->
<div class="pd-header">
<div class="pd-stats">
<span class="pd-stat-item">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" stroke="currentColor" stroke-width="2"/>
</svg>
{{ presetStats.total }} 个预设
</span>
<span v-if="presetStats.recent > 0" class="pd-stat-item pd-recent">
<svg width="14" height="14" 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"/>
<polyline points="12,6 12,12 16,14" stroke="currentColor" stroke-width="2"/>
</svg>
{{ presetStats.recent }} 个最近更新
</span>
</div>
<div class="pd-header-actions">
<label class="pd-import-btn" title="导入预设">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" stroke="currentColor" stroke-width="2"/>
<polyline points="17,8 12,3 7,8" stroke="currentColor" stroke-width="2"/>
<line x1="12" y1="3" x2="12" y2="15" stroke="currentColor" stroke-width="2"/>
</svg>
<input type="file" accept=".json" @change="importPreset" style="display: none;">
</label>
<button @click="showCreateForm = !showCreateForm" class="pd-create-btn" title="创建新预设">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="12" y1="5" x2="12" y2="19" stroke="currentColor" stroke-width="2"/>
<line x1="5" y1="12" x2="19" y2="12" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
</div>
</div>
<!-- 快速创建表单 -->
<Transition name="slide-down">
<div v-if="showCreateForm" class="pd-create-form">
<div class="pd-create-input">
<input
v-model="newPresetName"
placeholder="输入预设名称..."
@keyup.enter="saveNewPreset"
@keyup.escape="showCreateForm = false"
/>
<div class="pd-create-actions">
<button @click="saveNewPreset" class="pd-create-confirm" title="保存">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<polyline points="20,6 9,17 4,12" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<button @click="showCreateForm = false" class="pd-create-cancel" title="取消">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="18" y1="6" x2="6" y2="18" stroke="currentColor" stroke-width="2"/>
<line x1="6" y1="6" x2="18" y2="18" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
</div>
</div>
</div>
</Transition>
<!-- 搜索和排序 -->
<div class="pd-search-wrapper">
<div class="pd-search">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="11" cy="11" r="8" stroke="currentColor" stroke-width="2"/>
<path d="m21 21-4.35-4.35" stroke="currentColor" stroke-width="2"/>
</svg>
<input v-model="presetSearch" placeholder="搜索预设名称、内容、标签、文件夹..." />
</div>
<div class="pd-sort-controls">
<button
@click="toggleSort('name')"
:class="{ active: sortBy === 'name' }"
class="pd-sort-btn"
title="按名称排序"
>
名称
<svg v-if="sortBy === 'name'" width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<polyline :points="sortOrder === 'asc' ? '6,9 12,15 18,9' : '18,15 12,9 6,15'" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<button
@click="toggleSort('date')"
:class="{ active: sortBy === 'date' }"
class="pd-sort-btn"
title="按时间排序"
>
时间
<svg v-if="sortBy === 'date'" width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<polyline :points="sortOrder === 'asc' ? '6,9 12,15 18,9' : '18,15 12,9 6,15'" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
</div>
</div>
<!-- 预设列表 -->
<div class="pd-list">
<div v-if="filteredPresets.length === 0" class="pd-empty">
<div class="pd-empty-icon">📝</div>
<span>{{ presetSearch ? '未找到匹配的预设' : '暂无预设' }}</span>
</div>
<div v-for="p in filteredPresets" :key="`${p.name}_${p.type}`" class="pd-item">
<template v-if="renamingPreset !== p.name">
<div class="pd-item-main" @click="loadPreset(p.name)">
<div class="pd-item-header">
<div class="pd-item-title">
<span class="pd-item-name">{{ p.name }}</span>
<span v-if="p.isExtended" class="pd-item-type" :class="`type-${p.type}`">
{{ getTypeLabel(p.type) }}
</span>
</div>
<span class="pd-item-date">{{ formatDate(p.updatedAt) }}</span>
</div>
<div class="pd-item-preview">{{ getPresetPreview(p.text) }}</div>
<div v-if="p.description" class="pd-item-description">{{ p.description }}</div>
</div>
<div class="pd-item-actions">
<button @click.stop="copyPresetToClipboard(p)" class="pd-action-btn" title="复制到剪贴板">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke="currentColor" stroke-width="2"/>
<path d="m5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<button @click.stop="exportPreset(p)" class="pd-action-btn" title="导出">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" stroke="currentColor" stroke-width="2"/>
<polyline points="7,10 12,15 17,10" stroke="currentColor" stroke-width="2"/>
<line x1="12" y1="15" x2="12" y2="3" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<button @click.stop="beginRename(p.name)" class="pd-action-btn" title="重命名">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2"/>
<path d="m18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<button @click.stop="deletePreset(p)" class="pd-action-btn pd-delete" title="删除">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<polyline points="3,6 5,6 21,6" stroke="currentColor" stroke-width="2"/>
<path d="m19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
</div>
</template>
<template v-else>
<div class="pd-rename-form">
<input
v-model="renamingValue"
@keyup.enter="commitRename"
@keyup.escape="cancelRename"
@click.stop
class="pd-rename-input"
/>
<div class="pd-rename-actions">
<button @click.stop="commitRename" class="pd-rename-confirm" title="确定">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<polyline points="20,6 9,17 4,12" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<button @click.stop="cancelRename" class="pd-rename-cancel" title="取消">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="18" y1="6" x2="6" y2="18" stroke="currentColor" stroke-width="2"/>
<line x1="6" y1="6" x2="18" y2="18" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
</div>
</div>
</template>
</div>
</div>
<!-- 底部提示 -->
<div class="pd-footer">
<div class="pd-tips">
<span>💡 双击预设名称快速加载</span>
<span>🔍 支持搜索名称内容标签文件夹</span>
<span>⌨️ ESC 关闭面板</span>
</div>
</div>
<!-- 通知组件 -->
<NotificationToast
:message="notification.message"
:type="notification.type"
:show="notification.show"
/>
</div>
</Transition>
</template>
<style scoped>
.pd-dropdown {
position: absolute;
top: 100%;
right: 0;
z-index: 50;
min-width: 380px;
max-width: 500px;
max-height: 500px;
background-color: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xl);
margin-top: 0.5rem;
overflow: hidden;
display: flex;
flex-direction: column;
}
.pd-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--color-border);
background-color: var(--color-bg-secondary);
}
.pd-stats {
display: flex;
gap: 1rem;
font-size: 0.75rem;
}
.pd-stat-item {
display: flex;
align-items: center;
gap: 0.375rem;
color: var(--color-text-secondary);
}
.pd-stat-item.pd-recent {
color: var(--color-accent);
}
.pd-header-actions {
display: flex;
gap: 0.25rem;
}
.pd-import-btn, .pd-create-btn {
display: flex;
align-items: center;
justify-content: center;
width: 1.75rem;
height: 1.75rem;
border: 1px solid var(--color-border);
background-color: var(--color-bg-primary);
color: var(--color-text-secondary);
border-radius: var(--radius-sm);
cursor: pointer;
transition: all 0.2s ease;
}
.pd-import-btn:hover, .pd-create-btn:hover {
background-color: var(--color-accent);
color: white;
border-color: var(--color-accent);
}
.pd-create-form {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--color-border);
background-color: var(--color-bg-secondary);
}
.pd-create-input {
display: flex;
gap: 0.5rem;
}
.pd-create-input input {
flex: 1;
padding: 0.375rem 0.5rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
font-size: 0.875rem;
}
.pd-create-input input:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 2px var(--color-accent-light);
}
.pd-create-actions {
display: flex;
gap: 0.25rem;
}
.pd-create-confirm, .pd-create-cancel {
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
cursor: pointer;
transition: all 0.2s ease;
}
.pd-create-confirm {
background-color: var(--color-success);
color: white;
border-color: var(--color-success);
}
.pd-create-confirm:hover {
background-color: var(--color-success-hover);
}
.pd-create-cancel {
background-color: var(--color-bg-primary);
color: var(--color-text-secondary);
}
.pd-create-cancel:hover {
background-color: var(--color-error);
color: white;
border-color: var(--color-error);
}
.pd-search-wrapper {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--color-border);
display: flex;
gap: 0.75rem;
align-items: center;
}
.pd-search {
position: relative;
flex: 1;
}
.pd-search svg {
position: absolute;
left: 0.5rem;
top: 50%;
transform: translateY(-50%);
color: var(--color-text-tertiary);
}
.pd-search input {
width: 100%;
padding: 0.375rem 0.5rem 0.375rem 2rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background-color: var(--color-bg-secondary);
color: var(--color-text-primary);
font-size: 0.875rem;
transition: all 0.2s ease;
}
.pd-search input:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 2px var(--color-accent-light);
}
.pd-sort-controls {
display: flex;
gap: 0.25rem;
}
.pd-sort-btn {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
border: 1px solid var(--color-border);
background-color: var(--color-bg-primary);
color: var(--color-text-secondary);
border-radius: var(--radius-sm);
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s ease;
}
.pd-sort-btn:hover {
background-color: var(--color-bg-tertiary);
}
.pd-sort-btn.active {
background-color: var(--color-accent);
color: white;
border-color: var(--color-accent);
}
.pd-list {
flex: 1;
overflow-y: auto;
max-height: 300px;
}
.pd-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem 1rem;
text-align: center;
color: var(--color-text-tertiary);
font-size: 0.875rem;
}
.pd-empty-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
opacity: 0.5;
}
.pd-item {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--color-border);
transition: all 0.2s ease;
}
.pd-item:last-child {
border-bottom: none;
}
.pd-item:hover {
background-color: var(--color-bg-secondary);
}
.pd-item-main {
flex: 1;
cursor: pointer;
min-width: 0;
}
.pd-item-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 0.25rem;
}
.pd-item-title {
display: flex;
align-items: center;
gap: 0.5rem;
flex: 1;
min-width: 0;
}
.pd-item-name {
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.pd-item-type {
padding: 0.125rem 0.375rem;
border-radius: var(--radius-sm);
font-size: 0.6875rem;
font-weight: 500;
flex-shrink: 0;
}
.pd-item-type.type-positive {
background-color: #dcfce7;
color: #166534;
}
.pd-item-type.type-negative {
background-color: #fee2e2;
color: #991b1b;
}
.pd-item-type.type-setting {
background-color: #e0e7ff;
color: #3730a3;
}
.pd-item-type.type-style {
background-color: #fef3c7;
color: #92400e;
}
.pd-item-type.type-character {
background-color: #f3e8ff;
color: #6b21a8;
}
.pd-item-type.type-scene {
background-color: #ecfdf5;
color: #047857;
}
.pd-item-type.type-custom {
background-color: #f1f5f9;
color: #475569;
}
.pd-item-date {
font-size: 0.75rem;
color: var(--color-text-tertiary);
flex-shrink: 0;
margin-left: 0.5rem;
}
.pd-item-preview {
font-size: 0.75rem;
color: var(--color-text-secondary);
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 0.25rem;
}
.pd-item-description {
font-size: 0.6875rem;
color: var(--color-text-tertiary);
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-style: italic;
}
.pd-item-actions {
display: flex;
gap: 0.25rem;
opacity: 0.7;
transition: opacity 0.2s ease;
margin-left: 0.5rem;
}
.pd-item:hover .pd-item-actions {
opacity: 1;
}
.pd-action-btn {
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
border: 1px solid var(--color-border);
background-color: var(--color-bg-primary);
color: var(--color-text-secondary);
border-radius: var(--radius-sm);
cursor: pointer;
transition: all 0.2s ease;
}
.pd-action-btn:hover {
background-color: var(--color-bg-tertiary);
color: var(--color-text-primary);
}
.pd-action-btn.pd-delete:hover {
background-color: var(--color-error);
color: white;
border-color: var(--color-error);
}
.pd-rename-form {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
}
.pd-rename-input {
flex: 1;
padding: 0.375rem 0.5rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
font-size: 0.875rem;
}
.pd-rename-input:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 2px var(--color-accent-light);
}
.pd-rename-actions {
display: flex;
gap: 0.25rem;
}
.pd-rename-confirm, .pd-rename-cancel {
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
cursor: pointer;
transition: all 0.2s ease;
}
.pd-rename-confirm {
background-color: var(--color-success);
color: white;
border-color: var(--color-success);
}
.pd-rename-confirm:hover {
background-color: var(--color-success-hover);
}
.pd-rename-cancel {
background-color: var(--color-bg-primary);
color: var(--color-text-secondary);
}
.pd-rename-cancel:hover {
background-color: var(--color-error);
color: white;
border-color: var(--color-error);
}
.pd-footer {
padding: 0.5rem 1rem;
border-top: 1px solid var(--color-border);
background-color: var(--color-bg-secondary);
}
.pd-tips {
display: flex;
gap: 1rem;
font-size: 0.6875rem;
color: var(--color-text-tertiary);
}
/* 动画 */
.dropdown-enter-active {
transition: all 0.2s ease-out;
}
.dropdown-leave-active {
transition: all 0.2s ease-in;
}
.dropdown-enter-from {
opacity: 0;
transform: translateY(-10px) scale(0.95);
}
.dropdown-leave-to {
opacity: 0;
transform: translateY(-10px) scale(0.95);
}
.slide-down-enter-active {
transition: all 0.2s ease-out;
}
.slide-down-leave-active {
transition: all 0.2s ease-in;
}
.slide-down-enter-from {
opacity: 0;
max-height: 0;
transform: translateY(-10px);
}
.slide-down-leave-to {
opacity: 0;
max-height: 0;
transform: translateY(-10px);
}
/* 滚动条样式 */
.pd-list::-webkit-scrollbar {
width: 6px;
}
.pd-list::-webkit-scrollbar-track {
background: var(--color-bg-secondary);
}
.pd-list::-webkit-scrollbar-thumb {
background: var(--color-border-hover);
border-radius: 3px;
}
.pd-list::-webkit-scrollbar-thumb:hover {
background: var(--color-text-tertiary);
}
/* 响应式设计 */
@media (max-width: 768px) {
.pd-dropdown {
min-width: 320px;
max-width: 90vw;
}
.pd-search-wrapper {
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
}
.pd-sort-controls {
justify-content: center;
}
.pd-tips {
flex-direction: column;
gap: 0.25rem;
text-align: center;
}
.pd-item-actions {
opacity: 1;
}
}
</style>