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 @@ - - - - - diff --git a/platform/src/views/tools/notebook/README.md b/platform/src/views/tools/notebook/README.md new file mode 100644 index 0000000..e99ac66 --- /dev/null +++ b/platform/src/views/tools/notebook/README.md @@ -0,0 +1,247 @@ +# 记事本模块使用说明 + +## 功能概述 + +这是一个完整的记事本应用模块,支持富文本编辑、笔记管理等功能。 + +## 目录结构 + +``` +notebook/ +├── index.vue # 主页面(列表+编辑器) +├── components/ +│ ├── edit.vue # 编辑器组件 +│ └── WangEditor.vue # WangEditor富文本编辑器封装 +└── README.md # 说明文档 +``` + +## 数据库 + +### 表名 +`yz_platform_notebook` + +### 表结构 +- `id` - 主键ID +- `title` - 笔记标题 +- `content` - 笔记内容(HTML格式) +- `user_id` - 创建用户ID +- `user_name` - 创建用户名 +- `is_deleted` - 是否删除(0-否 1-是) +- `create_time` - 创建时间 +- `update_time` - 更新时间 +- `delete_time` - 删除时间 + +### SQL文件位置 +`sql/yz_platform_notebook.sql` + +## 后端接口 + +### 模型文件 +`go/models/platform_notebook.go` + +### 控制器文件 +`go/controllers/platform_notebook.go` + +### API端点 + +1. **获取笔记列表** + - URL: `GET /platform/notebook/list` + - 参数: + - `page`: 页码(默认1) + - `pageSize`: 每页数量(默认20,最大100) + - `keyword`: 搜索关键词(可选) + - 返回: 笔记列表和总数 + +2. **获取笔记详情** + - URL: `GET /platform/notebook/detail/:id` + - 参数: `id` - 笔记ID + - 返回: 笔记详细信息 + +3. **创建笔记** + - URL: `POST /platform/notebook/create` + - 参数: + ```json + { + "title": "笔记标题", + "content": "

笔记内容

" + } + ``` + - 返回: 创建的笔记信息 + +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 @@ + + + + + diff --git a/platform/src/views/apps/notebook/components/index.vue b/platform/src/views/tools/notebook/components/index.vue similarity index 100% rename from platform/src/views/apps/notebook/components/index.vue rename to platform/src/views/tools/notebook/components/index.vue diff --git a/platform/src/views/apps/notebook/index.vue b/platform/src/views/tools/notebook/index.vue similarity index 77% rename from platform/src/views/apps/notebook/index.vue rename to platform/src/views/tools/notebook/index.vue index 311c0ac..6252f37 100644 --- a/platform/src/views/apps/notebook/index.vue +++ b/platform/src/views/tools/notebook/index.vue @@ -40,7 +40,7 @@
{{ getPreviewText(note.content) }}
-
{{ formatTime(note.updated_at) }}
+
{{ formatTime(note.update_time || note.create_time) }}
@@ -50,9 +50,10 @@
@@ -66,33 +67,17 @@ import { ref, computed, onMounted } from 'vue'; import { ElMessage, ElMessageBox } from 'element-plus'; import { Plus, Search, Delete, MoreFilled } from '@element-plus/icons-vue'; import NoteEditor from './components/edit.vue'; +import { getNotebookList, deleteNotebook } from '@/api/notebook'; const searchKeyword = ref(''); const currentNoteId = ref(null); const notes = ref([]); - -// 模拟数据 - 实际使用时替换为 API 调用 -const mockNotes = [ - { - id: 1, - title: '欢迎使用笔记本', - content: '

这是一个功能丰富的笔记应用

支持富文本编辑、图片上传等功能

', - 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='平台记事本表';