name: multi-site-theming description: 用于在多站点CMS架构中实现每站点主题、白标或品牌覆盖系统。涵盖租户特定品牌、主题继承、CSS变量层次结构和动态主题切换。 allowed-tools: Read, Glob, Grep, Task, Skill
多站点主题化
在多站点CMS架构中实现每站点主题、白标和品牌定制的指南。
何时使用此技能
- 实现每租户或每站点品牌
- 设计主题继承层次结构
- 构建白标定制系统
- 配置动态主题切换
- 管理运行时品牌覆盖
主题架构
主题层次结构
┌─────────────────────────────────────────────────────────────────┐
│ 主题层次结构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 基础主题 │ │
│ │ (默认颜色、排版、间距、组件) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 品牌主题 │ │
│ │ (企业颜色、字体、徽标、品牌身份) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 站点主题 │ │
│ │ (站点特定覆盖、微品牌化) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 用户偏好 │ │
│ │ (深色/浅色模式、可访问性、对比度) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
主题模型
核心主题实体
public class Theme
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Slug { get; set; } = string.Empty;
public ThemeType Type { get; set; }
// 继承
public Guid? ParentThemeId { get; set; }
public Theme? ParentTheme { get; set; }
// 范围
public Guid? TenantId { get; set; } // Null = 全局/基础
public Guid? SiteId { get; set; } // Null = 租户范围
// 令牌
public ThemeTokens Tokens { get; set; } = new();
// 资产
public ThemeAssets Assets { get; set; } = new();
// 元数据
public bool IsDefault { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedUtc { get; set; }
public DateTime? ModifiedUtc { get; set; }
}
public enum ThemeType
{
Base, // 基础主题
Brand, // 租户/品牌级别
Site, // 单个站点
Variant // 浅色/深色变体
}
public class ThemeTokens
{
// 颜色
public ColorTokens Colors { get; set; } = new();
// 排版
public TypographyTokens Typography { get; set; } = new();
// 间距
public SpacingTokens Spacing { get; set; } = new();
// 边框和阴影
public BorderTokens Borders { get; set; } = new();
public ShadowTokens Shadows { get; set; } = new();
// 组件特定覆盖
public Dictionary<string, Dictionary<string, string>> Components { get; set; } = new();
}
public class ColorTokens
{
// 品牌颜色
public string Primary { get; set; } = "#3B82F6";
public string Secondary { get; set; } = "#6366F1";
public string Accent { get; set; } = "#F59E0B";
// 语义颜色
public string Success { get; set; } = "#10B981";
public string Warning { get; set; } = "#F59E0B";
public string Error { get; set; } = "#EF4444";
public string Info { get; set; } = "#3B82F6";
// 表面颜色
public string Background { get; set; } = "#FFFFFF";
public string Surface { get; set; } = "#F9FAFB";
public string Border { get; set; } = "#E5E7EB";
// 文本颜色
public string TextPrimary { get; set; } = "#111827";
public string TextSecondary { get; set; } = "#6B7280";
public string TextMuted { get; set; } = "#9CA3AF";
}
public class ThemeAssets
{
public string? LogoUrl { get; set; }
public string? LogoDarkUrl { get; set; }
public string? FaviconUrl { get; set; }
public string? BackgroundImageUrl { get; set; }
public List<string> FontUrls { get; set; } = new();
public string? CustomCss { get; set; }
}
主题解析
级联主题服务
public class ThemeResolver
{
public async Task<ResolvedTheme> ResolveAsync(ThemeContext context)
{
var themes = new List<Theme>();
// 1. 加载基础主题
var baseTheme = await _repository.GetBaseThemeAsync();
if (baseTheme != null) themes.Add(baseTheme);
// 2. 加载租户/品牌主题
if (context.TenantId.HasValue)
{
var tenantTheme = await _repository.GetTenantThemeAsync(context.TenantId.Value);
if (tenantTheme != null) themes.Add(tenantTheme);
}
// 3. 加载站点主题
if (context.SiteId.HasValue)
{
var siteTheme = await _repository.GetSiteThemeAsync(context.SiteId.Value);
if (siteTheme != null) themes.Add(siteTheme);
}
// 4. 应用用户偏好(深色模式、对比度)
if (context.UserPreferences != null)
{
var variantTheme = await ResolveVariantAsync(themes.Last(), context.UserPreferences);
if (variantTheme != null) themes.Add(variantTheme);
}
// 按顺序合并主题
return MergeThemes(themes);
}
private ResolvedTheme MergeThemes(IEnumerable<Theme> themes)
{
var resolved = new ResolvedTheme();
foreach (var theme in themes)
{
// 后续主题覆盖先前的
MergeTokens(resolved.Tokens, theme.Tokens);
MergeAssets(resolved.Assets, theme.Assets);
}
return resolved;
}
}
public class ThemeContext
{
public Guid? TenantId { get; set; }
public Guid? SiteId { get; set; }
public UserPreferences? UserPreferences { get; set; }
}
public class UserPreferences
{
public ColorScheme ColorScheme { get; set; } = ColorScheme.System;
public bool HighContrast { get; set; }
public bool ReducedMotion { get; set; }
}
public enum ColorScheme
{
Light,
Dark,
System
}
CSS变量生成
变量生成器
public class CssVariableGenerator
{
public string GenerateCssVariables(ResolvedTheme theme)
{
var sb = new StringBuilder();
sb.AppendLine(":root {");
// 颜色
GenerateColorVariables(sb, theme.Tokens.Colors);
// 排版
GenerateTypographyVariables(sb, theme.Tokens.Typography);
// 间距
GenerateSpacingVariables(sb, theme.Tokens.Spacing);
// 边框和阴影
GenerateBorderVariables(sb, theme.Tokens.Borders);
GenerateShadowVariables(sb, theme.Tokens.Shadows);
sb.AppendLine("}");
// 深色模式变体
if (theme.DarkVariant != null)
{
sb.AppendLine();
sb.AppendLine("@media (prefers-color-scheme: dark) {");
sb.AppendLine(" :root {");
GenerateColorVariables(sb, theme.DarkVariant.Colors, " ");
sb.AppendLine(" }");
sb.AppendLine("}");
// 手动深色模式类
sb.AppendLine();
sb.AppendLine("[data-theme=\"dark\"] {");
GenerateColorVariables(sb, theme.DarkVariant.Colors, " ");
sb.AppendLine("}");
}
return sb.ToString();
}
private void GenerateColorVariables(
StringBuilder sb,
ColorTokens colors,
string indent = " ")
{
sb.AppendLine($"{indent}/* 品牌颜色 */");
sb.AppendLine($"{indent}--color-primary: {colors.Primary};");
sb.AppendLine($"{indent}--color-secondary: {colors.Secondary};");
sb.AppendLine($"{indent}--color-accent: {colors.Accent};");
sb.AppendLine();
sb.AppendLine($"{indent}/* 语义颜色 */");
sb.AppendLine($"{indent}--color-success: {colors.Success};");
sb.AppendLine($"{indent}--color-warning: {colors.Warning};");
sb.AppendLine($"{indent}--color-error: {colors.Error};");
sb.AppendLine($"{indent}--color-info: {colors.Info};");
sb.AppendLine();
sb.AppendLine($"{indent}/* 表面颜色 */");
sb.AppendLine($"{indent}--color-background: {colors.Background};");
sb.AppendLine($"{indent}--color-surface: {colors.Surface};");
sb.AppendLine($"{indent}--color-border: {colors.Border};");
sb.AppendLine();
sb.AppendLine($"{indent}/* 文本颜色 */");
sb.AppendLine($"{indent}--color-text-primary: {colors.TextPrimary};");
sb.AppendLine($"{indent}--color-text-secondary: {colors.TextSecondary};");
sb.AppendLine($"{indent}--color-text-muted: {colors.TextMuted};");
}
}
生成的CSS输出
:root {
/* 品牌颜色 */
--color-primary: #3B82F6;
--color-secondary: #6366F1;
--color-accent: #F59E0B;
/* 语义颜色 */
--color-success: #10B981;
--color-warning: #F59E0B;
--color-error: #EF4444;
--color-info: #3B82F6;
/* 表面颜色 */
--color-background: #FFFFFF;
--color-surface: #F9FAFB;
--color-border: #E5E7EB;
/* 文本颜色 */
--color-text-primary: #111827;
--color-text-secondary: #6B7280;
--color-text-muted: #9CA3AF;
/* 排版 */
--font-family-base: 'Inter', system-ui, sans-serif;
--font-family-heading: 'Inter', system-ui, sans-serif;
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
/* 间距 */
--spacing-1: 0.25rem;
--spacing-2: 0.5rem;
--spacing-3: 0.75rem;
--spacing-4: 1rem;
--spacing-6: 1.5rem;
--spacing-8: 2rem;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: #111827;
--color-surface: #1F2937;
--color-border: #374151;
--color-text-primary: #F9FAFB;
--color-text-secondary: #D1D5DB;
--color-text-muted: #9CA3AF;
}
}
[data-theme="dark"] {
--color-background: #111827;
--color-surface: #1F2937;
--color-border: #374151;
--color-text-primary: #F9FAFB;
--color-text-secondary: #D1D5DB;
--color-text-muted: #9CA3AF;
}
主题API
REST端点
GET /api/themes # 列出所有主题
GET /api/themes/{id} # 按ID获取主题
POST /api/themes # 创建主题
PUT /api/themes/{id} # 更新主题
DELETE /api/themes/{id} # 删除主题
GET /api/themes/resolve # 解析当前主题(按上下文)
GET /api/themes/{id}/css # 获取生成的CSS
GET /api/themes/{id}/variables # 获取CSS变量JSON
POST /api/themes/{id}/preview # 预览主题更改
主题交付端点
[Route("api/themes")]
public class ThemeController : ControllerBase
{
[HttpGet("resolve/css")]
[ResponseCache(Duration = 3600, VaryByHeader = "X-Tenant-Id,X-Site-Id")]
public async Task<IActionResult> GetResolvedCss(
[FromHeader(Name = "X-Tenant-Id")] Guid? tenantId,
[FromHeader(Name = "X-Site-Id")] Guid? siteId)
{
var context = new ThemeContext
{
TenantId = tenantId,
SiteId = siteId
};
var theme = await _resolver.ResolveAsync(context);
var css = _generator.GenerateCssVariables(theme);
return Content(css, "text/css");
}
[HttpGet("{id}/variables")]
public async Task<ActionResult<ThemeTokens>> GetVariables(Guid id)
{
var theme = await _repository.GetByIdAsync(id);
if (theme == null) return NotFound();
return Ok(theme.Tokens);
}
}
白标配置
租户品牌设置
public class TenantBrandingSettings
{
// 身份
public string CompanyName { get; set; } = string.Empty;
public string ProductName { get; set; } = string.Empty;
// 视觉身份
public Guid? ThemeId { get; set; }
public string? CustomDomain { get; set; }
// 徽标
public string? LogoUrl { get; set; }
public string? LogoDarkUrl { get; set; }
public string? FaviconUrl { get; set; }
public string? AppIconUrl { get; set; }
// 联系
public string? SupportEmail { get; set; }
public string? SupportUrl { get; set; }
// 法律
public string? TermsUrl { get; set; }
public string? PrivacyUrl { get; set; }
// 功能标志
public bool ShowPoweredBy { get; set; } = true;
public bool CustomEmailTemplates { get; set; }
}
前端集成
Blazor主题提供者
@inject IThemeService ThemeService
<CascadingValue Value="@CurrentTheme">
@ChildContent
</CascadingValue>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
private ResolvedTheme? CurrentTheme { get; set; }
protected override async Task OnInitializedAsync()
{
CurrentTheme = await ThemeService.GetCurrentThemeAsync();
}
}
JavaScript主题切换
// 主题切换器
const ThemeSwitcher = {
setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
},
getTheme() {
return localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
},
init() {
this.setTheme(this.getTheme());
// 监听系统变化
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', e => {
if (!localStorage.getItem('theme')) {
this.setTheme(e.matches ? 'dark' : 'light');
}
});
}
};
相关技能
design-token-management- 令牌模式和Style Dictionaryheadless-api-design- 主题API交付content-type-modeling- 主题作为内容类型