初始提交:浏览器首页 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 模式)
This commit is contained in:
2026-07-05 05:09:56 +08:00
commit 68be41e7a2
129 changed files with 15900 additions and 0 deletions
@@ -0,0 +1,245 @@
using MyHomePage.Api.Models.Entities;
using SqlSugar;
namespace MyHomePage.Api.Infrastructure.Database;
/// <summary>
/// 数据库初始化器:CodeFirst 建表 + 种子数据。
/// 应用启动时调用一次。
/// </summary>
public class DatabaseInitializer
{
private readonly SqlSugarContext _ctx;
private readonly ILogger<DatabaseInitializer> _logger;
public DatabaseInitializer(SqlSugarContext ctx, ILogger<DatabaseInitializer> logger)
{
_ctx = ctx;
_logger = logger;
}
/// <summary>建表 + 种子数据</summary>
public async Task InitializeAsync()
{
try
{
_logger.LogInformation("开始 CodeFirst 建表...");
// ===== 兼容老库:表已存在则跳过 CodeFirst InitTablesSqlite 不支持 alter column primary key,触发表结构不一致会抛)=====
// 后续靠 MigrateSettingColumns / MigrateBookmarkColumns 给老库补列。
const string settingsTable = "settings";
if (_ctx.Db.DbMaintenance.IsAnyTable(settingsTable))
{
_logger.LogInformation("检测到 settings 表已存在,跳过 CodeFirst(已通过轻量迁移补齐列)");
}
else
{
_ctx.Db.CodeFirst.InitTables(
typeof(Category),
typeof(Bookmark),
typeof(SearchEngine),
typeof(Setting),
typeof(SyncLog)
);
}
// 对已存在数据库做轻量迁移:给 settings 表补上新增列(CodeFirst InitTables 不会自动 ALTER 老库)
MigrateSettingColumns();
// 给 bookmarks 表补充 ColorBg 列(P28 链接 logo 背景色)
MigrateBookmarkColumns();
// 给 search_engines 表补充 IconType / IconUrl / ColorBg 列(P37 引擎图标逻辑对齐链接)
MigrateSearchEngineColumns();
// 给 settings 表补充 OpenSearchInNewTab 列(P46 搜索框行为开关)
MigrateSettingColumnsV2();
await SeedAsync();
_logger.LogInformation("数据库初始化完成");
}
catch (Exception ex)
{
_logger.LogError(ex, "数据库初始化失败");
throw;
}
}
/// <summary>为 settings 表补充新列(已存在则跳过)。</summary>
private void MigrateSettingColumns()
{
const string tableName = "settings";
// P26.2
if (!_ctx.Db.DbMaintenance.IsAnyColumn(tableName, "OpenLinksInNewTab"))
{
_ctx.Db.DbMaintenance.AddColumn(tableName, new DbColumnInfo
{
DbColumnName = "OpenLinksInNewTab",
DataType = "INTEGER",
IsNullable = false,
DefaultValue = "1"
});
_logger.LogInformation("已为 settings 表补充列 {Column}", "OpenLinksInNewTab");
}
// P34 360 壁纸模式
if (!_ctx.Db.DbMaintenance.IsAnyColumn(tableName, "WallpaperEnabled"))
{
_ctx.Db.DbMaintenance.AddColumn(tableName, new DbColumnInfo
{
DbColumnName = "WallpaperEnabled",
DataType = "INTEGER",
IsNullable = false,
DefaultValue = "0"
});
_logger.LogInformation("已为 settings 表补充列 {Column}", "WallpaperEnabled");
}
if (!_ctx.Db.DbMaintenance.IsAnyColumn(tableName, "WallpaperCategoryId"))
{
_ctx.Db.DbMaintenance.AddColumn(tableName, new DbColumnInfo
{
DbColumnName = "WallpaperCategoryId",
DataType = "varchar(32)",
IsNullable = true,
DefaultValue = ""
});
_logger.LogInformation("已为 settings 表补充列 {Column}", "WallpaperCategoryId");
}
if (!_ctx.Db.DbMaintenance.IsAnyColumn(tableName, "WallpaperInterval"))
{
_ctx.Db.DbMaintenance.AddColumn(tableName, new DbColumnInfo
{
DbColumnName = "WallpaperInterval",
DataType = "INTEGER",
IsNullable = false,
DefaultValue = "30"
});
_logger.LogInformation("已为 settings 表补充列 {Column}", "WallpaperInterval");
}
}
/// <summary>为 bookmarks 表补充 ColorBg 列(已存在则跳过)。</summary>
private void MigrateBookmarkColumns()
{
const string tableName = "bookmarks";
const string newColumn = "ColorBg";
if (!_ctx.Db.DbMaintenance.IsAnyColumn(tableName, newColumn))
{
_ctx.Db.DbMaintenance.AddColumn(tableName, new DbColumnInfo
{
DbColumnName = newColumn,
DataType = "varchar(32)",
IsNullable = true
});
_logger.LogInformation("已为 bookmarks 表补充列 {Column}", newColumn);
}
}
/// <summary>为 search_engines 表补充 IconType / IconUrl / ColorBg 列(已存在则跳过,P37 引擎图标逻辑对齐链接)。</summary>
private void MigrateSearchEngineColumns()
{
const string tableName = "search_engines";
AddColumnIfMissing(tableName, "IconType", "varchar(16)", isNullable: false, defaultValue: "lucide");
AddColumnIfMissing(tableName, "IconUrl", "varchar(512)", isNullable: true);
AddColumnIfMissing(tableName, "ColorBg", "varchar(32)", isNullable: true);
}
/// <summary>P46:给 settings 表补 OpenSearchInNewTab 列(int default 1)—— 复刻 P37/P42 的「轻量迁移」模式</summary>
private void MigrateSettingColumnsV2()
{
const string tableName = "settings";
// 注意:int 类型的 column 在 SqlSugar + SQLite 下要显式声明 DataType
if (!_ctx.Db.DbMaintenance.IsAnyColumn(tableName, "OpenSearchInNewTab"))
{
_ctx.Db.DbMaintenance.AddColumn(tableName, new DbColumnInfo
{
DbColumnName = "OpenSearchInNewTab",
DataType = "int",
IsNullable = false,
DefaultValue = "1"
});
}
}
private void AddColumnIfMissing(string tableName, string columnName, string dataType, bool isNullable, string? defaultValue = null)
{
if (_ctx.Db.DbMaintenance.IsAnyColumn(tableName, columnName)) return;
_ctx.Db.DbMaintenance.AddColumn(tableName, new DbColumnInfo
{
DbColumnName = columnName,
DataType = dataType,
IsNullable = isNullable,
DefaultValue = defaultValue
});
_logger.LogInformation("已为 {Table} 表补充列 {Column}", tableName, columnName);
}
/// <summary>写入种子数据(仅当表为空时执行)</summary>
private async Task SeedAsync()
{
var db = _ctx.Db;
// 搜索引擎种子
if (!db.Queryable<SearchEngine>().Any())
{
var engines = new List<SearchEngine>
{
new() { Name = "百度", UrlTemplate = "https://www.baidu.com/s?wd={q}", Icon = "search", Sort = 0, IsDefault = true, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow },
new() { Name = "Google", UrlTemplate = "https://www.google.com/search?q={q}", Icon = "search", Sort = 1, IsDefault = false, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow },
new() { Name = "Bing", UrlTemplate = "https://www.bing.com/search?q={q}", Icon = "search", Sort = 2, IsDefault = false, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }
};
await db.Insertable(engines).ExecuteCommandAsync();
_logger.LogInformation("已写入搜索引擎种子数据 ({Count} 条)", engines.Count);
}
// 设置种子(单行)
if (!db.Queryable<Setting>().Any())
{
var setting = new Setting
{
Id = 1,
ThemeMode = "dark",
AccentColor = "#6c5ce7",
BackgroundImage = "wp1",
BackgroundType = "preset",
OpenLinksInNewTab = 1,
UpdatedAt = DateTime.UtcNow
};
await db.Insertable(setting).ExecuteCommandAsync();
_logger.LogInformation("已写入默认设置");
}
// 分类 + 链接种子
if (!db.Queryable<Category>().Any())
{
var now = DateTime.UtcNow;
// 一级:常用工具
var catTools = new Category
{
ParentId = 0,
Name = "常用工具",
Icon = "wrench",
Sort = 0,
CreatedAt = now,
UpdatedAt = now
};
catTools.Id = await db.Insertable(catTools).ExecuteReturnIdentityAsync();
var catToolsId = catTools.Id;
// 二级分类(单独插入回填 Id,方便后续链接绑定)
var subAi = new Category { ParentId = catToolsId, Name = "AI 工具", Icon = "bot", Sort = 1, CreatedAt = now, UpdatedAt = now };
var subDev = new Category { ParentId = catToolsId, Name = "开发工具", Icon = "code-2", Sort = 2, CreatedAt = now, UpdatedAt = now };
subAi.Id = await db.Insertable(subAi).ExecuteReturnIdentityAsync();
subDev.Id = await db.Insertable(subDev).ExecuteReturnIdentityAsync();
// 链接示例
var bookmarks = new List<Bookmark>
{
new() { CategoryId = subAi.Id, Title = "ChatGPT", Url = "https://chat.openai.com", Description = "AI 对话助手,智能问答", Icon = "bot", IconType = "lucide", Sort = 0, CreatedAt = now, UpdatedAt = now },
new() { CategoryId = subAi.Id, Title = "Claude", Url = "https://claude.ai", Description = "Anthropic 推出的 AI 助手", Icon = "bot", IconType = "lucide", Sort = 1, CreatedAt = now, UpdatedAt = now },
new() { CategoryId = subDev.Id, Title = "GitHub", Url = "https://github.com", Description = "代码托管与协作平台", Icon = "github", IconType = "lucide", Sort = 0, CreatedAt = now, UpdatedAt = now },
new() { CategoryId = subDev.Id, Title = "MDN", Url = "https://developer.mozilla.org", Description = "Web 技术文档参考", Icon = "book", IconType = "lucide", Sort = 1, CreatedAt = now, UpdatedAt = now },
new() { CategoryId = subDev.Id, Title = "Stack Overflow", Url = "https://stackoverflow.com", Description = "开发者问答社区", Icon = "message-circle", IconType = "lucide", Sort = 2, CreatedAt = now, UpdatedAt = now },
new() { CategoryId = subDev.Id, Title = "VS Code", Url = "https://code.visualstudio.com", Description = "轻量级代码编辑器", Icon = "code-2", IconType = "lucide", Sort = 3, CreatedAt = now, UpdatedAt = now }
};
await db.Insertable(bookmarks).ExecuteCommandAsync();
_logger.LogInformation("已写入分类 / 链接种子数据");
}
}
}