feat: 功能迭代
This commit is contained in:
11
package-lock.json
generated
11
package-lock.json
generated
@@ -17,6 +17,7 @@
|
||||
"electron": "^42.3.0",
|
||||
"electron-vite": "^5.0.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"lucide-vue-next": "^1.0.0",
|
||||
"nanoid": "^5.1.11",
|
||||
"pinia": "^3.0.4",
|
||||
"primeicons": "^7.0.0",
|
||||
@@ -5478,6 +5479,16 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-vue-next": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-1.0.0.tgz",
|
||||
"integrity": "sha512-V6SPvx1IHTj/UY+FrIYWV5faISsPSb8BnWSFDxAtezWKvWc9ZZ40PDrdu1/Qb5vg4lHWr1hs1BAMGVGm6V1Xdg==",
|
||||
"deprecated": "Package deprecated. Please use @lucide/vue instead.",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"vue": ">=3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"electron": "^42.3.0",
|
||||
"electron-vite": "^5.0.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"lucide-vue-next": "^1.0.0",
|
||||
"nanoid": "^5.1.11",
|
||||
"pinia": "^3.0.4",
|
||||
"primeicons": "^7.0.0",
|
||||
|
||||
42
src/renderer/src/components/LucideIcon.vue
Normal file
42
src/renderer/src/components/LucideIcon.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import * as LucideIcons from 'lucide-vue-next';
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: 24,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'currentColor',
|
||||
},
|
||||
strokeWidth: {
|
||||
type: [Number, String],
|
||||
default: 2,
|
||||
},
|
||||
});
|
||||
|
||||
const icon = computed(() => {
|
||||
// Lucide 图标通常是 PascalCase,用户输入可能是 kebab-case
|
||||
const pascalName = props.name
|
||||
.split('-')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join('');
|
||||
|
||||
return LucideIcons[pascalName] || LucideIcons[props.name] || null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="icon" v-if="icon" :size="size" :color="color" :stroke-width="strokeWidth" class="lucide-icon" />
|
||||
<span
|
||||
v-else
|
||||
class="lucide-icon-fallback"
|
||||
:style="{ width: size + 'px', height: size + 'px', display: 'inline-block' }"
|
||||
></span>
|
||||
</template>
|
||||
@@ -3,22 +3,15 @@ import {ref, computed, onMounted, nextTick} from 'vue'
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import Menu from 'primevue/menu'
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import Tree from 'primevue/tree'
|
||||
import {
|
||||
Search, Plus, File, Star, Trash2,
|
||||
Settings, Sun, Moon, Monitor,
|
||||
BookOpen, Tag, ChevronDown, ChevronRight, Hash,
|
||||
MoreHorizontal
|
||||
} from '@lucide/vue'
|
||||
import {Plus, File, Sun, Moon, Monitor, Settings} from '@lucide/vue'
|
||||
import {useThemeStore} from '../stores/theme'
|
||||
import {useNotesStore} from '../stores/notes'
|
||||
import {useNotebooksStore} from '../stores/notebooks'
|
||||
import {useSidebarTree} from '../composables/useSidebarTree'
|
||||
import ButtonIcon from "./ButtonIcon.vue";
|
||||
|
||||
import Menu from 'primevue/menu'
|
||||
import LucideIcon from "./LucideIcon.vue";
|
||||
|
||||
const themeStore = useThemeStore()
|
||||
const notesStore = useNotesStore()
|
||||
@@ -29,7 +22,22 @@ const props = defineProps({
|
||||
tags: {type: Array, default: () => []}
|
||||
})
|
||||
|
||||
const {sidebarTree, flatNotebooks, expandedKeys, toggleNodeExpanded, loadNotebooks, loadNotes} = useSidebarTree(computed(() => props.tags))
|
||||
async function loadNotebooks() {
|
||||
await notebooksStore.loadNotebooks()
|
||||
}
|
||||
|
||||
async function loadNotes() {
|
||||
await notesStore.loadNotes()
|
||||
}
|
||||
|
||||
|
||||
const items11 = ref([
|
||||
|
||||
{label: 'Documents', icon: 'search' },
|
||||
{label: '收藏夹', icon: 'search'},
|
||||
{label: '标签', icon: 'search'},
|
||||
{label: '笔记本', icon: 'search'},
|
||||
]);
|
||||
|
||||
const theme = computed(() => themeStore.theme)
|
||||
const toggleTheme = themeStore.toggleTheme
|
||||
@@ -42,22 +50,11 @@ const newNotebookParentName = computed(() => {
|
||||
|
||||
const emit = defineEmits(['navigate', 'create-note', 'select-notebook', 'select-tag', 'select-note'])
|
||||
|
||||
// Selected tree node
|
||||
const selectedTreeKey = ref(null)
|
||||
|
||||
// Load notebooks and notes on mount
|
||||
onMounted(async () => {
|
||||
await loadNotebooks()
|
||||
await loadNotes()
|
||||
})
|
||||
|
||||
// Collapse state
|
||||
const collapsed = ref(false)
|
||||
|
||||
function toggleCollapsed() {
|
||||
collapsed.value = !collapsed.value
|
||||
}
|
||||
|
||||
// Navigation state
|
||||
const activeNav = ref('all')
|
||||
|
||||
@@ -78,7 +75,6 @@ async function onSearchInput() {
|
||||
}, 200)
|
||||
}
|
||||
|
||||
// Methods
|
||||
function handleNav(id) {
|
||||
activeNav.value = id
|
||||
emit('navigate', id)
|
||||
@@ -89,72 +85,122 @@ function selectNotebook(notebook) {
|
||||
emit('select-notebook', notebook)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Tree node selection
|
||||
* PrimeVue v4 passes node directly (not wrapped in event object)
|
||||
*/
|
||||
function onTreeSelect(node) {
|
||||
const { type, id } = node.data
|
||||
|
||||
if (type === 'nav') {
|
||||
selectedTreeKey.value = node.key
|
||||
handleNav(id)
|
||||
} else if (type === 'notebook') {
|
||||
selectedTreeKey.value = node.key
|
||||
activeNav.value = `notebook-${id}`
|
||||
emit('select-notebook', node.data)
|
||||
} else if (type === 'note') {
|
||||
selectedTreeKey.value = node.key
|
||||
activeNav.value = `note-${id}`
|
||||
emit('select-note', node.data)
|
||||
} else if (type === 'tag') {
|
||||
selectedTreeKey.value = node.key
|
||||
activeNav.value = `tag-${id}`
|
||||
emit('select-tag', node.data)
|
||||
}
|
||||
}
|
||||
|
||||
function selectTag(tag) {
|
||||
activeNav.value = `tag-${tag.id}`
|
||||
emit('select-tag', tag)
|
||||
}
|
||||
|
||||
function selectSearchNote(note) {
|
||||
emit('select-note', note)
|
||||
searchQuery.value = ''
|
||||
searchResults.value = []
|
||||
}
|
||||
|
||||
function getNotebookName(note) {
|
||||
if (note.notebook_name) return note.notebook_name
|
||||
const nb = props.notebooks.find(n => n.id === note.notebook_id)
|
||||
return nb ? nb.name : ''
|
||||
}
|
||||
|
||||
function createNote(notebookId) {
|
||||
closeMenu()
|
||||
emit('create-note', notebookId)
|
||||
}
|
||||
|
||||
// Notebook actions menu (PrimeVue Menu)
|
||||
const notebookMenuRef = ref(null)
|
||||
const activeMenuNotebookId = ref(null)
|
||||
// Notebook tree for PanelMenu
|
||||
const notebookTreeItems = computed(() => {
|
||||
const notebookList = notebooksStore.notebooks
|
||||
const noteList = notesStore.notes
|
||||
|
||||
const notebookMenuItems = computed(() => [
|
||||
{
|
||||
label: '新建子笔记本',
|
||||
function buildNotebookChildren(parentId) {
|
||||
const children = []
|
||||
|
||||
const childNotebooks = notebookList.filter(nb => (nb.parent_id || null) === parentId)
|
||||
childNotebooks.sort((a, b) => a.name.localeCompare(b.name))
|
||||
childNotebooks.forEach(nb => {
|
||||
const nbChildren = buildNotebookChildren(nb.id)
|
||||
children.push({
|
||||
key: `notebook-${nb.id}`,
|
||||
label: nb.name,
|
||||
icon: 'pi pi-folder',
|
||||
command: () => startNewNotebook(activeMenuNotebookId.value)
|
||||
command: () => selectNotebook(nb),
|
||||
items: nbChildren.length > 0 ? nbChildren : undefined
|
||||
})
|
||||
})
|
||||
|
||||
const notebookNotes = noteList.filter(n => n.notebook_id === parentId && !n.is_trashed)
|
||||
notebookNotes.sort((a, b) => (a.title || '').localeCompare(b.title || ''))
|
||||
notebookNotes.forEach(note => {
|
||||
children.push({
|
||||
key: `note-${note.id}`,
|
||||
label: note.title || '无标题',
|
||||
icon: 'pi pi-file',
|
||||
command: () => emit('select-note', note)
|
||||
})
|
||||
})
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
const rootNotebooks = notebookList.filter(nb => !nb.parent_id)
|
||||
rootNotebooks.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
return rootNotebooks.map(nb => {
|
||||
const children = buildNotebookChildren(nb.id)
|
||||
return {
|
||||
key: `notebook-${nb.id}`,
|
||||
label: nb.name,
|
||||
icon: 'pi pi-folder',
|
||||
command: () => selectNotebook(nb),
|
||||
items: children.length > 0 ? children : undefined
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// PanelMenu items
|
||||
const items = computed(() => [
|
||||
{
|
||||
label: '导航',
|
||||
icon: 'pi pi-compass',
|
||||
items: [
|
||||
{
|
||||
label: '全部笔记',
|
||||
icon: 'pi pi-home',
|
||||
command: () => handleNav('all')
|
||||
},
|
||||
{
|
||||
label: '新建笔记',
|
||||
icon: 'pi pi-file',
|
||||
command: () => {
|
||||
closeMenu();
|
||||
emit('create-note', activeMenuNotebookId.value)
|
||||
}
|
||||
label: '收藏',
|
||||
icon: 'pi pi-star',
|
||||
command: () => handleNav('favorites')
|
||||
},
|
||||
{
|
||||
label: '最近编辑',
|
||||
icon: 'pi pi-clock',
|
||||
command: () => handleNav('recent')
|
||||
},
|
||||
{
|
||||
label: '回收站',
|
||||
icon: 'pi pi-trash',
|
||||
command: () => handleNav('trash')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '笔记本',
|
||||
icon: 'pi pi-book',
|
||||
items: notebookTreeItems.value
|
||||
},
|
||||
...(props.tags && props.tags.length > 0 ? [{
|
||||
label: '标签',
|
||||
icon: 'pi pi-tag',
|
||||
items: props.tags.map(tag => ({
|
||||
label: tag.name,
|
||||
icon: 'pi pi-tag',
|
||||
command: () => selectTag(tag)
|
||||
}))
|
||||
}] : [])
|
||||
])
|
||||
|
||||
function toggleNotebookMenu(notebookId, event) {
|
||||
event.stopPropagation()
|
||||
activeMenuNotebookId.value = notebookId
|
||||
notebookMenuRef.value.toggle(event)
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
activeMenuNotebookId.value = null
|
||||
}
|
||||
|
||||
// New notebook creation (supports parent_id for sub-notebooks)
|
||||
// New notebook dialog
|
||||
const showNewNotebookDialog = ref(false)
|
||||
const newNotebookName = ref('')
|
||||
const newNotebookInputRef = ref(null)
|
||||
@@ -164,7 +210,6 @@ function startNewNotebook(parentId = null) {
|
||||
newNotebookName.value = ''
|
||||
newNotebookParentId.value = parentId
|
||||
showNewNotebookDialog.value = true
|
||||
closeMenu()
|
||||
nextTick(() => {
|
||||
newNotebookInputRef.value?.$el?.focus()
|
||||
})
|
||||
@@ -192,52 +237,14 @@ function cancelNewNotebook() {
|
||||
newNotebookName.value = ''
|
||||
newNotebookParentId.value = null
|
||||
}
|
||||
|
||||
function selectSearchNote(note) {
|
||||
emit('select-note', note)
|
||||
searchQuery.value = ''
|
||||
searchResults.value = []
|
||||
}
|
||||
|
||||
// Get notebook display name for a note
|
||||
function getNotebookName(note) {
|
||||
if (note.notebook_name) return note.notebook_name
|
||||
const nb = props.notebooks.find(n => n.id === note.notebook_id)
|
||||
return nb ? nb.name : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active style class based on node type
|
||||
*/
|
||||
function getNodeActiveClass(node) {
|
||||
const { type, id } = node.data
|
||||
if (type === 'nav') {
|
||||
return activeNav.value === id
|
||||
? 'bg-surface-200/70 text-primary-700 font-medium shadow-sm'
|
||||
: 'text-surface-600 hover:bg-surface-100'
|
||||
}
|
||||
if (type === 'notebook') {
|
||||
return activeNav.value === `notebook-${id}`
|
||||
? 'bg-emerald-50 text-emerald-700 font-medium shadow-sm'
|
||||
: 'text-surface-600 hover:bg-surface-100'
|
||||
}
|
||||
if (type === 'tag') {
|
||||
return activeNav.value === `tag-${id}`
|
||||
? 'bg-amber-50 text-amber-700 font-medium shadow-sm'
|
||||
: 'text-surface-600 hover:bg-surface-100'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="sidebar-container flex flex-col h-full bg-surface-50 border-r border-app-border transition-all duration-300"
|
||||
:class="collapsed ? 'w-14' : 'w-64'">
|
||||
<div class="sidebar-container flex flex-col h-full bg-surface-50 border-r border-app-border overflow-hidden w-64">
|
||||
|
||||
<!-- Top Bar: Toggle + (Search & Add when expanded) -->
|
||||
<div class="p-3 border-b border-app-border flex items-center gap-2">
|
||||
<template v-if="!collapsed">
|
||||
<!-- Search bar -->
|
||||
<div class="p-3 border-b border-app-border flex-shrink-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex-1 relative">
|
||||
<IconField>
|
||||
<InputIcon class="pi pi-search"/>
|
||||
@@ -270,203 +277,20 @@ function getNodeActiveClass(node) {
|
||||
</div>
|
||||
</div>
|
||||
<Button icon="pi pi-plus" size="small" severity="secondary" aria-label="新建笔记" @click="createNote"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="w-full flex justify-center">
|
||||
<Button severity="primary" size="small" @click="createNote" v-tooltip.right="'新建笔记'"
|
||||
class="btn-filled w-8 h-8">
|
||||
<template #icon>
|
||||
<Plus :size="20"/>
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Content -->
|
||||
<ScrollPanel class="flex-1">
|
||||
<!-- Collapsed: Icon-only nav -->
|
||||
<template v-if="collapsed">
|
||||
<div class="py-3 flex flex-col items-center gap-1">
|
||||
<!-- Notes nav items (collapsed) -->
|
||||
<div
|
||||
class="w-10 h-10 flex items-center justify-center rounded-lg cursor-pointer transition-colors"
|
||||
:class="activeNav === 'all' ? 'bg-primary-50 text-primary-600' : 'text-surface-500 hover:bg-surface-100'"
|
||||
@click="handleNav('all')"
|
||||
v-tooltip.right="'所有笔记'"
|
||||
>
|
||||
<File :size="18"/>
|
||||
<!-- PanelMenu -->
|
||||
<div class="flex-1 px-2 gap-1 ">
|
||||
<Menu :model="items11"/>
|
||||
<div v-for="(item,index) in items11" :key="index" class="flex items-center gap-2 px-2 py-1 cursor-pointer hover:bg-[#f1f5f9]">
|
||||
<LucideIcon :name="item.icon" size="16 " color="red"></LucideIcon>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
<div
|
||||
class="w-10 h-10 flex items-center justify-center rounded-lg cursor-pointer transition-colors"
|
||||
:class="activeNav === 'favorites' ? 'bg-primary-50 text-primary-600' : 'text-surface-500 hover:bg-surface-100'"
|
||||
@click="handleNav('favorites')"
|
||||
v-tooltip.right="'收藏夹'"
|
||||
>
|
||||
<Star :size="18"/>
|
||||
</div>
|
||||
<div
|
||||
class="w-10 h-10 flex items-center justify-center rounded-lg cursor-pointer transition-colors"
|
||||
:class="activeNav === 'trash' ? 'bg-primary-50 text-primary-600' : 'text-surface-500 hover:bg-surface-100'"
|
||||
@click="handleNav('trash')"
|
||||
v-tooltip.right="'回收站'"
|
||||
>
|
||||
<Trash2 :size="18"/>
|
||||
</div>
|
||||
|
||||
<div class="w-8 h-px bg-surface-200 my-2"/>
|
||||
|
||||
<!-- Notebooks (collapsed) -->
|
||||
<div
|
||||
v-for="nb in flatNotebooks.filter(nb => !nb.parent_id)"
|
||||
:key="nb.id"
|
||||
class="w-10 h-10 flex items-center justify-center rounded-lg cursor-pointer transition-colors"
|
||||
:class="activeNav === `notebook-${nb.id}` ? 'bg-emerald-50 text-emerald-600' : 'text-surface-500 hover:bg-surface-100'"
|
||||
@click="selectNotebook(nb)"
|
||||
v-tooltip.right="nb.name"
|
||||
>
|
||||
<BookOpen :size="18"/>
|
||||
</div>
|
||||
|
||||
<div class="w-8 h-px bg-surface-200 my-2"/>
|
||||
|
||||
<!-- Tags (collapsed) -->
|
||||
<div
|
||||
v-for="tag in tags"
|
||||
:key="tag.id"
|
||||
class="w-10 h-10 flex items-center justify-center rounded-lg cursor-pointer transition-colors"
|
||||
:class="activeNav === `tag-${tag.id}` ? 'bg-amber-50 text-amber-600' : 'text-surface-500 hover:bg-surface-100'"
|
||||
@click="selectTag(tag)"
|
||||
v-tooltip.right="tag.name"
|
||||
>
|
||||
<Hash :size="18"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Expanded: Unified Tree -->
|
||||
<template v-else>
|
||||
<div class="py-2">
|
||||
<Tree
|
||||
:value="sidebarTree"
|
||||
:expandedKeys="expandedKeys"
|
||||
selectionMode="single"
|
||||
:selectionKeys="selectedTreeKey ? { [selectedTreeKey]: true } : {}"
|
||||
@node-select="onTreeSelect"
|
||||
class="w-full sidebar-tree"
|
||||
:pt="{
|
||||
root: { class: 'border-none bg-transparent w-full !p-0' },
|
||||
node: { class: 'py-0.5 w-full', style: 'width: 100%' },
|
||||
nodeContent: {
|
||||
class: ({ instance }) => [
|
||||
'rounded-lg transition-colors min-w-0 w-full',
|
||||
instance.selected
|
||||
? 'bg-primary-50 text-primary-700 font-medium dark:bg-primary-900/30 dark:text-primary-300'
|
||||
: 'hover:bg-surface-100 dark:hover:bg-surface-800'
|
||||
],
|
||||
style: 'width: 100%'
|
||||
},
|
||||
nodeLabel: { class: 'text-sm truncate' },
|
||||
nodeIcon: { class: 'hidden' },
|
||||
toggler: { class: 'w-6 h-6 shrink-0' }
|
||||
}"
|
||||
>
|
||||
<template #default="slotProps">
|
||||
<!-- Group node (non-selectable header) -->
|
||||
<div
|
||||
v-if="slotProps.node.data.type === 'group'"
|
||||
class="flex items-center gap-2 py-1 px-1 w-full cursor-pointer hover:bg-surface-100 dark:hover:bg-surface-800 rounded-lg transition-colors"
|
||||
@click="toggleNodeExpanded(slotProps.node.key)"
|
||||
>
|
||||
<span class="text-xs font-semibold text-surface-400 uppercase tracking-wider">
|
||||
{{ slotProps.node.label }}
|
||||
</span>
|
||||
<button
|
||||
v-if="slotProps.node.data.id === 'notebooks'"
|
||||
class="w-5 h-5 flex items-center justify-center rounded hover:bg-surface-200 text-surface-400 hover:text-surface-600 transition-colors ml-auto"
|
||||
@click.stop="startNewNotebook"
|
||||
title="新建笔记本"
|
||||
>
|
||||
<Plus :size="12"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Nav node (笔记导航项) -->
|
||||
<div
|
||||
v-else-if="slotProps.node.data.type === 'nav'"
|
||||
class="flex items-center gap-3 py-1.5 px-1 w-full"
|
||||
>
|
||||
<component
|
||||
:is="slotProps.node.data.id === 'all' ? File :
|
||||
slotProps.node.data.id === 'favorites' ? Star : Trash2"
|
||||
:size="18"
|
||||
/>
|
||||
<span class="text-sm">{{ slotProps.node.label }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Notebook node -->
|
||||
<div
|
||||
v-else-if="slotProps.node.data.type === 'notebook'"
|
||||
class="relative flex items-center justify-between w-full gap-1 py-1 px-1 min-w-0 group/node pr-8"
|
||||
>
|
||||
<span class="text-sm truncate flex-1">{{ slotProps.node.label }}</span>
|
||||
<button
|
||||
class="absolute right-1 w-6 h-6 flex items-center justify-center rounded hover:bg-surface-200 text-surface-400 hover:text-surface-600 opacity-0 group-hover/node:opacity-100 transition-opacity"
|
||||
@click.stop="toggleNotebookMenu(slotProps.node.data.id, $event)"
|
||||
>
|
||||
<MoreHorizontal :size="14"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Note node -->
|
||||
<div
|
||||
v-else-if="slotProps.node.data.type === 'note'"
|
||||
class="flex items-center gap-2 py-1 px-1 w-full"
|
||||
>
|
||||
<File :size="14" class="text-surface-400 shrink-0"/>
|
||||
<span class="text-sm truncate">{{ slotProps.node.label }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Tag node -->
|
||||
<div
|
||||
v-else-if="slotProps.node.data.type === 'tag'"
|
||||
class="flex items-center gap-3 py-1.5 px-1 w-full group/tag"
|
||||
>
|
||||
<div class="w-3 h-3 rounded-full flex-shrink-0 bg-amber-400"/>
|
||||
<span class="text-sm flex-1 truncate">{{ slotProps.node.label }}</span>
|
||||
<span class="text-xs text-surface-400 opacity-0 group-hover/tag:opacity-100 transition-opacity">
|
||||
{{ slotProps.node.data.note_count || 0 }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</Tree>
|
||||
<Menu ref="notebookMenuRef" :model="notebookMenuItems" :popup="true"/>
|
||||
</div>
|
||||
</template>
|
||||
</ScrollPanel>
|
||||
|
||||
<!-- Bottom Actions -->
|
||||
<div class="p-3 border-t border-app-border">
|
||||
<template v-if="collapsed">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<Button text severity="secondary" @click="toggleTheme"
|
||||
v-tooltip.right="theme === 'light' ? '切换暗色模式' : theme === 'dark' ? '跟随系统' : '切换亮色模式'"
|
||||
class="btn-icon w-10 h-10">
|
||||
<template #icon>
|
||||
<Sun v-if="theme === 'light'" :size="20"/>
|
||||
<Moon v-else-if="theme === 'dark'" :size="20"/>
|
||||
<Monitor v-else :size="20"/>
|
||||
</template>
|
||||
</Button>
|
||||
<Button text severity="secondary" @click="handleNav('settings')" v-tooltip.right="'设置'"
|
||||
class="btn-icon w-10 h-10">
|
||||
<template #icon>
|
||||
<Settings :size="20"/>
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- Bottom bar: theme + settings -->
|
||||
<div class="border-t border-app-border p-3 flex-shrink-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<Button text severity="secondary" @click="toggleTheme" class="btn-icon h-10 px-3 flex items-center gap-2">
|
||||
<template #icon>
|
||||
@@ -484,7 +308,6 @@ function getNodeActiveClass(node) {
|
||||
<span class="text-sm">设置</span>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- New Notebook Dialog -->
|
||||
@@ -521,74 +344,77 @@ function getNodeActiveClass(node) {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.collapse-enter-active,
|
||||
.collapse-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
overflow: hidden;
|
||||
.sidebar-container :deep(.p-menu) {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.collapse-enter-from,
|
||||
.collapse-leave-to {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
.collapse-enter-to,
|
||||
.collapse-leave-from {
|
||||
opacity: 1;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
/* Custom styles for sidebar tree */
|
||||
.sidebar-tree :deep(.p-tree) {
|
||||
/*
|
||||
.sidebar-container :deep(.p-menu-list) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sidebar-tree :deep(.p-tree-node) {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
.sidebar-container :deep(.p-menu-item-content) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.sidebar-tree :deep(.p-tree-node-content) {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: background-color 0.2s;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebar-tree :deep(.p-tree-node-content:hover) {
|
||||
.sidebar-container :deep(.p-menu-item-content:hover) {
|
||||
background-color: var(--surface-100);
|
||||
}
|
||||
|
||||
:root.dark .sidebar-tree :deep(.p-tree-node-content:hover) {
|
||||
:root.dark .sidebar-container :deep(.p-menu-item-content:hover) {
|
||||
background-color: var(--surface-800);
|
||||
}*/
|
||||
|
||||
.sidebar-container :deep(.p-panelmenu) {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sidebar-container :deep(.p-panelmenu-panel) {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sidebar-container :deep(.p-panelmenu-header) {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sidebar-container :deep(.p-panelmenu-header-content) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.sidebar-container :deep(.p-panelmenu-header-content:hover) {
|
||||
background-color: var(--surface-100);
|
||||
}
|
||||
|
||||
:root.dark .sidebar-container :deep(.p-panelmenu-header-content:hover) {
|
||||
background-color: var(--surface-800);
|
||||
}
|
||||
|
||||
.sidebar-tree :deep(.p-tree-node-selected) {
|
||||
/* Handled by pt nodeContent dynamic class */
|
||||
.sidebar-container :deep(.p-panelmenu-content) {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sidebar-tree :deep(.p-tree-toggler) {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
margin-right: 0;
|
||||
.sidebar-container :deep(.p-panelmenu-item-content) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.sidebar-tree :deep(.p-tree-node-label) {
|
||||
font-size: 0.875rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
.sidebar-container :deep(.p-panelmenu-item-content:hover) {
|
||||
background-color: var(--surface-100);
|
||||
}
|
||||
|
||||
/* Group nodes should not show hover effect */
|
||||
.sidebar-tree :deep(.p-tree-node-content:has(.text-xs.font-semibold)) {
|
||||
cursor: default;
|
||||
:root.dark .sidebar-container :deep(.p-panelmenu-item-content:hover) {
|
||||
background-color: var(--surface-800);
|
||||
}
|
||||
|
||||
.sidebar-tree :deep(.p-tree-node-content:has(.text-xs.font-semibold):hover) {
|
||||
background-color: transparent;
|
||||
.sidebar-container :deep(.p-panelmenu-submenu) {
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createPinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Aura from '@primeuix/themes/aura'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import Ripple from 'primevue/ripple'
|
||||
import App from './App.vue'
|
||||
import './assets/styles/main.css'
|
||||
import 'primeicons/primeicons.css'
|
||||
@@ -21,5 +22,6 @@ app.use(PrimeVue, {
|
||||
|
||||
app.use(pinia)
|
||||
app.directive('tooltip', Tooltip)
|
||||
app.directive('ripple', Ripple)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
Reference in New Issue
Block a user