From 588c50c289212a0cec452c30e4c7d026c2ddcb17 Mon Sep 17 00:00:00 2001 From: weizwz <1124725517@qq.com> Date: Wed, 19 Nov 2025 15:45:01 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DEPLOYMENT_CHECKLIST.md | 210 ++++++++++++++++++++++++++++++++++++++++ app/register-sw.tsx | 16 ++- 2 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 DEPLOYMENT_CHECKLIST.md diff --git a/DEPLOYMENT_CHECKLIST.md b/DEPLOYMENT_CHECKLIST.md new file mode 100644 index 0000000..729ae6d --- /dev/null +++ b/DEPLOYMENT_CHECKLIST.md @@ -0,0 +1,210 @@ +# 部署检查清单 + +## 🚀 部署步骤 + +### 1. 提交代码 + +```bash +# 查看修改 +git status + +# 添加所有修改 +git add . + +# 提交 +git commit -m "fix: 更新 CSP 配置和 Service Worker v4" + +# 推送到远程 +git push origin main +``` + +### 2. Cloudflare Pages 部署 + +访问 Cloudflare Pages 控制台: +1. 等待自动部署完成(约 2-3 分钟) +2. 查看部署日志确认成功 +3. 记录部署 URL + +### 3. 清除 Cloudflare 缓存 + +**重要!** 必须清除缓存才能让新的 `_headers` 生效 + +**方法 1:通过控制台** +``` +1. 登录 Cloudflare Dashboard +2. 选择你的域名 +3. Caching → Configuration +4. 点击 "Purge Everything" +5. 确认清除 +``` + +**方法 2:通过 API** +```bash +curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \ + -H "Authorization: Bearer {api_token}" \ + -H "Content-Type: application/json" \ + --data '{"purge_everything":true}' +``` + +### 4. 验证部署 + +**检查 CSP 配置**: +```bash +# 检查 HTTP 头 +curl -I https://nav.weizwz.com | grep -i content-security-policy + +# 应该看到: +# content-security-policy: ... connect-src 'self' https: ... +``` + +**检查 Service Worker**: +```bash +# 访问网站 +open https://nav.weizwz.com + +# 打开控制台 +# 应该看到:Service Worker 注册成功 +# 不应该看到:CSP 错误 +``` + +### 5. 用户端清除缓存 + +**提供清除工具**: +``` +https://nav.weizwz.com/clear-cache.html +``` + +**通知用户**(可选): +- 在网站上显示更新提示 +- 或发送通知 +- 或等待自动更新(可能需要几小时) + +## ✅ 验证清单 + +部署后检查以下项目: + +- [ ] Cloudflare Pages 部署成功 +- [ ] Cloudflare 缓存已清除 +- [ ] CSP 配置正确(`connect-src 'self' https:`) +- [ ] Service Worker 版本正确(v4) +- [ ] 控制台无 CSP 错误 +- [ ] 图标正常加载 +- [ ] 页面不会频繁刷新 +- [ ] 清除缓存工具可访问 + +## 🔍 故障排查 + +### 问题 1:仍然有 CSP 错误 + +**原因**:Cloudflare 缓存未清除 + +**解决**: +```bash +# 清除 Cloudflare 缓存 +# 等待 5-10 分钟 +# 硬刷新浏览器(Ctrl+Shift+R) +``` + +### 问题 2:页面频繁刷新 + +**原因**:Service Worker 不断检测到更新 + +**解决**: +```bash +# 检查 Service Worker 版本是否一致 +# 确保所有文件都已部署 +# 清除浏览器缓存 +``` + +### 问题 3:图标仍然加载失败 + +**原因**:浏览器缓存了旧的 Service Worker + +**解决**: +``` +1. 访问 /clear-cache.html +2. 点击"全部清除并刷新" +3. 或手动清除浏览器缓存 +``` + +## 📊 监控 + +部署后监控以下指标: + +### Cloudflare Analytics +``` +- 请求数 +- 错误率 +- 缓存命中率 +``` + +### 浏览器控制台 +``` +- CSP 错误数量(应该为 0) +- Service Worker 状态 +- 网络请求状态 +``` + +### 用户反馈 +``` +- 图标加载问题 +- 页面刷新问题 +- 功能异常 +``` + +## 🔄 回滚计划 + +如果部署出现问题: + +### 快速回滚 +```bash +# 回滚到上一个版本 +git revert HEAD +git push origin main + +# 或回滚到特定版本 +git reset --hard +git push origin main --force +``` + +### Cloudflare Pages 回滚 +``` +1. 进入 Cloudflare Pages 控制台 +2. 选择项目 +3. Deployments → 选择之前的部署 +4. 点击 "Rollback to this deployment" +``` + +## 📝 部署记录 + +记录每次部署的信息: + +``` +日期:2024-11-18 +版本:v1.0.4 (SW v4) +修改: +- 更新 CSP 配置(connect-src 'self' https:) +- 优化 Service Worker 自动刷新逻辑 +- 修复图标闪烁问题 +- 添加清除缓存工具 + +部署人:[你的名字] +部署时间:[时间] +验证状态:✅ 通过 +``` + +## 🎯 下次部署优化 + +- [ ] 自动化部署流程 +- [ ] 添加部署前测试 +- [ ] 设置 Staging 环境 +- [ ] 配置自动回滚 +- [ ] 添加部署通知 + +--- + +**记住**: +1. 每次修改 `_headers` 必须清除 Cloudflare 缓存 +2. 每次更新 Service Worker 必须递增版本号 +3. 部署后必须验证 CSP 配置 +4. 提供用户清除缓存的工具 diff --git a/app/register-sw.tsx b/app/register-sw.tsx index 613c6c7..d536eb2 100644 --- a/app/register-sw.tsx +++ b/app/register-sw.tsx @@ -58,9 +58,21 @@ export default function RegisterServiceWorker() { let refreshing = false; navigator.serviceWorker.addEventListener('controllerchange', () => { if (refreshing) return; + + // 检查是否是首次加载(没有旧的 controller) + const isFirstLoad = !navigator.serviceWorker.controller; + if (isFirstLoad) { + console.log('首次加载 Service Worker,无需刷新'); + return; + } + refreshing = true; - console.log('Service Worker 已更新,刷新页面...'); - window.location.reload(); + console.log('Service Worker 已更新,3秒后刷新页面...'); + + // 延迟刷新,给用户时间看到提示 + setTimeout(() => { + window.location.reload(); + }, 3000); }); }); } From c3df72db629770e8ae360a5ef3576e9c854cd241 Mon Sep 17 00:00:00 2001 From: weizwz <1124725517@qq.com> Date: Wed, 19 Nov 2025 16:22:31 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=9B=BE=E6=A0=87?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E5=A4=B1=E8=B4=A5=E6=97=B6=E7=9A=84=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ICON_FALLBACK_DEMO.md | 157 ++++++++++ .../ICON_FALLBACK_TEST.md | 112 +++++++ .../ICON_IMPROVEMENTS.md | 279 ++++++++++++++++++ .../specs/frontend-navigation-site/design.md | 32 ++ .../frontend-navigation-site/requirements.md | 13 + .kiro/specs/frontend-navigation-site/tasks.md | 11 +- components/layout/SearchBar.tsx | 56 ++-- components/navigation/LinkCard.tsx | 124 +++++--- 8 files changed, 724 insertions(+), 60 deletions(-) create mode 100644 .kiro/specs/frontend-navigation-site/ICON_FALLBACK_DEMO.md create mode 100644 .kiro/specs/frontend-navigation-site/ICON_FALLBACK_TEST.md create mode 100644 .kiro/specs/frontend-navigation-site/ICON_IMPROVEMENTS.md diff --git a/.kiro/specs/frontend-navigation-site/ICON_FALLBACK_DEMO.md b/.kiro/specs/frontend-navigation-site/ICON_FALLBACK_DEMO.md new file mode 100644 index 0000000..b795dc9 --- /dev/null +++ b/.kiro/specs/frontend-navigation-site/ICON_FALLBACK_DEMO.md @@ -0,0 +1,157 @@ +# 图标加载失败处理演示 + +## 快速测试方法 + +### 方法 1:使用浏览器开发者工具 + +1. 打开网站 +2. 按 F12 打开开发者工具 +3. 切换到 Console 标签 +4. 运行以下代码添加测试链接: + +```javascript +// 测试 1:无效的自定义图标 URL +const testLink1 = { + id: 'test-1', + name: '测试:无效图标', + url: 'https://github.com', + description: '这个链接使用了无效的图标URL', + category: '主页', + icon: 'https://invalid-domain-12345.com/icon.png', + backgroundColor: '#1890ff', + iconScale: 0.7, + tags: [], + order: 0, + createdAt: Date.now(), + updatedAt: Date.now() +}; + +// 测试 2:无效的域名(favicon 也会失败) +const testLink2 = { + id: 'test-2', + name: '测试:完全失败', + url: 'https://invalid-domain-99999.com', + description: '这个链接的图标和favicon都会失败', + category: '主页', + icon: 'https://invalid-domain-12345.com/icon.png', + backgroundColor: '#52c41a', + iconScale: 0.7, + tags: [], + order: 0, + createdAt: Date.now(), + updatedAt: Date.now() +}; + +// 测试 3:只依赖 favicon(应该成功) +const testLink3 = { + id: 'test-3', + name: '测试:Favicon回退', + url: 'https://www.google.com', + description: '这个链接没有自定义图标,应该显示Google的favicon', + category: '主页', + backgroundColor: '#4285f4', + iconScale: 0.7, + tags: [], + order: 0, + createdAt: Date.now(), + updatedAt: Date.now() +}; + +// 添加到 localStorage +const existingLinks = JSON.parse(localStorage.getItem('navigation-links') || '[]'); +existingLinks.push(testLink1, testLink2, testLink3); +localStorage.setItem('navigation-links', JSON.stringify(existingLinks)); + +// 刷新页面 +location.reload(); +``` + +### 方法 2:通过 UI 手动添加 + +1. 点击页面上的"添加链接"按钮 +2. 填写以下信息: + +**测试链接 1:无效图标** +- 名称:测试:无效图标 +- 地址:https://github.com +- 描述:这个链接使用了无效的图标URL +- 图标:https://invalid-domain-12345.com/icon.png +- 背景色:#1890ff + +**测试链接 2:完全失败** +- 名称:测试:完全失败 +- 地址:https://invalid-domain-99999.com +- 描述:这个链接的图标和favicon都会失败 +- 图标:https://invalid-domain-12345.com/icon.png +- 背景色:#52c41a + +**测试链接 3:Favicon回退** +- 名称:测试:Favicon回退 +- 地址:https://www.google.com +- 描述:这个链接没有自定义图标,应该显示Google的favicon +- 图标:(留空) +- 背景色:#4285f4 + +## 预期结果 + +### 测试链接 1(无效图标) +- ✅ 不显示破损的图片图标 +- ✅ 自动回退到 GitHub 的 favicon +- ✅ 控制台显示:`图标加载失败: https://invalid-domain-12345.com/icon.png` + +### 测试链接 2(完全失败) +- ✅ 不显示破损的图片图标 +- ✅ 显示默认的链接图标(LinkOutlined) +- ✅ 控制台显示两条警告: + - `图标加载失败: https://invalid-domain-12345.com/icon.png` + - `Favicon 加载失败: [favicon URL]` + +### 测试链接 3(Favicon回退) +- ✅ 显示 Google 的 favicon +- ✅ 图标清晰,没有破损 + +## 清理测试数据 + +测试完成后,可以通过以下方式清理测试数据: + +### 方法 1:通过 UI 删除 +右键点击测试链接卡片,选择"删除" + +### 方法 2:通过控制台清理 +```javascript +// 删除所有测试链接 +const links = JSON.parse(localStorage.getItem('navigation-links') || '[]'); +const cleanedLinks = links.filter(link => !link.name.startsWith('测试:')); +localStorage.setItem('navigation-links', JSON.stringify(cleanedLinks)); +location.reload(); +``` + +## 技术说明 + +### 修复前的问题 +```typescript +// 问题代码:使用 display: 'none' 隐藏 + setHasError(true)} +/> +``` + +浏览器可能在设置 `display: 'none'` 之前短暂显示破损图标。 + +### 修复后的方案 +```typescript +// 解决方案:条件渲染,完全不渲染失败的元素 +if (hasError && (faviconError || !fallbackUrl)) { + return ; +} + +if (hasError && fallbackUrl && !faviconError) { + return setFaviconError(true)} />; +} + +return setHasError(true)} />; +``` + +这样可以确保失败的 `` 元素完全不会被渲染到 DOM 中。 diff --git a/.kiro/specs/frontend-navigation-site/ICON_FALLBACK_TEST.md b/.kiro/specs/frontend-navigation-site/ICON_FALLBACK_TEST.md new file mode 100644 index 0000000..979e4b1 --- /dev/null +++ b/.kiro/specs/frontend-navigation-site/ICON_FALLBACK_TEST.md @@ -0,0 +1,112 @@ +# 图标加载失败处理测试指南 + +## 测试目的 + +验证链接卡片的图标加载失败时,能够正确显示默认图标而不是破损的图片。 + +## 修复说明 + +### 问题描述 +之前的实现使用 `display: 'none'` 来隐藏加载失败的图片,但浏览器可能仍会短暂显示破损的图片图标。 + +### 解决方案 +改用条件渲染(conditional rendering),根据加载状态完全不渲染失败的 `` 元素,而是直接渲染下一级回退选项。 + +### 代码变更 +- 移除了 `imageLoaded` 和 `faviconLoaded` 状态 +- 移除了 `display: 'none'` 样式 +- 使用条件渲染:只在对应状态下渲染对应的元素 +- 简化了逻辑,提高了性能 + +## 测试步骤 + +### 1. 测试自定义图标加载失败 + +**操作:** +1. 打开浏览器开发者工具(F12) +2. 切换到 Network 标签 +3. 在 Network 标签中启用 "Offline" 模式或使用 "Block request URL" 功能 +4. 添加一个新链接,使用一个无效的图标 URL(例如:`https://invalid-domain-12345.com/icon.png`) +5. 观察卡片显示 + +**预期结果:** +- 不应该看到破损的图片图标 +- 应该自动尝试加载该网站的 favicon +- 如果 favicon 也失败,应该显示默认的链接图标(LinkOutlined) +- 控制台应该有警告信息:`图标加载失败: https://invalid-domain-12345.com/icon.png` + +### 2. 测试 Favicon 回退 + +**操作:** +1. 添加一个链接,不设置自定义图标 +2. 确保该链接的域名有效(例如:`https://github.com`) +3. 观察卡片显示 + +**预期结果:** +- 应该自动加载 GitHub 的 favicon +- 图标应该清晰显示,没有破损图标 + +### 3. 测试完全失败的情况 + +**操作:** +1. 添加一个链接,使用无效的自定义图标 URL +2. 确保该链接的域名也无效或无法访问(例如:`https://invalid-domain-12345.com`) +3. 观察卡片显示 + +**预期结果:** +- 不应该看到任何破损的图片图标 +- 应该直接显示默认的 LinkOutlined 图标 +- 控制台应该有两条警告信息: + - `图标加载失败: [自定义图标URL]` + - `Favicon 加载失败: [favicon URL]` + +### 4. 测试图标大小一致性 + +**操作:** +1. 创建多个链接: + - 一个使用有效的自定义图标 + - 一个使用 favicon + - 一个使用默认图标 +2. 观察所有卡片的图标大小 + +**预期结果:** +- 所有图标的大小应该保持一致 +- 默认图标的大小应该与自定义图标相同(受 `iconScale` 属性控制) + +## 验证清单 + +- [ ] 自定义图标加载失败时不显示破损图标 +- [ ] 自动回退到 favicon +- [ ] Favicon 失败时自动回退到默认图标 +- [ ] 默认图标大小与自定义图标一致 +- [ ] 控制台正确记录警告信息 +- [ ] 没有闪烁或视觉跳动 +- [ ] 图标过渡平滑自然 + +## 相关文件 + +- `components/navigation/LinkCard.tsx` - 主要修复文件 +- `.kiro/specs/frontend-navigation-site/requirements.md` - 需求 11 +- `.kiro/specs/frontend-navigation-site/design.md` - LinkCard 组件设计 + +## 技术细节 + +### IconWithFallback 组件状态管理 + +```typescript +const [hasError, setHasError] = useState(false); // 自定义图标是否失败 +const [faviconError, setFaviconError] = useState(false); // Favicon 是否失败 +``` + +### 渲染逻辑 + +1. **初始状态**:渲染自定义图标 +2. **自定义图标失败**:`hasError = true`,渲染 favicon +3. **Favicon 失败**:`faviconError = true`,渲染默认图标 +4. **无 fallbackUrl**:直接从自定义图标失败跳到默认图标 + +### 性能优化 + +- 移除了不必要的状态变量 +- 减少了 DOM 元素数量(不再同时渲染多个隐藏的 img 元素) +- 简化了条件判断逻辑 diff --git a/.kiro/specs/frontend-navigation-site/ICON_IMPROVEMENTS.md b/.kiro/specs/frontend-navigation-site/ICON_IMPROVEMENTS.md new file mode 100644 index 0000000..48087ea --- /dev/null +++ b/.kiro/specs/frontend-navigation-site/ICON_IMPROVEMENTS.md @@ -0,0 +1,279 @@ +# 图标显示改进说明 + +## 改进内容 + +### 1. 白色背景下的默认图标颜色优化 + +**问题:** +当链接卡片的背景色是白色时,默认图标(LinkOutlined)也是白色,导致图标不可见。 + +**解决方案:** +- 添加 `isWhiteColor()` 函数判断背景色是否为白色 +- 如果背景是白色,默认图标使用主题色 `#1890ff`(蓝色) +- 如果背景不是白色,默认图标使用白色 `#ffffff` + +**支持的白色格式:** +- `#ffffff` / `#fff` +- `white` +- `rgb(255, 255, 255)` / `rgb(255,255,255)` +- `rgba(255, 255, 255, ...)` / `rgba(255,255,255,...)` + +### 2. 默认图标大小统一 + +**问题:** +默认图标的大小受 `iconScale` 属性影响,导致不同卡片的默认图标大小不一致。 + +**解决方案:** +- 默认图标使用固定大小 `48px`,不受 `iconScale` 影响 +- 确保所有默认图标大小一致,视觉效果更统一 + +### 3. Favicon.im URL 识别优化 + +**问题:** +如果用户设置的图标 URL 本身就是 `favicon.im` 的地址,会导致重复请求。 + +**解决方案:** +- 添加 `isFaviconUrl()` 函数识别 favicon.im 的 URL +- 如果 `link.icon` 是 favicon.im 的 URL,跳过"自定义图标"逻辑 +- 直接进入"使用 Favicon"逻辑,避免重复请求 + +## 代码变更 + +### IconWithFallback 组件 + +**新增参数:** +```typescript +interface IconWithFallbackProps { + src: string; + alt: string; + fallbackUrl?: string; + scale?: number; + backgroundColor?: string; // 新增:背景色 +} +``` + +**默认图标渲染逻辑:** +```typescript +// 第三级:所有图片都失败,显示默认图标 +const DefaultIcon = AntdIcons.LinkOutlined; +const defaultIconColor = isWhiteColor(backgroundColor) ? '#1890ff' : '#ffffff'; +const defaultIconSize = 48; // 固定大小 + +return ( + +); +``` + +### renderIcon 逻辑 + +**情况1:自定义图标 URL** +```typescript +// 排除 favicon.im 的 URL +if (link.icon && + (link.icon.startsWith('http://') || link.icon.startsWith('https://') || link.icon.startsWith('/')) && + !isFaviconUrl(link.icon)) { + return ( + + ); +} +``` + +**情况4:兜底默认图标** +```typescript +// 默认图标使用固定大小和智能颜色 +const DefaultIcon = AntdIcons.LinkOutlined; +const defaultIconColor = isWhiteColor(backgroundColor) ? '#1890ff' : '#ffffff'; +const defaultIconSize = 48; + +return ( + +); +``` + +## 测试场景 + +### 场景1:白色背景 + 图标加载失败 + +**测试步骤:** +1. 创建一个链接,背景色设置为 `#ffffff` +2. 图标 URL 设置为无效地址 +3. 观察默认图标 + +**预期结果:** +- 默认图标显示为蓝色(`#1890ff`) +- 图标大小为 48px +- 图标清晰可见 + +### 场景2:彩色背景 + 图标加载失败 + +**测试步骤:** +1. 创建一个链接,背景色设置为 `#1890ff`(蓝色) +2. 图标 URL 设置为无效地址 +3. 观察默认图标 + +**预期结果:** +- 默认图标显示为白色(`#ffffff`) +- 图标大小为 48px +- 图标清晰可见 + +### 场景3:多个卡片的默认图标大小一致性 + +**测试步骤:** +1. 创建多个链接,设置不同的 `iconScale` 值(0.5, 0.7, 1.0) +2. 所有链接的图标 URL 都设置为无效地址 +3. 观察所有默认图标 + +**预期结果:** +- 所有默认图标大小完全一致(48px) +- 不受 `iconScale` 影响 + +### 场景4:Favicon.im URL 不重复请求 + +**测试步骤:** +1. 创建一个链接,图标 URL 设置为 `https://favicon.im/example.com` +2. 打开浏览器开发者工具的 Network 标签 +3. 观察网络请求 + +**预期结果:** +- 只有一次对 `favicon.im` 的请求 +- 没有重复请求 + +## 视觉效果对比 + +### 修复前 + +| 背景色 | 默认图标颜色 | 可见性 | 大小 | +|--------|-------------|--------|------| +| 白色 | 白色 | ❌ 不可见 | 受 iconScale 影响 | +| 蓝色 | 白色 | ✅ 可见 | 受 iconScale 影响 | +| 绿色 | 白色 | ✅ 可见 | 受 iconScale 影响 | + +### 修复后 + +| 背景色 | 默认图标颜色 | 可见性 | 大小 | +|--------|-------------|--------|------| +| 白色 | 蓝色 (#1890ff) | ✅ 可见 | 固定 48px | +| 蓝色 | 白色 (#ffffff) | ✅ 可见 | 固定 48px | +| 绿色 | 白色 (#ffffff) | ✅ 可见 | 固定 48px | + +## 4. 搜索引擎图标加载失败处理 + +**问题:** +搜索栏左侧的搜索引擎图标加载失败时,显示破损的图片占位符,把输入框撑高了。 + +**解决方案:** +- 创建 `EngineIcon` 组件处理图标加载失败 +- 加载失败时显示 Ant Design 的 `SearchOutlined` 图标 +- 确保图标加载失败不影响输入框高度 + +**实现细节:** +```typescript +const EngineIcon: React.FC<{ iconUrl: string; name: string; size?: number }> = + ({ iconUrl, name, size = 20 }) => { + const [hasError, setHasError] = useState(false); + const faviconUrl = getFaviconUrl(iconUrl); + + // 如果图标加载失败,显示默认搜索图标 + if (hasError || !faviconUrl) { + return ; + } + + return ( + {`${name} setHasError(true)} + /> + ); +}; +``` + +## 相关文件 + +- `components/navigation/LinkCard.tsx` - 链接卡片图标修复 +- `components/layout/SearchBar.tsx` - 搜索栏图标修复 +- `.kiro/specs/frontend-navigation-site/requirements.md` - 需求 11 +- `.kiro/specs/frontend-navigation-site/design.md` - 设计说明 + +## 技术细节 + +### 颜色判断函数 + +```typescript +const isWhiteColor = (color?: string): boolean => { + if (!color) return false; + const normalizedColor = color.toLowerCase().trim(); + return ( + normalizedColor === '#ffffff' || + normalizedColor === '#fff' || + normalizedColor === 'white' || + normalizedColor === 'rgb(255, 255, 255)' || + normalizedColor === 'rgb(255,255,255)' || + normalizedColor.startsWith('rgba(255, 255, 255') || + normalizedColor.startsWith('rgba(255,255,255') + ); +}; +``` + +### Favicon URL 判断函数 + +```typescript +const isFaviconUrl = (url: string) => { + return url.includes('favicon.im/'); +}; +``` + +## 测试场景:搜索引擎图标 + +### 场景5:搜索引擎图标加载失败 + +**测试步骤:** +1. 打开浏览器开发者工具 +2. 在 Network 标签中阻止图片加载或设置离线模式 +3. 观察搜索栏左侧的搜索引擎图标 + +**预期结果:** +- 显示 SearchOutlined 图标(放大镜) +- 输入框高度正常,没有被撑高 +- 图标大小为 20px +- 控制台显示警告:`搜索引擎图标加载失败: [URL]` + +### 场景6:下拉菜单中的搜索引擎图标 + +**测试步骤:** +1. 点击搜索栏左侧的搜索引擎图标 +2. 观察下拉菜单中的所有搜索引擎图标 +3. 如果有图标加载失败,观察显示效果 + +**预期结果:** +- 加载成功的图标正常显示(16px) +- 加载失败的图标显示 SearchOutlined(16px) +- 所有图标大小一致 +- 菜单项高度一致,没有错位 + +## 性能影响 + +- ✅ 减少了重复的网络请求(favicon.im URL 识别) +- ✅ 简化了状态管理(移除了未使用的 iconSize 变量) +- ✅ 提升了代码可读性和可维护性 +- ✅ 搜索栏图标加载失败不影响布局 +- ✅ 用户体验更好,界面更稳定 diff --git a/.kiro/specs/frontend-navigation-site/design.md b/.kiro/specs/frontend-navigation-site/design.md index 3115d2c..0577738 100644 --- a/.kiro/specs/frontend-navigation-site/design.md +++ b/.kiro/specs/frontend-navigation-site/design.md @@ -257,6 +257,38 @@ sequenceDiagram - 悬停效果:轻微上浮 + 阴影增强 - 动画:使用 framer-motion 实现 +**图标加载策略(多级回退机制):** +1. **第一级:自定义图标** + - 如果用户提供了自定义图标 URL,优先加载 + - 使用 `` 标签的 `onError` 事件监听加载失败 + - 设置 `loading="lazy"` 和 `decoding="async"` 优化性能 + +2. **第二级:Favicon 回退** + - 当自定义图标加载失败时,自动尝试加载网站的 favicon + - 使用 `getFaviconUrl()` 函数获取 favicon URL + - 同样监听 `onError` 事件处理加载失败 + +3. **第三级:默认图标** + - 当所有图片加载都失败时,显示 Ant Design 的 `LinkOutlined` 图标 + - 确保默认图标大小与自定义图标保持一致 + - 使用 `iconScale` 属性控制图标缩放 + +**图标加载实现细节:** +- 使用 React state 跟踪加载状态(`hasError`, `faviconError`, `imageLoaded`, `faviconLoaded`) +- 通过 CSS `display` 属性控制图标显示/隐藏,避免显示破损图片 +- 在控制台记录加载失败的警告信息,便于调试 +- 使用 `onLoad` 事件标记图片成功加载,防止误判 + +**IconWithFallback 子组件:** +```typescript +interface IconWithFallbackProps { + src: string; // 主图标 URL + alt: string; // 图标描述 + fallbackUrl?: string; // 回退 favicon URL + scale?: number; // 图标缩放比例(默认 0.8) +} +``` + **右键菜单:** - 使用 Ant Design Dropdown 组件 - 菜单项:编辑、删除 diff --git a/.kiro/specs/frontend-navigation-site/requirements.md b/.kiro/specs/frontend-navigation-site/requirements.md index 8dd6c15..27d41f6 100644 --- a/.kiro/specs/frontend-navigation-site/requirements.md +++ b/.kiro/specs/frontend-navigation-site/requirements.md @@ -127,6 +127,19 @@ 5. THE 导航系统 SHALL 在所有交互操作中提供视觉反馈 6. THE 导航系统 SHALL 确保页面初始加载时间不超过 2 秒 +### 需求 11:图标加载失败处理 + +**用户故事:** 作为用户,当链接卡片的图标加载失败时,我希望看到默认图标而不是破损的图片,以便保持界面的美观和一致性 + +#### 验收标准 + +1. WHEN 链接卡片的自定义图标加载失败时,THE 导航系统 SHALL 自动尝试加载该网站的 favicon 图标 +2. WHEN favicon 图标也加载失败时,THE 导航系统 SHALL 显示默认的链接图标(LinkOutlined) +3. THE 导航系统 SHALL 在图标加载失败时不显示破损的图片占位符 +4. THE 导航系统 SHALL 在图标加载过程中提供平滑的视觉过渡 +5. WHEN 图标加载失败时,THE 导航系统 SHALL 在浏览器控制台记录警告信息以便调试 +6. THE 导航系统 SHALL 确保默认图标的大小与自定义图标保持一致 + ### 需求 10:性能优化 **用户故事:** 作为用户,我希望网站加载快速且运行流畅,以便高效完成任务 diff --git a/.kiro/specs/frontend-navigation-site/tasks.md b/.kiro/specs/frontend-navigation-site/tasks.md index 2e718d4..4a6bbbd 100644 --- a/.kiro/specs/frontend-navigation-site/tasks.md +++ b/.kiro/specs/frontend-navigation-site/tasks.md @@ -173,7 +173,16 @@ - 处理 LocalStorage 配额超限错误 - 处理图标加载失败(显示默认图标) - 添加表单验证错误提示 - - _需求: 2.4_ + - _需求: 2.4, 11.1, 11.2, 11.3, 11.4, 11.5, 11.6_ + +- [x] 24. 修复图标加载失败显示破损图片的问题 + - 优化 `IconWithFallback` 组件的渲染逻辑 + - 使用条件渲染替代 `display: 'none'` 隐藏失败的图片 + - 移除不必要的状态变量(`imageLoaded`, `faviconLoaded`) + - 确保图标加载失败时完全不渲染失败的 `` 元素 + - 验证三级回退机制:自定义图标 → Favicon → 默认图标 + - 创建测试指南文档 + - _需求: 11.1, 11.2, 11.3, 11.4, 11.5, 11.6_ - [x] 21. 实现可访问性优化 - 为所有交互元素添加合适的 ARIA 标签 diff --git a/components/layout/SearchBar.tsx b/components/layout/SearchBar.tsx index decc3e8..ab4df92 100644 --- a/components/layout/SearchBar.tsx +++ b/components/layout/SearchBar.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'; import { Input, Dropdown, Space } from 'antd'; +import { SearchOutlined } from '@ant-design/icons'; import type { MenuProps } from 'antd'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { performDebouncedSearch } from '@/store/slices/searchSlice'; @@ -87,6 +88,42 @@ export default function SearchBar() { storageService.saveSettings(updatedSettings); }, [dispatch, settings]); + /** + * 搜索引擎图标组件,支持加载失败回退 + */ + const EngineIcon: React.FC<{ iconUrl: string; name: string; size?: number }> = ({ iconUrl, name, size = 20 }) => { + const [hasError, setHasError] = useState(false); + const faviconUrl = getFaviconUrl(iconUrl); + + // 如果图标加载失败,显示默认搜索图标 + if (hasError || !faviconUrl) { + return ( + + ); + } + + return ( + {`${name} { + console.warn(`搜索引擎图标加载失败: ${faviconUrl}`); + setHasError(true); + }} + /> + ); + }; + /** * 构建搜索引擎下拉菜单 - 使用 useMemo 缓存 */ @@ -94,13 +131,7 @@ export default function SearchBar() { key: engine.id, label: ( - {`${engine.name} + {engine.name} ), @@ -111,16 +142,7 @@ export default function SearchBar() { * 获取搜索引擎图标 */ const getEngineIcon = React.useCallback((iconUrl: string) => { - const faviconUrl = getFaviconUrl(iconUrl); - return ( - {`${currentEngine.name} - ); + return ; }, [currentEngine.name]); /** diff --git a/components/navigation/LinkCard.tsx b/components/navigation/LinkCard.tsx index 320086e..e170cca 100644 --- a/components/navigation/LinkCard.tsx +++ b/components/navigation/LinkCard.tsx @@ -10,6 +10,23 @@ import * as AntdIcons from '@ant-design/icons'; import { Link } from '@/types/link'; import { getFaviconUrl } from '@/api/favicon'; +/** + * 判断颜色是否为白色或接近白色 + */ +const isWhiteColor = (color?: string): boolean => { + if (!color) return false; + const normalizedColor = color.toLowerCase().trim(); + return ( + normalizedColor === '#ffffff' || + normalizedColor === '#fff' || + normalizedColor === 'white' || + normalizedColor === 'rgb(255, 255, 255)' || + normalizedColor === 'rgb(255,255,255)' || + normalizedColor.startsWith('rgba(255, 255, 255') || + normalizedColor.startsWith('rgba(255,255,255') + ); +}; + /** * 图标组件,支持多级回退 * 1. 用户自定义图标 @@ -21,22 +38,32 @@ const IconWithFallback: React.FC<{ alt: string; fallbackUrl?: string; scale?: number; -}> = ({ src, alt, fallbackUrl, scale = 0.8 }) => { + backgroundColor?: string; +}> = ({ src, alt, fallbackUrl, scale = 0.8, backgroundColor }) => { const [hasError, setHasError] = useState(false); const [faviconError, setFaviconError] = useState(false); - const [imageLoaded, setImageLoaded] = useState(false); - const [faviconLoaded, setFaviconLoaded] = useState(false); - // 计算图标大小(基础大小 60px) - const iconSize = Math.round(60 * scale); - - // 如果用户图标和 favicon 都失败,显示默认图标 - if (hasError && faviconError) { - const DefaultIcon = AntdIcons.LinkOutlined; - return ; + // 第一级:尝试加载用户自定义图标 + if (!hasError) { + return ( + {`${alt}的图标`} { + console.warn(`图标加载失败: ${src}`); + setHasError(true); + }} + /> + ); } - // 如果用户图标失败,尝试 favicon + // 第二级:用户图标失败,尝试加载 Favicon if (hasError && fallbackUrl && !faviconError) { return ( setFaviconLoaded(true)} onError={() => { - // 只有在图片未成功加载时才标记为错误 - if (!faviconLoaded) { - console.warn(`Favicon 加载失败: ${fallbackUrl}`); - setFaviconError(true); - } + console.warn(`Favicon 加载失败: ${fallbackUrl}`); + setFaviconError(true); }} /> ); } - // 显示用户自定义图标 + // 第三级:所有图片都失败,显示默认图标 + // 默认图标使用固定大小(48px),不受 iconScale 影响 + // 如果背景是白色,使用主题色;否则使用白色 + const DefaultIcon = AntdIcons.LinkOutlined; + const defaultIconColor = isWhiteColor(backgroundColor) ? '#1890ff' : '#ffffff'; + const defaultIconSize = 48; + return ( - {`${alt}的图标`} setImageLoaded(true)} - onError={() => { - // 只有在图片未成功加载时才标记为错误 - if (!imageLoaded) { - console.warn(`图标加载失败: ${src}`); - setHasError(true); - } - }} + fontSize: defaultIconSize, + color: defaultIconColor, + }} + aria-label={`${alt}的默认图标`} /> ); }; @@ -175,43 +193,65 @@ const LinkCardBase: React.FC = ({ link, onEdit, onDelete, isDragg // 获取 favicon URL 作为回退选项,使用 larger=true 获取更高质量的图标 const faviconUrl = getFaviconUrl(link.url, { larger: true }); const scale = link.iconScale || 0.7; - const iconSize = Math.round(60 * scale); + const backgroundColor = link.backgroundColor; + + // 判断是否为 favicon.im 的 URL + const isFaviconUrl = (url: string) => { + return url.includes('favicon.im/'); + }; - // 情况1: 用户提供了自定义图标 URL - if (link.icon && (link.icon.startsWith('http://') || link.icon.startsWith('https://') || link.icon.startsWith('/'))) { + // 情况1: 用户提供了自定义图标 URL(但不是 favicon.im 的 URL) + if (link.icon && + (link.icon.startsWith('http://') || link.icon.startsWith('https://') || link.icon.startsWith('/')) && + !isFaviconUrl(link.icon)) { return ( ); } // 情况2: 用户提供了 Ant Design 图标名称 - if (link.icon) { + if (link.icon && !link.icon.startsWith('http://') && !link.icon.startsWith('https://') && !link.icon.startsWith('/')) { const IconComponent = (AntdIcons as any)[link.icon]; if (IconComponent) { - return ; + const antdIconSize = Math.round(60 * scale); + return ; } } - // 情况3: 没有自定义图标,尝试使用 favicon + // 情况3: 没有自定义图标,或者图标是 favicon.im URL,尝试使用 favicon if (faviconUrl) { return ( ); } // 情况4: 所有方式都失败,显示默认图标 + // 默认图标使用固定大小(48px),不受 iconScale 影响 + // 如果背景是白色,使用主题色;否则使用白色 const DefaultIcon = AntdIcons.LinkOutlined; - return ; - }, [link.icon, link.name, link.url, link.iconScale]); + const defaultIconColor = isWhiteColor(backgroundColor) ? '#1890ff' : '#ffffff'; + const defaultIconSize = 48; + + return ( + + ); + }, [link.icon, link.name, link.url, link.iconScale, link.backgroundColor]); return (
Date: Wed, 19 Nov 2025 16:30:52 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E4=BF=AE=E6=94=B9=20header=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20github=20=E5=9C=B0=E5=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/layout/Header.tsx | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/components/layout/Header.tsx b/components/layout/Header.tsx index 45d723b..c82b8d1 100644 --- a/components/layout/Header.tsx +++ b/components/layout/Header.tsx @@ -3,7 +3,7 @@ import React, { memo, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { Button, Tooltip } from 'antd'; -import { EditOutlined, MenuOutlined } from '@ant-design/icons'; +import { EditOutlined, MenuOutlined, GithubOutlined } from '@ant-design/icons'; import SearchBar from './SearchBar'; import ThemeToggle from './ThemeToggle'; @@ -75,6 +75,21 @@ const Header = memo(function Header({ onMenuClick }: HeaderProps) { }} /> + +
@@ -126,6 +141,21 @@ const Header = memo(function Header({ onMenuClick }: HeaderProps) { }} /> + +