style: 优化按钮禁用态与主题入口并将设置移至侧栏底部
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, h, ref } from 'vue'
|
||||
import { NButton, NDropdown, NTooltip } from 'naive-ui'
|
||||
import { Monitor, Moon, Sun } from '@lucide/vue'
|
||||
import type { ThemePreference } from '../../composables/useTheme'
|
||||
|
||||
/** 图标表示用户偏好;保留系统跟随选项,不以简单二态切换覆盖偏好。 */
|
||||
const preference = defineModel<ThemePreference>({ required: true })
|
||||
const open = ref(false)
|
||||
const modes = [
|
||||
{ key: 'light', label: '浅色模式', icon: Sun },
|
||||
{ key: 'dark', label: '暗黑模式', icon: Moon },
|
||||
{ key: 'system', label: '跟随系统', icon: Monitor }
|
||||
] as const
|
||||
const current = computed(() => modes.find(mode => mode.key === preference.value) ?? modes[2])
|
||||
const options = modes.map(mode => ({ ...mode, icon: () => h(mode.icon, { size: 16 }) }))
|
||||
|
||||
/** Dropdown 的键类型较宽,只接收受支持的主题值。 */
|
||||
function selectTheme(key: string | number) {
|
||||
if (key === 'light' || key === 'dark' || key === 'system') preference.value = key
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDropdown v-model:show="open" trigger="click" :value="preference" :options="options" @select="selectTheme">
|
||||
<NTooltip :disabled="open">
|
||||
<template #trigger>
|
||||
<NButton
|
||||
quaternary
|
||||
circle
|
||||
class="theme-toggle"
|
||||
:aria-label="`主题模式:${current.label}`"
|
||||
aria-haspopup="menu"
|
||||
:aria-expanded="open"
|
||||
>
|
||||
<component :is="current.icon" :size="18" aria-hidden="true" />
|
||||
</NButton>
|
||||
</template>
|
||||
{{ current.label }} · 切换主题
|
||||
</NTooltip>
|
||||
</NDropdown>
|
||||
</template>
|
||||
@@ -16,6 +16,48 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('全站平面主题', () => {
|
||||
it.each([false, true])('模式 %s 的绿色按钮使用白字,禁用后灰底灰字且不可提交', dark => {
|
||||
localStorage.setItem('drama-studio-theme', dark ? 'dark' : 'light')
|
||||
vi.stubGlobal('matchMedia', () => ({ matches: dark, addEventListener() {}, removeEventListener() {} }))
|
||||
const click = vi.fn<() => void>()
|
||||
wrapper = mount({
|
||||
setup() {
|
||||
const { theme, overrides } = useTheme()
|
||||
return () =>
|
||||
h(NConfigProvider, { theme: theme.value, themeOverrides: overrides.value }, () =>
|
||||
(['primary', 'success'] as const).flatMap(type => [
|
||||
h(NButton, { type, class: `${type}-button`, onClick: click }, () => '提交'),
|
||||
h(
|
||||
NButton,
|
||||
{ type, disabled: true, class: `${type}-disabled`, onClick: click },
|
||||
() => '提交'
|
||||
)
|
||||
])
|
||||
)
|
||||
}
|
||||
})
|
||||
for (const type of ['primary', 'success']) {
|
||||
const active = wrapper.get<HTMLButtonElement>(`.${type}-button`).element
|
||||
const disabled = wrapper.get<HTMLButtonElement>(`.${type}-disabled`).element
|
||||
for (const property of [
|
||||
'--n-text-color',
|
||||
'--n-text-color-hover',
|
||||
'--n-text-color-pressed',
|
||||
'--n-text-color-focus'
|
||||
]) {
|
||||
expect(active.style.getPropertyValue(property)).toBe('#ffffff')
|
||||
}
|
||||
expect(active.style.getPropertyValue('--n-color')).toBe('#078640')
|
||||
expect(disabled.style.getPropertyValue('--n-color-disabled')).toBe(dark ? '#303030' : '#e4e4e4')
|
||||
expect(disabled.style.getPropertyValue('--n-text-color-disabled')).toBe(dark ? '#808080' : '#8c8c8c')
|
||||
expect(disabled.disabled).toBe(true)
|
||||
disabled.click()
|
||||
expect(click).not.toHaveBeenCalled()
|
||||
}
|
||||
wrapper.get<HTMLButtonElement>('.primary-button').element.click()
|
||||
expect(click).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it.each([false, true])('模式 %s 的真实控件无静态描边、使用直角,并保留聚焦与错误反馈', dark => {
|
||||
localStorage.setItem('drama-studio-theme', dark ? 'dark' : 'light')
|
||||
vi.stubGlobal('matchMedia', () => ({ matches: dark, addEventListener() {}, removeEventListener() {} }))
|
||||
|
||||
@@ -92,7 +92,7 @@ describe('管理后台组件边界', () => {
|
||||
expect(wrapper.get('.workspace-split').findAll(':scope > *')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('真实主题选择器切换暗色,侧栏保持全部七个工作流入口', async () => {
|
||||
it('图标主题菜单保留三种模式,设置入口位于侧栏底部并支持折叠', async () => {
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
@@ -108,19 +108,56 @@ describe('管理后台组件边界', () => {
|
||||
await flushPromises()
|
||||
expect(wrapper.findAll('.n-menu a')).toHaveLength(8)
|
||||
expect(wrapper.get('#main-content .workspace-page').text()).toBe('镜头详情')
|
||||
await wrapper.get('.theme-select .n-base-selection').trigger('click')
|
||||
await flushPromises()
|
||||
const darkOption = [...document.querySelectorAll<HTMLElement>('.n-base-select-option')].find(
|
||||
item => item.textContent?.trim() === '暗黑模式'
|
||||
)
|
||||
expect(darkOption).toBeDefined()
|
||||
darkOption!.click()
|
||||
await flushPromises()
|
||||
expect(document.documentElement.dataset.theme).toBe('dark')
|
||||
expect(wrapper.getComponent(NConfigProvider).props('theme')?.name).toBe('dark')
|
||||
expect(localStorage.getItem('drama-studio-theme')).toBe('dark')
|
||||
expect(wrapper.get('.theme-toggle').text()).toBe('')
|
||||
expect(wrapper.get('.theme-toggle').attributes('aria-haspopup')).toBe('menu')
|
||||
expect(wrapper.get('.theme-toggle svg').classes()).toContain('lucide-monitor')
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
for (const [key, label, icon] of [
|
||||
['dark', '暗黑模式', 'moon'],
|
||||
['light', '浅色模式', 'sun'],
|
||||
['system', '跟随系统', 'monitor']
|
||||
] as const) {
|
||||
await wrapper.get('.theme-toggle').trigger('click')
|
||||
await flushPromises()
|
||||
const option = [...document.querySelectorAll<HTMLElement>('.n-dropdown-option-body')].find(
|
||||
item => item.textContent?.trim() === label
|
||||
)
|
||||
expect(option).toBeDefined()
|
||||
option!.click()
|
||||
await flushPromises()
|
||||
expect(localStorage.getItem('drama-studio-theme')).toBe(key)
|
||||
expect(wrapper.get('.theme-toggle svg').classes()).toContain(`lucide-${icon}`)
|
||||
expect(wrapper.get('.theme-toggle').attributes('aria-label')).toBe(`主题模式:${label}`)
|
||||
const expectedTheme = key === 'system' ? systemTheme : key
|
||||
expect(document.documentElement.dataset.theme).toBe(expectedTheme)
|
||||
expect(wrapper.getComponent(NConfigProvider).props('theme')?.name ?? 'light').toBe(expectedTheme)
|
||||
}
|
||||
expect(wrapper.find('.admin-topbar [aria-label="后端连接"]').exists()).toBe(false)
|
||||
expect(wrapper.find('.admin-nav-scroll [aria-label="后端连接"]').exists()).toBe(false)
|
||||
expect(wrapper.get('.admin-sider-footer [aria-label="后端连接"]').text()).toBe('后端连接')
|
||||
await wrapper.get('[aria-label="折叠侧栏"]').trigger('click')
|
||||
expect(wrapper.find('[aria-label="展开侧栏"]').exists()).toBe(true)
|
||||
const settings = wrapper.get('.admin-sider-footer [aria-label="后端连接"]')
|
||||
expect(settings.text()).toBe('')
|
||||
await settings.trigger('click')
|
||||
await flushPromises()
|
||||
expect(document.querySelector('[role="dialog"][aria-label="后端连接"]')).not.toBeNull()
|
||||
expect(wrapper.findAll('.admin-nav-scroll .n-menu a')).toHaveLength(8)
|
||||
})
|
||||
|
||||
it('侧栏菜单独立滚动,页脚不参与滚动且窄屏不超出视口底部', () => {
|
||||
const css = readFileSync('src/admin.css', 'utf8')
|
||||
expect(css).toMatch(
|
||||
/\.admin-sider-content\s*\{[^}]*height:\s*100%;[^}]*min-height:\s*0;[^}]*overflow:\s*hidden/
|
||||
)
|
||||
expect(css).toMatch(/\.admin-nav-scroll\.n-scrollbar\s*\{[^}]*flex:\s*1;[^}]*min-height:\s*0/)
|
||||
expect(css).toMatch(/\.admin-sider-footer\s*\{[^}]*flex-shrink:\s*0/)
|
||||
expect(css).toMatch(/\.admin-sider:not\([^)]*\)\s*\{[^}]*bottom:\s*0;[^}]*height:\s*auto/)
|
||||
for (const path of ['storyboard/StoryboardPage.vue', 'production/ProductionPage.vue']) {
|
||||
const source = readFileSync(`src/features/${path}`, 'utf8')
|
||||
expect(source).not.toContain('<span>剧集</span')
|
||||
expect(source).toMatch(/aria-label="选择(?:分镜|生产)剧集"/)
|
||||
}
|
||||
})
|
||||
|
||||
it('长弹窗使用内部滚动,提交期间禁止遮罩和 Esc 关闭', async () => {
|
||||
|
||||
Reference in New Issue
Block a user