Skip to content

Commit 257f118

Browse files
Azir-11honghuangdc
authored andcommitted
feat(projects): support theme preset function.
1 parent d731111 commit 257f118

File tree

11 files changed

+588
-4
lines changed

11 files changed

+588
-4
lines changed

src/layouts/modules/theme-drawer/index.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import AppearanceSettings from './modules/appearance/index.vue';
66
import LayoutSettings from './modules/layout/index.vue';
77
import GeneralSettings from './modules/general/index.vue';
88
import ConfigOperation from './modules/config-operation.vue';
9+
import PresetSettings from './modules/preset/index.vue';
910
1011
defineOptions({
1112
name: 'ThemeDrawer'
@@ -33,13 +34,15 @@ const drawerWidth = computed(() => {
3334
<NTab name="appearance" :tab="$t('theme.tabs.appearance')"></NTab>
3435
<NTab name="layout" :tab="$t('theme.tabs.layout')"></NTab>
3536
<NTab name="general" :tab="$t('theme.tabs.general')"></NTab>
37+
<NTab name="preset" :tab="$t('theme.tabs.preset')"></NTab>
3638
</NTabs>
3739

3840
<div class="min-h-400px">
3941
<KeepAlive>
4042
<AppearanceSettings v-if="activeTab === 'appearance'" />
4143
<LayoutSettings v-else-if="activeTab === 'layout'" />
4244
<GeneralSettings v-else-if="activeTab === 'general'" />
45+
<PresetSettings v-else-if="activeTab === 'preset'" />
4346
</KeepAlive>
4447
</div>
4548

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script setup lang="ts">
2+
import ThemePreset from './modules/theme-preset.vue';
3+
4+
defineOptions({
5+
name: 'PresetSettings'
6+
});
7+
</script>
8+
9+
<template>
10+
<div class="flex-col-stretch gap-16px">
11+
<ThemePreset />
12+
</div>
13+
</template>
14+
15+
<style scoped></style>
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue';
3+
import { useThemeStore } from '@/store/modules/theme';
4+
import { $t } from '@/locales';
5+
6+
defineOptions({
7+
name: 'ThemePreset'
8+
});
9+
10+
type ThemePreset = Pick<
11+
App.Theme.ThemeSetting,
12+
| 'themeScheme'
13+
| 'grayscale'
14+
| 'colourWeakness'
15+
| 'recommendColor'
16+
| 'themeColor'
17+
| 'otherColor'
18+
| 'isInfoFollowPrimary'
19+
| 'resetCacheStrategy'
20+
| 'layout'
21+
| 'page'
22+
| 'header'
23+
| 'tab'
24+
| 'fixedHeaderAndTab'
25+
| 'sider'
26+
| 'footer'
27+
| 'watermark'
28+
| 'tokens'
29+
> & {
30+
name: string;
31+
desc: string;
32+
i18nkey?: string;
33+
version: string;
34+
};
35+
36+
const presetModules = import.meta.glob('@/theme/preset/*.json', { eager: true, import: 'default' });
37+
38+
const themeStore = useThemeStore();
39+
40+
// Extract preset data
41+
const presets = computed(() =>
42+
Object.entries(presetModules)
43+
.map(([path, presetData]) => {
44+
const fileName = path.split('/').pop()?.replace('.json', '') || '';
45+
return {
46+
id: fileName,
47+
...(presetData as ThemePreset)
48+
};
49+
})
50+
.sort((a, b) => {
51+
if (a.name === 'default') return -1;
52+
if (b.name === 'default') return 1;
53+
return a.name.localeCompare(b.name);
54+
})
55+
);
56+
57+
const getPresetName = (preset: ThemePreset): string => {
58+
if (!preset.i18nkey) return preset.name;
59+
try {
60+
const key = `${preset.i18nkey}.name` as App.I18n.I18nKey;
61+
const translated = $t(key);
62+
return translated !== key ? translated : preset.name;
63+
} catch {
64+
return preset.name;
65+
}
66+
};
67+
68+
const getPresetDesc = (preset: ThemePreset): string => {
69+
if (!preset.i18nkey) return preset.desc;
70+
try {
71+
const key = `${preset.i18nkey}.desc` as App.I18n.I18nKey;
72+
const translated = $t(key);
73+
return translated !== key ? translated : preset.desc;
74+
} catch {
75+
return preset.desc;
76+
}
77+
};
78+
79+
const applyPreset = ({ themeScheme, grayscale, colourWeakness, layout, watermark, ...rest }: ThemePreset): void => {
80+
themeStore.setThemeScheme(themeScheme);
81+
themeStore.setGrayscale(grayscale);
82+
themeStore.setColourWeakness(colourWeakness);
83+
themeStore.setThemeLayout(layout.mode);
84+
themeStore.setWatermarkEnableUserName(watermark.enableUserName);
85+
themeStore.setWatermarkEnableTime(watermark.enableTime);
86+
87+
Object.assign(themeStore, {
88+
...rest,
89+
layout: { ...themeStore.layout, scrollMode: layout.scrollMode },
90+
page: { ...rest.page },
91+
header: { ...rest.header },
92+
tab: { ...rest.tab },
93+
sider: { ...rest.sider },
94+
footer: { ...rest.footer },
95+
watermark: { ...watermark },
96+
tokens: { ...rest.tokens }
97+
});
98+
99+
window.$message?.success($t('theme.appearance.preset.applySuccess'));
100+
};
101+
</script>
102+
103+
<template>
104+
<NDivider>{{ $t('theme.appearance.preset.title') }}</NDivider>
105+
106+
<div class="flex flex-col gap-3">
107+
<div
108+
v-for="preset in presets"
109+
:key="preset.id"
110+
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)"
111+
>
112+
<div class="mb-2 flex items-center justify-between">
113+
<div class="min-w-0 w-full flex flex-1 items-center justify-between gap-2">
114+
<h5 class="m-0 truncate text-sm text-primary font-600">
115+
{{ getPresetName(preset) }}
116+
</h5>
117+
<NBadge :value="`v${preset.version}`" type="info" size="small" class="flex-shrink-0 opacity-80" />
118+
</div>
119+
<NButton type="primary" size="tiny" ghost round class="ml-2 flex-shrink-0" @click="applyPreset(preset)">
120+
{{ $t('theme.appearance.preset.apply') }}
121+
</NButton>
122+
</div>
123+
124+
<p class="line-clamp-2 mb-3 text-xs text-gray-500 leading-4">{{ getPresetDesc(preset) }}</p>
125+
126+
<div class="flex items-center justify-between">
127+
<div class="flex gap-1">
128+
<div
129+
v-for="(color, key) in { primary: preset.themeColor, ...preset.otherColor }"
130+
:key="key"
131+
class="h-3 w-3 cursor-pointer border border-white/30 rounded-full transition-transform hover:scale-110"
132+
:style="{ backgroundColor: color }"
133+
:class="{ 'ring-1 ring-primary/50': key === 'primary' }"
134+
:title="key"
135+
/>
136+
</div>
137+
<div class="flex items-center gap-1">
138+
<div class="text-lg">
139+
{{ preset.themeScheme === 'dark' ? '🌙' : '☀️' }}
140+
</div>
141+
<div class="text-lg">
142+
{{ preset.grayscale ? '🎨' : '' }}
143+
</div>
144+
</div>
145+
</div>
146+
</div>
147+
</div>
148+
</template>

src/locales/langs/en-us.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ const local: App.I18n.Schema = {
6262
tabs: {
6363
appearance: 'Appearance',
6464
layout: 'Layout',
65-
general: 'General'
65+
general: 'General',
66+
preset: 'Preset'
6667
},
6768
appearance: {
6869
themeSchema: {
@@ -83,7 +84,28 @@ const local: App.I18n.Schema = {
8384
followPrimary: 'Follow Primary'
8485
},
8586
recommendColor: 'Apply Recommended Color Algorithm',
86-
recommendColorDesc: 'The recommended color algorithm refers to'
87+
recommendColorDesc: 'The recommended color algorithm refers to',
88+
preset: {
89+
title: 'Theme Presets',
90+
apply: 'Apply',
91+
applySuccess: 'Preset applied successfully',
92+
default: {
93+
name: 'Default Preset',
94+
desc: 'Default theme preset with balanced settings'
95+
},
96+
dark: {
97+
name: 'Dark Preset',
98+
desc: 'Dark theme preset for night time usage'
99+
},
100+
compact: {
101+
name: 'Compact Preset',
102+
desc: 'Compact layout preset for small screens'
103+
},
104+
azir: {
105+
name: "Azir's Preset",
106+
desc: 'It is a cold and elegant preset that Azir likes'
107+
}
108+
}
87109
},
88110
layout: {
89111
layoutMode: {

src/locales/langs/zh-cn.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ const local: App.I18n.Schema = {
6262
tabs: {
6363
appearance: '外观',
6464
layout: '布局',
65-
general: '通用'
65+
general: '通用',
66+
preset: '预设'
6667
},
6768
appearance: {
6869
themeSchema: {
@@ -83,7 +84,28 @@ const local: App.I18n.Schema = {
8384
followPrimary: '跟随主色'
8485
},
8586
recommendColor: '应用推荐算法的颜色',
86-
recommendColorDesc: '推荐颜色的算法参照'
87+
recommendColorDesc: '推荐颜色的算法参照',
88+
preset: {
89+
title: '主题预设',
90+
apply: '应用',
91+
applySuccess: '预设应用成功',
92+
default: {
93+
name: '默认预设',
94+
desc: 'Soybean 默认主题预设'
95+
},
96+
dark: {
97+
name: '暗色预设',
98+
desc: '适用于夜间使用的暗色主题预设'
99+
},
100+
compact: {
101+
name: '紧凑型',
102+
desc: '适用于小屏幕的紧凑布局预设'
103+
},
104+
azir: {
105+
name: 'Azir的预设',
106+
desc: '是 Azir 比较喜欢的莫兰迪色系冷淡风'
107+
}
108+
}
87109
},
88110
layout: {
89111
layoutMode: {

src/theme/preset/azir.json

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
{
2+
"name": "Azir's Preset",
3+
"desc": "It is a cold and elegant preset that Azir likes",
4+
"i18nkey": "theme.appearance.preset.azir",
5+
"version": "1.0.0",
6+
"themeScheme": "light",
7+
"grayscale": false,
8+
"colourWeakness": false,
9+
"recommendColor": true,
10+
"themeColor": "#78a878",
11+
"otherColor": {
12+
"info": "#89b989",
13+
"success": "#99c299",
14+
"warning": "#d4bb9d",
15+
"error": "#c49a9a"
16+
},
17+
"isInfoFollowPrimary": true,
18+
"resetCacheStrategy": "refresh",
19+
"layout": {
20+
"mode": "vertical-mix",
21+
"scrollMode": "wrapper"
22+
},
23+
"page": {
24+
"animate": true,
25+
"animateMode": "zoom-fade"
26+
},
27+
"header": {
28+
"height": 64,
29+
"breadcrumb": {
30+
"visible": true,
31+
"showIcon": true
32+
},
33+
"multilingual": {
34+
"visible": true
35+
},
36+
"globalSearch": {
37+
"visible": true
38+
}
39+
},
40+
"tab": {
41+
"visible": true,
42+
"cache": true,
43+
"height": 48,
44+
"mode": "chrome"
45+
},
46+
"fixedHeaderAndTab": true,
47+
"sider": {
48+
"inverted": false,
49+
"width": 220,
50+
"collapsedWidth": 64,
51+
"mixWidth": 90,
52+
"mixCollapsedWidth": 64,
53+
"mixChildMenuWidth": 200
54+
},
55+
"footer": {
56+
"visible": true,
57+
"fixed": true,
58+
"height": 56,
59+
"right": true
60+
},
61+
"watermark": {
62+
"visible": false,
63+
"text": "SoybeanAdmin",
64+
"enableUserName": false,
65+
"enableTime": true,
66+
"timeFormat": "YYYY-MM-DD HH:mm:ss"
67+
},
68+
"tokens": {
69+
"light": {
70+
"colors": {
71+
"container": "rgb(255, 255, 255)",
72+
"layout": "rgb(247, 250, 252)",
73+
"inverted": "rgb(0, 20, 40)",
74+
"base-text": "rgb(31, 31, 31)"
75+
},
76+
"boxShadow": {
77+
"header": "0 1px 2px rgb(0, 21, 41, 0.08)",
78+
"sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
79+
"tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
80+
}
81+
},
82+
"dark": {
83+
"colors": {
84+
"container": "rgb(28, 28, 28)",
85+
"layout": "rgb(18, 18, 18)",
86+
"base-text": "rgb(224, 224, 224)"
87+
}
88+
}
89+
}
90+
}

0 commit comments

Comments
 (0)