@using Microsoft.Extensions.Options
@using System.Globalization
@using XFEStudioWebSite.Localization
@using XFEStudioWebSite.Options
@using XFEStudioWebSite.Services
@inject IOptions<SiteSettings> SiteSettings
@inject IOptions<GitSettings> GitSettings
@inject IOptions<NuGetOptions> NuGetSettings
@inject IUserService UserService
@inject IPublicUrlService PublicUrls
@{
static string ResolveConfiguredHost(string value)
{
if (string.IsNullOrWhiteSpace(value))
return string.Empty;
var candidate = value.Contains("://", StringComparison.Ordinal)
? value
: $"https://{value}";
return Uri.TryCreate(candidate, UriKind.Absolute, out var uri)
? uri.Host
: value.Split(':', 2)[0];
}
var requestHost = Context.Request.Host.Host;
var currentController = ViewContext.RouteData.Values["controller"]?.ToString() ?? string.Empty;
var mainHost = ResolveConfiguredHost(SiteSettings.Value.MainDomain);
var docsHost = ResolveConfiguredHost(SiteSettings.Value.DocsDomain);
var gitHost = ResolveConfiguredHost(GitSettings.Value.PublicUrl);
var nuGetHost = ResolveConfiguredHost(NuGetSettings.Value.PublicUrl);
var mainPublicUrl = PublicUrls.Main();
var docsPublicUrl = PublicUrls.Docs();
var gitPublicUrl = PublicUrls.Git();
var nuGetPublicUrl = PublicUrls.NuGet();
var isMainSiteHost = string.IsNullOrWhiteSpace(mainHost)
|| requestHost.Equals(mainHost, StringComparison.OrdinalIgnoreCase);
var isGitAppHost = !string.IsNullOrWhiteSpace(gitHost)
&& requestHost.Equals(gitHost, StringComparison.OrdinalIgnoreCase);
var currentPublicUrl = $"{Context.Request.Scheme}://{Context.Request.Host}{Context.Request.PathBase}{Context.Request.Path}{Context.Request.QueryString}";
var loginUrl = mainPublicUrl + "/Account/Login" + QueryString.Create("returnUrl", currentPublicUrl);
var currentCulture = CultureInfo.CurrentUICulture.Name;
var textDirection = SiteLanguageCatalog.IsRightToLeft(currentCulture) ? "rtl" : "ltr";
var edgeCountry = SiteLanguageCatalog.NormalizeCountryCode(
Context.Request.Headers[SiteLanguageCatalog.EdgeOneCountryHeader].ToString());
var isNuGetSection = currentController.StartsWith("NuGet", StringComparison.OrdinalIgnoreCase)
|| requestHost.Equals(nuGetHost, StringComparison.OrdinalIgnoreCase);
var isGitSection = !isNuGetSection && (currentController.StartsWith("Git", StringComparison.OrdinalIgnoreCase)
|| requestHost.Equals(gitHost, StringComparison.OrdinalIgnoreCase));
var hasDedicatedDocsHost = !string.IsNullOrEmpty(docsHost)
&& !docsHost.Equals(mainHost, StringComparison.OrdinalIgnoreCase);
var isDocsSection = !isNuGetSection && !isGitSection
&& (currentController.Equals("Docs", StringComparison.OrdinalIgnoreCase)
|| (hasDedicatedDocsHost && requestHost.Equals(docsHost, StringComparison.OrdinalIgnoreCase)));
var isDownloadsSection = !isNuGetSection && !isGitSection && !isDocsSection
&& currentController.Equals("Downloads", StringComparison.OrdinalIgnoreCase);
var isDiscussionSection = !isNuGetSection && !isGitSection && !isDocsSection
&& currentController.Equals("Discussion", StringComparison.OrdinalIgnoreCase);
var isHomeSection = !isNuGetSection && !isGitSection && !isDocsSection
&& !isDownloadsSection && !isDiscussionSection
&& currentController.Equals("Home", StringComparison.OrdinalIgnoreCase);
}
<!DOCTYPE html>
<html lang="@currentCulture" dir="@textDirection">
<head>
<meta charset="utf-8" />
<title>寰宇朽力 - @ViewData["Title"]</title>
<link rel="icon" type="image/png" href="/favicon.png" />
@if (isGitAppHost)
{
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="apple-touch-icon" href="/img/pwa/icon-192.png" />
<meta name="application-name" content="XFE Studio Git" />
<meta name="theme-color" content="#9786d3" />
<script src="/js/pwa-install.js" defer></script>
}
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(() => {
const cookie = document.cookie
.split(';')
.map(item => item.trim())
.find(item => item.startsWith('xfe-theme='));
let theme = null;
try {
theme = cookie ? decodeURIComponent(cookie.slice('xfe-theme='.length)) : null;
} catch { }
if (theme !== 'light' && theme !== 'dark') {
try { theme = localStorage.getItem('theme'); } catch { }
}
if (theme !== 'light' && theme !== 'dark') theme = 'dark';
document.documentElement.classList.toggle('dark', theme === 'dark');
document.documentElement.setAttribute('data-bs-theme', theme);
})();
</script>
<link rel="stylesheet" href="~/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/theme-variables.css" asp-append-version="true" />
<link rel="stylesheet" href="~/css/site-language.css" asp-append-version="true" />
<link rel="stylesheet" href="~/css/notifications.css" asp-append-version="true" />
<link rel="stylesheet" href="https://lib.baomitu.com/bootstrap-icons/1.11.3/font/bootstrap-icons.min.css" />
<style>
body.no-transition {
transition: none; /* 页面加载时禁用过渡 */
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: sans-serif;
transition: background-color 0.3s, color 0.3s; /* 只在切换时生效 */
scrollbar-color: color-mix(in srgb, var(--main-color) 50%, transparent) transparent;
}
body.no-transition {
transition: none; /* 页面加载时禁用过渡 */
}
a {
color: var(--link-color);
}
a:hover {
color: var(--main-deep-color);
text-decoration: underline;
}
.navbar .navbar-nav .nav-link {
border-radius: .55rem;
transition: background-color .18s ease, color .18s ease;
}
.navbar .navbar-nav .nav-link.active {
background: rgba(255, 255, 255, .08);
color: #fff;
}
.navbar .navbar-nav .nav-link.active:hover,
.navbar .navbar-nav .nav-link.active:focus-visible {
background: rgba(255, 255, 255, .13);
color: #fff;
text-decoration: none;
}
.site-footer {
background: var(--footer-bg);
border-top: 1px solid var(--footer-border);
color: var(--text-color);
width: 100%;
}
.site-footer a {
color: var(--link-color);
text-decoration: none;
font-weight: 500;
}
.site-footer a:hover {
text-decoration: underline;
}
/* Breadcrumb Adaptation */
.breadcrumb-item.active {
color: var(--text-color) !important;
}
.breadcrumb-item + .breadcrumb-item::before {
color: var(--text-secondary-color) !important;
}
.radio-control {
color: var(--on-main-color);
background: var(--main-color);
}
.user-avatar {
width: 48px;
height: 48px;
background-color: var(--main-color);
color: var(--on-main-color);
border-radius: 999px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.5rem;
flex-shrink: 0;
border: 2px solid var(--bg-color);
box-shadow: 0 0 0 1px var(--main-color);
}
/* Dropdown Theme Adaptation */
.dropdown-menu {
background-color: var(--bg-color);
color: var(--text-color);
border-color: var(--footer-border);
}
.dropdown-item {
color: var(--text-color);
}
.dropdown-item:hover, .dropdown-item:focus {
background-color: color-mix(in srgb, var(--text-color) 5%, transparent);
color: var(--text-color);
}
.dropdown-header {
color: var(--text-secondary-color);
}
.dropdown-divider {
border-top-color: var(--footer-border);
}
.text-muted {
color: var(--text-secondary-color) !important;
}
.table {
color: var(--text-color);
--bs-table-color: var(--text-color);
--bs-table-bg: transparent;
--bs-table-border-color: var(--profile-border-color);
--bs-table-hover-color: var(--text-color);
--bs-table-hover-bg: color-mix(in srgb, var(--text-color) 5%, transparent);
}
.table th {
border-color: var(--profile-border-color);
color: var(--text-secondary-color);
font-weight: 600;
text-transform: uppercase;
font-size: 0.85rem;
}
.table td {
border-color: var(--profile-border-color);
vertical-align: middle;
}
.form-text {
color: var(--text-secondary-color);
}
.list-group-item {
background-color: var(--bg-color);
border-color: var(--footer-border);
color: var(--text-color);
transition: background-color 0.2s;
}
.list-group-item-action:hover, .list-group-item-action:focus {
background-color: color-mix(in srgb, var(--text-color) 4%, var(--bg-color));
color: var(--text-color);
}
/* Utility Class Overrides */
.bg-white {
background-color: var(--bg-color) !important;
color: var(--text-color) !important;
}
.text-dark {
color: var(--text-color) !important;
}
.text-muted {
color: var(--text-secondary-color) !important;
}
.bg-light {
background-color: color-mix(in srgb, var(--text-color) 5%, transparent) !important;
color: var(--text-color) !important;
}
/* Badge Theme Overrides */
.badge {
font-weight: 500;
}
.badge.bg-light {
background-color: color-mix(in srgb, var(--text-color) 10%, transparent) !important;
color: var(--text-color) !important;
}
/* Card & Layout Adaptation */
.card {
background-color: var(--bg-color);
border-color: var(--footer-border);
color: var(--text-color);
}
.card-body {
background: var(--bg-color);
/*Add radios corner*/
border-radius: 0.375rem;
/*Add main color shadow*/
/* box-shadow: 0px 0px 2px var(--main-color); */
}
.card-header {
background-color: var(--bg-color) !important;
border-bottom-color: var(--footer-border);
color: var(--text-color);
}
::-webkit-scrollbar {
width: 5px;
}
::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--main-color) 50%, transparent);
border-radius: 999px;
}
::-webkit-scrollbar-track {
background: transparent;
}
/* Tabs */
.nav-tabs .nav-link.active {
background-color: var(--bg-color);
border-color: var(--footer-border) var(--footer-border) var(--bg-color);
color: var(--main-color);
}
.nav-tabs .nav-link {
color: var(--text-secondary-color);
}
.nav-tabs {
border-bottom-color: var(--footer-border);
}
.tab-content {
border-color: var(--footer-border) !important;
}
.shadow-sm {
box-shadow: 0 .125rem .25rem var(--focus-main-color) !important;
}
.btn-close {
filter: var(--text-color, 0);
}
.btn-close-white {
filter: var(--text-color, 0);
}
/* Button Theme Overrides */
.btn-primary {
background-color: var(--main-color);
border-color: var(--main-color);
color: var(--on-main-color);
}
.btn-primary:hover, .btn-primary:focus, .btn-primary:active {
background-color: var(--main-deep-color);
border-color: var(--main-deep-color);
color: var(--on-main-color);
}
.btn-outline-primary {
color: var(--main-color);
border-color: var(--main-color);
}
.btn-outline-primary:hover, .btn-outline-primary:focus, .btn-outline-primary:active {
background-color: var(--main-color);
border-color: var(--main-color);
color: var(--on-main-color);
}
.btn-secondary {
border-color: var(--main-color);
background-color: var(--bg-color);
color: var(--main-color);
}
.btn-secondary:hover {
border-color: var(--main-deep-color);
background-color: var(--bg-color);
color: var(--main-deep-color);
}
.btn-outline-secondary {
color: var(--text-secondary-color);
border-color: var(--footer-border);
}
.btn-outline-secondary:hover {
background-color: var(--text-color);
color: var(--bg-color);
}
.btn-link {
color: var(--link-color);
}
.btn-link:hover {
color: var(--main-deep-color);
}
/* Form Theme Overrides */
.form-control, .form-select {
background-color: var(--bg-color);
border-color: var(--footer-border);
color: var(--text-color);
}
.form-control::placeholder {
color: var(--text-secondary-color);
opacity: 0.8;
}
.form-label {
color: var(--text-secondary-color);
font-weight: 500;
}
.form-control:focus, .form-select:focus {
border-color: var(--main-color);
box-shadow: 0 0 0 0.25rem var(--focus-main-color);
background-color: var(--bg-color);
color: var(--text-color);
}
.form-control:disabled, .form-control[readonly] {
background-color: color-mix(in srgb, var(--bg-color) 95%, #ffffff 5%);
border-color: var(--footer-border);
color: var(--text-secondary-color);
opacity: 1;
}
.input-group-text {
background-color: color-mix(in srgb, var(--bg-color) 96%, #000000 4%);
border-color: var(--footer-border);
color: var(--text-color);
}
.form-check-input:checked {
background-color: var(--main-color);
border-color: var(--main-color);
}
.form-check-input:focus {
border-color: var(--main-color);
box-shadow: 0 0 0 0.25rem var(--focus-main-color);
}
/* Navbar Hover Dropdown */
@@media (min-width: 992px) {
.navbar .dropdown:hover .dropdown-menu {
display: block;
margin-top: 0;
}
.navbar .dropdown:hover > .user-menu-dropdown {
right: 0;
left: auto;
}
}
</style>
<link rel="stylesheet" href="~/css/dialogs.css" asp-append-version="true" />
<link rel="stylesheet" href="~/css/dropdowns.css" asp-append-version="true" />
@RenderSection("Styles", required: false)
</head>
<body class="d-flex flex-column min-vh-100 @(ViewData["BodyClass"] as string)"
data-site-culture="@currentCulture" data-edgeone-country="@edgeCountry"
data-main-origin="@mainPublicUrl">
<partial name="_Notifications" />
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
<div class="container-fluid">
<a class="navbar-brand" href="@mainPublicUrl" data-no-localize>XFE-寰宇朽力</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link @(isHomeSection ? "active" : null)" href="@mainPublicUrl"
aria-current="@(isHomeSection ? "page" : null)">首页</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle @(isDocsSection ? "active" : null)" href="#" id="docsHeaderDropdown" role="button"
data-bs-toggle="dropdown" aria-expanded="false" aria-current="@(isDocsSection ? "page" : null)">
文档
</a>
<ul class="dropdown-menu" aria-labelledby="docsHeaderDropdown">
<li><a class="dropdown-item" href="@docsPublicUrl">文档中心</a></li>
<li><a class="dropdown-item" href="@docsPublicUrl/API">API 文档</a></li>
<li><a class="dropdown-item" href="@docsPublicUrl/Errors">错误文档</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link @(isDownloadsSection ? "active" : null)" href="@mainPublicUrl/Downloads"
aria-current="@(isDownloadsSection ? "page" : null)">下载</a>
</li>
<li class="nav-item">
<a class="nav-link @(isDiscussionSection ? "active" : null)" href="@mainPublicUrl/Discussion"
aria-current="@(isDiscussionSection ? "page" : null)">讨论</a>
</li>
<li class="nav-item">
<a class="nav-link site-git-app-link @(isGitSection ? "active" : null)" href="@gitPublicUrl"
target="_blank" rel="noopener noreferrer"
title="在新标签页打开 XFE Git"
aria-label="Git(在新标签页打开)"
aria-current="@(isGitSection ? "page" : null)">
<i class="bi bi-git me-1" aria-hidden="true"></i>Git
<i class="bi bi-box-arrow-up-right ms-1 small" aria-hidden="true"></i>
</a>
</li>
<li class="nav-item">
<a class="nav-link @(isNuGetSection ? "active" : null)" href="@nuGetPublicUrl"
aria-current="@(isNuGetSection ? "page" : null)">
<i class="bi bi-box-seam me-1"></i>NuGet
</a>
</li>
</ul>
<div class="d-flex align-items-center gap-2">
<partial name="_LanguagePicker" />
@if (User.Identity?.IsAuthenticated == true)
{
var userAvatar = await UserService.GetAvatarUrlByUserNameAsync(User.Identity.Name!);
var userDisplayName = await UserService.GetDisplayNameByUserNameAsync(User.Identity.Name!);
@await Component.InvokeAsync("HeaderNotifications")
<div class="dropdown">
<button class="btn btn-outline-light btn-sm dropdown-toggle d-flex align-items-center gap-1" type="button" id="userMenuDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<div class="user-avatar me-2" data-current-user-avatar style="width: 24px; height: 24px; font-size: 0.8rem; @(!string.IsNullOrEmpty(userAvatar) ? $"background-image: url('{userAvatar}'); background-size: cover; background-position: center; color: transparent; background-color: transparent;" : "")">
<i class="bi bi-person-fill" aria-hidden="true"></i>
</div>
<span>@userDisplayName</span>
</button>
<ul class="dropdown-menu dropdown-menu-end user-menu-dropdown shadow" aria-labelledby="userMenuDropdown">
<li><h6 class="dropdown-header">欢迎, @userDisplayName</h6></li>
<li>
<a class="dropdown-item d-flex align-items-center gap-2"
href="@PublicUrls.Git(Uri.EscapeDataString(User.Identity.Name!))">
<i class="bi bi-person-circle"></i> 个人主页
</a>
</li>
<li>
<a class="dropdown-item d-flex align-items-center gap-2" href="@mainPublicUrl/Account/Profile#v-pills-profile">
<i class="bi bi-gear"></i> 账户设置
</a>
</li>
@if (Context.User.IsInRole(XFEStudioWebSite.Core.AppRoles.Admin) || Context.User.IsInRole(XFEStudioWebSite.Core.AppRoles.SuperAdmin))
{
<li>
<a class="dropdown-item d-flex align-items-center gap-2" href="@mainPublicUrl/Admin">
<i class="bi bi-speedometer2"></i> 管理后台
</a>
</li>
}
<li><hr class="dropdown-divider"></li>
<li>
<form action="@mainPublicUrl/Account/Logout" method="post" class="d-inline">
@Html.AntiForgeryToken()
<button type="submit" class="dropdown-item d-flex align-items-center gap-2 text-danger">
<i class="bi bi-box-arrow-right"></i> 退出登录
</button>
</form>
</li>
</ul>
</div>
}
else
{
<a class="btn btn-outline-light btn-sm" href="@loginUrl">
<i class="bi bi-box-arrow-in-right me-1"></i> 登录
</a>
}
</div>
</div>
</div>
</nav>
<script>
// 页面刚开始加载,先禁止动画
document.body.classList.add('no-transition');
// 主题同时写入顶级站点 Cookie,使 www/docs/git 子域保持一致。
const themeCookieName = 'xfe-theme';
const hostParts = location.hostname.toLowerCase().split('.').filter(Boolean);
const configuredRootDomain = hostParts.length >= 2 ? hostParts.slice(-2).join('.') : '';
const readThemeCookie = () => {
const prefix = `${themeCookieName}=`;
const value = document.cookie
.split(';')
.map(item => item.trim())
.find(item => item.startsWith(prefix));
try {
return value ? decodeURIComponent(value.slice(prefix.length)) : null;
} catch {
return null;
}
};
const persistTheme = (theme) => {
localStorage.setItem('theme', theme);
const domain = configuredRootDomain &&
(location.hostname === configuredRootDomain || location.hostname.endsWith(`.${configuredRootDomain}`))
? `; Domain=.${configuredRootDomain}`
: '';
const secure = location.protocol === 'https:' ? '; Secure' : '';
document.cookie = `${themeCookieName}=${encodeURIComponent(theme)}; Path=/; Max-Age=31536000; SameSite=Lax${domain}${secure}`;
};
// ---- 优先使用共享 Cookie 与本地存储;新用户默认深色 ----
let savedTheme = readThemeCookie();
if (savedTheme !== 'light' && savedTheme !== 'dark') {
try { savedTheme = localStorage.getItem('theme'); } catch { }
}
let currentTheme = savedTheme === 'light' || savedTheme === 'dark' ? savedTheme : 'dark';
persistTheme(currentTheme);
if (currentTheme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
document.documentElement.setAttribute('data-bs-theme', currentTheme);
// 页面加载完成后再启用动画
window.addEventListener('load', () => {
document.body.classList.remove('no-transition');
});
// ---- 切换主题 ----
const setTheme = (theme) => {
if (theme !== 'light' && theme !== 'dark') return;
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
document.documentElement.setAttribute('data-bs-theme', theme);
persistTheme(theme);
updateThemeUI(theme);
};
window.xfeSetTheme = setTheme;
const updateThemeUI = (theme) => {
document.querySelectorAll('[data-theme]').forEach(el => {
if (el.getAttribute('data-theme') === theme) {
el.classList.add('active');
} else {
el.classList.remove('active');
}
});
};
// 等待 DOM 加载完成(确保 footer 中的元素已存在)
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-theme]').forEach(el => {
el.addEventListener('click', (e) => {
e.preventDefault();
setTheme(el.getAttribute('data-theme'));
});
});
// 初始化 UI 选中状态
updateThemeUI(currentTheme);
});
</script>
<div class="container flex-grow-1 mb-5">
@RenderBody()
</div>
<footer class="site-footer mt-auto">
<div class="container-fluid px-4 px-lg-5 py-4">
<div class="row g-3 align-items-center">
<div class="col-lg text-center text-lg-start">
<p class="mb-1 small" data-no-localize>鄂ICP备2023005524号</p>
<p class="mb-0 small"><span>© @DateTime.UtcNow.Year</span> <span>寰宇朽力网络科技有限公司 · 版权所有</span></p>
</div>
<div class="col-lg-auto d-flex flex-wrap justify-content-center justify-content-lg-end align-items-center gap-3">
<ul class="list-inline mb-0 small">
<li class="list-inline-item me-3">
<a href="@mainPublicUrl">首页</a>
</li>
<li class="list-inline-item me-3">
<a href="@docsPublicUrl">文档</a>
</li>
<li class="list-inline-item me-3">
<a href="@mainPublicUrl/Downloads">下载</a>
</li>
<li class="list-inline-item me-3">
<a href="@mainPublicUrl/Discussion">讨论</a>
</li>
<li class="list-inline-item me-3">
<a href="@gitPublicUrl">Git</a>
</li>
<li class="list-inline-item me-3">
<a href="@nuGetPublicUrl">NuGet</a>
</li>
<li class="list-inline-item me-3">
<a href="https://work.weixin.qq.com/kfid/kfc03513925fb38fc24">支持与帮助</a>
</li>
</ul>
<div class="dropdown dropup">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle d-flex align-items-center gap-1" type="button" id="themeDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-circle-half" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z" />
</svg>
主题
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="themeDropdown">
<li>
<button class="dropdown-item d-flex align-items-center gap-2" type="button" data-theme="light">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-sun-fill" viewBox="0 0 16 16">
<path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5-.5h-2a.5.5 0 0 1 0 1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z" />
</svg>
明亮
</button>
</li>
<li>
<button class="dropdown-item d-flex align-items-center gap-2" type="button" data-theme="dark">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-moon-stars-fill" viewBox="0 0 16 16">
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z" />
<path d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z" />
</svg>
黑暗
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
</footer>
<script src="~/js/bootstrap.bundle.min.js"></script>
<script src="~/js/dialogs.js" asp-append-version="true"></script>
<script src="~/js/dropdowns.js" asp-append-version="true"></script>
<script src="~/js/time-localizer.js" asp-append-version="true"></script>
<script src="~/js/notifications.js" asp-append-version="true"></script>
<script src="~/js/site-localization.js" asp-append-version="true"></script>
@if (isMainSiteHost)
{
<script>
// 主站不再作为 PWA:清理由旧版本可能留下的 Service Worker 与应用缓存。
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
void navigator.serviceWorker.getRegistrations()
.then(registrations => Promise.all(registrations.map(registration => registration.unregister())))
.catch(() => { });
if ('caches' in window) {
void caches.keys()
.then(keys => Promise.all(keys
.filter(key => key.startsWith('xfe-studio-shell-'))
.map(key => caches.delete(key))))
.catch(() => { });
}
});
}
</script>
}
<script>
document.addEventListener('click', function (e) {
const anchor = e.target.closest('a');
if (!anchor) return;
// Check if interception should be bypassed
if (anchor.getAttribute('data-bypass-interception') === 'true') return;
const href = anchor.getAttribute('href');
if (!href) return;
if (href.startsWith('http://') || href.startsWith('https://')) {
try {
const url = new URL(href);
const currentHostname = window.location.hostname;
const targetHostname = url.hostname;
let isInternal = false;
if (currentHostname === targetHostname) {
isInternal = true;
} else {
// Check if it is a subdomain of the same root domain
// Assuming xfe.studio structure or localhost
const currentParts = currentHostname.split('.');
let rootDomain = currentHostname;
// Basic check for x.y (e.g. xfe.studio) vs localhost
if (currentParts.length >= 2) {
// Use the last two parts as root domain
rootDomain = currentParts.slice(-2).join('.');
if (targetHostname.endsWith('.' + rootDomain) || targetHostname === rootDomain) {
isInternal = true;
}
}
}
if (!isInternal) {
e.preventDefault();
window.open('@mainPublicUrl/Redirect?url=' + encodeURIComponent(href), '_blank');
}
} catch (err) {
// Invalid URL
}
}
});
</script>
@RenderSection("Scripts", required: false)
</body>
</html>
@using Microsoft.Extensions.Options
@using System.Globalization
@using XFEStudioWebSite.Localization
@using XFEStudioWebSite.Options
@using XFEStudioWebSite.Services
@inject IOptions<SiteSettings> SiteSettings
@inject IOptions<GitSettings> GitSettings
@inject IOptions<NuGetOptions> NuGetSettings
@inject IUserService UserService
@inject IPublicUrlService PublicUrls
@{
static string ResolveConfiguredHost(string value)
{
if (string.IsNullOrWhiteSpace(value))
return string.Empty;
var candidate = value.Contains("://", StringComparison.Ordinal)
? value
: $"https://{value}";
return Uri.TryCreate(candidate, UriKind.Absolute, out var uri)
? uri.Host
: value.Split(':', 2)[0];
}
var requestHost = Context.Request.Host.Host;
var currentController = ViewContext.RouteData.Values["controller"]?.ToString() ?? string.Empty;
var mainHost = ResolveConfiguredHost(SiteSettings.Value.MainDomain);
var docsHost = ResolveConfiguredHost(SiteSettings.Value.DocsDomain);
var gitHost = ResolveConfiguredHost(GitSettings.Value.PublicUrl);
var nuGetHost = ResolveConfiguredHost(NuGetSettings.Value.PublicUrl);
var mainPublicUrl = PublicUrls.Main();
var docsPublicUrl = PublicUrls.Docs();
var gitPublicUrl = PublicUrls.Git();
var nuGetPublicUrl = PublicUrls.NuGet();
var isMainSiteHost = string.IsNullOrWhiteSpace(mainHost)
|| requestHost.Equals(mainHost, StringComparison.OrdinalIgnoreCase);
var isGitAppHost = !string.IsNullOrWhiteSpace(gitHost)
&& requestHost.Equals(gitHost, StringComparison.OrdinalIgnoreCase);
var currentPublicUrl = $"{Context.Request.Scheme}://{Context.Request.Host}{Context.Request.PathBase}{Context.Request.Path}{Context.Request.QueryString}";
var loginUrl = mainPublicUrl + "/Account/Login" + QueryString.Create("returnUrl", currentPublicUrl);
var currentCulture = CultureInfo.CurrentUICulture.Name;
var textDirection = SiteLanguageCatalog.IsRightToLeft(currentCulture) ? "rtl" : "ltr";
var edgeCountry = SiteLanguageCatalog.NormalizeCountryCode(
Context.Request.Headers[SiteLanguageCatalog.EdgeOneCountryHeader].ToString());
var isNuGetSection = currentController.StartsWith("NuGet", StringComparison.OrdinalIgnoreCase)
|| requestHost.Equals(nuGetHost, StringComparison.OrdinalIgnoreCase);
var isGitSection = !isNuGetSection && (currentController.StartsWith("Git", StringComparison.OrdinalIgnoreCase)
|| requestHost.Equals(gitHost, StringComparison.OrdinalIgnoreCase));
var hasDedicatedDocsHost = !string.IsNullOrEmpty(docsHost)
&& !docsHost.Equals(mainHost, StringComparison.OrdinalIgnoreCase);
var isDocsSection = !isNuGetSection && !isGitSection
&& (currentController.Equals("Docs", StringComparison.OrdinalIgnoreCase)
|| (hasDedicatedDocsHost && requestHost.Equals(docsHost, StringComparison.OrdinalIgnoreCase)));
var isDownloadsSection = !isNuGetSection && !isGitSection && !isDocsSection
&& currentController.Equals("Downloads", StringComparison.OrdinalIgnoreCase);
var isDiscussionSection = !isNuGetSection && !isGitSection && !isDocsSection
&& currentController.Equals("Discussion", StringComparison.OrdinalIgnoreCase);
var isHomeSection = !isNuGetSection && !isGitSection && !isDocsSection
&& !isDownloadsSection && !isDiscussionSection
&& currentController.Equals("Home", StringComparison.OrdinalIgnoreCase);
}
<!DOCTYPE html>
<html lang="@currentCulture" dir="@textDirection">
<head>
<meta charset="utf-8" />
<title>寰宇朽力 - @ViewData["Title"]</title>
<link rel="icon" type="image/png" href="/favicon.png" />
@if (isGitAppHost)
{
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="apple-touch-icon" href="/img/pwa/icon-192.png" />
<meta name="application-name" content="XFE Studio Git" />
<meta name="theme-color" content="#9786d3" />
<script src="/js/pwa-install.js" defer></script>
}
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(() => {
const cookie = document.cookie
.split(';')
.map(item => item.trim())
.find(item => item.startsWith('xfe-theme='));
let theme = null;
try {
theme = cookie ? decodeURIComponent(cookie.slice('xfe-theme='.length)) : null;
} catch { }
if (theme !== 'light' && theme !== 'dark') {
try { theme = localStorage.getItem('theme'); } catch { }
}
if (theme !== 'light' && theme !== 'dark') theme = 'dark';
document.documentElement.classList.toggle('dark', theme === 'dark');
document.documentElement.setAttribute('data-bs-theme', theme);
})();
</script>
<link rel="stylesheet" href="~/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/theme-variables.css" asp-append-version="true" />
<link rel="stylesheet" href="~/css/site-language.css" asp-append-version="true" />
<link rel="stylesheet" href="~/css/notifications.css" asp-append-version="true" />
<link rel="stylesheet" href="https://lib.baomitu.com/bootstrap-icons/1.11.3/font/bootstrap-icons.min.css" />
<style>
body.no-transition {
transition: none; /* 页面加载时禁用过渡 */
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: sans-serif;
transition: background-color 0.3s, color 0.3s; /* 只在切换时生效 */
scrollbar-color: color-mix(in srgb, var(--main-color) 50%, transparent) transparent;
}
body.no-transition {
transition: none; /* 页面加载时禁用过渡 */
}
a {
color: var(--link-color);
}
a:hover {
color: var(--main-deep-color);
text-decoration: underline;
}
.navbar .navbar-nav .nav-link {
border-radius: .55rem;
transition: background-color .18s ease, color .18s ease;
}
.navbar .navbar-nav .nav-link.active {
background: rgba(255, 255, 255, .08);
color: #fff;
}
.navbar .navbar-nav .nav-link.active:hover,
.navbar .navbar-nav .nav-link.active:focus-visible {
background: rgba(255, 255, 255, .13);
color: #fff;
text-decoration: none;
}
.site-footer {
background: var(--footer-bg);
border-top: 1px solid var(--footer-border);
color: var(--text-color);
width: 100%;
}
.site-footer a {
color: var(--link-color);
text-decoration: none;
font-weight: 500;
}
.site-footer a:hover {
text-decoration: underline;
}
/* Breadcrumb Adaptation */
.breadcrumb-item.active {
color: var(--text-color) !important;
}
.breadcrumb-item + .breadcrumb-item::before {
color: var(--text-secondary-color) !important;
}
.radio-control {
color: var(--on-main-color);
background: var(--main-color);
}
.user-avatar {
width: 48px;
height: 48px;
background-color: var(--main-color);
color: var(--on-main-color);
border-radius: 999px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.5rem;
flex-shrink: 0;
border: 2px solid var(--bg-color);
box-shadow: 0 0 0 1px var(--main-color);
}
/* Dropdown Theme Adaptation */
.dropdown-menu {
background-color: var(--bg-color);
color: var(--text-color);
border-color: var(--footer-border);
}
.dropdown-item {
color: var(--text-color);
}
.dropdown-item:hover, .dropdown-item:focus {
background-color: color-mix(in srgb, var(--text-color) 5%, transparent);
color: var(--text-color);
}
.dropdown-header {
color: var(--text-secondary-color);
}
.dropdown-divider {
border-top-color: var(--footer-border);
}
.text-muted {
color: var(--text-secondary-color) !important;
}
.table {
color: var(--text-color);
--bs-table-color: var(--text-color);
--bs-table-bg: transparent;
--bs-table-border-color: var(--profile-border-color);
--bs-table-hover-color: var(--text-color);
--bs-table-hover-bg: color-mix(in srgb, var(--text-color) 5%, transparent);
}
.table th {
border-color: var(--profile-border-color);
color: var(--text-secondary-color);
font-weight: 600;
text-transform: uppercase;
font-size: 0.85rem;
}
.table td {
border-color: var(--profile-border-color);
vertical-align: middle;
}
.form-text {
color: var(--text-secondary-color);
}
.list-group-item {
background-color: var(--bg-color);
border-color: var(--footer-border);
color: var(--text-color);
transition: background-color 0.2s;
}
.list-group-item-action:hover, .list-group-item-action:focus {
background-color: color-mix(in srgb, var(--text-color) 4%, var(--bg-color));
color: var(--text-color);
}
/* Utility Class Overrides */
.bg-white {
background-color: var(--bg-color) !important;
color: var(--text-color) !important;
}
.text-dark {
color: var(--text-color) !important;
}
.text-muted {
color: var(--text-secondary-color) !important;
}
.bg-light {
background-color: color-mix(in srgb, var(--text-color) 5%, transparent) !important;
color: var(--text-color) !important;
}
/* Badge Theme Overrides */
.badge {
font-weight: 500;
}
.badge.bg-light {
background-color: color-mix(in srgb, var(--text-color) 10%, transparent) !important;
color: var(--text-color) !important;
}
/* Card & Layout Adaptation */
.card {
background-color: var(--bg-color);
border-color: var(--footer-border);
color: var(--text-color);
}
.card-body {
background: var(--bg-color);
/*Add radios corner*/
border-radius: 0.375rem;
/*Add main color shadow*/
/* box-shadow: 0px 0px 2px var(--main-color); */
}
.card-header {
background-color: var(--bg-color) !important;
border-bottom-color: var(--footer-border);
color: var(--text-color);
}
::-webkit-scrollbar {
width: 5px;
}
::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--main-color) 50%, transparent);
border-radius: 999px;
}
::-webkit-scrollbar-track {
background: transparent;
}
/* Tabs */
.nav-tabs .nav-link.active {
background-color: var(--bg-color);
border-color: var(--footer-border) var(--footer-border) var(--bg-color);
color: var(--main-color);
}
.nav-tabs .nav-link {
color: var(--text-secondary-color);
}
.nav-tabs {
border-bottom-color: var(--footer-border);
}
.tab-content {
border-color: var(--footer-border) !important;
}
.shadow-sm {
box-shadow: 0 .125rem .25rem var(--focus-main-color) !important;
}
.btn-close {
filter: var(--text-color, 0);
}
.btn-close-white {
filter: var(--text-color, 0);
}
/* Button Theme Overrides */
.btn-primary {
background-color: var(--main-color);
border-color: var(--main-color);
color: var(--on-main-color);
}
.btn-primary:hover, .btn-primary:focus, .btn-primary:active {
background-color: var(--main-deep-color);
border-color: var(--main-deep-color);
color: var(--on-main-color);
}
.btn-outline-primary {
color: var(--main-color);
border-color: var(--main-color);
}
.btn-outline-primary:hover, .btn-outline-primary:focus, .btn-outline-primary:active {
background-color: var(--main-color);
border-color: var(--main-color);
color: var(--on-main-color);
}
.btn-secondary {
border-color: var(--main-color);
background-color: var(--bg-color);
color: var(--main-color);
}
.btn-secondary:hover {
border-color: var(--main-deep-color);
background-color: var(--bg-color);
color: var(--main-deep-color);
}
.btn-outline-secondary {
color: var(--text-secondary-color);
border-color: var(--footer-border);
}
.btn-outline-secondary:hover {
background-color: var(--text-color);
color: var(--bg-color);
}
.btn-link {
color: var(--link-color);
}
.btn-link:hover {
color: var(--main-deep-color);
}
/* Form Theme Overrides */
.form-control, .form-select {
background-color: var(--bg-color);
border-color: var(--footer-border);
color: var(--text-color);
}
.form-control::placeholder {
color: var(--text-secondary-color);
opacity: 0.8;
}
.form-label {
color: var(--text-secondary-color);
font-weight: 500;
}
.form-control:focus, .form-select:focus {
border-color: var(--main-color);
box-shadow: 0 0 0 0.25rem var(--focus-main-color);
background-color: var(--bg-color);
color: var(--text-color);
}
.form-control:disabled, .form-control[readonly] {
background-color: color-mix(in srgb, var(--bg-color) 95%, #ffffff 5%);
border-color: var(--footer-border);
color: var(--text-secondary-color);
opacity: 1;
}
.input-group-text {
background-color: color-mix(in srgb, var(--bg-color) 96%, #000000 4%);
border-color: var(--footer-border);
color: var(--text-color);
}
.form-check-input:checked {
background-color: var(--main-color);
border-color: var(--main-color);
}
.form-check-input:focus {
border-color: var(--main-color);
box-shadow: 0 0 0 0.25rem var(--focus-main-color);
}
/* Navbar Hover Dropdown */
@@media (min-width: 992px) {
.navbar .dropdown:hover .dropdown-menu {
display: block;
margin-top: 0;
}
.navbar .dropdown:hover > .user-menu-dropdown {
right: 0;
left: auto;
}
}
</style>
<link rel="stylesheet" href="~/css/dialogs.css" asp-append-version="true" />
<link rel="stylesheet" href="~/css/dropdowns.css" asp-append-version="true" />
@RenderSection("Styles", required: false)
</head>
<body class="d-flex flex-column min-vh-100 @(ViewData["BodyClass"] as string)"
data-site-culture="@currentCulture" data-edgeone-country="@edgeCountry"
data-main-origin="@mainPublicUrl">
<partial name="_Notifications" />
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
<div class="container-fluid">
<a class="navbar-brand" href="@mainPublicUrl" data-no-localize>XFE-寰宇朽力</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link @(isHomeSection ? "active" : null)" href="@mainPublicUrl"
aria-current="@(isHomeSection ? "page" : null)">首页</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle @(isDocsSection ? "active" : null)" href="#" id="docsHeaderDropdown" role="button"
data-bs-toggle="dropdown" aria-expanded="false" aria-current="@(isDocsSection ? "page" : null)">
文档
</a>
<ul class="dropdown-menu" aria-labelledby="docsHeaderDropdown">
<li><a class="dropdown-item" href="@docsPublicUrl">文档中心</a></li>
<li><a class="dropdown-item" href="@docsPublicUrl/API">API 文档</a></li>
<li><a class="dropdown-item" href="@docsPublicUrl/Errors">错误文档</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link @(isDownloadsSection ? "active" : null)" href="@mainPublicUrl/Downloads"
aria-current="@(isDownloadsSection ? "page" : null)">下载</a>
</li>
<li class="nav-item">
<a class="nav-link @(isDiscussionSection ? "active" : null)" href="@mainPublicUrl/Discussion"
aria-current="@(isDiscussionSection ? "page" : null)">讨论</a>
</li>
<li class="nav-item">
<a class="nav-link site-git-app-link @(isGitSection ? "active" : null)" href="@gitPublicUrl"
target="_blank" rel="noopener noreferrer"
title="在新标签页打开 XFE Git"
aria-label="Git(在新标签页打开)"
aria-current="@(isGitSection ? "page" : null)">
<i class="bi bi-git me-1" aria-hidden="true"></i>Git
<i class="bi bi-box-arrow-up-right ms-1 small" aria-hidden="true"></i>
</a>
</li>
<li class="nav-item">
<a class="nav-link @(isNuGetSection ? "active" : null)" href="@nuGetPublicUrl"
aria-current="@(isNuGetSection ? "page" : null)">
<i class="bi bi-box-seam me-1"></i>NuGet
</a>
</li>
</ul>
<div class="d-flex align-items-center gap-2">
<partial name="_LanguagePicker" />
@if (User.Identity?.IsAuthenticated == true)
{
var userAvatar = await UserService.GetAvatarUrlByUserNameAsync(User.Identity.Name!);
var userDisplayName = await UserService.GetDisplayNameByUserNameAsync(User.Identity.Name!);
@await Component.InvokeAsync("HeaderNotifications")
<div class="dropdown">
<button class="btn btn-outline-light btn-sm dropdown-toggle d-flex align-items-center gap-1" type="button" id="userMenuDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<div class="user-avatar me-2" data-current-user-avatar style="width: 24px; height: 24px; font-size: 0.8rem; @(!string.IsNullOrEmpty(userAvatar) ? $"background-image: url('{userAvatar}'); background-size: cover; background-position: center; color: transparent; background-color: transparent;" : "")">
<i class="bi bi-person-fill" aria-hidden="true"></i>
</div>
<span>@userDisplayName</span>
</button>
<ul class="dropdown-menu dropdown-menu-end user-menu-dropdown shadow" aria-labelledby="userMenuDropdown">
<li><h6 class="dropdown-header">欢迎, @userDisplayName</h6></li>
<li>
<a class="dropdown-item d-flex align-items-center gap-2"
href="@PublicUrls.Git(Uri.EscapeDataString(User.Identity.Name!))">
<i class="bi bi-person-circle"></i> 个人主页
</a>
</li>
<li>
<a class="dropdown-item d-flex align-items-center gap-2" href="@mainPublicUrl/Account/Profile#v-pills-profile">
<i class="bi bi-gear"></i> 账户设置
</a>
</li>
@if (Context.User.IsInRole(XFEStudioWebSite.Core.AppRoles.Admin) || Context.User.IsInRole(XFEStudioWebSite.Core.AppRoles.SuperAdmin))
{
<li>
<a class="dropdown-item d-flex align-items-center gap-2" href="@mainPublicUrl/Admin">
<i class="bi bi-speedometer2"></i> 管理后台
</a>
</li>
}
<li><hr class="dropdown-divider"></li>
<li>
<form action="@mainPublicUrl/Account/Logout" method="post" class="d-inline">
@Html.AntiForgeryToken()
<button type="submit" class="dropdown-item d-flex align-items-center gap-2 text-danger">
<i class="bi bi-box-arrow-right"></i> 退出登录
</button>
</form>
</li>
</ul>
</div>
}
else
{
<a class="btn btn-outline-light btn-sm" href="@loginUrl">
<i class="bi bi-box-arrow-in-right me-1"></i> 登录
</a>
}
</div>
</div>
</div>
</nav>
<script>
// 页面刚开始加载,先禁止动画
document.body.classList.add('no-transition');
// 主题同时写入顶级站点 Cookie,使 www/docs/git 子域保持一致。
const themeCookieName = 'xfe-theme';
const hostParts = location.hostname.toLowerCase().split('.').filter(Boolean);
const configuredRootDomain = hostParts.length >= 2 ? hostParts.slice(-2).join('.') : '';
const readThemeCookie = () => {
const prefix = `${themeCookieName}=`;
const value = document.cookie
.split(';')
.map(item => item.trim())
.find(item => item.startsWith(prefix));
try {
return value ? decodeURIComponent(value.slice(prefix.length)) : null;
} catch {
return null;
}
};
const persistTheme = (theme) => {
localStorage.setItem('theme', theme);
const domain = configuredRootDomain &&
(location.hostname === configuredRootDomain || location.hostname.endsWith(`.${configuredRootDomain}`))
? `; Domain=.${configuredRootDomain}`
: '';
const secure = location.protocol === 'https:' ? '; Secure' : '';
document.cookie = `${themeCookieName}=${encodeURIComponent(theme)}; Path=/; Max-Age=31536000; SameSite=Lax${domain}${secure}`;
};
// ---- 优先使用共享 Cookie 与本地存储;新用户默认深色 ----
let savedTheme = readThemeCookie();
if (savedTheme !== 'light' && savedTheme !== 'dark') {
try { savedTheme = localStorage.getItem('theme'); } catch { }
}
let currentTheme = savedTheme === 'light' || savedTheme === 'dark' ? savedTheme : 'dark';
persistTheme(currentTheme);
if (currentTheme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
document.documentElement.setAttribute('data-bs-theme', currentTheme);
// 页面加载完成后再启用动画
window.addEventListener('load', () => {
document.body.classList.remove('no-transition');
});
// ---- 切换主题 ----
const setTheme = (theme) => {
if (theme !== 'light' && theme !== 'dark') return;
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
document.documentElement.setAttribute('data-bs-theme', theme);
persistTheme(theme);
updateThemeUI(theme);
};
window.xfeSetTheme = setTheme;
const updateThemeUI = (theme) => {
document.querySelectorAll('[data-theme]').forEach(el => {
if (el.getAttribute('data-theme') === theme) {
el.classList.add('active');
} else {
el.classList.remove('active');
}
});
};
// 等待 DOM 加载完成(确保 footer 中的元素已存在)
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-theme]').forEach(el => {
el.addEventListener('click', (e) => {
e.preventDefault();
setTheme(el.getAttribute('data-theme'));
});
});
// 初始化 UI 选中状态
updateThemeUI(currentTheme);
});
</script>
<div class="container flex-grow-1 mb-5">
@RenderBody()
</div>
<footer class="site-footer mt-auto">
<div class="container-fluid px-4 px-lg-5 py-4">
<div class="row g-3 align-items-center">
<div class="col-lg text-center text-lg-start">
<p class="mb-1 small" data-no-localize>鄂ICP备2023005524号</p>
<p class="mb-0 small"><span>© @DateTime.UtcNow.Year</span> <span>寰宇朽力网络科技有限公司 · 版权所有</span></p>
</div>
<div class="col-lg-auto d-flex flex-wrap justify-content-center justify-content-lg-end align-items-center gap-3">
<ul class="list-inline mb-0 small">
<li class="list-inline-item me-3">
<a href="@mainPublicUrl">首页</a>
</li>
<li class="list-inline-item me-3">
<a href="@docsPublicUrl">文档</a>
</li>
<li class="list-inline-item me-3">
<a href="@mainPublicUrl/Downloads">下载</a>
</li>
<li class="list-inline-item me-3">
<a href="@mainPublicUrl/Discussion">讨论</a>
</li>
<li class="list-inline-item me-3">
<a href="@gitPublicUrl">Git</a>
</li>
<li class="list-inline-item me-3">
<a href="@nuGetPublicUrl">NuGet</a>
</li>
<li class="list-inline-item me-3">
<a href="https://work.weixin.qq.com/kfid/kfc03513925fb38fc24">支持与帮助</a>
</li>
</ul>
<div class="dropdown dropup">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle d-flex align-items-center gap-1" type="button" id="themeDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-circle-half" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z" />
</svg>
主题
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="themeDropdown">
<li>
<button class="dropdown-item d-flex align-items-center gap-2" type="button" data-theme="light">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-sun-fill" viewBox="0 0 16 16">
<path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5-.5h-2a.5.5 0 0 1 0 1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z" />
</svg>
明亮
</button>
</li>
<li>
<button class="dropdown-item d-flex align-items-center gap-2" type="button" data-theme="dark">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-moon-stars-fill" viewBox="0 0 16 16">
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z" />
<path d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z" />
</svg>
黑暗
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
</footer>
<script src="~/js/bootstrap.bundle.min.js"></script>
<script src="~/js/dialogs.js" asp-append-version="true"></script>
<script src="~/js/dropdowns.js" asp-append-version="true"></script>
<script src="~/js/time-localizer.js" asp-append-version="true"></script>
<script src="~/js/notifications.js" asp-append-version="true"></script>
<script src="~/js/site-localization.js" asp-append-version="true"></script>
@if (isMainSiteHost)
{
<script>
// 主站不再作为 PWA:清理由旧版本可能留下的 Service Worker 与应用缓存。
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
void navigator.serviceWorker.getRegistrations()
.then(registrations => Promise.all(registrations.map(registration => registration.unregister())))
.catch(() => { });
if ('caches' in window) {
void caches.keys()
.then(keys => Promise.all(keys
.filter(key => key.startsWith('xfe-studio-shell-'))
.map(key => caches.delete(key))))
.catch(() => { });
}
});
}
</script>
}
<script>
document.addEventListener('click', function (e) {
const anchor = e.target.closest('a');
if (!anchor) return;
// Check if interception should be bypassed
if (anchor.getAttribute('data-bypass-interception') === 'true') return;
const href = anchor.getAttribute('href');
if (!href) return;
if (href.startsWith('http://') || href.startsWith('https://')) {
try {
const url = new URL(href);
const currentHostname = window.location.hostname;
const targetHostname = url.hostname;
let isInternal = false;
if (currentHostname === targetHostname) {
isInternal = true;
} else {
// Check if it is a subdomain of the same root domain
// Assuming xfe.studio structure or localhost
const currentParts = currentHostname.split('.');
let rootDomain = currentHostname;
// Basic check for x.y (e.g. xfe.studio) vs localhost
if (currentParts.length >= 2) {
// Use the last two parts as root domain
rootDomain = currentParts.slice(-2).join('.');
if (targetHostname.endsWith('.' + rootDomain) || targetHostname === rootDomain) {
isInternal = true;
}
}
}
if (!isInternal) {
e.preventDefault();
window.open('@mainPublicUrl/Redirect?url=' + encodeURIComponent(href), '_blank');
}
} catch (err) {
// Invalid URL
}
}
});
</script>
@RenderSection("Scripts", required: false)
</body>
</html>