多站点主题化Skill multi-site-theming

该技能用于在多站点内容管理系统架构中实现每站点主题、白标和品牌定制,涵盖主题继承、CSS变量层次结构和动态主题切换。关键词包括多站点主题化、白标、品牌定制、CMS、CSS变量、主题切换、前端开发、后端集成。

架构设计 0 次安装 0 次浏览 更新于 3/11/2026

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 Dictionary
  • headless-api-design - 主题API交付
  • content-type-modeling - 主题作为内容类型