mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-21 07:33:49 +08:00
feat(theme): 添加阴影级别支持并优化主题定制器的阴影设置
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
themeCustomizerPrimaryColors,
|
||||
themeCustomizerShadowLevels,
|
||||
useThemeCustomizer,
|
||||
type ThemeCustomizerLayout,
|
||||
type ThemeCustomizerRadius,
|
||||
@@ -86,62 +87,35 @@ const skinOptions = computed<Array<{ title: string; value: ThemeCustomizerSkin }
|
||||
{ title: t('theme.customizer.skinBordered'), value: 'bordered' },
|
||||
])
|
||||
|
||||
const shadowOptions = computed<
|
||||
Array<{
|
||||
title: string
|
||||
value: ThemeCustomizerShadow
|
||||
}>
|
||||
>(() => [
|
||||
{
|
||||
title: t('theme.customizer.shadowNone'),
|
||||
value: 'none',
|
||||
},
|
||||
{
|
||||
title: t('theme.customizer.shadowLow'),
|
||||
value: 'low',
|
||||
},
|
||||
{
|
||||
title: t('theme.customizer.shadowMedium'),
|
||||
value: 'medium',
|
||||
},
|
||||
{
|
||||
title: t('theme.customizer.shadowHigh'),
|
||||
value: 'high',
|
||||
},
|
||||
])
|
||||
// 当前阴影滑杆数值,界面使用 number,主题设置继续存储 Vuetify elevation 字符串档位。
|
||||
const shadowSliderValue = computed(() => Number(settings.value.shadow))
|
||||
|
||||
const radiusOptions = computed<
|
||||
Array<{
|
||||
previewRadius: string
|
||||
title: string
|
||||
value: ThemeCustomizerRadius
|
||||
}>
|
||||
>(() => [
|
||||
{
|
||||
previewRadius: '4px',
|
||||
title: t('theme.customizer.radiusNone'),
|
||||
value: 'none',
|
||||
},
|
||||
{
|
||||
title: t('theme.customizer.radiusSmall'),
|
||||
value: 'small',
|
||||
},
|
||||
{
|
||||
previewRadius: '8px',
|
||||
title: t('theme.customizer.radiusDefault'),
|
||||
value: 'default',
|
||||
},
|
||||
{
|
||||
previewRadius: '12px',
|
||||
title: t('theme.customizer.radiusLarge'),
|
||||
value: 'large',
|
||||
},
|
||||
{
|
||||
previewRadius: '16px',
|
||||
title: t('theme.customizer.radiusExtra'),
|
||||
value: 'extra',
|
||||
},
|
||||
{
|
||||
previewRadius: '24px',
|
||||
title: t('theme.customizer.radiusHuge'),
|
||||
value: 'huge',
|
||||
},
|
||||
])
|
||||
|
||||
const layoutOptions = computed<Array<{ icon: string; title: string; value: ThemeCustomizerLayout }>>(() => [
|
||||
@@ -156,7 +130,7 @@ const hasAppModeCustomization = computed(() => {
|
||||
return (
|
||||
settings.value.primaryColor !== defaultPrimaryColor ||
|
||||
settings.value.radius !== 'default' ||
|
||||
settings.value.shadow !== 'none' ||
|
||||
settings.value.shadow !== '0' ||
|
||||
settings.value.skin !== 'default' ||
|
||||
settings.value.theme !== 'auto'
|
||||
)
|
||||
@@ -189,6 +163,19 @@ function handleLayoutChange(layout: ThemeCustomizerLayout) {
|
||||
setLayout(layout)
|
||||
}
|
||||
|
||||
// 将 Vuetify 滑杆的数字步进写回字符串型 elevation 档位。
|
||||
function handleShadowSliderChange(value: unknown) {
|
||||
const rawValue = Array.isArray(value) ? value[0] : value
|
||||
const numericValue = Number(rawValue)
|
||||
|
||||
if (!Number.isFinite(numericValue)) return
|
||||
|
||||
const clampedValue = Math.min(24, Math.max(0, Math.round(numericValue)))
|
||||
const shadow = String(clampedValue) as ThemeCustomizerShadow
|
||||
|
||||
if (themeCustomizerShadowLevels.includes(shadow)) setShadow(shadow)
|
||||
}
|
||||
|
||||
async function handleResetSettings() {
|
||||
if (!appMode.value) {
|
||||
await resetSettings()
|
||||
@@ -199,7 +186,7 @@ async function handleResetSettings() {
|
||||
// App 模式共享定制器,但保留桌面导航相关偏好,只重置 App 侧可调整的外观设置。
|
||||
await setPrimaryColor(defaultPrimaryColor)
|
||||
await setRadius('default')
|
||||
await setShadow('none')
|
||||
await setShadow('0')
|
||||
await setSkin('default')
|
||||
await setTheme('auto')
|
||||
}
|
||||
@@ -320,7 +307,7 @@ async function handleResetSettings() {
|
||||
>
|
||||
<span
|
||||
class="theme-customizer-radius-scene"
|
||||
:style="{ '--theme-customizer-radius-preview': radius.previewRadius }"
|
||||
:class="`theme-customizer-radius-scene--${radius.value}`"
|
||||
>
|
||||
<span class="theme-customizer-radius-scene__card">
|
||||
<span class="theme-customizer-radius-scene__badge" />
|
||||
@@ -335,29 +322,41 @@ async function handleResetSettings() {
|
||||
<VDivider class="mt-7" />
|
||||
|
||||
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.shadow') }}</h3>
|
||||
<div class="theme-customizer-preview-grid theme-customizer-preview-grid--shadow">
|
||||
<div
|
||||
v-for="shadow in shadowOptions"
|
||||
:key="shadow.value"
|
||||
class="theme-customizer-preview-option"
|
||||
:class="{ 'is-active': settings.shadow === shadow.value }"
|
||||
@click="setShadow(shadow.value)"
|
||||
>
|
||||
<span class="theme-customizer-shadow-scene" :class="`theme-customizer-shadow-scene--${shadow.value}`">
|
||||
<span class="theme-customizer-shadow-scene__panel">
|
||||
<span class="theme-customizer-shadow-scene__panel-line" />
|
||||
<span
|
||||
class="theme-customizer-shadow-scene__panel-line theme-customizer-shadow-scene__panel-line--short"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span class="theme-customizer-shadow-scene__card">
|
||||
<span class="theme-customizer-shadow-scene__badge" />
|
||||
<span class="theme-customizer-shadow-scene__line theme-customizer-shadow-scene__line--short" />
|
||||
<span class="theme-customizer-shadow-scene__line" />
|
||||
</span>
|
||||
<div class="theme-customizer-shadow-slider">
|
||||
<div class="theme-customizer-shadow-slider__header">
|
||||
<span>{{ t('theme.customizer.shadowLevel', { level: settings.shadow }) }}</span>
|
||||
<span>0 - 24</span>
|
||||
</div>
|
||||
<div class="theme-customizer-shadow-slider__control">
|
||||
<span
|
||||
class="theme-customizer-shadow-slider__sample"
|
||||
:style="{ boxShadow: `var(--app-elevation-${settings.shadow})` }"
|
||||
>
|
||||
<span class="theme-customizer-shadow-slider__sample-accent" />
|
||||
<span class="theme-customizer-shadow-slider__sample-line" />
|
||||
<span class="theme-customizer-shadow-slider__sample-line theme-customizer-shadow-slider__sample-line--short" />
|
||||
</span>
|
||||
<span>{{ shadow.title }}</span>
|
||||
<VSlider
|
||||
:model-value="shadowSliderValue"
|
||||
:aria-label="t('theme.customizer.shadow')"
|
||||
:max="24"
|
||||
:min="0"
|
||||
:step="1"
|
||||
color="primary"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
show-ticks="always"
|
||||
thumb-label
|
||||
tick-size="2"
|
||||
@update:model-value="handleShadowSliderChange"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="theme-customizer-shadow-slider__scale"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span>0</span>
|
||||
<span>24</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -626,10 +625,6 @@ async function handleResetSettings() {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.theme-customizer-preview-grid--shadow {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.theme-customizer-preview-grid--radius {
|
||||
grid-template-columns: repeat(auto-fit, minmax(92px, 1fr));
|
||||
}
|
||||
@@ -646,8 +641,7 @@ async function handleResetSettings() {
|
||||
box-shadow: none !important;
|
||||
|
||||
.theme-customizer-mini-layout,
|
||||
.theme-customizer-radius-scene,
|
||||
.theme-customizer-shadow-scene {
|
||||
.theme-customizer-radius-scene {
|
||||
border-width: 2px;
|
||||
border-color: rgb(var(--v-theme-primary));
|
||||
background: rgba(var(--v-theme-primary), 0.04);
|
||||
@@ -747,6 +741,24 @@ async function handleResetSettings() {
|
||||
block-size: 90px;
|
||||
inline-size: 100%;
|
||||
min-inline-size: 0;
|
||||
|
||||
--theme-customizer-preview-radius: var(--app-vuetify-rounded);
|
||||
}
|
||||
|
||||
.theme-customizer-radius-scene--none {
|
||||
--theme-customizer-preview-radius: var(--app-vuetify-rounded-0);
|
||||
}
|
||||
|
||||
.theme-customizer-radius-scene--small {
|
||||
--theme-customizer-preview-radius: var(--app-vuetify-rounded-sm);
|
||||
}
|
||||
|
||||
.theme-customizer-radius-scene--large {
|
||||
--theme-customizer-preview-radius: var(--app-vuetify-rounded-lg);
|
||||
}
|
||||
|
||||
.theme-customizer-radius-scene--extra {
|
||||
--theme-customizer-preview-radius: var(--app-vuetify-rounded-xl);
|
||||
}
|
||||
|
||||
.theme-customizer-radius-scene__card {
|
||||
@@ -754,7 +766,7 @@ async function handleResetSettings() {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
border-radius: var(--theme-customizer-radius-preview);
|
||||
border-radius: var(--theme-customizer-preview-radius);
|
||||
background: rgb(var(--v-theme-surface));
|
||||
gap: 8px;
|
||||
inset: 16px;
|
||||
@@ -765,17 +777,18 @@ async function handleResetSettings() {
|
||||
.theme-customizer-radius-scene__badge,
|
||||
.theme-customizer-radius-scene__line {
|
||||
display: block;
|
||||
border-radius: 999px;
|
||||
background: rgba(var(--v-theme-on-surface), 0.1);
|
||||
}
|
||||
|
||||
.theme-customizer-radius-scene__badge {
|
||||
border-radius: var(--theme-customizer-preview-radius);
|
||||
block-size: 8px;
|
||||
inline-size: 42%;
|
||||
min-inline-size: 28px;
|
||||
}
|
||||
|
||||
.theme-customizer-radius-scene__line {
|
||||
border-radius: var(--theme-customizer-preview-radius);
|
||||
block-size: 7px;
|
||||
}
|
||||
|
||||
@@ -783,112 +796,89 @@ async function handleResetSettings() {
|
||||
inline-size: 66%;
|
||||
}
|
||||
|
||||
.theme-customizer-shadow-scene {
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
.theme-customizer-shadow-slider {
|
||||
padding: 16px 18px 12px;
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
|
||||
border-radius: 10px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(var(--v-theme-on-surface), 0.02), rgba(var(--v-theme-on-surface), 0.06)),
|
||||
rgb(var(--v-theme-surface));
|
||||
block-size: 110px;
|
||||
inline-size: 100%;
|
||||
min-inline-size: 0;
|
||||
border-radius: var(--app-vuetify-rounded-lg);
|
||||
background: rgba(var(--v-theme-surface), 0.72);
|
||||
}
|
||||
|
||||
.theme-customizer-shadow-scene__panel,
|
||||
.theme-customizer-shadow-scene__card {
|
||||
position: absolute;
|
||||
.theme-customizer-shadow-slider__header,
|
||||
.theme-customizer-shadow-slider__scale {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.theme-customizer-shadow-slider__header {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.3;
|
||||
margin-block-end: 14px;
|
||||
|
||||
> span:first-child {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-customizer-shadow-slider__control {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
grid-template-columns: 56px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.theme-customizer-shadow-slider__sample {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
border-radius: var(--app-vuetify-rounded);
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
background: rgb(var(--v-theme-surface));
|
||||
box-shadow: none;
|
||||
transition: box-shadow 0.18s ease;
|
||||
block-size: 42px;
|
||||
gap: 5px;
|
||||
inline-size: 42px;
|
||||
padding-block: 8px;
|
||||
padding-inline: 9px;
|
||||
}
|
||||
|
||||
.theme-customizer-shadow-scene__panel {
|
||||
padding: 12px;
|
||||
gap: 8px;
|
||||
inset-block-start: 16px;
|
||||
inset-inline: 14px;
|
||||
min-block-size: 54px;
|
||||
}
|
||||
|
||||
.theme-customizer-shadow-scene__card {
|
||||
gap: 8px;
|
||||
inset-block-end: 12px;
|
||||
inset-inline: 20px 16px;
|
||||
min-block-size: 46px;
|
||||
padding-block: 10px;
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
.theme-customizer-shadow-scene__panel-line,
|
||||
.theme-customizer-shadow-scene__line,
|
||||
.theme-customizer-shadow-scene__badge {
|
||||
.theme-customizer-shadow-slider__sample-accent,
|
||||
.theme-customizer-shadow-slider__sample-line {
|
||||
display: block;
|
||||
border-radius: 999px;
|
||||
background: rgba(var(--v-theme-on-surface), 0.1);
|
||||
}
|
||||
|
||||
.theme-customizer-shadow-scene__badge {
|
||||
block-size: 6px;
|
||||
inline-size: 34%;
|
||||
min-inline-size: 28px;
|
||||
.theme-customizer-shadow-slider__sample-accent {
|
||||
background: rgba(var(--v-theme-primary), 0.48);
|
||||
block-size: 5px;
|
||||
inline-size: 44%;
|
||||
}
|
||||
|
||||
.theme-customizer-shadow-scene__panel-line,
|
||||
.theme-customizer-shadow-scene__line {
|
||||
block-size: 7px;
|
||||
.theme-customizer-shadow-slider__sample-line {
|
||||
background: rgba(var(--v-theme-on-surface), 0.12);
|
||||
block-size: 4px;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.theme-customizer-shadow-scene__panel-line--short,
|
||||
.theme-customizer-shadow-scene__line--short {
|
||||
inline-size: 62%;
|
||||
.theme-customizer-shadow-slider__sample-line--short {
|
||||
inline-size: 68%;
|
||||
}
|
||||
|
||||
.theme-customizer-shadow-scene--low {
|
||||
.theme-customizer-shadow-scene__panel {
|
||||
box-shadow:
|
||||
0 8px 18px rgba(var(--v-theme-on-surface), 0.08),
|
||||
0 2px 6px rgba(var(--v-theme-on-surface), 0.05);
|
||||
}
|
||||
|
||||
.theme-customizer-shadow-scene__card {
|
||||
box-shadow:
|
||||
0 10px 22px rgba(var(--v-theme-on-surface), 0.1),
|
||||
0 4px 10px rgba(var(--v-theme-on-surface), 0.06);
|
||||
}
|
||||
.theme-customizer-shadow-slider__scale {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
font-size: 0.75rem;
|
||||
line-height: 1;
|
||||
margin-block-start: 2px;
|
||||
margin-inline-start: 72px;
|
||||
}
|
||||
|
||||
.theme-customizer-shadow-scene--medium {
|
||||
.theme-customizer-shadow-scene__panel {
|
||||
box-shadow:
|
||||
0 12px 28px rgba(var(--v-theme-on-surface), 0.12),
|
||||
0 4px 12px rgba(var(--v-theme-on-surface), 0.08);
|
||||
}
|
||||
|
||||
.theme-customizer-shadow-scene__card {
|
||||
box-shadow:
|
||||
0 16px 34px rgba(var(--v-theme-on-surface), 0.14),
|
||||
0 6px 16px rgba(var(--v-theme-on-surface), 0.09);
|
||||
}
|
||||
.theme-customizer-shadow-slider :deep(.v-slider.v-input) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.theme-customizer-shadow-scene--high {
|
||||
.theme-customizer-shadow-scene__panel {
|
||||
box-shadow:
|
||||
0 16px 38px rgba(var(--v-theme-on-surface), 0.16),
|
||||
0 6px 18px rgba(var(--v-theme-on-surface), 0.1);
|
||||
}
|
||||
|
||||
.theme-customizer-shadow-scene__card {
|
||||
box-shadow:
|
||||
0 22px 48px rgba(var(--v-theme-on-surface), 0.18),
|
||||
0 8px 22px rgba(var(--v-theme-on-surface), 0.12);
|
||||
}
|
||||
.theme-customizer-shadow-slider :deep(.v-slider-track__tick) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
|
||||
@@ -24,9 +24,37 @@ export const themeCustomizerPrimaryColors = [
|
||||
{ name: 'Slate', value: '#607D8B' },
|
||||
] as const
|
||||
|
||||
export const themeCustomizerShadowLevels = [
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
'10',
|
||||
'11',
|
||||
'12',
|
||||
'13',
|
||||
'14',
|
||||
'15',
|
||||
'16',
|
||||
'17',
|
||||
'18',
|
||||
'19',
|
||||
'20',
|
||||
'21',
|
||||
'22',
|
||||
'23',
|
||||
'24',
|
||||
] as const
|
||||
|
||||
export type ThemeCustomizerLayout = 'collapsed' | 'horizontal' | 'vertical'
|
||||
export type ThemeCustomizerRadius = 'default' | 'extra' | 'huge' | 'large' | 'small'
|
||||
export type ThemeCustomizerShadow = 'none' | 'low' | 'medium' | 'high'
|
||||
export type ThemeCustomizerRadius = 'default' | 'extra' | 'large' | 'none' | 'small'
|
||||
export type ThemeCustomizerShadow = (typeof themeCustomizerShadowLevels)[number]
|
||||
export type ThemeCustomizerSkin = 'bordered' | 'default'
|
||||
export type ThemeCustomizerTheme = 'auto' | 'dark' | 'light' | 'purple' | 'transparent'
|
||||
|
||||
@@ -44,10 +72,16 @@ type VuetifyThemeApi = ReturnType<typeof useTheme>
|
||||
|
||||
const defaultPrimaryColor = themeCustomizerPrimaryColors[0].value
|
||||
const validLayouts: ThemeCustomizerLayout[] = ['vertical', 'collapsed', 'horizontal']
|
||||
const validRadii: ThemeCustomizerRadius[] = ['small', 'default', 'large', 'extra', 'huge']
|
||||
const validShadows: ThemeCustomizerShadow[] = ['none', 'low', 'medium', 'high']
|
||||
const validRadii: ThemeCustomizerRadius[] = ['none', 'small', 'default', 'large', 'extra']
|
||||
const validShadows: readonly ThemeCustomizerShadow[] = themeCustomizerShadowLevels
|
||||
const validSkins: ThemeCustomizerSkin[] = ['default', 'bordered']
|
||||
const validThemes: ThemeCustomizerTheme[] = ['auto', 'light', 'dark', 'purple', 'transparent']
|
||||
const legacyShadowMap: Record<string, ThemeCustomizerShadow> = {
|
||||
high: '24',
|
||||
low: '6',
|
||||
medium: '12',
|
||||
none: '0',
|
||||
}
|
||||
|
||||
let themeApplyVersion = 0
|
||||
|
||||
@@ -73,27 +107,35 @@ function getDefaultThemeCustomizerSettings(): ThemeCustomizerSettings {
|
||||
primaryColor: defaultPrimaryColor,
|
||||
radius: 'default',
|
||||
semiDarkMenu: false,
|
||||
shadow: 'none',
|
||||
shadow: '0',
|
||||
skin: 'default',
|
||||
theme: readStoredThemePreference(),
|
||||
}
|
||||
}
|
||||
|
||||
/** 将旧版语义阴影档位迁移到 Vuetify elevation 数值档位。 */
|
||||
function normalizeThemeCustomizerShadow(shadow: unknown): ThemeCustomizerShadow {
|
||||
if (validShadows.includes(shadow as ThemeCustomizerShadow)) return shadow as ThemeCustomizerShadow
|
||||
if (typeof shadow === 'string' && legacyShadowMap[shadow]) return legacyShadowMap[shadow]
|
||||
|
||||
return getDefaultThemeCustomizerSettings().shadow
|
||||
}
|
||||
|
||||
function normalizeThemeCustomizerSettings(settings: Partial<ThemeCustomizerSettings>): ThemeCustomizerSettings {
|
||||
const fallback = getDefaultThemeCustomizerSettings()
|
||||
const storedRadius = settings.radius as string | undefined
|
||||
const radius = storedRadius === 'huge' ? 'extra' : storedRadius
|
||||
|
||||
return {
|
||||
layout: validLayouts.includes(settings.layout as ThemeCustomizerLayout)
|
||||
? (settings.layout as ThemeCustomizerLayout)
|
||||
: fallback.layout,
|
||||
primaryColor: isHexColor(settings.primaryColor) ? settings.primaryColor.toUpperCase() : fallback.primaryColor,
|
||||
radius: validRadii.includes(settings.radius as ThemeCustomizerRadius)
|
||||
? (settings.radius as ThemeCustomizerRadius)
|
||||
radius: validRadii.includes(radius as ThemeCustomizerRadius)
|
||||
? (radius as ThemeCustomizerRadius)
|
||||
: fallback.radius,
|
||||
semiDarkMenu: typeof settings.semiDarkMenu === 'boolean' ? settings.semiDarkMenu : fallback.semiDarkMenu,
|
||||
shadow: validShadows.includes(settings.shadow as ThemeCustomizerShadow)
|
||||
? (settings.shadow as ThemeCustomizerShadow)
|
||||
: fallback.shadow,
|
||||
shadow: normalizeThemeCustomizerShadow(settings.shadow),
|
||||
skin: validSkins.includes(settings.skin as ThemeCustomizerSkin)
|
||||
? (settings.skin as ThemeCustomizerSkin)
|
||||
: fallback.skin,
|
||||
@@ -247,7 +289,7 @@ export function isDefaultThemeCustomizerSettings(settings: ThemeCustomizerSettin
|
||||
primaryColor: defaultPrimaryColor,
|
||||
radius: 'default',
|
||||
semiDarkMenu: false,
|
||||
shadow: 'none',
|
||||
shadow: '0',
|
||||
skin: 'default',
|
||||
theme: 'auto',
|
||||
})
|
||||
@@ -324,7 +366,7 @@ export function useThemeCustomizer() {
|
||||
primaryColor: defaultPrimaryColor,
|
||||
radius: 'default',
|
||||
semiDarkMenu: false,
|
||||
shadow: 'none',
|
||||
shadow: '0',
|
||||
skin: 'default',
|
||||
theme: 'auto',
|
||||
})
|
||||
|
||||
@@ -223,7 +223,7 @@ function handleDynamicMenuItemClick(item: DynamicButtonMenuItem) {
|
||||
<VCardText class="footer-card-content">
|
||||
<!-- 添加指示器 -->
|
||||
<div ref="indicator" class="nav-indicator"></div>
|
||||
<VBtnToggle class="footer-btn-group" :mandatory="true" v-model="currentMenu">
|
||||
<VBtnToggle class="footer-btn-group" :mandatory="true" variant="plain" v-model="currentMenu">
|
||||
<!-- 遍历底部菜单项 -->
|
||||
<VBtn
|
||||
v-for="menu in footerMenus"
|
||||
@@ -343,6 +343,9 @@ function handleDynamicMenuItemClick(item: DynamicButtonMenuItem) {
|
||||
transition: all 0.5s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
will-change: transform, max-inline-size, opacity;
|
||||
|
||||
--app-control-radius: var(--app-vuetify-rounded-pill);
|
||||
--app-surface-radius: var(--app-vuetify-rounded-pill);
|
||||
|
||||
// 透明主题下的特殊样式
|
||||
.v-theme--transparent & {
|
||||
backdrop-filter: blur(var(--transparent-blur-heavy, 16px));
|
||||
@@ -361,13 +364,19 @@ function handleDynamicMenuItemClick(item: DynamicButtonMenuItem) {
|
||||
padding-inline: 6px;
|
||||
}
|
||||
|
||||
.footer-btn-group {
|
||||
.footer-nav-card .footer-btn-group.v-btn-group {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
border: none;
|
||||
border-radius: 9999px !important;
|
||||
background-color: transparent;
|
||||
box-shadow: none !important;
|
||||
inline-size: 100%;
|
||||
|
||||
&:hover {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-nav-btn {
|
||||
@@ -377,12 +386,15 @@ function handleDynamicMenuItemClick(item: DynamicButtonMenuItem) {
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 9999px !important;
|
||||
background-color: transparent;
|
||||
block-size: 48px;
|
||||
box-shadow: none !important;
|
||||
|
||||
&:hover,
|
||||
&.v-btn--active {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.btn-content {
|
||||
|
||||
@@ -170,16 +170,13 @@ export default {
|
||||
skinDefault: 'Default',
|
||||
skinBordered: 'Bordered',
|
||||
radius: 'Corners',
|
||||
radiusNone: 'Square',
|
||||
radiusSmall: 'Small',
|
||||
radiusDefault: 'Default',
|
||||
radiusLarge: 'Large',
|
||||
radiusExtra: 'Larger',
|
||||
radiusHuge: 'Extra Large',
|
||||
shadow: 'Shadows',
|
||||
shadowNone: 'Flat',
|
||||
shadowLow: 'Soft',
|
||||
shadowMedium: 'Balanced',
|
||||
shadowHigh: 'Bold',
|
||||
shadowLevel: 'Level {level}',
|
||||
semiDarkMenu: 'Semi Dark Menu',
|
||||
layout: 'Layout',
|
||||
layoutVertical: 'Vertical',
|
||||
|
||||
@@ -170,16 +170,13 @@ export default {
|
||||
skinDefault: '默认',
|
||||
skinBordered: '边框',
|
||||
radius: '圆角',
|
||||
radiusNone: '无圆角',
|
||||
radiusSmall: '小圆角',
|
||||
radiusDefault: '默认',
|
||||
radiusLarge: '大圆角',
|
||||
radiusExtra: '更大圆角',
|
||||
radiusHuge: '超大圆角',
|
||||
shadow: '阴影',
|
||||
shadowNone: '无阴影',
|
||||
shadowLow: '柔和',
|
||||
shadowMedium: '标准',
|
||||
shadowHigh: '强烈',
|
||||
shadowLevel: '层级 {level}',
|
||||
semiDarkMenu: '半暗菜单',
|
||||
layout: '布局',
|
||||
layoutVertical: '垂直',
|
||||
|
||||
@@ -170,16 +170,13 @@ export default {
|
||||
skinDefault: '默認',
|
||||
skinBordered: '邊框',
|
||||
radius: '圓角',
|
||||
radiusNone: '無圓角',
|
||||
radiusSmall: '小圓角',
|
||||
radiusDefault: '默認',
|
||||
radiusLarge: '大圓角',
|
||||
radiusExtra: '更大圓角',
|
||||
radiusHuge: '超大圓角',
|
||||
shadow: '陰影',
|
||||
shadowNone: '無陰影',
|
||||
shadowLow: '柔和',
|
||||
shadowMedium: '標準',
|
||||
shadowHigh: '強烈',
|
||||
shadowLevel: '層級 {level}',
|
||||
semiDarkMenu: '半暗菜單',
|
||||
layout: '佈局',
|
||||
layoutVertical: '垂直',
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
/* stylelint-disable custom-property-pattern */
|
||||
/* stylelint-disable no-duplicate-selectors */
|
||||
/* stylelint-disable scss/at-rule-no-unknown */
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
@use 'sass:map';
|
||||
@use 'vuetify/settings' as vuetify-settings;
|
||||
|
||||
// 返回 Vuetify 指定 elevation 的完整三层 box-shadow,供主题定制器映射全局阴影档位。
|
||||
@function app-vuetify-elevation($level) {
|
||||
@return map.get(vuetify-settings.$shadow-key-umbra, $level),
|
||||
map.get(vuetify-settings.$shadow-key-penumbra, $level),
|
||||
map.get(vuetify-settings.$shadow-key-ambient, $level);
|
||||
}
|
||||
|
||||
// 将相对阴影层级限制在 Vuetify elevation 的 0 到 24 档范围内。
|
||||
@function app-clamp-elevation($level) {
|
||||
@if $level < 0 {
|
||||
@return 0;
|
||||
}
|
||||
|
||||
@if $level > 24 {
|
||||
@return 24;
|
||||
}
|
||||
|
||||
@return $level;
|
||||
}
|
||||
|
||||
// 公共样式 - 所有主题都需要
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@@ -38,24 +62,36 @@ html.quick-access-scroll-locked body {
|
||||
}
|
||||
}
|
||||
|
||||
// 全局卡片外观 token:圆角和阴影在主题定制器中即时切换。
|
||||
// 全局外观 token:圆角和阴影复用 Vuetify 的 rounded / elevation 分级。
|
||||
html {
|
||||
--app-theme-surface-radius: 8px;
|
||||
--app-vuetify-rounded-0: #{map.get(vuetify-settings.$rounded, 0)};
|
||||
--app-vuetify-rounded-sm: #{map.get(vuetify-settings.$rounded, 'sm')};
|
||||
--app-vuetify-rounded: #{map.get(vuetify-settings.$rounded, null)};
|
||||
--app-vuetify-rounded-lg: #{map.get(vuetify-settings.$rounded, 'lg')};
|
||||
--app-vuetify-rounded-xl: #{map.get(vuetify-settings.$rounded, 'xl')};
|
||||
--app-vuetify-rounded-pill: #{map.get(vuetify-settings.$rounded, 'pill')};
|
||||
|
||||
@for $level from 0 through 24 {
|
||||
--app-elevation-#{$level}: #{app-vuetify-elevation($level)};
|
||||
}
|
||||
|
||||
--app-theme-surface-radius: var(--app-vuetify-rounded);
|
||||
--app-surface-radius: var(--app-theme-surface-radius);
|
||||
--app-field-radius: var(--app-theme-surface-radius);
|
||||
--app-field-radius: var(--app-vuetify-rounded);
|
||||
--app-control-radius: var(--app-vuetify-rounded);
|
||||
--app-overlay-radius: var(--app-vuetify-rounded);
|
||||
--app-surface-border-opacity: 0.06;
|
||||
--app-surface-border: 1px solid rgba(var(--v-theme-on-surface), var(--app-surface-border-opacity));
|
||||
--app-shadow-rgb: 15, 23, 42;
|
||||
--app-card-rest-shadow: none;
|
||||
--app-card-hover-shadow: none;
|
||||
--app-fab-shadow: none;
|
||||
--app-fab-shadow-strong: none;
|
||||
--app-fab-shadow-hover: none;
|
||||
--app-fab-shadow-strong-hover: none;
|
||||
--app-fab-shadow-active: none;
|
||||
--app-overlay-shadow: none;
|
||||
--app-surface-shadow: none;
|
||||
--app-surface-hover-shadow: none;
|
||||
--app-card-rest-shadow: var(--app-elevation-0);
|
||||
--app-card-hover-shadow: var(--app-elevation-0);
|
||||
--app-fab-shadow: var(--app-elevation-0);
|
||||
--app-fab-shadow-strong: var(--app-elevation-0);
|
||||
--app-fab-shadow-hover: var(--app-elevation-0);
|
||||
--app-fab-shadow-strong-hover: var(--app-elevation-0);
|
||||
--app-fab-shadow-active: var(--app-elevation-0);
|
||||
--app-overlay-shadow: var(--app-elevation-0);
|
||||
--app-surface-shadow: var(--app-elevation-0);
|
||||
--app-surface-hover-shadow: var(--app-elevation-0);
|
||||
--mp-motion-duration-page: 180ms;
|
||||
--mp-motion-duration-overlay: 160ms;
|
||||
--mp-motion-ease-standard: cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
@@ -66,65 +102,47 @@ html[data-theme-skin='bordered'] {
|
||||
--app-surface-border-opacity: 0.1;
|
||||
}
|
||||
|
||||
html[data-theme-radius='none'] {
|
||||
--app-theme-surface-radius: var(--app-vuetify-rounded-0);
|
||||
--app-field-radius: var(--app-vuetify-rounded-0);
|
||||
--app-control-radius: var(--app-vuetify-rounded-0);
|
||||
--app-overlay-radius: var(--app-vuetify-rounded-0);
|
||||
}
|
||||
|
||||
html[data-theme-radius='small'] {
|
||||
--app-theme-surface-radius: 4px;
|
||||
--app-theme-surface-radius: var(--app-vuetify-rounded-sm);
|
||||
--app-field-radius: var(--app-vuetify-rounded-sm);
|
||||
--app-control-radius: var(--app-vuetify-rounded-sm);
|
||||
--app-overlay-radius: var(--app-vuetify-rounded-sm);
|
||||
}
|
||||
|
||||
html[data-theme-radius='large'] {
|
||||
--app-theme-surface-radius: 12px;
|
||||
--app-theme-surface-radius: var(--app-vuetify-rounded-lg);
|
||||
--app-field-radius: var(--app-vuetify-rounded-lg);
|
||||
--app-control-radius: var(--app-vuetify-rounded-lg);
|
||||
--app-overlay-radius: var(--app-vuetify-rounded-lg);
|
||||
}
|
||||
|
||||
html[data-theme-radius='extra'] {
|
||||
--app-theme-surface-radius: 16px;
|
||||
--app-theme-surface-radius: var(--app-vuetify-rounded-xl);
|
||||
--app-field-radius: var(--app-vuetify-rounded-xl);
|
||||
--app-control-radius: var(--app-vuetify-rounded-xl);
|
||||
--app-overlay-radius: var(--app-vuetify-rounded-xl);
|
||||
}
|
||||
|
||||
html[data-theme-radius='huge'] {
|
||||
--app-theme-surface-radius: 24px;
|
||||
}
|
||||
|
||||
html[data-theme='dark'],
|
||||
html[data-theme='purple'],
|
||||
html[data-theme='transparent'] {
|
||||
--app-shadow-rgb: 0, 0, 0;
|
||||
}
|
||||
|
||||
html[data-theme-shadow='low'] {
|
||||
--app-card-rest-shadow: 0 10px 24px rgba(var(--app-shadow-rgb), 0.06), 0 2px 8px rgba(var(--app-shadow-rgb), 0.04);
|
||||
--app-card-hover-shadow: 0 14px 30px rgba(var(--app-shadow-rgb), 0.08), 0 4px 12px rgba(var(--app-shadow-rgb), 0.05);
|
||||
--app-fab-shadow: 0 16px 34px rgba(var(--app-shadow-rgb), 0.16), 0 6px 16px rgba(var(--app-shadow-rgb), 0.1);
|
||||
--app-fab-shadow-strong: 0 20px 40px rgba(var(--app-shadow-rgb), 0.2), 0 8px 18px rgba(var(--app-shadow-rgb), 0.12);
|
||||
--app-fab-shadow-hover: 0 22px 42px rgba(var(--app-shadow-rgb), 0.22), 0 8px 18px rgba(var(--app-shadow-rgb), 0.12);
|
||||
--app-fab-shadow-strong-hover: 0 26px 46px rgba(var(--app-shadow-rgb), 0.24), 0 10px 22px rgba(var(--app-shadow-rgb), 0.14);
|
||||
--app-fab-shadow-active: 0 10px 22px rgba(var(--app-shadow-rgb), 0.16), 0 3px 8px rgba(var(--app-shadow-rgb), 0.1);
|
||||
--app-overlay-shadow: 0 18px 42px rgba(var(--app-shadow-rgb), 0.14), 0 6px 18px rgba(var(--app-shadow-rgb), 0.08);
|
||||
--app-surface-shadow: 0 10px 24px rgba(var(--app-shadow-rgb), 0.07), 0 2px 8px rgba(var(--app-shadow-rgb), 0.05);
|
||||
--app-surface-hover-shadow: 0 14px 30px rgba(var(--app-shadow-rgb), 0.09), 0 4px 12px rgba(var(--app-shadow-rgb), 0.06);
|
||||
}
|
||||
|
||||
html[data-theme-shadow='medium'] {
|
||||
--app-card-rest-shadow: 0 14px 32px rgba(var(--app-shadow-rgb), 0.09), 0 4px 12px rgba(var(--app-shadow-rgb), 0.06);
|
||||
--app-card-hover-shadow: 0 18px 40px rgba(var(--app-shadow-rgb), 0.11), 0 6px 16px rgba(var(--app-shadow-rgb), 0.07);
|
||||
--app-fab-shadow: 0 18px 40px rgba(var(--app-shadow-rgb), 0.2), 0 7px 18px rgba(var(--app-shadow-rgb), 0.12);
|
||||
--app-fab-shadow-strong: 0 24px 48px rgba(var(--app-shadow-rgb), 0.24), 0 10px 24px rgba(var(--app-shadow-rgb), 0.14);
|
||||
--app-fab-shadow-hover: 0 24px 46px rgba(var(--app-shadow-rgb), 0.24), 0 10px 22px rgba(var(--app-shadow-rgb), 0.14);
|
||||
--app-fab-shadow-strong-hover: 0 30px 54px rgba(var(--app-shadow-rgb), 0.28), 0 12px 28px rgba(var(--app-shadow-rgb), 0.16);
|
||||
--app-fab-shadow-active: 0 12px 26px rgba(var(--app-shadow-rgb), 0.18), 0 4px 10px rgba(var(--app-shadow-rgb), 0.12);
|
||||
--app-overlay-shadow: 0 24px 56px rgba(var(--app-shadow-rgb), 0.18), 0 10px 24px rgba(var(--app-shadow-rgb), 0.1);
|
||||
--app-surface-shadow: 0 14px 32px rgba(var(--app-shadow-rgb), 0.1), 0 4px 12px rgba(var(--app-shadow-rgb), 0.07);
|
||||
--app-surface-hover-shadow: 0 18px 40px rgba(var(--app-shadow-rgb), 0.12), 0 6px 16px rgba(var(--app-shadow-rgb), 0.08);
|
||||
}
|
||||
|
||||
html[data-theme-shadow='high'] {
|
||||
--app-card-rest-shadow: 0 18px 40px rgba(var(--app-shadow-rgb), 0.12), 0 6px 18px rgba(var(--app-shadow-rgb), 0.08);
|
||||
--app-card-hover-shadow: 0 22px 50px rgba(var(--app-shadow-rgb), 0.15), 0 8px 22px rgba(var(--app-shadow-rgb), 0.1);
|
||||
--app-fab-shadow: 0 22px 48px rgba(var(--app-shadow-rgb), 0.24), 0 10px 24px rgba(var(--app-shadow-rgb), 0.14);
|
||||
--app-fab-shadow-strong: 0 28px 58px rgba(var(--app-shadow-rgb), 0.3), 0 12px 30px rgba(var(--app-shadow-rgb), 0.18);
|
||||
--app-fab-shadow-hover: 0 28px 56px rgba(var(--app-shadow-rgb), 0.28), 0 12px 28px rgba(var(--app-shadow-rgb), 0.17);
|
||||
--app-fab-shadow-strong-hover: 0 34px 64px rgba(var(--app-shadow-rgb), 0.34), 0 14px 32px rgba(var(--app-shadow-rgb), 0.2);
|
||||
--app-fab-shadow-active: 0 14px 30px rgba(var(--app-shadow-rgb), 0.22), 0 5px 12px rgba(var(--app-shadow-rgb), 0.14);
|
||||
--app-overlay-shadow: 0 30px 70px rgba(var(--app-shadow-rgb), 0.22), 0 14px 30px rgba(var(--app-shadow-rgb), 0.12);
|
||||
--app-surface-shadow: 0 18px 40px rgba(var(--app-shadow-rgb), 0.13), 0 6px 18px rgba(var(--app-shadow-rgb), 0.09);
|
||||
--app-surface-hover-shadow: 0 22px 50px rgba(var(--app-shadow-rgb), 0.16), 0 8px 22px rgba(var(--app-shadow-rgb), 0.11);
|
||||
@for $level from 1 through 24 {
|
||||
html[data-theme-shadow='#{$level}'] {
|
||||
--app-card-rest-shadow: var(--app-elevation-#{app-clamp-elevation($level - 1)});
|
||||
--app-card-hover-shadow: var(--app-elevation-#{app-clamp-elevation($level + 1)});
|
||||
--app-fab-shadow: var(--app-elevation-#{$level});
|
||||
--app-fab-shadow-active: var(--app-elevation-#{app-clamp-elevation($level - 2)});
|
||||
--app-fab-shadow-hover: var(--app-elevation-#{app-clamp-elevation($level + 3)});
|
||||
--app-fab-shadow-strong: var(--app-elevation-#{app-clamp-elevation($level + 2)});
|
||||
--app-fab-shadow-strong-hover: var(--app-elevation-#{app-clamp-elevation($level + 4)});
|
||||
--app-overlay-shadow: var(--app-elevation-#{app-clamp-elevation($level + 6)});
|
||||
--app-surface-shadow: var(--app-elevation-#{$level});
|
||||
--app-surface-hover-shadow: var(--app-elevation-#{app-clamp-elevation($level + 2)});
|
||||
}
|
||||
}
|
||||
|
||||
// 进度条样式
|
||||
@@ -421,12 +439,12 @@ html[data-theme-shadow='high'] {
|
||||
}
|
||||
|
||||
.v-btn:not(.v-btn--rounded, .v-btn--flat, .v-btn--icon, [class^='rounded-'], [class*=' rounded-']) {
|
||||
border-radius: var(--app-surface-radius);
|
||||
border-radius: var(--app-control-radius);
|
||||
transition: border-radius 0.2s ease;
|
||||
}
|
||||
|
||||
.v-btn-group:not(.v-btn-group--variant-plain, .v-btn-group--variant-text) {
|
||||
border-radius: var(--app-surface-radius);
|
||||
border-radius: var(--app-control-radius);
|
||||
box-shadow: var(--app-surface-shadow) !important;
|
||||
transition: box-shadow 0.2s ease, border-radius 0.2s ease;
|
||||
}
|
||||
@@ -703,7 +721,7 @@ html[data-theme="transparent"] .app-card-colorful,
|
||||
}
|
||||
|
||||
.Vue-Toastification__toast {
|
||||
border-radius: var(--app-surface-radius);
|
||||
border-radius: var(--app-overlay-radius);
|
||||
box-shadow: var(--app-overlay-shadow);
|
||||
}
|
||||
|
||||
@@ -1175,6 +1193,7 @@ html[data-theme="transparent"] .app-card-colorful,
|
||||
.v-bottom-sheet > .v-bottom-sheet__content.v-overlay__content > .v-card,
|
||||
.v-menu > .v-overlay__content > .v-card,
|
||||
.v-menu > .v-overlay__content > .v-list {
|
||||
border-radius: var(--app-overlay-radius) !important;
|
||||
box-shadow: var(--app-overlay-shadow) !important;
|
||||
}
|
||||
|
||||
@@ -1191,8 +1210,8 @@ html[data-theme="transparent"] .app-card-colorful,
|
||||
overflow: hidden;
|
||||
border-end-end-radius: 0 !important;
|
||||
border-end-start-radius: 0 !important;
|
||||
border-start-end-radius: var(--app-theme-surface-radius) !important;
|
||||
border-start-start-radius: var(--app-theme-surface-radius) !important;
|
||||
border-start-end-radius: var(--app-overlay-radius) !important;
|
||||
border-start-start-radius: var(--app-overlay-radius) !important;
|
||||
}
|
||||
|
||||
.v-dialog--fullscreen > .v-overlay__content > .v-card,
|
||||
@@ -1201,8 +1220,8 @@ html[data-theme="transparent"] .app-card-colorful,
|
||||
.v-dialog--fullscreen > .v-overlay__content > form > .v-sheet {
|
||||
border-end-end-radius: 0 !important;
|
||||
border-end-start-radius: 0 !important;
|
||||
border-start-end-radius: var(--app-theme-surface-radius) !important;
|
||||
border-start-start-radius: var(--app-theme-surface-radius) !important;
|
||||
border-start-end-radius: var(--app-overlay-radius) !important;
|
||||
border-start-start-radius: var(--app-overlay-radius) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user