feat: 初始化 Halo 暗色模式插件

- Halo Plugin 后端(Java/Gradle),含 DarkModePlugin 主类和测试
- Vue 3 + TypeScript 前端 UI,包含主题切换组件和设置页面
- 暗色模式 CSS 变量和覆盖样式(布局/编辑器/表单/滚动条等)
- 设计文档和调查文档
- Halo 插件/主题开发 Agent Skills

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-06 21:03:00 +08:00
commit edbf78f236
115 changed files with 14745 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
plugins {
id 'base'
id "com.github.node-gradle.node" version "7.1.0"
}
group 'run.halo.darkmode.ui'
tasks.register('pnpmBuild', PnpmTask) {
group = 'build'
description = 'Build the UI project using pnpm'
args = ['build']
dependsOn tasks.named('pnpmInstall')
inputs.dir(layout.projectDirectory.dir('src'))
inputs.files(fileTree(
dir: layout.projectDirectory,
includes: ['*.cjs', '*.ts', '*.js', '*.json', '*.yaml']))
outputs.dir(layout.buildDirectory.dir('dist'))
}
tasks.register('pnpmCheck', PnpmTask) {
group = 'verification'
description = 'Run unit tests for the UI project using pnpm'
args = ['test:unit']
dependsOn tasks.named('pnpmInstall')
}
tasks.named('check') {
dependsOn tasks.named('pnpmCheck')
}
tasks.named('assemble') {
dependsOn tasks.named('pnpmBuild')
}
Vendored
+2
View File
@@ -0,0 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="unplugin-icons/types/vue" />
+30
View File
@@ -0,0 +1,30 @@
import { globalIgnores } from 'eslint/config'
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
import pluginVue from 'eslint-plugin-vue'
import pluginVitest from '@vitest/eslint-plugin'
import pluginOxlint from 'eslint-plugin-oxlint'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
// import { configureVueProject } from '@vue/eslint-config-typescript'
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
export default defineConfigWithVueTs(
{
name: 'app/files-to-lint',
files: ['**/*.{ts,mts,tsx,vue}'],
},
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
pluginVue.configs['flat/essential'],
vueTsConfigs.recommended,
{
...pluginVitest.configs.recommended,
files: ['src/**/__tests__/*'],
},
...pluginOxlint.configs['flat/recommended'],
skipFormatting,
)
+55
View File
@@ -0,0 +1,55 @@
{
"type": "module",
"scripts": {
"build": "run-p type-check \"build-only {@}\" --",
"build-only": "vite build",
"dev": "vite build --watch --mode=development",
"lint:oxlint": "oxlint . --fix -D correctness --ignore-path ../.gitignore",
"lint:eslint": "eslint . --fix",
"lint": "run-s lint:*",
"prettier": "prettier --write src/",
"test:unit": "vitest --passWithNoTests",
"type-check": "vue-tsc --build"
},
"prettier": {
"printWidth": 100,
"semi": false,
"singleQuote": true,
"tabWidth": 2
},
"dependencies": {
"@halo-dev/api-client": "^2.25.1",
"@halo-dev/components": "^2.25.1",
"@halo-dev/ui-shared": "^2.25.1",
"axios": "^1.13.5",
"canvas-confetti": "^1.9.3",
"vue": "^3.5.28"
},
"devDependencies": {
"vite": "^8.0.16",
"@halo-dev/ui-plugin-bundler-kit": "^2.25.1",
"@iconify-json/ri": "^1.2.10",
"@tsconfig/node20": "^20.1.6",
"@types/canvas-confetti": "^1.9.0",
"@types/jsdom": "^21.1.7",
"@types/node": "^24.13.1",
"@vitest/eslint-plugin": "^1.2.7",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.5.1",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.29.0",
"eslint-plugin-oxlint": "^0.16.12",
"eslint-plugin-vue": "~10.0.1",
"jsdom": "^26.1.0",
"npm-run-all2": "^7.0.2",
"oxlint": "^0.16.12",
"prettier": "^3.8.3",
"sass": "^1.89.2",
"typescript": "~5.8.3",
"unplugin-icons": "^23.0.1",
"vitest": "^4.1.0",
"vue-tsc": "^3.3.3"
},
"packageManager": "pnpm@10.12.4"
}
+4486
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.5 KiB

+46
View File
@@ -0,0 +1,46 @@
<script setup lang="ts">
import { IconMoon, IconSunny } from '@halo-dev/components'
import { useDarkMode } from '../composables/useDarkMode'
const { isDark, toggle } = useDarkMode()
</script>
<template>
<div class="theme-toggle" @click="toggle">
<IconSunny v-if="isDark" class="theme-toggle__icon" />
<IconMoon v-else class="theme-toggle__icon" />
<span class="theme-toggle__label">
{{ isDark ? '浅色模式' : '深色模式' }}
</span>
</div>
</template>
<style scoped>
.theme-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
cursor: pointer;
color: var(--halo-text-secondary);
border-radius: 0.25rem;
transition: background-color 0.15s, color 0.15s;
user-select: none;
}
.theme-toggle:hover {
background-color: var(--halo-bg-hover);
color: var(--halo-text-primary);
}
.theme-toggle__icon {
width: 1.125rem;
height: 1.125rem;
flex-shrink: 0;
}
.theme-toggle__label {
font-size: 0.8125rem;
white-space: nowrap;
}
</style>
+86
View File
@@ -0,0 +1,86 @@
import { computed, ref, watch } from 'vue'
import { useSystemPreference } from './useSystemPreference'
const STORAGE_KEY = 'halo-dark-mode-theme'
export type ThemeMode = 'light' | 'dark' | 'auto'
/** 从 localStorage 读取持久化的主题偏好 */
function loadPersistedTheme(): ThemeMode {
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored === 'light' || stored === 'dark' || stored === 'auto') {
return stored
}
} catch {
// localStorage 不可用时忽略
}
return 'auto'
}
/** 持久化主题偏好到 localStorage */
function persistTheme(theme: ThemeMode): void {
try {
localStorage.setItem(STORAGE_KEY, theme)
} catch {
// localStorage 不可用时忽略
}
}
/** 将 data-halo-theme 属性应用到 <html> 元素 */
function applyHtmlAttribute(isDark: boolean): void {
if (isDark) {
document.documentElement.setAttribute('data-halo-theme', 'dark')
} else {
document.documentElement.removeAttribute('data-halo-theme')
}
}
// 模块级单例 — 所有组件共享同一个状态
const theme = ref<ThemeMode>(loadPersistedTheme())
const { isSystemDark, onChange } = useSystemPreference()
const isDark = computed<boolean>(() => {
if (theme.value === 'dark') return true
if (theme.value === 'light') return false
// 'auto' — 跟随系统偏好
return isSystemDark.value
})
// 监听 isDark 变化 → 同步到 DOM
watch(isDark, (dark) => applyHtmlAttribute(dark), { immediate: true })
// 监听 theme 变化 → 持久化
watch(theme, (t) => persistTheme(t))
// 监听系统偏好变化 → 仅在 'auto' 模式下响应
onChange((systemDark) => {
if (theme.value === 'auto') {
applyHtmlAttribute(systemDark)
}
})
/**
* 核心 dark mode composable。
* 模块级单例 — 所有使用者共享同一份状态。
*/
export function useDarkMode() {
function setTheme(t: ThemeMode): void {
theme.value = t
}
function toggle(): void {
// 在 light 和 dark 之间切换,如果当前是 auto 则切换到与系统相反
if (theme.value === 'auto') {
theme.value = isSystemDark.value ? 'light' : 'dark'
} else {
theme.value = theme.value === 'dark' ? 'light' : 'dark'
}
}
return {
theme,
isDark,
setTheme,
toggle,
}
}
+19
View File
@@ -0,0 +1,19 @@
import { ref } from 'vue'
/**
* 监听系统级 prefers-color-scheme 偏好。
* matchMedia 查询 + 事件监听,支持运行时切换响应。
*/
export function useSystemPreference() {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const isSystemDark = ref<boolean>(mediaQuery.matches)
function onChange(callback: (isDark: boolean) => void): void {
mediaQuery.addEventListener('change', (event: MediaQueryListEvent) => {
isSystemDark.value = event.matches
callback(event.matches)
})
}
return { isSystemDark, onChange }
}
+29
View File
@@ -0,0 +1,29 @@
import { definePlugin } from '@halo-dev/ui-shared'
import { IconMoon } from '@halo-dev/components'
import { markRaw } from 'vue'
import './styles/index.css'
export default definePlugin({
components: {},
routes: [
{
parentName: 'Root',
route: {
path: '/dark-mode-settings',
name: 'DarkModeSettings',
component: () => import('./views/SettingsView.vue'),
meta: {
title: '深色模式设置',
searchable: true,
menu: {
name: '深色模式',
group: '偏好设置',
icon: markRaw(IconMoon),
priority: 50,
},
},
},
},
],
extensionPoints: {},
})
+12
View File
@@ -0,0 +1,12 @@
/* ============================================================
Halo Dark Mode — 样式入口
按加载顺序导入所有样式文件
============================================================ */
@import './variables.css';
@import './overrides/utilities.css';
@import './overrides/layout.css';
@import './overrides/components.css';
@import './overrides/forms.css';
@import './overrides/editor.css';
@import './overrides/scrollbar.css';
+142
View File
@@ -0,0 +1,142 @@
/* ============================================================
Halo Dark Mode — @halo-dev/components 组件库覆盖
覆盖 Halo 组件库中的 VCard, VModal, VDropdown, VTag 等
============================================================ */
[data-halo-theme="dark"] {
/* ===== 卡片 VCard ===== */
.v-card,
[class*="v-card"] {
background-color: var(--halo-bg-card);
border-color: var(--halo-border-base);
color: var(--halo-text-primary);
}
/* ===== 模态框 VModal ===== */
.v-modal,
.modal-container,
[class*="modal"] {
background-color: var(--halo-bg-card);
color: var(--halo-text-primary);
}
/* ===== 下拉菜单 VDropdown ===== */
.v-dropdown,
.dropdown-menu,
[class*="dropdown"] {
background-color: var(--halo-bg-dropdown);
border-color: var(--halo-border-base);
color: var(--halo-text-primary);
}
.v-dropdown-item:hover,
.dropdown-item:hover {
background-color: var(--halo-bg-hover);
}
/* ===== 提示框 VTooltip ===== */
.v-tooltip,
[class*="tooltip"] {
background-color: var(--halo-bg-tooltip);
color: var(--halo-text-inverse);
}
/* ===== 标签/徽章 VTag, VBadge ===== */
.v-tag,
[class*="tag"],
.v-badge,
[class*="badge"] {
background-color: var(--halo-tag-bg);
color: var(--halo-tag-text);
}
/* ===== 按钮 ===== */
.btn-default,
.btn-secondary,
button:not([class*="btn-primary"]):not([class*="btn-danger"]) {
background-color: var(--halo-bg-card);
color: var(--halo-text-primary);
border-color: var(--halo-border-base);
}
.btn-default:hover,
.btn-secondary:hover {
background-color: var(--halo-bg-hover);
}
/* ===== 表格 VTable ===== */
table,
.v-table,
[class*="table"] {
background-color: var(--halo-bg-card);
color: var(--halo-text-primary);
}
thead,
.table-header {
background-color: var(--halo-table-header-bg);
}
thead th,
.table-header th {
color: var(--halo-text-secondary);
border-color: var(--halo-table-border);
}
tbody td,
.table-body td {
border-color: var(--halo-table-border);
}
tbody tr:hover,
.table-row:hover {
background-color: var(--halo-table-row-hover);
}
/* ===== 分页 ===== */
.pagination,
.v-pagination {
color: var(--halo-text-secondary);
}
.pagination .active,
.v-pagination .active {
background-color: var(--halo-accent-primary);
color: var(--halo-accent-primary-text);
}
/* ===== 面包屑 ===== */
.breadcrumb,
.v-breadcrumb {
color: var(--halo-text-secondary);
}
.breadcrumb a,
.v-breadcrumb a {
color: var(--halo-text-link);
}
/* ===== Toast / 通知 ===== */
.v-toast,
.toast-notification,
[class*="toast"] {
background-color: var(--halo-bg-card);
color: var(--halo-text-primary);
border-color: var(--halo-border-base);
}
/* ===== 步骤条 ===== */
.v-steps,
[class*="steps"] {
color: var(--halo-text-secondary);
}
/* ===== 开关 VSwitch ===== */
.v-switch-track {
background-color: var(--halo-bg-disabled);
}
.v-switch-track[aria-checked="true"] {
background-color: var(--halo-accent-primary);
}
}
+71
View File
@@ -0,0 +1,71 @@
/* ============================================================
Halo Dark Mode — 富文本编辑器覆盖
@halo-dev/richtext-editor + TipTap / ProseMirror
============================================================ */
[data-halo-theme="dark"] {
/* ===== ProseMirror / TipTap 编辑器主体 ===== */
.ProseMirror,
.tiptap,
[class*="editor-content"] {
color: var(--halo-text-primary);
background-color: var(--halo-bg-input);
}
/* ===== 编辑器菜单栏 ===== */
.editor-toolbar,
.editor-menubar,
[class*="editor-toolbar"],
[class*="editor-menubar"] {
background-color: var(--halo-bg-card);
border-color: var(--halo-border-base);
}
.editor-toolbar button,
.editor-menubar button {
color: var(--halo-text-secondary);
}
.editor-toolbar button:hover,
.editor-menubar button:hover,
.editor-toolbar button.is-active,
.editor-menubar button.is-active {
background-color: var(--halo-bg-hover);
color: var(--halo-text-primary);
}
/* ===== 代码块 ===== */
.ProseMirror pre,
.ProseMirror code,
.tiptap pre,
.tiptap code {
background-color: oklch(20% 0.015 250);
color: oklch(85% 0.03 160);
}
/* ===== Bubble menu / floating menu ===== */
.tippy-box,
.tiptap-bubble-menu,
.floating-menu {
background-color: var(--halo-bg-dropdown);
border-color: var(--halo-border-base);
color: var(--halo-text-primary);
}
/* ===== 编辑器占位符 ===== */
.ProseMirror p.is-editor-empty:first-child::before {
color: var(--halo-text-tertiary);
}
/* ===== 链接 ===== */
.ProseMirror a,
.tiptap a {
color: var(--halo-text-link);
}
/* ===== 选中文字 ===== */
.ProseMirror ::selection,
.tiptap ::selection {
background-color: oklch(30% 0.04 160 / 50%);
}
}
+89
View File
@@ -0,0 +1,89 @@
/* ============================================================
Halo Dark Mode — FormKit 表单组件覆盖
覆盖输入框、选择器、开关、复选框等表单元素
============================================================ */
[data-halo-theme="dark"] {
/* ===== 输入框 ===== */
input[type="text"],
input[type="password"],
input[type="email"],
input[type="number"],
input[type="search"],
input[type="url"],
input[type="tel"],
textarea,
select,
.formkit-input,
[data-formkit-input] {
background-color: var(--halo-bg-input);
color: var(--halo-text-primary);
border-color: var(--halo-border-input);
}
input:focus,
textarea:focus,
select:focus,
.formkit-input:focus {
border-color: var(--halo-border-focus);
outline-color: var(--halo-border-focus);
}
input::placeholder,
textarea::placeholder,
.formkit-input::placeholder {
color: var(--halo-text-tertiary);
}
input:disabled,
textarea:disabled,
select:disabled {
background-color: var(--halo-bg-disabled);
color: var(--halo-text-tertiary);
}
/* ===== 复选框/单选框 ===== */
input[type="checkbox"],
input[type="radio"],
.formkit-checkbox,
.formkit-radio {
accent-color: var(--halo-accent-primary);
}
/* ===== FormKit 标签和帮助文本 ===== */
.formkit-label,
.formkit-legend {
color: var(--halo-text-secondary);
}
.formkit-help,
.formkit-message {
color: var(--halo-text-tertiary);
}
/* ===== FormKit 外层 ===== */
.formkit-outer {
color: var(--halo-text-primary);
}
/* ===== FormKit 前缀/后缀 ===== */
.formkit-prefix,
.formkit-suffix {
background-color: var(--halo-bg-hover);
color: var(--halo-text-secondary);
border-color: var(--halo-border-input);
}
/* ===== 选择器下拉 ===== */
select option {
background-color: var(--halo-bg-dropdown);
color: var(--halo-text-primary);
}
/* ===== 代码块 in forms ===== */
.formkit-code {
background-color: var(--halo-bg-hover);
color: var(--halo-text-primary);
border-color: var(--halo-border-base);
}
}
+62
View File
@@ -0,0 +1,62 @@
/* ============================================================
Halo Dark Mode — 核心布局覆盖
覆盖 BasicLayout.vue 的硬编码颜色
============================================================ */
[data-halo-theme="dark"] {
/* ===== 页面整体 ===== */
.layout {
background-color: var(--halo-bg-body);
}
/* ===== 侧边栏 ===== */
.sidebar {
background-color: var(--halo-bg-sidebar);
box-shadow: var(--halo-shadow-sm);
}
.sidebar__search {
background-color: var(--halo-search-bg);
color: var(--halo-search-placeholder);
}
.sidebar__search:hover {
color: var(--halo-search-text);
}
.sidebar__search-icon {
color: inherit;
}
.sidebar__search-shortcut {
color: var(--halo-text-tertiary);
}
.sidebar__logo {
filter: brightness(0.9) invert(0);
}
.sidebar__profile {
border-top-color: var(--halo-border-light);
}
/* ===== 内容区 ===== */
.main-content {
background-color: var(--halo-bg-content);
color: var(--halo-text-primary);
}
.main-content__footer-text {
color: var(--halo-text-tertiary);
}
.main-content__footer-link {
color: var(--halo-text-link);
}
/* ===== 页面标题 ===== */
.page-title,
.v-card-title {
color: var(--halo-text-primary);
}
}
+48
View File
@@ -0,0 +1,48 @@
/* ============================================================
Halo Dark Mode — 滚动条覆盖
OverlayScrollbars + 原生滚动条
============================================================ */
[data-halo-theme="dark"] {
/* ===== OverlayScrollbars ===== */
.os-scrollbar .os-scrollbar-handle {
background-color: var(--halo-scrollbar-thumb);
}
.os-scrollbar .os-scrollbar-track {
background-color: var(--halo-scrollbar-track);
}
.os-scrollbar .os-scrollbar-handle:hover {
background-color: oklch(45% 0.03 250);
}
/* ===== 原生滚动条(Firefox ===== */
* {
scrollbar-color: var(--halo-scrollbar-thumb) var(--halo-scrollbar-track);
scrollbar-width: thin;
}
/* ===== 原生滚动条(WebkitChrome/Edge/Safari ===== */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--halo-scrollbar-track);
}
::-webkit-scrollbar-thumb {
background: var(--halo-scrollbar-thumb);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: oklch(45% 0.03 250);
}
::-webkit-scrollbar-corner {
background: var(--halo-scrollbar-track);
}
}
+77
View File
@@ -0,0 +1,77 @@
/* ============================================================
Halo Dark Mode — Tailwind 工具类覆盖
覆盖 Tailwind 的颜色类和常见 UI 原子类
============================================================ */
[data-halo-theme="dark"] {
/* ===== 背景色 ===== */
.bg-white {
background-color: var(--halo-bg-card);
}
.bg-gray-50,
.bg-gray-100 {
background-color: var(--halo-bg-content);
}
.bg-gray-200 {
background-color: var(--halo-bg-hover);
}
/* ===== 文字色 ===== */
.text-gray-900,
.text-gray-800 {
color: var(--halo-text-primary);
}
.text-gray-700,
.text-gray-600,
.text-gray-500 {
color: var(--halo-text-secondary);
}
.text-gray-400,
.text-gray-300 {
color: var(--halo-text-tertiary);
}
/* ===== 边框 ===== */
.border,
.border-gray-200,
.border-gray-300 {
border-color: var(--halo-border-base);
}
.divide-y > :not([hidden]) ~ :not([hidden]),
.divide-x > :not([hidden]) ~ :not([hidden]) {
border-color: var(--halo-border-light);
}
/* ===== 阴影 ===== */
.shadow {
box-shadow: var(--halo-shadow-base);
}
.shadow-sm {
box-shadow: var(--halo-shadow-sm);
}
.shadow-lg,
.shadow-xl {
box-shadow: var(--halo-shadow-lg);
}
/* ===== 链接 ===== */
a:not([class*="text-"]),
.hover\:text-gray-900:hover,
.hover\:text-gray-800:hover,
.hover\:text-gray-700:hover {
color: var(--halo-text-link);
}
/* ===== 通用悬停 ===== */
.hover\:bg-gray-50:hover,
.hover\:bg-gray-100:hover {
background-color: var(--halo-bg-hover);
}
}
+116
View File
@@ -0,0 +1,116 @@
/* ============================================================
Halo Dark Mode — CSS Variables
配色策略: OKLCH 颜色空间 | 中性色微蓝着色调 | 亮度层次替代阴影
============================================================ */
/* ===== 浅色模式(Halo 默认,此处定义为显式回退) ===== */
:root {
--halo-bg-body: oklch(97% 0.005 250);
--halo-bg-sidebar: oklch(100% 0 0);
--halo-bg-content: oklch(97% 0.005 250);
--halo-bg-card: oklch(100% 0 0);
--halo-bg-input: oklch(100% 0 0);
--halo-bg-hover: oklch(95% 0.01 250);
--halo-bg-active: oklch(90% 0.02 160);
--halo-bg-disabled: oklch(95% 0.005 250);
--halo-bg-tooltip: oklch(20% 0.01 250);
--halo-bg-modal: oklch(0% 0 0 / 60%);
--halo-bg-dropdown: oklch(100% 0 0);
--halo-text-primary: oklch(20% 0.01 250);
--halo-text-secondary: oklch(45% 0.01 250);
--halo-text-tertiary: oklch(60% 0.01 250);
--halo-text-link: oklch(45% 0.15 160);
--halo-text-inverse: oklch(100% 0 0);
--halo-border-base: oklch(88% 0.01 250);
--halo-border-light: oklch(93% 0.005 250);
--halo-border-input: oklch(80% 0.01 250);
--halo-border-focus: oklch(55% 0.15 160);
--halo-accent-primary: oklch(55% 0.14 160);
--halo-accent-primary-hover: oklch(48% 0.15 160);
--halo-accent-primary-text: oklch(100% 0 0);
--halo-accent-danger: oklch(45% 0.18 25);
--halo-accent-danger-hover: oklch(40% 0.19 25);
--halo-accent-success: oklch(50% 0.16 150);
--halo-accent-warning: oklch(60% 0.16 85);
--halo-shadow-sm: 0 1px 2px oklch(0% 0 0 / 6%);
--halo-shadow-base: 0 2px 8px oklch(0% 0 0 / 10%);
--halo-shadow-lg: 0 4px 16px oklch(0% 0 0 / 14%);
--halo-scrollbar-thumb: oklch(80% 0.01 250);
--halo-scrollbar-track: oklch(95% 0.005 250);
--halo-search-bg: oklch(95% 0.01 250);
--halo-search-text: oklch(45% 0.01 250);
--halo-search-placeholder: oklch(60% 0.01 250);
--halo-menu-item-hover: oklch(93% 0.02 160);
--halo-menu-item-active: oklch(88% 0.03 160);
--halo-menu-group-title: oklch(55% 0.01 250);
--halo-table-header-bg: oklch(96% 0.005 250);
--halo-table-row-hover: oklch(93% 0.015 250);
--halo-table-border: oklch(88% 0.01 250);
--halo-tag-bg: oklch(90% 0.04 160);
--halo-tag-text: oklch(40% 0.1 160);
}
/* ===== 深色模式 ===== */
[data-halo-theme="dark"] {
--halo-bg-body: oklch(14% 0.01 250);
--halo-bg-sidebar: oklch(16% 0.015 250);
--halo-bg-content: oklch(14% 0.01 250);
--halo-bg-card: oklch(18% 0.015 250);
--halo-bg-input: oklch(20% 0.015 250);
--halo-bg-hover: oklch(24% 0.02 250);
--halo-bg-active: oklch(28% 0.03 160);
--halo-bg-disabled: oklch(16% 0.005 250);
--halo-bg-tooltip: oklch(25% 0.01 250);
--halo-bg-modal: oklch(0% 0 0 / 60%);
--halo-bg-dropdown: oklch(20% 0.015 250);
--halo-text-primary: oklch(92% 0.005 250);
--halo-text-secondary: oklch(70% 0.01 250);
--halo-text-tertiary: oklch(50% 0.01 250);
--halo-text-link: oklch(72% 0.14 160);
--halo-text-inverse: oklch(14% 0.01 250);
--halo-border-base: oklch(28% 0.015 250);
--halo-border-light: oklch(22% 0.01 250);
--halo-border-input: oklch(30% 0.015 250);
--halo-border-focus: oklch(65% 0.14 160);
--halo-accent-primary: oklch(60% 0.13 160);
--halo-accent-primary-hover: oklch(66% 0.12 160);
--halo-accent-primary-text: oklch(14% 0.02 160);
--halo-accent-danger: oklch(50% 0.18 25);
--halo-accent-danger-hover: oklch(56% 0.17 25);
--halo-accent-success: oklch(58% 0.16 150);
--halo-accent-warning: oklch(65% 0.16 85);
--halo-shadow-sm: 0 1px 2px oklch(0% 0 0 / 30%);
--halo-shadow-base: 0 2px 8px oklch(0% 0 0 / 40%);
--halo-shadow-lg: 0 4px 16px oklch(0% 0 0 / 50%);
--halo-scrollbar-thumb: oklch(35% 0.02 250);
--halo-scrollbar-track: oklch(18% 0.01 250);
--halo-search-bg: oklch(20% 0.015 250);
--halo-search-text: oklch(70% 0.01 250);
--halo-search-placeholder: oklch(50% 0.01 250);
--halo-menu-item-hover: oklch(22% 0.02 160);
--halo-menu-item-active: oklch(28% 0.04 160);
--halo-menu-group-title: oklch(55% 0.01 250);
--halo-table-header-bg: oklch(18% 0.01 250);
--halo-table-row-hover: oklch(22% 0.015 250);
--halo-table-border: oklch(26% 0.015 250);
--halo-tag-bg: oklch(22% 0.03 160);
--halo-tag-text: oklch(80% 0.08 160);
}
+125
View File
@@ -0,0 +1,125 @@
<script setup lang="ts">
import type { ThemeMode } from '../composables/useDarkMode'
import { useDarkMode } from '../composables/useDarkMode'
import { computed } from 'vue'
const { theme, isDark, setTheme } = useDarkMode()
const currentEffectiveMode = computed(() => {
if (theme.value === 'auto') {
return isDark.value ? '深色(跟随系统)' : '浅色(跟随系统)'
}
return theme.value === 'dark' ? '深色' : '浅色'
})
const modeOptions: { value: ThemeMode; label: string; description: string }[] = [
{ value: 'light', label: '浅色', description: '始终使用浅色模式' },
{ value: 'dark', label: '深色', description: '始终使用深色模式' },
{ value: 'auto', label: '跟随系统', description: '根据系统设置自动切换' },
]
</script>
<template>
<div class="dark-mode-settings">
<div class="dark-mode-settings__header">
<h1 class="dark-mode-settings__title">深色模式设置</h1>
<p class="dark-mode-settings__desc">选择后台管理面板的显示模式</p>
</div>
<div class="dark-mode-settings__card">
<div class="dark-mode-settings__current">
当前生效<strong>{{ currentEffectiveMode }}</strong>
</div>
<div class="dark-mode-settings__options">
<div
v-for="option in modeOptions"
:key="option.value"
class="dark-mode-settings__option"
:class="{ 'is-active': theme === option.value }"
@click="setTheme(option.value)"
>
<div class="dark-mode-settings__option-label">{{ option.label }}</div>
<div class="dark-mode-settings__option-desc">{{ option.description }}</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.dark-mode-settings {
max-width: 640px;
padding: 1.5rem;
}
.dark-mode-settings__header {
margin-bottom: 1.5rem;
}
.dark-mode-settings__title {
font-size: 1.25rem;
font-weight: 600;
color: var(--halo-text-primary);
margin-bottom: 0.25rem;
}
.dark-mode-settings__desc {
font-size: 0.875rem;
color: var(--halo-text-secondary);
}
.dark-mode-settings__card {
padding: 1.25rem;
background-color: var(--halo-bg-card);
border: 1px solid var(--halo-border-base);
border-radius: 0.25rem;
}
.dark-mode-settings__current {
font-size: 0.875rem;
color: var(--halo-text-secondary);
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--halo-border-light);
}
.dark-mode-settings__current strong {
color: var(--halo-accent-primary);
}
.dark-mode-settings__options {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.dark-mode-settings__option {
padding: 0.75rem 1rem;
border-radius: 0.375rem;
cursor: pointer;
border: 1px solid var(--halo-border-base);
transition: background-color 0.15s, border-color 0.15s;
}
.dark-mode-settings__option:hover {
background-color: var(--halo-bg-hover);
}
.dark-mode-settings__option.is-active {
background-color: var(--halo-menu-item-active);
border-color: var(--halo-accent-primary);
}
.dark-mode-settings__option-label {
font-size: 0.9375rem;
font-weight: 500;
color: var(--halo-text-primary);
margin-bottom: 0.125rem;
}
.dark-mode-settings__option-desc {
font-size: 0.8125rem;
color: var(--halo-text-tertiary);
}
</style>
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": {
"@/*": ["./src/*"]
}
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.vitest.json"
}
]
}
+15
View File
@@ -0,0 +1,15 @@
{
"extends": "@tsconfig/node20/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*"
],
"compilerOptions": {
"composite": true,
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "./tsconfig.app.json",
"include": ["src/**/__tests__/*", "env.d.ts"],
"exclude": [],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo",
"lib": [],
"types": ["node", "jsdom"]
}
}
+25
View File
@@ -0,0 +1,25 @@
import { fileURLToPath, URL } from 'url'
import { viteConfig } from '@halo-dev/ui-plugin-bundler-kit'
import Icons from 'unplugin-icons/vite'
import { configDefaults } from 'vitest/config'
// For more info,
// please see https://github.com/halo-dev/halo/tree/main/ui/packages/ui-plugin-bundler-kit
export default viteConfig({
vite: {
plugins: [Icons({ compiler: 'vue3' })],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
// If you don't use Vitest, you can remove the following configuration
test: {
environment: 'jsdom',
exclude: [...configDefaults.exclude, 'e2e/**'],
root: fileURLToPath(new URL('./', import.meta.url)),
},
},
})