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": "^42.3.0",
|
||||||
"electron-vite": "^5.0.0",
|
"electron-vite": "^5.0.0",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
|
"lucide-vue-next": "^1.0.0",
|
||||||
"nanoid": "^5.1.11",
|
"nanoid": "^5.1.11",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
@@ -5478,6 +5479,16 @@
|
|||||||
"yallist": "^3.0.2"
|
"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": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"electron": "^42.3.0",
|
"electron": "^42.3.0",
|
||||||
"electron-vite": "^5.0.0",
|
"electron-vite": "^5.0.0",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
|
"lucide-vue-next": "^1.0.0",
|
||||||
"nanoid": "^5.1.11",
|
"nanoid": "^5.1.11",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"primeicons": "^7.0.0",
|
"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 Button from 'primevue/button'
|
||||||
import InputText from 'primevue/inputtext'
|
import InputText from 'primevue/inputtext'
|
||||||
import Dialog from 'primevue/dialog'
|
import Dialog from 'primevue/dialog'
|
||||||
import Menu from 'primevue/menu'
|
|
||||||
import IconField from 'primevue/iconfield'
|
import IconField from 'primevue/iconfield'
|
||||||
import InputIcon from 'primevue/inputicon'
|
import InputIcon from 'primevue/inputicon'
|
||||||
import ScrollPanel from 'primevue/scrollpanel'
|
import {Plus, File, Sun, Moon, Monitor, Settings} from '@lucide/vue'
|
||||||
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 {useThemeStore} from '../stores/theme'
|
import {useThemeStore} from '../stores/theme'
|
||||||
import {useNotesStore} from '../stores/notes'
|
import {useNotesStore} from '../stores/notes'
|
||||||
import {useNotebooksStore} from '../stores/notebooks'
|
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 themeStore = useThemeStore()
|
||||||
const notesStore = useNotesStore()
|
const notesStore = useNotesStore()
|
||||||
@@ -29,7 +22,22 @@ const props = defineProps({
|
|||||||
tags: {type: Array, default: () => []}
|
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 theme = computed(() => themeStore.theme)
|
||||||
const toggleTheme = themeStore.toggleTheme
|
const toggleTheme = themeStore.toggleTheme
|
||||||
@@ -42,22 +50,11 @@ const newNotebookParentName = computed(() => {
|
|||||||
|
|
||||||
const emit = defineEmits(['navigate', 'create-note', 'select-notebook', 'select-tag', 'select-note'])
|
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 () => {
|
onMounted(async () => {
|
||||||
await loadNotebooks()
|
await loadNotebooks()
|
||||||
await loadNotes()
|
await loadNotes()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Collapse state
|
|
||||||
const collapsed = ref(false)
|
|
||||||
|
|
||||||
function toggleCollapsed() {
|
|
||||||
collapsed.value = !collapsed.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigation state
|
// Navigation state
|
||||||
const activeNav = ref('all')
|
const activeNav = ref('all')
|
||||||
|
|
||||||
@@ -78,7 +75,6 @@ async function onSearchInput() {
|
|||||||
}, 200)
|
}, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Methods
|
|
||||||
function handleNav(id) {
|
function handleNav(id) {
|
||||||
activeNav.value = id
|
activeNav.value = id
|
||||||
emit('navigate', id)
|
emit('navigate', id)
|
||||||
@@ -89,72 +85,122 @@ function selectNotebook(notebook) {
|
|||||||
emit('select-notebook', 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) {
|
function selectTag(tag) {
|
||||||
activeNav.value = `tag-${tag.id}`
|
activeNav.value = `tag-${tag.id}`
|
||||||
emit('select-tag', tag)
|
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) {
|
function createNote(notebookId) {
|
||||||
closeMenu()
|
|
||||||
emit('create-note', notebookId)
|
emit('create-note', notebookId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notebook actions menu (PrimeVue Menu)
|
// Notebook tree for PanelMenu
|
||||||
const notebookMenuRef = ref(null)
|
const notebookTreeItems = computed(() => {
|
||||||
const activeMenuNotebookId = ref(null)
|
const notebookList = notebooksStore.notebooks
|
||||||
|
const noteList = notesStore.notes
|
||||||
|
|
||||||
const notebookMenuItems = computed(() => [
|
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: () => 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: '新建子笔记本',
|
label: '导航',
|
||||||
icon: 'pi pi-folder',
|
icon: 'pi pi-compass',
|
||||||
command: () => startNewNotebook(activeMenuNotebookId.value)
|
items: [
|
||||||
|
{
|
||||||
|
label: '全部笔记',
|
||||||
|
icon: 'pi pi-home',
|
||||||
|
command: () => handleNav('all')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: '新建笔记',
|
label: '笔记本',
|
||||||
icon: 'pi pi-file',
|
icon: 'pi pi-book',
|
||||||
command: () => {
|
items: notebookTreeItems.value
|
||||||
closeMenu();
|
},
|
||||||
emit('create-note', activeMenuNotebookId.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) {
|
// New notebook dialog
|
||||||
event.stopPropagation()
|
|
||||||
activeMenuNotebookId.value = notebookId
|
|
||||||
notebookMenuRef.value.toggle(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeMenu() {
|
|
||||||
activeMenuNotebookId.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// New notebook creation (supports parent_id for sub-notebooks)
|
|
||||||
const showNewNotebookDialog = ref(false)
|
const showNewNotebookDialog = ref(false)
|
||||||
const newNotebookName = ref('')
|
const newNotebookName = ref('')
|
||||||
const newNotebookInputRef = ref(null)
|
const newNotebookInputRef = ref(null)
|
||||||
@@ -164,7 +210,6 @@ function startNewNotebook(parentId = null) {
|
|||||||
newNotebookName.value = ''
|
newNotebookName.value = ''
|
||||||
newNotebookParentId.value = parentId
|
newNotebookParentId.value = parentId
|
||||||
showNewNotebookDialog.value = true
|
showNewNotebookDialog.value = true
|
||||||
closeMenu()
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
newNotebookInputRef.value?.$el?.focus()
|
newNotebookInputRef.value?.$el?.focus()
|
||||||
})
|
})
|
||||||
@@ -192,74 +237,36 @@ function cancelNewNotebook() {
|
|||||||
newNotebookName.value = ''
|
newNotebookName.value = ''
|
||||||
newNotebookParentId.value = null
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div class="sidebar-container flex flex-col h-full bg-surface-50 border-r border-app-border overflow-hidden w-64">
|
||||||
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'">
|
|
||||||
|
|
||||||
<!-- Top Bar: Toggle + (Search & Add when expanded) -->
|
<!-- Search bar -->
|
||||||
<div class="p-3 border-b border-app-border flex items-center gap-2">
|
<div class="p-3 border-b border-app-border flex-shrink-0">
|
||||||
<template v-if="!collapsed">
|
<div class="flex items-center gap-2">
|
||||||
<div class="flex-1 relative">
|
<div class="flex-1 relative">
|
||||||
<IconField>
|
<IconField>
|
||||||
<InputIcon class="pi pi-search"/>
|
<InputIcon class="pi pi-search"/>
|
||||||
<InputText
|
<InputText
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
placeholder="搜索笔记..."
|
placeholder="搜索笔记..."
|
||||||
class="w-full"
|
class="w-full"
|
||||||
size="small"
|
size="small"
|
||||||
@input="onSearchInput"
|
@input="onSearchInput"
|
||||||
/>
|
/>
|
||||||
</IconField>
|
</IconField>
|
||||||
|
|
||||||
<!-- Search Results Dropdown -->
|
<!-- Search Results Dropdown -->
|
||||||
<div
|
<div
|
||||||
v-if="searchResults.length > 0"
|
v-if="searchResults.length > 0"
|
||||||
class="absolute top-full left-0 right-0 mt-1 bg-surface-0 border border-app-border rounded-lg shadow-lg z-50 max-h-48 overflow-auto"
|
class="absolute top-full left-0 right-0 mt-1 bg-surface-0 border border-app-border rounded-lg shadow-lg z-50 max-h-48 overflow-auto"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="note in searchResults"
|
v-for="note in searchResults"
|
||||||
:key="note.id"
|
:key="note.id"
|
||||||
class="px-3 py-2 hover:bg-surface-50 cursor-pointer flex items-center gap-2"
|
class="px-3 py-2 hover:bg-surface-50 cursor-pointer flex items-center gap-2"
|
||||||
@click="selectSearchNote(note)"
|
@click="selectSearchNote(note)"
|
||||||
>
|
>
|
||||||
<File :size="14" class="text-surface-400"/>
|
<File :size="14" class="text-surface-400"/>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
@@ -270,232 +277,48 @@ function getNodeActiveClass(node) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button icon="pi pi-plus" size="small" severity="secondary" aria-label="新建笔记" @click="createNote"/>
|
<Button icon="pi pi-plus" size="small" severity="secondary" aria-label="新建笔记" @click="createNote"/>
|
||||||
</template>
|
</div>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Navigation Content -->
|
<!-- PanelMenu -->
|
||||||
<ScrollPanel class="flex-1">
|
<div class="flex-1 px-2 gap-1 ">
|
||||||
<!-- Collapsed: Icon-only nav -->
|
<Menu :model="items11"/>
|
||||||
<template v-if="collapsed">
|
<div v-for="(item,index) in items11" :key="index" class="flex items-center gap-2 px-2 py-1 cursor-pointer hover:bg-[#f1f5f9]">
|
||||||
<div class="py-3 flex flex-col items-center gap-1">
|
<LucideIcon :name="item.icon" size="16 " color="red"></LucideIcon>
|
||||||
<!-- Notes nav items (collapsed) -->
|
{{ item.label }}
|
||||||
<div
|
</div>
|
||||||
class="w-10 h-10 flex items-center justify-center rounded-lg cursor-pointer transition-colors"
|
</div>
|
||||||
: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"/>
|
|
||||||
</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"/>
|
<!-- Bottom bar: theme + settings -->
|
||||||
|
<div class="border-t border-app-border p-3 flex-shrink-0">
|
||||||
<!-- Notebooks (collapsed) -->
|
<div class="flex items-center gap-2">
|
||||||
<div
|
<Button text severity="secondary" @click="toggleTheme" class="btn-icon h-10 px-3 flex items-center gap-2">
|
||||||
v-for="nb in flatNotebooks.filter(nb => !nb.parent_id)"
|
<template #icon>
|
||||||
:key="nb.id"
|
<Sun v-if="theme === 'light'" :size="18"/>
|
||||||
class="w-10 h-10 flex items-center justify-center rounded-lg cursor-pointer transition-colors"
|
<Moon v-else-if="theme === 'dark'" :size="18"/>
|
||||||
:class="activeNav === `notebook-${nb.id}` ? 'bg-emerald-50 text-emerald-600' : 'text-surface-500 hover:bg-surface-100'"
|
<Monitor v-else :size="18"/>
|
||||||
@click="selectNotebook(nb)"
|
</template>
|
||||||
v-tooltip.right="nb.name"
|
<span class="text-sm">{{ theme === 'light' ? '浅色' : theme === 'dark' ? '深色' : '系统' }}</span>
|
||||||
>
|
</Button>
|
||||||
<BookOpen :size="18"/>
|
<Button text severity="secondary" @click="handleNav('settings')"
|
||||||
</div>
|
class="btn-icon h-10 px-3 flex items-center gap-2 flex-1">
|
||||||
|
<template #icon>
|
||||||
<div class="w-8 h-px bg-surface-200 my-2"/>
|
<Settings :size="18"/>
|
||||||
|
</template>
|
||||||
<!-- Tags (collapsed) -->
|
<span class="text-sm">设置</span>
|
||||||
<div
|
</Button>
|
||||||
v-for="tag in tags"
|
</div>
|
||||||
: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>
|
|
||||||
<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>
|
|
||||||
<Sun v-if="theme === 'light'" :size="18"/>
|
|
||||||
<Moon v-else-if="theme === 'dark'" :size="18"/>
|
|
||||||
<Monitor v-else :size="18"/>
|
|
||||||
</template>
|
|
||||||
<span class="text-sm">{{ theme === 'light' ? '浅色' : theme === 'dark' ? '深色' : '系统' }}</span>
|
|
||||||
</Button>
|
|
||||||
<Button text severity="secondary" @click="handleNav('settings')"
|
|
||||||
class="btn-icon h-10 px-3 flex items-center gap-2 flex-1">
|
|
||||||
<template #icon>
|
|
||||||
<Settings :size="18"/>
|
|
||||||
</template>
|
|
||||||
<span class="text-sm">设置</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- New Notebook Dialog -->
|
<!-- New Notebook Dialog -->
|
||||||
<Dialog
|
<Dialog
|
||||||
v-model:visible="showNewNotebookDialog"
|
v-model:visible="showNewNotebookDialog"
|
||||||
:header="newNotebookParentId ? '新建子笔记本' : '新建笔记本'"
|
:header="newNotebookParentId ? '新建子笔记本' : '新建笔记本'"
|
||||||
:modal="true"
|
:modal="true"
|
||||||
:closable="true"
|
:closable="true"
|
||||||
:draggable="false"
|
:draggable="false"
|
||||||
class="w-80"
|
class="w-80"
|
||||||
@hide="cancelNewNotebook"
|
@hide="cancelNewNotebook"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<div v-if="newNotebookParentId" class="text-sm text-surface-400">
|
<div v-if="newNotebookParentId" class="text-sm text-surface-400">
|
||||||
@@ -503,13 +326,13 @@ function getNodeActiveClass(node) {
|
|||||||
</div>
|
</div>
|
||||||
<label for="new-notebook-name" class="text-sm text-surface-600">笔记本名称</label>
|
<label for="new-notebook-name" class="text-sm text-surface-600">笔记本名称</label>
|
||||||
<InputText
|
<InputText
|
||||||
id="new-notebook-name"
|
id="new-notebook-name"
|
||||||
ref="newNotebookInputRef"
|
ref="newNotebookInputRef"
|
||||||
v-model="newNotebookName"
|
v-model="newNotebookName"
|
||||||
placeholder="请输入笔记本名称"
|
placeholder="请输入笔记本名称"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
autofocus
|
autofocus
|
||||||
@keyup.enter="confirmNewNotebook"
|
@keyup.enter="confirmNewNotebook"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -521,74 +344,77 @@ function getNodeActiveClass(node) {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.collapse-enter-active,
|
.sidebar-container :deep(.p-menu) {
|
||||||
.collapse-leave-active {
|
border: 0;
|
||||||
transition: all 0.2s ease;
|
background: transparent;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapse-enter-from,
|
/*
|
||||||
.collapse-leave-to {
|
.sidebar-container :deep(.p-menu-list) {
|
||||||
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) {
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-tree :deep(.p-tree-node) {
|
.sidebar-container :deep(.p-menu-item-content) {
|
||||||
padding: 0;
|
border-radius: 0;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-tree :deep(.p-tree-node-content) {
|
.sidebar-container :deep(.p-menu-item-content:hover) {
|
||||||
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) {
|
|
||||||
background-color: var(--surface-100);
|
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);
|
background-color: var(--surface-800);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-tree :deep(.p-tree-node-selected) {
|
.sidebar-container :deep(.p-panelmenu-content) {
|
||||||
/* Handled by pt nodeContent dynamic class */
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-tree :deep(.p-tree-toggler) {
|
.sidebar-container :deep(.p-panelmenu-item-content) {
|
||||||
width: 1.5rem;
|
border-radius: 0;
|
||||||
height: 1.5rem;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-tree :deep(.p-tree-node-label) {
|
.sidebar-container :deep(.p-panelmenu-item-content:hover) {
|
||||||
font-size: 0.875rem;
|
background-color: var(--surface-100);
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Group nodes should not show hover effect */
|
:root.dark .sidebar-container :deep(.p-panelmenu-item-content:hover) {
|
||||||
.sidebar-tree :deep(.p-tree-node-content:has(.text-xs.font-semibold)) {
|
background-color: var(--surface-800);
|
||||||
cursor: default;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-tree :deep(.p-tree-node-content:has(.text-xs.font-semibold):hover) {
|
.sidebar-container :deep(.p-panelmenu-submenu) {
|
||||||
background-color: transparent;
|
padding-left: 0.25rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createPinia } from 'pinia'
|
|||||||
import PrimeVue from 'primevue/config'
|
import PrimeVue from 'primevue/config'
|
||||||
import Aura from '@primeuix/themes/aura'
|
import Aura from '@primeuix/themes/aura'
|
||||||
import Tooltip from 'primevue/tooltip'
|
import Tooltip from 'primevue/tooltip'
|
||||||
|
import Ripple from 'primevue/ripple'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import './assets/styles/main.css'
|
import './assets/styles/main.css'
|
||||||
import 'primeicons/primeicons.css'
|
import 'primeicons/primeicons.css'
|
||||||
@@ -21,5 +22,6 @@ app.use(PrimeVue, {
|
|||||||
|
|
||||||
app.use(pinia)
|
app.use(pinia)
|
||||||
app.directive('tooltip', Tooltip)
|
app.directive('tooltip', Tooltip)
|
||||||
|
app.directive('ripple', Ripple)
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|||||||
Reference in New Issue
Block a user