初始提交:浏览器首页 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:
@@ -0,0 +1,10 @@
|
||||
namespace MyHomePage.Api.Infrastructure.Configuration;
|
||||
|
||||
/// <summary>跨域配置节点(对应 appsettings.json 中的 Cors)</summary>
|
||||
public class CorsOptions
|
||||
{
|
||||
public const string SectionName = "Cors";
|
||||
|
||||
/// <summary>允许的来源列表</summary>
|
||||
public string[] Origins { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace MyHomePage.Api.Infrastructure.Configuration;
|
||||
|
||||
/// <summary>数据库配置节点(对应 appsettings.json 中的 Database)</summary>
|
||||
public class DatabaseOptions
|
||||
{
|
||||
public const string SectionName = "Database";
|
||||
|
||||
/// <summary>数据库提供者:MySql | Sqlite</summary>
|
||||
public string Provider { get; set; } = "Sqlite";
|
||||
|
||||
/// <summary>连接字符串</summary>
|
||||
public string ConnectionString { get; set; } = "Data Source=myhomepage.db";
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace MyHomePage.Api.Infrastructure.Configuration;
|
||||
|
||||
/// <summary>文件上传配置节点(对应 appsettings.json 中的 Upload)</summary>
|
||||
public class UploadOptions
|
||||
{
|
||||
public const string SectionName = "Upload";
|
||||
|
||||
/// <summary>上传文件保存目录(相对 ContentRoot 解析)</summary>
|
||||
public string Path { get; set; } = "Uploads";
|
||||
|
||||
/// <summary>前端访问上传文件时使用的基础 URL 前缀</summary>
|
||||
public string BaseUrl { get; set; } = "/uploads";
|
||||
|
||||
/// <summary>单文件最大字节数(默认 10MB)</summary>
|
||||
public long MaxSizeBytes { get; set; } = 10 * 1024 * 1024;
|
||||
}
|
||||
@@ -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 InitTables(Sqlite 不支持 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("已写入分类 / 链接种子数据");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using MyHomePage.Api.Infrastructure.Configuration;
|
||||
using SqlSugar;
|
||||
|
||||
namespace MyHomePage.Api.Infrastructure.Database;
|
||||
|
||||
/// <summary>
|
||||
/// SqlSugar 上下文(单例生命周期)。
|
||||
/// 根据 <see cref="DatabaseOptions.Provider"/> 自动切换 MySQL / SQLite。
|
||||
/// </summary>
|
||||
public class SqlSugarContext : IDisposable
|
||||
{
|
||||
private readonly DatabaseOptions _options;
|
||||
public ISqlSugarClient Db { get; }
|
||||
|
||||
public SqlSugarContext(IOptions<DatabaseOptions> options)
|
||||
{
|
||||
_options = options.Value;
|
||||
Db = new SqlSugarScope(BuildConnectionConfig(_options), BuildAopConfig());
|
||||
}
|
||||
|
||||
/// <summary>根据配置构建 SqlSugar 连接配置</summary>
|
||||
private static ConnectionConfig BuildConnectionConfig(DatabaseOptions options)
|
||||
{
|
||||
var dbType = options.Provider.Equals("MySql", StringComparison.OrdinalIgnoreCase)
|
||||
? DbType.MySql
|
||||
: DbType.Sqlite;
|
||||
|
||||
return new ConnectionConfig
|
||||
{
|
||||
ConfigId = "default",
|
||||
ConnectionString = options.ConnectionString,
|
||||
DbType = dbType,
|
||||
IsAutoCloseConnection = true,
|
||||
InitKeyType = InitKeyType.Attribute,
|
||||
// SqlSugar AOP 启用默认值
|
||||
MoreSettings = new ConnMoreSettings
|
||||
{
|
||||
IsAutoRemoveDataCache = true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>配置 AOP:日志 + 性能监控</summary>
|
||||
private static Action<SqlSugarClient> BuildAopConfig() => db =>
|
||||
{
|
||||
db.Aop.OnLogExecuting = (sql, parameters) =>
|
||||
{
|
||||
// 由 Serilog / 默认 logger 接管,避免在控制台双打
|
||||
// 这里只做轻量占位,实际日志由 SqlSugarScopeClientConfiguration 注入的 logger 输出
|
||||
};
|
||||
};
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Db?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user