From 98febc798a24d4c8e919efac58966ed406b6f514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E7=BB=8D=E5=BA=B7?= <3052147989@qq.com> Date: Mon, 9 Mar 2026 12:58:01 +0800 Subject: [PATCH] fix: resolve copy logout and favicon regressions --- ...0012-邮箱复制功能失效与剪贴板兼容性问题.md | 241 ++++++++++++++++++ img/ico.png | Bin 0 -> 17145 bytes outlook_web/controllers/pages.py | 15 +- outlook_web/routes/pages.py | 2 + static/js/features/emails.js | 38 ++- static/js/features/groups.js | 5 +- static/js/main.js | 12 +- templates/index.html | 1 + templates/login.html | 1 + 9 files changed, 296 insertions(+), 19 deletions(-) create mode 100644 docs/BUG/BUG-00012-邮箱复制功能失效与剪贴板兼容性问题.md create mode 100644 img/ico.png diff --git a/docs/BUG/BUG-00012-邮箱复制功能失效与剪贴板兼容性问题.md b/docs/BUG/BUG-00012-邮箱复制功能失效与剪贴板兼容性问题.md new file mode 100644 index 0000000..2bfa100 --- /dev/null +++ b/docs/BUG/BUG-00012-邮箱复制功能失效与剪贴板兼容性问题.md @@ -0,0 +1,241 @@ +# BUG-00012 邮箱复制功能失效与剪贴板兼容性问题 + +> 关联功能: PRD-00005 / FD-00005 多邮箱统一管理 +> 关联历史缺陷: BUG-00004 临时邮箱复制与邮箱管理交互问题 +> 分析时间: 2026-03-09 +> 状态: 待修复 + +--- + +## 一、问题概述 + +| 编号 | 标题 | 严重程度 | 类型 | +|------|------|---------|------| +| **D-1** | 邮箱管理页账号列表中的邮箱名点击无效 | 🟠 中 | 交互绑定缺失 | +| **D-2** | 复制按钮/当前邮箱栏在部署环境下可能完全无法复制 | 🔴 高 | 剪贴板兼容性 / 前端异常 | + +--- + +## 二、用户反馈现象 + +在 VPS 更新部署后,前端出现以下问题: + +1. 在邮箱管理页点击账号卡片中的邮箱名,没有任何复制效果。 +2. 在邮件列表顶部“当前邮箱”栏点击邮箱名,没有复制效果。 +3. 点击账号卡片右侧的“📋 复制”按钮,也没有完成复制。 + +该问题直接影响账号复制、外部粘贴、手工校验等高频操作。 + +--- + +## 三、复现路径 + +### 3.1 邮箱管理页邮箱名点击无效 + +1. 进入“账号管理”页面。 +2. 选择任一普通邮箱分组。 +3. 在账号卡片中直接点击邮箱地址文本。 +4. 观察:无 toast、无复制结果。 + +### 3.2 当前邮箱栏 / 复制按钮无效 + +1. 在账号管理页选择任一邮箱账号。 +2. 点击顶部“当前邮箱:xxx@xxx.com”。 +3. 或点击账号卡片右下角“📋”复制按钮。 +4. 观察:未复制到系统剪贴板;在部分环境中前端可能静默失败。 + +--- + +## 四、根因分析 + +### 4.1 D-1: 账号列表中的邮箱名没有绑定复制事件 + +邮箱管理页账号卡片由 `renderAccountList()` 动态渲染。当前实现中,邮箱名节点仅负责展示文本,没有绑定 `onclick` 复制逻辑: + +文件: `static/js/features/groups.js` + +```javascript +
+ ${escapeHtml(acc.email)} +
+``` + +对比临时邮箱页面的实现,临时邮箱列表中邮箱名是显式可点击复制的: + +文件: `static/js/features/temp_emails.js` + +```javascript +
${escapeHtml(email.email)}
+``` + +结论: + +- 普通邮箱列表“点击邮箱名可复制”这条交互在当前代码中并未真正实现。 +- 因此前端表现为“点击邮箱名无反应”是代码级确定问题,不依赖浏览器环境。 + +### 4.2 D-2: `copyEmail()` 对 Clipboard API 的调用缺少可用性判断 + +多个复制入口最终都走到了共享函数 `copyEmail()`: + +- 账号卡片复制按钮 `📋` +- 顶部当前邮箱栏 `copyCurrentEmail()` +- 临时邮箱顶部邮箱名 `copyTempEmailCurrent()` + +当前实现如下: + +文件: `static/js/features/emails.js` + +```javascript +function copyEmail(email) { + navigator.clipboard.writeText(email).then(() => { + showToast('邮箱地址已复制', 'success'); + }).catch(() => { + const textarea = document.createElement('textarea'); + textarea.value = email; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + document.body.removeChild(textarea); + showToast('邮箱地址已复制', 'success'); + }); +} +``` + +这里有两个关键缺陷: + +1. 代码直接调用 `navigator.clipboard.writeText(email)`,没有先判断 `navigator.clipboard` 是否存在。 +2. 当页面运行在非安全上下文(例如 HTTP 部署、浏览器权限策略限制、特定 WebView 环境)时,`navigator.clipboard` 可能为 `undefined`,此时会在 `.then()` 之前直接抛出同步异常,导致后面的 `catch` 降级逻辑根本不会执行。 + +这意味着: + +- 用户点击复制按钮时,前端可能直接报 `TypeError`。 +- 页面上看起来就是“点了没反应”。 +- 降级方案 `document.execCommand('copy')` 实际上并未被触发。 + +### 4.3 降级复制逻辑缺少失败校验,导致问题难以诊断 + +即使进入了 `document.execCommand('copy')` 分支,当前代码也没有检查返回值或捕获异常: + +```javascript +document.execCommand('copy'); +showToast('邮箱地址已复制', 'success'); +``` + +风险: + +- 即便浏览器不支持或执行失败,前端仍可能提示“复制成功”。 +- 这会掩盖真实失败原因,增加线上排查难度。 + +--- + +## 五、影响范围 + +### 5.1 受影响入口 + +| 入口 | 调用链路 | 当前状态 | +|------|---------|---------| +| 账号管理页卡片邮箱名 | `renderAccountList()` | ❌ 未绑定复制事件 | +| 账号管理页卡片复制按钮 | `copyEmail()` | ❌ 在部分部署环境可能失效 | +| 顶部“当前邮箱”栏 | `copyCurrentEmail()` -> `copyEmail()` | ❌ 在部分部署环境可能失效 | +| 临时邮箱页顶部邮箱名 | `copyTempEmailCurrent()` -> `copyEmail()` | ❌ 在部分部署环境可能失效 | +| 临时邮箱卡片复制按钮 | `copyEmail()` | ❌ 在部分部署环境可能失效 | + +### 5.2 受影响文件 + +- `static/js/features/groups.js` +- `static/js/features/emails.js` +- `static/js/features/temp_emails.js` +- `static/js/features/accounts.js` +- `templates/index.html` + +--- + +## 六、疑似触发条件 + +以下场景更容易触发 D-2: + +1. 站点通过 HTTP 暴露,而非 HTTPS。 +2. 通过 Nginx 反代后,实际访问域名不是 `localhost`,浏览器将其视为非安全上下文。 +3. 浏览器或内嵌 WebView 对 Clipboard API 做了权限限制。 +4. 更新后前端脚本命中该路径,但没有在控制台和 UI 中暴露异常信息。 + +说明: + +- D-1 与部署环境无关,任何环境都会复现。 +- D-2 与浏览器上下文、权限模型、部署方式强相关,因此“本地可用、VPS 不可用”是合理现象。 + +--- + +## 七、修复建议 + +### 7.1 修复 D-1: 为普通邮箱列表邮箱名补充点击复制能力 + +建议对齐临时邮箱实现,在账号卡片邮箱名节点上绑定: + +```javascript +onclick="event.stopPropagation(); copyEmail('${escapeJs(acc.email)}')" +``` + +并补充: + +- `cursor:pointer` +- `title="点击复制"` + +### 7.2 修复 D-2: 重写 `copyEmail()`,先检测能力,再降级 + +建议改为显式判断: + +1. 如果 `navigator.clipboard && navigator.clipboard.writeText` 可用,则优先使用 Clipboard API。 +2. 如果不可用,则进入 `textarea + execCommand('copy')` 降级方案。 +3. 如果降级也失败,应明确提示“复制失败”,不要误报成功。 + +建议伪代码: + +```javascript +async function copyEmail(email) { + try { + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(email); + showToast('邮箱地址已复制', 'success'); + return true; + } + + const textarea = document.createElement('textarea'); + textarea.value = email; + document.body.appendChild(textarea); + textarea.select(); + const copied = document.execCommand('copy'); + document.body.removeChild(textarea); + + if (!copied) throw new Error('execCommand copy failed'); + + showToast('邮箱地址已复制', 'success'); + return true; + } catch (error) { + console.error('复制邮箱失败:', error); + showToast('复制失败,请手动复制', 'error'); + return false; + } +} +``` + +### 7.3 补充验证 + +建议增加以下回归验证项: + +1. 普通邮箱列表中点击邮箱名可以复制。 +2. 复制按钮可以复制。 +3. 顶部“当前邮箱”栏可以复制。 +4. 临时邮箱页面顶部邮箱名可以复制。 +5. 在 HTTP 部署环境下,降级方案仍可工作,或至少给出明确失败提示。 + +--- + +## 八、结论 + +本次“邮箱复制失效”并非单一问题,而是两个缺陷叠加: + +1. **普通邮箱列表邮箱名本身没有绑定复制事件**,这是确定性代码遗漏。 +2. **共享复制函数对 `navigator.clipboard` 的使用过于乐观**,在 VPS/反代/非 HTTPS 场景下可能直接抛错,导致复制按钮和顶部邮箱栏全部失效。 + +因此该问题应按一个缺陷文档统一跟踪,但修复时需要拆成“交互绑定修复”和“剪贴板兼容性修复”两部分分别验证。 diff --git a/img/ico.png b/img/ico.png new file mode 100644 index 0000000000000000000000000000000000000000..54deb5d11f50621fe2a1db59871ee857289ff93d GIT binary patch literal 17145 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 G>i-8wDClPZ literal 0 HcmV?d00001 diff --git a/outlook_web/controllers/pages.py b/outlook_web/controllers/pages.py index 87dc9ba..7196f6e 100644 --- a/outlook_web/controllers/pages.py +++ b/outlook_web/controllers/pages.py @@ -1,8 +1,9 @@ from __future__ import annotations +from pathlib import Path from typing import Any -from flask import g, jsonify, redirect, render_template, request, session, url_for +from flask import g, jsonify, redirect, render_template, request, send_from_directory, session, url_for from outlook_web.errors import build_error_payload from outlook_web.repositories import settings as settings_repo @@ -106,6 +107,18 @@ def logout() -> Any: return redirect(url_for("pages.login")) +def image_asset(filename: str) -> Any: + """提供仓库 img/ 目录中的静态图片资源。""" + img_dir = Path(__file__).resolve().parents[2] / "img" + return send_from_directory(str(img_dir), filename) + + +def favicon() -> Any: + """站点 favicon,复用 img/ico.png。""" + img_dir = Path(__file__).resolve().parents[2] / "img" + return send_from_directory(str(img_dir), "ico.png", mimetype="image/png") + + @login_required def index() -> Any: """主页""" diff --git a/outlook_web/routes/pages.py b/outlook_web/routes/pages.py index 442b131..737ef4f 100644 --- a/outlook_web/routes/pages.py +++ b/outlook_web/routes/pages.py @@ -21,5 +21,7 @@ def create_blueprint(csrf_exempt: Optional[Callable] = None) -> Blueprint: bp.add_url_rule("/login", view_func=login_view, methods=["GET", "POST"]) bp.add_url_rule("/logout", view_func=pages_controller.logout, methods=["GET"]) bp.add_url_rule("/", view_func=pages_controller.index, methods=["GET"]) + bp.add_url_rule("/img/", view_func=pages_controller.image_asset, methods=["GET"]) + bp.add_url_rule("/favicon.ico", view_func=pages_controller.favicon, methods=["GET"]) bp.add_url_rule("/api/csrf-token", view_func=csrf_token_view, methods=["GET"]) return bp diff --git a/static/js/features/emails.js b/static/js/features/emails.js index acb5132..35f3841 100644 --- a/static/js/features/emails.js +++ b/static/js/features/emails.js @@ -753,19 +753,39 @@ } // 复制邮箱地址 - function copyEmail(email) { - navigator.clipboard.writeText(email).then(() => { - showToast('邮箱地址已复制', 'success'); - }).catch(() => { - // 降级方案 + async function copyEmail(email) { + try { + if (navigator.clipboard && navigator.clipboard.writeText && window.isSecureContext) { + await navigator.clipboard.writeText(email); + showToast('邮箱地址已复制', 'success'); + return true; + } + const textarea = document.createElement('textarea'); textarea.value = email; + textarea.setAttribute('readonly', 'readonly'); + textarea.style.position = 'fixed'; + textarea.style.top = '-9999px'; + textarea.style.left = '-9999px'; + document.body.appendChild(textarea); + textarea.focus(); textarea.select(); - document.execCommand('copy'); + + const copied = document.execCommand('copy'); document.body.removeChild(textarea); + + if (!copied) { + throw new Error('document.execCommand(copy) returned false'); + } + showToast('邮箱地址已复制', 'success'); - }); + return true; + } catch (error) { + console.error('复制邮箱地址失败:', error); + showToast('复制失败,请手动复制', 'error'); + return false; + } } // 复制当前邮箱 @@ -780,9 +800,7 @@ // 退出登录 function logout() { if (confirm('确定要退出登录吗?')) { - fetch('/logout', { method: 'POST' }) - .then(() => { window.location.href = '/login'; }) - .catch(() => { window.location.href = '/logout'; }); + window.location.href = '/logout'; } } diff --git a/static/js/features/groups.js b/static/js/features/groups.js index c8fde89..6415ab0 100644 --- a/static/js/features/groups.js +++ b/static/js/features/groups.js @@ -264,7 +264,10 @@ onclick="event.stopPropagation(); updateBatchActionBar(); updateSelectAllCheckbox()">