feat(theme): refine glass and transparency settings (#578)

This commit is contained in:
InfinityPacer
2026-07-23 14:06:46 +08:00
committed by GitHub
parent f7ef9dd116
commit 590755169f
8 changed files with 368 additions and 72 deletions
+22 -49
View File
@@ -88,6 +88,16 @@ function updateQuality(value: unknown) {
previewGlassSettings({ glassQuality: option.value })
}
/** 将当前草稿恢复为玻璃主题默认值并立即预览。 */
function resetSettings() {
draftAppearance.value = 'clear'
draftQuality.value = 'css'
previewGlassSettings({
glassAppearance: draftAppearance.value,
glassQuality: draftQuality.value,
})
}
/** 一次提交当前预览,持久化后关闭不会发生视觉回跳。 */
async function saveSettings() {
if (isSaving.value) return
@@ -111,19 +121,16 @@ onScopeDispose(cancelGlassPreview)
</script>
<template>
<VDialog v-if="visible" v-model="visible" max-width="28rem" scrollable :fullscreen="display.xs.value">
<VCard class="glass-settings-dialog">
<VCardItem class="glass-settings-dialog__header py-3">
<template #prepend>
<VAvatar color="primary" variant="tonal" rounded size="40" class="me-2">
<VIcon icon="mdi-blur-radial" size="22" />
</VAvatar>
</template>
<VDialog v-if="visible" v-model="visible" max-width="30rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard>
<VCardItem>
<VCardTitle>
<VIcon icon="mdi-blur-radial" class="me-2" />
{{ t('theme.glassSettings') }}
</VCardTitle>
<VDialogCloseBtn v-model="visible" />
</VCardItem>
<VDivider />
<VCardText class="glass-settings-dialog__body">
<section>
@@ -169,33 +176,20 @@ onScopeDispose(cancelGlassPreview)
</section>
</VCardText>
<VCardActions class="app-dialog-actions glass-settings-dialog__actions">
<VSpacer />
<VBtn
color="primary"
variant="flat"
prepend-icon="mdi-content-save"
class="px-5"
:loading="isSaving"
@click="saveSettings"
>
<VDivider />
<VCardText class="text-center">
<VBtn variant="outlined" prepend-icon="mdi-refresh" class="me-2" @click="resetSettings">
{{ t('common.reset') }}
</VBtn>
<VBtn color="primary" prepend-icon="mdi-content-save" :loading="isSaving" @click="saveSettings">
{{ t('common.save') }}
</VBtn>
</VCardActions>
</VCardText>
</VCard>
</VDialog>
</template>
<style scoped lang="scss">
.glass-settings-dialog {
overflow: hidden;
max-block-size: calc(100dvh - 2rem);
}
.glass-settings-dialog__header {
border-block-end: 1px solid rgba(var(--v-theme-on-surface), 0.08);
}
.glass-settings-dialog__body {
display: grid;
align-content: start;
@@ -260,25 +254,4 @@ onScopeDispose(cancelGlassPreview)
background-color: rgba(var(--v-theme-primary), 0.14) !important;
box-shadow: inset 0 0 0 1px rgba(var(--v-theme-primary), 0.38) !important;
}
.glass-settings-dialog__actions {
border-block-start: 1px solid rgba(var(--v-theme-on-surface), 0.08);
padding-block: 0.875rem;
padding-inline: 1rem;
}
@media (width <= 600px) {
.glass-settings-dialog {
block-size: 100dvh;
max-block-size: 100dvh;
}
.glass-settings-dialog__body {
padding-inline: 18px;
}
.glass-settings-dialog__actions {
padding-block-end: max(0.875rem, calc(env(safe-area-inset-bottom) + 0.75rem));
}
}
</style>
@@ -29,6 +29,7 @@ const emit = defineEmits<{
const visible = computed({
get: () => props.modelValue,
set: value => {
if (!value) cancelTransparencySettings()
emit('update:modelValue', value)
if (!value) emit('close')
},
@@ -38,6 +39,7 @@ const {
adjustTransparency,
backgroundBlur,
backgroundPosterOpacity,
cancelTransparencySettings,
currentPresetLevel,
onBackgroundBlurChange,
onBackgroundPosterOpacityChange,
@@ -45,10 +47,28 @@ const {
onGlassQualityChange,
onOpacityChange,
resetTransparencySettings,
saveTransparencySettings,
transparencyBlur,
transparencyGlassQuality,
transparencyOpacity,
} = useTransparencySettings()
/** 父级控制弹窗生命周期时,未保存关闭仍需恢复持久化设置。 */
watch(
() => props.modelValue,
(value, previous) => {
if (!value && previous) cancelTransparencySettings()
},
)
/** 保存当前透明度预览,随后关闭面板。 */
function saveSettings() {
saveTransparencySettings()
visible.value = false
}
// 弹窗的任意未保存销毁路径都应恢复持久化快照。
onScopeDispose(cancelTransparencySettings)
</script>
<template>
@@ -185,14 +205,11 @@ const {
</VCardText>
<VDivider />
<VCardText class="text-center">
<VBtn @click="resetTransparencySettings" variant="outlined" class="me-2">
<template #prepend>
<VIcon icon="mdi-refresh" />
</template>
{{ t('theme.transparencyReset') }}
<VBtn variant="outlined" prepend-icon="mdi-refresh" class="me-2" @click="resetTransparencySettings">
{{ t('common.reset') }}
</VBtn>
<VBtn @click="visible = false" color="primary">
{{ t('common.confirm') }}
<VBtn color="primary" prepend-icon="mdi-content-save" @click="saveSettings">
{{ t('common.save') }}
</VBtn>
</VCardText>
</VCard>
@@ -2,14 +2,18 @@ import { shallowMount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import GlassSettingsDialog from '@/components/dialog/GlassSettingsDialog.vue'
const slotStub = { template: '<div><slot /></div>' }
const mocks = vi.hoisted(() => ({
cancelGlassPreview: vi.fn(),
commitGlassPreview: vi.fn(),
previewGlassSettings: vi.fn(),
}))
vi.mock('@/composables/useThemeCustomizer', () => ({
cancelGlassPreview: mocks.cancelGlassPreview,
commitGlassPreview: vi.fn(),
previewGlassSettings: vi.fn(),
commitGlassPreview: mocks.commitGlassPreview,
previewGlassSettings: mocks.previewGlassSettings,
useThemeCustomizer: () => ({
settings: {
value: {
@@ -25,12 +29,14 @@ vi.mock('vue-i18n', () => ({
}))
vi.mock('vuetify', () => ({
useDisplay: () => ({ xs: { value: false } }),
useDisplay: () => ({ mdAndUp: { value: true } }),
}))
describe('GlassSettingsDialog', () => {
beforeEach(() => {
mocks.cancelGlassPreview.mockClear()
mocks.commitGlassPreview.mockClear()
mocks.previewGlassSettings.mockClear()
})
it('cancels an active preview when the parent closes the dialog', async () => {
@@ -45,4 +51,33 @@ describe('GlassSettingsDialog', () => {
expect(mocks.cancelGlassPreview).toHaveBeenCalledOnce()
})
it('resets the draft to the default glass settings without committing', async () => {
const wrapper = shallowMount(GlassSettingsDialog, {
global: {
stubs: {
VCard: slotStub,
VCardText: slotStub,
VBtn: {
emits: ['click'],
props: ['prependIcon'],
template: '<button :data-icon="prependIcon" @click="$emit(\'click\')"><slot /></button>',
},
VDialog: slotStub,
VDialogCloseBtn: true,
VDivider: true,
},
},
props: { modelValue: true },
})
const resetButton = wrapper.find('[data-icon="mdi-refresh"]')
await resetButton.trigger('click')
expect(mocks.previewGlassSettings).toHaveBeenCalledWith({
glassAppearance: 'clear',
glassQuality: 'css',
})
expect(mocks.commitGlassPreview).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,87 @@
import { shallowMount } from '@vue/test-utils'
import { ref } from 'vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import TransparencySettingsDialog from '@/components/dialog/TransparencySettingsDialog.vue'
const slotStub = { template: '<div><slot /></div>' }
const mocks = vi.hoisted(() => ({
cancelTransparencySettings: vi.fn(),
saveTransparencySettings: vi.fn(),
}))
vi.mock('@/composables/useTransparencySettings', () => ({
useTransparencySettings: () => ({
adjustTransparency: vi.fn(),
backgroundBlur: ref(16),
backgroundPosterOpacity: ref(0),
cancelTransparencySettings: mocks.cancelTransparencySettings,
currentPresetLevel: ref('medium'),
onBackgroundBlurChange: vi.fn(),
onBackgroundPosterOpacityChange: vi.fn(),
onBlurChange: vi.fn(),
onGlassQualityChange: vi.fn(),
onOpacityChange: vi.fn(),
resetTransparencySettings: vi.fn(),
saveTransparencySettings: mocks.saveTransparencySettings,
transparencyBlur: ref(10),
transparencyGlassQuality: ref('lightweight'),
transparencyOpacity: ref(0.3),
}),
}))
vi.mock('vue-i18n', () => ({
useI18n: () => ({ t: (key: string) => key }),
}))
vi.mock('vuetify', () => ({
useDisplay: () => ({ mdAndUp: { value: true } }),
}))
describe('TransparencySettingsDialog', () => {
beforeEach(() => {
mocks.cancelTransparencySettings.mockClear()
mocks.saveTransparencySettings.mockClear()
})
it('cancels an active preview when the parent closes the dialog', async () => {
const wrapper = shallowMount(TransparencySettingsDialog, {
global: {
stubs: { VDialogCloseBtn: true, VSlider: true },
},
props: { modelValue: true },
})
await wrapper.setProps({ modelValue: false })
expect(mocks.cancelTransparencySettings).toHaveBeenCalledOnce()
})
it('persists the preview only from the save action', async () => {
const wrapper = shallowMount(TransparencySettingsDialog, {
global: {
stubs: {
VCard: slotStub,
VCardText: slotStub,
VBtn: {
emits: ['click'],
props: ['prependIcon'],
template: '<button :data-icon="prependIcon" @click="$emit(\'click\')"><slot /></button>',
},
VDialog: slotStub,
VDialogCloseBtn: true,
VDivider: true,
VSlider: true,
},
},
props: { modelValue: true },
})
const saveButton = wrapper.find('[data-icon="mdi-content-save"]')
expect(mocks.saveTransparencySettings).not.toHaveBeenCalled()
await saveButton.trigger('click')
expect(mocks.saveTransparencySettings).toHaveBeenCalledOnce()
})
})