feat(projects): refactor theme drawer with tabbed layout for better UX.

This commit is contained in:
Azir 2025-06-14 19:54:43 +08:00 committed by Soybean
parent e89b86ce56
commit 8ba71a0857
19 changed files with 739 additions and 418 deletions

View File

@ -5,9 +5,9 @@ export const GLOBAL_HEADER_MENU_ID = '__GLOBAL_HEADER_MENU__';
export const GLOBAL_SIDER_MENU_ID = '__GLOBAL_SIDER_MENU__';
export const themeSchemaRecord: Record<UnionKey.ThemeScheme, App.I18n.I18nKey> = {
light: 'theme.themeSchema.light',
dark: 'theme.themeSchema.dark',
auto: 'theme.themeSchema.auto'
light: 'theme.appearance.themeSchema.light',
dark: 'theme.appearance.themeSchema.dark',
auto: 'theme.appearance.themeSchema.auto'
};
export const themeSchemaOptions = transformRecordToOption(themeSchemaRecord);
@ -21,43 +21,43 @@ export const loginModuleRecord: Record<UnionKey.LoginModule, App.I18n.I18nKey> =
};
export const themeLayoutModeRecord: Record<UnionKey.ThemeLayoutMode, App.I18n.I18nKey> = {
vertical: 'theme.layoutMode.vertical',
'vertical-mix': 'theme.layoutMode.vertical-mix',
horizontal: 'theme.layoutMode.horizontal',
'horizontal-mix': 'theme.layoutMode.horizontal-mix'
vertical: 'theme.layout.layoutMode.vertical',
'vertical-mix': 'theme.layout.layoutMode.vertical-mix',
horizontal: 'theme.layout.layoutMode.horizontal',
'horizontal-mix': 'theme.layout.layoutMode.horizontal-mix'
};
export const themeLayoutModeOptions = transformRecordToOption(themeLayoutModeRecord);
export const themeScrollModeRecord: Record<UnionKey.ThemeScrollMode, App.I18n.I18nKey> = {
wrapper: 'theme.scrollMode.wrapper',
content: 'theme.scrollMode.content'
wrapper: 'theme.layout.content.scrollMode.wrapper',
content: 'theme.layout.content.scrollMode.content'
};
export const themeScrollModeOptions = transformRecordToOption(themeScrollModeRecord);
export const themeTabModeRecord: Record<UnionKey.ThemeTabMode, App.I18n.I18nKey> = {
chrome: 'theme.tab.mode.chrome',
button: 'theme.tab.mode.button'
chrome: 'theme.layout.tab.mode.chrome',
button: 'theme.layout.tab.mode.button'
};
export const themeTabModeOptions = transformRecordToOption(themeTabModeRecord);
export const themePageAnimationModeRecord: Record<UnionKey.ThemePageAnimateMode, App.I18n.I18nKey> = {
'fade-slide': 'theme.page.mode.fade-slide',
fade: 'theme.page.mode.fade',
'fade-bottom': 'theme.page.mode.fade-bottom',
'fade-scale': 'theme.page.mode.fade-scale',
'zoom-fade': 'theme.page.mode.zoom-fade',
'zoom-out': 'theme.page.mode.zoom-out',
none: 'theme.page.mode.none'
'fade-slide': 'theme.layout.content.page.mode.fade-slide',
fade: 'theme.layout.content.page.mode.fade',
'fade-bottom': 'theme.layout.content.page.mode.fade-bottom',
'fade-scale': 'theme.layout.content.page.mode.fade-scale',
'zoom-fade': 'theme.layout.content.page.mode.zoom-fade',
'zoom-out': 'theme.layout.content.page.mode.zoom-out',
none: 'theme.layout.content.page.mode.none'
};
export const themePageAnimationModeOptions = transformRecordToOption(themePageAnimationModeRecord);
export const resetCacheStrategyRecord: Record<UnionKey.ResetCacheStrategy, App.I18n.I18nKey> = {
close: 'theme.resetCacheStrategy.close',
refresh: 'theme.resetCacheStrategy.refresh'
close: 'theme.layout.resetCacheStrategy.close',
refresh: 'theme.layout.resetCacheStrategy.refresh'
};
export const resetCacheStrategyOptions = transformRecordToOption(resetCacheStrategyRecord);

View File

@ -68,11 +68,11 @@ function handleChangeMode(mode: UnionKey.ThemeLayoutMode) {
</script>
<template>
<div class="flex-center flex-wrap gap-x-32px gap-y-16px">
<div class="grid grid-cols-3 gap-x-32px gap-y-16px">
<div
v-for="(item, key) in layoutConfig"
:key="key"
class="flex cursor-pointer border-2px rounded-6px hover:border-primary"
class="flex-center cursor-pointer border-2px rounded-6px hover:border-primary"
:class="[mode === key ? 'border-primary' : 'border-transparent']"
@click="handleChangeMode(key)"
>

View File

@ -1,10 +1,10 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useAppStore } from '@/store/modules/app';
import { $t } from '@/locales';
import DarkMode from './modules/dark-mode.vue';
import LayoutMode from './modules/layout-mode.vue';
import ThemeColor from './modules/theme-color.vue';
import PageFun from './modules/page-fun.vue';
import AppearanceSettings from './modules/appearance/index.vue';
import LayoutSettings from './modules/layout/index.vue';
import GeneralSettings from './modules/general/index.vue';
import ConfigOperation from './modules/config-operation.vue';
defineOptions({
@ -12,15 +12,26 @@ defineOptions({
});
const appStore = useAppStore();
const activeTab = ref('appearance');
</script>
<template>
<NDrawer v-model:show="appStore.themeDrawerVisible" display-directive="show" :width="360">
<NDrawer v-model:show="appStore.themeDrawerVisible" display-directive="show" :width="460">
<NDrawerContent :title="$t('theme.themeDrawerTitle')" :native-scrollbar="false" closable>
<DarkMode />
<LayoutMode />
<ThemeColor />
<PageFun />
<NTabs v-model:value="activeTab" type="segment" size="medium" class="mb-16px">
<NTab name="appearance" :tab="$t('theme.tabs.appearance')"></NTab>
<NTab name="layout" :tab="$t('theme.tabs.layout')"></NTab>
<NTab name="general" :tab="$t('theme.tabs.general')"></NTab>
</NTabs>
<div class="min-h-400px">
<KeepAlive>
<AppearanceSettings v-if="activeTab === 'appearance'" />
<LayoutSettings v-else-if="activeTab === 'layout'" />
<GeneralSettings v-else-if="activeTab === 'general'" />
</KeepAlive>
</div>
<template #footer>
<ConfigOperation />
</template>
@ -28,4 +39,14 @@ const appStore = useAppStore();
</NDrawer>
</template>
<style scoped></style>
<style scoped>
:deep(.n-tab) {
display: flex;
align-items: center;
gap: 8px;
}
:deep(.n-tab-pane) {
padding: 0;
}
</style>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import ThemeSchema from './modules/theme-schema.vue';
import ThemeColor from './modules/theme-color.vue';
defineOptions({
name: 'AppearanceSettings'
});
</script>
<template>
<div class="flex-col-stretch gap-16px">
<ThemeSchema />
<ThemeColor />
</div>
</template>
<style scoped></style>

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales';
import SettingItem from '../components/setting-item.vue';
import SettingItem from '../../../components/setting-item.vue';
defineOptions({
name: 'ThemeColor'
@ -34,16 +34,16 @@ const swatches: string[] = [
</script>
<template>
<NDivider>{{ $t('theme.themeColor.title') }}</NDivider>
<NDivider>{{ $t('theme.appearance.themeColor.title') }}</NDivider>
<div class="flex-col-stretch gap-12px">
<NTooltip placement="top-start">
<template #trigger>
<SettingItem key="recommend-color" :label="$t('theme.recommendColor')">
<SettingItem key="recommend-color" :label="$t('theme.appearance.recommendColor')">
<NSwitch v-model:value="themeStore.recommendColor" />
</SettingItem>
</template>
<p>
<span class="pr-12px">{{ $t('theme.recommendColorDesc') }}</span>
<span class="pr-12px">{{ $t('theme.appearance.recommendColorDesc') }}</span>
<br />
<NButton
text
@ -57,10 +57,14 @@ const swatches: string[] = [
</NButton>
</p>
</NTooltip>
<SettingItem v-for="(_, key) in themeStore.themeColors" :key="key" :label="$t(`theme.themeColor.${key}`)">
<SettingItem
v-for="(_, key) in themeStore.themeColors"
:key="key"
:label="$t(`theme.appearance.themeColor.${key}`)"
>
<template v-if="key === 'info'" #suffix>
<NCheckbox v-model:checked="themeStore.isInfoFollowPrimary">
{{ $t('theme.themeColor.followPrimary') }}
{{ $t('theme.appearance.themeColor.followPrimary') }}
</NCheckbox>
</template>
<NColorPicker

View File

@ -3,10 +3,10 @@ import { computed } from 'vue';
import { themeSchemaRecord } from '@/constants/app';
import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales';
import SettingItem from '../components/setting-item.vue';
import SettingItem from '../../../components/setting-item.vue';
defineOptions({
name: 'DarkMode'
name: 'ThemeSchema'
});
const themeStore = useThemeStore();
@ -33,7 +33,7 @@ const showSiderInverted = computed(() => !themeStore.darkMode && themeStore.layo
</script>
<template>
<NDivider>{{ $t('theme.themeSchema.title') }}</NDivider>
<NDivider>{{ $t('theme.appearance.themeSchema.title') }}</NDivider>
<div class="flex-col-stretch gap-16px">
<div class="i-flex-center">
<NTabs
@ -50,14 +50,14 @@ const showSiderInverted = computed(() => !themeStore.darkMode && themeStore.layo
</NTabs>
</div>
<Transition name="sider-inverted">
<SettingItem v-if="showSiderInverted" :label="$t('theme.sider.inverted')">
<SettingItem v-if="showSiderInverted" :label="$t('theme.layout.sider.inverted')">
<NSwitch v-model:value="themeStore.sider.inverted" />
</SettingItem>
</Transition>
<SettingItem :label="$t('theme.grayscale')">
<SettingItem :label="$t('theme.appearance.grayscale')">
<NSwitch :value="themeStore.grayscale" @update:value="handleGrayscaleChange" />
</SettingItem>
<SettingItem :label="$t('theme.colourWeakness')">
<SettingItem :label="$t('theme.appearance.colourWeakness')">
<NSwitch :value="themeStore.colourWeakness" @update:value="handleColourWeaknessChange" />
</SettingItem>
</div>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import GlobalSettings from './modules/global-settings.vue';
defineOptions({
name: 'GeneralSettings'
});
</script>
<template>
<div class="flex-col-stretch gap-16px">
<GlobalSettings />
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,59 @@
<script setup lang="ts">
import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales';
import SettingItem from '../../../components/setting-item.vue';
defineOptions({
name: 'GlobalSettings'
});
const themeStore = useThemeStore();
</script>
<template>
<div class="flex-col-stretch gap-16px">
<SettingItem :label="$t('theme.general.multilingual.visible')">
<NSwitch v-model:value="themeStore.header.multilingual.visible" />
</SettingItem>
<SettingItem :label="$t('theme.general.globalSearch.visible')">
<NSwitch v-model:value="themeStore.header.globalSearch.visible" />
</SettingItem>
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
<SettingItem key="1" :label="$t('theme.general.watermark.visible')">
<NSwitch v-model:value="themeStore.watermark.visible" />
</SettingItem>
<SettingItem v-if="themeStore.watermark.visible" key="2" :label="$t('theme.general.watermark.enableUserName')">
<NSwitch v-model:value="themeStore.watermark.enableUserName" />
</SettingItem>
<SettingItem v-if="themeStore.watermark.visible" key="3" :label="$t('theme.general.watermark.text')">
<NInput
v-model:value="themeStore.watermark.text"
autosize
type="text"
size="small"
class="w-120px"
placeholder="SoybeanAdmin"
/>
</SettingItem>
</TransitionGroup>
</div>
</template>
<style scoped>
.setting-list-move,
.setting-list-enter-active,
.setting-list-leave-active {
--uno: transition-all-300;
}
.setting-list-enter-from,
.setting-list-leave-to {
--uno: opacity-0 -translate-x-30px;
}
.setting-list-leave-active {
--uno: absolute;
}
</style>

View File

@ -0,0 +1,25 @@
<script setup lang="ts">
import LayoutMode from './modules/layout-mode.vue';
import TabSettings from './modules/tab-settings.vue';
import HeaderSettings from './modules/header-settings.vue';
import SiderSettings from './modules/sider-settings.vue';
import FooterSettings from './modules/footer-settings.vue';
import ContentSettings from './modules/content-settings.vue';
defineOptions({
name: 'LayoutSettings'
});
</script>
<template>
<div class="flex-col-stretch gap-16px">
<LayoutMode />
<TabSettings />
<HeaderSettings />
<SiderSettings />
<FooterSettings />
<ContentSettings />
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,61 @@
<script setup lang="ts">
import { computed } from 'vue';
import { themePageAnimationModeOptions, themeScrollModeOptions } from '@/constants/app';
import { useThemeStore } from '@/store/modules/theme';
import { translateOptions } from '@/utils/common';
import { $t } from '@/locales';
import SettingItem from '../../../components/setting-item.vue';
defineOptions({
name: 'ContentSettings'
});
const themeStore = useThemeStore();
const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wrapper');
</script>
<template>
<NDivider>{{ $t('theme.layout.content.title') }}</NDivider>
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
<SettingItem key="1" :label="$t('theme.layout.content.scrollMode.title')">
<NSelect
v-model:value="themeStore.layout.scrollMode"
:options="translateOptions(themeScrollModeOptions)"
size="small"
class="w-120px"
/>
</SettingItem>
<SettingItem key="2" :label="$t('theme.layout.content.page.animate')">
<NSwitch v-model:value="themeStore.page.animate" />
</SettingItem>
<SettingItem v-if="themeStore.page.animate" key="3" :label="$t('theme.layout.content.page.mode.title')">
<NSelect
v-model:value="themeStore.page.animateMode"
:options="translateOptions(themePageAnimationModeOptions)"
size="small"
class="w-120px"
/>
</SettingItem>
<SettingItem v-if="isWrapperScrollMode" key="4" :label="$t('theme.layout.content.fixedHeaderAndTab')">
<NSwitch v-model:value="themeStore.fixedHeaderAndTab" />
</SettingItem>
</TransitionGroup>
</template>
<style scoped>
.setting-list-move,
.setting-list-enter-active,
.setting-list-leave-active {
--uno: transition-all-300;
}
.setting-list-enter-from,
.setting-list-leave-to {
--uno: opacity-0 -translate-x-30px;
}
.setting-list-leave-active {
--uno: absolute;
}
</style>

View File

@ -0,0 +1,58 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales';
import SettingItem from '../../../components/setting-item.vue';
defineOptions({
name: 'FooterSettings'
});
const themeStore = useThemeStore();
const layoutMode = computed(() => themeStore.layout.mode);
const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wrapper');
</script>
<template>
<NDivider>{{ $t('theme.layout.footer.title') }}</NDivider>
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
<SettingItem key="1" :label="$t('theme.layout.footer.visible')">
<NSwitch v-model:value="themeStore.footer.visible" />
</SettingItem>
<SettingItem
v-if="themeStore.footer.visible && isWrapperScrollMode"
key="2"
:label="$t('theme.layout.footer.fixed')"
>
<NSwitch v-model:value="themeStore.footer.fixed" />
</SettingItem>
<SettingItem v-if="themeStore.footer.visible" key="3" :label="$t('theme.layout.footer.height')">
<NInputNumber v-model:value="themeStore.footer.height" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem
v-if="themeStore.footer.visible && layoutMode === 'horizontal-mix'"
key="4"
:label="$t('theme.layout.footer.right')"
>
<NSwitch v-model:value="themeStore.footer.right" />
</SettingItem>
</TransitionGroup>
</template>
<style scoped>
.setting-list-move,
.setting-list-enter-active,
.setting-list-leave-active {
--uno: transition-all-300;
}
.setting-list-enter-from,
.setting-list-leave-to {
--uno: opacity-0 -translate-x-30px;
}
.setting-list-leave-active {
--uno: absolute;
}
</style>

View File

@ -0,0 +1,47 @@
<script setup lang="ts">
import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales';
import SettingItem from '../../../components/setting-item.vue';
defineOptions({
name: 'HeaderSettings'
});
const themeStore = useThemeStore();
</script>
<template>
<NDivider>{{ $t('theme.layout.header.title') }}</NDivider>
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
<SettingItem key="1" :label="$t('theme.layout.header.height')">
<NInputNumber v-model:value="themeStore.header.height" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem key="2" :label="$t('theme.layout.header.breadcrumb.visible')">
<NSwitch v-model:value="themeStore.header.breadcrumb.visible" />
</SettingItem>
<SettingItem
v-if="themeStore.header.breadcrumb.visible"
key="3"
:label="$t('theme.layout.header.breadcrumb.showIcon')"
>
<NSwitch v-model:value="themeStore.header.breadcrumb.showIcon" />
</SettingItem>
</TransitionGroup>
</template>
<style scoped>
.setting-list-move,
.setting-list-enter-active,
.setting-list-leave-active {
--uno: transition-all-300;
}
.setting-list-enter-from,
.setting-list-leave-to {
--uno: opacity-0 -translate-x-30px;
}
.setting-list-leave-active {
--uno: absolute;
}
</style>

View File

@ -2,8 +2,8 @@
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales';
import LayoutModeCard from '../components/layout-mode-card.vue';
import SettingItem from '../components/setting-item.vue';
import LayoutModeCard from '../../../components/layout-mode-card.vue';
import SettingItem from '../../../components/setting-item.vue';
defineOptions({
name: 'LayoutMode'
@ -18,7 +18,7 @@ function handleReverseHorizontalMixChange(value: boolean) {
</script>
<template>
<NDivider>{{ $t('theme.layoutMode.title') }}</NDivider>
<NDivider>{{ $t('theme.layout.layoutMode.title') }}</NDivider>
<LayoutModeCard v-model:mode="themeStore.layout.mode" :disabled="appStore.isMobile">
<template #vertical>
<div class="layout-sider h-full w-18px"></div>
@ -51,7 +51,7 @@ function handleReverseHorizontalMixChange(value: boolean) {
</LayoutModeCard>
<SettingItem
v-if="themeStore.layout.mode === 'horizontal-mix'"
:label="$t('theme.layoutMode.reverseHorizontalMix')"
:label="$t('theme.layout.layoutMode.reverseHorizontalMix')"
class="mt-16px"
>
<NSwitch :value="themeStore.layout.reverseHorizontalMix" @update:value="handleReverseHorizontalMixChange" />

View File

@ -0,0 +1,53 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales';
import SettingItem from '../../../components/setting-item.vue';
defineOptions({
name: 'SiderSettings'
});
const themeStore = useThemeStore();
const layoutMode = computed(() => themeStore.layout.mode);
const isMixLayoutMode = computed(() => layoutMode.value.includes('mix'));
</script>
<template>
<NDivider>{{ $t('theme.layout.sider.title') }}</NDivider>
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
<SettingItem v-if="layoutMode === 'vertical'" key="1" :label="$t('theme.layout.sider.width')">
<NInputNumber v-model:value="themeStore.sider.width" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem v-if="layoutMode === 'vertical'" key="2" :label="$t('theme.layout.sider.collapsedWidth')">
<NInputNumber v-model:value="themeStore.sider.collapsedWidth" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem v-if="isMixLayoutMode" key="3" :label="$t('theme.layout.sider.mixWidth')">
<NInputNumber v-model:value="themeStore.sider.mixWidth" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem v-if="isMixLayoutMode" key="4" :label="$t('theme.layout.sider.mixCollapsedWidth')">
<NInputNumber v-model:value="themeStore.sider.mixCollapsedWidth" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem v-if="layoutMode === 'vertical-mix'" key="5" :label="$t('theme.layout.sider.mixChildMenuWidth')">
<NInputNumber v-model:value="themeStore.sider.mixChildMenuWidth" size="small" :step="1" class="w-120px" />
</SettingItem>
</TransitionGroup>
</template>
<style scoped>
.setting-list-move,
.setting-list-enter-active,
.setting-list-leave-active {
--uno: transition-all-300;
}
.setting-list-enter-from,
.setting-list-leave-to {
--uno: opacity-0 -translate-x-30px;
}
.setting-list-leave-active {
--uno: absolute;
}
</style>

View File

@ -0,0 +1,61 @@
<script setup lang="ts">
import { resetCacheStrategyOptions, themeTabModeOptions } from '@/constants/app';
import { useThemeStore } from '@/store/modules/theme';
import { translateOptions } from '@/utils/common';
import { $t } from '@/locales';
import SettingItem from '../../../components/setting-item.vue';
defineOptions({
name: 'TabSettings'
});
const themeStore = useThemeStore();
</script>
<template>
<NDivider>{{ $t('theme.layout.tab.title') }}</NDivider>
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
<SettingItem key="0" :label="$t('theme.layout.resetCacheStrategy.title')">
<NSelect
v-model:value="themeStore.resetCacheStrategy"
:options="translateOptions(resetCacheStrategyOptions)"
size="small"
class="w-120px"
/>
</SettingItem>
<SettingItem key="1" :label="$t('theme.layout.tab.visible')">
<NSwitch v-model:value="themeStore.tab.visible" />
</SettingItem>
<SettingItem v-if="themeStore.tab.visible" key="2" :label="$t('theme.layout.tab.cache')">
<NSwitch v-model:value="themeStore.tab.cache" />
</SettingItem>
<SettingItem v-if="themeStore.tab.visible" key="3" :label="$t('theme.layout.tab.height')">
<NInputNumber v-model:value="themeStore.tab.height" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem v-if="themeStore.tab.visible" key="4" :label="$t('theme.layout.tab.mode.title')">
<NSelect
v-model:value="themeStore.tab.mode"
:options="translateOptions(themeTabModeOptions)"
size="small"
class="w-120px"
/>
</SettingItem>
</TransitionGroup>
</template>
<style scoped>
.setting-list-move,
.setting-list-enter-active,
.setting-list-leave-active {
--uno: transition-all-300;
}
.setting-list-enter-from,
.setting-list-leave-to {
--uno: opacity-0 -translate-x-30px;
}
.setting-list-leave-active {
--uno: absolute;
}
</style>

View File

@ -1,157 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue';
import {
resetCacheStrategyOptions,
themePageAnimationModeOptions,
themeScrollModeOptions,
themeTabModeOptions
} from '@/constants/app';
import { useThemeStore } from '@/store/modules/theme';
import { translateOptions } from '@/utils/common';
import { $t } from '@/locales';
import SettingItem from '../components/setting-item.vue';
defineOptions({
name: 'PageFun'
});
const themeStore = useThemeStore();
const layoutMode = computed(() => themeStore.layout.mode);
const isMixLayoutMode = computed(() => layoutMode.value.includes('mix'));
const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wrapper');
</script>
<template>
<NDivider>{{ $t('theme.pageFunTitle') }}</NDivider>
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
<SettingItem key="0" :label="$t('theme.resetCacheStrategy.title')">
<NSelect
v-model:value="themeStore.resetCacheStrategy"
:options="translateOptions(resetCacheStrategyOptions)"
size="small"
class="w-120px"
/>
</SettingItem>
<SettingItem key="1" :label="$t('theme.scrollMode.title')">
<NSelect
v-model:value="themeStore.layout.scrollMode"
:options="translateOptions(themeScrollModeOptions)"
size="small"
class="w-120px"
/>
</SettingItem>
<SettingItem key="1-1" :label="$t('theme.page.animate')">
<NSwitch v-model:value="themeStore.page.animate" />
</SettingItem>
<SettingItem v-if="themeStore.page.animate" key="1-2" :label="$t('theme.page.mode.title')">
<NSelect
v-model:value="themeStore.page.animateMode"
:options="translateOptions(themePageAnimationModeOptions)"
size="small"
class="w-120px"
/>
</SettingItem>
<SettingItem v-if="isWrapperScrollMode" key="2" :label="$t('theme.fixedHeaderAndTab')">
<NSwitch v-model:value="themeStore.fixedHeaderAndTab" />
</SettingItem>
<SettingItem key="3" :label="$t('theme.header.height')">
<NInputNumber v-model:value="themeStore.header.height" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem key="4" :label="$t('theme.header.breadcrumb.visible')">
<NSwitch v-model:value="themeStore.header.breadcrumb.visible" />
</SettingItem>
<SettingItem v-if="themeStore.header.breadcrumb.visible" key="4-1" :label="$t('theme.header.breadcrumb.showIcon')">
<NSwitch v-model:value="themeStore.header.breadcrumb.showIcon" />
</SettingItem>
<SettingItem key="5" :label="$t('theme.tab.visible')">
<NSwitch v-model:value="themeStore.tab.visible" />
</SettingItem>
<SettingItem v-if="themeStore.tab.visible" key="5-1" :label="$t('theme.tab.cache')">
<NSwitch v-model:value="themeStore.tab.cache" />
</SettingItem>
<SettingItem v-if="themeStore.tab.visible" key="5-2" :label="$t('theme.tab.height')">
<NInputNumber v-model:value="themeStore.tab.height" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem v-if="themeStore.tab.visible" key="5-3" :label="$t('theme.tab.mode.title')">
<NSelect
v-model:value="themeStore.tab.mode"
:options="translateOptions(themeTabModeOptions)"
size="small"
class="w-120px"
/>
</SettingItem>
<SettingItem v-if="layoutMode === 'vertical'" key="6-1" :label="$t('theme.sider.width')">
<NInputNumber v-model:value="themeStore.sider.width" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem v-if="layoutMode === 'vertical'" key="6-2" :label="$t('theme.sider.collapsedWidth')">
<NInputNumber v-model:value="themeStore.sider.collapsedWidth" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem v-if="isMixLayoutMode" key="6-3" :label="$t('theme.sider.mixWidth')">
<NInputNumber v-model:value="themeStore.sider.mixWidth" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem v-if="isMixLayoutMode" key="6-4" :label="$t('theme.sider.mixCollapsedWidth')">
<NInputNumber v-model:value="themeStore.sider.mixCollapsedWidth" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem v-if="layoutMode === 'vertical-mix'" key="6-5" :label="$t('theme.sider.mixChildMenuWidth')">
<NInputNumber v-model:value="themeStore.sider.mixChildMenuWidth" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem key="7" :label="$t('theme.footer.visible')">
<NSwitch v-model:value="themeStore.footer.visible" />
</SettingItem>
<SettingItem v-if="themeStore.footer.visible && isWrapperScrollMode" key="7-1" :label="$t('theme.footer.fixed')">
<NSwitch v-model:value="themeStore.footer.fixed" />
</SettingItem>
<SettingItem v-if="themeStore.footer.visible" key="7-2" :label="$t('theme.footer.height')">
<NInputNumber v-model:value="themeStore.footer.height" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem
v-if="themeStore.footer.visible && layoutMode === 'horizontal-mix'"
key="7-3"
:label="$t('theme.footer.right')"
>
<NSwitch v-model:value="themeStore.footer.right" />
</SettingItem>
<SettingItem key="8" :label="$t('theme.watermark.visible')">
<NSwitch v-model:value="themeStore.watermark.visible" />
</SettingItem>
<SettingItem v-if="themeStore.watermark.visible" key="8-1" :label="$t('theme.watermark.enableUserName')">
<NSwitch v-model:value="themeStore.watermark.enableUserName" />
</SettingItem>
<SettingItem v-if="themeStore.watermark.visible" key="8-2" :label="$t('theme.watermark.text')">
<NInput
v-model:value="themeStore.watermark.text"
autosize
type="text"
size="small"
class="w-120px"
placeholder="SoybeanAdmin"
/>
</SettingItem>
<SettingItem key="9" :label="$t('theme.header.multilingual.visible')">
<NSwitch v-model:value="themeStore.header.multilingual.visible" />
</SettingItem>
<SettingItem key="10" :label="$t('theme.header.globalSearch.visible')">
<NSwitch v-model:value="themeStore.header.globalSearch.visible" />
</SettingItem>
</TransitionGroup>
</template>
<style scoped>
.setting-list-move,
.setting-list-enter-active,
.setting-list-leave-active {
--uno: transition-all-300;
}
.setting-list-enter-from,
.setting-list-leave-to {
--uno: opacity-0 -translate-x-30px;
}
.setting-list-leave-active {
--uno: absolute;
}
</style>

View File

@ -58,6 +58,13 @@ const local: App.I18n.Schema = {
tokenExpired: 'The requested token has expired'
},
theme: {
themeDrawerTitle: 'Theme Configuration',
tabs: {
appearance: 'Appearance',
layout: 'Layout',
general: 'General'
},
appearance: {
themeSchema: {
title: 'Theme Schema',
light: 'Light',
@ -66,16 +73,6 @@ const local: App.I18n.Schema = {
},
grayscale: 'Grayscale',
colourWeakness: 'Colour Weakness',
layoutMode: {
title: 'Layout Mode',
vertical: 'Vertical Menu Mode',
horizontal: 'Horizontal Menu Mode',
'vertical-mix': 'Vertical Mix Menu Mode',
'horizontal-mix': 'Horizontal Mix menu Mode',
reverseHorizontalMix: 'Reverse first level menus and child level menus position'
},
recommendColor: 'Apply Recommended Color Algorithm',
recommendColorDesc: 'The recommended color algorithm refers to',
themeColor: {
title: 'Theme Color',
primary: 'Primary',
@ -85,6 +82,55 @@ const local: App.I18n.Schema = {
error: 'Error',
followPrimary: 'Follow Primary'
},
recommendColor: 'Apply Recommended Color Algorithm',
recommendColorDesc: 'The recommended color algorithm refers to'
},
layout: {
layoutMode: {
title: 'Layout Mode',
vertical: 'Vertical Menu Mode',
horizontal: 'Horizontal Menu Mode',
'vertical-mix': 'Vertical Mix Menu Mode',
'horizontal-mix': 'Horizontal Mix menu Mode',
reverseHorizontalMix: 'Reverse first level menus and child level menus position'
},
tab: {
title: 'Tab Settings',
visible: 'Tab Visible',
cache: 'Tag Bar Info Cache',
height: 'Tab Height',
mode: {
title: 'Tab Mode',
chrome: 'Chrome',
button: 'Button'
}
},
header: {
title: 'Header Settings',
height: 'Header Height',
breadcrumb: {
visible: 'Breadcrumb Visible',
showIcon: 'Breadcrumb Icon Visible'
}
},
sider: {
title: 'Sider Settings',
inverted: 'Dark Sider',
width: 'Sider Width',
collapsedWidth: 'Sider Collapsed Width',
mixWidth: 'Mix Sider Width',
mixCollapsedWidth: 'Mix Sider Collapse Width',
mixChildMenuWidth: 'Mix Child Menu Width'
},
footer: {
title: 'Footer Settings',
visible: 'Footer Visible',
fixed: 'Fixed Footer',
height: 'Footer Height',
right: 'Right Footer'
},
content: {
title: 'Content Area Settings',
scrollMode: {
title: 'Scroll Mode',
wrapper: 'Wrapper',
@ -103,55 +149,28 @@ const local: App.I18n.Schema = {
none: 'None'
}
},
fixedHeaderAndTab: 'Fixed Header And Tab',
header: {
height: 'Header Height',
breadcrumb: {
visible: 'Breadcrumb Visible',
showIcon: 'Breadcrumb Icon Visible'
fixedHeaderAndTab: 'Fixed Header And Tab'
},
multilingual: {
visible: 'Display multilingual button'
},
globalSearch: {
visible: 'Display GlobalSearch button'
resetCacheStrategy: {
title: 'Reset Cache Strategy',
close: 'Close Page',
refresh: 'Refresh Page'
}
},
tab: {
visible: 'Tab Visible',
cache: 'Tag Bar Info Cache',
height: 'Tab Height',
mode: {
title: 'Tab Mode',
chrome: 'Chrome',
button: 'Button'
}
},
sider: {
inverted: 'Dark Sider',
width: 'Sider Width',
collapsedWidth: 'Sider Collapsed Width',
mixWidth: 'Mix Sider Width',
mixCollapsedWidth: 'Mix Sider Collapse Width',
mixChildMenuWidth: 'Mix Child Menu Width'
},
footer: {
visible: 'Footer Visible',
fixed: 'Fixed Footer',
height: 'Footer Height',
right: 'Right Footer'
},
general: {
watermark: {
visible: 'Watermark Full Screen Visible',
text: 'Watermark Text',
enableUserName: 'Enable User Name Watermark'
},
themeDrawerTitle: 'Theme Configuration',
pageFunTitle: 'Page Function',
resetCacheStrategy: {
title: 'Reset Cache Strategy',
close: 'Close Page',
refresh: 'Refresh Page'
multilingual: {
title: 'Multilingual Settings',
visible: 'Display multilingual button'
},
globalSearch: {
title: 'Global Search Settings',
visible: 'Display GlobalSearch button'
}
},
configOperation: {
copyConfig: 'Copy Config',

View File

@ -58,6 +58,13 @@ const local: App.I18n.Schema = {
tokenExpired: 'token已过期'
},
theme: {
themeDrawerTitle: '主题配置',
tabs: {
appearance: '外观',
layout: '布局',
general: '通用'
},
appearance: {
themeSchema: {
title: '主题模式',
light: '亮色模式',
@ -66,16 +73,6 @@ const local: App.I18n.Schema = {
},
grayscale: '灰色模式',
colourWeakness: '色弱模式',
layoutMode: {
title: '布局模式',
vertical: '左侧菜单模式',
'vertical-mix': '左侧菜单混合模式',
horizontal: '顶部菜单模式',
'horizontal-mix': '顶部菜单混合模式',
reverseHorizontalMix: '一级菜单与子级菜单位置反转'
},
recommendColor: '应用推荐算法的颜色',
recommendColorDesc: '推荐颜色的算法参照',
themeColor: {
title: '主题颜色',
primary: '主色',
@ -85,6 +82,55 @@ const local: App.I18n.Schema = {
error: '错误色',
followPrimary: '跟随主色'
},
recommendColor: '应用推荐算法的颜色',
recommendColorDesc: '推荐颜色的算法参照'
},
layout: {
layoutMode: {
title: '布局模式',
vertical: '左侧菜单模式',
'vertical-mix': '左侧菜单混合模式',
horizontal: '顶部菜单模式',
'horizontal-mix': '顶部菜单混合模式',
reverseHorizontalMix: '一级菜单与子级菜单位置反转'
},
tab: {
title: '标签栏设置',
visible: '显示标签栏',
cache: '标签栏信息缓存',
height: '标签栏高度',
mode: {
title: '标签栏风格',
chrome: '谷歌风格',
button: '按钮风格'
}
},
header: {
title: '头部设置',
height: '头部高度',
breadcrumb: {
visible: '显示面包屑',
showIcon: '显示面包屑图标'
}
},
sider: {
title: '侧边栏设置',
inverted: '深色侧边栏',
width: '侧边栏宽度',
collapsedWidth: '侧边栏折叠宽度',
mixWidth: '混合布局侧边栏宽度',
mixCollapsedWidth: '混合布局侧边栏折叠宽度',
mixChildMenuWidth: '混合布局子菜单宽度'
},
footer: {
title: '底部设置',
visible: '显示底部',
fixed: '固定底部',
height: '底部高度',
right: '底部局右'
},
content: {
title: '内容区域设置',
scrollMode: {
title: '滚动模式',
wrapper: '外层滚动',
@ -103,55 +149,28 @@ const local: App.I18n.Schema = {
none: '无'
}
},
fixedHeaderAndTab: '固定头部和标签栏',
header: {
height: '头部高度',
breadcrumb: {
visible: '显示面包屑',
showIcon: '显示面包屑图标'
fixedHeaderAndTab: '固定头部和标签栏'
},
multilingual: {
visible: '显示多语言按钮'
},
globalSearch: {
visible: '显示全局搜索按钮'
resetCacheStrategy: {
title: '重置缓存策略',
close: '关闭页面',
refresh: '刷新页面'
}
},
tab: {
visible: '显示标签栏',
cache: '标签栏信息缓存',
height: '标签栏高度',
mode: {
title: '标签栏风格',
chrome: '谷歌风格',
button: '按钮风格'
}
},
sider: {
inverted: '深色侧边栏',
width: '侧边栏宽度',
collapsedWidth: '侧边栏折叠宽度',
mixWidth: '混合布局侧边栏宽度',
mixCollapsedWidth: '混合布局侧边栏折叠宽度',
mixChildMenuWidth: '混合布局子菜单宽度'
},
footer: {
visible: '显示底部',
fixed: '固定底部',
height: '底部高度',
right: '底部局右'
},
general: {
watermark: {
visible: '显示全屏水印',
text: '水印文本',
enableUserName: '启用用户名水印'
},
themeDrawerTitle: '主题配置',
pageFunTitle: '页面功能',
resetCacheStrategy: {
title: '重置缓存策略',
close: '关闭页面',
refresh: '刷新页面'
multilingual: {
title: '多语言设置',
visible: '显示多语言按钮'
},
globalSearch: {
title: '全局搜索设置',
visible: '显示全局搜索按钮'
}
},
configOperation: {
copyConfig: '复制配置',

67
src/typings/app.d.ts vendored
View File

@ -358,42 +358,42 @@ declare namespace App {
tokenExpired: string;
};
theme: {
themeDrawerTitle: string;
tabs: {
appearance: string;
layout: string;
general: string;
};
appearance: {
themeSchema: { title: string } & Record<UnionKey.ThemeScheme, string>;
grayscale: string;
colourWeakness: string;
layoutMode: { title: string; reverseHorizontalMix: string } & Record<UnionKey.ThemeLayoutMode, string>;
recommendColor: string;
recommendColorDesc: string;
themeColor: {
title: string;
followPrimary: string;
} & Theme.ThemeColor;
scrollMode: { title: string } & Record<UnionKey.ThemeScrollMode, string>;
page: {
animate: string;
mode: { title: string } & Record<UnionKey.ThemePageAnimateMode, string>;
};
fixedHeaderAndTab: string;
header: {
height: string;
breadcrumb: {
visible: string;
showIcon: string;
};
multilingual: {
visible: string;
};
globalSearch: {
visible: string;
};
recommendColor: string;
recommendColorDesc: string;
};
layout: {
layoutMode: { title: string; reverseHorizontalMix: string } & Record<UnionKey.ThemeLayoutMode, string>;
tab: {
title: string;
visible: string;
cache: string;
height: string;
mode: { title: string } & Record<UnionKey.ThemeTabMode, string>;
};
header: {
title: string;
height: string;
breadcrumb: {
visible: string;
showIcon: string;
};
};
sider: {
title: string;
inverted: string;
width: string;
collapsedWidth: string;
@ -402,19 +402,38 @@ declare namespace App {
mixChildMenuWidth: string;
};
footer: {
title: string;
visible: string;
fixed: string;
height: string;
right: string;
};
content: {
title: string;
scrollMode: { title: string } & Record<UnionKey.ThemeScrollMode, string>;
page: {
animate: string;
mode: { title: string } & Record<UnionKey.ThemePageAnimateMode, string>;
};
fixedHeaderAndTab: string;
};
resetCacheStrategy: { title: string } & Record<UnionKey.ResetCacheStrategy, string>;
};
general: {
watermark: {
visible: string;
text: string;
enableUserName: string;
};
themeDrawerTitle: string;
pageFunTitle: string;
resetCacheStrategy: { title: string } & Record<UnionKey.ResetCacheStrategy, string>;
multilingual: {
title: string;
visible: string;
};
globalSearch: {
title: string;
visible: string;
};
};
configOperation: {
copyConfig: string;
copySuccessMsg: string;