mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-11-24 00:56:47 +08:00
feat(projects): 1.0 beta
This commit is contained in:
100
src/layouts/modules/theme-drawer/components/layout-mode-card.vue
Normal file
100
src/layouts/modules/theme-drawer/components/layout-mode-card.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverPlacement } from 'naive-ui';
|
||||
import { themeLayoutModeRecord } from '@/constants/app';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'LayoutModeCard'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* layout mode
|
||||
*/
|
||||
mode: UnionKey.ThemeLayoutMode;
|
||||
/**
|
||||
* disabled
|
||||
*/
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
/**
|
||||
* layout mode change
|
||||
*/
|
||||
(e: 'update:mode', mode: UnionKey.ThemeLayoutMode): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
type LayoutConfig = Record<
|
||||
UnionKey.ThemeLayoutMode,
|
||||
{
|
||||
placement: PopoverPlacement;
|
||||
headerClass: string;
|
||||
menuClass: string;
|
||||
mainClass: string;
|
||||
}
|
||||
>;
|
||||
|
||||
const layoutConfig: LayoutConfig = {
|
||||
vertical: {
|
||||
placement: 'bottom',
|
||||
headerClass: '',
|
||||
menuClass: 'w-1/3 h-full',
|
||||
mainClass: 'w-2/3 h-3/4'
|
||||
},
|
||||
'vertical-mix': {
|
||||
placement: 'bottom',
|
||||
headerClass: '',
|
||||
menuClass: 'w-1/4 h-full',
|
||||
mainClass: 'w-2/3 h-3/4'
|
||||
},
|
||||
horizontal: {
|
||||
placement: 'bottom',
|
||||
headerClass: '',
|
||||
menuClass: 'w-full h-1/4',
|
||||
mainClass: 'w-full h-3/4'
|
||||
},
|
||||
'horizontal-mix': {
|
||||
placement: 'bottom',
|
||||
headerClass: '',
|
||||
menuClass: 'w-full h-1/4',
|
||||
mainClass: 'w-2/3 h-3/4'
|
||||
}
|
||||
};
|
||||
|
||||
function handleChangeMode(mode: UnionKey.ThemeLayoutMode) {
|
||||
if (props.disabled) return;
|
||||
|
||||
emit('update:mode', mode);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-center flex-wrap gap-x-32px gap-y-16px">
|
||||
<div
|
||||
v-for="(item, key) in layoutConfig"
|
||||
:key="key"
|
||||
class="flex border-2px rounded-6px cursor-pointer hover:border-primary"
|
||||
:class="[mode === key ? 'border-primary' : 'border-transparent']"
|
||||
@click="handleChangeMode(key)"
|
||||
>
|
||||
<NTooltip :placement="item.placement">
|
||||
<template #trigger>
|
||||
<div
|
||||
class="gap-6px w-96px h-64px p-6px rd-4px shadow dark:shadow-coolGray-5"
|
||||
:class="[key.includes('vertical') ? 'flex' : 'flex-vertical']"
|
||||
>
|
||||
<slot :name="key"></slot>
|
||||
</div>
|
||||
</template>
|
||||
{{ $t(themeLayoutModeRecord[key]) }}
|
||||
</NTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
26
src/layouts/modules/theme-drawer/components/setting-item.vue
Normal file
26
src/layouts/modules/theme-drawer/components/setting-item.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'SettingItem'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* label
|
||||
*/
|
||||
label: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-y-center justify-between w-full">
|
||||
<div>
|
||||
<span class="text-base_text pr-8px">{{ label }}</span>
|
||||
<slot name="suffix"></slot>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
31
src/layouts/modules/theme-drawer/index.vue
Normal file
31
src/layouts/modules/theme-drawer/index.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
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 ConfigOperation from './modules/config-operation.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'ThemeDrawer'
|
||||
});
|
||||
|
||||
const appStore = useAppStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="appStore.themeDrawerVisible" display-directive="show" :width="378">
|
||||
<NDrawerContent :title="$t('theme.themeDrawerTitle')" :native-scrollbar="false" closable>
|
||||
<DarkMode />
|
||||
<LayoutMode />
|
||||
<ThemeColor />
|
||||
<PageFun />
|
||||
<template #footer>
|
||||
<ConfigOperation />
|
||||
</template>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import Clipboard from 'clipboard';
|
||||
import { $t } from '@/locales';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
|
||||
defineOptions({
|
||||
name: 'ConfigOperation'
|
||||
});
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
const domRef = ref<HTMLElement | null>(null);
|
||||
|
||||
function initClipboard() {
|
||||
if (!domRef.value) return;
|
||||
|
||||
const clipboard = new Clipboard(domRef.value, {
|
||||
text: () => getClipboardText()
|
||||
});
|
||||
|
||||
clipboard.on('success', () => {
|
||||
window.$message?.success($t('theme.configOperation.copySuccessMsg'));
|
||||
});
|
||||
}
|
||||
|
||||
function getClipboardText() {
|
||||
const reg = /"\w+":/g;
|
||||
|
||||
const json = themeStore.settingsJson;
|
||||
|
||||
return json.replace(reg, match => match.replace(/"/g, ''));
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
themeStore.resetStore();
|
||||
|
||||
setTimeout(() => {
|
||||
window.$message?.success($t('theme.configOperation.resetSuccessMsg'));
|
||||
}, 50);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initClipboard();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex justify-between w-full">
|
||||
<NButton type="error" ghost @click="handleReset">{{ $t('theme.configOperation.resetConfig') }}</NButton>
|
||||
<div ref="domRef">
|
||||
<NButton type="primary">{{ $t('theme.configOperation.copyConfig') }}</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
62
src/layouts/modules/theme-drawer/modules/dark-mode.vue
Normal file
62
src/layouts/modules/theme-drawer/modules/dark-mode.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
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';
|
||||
|
||||
defineOptions({
|
||||
name: 'DarkMode'
|
||||
});
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
const icons: Record<UnionKey.ThemeScheme, string> = {
|
||||
light: 'material-symbols:sunny',
|
||||
dark: 'material-symbols:nightlight-rounded',
|
||||
auto: 'material-symbols:hdr-auto'
|
||||
};
|
||||
|
||||
function handleSegmentChange(value: string | number) {
|
||||
themeStore.setThemeScheme(value as UnionKey.ThemeScheme);
|
||||
}
|
||||
|
||||
const showSiderInverted = computed(() => !themeStore.darkMode && themeStore.layout.mode.includes('vertical'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDivider>{{ $t('theme.themeSchema.title') }}</NDivider>
|
||||
<div class="flex-vertical-stretch gap-16px">
|
||||
<div class="i-flex-center">
|
||||
<NTabs type="segment" size="small" :value="themeStore.themeScheme" @update:value="handleSegmentChange">
|
||||
<NTab v-for="(_, key) in themeSchemaRecord" :key="key" :name="key">
|
||||
<SvgIcon :icon="icons[key]" class="h-28px text-icon-small" />
|
||||
</NTab>
|
||||
</NTabs>
|
||||
</div>
|
||||
<Transition name="sider-inverted">
|
||||
<SettingItem v-if="showSiderInverted" :label="$t('theme.sider.inverted')">
|
||||
<NSwitch v-model:value="themeStore.sider.inverted" />
|
||||
</SettingItem>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sider-inverted-enter-active {
|
||||
height: 22px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.sider-inverted-leave-active {
|
||||
height: 22px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.sider-inverted-enter-from,
|
||||
.sider-inverted-leave-to {
|
||||
transform: translateX(20px);
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
}
|
||||
</style>
|
||||
69
src/layouts/modules/theme-drawer/modules/layout-mode.vue
Normal file
69
src/layouts/modules/theme-drawer/modules/layout-mode.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { $t } from '@/locales';
|
||||
import LayoutModeCard from '../components/layout-mode-card.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'LayoutMode'
|
||||
});
|
||||
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDivider>{{ $t('theme.layoutMode.title') }}</NDivider>
|
||||
<LayoutModeCard v-model:mode="themeStore.layout.mode" :disabled="appStore.isMobile">
|
||||
<template #vertical>
|
||||
<div class="layout-sider w-18px h-full"></div>
|
||||
<div class="vertical-wrapper">
|
||||
<div class="layout-header"></div>
|
||||
<div class="layout-main"></div>
|
||||
</div>
|
||||
</template>
|
||||
<template #vertical-mix>
|
||||
<div class="layout-sider w-8px h-full"></div>
|
||||
<div class="layout-sider w-16px h-full"></div>
|
||||
<div class="vertical-wrapper">
|
||||
<div class="layout-header"></div>
|
||||
<div class="layout-main"></div>
|
||||
</div>
|
||||
</template>
|
||||
<template #horizontal>
|
||||
<div class="layout-header"></div>
|
||||
<div class="horizontal-wrapper">
|
||||
<div class="layout-main"></div>
|
||||
</div>
|
||||
</template>
|
||||
<template #horizontal-mix>
|
||||
<div class="layout-header"></div>
|
||||
<div class="horizontal-wrapper">
|
||||
<div class="layout-sider w-18px"></div>
|
||||
<div class="layout-main"></div>
|
||||
</div>
|
||||
</template>
|
||||
</LayoutModeCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.layout-header {
|
||||
--uno: h-16px bg-primary rd-4px;
|
||||
}
|
||||
|
||||
.layout-sider {
|
||||
--uno: bg-primary-300 rd-4px;
|
||||
}
|
||||
|
||||
.layout-main {
|
||||
--uno: flex-1 bg-primary-200 rd-4px;
|
||||
}
|
||||
|
||||
.vertical-wrapper {
|
||||
--uno: flex-1 flex-vertical gap-6px;
|
||||
}
|
||||
|
||||
.horizontal-wrapper {
|
||||
--uno: flex-1 flex gap-6px;
|
||||
}
|
||||
</style>
|
||||
129
src/layouts/modules/theme-drawer/modules/page-fun.vue
Normal file
129
src/layouts/modules/theme-drawer/modules/page-fun.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { themeScrollModeOptions, themePageAnimationModeOptions, themeTabModeOptions } from '@/constants/app';
|
||||
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');
|
||||
|
||||
function translateOptions(options: Common.Option<string>[]) {
|
||||
return options.map(option => ({
|
||||
...option,
|
||||
label: $t(option.label as App.I18n.I18nKey)
|
||||
}));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDivider>{{ $t('theme.pageFunTitle') }}</NDivider>
|
||||
<TransitionGroup tag="div" name="setting-list" class="flex-vertical-stretch gap-12px">
|
||||
<SettingItem key="1" :label="$t('theme.scrollMode.title')">
|
||||
<NSelect
|
||||
v-model:value="themeStore.layout.scrollMode"
|
||||
:options="translateOptions(themeScrollModeOptions)"
|
||||
size="small"
|
||||
class="w-120px"
|
||||
></NSelect>
|
||||
</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>
|
||||
</TransitionGroup>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.setting-list-move,
|
||||
.setting-list-enter-active,
|
||||
.setting-list-leave-active {
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.setting-list-enter-from,
|
||||
.setting-list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.setting-list-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
36
src/layouts/modules/theme-drawer/modules/theme-color.vue
Normal file
36
src/layouts/modules/theme-drawer/modules/theme-color.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { ColorPicker } from '@sa/materials';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { $t } from '@/locales';
|
||||
import SettingItem from '../components/setting-item.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'ThemeColor'
|
||||
});
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
function handleUpdateColor(color: string, key: App.Theme.ThemeColorKey) {
|
||||
themeStore.updateThemeColors(key, color);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDivider>{{ $t('theme.themeColor.title') }}</NDivider>
|
||||
<div class="flex-vertical-stretch gap-12px">
|
||||
<SettingItem v-for="(_, key) in themeStore.themeColors" :key="key" :label="$t(`theme.themeColor.${key}`)">
|
||||
<template v-if="key === 'info'" #suffix>
|
||||
<NCheckbox v-model:checked="themeStore.isInfoFollowPrimary">
|
||||
{{ $t('theme.themeColor.followPrimary') }}
|
||||
</NCheckbox>
|
||||
</template>
|
||||
<ColorPicker
|
||||
:color="themeStore.themeColors[key]"
|
||||
:disabled="key === 'info' && themeStore.isInfoFollowPrimary"
|
||||
@update:color="handleUpdateColor($event, key)"
|
||||
/>
|
||||
</SettingItem>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in New Issue
Block a user