第二十一章:LeptonX Lite 主題客製
學習目標
- 理解 ABP LeptonX Lite 主題結構與限制(社群版)
- 掌握 CSS 變數與 SCSS 客製方法
- 實作深色/淺色模式動態切換
- 能建立與發佈自訂主題
先備知識
- 熟悉 CSS、SCSS 基礎
- 已完成第四章(前端開發)或第二十章(React)
- 了解 ABP 專案結構
正文
一、LeptonX Lite 主題簡介
特性
- 輕量級、模組化設計
- 支援深色/淺色主題切換
- 響應式佈局(Mobile-first)
- 內建暗黑模式支援
- 社群版可用,Pro 版有更多主題
限制
- 社群版功能較少,部分高級客製屬於 Pro
- 某些企業級主題不可用
- 支援 Razor Pages 與 Blazor Server/WebAssembly
二、主題資源位置
在 Razor Pages 應用中
YourApp.Web/
├── wwwroot/
│ ├── css/
│ │ ├── theme.css # 自訂樣式
│ │ └── variables.css # CSS 變數
│ ├── js/
│ │ └── theme-switcher.js # 主題切換邏輯
│ └── lib/
│ └── bootstrap/ # Bootstrap 資源
├── Pages/
│ ├── _Layout.cshtml # 主佈局
│ └── _Host.cshtml # Blazor 佈局
└── Themes/ # 主題配置(若使用主題系統)在 React SPA 中
my-app/
├── src/
│ ├── styles/
│ │ ├── theme.css
│ │ ├── variables.css
│ │ └── dark-mode.css
│ ├── components/
│ │ └── ThemeSwitcher.tsx
│ └── App.tsx
└── public/
└── themes/ # 主題資源三、CSS 變數與主題客製
使用 CSS 變數定義主題
css
/* css - src/styles/variables.css */
:root {
/* Light 模式(預設) */
--color-primary: #007bff;
--color-secondary: #6c757d;
--color-danger: #dc3545;
--color-success: #28a745;
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--text-primary: #212529;
--text-secondary: #6c757d;
--border-color: #dee2e6;
--shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
/* Dark 模式 */
[data-theme="dark"] {
--color-primary: #0d6efd;
--color-secondary: #adb5bd;
--color-danger: #f8615d;
--color-success: #51cf66;
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--text-primary: #e9ecef;
--text-secondary: #adb5bd;
--border-color: #495057;
--shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.3);
}應用 CSS 變數
css
/* css - src/styles/theme.css */
body {
background-color: var(--bg-primary);
color: var(--text-primary);
transition: background-color 0.3s ease, color 0.3s ease;
}
.btn-primary {
background-color: var(--color-primary);
border-color: var(--color-primary);
}
.card {
background-color: var(--bg-secondary);
border-color: var(--border-color);
box-shadow: var(--shadow);
}
.navbar {
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}四、深色/淺色模式切換
在 Razor Pages 中實作
csharp
// csharp - Pages/_Layout.cshtml
<script>
// 初始化主題
(function() {
const theme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', theme);
})();
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme');
const newTheme = current === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
// 通知伺服器更新使用者設定
fetch('/api/user-settings/theme', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ theme: newTheme })
});
}
</script>
<button onclick="toggleTheme()" class="btn btn-outline-secondary">
<i class="fas fa-moon"></i> 切換主題
</button>在 React 中實作
typescript
// typescript - src/components/ThemeSwitcher.tsx
import React from 'react';
export function ThemeSwitcher() {
const [theme, setTheme] = React.useState<'light' | 'dark'>(
(localStorage.getItem('theme') as any) || 'light'
);
const toggleTheme = React.useCallback(() => {
const newTheme = theme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
// 應用主題至 DOM
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
// 通知伺服器(可選)
fetch('/api/user-settings/theme', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ theme: newTheme })
}).catch(err => console.error('Failed to save theme:', err));
}, [theme]);
React.useEffect(() => {
// 初始化主題
document.documentElement.setAttribute('data-theme', theme);
}, []);
return (
<button
onClick={toggleTheme}
className="btn btn-outline-secondary"
title={`切換至 ${theme === 'light' ? '深色' : '淺色'} 模式`}
>
<i className={theme === 'light' ? 'fas fa-moon' : 'fas fa-sun'}></i>
</button>
);
}在 App 中使用
typescript
// typescript - src/App.tsx
import { ThemeSwitcher } from '@/components/ThemeSwitcher';
export function App() {
return (
<div>
<header>
<nav>
{/* ... 導覽內容 ... */}
<ThemeSwitcher />
</nav>
</header>
<main>{/* 頁面內容 */}</main>
</div>
);
}五、系統偏好檢測
根據作業系統設定自動選擇主題
typescript
// typescript
function getSystemTheme(): 'light' | 'dark' {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
}
function initializeTheme() {
const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null;
const theme = savedTheme || getSystemTheme();
document.documentElement.setAttribute('data-theme', theme);
}
// 監聽系統主題變更
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
const newTheme = e.matches ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', newTheme);
});
initializeTheme();六、佈局客製
修改主佈局(Razor Pages)
html
<!-- html - Pages/_Layout.cshtml -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>@ViewData["Title"] - 我的應用</title>
<!-- 主題樣式 -->
<link rel="stylesheet" href="~/css/variables.css" />
<link rel="stylesheet" href="~/css/theme.css" />
<!-- Bootstrap 或其他 CSS 框架 -->
<link rel="stylesheet" href="~/lib/bootstrap/css/bootstrap.min.css" />
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light">
<div class="container">
<a class="navbar-brand" href="/">我的應用</a>
<button class="btn btn-outline-secondary ms-auto" onclick="toggleTheme()">
<i class="fas fa-moon"></i>
</button>
</div>
</nav>
<main class="container mt-4">
@RenderBody()
</main>
<script src="~/js/theme-switcher.js"></script>
</body>
</html>七、SCSS 變數與 Mixin 進階客製
使用 SCSS 簡化主題定義
scss
// scss - src/styles/theme.scss
$light-primary: #007bff;
$light-secondary: #6c757d;
$dark-primary: #0d6efd;
$dark-secondary: #adb5bd;
$light-bg: #ffffff;
$dark-bg: #1a1a1a;
// Mixin 定義主題樣式
@mixin themed {
@media (prefers-color-scheme: light) {
@content(
$light-primary,
$light-secondary,
$light-bg
);
}
@media (prefers-color-scheme: dark) {
@content(
$dark-primary,
$dark-secondary,
$dark-bg
);
}
}
// 使用 Mixin
.btn-primary {
@include themed using ($primary, $secondary, $bg) {
background-color: $primary;
border-color: $primary;
}
}
body {
@include themed using ($primary, $secondary, $bg) {
background-color: $bg;
color: $secondary;
}
}八、主題配置檔與預設
建立主題配置
json
/* json - src/themes/default.json */
{
"name": "Default Light",
"isDark": false,
"colors": {
"primary": "#007bff",
"secondary": "#6c757d",
"danger": "#dc3545",
"success": "#28a745",
"warning": "#ffc107",
"info": "#17a2b8"
},
"typography": {
"fontFamily": "Segoe UI, Arial, sans-serif",
"fontSize": "14px",
"lineHeight": "1.5"
},
"spacing": {
"unit": "8px"
}
}動態載入主題配置
typescript
// typescript
async function loadThemeConfig(themeName: string) {
const response = await fetch(`/themes/${themeName}.json`);
const config = await response.json();
// 應用 CSS 變數
Object.entries(config.colors).forEach(([key, value]) => {
document.documentElement.style.setProperty(`--color-${key}`, value as string);
});
localStorage.setItem('current-theme', themeName);
}
// 使用
await loadThemeConfig('dark');九、實務最佳實務
- 為主題提供合理預設(遵循系統偏好)
- 使用 CSS 變數而非硬編碼顏色
- 確保無障礙性(對比度、字型大小)
- 測試淺色/深色模式的可讀性
- 提供主題切換的使用者回饋(動畫、圖示變更)
- 支援偏好持久化(localStorage 或伺服器)
實作練習
- 為 React 應用建立完整的主題系統(淺/深色 + CSS 變數)
- 實作主題切換器組件與系統偏好檢測
- 建立主題配置檔並動態載入
- 測試不同主題下的可讀性與一致性
習題(至少 6 題)
概念題(易)
- CSS 變數與 SCSS 變數的差異為何?(難度:易)
- 為什麼應該支援
prefers-color-scheme?(難度:中)
計算 / 練習題(中) 3. 設計一個包含 10 個顏色、5 個間距、3 種字型的主題配置結構。(難度:中) 4. 比較三種主題儲存方式(localStorage、sessionStorage、伺服器資料庫)的優缺點。(難度:中)
實作 / 編碼題(較難) 5. 實作一個完整的主題系統(包括淺/深色、多主題切換、系統偏好檢測、過渡動畫)。(難度:較難) 6. 建立一個主題編輯器 UI,允許使用者自訂顏色並即時預覽。(難度:較難)
術語表
- CSS Variable(CSS 變數):在 CSS 中定義可重用的值,如
--color-primary - prefers-color-scheme:CSS Media Query,檢測使用者系統深色/淺色偏好
- Theme Switching(主題切換):動態變更應用的視覺樣式
- Accessibility(可及性):確保應用易於使用(含視障、色弱)
參考資料
- ABP UI & Themes:https://docs.abp.io/en/abp/latest/UI (來源:content7)
- MDN - prefers-color-scheme:https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme
- CSS Variables:https://developer.mozilla.org/en-US/docs/Web/CSS/--*
習題解答:content/solutions/ch21-solutions.md