Skip to content

Commit ca24945

Browse files
Azir-11pixelmaxQm
andauthored
feat: 重构设置界面,添加主题模式、颜色选择和布局配置模块,移除冗余组件 (#2050)
* feat: 重构设置界面,添加主题模式、颜色选择和布局配置模块,移除冗余组件 --------- Co-authored-by: PiexlMax(奇淼 <[email protected]>
1 parent ea7455e commit ca24945

File tree

9 files changed

+1237
-190
lines changed

9 files changed

+1237
-190
lines changed
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
<template>
2+
<div class="grid grid-cols-2 gap-6 font-inter px-6">
3+
<div
4+
v-for="layout in layoutModes"
5+
:key="layout.value"
6+
class="bg-white dark:bg-gray-700 border-2 border-gray-200 dark:border-gray-600 rounded-xl p-6 cursor-pointer transition-all duration-150 ease-in-out hover:transform hover:-translate-y-1 hover:shadow-xl"
7+
:class="{
8+
'ring-2 ring-offset-2 ring-offset-gray-50 dark:ring-offset-gray-900 transform -translate-y-1 shadow-xl': modelValue === layout.value
9+
}"
10+
:style="modelValue === layout.value ? {
11+
borderColor: primaryColor,
12+
ringColor: primaryColor + '40'
13+
} : {}"
14+
@click="handleLayoutChange(layout.value)"
15+
>
16+
<div class="flex justify-center mb-5">
17+
<div
18+
class="w-28 h-20 bg-gray-50 dark:bg-gray-600 border border-gray-200 dark:border-gray-500 rounded-lg p-2 flex gap-1.5 shadow-inner"
19+
:class="layout.containerClass"
20+
>
21+
<div
22+
v-if="layout.showSidebar"
23+
class="rounded-sm"
24+
:class="[layout.sidebarClass]"
25+
:style="getSidebarStyle(layout)"
26+
></div>
27+
28+
<div class="flex-1 flex flex-col gap-1.5">
29+
<div
30+
v-if="layout.showHeader"
31+
class="rounded-sm"
32+
:class="layout.headerClass"
33+
:style="getHeaderStyle(layout)"
34+
></div>
35+
36+
<div
37+
class="flex-1 rounded-sm"
38+
:class="layout.contentClass"
39+
:style="getContentStyle(layout)"
40+
></div>
41+
</div>
42+
</div>
43+
</div>
44+
45+
<div class="text-center">
46+
<span class="block text-base font-semibold text-gray-900 dark:text-white mb-2" :class="{ 'text-current': modelValue === layout.value }" :style="modelValue === layout.value ? { color: primaryColor } : {}">{{ layout.label }}</span>
47+
<span class="block text-sm text-gray-500 dark:text-gray-400">{{ layout.description }}</span>
48+
</div>
49+
</div>
50+
</div>
51+
</template>
52+
53+
<script setup>
54+
import { computed } from 'vue'
55+
import { storeToRefs } from 'pinia'
56+
import { useAppStore } from '@/pinia'
57+
58+
defineOptions({
59+
name: 'LayoutModeCard'
60+
})
61+
62+
const props = defineProps({
63+
modelValue: {
64+
type: String,
65+
default: 'normal'
66+
}
67+
})
68+
69+
const emit = defineEmits(['update:modelValue'])
70+
71+
const appStore = useAppStore()
72+
const { config } = storeToRefs(appStore)
73+
74+
const primaryColor = computed(() => config.value.primaryColor)
75+
const lighterPrimaryColor = computed(() => {
76+
const hex = config.value.primaryColor.replace('#', '')
77+
const r = parseInt(hex.substr(0, 2), 16)
78+
const g = parseInt(hex.substr(2, 2), 16)
79+
const b = parseInt(hex.substr(4, 2), 16)
80+
return `rgba(${r}, ${g}, ${b}, 0.7)`
81+
})
82+
const lightestPrimaryColor = computed(() => {
83+
const hex = config.value.primaryColor.replace('#', '')
84+
const r = parseInt(hex.substr(0, 2), 16)
85+
const g = parseInt(hex.substr(2, 2), 16)
86+
const b = parseInt(hex.substr(4, 2), 16)
87+
return `rgba(${r}, ${g}, ${b}, 0.4)`
88+
})
89+
90+
const layoutModes = [
91+
{
92+
value: 'normal',
93+
label: '经典布局',
94+
description: '左侧导航,顶部标题栏',
95+
containerClass: '',
96+
showSidebar: true,
97+
sidebarClass: 'w-1/4',
98+
showHeader: true,
99+
headerClass: 'h-1/4',
100+
contentClass: '',
101+
showRightSidebar: false,
102+
primaryElement: 'sidebar'
103+
},
104+
{
105+
value: 'head',
106+
label: '顶部导航',
107+
description: '水平导航栏布局',
108+
containerClass: 'flex-col',
109+
showSidebar: false,
110+
showHeader: true,
111+
headerClass: 'h-1/3',
112+
contentClass: '',
113+
showRightSidebar: false,
114+
primaryElement: 'header'
115+
},
116+
{
117+
value: 'combination',
118+
label: '混合布局',
119+
description: '多级导航组合模式',
120+
containerClass: '',
121+
showSidebar: true,
122+
sidebarClass: 'w-1/5',
123+
showHeader: true,
124+
headerClass: 'h-1/4',
125+
contentClass: '',
126+
showRightSidebar: true,
127+
rightSidebarClass: 'w-1/5',
128+
primaryElement: 'header',
129+
secondaryElement: 'sidebar'
130+
},
131+
{
132+
value: 'sidebar',
133+
label: '侧栏常驻',
134+
description: '二级菜单会始终打开',
135+
containerClass: '',
136+
showSidebar: true,
137+
sidebarClass: 'w-1/3',
138+
showHeader: true,
139+
headerClass: 'h-1/4',
140+
contentClass: '',
141+
showRightSidebar: false,
142+
primaryElement: 'sidebar'
143+
}
144+
]
145+
146+
const getSidebarStyle = (layout) => {
147+
if (layout.primaryElement === 'sidebar') {
148+
return { backgroundColor: primaryColor.value, opacity: '0.95' }
149+
} else if (layout.secondaryElement === 'sidebar') {
150+
return { backgroundColor: lighterPrimaryColor.value, opacity: '0.85' }
151+
} else {
152+
return { backgroundColor: lightestPrimaryColor.value, opacity: '0.6' }
153+
}
154+
}
155+
156+
const getHeaderStyle = (layout) => {
157+
if (layout.primaryElement === 'header') {
158+
return { backgroundColor: primaryColor.value, opacity: '0.95' }
159+
} else if (layout.secondaryElement === 'header') {
160+
return { backgroundColor: lighterPrimaryColor.value, opacity: '0.85' }
161+
} else {
162+
return { backgroundColor: lightestPrimaryColor.value, opacity: '0.6' }
163+
}
164+
}
165+
166+
const getContentStyle = (layout) => {
167+
return { backgroundColor: lightestPrimaryColor.value, opacity: '0.5' }
168+
}
169+
170+
const getRightSidebarStyle = (layout) => {
171+
if (layout.primaryElement === 'rightSidebar') {
172+
return { backgroundColor: primaryColor.value, opacity: '0.95' }
173+
} else if (layout.secondaryElement === 'rightSidebar') {
174+
return { backgroundColor: lighterPrimaryColor.value, opacity: '0.85' }
175+
} else {
176+
return { backgroundColor: lightestPrimaryColor.value, opacity: '0.6' }
177+
}
178+
}
179+
180+
const handleLayoutChange = (layout) => {
181+
emit('update:modelValue', layout)
182+
}
183+
</script>
184+
185+
<style scoped>
186+
.font-inter {
187+
font-family: 'Inter', sans-serif;
188+
}
189+
190+
.flex-col {
191+
flex-direction: column;
192+
}
193+
194+
.w-1\/4 {
195+
width: 25%;
196+
}
197+
198+
.w-1\/3 {
199+
width: 33.333333%;
200+
}
201+
202+
.w-1\/5 {
203+
width: 20%;
204+
}
205+
206+
.h-1\/4 {
207+
height: 25%;
208+
}
209+
210+
.h-1\/3 {
211+
height: 33.333333%;
212+
}
213+
214+
@media (max-width: 480px) {
215+
.grid-cols-2 {
216+
grid-template-columns: repeat(1, minmax(0, 1fr));
217+
}
218+
}
219+
</style>
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<template>
2+
<div class="flex items-center justify-between py-4 font-inter border-b border-gray-100 dark:border-gray-700 last:border-b-0">
3+
<div class="flex items-center gap-2">
4+
<span class="text-sm font-medium text-gray-900 dark:text-white">{{ label }}</span>
5+
<slot name="suffix"></slot>
6+
</div>
7+
<div class="flex items-center setting-controls">
8+
<slot></slot>
9+
</div>
10+
</div>
11+
</template>
12+
13+
<script setup>
14+
import { computed } from 'vue'
15+
import { storeToRefs } from 'pinia'
16+
import { useAppStore } from '@/pinia'
17+
18+
defineOptions({
19+
name: 'SettingItem'
20+
})
21+
22+
defineProps({
23+
label: {
24+
type: String,
25+
required: true
26+
}
27+
})
28+
29+
const appStore = useAppStore()
30+
const { config } = storeToRefs(appStore)
31+
32+
const primaryColor = computed(() => config.value.primaryColor)
33+
const primaryColorWithOpacity = computed(() => config.value.primaryColor + '40')
34+
</script>
35+
36+
<style scoped>
37+
.font-inter {
38+
font-family: 'Inter', sans-serif;
39+
}
40+
41+
.setting-controls {
42+
::v-deep(.el-switch) {
43+
--el-switch-on-color: v-bind(primaryColor);
44+
--el-switch-off-color: #d1d5db;
45+
}
46+
47+
::v-deep(.el-select) {
48+
.el-input__wrapper {
49+
border: 1px solid #e5e7eb;
50+
border-radius: 6px;
51+
transition: all 150ms ease-in-out;
52+
53+
&:hover {
54+
border-color: v-bind(primaryColor);
55+
transform: translateY(-1px);
56+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
57+
}
58+
59+
&.is-focus {
60+
border-color: v-bind(primaryColor);
61+
box-shadow: 0 0 0 2px v-bind(primaryColorWithOpacity);
62+
}
63+
}
64+
}
65+
66+
::v-deep(.el-input-number) {
67+
.el-input__wrapper {
68+
border: 1px solid #e5e7eb;
69+
border-radius: 6px;
70+
transition: all 150ms ease-in-out;
71+
72+
&:hover {
73+
border-color: v-bind(primaryColor);
74+
transform: translateY(-1px);
75+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
76+
}
77+
78+
&.is-focus {
79+
border-color: v-bind(primaryColor);
80+
box-shadow: 0 0 0 2px v-bind(primaryColorWithOpacity);
81+
}
82+
}
83+
}
84+
}
85+
86+
.dark .setting-controls {
87+
::v-deep(.el-switch) {
88+
--el-switch-off-color: #4b5563;
89+
}
90+
91+
::v-deep(.el-select) {
92+
.el-input__wrapper {
93+
border-color: #4b5563;
94+
background-color: #374151;
95+
96+
&:hover {
97+
border-color: v-bind(primaryColor);
98+
}
99+
}
100+
}
101+
102+
::v-deep(.el-input-number) {
103+
.el-input__wrapper {
104+
border-color: #4b5563;
105+
background-color: #374151;
106+
107+
&:hover {
108+
border-color: v-bind(primaryColor);
109+
}
110+
}
111+
}
112+
}
113+
</style>

0 commit comments

Comments
 (0)