Merge remote-tracking branch 'Soybean/v2.0' into v2.0

# Conflicts:
#	build/config/proxy.ts
#	package.json
#	packages/scripts/package.json
#	pnpm-lock.yaml
#	src/typings/components.d.ts
This commit is contained in:
xlsea
2025-09-01 15:28:16 +08:00
15 changed files with 595 additions and 7 deletions

View File

@ -22,6 +22,7 @@
"execa": "9.6.0",
"kolorist": "1.8.0",
"npm-check-updates": "18.0.3",
"picomatch": "4.0.3",
"rimraf": "6.0.1"
}
}

3
pnpm-lock.yaml generated
View File

@ -287,6 +287,9 @@ importers:
npm-check-updates:
specifier: 18.0.3
version: 18.0.3
picomatch:
specifier: 4.0.3
version: 4.0.3
rimraf:
specifier: 6.0.1
version: 6.0.1

View File

@ -58,8 +58,8 @@ export const themePageAnimationModeRecord: Record<UnionKey.ThemePageAnimateMode,
export const themePageAnimationModeOptions = transformRecordToOption(themePageAnimationModeRecord);
export const resetCacheStrategyRecord: Record<UnionKey.ResetCacheStrategy, App.I18n.I18nKey> = {
close: 'theme.layout.resetCacheStrategy.close',
refresh: 'theme.layout.resetCacheStrategy.refresh'
refresh: 'theme.layout.resetCacheStrategy.refresh',
close: 'theme.layout.resetCacheStrategy.close'
};
export const resetCacheStrategyOptions = transformRecordToOption(resetCacheStrategyRecord);

View File

@ -6,6 +6,7 @@ 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';
import PresetSettings from './modules/preset/index.vue';
defineOptions({
name: 'ThemeDrawer'
@ -33,6 +34,7 @@ const drawerWidth = computed(() => {
<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>
<NTab name="preset" :tab="$t('theme.tabs.preset')"></NTab>
</NTabs>
<div class="min-h-400px">
@ -40,6 +42,7 @@ const drawerWidth = computed(() => {
<AppearanceSettings v-if="activeTab === 'appearance'" />
<LayoutSettings v-else-if="activeTab === 'layout'" />
<GeneralSettings v-else-if="activeTab === 'general'" />
<PresetSettings v-else-if="activeTab === 'preset'" />
</KeepAlive>
</div>

View File

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

View File

@ -0,0 +1,148 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales';
defineOptions({
name: 'ThemePreset'
});
type ThemePreset = Pick<
App.Theme.ThemeSetting,
| 'themeScheme'
| 'grayscale'
| 'colourWeakness'
| 'recommendColor'
| 'themeColor'
| 'otherColor'
| 'isInfoFollowPrimary'
| 'resetCacheStrategy'
| 'layout'
| 'page'
| 'header'
| 'tab'
| 'fixedHeaderAndTab'
| 'sider'
| 'footer'
| 'watermark'
| 'tokens'
> & {
name: string;
desc: string;
i18nkey?: string;
version: string;
};
const presetModules = import.meta.glob('@/theme/preset/*.json', { eager: true, import: 'default' });
const themeStore = useThemeStore();
// Extract preset data
const presets = computed(() =>
Object.entries(presetModules)
.map(([path, presetData]) => {
const fileName = path.split('/').pop()?.replace('.json', '') || '';
return {
id: fileName,
...(presetData as ThemePreset)
};
})
.sort((a, b) => {
if (a.name === 'default') return -1;
if (b.name === 'default') return 1;
return a.name.localeCompare(b.name);
})
);
const getPresetName = (preset: ThemePreset): string => {
if (!preset.i18nkey) return preset.name;
try {
const key = `${preset.i18nkey}.name` as App.I18n.I18nKey;
const translated = $t(key);
return translated !== key ? translated : preset.name;
} catch {
return preset.name;
}
};
const getPresetDesc = (preset: ThemePreset): string => {
if (!preset.i18nkey) return preset.desc;
try {
const key = `${preset.i18nkey}.desc` as App.I18n.I18nKey;
const translated = $t(key);
return translated !== key ? translated : preset.desc;
} catch {
return preset.desc;
}
};
const applyPreset = ({ themeScheme, grayscale, colourWeakness, layout, watermark, ...rest }: ThemePreset): void => {
themeStore.setThemeScheme(themeScheme);
themeStore.setGrayscale(grayscale);
themeStore.setColourWeakness(colourWeakness);
themeStore.setThemeLayout(layout.mode);
themeStore.setWatermarkEnableUserName(watermark.enableUserName);
themeStore.setWatermarkEnableTime(watermark.enableTime);
Object.assign(themeStore, {
...rest,
layout: { ...themeStore.layout, scrollMode: layout.scrollMode },
page: { ...rest.page },
header: { ...rest.header },
tab: { ...rest.tab },
sider: { ...rest.sider },
footer: { ...rest.footer },
watermark: { ...watermark },
tokens: { ...rest.tokens }
});
window.$message?.success($t('theme.appearance.preset.applySuccess'));
};
</script>
<template>
<NDivider>{{ $t('theme.appearance.preset.title') }}</NDivider>
<div class="flex flex-col gap-3">
<div
v-for="preset in presets"
:key="preset.id"
class="border border-primary/10 rounded-lg border-solid bg-white/5 p-3 backdrop-blur-10 transition-all duration-300 hover:(shadow-md -translate-y-0.5)"
>
<div class="mb-2 flex items-center justify-between">
<div class="min-w-0 w-full flex flex-1 items-center justify-between gap-2">
<h5 class="m-0 truncate text-sm text-primary font-600">
{{ getPresetName(preset) }}
</h5>
<NBadge :value="`v${preset.version}`" type="info" size="small" class="flex-shrink-0 opacity-80" />
</div>
<NButton type="primary" size="tiny" ghost round class="ml-2 flex-shrink-0" @click="applyPreset(preset)">
{{ $t('theme.appearance.preset.apply') }}
</NButton>
</div>
<p class="line-clamp-2 mb-3 text-xs text-gray-500 leading-4">{{ getPresetDesc(preset) }}</p>
<div class="flex items-center justify-between">
<div class="flex gap-1">
<div
v-for="(color, key) in { primary: preset.themeColor, ...preset.otherColor }"
:key="key"
class="h-3 w-3 cursor-pointer border border-white/30 rounded-full transition-transform hover:scale-110"
:style="{ backgroundColor: color }"
:class="{ 'ring-1 ring-primary/50': key === 'primary' }"
:title="key"
/>
</div>
<div class="flex items-center gap-1">
<div class="text-lg">
{{ preset.themeScheme === 'dark' ? '🌙' : '☀️' }}
</div>
<div class="text-lg">
{{ preset.grayscale ? '🎨' : '' }}
</div>
</div>
</div>
</div>
</div>
</template>

View File

@ -86,7 +86,8 @@ const local: App.I18n.Schema = {
tabs: {
appearance: 'Appearance',
layout: 'Layout',
general: 'General'
general: 'General',
preset: 'Preset'
},
appearance: {
themeSchema: {
@ -107,7 +108,28 @@ const local: App.I18n.Schema = {
followPrimary: 'Follow Primary'
},
recommendColor: 'Apply Recommended Color Algorithm',
recommendColorDesc: 'The recommended color algorithm refers to'
recommendColorDesc: 'The recommended color algorithm refers to',
preset: {
title: 'Theme Presets',
apply: 'Apply',
applySuccess: 'Preset applied successfully',
default: {
name: 'Default Preset',
desc: 'Default theme preset with balanced settings'
},
dark: {
name: 'Dark Preset',
desc: 'Dark theme preset for night time usage'
},
compact: {
name: 'Compact Preset',
desc: 'Compact layout preset for small screens'
},
azir: {
name: "Azir's Preset",
desc: 'It is a cold and elegant preset that Azir likes'
}
}
},
layout: {
layoutMode: {

View File

@ -86,7 +86,8 @@ const local: App.I18n.Schema = {
tabs: {
appearance: '外观',
layout: '布局',
general: '通用'
general: '通用',
preset: '预设'
},
appearance: {
themeSchema: {
@ -107,7 +108,28 @@ const local: App.I18n.Schema = {
followPrimary: '跟随主色'
},
recommendColor: '应用推荐算法的颜色',
recommendColorDesc: '推荐颜色的算法参照'
recommendColorDesc: '推荐颜色的算法参照',
preset: {
title: '主题预设',
apply: '应用',
applySuccess: '预设应用成功',
default: {
name: '默认预设',
desc: 'Soybean 默认主题预设'
},
dark: {
name: '暗色预设',
desc: '适用于夜间使用的暗色主题预设'
},
compact: {
name: '紧凑型',
desc: '适用于小屏幕的紧凑布局预设'
},
azir: {
name: 'Azir的预设',
desc: '是 Azir 比较喜欢的莫兰迪色系冷淡风'
}
}
},
layout: {
layoutMode: {

View File

@ -0,0 +1,90 @@
{
"name": "Azir's Preset",
"desc": "It is a cold and elegant preset that Azir likes",
"i18nkey": "theme.appearance.preset.azir",
"version": "1.0.0",
"themeScheme": "light",
"grayscale": false,
"colourWeakness": false,
"recommendColor": true,
"themeColor": "#78a878",
"otherColor": {
"info": "#89b989",
"success": "#99c299",
"warning": "#d4bb9d",
"error": "#c49a9a"
},
"isInfoFollowPrimary": true,
"resetCacheStrategy": "refresh",
"layout": {
"mode": "vertical-mix",
"scrollMode": "wrapper"
},
"page": {
"animate": true,
"animateMode": "zoom-fade"
},
"header": {
"height": 64,
"breadcrumb": {
"visible": true,
"showIcon": true
},
"multilingual": {
"visible": true
},
"globalSearch": {
"visible": true
}
},
"tab": {
"visible": true,
"cache": true,
"height": 48,
"mode": "chrome"
},
"fixedHeaderAndTab": true,
"sider": {
"inverted": false,
"width": 220,
"collapsedWidth": 64,
"mixWidth": 90,
"mixCollapsedWidth": 64,
"mixChildMenuWidth": 200
},
"footer": {
"visible": true,
"fixed": true,
"height": 56,
"right": true
},
"watermark": {
"visible": false,
"text": "SoybeanAdmin",
"enableUserName": false,
"enableTime": true,
"timeFormat": "YYYY-MM-DD HH:mm:ss"
},
"tokens": {
"light": {
"colors": {
"container": "rgb(255, 255, 255)",
"layout": "rgb(247, 250, 252)",
"inverted": "rgb(0, 20, 40)",
"base-text": "rgb(31, 31, 31)"
},
"boxShadow": {
"header": "0 1px 2px rgb(0, 21, 41, 0.08)",
"sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
"tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
}
},
"dark": {
"colors": {
"container": "rgb(28, 28, 28)",
"layout": "rgb(18, 18, 18)",
"base-text": "rgb(224, 224, 224)"
}
}
}
}

View File

@ -0,0 +1,90 @@
{
"name": "Compact Preset",
"desc": "Compact layout preset for small screens",
"i18nkey": "theme.appearance.preset.compact",
"version": "1.0.0",
"themeScheme": "light",
"grayscale": false,
"colourWeakness": false,
"recommendColor": false,
"themeColor": "#646cff",
"otherColor": {
"info": "#2080f0",
"success": "#52c41a",
"warning": "#faad14",
"error": "#f5222d"
},
"isInfoFollowPrimary": true,
"resetCacheStrategy": "close",
"layout": {
"mode": "vertical",
"scrollMode": "content"
},
"page": {
"animate": true,
"animateMode": "fade-slide"
},
"header": {
"height": 48,
"breadcrumb": {
"visible": true,
"showIcon": true
},
"multilingual": {
"visible": false
},
"globalSearch": {
"visible": false
}
},
"tab": {
"visible": true,
"cache": true,
"height": 36,
"mode": "button"
},
"fixedHeaderAndTab": true,
"sider": {
"inverted": false,
"width": 180,
"collapsedWidth": 48,
"mixWidth": 80,
"mixCollapsedWidth": 48,
"mixChildMenuWidth": 180
},
"footer": {
"visible": false,
"fixed": false,
"height": 40,
"right": true
},
"watermark": {
"visible": false,
"text": "SoybeanAdmin",
"enableUserName": false,
"enableTime": false,
"timeFormat": "YYYY-MM-DD HH:mm"
},
"tokens": {
"light": {
"colors": {
"container": "rgb(255, 255, 255)",
"layout": "rgb(247, 250, 252)",
"inverted": "rgb(0, 20, 40)",
"base-text": "rgb(31, 31, 31)"
},
"boxShadow": {
"header": "0 1px 2px rgb(0, 21, 41, 0.08)",
"sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
"tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
}
},
"dark": {
"colors": {
"container": "rgb(28, 28, 28)",
"layout": "rgb(18, 18, 18)",
"base-text": "rgb(224, 224, 224)"
}
}
}
}

View File

@ -0,0 +1,90 @@
{
"name": "Dark Preset",
"desc": "Dark theme preset for night time usage",
"i18nkey": "theme.appearance.preset.dark",
"version": "1.0.0",
"themeScheme": "dark",
"grayscale": false,
"colourWeakness": false,
"recommendColor": false,
"themeColor": "#409eff",
"otherColor": {
"info": "#2080f0",
"success": "#52c41a",
"warning": "#faad14",
"error": "#f5222d"
},
"isInfoFollowPrimary": true,
"resetCacheStrategy": "close",
"layout": {
"mode": "vertical",
"scrollMode": "content"
},
"page": {
"animate": true,
"animateMode": "fade-slide"
},
"header": {
"height": 56,
"breadcrumb": {
"visible": true,
"showIcon": true
},
"multilingual": {
"visible": true
},
"globalSearch": {
"visible": true
}
},
"tab": {
"visible": true,
"cache": true,
"height": 44,
"mode": "chrome"
},
"fixedHeaderAndTab": true,
"sider": {
"inverted": true,
"width": 220,
"collapsedWidth": 64,
"mixWidth": 90,
"mixCollapsedWidth": 64,
"mixChildMenuWidth": 200
},
"footer": {
"visible": true,
"fixed": false,
"height": 48,
"right": true
},
"watermark": {
"visible": false,
"text": "SoybeanAdmin",
"enableUserName": false,
"enableTime": false,
"timeFormat": "YYYY-MM-DD HH:mm"
},
"tokens": {
"light": {
"colors": {
"container": "rgb(255, 255, 255)",
"layout": "rgb(247, 250, 252)",
"inverted": "rgb(0, 20, 40)",
"base-text": "rgb(31, 31, 31)"
},
"boxShadow": {
"header": "0 1px 2px rgb(0, 21, 41, 0.08)",
"sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
"tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
}
},
"dark": {
"colors": {
"container": "rgb(28, 28, 28)",
"layout": "rgb(18, 18, 18)",
"base-text": "rgb(224, 224, 224)"
}
}
}
}

View File

@ -0,0 +1,90 @@
{
"name": "default",
"desc": "Default theme preset with balanced settings",
"i18nkey": "theme.appearance.preset.default",
"version": "1.0.0",
"themeScheme": "light",
"grayscale": false,
"colourWeakness": false,
"recommendColor": false,
"themeColor": "#646cff",
"otherColor": {
"info": "#2080f0",
"success": "#52c41a",
"warning": "#faad14",
"error": "#f5222d"
},
"isInfoFollowPrimary": true,
"resetCacheStrategy": "close",
"layout": {
"mode": "vertical",
"scrollMode": "content"
},
"page": {
"animate": true,
"animateMode": "fade-slide"
},
"header": {
"height": 56,
"breadcrumb": {
"visible": true,
"showIcon": true
},
"multilingual": {
"visible": true
},
"globalSearch": {
"visible": true
}
},
"tab": {
"visible": true,
"cache": true,
"height": 44,
"mode": "chrome"
},
"fixedHeaderAndTab": true,
"sider": {
"inverted": false,
"width": 220,
"collapsedWidth": 64,
"mixWidth": 90,
"mixCollapsedWidth": 64,
"mixChildMenuWidth": 200
},
"footer": {
"visible": true,
"fixed": false,
"height": 48,
"right": true
},
"watermark": {
"visible": false,
"text": "SoybeanAdmin",
"enableUserName": false,
"enableTime": false,
"timeFormat": "YYYY-MM-DD HH:mm"
},
"tokens": {
"light": {
"colors": {
"container": "rgb(255, 255, 255)",
"layout": "rgb(247, 250, 252)",
"inverted": "rgb(0, 20, 40)",
"base-text": "rgb(31, 31, 31)"
},
"boxShadow": {
"header": "0 1px 2px rgb(0, 21, 41, 0.08)",
"sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
"tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
}
},
"dark": {
"colors": {
"container": "rgb(28, 28, 28)",
"layout": "rgb(18, 18, 18)",
"base-text": "rgb(224, 224, 224)"
}
}
}
}

View File

@ -12,7 +12,7 @@ export const themeSettings: App.Theme.ThemeSetting = {
error: '#CB2634'
},
isInfoFollowPrimary: true,
resetCacheStrategy: 'close',
resetCacheStrategy: 'refresh',
layout: {
mode: 'vertical',
scrollMode: 'content'

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

@ -406,6 +406,7 @@ declare namespace App {
appearance: string;
layout: string;
general: string;
preset: string;
};
appearance: {
themeSchema: { title: string } & Record<UnionKey.ThemeScheme, string>;
@ -417,6 +418,17 @@ declare namespace App {
} & Theme.ThemeColor;
recommendColor: string;
recommendColorDesc: string;
preset: {
title: string;
apply: string;
applySuccess: string;
[key: string]:
| {
name: string;
desc: string;
}
| string;
};
};
layout: {
layoutMode: { title: string } & Record<UnionKey.ThemeLayoutMode, string> & {

View File

@ -68,6 +68,8 @@ declare module 'vue' {
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
NColorPicker: typeof import('naive-ui')['NColorPicker']
NDataTable: typeof import('naive-ui')['NDataTable']
NDescriptions: typeof import('naive-ui')['NDescriptions']
NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDivider: typeof import('naive-ui')['NDivider']
NDrawer: typeof import('naive-ui')['NDrawer']