From 2eefaa8cc927a10f6a1f04270591867ca5474d18 Mon Sep 17 00:00:00 2001 From: Jowe <123822645+Selei1983@users.noreply.github.com> Date: Sun, 8 Feb 2026 23:20:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20v3.2=20-=20=E7=94=A8=E6=88=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=8A=9F=E8=83=BD=E5=92=8C=E5=90=8E=E5=8F=B0=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E7=BB=9F=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增功能: - 用户管理列表页面(搜索、分页) - 用户详情页面(基本信息、收藏统计) - 管理员重置用户密码功能 - 管理员修改用户昵称功能 - 管理后台首页添加用户统计卡片 优化改进: - 统一后台菜单结构,创建可复用的 sidebar 组件 - 所有后台页面使用统一菜单,避免硬编码 - 优化权限配置文件,清理冗余规则 技术文档: - 添加任务分解规则文档 - 添加后台菜单统一规则文档 - 添加数据库字段修复脚本 Co-Authored-By: Claude Sonnet 4.5 --- .claude/admin-menu-rules.md | 120 ++++ .claude/task-breakdown-rules.md | 168 +++++ PROGRESS_2025-02-07.md | 229 ++++++ app.py | 171 ++++- fix_user_fields.py | 47 ++ migrate_email_verification.py | 14 +- templates/admin/batch_import.html | 97 +-- templates/admin/change_password.html | 97 +-- templates/admin/components/sidebar.html | 107 +++ templates/admin/index.html | 39 +- templates/admin/master.html | 12 + templates/admin/seo_tools.html | 97 +-- templates/admin/users/detail.html | 884 ++++++++++++++++++++++++ templates/admin/users/list.html | 383 ++++++++++ 14 files changed, 2168 insertions(+), 297 deletions(-) create mode 100644 .claude/admin-menu-rules.md create mode 100644 .claude/task-breakdown-rules.md create mode 100644 PROGRESS_2025-02-07.md create mode 100644 fix_user_fields.py create mode 100644 templates/admin/components/sidebar.html create mode 100644 templates/admin/users/detail.html create mode 100644 templates/admin/users/list.html diff --git a/.claude/admin-menu-rules.md b/.claude/admin-menu-rules.md new file mode 100644 index 0000000..52c93f7 --- /dev/null +++ b/.claude/admin-menu-rules.md @@ -0,0 +1,120 @@ +# 后台管理菜单统一规则 + +## 📋 规则说明 + +所有后台管理页面必须使用统一的菜单结构,不允许硬编码不同的菜单。 + +--- + +## 🎯 菜单结构 + +### 主菜单(按顺序) +1. **控制台** - `{{ url_for('admin.index') }}` +2. **网站管理** - `{{ url_for('site.index_view') }}` +3. **标签管理** - `{{ url_for('tag.index_view') }}` +4. **新闻管理** - `{{ url_for('news.index_view') }}` +5. **Prompt管理** - `{{ url_for('prompttemplate.index_view') }}` +6. **管理员** - `{{ url_for('admin_users.index_view') }}` + +### 系统菜单(按顺序) +1. **用户管理** - `{{ url_for('admin_users') }}` +2. **SEO工具** - `{{ url_for('seo_tools') }}` +3. **批量导入** - `{{ url_for('batch_import') }}` +4. **修改密码** - `{{ url_for('change_password') }}` +5. **查看网站** - `{{ url_for('index') }}` (target="_blank") +6. **退出登录** - `{{ url_for('admin_logout') }}` + +--- + +## 📝 实现方式 + +### 方式1:使用 admin/master.html(推荐) +对于新增的后台页面,应该继承 `admin/master.html`: + +```jinja2 +{% extends 'admin/master.html' %} + +{% block body %} + +{% endblock %} +``` + +**注意**:需要在路由中传递 `admin_view` 对象。 + +### 方式2:使用统一菜单组件 +对于独立HTML页面,使用 `{% include %}` 引入统一菜单组件: + +```jinja2 +{% set active_page = 'page_name' %} +{% include 'admin/components/sidebar.html' %} +``` + +**注意**: +- `active_page` 变量用于标记当前激活的菜单项 +- 统一菜单组件位于 `templates/admin/components/sidebar.html` +- 禁止复制粘贴菜单代码,必须使用 include 方式 + +--- + +## ⚠️ 重要提醒 + +1. **禁止删减菜单项** - 所有页面必须显示完整菜单 +2. **禁止修改顺序** - 菜单顺序必须一致 +3. **禁止硬编码菜单** - 必须使用统一组件 `admin/components/sidebar.html` +4. **新增菜单项** - 只需在统一组件中添加,所有页面自动生效 +5. **endpoint 名称** - 必须使用正确的 Flask-Admin endpoint + +--- + +## 🔍 检查清单 + +添加新后台页面时,必须检查: +- [ ] 主菜单包含6个项目 +- [ ] 系统菜单包含6个项目 +- [ ] endpoint 名称正确 +- [ ] 图标使用 Material Symbols +- [ ] 当前页面有 `active` 类 + +--- + +## 📂 涉及的文件 + +**统一菜单组件:** +- `templates/admin/components/sidebar.html` - 唯一的菜单源文件 + +**使用统一菜单的页面:** +- `templates/admin/master.html` - Flask-Admin 页面基础模板 +- `templates/admin/batch_import.html` - 批量导入页面 +- `templates/admin/change_password.html` - 修改密码页面 +- `templates/admin/seo_tools.html` - SEO工具页面 +- `templates/admin/users/list.html` - 用户列表页面 +- `templates/admin/users/detail.html` - 用户详情页面 + +--- + +## 🛠️ 维护指南 + +### 添加新菜单项 +1. 在 `templates/admin/components/sidebar.html` 中添加 +2. 更新本文档 +3. 所有使用该组件的页面自动生效 + +### 删除菜单项 +1. 从 `templates/admin/components/sidebar.html` 中删除 +2. 更新本文档 +3. 所有使用该组件的页面自动生效 + +### 修改菜单顺序 +1. 在 `templates/admin/components/sidebar.html` 中调整 +2. 更新本文档 +3. 所有使用该组件的页面自动生效 + +### 新增后台页面 +1. 如果是 Flask-Admin 页面,继承 `admin/master.html` +2. 如果是独立页面,使用 `{% include 'admin/components/sidebar.html' %}` +3. 设置 `active_page` 变量标记当前页面 + +--- + +**最后更新**: 2025-02-08 +**维护人**: Claude Sonnet 4.5 diff --git a/.claude/task-breakdown-rules.md b/.claude/task-breakdown-rules.md new file mode 100644 index 0000000..055aefe --- /dev/null +++ b/.claude/task-breakdown-rules.md @@ -0,0 +1,168 @@ +# 任务分解规则 + +## 📋 规则说明 + +在接到新任务后,必须将主任务分解成合适的子任务,通过分步执行的方式来保障开发的稳定性和准确性。 + +--- + +## 🎯 核心原则 + +1. **先分解,后执行** - 不要直接开始编码,先进行任务分解 +2. **小步快跑** - 每个子任务应该是独立、可测试的小单元 +3. **逐步验证** - 完成一个子任务后,验证无误再进行下一个 +4. **降低风险** - 避免一次性修改过多文件导致的连锁错误 + +--- + +## 📝 任务分解流程 + +### 第一步:理解需求 +- 仔细阅读用户的需求描述 +- 明确功能目标和验收标准 +- 识别涉及的技术栈和文件 + +### 第二步:分解任务 +将主任务分解为 3-8 个子任务,每个子任务应该: +- **独立性** - 可以独立完成和测试 +- **原子性** - 只做一件事情 +- **可验证** - 有明确的完成标准 +- **有序性** - 按照依赖关系排序 + +### 第三步:列出任务清单 +使用 TaskCreate 工具创建任务清单,包括: +- 任务标题(简短、动词开头) +- 详细描述(包含具体要做什么) +- 预期结果(如何验证完成) + +### 第四步:逐个执行 +- 使用 TaskUpdate 标记任务为 in_progress +- 完成后标记为 completed +- 遇到问题及时反馈,不要继续下一个任务 + +--- + +## ✅ 良好的任务分解示例 + +**主任务:** 添加用户管理功能 + +**子任务分解:** +1. 创建用户列表页面路由和模板 +2. 创建用户详情页面路由和模板 +3. 实现重置密码 API 接口 +4. 实现修改用户名 API 接口 +5. 在管理后台首页添加用户统计 +6. 在管理后台导航添加用户管理入口 + +--- + +## ❌ 不良的任务分解示例 + +**错误示例1:任务过大** +- ❌ "完成用户管理功能" - 太笼统,无法独立验证 + +**错误示例2:任务过细** +- ❌ "创建 user_list.html 文件" +- ❌ "在 user_list.html 中添加表格" +- ❌ "在表格中添加用户名列" +- 这样分解过于琐碎,失去了任务管理的意义 + +**错误示例3:任务无序** +- ❌ 先做"添加导航入口",后做"创建页面路由" +- 应该先有功能,再添加入口 + +--- + +## 🔍 任务粒度参考 + +### 合适的任务粒度: +- 创建一个完整的页面(路由 + 模板 + 样式) +- 实现一个 API 接口(路由 + 逻辑 + 错误处理) +- 添加一个数据库表(模型 + 迁移脚本) +- 实现一个完整的功能模块(前端 + 后端) + +### 任务太大的信号: +- 需要修改超过 5 个文件 +- 预计耗时超过 30 分钟 +- 包含多个不相关的功能点 +- 难以用一句话描述清楚 + +### 任务太小的信号: +- 只修改几行代码 +- 无法独立测试 +- 必须和其他任务一起才有意义 + +--- + +## 🛠️ 使用工具 + +### TaskCreate - 创建任务 +``` +subject: "创建用户列表页面" +description: "创建 /admin/users 路由,实现用户列表展示,包括搜索和分页功能" +activeForm: "创建用户列表页面" +``` + +### TaskUpdate - 更新任务状态 +``` +taskId: "1" +status: "in_progress" # 开始工作时 +``` + +``` +taskId: "1" +status: "completed" # 完成后 +``` + +### TaskList - 查看任务列表 +定期查看任务列表,了解整体进度 + +--- + +## 📊 任务分解模板 + +### 模板1:新增功能页面 +1. 创建后端路由和数据查询逻辑 +2. 创建前端页面模板 +3. 添加页面样式和交互 +4. 在导航菜单中添加入口 +5. 测试功能完整性 + +### 模板2:API 接口开发 +1. 设计 API 接口规范(URL、参数、返回值) +2. 实现后端路由和业务逻辑 +3. 添加参数验证和错误处理 +4. 实现前端调用逻辑 +5. 测试接口功能 + +### 模板3:数据库变更 +1. 设计数据库表结构 +2. 创建数据库迁移脚本 +3. 更新 ORM 模型定义 +4. 运行迁移并验证 +5. 更新相关业务逻辑 + +--- + +## ⚠️ 注意事项 + +1. **不要跳过分解步骤** - 即使任务看起来简单,也要先分解 +2. **及时调整计划** - 如果发现任务分解不合理,及时调整 +3. **记录遇到的问题** - 在任务描述中记录遇到的问题和解决方案 +4. **保持沟通** - 遇到不确定的地方,及时向用户确认 + +--- + +## 🎯 预期效果 + +通过任务分解,可以达到: +- ✅ 降低开发风险,减少大规模返工 +- ✅ 提高代码质量,每个子任务都经过验证 +- ✅ 便于进度跟踪,用户可以看到实时进展 +- ✅ 提升开发效率,问题可以及早发现和解决 + +--- + +**创建日期**: 2025-02-08 +**最后更新**: 2025-02-08 +**维护人**: Claude Sonnet 4.5 diff --git a/PROGRESS_2025-02-07.md b/PROGRESS_2025-02-07.md new file mode 100644 index 0000000..b0e2e29 --- /dev/null +++ b/PROGRESS_2025-02-07.md @@ -0,0 +1,229 @@ +# ZJPB 开发进度记录 - 2025-02-07 + +## 📅 开发日期 +2025年2月7日 + +--- + +## ✅ 今天完成的功能 + +### 1. 修改密码功能 +- **后端API**: `PUT /api/user/change-password` + - 验证旧密码 + - 检查新密码长度(至少6位) + - 确保新旧密码不同 +- **前端页面**: `templates/user/change_password.html` + - 密码可见性切换 + - 实时表单验证 + - AJAX提交 +- **导航入口**: + - 用户菜单(base_new.html) + - 个人中心侧边栏(profile.html) + +### 2. 邮箱绑定功能 +- **后端API**: `PUT /api/user/email` + - 邮箱格式验证(正则表达式) + - 唯一性检查(防止重复绑定) + - 修改邮箱后重置验证状态 +- **前端界面**: 集成到 `templates/user/profile.html` + - 邮箱管理模块 + - 弹窗式编辑 + - 验证状态显示 + +### 3. 邮箱验证功能 +- **数据库字段**: 在 User 模型新增4个字段 + - `email_verified` (Boolean) - 是否已验证 + - `email_verified_at` (DateTime) - 验证时间 + - `email_verify_token` (String) - 验证令牌 + - `email_verify_token_expires` (DateTime) - 令牌过期时间 +- **邮件工具**: `utils/email_sender.py` + - SMTP邮件发送 + - HTML邮件模板 + - 错误处理和日志 +- **验证流程API**: + - `POST /api/user/send-verify-email` - 发送验证邮件 + - `GET /verify-email/` - 验证邮箱链接 + - 令牌24小时有效期 +- **前端界面**: 集成到个人中心 + - 验证状态徽章 + - 发送验证邮件按钮 + +### 4. 项目文档 +- **服务器更新流程**: `.claude/skills/server-update.md` +- **服务器重启指南**: `SERVER_RESTART_GUIDE.md` +- **数据库迁移脚本**: `migrate_email_verification.py` + +--- + +## 📦 代码提交记录 + +**提交ID**: c61969d +**分支**: master +**提交信息**: feat: v3.1 - 用户密码管理和邮箱验证功能 + +**变更文件**: +- 修改: app.py, models.py, templates/base_new.html, templates/user/profile.html +- 新增: templates/user/change_password.html, utils/email_sender.py, migrate_email_verification.py +- 文档: .claude/skills/server-update.md, SERVER_RESTART_GUIDE.md + +**代码统计**: +- 9 个文件变更 +- 1242 行新增 +- 1 行删除 + +--- + +## 🚀 部署状态 + +### 已完成 +- ✅ 代码已推送到 Gitea +- ✅ 服务器已拉取最新代码 +- ✅ 数据库迁移已执行 +- ✅ 邮件环境变量已配置 +- ✅ 应用已重启 + +### 服务器信息 +- **服务器地址**: server.zjpb.net +- **项目路径**: /opt/1panel/apps/zjpb +- **运行方式**: python app.py (临时) +- **监听端口**: 5000 +- **进程ID**: 1644430, 1644432 + +### 环境变量配置 +已在 `.env` 文件添加: +``` +SMTP_SERVER=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER=配置完成 +SMTP_PASSWORD=配置完成 +FROM_EMAIL=配置完成 +FROM_NAME=ZJPB +``` + +--- + +## ⚠️ 待优化事项 + +### 高优先级 +1. **改回 Gunicorn 启动** (生产环境推荐) + ```bash + pkill -f "python app.py" + nohup gunicorn -c gunicorn_config.py wsgi:app --daemon + ``` + +2. **更新 server-update.md** 文档 + - 记录 Gunicorn 启动方式 + - 补充邮件配置说明 + +### 中优先级 +3. **安全性增强** (后续版本) + - 登录失败次数限制 + - 密码强度检查(大小写+数字+特殊字符) + - CSRF 保护 + - 会话安全配置 + +4. **功能完善** + - 密码重置功能(忘记密码) + - 两因素认证(2FA) + - 登录日志审计 + +--- + +## 📋 下次开发建议 + +### 可选方向1: 安全性加固 +- 实现登录失败限制 +- 添加 CSRF 保护 +- 增强密码复杂性要求 +- 配置安全的 Cookie 属性 + +### 可选方向2: 功能扩展 +- 密码重置功能 +- 用户头像上传 +- 账户安全中心 +- 登录设备管理 + +### 可选方向3: 用户体验优化 +- 前端密码强度提示 +- 邮箱可用性实时检查 +- 更友好的错误提示 +- 社交账号登录(OAuth) + +--- + +## 🔧 技术细节 + +### API 接口清单 +``` +# 密码管理 +PUT /api/user/change-password 修改密码 +GET /user/change-password 修改密码页面 + +# 邮箱管理 +PUT /api/user/email 更新邮箱 +POST /api/user/send-verify-email 发送验证邮件 +GET /verify-email/ 验证邮箱 +``` + +### 数据库模型变更 +```python +# User 模型新增字段 +email_verified = db.Column(db.Boolean, default=False) +email_verified_at = db.Column(db.DateTime) +email_verify_token = db.Column(db.String(100)) +email_verify_token_expires = db.Column(db.DateTime) +``` + +### 核心依赖 +- Flask-Login: 用户会话管理 +- Flask-SQLAlchemy: 数据库ORM +- smtplib: 邮件发送 +- secrets: 生成安全令牌 + +--- + +## 📝 开发日志 + +### 开发过程 +1. **需求分析** (15分钟) + - 讨论用户系统现状 + - 确定优化方向和优先级 + +2. **功能细分** (10分钟) + - 将大功能拆解为9个小模块 + - 创建任务清单 + +3. **功能开发** (90分钟) + - 逐个实现各模块 + - 保持代码独立性和稳定性 + +4. **测试与部署** (30分钟) + - 功能完整性检查 + - 提交到 Gitea + - 服务器部署 + +### 遇到的问题 +1. **服务器运行方式不明确** + - 解决:查看进程发现是 Gunicorn + - 使用 `pkill -f gunicorn` 停止 + +2. **邮箱验证令牌生成** + - 解决:使用 `secrets.token_urlsafe(32)` 生成安全令牌 + +--- + +## 🎯 成果总结 + +本次开发成功实现了用户密码管理和邮箱验证功能,为后续的安全性加固和功能扩展打下了良好基础。 + +**核心亮点**: +- ✅ 模块化开发,功能独立稳定 +- ✅ 完善的错误处理和用户提示 +- ✅ 标准化的服务器部署流程 +- ✅ 详细的开发文档和进度记录 + +--- + +**记录人**: Claude Sonnet 4.5 +**记录时间**: 2025-02-07 23:50 +**版本**: v3.1 diff --git a/app.py b/app.py index 2d498d2..4ad6baf 100644 --- a/app.py +++ b/app.py @@ -2518,6 +2518,174 @@ Sitemap: {}sitemap.xml return render_template('admin/batch_import.html', results=results) + # ========== 用户管理路由 ========== + @app.route('/admin/users') + @login_required + def admin_users(): + """用户管理列表页""" + if not isinstance(current_user, AdminModel): + flash('无权访问', 'error') + return redirect(url_for('index')) + + # 获取分页参数 + page = request.args.get('page', 1, type=int) + per_page = 20 + + # 获取搜索参数 + search = request.args.get('search', '').strip() + + # 构建查询 + query = User.query + + if search: + query = query.filter( + db.or_( + User.username.like(f'%{search}%'), + User.email.like(f'%{search}%') + ) + ) + + # 排序:按注册时间倒序 + query = query.order_by(User.created_at.desc()) + + # 分页 + pagination = query.paginate(page=page, per_page=per_page, error_out=False) + users = pagination.items + + # 为每个用户统计收藏数据 + user_stats = {} + for user in users: + user_stats[user.id] = { + 'collections_count': Collection.query.filter_by(user_id=user.id).count(), + 'folders_count': Folder.query.filter_by(user_id=user.id).count() + } + + # 获取真实的 admin 实例 + from flask import current_app + admin_instance = current_app.extensions['admin'][0] + + # 创建模拟的 admin_view 对象 + class MockAdminView: + name = '用户管理' + category = None + admin = admin_instance + + return render_template('admin/users/list.html', + users=users, + pagination=pagination, + user_stats=user_stats, + search=search, + admin_view=MockAdminView()) + + @app.route('/admin/users/') + @login_required + def admin_user_detail(user_id): + """用户详情页""" + if not isinstance(current_user, AdminModel): + flash('无权访问', 'error') + return redirect(url_for('index')) + + user = User.query.get_or_404(user_id) + + # 统计数据 + collections_count = Collection.query.filter_by(user_id=user.id).count() + folders_count = Folder.query.filter_by(user_id=user.id).count() + + # 获取最近的收藏(前10条) + recent_collections = Collection.query.filter_by(user_id=user.id)\ + .order_by(Collection.created_at.desc())\ + .limit(10).all() + + # 获取所有文件夹 + folders = Folder.query.filter_by(user_id=user.id)\ + .order_by(Folder.sort_order.desc(), Folder.created_at.desc())\ + .all() + + # 获取真实的 admin 实例 + from flask import current_app + admin_instance = current_app.extensions['admin'][0] + + # 创建模拟的 admin_view 对象 + class MockAdminView: + name = '用户详情' + category = None + admin = admin_instance + + return render_template('admin/users/detail.html', + user=user, + collections_count=collections_count, + folders_count=folders_count, + recent_collections=recent_collections, + folders=folders, + admin_view=MockAdminView()) + + @app.route('/api/admin/users//reset-password', methods=['POST']) + @login_required + def admin_reset_user_password(user_id): + """管理员重置用户密码""" + if not isinstance(current_user, AdminModel): + return jsonify({'success': False, 'message': '无权操作'}), 403 + + user = User.query.get_or_404(user_id) + data = request.get_json() + new_password = data.get('new_password', '').strip() + + # 验证新密码 + if not new_password: + return jsonify({'success': False, 'message': '新密码不能为空'}), 400 + + if len(new_password) < 6: + return jsonify({'success': False, 'message': '新密码至少需要6位'}), 400 + + try: + # 设置新密码 + user.set_password(new_password) + db.session.commit() + + return jsonify({ + 'success': True, + 'message': f'已成功重置用户 {user.username} 的密码' + }) + except Exception as e: + db.session.rollback() + return jsonify({'success': False, 'message': f'重置密码失败: {str(e)}'}), 500 + + @app.route('/api/admin/users//update-username', methods=['POST']) + @login_required + def admin_update_username(user_id): + """管理员修改用户昵称""" + if not isinstance(current_user, AdminModel): + return jsonify({'success': False, 'message': '无权操作'}), 403 + + user = User.query.get_or_404(user_id) + data = request.get_json() + new_username = data.get('new_username', '').strip() + + # 验证新昵称 + if not new_username: + return jsonify({'success': False, 'message': '昵称不能为空'}), 400 + + if len(new_username) < 2 or len(new_username) > 50: + return jsonify({'success': False, 'message': '昵称长度需要在2-50个字符之间'}), 400 + + # 检查昵称是否已被使用 + existing_user = User.query.filter_by(username=new_username).first() + if existing_user and existing_user.id != user.id: + return jsonify({'success': False, 'message': '该昵称已被使用'}), 400 + + try: + old_username = user.username + user.username = new_username + db.session.commit() + + return jsonify({ + 'success': True, + 'message': f'已成功将昵称从 {old_username} 修改为 {new_username}' + }) + except Exception as e: + db.session.rollback() + return jsonify({'success': False, 'message': f'修改昵称失败: {str(e)}'}), 500 + # ========== Flask-Admin 配置 ========== class SecureModelView(ModelView): """需要登录的模型视图""" @@ -2557,7 +2725,8 @@ Sitemap: {}sitemap.xml 'sites_count': Site.query.filter_by(is_active=True).count(), 'tags_count': Tag.query.count(), 'news_count': News.query.filter_by(is_active=True).count(), - 'total_views': db.session.query(db.func.sum(Site.view_count)).scalar() or 0 + 'total_views': db.session.query(db.func.sum(Site.view_count)).scalar() or 0, + 'users_count': User.query.count() } # 最近添加的工具(最多5个) diff --git a/fix_user_fields.py b/fix_user_fields.py new file mode 100644 index 0000000..94e2a16 --- /dev/null +++ b/fix_user_fields.py @@ -0,0 +1,47 @@ +""" +修复用户表缺失字段 +""" +from app import create_app +from models import db + +def fix_fields(): + app = create_app() + with app.app_context(): + with db.engine.connect() as conn: + try: + # 添加 email_verified_at + conn.execute(db.text(""" + ALTER TABLE users + ADD COLUMN email_verified_at DATETIME COMMENT '邮箱验证时间' + """)) + conn.commit() + print("[OK] 添加 email_verified_at") + except Exception as e: + print(f"[SKIP] email_verified_at: {e}") + + try: + # 添加 email_verify_token + conn.execute(db.text(""" + ALTER TABLE users + ADD COLUMN email_verify_token VARCHAR(100) COMMENT '邮箱验证令牌' + """)) + conn.commit() + print("[OK] 添加 email_verify_token") + except Exception as e: + print(f"[SKIP] email_verify_token: {e}") + + try: + # 添加 email_verify_token_expires + conn.execute(db.text(""" + ALTER TABLE users + ADD COLUMN email_verify_token_expires DATETIME COMMENT '验证令牌过期时间' + """)) + conn.commit() + print("[OK] 添加 email_verify_token_expires") + except Exception as e: + print(f"[SKIP] email_verify_token_expires: {e}") + + print("\n[SUCCESS] 字段修复完成!") + +if __name__ == '__main__': + fix_fields() diff --git a/migrate_email_verification.py b/migrate_email_verification.py index 5f385df..5e998f5 100644 --- a/migrate_email_verification.py +++ b/migrate_email_verification.py @@ -31,7 +31,7 @@ def migrate(): ADD COLUMN email_verified BOOLEAN DEFAULT FALSE COMMENT '邮箱是否已验证' """)) conn.commit() - print("✓ 添加 email_verified 字段") + print("[OK] 添加 email_verified 字段") # 添加 email_verified_at 字段 conn.execute(db.text(""" @@ -39,7 +39,7 @@ def migrate(): ADD COLUMN email_verified_at DATETIME COMMENT '邮箱验证时间' """)) conn.commit() - print("✓ 添加 email_verified_at 字段") + print("[OK] 添加 email_verified_at 字段") # 添加 email_verify_token 字段 conn.execute(db.text(""" @@ -47,7 +47,7 @@ def migrate(): ADD COLUMN email_verify_token VARCHAR(100) COMMENT '邮箱验证令牌' """)) conn.commit() - print("✓ 添加 email_verify_token 字段") + print("[OK] 添加 email_verify_token 字段") # 添加 email_verify_token_expires 字段 conn.execute(db.text(""" @@ -55,14 +55,14 @@ def migrate(): ADD COLUMN email_verify_token_expires DATETIME COMMENT '验证令牌过期时间' """)) conn.commit() - print("✓ 添加 email_verify_token_expires 字段") + print("[OK] 添加 email_verify_token_expires 字段") - print("\n✅ 邮箱验证字段迁移完成!") + print("\n[SUCCESS] 邮箱验证字段迁移完成!") else: - print("⚠️ 邮箱验证字段已存在,跳过迁移") + print("[SKIP] 邮箱验证字段已存在,跳过迁移") except Exception as e: - print(f"❌ 迁移失败: {str(e)}") + print(f"[ERROR] 迁移失败: {str(e)}") raise if __name__ == '__main__': diff --git a/templates/admin/batch_import.html b/templates/admin/batch_import.html index 7da0de0..0242daf 100644 --- a/templates/admin/batch_import.html +++ b/templates/admin/batch_import.html @@ -21,101 +21,8 @@ - - + {% set active_page = 'batch_import' %} + {% include 'admin/components/sidebar.html' %}
diff --git a/templates/admin/change_password.html b/templates/admin/change_password.html index fca9cfc..ff9ec75 100644 --- a/templates/admin/change_password.html +++ b/templates/admin/change_password.html @@ -21,101 +21,8 @@ - - + {% set active_page = 'change_password' %} + {% include 'admin/components/sidebar.html' %}
diff --git a/templates/admin/components/sidebar.html b/templates/admin/components/sidebar.html new file mode 100644 index 0000000..1455420 --- /dev/null +++ b/templates/admin/components/sidebar.html @@ -0,0 +1,107 @@ + + \ No newline at end of file diff --git a/templates/admin/index.html b/templates/admin/index.html index 002160b..9e91771 100644 --- a/templates/admin/index.html +++ b/templates/admin/index.html @@ -4,7 +4,7 @@
-
+
public @@ -16,7 +16,7 @@
-
+
label @@ -28,7 +28,7 @@
-
+
newspaper @@ -40,7 +40,19 @@
-
+
+
+
+ group +
+
+
{{ stats.users_count or 0 }}
+
注册用户
+
+
+
+ +
visibility @@ -189,6 +201,25 @@ color: #606266; } +.col-md-2-4 { + flex: 0 0 20%; + max-width: 20%; +} + +@media (max-width: 768px) { + .col-md-2-4 { + flex: 0 0 50%; + max-width: 50%; + } +} + +@media (max-width: 480px) { + .col-md-2-4 { + flex: 0 0 100%; + max-width: 100%; + } +} + .quick-actions { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); diff --git a/templates/admin/master.html b/templates/admin/master.html index 66c5a87..a2372a9 100644 --- a/templates/admin/master.html +++ b/templates/admin/master.html @@ -79,6 +79,18 @@