当前位置: 首页 > news >正文

AI书签管理工具开发全记录(十五):TUI基本逻辑实现与数据展示

文章目录

  • AI书签管理工具开发全记录(十五):TUI基本逻辑实现与数据展示
    • 1.前言 📝
    • 2.核心要点分析
      • 2.1 核心分析
      • 2.2 解决方案
    • 3. 程序流程
      • 3.1 用户交互流程
    • 3.功能实现
      • 3.1 书签分类渲染
      • 3.2 书签分类渲染
      • 3.3 书签描述渲染
      • 3.3 搜索功能实现
      • 3.4 事件绑定
      • 3.5 启动时渲染分类
    • 4.演示

AI书签管理工具开发全记录(十五):TUI基本逻辑实现与数据展示

1.前言 📝

在上一篇文章中,我们完善了TUI的基本功能框架,增加了焦点切换功能。并且增加了日志显示功能支持。本文将聚焦于书签数据的页面展示。

2.核心要点分析

2.1 核心分析

categoryList    *tview.List
bookmarkList    *tview.List

categoryList和bookmarkList都是tview.List。
AddItem定义如下:

func (l *List) AddItem(mainText, secondaryText string, shortcut rune, selected func()) *List {l.InsertItem(-1, mainText, secondaryText, shortcut, selected)return l
}

这意味着我们只能存储一级分类标题,和二级分类标题。无法存放id之类的数据,更别说更多的自定义数据。

2.2 解决方案

将当前分类数据完整信息进行存储,后续通过选中的index判断当前选中的数据项。

// 存储分类信息 按index存储
categoryCache map[int]models.Category
// 存储书签信息 按index存储
bookmarkCache map[int]models.Bookmark

每次进行渲染分类时,除了需要清空列表数据,还需要重建分类信息缓存。

// 清空分类列表
t.categoryList.Clear()
t.categoryCache = make(map[int]models.Category)

3. 程序流程

3.1 用户交互流程

焦点变化
左箭头
右箭头
Ctrl+F
Ctrl+R
ESC或q
Enter
更新焦点索引
向左切换焦点
向右切换焦点
设置新焦点组件样式
用户按键
按键类型
激活搜索框
重置搜索
退出程序
用户输入搜索词
执行搜索
清空当前数据
查询数据库
分组匹配结果
显示分类列表
显示匹配的第一个分类的书签
显示书签详情
重置搜索状态
加载完整分类列表
显示第一个分类的书签
显示书签详情

3.功能实现

3.1 书签分类渲染

func (t *TuiView) RenderCategoryList() {// 清空分类列表t.categoryList.Clear()// 从数据库查询分类列表var categories []models.Categoryif err := t.db.Find(&categories).Error; err != nil {// 处理错误,例如记录日志或显示错误信息return}// 清空分类列表t.categoryList.Clear()t.categoryCache = make(map[int]models.Category)// 渲染分类列表到tui界面for _, category := range categories {t.categoryList.AddItem(category.Name, category.Description, 0, nil)// 存储分类信息t.categoryCache[t.categoryList.GetItemCount()-1] = category}// 如果没有分类,显示提示信息if t.categoryList.GetItemCount() == 0 {t.categoryList.AddItem("无分类", "先去添加分类吧", 0, nil)} else {t.categoryList.SetCurrentItem(0)t.RenderBookmarksView(categories[0].ID)}
}

3.2 书签分类渲染

// 添加RenderBookmarksView函数
func (t *TuiView) RenderBookmarksView(categoryID uint) {t.LogInfo("RenderBookmarksView: " + strconv.FormatUint(uint64(categoryID), 10))// 清空书签列表t.bookmarkList.Clear()t.bookmarkCache = make(map[int]models.Bookmark)// 从数据库查询指定分类的书签列表var bookmarks []models.Bookmarkif err := t.db.Where("category_id = ?", categoryID).Find(&bookmarks).Error; err != nil {// 处理错误,例如记录日志或显示错误信息return}// 渲染书签列表到tui界面for _, bookmark := range bookmarks {t.bookmarkList.AddItem(bookmark.Title, bookmark.URL, 0, nil)// 存储书签信息t.bookmarkCache[t.bookmarkList.GetItemCount()-1] = bookmark}if t.bookmarkList.GetItemCount() > 0 {t.bookmarkList.SetCurrentItem(0)t.RenderBookmarkDetail(bookmarks[0].ID)}
}

3.3 书签描述渲染

书签描述支持markdown,所以我们需要对markdown语法进行支持,更好的在终端中进行渲染。

我们需要使用到glamour
安装

go get -u "github.com/charmbracelet/glamour"

编写工具函数

package utilsimport ("fmt""github.com/charmbracelet/glamour"
)var MarkdownInstance = Markdown{}type Markdown struct {renderer *glamour.TermRenderer
}func init() {renderer, err := glamour.NewTermRenderer(glamour.WithStandardStyle("dark"),glamour.WithWordWrap(120),glamour.WithEmoji(),)if err != nil {fmt.Println(err)panic(err)}MarkdownInstance.renderer = renderer}func (c *Markdown) MarkdownRender(content string) (string, error) {out, err := c.renderer.Render(content)if err != nil {return "", err}return out, nil
}

书签描述渲染

// 完善RenderBookmarkDetail函数,调用markdown渲染
func (t *TuiView) RenderBookmarkDetail(bookmarkID uint) {// 从数据库查询书签详情var bookmark models.Bookmarkif err := t.db.First(&bookmark, bookmarkID).Error; err != nil {// 处理错误,例如记录日志或显示错误信息return}// 调用markdown渲染renderedContent, err := utils.MarkdownInstance.MarkdownRender(bookmark.Description)if err != nil {// 处理错误,例如记录日志或显示错误信息return}// 渲染书签详情到tui界面t.descriptionView.SetText(tview.TranslateANSI(renderedContent))
}

注意必须调用tview.TranslateANSI方法,否则终端会展示一堆乱码。

3.3 搜索功能实现

func (t *TuiView) SearchBookmarks(keyword string) {t.isSearching = truet.searchBox.SetText(keyword)t.bookmarkList.Clear()t.categoryList.Clear()t.categoryCache = make(map[int]models.Category)t.bookmarkCache = make(map[int]models.Bookmark)// 从数据库查询匹配的书签var bookmarks []models.Bookmarkif err := t.db.Where("title LIKE ? OR category_id IN (SELECT id FROM categories WHERE name LIKE ?)", "%"+keyword+"%", "%"+keyword+"%").Find(&bookmarks).Error; err != nil {// 处理错误,例如记录日志或显示错误信息return}// 将书签按分类分组categoryBookmarks := make(map[uint][]models.Bookmark)for _, bookmark := range bookmarks {categoryBookmarks[bookmark.CategoryID] = append(categoryBookmarks[bookmark.CategoryID], bookmark)}// 查询所有包含匹配书签的分类var categories []models.Categoryif err := t.db.Where("id IN (?)", t.db.Table("bookmarks").Select("category_id").Where("title LIKE ? OR category_id IN (SELECT id FROM categories WHERE name LIKE ?)", "%"+keyword+"%", "%"+keyword+"%")).Find(&categories).Error; err != nil {// 处理错误,例如记录日志或显示错误信息return}// 渲染分类列表到tui界面for _, category := range categories {t.categoryList.AddItem(category.Name, category.Description, 0, nil)// 存储分类信息t.categoryCache[t.categoryList.GetItemCount()-1] = category}// 如果没有匹配的分类,显示提示信息if t.categoryList.GetItemCount() == 0 {t.categoryList.AddItem("无匹配分类", "请尝试其他关键词", 0, nil)} else {// 渲染第一个分类下的书签if bookmarks, ok := categoryBookmarks[categories[0].ID]; ok {for _, bookmark := range bookmarks {t.bookmarkList.AddItem(bookmark.Title, bookmark.URL, 0, nil)// 存储书签信息t.bookmarkCache[t.bookmarkList.GetItemCount()-1] = bookmark}if t.bookmarkList.GetItemCount() > 0 {t.bookmarkList.SetCurrentItem(0)t.RenderBookmarkDetail(bookmarks[0].ID)}}t.categoryList.SetCurrentItem(0)}
}

需要注意,一定要屏蔽掉原有的书签查询逻辑,否则会造成数据异常。

3.4 事件绑定

绑定事件后,切换分类时,数据才会实时查询渲染。当搜索时,我们交由搜索函数自生处理搜索逻辑。

func (t *TuiView) BindChangeFunc() {t.categoryList.SetChangedFunc(func(index int, mainText, secondaryText string, shortcut rune) {if t.isSearching {return}// 获取当前分类列表category := t.categoryCache[index]t.LogInfo(fmt.Sprintf("category: %v", category))t.RenderBookmarksView(category.ID)})t.bookmarkList.SetChangedFunc(func(index int, mainText, secondaryText string, shortcut rune) {bookmark := t.bookmarkCache[index]t.LogInfo(fmt.Sprintf("bookmark: %v", bookmark))t.RenderBookmarkDetail(bookmark.ID)})t.searchBox.SetDoneFunc(func(key tcell.Key) {if key == tcell.KeyEnter {t.SearchBookmarks(t.searchBox.GetText())// 设置焦点到书签列表t.Focus(t.bookmarkList)} else if key == tcell.KeyF2 {t.ResetSearch()t.searchBox.SetText("")}})
}

3.5 启动时渲染分类

根据是否传入了搜索关键词来决定渲染逻辑。

// 如果搜索词不为空,直接加载搜索结果
if searchKeyword != "" {c.searchBox.SetText(searchKeyword)c.Search(searchKeyword)c.focusIndex = 1
} else {c.RenderCategoryView()
}

4.演示

aibookmark04.gif

可以快速进行书签的浏览和查询


往期系列

  • Ai书签管理工具开发全记录(一):项目总览与技术蓝图
  • Ai书签管理工具开发全记录(二):项目基础框架搭建
  • AI书签管理工具开发全记录(三):配置及数据系统设计
  • AI书签管理工具开发全记录(四):日志系统设计与实现
  • AI书签管理工具开发全记录(五):后端服务搭建与API实现
  • AI书签管理工具开发全记录(六):前端管理基础框框搭建 Vue3+Element Plus
  • AI书签管理工具开发全记录(七):页面编写与接口对接
  • AI书签管理工具开发全记录(八):Ai创建书签功能实现
  • AI书签管理工具开发全记录(九):用户端页面集成与展示
  • AI书签管理工具开发全记录(十):命令行中结合ai高效添加书签
  • AI书签管理工具开发全记录(十一):MCP集成
  • AI书签管理工具开发全记录(十二):MCP集成查询
  • AI书签管理工具开发全记录(十三):TUI基本框架搭建
  • AI书签管理工具开发全记录(十四):TUI基本界面完善

相关文章:

  • 理解 RAG_HYBRID_BM25_WEIGHT:打造更智能的混合检索增强生成系统
  • 使用 C++/OpenCV 创建动态流星雨特效 (实时动画)
  • PyTorch 中cumprod函数计算张量沿指定维度的累积乘积详解和代码示例
  • 常用函数库之 - std::function
  • 计算机操作系统(十五)死锁的概念与死锁的处理方法
  • 轮廓上距离最大的两个点
  • 温控加热电路【比较器输出作为MOS开关】
  • Python Copilot【代码辅助工具】 简介
  • C++修炼:C++11(二)
  • 鸿蒙仓颉语言开发实战教程:商城应用个人中心页面
  • 数 据 结 构 进 阶:哨 兵 位 的 头 结 点 如 何 简 化 链 表 操 作
  • conda环境配置(二) —— 报错
  • Macbook M3 使用 VMware Fusion 安装 openEuler24.03LTS
  • 性能测试-jmeter实战2
  • ​React Hooks 的闭包陷阱问题
  • 【看到哪里写到哪里】C的“数组指针”
  • 宝塔安装配置FRP
  • 【第七篇】 SpringBoot项目的热部署
  • 基于SpringBoot解决RabbitMQ消息丢失问题
  • 嵌入:AI 的翻译器
  • 怎样做diy家具网站/百度分析
  • 美女直接做的网站/百度竞价排名什么意思
  • 网站制作的软件/搜索关键词排名查询
  • 网站建设正文字体多大合适/seo查询排名软件
  • 深圳有做网站最近价格/免费的网站申请
  • 网站怎么做图片动态图片不显示/优化大师网页版