样式美化-预设
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||||
import { usePromptStore } from '../stores/promptStore';
|
import { usePromptStore } from '../stores/promptStore';
|
||||||
import type { PromptPreset } from '../types';
|
import type { PromptPreset, PresetType } from '../types';
|
||||||
import NotificationToast from './NotificationToast.vue';
|
import NotificationToast from './NotificationToast.vue';
|
||||||
|
|
||||||
const store = usePromptStore();
|
const store = usePromptStore();
|
||||||
@@ -48,21 +48,28 @@ const filteredPresets = computed(() => {
|
|||||||
const q = presetSearch.value.trim().toLowerCase();
|
const q = presetSearch.value.trim().toLowerCase();
|
||||||
// 合并旧预设和新预设,优先显示新预设
|
// 合并旧预设和新预设,优先显示新预设
|
||||||
let list = [
|
let list = [
|
||||||
...store.extendedPresets.map(p => ({
|
...store.extendedPresets.map(p => {
|
||||||
|
const folder = p.folderId ? store.presetFolders.find(f => f.id === p.folderId) : null;
|
||||||
|
return {
|
||||||
name: p.name,
|
name: p.name,
|
||||||
text: p.content,
|
text: p.content,
|
||||||
updatedAt: p.updatedAt,
|
updatedAt: p.updatedAt,
|
||||||
type: p.type,
|
type: p.type,
|
||||||
description: p.description,
|
description: p.description,
|
||||||
isExtended: true
|
isExtended: true,
|
||||||
})),
|
folderId: p.folderId,
|
||||||
|
folderName: folder ? folder.name : '未分类'
|
||||||
|
};
|
||||||
|
}),
|
||||||
...store.presets.map(p => ({
|
...store.presets.map(p => ({
|
||||||
name: p.name,
|
name: p.name,
|
||||||
text: p.text,
|
text: p.text,
|
||||||
updatedAt: p.updatedAt,
|
updatedAt: p.updatedAt,
|
||||||
type: 'positive' as const,
|
type: 'positive' as const,
|
||||||
description: undefined,
|
description: undefined,
|
||||||
isExtended: false
|
isExtended: false,
|
||||||
|
folderId: null,
|
||||||
|
folderName: '未分类'
|
||||||
}))
|
}))
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -127,6 +134,31 @@ const filteredPresets = computed(() => {
|
|||||||
return list;
|
return list;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const groupedPresets = computed(() => {
|
||||||
|
const list = filteredPresets.value;
|
||||||
|
// 如果有搜索或者排序不是默认的,使用平铺列表(视为一个组)
|
||||||
|
if (presetSearch.value || sortBy.value !== 'date') {
|
||||||
|
return [{ name: presetSearch.value ? '搜索结果' : '所有预设', presets: list }];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分组
|
||||||
|
const groups: Record<string, typeof list> = {};
|
||||||
|
list.forEach(p => {
|
||||||
|
const key = p.folderName || '未分类';
|
||||||
|
if (!groups[key]) groups[key] = [];
|
||||||
|
groups[key].push(p);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 转换为数组并排序
|
||||||
|
return Object.entries(groups)
|
||||||
|
.map(([name, presets]) => ({ name, presets }))
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.name === '未分类') return 1;
|
||||||
|
if (b.name === '未分类') return -1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const presetStats = computed(() => {
|
const presetStats = computed(() => {
|
||||||
const totalOld = store.presets.length;
|
const totalOld = store.presets.length;
|
||||||
const totalExtended = store.extendedPresets.length;
|
const totalExtended = store.extendedPresets.length;
|
||||||
@@ -293,6 +325,19 @@ function importPreset(event: Event) {
|
|||||||
(event.target as HTMLInputElement).value = '';
|
(event.target as HTMLInputElement).value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTypeIcon(type: string) {
|
||||||
|
const icons: Record<string, string> = {
|
||||||
|
positive: '👍',
|
||||||
|
negative: '👎',
|
||||||
|
setting: '⚙️',
|
||||||
|
style: '🎨',
|
||||||
|
character: '👤',
|
||||||
|
scene: '🌍',
|
||||||
|
custom: '📝'
|
||||||
|
};
|
||||||
|
return icons[type] || '📝';
|
||||||
|
}
|
||||||
|
|
||||||
function getTypeLabel(type: string) {
|
function getTypeLabel(type: string) {
|
||||||
const typeMap: Record<string, string> = {
|
const typeMap: Record<string, string> = {
|
||||||
'positive': '正面',
|
'positive': '正面',
|
||||||
@@ -462,20 +507,23 @@ onUnmounted(() => {
|
|||||||
<span>{{ presetSearch ? '未找到匹配的预设' : '暂无预设' }}</span>
|
<span>{{ presetSearch ? '未找到匹配的预设' : '暂无预设' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="p in filteredPresets" :key="`${p.name}_${p.type}`" class="pd-item">
|
<div v-for="group in groupedPresets" :key="group.name" class="pd-group">
|
||||||
|
<div v-if="groupedPresets.length > 1 || group.name !== '所有预设'" class="pd-group-header">
|
||||||
|
{{ group.name }}
|
||||||
|
<span class="pd-group-count">{{ group.presets.length }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="p in group.presets" :key="`${p.name}_${p.type}`" class="pd-item">
|
||||||
<template v-if="renamingPreset !== p.name">
|
<template v-if="renamingPreset !== p.name">
|
||||||
<div class="pd-item-main" @click="loadPreset(p.name)">
|
<div class="pd-item-main" @click="loadPreset(p.name)">
|
||||||
<div class="pd-item-header">
|
<div class="pd-item-header">
|
||||||
<div class="pd-item-title">
|
<div class="pd-item-title">
|
||||||
|
<span class="pd-item-icon" :title="getTypeLabel(p.type)">{{ getTypeIcon(p.type) }}</span>
|
||||||
<span class="pd-item-name">{{ p.name }}</span>
|
<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>
|
</div>
|
||||||
<span class="pd-item-date">{{ formatDate(p.updatedAt) }}</span>
|
<span class="pd-item-date">{{ formatDate(p.updatedAt) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pd-item-preview">{{ getPresetPreview(p.text) }}</div>
|
<div class="pd-item-preview">{{ getPresetPreview(p.text) }}</div>
|
||||||
<div v-if="p.description" class="pd-item-description">{{ p.description }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pd-item-actions">
|
<div class="pd-item-actions">
|
||||||
@@ -533,6 +581,7 @@ onUnmounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 底部提示 -->
|
<!-- 底部提示 -->
|
||||||
<div class="pd-footer">
|
<div class="pd-footer">
|
||||||
@@ -758,7 +807,7 @@ onUnmounted(() => {
|
|||||||
.pd-list {
|
.pd-list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 300px;
|
max-height: 350px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pd-empty {
|
.pd-empty {
|
||||||
@@ -778,10 +827,33 @@ onUnmounted(() => {
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pd-group-header {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background-color: var(--color-bg-tertiary);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pd-group-count {
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
padding: 0.125rem 0.375rem;
|
||||||
|
border-radius: 99px;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
.pd-item {
|
.pd-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.625rem 1rem;
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
@@ -802,7 +874,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.pd-item-header {
|
.pd-item-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
@@ -822,52 +894,14 @@ onUnmounted(() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pd-item-type {
|
.pd-item-icon {
|
||||||
padding: 0.125rem 0.375rem;
|
font-size: 1rem;
|
||||||
border-radius: var(--radius-sm);
|
line-height: 1;
|
||||||
font-size: 0.6875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
flex-shrink: 0;
|
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 {
|
.pd-item-date {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--color-text-tertiary);
|
color: var(--color-text-tertiary);
|
||||||
@@ -878,29 +912,19 @@ onUnmounted(() => {
|
|||||||
.pd-item-preview {
|
.pd-item-preview {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
line-height: 1.3;
|
line-height: 1.4;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
margin-bottom: 0.25rem;
|
opacity: 0.8;
|
||||||
}
|
|
||||||
|
|
||||||
.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 {
|
.pd-item-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
opacity: 0.7;
|
opacity: 0;
|
||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pd-item:hover .pd-item-actions {
|
.pd-item:hover .pd-item-actions {
|
||||||
|
|||||||
+404
-915
File diff suppressed because it is too large
Load Diff
@@ -631,6 +631,7 @@ function isRemoveDisabled(token: string): boolean {
|
|||||||
<template>
|
<template>
|
||||||
<div class="pe-root">
|
<div class="pe-root">
|
||||||
<header class="pe-toolbar">
|
<header class="pe-toolbar">
|
||||||
|
<div class="pe-toolbar-content">
|
||||||
<div class="pe-left">
|
<div class="pe-left">
|
||||||
<label>语言</label>
|
<label>语言</label>
|
||||||
<select v-model="selectedLang">
|
<select v-model="selectedLang">
|
||||||
@@ -686,6 +687,7 @@ function isRemoveDisabled(token: string): boolean {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="pe-main">
|
<main class="pe-main">
|
||||||
@@ -972,12 +974,18 @@ function isRemoveDisabled(token: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pe-toolbar {
|
.pe-toolbar {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 1rem 1.5rem;
|
padding: 1rem 1.5rem;
|
||||||
background-color: var(--color-bg-secondary);
|
background-color: var(--color-bg-secondary);
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pe-toolbar-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
@@ -1229,6 +1237,9 @@ function isRemoveDisabled(token: string): boolean {
|
|||||||
height: calc(100vh - 8rem);
|
height: calc(100vh - 8rem);
|
||||||
gap: 1px;
|
gap: 1px;
|
||||||
background-color: var(--color-border);
|
background-color: var(--color-border);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pe-left-pane, .pe-right-pane {
|
.pe-left-pane, .pe-right-pane {
|
||||||
|
|||||||
@@ -114,13 +114,8 @@ function resetDefault() {
|
|||||||
<select v-model="selectedLang" class="pm-select pm-lang">
|
<select v-model="selectedLang" class="pm-select pm-lang">
|
||||||
<option v-for="l in languages" :key="l" :value="l">{{ l }}</option>
|
<option v-for="l in languages" :key="l" :value="l">{{ l }}</option>
|
||||||
</select>
|
</select>
|
||||||
<input
|
<input class="pm-search" type="search" placeholder="搜索关键字/翻译" :value="store.searchQuery"
|
||||||
class="pm-search"
|
@input="store.setSearch(($event.target as HTMLInputElement).value)" />
|
||||||
type="search"
|
|
||||||
placeholder="搜索关键字/翻译"
|
|
||||||
:value="store.searchQuery"
|
|
||||||
@input="store.setSearch(($event.target as HTMLInputElement).value)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pm-right">
|
<div class="pm-right">
|
||||||
<button class="pm-btn" @click="exportAll">导出 JSON</button>
|
<button class="pm-btn" @click="exportAll">导出 JSON</button>
|
||||||
@@ -136,13 +131,15 @@ function resetDefault() {
|
|||||||
<aside class="pm-cats">
|
<aside class="pm-cats">
|
||||||
<div class="pm-section-title">分类</div>
|
<div class="pm-section-title">分类</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(c,ci) in categories" :key="c.id" :class="{ active: ci===store.selectedCategoryIndex }" @click="store.selectCategory(ci)">
|
<li v-for="(c, ci) in categories" :key="c.id" :class="{ active: ci === store.selectedCategoryIndex }"
|
||||||
|
@click="store.selectCategory(ci)">
|
||||||
{{ c.name }}
|
{{ c.name }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="pm-section-title">分组</div>
|
<div class="pm-section-title">分组</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(g,gi) in currentCategory?.groups" :key="g.id" :class="{ active: gi===store.selectedGroupIndex }" @click="store.selectGroup(gi)">
|
<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>
|
<span class="pm-color" :style="{ background: g.color || 'transparent' }"></span>
|
||||||
{{ g.name }}
|
{{ g.name }}
|
||||||
</li>
|
</li>
|
||||||
@@ -155,18 +152,13 @@ function resetDefault() {
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="!currentGroup" class="pm-empty">请选择一个分组</div>
|
<div v-if="!currentGroup" class="pm-empty">请选择一个分组</div>
|
||||||
<ul v-else class="pm-tags">
|
<ul v-else class="pm-tags">
|
||||||
<li
|
<li v-for="(t, ti) in filteredTags" :key="t.key + '_' + ti" :draggable="true" @dragstart="onDragStart(ti)"
|
||||||
v-for="(t,ti) in filteredTags"
|
@dragover="onDragOver(ti, $event)" @drop="onDrop(ti)"
|
||||||
:key="t.key + '_' + ti"
|
:class="{ hidden: t.hidden, 'pm-over': ti === overIndex }">
|
||||||
:draggable="true"
|
|
||||||
@dragstart="onDragStart(ti)"
|
|
||||||
@dragover="onDragOver(ti, $event)"
|
|
||||||
@drop="onDrop(ti)"
|
|
||||||
:class="{ hidden: t.hidden, 'pm-over': ti===overIndex }"
|
|
||||||
>
|
|
||||||
<span class="pm-handle">⋮⋮</span>
|
<span class="pm-handle">⋮⋮</span>
|
||||||
<input class="pm-key" :value="t.key" @input="updateKey(t, ($event.target as HTMLInputElement).value)" />
|
<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)" />
|
<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-hide" @click="toggleHidden(t)">{{ t.hidden ? '显示' : '隐藏' }}</button>
|
||||||
<button class="pm-del" @click="removeTag(t)">删除</button>
|
<button class="pm-del" @click="removeTag(t)">删除</button>
|
||||||
</li>
|
</li>
|
||||||
@@ -178,59 +170,326 @@ function resetDefault() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.pm-root { display: flex; flex-direction: column; height: 100vh; }
|
.pm-root {
|
||||||
.pm-toolbar { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; border-bottom: 1px solid var(--color-border); gap: 12px; background-color: var(--color-bg-primary); }
|
display: flex;
|
||||||
.pm-left { display: flex; align-items: center; gap: 8px; }
|
width: 100%;
|
||||||
.pm-right { display: flex; align-items: center; gap: 8px; }
|
max-width: 1400px;
|
||||||
.pm-tip { font-size: 12px; color: var(--color-text-tertiary); }
|
margin: 0 auto;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
gap: 12px;
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
/* 按钮与输入样式,与 PresetManager 保持一致 */
|
/* 按钮与输入样式,与 PresetManager 保持一致 */
|
||||||
.pm-btn { display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-bg-primary); cursor: pointer; line-height: 1; font-size: 14px; color: var(--color-text-primary); transition: all 0.2s ease; }
|
.pm-btn {
|
||||||
.pm-btn:hover { background-color: var(--color-bg-tertiary); border-color: var(--color-border-hover); }
|
display: inline-flex;
|
||||||
.pm-btn-primary, .pm-btn-secondary { display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; border: 1px solid var(--color-border); border-radius: var(--radius-md); font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; line-height: 1; }
|
align-items: center;
|
||||||
.pm-btn-primary { background-color: var(--color-accent); color: var(--color-text-primary); border-color: var(--color-accent); }
|
gap: 6px;
|
||||||
.pm-btn-primary:hover { background-color: var(--color-accent-hover); }
|
padding: 6px 10px;
|
||||||
.pm-btn-secondary { background-color: var(--color-bg-primary); color: var(--color-text-primary); }
|
border: 1px solid var(--color-border);
|
||||||
.pm-btn-secondary:hover { background-color: var(--color-bg-tertiary); border-color: var(--color-border-hover); }
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--color-bg-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
.pm-import { display: inline-flex; align-items: center; gap: 6px; }
|
.pm-btn:hover {
|
||||||
.pm-import input { display: none; }
|
background-color: var(--color-bg-tertiary);
|
||||||
.pm-search { width: 240px; padding: 6px 8px; border: 1px solid var(--color-border); border-radius: var(--radius-md); background-color: var(--color-bg-secondary); color: var(--color-text-primary); }
|
border-color: var(--color-border-hover);
|
||||||
.pm-search:focus { outline: none; border-color: var(--color-accent); box-shadow: 0 0 0 3px var(--color-accent-light); }
|
}
|
||||||
.pm-select { appearance: none; -webkit-appearance: none; -moz-appearance: none; background-color: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: 6px 10px; height: 32px; line-height: 20px; font-size: 14px; color: var(--color-text-primary); }
|
|
||||||
.pm-select:focus { outline: none; border-color: var(--color-accent); box-shadow: 0 0 0 3px var(--color-accent-light); }
|
|
||||||
|
|
||||||
.pm-main { display: grid; grid-template-columns: 280px 1fr; height: calc(100vh - 50px); }
|
.pm-btn-primary,
|
||||||
.pm-cats { border-right: 1px solid var(--color-border); overflow: auto; padding: 8px; background-color: var(--color-bg-primary); }
|
.pm-btn-secondary {
|
||||||
.pm-section-title { font-size: 12px; color: var(--color-text-tertiary); margin: 8px 0; }
|
display: inline-flex;
|
||||||
.pm-cats ul { list-style: none; margin: 0; padding: 0; }
|
align-items: center;
|
||||||
.pm-cats li { padding: 6px 8px; border-radius: var(--radius-md); cursor: pointer; }
|
gap: 6px;
|
||||||
.pm-cats li.active { background-color: var(--color-accent-light); }
|
padding: 6px 10px;
|
||||||
.pm-color { display: inline-block; width: 12px; height: 12px; border-radius: 2px; margin-right: 6px; vertical-align: middle; }
|
border: 1px solid var(--color-border);
|
||||||
.pm-list { padding: 8px; overflow: auto; }
|
border-radius: var(--radius-md);
|
||||||
.pm-list-toolbar { display: flex; justify-content: flex-end; align-items: center; gap: 8px; margin-bottom: 8px; }
|
font-size: 14px;
|
||||||
.pm-empty { color: var(--color-text-tertiary); padding: 20px; }
|
font-weight: 500;
|
||||||
.pm-tags { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
|
cursor: pointer;
|
||||||
.pm-tags li { display: grid; grid-template-columns: 24px 1fr 1fr auto auto; align-items: center; gap: 6px; padding: 6px; border: 1px solid var(--color-border); border-radius: var(--radius-md); background-color: var(--color-bg-primary); }
|
transition: all 0.2s ease;
|
||||||
.pm-tags li { will-change: transform; }
|
line-height: 1;
|
||||||
.pm-tags li.hidden { opacity: 0.5; }
|
}
|
||||||
.pm-tags li.pm-over { border-color: var(--color-accent); box-shadow: 0 0 0 3px var(--color-accent-light); }
|
|
||||||
.pm-handle { cursor: grab; user-select: none; color: var(--color-text-tertiary); text-align: center; }
|
.pm-btn-primary {
|
||||||
.pm-key, .pm-trans { padding: 6px 8px; border: 1px solid var(--color-border); border-radius: var(--radius-md); background-color: var(--color-bg-secondary); color: var(--color-text-primary); }
|
background-color: var(--color-accent);
|
||||||
.pm-key:focus, .pm-trans:focus { outline: none; border-color: var(--color-accent); box-shadow: 0 0 0 3px var(--color-accent-light); }
|
color: var(--color-text-primary);
|
||||||
.pm-hide, .pm-del, .pm-list-toolbar button, .pm-right button { padding: 6px 10px; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-bg-primary); cursor: pointer; line-height: 1; }
|
border-color: var(--color-accent);
|
||||||
.pm-hide:hover, .pm-del:hover, .pm-list-toolbar button:hover, .pm-right button:hover, .pm-btn:hover { background: var(--color-bg-tertiary); border-color: var(--color-border-hover); }
|
}
|
||||||
|
|
||||||
|
.pm-btn-primary:hover {
|
||||||
|
background-color: var(--color-accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-btn-secondary {
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-btn-secondary:hover {
|
||||||
|
background-color: var(--color-bg-tertiary);
|
||||||
|
border-color: var(--color-border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background-color: var(--color-bg-secondary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-search:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
box-shadow: 0 0 0 3px var(--color-accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-select {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
background-color: var(--color-bg-secondary);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 6px 10px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
box-shadow: 0 0 0 3px var(--color-accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 280px 1fr;
|
||||||
|
height: calc(100vh - 50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-cats {
|
||||||
|
border-right: 1px solid var(--color-border);
|
||||||
|
overflow: auto;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-section-title {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-cats ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-cats li {
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-cats li.active {
|
||||||
|
background-color: var(--color-accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-empty {
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
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 var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-tags li {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-tags li.hidden {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-tags li.pm-over {
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
box-shadow: 0 0 0 3px var(--color-accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-handle {
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-key,
|
||||||
|
.pm-trans {
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background-color: var(--color-bg-secondary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-key:focus,
|
||||||
|
.pm-trans:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
box-shadow: 0 0 0 3px var(--color-accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-hide,
|
||||||
|
.pm-del,
|
||||||
|
.pm-list-toolbar button,
|
||||||
|
.pm-right button {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--color-bg-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-hide:hover,
|
||||||
|
.pm-del:hover,
|
||||||
|
.pm-list-toolbar button:hover,
|
||||||
|
.pm-right button:hover,
|
||||||
|
.pm-btn:hover {
|
||||||
|
background: var(--color-bg-tertiary);
|
||||||
|
border-color: var(--color-border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
/* 手机端适配 */
|
/* 手机端适配 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.pm-toolbar { flex-direction: column; align-items: stretch; gap: 10px; }
|
.pm-toolbar {
|
||||||
.pm-left, .pm-right { flex-wrap: wrap; width: 100%; }
|
flex-direction: column;
|
||||||
.pm-search { width: 100%; }
|
align-items: stretch;
|
||||||
.pm-tip { display: none; }
|
gap: 10px;
|
||||||
.pm-main { grid-template-columns: 1fr; height: auto; }
|
}
|
||||||
.pm-cats { border-right: 0; border-bottom: 1px solid var(--color-border); }
|
|
||||||
.pm-tags li { grid-template-columns: 1fr; gap: 8px; }
|
.pm-left,
|
||||||
.pm-handle { text-align: left; }
|
.pm-right {
|
||||||
.pm-key, .pm-trans { width: 100%; }
|
flex-wrap: wrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-search {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-tip {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-main {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-cats {
|
||||||
|
border-right: 0;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-tags li {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-handle {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-key,
|
||||||
|
.pm-trans {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, nextTick } from 'vue';
|
||||||
|
import type { PresetFolder } from '../../types';
|
||||||
|
|
||||||
|
// 定义树节点类型,包含 children
|
||||||
|
interface FolderNode extends PresetFolder {
|
||||||
|
children: FolderNode[];
|
||||||
|
presetCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
folder: FolderNode;
|
||||||
|
level: number;
|
||||||
|
selectedFolderId: string | null;
|
||||||
|
expandedIds: Set<string>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'select', id: string): void;
|
||||||
|
(e: 'toggle', id: string): void;
|
||||||
|
(e: 'create-sub', parentId: string): void;
|
||||||
|
(e: 'edit', folder: PresetFolder): void;
|
||||||
|
(e: 'delete', folder: PresetFolder): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isExpanded = computed(() => props.expandedIds.has(props.folder.id));
|
||||||
|
const isSelected = computed(() => props.selectedFolderId === props.folder.id);
|
||||||
|
|
||||||
|
function handleToggle(e: Event) {
|
||||||
|
e.stopPropagation();
|
||||||
|
emit('toggle', props.folder.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelect() {
|
||||||
|
emit('select', props.folder.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上下文菜单或操作按钮
|
||||||
|
const showActions = ref(false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="folder-tree-item">
|
||||||
|
<div
|
||||||
|
class="folder-row"
|
||||||
|
:class="{ active: isSelected }"
|
||||||
|
:style="{ paddingLeft: `${level * 1.2 + 0.5}rem` }"
|
||||||
|
@click="handleSelect"
|
||||||
|
@mouseenter="showActions = true"
|
||||||
|
@mouseleave="showActions = false"
|
||||||
|
>
|
||||||
|
<!-- 展开/折叠箭头 -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 文件夹图标 -->
|
||||||
|
<span class="folder-icon" :style="{ color: folder.color || '#6366f1' }">
|
||||||
|
<svg v-if="isExpanded" 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>
|
||||||
|
<svg v-else 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>
|
||||||
|
|
||||||
|
<!-- 计数 -->
|
||||||
|
<span class="folder-count" v-if="folder.presetCount > 0">{{ folder.presetCount }}</span>
|
||||||
|
|
||||||
|
<!-- 操作按钮组 (悬停显示) -->
|
||||||
|
<div class="folder-actions" v-show="showActions || isSelected">
|
||||||
|
<button @click.stop="emit('create-sub', folder.id)" title="新建子文件夹">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M12 5v14M5 12h14"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button @click.stop="emit('edit', folder)" title="编辑文件夹">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||||
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button @click.stop="emit('delete', folder)" class="delete-btn" title="删除文件夹">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M3 6h18M19 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"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 递归渲染子节点 -->
|
||||||
|
<div v-if="isExpanded && folder.children.length" class="folder-children">
|
||||||
|
<FolderTreeItem
|
||||||
|
v-for="child in folder.children"
|
||||||
|
:key="child.id"
|
||||||
|
:folder="child"
|
||||||
|
:level="level + 1"
|
||||||
|
:selected-folder-id="selectedFolderId"
|
||||||
|
:expanded-ids="expandedIds"
|
||||||
|
@select="emit('select', $event)"
|
||||||
|
@toggle="emit('toggle', $event)"
|
||||||
|
@create-sub="emit('create-sub', $event)"
|
||||||
|
@edit="emit('edit', $event)"
|
||||||
|
@delete="emit('delete', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.folder-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.375rem 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-row:hover {
|
||||||
|
background-color: var(--color-bg-secondary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-row.active {
|
||||||
|
background-color: var(--color-accent-light);
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 适配暗色模式下的选中状态 */
|
||||||
|
:global(.dark) .folder-row.active {
|
||||||
|
background-color: rgba(59, 130, 246, 0.2);
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-count {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
background-color: var(--color-bg-tertiary);
|
||||||
|
padding: 0 0.375rem;
|
||||||
|
border-radius: 99px;
|
||||||
|
min-width: 1.25rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-actions button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-actions button:hover {
|
||||||
|
background-color: var(--color-bg-tertiary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-actions button.delete-btn:hover {
|
||||||
|
color: var(--color-error);
|
||||||
|
background-color: rgba(239, 68, 68, 0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { ExtendedPreset, PresetType } from '../../types';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
presets: ExtendedPreset[];
|
||||||
|
searchQuery: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'apply', preset: ExtendedPreset): void;
|
||||||
|
(e: 'edit', preset: ExtendedPreset): void;
|
||||||
|
(e: 'delete', preset: ExtendedPreset): void;
|
||||||
|
(e: 'copy', preset: ExtendedPreset): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function getTypeIcon(type: PresetType) {
|
||||||
|
const icons: Record<string, string> = {
|
||||||
|
positive: '👍',
|
||||||
|
negative: '👎',
|
||||||
|
setting: '⚙️',
|
||||||
|
style: '🎨',
|
||||||
|
character: '👤',
|
||||||
|
scene: '🌍',
|
||||||
|
custom: '📝'
|
||||||
|
};
|
||||||
|
return icons[type] || '📝';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTypeLabel(type: PresetType) {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
positive: '正面',
|
||||||
|
negative: '负面',
|
||||||
|
setting: '设定',
|
||||||
|
style: '风格',
|
||||||
|
character: '角色',
|
||||||
|
scene: '场景',
|
||||||
|
custom: '自定义'
|
||||||
|
};
|
||||||
|
return labels[type] || type;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateStr: string) {
|
||||||
|
return new Date(dateStr).toLocaleDateString('zh-CN');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="preset-list-container">
|
||||||
|
<div v-if="presets.length === 0" class="empty-state">
|
||||||
|
<div class="empty-icon">📭</div>
|
||||||
|
<p class="empty-text">暂无预设</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="preset-grid">
|
||||||
|
<div v-for="preset in presets" :key="preset.id" class="preset-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="preset-type" :title="getTypeLabel(preset.type)">
|
||||||
|
{{ getTypeIcon(preset.type) }}
|
||||||
|
</div>
|
||||||
|
<h4 class="preset-name" :title="preset.name">{{ preset.name }}</h4>
|
||||||
|
<div class="preset-actions">
|
||||||
|
<button @click="emit('apply', preset)" class="action-btn apply-btn" title="应用预设">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polyline points="20,6 9,17 4,12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button @click="emit('copy', preset)" class="action-btn" title="复制内容">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
|
||||||
|
<path d="m5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<button class="action-btn more-btn">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="1"/>
|
||||||
|
<circle cx="19" cy="12" r="1"/>
|
||||||
|
<circle cx="5" cy="12" r="1"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<button @click="emit('edit', preset)">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||||
|
<path d="m18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||||
|
</svg>
|
||||||
|
编辑
|
||||||
|
</button>
|
||||||
|
<button @click="emit('delete', preset)" class="delete-item">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polyline points="3,6 5,6 21,6"/>
|
||||||
|
<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"/>
|
||||||
|
</svg>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="preset-preview">{{ preset.content }}</div>
|
||||||
|
<div v-if="preset.description" class="preset-desc">{{ preset.description }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="tags-list" v-if="preset.tags && preset.tags.length">
|
||||||
|
<span v-for="tag in preset.tags.slice(0, 3)" :key="tag" class="tag">{{ tag }}</span>
|
||||||
|
<span v-if="preset.tags.length > 3" class="tag-more">+{{ preset.tags.length - 3 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="date-info" v-else>
|
||||||
|
{{ formatDate(preset.updatedAt) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.preset-list-container {
|
||||||
|
padding: 1rem;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-card {
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
border-color: var(--color-border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-type {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-card:hover .preset-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-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-secondary);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
background-color: var(--color-bg-tertiary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.apply-btn:hover {
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown menu implementation */
|
||||||
|
.dropdown-menu {
|
||||||
|
position: relative;
|
||||||
|
height: 1.75rem; /* Match button height to ensure alignment */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 0.5rem; /* Bridge the gap */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: calc(100% + 0.25rem);
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
min-width: 120px;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu:hover .dropdown-content {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content button:hover {
|
||||||
|
background-color: var(--color-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content button.delete-item:hover {
|
||||||
|
color: var(--color-error);
|
||||||
|
background-color: rgba(239, 68, 68, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
min-height: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-preview {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
background-color: var(--color-bg-tertiary);
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
height: 3.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-desc {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-list {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
background-color: var(--color-bg-secondary);
|
||||||
|
padding: 0.125rem 0.375rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-more {
|
||||||
|
background-color: var(--color-bg-tertiary);
|
||||||
|
padding: 0.125rem 0.375rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import FolderTreeItem from './FolderTreeItem.vue';
|
||||||
|
import type { PresetFolder } from '../../types';
|
||||||
|
|
||||||
|
interface FolderNode extends PresetFolder {
|
||||||
|
children: FolderNode[];
|
||||||
|
presetCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
folderTree: FolderNode[];
|
||||||
|
selectedFolderId: string | null;
|
||||||
|
expandedIds: Set<string>;
|
||||||
|
allCount: number;
|
||||||
|
uncategorizedCount: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:selectedFolderId', id: string | null): void;
|
||||||
|
(e: 'toggle-expand', id: string): void;
|
||||||
|
(e: 'create-folder'): void;
|
||||||
|
(e: 'create-sub-folder', parentId: string): void;
|
||||||
|
(e: 'edit-folder', folder: PresetFolder): void;
|
||||||
|
(e: 'delete-folder', folder: PresetFolder): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function selectFolder(id: string | null) {
|
||||||
|
emit('update:selectedFolderId', id);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="preset-sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<h3 class="sidebar-title">预设文件夹</h3>
|
||||||
|
<button @click="emit('create-folder')" class="add-folder-btn" title="新建根文件夹">
|
||||||
|
<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" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<line x1="12" y1="11" x2="12" y2="17" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<line x1="9" y1="14" x2="15" y2="14" stroke="currentColor" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-content">
|
||||||
|
<!-- 固定选项 -->
|
||||||
|
<div class="system-folders">
|
||||||
|
<div
|
||||||
|
class="sidebar-item"
|
||||||
|
:class="{ active: selectedFolderId === null }"
|
||||||
|
@click="selectFolder(null)"
|
||||||
|
>
|
||||||
|
<span class="item-icon">📋</span>
|
||||||
|
<span class="item-name">所有预设</span>
|
||||||
|
<span class="item-count">{{ allCount }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="sidebar-item"
|
||||||
|
:class="{ active: selectedFolderId === '' }"
|
||||||
|
@click="selectFolder('')"
|
||||||
|
>
|
||||||
|
<span class="item-icon">📂</span>
|
||||||
|
<span class="item-name">未分类</span>
|
||||||
|
<span class="item-count">{{ uncategorizedCount }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="folder-tree-container">
|
||||||
|
<div v-if="folderTree.length === 0" class="empty-tree">
|
||||||
|
暂无文件夹
|
||||||
|
</div>
|
||||||
|
<FolderTreeItem
|
||||||
|
v-for="folder in folderTree"
|
||||||
|
:key="folder.id"
|
||||||
|
:folder="folder"
|
||||||
|
:level="0"
|
||||||
|
:selected-folder-id="selectedFolderId"
|
||||||
|
:expanded-ids="expandedIds"
|
||||||
|
@select="selectFolder"
|
||||||
|
@toggle="emit('toggle-expand', $event)"
|
||||||
|
@create-sub="emit('create-sub-folder', $event)"
|
||||||
|
@edit="emit('edit-folder', $event)"
|
||||||
|
@delete="emit('delete-folder', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.preset-sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--color-bg-secondary);
|
||||||
|
border-right: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-folder-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-folder-btn:hover {
|
||||||
|
background-color: var(--color-bg-tertiary);
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-folders {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item:hover {
|
||||||
|
background-color: var(--color-bg-tertiary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item.active {
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item.active .item-count {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-icon {
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-count {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
background-color: var(--color-bg-tertiary);
|
||||||
|
padding: 0.125rem 0.375rem;
|
||||||
|
border-radius: 99px;
|
||||||
|
min-width: 1.25rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-tree {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式优化 */
|
||||||
|
.sidebar-content::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-content::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-content::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--color-border);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-content::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: var(--color-text-tertiary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+2
-15
@@ -24,26 +24,13 @@ a:hover {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3.2em;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
max-width: 1280px;
|
width: 100%;
|
||||||
margin: 0 auto;
|
height: 100%;
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
|
|||||||
Reference in New Issue
Block a user