AI书签管理工具开发全记录(十八):书签导入导出

news2025/7/29 22:15:26

文章目录

  • AI书签管理工具开发全记录(十八):书签导入导出
    • 1.前言 📝
    • 2.书签结构分析 📖
      • 3.书签示例 📑
      • 4.书签文件结构定义描述 🔣
        • 4.1. ​整体文档结构​​
        • 4.2. ​核心元素类型​​
        • 4.3. ​层级关系
        • 4.4. ​​嵌套规则​​
        • 4.5. ​​属性规范​​
        • 4.6. ​​特殊结构​​
        • 4.7. ​文档终止​​
      • 5. 导出 🚀
        • 5.1 定义导出方法
        • 5.2 前端调用
      • 6. 导入 📥
        • 6.1 添加书签处理方法
        • 6.2 添加后端处理方法

AI书签管理工具开发全记录(十八):书签导入导出

1.前言 📝

在上一篇文章中,我们完成了sun-panel同步功能的实现。本篇文章将聚焦于书签的导入导出功能。

2.书签结构分析 📖

这里我们以chrome书签格式作为参考标准。

我们可以在chrome://bookmarks/中点击导出书签
image.png

3.书签示例 📑

image.png

4.书签文件结构定义描述 🔣

4.1. ​整体文档结构​​
  • ​文档类型声明​:<!DOCTYPE NETSCAPE-Bookmark-file-1>
  • ​​元数据​​:
    • 注释说明
    • 字符集声明:<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
  • ​​标题​​:<TITLE>Bookmarks</TITLE>
  • ​主容器​:所有书签内容包裹在顶级 <DL><p> 标签中
4.2. ​核心元素类型​​
  • ​文件夹 (Folder)​​:

    • 定义标签:<DT><H3>
    • 关键属性:
      • ADD_DATE:创建时间戳(Unix时间戳)
      • LAST_MODIFIED:最后修改时间戳(可选)
      • PERSONAL_TOOLBAR_FOLDER:标记是否为书签栏(布尔值,可选)
    • ​子容器​​:后跟 <DL><p> 包裹下级内容
  • ​书签项 (Bookmark)​​:

    • 定义标签:<DT><A>
    • 关键属性:
      • HREF:URL地址(必需)
      • ADD_DATE:添加时间戳(Unix时间戳,必需)
4.3. ​层级关系
<DL><p> <!-- 容器开始 -->
  <DT><H3 属性>文件夹名称</H3> <!-- 文件夹定义 -->
  <DL><p> <!-- 文件夹子容器开始 -->
    <DT><A 属性>书签名</A> <!-- 书签项 -->
    <DT><H3 属性>子文件夹</H3> <!-- 嵌套文件夹 -->
    <DL><p>...</DL><p> <!-- 嵌套文件夹子容器 -->
  </DL><p> <!-- 文件夹子容器结束 -->
</DL><p> <!-- 容器结束 -->
4.4. ​​嵌套规则​​
  • 任意层级可包含混合内容(文件夹 + 书签项)
  • 文件夹必须包含子容器 <DL><p>(即使为空)
  • 最大嵌套深度无限制(支持无限层级)
  • 同级元素按顺序排列,不依赖特殊分组标签
4.5. ​​属性规范​​
  • ​时间戳格式​​:所有时间戳均为 Unix 时间戳(10位整数)
  • ​保留属性​​:
    • 文件夹:ADD_DATE, LAST_MODIFIED, PERSONAL_TOOLBAR_FOLDER
    • 书签项:HREF, ADD_DATE
4.6. ​​特殊结构​​
  • ​空文件夹​​:包含完整的 <DT><H3> 和空 <DL><p> 标签对
  • ​根级混合内容​​:顶级容器可直接包含书签项(不强制要求文件夹包裹)
  • ​文本节点​​:仅作为元素内容(如书签名/文件夹名),无独立标签
4.7. ​文档终止​​
  • 以闭合标签 </DL><p> 结束所有层级
  • 无额外文档结构结束标签(如 </HTML>

5. 导出 🚀

5.1 定义导出方法
// internal/api/api.go

// ExportBookmarks godoc
// @Summary 导出书签
// @Description 导出书签为HTML格式
// @Tags bookmarks
// @Accept json
// @Produce text/html
// @Param request body models.ExportBookmarkRequest true "导出参数"
// @Success 200 {string} string "HTML格式的书签"
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/bookmarks/export [post]
func (s *Server) exportBookmarks(c *gin.Context) {
	var req models.ExportBookmarkRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(400, gin.H{"error": "无效的请求数据"})
		return
	}

	// 构建查询
	query := s.db.Model(&models.Bookmark{}).Preload("Category")

	// 按分类过滤
	if req.CategoryID != 0 {
		query = query.Where("category_id = ?", req.CategoryID)
	}

	// 关键词搜索
	if req.Keyword != "" {
		query = query.Where("title LIKE ? OR url LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
	}

	// 获取书签
	var bookmarks []models.Bookmark
	if err := query.Find(&bookmarks).Error; err != nil {
		c.JSON(500, gin.H{"error": "获取书签失败"})
		return
	}

	// 生成Chrome书签HTML
	now := time.Now().Unix()
	html := `<!DOCTYPE NETSCAPE-Bookmark-file-1>
<!-- This is an automatically generated file.
     It will be read and overwritten.
     DO NOT EDIT! -->
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
<DL><p>
    <DT><H3>AI书签</H3>
    <DL><p>`

	// 按分类分组书签
	categoryBookmarks := make(map[string][]models.Bookmark)
	for _, b := range bookmarks {
		categoryName := b.Category.Name
		if categoryName == "" {
			categoryName = "未分类"
		}
		categoryBookmarks[categoryName] = append(categoryBookmarks[categoryName], b)
	}

	// 生成HTML
	for category, bookmarks := range categoryBookmarks {
		html += fmt.Sprintf("\n        <DT><H3>%s</H3>\n        <DL><p>", category)
		for _, b := range bookmarks {
			html += fmt.Sprintf("\n            <DT><A HREF=\"%s\" ADD_DATE=\"%d\">%s</A>", b.URL, now, b.Title)
			if b.Description != "" {
				html += fmt.Sprintf("\n            <DD>%s</DD>", b.Description)
			}
		}
		html += "\n        </DL><p>"
	}

	html += "\n    </DL><p>\n</DL><p>"

	// 设置响应头
	c.Header("Content-Type", "text/html; charset=utf-8")
	c.Header("Content-Disposition", "attachment; filename=bookmarks.html")
	c.String(200, html)
}
5.2 前端调用

添加导出按钮

 <el-button type="success" @click="handleExport">导出书签</el-button>

定义导出方法

// 导出书签
const handleExport = async () => {
  try {
    loading.value = true
    const response = await exportBookmarks({
      category_id: selectedCategory.value || undefined,
      keyword: searchKeyword.value || undefined
    })
    
    // 生成带时间戳的文件名
    const now = new Date()
    const timestamp = now.getFullYear() +
      String(now.getMonth() + 1).padStart(2, '0') +
      String(now.getDate()).padStart(2, '0') +
      String(now.getHours()).padStart(2, '0') +
      String(now.getMinutes()).padStart(2, '0')
    const filename = `bookmarks_${timestamp}.html`
    
    // 创建下载链接
    const url = window.URL.createObjectURL(new Blob([response]))
    const link = document.createElement('a')
    link.href = url
    link.download = filename
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
    window.URL.revokeObjectURL(url)
    
    ElMessage.success('书签导出成功')
  } catch (error) {
    console.error('导出书签失败:', error)
    ElMessage.error('导出书签失败')
  } finally {
    loading.value = false
  }
}

6. 导入 📥

6.1 添加书签处理方法

添加书签处理方法,处理书签节点。

// 处理文件选择
const handleFileChange = async (file) => {
  if (!file) return
  
  try {
    // 使用FileReader读取文件内容
    const content = await new Promise((resolve, reject) => {
      const reader = new FileReader()
      reader.onload = (e) => resolve(e.target.result)
      reader.onerror = (e) => reject(e)
      reader.readAsText(file.raw)
    })
    
    console.log('文件内容:', content)
    
    // 创建DOM解析器
    const parser = new DOMParser()
    const doc = parser.parseFromString(content, 'text/html')
    console.log('解析后的DOM:', doc)
    
    const bookmarks = []
    let currentCategory = '未分类'
    
    // 递归处理节点
    const processNode = (node) => {
      if (!node) return
      
      // 处理H3标签(分类)
      if (node.tagName === 'H3') {
        currentCategory = node.textContent.trim()
        console.log('找到分类:', currentCategory)
      }
      
      // 处理A标签(书签)
      if (node.tagName === 'A') {
        const bookmark = {
          title: node.textContent.trim(),
          url: node.getAttribute('href'),
          category: currentCategory,
          description: '',
          icon: node.getAttribute('icon') || ''
        }
        console.log('创建书签:', bookmark)
        bookmarks.push(bookmark)
      }
      
      // 递归处理子节点
      for (const child of node.children) {
        processNode(child)
      }
    }
    
    // 从根节点开始处理
    const rootDL = doc.querySelector('DL')
    if (rootDL) {
      processNode(rootDL)
    }
    
    console.log('解析完成,书签数量:', bookmarks.length)
    
    // 调用导入API
    const response = await importBookmarks({ bookmarks })
    console.log('导入响应:', response)
    
    if (response.stats) {
      ElMessage.success(`导入完成:成功 ${response.stats.success} 个,重复 ${response.stats.duplicate} 个,失败 ${response.stats.failed}`)
      // 刷新列表
      fetchBookmarks()
    } else {
      ElMessage.error(response.msg || '导入失败')
    }
  } catch (error) {
    console.error('解析文件失败:', error)
    ElMessage.error('解析文件失败')
  }
}

6.2 添加后端处理方法

不存在的分类则自动创建分类。

// ImportBookmarks godoc
// @Summary 导入书签
// @Description 批量导入书签
// @Tags bookmarks
// @Accept json
// @Produce json
// @Param request body models.ImportBookmarkRequest true "书签导入信息"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/bookmarks/import [post]
func (s *Server) importBookmarks(c *gin.Context) {
	var req models.ImportBookmarkRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(400, gin.H{"error": "无效的请求数据"})
		return
	}

	// 用于统计导入结果
	stats := map[string]int{
		"total":     len(req.Bookmarks),
		"success":   0,
		"duplicate": 0,
		"failed":    0,
	}

	// 获取所有现有书签用于去重
	var existingBookmarks []models.Bookmark
	if err := s.db.Find(&existingBookmarks).Error; err != nil {
		c.JSON(500, gin.H{"error": "获取现有书签失败"})
		return
	}

	// 创建URL映射用于快速查找
	urlMap := make(map[string]bool)
	for _, b := range existingBookmarks {
		urlMap[b.URL] = true
	}

	// 获取所有分类
	var categories []models.Category
	if err := s.db.Find(&categories).Error; err != nil {
		c.JSON(500, gin.H{"error": "获取分类失败"})
		return
	}

	// 创建分类名称到ID的映射
	categoryMap := make(map[string]uint)
	for _, cat := range categories {
		categoryMap[cat.Name] = cat.ID
	}

	// 处理每个书签
	for _, b := range req.Bookmarks {
		// 检查URL是否已存在
		if urlMap[b.URL] {
			stats["duplicate"]++
			continue
		}

		// 获取或创建分类
		categoryID := categoryMap[b.Category]
		if categoryID == 0 && b.Category != "" {
			// 创建新分类
			newCategory := models.Category{
				Name:        b.Category,
				Description: "从导入的书签创建",
			}
			if err := s.db.Create(&newCategory).Error; err != nil {
				stats["failed"]++
				continue
			}
			categoryID = newCategory.ID
			categoryMap[b.Category] = categoryID
		}

		// 创建书签
		bookmark := models.Bookmark{
			Title:       b.Title,
			URL:         b.URL,
			Description: b.Description,
			CategoryID:  categoryID,
		}

		if err := s.db.Create(&bookmark).Error; err != nil {
			stats["failed"]++
			continue
		}

		stats["success"]++
		urlMap[b.URL] = true
	}

	c.JSON(200, gin.H{
		"message": "导入完成",
		"stats":   stats,
	})
}

往期系列

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2406322.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

docker容器互联

1.docker可以通过网路访问 2.docker允许映射容器内应用的服务端口到本地宿主主机 3.互联机制实现多个容器间通过容器名来快速访问 一 、端口映射实现容器访问 1.从外部访问容器应用 我们先把之前的删掉吧&#xff08;如果不删的话&#xff0c;容器就提不起来&#xff0c;因…

安宝特案例丨寻医不再长途跋涉?Vuzix再次以AR技术智能驱动远程医疗

加拿大领先科技公司TeleVU基于Vuzix智能眼镜打造远程医疗生态系统&#xff0c;彻底革新患者护理模式。 安宝特合作伙伴TeleVU成立30余年&#xff0c;沉淀医疗技术、计算机科学与人工智能经验&#xff0c;聚焦医疗保健领域&#xff0c;提供AR、AI、IoT解决方案。 该方案使医疗…

Modbus转Ethernet IP深度解析:磨粉设备效率跃升的底层技术密码

在建材矿粉磨系统中&#xff0c;开疆智能Modbus转Ethernet IP网关KJ-EIP-101的应用案例是一个重要的技术革新。这个转换过程涉及到两种主要的通信协议&#xff1a;Modbus和Ethernet IP。Modbus是一种串行通信协议&#xff0c;广泛应用于工业控制系统中。它简单、易于部署和维护…

在MobaXterm 打开图形工具firefox

目录 1.安装 X 服务器软件 2.服务器端配置 3.客户端配置 4.安装并打开 Firefox 1.安装 X 服务器软件 Centos系统 # CentOS/RHEL 7 及之前&#xff08;YUM&#xff09; sudo yum install xorg-x11-server-Xorg xorg-x11-xinit xorg-x11-utils mesa-libEGL mesa-libGL mesa-…

旋量理论:刚体运动的几何描述与机器人应用

旋量理论为描述刚体在三维空间中的运动提供了强大而优雅的数学框架。与传统的欧拉角或方向余弦矩阵相比&#xff0c;旋量理论通过螺旋运动的概念统一了旋转和平移&#xff0c;在机器人学、计算机图形学和多体动力学领域具有显著优势。这种描述不仅几何直观&#xff0c;而且计算…

运动控制--BLDC电机

一、电机的分类 按照供电电源 1.直流电机 1.1 有刷直流电机(BDC) 通过电刷与换向器实现电流方向切换&#xff0c;典型应用于电动工具、玩具等 1.2 无刷直流电机&#xff08;BLDC&#xff09; 电子换向替代机械电刷&#xff0c;具有高可靠性&#xff0c;常用于无人机、高端家电…

Redis专题-实战篇一-基于Session和Redis实现登录业务

GitHub项目地址&#xff1a;https://github.com/whltaoin/redisLearningProject_hm-dianping 基于Session实现登录业务功能提交版本码&#xff1a;e34399f 基于Redis实现登录业务提交版本码&#xff1a;60bf740 一、导入黑马点评后端项目 项目架构图 1. 前期阶段2. 后续阶段导…

【前端实战】如何让用户回到上次阅读的位置?

目录 【前端实战】如何让用户回到上次阅读的位置&#xff1f; 一、总体思路 1、核心目标 2、涉及到的技术 二、实现方案详解 1、基础方法&#xff1a;监听滚动&#xff0c;记录 scrollTop&#xff08;不推荐&#xff09; 2、Intersection Observer 插入探针元素 3、基…

dvwa11——XSS(Reflected)

LOW 分析源码&#xff1a;无过滤 和上一关一样&#xff0c;这一关在输入框内输入&#xff0c;成功回显 <script>alert(relee);</script> MEDIUM 分析源码&#xff0c;是把<script>替换成了空格&#xff0c;但没有禁用大写 改大写即可&#xff0c;注意函数…

【Axure高保真原型】图片列表添加和删除图片

今天和大家分享图片列表添加和删除图片的原型模板&#xff0c;效果包括&#xff1a; 点击图片列表的加号可以显示图片选择器&#xff0c;选择里面的图片&#xff1b; 选择图片后点击添加按钮&#xff0c;可以将该图片添加到图片列表&#xff1b; 鼠标移入图片列表的图片&…

XXE漏洞知识

目录 1.XXE简介与危害 XML概念 XML与HTML的区别 1.pom.xml 主要作用 2.web.xml 3.mybatis 2.XXE概念与危害 案例&#xff1a;文件读取&#xff08;需要Apache >5.4版本&#xff09; 案例&#xff1a;内网探测&#xff08;鸡肋&#xff09; 案例&#xff1a;执行命…

mq安装新版-3.13.7的安装

一、下载包&#xff0c;上传到服务器 https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.13.7/rabbitmq-server-generic-unix-3.13.7.tar.xz 二、 erlang直接安装 rpm -ivh erlang-26.2.4-1.el8.x86_64.rpm不需要配置环境变量&#xff0c;直接就安装了。 erl…

DL00871-基于深度学习YOLOv11的盲人障碍物目标检测含完整数据集

基于深度学习YOLOv11的盲人障碍物目标检测&#xff1a;开启盲人出行新纪元 在全球范围内&#xff0c;盲人及视觉障碍者的出行问题一直是社会关注的重点。尽管技术不断进步&#xff0c;许多城市的无障碍设施依然未能满足盲人出行的实际需求。尤其是在复杂的城市环境中&#xff…

华硕电脑,全新的超频方式,无需进入BIOS

想要追求更佳性能释放 或探索更多可玩性的小伙伴&#xff0c; 可能会需要为你的电脑超频。 但我们常用的不论是BIOS里的超频&#xff0c; 还是Armoury Crate奥创智控中心超频&#xff0c; 每次调节都要重启&#xff0c;有点麻烦。 TurboV Core 全新的超频方案来了 4不规…

安全领域新突破:可视化让隐患无处遁形

在安全领域&#xff0c;隐患就像暗处的 “幽灵”&#xff0c;随时可能引发严重事故。传统安全排查手段&#xff0c;常常难以将它们一网打尽。你是否好奇&#xff0c;究竟是什么神奇力量&#xff0c;能让这些潜藏的隐患无所遁形&#xff1f;没错&#xff0c;就是可视化技术。它如…

Vue.js教学第二十一章:vue实战项目二,个人博客搭建

基于 Vue 的个人博客网站搭建 摘要: 随着前端技术的不断发展,Vue 作为一种轻量级、高效的前端框架,为个人博客网站的搭建提供了极大的便利。本文详细介绍了基于 Vue 搭建个人博客网站的全过程,包括项目背景、技术选型、项目架构设计、功能模块实现、性能优化与测试等方面。…

[KCTF]CORE CrackMe v2.0

这个Reverse比较古老&#xff0c;已经有20多年了&#xff0c;但难度确实不小。 先查壳 upx压缩壳&#xff0c;0.72&#xff0c;废弃版本&#xff0c;工具无法解压。 反正不用IDA进行调试&#xff0c;直接x32dbg中&#xff0c;dump内存&#xff0c;保存后拖入IDA。 这里说一下…

Ubuntu 安装 Mysql 数据库

首先更新apt-get工具&#xff0c;执行命令如下&#xff1a; apt-get upgrade安装Mysql&#xff0c;执行如下命令&#xff1a; apt-get install mysql-server 开启Mysql 服务&#xff0c;执行命令如下&#xff1a; service mysql start并确认是否成功开启mysql,执行命令如下&am…

Java高级 |【实验八】springboot 使用Websocket

隶属文章&#xff1a;Java高级 | &#xff08;二十二&#xff09;Java常用类库-CSDN博客 系列文章&#xff1a;Java高级 | 【实验一】Springboot安装及测试 |最新-CSDN博客 Java高级 | 【实验二】Springboot 控制器类相关注解知识-CSDN博客 Java高级 | 【实验三】Springboot 静…

设计模式-3 行为型模式

一、观察者模式 1、定义 定义对象之间的一对多的依赖关系&#xff0c;这样当一个对象改变状态时&#xff0c;它的所有依赖项都会自动得到通知和更新。 描述复杂的流程控制 描述多个类或者对象之间怎样互相协作共同完成单个对象都无法单独度完成的任务 它涉及算法与对象间职责…