跳转至

深色模式

Django Admin Dashboards 提供全面的深色模式支持,可无缝集成到 Django Admin 的内置主题系统中。本指南涵盖从基础使用到高级自定义的所有内容。

目录

概述

什么是深色模式?

深色模式是一种在深色背景上使用浅色文本和图标的配色方案。优点包括:

  • 减少眼睛疲劳:在低光环境下
  • 提高电池寿命:在 OLED/AMOLED 显示屏上
  • 更好的可读性:对部分用户而言
  • 现代外观:符合当代设计趋势

Django Admin Dashboards 中的深色模式工作原理

  1. 主题检测:系统通过 prefers-color-scheme CSS 媒体查询检测用户主题偏好
  2. Django Admin 集成:利用 Django Admin 的内置主题切换系统
  3. URL 参数控制:允许通过 URL 参数强制指定主题
  4. CSS 自定义属性:使用 CSS 变量实现跨组件的一致主题
  5. 组件感知:所有组件自动适应深色模式

与 Django Admin 的集成

Django Admin Dashboards 与 Django Admin 的主题系统集成:

  • <html> 元素上使用相同的 data-theme 属性
  • 尊重 Django Admin 的主题切换按钮(当 _dark_mode_on=auto 时)
  • 与 Django Admin 的调色板保持一致
  • 支持所有三种主题模式:lightdarkauto

快速开始

通过 URL 启用深色模式

控制深色模式最简单的方法是通过 URL 参数:

    # 强制深色模式

    /admin/?_dark_mode_on=true

    # 强制浅色模式

    /admin/?_dark_mode_on=false

    # 跟随 Django Admin 主题(默认)

    /admin/?_dark_mode_on=auto

数据看板配置

在数据看板中启用深色模式支持:

    from django_admin_dashboards.base import Dashboard

    class MyDashboard(Dashboard):
        # 默认启用深色模式

        supports_dark_mode = True

        def get_context_data(self, request):
            context = super().get_context_data(request)

            # 深色模式上下文变量

            context['is_dark_mode'] = self.is_dark_mode(request)
            context['current_theme'] = self.get_current_theme(request)

            return context

在模板中检查当前主题

    {% if is_dark_mode %}

        <div class="dark-mode-warning">
            深色模式已激活
        </div>
    {% endif %}

URL 参数

_dark_mode_on 参数

使用 _dark_mode_on URL 参数控制深色模式行为:

描述 示例
true 强制深色模式 /admin/?_dark_mode_on=true
false 强制浅色模式 /admin/?_dark_mode_on=false
auto 跟随 Django Admin 主题 /admin/?_dark_mode_on=auto
1, yes 替代真值 /admin/?_dark_mode_on=1
0, no 替代假值 /admin/?_dark_mode_on=0

参数持久性

深色模式偏好保存在 localStorage 中:

    // 当使用 _dark_mode_on=true 
    localStorage.setItem('theme', 'dark');

    // 当使用 _dark_mode_on=false 
    localStorage.setItem('theme', 'light');

    // 当使用 _dark_mode_on=auto 
    localStorage.setItem('theme', 'auto');

清除深色模式覆盖

要返回系统默认:

    # 使用自动模式

    /admin/?_dark_mode_on=auto

    # 或手动清除 localStorage

    localStorage.removeItem('theme');

与其他参数组合

深色模式参数可以与其他数据看板控制参数组合:

    # 深色模式 + 全屏

    /admin/?_dark_mode_on=true&_hide_others_in_fullscreen=true

    # 浅色模式 + 强制隐藏

    /admin/?_dark_mode_on=false&_force_hide_others=true

    # 自动主题 + 自定义视图

    /admin/?_dark_mode_on=auto&_view=detailed

数据看板配置

启用/禁用深色模式支持

在数据看板级别控制深色模式:

    class MyDashboard(Dashboard):
        # 启用深色模式支持(默认:True)

        supports_dark_mode = True

        # 自定义深色模式 CSS 类

        dark_mode_css_class = 'custom-dark-mode'

        # 主题特定设置

        dark_mode_settings = {
            'primary_color': '#3b82f6',
            'background_color': '#1f2937',
            'text_color': '#f9fafb',
        }

        light_mode_settings = {
            'primary_color': '#2563eb',
            'background_color': '#ffffff',
            'text_color': '#111827',
        }

主题检测方法

    class ThemeAwareDashboard(Dashboard):

        def get_current_theme(self, request):
            """基于多种因素确定当前主题"""

            # 1. 检查 URL 参数(最高优先级)

            url_theme = request.GET.get('_dark_mode_on')
            if url_theme in ['true', 'false', 'auto']:
                return self._normalize_theme(url_theme)

            # 2. 通过 JavaScript 上下文检查 localStorage

            js_theme = getattr(request, 'theme_from_js', None)
            if js_theme:
                return js_theme

            # 3. 检查 Django Admin 的主题

            admin_theme = get_admin_theme(request)
            if admin_theme:
                return admin_theme

            # 4. 默认为 auto(跟随系统)

            return 'auto'

        def _normalize_theme(self, theme):
            """规范化主题值"""
            if theme in ['true', '1', 'yes']:
                return 'dark'
            elif theme in ['false', '0', 'no']:
                return 'light'
            else:
                return theme  # 'auto' 或未更改

上下文变量

将主题信息添加到模板上下文:

    class ContextRichDashboard(Dashboard):

        def get_context_data(self, request):
            context = super().get_context_data(request)

            # 主题信息

            context['theme_info'] = {
                'current': self.get_current_theme(request),
                'is_dark': self.is_dark_mode(request),
                'is_light': self.is_light_mode(request),
                'is_auto': self.is_auto_theme(request),
                'supports_dark': self.supports_dark_mode,
            }

            # 主题特定设置

            if context['theme_info']['is_dark']:
                context.update(self.dark_mode_settings)
            else:
                context.update(self.light_mode_settings)

            return context

CSS 实现

CSS 自定义属性(变量)

Django Admin Dashboards 使用 CSS 自定义属性进行主题化:

    :root {
        /* 浅色主题默认 */
        --dashboard-bg-color: #ffffff;
        --dashboard-text-color: #111827;
        --dashboard-primary-color: #2563eb;
        --dashboard-border-color: #e5e7eb;
        --dashboard-card-bg: #f9fafb;
    }

    [data-theme="dark"] {
        /* 深色主题覆盖 */
        --dashboard-bg-color: #1f2937;
        --dashboard-text-color: #f9fafb;
        --dashboard-primary-color: #3b82f6;
        --dashboard-border-color: #374151;
        --dashboard-card-bg: #111827;
    }

组件样式

组件使用 CSS 变量自动适应深色模式:

    /* 基础组件样式 */
    .dashboard-component {
        background-color: var(--dashboard-card-bg);
        color: var(--dashboard-text-color);
        border: 1px solid var(--dashboard-border-color);
    }

    /* 深色模式特定调整 */
    [data-theme="dark"] .dashboard-component {
        /* 额外的深色模式调整 */
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
    }

    /* 组件部分 */
    .dashboard-component-header {
        border-bottom: 1px solid var(--dashboard-border-color);
        color: var(--dashboard-primary-color);
    }

    .dashboard-component-body {
        padding: 1rem;
    }

高对比度支持

    /* 高对比度模式支持 */
    @media (prefers-contrast: high) {
        [data-theme="dark"] {
            --dashboard-border-color: #ffffff;
            --dashboard-text-color: #ffffff;
        }

        [data-theme="light"] {
            --dashboard-border-color: #000000;
            --dashboard-text-color: #000000;
        }
    }

    /* 减少运动支持 */
    @media (prefers-reduced-motion: reduce)
    }

CSS 文件结构

深色模式样式在 dashboard.css 中组织:

    /* dashboard.css - 深色模式部分 */

    /* 1. CSS 变量 */
    :root { /* 浅色主题变量 */ }
    [data-theme="dark"] { /* 深色主题变量 */ }

    /* 2. 基础元素 */
    body { /* 基础 body 样式 */ }
    [data-theme="dark"] body { /* 深色 body 样式 */ }

    /* 3. 数据看板容器 */
    .dashboard { /* 数据看板容器 */ }
    [data-theme="dark"] .dashboard { /* 深色数据看板 */ }

    /* 4. 组件 */
    .dashboard .card { /* 卡片组件 */ }
    [data-theme="dark"] .dashboard .card { /* 深色卡片 */ }

    /* 5. 表格 */
    .dashboard table { /* 表格样式 */ }
    [data-theme="dark"] .dashboard table { /* 深色表格 */ }

    /* 6. 表单和输入 */
    .dashboard input, .dashboard select { /* 表单元素 */ }
    [data-theme="dark"] .dashboard input { /* 深色表单元素 */ }

    /* 7. 工具类 */
    .dashboard-fullscreen-mode { /* 全屏模式 */ }
    [data-theme="dark"].dashboard-fullscreen-mode { /* 深色全屏 */ }

JavaScript 集成

主题检测和切换

    // 主题检测和切换
    class DashboardThemeManager {

        constructor() {
            this.theme = localStorage.getItem('theme') || 'auto';
            this.initialize();
        }

        initialize() {
            // 首先检查 URL 参数
            const urlTheme = this.getUrlParameter('_dark_mode_on');
            if (urlTheme) {
                this.applyTheme(urlTheme);
                return;
            }

            // 应用当前主题
            this.applyTheme(this.theme);

            // 监听 Django Admin 的主题变化
            this.listenToAdminThemeChanges();
        }

        getUrlParameter(name) {
            // URL 参数解析逻辑
            const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)');
            const results = regex.exec(window.location.href);
            return results ? decodeURIComponent(results[2].replace(/\+/g, ' ')) : null;
        }

        applyTheme(theme) {
            // 规范化主题值
            theme = this.normalizeTheme(theme);

            // 更新 localStorage
            localStorage.setItem('theme', theme);

            // 更新 DOM
            if (theme === 'auto') {
                //  Django Admin 处理自动检测
                document.documentElement.removeAttribute('data-theme');
            } else {
                document.documentElement.setAttribute('data-theme', theme);
            }

            // 分发事件
            this.dispatchThemeChangeEvent(theme);
        }

        normalizeTheme(theme) {
            if (theme === 'true' || theme === '1' || theme === 'yes') {
                return 'dark';
            } else if (theme === 'false' || theme === '0' || theme === 'no') {
                return 'light';
            }
            return theme; // 'auto', 'dark',  'light'
        }

        listenToAdminThemeChanges() {
            // 监听 Django Admin 主题变化
            const observer = new MutationObserver((mutations) => {
                mutations.forEach((mutation) => {
                    if (mutation.attributeName === 'data-theme') {
                        const theme = document.documentElement.getAttribute('data-theme');
                        if (theme && this.theme === 'auto') {
                            // Django Admin 更改了主题
                            this.dispatchThemeChangeEvent(theme);
                        }
                    }
                });
            });

            observer.observe(document.documentElement, {
                attributes: true,
                attributeFilter: ['data-theme']
            });
        }

        dispatchThemeChangeEvent(theme) {
            window.dispatchEvent(new CustomEvent('dashboardthemechange', {
                detail: {
                    theme: theme,
                    isDark: theme === 'dark',
                    isLight: theme === 'light',
                    isAuto: theme === 'auto'
                }
            }));
        }
    }

    // 初始化主题管理器
    document.addEventListener('DOMContentLoaded', () => {
        window.dashboardThemeManager = new DashboardThemeManager();
    });

主题变更事件

在 JavaScript 中监听主题变化:

    // 监听主题变化
    window.addEventListener('dashboardthemechange', (event) => {
        const { theme, isDark, isLight, isAuto } = event.detail;

        console.log(`主题更改为:${theme}`);

        // 基于主题更新 UI
        if (isDark) {
            document.body.classList.add('theme-dark');
            document.body.classList.remove('theme-light');
        } else if (isLight) {
            document.body.classList.add('theme-light');
            document.body.classList.remove('theme-dark');
        }

        // 更新图表和可视化
        updateChartsForTheme(theme);
    });

    // 手动主题切换
    function switchTheme(theme) {
        // 更新 URL 参数
        const url = new URL(window.location);
        url.searchParams.set('_dark_mode_on', theme);
        window.history.replaceState({}, '', url);

        // 应用主题
        if (window.dashboardThemeManager) {
            window.dashboardThemeManager.applyTheme(theme);
        }
    }

与 Django Admin 的 theme.js 集成

    //  Django Admin 的主题系统协作
    function integrateWithAdminTheme() {
        // 检查 Django Admin  theme.js 是否已加载
        if (typeof window.theme !== 'undefined') {
            // 使用 Django Admin 的主题对象
            const adminTheme = window.theme;

            // 与数据看板主题管理器同步
            adminTheme.addEventListener('change', (theme) => {
                if (window.dashboardThemeManager.theme === 'auto') {
                    // 数据看板正在跟随 Django Admin 主题
                    window.dashboardThemeManager.dispatchThemeChangeEvent(theme);
                }
            });

            // 初始同步
            if (window.dashboardThemeManager.theme === 'auto') {
                window.dashboardThemeManager.dispatchThemeChangeEvent(adminTheme.current);
            }
        }
    }

自定义主题

创建自定义配色方案

使用自定义配色方案扩展默认深色模式:

    # dashboard.py

    class CustomThemedDashboard(Dashboard):

        def get_theme_settings(self, request):
            """返回主题特定设置"""

            base_settings = super().get_theme_settings(request)

            # 添加自定义主题设置

            if self.is_dark_mode(request):
                base_settings.update({
                    'accent_color': '#8b5cf6',
                    'success_color': '#10b981',
                    'warning_color': '#f59e0b',
                    'danger_color': '#ef4444',
                    'info_color': '#06b6d4',
                })

            return base_settings
    /* custom-theme.css */
    [data-theme="dark"] {
        /* 自定义深色主题 */
        --dashboard-accent-color: #8b5cf6;
        --dashboard-success-color: #10b981;
        --dashboard-warning-color: #f59e0b;
        --dashboard-danger-color: #ef4444;
        --dashboard-info-color: #06b6d4;

        /* 组件特定自定义 */
        --dashboard-card-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3);
        --dashboard-border-radius: 12px;
    }

    /* 将自定义主题应用于特定数据看板 */
    .dashboard-custom-theme[data-theme="dark"] {
        /* 额外的自定义 */
        background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
    }

主题变体

    # 多个主题变体

    class MultiThemeDashboard(Dashboard):

        THEME_VARIANTS = {
            'default': {
                'dark': {
                    'primary': '#3b82f6',
                    'background': '#1f2937',
                },
                'light': {
                    'primary': '#2563eb',
                    'background': '#ffffff',
                }
            },
            'blue': {
                'dark': {
                    'primary': '#60a5fa',
                    'background': '#1e3a8a',
                },
                'light': {
                    'primary': '#1d4ed8',
                    'background': '#dbeafe',
                }
            },
            'green': {
                'dark': {
                    'primary': '#34d399',
                    'background': '#064e3b',
                },
                'light': {
                    'primary': '#059669',
                    'background': '#d1fae5',
                }
            }
        }

        def get_theme_variant(self, request):
            """从 URL 或用户偏好获取当前主题变体"""
            return request.GET.get('_theme_variant', 'default')

        def get_context_data(self, request):
            context = super().get_context_data(request)

            variant = self.get_theme_variant(request)
            theme_type = 'dark' if self.is_dark_mode(request) else 'light'

            # 添加变体特定的 CSS 类

            context['dashboard_css_class'] = f'dashboard-variant-{variant}'

            # 将变体颜色添加到上下文

            if variant in self.THEME_VARIANTS:
                context.update(self.THEME_VARIANTS[variant][theme_type])

            return context

动态主题切换

    // 带有平滑过渡的动态主题切换
    class DynamicThemeSwitcher {

        constructor() {
            this.transitionDuration = 300;
            this.init();
        }

        init() {
            // 添加过渡样式
            this.addTransitionStyles();

            // 监听主题变化
            window.addEventListener('dashboardthemechange', (event) => {
                this.handleThemeChange(event.detail);
            });
        }

        addTransitionStyles() {
            const style = document.createElement('style');
            style.textContent = `
                .dashboard,
                .dashboard * {
                    transition: background-color ${this.transitionDuration}ms ease,
                              color ${this.transitionDuration}ms ease,
                              border-color ${this.transitionDuration}ms ease,
                              box-shadow ${this.transitionDuration}ms ease;
                }

                /* 在初始加载期间禁用过渡 */
                .dashboard-preload * {
                    transition: none !important;
                }
            `;
            document.head.appendChild(style);

            // 初始加载后移除 preload 
            setTimeout(() => {
                document.body.classList.remove('dashboard-preload');
            }, 100);
        }

        handleThemeChange(themeDetail) {
            const { theme, isDark } = themeDetail;

            // 添加过渡类
            document.body.classList.add('dashboard-theme-transitioning');

            // 更新主题
            if (theme === 'auto') {
                document.documentElement.removeAttribute('data-theme');
            } else {
                document.documentElement.setAttribute('data-theme', theme);
            }

            // 过渡后移除过渡类
            setTimeout(() => {
                document.body.classList.remove('dashboard-theme-transitioning');
            }, this.transitionDuration);
        }
    }

组件深色模式

组件特定的深色模式样式

组件可以具有主题特定样式:

    # components.py

    class DarkModeAwareComponent(Component):

        def get_context_data(self, request):
            context = super().get_context_data(request)

            # 添加主题信息

            context['is_dark_mode'] = self.dashboard.is_dark_mode(request)
            context['theme_class'] = 'dark-mode' if context['is_dark_mode'] else 'light-mode'

            # 主题特定数据

            if context['is_dark_mode']:
                context['icon_color'] = '#f9fafb'
                context['background_color'] = '#374151'
            else:
                context['icon_color'] = '#111827'
                context['background_color'] = '#f3f4f6'

            return context
    <div class="dashboard-component {{ theme_class }}"
         style="background-color: {{ background_color }};">

        <div class="component-header">
            <i class="ri-icon" style="color: {{ icon_color }};"></i>
            <h3>{{ title }}</h3>
        </div>

        <div class="component-body">
            {% if is_dark_mode %}

                <p class="dark-mode-note">
                    正在深色模式下查看
                </p>
            {% endif %}

            {{ content }}
        </div>
    </div>

图表组件主题化

    # chart_component.py

    class ThemedChartComponent(ChartComponent):

        def get_chart_options(self, request):
            options = super().get_chart_options(request)

            if self.dashboard.is_dark_mode(request):
                # 深色模式图表选项

                options.update({
                    'backgroundColor': '#1f2937',
                    'color': '#f9fafb',
                    'borderColor': '#374151',
                    'plugins': {
                        'legend': {
                            'labels': {
                                'color': '#f9fafb'
                            }
                        }
                    },
                    'scales': {
                        'x': {
                            'grid': {
                                'color': '#374151'
                            },
                            'ticks': {
                                'color': '#9ca3af'
                            }
                        },
                        'y': {
                            'grid': {
                                'color': '#374151'
                            },
                            'ticks': {
                                'color': '#9ca3af'
                            }
                        }
                    }
                })

            return options

表格组件主题化

    /* 表格深色模式样式 */
    [data-theme="dark"] .dashboard table {
        background-color: #111827;
        color: #f9fafb;
        border-color: #374151;
    }

    [data-theme="dark"] .dashboard table thead {
        background-color: #1f2937;
        border-bottom: 2px solid #374151;
    }

    [data-theme="dark"] .dashboard table th {
        color: #d1d5db;
        border-color: #374151;
    }

    [data-theme="dark"] .dashboard table td {
        border-color: #374151;
    }

    [data-theme="dark"] .dashboard table tr:hover {
        background-color: #1f2937;
    }

    [data-theme="dark"] .dashboard table tr:nth-child(even) {
        background-color: rgba(31, 41, 55, 0.5);
    }

测试深色模式

手动测试清单

使用此清单测试深色模式功能:

  1. URL 参数测试

  2. /?_dark_mode_on=true → 应强制深色模式

  3. /?_dark_mode_on=false → 应强制浅色模式

  4. /?_dark_mode_on=auto → 应跟随 Django Admin 主题

  5. /?_dark_mode_on=invalid → 应默认为 auto

  6. 主题持久性

  7. 刷新页面 → 主题应持久

  8. 打开新标签页 → 主题应持久

  9. 清除 localStorage → 应返回默认

  10. Django Admin 集成

  11. 主题切换按钮应在 _dark_mode_on=auto 时工作

  12. 应检测系统主题变化

  13. 主题应与 Django Admin 同步

  14. 组件测试

  15. 所有组件应适应深色模式

  16. 两种主题下文本应可读

  17. 颜色应有足够的对比度

  18. 图像和图标应可见

  19. 浏览器兼容性

  20. Chrome、Firefox、Safari、Edge

  21. 移动浏览器

  22. 不同屏幕尺寸

自动化测试

    # tests.py

    from django.test import TestCase
    from django.test.client import RequestFactory

    class DarkModeTests(TestCase):

        def setUp(self):
            self.factory = RequestFactory()
            self.dashboard = AuthAppDashboard()

        def test_dark_mode_url_parameter(self):
            """测试深色模式 URL 参数解析"""

            # 测试真值

            request = self.factory.get('/admin/?_dark_mode_on=true')
            self.assertTrue(self.dashboard.is_dark_mode(request))

            request = self.factory.get('/admin/?_dark_mode_on=1')
            self.assertTrue(self.dashboard.is_dark_mode(request))

            request = self.factory.get('/admin/?_dark_mode_on=yes')
            self.assertTrue(self.dashboard.is_dark_mode(request))

            # 测试假值

            request = self.factory.get('/admin/?_dark_mode_on=false')
            self.assertFalse(self.dashboard.is_dark_mode(request))

            request = self.factory.get('/admin/?_dark_mode_on=0')
            self.assertFalse(self.dashboard.is_dark_mode(request))

            request = self.factory.get('/admin/?_dark_mode_on=no')
            self.assertFalse(self.dashboard.is_dark_mode(request))

            # 测试 auto

            request = self.factory.get('/admin/?_dark_mode_on=auto')
            self.assertEqual(self.dashboard.get_current_theme(request), 'auto')

        def test_dark_mode_context(self):
            """测试深色模式上下文变量"""

            # 深色模式请求

            request = self.factory.get('/admin/?_dark_mode_on=true')
            context = self.dashboard.get_context_data(request)

            self.assertIn('is_dark_mode', context)
            self.assertTrue(context['is_dark_mode'])

            self.assertIn('current_theme', context)
            self.assertEqual(context['current_theme'], 'dark')

            # 浅色模式请求

            request = self.factory.get('/admin/?_dark_mode_on=false')
            context = self.dashboard.get_context_data(request)

            self.assertIn('is_dark_mode', context)
            self.assertFalse(context['is_dark_mode'])

            self.assertIn('current_theme', context)
            self.assertEqual(context['current_theme'], 'light')

        def test_dark_mode_css_classes(self):
            """测试渲染 HTML 中的深色模式 CSS 类"""

            from django.test import Client
            from django.urls import reverse

            client = Client()

            # 测试深色模式渲染带有深色类

            response = client.get(reverse('admin:index') + '?_dark_mode_on=true')
            self.assertContains(response, 'data-theme="dark"')
            self.assertContains(response, 'dashboard-dark-mode')

            # 测试浅色模式渲染不带深色类

            response = client.get(reverse('admin:index') + '?_dark_mode_on=false')
            self.assertContains(response, 'data-theme="light"')
            self.assertNotContains(response, 'dashboard-dark-mode')

可访问性测试

    # accessibility_tests.py

    import pytest
    from selenium import webdriver
    from selenium.webdriver.common.by import By

    @pytest.mark.accessibility
    class TestDarkModeAccessibility:

        @pytest.fixture
        def driver(self):
            driver = webdriver.Chrome()
            yield driver
            driver.quit()

        def test_contrast_ratio(self, driver):
            """测试深色模式下的对比度比率"""

            # 加载深色模式页面

            driver.get('http://localhost:8000/admin/?_dark_mode_on=true')

            # 测试文本对比度

            text_elements = driver.find_elements(By.CSS_SELECTOR, '.dashboard p, .dashboard h1, .dashboard h2, .dashboard h3')

            for element in text_elements:
                color = element.value_of_css_property('color')
                bg_color = element.value_of_css_property('background-color')

                # 计算对比度比率(简化版)

                # 在实际测试中,使用适当的对比度比率计算

                assert color != bg_color, f"文本颜色与背景匹配:{element.text}"

        def test_focus_indicators(self, driver):
            """测试深色模式下焦点指示器是否可见"""

            driver.get('http://localhost:8000/admin/?_dark_mode_on=true')

            # 查找可聚焦元素

            focusable = driver.find_elements(By.CSS_SELECTOR, 'button, [href], input, select, textarea, [tabindex]')

            for element in focusable:
                # 聚焦元素

                driver.execute_script("arguments[0].focus();", element)

                # 检查焦点样式

                outline = element.value_of_css_property('outline')
                box_shadow = element.value_of_css_property('box-shadow')

                # 焦点应可见

                assert outline != 'none' or box_shadow != 'none', \
                    f"元素上焦点不可见:{element.tag_name}"

故障排除

常见问题和解决方案

1. 深色模式未应用

症状:URL 参数无效,主题不改变。

解决方案

  • 检查 JavaScript 控制台是否有错误 - 验证数据看板是否设置了 supports_dark_mode = True - 清除浏览器缓存和 localStorage - 检查与 !important 规则的 CSS 冲突
    // 调试深色模式
    console.log('当前主题:', localStorage.getItem('theme'));
    console.log('URL 参数:', new URLSearchParams(window.location.search).get('_dark_mode_on'));
    console.log('DOM data-theme:', document.documentElement.getAttribute('data-theme'));

2. 主题切换按钮不工作

症状:Django Admin 主题切换按钮不改变数据看板主题。

解决方案

  • 确保设置了 _dark_mode_on=auto(或没有 _dark_mode_on 参数) - 检查数据看板 JavaScript 是否干扰了 Django Admin 的 theme.js - 验证数据看板是否正在监听主题变更事件
    // 检查 theme.js 是否已加载
    if (typeof window.theme !== 'undefined') {
        console.log('Django Admin theme.js 已加载:', window.theme.current);
    }

3. 错误主题闪烁 (FOWT)

症状:在深色模式应用之前短暂闪烁浅色主题。

解决方案

  • 在 HTML head 中添加初始主题检测 - 使用不依赖 JavaScript 进行初始渲染的 CSS - 在服务器端添加 data-theme 属性
    <script>
        // 立即设置主题以防止闪烁
        (function() {
            const urlParams = new URLSearchParams(window.location.search);
            const darkModeParam = urlParams.get('_dark_mode_on');

            if (darkModeParam === 'true' || darkModeParam === '1' || darkModeParam === 'yes') {
                document.documentElement.setAttribute('data-theme', 'dark');
            } else if (darkModeParam === 'false' || darkModeParam === '0' || darkModeParam === 'no') {
                document.documentElement.setAttribute('data-theme', 'light');
            }
            // 如果是 'auto' 或未指定 Django Admin 处理
        })();
    </script>

4. CSS 变量不工作

症状:颜色不随主题改变。

解决方案

  • 检查 CSS 变量定义是否在正确的作用域内 - 验证 CSS 在主题设置后加载 - 使用更简单的 CSS 进行测试以隔离问题
    /* 测试 CSS 变量是否工作 */
    .test-var {
        color: var(--dashboard-text-color, red); /* 回退到红色 */
    }

    /* 在浏览器开发者工具中强制检查 */
    [data-theme="dark"] {
        --dashboard-text-color: #f9fafb !important;
    }

5. 性能问题

症状:主题切换缓慢,页面卡顿。

解决方案

  • 减少 CSS 选择器的复杂性 - 最小化使用 !important - 谨慎使用 CSS 过渡 - 防抖快速主题更改
    // 防抖主题更改
    let themeChangeTimeout;
    function debouncedThemeChange(theme) {
        clearTimeout(themeChangeTimeout);
        themeChangeTimeout = setTimeout(() => {
            applyTheme(theme);
        }, 100); // 100ms 防抖
    }

调试工具

浏览器开发者工具

  1. 元素面板:检查 <html> 元素上的 data-theme 属性
  2. 控制台:检查 JavaScript 错误
  3. 网络面板:验证 CSS 文件是否正在加载
  4. 应用程序面板:检查 localStorage 值

调试 CSS

    /* 添加调试样式 */
    [data-theme="dark"] * {
        outline: 1px solid rgba(255, 0, 0, 0.1) !important;
    }

    /* 高亮主题特定元素 */
    [data-theme="dark"]::before {
        content: '深色模式已激活';
        position: fixed;
        top: 0;
        right: 0;
        background: red;
        color: white;
        padding: 5px;
        z-index: 9999;
        font-size: 12px;
    }

JavaScript 调试助手

    // 添加到浏览器控制台进行调试
    window.debugDarkMode = function() {
        console.group('深色模式调试');
        console.log('URL:', window.location.href);
        console.log('参数:', new URLSearchParams(window.location.search).toString());
        console.log('localStorage 主题:', localStorage.getItem('theme'));
        console.log('DOM data-theme:', document.documentElement.getAttribute('data-theme'));
        console.log('CSS 变量:');
        console.log('  --dashboard-bg-color:', getComputedStyle(document.documentElement)
            .getPropertyValue('--dashboard-bg-color').trim());
        console.groupEnd();
    };

获取帮助

如果仍有问题:

  1. 检查文档:查看本指南和其他文档
  2. 搜索问题:检查 GitHub 仓库是否有类似问题
  3. 创建最小示例:在最小测试用例中重现问题
  4. 报告问题:包括浏览器版本、Django 版本和错误消息

下一步