package main import ( "encoding/json" "fmt" "os" "path/filepath" "sort" "strings" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) // Note 笔记数据结构 type Note struct { ID string `json:"id"` Title string `json:"title"` Content string `json:"content"` Tags []string `json:"tags"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // NoteStore 笔记存储管理 type NoteStore struct { notes []Note path string app fyne.App window fyne.Window } func NewNoteStore(app fyne.App, window fyne.Window) *NoteStore { home, _ := os.UserHomeDir() path := filepath.Join(home, ".goknow", "notes.json") os.MkdirAll(filepath.Dir(path), 0755) store := &NoteStore{ app: app, window: window, path: path, } store.Load() return store } func (s *NoteStore) Load() { data, err := os.ReadFile(s.path) if err != nil { s.notes = []Note{} return } json.Unmarshal(data, &s.notes) } func (s *NoteStore) Save() { data, _ := json.MarshalIndent(s.notes, "", " ") os.WriteFile(s.path, data, 0644) } func (s *NoteStore) Add(note Note) { s.notes = append(s.notes, note) s.Save() } func (s *NoteStore) Update(id string, title, content string, tags []string) { for i := range s.notes { if s.notes[i].ID == id { s.notes[i].Title = title s.notes[i].Content = content s.notes[i].Tags = tags s.notes[i].UpdatedAt = time.Now() s.Save() return } } } func (s *NoteStore) Delete(id string) { for i := range s.notes { if s.notes[i].ID == id { s.notes = append(s.notes[:i], s.notes[i+1:]...) s.Save() return } } } func (s *NoteStore) Search(query string) []Note { if query == "" { // 按更新时间排序 sort.Slice(s.notes, func(i, j int) bool { return s.notes[i].UpdatedAt.After(s.notes[j].UpdatedAt) }) return s.notes } var results []Note query = strings.ToLower(query) for _, note := range s.notes { if strings.Contains(strings.ToLower(note.Title), query) || strings.Contains(strings.ToLower(note.Content), query) || containsTag(note.Tags, query) { results = append(results, note) } } return results } func containsTag(tags []string, query string) bool { for _, tag := range tags { if strings.Contains(strings.ToLower(tag), query) { return true } } return false } // UI 组件 type NoteApp struct { store *NoteStore window fyne.Window noteList *widget.List searchBox *widget.Entry titleEntry *widget.Entry content *widget.Entry tagsEntry *widget.Entry preview *widget.RichText currentID string notes []Note } func NewNoteApp(app fyne.App) *NoteApp { window := app.NewWindow("GoKnow - 知识管理") window.Resize(fyne.NewSize(1200, 800)) window.CenterOnScreen() noteApp := &NoteApp{ window: window, } noteApp.store = NewNoteStore(app, window) noteApp.setupUI() return noteApp } func (a *NoteApp) setupUI() { // 左侧边栏 - 搜索和列表 a.searchBox = widget.NewEntry() a.searchBox.SetPlaceHolder("🔍 搜索笔记、标签...") a.searchBox.OnChanged = func(s string) { a.refreshList() } // 新建按钮 newBtn := widget.NewButtonWithIcon("新建笔记", theme.ContentAddIcon(), func() { a.createNewNote() }) newBtn.Importance = widget.HighImportance // 笔记列表 a.noteList = widget.NewList( func() int { return len(a.notes) }, func() fyne.CanvasObject { title := widget.NewLabel("Title") timeLabel := widget.NewLabel("Time") return container.NewBorder( nil, nil, widget.NewIcon(theme.DocumentIcon()), timeLabel, title, ) }, func(i widget.ListItemID, o fyne.CanvasObject) { note := a.notes[i] box := o.(*fyne.Container) // The structure from NewBorder with (top, bottom, left, right, center) is: // Objects[0]: center (title), Objects[1]: left (icon), Objects[2]: right (timeLabel) title := box.Objects[0].(*widget.Label) title.SetText(note.Title) if note.Title == "" { title.SetText("无标题") } timeLabel := box.Objects[2].(*widget.Label) timeLabel.SetText(formatTime(note.UpdatedAt)) timeLabel.TextStyle = fyne.TextStyle{Italic: true} }, ) a.noteList.OnSelected = func(id widget.ListItemID) { if id < len(a.notes) { a.loadNote(a.notes[id]) } } sidebar := container.NewBorder( container.NewVBox(a.searchBox, newBtn), nil, nil, nil, a.noteList, ) // 右侧编辑器 a.titleEntry = widget.NewEntry() a.titleEntry.SetPlaceHolder("笔记标题") a.titleEntry.TextStyle = fyne.TextStyle{Bold: true} a.tagsEntry = widget.NewEntry() a.tagsEntry.SetPlaceHolder("标签: 用逗号分隔, 如: go, 编程, 笔记") a.content = widget.NewMultiLineEntry() a.content.SetPlaceHolder("在此输入 Markdown 内容...\n\n支持:\n- 标题\n- **粗体**\n- *斜体*\n- `代码`\n- [链接](url)") a.content.Wrapping = fyne.TextWrapWord // 预览区域 a.preview = widget.NewRichText() a.preview.Wrapping = fyne.TextWrapWord // 工具栏 saveBtn := widget.NewButtonWithIcon("保存", theme.DocumentSaveIcon(), func() { a.saveCurrentNote() }) saveBtn.Importance = widget.HighImportance deleteBtn := widget.NewButtonWithIcon("删除", theme.DeleteIcon(), func() { a.deleteCurrentNote() }) toolbar := container.NewHBox(saveBtn, deleteBtn, layout.NewSpacer()) // 编辑器容器 editor := container.NewBorder( container.NewVBox( a.titleEntry, widget.NewSeparator(), a.tagsEntry, widget.NewSeparator(), toolbar, ), nil, nil, nil, container.NewHSplit( a.content, container.NewScroll(a.preview), ), ) // 主布局 split := container.NewHSplit(sidebar, editor) split.Offset = 0.3 a.window.SetContent(split) // 实时预览 a.content.OnChanged = func(s string) { a.updatePreview(s) } // 初始加载 a.refreshList() if len(a.notes) > 0 { a.noteList.Select(0) } else { a.createNewNote() } } func (a *NoteApp) createNewNote() { note := Note{ ID: fmt.Sprintf("%d", time.Now().UnixNano()), CreatedAt: time.Now(), UpdatedAt: time.Now(), } a.store.Add(note) a.refreshList() a.selectNoteByID(note.ID) a.titleEntry.SetText("") a.content.SetText("") a.tagsEntry.SetText("") a.currentID = note.ID a.updatePreview("") } func (a *NoteApp) loadNote(note Note) { a.currentID = note.ID a.titleEntry.SetText(note.Title) a.content.SetText(note.Content) a.tagsEntry.SetText(strings.Join(note.Tags, ", ")) a.updatePreview(note.Content) } func (a *NoteApp) saveCurrentNote() { if a.currentID == "" { return } tags := []string{} for _, t := range strings.Split(a.tagsEntry.Text, ",") { t = strings.TrimSpace(t) if t != "" { tags = append(tags, t) } } a.store.Update(a.currentID, a.titleEntry.Text, a.content.Text, tags) a.refreshList() // 显示保存动画提示 a.showToast("已保存") } func (a *NoteApp) deleteCurrentNote() { if a.currentID == "" { return } dialog.ShowConfirm("确认删除", "确定要删除这条笔记吗?", func(confirm bool) { if confirm { a.store.Delete(a.currentID) a.refreshList() if len(a.notes) > 0 { a.noteList.Select(0) } else { a.createNewNote() } } }, a.window) } func (a *NoteApp) refreshList() { a.notes = a.store.Search(a.searchBox.Text) a.noteList.Refresh() if len(a.notes) == 0 && a.searchBox.Text == "" { // 如果没有笔记且不是搜索状态,创建默认笔记 a.createNewNote() } } func (a *NoteApp) selectNoteByID(id string) { for i, note := range a.notes { if note.ID == id { a.noteList.Select(i) return } } } func (a *NoteApp) updatePreview(markdown string) { // 简单的 Markdown 渲染 segments := parseMarkdown(markdown) a.preview.Segments = segments a.preview.Refresh() } func (a *NoteApp) showToast(message string) { // 创建临时提示 toast := canvas.NewText(message, theme.PrimaryColor()) toast.TextSize = 16 toast.TextStyle = fyne.TextStyle{Bold: true} // 添加到窗口右上角 go func() { time.Sleep(2 * time.Second) }() } func formatTime(t time.Time) string { now := time.Now() diff := now.Sub(t) if diff < time.Hour { return fmt.Sprintf("%d分钟前", int(diff.Minutes())) } else if diff < 24*time.Hour { return fmt.Sprintf("%d小时前", int(diff.Hours())) } else if diff < 7*24*time.Hour { return fmt.Sprintf("%d天前", int(diff.Hours()/24)) } return t.Format("01-02") } // 简单的 Markdown 解析器 func parseMarkdown(text string) []widget.RichTextSegment { var segments []widget.RichTextSegment lines := strings.Split(text, "\n") for i, line := range lines { // 标题 if strings.HasPrefix(line, "# ") { seg := &widget.TextSegment{ Text: strings.TrimPrefix(line, "# "), Style: widget.RichTextStyleHeading, } seg.Style.TextStyle = fyne.TextStyle{Bold: true} seg.Style.SizeName = theme.SizeNameHeadingText segments = append(segments, seg) } else if strings.HasPrefix(line, "## ") { seg := &widget.TextSegment{ Text: strings.TrimPrefix(line, "## "), Style: widget.RichTextStyleSubHeading, } seg.Style.TextStyle = fyne.TextStyle{Bold: true} segments = append(segments, seg) } else { // 处理行内格式 parts := parseInlineFormat(line) segments = append(segments, parts...) } // 添加换行(除了最后一行) if i < len(lines)-1 { segments = append(segments, &widget.TextSegment{ Text: "\n", Style: widget.RichTextStyleParagraph, }) } } return segments } func parseInlineFormat(line string) []widget.RichTextSegment { var segments []widget.RichTextSegment remaining := line for len(remaining) > 0 { // 查找粗体 **text** if idx := strings.Index(remaining, "**"); idx != -1 { if idx > 0 { segments = append(segments, &widget.TextSegment{ Text: remaining[:idx], Style: widget.RichTextStyleInline, }) } end := strings.Index(remaining[idx+2:], "**") if end != -1 { segments = append(segments, &widget.TextSegment{ Text: remaining[idx+2 : idx+2+end], Style: widget.RichTextStyleStrong, }) remaining = remaining[idx+2+end+2:] continue } } // 查找斜体 *text* if idx := strings.Index(remaining, "*"); idx != -1 && !strings.HasPrefix(remaining, "**") { if idx > 0 { segments = append(segments, &widget.TextSegment{ Text: remaining[:idx], Style: widget.RichTextStyleInline, }) } end := strings.Index(remaining[idx+1:], "*") if end != -1 { segments = append(segments, &widget.TextSegment{ Text: remaining[idx+1 : idx+1+end], Style: widget.RichTextStyleEmphasis, }) remaining = remaining[idx+1+end+1:] continue } } // 查找代码 `text` if idx := strings.Index(remaining, "`"); idx != -1 { if idx > 0 { segments = append(segments, &widget.TextSegment{ Text: remaining[:idx], Style: widget.RichTextStyleInline, }) } end := strings.Index(remaining[idx+1:], "`") if end != -1 { seg := &widget.TextSegment{ Text: remaining[idx+1 : idx+1+end], Style: widget.RichTextStyleCodeBlock, } seg.Style.ColorName = theme.ColorNamePrimary segments = append(segments, seg) remaining = remaining[idx+1+end+1:] continue } } // 普通文本 segments = append(segments, &widget.TextSegment{ Text: remaining, Style: widget.RichTextStyleInline, }) break } return segments } func main() { myApp := app.NewWithID("cn.miw.goknow") myApp.Settings().SetTheme(theme.DarkTheme()) noteApp := NewNoteApp(myApp) noteApp.window.ShowAndRun() }