Files
MyHomepage/说明文档.md
T
g82tt 68be41e7a2 初始提交:浏览器首页 MyHomePage 全栈项目
# 项目概述
个人浏览器首页导航应用,支持书签分类管理、搜索引擎快捷搜索、
必应每日壁纸轮播、前后端分离部署,适配 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 模式)
2026-07-05 05:09:56 +08:00

78 KiB
Raw Blame History

浏览器首页(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 骨架 + SqlSugarMySQL/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 接入后端 APICRUD 联调 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 + SqlSugarMySQL/SQLite)后端 CRUD 全部跑通,Swagger + 冒烟测试通过 已完成
2026-07-04 P3-P9 Vue 3 + Vite + Pinia + 桌面/移动双布局 + 设置面板 + 全量 CRUD 联调通过 已完成
2026-07-04 P10 多端同步:手动 + 30s 自动轮询 + visibility 触发 已完成
2026-07-04 P11 Docker ComposeMySQL + 后端 + 前端一体镜像) 已完成
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.Code400/404);axios 拦截器读 body messageE2E 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 弹出菜单 stackingz-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-horizontalmore-verticalvue-tsc + build 通过 已完成
2026-07-04 P21.1 P21 遗漏 fixicon.ts map 漏注册 MoreVerticalAppIcon 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.tsHomeView.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.vueCategoryForm.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.csBuildTree + BuildTreeFromFlatparentId==0 → 顶级;孤儿降级);清理前端调试日志;dotnet build 0 错误;后端重启;curl /api/sync/changes 看到树形结构 已完成
2026-07-04 P26 设置面板增加「链接行为」开关(6 文件全链路):后端 Setting.csOpenLinksInNewTab int default 1int 不用 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 + SettingUpdateopenLinksInNewTabstores/settings.ts 默认 true + setOpenLinksInNewTabHomeView.vue openBookmarktarget = settings.openLinksInNewTab ? '_blank' : '_self'SettingsView.vue 新增「链接行为」sectiontoggle + 反转逻辑 + P26.1 修复点击传同值 bugP26.2 修复 sync 漏字段) 已完成
2026-07-04 P27 前端页面美化:新建 utils/color.ts colorFromUrl + firstChar27 个品牌色板 + 32-bit HSL 哈希);tokens.css--glass-bg-faint / --glass-blur-sm/-lg / --link-logo-size: 96px / --link-card-min-height: 88pxAppLinkCard.vue 横排彩色 logo + 标题描述改造(96×88 品牌色块 + 玻璃面板 + 编辑/删除绝对定位 hover 浮现);AppLinkListItem.vue 移动端同步升级(56px logo);AppSidebar.vue 顶部加用户头像 header48px 蓝渐变 + "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: 64pxAppLinkCard.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 棋盘底色 + "自适应"按钮 + 动态 hintform.colorBg: string | null);④ extractDominantColor 主色提取(utils/color.ts <img crossOrigin="anonymous"> 缩放 32×32 + getImageData + 像素过滤 + 5-bit 桶量化 + 6s timeout);⑤ 后端 ColorBg 字段全链路(Bookmark.cs ColorBg string? Length=32BookmarkDtos.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 防护 IsPrivateOrLocalhostAsyncDns.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 DIAddMemoryCache + 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 误识别 bugcurl 真实 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/lengthLogDebug 输出 candidate linksLogWarning 在 HTML 不含 rel=icon 时输出前 200 字符的 HTML 摘要。⑧ 3 项验证全过:POST /api/utility/favicon {url:github.com} → 抓取成功(之前 null 修好);{url:baidu.com} → 抓取成功;{url:gitea.snow82.fnos.net}iconUrl: nullHTTP 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=360chromeURL 改造规则 bdr/__85bdm/{W}_{H}_{Q=80画质} + p15.qhimg.comp19.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 UpdateAsyncAllowedWallpaperIntervals = {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.csWallpaperCategoryDto { 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},分类空时兜底用 364K专区);RewriteUrl(original, w, h)bdr/__85bdm/{w}_{h}_80只改路径段、保留主机 — 实测 360 CDN 主机是 p3/p5/p6/p7/p8/p9.qhimg.com 任意一个,不能用接口说明里"p15→p19"硬写)。⑧ 后端新建 WallpaperController.csGET /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 DIAddHttpClient(nameof(WallpaperService), c => c.Timeout = 10s + UA) + AddScoped<WallpaperService>()。⑩ 前端 types/api.ts 加 P34 字段。⑪ 前端新建 api/wallpaper.tsfetchWallpaperCategories() 12s / fetchWallpaperRandom(cid, w, h) 12s / refreshWallpaperRandom(cid, w, h) 15s。⑫ 前端 stores/settings.ts 加 P34 state + actionsstate 加 wallpaperUrl / wallpaperCategories / wallpaperLoading / wallpaperError 4 个 refactions 加 6 个;applyBackground 加 P34 分支(wallpaperEnabled=true不覆盖 --bg-image)。⑬ 前端 SettingsView.vue 加 360 壁纸 sectiontoggle + 仅启用时显示的表单(分类 select + 间隔 select + 「立即切换」按钮)+ 状态显示。⑭ 前端 AppWallpaper.vue 改造支持 360 模式 + 三端 + 定时器:style computed 二选一;onMounted 拉图 + rebuildTimer()onBeforeUnmount 清理;resize 用 rAF 节流;watch(wallpaperEnabled, ...) 开关时立即拉图 + 重建 timer;rebuildTimersetInterval 每 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 个红 500GET /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 根本没把请求转到 5141Vite 自己返了 500(不是 .NET 后端的 500,是 Vite proxy 找不到 target 的 500)。④ 根因 — vite.config.ts:7 默认 target 是 5080const 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:7apiTarget = 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:5since=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__statscolor: 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-hashVue 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-hashCSS 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 = 多类型 fallbackvs「business 渲染」(searchbar logo = 已知图片 URL)应分开 —— AppIcon 适合「多类型 + 兜底」,不适合「已知 URL 高频展示」;④ build passedrendered 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 AAAvue-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.csOpenSearchInNewTab int default 1int 不用 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 + SettingUpdateopenSearchInNewTab: 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 必须显式 DataTypevarchar/string 推断 OKint/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-stoppedalwaysK8s/监控/orchestration 需要);b) 删除 MySQL 3306 公网暴露(之前 ports: 3306:3306 是 OWASP 严重漏洞,互联网上任何人都能连 MySQL,注释引导改 127.0.0.1:3306:3306 或 SSH 隧道);c) 后端加 healthcheck: wget --spider http://localhost:8080/healthinterval: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 项生产 checklistrestart: 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 daemon1Panel 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 已部署就不用教装 MySQL1Panel 已装就不用教装 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.jsonapiBase → 浏览器 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:32System.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://后端地址:端口/swaggerP50 教训:① 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 映射到容器 /uploadsappsettings.jsonUpload: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/catchcatch 块 LogError 完整异常堆栈 + 全部上下文(UploadOptions.Path / IsRooted / ContentRoot / env / root / dir / fullPath),再抛 BusinessException("文件保存失败: {ExType}: {Message}", 500);③ FaviconService.cs:80-100 LogWarning → LogErrordocker 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.shchmod 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.jsonapiBase 改成 "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/


十、前端文件清单(frontend/


十一、部署 / 文档

  • 根目录 README.md
  • 完整部署手册 docs/DEPLOY.mdP47 · 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 build 0 错误;CodeFirst 自动建表;SQLite / MySQL 双驱动切换已配置
  • 后端 API8 个模块 GET/POST/PUT/DELETE + upload + sync + utility + wallpaper 全部 200swagger 可见
  • 前端:vue-tsc --noEmit 通过;vite build 产出 ~46KB CSS + 117KB JSgzip 后 81KB
  • 联调:vite proxy 5173→5141(默认端口已与后端 --urls 同步,P34.2 修复),浏览器无报错
  • 多端同步:手动 / 30s 轮询 / visibilitychange 触发
  • 壁纸三端不变形:aspect ratio preset 命中 + fallback RewriteUrlP34.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