feat: 功能迭代

This commit is contained in:
2026-05-31 21:03:35 +08:00
parent ab26ea7ef9
commit e49865600b
4 changed files with 217 additions and 188 deletions

View File

@@ -15,7 +15,8 @@
"Bash(npx electron-vite *)",
"WebSearch",
"WebFetch(domain:primevue.org)",
"Bash(curl -s http://localhost:5173)"
"Bash(curl -s http://localhost:5173)",
"Bash(curl -s http://localhost:5173/)"
]
}
}

View File

@@ -61,6 +61,8 @@ async function onNavigate(id) {
if (id === 'trash') {
await notesStore.loadNotes({ isTrashed: 1 })
} else if (id === 'favorites') {
await notesStore.loadNotes({ isTrashed: 0, isFavorited: 1 })
} else {
await notesStore.loadNotes({ isTrashed: 0 })
}
@@ -71,6 +73,15 @@ function onSelectNote(note) {
notesStore.getNote(note.id)
}
async function onToggleFavorite(note) {
await notesStore.toggleFavorite(note.id)
// If we're in the favorites view, reload to remove unfavorited notes
if (currentNav.value === 'favorites') {
await notesStore.loadNotes({ isTrashed: 0, isFavorited: 1 })
computeOverviewNotes()
}
}
function onBackToOverview() {
notesStore.clearCurrentNote()
// Reload notes to reflect any changes
@@ -241,6 +252,7 @@ async function onWorkspaceConfirm(path) {
:notes="overviewNotes"
:title="getOverviewTitle()"
@select-note="onSelectNote"
@toggle-favorite="onToggleFavorite"
/>
<!-- Editor Panel (when a note is selected) -->

View File

@@ -3,14 +3,14 @@ import { ref, computed } from 'vue'
import Button from 'primevue/button'
import Tag from 'primevue/tag'
import ScrollPanel from 'primevue/scrollpanel'
import { Search, LayoutGrid, List, FileText, Clock, Calendar, Folder } from '@lucide/vue'
import { Search, LayoutGrid, List, FileText, Clock, Calendar, Folder, Star } from '@lucide/vue'
const props = defineProps({
notes: { type: Array, required: true },
title: { type: String, default: '所有笔记' }
})
const emit = defineEmits(['select-note'])
const emit = defineEmits(['select-note', 'toggle-favorite'])
const viewMode = ref('card') // 'card' | 'list'
@@ -18,6 +18,11 @@ function selectNote(note) {
emit('select-note', note)
}
function toggleFavorite(note, event) {
event.stopPropagation()
emit('toggle-favorite', note)
}
function formatDate(dateStr) {
if (!dateStr) return ''
const d = new Date(dateStr)
@@ -104,9 +109,16 @@ function getPreview(note) {
<!-- Card Header -->
<div class="flex items-start gap-2 mb-3">
<FileText :size="18" class="text-primary-400 shrink-0 mt-0.5" />
<h3 class="text-sm font-semibold text-surface-900 line-clamp-2 group-hover:text-primary-600 transition-colors">
<h3 class="flex-1 text-sm font-semibold text-surface-900 line-clamp-2 group-hover:text-primary-600 transition-colors">
{{ note.title || '无标题' }}
</h3>
<button
class="shrink-0 p-0.5 rounded transition-colors"
:class="note.is_favorited ? 'text-amber-400 hover:text-amber-500' : 'text-surface-300 hover:text-amber-400 opacity-0 group-hover:opacity-100'"
@click="toggleFavorite(note, $event)"
>
<Star :size="16" :fill="note.is_favorited ? 'currentColor' : 'none'" />
</button>
</div>
<!-- Preview -->
@@ -150,9 +162,18 @@ function getPreview(note) {
<FileText :size="18" class="text-primary-400 shrink-0" />
<div class="flex-1 min-w-0">
<h3 class="text-sm font-semibold text-surface-900 line-clamp-2 group-hover:text-primary-600 transition-colors">
<div class="flex items-center gap-2">
<h3 class="text-sm font-semibold text-surface-900 line-clamp-1 group-hover:text-primary-600 transition-colors">
{{ note.title || '无标题' }}
</h3>
<button
class="shrink-0 p-0.5 rounded transition-colors"
:class="note.is_favorited ? 'text-amber-400 hover:text-amber-500' : 'text-surface-300 hover:text-amber-400 opacity-0 group-hover:opacity-100'"
@click="toggleFavorite(note, $event)"
>
<Star :size="14" :fill="note.is_favorited ? 'currentColor' : 'none'" />
</button>
</div>
<p class="text-xs text-surface-500 line-clamp-2 mt-0.5">{{ getPreview(note) }}</p>
</div>

View File

@@ -17,13 +17,19 @@ import {
import {useThemeStore} from '../stores/theme'
import {useNotesStore} from '../stores/notes'
import {useNotebooksStore} from '../stores/notebooks'
import {useNotebookTree} from '../composables/useNotebookTree'
import {useSidebarTree} from '../composables/useSidebarTree'
import ButtonIcon from "./ButtonIcon.vue";
const themeStore = useThemeStore()
const notesStore = useNotesStore()
const notebooksStore = useNotebooksStore()
const {notebookTree, flatNotebooks, expandedKeys, loadNotebooks, loadNotes} = useNotebookTree()
const props = defineProps({
notebooks: {type: Array, default: () => []},
tags: {type: Array, default: () => []}
})
const {sidebarTree, flatNotebooks, expandedKeys, toggleNodeExpanded, loadNotebooks, loadNotes} = useSidebarTree(computed(() => props.tags))
const theme = computed(() => themeStore.theme)
const toggleTheme = themeStore.toggleTheme
@@ -34,11 +40,6 @@ const newNotebookParentName = computed(() => {
return nb ? nb.name : ''
})
const props = defineProps({
notebooks: {type: Array, default: () => []},
tags: {type: Array, default: () => []}
})
const emit = defineEmits(['navigate', 'create-note', 'select-notebook', 'select-tag', 'select-note'])
// Selected tree node
@@ -60,17 +61,6 @@ function toggleCollapsed() {
// Navigation state
const activeNav = ref('all')
// Section expand state
const expandedSections = ref({
notes: true,
notebooks: true,
tags: true
})
function toggleSection(section) {
expandedSections.value[section] = !expandedSections.value[section]
}
// Search
const searchQuery = ref('')
const searchResults = ref([])
@@ -88,18 +78,6 @@ async function onSearchInput() {
}, 200)
}
// Navigation items
const navSections = [
{
id: 'notes',
items: [
{id: 'all', icon: File, label: '所有笔记'},
{id: 'favorites', icon: Star, label: '收藏夹'},
{id: 'trash', icon: Trash2, label: '回收站'},
]
}
]
// Methods
function handleNav(id) {
activeNav.value = id
@@ -116,14 +94,23 @@ function selectNotebook(notebook) {
* PrimeVue v4 passes node directly (not wrapped in event object)
*/
function onTreeSelect(node) {
if (node.data.type === 'notebook') {
const { type, id } = node.data
if (type === 'nav') {
selectedTreeKey.value = node.key
activeNav.value = `notebook-${node.data.id}`
handleNav(id)
} else if (type === 'notebook') {
selectedTreeKey.value = node.key
activeNav.value = `notebook-${id}`
emit('select-notebook', node.data)
} else if (node.data.type === 'note') {
} else if (type === 'note') {
selectedTreeKey.value = node.key
activeNav.value = `note-${node.data.id}`
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)
}
}
@@ -218,6 +205,29 @@ function getNotebookName(note) {
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>
@@ -278,15 +288,30 @@ function getNotebookName(note) {
<!-- Collapsed: Icon-only nav -->
<template v-if="collapsed">
<div class="py-3 flex flex-col items-center gap-1">
<!-- Notes nav items (collapsed) -->
<div
v-for="item in navSections[0].items"
:key="item.id"
class="w-10 h-10 flex items-center justify-center rounded-lg cursor-pointer transition-colors"
:class="activeNav === item.id ? 'bg-primary-50 text-primary-600' : 'text-surface-500 hover:bg-surface-100'"
@click="handleNav(item.id)"
v-tooltip.right="item.label"
:class="activeNav === 'all' ? 'bg-primary-50 text-primary-600' : 'text-surface-500 hover:bg-surface-100'"
@click="handleNav('all')"
v-tooltip.right="'所有笔记'"
>
<component :is="item.icon" :size="18"/>
<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"/>
@@ -319,146 +344,104 @@ function getNotebookName(note) {
</div>
</template>
<!-- Expanded: Full nav -->
<!-- Expanded: Unified Tree -->
<template v-else>
<!-- Notes Section -->
<div class="py-3">
<div
class="px-4 py-1.5 flex items-center justify-between cursor-pointer hover:bg-surface-100 transition-colors"
@click="toggleSection('notes')"
>
<span class="text-xs font-semibold text-surface-400 uppercase tracking-wider">笔记</span>
<ChevronDown
v-if="expandedSections.notes"
:size="14"
class="text-surface-400 transition-transform"
/>
<ChevronRight
v-else
:size="14"
class="text-surface-400 transition-transform"
/>
</div>
<Transition name="collapse">
<div v-show="expandedSections.notes">
<div
v-for="item in navSections[0].items"
:key="item.id"
class="flex items-center gap-3 px-4 py-2 mx-2 cursor-pointer transition-colors rounded-lg"
:class="activeNav === item.id
? 'bg-surface-200/70 text-primary-700 font-medium shadow-sm'
: 'text-surface-600 hover:bg-surface-100'"
@click="handleNav(item.id)"
>
<component :is="item.icon" :size="18"/>
<span class="text-sm">{{ item.label }}</span>
</div>
</div>
</Transition>
</div>
<!-- Notebooks Section -->
<div class="py-1">
<div class="px-4 py-1.5 flex items-center justify-between">
<div
class="flex items-center gap-1 cursor-pointer hover:bg-surface-100 transition-colors rounded px-1 py-0.5 -ml-1"
@click="toggleSection('notebooks')"
>
<span class="text-xs font-semibold text-surface-400 uppercase tracking-wider">笔记本</span>
<ChevronDown
v-if="expandedSections.notebooks"
:size="14"
class="text-surface-400 transition-transform"
/>
<ChevronRight
v-else
:size="14"
class="text-surface-400 transition-transform"
/>
</div>
<button
class="w-6 h-6 flex items-center justify-center rounded hover:bg-surface-200 text-surface-400 hover:text-surface-600 transition-colors"
@click.stop="startNewNotebook"
title="新建笔记本"
>
<Plus :size="14"/>
</button>
</div>
<Transition name="collapse">
<div v-show="expandedSections.notebooks">
<div class="py-2">
<Tree
:value="notebookTree"
:value="sidebarTree"
:expandedKeys="expandedKeys"
selectionMode="single"
:selectionKeys="selectedTreeKey ? { [selectedTreeKey]: true } : {}"
@node-select="onTreeSelect"
class="w-full notebook-tree"
class="w-full sidebar-tree"
:pt="{
root: { class: 'border-none bg-transparent w-full' },
node: { class: 'py-0.5', style: 'width: 100%' },
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',
'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'
: '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">
<div 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 }}1</span>
<span>1</span>
<!-- 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>
</Transition>
</div>
<!-- Tags Section -->
<div class="py-1">
<div
class="px-4 py-1.5 flex items-center justify-between cursor-pointer hover:bg-surface-100 transition-colors"
@click="toggleSection('tags')"
>
<span class="text-xs font-semibold text-surface-400 uppercase tracking-wider">标签</span>
<ChevronDown
v-if="expandedSections.tags"
:size="14"
class="text-surface-400 transition-transform"
/>
<ChevronRight
v-else
:size="14"
class="text-surface-400 transition-transform"
/>
</div>
<Transition name="collapse">
<div v-show="expandedSections.tags">
<div
v-for="tag in tags"
:key="tag.id"
class="flex items-center gap-3 px-4 py-2 mx-2 cursor-pointer transition-colors rounded-lg group"
:class="activeNav === `tag-${tag.id}`
? 'bg-amber-50 text-amber-700 font-medium shadow-sm'
: 'text-surface-600 hover:bg-surface-100'"
@click="selectTag(tag)"
>
<div class="w-3 h-3 rounded-full flex-shrink-0 bg-amber-400"/>
<span class="text-sm flex-1">{{ tag.name }}</span>
<span class="text-xs text-surface-400 opacity-0 group-hover:opacity-100 transition-opacity">{{
tag.note_count || 0
}}</span>
</div>
</div>
</Transition>
</div>
</template>
</ScrollPanel>
@@ -556,44 +539,56 @@ function getNotebookName(note) {
max-height: 500px;
}
/* Custom styles for notebook tree */
.notebook-tree :deep(.p-tree) {
/* Custom styles for sidebar tree */
.sidebar-tree :deep(.p-tree) {
padding: 0;
}
.notebook-tree :deep(.p-tree-node) {
.sidebar-tree :deep(.p-tree-node) {
padding: 0;
width: 100%;
}
.notebook-tree :deep(.p-tree-node-content) {
.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%;
}
.notebook-tree :deep(.p-tree-node-content:hover) {
.sidebar-tree :deep(.p-tree-node-content:hover) {
background-color: var(--surface-100);
}
.notebook-tree :deep(.p-tree-node-selected) {
:root.dark .sidebar-tree :deep(.p-tree-node-content:hover) {
background-color: var(--surface-800);
}
.sidebar-tree :deep(.p-tree-node-selected) {
/* Handled by pt nodeContent dynamic class */
}
.notebook-tree :deep(.p-tree-toggler) {
.sidebar-tree :deep(.p-tree-toggler) {
width: 1.5rem;
height: 1.5rem;
margin-right: 0;
}
.notebook-tree :deep(.p-tree-node-icon) {
margin-right: 0.5rem;
}
.notebook-tree :deep(.p-tree-node-label) {
.sidebar-tree :deep(.p-tree-node-label) {
font-size: 0.875rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
/* Group nodes should not show hover effect */
.sidebar-tree :deep(.p-tree-node-content:has(.text-xs.font-semibold)) {
cursor: default;
}
.sidebar-tree :deep(.p-tree-node-content:has(.text-xs.font-semibold):hover) {
background-color: transparent;
}
</style>