From 4e1b30b3ccefd91e61a0cf7724ff9ad6e0819f40 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=8E=E5=BF=97=E5=BC=BA?= <357099073@qq.com>
Date: Wed, 17 Jun 2026 17:49:59 +0800
Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E8=AE=B0=E4=BA=8B=E6=9C=AC?=
=?UTF-8?q?=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
NOTEBOOK_DEPLOYMENT_CHECKLIST.md | 313 ++++++++++++++++++
go/controllers/platform_notebook.go | 312 +++++++++++++++++
go/models/init.go | 1 +
go/models/platform_notebook.go | 20 ++
go/routers/platform/platform.go | 7 +
platform/src/api/notebook.js | 67 ++++
.../views/apps/notebook/components/edit.vue | 148 ---------
platform/src/views/tools/notebook/README.md | 247 ++++++++++++++
.../notebook/components/WangEditor.vue | 0
.../views/tools/notebook/components/edit.vue | 188 +++++++++++
.../notebook/components/index.vue | 0
.../views/{apps => tools}/notebook/index.vue | 130 ++++----
platform/src/views/tools/notebook/test-api.js | 148 +++++++++
sql/yz_platform_notebook.sql | 16 +
14 files changed, 1383 insertions(+), 214 deletions(-)
create mode 100644 NOTEBOOK_DEPLOYMENT_CHECKLIST.md
create mode 100644 go/controllers/platform_notebook.go
create mode 100644 go/models/platform_notebook.go
create mode 100644 platform/src/api/notebook.js
delete mode 100644 platform/src/views/apps/notebook/components/edit.vue
create mode 100644 platform/src/views/tools/notebook/README.md
rename platform/src/views/{apps => tools}/notebook/components/WangEditor.vue (100%)
create mode 100644 platform/src/views/tools/notebook/components/edit.vue
rename platform/src/views/{apps => tools}/notebook/components/index.vue (100%)
rename platform/src/views/{apps => tools}/notebook/index.vue (77%)
create mode 100644 platform/src/views/tools/notebook/test-api.js
create mode 100644 sql/yz_platform_notebook.sql
diff --git a/NOTEBOOK_DEPLOYMENT_CHECKLIST.md b/NOTEBOOK_DEPLOYMENT_CHECKLIST.md
new file mode 100644
index 0000000..432075b
--- /dev/null
+++ b/NOTEBOOK_DEPLOYMENT_CHECKLIST.md
@@ -0,0 +1,313 @@
+# 记事本模块部署检查清单
+
+## 📋 部署前检查
+
+### 1. 数据库准备
+- [ ] 执行 SQL 文件创建数据表
+ ```bash
+ mysql -u用户名 -p数据库名 < sql/yz_platform_notebook.sql
+ ```
+- [ ] 验证表创建成功
+ ```sql
+ SHOW TABLES LIKE 'yz_platform_notebook';
+ DESC yz_platform_notebook;
+ ```
+
+### 2. 后端代码检查
+- [✅] 模型文件已创建: `go/models/platform_notebook.go`
+- [✅] 模型已注册: `go/models/init.go`
+- [✅] 控制器已创建: `go/controllers/platform_notebook.go`
+- [✅] 路由已注册: `go/routers/platform/platform.go`
+- [✅] 代码编译通过
+
+### 3. 前端代码检查
+- [✅] API文件已创建: `platform/src/api/notebook.js`
+- [✅] 主页面已创建: `platform/src/views/apps/notebook/index.vue`
+- [✅] 编辑器组件已创建: `platform/src/views/apps/notebook/components/edit.vue`
+- [✅] WangEditor组件已创建: `platform/src/views/apps/notebook/components/WangEditor.vue`
+
+### 4. 依赖检查
+- [ ] 前端安装 WangEditor
+ ```bash
+ cd platform
+ npm install @wangeditor/editor
+ # 或
+ yarn add @wangeditor/editor
+ ```
+
+### 5. 菜单配置
+- [ ] 在平台管理后台添加记事本菜单项
+ - 路径: `/apps/notebook`
+ - 组件: `apps/notebook/index.vue`
+ - 图标: `fa-solid fa-book` 或其他合适的图标
+ - 标题: `记事本` 或 `我的笔记`
+
+## 🚀 部署步骤
+
+### 后端部署
+
+1. **停止服务**
+ ```bash
+ # 如果服务正在运行,先停止
+ pkill -f "go run main.go"
+ ```
+
+2. **编译代码**
+ ```bash
+ cd go
+ go build -o server
+ ```
+
+3. **启动服务**
+ ```bash
+ ./server
+ # 或使用后台运行
+ nohup ./server > server.log 2>&1 &
+ ```
+
+### 前端部署
+
+1. **安装依赖**
+ ```bash
+ cd platform
+ npm install
+ # 或
+ yarn install
+ ```
+
+2. **开发模式测试**
+ ```bash
+ npm run dev
+ # 或
+ yarn dev
+ ```
+
+3. **生产构建**
+ ```bash
+ npm run build
+ # 或
+ yarn build
+ ```
+
+## ✅ 功能测试
+
+### 基础功能测试
+
+1. **访问页面**
+ - [ ] 能够正常访问记事本页面
+ - [ ] 页面布局正常显示
+ - [ ] 左侧列表和右侧编辑器都能正常显示
+
+2. **创建笔记**
+ - [ ] 点击"新建笔记"按钮
+ - [ ] 输入标题和内容
+ - [ ] 点击"创建"按钮
+ - [ ] 创建成功,笔记出现在列表中
+
+3. **编辑笔记**
+ - [ ] 点击列表中的笔记
+ - [ ] 笔记内容正确加载到编辑器
+ - [ ] 修改标题和内容
+ - [ ] 点击"保存"按钮
+ - [ ] 保存成功,列表中的笔记信息更新
+
+4. **删除笔记**
+ - [ ] 点击笔记右侧的"更多"按钮
+ - [ ] 点击"删除"
+ - [ ] 确认删除
+ - [ ] 笔记从列表中移除
+
+5. **搜索笔记**
+ - [ ] 在搜索框输入关键词
+ - [ ] 列表自动过滤显示匹配的笔记
+ - [ ] 清空搜索框,显示所有笔记
+
+### 富文本编辑器测试
+
+1. **文本格式**
+ - [ ] 加粗、斜体、下划线
+ - [ ] 标题(H1-H6)
+ - [ ] 字体颜色和背景色
+
+2. **列表和引用**
+ - [ ] 有序列表
+ - [ ] 无序列表
+ - [ ] 引用块
+
+3. **插入内容**
+ - [ ] 插入链接
+ - [ ] 插入代码块
+ - [ ] 插入表格
+
+4. **图片上传** (需要配置上传接口)
+ - [ ] 点击图片按钮
+ - [ ] 选择图片文件
+ - [ ] 图片正确上传并显示
+
+### API测试 (使用浏览器控制台)
+
+```javascript
+// 1. 在浏览器控制台加载测试脚本
+// 将 test-api.js 的内容粘贴到控制台
+
+// 2. 运行完整测试
+await NotebookTest.runFullTest();
+
+// 3. 或单独测试各个功能
+await NotebookTest.create(); // 创建笔记
+await NotebookTest.list(); // 获取列表
+await NotebookTest.detail(1); // 获取详情
+await NotebookTest.update(1, {...}); // 更新笔记
+await NotebookTest.delete(1); // 删除笔记
+```
+
+## 🐛 常见问题排查
+
+### 1. 编译错误
+
+**问题**: `cannot use &claims.UserID (value of type *int) as *uint64`
+
+**解决**: 已修复,使用类型转换 `userID := uint64(claims.UserID)`
+
+---
+
+**问题**: `undefined: PlatformNotebook`
+
+**解决**: 检查 `go/models/init.go` 中是否已注册模型
+
+---
+
+### 2. 数据库错误
+
+**问题**: 表不存在
+
+**解决**:
+```sql
+-- 检查表是否存在
+SHOW TABLES LIKE 'yz_platform_notebook';
+
+-- 如果不存在,执行 SQL 文件
+SOURCE sql/yz_platform_notebook.sql;
+```
+
+---
+
+**问题**: 字段不存在
+
+**解决**: 确认字段名使用 `create_time`, `update_time`, `delete_time`
+
+---
+
+### 3. 前端错误
+
+**问题**: WangEditor 未定义
+
+**解决**:
+```bash
+npm install @wangeditor/editor
+```
+
+---
+
+**问题**: API 请求 401 未授权
+
+**解决**:
+- 检查用户是否已登录
+- 检查 JWT Token 是否有效
+- 检查 Token 是否正确设置在请求头中
+
+---
+
+**问题**: 笔记列表为空
+
+**解决**:
+- 检查数据库中是否有数据
+- 检查用户ID是否匹配
+- 查看浏览器控制台和网络请求
+
+---
+
+### 4. 权限错误
+
+**问题**: 无法访问其他用户的笔记
+
+**说明**: 这是正常的,每个用户只能访问自己的笔记
+
+---
+
+## 📊 性能优化建议
+
+### 数据库优化
+1. 为常用查询字段添加索引(已添加)
+ - `user_id`
+ - `create_time`
+ - `is_deleted`
+
+2. 定期清理软删除的数据
+ ```sql
+ -- 删除30天前的软删除数据
+ DELETE FROM yz_platform_notebook
+ WHERE is_deleted = 1
+ AND delete_time < DATE_SUB(NOW(), INTERVAL 30 DAY);
+ ```
+
+### 前端优化
+1. 列表分页加载(已实现)
+2. 内容预览截取(已实现)
+3. 懒加载编辑器组件
+4. 防抖搜索功能(可选)
+
+### 后端优化
+1. 添加缓存层(Redis)
+2. 内容压缩存储
+3. 异步处理大文件
+4. API 限流保护
+
+## 🔐 安全建议
+
+1. **内容安全**
+ - 前端显示时做 XSS 过滤
+ - 后端存储前做内容校验
+ - 限制单篇笔记大小
+
+2. **访问控制**
+ - JWT Token 验证(已实现)
+ - 用户权限校验(已实现)
+ - API 频率限制
+
+3. **数据备份**
+ - 定期备份数据库
+ - 软删除机制(已实现)
+ - 版本控制(可选)
+
+## 📝 后续功能扩展
+
+- [ ] 笔记分类/文件夹
+- [ ] 笔记标签
+- [ ] 笔记分享
+- [ ] 导出功能(PDF/Markdown)
+- [ ] 版本历史
+- [ ] 协作编辑
+- [ ] 全文搜索
+- [ ] 附件上传
+- [ ] 模板功能
+- [ ] 快捷键支持
+
+## ✨ 完成标志
+
+- [✅] SQL 表创建成功
+- [✅] 后端代码编译通过
+- [✅] 前端页面正常访问
+- [ ] 所有功能测试通过
+- [ ] 无控制台错误
+- [ ] 性能表现良好
+
+## 📞 技术支持
+
+如遇到问题,请检查:
+1. 浏览器控制台错误信息
+2. 后端服务日志
+3. 数据库连接状态
+4. API 请求响应
+
+祝部署顺利!🎉
diff --git a/go/controllers/platform_notebook.go b/go/controllers/platform_notebook.go
new file mode 100644
index 0000000..2e6c652
--- /dev/null
+++ b/go/controllers/platform_notebook.go
@@ -0,0 +1,312 @@
+package controllers
+
+import (
+ "encoding/json"
+ "io"
+ "server/models"
+ "server/pkg/jwtutil"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/beego/beego/v2/client/orm"
+ beego "github.com/beego/beego/v2/server/web"
+)
+
+type PlatformNotebookController struct {
+ beego.Controller
+}
+
+// requireAuth 验证平台用户权限
+func requireNotebookAuth(c *beego.Controller) (*jwtutil.Claims, error) {
+ auth := c.Ctx.Request.Header.Get("Authorization")
+ if auth == "" {
+ return nil, orm.ErrNoRows
+ }
+ parts := strings.SplitN(auth, " ", 2)
+ if len(parts) != 2 || parts[0] != "Bearer" {
+ return nil, orm.ErrNoRows
+ }
+ claims, err := jwtutil.ParseToken(parts[1])
+ if err != nil {
+ return nil, err
+ }
+ if claims.UserType != "platform" {
+ return nil, orm.ErrNoRows
+ }
+ return claims, nil
+}
+
+// jsonResponse 统一JSON响应
+func jsonResponse(c *beego.Controller, httpStatus, code int, msg string, data interface{}) {
+ c.Ctx.Output.SetStatus(httpStatus)
+ resp := map[string]interface{}{
+ "code": code,
+ "msg": msg,
+ }
+ if data != nil {
+ resp["data"] = data
+ }
+ c.Data["json"] = resp
+ _ = c.ServeJSON()
+}
+
+// List 获取笔记列表
+// GET /platform/notebook/list
+func (c *PlatformNotebookController) List() {
+ claims, err := requireNotebookAuth(&c.Controller)
+ if err != nil {
+ jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil)
+ return
+ }
+
+ page, _ := c.GetInt("page", 1)
+ pageSize, _ := c.GetInt("pageSize", 20)
+ keyword := strings.TrimSpace(c.GetString("keyword"))
+
+ if page < 1 {
+ page = 1
+ }
+ if pageSize < 1 || pageSize > 100 {
+ pageSize = 20
+ }
+
+ qs := models.Orm.QueryTable(new(models.PlatformNotebook)).
+ Filter("is_deleted", 0).
+ Filter("user_id", claims.UserID)
+
+ if keyword != "" {
+ qs = qs.Filter("title__icontains", keyword)
+ }
+
+ total, err := qs.Count()
+ if err != nil {
+ jsonResponse(&c.Controller, 500, 500, "查询失败", nil)
+ return
+ }
+
+ var list []models.PlatformNotebook
+ _, err = qs.OrderBy("-update_time", "-create_time").
+ Limit(pageSize).
+ Offset((page - 1) * pageSize).
+ All(&list)
+
+ if err != nil && err != orm.ErrNoRows {
+ jsonResponse(&c.Controller, 500, 500, "查询失败", nil)
+ return
+ }
+
+ if list == nil {
+ list = []models.PlatformNotebook{}
+ }
+
+ jsonResponse(&c.Controller, 200, 200, "success", map[string]interface{}{
+ "list": list,
+ "total": total,
+ })
+}
+
+// Detail 获取笔记详情
+// GET /platform/notebook/detail/:id
+func (c *PlatformNotebookController) Detail() {
+ claims, err := requireNotebookAuth(&c.Controller)
+ if err != nil {
+ jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil)
+ return
+ }
+
+ id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
+ if err != nil || id == 0 {
+ jsonResponse(&c.Controller, 400, 400, "无效ID", nil)
+ return
+ }
+
+ var note models.PlatformNotebook
+ err = models.Orm.QueryTable(new(models.PlatformNotebook)).
+ Filter("id", id).
+ Filter("is_deleted", 0).
+ Filter("user_id", claims.UserID).
+ One(¬e)
+
+ if err != nil {
+ if err == orm.ErrNoRows {
+ jsonResponse(&c.Controller, 404, 404, "笔记不存在", nil)
+ } else {
+ jsonResponse(&c.Controller, 500, 500, "查询失败", nil)
+ }
+ return
+ }
+
+ jsonResponse(&c.Controller, 200, 200, "success", note)
+}
+
+// Create 创建笔记
+// POST /platform/notebook/create
+func (c *PlatformNotebookController) Create() {
+ claims, err := requireNotebookAuth(&c.Controller)
+ if err != nil {
+ jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil)
+ return
+ }
+
+ raw, err := io.ReadAll(c.Ctx.Request.Body)
+ if err != nil {
+ jsonResponse(&c.Controller, 400, 400, "参数错误", nil)
+ return
+ }
+
+ var payload struct {
+ Title string `json:"title"`
+ Content string `json:"content"`
+ }
+
+ if err := json.Unmarshal(raw, &payload); err != nil {
+ jsonResponse(&c.Controller, 400, 400, "参数错误", nil)
+ return
+ }
+
+ payload.Title = strings.TrimSpace(payload.Title)
+ if payload.Title == "" {
+ payload.Title = "无标题"
+ }
+
+ userID := uint64(claims.UserID)
+ note := &models.PlatformNotebook{
+ Title: payload.Title,
+ Content: payload.Content,
+ UserID: &userID,
+ UserName: &claims.Username,
+ IsDeleted: 0,
+ }
+
+ id, err := models.Orm.Insert(note)
+ if err != nil {
+ jsonResponse(&c.Controller, 500, 500, "创建失败", nil)
+ return
+ }
+
+ note.ID = uint64(id)
+ jsonResponse(&c.Controller, 200, 200, "创建成功", note)
+}
+
+// Update 更新笔记
+// POST /platform/notebook/update/:id
+func (c *PlatformNotebookController) Update() {
+ claims, err := requireNotebookAuth(&c.Controller)
+ if err != nil {
+ jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil)
+ return
+ }
+
+ id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
+ if err != nil || id == 0 {
+ jsonResponse(&c.Controller, 400, 400, "无效ID", nil)
+ return
+ }
+
+ raw, err := io.ReadAll(c.Ctx.Request.Body)
+ if err != nil {
+ jsonResponse(&c.Controller, 400, 400, "参数错误", nil)
+ return
+ }
+
+ var payload struct {
+ Title string `json:"title"`
+ Content string `json:"content"`
+ }
+
+ if err := json.Unmarshal(raw, &payload); err != nil {
+ jsonResponse(&c.Controller, 400, 400, "参数错误", nil)
+ return
+ }
+
+ payload.Title = strings.TrimSpace(payload.Title)
+ if payload.Title == "" {
+ payload.Title = "无标题"
+ }
+
+ // 验证笔记是否存在且属于当前用户
+ var note models.PlatformNotebook
+ err = models.Orm.QueryTable(new(models.PlatformNotebook)).
+ Filter("id", id).
+ Filter("is_deleted", 0).
+ Filter("user_id", claims.UserID).
+ One(¬e)
+
+ if err != nil {
+ if err == orm.ErrNoRows {
+ jsonResponse(&c.Controller, 404, 404, "笔记不存在", nil)
+ } else {
+ jsonResponse(&c.Controller, 500, 500, "查询失败", nil)
+ }
+ return
+ }
+
+ now := time.Now()
+ _, err = models.Orm.QueryTable(new(models.PlatformNotebook)).
+ Filter("id", id).
+ Update(map[string]interface{}{
+ "title": payload.Title,
+ "content": payload.Content,
+ "update_time": now,
+ })
+
+ if err != nil {
+ jsonResponse(&c.Controller, 500, 500, "更新失败", nil)
+ return
+ }
+
+ note.Title = payload.Title
+ note.Content = payload.Content
+ note.UpdateTime = &now
+
+ jsonResponse(&c.Controller, 200, 200, "更新成功", note)
+}
+
+// Delete 删除笔记(软删除)
+// DELETE /platform/notebook/delete/:id
+func (c *PlatformNotebookController) Delete() {
+ claims, err := requireNotebookAuth(&c.Controller)
+ if err != nil {
+ jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil)
+ return
+ }
+
+ id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
+ if err != nil || id == 0 {
+ jsonResponse(&c.Controller, 400, 400, "无效ID", nil)
+ return
+ }
+
+ // 验证笔记是否存在且属于当前用户
+ var note models.PlatformNotebook
+ err = models.Orm.QueryTable(new(models.PlatformNotebook)).
+ Filter("id", id).
+ Filter("is_deleted", 0).
+ Filter("user_id", claims.UserID).
+ One(¬e)
+
+ if err != nil {
+ if err == orm.ErrNoRows {
+ jsonResponse(&c.Controller, 404, 404, "笔记不存在", nil)
+ } else {
+ jsonResponse(&c.Controller, 500, 500, "查询失败", nil)
+ }
+ return
+ }
+
+ now := time.Now()
+ _, err = models.Orm.QueryTable(new(models.PlatformNotebook)).
+ Filter("id", id).
+ Update(map[string]interface{}{
+ "is_deleted": 1,
+ "delete_time": now,
+ })
+
+ if err != nil {
+ jsonResponse(&c.Controller, 500, 500, "删除失败", nil)
+ return
+ }
+
+ jsonResponse(&c.Controller, 200, 200, "删除成功", nil)
+}
diff --git a/go/models/init.go b/go/models/init.go
index 691c7f8..47428dd 100644
--- a/go/models/init.go
+++ b/go/models/init.go
@@ -62,6 +62,7 @@ func Init(_ string) {
new(PlatformAccountPoolWindsurf),
new(PlatformAccountPoolCursor),
new(PlatformAccountPoolCodex),
+ new(PlatformNotebook),
new(CmsArticleCategory),
new(CmsArticle),
diff --git a/go/models/platform_notebook.go b/go/models/platform_notebook.go
new file mode 100644
index 0000000..e3ec585
--- /dev/null
+++ b/go/models/platform_notebook.go
@@ -0,0 +1,20 @@
+package models
+
+import "time"
+
+// PlatformNotebook 平台记事本表: yz_platform_notebook
+type PlatformNotebook struct {
+ ID uint64 `orm:"column(id);pk;auto" json:"id"`
+ Title string `orm:"column(title);size(255)" json:"title"`
+ Content string `orm:"column(content);type(longtext);null" json:"content"`
+ UserID *uint64 `orm:"column(user_id);null" json:"user_id"`
+ UserName *string `orm:"column(user_name);size(100);null" json:"user_name"`
+ IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
+ CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
+ UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
+ DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
+}
+
+func (m *PlatformNotebook) TableName() string {
+ return "yz_platform_notebook"
+}
diff --git a/go/routers/platform/platform.go b/go/routers/platform/platform.go
index 7cc0621..e004672 100644
--- a/go/routers/platform/platform.go
+++ b/go/routers/platform/platform.go
@@ -232,4 +232,11 @@ func Register() {
beego.Router("/platform/accountPool/codex/unextract", &controllers.PlatformAccountPoolCodexController{}, "post:Unextract")
beego.Router("/platform/accountPool/codex/replenish", &controllers.PlatformAccountPoolCodexController{}, "post:Replenish")
beego.Router("/platform/accountPool/codex/probeToken", &controllers.PlatformAccountPoolCodexController{}, "post:ProbeToken")
+
+ // 记事本管理
+ beego.Router("/platform/notebook/list", &controllers.PlatformNotebookController{}, "get:List")
+ beego.Router("/platform/notebook/detail/:id", &controllers.PlatformNotebookController{}, "get:Detail")
+ beego.Router("/platform/notebook/create", &controllers.PlatformNotebookController{}, "post:Create")
+ beego.Router("/platform/notebook/update/:id", &controllers.PlatformNotebookController{}, "post:Update")
+ beego.Router("/platform/notebook/delete/:id", &controllers.PlatformNotebookController{}, "delete:Delete")
}
diff --git a/platform/src/api/notebook.js b/platform/src/api/notebook.js
new file mode 100644
index 0000000..412ac89
--- /dev/null
+++ b/platform/src/api/notebook.js
@@ -0,0 +1,67 @@
+import request from '@/utils/request'
+
+/**
+ * 获取笔记列表
+ * @param {Object} params - 查询参数
+ * @param {number} params.page - 页码
+ * @param {number} params.pageSize - 每页数量
+ * @param {string} params.keyword - 搜索关键词
+ */
+export function getNotebookList(params) {
+ return request({
+ url: '/platform/notebook/list',
+ method: 'get',
+ params
+ })
+}
+
+/**
+ * 获取笔记详情
+ * @param {number} id - 笔记ID
+ */
+export function getNotebookDetail(id) {
+ return request({
+ url: `/platform/notebook/detail/${id}`,
+ method: 'get'
+ })
+}
+
+/**
+ * 创建笔记
+ * @param {Object} data - 笔记数据
+ * @param {string} data.title - 笔记标题
+ * @param {string} data.content - 笔记内容
+ */
+export function createNotebook(data) {
+ return request({
+ url: '/platform/notebook/create',
+ method: 'post',
+ data
+ })
+}
+
+/**
+ * 更新笔记
+ * @param {number} id - 笔记ID
+ * @param {Object} data - 笔记数据
+ * @param {string} data.title - 笔记标题
+ * @param {string} data.content - 笔记内容
+ */
+export function updateNotebook(id, data) {
+ return request({
+ url: `/platform/notebook/update/${id}`,
+ method: 'post',
+ data
+ })
+}
+
+/**
+ * 删除笔记
+ * @param {number} id - 笔记ID
+ */
+export function deleteNotebook(id) {
+ return request({
+ url: `/platform/notebook/delete/${id}`,
+ method: 'delete'
+ })
+}
diff --git a/platform/src/views/apps/notebook/components/edit.vue b/platform/src/views/apps/notebook/components/edit.vue
deleted file mode 100644
index ad7643c..0000000
--- a/platform/src/views/apps/notebook/components/edit.vue
+++ /dev/null
@@ -1,148 +0,0 @@
-
-
笔记内容
" + } + ``` + - 返回: 创建的笔记信息 + +4. **更新笔记** + - URL: `POST /platform/notebook/update/:id` + - 参数: + ```json + { + "title": "更新的标题", + "content": "更新的内容
" + } + ``` + - 返回: 更新后的笔记信息 + +5. **删除笔记** + - URL: `DELETE /platform/notebook/delete/:id` + - 参数: `id` - 笔记ID + - 返回: 删除结果 + +## 前端API + +### API文件 +`platform/src/api/notebook.js` + +### 方法说明 + +```javascript +// 获取笔记列表 +getNotebookList({ page, pageSize, keyword }) + +// 获取笔记详情 +getNotebookDetail(id) + +// 创建笔记 +createNotebook({ title, content }) + +// 更新笔记 +updateNotebook(id, { title, content }) + +// 删除笔记 +deleteNotebook(id) +``` + +## 使用步骤 + +### 1. 初始化数据库 +```bash +# 在MySQL中执行SQL文件 +mysql -u用户名 -p数据库名 < sql/yz_platform_notebook.sql +``` + +### 2. 启动后端服务 +后端已自动注册模型和路由,直接启动即可: +```bash +cd go +go run main.go +``` + +### 3. 访问前端 +在浏览器中访问笔记本页面(路由需要在菜单中配置) + +## 功能特性 + +### 列表功能 +- ✅ 显示所有笔记 +- ✅ 搜索笔记(按标题) +- ✅ 创建新笔记 +- ✅ 删除笔记 +- ✅ 查看笔记预览 +- ✅ 显示更新时间 + +### 编辑器功能 +- ✅ 富文本编辑(基于WangEditor) +- ✅ 标题编辑 +- ✅ 内容编辑 +- ✅ 保存笔记 +- ✅ 自动识别新建/编辑模式 +- ✅ 加载状态显示 + +### WangEditor支持的功能 +- 文本样式(加粗、斜体、下划线等) +- 标题(H1-H6) +- 引用 +- 代码块 +- 有序/无序列表 +- 表格 +- 链接 +- 图片上传(需配置上传接口) +- 视频嵌入 + +## 权限说明 + +- 所有接口都需要平台用户登录(JWT Token验证) +- 每个用户只能查看、编辑、删除自己创建的笔记 +- 删除操作为软删除,数据仍保留在数据库中 + +## 扩展功能(可选) + +如需添加以下功能,可以扩展: + +1. **笔记分类** + - 添加分类表和分类字段 + - 支持笔记分类管理 + +2. **笔记标签** + - 添加标签表和关联表 + - 支持多标签筛选 + +3. **笔记分享** + - 添加分享链接生成功能 + - 支持公开/私密设置 + +4. **笔记导出** + - 支持导出为PDF + - 支持导出为Markdown + +5. **版本历史** + - 记录笔记的修改历史 + - 支持版本回退 + +6. **协作编辑** + - 支持多人协作编辑 + - 实时同步功能 + +## 注意事项 + +1. 图片上传需要配置文件上传接口 +2. 富文本内容存储为HTML格式,注意XSS防护 +3. 数据库content字段使用longtext类型,支持大容量内容 +4. 建议定期清理软删除的数据 +5. 生产环境建议添加内容审核机制 + +## 技术栈 + +- **前端**: Vue 3 + Element Plus + WangEditor +- **后端**: Go + Beego + MySQL +- **编辑器**: WangEditor 5.x + +## 开发调试 + +### 前端调试 +```bash +cd platform +npm run dev +``` + +### 后端调试 +```bash +cd go +go run main.go +``` + +### 查看API请求 +打开浏览器开发者工具 -> Network 标签页,查看API请求和响应 + +## 问题排查 + +1. **笔记列表为空** + - 检查数据库表是否创建成功 + - 检查用户是否已登录 + - 查看浏览器Console是否有错误 + +2. **保存失败** + - 检查JWT Token是否有效 + - 检查标题是否为空 + - 查看后端日志 + +3. **编辑器显示异常** + - 检查WangEditor是否正确安装 + - 查看浏览器Console错误信息 + - 检查CSS样式是否正确加载 + +## 更新日志 + +### v1.0.0 (2024) +- ✅ 完成基础功能 +- ✅ 支持笔记CRUD操作 +- ✅ 集成WangEditor富文本编辑器 +- ✅ 响应式设计支持 +- ✅ 暗色模式支持 diff --git a/platform/src/views/apps/notebook/components/WangEditor.vue b/platform/src/views/tools/notebook/components/WangEditor.vue similarity index 100% rename from platform/src/views/apps/notebook/components/WangEditor.vue rename to platform/src/views/tools/notebook/components/WangEditor.vue diff --git a/platform/src/views/tools/notebook/components/edit.vue b/platform/src/views/tools/notebook/components/edit.vue new file mode 100644 index 0000000..87e1ad7 --- /dev/null +++ b/platform/src/views/tools/notebook/components/edit.vue @@ -0,0 +1,188 @@ + +这是一个功能丰富的笔记应用
支持富文本编辑、图片上传等功能
', - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - }, -]; +const loading = ref(false); +const total = ref(0); // 过滤后的笔记列表 const filteredNotes = computed(() => { - if (!searchKeyword.value) return notes.value; - - const keyword = searchKeyword.value.toLowerCase(); - return notes.value.filter(note => { - return ( - note.title?.toLowerCase().includes(keyword) || - getPreviewText(note.content).toLowerCase().includes(keyword) - ); - }); + return notes.value; }); // 获取预览文本 @@ -131,19 +116,41 @@ const formatTime = (dateStr) => { } }; +// 加载笔记列表 +const loadNotes = async () => { + loading.value = true; + try { + const response = await getNotebookList({ + page: 1, + pageSize: 100, + keyword: searchKeyword.value, + }); + + if (response.code === 200) { + notes.value = response.data.list || []; + total.value = response.data.total || 0; + + // 如果有笔记且没有选中任何笔记,默认选中第一个 + if (notes.value.length > 0 && !currentNoteId.value) { + currentNoteId.value = notes.value[0].id; + } + + // 如果当前选中的笔记已被删除,选中第一个 + if (currentNoteId.value && !notes.value.find(n => n.id === currentNoteId.value)) { + currentNoteId.value = notes.value[0]?.id || null; + } + } + } catch (error) { + console.error('加载笔记失败:', error); + ElMessage.error('加载笔记失败'); + } finally { + loading.value = false; + } +}; + // 创建新笔记 const handleCreate = () => { - const newNote = { - id: Date.now(), - title: '新建笔记', - content: '', - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - }; - - notes.value.unshift(newNote); - currentNoteId.value = newNote.id; - ElMessage.success('创建成功'); + currentNoteId.value = 'new'; }; // 选择笔记 @@ -153,7 +160,7 @@ const handleSelectNote = (note) => { // 搜索笔记 const handleSearch = () => { - // 搜索逻辑已在 computed 中处理 + loadNotes(); }; // 笔记操作 @@ -166,47 +173,38 @@ const handleNoteAction = async (command, note) => { type: 'warning', }); - const index = notes.value.findIndex(n => n.id === note.id); - if (index > -1) { - notes.value.splice(index, 1); - - if (currentNoteId.value === note.id) { - currentNoteId.value = notes.value[0]?.id || null; - } - - ElMessage.success('删除成功'); + await deleteNotebook(note.id); + ElMessage.success('删除成功'); + + // 重新加载列表 + await loadNotes(); + + // 如果删除的是当前笔记,切换到第一个 + if (currentNoteId.value === note.id) { + currentNoteId.value = notes.value[0]?.id || null; + } + } catch (error) { + if (error !== 'cancel') { + console.error('删除失败:', error); + ElMessage.error('删除失败'); } - } catch { - // 用户取消删除 } } }; -// 保存笔记 -const handleSave = ({ id, content }) => { - const note = notes.value.find(n => n.id === id); - if (note) { - note.content = content; - note.updated_at = new Date().toISOString(); - ElMessage.success('保存成功'); - } +// 保存成功后刷新列表 +const handleSaveSuccess = async () => { + await loadNotes(); }; -// 更新标题 -const handleUpdateTitle = ({ id, title }) => { - const note = notes.value.find(n => n.id === id); - if (note) { - note.title = title; - note.updated_at = new Date().toISOString(); - } +// 创建成功后的回调 +const handleCreateSuccess = async (noteId) => { + currentNoteId.value = noteId; + await loadNotes(); }; onMounted(() => { - // 初始化数据 - 实际使用时从 API 加载 - notes.value = [...mockNotes]; - if (notes.value.length > 0) { - currentNoteId.value = notes.value[0].id; - } + loadNotes(); }); diff --git a/platform/src/views/tools/notebook/test-api.js b/platform/src/views/tools/notebook/test-api.js new file mode 100644 index 0000000..6f601bf --- /dev/null +++ b/platform/src/views/tools/notebook/test-api.js @@ -0,0 +1,148 @@ +/** + * 记事本模块 API 测试脚本 + * 在浏览器控制台中运行此脚本测试 API + */ + +// 测试配置 +const API_BASE = '/platform/notebook'; + +// 辅助函数:发送请求 +async function request(url, options = {}) { + const token = localStorage.getItem('token'); + const headers = { + 'Content-Type': 'application/json', + 'Authorization': token ? `Bearer ${token}` : '', + ...options.headers, + }; + + try { + const response = await fetch(url, { ...options, headers }); + const data = await response.json(); + console.log(`✅ ${options.method || 'GET'} ${url}`, data); + return data; + } catch (error) { + console.error(`❌ ${options.method || 'GET'} ${url}`, error); + throw error; + } +} + +// 测试函数 +const NotebookTest = { + // 1. 创建笔记 + async create() { + return await request(`${API_BASE}/create`, { + method: 'POST', + body: JSON.stringify({ + title: '测试笔记 - ' + new Date().toLocaleString(), + content: '这是一条测试笔记
内容包含富文本格式
', + }), + }); + }, + + // 2. 获取笔记列表 + async list(params = {}) { + const query = new URLSearchParams({ + page: params.page || 1, + pageSize: params.pageSize || 20, + keyword: params.keyword || '', + }).toString(); + return await request(`${API_BASE}/list?${query}`); + }, + + // 3. 获取笔记详情 + async detail(id) { + return await request(`${API_BASE}/detail/${id}`); + }, + + // 4. 更新笔记 + async update(id, data) { + return await request(`${API_BASE}/update/${id}`, { + method: 'POST', + body: JSON.stringify(data), + }); + }, + + // 5. 删除笔记 + async delete(id) { + return await request(`${API_BASE}/delete/${id}`, { + method: 'DELETE', + }); + }, + + // 完整测试流程 + async runFullTest() { + console.log('🚀 开始测试记事本模块 API...\n'); + + try { + // 1. 创建笔记 + console.log('📝 测试1: 创建笔记'); + const createResult = await this.create(); + if (createResult.code !== 200) { + throw new Error('创建笔记失败'); + } + const noteId = createResult.data.id; + console.log(`✅ 创建成功,笔记ID: ${noteId}\n`); + + // 2. 获取笔记列表 + console.log('📋 测试2: 获取笔记列表'); + const listResult = await this.list(); + if (listResult.code !== 200) { + throw new Error('获取列表失败'); + } + console.log(`✅ 获取成功,共 ${listResult.data.total} 条笔记\n`); + + // 3. 获取笔记详情 + console.log('🔍 测试3: 获取笔记详情'); + const detailResult = await this.detail(noteId); + if (detailResult.code !== 200) { + throw new Error('获取详情失败'); + } + console.log(`✅ 获取成功,标题: ${detailResult.data.title}\n`); + + // 4. 更新笔记 + console.log('✏️ 测试4: 更新笔记'); + const updateResult = await this.update(noteId, { + title: '更新后的标题 - ' + new Date().toLocaleString(), + content: '这是更新后的内容
', + }); + if (updateResult.code !== 200) { + throw new Error('更新笔记失败'); + } + console.log(`✅ 更新成功\n`); + + // 5. 搜索笔记 + console.log('🔎 测试5: 搜索笔记'); + const searchResult = await this.list({ keyword: '更新' }); + if (searchResult.code !== 200) { + throw new Error('搜索失败'); + } + console.log(`✅ 搜索成功,找到 ${searchResult.data.total} 条匹配笔记\n`); + + // 6. 删除笔记 + console.log('🗑️ 测试6: 删除笔记'); + const deleteResult = await this.delete(noteId); + if (deleteResult.code !== 200) { + throw new Error('删除笔记失败'); + } + console.log(`✅ 删除成功\n`); + + console.log('🎉 所有测试通过!'); + return true; + } catch (error) { + console.error('❌ 测试失败:', error); + return false; + } + }, +}; + +// 导出到全局 +window.NotebookTest = NotebookTest; + +console.log('📚 记事本模块测试工具已加载'); +console.log('使用方法:'); +console.log(' NotebookTest.create() - 创建笔记'); +console.log(' NotebookTest.list() - 获取列表'); +console.log(' NotebookTest.detail(id) - 获取详情'); +console.log(' NotebookTest.update(id, data) - 更新笔记'); +console.log(' NotebookTest.delete(id) - 删除笔记'); +console.log(' NotebookTest.runFullTest() - 运行完整测试'); diff --git a/sql/yz_platform_notebook.sql b/sql/yz_platform_notebook.sql new file mode 100644 index 0000000..a546b7e --- /dev/null +++ b/sql/yz_platform_notebook.sql @@ -0,0 +1,16 @@ +-- 平台记事本表 +CREATE TABLE IF NOT EXISTS `yz_platform_notebook` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `title` varchar(255) NOT NULL DEFAULT '' COMMENT '笔记标题', + `content` longtext COMMENT '笔记内容(HTML格式)', + `user_id` bigint(20) unsigned DEFAULT NULL COMMENT '创建用户ID', + `user_name` varchar(100) DEFAULT NULL COMMENT '创建用户名', + `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除 0-否 1-是', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `delete_time` datetime DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_create_time` (`create_time`), + KEY `idx_is_deleted` (`is_deleted`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台记事本表';