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:
@@ -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
@@ -0,0 +1,2 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="unplugin-icons/types/vue" />
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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"
|
||||
}
|
||||
Generated
+4486
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 |
@@ -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>
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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: {},
|
||||
})
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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%);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/* ===== 原生滚动条(Webkit:Chrome/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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.vitest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user