68be41e7a2
# 项目概述 个人浏览器首页导航应用,支持书签分类管理、搜索引擎快捷搜索、 必应每日壁纸轮播、前后端分离部署,适配 1Panel 服务器(Docker 模式)。 # 技术栈 - 前端:Vue 3 + TypeScript + Vite + Pinia + Capacitor(Android 打包) - 后端:.NET 8 + SqlSugar(多数据库) + SQLite/MySQL + Swashbuckle - 部署:1Panel 应用商店自定义应用(Docker Compose 模式) # 项目结构 - backend/ .NET 8 API 后端(8 个 Controller + 15 个 Service) - frontend/ Vue 3 前端(19 个组件 + 9 个 API 模块 + 5 个 Store) - docker/ Docker 部署文件(后端镜像 + Nginx 反代) - docs/ 部署手册(1Panel 实战版) - scripts/ E2E 测试脚本 # 已实现功能 - 书签管理:增删改查 + 树形分类 + 拖拽排序 + 主色自适应 - 搜索引擎:8 个内置引擎 + 自定义引擎 + favicon 自动抓取 - 必应壁纸:每日轮播 + 多分辨率自动选择 + 1.6MP 质量优先 - 全局设置:主题/行为/数据/工具 4 分类 + 跨设备同步 - 文件上传:图标/书签/通用(容器持久化 + 跨域 URL 拼接) - 同步:基于变更日志的设备间数据同步 - 跨域部署:前后端分离 + runtime config.json 无需重新编译 # 进度记录 - 已完成 P0~P52 共 53 个开发节点(详细见 说明文档.md) - 当前版本:v1.0 部署就绪 # 部署文档 - README.md:项目说明 + 快速开始 - 说明文档.md:完整开发进度(中文) - docs/DEPLOY.md:1Panel 部署手册(Docker 模式)
78 KiB
78 KiB
浏览器首页(MyHomePage)项目说明文档
单一项目管理载体:所有规划、变更、进度、问题均记录在此。 角色:猫娘工程师 幽浮喵 创建日期:2026-07-04
一、项目概述
打造一款跨端可用的浏览器首页 / 起始页:PC、平板、手机浏览器及 Android APP 共享同一套数据,实时同步。
设计稿已定型(browser-homepage/),包含桌面端、桌面端设置弹窗、移动端三个页面。
核心能力
- 二级分类导航(常用工具 > 搜索引擎 / AI 工具 / 开发工具 等)
- 搜索引擎可管理(增删改、设置默认)
- 链接卡片 / 列表:图标 + 标题 + 简介
- 设置面板:主题模式(暗/亮/跟随系统)、主色调、背景图
- PC / 平板 / 手机浏览器自适应
- PC / 平板 / 手机 / Android APP 四端实时数据同步
- 链接 / 背景图支持图片上传(落盘到后端可配路径)
二、技术栈
| 层 | 选型 |
|---|---|
| 前端框架 | 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 关键节点:
{
"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 终极修复:<Teleport to="body"> + position: fixed;图标 more-horizontal → more-vertical;vue-tsc + build 通过 |
✅ 已完成 |
| 2026-07-04 | P21.1 | P21 遗漏 fix: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;HomeView.vue displayedBookmarks 改造(null=全部 / 顶级聚合 / 二级单分类 / 失效 fallback);HomeView onMounted 恢复 + watch 持久化;AppSidebar.vue onRootClick 改造(未选→选中+展开 / 已选→取消+折叠) | ✅ 已完成 |
| 2026-07-04 | P23 | sidebar 「+」号移到 root row + 图标选择器:AppSidebar root row 加 + 按钮(hover 显示);utils/icon.ts 扩到 130+ 导出 SUPPORTED_ICONS;新建 AppIconPicker.vue;CategoryForm.vue / BookmarkForm.vue 集成 IconPicker |
✅ 已完成 |
| 2026-07-04 | P23.1 | 「+」号改回与 ⋯ 一样的 hover 行为(去掉 .sidebar__row-action--add 显式 opacity:1) | ✅ 已完成 |
| 2026-07-04 | P24 | 移动端 bug 修复:搜索栏「百度」被拆两行(AppSearchBar.vue flex-shrink:0 + @media (max-width:640px) 适配);drawer 一级点击没反应(HomeView.vue root 整行 @click + 内嵌按钮 @click.stop) | ✅ 已完成 |
| 2026-07-04 | P25 | sync 接口 snapshot.categories 扁平化 bug 修复:SyncService.cs Select 后未构建树形;CategoryDtos.cs 加 BuildTree + BuildTreeFromFlat(parentId==0 → 顶级;孤儿降级);清理前端调试日志;dotnet build 0 错误;后端重启;curl /api/sync/changes 看到树形结构 |
✅ 已完成 |
| 2026-07-04 | P26 | 设置面板增加「链接行为」开关(6 文件全链路):后端 Setting.cs 加 OpenLinksInNewTab int default 1(int 不用 bool 避 SqlSugar+SQLite bit 兼容性);SettingDtos.cs 加字段 + SettingDto.FromEntity(s) 静态映射;SettingService.cs UpdateAsync bool→int 转换;DatabaseInitializer.cs MigrateSettingColumns() AddColumn INTEGER DEFAULT 1;前端 types/api.ts AppSettings + SettingUpdate 加 openLinksInNewTab;stores/settings.ts 默认 true + setOpenLinksInNewTab;HomeView.vue openBookmark 用 target = settings.openLinksInNewTab ? '_blank' : '_self';SettingsView.vue 新增「链接行为」section(toggle + 反转逻辑 + P26.1 修复点击传同值 bug;P26.2 修复 sync 漏字段) |
✅ 已完成 |
| 2026-07-04 | P27 | 前端页面美化:新建 utils/color.ts colorFromUrl + firstChar(27 个品牌色板 + 32-bit HSL 哈希);tokens.css 加 --glass-bg-faint / --glass-blur-sm/-lg / --link-logo-size: 96px / --link-card-min-height: 88px;AppLinkCard.vue 横排彩色 logo + 标题描述改造(96×88 品牌色块 + 玻璃面板 + 编辑/删除绝对定位 hover 浮现);AppLinkListItem.vue 移动端同步升级(56px logo);AppSidebar.vue 顶部加用户头像 header(48px 蓝渐变 + "M" 字符 + 用户名 + tagline);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 补 --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 完全重写 logo 块 width: var(--link-logo-size); height: var(--link-logo-size); flex-shrink: 0; 完美正方形 + border-right: 1px solid var(--glass-border); + <AppIcon> 真正显示 + 三级 fallback 背景色);AppLinkListItem.vue 移动端 56px 正方形同步升级);③ 添加链接弹窗加背景色选择器(BookmarkForm.vue 新增 10 套预设色块 grid + 36×36 自定义颜色 <input type="color"> 透明覆盖 + conic-gradient 棋盘底色 + "自适应"按钮 + 动态 hint;form.colorBg: string | null);④ extractDominantColor 主色提取(utils/color.ts <img crossOrigin="anonymous"> 缩放 32×32 + getImageData + 像素过滤 + 5-bit 桶量化 + 6s timeout);⑤ 后端 ColorBg 字段全链路(Bookmark.cs ColorBg string? Length=32;BookmarkDtos.cs 加字段 + BookmarkDto.FromEntity(b) 静态映射;BookmarkService.cs Create/Update NormalizeColor() 验证);⑥ DB 轻量迁移(DatabaseInitializer.cs MigrateBookmarkColumns() AddColumn varchar(32) NULL);⑦ 两个 P28 关键 bug 修复(BookmarkService.UpdateAsync 漏处理 ColorBg / SyncService.snapshot.Bookmarks 漏 ColorBg — 根因方案 BookmarkDto.FromEntity 共享映射) |
✅ 已完成 |
| 2026-07-04 | P29 | 主人反馈两项修复:① 删过头 — 齿轮设置按钮复原(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 删除 .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 categoryId: props.defaultCategoryId ?? props.bookmark?.categoryId ?? ... — ?? 左侧有值时永远短路右侧,而 HomeView.vue:375 编辑模式也传 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 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 新建(~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 扩展 SaveStreamAsync + ContentTypeToExt 字典。④ 后端 BookmarkService.cs 集成:IsIconUnspecified 私有静态判定 + MaybeFetchFaviconAsync 在 Create/Update 之后调用 + UpdateColumns 增量更新。⑤ 后端 UtilityController.cs 新建 POST /api/utility/favicon。⑥ 后端 Program.cs DI:AddMemoryCache + AddHttpClient(5s + UA) + AddScoped FaviconService。⑦ 前端 types/api.ts iconType 加 'favicon' 枚举值。⑧ 前端 AppLinkCard.vue + AppLinkListItem.vue isImage 改 (iconType==='image' || iconType==='favicon') && !!iconUrl。⑨ 前端 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 fetchFavicon(url) 包装 axios POST /api/utility/favicon timeout:15s。③ BookmarkForm.vue 改造:新增「自动获取」按钮 <AppButton variant="ghost" :loading="fetchingFavicon" :disabled="fetchingFavicon" @click="autoFetchIcon"> + <AppIcon name="download" :size="14" /> 自动获取;autoFetchIcon 4 步 — 清旧错误 → 检查 URL 必填 + /^https?:\/\//i 协议前缀 → 调 fetchFavicon(url) → 成功 form.iconType='favicon' + form.iconUrl=服务端返回 URL + form.icon=null / 失败设置 faviconError 红色提示;fetchingFavicon: ref(false) + faviconError: ref<string | null>(null);watch(() => form.value.url, () => faviconError.value = null) URL 变化时清掉旧错误;模板新增错误提示 <p v-if="faviconError" class="form__hint form__hint--error"><AppIcon name="alert-circle" :size="12" /> {{ faviconError }}</p>;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 看到 <link rel="icon" class="js-site-favicon" type="image/svg+xml" href="https://github.githubassets.com/favicons/favicon.svg" data-base-href="https://github.githubassets.com/favicons/favicon"> — 含有 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 attrPattern 改 @"(?<![-\w])(rel|href|size|sizes|type|as)\s*=\s*[""']([^""']*)[""']" — 属性名前用 (?<![-\w]) 负向后行断言 防止「前面是 - 或字母数字」时误匹配。⑤ 修复二 — ParseIconLinks 重写:先抓整个 <link ...> 块 → 再用 attrPattern 提取每个属性(顺序无关);priority 映射扩展支持 alternate icon / fluid-icon / icon-zzz。⑥ 修复三 — 增强 og:image 兜底:很多现代站没 favicon 但有 og:image,加 <meta property="og:image" content="..."> 正则解析(priority=30)。⑦ 修复四 — 详细日志:FetchHtmlAsync 抓完 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),折中方案是用 (?<![-\w]) 负向后行断言 + 把 <link> 整块先抓出来再属性提取(顺序无关);②「主人反馈 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 加 3 字段:WallpaperEnabled (int 0/1, default 0) / WallpaperCategoryId (varchar(32), default "") / WallpaperInterval (int, default 30);SqlSugar + SQLite 用 int 0/1 不用 bool。④ 后端 SettingDtos.cs 加 3 字段 + SettingDto.FromEntity(s) 同步 + SettingUpdateRequest 加 3 个可选字段;SettingService.cs UpdateAsync 加 AllowedWallpaperIntervals = {0,1,5,15,30,60} 白名单 + 校验抛 BusinessException 400。⑤ 后端 DatabaseInitializer.cs 修复老库兼容:原 CodeFirst InitTables(Setting) 触发 Sqlite no support alter column primary key 异常;改用 IsAnyTable("settings") 检测 → 表已存在跳过 CodeFirst → 走轻量迁移 MigrateSettingColumns 补 3 列(与 P26 OpenLinksInNewTab 风格一致)。⑥ 后端新建 WallpaperDtos.cs:WallpaperCategoryDto { id, name } + WallpaperRandomDto { url, originalUrl, width, height }。⑦ 后端新建 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:GET /api/wallpaper/categories / GET /api/wallpaper/random?cid=&w=&h= / POST /api/wallpaper/refresh?cid=&w=&h=;SanitizeViewport 把 w/h 限制在 0<w<8000 范围。⑨ 后端 Program.cs DI:AddHttpClient(nameof(WallpaperService), c => c.Timeout = 10s + UA) + AddScoped<WallpaperService>()。⑩ 前端 types/api.ts 加 P34 字段。⑪ 前端新建 api/wallpaper.ts:fetchWallpaperCategories() 12s / fetchWallpaperRandom(cid, w, h) 12s / refreshWallpaperRandom(cid, w, h) 15s。⑫ 前端 stores/settings.ts 加 P34 state + actions:state 加 wallpaperUrl / wallpaperCategories / wallpaperLoading / wallpaperError 4 个 ref;actions 加 6 个;applyBackground 加 P34 分支(wallpaperEnabled=true 时不覆盖 --bg-image)。⑬ 前端 SettingsView.vue 加 360 壁纸 section:toggle + 仅启用时显示的表单(分类 select + 间隔 select + 「立即切换」按钮)+ 状态显示。⑭ 前端 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<w<8000) on server;⑤ P26 风格的 FromEntity 静态映射 + DB 轻量迁移再次救命 |
✅ 已完成 |
| 2026-07-04 | P34.1 | 360 壁纸接口解析修正(主人反馈实际返回内容)—— 主人贴出了 2 个接口的真实 JSON 后,浮浮酱发现 3 个 P34 的实现错误,全部修复:① 【数据解析层】分类排序错误:主人截图显示分类有 order_num 字段("4K专区":110, "文字控":9),P34 用了 OrderBy(c => 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<string>(只有 url)升级为 List<PoolItem>(含 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 |
| 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 默认 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: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 的 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 后端二次防御: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 .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 内部 <img v-if="isImage"> 在父级 <AppSearchBar> 没传 class 时不获得 scoped data-v-hash(Vue 3 scoped CSS 边界),编译产物 .searchbar__engine img[data-v-xxx] 永远不匹配该 img → CSS 失效 → img 渲染为浏览器默认 placeholder(浅蓝色方块)。修复:移除 AppIcon 依赖,AppSearchBar.vue 引擎 logo 改用 父级 template 直接 <img :src="engine.icon" :alt="engine.name" class="searchbar__engine-logo" /> —— Vue 模板字面量分析 100% 保证获得 data-v-hash,CSS 100% 命中;下拉菜单 searchbar__menu-item-logo 同样改 <img>;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 直接 <img> 更可控;③ 「tool 组件」(AppIcon = 多类型 fallback)vs「business 渲染」(searchbar logo = 已知图片 URL)应分开 —— AppIcon 适合「多类型 + 兜底」,不适合「已知 URL 高频展示」;④ build passed ≠ rendered correctly —— 必须手动点穿所有 UI 入口验证(编辑预览 / 列表显示 / 搜索栏 / 同步刷新 / 移动 / 桌面) |
✅ 已完成 |
| 2026-07-05 | P44 | 侧边栏分类显示区域(nav)对比度修复(主人反馈"左侧分类栏的对比度还是要调整一下,主要是分类显示区域,footer显示正常,无须调整"):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 三处改:① .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 加 OpenSearchInNewTab int default 1(int 不用 bool 避开 SqlSugar+SQLite bit 兼容性);② SettingDtos.cs SettingDto / SettingUpdateRequest 加字段,复用 P26 风格 SettingDto.FromEntity(s) 静态映射;③ SettingService.cs UpdateAsync 处理 bool→int 转换 + ToDto 改用 SettingDto.FromEntity;④ DatabaseInitializer.cs 新增 MigrateSettingColumnsV2() 补 OpenSearchInNewTab 列(AddColumn int 必须显式 DataType = "int" + IsNullable=false + DefaultValue="1",否则 SqlSugar 推断可能错);⑤ 前端 types/api.ts AppSettings + SettingUpdate 加 openSearchInNewTab: boolean;⑥ stores/settings.ts 默认 openSearchInNewTab: true + 新增 setOpenSearchInNewTab(openInNewTab: boolean);⑦ AppSearchBar.vue submit 改用 const target = settings.settings.openSearchInNewTab ? '_blank' : '_self';⑧ 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 约 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 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 从 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 = {"apiBase": ""}(纯净无注释,编译时拷贝到 dist 根);② src/config.ts 新建 = loadRuntimeConfig() 异步 fetch + 时间戳 bust 缓存 + 失败兜底 apiBase="" + getRuntimeConfig() 同步取缓存 + RuntimeConfig interface 类型强约束;③ src/api/http.ts 改造 = 保留 VITE_API_BASE 编译时兜底(向后兼容旧构建)+ 加 initHttp() async 函数(load config + 拼 baseURL + 注入 axios.defaults.baseURL + 控制台 log 来源);④ 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 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 加 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)—— docker logs 默认 Information 级别看不到 Warning 堆栈;② CacheNull 负缓存 10 分钟 —— 失败结果被缓存,同 URL 反复请求直接返 null 不重试(主人测试时看到的"失败"可能是 10 分钟前的旧失败);③ UploadService.SaveStreamInternalAsync 无 try/catch —— 真正的 IO 异常(容器权限 / 路径 / 磁盘满)一路冒泡到 FaviconService 顶层 catch 后被吞 + 缓存。修复 3 文件:① UploadService.cs:46-62 EnsureRoot 加一次性 _rootLogged 机制 —— 第一次调用时 LogInformation 输出 "UploadOptions.Path 解析后的实际 root"(暴露容器内 /uploads 路径覆盖问题,主人只需 grep 一次 docker logs 就能确认容器看到的真实路径);② UploadService.cs:95-138 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 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 是直接给 <img> / <AppIcon> 用的相对路径,浏览器不会自动用 apiBase 的 host —— 必须前端手动拼。修复策略:在 config.ts 新增 resolveAssetUrl(path) 工具函数集中处理 —— 4 条规则按顺序短路:① 空 → '';② 已是 http(s):// / data: / blob: / 协议相对 // → 原样;③ apiBase 为空(同域反代模式)→ 原样(让浏览器用当前 host);④ apiBase 非空(跨域直连模式)→ ${apiBase}${path} 拼成绝对 URL。6 处使用点全改完(用 computed 包装保证响应式):① AppLinkCard.vue:28 桌面端链接卡 logo + extractDominantColor;② AppLinkListItem.vue:26 移动端链接行 logo + extractDominantColor;③ AppSearchBar.vue:40 顶部 + 下拉菜单 2 处引擎 logo;④ SettingsView.vue:518,568 引擎列表 + 编辑器预览 + extractDominantColor;⑤ BookmarkForm.vue:278 表单图标预览 + 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 Upload.Path 从 "Uploads" 改成 "/uploads"(绝对路径匹配 volume 挂载点 /data/myhomepage/upload:/uploads)—— 修这个之前 favicon 是存到容器内 /app/Uploads(污染代码目录 + 容器销毁就丢),改完才真正持久化到宿主机。P52 教训:① /uploads/* 这种相对路径给 <img> 用 —— 浏览器自动用当前 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/)
- MyHomePage.Api.csproj
- Program.cs · appsettings.json · appsettings.Development.json
- Common: ApiResponse.cs · BusinessException.cs · ExceptionHandlingMiddleware.cs
- Infrastructure: SqlSugarContext.cs · DatabaseInitializer.cs · DatabaseOptions.cs · UploadOptions.cs · CorsOptions.cs
- Models/Entities: BaseEntity.cs · Category.cs · Bookmark.cs · SearchEngine.cs · Setting.cs · SyncLog.cs
- Models/Dtos: CategoryDtos.cs · BookmarkDtos.cs · SearchEngineDtos.cs · SettingDtos.cs · SyncDtos.cs · UploadDtos.cs · WallpaperDtos.cs
- Repositories: BaseRepository.cs
- Services: CategoryService.cs · BookmarkService.cs · SearchEngineService.cs · SettingService.cs · UploadService.cs · SyncService.cs · SyncLogHelper.cs · FaviconService.cs · WallpaperService.cs + 各 IService 接口
- Controllers: CategoriesController.cs · BookmarksController.cs · SearchEnginesController.cs · SettingsController.cs · UploadController.cs · SyncController.cs · UtilityController.cs · WallpaperController.cs
十、前端文件清单(frontend/)
- 配置: package.json · vite.config.ts · tsconfig.json · capacitor.config.ts · index.html · .env.development · .env.production
- 入口: main.ts · App.vue · router/index.ts
- 样式: styles/tokens.css · styles/global.css
- API 层: api/http.ts · api/categories.ts · api/bookmarks.ts · api/searchEngines.ts · api/settings.ts · api/upload.ts · api/sync.ts · api/utility.ts · api/wallpaper.ts
- 类型: types/api.ts
- Stores: stores/settings.ts · stores/categories.ts · stores/bookmarks.ts · stores/searchEngines.ts · stores/sync.ts
- 工具: utils/storage.ts · utils/toast.ts · utils/icon.ts · utils/color.ts
- 通用组件: AppIcon.vue · AppButton.vue · AppCard.vue · AppModal.vue · AppDrawer.vue · AppWallpaper.vue · AppToastHost.vue · AppIconPicker.vue
- 业务组件: AppSearchBar.vue · AppSidebar.vue · AppLinkCard.vue · AppLinkListItem.vue · AppCategoryTabs.vue · AppMobileTopBar.vue · AppFab.vue · BookmarkForm.vue · CategoryForm.vue
- 视图: views/HomeView.vue · views/SettingsView.vue
- 文档: ANDROID.md
十一、部署 / 文档
- 根目录 README.md
- 完整部署手册 docs/DEPLOY.md(P47 · 1000 行 / 10 大章:架构 → 准备 → 模式选择 → 编译 → 上传 → 配置 → 上线 → 日志 → 运维 → 附录)
- docker-compose.yml(P47 · 已补全 5 项生产级配置:restart:always / healthcheck / logging 轮转 / TZ / 删 MySQL 3306 公网暴露)
- docker/backend.Dockerfile(多阶段:前端构建 → .NET 发布 → 运行时)
- docker/nginx.conf(参考用,部署采用前后端同镜像)
- docker/README.md
十二、验证情况
- ✅ 后端:
dotnet build0 错误;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 步骤
- ⏸ Docker 部署:docker compose 配置文件已写好并补强,未实际构建镜像(需 Docker Desktop)