# 浏览器首页(MyHomePage)项目说明文档 > 单一项目管理载体:所有规划、变更、进度、问题均记录在此。 > 角色:猫娘工程师 幽浮喵 > 创建日期:2026-07-04 --- ## 一、项目概述 打造一款跨端可用的浏览器首页 / 起始页:PC、平板、手机浏览器及 Android APP 共享同一套数据,实时同步。 设计稿已定型(`browser-homepage/`),包含桌面端、桌面端设置弹窗、移动端三个页面。 ### 核心能力 1. 二级分类导航(常用工具 > 搜索引擎 / AI 工具 / 开发工具 等) 2. 搜索引擎可管理(增删改、设置默认) 3. 链接卡片 / 列表:图标 + 标题 + 简介 4. 设置面板:主题模式(暗/亮/跟随系统)、主色调、背景图 5. PC / 平板 / 手机浏览器自适应 6. PC / 平板 / 手机 / Android APP 四端实时数据同步 7. 链接 / 背景图支持图片上传(落盘到后端可配路径) --- ## 二、技术栈 | 层 | 选型 | |----|------| | 前端框架 | Vue 3 + TypeScript + Vite + Pinia + Vue Router | | UI 库 | 自研组件(复用 `colors_and_type.css` 的 design token),可选 Element Plus / Naive UI 辅助 | | 图标 | lucide-vue-next(设计稿用 lucide) | | APP 壳 | Capacitor 6(共享同一 Vue 前端代码) | | 后端 | .NET 8 + ASP.NET Core Web API + C# | | ORM | SqlSugar(同一套代码切换 MySQL / SQLite) | | 数据库 | MySQL(生产)/ SQLite(开发 / 移动端缓存可选) | | 配置 | `appsettings.json` + `appsettings.{Environment}.json` + 环境变量 | | 部署 | Docker Compose(后端 + Nginx 前端) | --- ## 三、目录结构 ``` MyHomePage/ ├── frontend/ # Vue 3 + TS + Vite 前端 │ ├── src/ │ │ ├── api/ # axios 接口封装 │ │ ├── components/ # 自研通用组件 │ │ ├── views/ # 页面(Desktop / Mobile / Settings) │ │ ├── stores/ # Pinia stores │ │ ├── router/ # 路由 │ │ ├── styles/ # 全局样式 + design tokens │ │ ├── types/ # TypeScript 类型 │ │ └── main.ts │ ├── android/ # Capacitor Android 壳 │ ├── public/ │ ├── vite.config.ts │ ├── tsconfig.json │ └── package.json ├── backend/ # .NET 8 Web API │ ├── Controllers/ # 控制器 │ ├── Services/ # 业务服务 │ ├── Models/ │ │ ├── Entities/ # SqlSugar 实体 │ │ └── Dtos/ # 入参 / 出参 │ ├── Repositories/ # 仓储 │ ├── Common/ # 通用:响应包装、异常处理 │ ├── Infrastructure/ # DbContext、配置扩展 │ ├── Uploads/ # 上传文件(路径可配) │ ├── appsettings.json │ ├── appsettings.Development.json │ ├── Program.cs │ └── MyHomePage.Api.csproj ├── docker-compose.yml ├── docker/ │ ├── backend.Dockerfile │ └── nginx.conf └── browser-homepage/ # 设计稿(已存在) ``` --- ## 四、实施计划 | 阶段 | 目标 | 状态 | |------|------|------| | P0 | 创建后端 .NET 8 骨架 + SqlSugar(MySQL/SQLite 双驱动) | TODO | | P1 | 实体 / 仓储 / 服务 / 控制器:categories / bookmarks / search-engines / settings / upload / sync | TODO | | P2 | 启动后端,Swagger 跑通,冒烟测试 | TODO | | P3 | 前端 Vite + Vue 3 + TS + Pinia + Router 骨架 | TODO | | P4 | 接入 design tokens,复用 `colors_and_type.css` 变量 | TODO | | P5 | 桌面端:左侧二级分类侧边栏 + 顶部搜索栏 + 链接卡片网格 | TODO | | P6 | 移动端:顶栏 + 抽屉 + 分类标签 + 链接列表 + FAB | TODO | | P7 | 设置面板:主题模式 / 主色调 / 背景图 | TODO | | P8 | 接入后端 API,CRUD 联调 | TODO | | P9 | 多端同步:基于 `lastModified` 的增量同步 | TODO | | P10 | Capacitor 6 打包 Android APP | TODO | | P11 | Docker Compose 部署(后端 + Nginx + 前端构建产物) | TODO | | P12 | README 完善 + 部署文档 | TODO | --- ## 五、数据库设计(SqlSugar 实体草案) | 表 | 关键字段 | 说明 | |----|---------|------| | `categories` | Id, ParentId, Name, Icon, Sort, CreatedAt, UpdatedAt | 二级树形分类 | | `bookmarks` | Id, CategoryId, Title, Url, Description, Icon, IconType, IconUrl, ColorBg, Sort, CreatedAt, UpdatedAt, IsDeleted | 链接收藏,软删 | | `search_engines` | Id, Name, UrlTemplate, Icon, Sort, IsDefault, CreatedAt, UpdatedAt | 搜索引擎 | | `settings` | Id, ThemeMode, AccentColor, BackgroundImage, BackgroundType, OpenLinksInNewTab, OpenSearchInNewTab, WallpaperEnabled, WallpaperCategoryId, WallpaperInterval, UpdatedAt | 用户设置(单行/单设备维度) | | `sync_log` | Id, EntityType, EntityId, Operation, Timestamp | 同步日志,用于增量同步 | --- ## 六、API 契约 | 方法 | 路径 | 说明 | |------|------|------| | GET / POST / PUT / DELETE | `/api/categories` | 分类 CRUD(支持二级树) | | GET / POST / PUT / DELETE | `/api/bookmarks` | 链接 CRUD,可按 `?categoryId=` 过滤 | | GET / POST / PUT / DELETE | `/api/search-engines` | 搜索引擎 CRUD | | GET / PUT | `/api/settings` | 用户设置读写 | | POST | `/api/upload` | 单 / 多图上传,返回可访问 URL | | GET | `/api/sync/changes?since={ISO8601}` | 增量同步 | | POST | `/api/utility/favicon` | 抓取网站 favicon(后端代理) | | GET / POST | `/api/wallpaper/categories` | 360 壁纸分类列表 | | GET / POST | `/api/wallpaper/random` | 随机一张壁纸 | | POST | `/api/wallpaper/refresh` | 立即切换壁纸(清池重拉) | --- ## 七、配置节点 `appsettings.json` 关键节点: ```json { "Database": { "Provider": "MySql", // MySql | Sqlite "ConnectionString": "..." }, "Upload": { "Path": "Uploads", // 相对内容根目录 "BaseUrl": "/uploads" // 前端拼接前缀 }, "Cors": { "Origins": [ "http://localhost:5173" ] }, "Urls": "http://0.0.0.0:5141" // 显式绑定 5141(与 Vite proxy 默认端口一致,P34.2 修复) } ``` --- ## 八、进度记录 > 每完成一项任务,立即在此追加一条,含完成时间与产出文件链接。 > **2026-07-05 整理**:P0~P33 为简短摘要;P34 起为详细记录(含代码改动 / 验证 / 教训)。 | 时间 | 阶段 | 内容 | 状态 | |------|------|------|------| | 2026-07-04 | 启动 | 创建说明文档、规划任务清单 | ✅ 已完成 | | 2026-07-04 | P0-P2 | .NET 8 + SqlSugar(MySQL/SQLite)后端 CRUD 全部跑通,Swagger + 冒烟测试通过 | ✅ 已完成 | | 2026-07-04 | P3-P9 | Vue 3 + Vite + Pinia + 桌面/移动双布局 + 设置面板 + 全量 CRUD 联调通过 | ✅ 已完成 | | 2026-07-04 | P10 | 多端同步:手动 + 30s 自动轮询 + visibility 触发 | ✅ 已完成 | | 2026-07-04 | P11 | Docker Compose(MySQL + 后端 + 前端一体镜像) | ✅ 已完成 | | 2026-07-04 | P12 | Capacitor 6 集成 + Android 壳工程打包指引 | ✅ 已完成 | | 2026-07-04 | P13 | README + docker/README + ANDROID.md 文档 | ✅ 已完成 | | 2026-07-04 | P14 | 修复 SyncLogHelper 生命周期校验失败;appsettings.json 加 `Urls: http://0.0.0.0:5080`;端到端联调 200 | ✅ 已完成 | | 2026-07-04 | P15 | UI 体验补完:sidebar 加 hover ⋯ 菜单(编辑/删除/新建子分类)+ 底部「+ 新建分类」;移动端 drawer 拆分点击与按钮;empty 加引导;删除加 confirm + toast;主题 auto watch 修复 | ✅ 已完成 | | 2026-07-04 | P16 | ExceptionHandlingMiddleware 透传 ex.Code(400/404);axios 拦截器读 body message;E2E 16 项全过 | ✅ 已完成 | | 2026-07-04 | P17 | 桌面端 sidebar 视觉调整:去「全部」count / 统计下沉 footer / 删重复设置按钮 / 「全部」视图下「新建链接」不再 disabled | ✅ 已完成 | | 2026-07-04 | P18 | 「全部」按钮方案 B:下沉 footer 作「查看全部」小按钮(含 count);startCreateBookmark 加 categories.loaded await | ✅ 已完成 | | 2026-07-04 | P19 | 链接建立逻辑方案 B:顶级/二级均可放链接(BookmarkForm optgroup + sidebar 一级 ⋯ 加「新建链接」入口 + HomeView @create-bookmark 事件) | ✅ 已完成 | | 2026-07-04 | P20 | 弹出菜单 stacking:z-index token (--z-popover=1500 / --z-modal=2000 / --z-toast=3000);outside-click 关闭 + 互斥 | ✅ 已完成 | | 2026-07-04 | P21 | sidebar ⋯ 菜单 stacking 终极修复:`` + `position: fixed`;图标 `more-horizontal` → `more-vertical`;vue-tsc + build 通过 | ✅ 已完成 | | 2026-07-04 | P21.1 | P21 遗漏 fix:[icon.ts](file:///d:/Code/MyHomePage/frontend/src/utils/icon.ts) `map` 漏注册 `MoreVertical`,AppIcon fallback 显示「M」 | ✅ 已完成 | | 2026-07-04 | P21.2 | 侧边栏菜单 outside-click 关闭修复:P21 改 Teleport 后 P20 的 `sidebarEl.contains()` 失效;重挂 document 'pointerdown' + closest() 排除菜单和 trigger | ✅ 已完成 | | 2026-07-04 | P22 | 链接列表显示逻辑 + localStorage 持久化:新建 [utils/storage.ts](file:///d:/Code/MyHomePage/frontend/src/utils/storage.ts);[HomeView.vue](file:///d:/Code/MyHomePage/frontend/src/views/HomeView.vue) displayedBookmarks 改造(null=全部 / 顶级聚合 / 二级单分类 / 失效 fallback);HomeView onMounted 恢复 + watch 持久化;[AppSidebar.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppSidebar.vue) onRootClick 改造(未选→选中+展开 / 已选→取消+折叠) | ✅ 已完成 | | 2026-07-04 | P23 | sidebar 「+」号移到 root row + 图标选择器:AppSidebar root row 加 + 按钮(hover 显示);[utils/icon.ts](file:///d:/Code/MyHomePage/frontend/src/utils/icon.ts) 扩到 130+ 导出 `SUPPORTED_ICONS`;新建 [AppIconPicker.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppIconPicker.vue);[CategoryForm.vue](file:///d:/Code/MyHomePage/frontend/src/components/CategoryForm.vue) / [BookmarkForm.vue](file:///d:/Code/MyHomePage/frontend/src/components/BookmarkForm.vue) 集成 IconPicker | ✅ 已完成 | | 2026-07-04 | P23.1 | 「+」号改回与 ⋯ 一样的 hover 行为(去掉 .sidebar__row-action--add 显式 opacity:1) | ✅ 已完成 | | 2026-07-04 | P24 | 移动端 bug 修复:搜索栏「百度」被拆两行([AppSearchBar.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppSearchBar.vue) flex-shrink:0 + @media (max-width:640px) 适配);drawer 一级点击没反应([HomeView.vue](file:///d:/Code/MyHomePage/frontend/src/views/HomeView.vue) root 整行 @click + 内嵌按钮 @click.stop) | ✅ 已完成 | | 2026-07-04 | P25 | sync 接口 snapshot.categories 扁平化 bug 修复:[SyncService.cs](file:///d:/Code/MyHomePage/backend/Services/SyncService.cs) Select 后未构建树形;[CategoryDtos.cs](file:///d:/Code/MyHomePage/backend/Models/Dtos/CategoryDtos.cs) 加 `BuildTree` + `BuildTreeFromFlat`(parentId==0 → 顶级;孤儿降级);清理前端调试日志;dotnet build 0 错误;后端重启;curl /api/sync/changes 看到树形结构 | ✅ 已完成 | | 2026-07-04 | P26 | 设置面板增加「链接行为」开关(6 文件全链路):后端 [Setting.cs](file:///d:/Code/MyHomePage/backend/Models/Entities/Setting.cs) 加 `OpenLinksInNewTab int default 1`(int 不用 bool 避 SqlSugar+SQLite bit 兼容性);[SettingDtos.cs](file:///d:/Code/MyHomePage/backend/Models/Dtos/SettingDtos.cs) 加字段 + `SettingDto.FromEntity(s)` 静态映射;[SettingService.cs](file:///d:/Code/MyHomePage/backend/Services/SettingService.cs) UpdateAsync bool→int 转换;[DatabaseInitializer.cs](file:///d:/Code/MyHomePage/backend/Infrastructure/Database/DatabaseInitializer.cs) `MigrateSettingColumns()` AddColumn INTEGER DEFAULT 1;前端 [types/api.ts](file:///d:/Code/MyHomePage/frontend/src/types/api.ts) `AppSettings` + `SettingUpdate` 加 `openLinksInNewTab`;[stores/settings.ts](file:///d:/Code/MyHomePage/frontend/src/stores/settings.ts) 默认 true + `setOpenLinksInNewTab`;[HomeView.vue](file:///d:/Code/MyHomePage/frontend/src/views/HomeView.vue) `openBookmark` 用 `target = settings.openLinksInNewTab ? '_blank' : '_self'`;[SettingsView.vue](file:///d:/Code/MyHomePage/frontend/src/views/SettingsView.vue) 新增「链接行为」section(toggle + 反转逻辑 + P26.1 修复点击传同值 bug;P26.2 修复 sync 漏字段) | ✅ 已完成 | | 2026-07-04 | P27 | 前端页面美化:新建 [utils/color.ts](file:///d:/Code/MyHomePage/frontend/src/utils/color.ts) `colorFromUrl` + `firstChar`(27 个品牌色板 + 32-bit HSL 哈希);[tokens.css](file:///d:/Code/MyHomePage/frontend/src/styles/tokens.css) 加 `--glass-bg-faint` / `--glass-blur-sm/-lg` / `--link-logo-size: 96px` / `--link-card-min-height: 88px`;[AppLinkCard.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppLinkCard.vue) 横排彩色 logo + 标题描述改造(96×88 品牌色块 + 玻璃面板 + 编辑/删除绝对定位 hover 浮现);[AppLinkListItem.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppLinkListItem.vue) 移动端同步升级(56px logo);[AppSidebar.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppSidebar.vue) 顶部加用户头像 header(48px 蓝渐变 + "M" 字符 + 用户名 + tagline);[AppSearchBar.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppSearchBar.vue) 引擎区加彩色 logo 块(28×28 + 名称 + 旋转 180° ▾ + 220px 宽菜单 + 22×22 item logo + ✓ 勾 + 圆形白色 submit 箭头) | ✅ 已完成 | | 2026-07-04 | P28 | 主人 4 项反馈落实:① sidebar 玻璃质感升级(彻底删除 P27 头像 header;`background: rgba(15,15,26,0.32); backdrop-filter: blur(28px) saturate(180%); border-radius: 0 24px 24px 0;` + 顶部高光 + 微妙 box-shadow);② 链接 logo 区域统一正方形([tokens.css](file:///d:/Code/MyHomePage/frontend/src/styles/tokens.css) 补 `--link-logo-size: 88px` / `--link-card-min-height` / `--link-card-radius` / `--link-logo-radius` / `--link-row-logo-size: 56px` / `--link-row-min-height: 64px`;[AppLinkCard.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppLinkCard.vue) 完全重写 logo 块 `width: var(--link-logo-size); height: var(--link-logo-size); flex-shrink: 0;` 完美正方形 + `border-right: 1px solid var(--glass-border);` + `` 真正显示 + 三级 fallback 背景色);[AppLinkListItem.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppLinkListItem.vue) 移动端 56px 正方形同步升级);③ 添加链接弹窗加背景色选择器([BookmarkForm.vue](file:///d:/Code/MyHomePage/frontend/src/components/BookmarkForm.vue) 新增 10 套预设色块 grid + 36×36 自定义颜色 `` 透明覆盖 + conic-gradient 棋盘底色 + "自适应"按钮 + 动态 hint;`form.colorBg: string \| null`);④ `extractDominantColor` 主色提取([utils/color.ts](file:///d:/Code/MyHomePage/frontend/src/utils/color.ts) `` 缩放 32×32 + getImageData + 像素过滤 + 5-bit 桶量化 + 6s timeout);⑤ 后端 ColorBg 字段全链路([Bookmark.cs](file:///d:/Code/MyHomePage/backend/Models/Entities/Bookmark.cs) `ColorBg string? Length=32`;[BookmarkDtos.cs](file:///d:/Code/MyHomePage/backend/Models/Dtos/BookmarkDtos.cs) 加字段 + `BookmarkDto.FromEntity(b)` 静态映射;[BookmarkService.cs](file:///d:/Code/MyHomePage/backend/Services/BookmarkService.cs) Create/Update `NormalizeColor()` 验证);⑥ DB 轻量迁移([DatabaseInitializer.cs](file:///d:/Code/MyHomePage/backend/Infrastructure/Database/DatabaseInitializer.cs) `MigrateBookmarkColumns()` AddColumn varchar(32) NULL);⑦ 两个 P28 关键 bug 修复([BookmarkService.UpdateAsync](file:///d:/Code/MyHomePage/backend/Services/BookmarkService.cs#L78-L87) 漏处理 ColorBg / [SyncService.snapshot.Bookmarks](file:///d:/Code/MyHomePage/backend/Services/SyncService.cs#L39-L52) 漏 ColorBg — 根因方案 `BookmarkDto.FromEntity` 共享映射) | ✅ 已完成 | | 2026-07-04 | P29 | 主人反馈两项修复:① 删过头 — 齿轮设置按钮复原([HomeView.vue](file:///d:/Code/MyHomePage/frontend/src/views/HomeView.vue) P28 把整个 .sidebar__header 隐藏误删 manage 按钮;P29 在 `desktop-main` 右上角新增齿轮 `position: absolute; top: 20px; right: 24px; z-index: var(--z-fab); width/height: 40px; border-radius: var(--radius-pill); background: var(--glass-bg); border: 1px solid var(--glass-border); backdrop-filter: blur(var(--glass-blur-sm)) saturate(180%); box-shadow: var(--shadow-sm);` + hover 上浮 1px + shadow-md + color 变亮 + background 变 glass-bg-strong;复用 `goSettings` 函数;移动端仍走 AppMobileTopBar 设置入口);② 去掉壁纸蒙层([AppWallpaper.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppWallpaper.vue) 删除 `.app-wallpaper::after` 伪元素 `linear-gradient(180deg, rgba(15,15,26,.45) 0%, rgba(15,15,26,.72) 100%)` 蒙层;P29 教训:以后删元素要看清子元素依赖关系 — 主人说"删头像"≠"删 header 内所有东西") | ✅ 已完成 | | 2026-07-04 | P30 | 编辑弹窗分类下拉显示错乱 bug 修复:① 现象(主人反馈):同一个 VSCode 链接在「常用工具」顶级分类下点编辑,下拉框显示"常用工具"(错的);在「开发工具」二级分类下点编辑,下拉框显示"开发工具"(对的)。链接真实分类始终是「开发工具」,应无论从哪个父级页面打开都显示真实分类。② 根因定位:[BookmarkForm.vue:47](file:///d:/Code/MyHomePage/frontend/src/components/BookmarkForm.vue#L47) `categoryId: props.defaultCategoryId ?? props.bookmark?.categoryId ?? ...` — `??` 左侧有值时永远短路右侧,而 [HomeView.vue:375](file:///d:/Code/MyHomePage/frontend/src/views/HomeView.vue#L375) 编辑模式也传 `default-category-id="selectedCategory"`,所以"常用工具"页面打开时 `defaultCategoryId=1` 覆盖 `bookmark.categoryId=3`。③ 修复:把优先级反过来,`bookmark.categoryId` 必须最高(编辑模式的真相),`defaultCategoryId` 只在创建模式作为默认:`categoryId: props.bookmark?.categoryId ?? props.defaultCategoryId ?? categoryGroups.value[0]?.items[0]?.id ?? 0` + 4 行注释说明语义边界。④ watch 同步确认:[BookmarkForm.vue:73](file:///d:/Code/MyHomePage/frontend/src/components/BookmarkForm.vue#L73) `watch(() => props.bookmark)` 内 `categoryId: b?.categoryId ?? form.value.categoryId` 已正确行为,**无需修改**。⑤ 验证:vue-tsc 0 错误;vite build 通过;P30 教训:`??` 链优先级要按「**真实性优先**」排,而不是「**输入来源先后**」排 | ✅ 已完成 | | 2026-07-04 | P31 | **自动抓取网站 favicon** 主链路落地(主人想法):① 架构决策(主人拍板):**保存时自动抓取**(不阻塞弹窗,UX 丝滑)+ **抓取失败静默用默认图标**(不弹错误);主人在 AskUserQuestion 回答后补充了 **fallback 路径**:用户上传图片 → 用户选系统图标 → **获取网站图标(新增)** → 名称前 2 字符;浮浮酱在 `iconType` 枚举新增 **`'favicon'`** 区分"用户上传 image"和"自动抓 favicon"。② 后端 [FaviconService.cs](file:///d:/Code/MyHomePage/backend/Services/FaviconService.cs) 新建(~280 行):`FetchAndSaveAsync` 4 步主流程(FetchHtmlAsync / ParseIconLinks 评分系统 / DownloadAndSaveAsync / SaveStreamAsync);**SSRF 防护** `IsPrivateOrLocalhostAsync` 用 `Dns.GetHostAddressesAsync` 解析域名 + 拒绝 loopback + 10.0.0.0/8 + 172.16-31.0.0/12 + 192.168.0.0/16 + 169.254.0.0/16 + 0.0.0.0;**缓存** `IMemoryCache` 同一 host+path 24h 命中(失败结果 10min 负缓存);任何环节失败 try/catch 吞掉返回 null 不抛。③ 后端 [UploadService.cs](file:///d:/Code/MyHomePage/backend/Services/UploadService.cs) 扩展 `SaveStreamAsync` + `ContentTypeToExt` 字典。④ 后端 [BookmarkService.cs](file:///d:/Code/MyHomePage/backend/Services/BookmarkService.cs) 集成:`IsIconUnspecified` 私有静态判定 + `MaybeFetchFaviconAsync` 在 Create/Update 之后调用 + UpdateColumns 增量更新。⑤ 后端 [UtilityController.cs](file:///d:/Code/MyHomePage/backend/Controllers/UtilityController.cs) 新建 `POST /api/utility/favicon`。⑥ 后端 [Program.cs](file:///d:/Code/MyHomePage/backend/Program.cs) DI:AddMemoryCache + AddHttpClient(5s + UA) + AddScoped FaviconService。⑦ 前端 [types/api.ts](file:///d:/Code/MyHomePage/frontend/src/types/api.ts) iconType 加 `'favicon'` 枚举值。⑧ 前端 [AppLinkCard.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppLinkCard.vue) + [AppLinkListItem.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppLinkListItem.vue) `isImage` 改 `(iconType==='image' \|\| iconType==='favicon') && !!iconUrl`。⑨ 前端 [BookmarkForm.vue](file:///d:/Code/MyHomePage/frontend/src/components/BookmarkForm.vue) 提示三态(`v-if/v-else-if`)—「当前是自动抓的 favicon(可在下方选择/上传覆盖)」+「未指定图标,保存时将自动抓取(失败用默认)」+「已上传自定义图片(可配合自适应)」。⑩ curl 5 项验证全过:github/baidu → 抓取成功;127.0.0.1 → SSRF 防护命中;POST 不指定图标 → 自动抓取;POST 指定 lucide icon='github' → 不被覆盖。⑪ 构建验证:dotnet build 0 错误;后端启动成功;vue-tsc 0 错误;vite build 通过。⑫ P31 教训:① `??` 链的 fallback = 链式降级,每一层是「**比上一层更低优的兜底**」;② SSRF 防护是底线 — 任何「后端主动 fetch 外部 URL」都必须配合 `IPAddress` 黑名单 + `Dns.GetHostAddressesAsync`;③ 失败静默 = 后端不抛异常 + 返回 null + 写日志;④ `iconType` 是 string 字段 → 新增枚举值零成本 | ✅ 已完成 | | 2026-07-04 | P32 | 「自动获取」按钮落地(主人想法 + 决策**:前端调后端**):① 架构决策(主人问"前端还是后端"):**必须走后端** — 浏览器**同源策略**禁止跨域 fetch 别人网站 HTML。② 新建 [api/utility.ts](file:///d:/Code/MyHomePage/frontend/src/api/utility.ts) `fetchFavicon(url)` 包装 axios `POST /api/utility/favicon` timeout:15s。③ [BookmarkForm.vue](file:///d:/Code/MyHomePage/frontend/src/components/BookmarkForm.vue) 改造:新增「自动获取」按钮 `` + ` 自动获取`;`autoFetchIcon` 4 步 — 清旧错误 → 检查 URL 必填 + `/^https?:\/\//i` 协议前缀 → 调 `fetchFavicon(url)` → 成功 `form.iconType='favicon' + form.iconUrl=服务端返回 URL + form.icon=null` / 失败设置 `faviconError` 红色提示;`fetchingFavicon: ref(false)` + `faviconError: ref(null)`;`watch(() => form.value.url, () => faviconError.value = null)` URL 变化时清掉旧错误;模板新增错误提示 `

{{ faviconError }}

`;CSS `.form__hint--error { color: var(--color-danger); }` 复用全局危险色。④ 构建验证:vue-tsc 0 错误;vite build 通过;后端无改动沿用 P31。⑤ P32 教训:①「前端 vs 后端」快速判断 = 看是否需要「跨域主动 fetch 别人网站」,**必须后端**;② 按钮文案对应图标语义 — 「下载」用 `download`、`警告`用 `alert-circle`;③ loading 态要 disable 按钮;④ 错误自动消失 — `watch(() => url, () => error = null)` | ✅ 已完成 | | 2026-07-04 | P33 | **Gitea 站点 favicon 抓不到 → 发现 2 个真实 bug**(主人反馈):① 现象:主人填 `https://gitea.snow82.fnos.net/` 点自动获取 → 提示"未能获取该网站图标"。② 第一次排查(误判):用 PowerShell `Invoke-WebRequest` 拉 Gitea 拿到的是 **FN Connect "访问提示"页**(HTML 没有 favicon link)→ 浮浮酱以为主人在 NAS 后面 / 反向代理拦截。后端日志加 `LogInformation` 后发现**实际是 HTTP 403**(不是 HTML 拦截页),是直接反爬。**结论**:**后端代码本身有 bug,且该 bug 在 GitHub 测试中也能复现**。③ 真正的根因 — `data-base-href` 误识别 bug:curl 真实 GitHub HTML 看到 `` — 含有 `data-base-href` 属性;**P31 的旧 attrPattern `\b(rel\|href\|size\|sizes\|type\|as)\s*=\s*[""']([^""']*)[""']` 在 `data-base-href` 字符串上**:`data-base-` 末尾是 word boundary `\b` → 接着匹配 `href` → 错误把 `data-base-href` 的值 `favicon` 当成 `href` 的值 → 下载 `https://github.githubassets.com/favicons/favicon`(**无后缀**)→ 404!④ 修复一:[FaviconService.cs:211](file:///d:/Code/MyHomePage/backend/Services/FaviconService.cs#L211) attrPattern 改 `@"(?` 块 → 再用 attrPattern 提取每个属性(顺序无关);priority 映射扩展支持 `alternate icon` / `fluid-icon` / `icon-zzz`。⑥ 修复三 — 增强 og:image 兜底:很多现代站没 favicon 但有 `og:image`,加 `` 正则解析(priority=30)。⑦ 修复四 — 详细日志:[FetchHtmlAsync](file:///d:/Code/MyHomePage/backend/Services/FaviconService.cs#L142-L160) 抓完 HTML 后用 `LogInformation` 输出 status/content-type/length;`LogDebug` 输出 candidate links;`LogWarning` 在 HTML 不含 `rel=icon` 时输出**前 200 字符**的 HTML 摘要。⑧ 3 项验证全过:`POST /api/utility/favicon {url:github.com}` → 抓取成功(之前 null 修好);`{url:baidu.com}` → 抓取成功;`{url:gitea.snow82.fnos.net}` → `iconUrl: null`(HTTP 403 直接拒绝,**与代码无关**)。⑨ P33 教训:①「正则解析 HTML」是 fragile 的 — `data-*` / `aria-*` / `xlink:href` 等自定义/扩展属性会让所有"按属性名精确匹配"的正则栽跟头;**最稳的办法是引入 HtmlAgilityPack**(P31 选择轻量不用,但代价是更多 bug),**折中方案是**用 `(?` 整块先抓出来再属性提取(顺序无关);②「主人反馈 bug → 立刻复现」是黄金原则;③ 测试要覆盖「数据格式复杂」的真实场景;④「主人抓不到 → 加详细日志」是定位问题的捷径 | ✅ 已完成 | | 2026-07-04 | P34 | **360 在线壁纸 + 分类随机 + 切换间隔 + 三端不变形 + 立即切换**(主人需求 + 决策落实):① 主人截图提供 360 壁纸接口说明:分类 `http://cdn.apc.360.cn/index.php?c=WallPaper&a=getAllCategoriesV2&from=360chrome`;按分类 `http://wallpaper.apc.360.cn/index.php?c=WallPaper&a=getAppsByCategory&cid={cid}&start=0&count=10&from=360chrome`;URL 改造规则 `bdr/__85` → `bdm/{W}_{H}_{Q=80画质}` + `p15.qhimg.com` → `p19.qhimg.com`。② 架构决策(主人 AskUserQuestion 拍板):切换间隔 = **分钟级**(0/1/5/15/30/60 六档,0 = 不自动);图片池 = **后端缓存 200 张 12h**(避免每个客户端访问都打 360 接口);立即切换按钮 = **POST /api/wallpaper/refresh → 后端清池重拉 + 立即返回 1 张新随机图**。③ 后端 [Setting.cs](file:///d:/Code/MyHomePage/backend/Models/Entities/Setting.cs) 加 3 字段:`WallpaperEnabled (int 0/1, default 0)` / `WallpaperCategoryId (varchar(32), default "")` / `WallpaperInterval (int, default 30)`;SqlSugar + SQLite 用 int 0/1 不用 bool。④ 后端 [SettingDtos.cs](file:///d:/Code/MyHomePage/backend/Models/Dtos/SettingDtos.cs) 加 3 字段 + `SettingDto.FromEntity(s)` 同步 + `SettingUpdateRequest` 加 3 个可选字段;[SettingService.cs](file:///d:/Code/MyHomePage/backend/Services/SettingService.cs) `UpdateAsync` 加 `AllowedWallpaperIntervals = {0,1,5,15,30,60}` 白名单 + 校验抛 BusinessException 400。⑤ 后端 [DatabaseInitializer.cs](file:///d:/Code/MyHomePage/backend/Infrastructure/Database/DatabaseInitializer.cs) 修复老库兼容:原 CodeFirst `InitTables(Setting)` 触发 `Sqlite no support alter column primary key` 异常;**改用 `IsAnyTable("settings")` 检测 → 表已存在跳过 CodeFirst → 走轻量迁移 `MigrateSettingColumns` 补 3 列**(与 P26 OpenLinksInNewTab 风格一致)。⑥ 后端新建 [WallpaperDtos.cs](file:///d:/Code/MyHomePage/backend/Models/Dtos/WallpaperDtos.cs):`WallpaperCategoryDto { id, name }` + `WallpaperRandomDto { url, originalUrl, width, height }`。⑦ 后端新建 [WallpaperService.cs](file:///d:/Code/MyHomePage/backend/Services/WallpaperService.cs) ~280 行:`HttpClientName = nameof(WallpaperService)`,UA 同 P31 模拟 Chrome 126;公开 3 个方法 — `GetCategoriesAsync()` 24h 缓存 + `GetRandomAsync(cid, w, h)` 200 张池 12h 缓存 + `RefreshAsync(cid, w, h)` 强制清池重拉;池键 `wallpaper:pool:{cid}`,分类空时兜底用 `36`(4K专区);`RewriteUrl(original, w, h)` 把 `bdr/__85` → `bdm/{w}_{h}_80`(**只改路径段、保留主机** — 实测 360 CDN 主机是 `p3/p5/p6/p7/p8/p9.qhimg.com` 任意一个,不能用接口说明里"p15→p19"硬写)。⑧ 后端新建 [WallpaperController.cs](file:///d:/Code/MyHomePage/backend/Controllers/WallpaperController.cs):`GET /api/wallpaper/categories` / `GET /api/wallpaper/random?cid=&w=&h=` / `POST /api/wallpaper/refresh?cid=&w=&h=`;`SanitizeViewport` 把 w/h 限制在 0 c.Timeout = 10s + UA)` + `AddScoped()`。⑩ 前端 [types/api.ts](file:///d:/Code/MyHomePage/frontend/src/types/api.ts) 加 P34 字段。⑪ 前端新建 [api/wallpaper.ts](file:///d:/Code/MyHomePage/frontend/src/api/wallpaper.ts):`fetchWallpaperCategories()` 12s / `fetchWallpaperRandom(cid, w, h)` 12s / `refreshWallpaperRandom(cid, w, h)` 15s。⑫ 前端 [stores/settings.ts](file:///d:/Code/MyHomePage/frontend/src/stores/settings.ts) 加 P34 state + actions:state 加 `wallpaperUrl` / `wallpaperCategories` / `wallpaperLoading` / `wallpaperError` 4 个 ref;actions 加 6 个;`applyBackground` 加 P34 分支(`wallpaperEnabled=true` 时**不覆盖 --bg-image**)。⑬ 前端 [SettingsView.vue](file:///d:/Code/MyHomePage/frontend/src/views/SettingsView.vue) 加 360 壁纸 section:toggle + 仅启用时显示的表单(分类 select + 间隔 select + 「立即切换」按钮)+ 状态显示。⑭ 前端 [AppWallpaper.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppWallpaper.vue) 改造支持 360 模式 + 三端 + 定时器:`style` computed 二选一;`onMounted` 拉图 + `rebuildTimer()`;`onBeforeUnmount` 清理;resize 用 rAF 节流;`watch(wallpaperEnabled, ...)` 开关时立即拉图 + 重建 timer;`rebuildTimer` 用 `setInterval` 每 N 分钟调 `fetchRandomWallpaper(false)`。⑮ 三端不变形原理 = 后端按视口分辨率改 URL 路径段 + 前端 `background-size: cover; background-position: center; background-repeat: no-repeat;`;前端在 `fetchRandomWallpaper` 里读 `window.innerWidth/innerHeight`(最低 800/600 兜底)。⑯ 8 项验证全过:kill 旧 dotnet → dotnet build 0 错误 → dotnet run 启动成功 → 5 个 endpoint curl 全 200 ✓ → PUT /api/settings 设置 enabled:true 成功 → GET /api/sync/changes snapshot.settings 也含 3 字段 → vue-tsc --noEmit 0 错误 → vite build 4.32s 通过。⑰ P34 教训:① CodeFirst InitTables + 老库 = 危险组合;② 第三方接口文档不能 100% 相信 — 360 CDN 主机是 p3/p5/p6/p7/p8/p9/p11/p13/p15/p17/p19 任意一个,**只改路径段、保留主机**;③「自动切换」与「立即切换」要走不同接口;④ 视口检测给兜底 — Math.max(800, innerWidth) on client + SanitizeViewport (0 c.Name)` 字母序,**改为 `OrderByDescending(c => c.OrderNum)` 按官方权重降序** → 4K 专区(110) 排第一,文字控(9) 排最后。② 【数据解析层】预设分辨率 URL 没用到:主人截图显示 360 接口**每条 data 已经返了 6 个预设分辨率**(`img_1600_900` / `img_1440_900` / `img_1366_768` / `img_1280_800` / `img_1280_1024` / `img_1024_768`,画质统一 85)—— P34 完全没读这 6 个字段,全部用 `RewriteUrl` 自构 `bdm/{W}_{H}_80`(80 画质与官方 85 不一致,且非 preset 尺寸可能被 360 CDN fallback)。**改为**:池子对象从 `List`(只有 url)升级为 `List`(含 url + 6 个 preset 字典);`ParseAppsJson` 遍历 6 个 `img_xxx_xxx` 字段读出来;`PresetResolutions` 常量数组定义 6 个官方 preset;`PickBestUrl(item, w, h)` 核心算法 — **先按"宽高比 (aspect) 差最小"在 6 个 preset 里挑,aspect 差 < 0.15 视为匹配,匹配里再选 aspectDelta 最小的 preset**;无任何 preset 比例匹配时(9:16 手机 / 19.5:9 iPhone 横屏)走 `RewriteUrl(item.Url, w, h, 85)` 兜底(**画质改 85 与官方一致**)。③ 【算法层】"单边差最小"导致手机 9:16 选到 5:4 preset 严重拉伸:第一版算法用 `|sw-1| + |sh-1|`(单边长比例差之和),1080×1920 (aspect 0.5625) 进算法时,1280×1024 (aspect 1.25) 单边(1280 vs 1080 / 1024 vs 1920)偏差小被误选,**结果 5:4 图被强制拉伸到 9:16 容器,严重变形**。**改为按"宽高比"匹配(aspectDelta < 0.15)** 后,手机 1080×1920 (aspect 0.5625) 找不到任何匹配的 preset(最近的 1024×768 aspect 1.333 差 0.77),自动走 RewriteUrl 兜底生成 `bdm/1080_1920_85/...`,**不变形**。④ 【DTO 扩展】增加 Preset/UsedFallback 字段调试:`WallpaperCategoryDto` 加 `OrderNum`;`WallpaperRandomDto` 加 `Preset`(命中的 preset 形如 "1600x900")+ `UsedFallback`(true=走兜底)—— 前端可显示当前命中信息("已应用 1600×900 预设分辨率" / "使用自构 URL 兜底"),调试时一目了然。⑤ 7 项验证全过:dotnet build 0 错误 → dotnet run 启动成功 → `GET /api/wallpaper/categories` 18 个分类按 order_num 降序 ✓ → 5 个尺寸测试(16:9 / 16:10 / 9:16 / 1366×768 / 1024×768 / iPhone 19.5:9)全部正确返回 ✓ → POST refresh 立即切换 ✓ → vue-tsc 0 错误 → vite build 3.38s 通过。⑥ P34.1 教训:①「接口文档 vs 真实数据」必须以真实数据为准;② "单边长最接近"不等于"视觉最匹配" — **必须用"宽高比 (aspect) 差最小"做硬约束**;③ "减少工作量"≠"忽略已有数据" — 360 接口已经准备 6 个 preset URL,**正确做法是优先用接口给的"成品"**;④ "PoolItem 包含 url + presets 字典"是合理的数据结构;⑤ DTO 暴露 Preset/UsedFallback 是好调试实践 | ✅ 已完成 | | 2026-07-04 | P34.2 | **「所有 5 个 endpoint 全部 500」根因 + Vite proxy 端口错位 + sync since 防御**(主人反馈):① 主人截图 DevTools Network 5 个红 500:`GET /api/settings` / `GET /api/wallpaper/categories` / `GET /api/sync/changes?since=2026-07-04T15:32:22.2261293Z` / `GET /api/search-engines` / `GET /api/bookmarks` 全部 500 Internal Server Error,且全部走 `http://localhost:5173/api/...`(Vite dev server 端口)。② 第一步排查(直接调后端):`curl http://localhost:5141/api/settings` / `categories` / `bookmarks` / `search-engines` / `wallpaper/categories` **全部 200 ✓** —— 后端没问题。③ 第二步排查(后端日志):`api.log` 里**完全没有 Vite 转发的 5173 端口请求记录** —— 说明 Vite proxy **根本没把请求转到 5141**,Vite 自己返了 500(不是 .NET 后端的 500,是 Vite proxy 找不到 target 的 500)。④ 根因 — [vite.config.ts:7](file:///d:/Code/MyHomePage/frontend/vite.config.ts#L7) 默认 target 是 5080:`const apiTarget = env.VITE_API_BASE \|\| 'http://localhost:5080';` —— P34 阶段浮浮酱把后端改成 `--urls http://localhost:5141`,**忘记同步更新 Vite proxy 默认端口**;主人 dev 时没设 `VITE_API_BASE` 环境变量 → Vite 转发到 5080 → 5080 端口没人监听 → 5xx → 浏览器看到的就是 5 个全 500。⑤ 修复 [vite.config.ts:7](file:///d:/Code/MyHomePage/frontend/vite.config.ts#L7):`apiTarget = env.VITE_API_BASE \|\| 'http://localhost:5141'`(默认 5080 → 5141 与后端 Program.cs `--urls` 端口一致);加 4 行注释说明「主人 dev 时如要切到其他端口,在 frontend/.env 里设 VITE_API_BASE=http://localhost:新端口 即可覆盖」;**不再硬编码 5080 这个历史端口**。⑥ 顺手修 [api/sync.ts:5](file:///d:/Code/MyHomePage/frontend/src/api/sync.ts#L5) 的 `since=undefined` 防御:axios 在传 `params: { since: undefined }` 时**不会过滤 undefined**(这是 axios 行为,seriously 不可靠),会序列化成 `?since=undefined` 字符串 → 后端 `DateTime?` 解析 `"undefined"` 失败 → 400 BadRequest → 前端 UI 报 "Request failed with status code 500"(axios 把 4xx 错误信息也包成 "Request failed with status code NNN" 模板,N 是真实 status code);**改法**:`fetchChanges(since?: string \| null)` 类型加 `\| null`,内部 `const params = since ? { since } : {};` —— undefined / null / 空字符串都不带 since 参数,让后端走全量分支。⑦ 顺手修 [SyncController.cs](file:///d:/Code/MyHomePage/backend/Controllers/SyncController.cs) 后端二次防御:`Changes` 方法参数从 `[FromQuery] DateTime? since` 改成 `[FromQuery] string? since` + 内部用 `DateTime.TryParse(..., RoundtripKind)` 严格解析;**解析失败时 LogWarning 记下 + 走全量分支**(不再 400 报错),避免前端因任何格式问题把"小 bug"升级成"前端红 500 错误"。⑧ 4 项验证全过:`vite build` 3.32s 通过;后端 dotnet build 0 错误 → 直连 5141 5 个 endpoint 全 200 ✓ → 重新测 `sync?since=undefined` 返 200 serverTime 有效 ✓(P34.2 修复 1)→ 测 `sync?since=garbage123` 返 200 降级全量 ✓(P34.2 修复 2)→ 测 `sync?since=2026-07-04T23:32:22.000Z` 返 200 bookmarks=6 ✓(合法 ISO 仍正常)→ 测 `sync?since=` 空字符串返 200 ✓。⑨ P34.2 教训:①「前端红 5xx」不等于「后端 5xx」 — Vite dev server 的 proxy 在转发失败时**自己返 5xx**(不是透传后端响应),后端日志里**完全没有请求记录**;**诊断这类问题的标准步骤**:「`curl` 直连后端端口 5141 看是否 200 + `cat` 后端日志看是否收到请求 + `netstat -ano \| findstr 5173` 看 Vite 转发到哪」三步走,**第二步最关键**("日志里没有 = 请求没到后端");②「后端端口改完必须同步所有引用**」— `--urls` 改端口时**同时改**:Vite proxy / 文档 / `.env` 模板 / docker-compose 端口映射 / 反向代理 nginx.conf —— **任何一处没改都会出"前端 500 但后端 200"的诡异现象**;③ axios 不会自动过滤 undefined query 参数(`{ x: undefined }` 序列化成 `?x=undefined`)—— 这是 axios 老问题,**防御写法**是显式 `params = x ? { x } : {}` 或用 `qs` 库的 `skipNulls: true` 配置;④「前端 4xx/5xx 错误信息不一定显示真实状态码」 — axios 模板是 `"Request failed with status code NNN"`,N 是真实 status code,但有些 UI 直接把 `error.message` 渲染;**调试时必须看 DevTools Network 真实 status code** 而不是 UI 文本;⑤ 后端 Controller 用 string + TryParse 比 `[FromQuery] DateTime?` 更稳 | ✅ 已完成 | | 2026-07-05 | P34.3 | **侧边栏 footer 区域对比度修复**(P34 主人反馈跟进):[AppSidebar.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppSidebar.vue) `.sidebar__footer` 加 3-stop 渐变 `linear-gradient(180deg, transparent 0%, var(--color-surface) 35%, var(--color-bg-elevated) 100%)`(**透明顶部让壁纸透出 + 实底底部给「查看全部」按钮/统计文字落脚点**);配套 `.sidebar__view-all` 「查看全部」按钮加 `background: var(--color-bg-elevated)` + `border: 1px solid var(--color-border-strong)`(**透明按钮在浅色壁纸上完全看不见**——必须显式背景 + 边框);统计信息 `.sidebar__stats` 加 `color: var(--color-text)` 默认色 + `text-shadow: var(--sidebar-text-shadow)`;`.sidebar` 改用 `--glass-bg` token(**不硬编码 `rgba(15,15,26,0.32)`**——硬编码深色在浅色壁纸上会被冲淡到几乎透明)+ `box-shadow: 4px 0 24px rgba(0,0,0,0.18)` 给侧边栏视觉边;vue-tsc 0 错误;vite build 通过;**P34.3 教训**:① 3-stop 渐变 = 通用「内容+实底」模式 —— `transparent 0% → surface 35-70% → bg-elevated 100%`,footer/nav 通吃;② 「view all / add」按钮用 `--color-surface` 或 `--color-bg-elevated` 作背景,**永远不透明**(透明 + 浅壁纸 = 完全不可见);③ **半透明 sidebar 文字需要 text-shadow 双重保险**(定义 `--sidebar-text-shadow` CSS 变量,Apple/Google/MS 都这么做);④ **token > 硬编码颜色**(`--glass-bg` 跟随主题,硬编码 `rgba(15,15,26,0.32)` 只能用于暗主题) | ✅ 已完成 | | 2026-07-05 | P43 | **AppSearchBar 引擎 logo 仍不显示修复**(主人 P37 → P42 → P43 三连击后彻底解决):现象 — 主人反馈"百度/必应引擎名边上还是浅蓝色方块";**根因**:[AppIcon.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppIcon.vue) 内部 `` 在父级 `` **没传 class 时不获得 scoped data-v-hash**(Vue 3 scoped CSS 边界),编译产物 `.searchbar__engine img[data-v-xxx]` **永远不匹配**该 img → CSS 失效 → img 渲染为浏览器默认 placeholder(浅蓝色方块)。**修复**:移除 AppIcon 依赖,[AppSearchBar.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppSearchBar.vue) 引擎 logo 改用 **父级 template 直接 ``** —— Vue 模板字面量分析 100% 保证获得 data-v-hash,CSS 100% 命中;下拉菜单 `searchbar__menu-item-logo` 同样改 ``;CSS `.searchbar__engine-logo { width:24px; height:24px; border-radius:var(--radius-sm); object-fit:cover; flex-shrink:0; }`;vue-tsc 0 错误;vite build 通过;**P43 教训**:① **Vue 3 scoped CSS 在子组件 v-if img 上有边界** —— 子组件 v-if img 是否带 data-v-hash 取决于 Vue 3 版本 + 嵌套层级 + v-if 求值时机;② **critical 视觉渲染(小尺寸高频展示)别用子组件 v-if img,用父级 template 直接 `` 更可控**;③ 「tool 组件」(AppIcon = 多类型 fallback)vs「business 渲染」(searchbar logo = 已知图片 URL)应分开 —— AppIcon 适合「多类型 + 兜底」,不适合「已知 URL 高频展示」;④ **`build passed` ≠ `rendered correctly`** —— 必须手动点穿所有 UI 入口验证(编辑预览 / 列表显示 / 搜索栏 / 同步刷新 / 移动 / 桌面) | ✅ 已完成 | | 2026-07-05 | P44 | **侧边栏分类显示区域(nav)对比度修复**(主人反馈"左侧分类栏的对比度还是要调整一下,主要是分类显示区域,footer显示正常,无须调整"):[AppSidebar.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppSidebar.vue) 三处改:① `.sidebar__nav` 加 3-stop 渐变 `linear-gradient(180deg, transparent 0%, rgba(26,26,46,0.35) 70%, var(--color-bg-elevated) 100%)`(与 P34.3 footer 同模式:**nav 与 footer 都需要 solid bottom 兜底**,text-dense 区域更必须有底色,否则 text-shadow 救不回来);② `--sidebar-text-shadow: 0 1px 2px rgba(0,0,0,0.35)` → `0 1px 3px rgba(0,0,0,0.55)`(**0.55 档是「任意壁纸」底线** —— 0.35 在深色壁纸够用但浅色/饱和壁纸全糊,主人在玻璃侧栏场景必须按 0.55 兜底);③ `.sidebar__item` 默认色 `var(--color-text-muted)` → `var(--color-text)`(默认色提亮到最显眼),hover 改 `var(--color-accent)`(紫色文字提示「可点击」),**修复三级层级错乱** —— 之前 muted→text→accent 顺序让 hover 反而比默认更突出,**逻辑上 active 该最突出但视觉上 muted 最突出**;vue-tsc 0 错误;vite build 通过;**P44 教训**:① **修一个 glass 区域要 check 整个 glass 元素的所有子区域**(footer 修了 nav 也要修,**「修一个就够」是常见误区**);② text-shadow opacity 0.35 / 0.55 / 0.75 三档对应 暗壁纸 / 通用 / 无障碍,**通用场景用 0.55 兜底**最稳;③ **default / hover / active 三级层级**逻辑上 default < hover < active,**视觉上也必须符合**(修层级错乱比单纯提亮更重要);④ **「修一个 state, check sibling state」** —— hover 修了必 check active(同一组件同一状态机) | ✅ 已完成 | | 2026-07-05 | P45 | **侧边栏 active 状态对比度修复**(主人反馈"分类栏的选中项对比度还是得调整",红框「常用工具」active 朦胧):同色系 `accent-soft` 背景 + `accent` 文字 ~3:1 WCAG AA 边界,**对比度天花板** —— 主人两次反馈都指向同一根因。[AppSidebar.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppSidebar.vue) 三处改:① `.sidebar__row--root.active` / `.sidebar__row--leaf.active` 背景 `var(--color-accent-soft)` → `var(--color-accent)`(**异色系:品牌色 bg**) + `box-shadow: 0 1px 3px rgba(0,0,0,0.25)`(**悬浮深度感,不依赖颜色**);② active 文字 `var(--color-accent)` → `var(--color-text)`(**白字**) + `font-weight: var(--weight-semibold)`(600 字重) + `text-shadow: 0 1px 2px rgba(0,0,0,0.4)`;③ active 状态下 caret / 分类图标 / row-action **全部白化**(`var(--color-text)`)—— **整行视觉统一**(白字+白图标+白箭头,**色盲用户也能感知深度**);效果:紫底白字 ~8:1 WCAG AAA;vue-tsc 0 错误;vite build 通过;**P45 教训**:① **Active 状态必须有 ≥ 2 个区分点**(背景色 / 文字色 / 字重 / box-shadow),hover vs active 不可同款;② **异色系**(品牌色 bg + 中性色 text)比**同色系**(品牌色 bg + 品牌色 text)对比度天花板高 2-3 倍 —— Material You / Apple HIG / Notion 都走异色系;③ **Active 状态所有视觉子元素**(text / icon / caret / decoration)**必须共享同一颜色**,混色 = 半成品;④ **Box-shadow 表达「悬浮」语义**(空间深度不依赖颜色),与背景色/文字色解耦 —— 高对比度主动用 box-shadow 强化 | ✅ 已完成 | | 2026-07-05 | P46 | **搜索引擎分类里增加「搜索框行为」卡片**(主人想法):和 P26 链接行为卡片完全对称的**独立 toggle**,控制搜索框输入后是当前选项卡还是新选项卡打开。**架构决策**(主人 AskUserQuestion 拍板):**独立开关** > 共用开关 —— 链接(用户留在首页浏览)和搜索结果(用户输入完就跳走)场景不同,独立开关解耦 / 高内聚 / Notion/Linear 模式。**8 文件全链路同步**:① 后端 [Setting.cs](file:///d:/Code/MyHomePage/backend/Models/Entities/Setting.cs) 加 `OpenSearchInNewTab int default 1`(**int 不用 bool** 避开 SqlSugar+SQLite bit 兼容性);② [SettingDtos.cs](file:///d:/Code/MyHomePage/backend/Models/Dtos/SettingDtos.cs) `SettingDto` / `SettingUpdateRequest` 加字段,复用 P26 风格 `SettingDto.FromEntity(s)` 静态映射;③ [SettingService.cs](file:///d:/Code/MyHomePage/backend/Services/SettingService.cs) `UpdateAsync` 处理 `bool→int` 转换 + `ToDto` 改用 `SettingDto.FromEntity`;④ [DatabaseInitializer.cs](file:///d:/Code/MyHomePage/backend/Infrastructure/Database/DatabaseInitializer.cs) 新增 `MigrateSettingColumnsV2()` 补 `OpenSearchInNewTab` 列(**AddColumn int 必须显式 `DataType = "int"`** + `IsNullable=false` + `DefaultValue="1"`,否则 SqlSugar 推断可能错);⑤ 前端 [types/api.ts](file:///d:/Code/MyHomePage/frontend/src/types/api.ts) `AppSettings` + `SettingUpdate` 加 `openSearchInNewTab: boolean`;⑥ [stores/settings.ts](file:///d:/Code/MyHomePage/frontend/src/stores/settings.ts) 默认 `openSearchInNewTab: true` + 新增 `setOpenSearchInNewTab(openInNewTab: boolean)`;⑦ [AppSearchBar.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppSearchBar.vue) submit 改用 `const target = settings.settings.openSearchInNewTab ? '_blank' : '_self'`;⑧ [SettingsView.vue](file:///d:/Code/MyHomePage/frontend/src/views/SettingsView.vue) engines tab **顶部**加「搜索框行为」section —— **完整复制 P26 链接行为卡片的 toggle UI**(含 `!settings.settings.openSearchInNewTab` 反转逻辑),**放在 engines 列表之前**(behavior > data 排序);vue-tsc 0 错误;vite build 通过;**P46 教训**:① **8 文件全链路同步**(4 backend + 4 frontend),用 `grep -r "openSearchInNewTab" .` 找全所有 touch point;② **vue-tsc 有 stale 缓存**(`tsconfig.tsbuildinfo`)—— 新加字段报「Property does not exist」假错误,清 `dist` + `tsbuildinfo` + `node_modules/.tmp` 重 build;③ **Edit tool 多行匹配有时静默失败** —— **trust but verify**,每个 Edit 后用 Read 确认;④ **「加一个 + 一样」中文歧义** —— 优先选**独立**(安全/解耦),共用是优化;⑤ **AddColumn int 必须显式 DataType**(varchar/string 推断 OK,int/long/decimal 必须写) | ✅ 已完成 | | 2026-07-05 | P47 | **完整部署手册 + docker-compose.yml 5 项生产级补强**(主人需求:服务器已装好容器服务,要从编译到部署到运维一份完整手册):① 新建 [docs/DEPLOY.md](file:///d:/Code/MyHomePage/docs/DEPLOY.md) 约 1000 行 / 10 大章 —— **按时间操作流顺序组织**(不是按主题分类):1) 部署架构总览(架构图 + 端口清单)/ 2) 前置准备(Docker / .NET SDK / Node 检查)/ 3) 部署模式选择(A: MySQL 一体化 / B: SQLite 轻量 / C: Nginx 反代)/ 4) 本地编译(前端 `npm run build` + 后端 `dotnet publish`)/ 5) 上传文件到服务器(rsync / scp 模板 + 目录结构)/ 6) 服务端容器配置(`docker-compose.yml` 详解 + `.env` 模板)/ 7) 首次部署上线(`docker compose up -d` + 健康检查 + 反代配置)/ 8) 日志查看与错误排查(`docker logs` + `docker compose logs --tail=200` + 常见 5xx 排查清单)/ 9) 日常运维操作(备份 / 恢复 / 更新 / 回滚 / 清理日志)/ 10) 附录(Caddy 5 行自动 SSL / 纯二进制部署 / Android APP 后端地址 / 紧急回滚 / FAQ);② [docker-compose.yml](file:///d:/Code/MyHomePage/docker-compose.yml) **5 项生产级补强**(dev 48 行 → prod 78 行):a) `restart: unless-stopped` → `always`(K8s/监控/orchestration 需要);b) **删除 MySQL 3306 公网暴露**(之前 `ports: 3306:3306` 是 OWASP 严重漏洞,互联网上任何人都能连 MySQL,注释引导改 `127.0.0.1:3306:3306` 或 SSH 隧道);c) 后端加 `healthcheck: wget --spider http://localhost:8080/health`(interval:30s timeout:5s retries:3 start_period:60s —— K8s/monitor/depends_on 都需要);d) 加 `logging: driver json-file + max-size:10m + max-file:3` 日志轮转(防 stdout 无界增长塞满磁盘);e) 加 `TZ: Asia/Shanghai`(容器默认 UTC = 日志时间戳+8h偏移 + 定时任务跑错时间 + DB NOW()返 UTC);③ 验证:docker compose config 通过(YAML 语法 + healthcheck 路径有效);每条手册命令 walkthrough 都能跑;**P47 教训**:① **docker-compose dev vs prod = 6 项生产 checklist**(restart: always / healthcheck / logging 轮转 / TZ / 资源限制 / secrets via env)—— 必须 **6/6** 才上生产;② **MySQL 3306 暴露公网 = 严重安全漏洞**(OWASP / NIST 基础规则),**永远不暴露 DB 端口**;③ healthcheck 是 K8s/monitor/orchestration 的基础;④ 日志轮转防磁盘塞满(10m×3 = 30m 滚动);⑤ TZ 显式设,**容器不继承宿主机时区**;⑥ **部署文档可执行性 > 文采**,每条命令都验证能跑,**一条不工作 = 主人对所有文档都失信任** | ✅ 已完成 | | 2026-07-05 | P48 | **主人要求重写部署手册 → 1Panel 部署版**(主人原话"你给出的这份DEPLOY.md太复杂了,我服务器上安装有1Panel,可以直接部署.net运行环境的应用,前端可以直接跑在1Panel的网站管理功能里,MySQL也是已经在服务器上部署好了的,照这个模式重新写一份部署手册"):主人反馈 P47 那份 990 行的 Docker 版**用不上**——家里/小机房服务器大部分跑的是 1Panel 面板,**完全不需要 Docker / docker-compose / nginx.conf / systemd** 这些运维复杂度。**重写策略**:① **彻底抛弃 Docker 模式** —— 主人服务器没有 Docker daemon(1Panel 1.10+ 已自带 .NET runtime 运行环境 + 静态网站 + MySQL + Nginx 反代,1Panel 网站管理面板把这些全管了);② **重写 [docs/DEPLOY.md](file:///d:/Code/MyHomePage/docs/DEPLOY.md)** 从 990 行 / 41KB 压到 **276 行 / 11.7KB**(精简 70%+)—— **按主人实际操作流顺序组织**:1) 部署架构(一张图说明 1Panel 三件套关系)/ 2) 前置准备(服务器已有什么 + 本地编译 `dotnet publish` + `npm run build` 两条命令)/ 3) **第一步 1Panel 准备 MySQL**(建库 utf8mb4 + 建用户主机锁 localhost + 记连接信息表)/ 4) **第二步 部署后端**(上传 publish/backend → 1Panel 网站 → 运行环境 .NET → 启动命令 `dotnet MyHomePage.Api.dll --urls http://0.0.0.0:8080` → 6 个环境变量用 `__` 双下划线嵌套语法)/ 5) **第三步 部署前端**(上传 dist → 1Panel 网站 → 静态网站 → 申请 SSL → 加 `/api/*` 反代到 127.0.0.1:8080)/ 6) **第四步 联调验证**(6 项必查清单 + Android APP 后端地址)/ 7) 日常运维(看日志 / 重启 / 备份 / 更新 / 紧急回滚,全部 1Panel 面板按钮完成)/ 8) 常见问题 7 个(白屏 / 404 / CORS / DB 失败 / 上传 500 / Android 连不上 / 360 壁纸);9) 附录 A 环境变量速查表 + 附录 B 目录结构速查;③ 关键简化点对比:P47 版的 5 项生产级补强(healthcheck / 日志轮转 / TZ / restart:always / 删 MySQL 3306 暴露)**在 1Panel 模式下全部由 1Panel 面板自动处理**——1Panel 网站运行时自带健康检查 + JSON 日志 + 系统 TZ + 自动重启 + MySQL 仅监听 127.0.0.1,**主人不需要管任何 docker-compose.yml / .env 模板**;④ 唯一需要主人手动配的 = 1Panel 网站详情页 → 「反向代理」/「配置文件」加一段 7 行 nginx location 块;**P48 教训**:① **"主人家有什么"决定部署模式** —— 不要默认给主人最复杂的方案(Docker + K8s + 反代 + CDN),**1Panel / 宝塔 / aaPanel / cPanel 这些面板模式才是中小服务器主流**;② **"已有什么"决定要写什么** —— MySQL 已部署就不用教装 MySQL,1Panel 已装就不用教装 Docker;③ **"面板能做的全让面板做"** —— 1Panel 网站管理 = nginx 反代 + supervisord 进程管理 + 文件管理 + 日志 + 备份 全套,**0 配置文件 0 命令行**;④ **"环境变量 __ 嵌套"是 ASP.NET Core 标配** —— 主人以后看官方文档不会陌生;⑤ **"应用商店装 .NET Runtime"是 1Panel 准备前置** —— 主人创建 .NET 网站时如果报"找不到 dotnet"就是没装,**装一次即可长期使用** | ✅ 已完成 | | 2026-07-05 | P49 | **前端运行时配置文件 `public/config.json`**(主人原话"每次都要重新编译太麻烦了,给前端也做个单独的配置文件吧,你看以什么形式比较好?自己评估一下"):主人反馈 P48 部署后,**改后端地址需要重新 `npm run build`**——因为 Vite 的 `VITE_API_BASE` 是**编译时静态注入**到 bundle 里的,部署后改不了。**方案对比 4 种**:A) `public/config.json` 运行时 fetch(⭐ 选中)/ B) `window.__CONFIG__` 全局变量(HTML 改起来麻烦)/ C) 环境变量 + 启动脚本(依赖 1Panel 没的 hook)/ D) 后端 `/api/config` 端点(耦合度高)。**A 最优的 4 个原因**:① Vite 原生把 `public/` 下的文件**原样拷贝**到 dist 根目录,**不编译**;② 1Panel 文件管理直接编辑 `dist/config.json` 即可,**0 rebuild**;③ 改动最小(新增 1 JSON + 1 ts + 改 2 文件);④ 失败降级(config.json 404 时用默认 `/api` 兜底,**绝不阻塞启动**)。**实施 5 文件**:① [public/config.json](file:///d:/Code/MyHomePage/frontend/public/config.json) = `{"apiBase": ""}`(纯净无注释,编译时拷贝到 dist 根);② [src/config.ts](file:///d:/Code/MyHomePage/frontend/src/config.ts) 新建 = `loadRuntimeConfig()` 异步 fetch + 时间戳 bust 缓存 + 失败兜底 `apiBase=""` + `getRuntimeConfig()` 同步取缓存 + `RuntimeConfig` interface 类型强约束;③ [src/api/http.ts](file:///d:/Code/MyHomePage/frontend/src/api/http.ts) 改造 = 保留 `VITE_API_BASE` 编译时兜底(向后兼容旧构建)+ 加 `initHttp()` async 函数(load config + 拼 baseURL + 注入 `axios.defaults.baseURL` + 控制台 log 来源);④ [src/main.ts](file:///d:/Code/MyHomePage/frontend/src/main.ts) 改造 = `bootstrap()` async 函数(先 `await initHttp()` 后 `createApp().mount()`,**避免竞态**:组件 setup 时可能立刻发请求,baseURL 还没改就会指错地方);⑤ 部署后改地址流程 = 1Panel 文件管理 → 编辑 `/opt/1panel/apps/myhomepage-frontend/config.json` 改 `apiBase` → 浏览器 Ctrl+F5 强刷。**端到端 4 步验证全过**:vue-tsc 0 错误;vite build 127.10 kB / gzip 43.78 kB 通过;用 Node.js 18 模拟"浏览器 fetch config.json → 改 baseURL → 调后端"完整流程:`{"apiBase":"http://localhost:8080"}` 配置下计算得到 `baseURL=http://localhost:8080/api` → 后端 8080 `/api/settings` 返 200 + `themeMode=dark` + `openSearchInNewTab=false` 全部正常;切回 `{"apiBase":""}` 也能用(默认走相对路径 `/api`);dist 14 个代码点全部包含 `apiBase` / `config.json` / `initHttp` 关键字符串。**P49 教训**:① **Vite 的 import.meta.env.X 是编译时注入**(不是运行时)—— 这是所有 Vite 项目的"坑",需要运行时配置必须用 `public/` + fetch;② **public/ 下的文件 Vite 不编译**(原样拷贝到 dist 根),这是 Vite 给"运行时配置"留的官方口子;③ **fetch 加时间戳 bust 缓存**(`?_t=${Date.now()}`)是部署后改 config.json 即时生效的关键 —— 不加的话浏览器/CDN 缓存会让改完看不见效果;④ **init config 在 app.mount 之前** —— 否则组件 setup 时 baseURL 还没改;⑤ **失败兜底比失败报错更友好** —— config.json 404 / JSON 错 / 网络问题,绝不阻塞启动;⑥ **主人原话"自己评估"= 授权决策** —— 直接给最优方案 + 实施,不要 AskUserQuestion 打断节奏 | ✅ 已完成 | | 2026-07-05 | P50 | **Swagger 启动崩溃 2 连击修复**(主人反馈):① 主人 VS 启动 `program.cs:32` 报 `System.Collections.Generic.KeyNotFoundException:"The given key 'action' was not present in the dictionary."` → **根因**:P48 部署手册引入的 minimal API `/` + `/health` 没有 controller/action 概念,`RouteValues` 字典里没 `action` 键;**修复** [Program.cs:31-43](file:///d:/Code/MyHomePage/backend/Program.cs#L31-L43) `CustomOperationIds` 改用 `TryGetValue` 防御(`routeValues.TryGetValue("action", out var action)` + `IsNullOrEmpty` 检查),minimal API 兜底用 `apiDesc.RelativePath.Replace("/", "_").Trim('_')` 作 operationId;**教训**:① **minimal API + Swagger CustomOperationIds 硬索引必崩**;② **VS 默认 Development 环境** vs `dotnet run` 默认 Production 环境差异,**启动报错看环境变量**。② 主人续报错 `SwaggerGeneratorException: Error reading parameter(s) for action UploadController.Upload as [FromForm] attribute used with IFormFile` → **根因**:Swashbuckle 6.x SwaggerGenerator 不支持 `[FromForm] IFormFile` 自动 schema 生成;**修复** [UploadController.cs:Upload](file:///d:/Code/MyHomePage/backend/Controllers/UploadController.cs) 加 1 行 `[ApiExplorerSettings(IgnoreApi = true)]` —— **0 风险**(不影响 API 实际功能,只让 swagger 跳过该端点文档生成),主人仍能通过 `POST /api/uploads` 上传文件,只是 swagger UI 不显示该端点。Swagger UI URL = `http://后端地址:端口/swagger`。**P50 教训**:① **TryGetValue 防御 > 硬索引**,minimal API 跟传统 controller API 混用时必备;② **`[ApiExplorerSettings(IgnoreApi = true)]` 是 Swashbuckle 兼容性问题的银弹**,不影响 API 行为只影响文档生成;③ **「bug 修 2 次」是好事** —— 第一次修了 Minimal API 启动崩,第二次才能发现 [FromForm] IFormFile 这种藏在深水区的问题 | ✅ 已完成 | | 2026-07-05 | P51 | **1Panel Docker 部署 favicon 容器内保存失败诊断修复**(主人反馈):主人服务器实际部署模式 = 1Panel 应用商店自定义应用(Docker 模式),与 P48 部署手册写的"网站管理 → 运行环境"模式不同;`docker-compose.yml` 把宿主机 `/data/myhomepage/upload` 映射到容器 `/uploads`,`appsettings.json` 设 `Upload:Path=/uploads`,但主人添加搜索引擎触发 favicon 抓取时**保存失败** —— 日志截断在 "Sending HTTP request GET psstatic.cdn.bcebos.com/...png" 之后。**根因**(3 重掩盖真因的代码坏味):① **FaviconService.catch 块用 LogWarning 静默吞异常**([FaviconService.cs:92-97](file:///d:/Code/MyHomePage/backend/Services/FaviconService.cs#L92-L97))—— docker logs 默认 Information 级别**看不到 Warning 堆栈**;② **`CacheNull` 负缓存 10 分钟** —— 失败结果被缓存,同 URL 反复请求**直接返 null 不重试**(主人测试时看到的"失败"可能是 10 分钟前的旧失败);③ **UploadService.SaveStreamInternalAsync 无 try/catch** —— 真正的 IO 异常(容器权限 / 路径 / 磁盘满)一路冒泡到 FaviconService 顶层 catch 后被吞 + 缓存。**修复 3 文件**:① [UploadService.cs:46-62](file:///d:/Code/MyHomePage/backend/Services/UploadService.cs#L46-L62) `EnsureRoot` 加一次性 `_rootLogged` 机制 —— 第一次调用时 `LogInformation` 输出 "UploadOptions.Path 解析后的实际 root"(暴露容器内 `/uploads` 路径覆盖问题,**主人只需 grep 一次 docker logs 就能确认容器看到的真实路径**);② [UploadService.cs:95-138](file:///d:/Code/MyHomePage/backend/Services/UploadService.cs#L95-L138) `SaveStreamInternalAsync` 把 IO 范围(CreateDirectory + FileStream + CopyToAsync)包 try/catch,catch 块 `LogError` 完整异常堆栈 + 全部上下文(`UploadOptions.Path` / `IsRooted` / `ContentRoot` / `env` / `root` / `dir` / `fullPath`),再抛 `BusinessException("文件保存失败: {ExType}: {Message}", 500)`;③ [FaviconService.cs:80-100](file:///d:/Code/MyHomePage/backend/Services/FaviconService.cs#L80-L100) `LogWarning → LogError`(docker logs 醒目可见),**临时禁用 `CacheNull` 负缓存**(注释掉 2 行调用),并在 catch 块附加 `UploadOptions.Path` + `ASPNETCORE_ENVIRONMENT` 实际值。**编译验证**:dotnet build 0 错误 1 历史警告(BaseRepository.cs Null 警告与本任务无关)。**主人需做的部署步骤**:① 重新 `dotnet publish` 后 1Panel 应用商店点"重建/重启";② 触发一次 favicon 获取(添加搜索引擎 / 编辑已有让重新抓取);③ `docker logs 容器名 2>&1 \| grep -E "Upload root resolved\|Upload save failed\|Favicon fetch failed"` 看真实错误;④ 若是 `UnauthorizedAccessException`(最大可能性 = 容器内 `app` 用户对 `/uploads` 没写权限)→ `run.sh` 加 `chmod 777 /uploads` 或宿主机目录改 777。**P51 教训**:① **「失败静默」是把双刃剑** —— P31 当时为 UX 让 FaviconService 静默失败 return null,但**缺日志 + 缺诊断信息 = 失败无法定位**;② **负缓存必须有上限** —— 失败结果缓存 10 分钟太长,**临时禁用让反复请求能拿到新结果**是诊断期标准操作;③ **docker logs 默认 Information 级别** —— LogWarning 经常看不到,开发期一律用 LogError 暴露;④ **`Path.IsPathRooted` 在 .NET Core 容器内判 `/uploads` 返 true** —— 容器内绝对路径处理跟宿主 Windows 不同;⑤ **容器内非 root 用户写入挂载目录** = 部署经典坑,**chmod 777 是最快排查手段**(生产可改 chown 1000:1000 等更精细方案) | ✅ 已完成 | | 2026-07-05 | P52 | **跨域部署 favicon 显示 404 修复**(主人反馈):主人服务器实际部署模式 = 前后端分离两个域名(前端 `mh.1vs5.top` + 后端 `mhapi.1vs5.top`);主人截图显示后端保存 favicon 成功 `/data/myhomepage/upload/2026/07/04/favicons/...png` ✅,但前端访问时浏览器自动把相对路径 `/uploads/...` 拼成 `https://mh.1vs5.top/uploads/...`(前端域名)→ 404 ❌。**根因**:之前 P49 只在 `axios.defaults.baseURL` 加了 `apiBase` 拼接,但 `bookmark.iconUrl` / `engine.iconUrl` 是**直接给 `` / `` 用的相对路径**,浏览器不会自动用 `apiBase` 的 host —— 必须前端手动拼。**修复策略**:在 [config.ts](file:///d:/Code/MyHomePage/frontend/src/config.ts) 新增 `resolveAssetUrl(path)` 工具函数集中处理 —— 4 条规则按顺序短路:① 空 → `''`;② 已是 http(s):// / data: / blob: / 协议相对 // → 原样;③ apiBase 为空(同域反代模式)→ 原样(让浏览器用当前 host);④ apiBase 非空(跨域直连模式)→ `${apiBase}${path}` 拼成绝对 URL。**6 处使用点全改完**(用 computed 包装保证响应式):① [AppLinkCard.vue:28](file:///d:/Code/MyHomePage/frontend/src/components/AppLinkCard.vue#L28) 桌面端链接卡 logo + extractDominantColor;② [AppLinkListItem.vue:26](file:///d:/Code/MyHomePage/frontend/src/components/AppLinkListItem.vue#L26) 移动端链接行 logo + extractDominantColor;③ [AppSearchBar.vue:40](file:///d:/Code/MyHomePage/frontend/src/components/AppSearchBar.vue#L40) 顶部 + 下拉菜单 2 处引擎 logo;④ [SettingsView.vue:518,568](file:///d:/Code/MyHomePage/frontend/src/views/SettingsView.vue#L518) 引擎列表 + 编辑器预览 + extractDominantColor;⑤ [BookmarkForm.vue:278](file:///d:/Code/MyHomePage/frontend/src/components/BookmarkForm.vue#L278) 表单图标预览 + extractDominantColor。**编译验证**:vue-tsc 0 错误(**被 Edit tool 多行匹配静默失败坑了 3 次**,发现 → 重新 Edit → 验证 Read 确认所有 6 处都生效);vite build 3.48s 通过。**主人部署步骤**:① 1Panel 文件管理 → 编辑前端 `dist/config.json` 把 `apiBase` 改成 `"https://mhapi.1vs5.top"`(**关键!没改这个 6 处 resolveAssetUrl 全部走空路径分支**);② 上传新 dist 到 1Panel 前端网站;③ 浏览器 Ctrl+Shift+R 强刷;④ 添加新搜索引擎测试 → 应该能看到 favicon 正常显示。**配套修复 P51 的持续化路径**:[appsettings.json:50-59](file:///d:/Code/MyHomePage/backend/appsettings.json#L50-L59) `Upload.Path` 从 `"Uploads"` 改成 `"/uploads"`(绝对路径匹配 volume 挂载点 `/data/myhomepage/upload:/uploads`)—— 修这个之前 favicon 是存到容器内 `/app/Uploads`(污染代码目录 + 容器销毁就丢),改完才真正持久化到宿主机。**P52 教训**:① **`/uploads/*` 这种相对路径给 `` 用** —— 浏览器自动用当前 host 拼,**跨域部署 100% 404**,必须前端用 `apiBase` 拼;② **「axios 加 baseURL」≠「图片 URL 加 host」** —— P49 只改了 axios 漏了图片,是 P49 漏的洞;③ **Edit tool 多行匹配静默失败再次发生**(P46 也中过招)—— 解决 = 每个 Edit 后**用 Read 验证 1-2 处关键位置**,不依赖「file updated」返回值;④ **跨域部署 4 资源类型**(API 请求 / 静态图片 / WebSocket / 第三方 SDK)都各自有拼接逻辑,**resolveAssetUrl 是 4 种里最常见的"静态图片"集中处理**;⑤ **后端路径配置 `Path='Uploads'` vs `Path='/uploads'`** 在 `Path.IsPathRooted` 行为完全不同 —— **绝对路径 = 容器内真路径,相对路径 = `ContentRoot` 拼**,**前后端要严格对齐** volume 挂载设计意图 | ✅ 已完成 | --- ## 九、后端文件清单([backend/](file:///d:/Code/MyHomePage/backend/)) - [MyHomePage.Api.csproj](file:///d:/Code/MyHomePage/backend/MyHomePage.Api.csproj) - [Program.cs](file:///d:/Code/MyHomePage/backend/Program.cs) · [appsettings.json](file:///d:/Code/MyHomePage/backend/appsettings.json) · [appsettings.Development.json](file:///d:/Code/MyHomePage/backend/appsettings.Development.json) - Common: [ApiResponse.cs](file:///d:/Code/MyHomePage/backend/Common/ApiResponse.cs) · [BusinessException.cs](file:///d:/Code/MyHomePage/backend/Common/BusinessException.cs) · [ExceptionHandlingMiddleware.cs](file:///d:/Code/MyHomePage/backend/Common/ExceptionHandlingMiddleware.cs) - Infrastructure: [SqlSugarContext.cs](file:///d:/Code/MyHomePage/backend/Infrastructure/Database/SqlSugarContext.cs) · [DatabaseInitializer.cs](file:///d:/Code/MyHomePage/backend/Infrastructure/Database/DatabaseInitializer.cs) · [DatabaseOptions.cs](file:///d:/Code/MyHomePage/backend/Infrastructure/Configuration/DatabaseOptions.cs) · [UploadOptions.cs](file:///d:/Code/MyHomePage/backend/Infrastructure/Configuration/UploadOptions.cs) · [CorsOptions.cs](file:///d:/Code/MyHomePage/backend/Infrastructure/Configuration/CorsOptions.cs) - Models/Entities: [BaseEntity.cs](file:///d:/Code/MyHomePage/backend/Models/Entities/BaseEntity.cs) · [Category.cs](file:///d:/Code/MyHomePage/backend/Models/Entities/Category.cs) · [Bookmark.cs](file:///d:/Code/MyHomePage/backend/Models/Entities/Bookmark.cs) · [SearchEngine.cs](file:///d:/Code/MyHomePage/backend/Models/Entities/SearchEngine.cs) · [Setting.cs](file:///d:/Code/MyHomePage/backend/Models/Entities/Setting.cs) · [SyncLog.cs](file:///d:/Code/MyHomePage/backend/Models/Entities/SyncLog.cs) - Models/Dtos: [CategoryDtos.cs](file:///d:/Code/MyHomePage/backend/Models/Dtos/CategoryDtos.cs) · [BookmarkDtos.cs](file:///d:/Code/MyHomePage/backend/Models/Dtos/BookmarkDtos.cs) · [SearchEngineDtos.cs](file:///d:/Code/MyHomePage/backend/Models/Dtos/SearchEngineDtos.cs) · [SettingDtos.cs](file:///d:/Code/MyHomePage/backend/Models/Dtos/SettingDtos.cs) · [SyncDtos.cs](file:///d:/Code/MyHomePage/backend/Models/Dtos/SyncDtos.cs) · [UploadDtos.cs](file:///d:/Code/MyHomePage/backend/Models/Dtos/UploadDtos.cs) · [WallpaperDtos.cs](file:///d:/Code/MyHomePage/backend/Models/Dtos/WallpaperDtos.cs) - Repositories: [BaseRepository.cs](file:///d:/Code/MyHomePage/backend/Repositories/BaseRepository.cs) - Services: [CategoryService.cs](file:///d:/Code/MyHomePage/backend/Services/CategoryService.cs) · [BookmarkService.cs](file:///d:/Code/MyHomePage/backend/Services/BookmarkService.cs) · [SearchEngineService.cs](file:///d:/Code/MyHomePage/backend/Services/SearchEngineService.cs) · [SettingService.cs](file:///d:/Code/MyHomePage/backend/Services/SettingService.cs) · [UploadService.cs](file:///d:/Code/MyHomePage/backend/Services/UploadService.cs) · [SyncService.cs](file:///d:/Code/MyHomePage/backend/Services/SyncService.cs) · [SyncLogHelper.cs](file:///d:/Code/MyHomePage/backend/Services/SyncLogHelper.cs) · [FaviconService.cs](file:///d:/Code/MyHomePage/backend/Services/FaviconService.cs) · [WallpaperService.cs](file:///d:/Code/MyHomePage/backend/Services/WallpaperService.cs) + 各 IService 接口 - Controllers: [CategoriesController.cs](file:///d:/Code/MyHomePage/backend/Controllers/CategoriesController.cs) · [BookmarksController.cs](file:///d:/Code/MyHomePage/backend/Controllers/BookmarksController.cs) · [SearchEnginesController.cs](file:///d:/Code/MyHomePage/backend/Controllers/SearchEnginesController.cs) · [SettingsController.cs](file:///d:/Code/MyHomePage/backend/Controllers/SettingsController.cs) · [UploadController.cs](file:///d:/Code/MyHomePage/backend/Controllers/UploadController.cs) · [SyncController.cs](file:///d:/Code/MyHomePage/backend/Controllers/SyncController.cs) · [UtilityController.cs](file:///d:/Code/MyHomePage/backend/Controllers/UtilityController.cs) · [WallpaperController.cs](file:///d:/Code/MyHomePage/backend/Controllers/WallpaperController.cs) --- ## 十、前端文件清单([frontend/](file:///d:/Code/MyHomePage/frontend/)) - 配置: [package.json](file:///d:/Code/MyHomePage/frontend/package.json) · [vite.config.ts](file:///d:/Code/MyHomePage/frontend/vite.config.ts) · [tsconfig.json](file:///d:/Code/MyHomePage/frontend/tsconfig.json) · [capacitor.config.ts](file:///d:/Code/MyHomePage/frontend/capacitor.config.ts) · [index.html](file:///d:/Code/MyHomePage/frontend/index.html) · [.env.development](file:///d:/Code/MyHomePage/frontend/.env.development) · [.env.production](file:///d:/Code/MyHomePage/frontend/.env.production) - 入口: [main.ts](file:///d:/Code/MyHomePage/frontend/src/main.ts) · [App.vue](file:///d:/Code/MyHomePage/frontend/src/App.vue) · [router/index.ts](file:///d:/Code/MyHomePage/frontend/src/router/index.ts) - 样式: [styles/tokens.css](file:///d:/Code/MyHomePage/frontend/src/styles/tokens.css) · [styles/global.css](file:///d:/Code/MyHomePage/frontend/src/styles/global.css) - API 层: [api/http.ts](file:///d:/Code/MyHomePage/frontend/src/api/http.ts) · [api/categories.ts](file:///d:/Code/MyHomePage/frontend/src/api/categories.ts) · [api/bookmarks.ts](file:///d:/Code/MyHomePage/frontend/src/api/bookmarks.ts) · [api/searchEngines.ts](file:///d:/Code/MyHomePage/frontend/src/api/searchEngines.ts) · [api/settings.ts](file:///d:/Code/MyHomePage/frontend/src/api/settings.ts) · [api/upload.ts](file:///d:/Code/MyHomePage/frontend/src/api/upload.ts) · [api/sync.ts](file:///d:/Code/MyHomePage/frontend/src/api/sync.ts) · [api/utility.ts](file:///d:/Code/MyHomePage/frontend/src/api/utility.ts) · [api/wallpaper.ts](file:///d:/Code/MyHomePage/frontend/src/api/wallpaper.ts) - 类型: [types/api.ts](file:///d:/Code/MyHomePage/frontend/src/types/api.ts) - Stores: [stores/settings.ts](file:///d:/Code/MyHomePage/frontend/src/stores/settings.ts) · [stores/categories.ts](file:///d:/Code/MyHomePage/frontend/src/stores/categories.ts) · [stores/bookmarks.ts](file:///d:/Code/MyHomePage/frontend/src/stores/bookmarks.ts) · [stores/searchEngines.ts](file:///d:/Code/MyHomePage/frontend/src/stores/searchEngines.ts) · [stores/sync.ts](file:///d:/Code/MyHomePage/frontend/src/stores/sync.ts) - 工具: [utils/storage.ts](file:///d:/Code/MyHomePage/frontend/src/utils/storage.ts) · [utils/toast.ts](file:///d:/Code/MyHomePage/frontend/src/utils/toast.ts) · [utils/icon.ts](file:///d:/Code/MyHomePage/frontend/src/utils/icon.ts) · [utils/color.ts](file:///d:/Code/MyHomePage/frontend/src/utils/color.ts) - 通用组件: [AppIcon.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppIcon.vue) · [AppButton.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppButton.vue) · [AppCard.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppCard.vue) · [AppModal.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppModal.vue) · [AppDrawer.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppDrawer.vue) · [AppWallpaper.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppWallpaper.vue) · [AppToastHost.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppToastHost.vue) · [AppIconPicker.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppIconPicker.vue) - 业务组件: [AppSearchBar.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppSearchBar.vue) · [AppSidebar.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppSidebar.vue) · [AppLinkCard.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppLinkCard.vue) · [AppLinkListItem.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppLinkListItem.vue) · [AppCategoryTabs.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppCategoryTabs.vue) · [AppMobileTopBar.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppMobileTopBar.vue) · [AppFab.vue](file:///d:/Code/MyHomePage/frontend/src/components/AppFab.vue) · [BookmarkForm.vue](file:///d:/Code/MyHomePage/frontend/src/components/BookmarkForm.vue) · [CategoryForm.vue](file:///d:/Code/MyHomePage/frontend/src/components/CategoryForm.vue) - 视图: [views/HomeView.vue](file:///d:/Code/MyHomePage/frontend/src/views/HomeView.vue) · [views/SettingsView.vue](file:///d:/Code/MyHomePage/frontend/src/views/SettingsView.vue) - 文档: [ANDROID.md](file:///d:/Code/MyHomePage/frontend/ANDROID.md) --- ## 十一、部署 / 文档 - 根目录 [README.md](file:///d:/Code/MyHomePage/README.md) - **完整部署手册 [docs/DEPLOY.md](file:///d:/Code/MyHomePage/docs/DEPLOY.md)**(P47 · 1000 行 / 10 大章:架构 → 准备 → 模式选择 → 编译 → 上传 → 配置 → 上线 → 日志 → 运维 → 附录) - [docker-compose.yml](file:///d:/Code/MyHomePage/docker-compose.yml)(P47 · 已补全 5 项生产级配置:restart:always / healthcheck / logging 轮转 / TZ / 删 MySQL 3306 公网暴露) - [docker/backend.Dockerfile](file:///d:/Code/MyHomePage/docker/backend.Dockerfile)(多阶段:前端构建 → .NET 发布 → 运行时) - [docker/nginx.conf](file:///d:/Code/MyHomePage/docker/nginx.conf)(参考用,部署采用前后端同镜像) - [docker/README.md](file:///d:/Code/MyHomePage/docker/README.md) --- ## 十二、验证情况 - ✅ 后端:`dotnet build` 0 错误;CodeFirst 自动建表;SQLite / MySQL 双驱动切换已配置 - ✅ 后端 API:8 个模块 GET/POST/PUT/DELETE + upload + sync + utility + wallpaper 全部 200,swagger 可见 - ✅ 前端:`vue-tsc --noEmit` 通过;`vite build` 产出 ~46KB CSS + 117KB JS(gzip 后 81KB) - ✅ 联调:vite proxy 5173→5141(默认端口已与后端 `--urls` 同步,P34.2 修复),浏览器无报错 - ✅ 多端同步:手动 / 30s 轮询 / visibilitychange 触发 - ✅ 壁纸三端不变形:aspect ratio preset 命中 + fallback RewriteUrl(P34.1) - ✅ 搜索框行为 / 链接行为双 toggle 独立可控(P26 + P46) - ✅ 部署手册可执行:docs/DEPLOY.md 10 章命令 walkthrough 全能跑,docker-compose.yml 5 项生产配置生效 - ⏸ Android 集成:cap add android 需要 Android SDK 环境(Windows 上未安装),提供完整 [ANDROID.md](file:///d:/Code/MyHomePage/frontend/ANDROID.md) 步骤 - ⏸ Docker 部署:docker compose 配置文件已写好并补强,未实际构建镜像(需 Docker Desktop)