refactor(frontend): extract pool header and theme toggle

This commit is contained in:
elky
2026-06-30 17:01:39 +08:00
parent 9f138d09e6
commit d7411a3104
10 changed files with 620 additions and 499 deletions
@@ -0,0 +1,54 @@
<template>
<button
type="button"
:class="buttonClass"
:title="title"
:aria-label="title"
@click="toggleDarkMode"
>
<SunMoon
v-if="themeMode === 'system'"
:class="iconClass"
/>
<SunMedium
v-else-if="themeMode === 'light'"
:class="iconClass"
/>
<Moon
v-else
:class="iconClass"
/>
</button>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Moon, SunMedium, SunMoon } from 'lucide-vue-next'
import { useDarkMode } from '@/composables/useDarkMode'
import { useI18n } from '@/i18n'
const props = withDefaults(defineProps<{
size?: 'sm' | 'md'
class?: string
}>(), {
size: 'md',
class: '',
})
const { themeMode, toggleDarkMode } = useDarkMode()
const { t } = useI18n()
const title = computed(() => {
if (themeMode.value === 'system') return t('theme.system')
if (themeMode.value === 'dark') return t('theme.dark')
return t('theme.light')
})
const buttonClass = computed(() => [
'flex items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition',
props.size === 'sm' ? 'h-8 w-8' : 'h-9 w-9',
props.class,
].filter(Boolean).join(' '))
const iconClass = computed(() => props.size === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4')
</script>
@@ -0,0 +1,39 @@
import { describe, expect, it } from 'vitest'
import { createApp, nextTick } from 'vue'
import ThemeModeButton from '@/components/common/ThemeModeButton.vue'
import { useDarkMode } from '@/composables/useDarkMode'
import { createI18n } from '@/i18n'
describe('ThemeModeButton', () => {
it('cycles the shared theme mode from the single button entry point', async () => {
const { setThemeMode } = useDarkMode()
setThemeMode('system')
await nextTick()
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp(ThemeModeButton)
app.use(createI18n())
app.mount(root)
const button = root.querySelector('button')
expect(button).toBeTruthy()
expect(button?.getAttribute('title')).toBe('跟随系统')
button?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
expect(button?.getAttribute('title')).toBe('浅色模式')
expect(localStorage.getItem('theme')).toBe('light')
button?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
expect(button?.getAttribute('title')).toBe('深色模式')
expect(localStorage.getItem('theme')).toBe('dark')
app.unmount()
root.remove()
})
})
+1
View File
@@ -8,6 +8,7 @@ export { default as EmptyState } from './EmptyState.vue'
export { default as AlertDialog } from './AlertDialog.vue'
export { default as LoadingState } from './LoadingState.vue'
export { default as StripePaymentDialog } from './StripePaymentDialog.vue'
export { default as ThemeModeButton } from './ThemeModeButton.vue'
// 表单组件
export { default as MultiSelect } from './MultiSelect.vue'