feat: add theme radius customization

This commit is contained in:
jxxghp
2026-06-05 23:06:10 +08:00
parent a73c28c1f7
commit f3c524b6b5
34 changed files with 231 additions and 52 deletions

View File

@@ -178,7 +178,7 @@ const instructions = computed(() => {
position: fixed;
z-index: 1000;
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 12px;
border-radius: var(--app-surface-radius);
background: rgb(var(--v-theme-surface));
box-shadow: 0 4px 20px rgba(0, 0, 0, 10%);
inset-block-end: 5rem;

View File

@@ -4,6 +4,7 @@ import {
themeCustomizerPrimaryColors,
useThemeCustomizer,
type ThemeCustomizerLayout,
type ThemeCustomizerRadius,
type ThemeCustomizerShadow,
type ThemeCustomizerSkin,
type ThemeCustomizerTheme,
@@ -34,6 +35,7 @@ const {
resetSettings,
setLayout,
setPrimaryColor,
setRadius,
setSemiDarkMenu,
setShadow,
setSkin,
@@ -175,6 +177,40 @@ const shadowOptions = computed<
},
])
const radiusOptions = computed<
Array<{
previewRadius: string
title: string
value: ThemeCustomizerRadius
}>
>(() => [
{
previewRadius: '0',
title: t('theme.customizer.radiusSquare'),
value: 'square',
},
{
previewRadius: '4px',
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',
},
])
const layoutOptions = computed<Array<{ icon: string; title: string; value: ThemeCustomizerLayout }>>(() => [
{ title: t('theme.customizer.layoutVertical'), value: 'vertical', icon: 'mdi-dock-left' },
{ title: t('theme.customizer.layoutCollapsed'), value: 'collapsed', icon: 'mdi-dock-window' },
@@ -186,6 +222,7 @@ const showLayoutSection = computed(() => !appMode.value)
const hasAppModeCustomization = computed(() => {
return (
settings.value.primaryColor !== defaultPrimaryColor ||
settings.value.radius !== 'default' ||
settings.value.shadow !== 'none' ||
settings.value.skin !== 'default' ||
settings.value.theme !== 'auto'
@@ -228,6 +265,7 @@ async function handleResetSettings() {
// App 模式共享定制器,但保留桌面导航相关偏好,只重置 App 侧可调整的外观设置。
await setPrimaryColor(defaultPrimaryColor)
await setRadius('default')
await setShadow('none')
await setSkin('default')
await setTheme('auto')
@@ -348,6 +386,31 @@ async function handleResetSettings() {
<VDivider class="mt-7" />
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.radius') }}</h3>
<div class="theme-customizer-preview-grid theme-customizer-preview-grid--radius">
<div
v-for="radius in radiusOptions"
:key="radius.value"
class="theme-customizer-preview-option"
:class="{ 'is-active': settings.radius === radius.value }"
@click="setRadius(radius.value)"
>
<span
class="theme-customizer-radius-scene"
:style="{ '--theme-customizer-radius-preview': radius.previewRadius }"
>
<span class="theme-customizer-radius-scene__card">
<span class="theme-customizer-radius-scene__badge" />
<span class="theme-customizer-radius-scene__line" />
<span class="theme-customizer-radius-scene__line theme-customizer-radius-scene__line--short" />
</span>
</span>
<span>{{ radius.title }}</span>
</div>
</div>
<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
@@ -469,7 +532,7 @@ async function handleResetSettings() {
.theme-customizer-panel--dialog {
overflow: hidden;
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
border-radius: 16px;
border-radius: var(--app-surface-radius);
background: rgb(var(--v-theme-surface));
block-size: var(--theme-customizer-viewport-height, 100dvh);
max-block-size: var(--theme-customizer-viewport-height, 100dvh);
@@ -703,6 +766,10 @@ html[data-theme='transparent'] .v-overlay__content:has(.theme-customizer-drawer)
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.theme-customizer-preview-grid--radius {
grid-template-columns: repeat(auto-fit, minmax(92px, 1fr));
}
.theme-customizer-preview-option {
align-items: flex-start;
padding: 0;
@@ -715,6 +782,7 @@ html[data-theme='transparent'] .v-overlay__content:has(.theme-customizer-drawer)
box-shadow: none !important;
.theme-customizer-mini-layout,
.theme-customizer-radius-scene,
.theme-customizer-shadow-scene {
border-width: 2px;
border-color: rgb(var(--v-theme-primary));
@@ -803,6 +871,54 @@ html[data-theme='transparent'] .v-overlay__content:has(.theme-customizer-drawer)
}
}
.theme-customizer-radius-scene {
position: relative;
display: block;
overflow: hidden;
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.05)),
rgb(var(--v-theme-surface));
block-size: 90px;
inline-size: 100%;
min-inline-size: 0;
}
.theme-customizer-radius-scene__card {
position: absolute;
display: flex;
flex-direction: column;
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
border-radius: var(--theme-customizer-radius-preview);
background: rgb(var(--v-theme-surface));
gap: 8px;
inset: 16px;
padding-block: 12px;
padding-inline: 14px;
}
.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 {
block-size: 8px;
inline-size: 42%;
min-inline-size: 28px;
}
.theme-customizer-radius-scene__line {
block-size: 7px;
}
.theme-customizer-radius-scene__line--short {
inline-size: 66%;
}
.theme-customizer-shadow-scene {
position: relative;
display: block;

View File

@@ -75,7 +75,6 @@ async function deleteDownload() {
<VCard
:key="props.info?.hash"
class="downloading-card flex flex-col h-full overflow-hidden"
rounded="lg"
min-height="150"
>
<template #image>
@@ -133,11 +132,11 @@ async function deleteDownload() {
// 外层壳承载主题阴影,避免 VCard 的圆角裁切层影响阴影外扩。
.downloading-card-shadow-shell {
position: relative;
border-radius: 8px;
border-radius: var(--app-surface-radius);
block-size: 100%;
box-shadow: var(--app-surface-shadow);
inline-size: 100%;
transition: box-shadow 0.2s ease;
transition: border-radius 0.2s ease, box-shadow 0.2s ease;
}
@media (hover: hover) {

View File

@@ -137,7 +137,7 @@ function handleDropToFolder(event: DragEvent) {
&.sortable-ghost {
border: 2px dashed #2196f3;
border-radius: 16px;
border-radius: var(--app-surface-radius);
background: rgba(33, 150, 243, 10%);
opacity: 0.3;
}
@@ -151,7 +151,7 @@ function handleDropToFolder(event: DragEvent) {
&.drag-over {
border: 2px dashed #2196f3;
border-radius: 16px;
border-radius: var(--app-surface-radius);
box-shadow: 0 0 20px rgba(33, 150, 243, 50%);
transform: scale(1.02);
}

View File

@@ -255,7 +255,6 @@ onMounted(() => {
:ripple="false"
variant="flat"
elevation="0"
rounded="lg"
:hover="!cardProps.sortable"
@click="handleCardClick"
>

View File

@@ -404,7 +404,7 @@ function handleCardClick() {
<VHover>
<template #default="hover">
<div
class="w-full h-full rounded-lg relative"
class="subscribe-card-shell w-full h-full relative"
:class="{
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering && !props.sortable,
'outline-dotted outline-pink-500 outline-2': props.batchMode && props.selected,
@@ -419,7 +419,6 @@ function handleCardClick() {
'subscribe-card-paused': subscribeState === 'S',
'cursor-move': props.sortable,
}"
rounded="lg"
min-height="150"
@click="handleCardClick"
:ripple="!props.batchMode && !props.sortable"
@@ -594,10 +593,14 @@ function handleCardClick() {
position: relative;
}
.subscribe-card-shell {
border-radius: var(--app-surface-radius);
}
.subscribe-card-pending-tint::after {
position: absolute;
z-index: 3;
border-radius: 8px;
border-radius: inherit;
box-shadow: inset 0 0 48px rgba(56, 189, 248, 40%); // sky-400
content: '';
inset: 0;

View File

@@ -94,7 +94,7 @@ function doDelete() {
<VHover>
<template #default="hover">
<div
class="w-full h-full rounded-lg overflow-hidden"
class="w-full h-full overflow-hidden"
:class="{
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
}"
@@ -103,7 +103,6 @@ function doDelete() {
v-bind="hover.props"
:key="props.media?.id"
class="flex flex-col h-full"
rounded="0"
min-height="150"
@click="showForkSubscribe"
>

View File

@@ -102,7 +102,6 @@ function doDelete() {
:class="{
'workflow-share-card--hovering': hover.isHovering,
}"
rounded="lg"
min-height="150"
:style="{ background: gradientStyle }"
@click="showForkWorkflow"

View File

@@ -196,7 +196,7 @@ function submitSettings() {
position: relative;
overflow: hidden;
border: 1px solid rgba(var(--v-theme-on-surface), 0.1);
border-radius: 8px;
border-radius: var(--app-surface-radius);
background-color: rgba(var(--v-theme-surface-variant), 0.3);
cursor: pointer;
padding-block: 10px;

View File

@@ -118,7 +118,7 @@ function submitOrder() {
position: relative;
overflow: hidden;
border: 1px solid rgba(var(--v-theme-primary), 0.3);
border-radius: 8px;
border-radius: var(--app-surface-radius);
background-color: rgba(var(--v-theme-primary), 0.08);
cursor: grab;
padding-block: 10px;

View File

@@ -278,7 +278,7 @@ async function doDelete() {
position: relative;
overflow: hidden;
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 8px;
border-radius: var(--app-surface-radius);
background-color: rgba(var(--v-theme-surface), 0.8);
block-size: 280px;
inline-size: 240px;
@@ -290,7 +290,7 @@ async function doDelete() {
.vue-flow__node {
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 8px;
border-radius: var(--app-surface-radius);
font-size: 10px;
&:hover {

View File

@@ -116,7 +116,7 @@ const colorTheme = computed(() => {
<style scoped>
.offline-dialog {
border-radius: 16px;
border-radius: var(--app-surface-radius);
}
.status-icon-wrapper {

View File

@@ -505,7 +505,7 @@ onMounted(() => {
.plugin-market-list-wrap {
flex: 1;
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 8px;
border-radius: var(--app-surface-radius);
background: rgba(var(--v-theme-surface), 0.72);
min-block-size: 0;
overflow-y: auto;
@@ -563,7 +563,7 @@ onMounted(() => {
display: flex;
flex: 1;
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 8px;
border-radius: var(--app-surface-radius);
background: rgba(var(--v-theme-surface), 0.72);
min-block-size: 0;
overflow: hidden;

View File

@@ -1914,7 +1914,7 @@ onUnmounted(() => {
.preview-note {
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 1rem;
border-radius: var(--app-surface-radius);
color: rgb(var(--v-theme-error));
font-size: 0.875rem;
line-height: 1.5;
@@ -1932,7 +1932,7 @@ onUnmounted(() => {
display: flex;
flex-direction: column;
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 1rem;
border-radius: var(--app-surface-radius);
gap: 0.375rem;
min-inline-size: 0;
padding-block: 0.875rem;
@@ -1959,7 +1959,7 @@ onUnmounted(() => {
display: flex;
flex-direction: column;
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 0.75rem;
border-radius: var(--app-surface-radius);
gap: 0.75rem;
padding-block: 0.875rem;
padding-inline: 1rem;
@@ -2052,7 +2052,7 @@ onUnmounted(() => {
flex: 0 0 auto;
flex-direction: column;
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 1rem;
border-radius: var(--app-surface-radius);
margin-block-end: 1.5rem;
margin-inline: 1.5rem;
min-block-size: 0;

View File

@@ -698,7 +698,7 @@ onMounted(() => {
display: flex;
overflow: hidden;
flex-direction: column;
border-radius: 16px !important;
border-radius: var(--app-surface-radius) !important;
box-shadow: 0 12px 40px rgba(0, 0, 0, 12%) !important;
}

View File

@@ -394,7 +394,7 @@ watch(selectedFile, async newFile => {
.upload-zone {
padding: 2rem;
border: 2px dashed #ccc;
border-radius: 8px;
border-radius: var(--app-surface-radius);
text-align: center;
transition: all 0.3s ease;
}

View File

@@ -719,7 +719,7 @@ onMounted(() => {
.site-resource-card__meta-item {
border: 1px solid rgba(var(--v-border-color), calc(var(--v-border-opacity) * 0.7));
border-radius: 0.6rem;
border-radius: var(--app-surface-radius);
background: rgba(var(--v-theme-surface), 0.78);
min-block-size: 0;
padding-block: 0.55rem;

View File

@@ -392,7 +392,7 @@ onMounted(() => {
.stat-card {
padding: 16px;
border: 1px solid var(--v-border-color);
border-radius: 8px;
border-radius: var(--app-surface-radius);
background: var(--v-theme-surface);
min-inline-size: 100px;
text-align: center;

View File

@@ -602,7 +602,7 @@ const isMacOS = computed(() => {
flex-direction: column;
padding: 16px;
border: 1px solid rgb(var(--v-theme-primary));
border-radius: 8px;
border-radius: var(--app-surface-radius);
background-color: rgb(var(--v-theme-surface));
box-shadow: 0 8px 24px rgba(var(--v-shadow-key-umbra-color), 0.32);
gap: 14px;
@@ -656,7 +656,7 @@ const isMacOS = computed(() => {
.vue-flow__minimap {
overflow: hidden;
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 8px;
border-radius: var(--app-surface-radius);
background-color: rgba(var(--v-theme-surface), 0.8);
box-shadow: 0 4px 15px rgba(var(--v-shadow-key-umbra-color), 0.1);
inset-block-end: 20px;
@@ -687,7 +687,7 @@ const isMacOS = computed(() => {
// 自定义节点样式
.vue-flow__node {
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 12px;
border-radius: var(--app-surface-radius);
&:hover {
box-shadow: 0 8px 16px rgba(var(--v-shadow-key-umbra-color), 0.15) !important;

View File

@@ -778,7 +778,7 @@ onUnmounted(() => {
<template>
<div>
<VCard class="d-flex flex-column w-full h-full rounded-t-0" :class="{ 'rounded-s-0': showTree }">
<VCard class="d-flex flex-column w-full h-full">
<div v-if="!loading" class="flex">
<IconBtn v-if="display.mdAndUp.value">
<VIcon v-if="showTree" icon="mdi-file-tree" @click="switchFileTree(false)" />

View File

@@ -253,7 +253,7 @@ onMounted(async () => {
</script>
<template>
<VCard class="file-navigator rounded-e-0 rounded-t-0" v-if="!isMobile" :height="`${availableHeight}px`">
<VCard class="file-navigator" v-if="!isMobile" :height="`${availableHeight}px`">
<VVirtualScroll :items="visibleTreeRows" :item-height="32" class="tree-container">
<template #default="{ item }">
<div

View File

@@ -610,7 +610,7 @@ onMounted(() => {
.filter-toolbar-card {
overflow: hidden;
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
border-radius: 8px;
border-radius: var(--app-surface-radius);
background: rgba(var(--v-theme-surface), 0.82);
}
@@ -759,7 +759,7 @@ onMounted(() => {
@media (width <= 600px) {
.filter-toolbar-card {
border-radius: 8px;
border-radius: var(--app-surface-radius);
}
.filter-buttons-grid {