feat: 重构设置界面,添加主题模式、颜色选择和布局配置模块,移除冗余组件 (#2050)
* feat: 重构设置界面,添加主题模式、颜色选择和布局配置模块,移除冗余组件 --------- Co-authored-by: PiexlMax(奇淼 <165128580+pixelmaxQm@users.noreply.github.com>
This commit is contained in:
219
web/src/view/layout/setting/components/layoutModeCard.vue
Normal file
219
web/src/view/layout/setting/components/layoutModeCard.vue
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
<template>
|
||||||
|
<div class="grid grid-cols-2 gap-6 font-inter px-6">
|
||||||
|
<div
|
||||||
|
v-for="layout in layoutModes"
|
||||||
|
:key="layout.value"
|
||||||
|
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"
|
||||||
|
:class="{
|
||||||
|
'ring-2 ring-offset-2 ring-offset-gray-50 dark:ring-offset-gray-900 transform -translate-y-1 shadow-xl': modelValue === layout.value
|
||||||
|
}"
|
||||||
|
:style="modelValue === layout.value ? {
|
||||||
|
borderColor: primaryColor,
|
||||||
|
ringColor: primaryColor + '40'
|
||||||
|
} : {}"
|
||||||
|
@click="handleLayoutChange(layout.value)"
|
||||||
|
>
|
||||||
|
<div class="flex justify-center mb-5">
|
||||||
|
<div
|
||||||
|
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"
|
||||||
|
:class="layout.containerClass"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="layout.showSidebar"
|
||||||
|
class="rounded-sm"
|
||||||
|
:class="[layout.sidebarClass]"
|
||||||
|
:style="getSidebarStyle(layout)"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div class="flex-1 flex flex-col gap-1.5">
|
||||||
|
<div
|
||||||
|
v-if="layout.showHeader"
|
||||||
|
class="rounded-sm"
|
||||||
|
:class="layout.headerClass"
|
||||||
|
:style="getHeaderStyle(layout)"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex-1 rounded-sm"
|
||||||
|
:class="layout.contentClass"
|
||||||
|
:style="getContentStyle(layout)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<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>
|
||||||
|
<span class="block text-sm text-gray-500 dark:text-gray-400">{{ layout.description }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useAppStore } from '@/pinia'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'LayoutModeCard'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: 'normal'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const { config } = storeToRefs(appStore)
|
||||||
|
|
||||||
|
const primaryColor = computed(() => config.value.primaryColor)
|
||||||
|
const lighterPrimaryColor = computed(() => {
|
||||||
|
const hex = config.value.primaryColor.replace('#', '')
|
||||||
|
const r = parseInt(hex.substr(0, 2), 16)
|
||||||
|
const g = parseInt(hex.substr(2, 2), 16)
|
||||||
|
const b = parseInt(hex.substr(4, 2), 16)
|
||||||
|
return `rgba(${r}, ${g}, ${b}, 0.7)`
|
||||||
|
})
|
||||||
|
const lightestPrimaryColor = computed(() => {
|
||||||
|
const hex = config.value.primaryColor.replace('#', '')
|
||||||
|
const r = parseInt(hex.substr(0, 2), 16)
|
||||||
|
const g = parseInt(hex.substr(2, 2), 16)
|
||||||
|
const b = parseInt(hex.substr(4, 2), 16)
|
||||||
|
return `rgba(${r}, ${g}, ${b}, 0.4)`
|
||||||
|
})
|
||||||
|
|
||||||
|
const layoutModes = [
|
||||||
|
{
|
||||||
|
value: 'normal',
|
||||||
|
label: '经典布局',
|
||||||
|
description: '左侧导航,顶部标题栏',
|
||||||
|
containerClass: '',
|
||||||
|
showSidebar: true,
|
||||||
|
sidebarClass: 'w-1/4',
|
||||||
|
showHeader: true,
|
||||||
|
headerClass: 'h-1/4',
|
||||||
|
contentClass: '',
|
||||||
|
showRightSidebar: false,
|
||||||
|
primaryElement: 'sidebar'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'head',
|
||||||
|
label: '顶部导航',
|
||||||
|
description: '水平导航栏布局',
|
||||||
|
containerClass: 'flex-col',
|
||||||
|
showSidebar: false,
|
||||||
|
showHeader: true,
|
||||||
|
headerClass: 'h-1/3',
|
||||||
|
contentClass: '',
|
||||||
|
showRightSidebar: false,
|
||||||
|
primaryElement: 'header'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'combination',
|
||||||
|
label: '混合布局',
|
||||||
|
description: '多级导航组合模式',
|
||||||
|
containerClass: '',
|
||||||
|
showSidebar: true,
|
||||||
|
sidebarClass: 'w-1/5',
|
||||||
|
showHeader: true,
|
||||||
|
headerClass: 'h-1/4',
|
||||||
|
contentClass: '',
|
||||||
|
showRightSidebar: true,
|
||||||
|
rightSidebarClass: 'w-1/5',
|
||||||
|
primaryElement: 'header',
|
||||||
|
secondaryElement: 'sidebar'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'sidebar',
|
||||||
|
label: '侧栏常驻',
|
||||||
|
description: '二级菜单会始终打开',
|
||||||
|
containerClass: '',
|
||||||
|
showSidebar: true,
|
||||||
|
sidebarClass: 'w-1/3',
|
||||||
|
showHeader: true,
|
||||||
|
headerClass: 'h-1/4',
|
||||||
|
contentClass: '',
|
||||||
|
showRightSidebar: false,
|
||||||
|
primaryElement: 'sidebar'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const getSidebarStyle = (layout) => {
|
||||||
|
if (layout.primaryElement === 'sidebar') {
|
||||||
|
return { backgroundColor: primaryColor.value, opacity: '0.95' }
|
||||||
|
} else if (layout.secondaryElement === 'sidebar') {
|
||||||
|
return { backgroundColor: lighterPrimaryColor.value, opacity: '0.85' }
|
||||||
|
} else {
|
||||||
|
return { backgroundColor: lightestPrimaryColor.value, opacity: '0.6' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getHeaderStyle = (layout) => {
|
||||||
|
if (layout.primaryElement === 'header') {
|
||||||
|
return { backgroundColor: primaryColor.value, opacity: '0.95' }
|
||||||
|
} else if (layout.secondaryElement === 'header') {
|
||||||
|
return { backgroundColor: lighterPrimaryColor.value, opacity: '0.85' }
|
||||||
|
} else {
|
||||||
|
return { backgroundColor: lightestPrimaryColor.value, opacity: '0.6' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getContentStyle = (layout) => {
|
||||||
|
return { backgroundColor: lightestPrimaryColor.value, opacity: '0.5' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRightSidebarStyle = (layout) => {
|
||||||
|
if (layout.primaryElement === 'rightSidebar') {
|
||||||
|
return { backgroundColor: primaryColor.value, opacity: '0.95' }
|
||||||
|
} else if (layout.secondaryElement === 'rightSidebar') {
|
||||||
|
return { backgroundColor: lighterPrimaryColor.value, opacity: '0.85' }
|
||||||
|
} else {
|
||||||
|
return { backgroundColor: lightestPrimaryColor.value, opacity: '0.6' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLayoutChange = (layout) => {
|
||||||
|
emit('update:modelValue', layout)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.font-inter {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-col {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-1\/4 {
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-1\/3 {
|
||||||
|
width: 33.333333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-1\/5 {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-1\/4 {
|
||||||
|
height: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-1\/3 {
|
||||||
|
height: 33.333333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.grid-cols-2 {
|
||||||
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
113
web/src/view/layout/setting/components/settingItem.vue
Normal file
113
web/src/view/layout/setting/components/settingItem.vue
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-between py-4 font-inter border-b border-gray-100 dark:border-gray-700 last:border-b-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm font-medium text-gray-900 dark:text-white">{{ label }}</span>
|
||||||
|
<slot name="suffix"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center setting-controls">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useAppStore } from '@/pinia'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'SettingItem'
|
||||||
|
})
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const { config } = storeToRefs(appStore)
|
||||||
|
|
||||||
|
const primaryColor = computed(() => config.value.primaryColor)
|
||||||
|
const primaryColorWithOpacity = computed(() => config.value.primaryColor + '40')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.font-inter {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-controls {
|
||||||
|
::v-deep(.el-switch) {
|
||||||
|
--el-switch-on-color: v-bind(primaryColor);
|
||||||
|
--el-switch-off-color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.el-select) {
|
||||||
|
.el-input__wrapper {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: v-bind(primaryColor);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-focus {
|
||||||
|
border-color: v-bind(primaryColor);
|
||||||
|
box-shadow: 0 0 0 2px v-bind(primaryColorWithOpacity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.el-input-number) {
|
||||||
|
.el-input__wrapper {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: v-bind(primaryColor);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-focus {
|
||||||
|
border-color: v-bind(primaryColor);
|
||||||
|
box-shadow: 0 0 0 2px v-bind(primaryColorWithOpacity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .setting-controls {
|
||||||
|
::v-deep(.el-switch) {
|
||||||
|
--el-switch-off-color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.el-select) {
|
||||||
|
.el-input__wrapper {
|
||||||
|
border-color: #4b5563;
|
||||||
|
background-color: #374151;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: v-bind(primaryColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.el-input-number) {
|
||||||
|
.el-input__wrapper {
|
||||||
|
border-color: #4b5563;
|
||||||
|
background-color: #374151;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: v-bind(primaryColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
152
web/src/view/layout/setting/components/themeColorPicker.vue
Normal file
152
web/src/view/layout/setting/components/themeColorPicker.vue
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
<template>
|
||||||
|
<div class="font-inter">
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-8 shadow-sm">
|
||||||
|
<div class="mb-8">
|
||||||
|
<p class="text-base font-semibold text-gray-700 dark:text-gray-300 mb-5">精选色彩</p>
|
||||||
|
<div class="grid grid-cols-3 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="colorItem in presetColors"
|
||||||
|
:key="colorItem.color"
|
||||||
|
class="flex items-center gap-4 p-4 bg-white dark:bg-gray-700 border-2 border-gray-200 dark:border-gray-600 rounded-xl cursor-pointer transition-all duration-150 ease-in-out hover:transform hover:-translate-y-1 hover:shadow-lg"
|
||||||
|
:class="{
|
||||||
|
'ring-2 ring-offset-2 ring-offset-gray-50 dark:ring-offset-gray-800 transform -translate-y-1 shadow-lg': modelValue === colorItem.color
|
||||||
|
}"
|
||||||
|
:style="modelValue === colorItem.color ? {
|
||||||
|
borderColor: colorItem.color,
|
||||||
|
ringColor: colorItem.color + '40'
|
||||||
|
} : {}"
|
||||||
|
@click="handleColorChange(colorItem.color)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="relative w-10 h-10 rounded-lg border border-gray-300 dark:border-gray-500 flex-shrink-0 shadow-sm"
|
||||||
|
:style="{ backgroundColor: colorItem.color }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="modelValue === colorItem.color"
|
||||||
|
class="absolute inset-0 flex items-center justify-center text-white text-base"
|
||||||
|
style="text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);"
|
||||||
|
>
|
||||||
|
<el-icon>
|
||||||
|
<Check />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<span class="block text-sm font-semibold text-gray-900 dark:text-white">{{ colorItem.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between p-5 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-xl mb-6 shadow-sm">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h4 class="text-base font-semibold text-gray-900 dark:text-white">自定义颜色</h4>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">选择任意颜色作为主题色</p>
|
||||||
|
</div>
|
||||||
|
<el-color-picker
|
||||||
|
v-model="customColor"
|
||||||
|
size="large"
|
||||||
|
:predefine="presetColors.map(item => item.color)"
|
||||||
|
@change="handleCustomColorChange"
|
||||||
|
class="custom-color-picker"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-xl p-5 shadow-sm">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-base font-semibold text-gray-700 dark:text-gray-300">当前主题色</span>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="w-6 h-6 rounded-lg border border-gray-300 dark:border-gray-500 shadow-sm"
|
||||||
|
:style="{ backgroundColor: modelValue }"
|
||||||
|
></div>
|
||||||
|
<code class="text-sm font-mono bg-gray-100 dark:bg-gray-600 text-gray-700 dark:text-gray-300 px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-500">
|
||||||
|
{{ modelValue }}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { Check } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ThemeColorPicker'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: '#3b82f6'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const customColor = ref(props.modelValue)
|
||||||
|
|
||||||
|
const presetColors = [
|
||||||
|
{ color: '#4E80EE', name: '默认' },
|
||||||
|
{ color: '#8bb5d1', name: '晨雾蓝' },
|
||||||
|
{ color: '#a8c8a8', name: '薄荷绿' },
|
||||||
|
{ color: '#d4a5a5', name: '玫瑰粉' },
|
||||||
|
{ color: '#c8a8d8', name: '薰衣草' },
|
||||||
|
{ color: '#f0c674', name: '暖阳黄' },
|
||||||
|
{ color: '#b8b8b8', name: '月光银' },
|
||||||
|
{ color: '#d8a8a8', name: '珊瑚橙' },
|
||||||
|
{ color: '#a8d8d8', name: '海雾青' },
|
||||||
|
{ color: '#c8c8a8', name: '橄榄绿' },
|
||||||
|
{ color: '#d8c8a8', name: '奶茶棕' },
|
||||||
|
{ color: '#a8a8d8', name: '梦幻紫' },
|
||||||
|
{ color: '#c8d8a8', name: '抹茶绿' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleColorChange = (color) => {
|
||||||
|
customColor.value = color
|
||||||
|
emit('update:modelValue', color)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCustomColorChange = (color) => {
|
||||||
|
if (color) {
|
||||||
|
emit('update:modelValue', color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (newValue) => {
|
||||||
|
customColor.value = newValue
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.font-inter {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-color-picker {
|
||||||
|
::v-deep(.el-color-picker__trigger) {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #9ca3af;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .custom-color-picker {
|
||||||
|
::v-deep(.el-color-picker__trigger) {
|
||||||
|
border-color: #4b5563;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #6b7280;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
70
web/src/view/layout/setting/components/themeModeSelector.vue
Normal file
70
web/src/view/layout/setting/components/themeModeSelector.vue
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="inline-flex bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-1 gap-1">
|
||||||
|
<div
|
||||||
|
v-for="mode in themeModes"
|
||||||
|
:key="mode.value"
|
||||||
|
class="flex flex-col items-center justify-center px-4 py-3 rounded-md cursor-pointer transition-all duration-150 ease-in-out min-w-[64px]"
|
||||||
|
:class="[
|
||||||
|
modelValue === mode.value
|
||||||
|
? 'text-white shadow-sm transform -translate-y-0.5'
|
||||||
|
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
|
||||||
|
]"
|
||||||
|
:style="modelValue === mode.value ? { backgroundColor: primaryColor } : {}"
|
||||||
|
@click="handleModeChange(mode.value)"
|
||||||
|
>
|
||||||
|
<el-icon class="text-lg mb-1">
|
||||||
|
<component :is="mode.icon" />
|
||||||
|
</el-icon>
|
||||||
|
<span class="text-xs font-medium">{{ mode.label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { Sunny, Moon, Monitor } from '@element-plus/icons-vue'
|
||||||
|
import { useAppStore } from '@/pinia'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ThemeModeSelector'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: 'auto'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const { config } = storeToRefs(appStore)
|
||||||
|
|
||||||
|
const primaryColor = computed(() => config.value.primaryColor)
|
||||||
|
|
||||||
|
const themeModes = [
|
||||||
|
{
|
||||||
|
value: 'light',
|
||||||
|
label: '浅色',
|
||||||
|
icon: Sunny
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'dark',
|
||||||
|
label: '深色',
|
||||||
|
icon: Moon
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'auto',
|
||||||
|
label: '跟随系统',
|
||||||
|
icon: Monitor
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleModeChange = (mode) => {
|
||||||
|
emit('update:modelValue', mode)
|
||||||
|
}
|
||||||
|
</script>
|
@@ -5,214 +5,175 @@
|
|||||||
direction="rtl"
|
direction="rtl"
|
||||||
:size="width"
|
:size="width"
|
||||||
:show-close="false"
|
:show-close="false"
|
||||||
|
class="theme-config-drawer"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex items-center justify-between w-full px-6 py-4 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
|
||||||
<span class="text-lg">系统配置</span>
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white font-inter">系统配置</h2>
|
||||||
<el-button type="primary" @click="resetConfig">重置配置</el-button>
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
class="reset-btn"
|
||||||
|
:style="{ backgroundColor: config.primaryColor, borderColor: config.primaryColor }"
|
||||||
|
@click="resetConfig"
|
||||||
|
>
|
||||||
|
重置配置
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex flex-col">
|
|
||||||
<div class="mb-8">
|
<div class="h-full bg-white dark:bg-gray-900">
|
||||||
<Title title="默认主题"></Title>
|
<div class="px-8 pt-4 pb-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
<div class="mt-2 text-sm p-2 flex items-center justify-center gap-2">
|
<div class="flex justify-center">
|
||||||
<el-segmented
|
<div class="inline-flex bg-gray-100 dark:bg-gray-800 rounded-xl p-1.5 border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||||
v-model="config.darkMode"
|
<button
|
||||||
:options="options"
|
v-for="tab in tabs"
|
||||||
size="default"
|
:key="tab.key"
|
||||||
@change="appStore.toggleDarkMode"
|
class="px-6 py-3 text-base font-medium rounded-lg transition-all duration-150 ease-in-out min-w-[80px]"
|
||||||
/>
|
:class="[
|
||||||
</div>
|
activeTab === tab.key
|
||||||
</div>
|
? 'text-white shadow-md transform -translate-y-0.5'
|
||||||
<div class="mb-8">
|
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
|
||||||
<Title title="主题色"></Title>
|
]"
|
||||||
<div class="mt-2 text-sm p-2 flex items-center gap-2 justify-center">
|
:style="activeTab === tab.key ? { backgroundColor: config.primaryColor } : {}"
|
||||||
<div
|
@click="activeTab = tab.key"
|
||||||
v-for="item in colors"
|
|
||||||
:key="item"
|
|
||||||
class="w-5 h-5 rounded cursor-pointer flex items-center justify-center"
|
|
||||||
:style="`background:${item}`"
|
|
||||||
@click="appStore.togglePrimaryColor(item)"
|
|
||||||
>
|
>
|
||||||
<el-icon v-if="config.primaryColor === item">
|
{{ tab.label }}
|
||||||
<Select />
|
</button>
|
||||||
</el-icon>
|
|
||||||
</div>
|
|
||||||
<el-color-picker
|
|
||||||
v-model="customColor"
|
|
||||||
@change="appStore.togglePrimaryColor"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-8">
|
|
||||||
<Title title="主题配置"></Title>
|
|
||||||
<div class="mt-2 text-md p-2 flex flex-col gap-2">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>展示水印</div>
|
|
||||||
<el-switch
|
|
||||||
v-model="config.show_watermark"
|
|
||||||
@change="appStore.toggleConfigWatermark"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>灰色模式</div>
|
|
||||||
<el-switch v-model="config.grey" @change="appStore.toggleGrey" />
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>色弱模式</div>
|
|
||||||
<el-switch
|
|
||||||
v-model="config.weakness"
|
|
||||||
@change="appStore.toggleWeakness"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>菜单模式</div>
|
|
||||||
<el-segmented
|
|
||||||
v-model="config.side_mode"
|
|
||||||
:options="sideModes"
|
|
||||||
size="default"
|
|
||||||
@change="appStore.toggleSideMode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>显示标签页</div>
|
|
||||||
<el-switch
|
|
||||||
v-model="config.showTabs"
|
|
||||||
@change="appStore.toggleTabs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
|
||||||
<div class="flex-shrink-0">页面切换动画</div>
|
|
||||||
<el-select
|
|
||||||
v-model="config.transition_type"
|
|
||||||
@change="appStore.toggleTransition"
|
|
||||||
class="w-40"
|
|
||||||
>
|
|
||||||
<el-option value="fade" label="淡入淡出" />
|
|
||||||
<el-option value="slide" label="滑动" />
|
|
||||||
<el-option value="zoom" label="缩放" />
|
|
||||||
<el-option value="none" label="无动画" />
|
|
||||||
</el-select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-8">
|
<div class="pb-8 h-full overflow-y-auto">
|
||||||
<Title title="layout 大小配置"></Title>
|
<div class="transition-all duration-300 ease-in-out">
|
||||||
<div class="mt-2 text-md p-2 flex flex-col gap-2">
|
<AppearanceSettings v-if="activeTab === 'appearance'" />
|
||||||
<div class="flex items-center justify-between mb-2">
|
<LayoutSettings v-else-if="activeTab === 'layout'" />
|
||||||
<div>侧边栏展开宽度</div>
|
<GeneralSettings v-else-if="activeTab === 'general'" />
|
||||||
<el-input-number
|
|
||||||
v-model="config.layout_side_width"
|
|
||||||
:min="150"
|
|
||||||
:max="400"
|
|
||||||
:step="10"
|
|
||||||
></el-input-number>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between mb-2">
|
|
||||||
<div>侧边栏收缩宽度</div>
|
|
||||||
<el-input-number
|
|
||||||
v-model="config.layout_side_collapsed_width"
|
|
||||||
:min="60"
|
|
||||||
:max="100"
|
|
||||||
></el-input-number>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between mb-2">
|
|
||||||
<div>侧边栏子项高度</div>
|
|
||||||
<el-input-number
|
|
||||||
v-model="config.layout_side_item_height"
|
|
||||||
:min="30"
|
|
||||||
:max="50"
|
|
||||||
></el-input-number>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <el-alert type="warning" :closable="false">
|
|
||||||
请注意,所有配置请保存到本地文件的
|
|
||||||
<el-tag>config.json</el-tag> 文件中,否则刷新页面后会丢失配置
|
|
||||||
</el-alert>-->
|
|
||||||
</div>
|
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useAppStore } from '@/pinia'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { ref, computed } from 'vue'
|
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useAppStore } from '@/pinia'
|
||||||
import { setSelfSetting } from '@/api/user'
|
import { setSelfSetting } from '@/api/user'
|
||||||
import Title from './title.vue'
|
import AppearanceSettings from './modules/appearance/index.vue'
|
||||||
import { watch } from 'vue';
|
import LayoutSettings from './modules/layout/index.vue'
|
||||||
|
import GeneralSettings from './modules/general/index.vue'
|
||||||
|
|
||||||
const appStore = useAppStore()
|
|
||||||
const { config, device } = storeToRefs(appStore)
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'GvaSetting'
|
name: 'GvaSetting'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const { config, device } = storeToRefs(appStore)
|
||||||
|
|
||||||
|
const activeTab = ref('appearance')
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ key: 'appearance', label: '外观' },
|
||||||
|
{ key: 'layout', label: '布局' },
|
||||||
|
{ key: 'general', label: '通用' }
|
||||||
|
]
|
||||||
|
|
||||||
const width = computed(() => {
|
const width = computed(() => {
|
||||||
return device.value === 'mobile' ? '100%' : '500px'
|
return device.value === 'mobile' ? '100%' : '500px'
|
||||||
})
|
})
|
||||||
|
|
||||||
const colors = [
|
|
||||||
'#EB2F96',
|
|
||||||
'#3b82f6',
|
|
||||||
'#2FEB54',
|
|
||||||
'#EBEB2F',
|
|
||||||
'#EB2F2F',
|
|
||||||
'#2FEBEB'
|
|
||||||
]
|
|
||||||
|
|
||||||
const drawer = defineModel('drawer', {
|
const drawer = defineModel('drawer', {
|
||||||
default: true,
|
default: true,
|
||||||
type: Boolean
|
type: Boolean
|
||||||
})
|
})
|
||||||
|
|
||||||
const options = ['dark', 'light', 'auto']
|
|
||||||
const sideModes = [
|
|
||||||
{
|
|
||||||
label: '正常模式',
|
|
||||||
value: 'normal'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '顶部菜单栏模式',
|
|
||||||
value: 'head'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '组合模式',
|
|
||||||
value: 'combination'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '侧边栏常驻',
|
|
||||||
value: 'sidebar'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const saveConfig = async () => {
|
const saveConfig = async () => {
|
||||||
const res = await setSelfSetting(config.value)
|
const res = await setSelfSetting(config.value)
|
||||||
console.log(config.value)
|
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
localStorage.setItem('originSetting', JSON.stringify(config.value))
|
localStorage.setItem('originSetting', JSON.stringify(config.value))
|
||||||
ElMessage.success('保存成功')
|
ElMessage.success('保存成功')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const customColor = ref('')
|
|
||||||
|
|
||||||
const resetConfig = () => {
|
const resetConfig = () => {
|
||||||
appStore.resetConfig()
|
appStore.resetConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
watch(config, async () => {
|
watch(config, async () => {
|
||||||
await saveConfig();
|
await saveConfig();
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
::v-deep(.el-drawer__header) {
|
.theme-config-drawer {
|
||||||
@apply border-gray-400 dark:border-gray-600;
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
|
||||||
|
::v-deep(.el-drawer) {
|
||||||
|
background: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::v-deep(.el-drawer__header) {
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.el-drawer__body) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .theme-config-drawer {
|
||||||
|
::v-deep(.el-drawer) {
|
||||||
|
background: #111827;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-inter {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
filter: brightness(0.9);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar for webkit browsers */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #d1d5db;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark ::-webkit-scrollbar-track {
|
||||||
|
background: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark ::-webkit-scrollbar-thumb {
|
||||||
|
background: #4b5563;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #6b7280;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
114
web/src/view/layout/setting/modules/appearance/index.vue
Normal file
114
web/src/view/layout/setting/modules/appearance/index.vue
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<template>
|
||||||
|
<div class="font-inter">
|
||||||
|
<!-- Theme Mode Section -->
|
||||||
|
<div class="mb-10">
|
||||||
|
<div class="flex items-center justify-center mb-6">
|
||||||
|
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
|
||||||
|
<span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">主题模式</span>
|
||||||
|
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-content">
|
||||||
|
<ThemeModeSelector
|
||||||
|
v-model="config.darkMode"
|
||||||
|
@update:modelValue="appStore.toggleDarkMode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Theme Color Section -->
|
||||||
|
<div class="mb-10">
|
||||||
|
<div class="flex items-center justify-center mb-6">
|
||||||
|
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
|
||||||
|
<span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">主题颜色</span>
|
||||||
|
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-content">
|
||||||
|
<ThemeColorPicker
|
||||||
|
v-model="config.primaryColor"
|
||||||
|
@update:modelValue="appStore.togglePrimaryColor"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Visual Accessibility Section -->
|
||||||
|
<div class="mb-10">
|
||||||
|
<div class="flex items-center justify-center mb-6">
|
||||||
|
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
|
||||||
|
<span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">视觉辅助</span>
|
||||||
|
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-content">
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 shadow-sm">
|
||||||
|
<SettingItem label="灰色模式">
|
||||||
|
<template #suffix>
|
||||||
|
<span class="text-xs text-gray-400 dark:text-gray-500 ml-2">降低色彩饱和度</span>
|
||||||
|
</template>
|
||||||
|
<el-switch
|
||||||
|
v-model="config.grey"
|
||||||
|
@change="appStore.toggleGrey"
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
<SettingItem label="色弱模式">
|
||||||
|
<template #suffix>
|
||||||
|
<span class="text-xs text-gray-400 dark:text-gray-500 ml-2">优化色彩对比度</span>
|
||||||
|
</template>
|
||||||
|
<el-switch
|
||||||
|
v-model="config.weakness"
|
||||||
|
@change="appStore.toggleWeakness"
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
<SettingItem label="显示水印">
|
||||||
|
<template #suffix>
|
||||||
|
<span class="text-xs text-gray-400 dark:text-gray-500 ml-2">在页面显示水印标识</span>
|
||||||
|
</template>
|
||||||
|
<el-switch
|
||||||
|
v-model="config.show_watermark"
|
||||||
|
@change="appStore.toggleConfigWatermark"
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useAppStore } from '@/pinia'
|
||||||
|
import ThemeModeSelector from '../../components/themeModeSelector.vue'
|
||||||
|
import ThemeColorPicker from '../../components/themeColorPicker.vue'
|
||||||
|
import SettingItem from '../../components/settingItem.vue'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'AppearanceSettings'
|
||||||
|
})
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const { config } = storeToRefs(appStore)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.font-inter {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content {
|
||||||
|
animation: fadeInUp 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(12px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
288
web/src/view/layout/setting/modules/general/index.vue
Normal file
288
web/src/view/layout/setting/modules/general/index.vue
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
<template>
|
||||||
|
<div class="font-inter">
|
||||||
|
<div class="mb-10">
|
||||||
|
<div class="flex items-center justify-center mb-6">
|
||||||
|
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
|
||||||
|
<span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">系统信息</span>
|
||||||
|
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-content">
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 shadow-sm">
|
||||||
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div class="flex justify-between items-center py-3 border-b border-gray-200 dark:border-gray-600">
|
||||||
|
<span class="text-gray-600 dark:text-gray-400 font-medium">版本</span>
|
||||||
|
<span class="font-mono text-gray-900 dark:text-white font-semibold">v2.7.4</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center py-3 border-b border-gray-200 dark:border-gray-600">
|
||||||
|
<span class="text-gray-600 dark:text-gray-400 font-medium">前端框架</span>
|
||||||
|
<span class="font-mono text-gray-900 dark:text-white font-semibold">Vue 3</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center py-3 border-b border-gray-200 dark:border-gray-600">
|
||||||
|
<span class="text-gray-600 dark:text-gray-400 font-medium">UI 组件库</span>
|
||||||
|
<span class="font-mono text-gray-900 dark:text-white font-semibold">Element Plus</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center py-3 border-b border-gray-200 dark:border-gray-600">
|
||||||
|
<span class="text-gray-600 dark:text-gray-400 font-medium">构建工具</span>
|
||||||
|
<span class="font-mono text-gray-900 dark:text-white font-semibold">Vite</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center py-3">
|
||||||
|
<span class="text-gray-600 dark:text-gray-400 font-medium">浏览器</span>
|
||||||
|
<span class="font-mono text-gray-900 dark:text-white font-semibold">{{ browserInfo }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center py-3">
|
||||||
|
<span class="text-gray-600 dark:text-gray-400 font-medium">屏幕分辨率</span>
|
||||||
|
<span class="font-mono text-gray-900 dark:text-white font-semibold">{{ screenResolution }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-10">
|
||||||
|
<div class="flex items-center justify-center mb-6">
|
||||||
|
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
|
||||||
|
<span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">配置管理</span>
|
||||||
|
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-content">
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 shadow-sm">
|
||||||
|
<div class="space-y-5">
|
||||||
|
<div class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 flex items-center justify-between hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-12 h-12 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl flex items-center justify-center text-red-600 dark:text-red-400 text-xl">
|
||||||
|
🔄
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">重置配置</h4>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">将所有设置恢复为默认值</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
class="rounded-lg font-medium transition-all duration-150 ease-in-out hover:-translate-y-0.5"
|
||||||
|
@click="handleResetConfig"
|
||||||
|
>
|
||||||
|
重置配置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 flex items-center justify-between hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-12 h-12 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-xl flex items-center justify-center text-blue-600 dark:text-blue-400 text-xl">
|
||||||
|
📤
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">导出配置</h4>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">导出当前配置为 JSON 文件</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
class="rounded-lg font-medium transition-all duration-150 ease-in-out hover:-translate-y-0.5"
|
||||||
|
:style="{ backgroundColor: config.primaryColor, borderColor: config.primaryColor }"
|
||||||
|
@click="handleExportConfig"
|
||||||
|
>
|
||||||
|
导出配置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 flex items-center justify-between hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-12 h-12 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-xl flex items-center justify-center text-green-600 dark:text-green-400 text-xl">
|
||||||
|
📥
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">导入配置</h4>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">从 JSON 文件导入配置</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-upload
|
||||||
|
ref="uploadRef"
|
||||||
|
:auto-upload="false"
|
||||||
|
:show-file-list="false"
|
||||||
|
accept=".json"
|
||||||
|
@change="handleImportConfig"
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
class="rounded-lg font-medium transition-all duration-150 ease-in-out hover:-translate-y-0.5"
|
||||||
|
>
|
||||||
|
导入配置
|
||||||
|
</el-button>
|
||||||
|
</el-upload>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-10">
|
||||||
|
<div class="flex items-center justify-center mb-6">
|
||||||
|
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
|
||||||
|
<span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">关于项目</span>
|
||||||
|
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-content">
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 shadow-sm">
|
||||||
|
<div class="flex items-start gap-5">
|
||||||
|
<div class="w-16 h-16 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-xl flex items-center justify-center flex-shrink-0 shadow-sm">
|
||||||
|
<img
|
||||||
|
src="/logo.png"
|
||||||
|
alt="Gin-Vue-Admin Logo"
|
||||||
|
class="w-10 h-10 object-contain"
|
||||||
|
@error="handleLogoError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h4 class="text-xl font-semibold text-gray-900 dark:text-white mb-3">Gin-Vue-Admin</h4>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-5 leading-relaxed">
|
||||||
|
基于 Vue3 + Gin 的全栈开发基础平台,提供完整的后台管理解决方案
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center gap-3 text-sm">
|
||||||
|
<a
|
||||||
|
href="https://github.com/flipped-aurora/gin-vue-admin"
|
||||||
|
target="_blank"
|
||||||
|
class="font-medium transition-colors duration-150 hover:underline"
|
||||||
|
:style="{ color: config.primaryColor }"
|
||||||
|
>
|
||||||
|
GitHub 仓库
|
||||||
|
</a>
|
||||||
|
<span class="text-gray-400 dark:text-gray-500">·</span>
|
||||||
|
<a
|
||||||
|
href="https://www.gin-vue-admin.com/"
|
||||||
|
target="_blank"
|
||||||
|
class="font-medium transition-colors duration-150 hover:underline"
|
||||||
|
:style="{ color: config.primaryColor }"
|
||||||
|
>
|
||||||
|
官方文档
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useAppStore } from '@/pinia'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'GeneralSettings'
|
||||||
|
})
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const { config } = storeToRefs(appStore)
|
||||||
|
const uploadRef = ref()
|
||||||
|
|
||||||
|
const browserInfo = ref('')
|
||||||
|
const screenResolution = ref('')
|
||||||
|
const logoUrl = ref('')
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const userAgent = navigator.userAgent
|
||||||
|
if (userAgent.includes('Chrome')) {
|
||||||
|
browserInfo.value = 'Chrome'
|
||||||
|
} else if (userAgent.includes('Firefox')) {
|
||||||
|
browserInfo.value = 'Firefox'
|
||||||
|
} else if (userAgent.includes('Safari')) {
|
||||||
|
browserInfo.value = 'Safari'
|
||||||
|
} else if (userAgent.includes('Edge')) {
|
||||||
|
browserInfo.value = 'Edge'
|
||||||
|
} else {
|
||||||
|
browserInfo.value = 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
screenResolution.value = `${screen.width}×${screen.height}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleLogoError = () => {
|
||||||
|
logoUrl.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResetConfig = async () => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'确定要重置所有配置吗?此操作不可撤销。',
|
||||||
|
'重置配置',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
appStore.resetConfig()
|
||||||
|
ElMessage.success('配置已重置')
|
||||||
|
} catch {
|
||||||
|
// User cancelled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExportConfig = () => {
|
||||||
|
const configData = JSON.stringify(config.value, null, 2)
|
||||||
|
const blob = new Blob([configData], { type: 'application/json' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = `gin-vue-admin-config-${new Date().toISOString().split('T')[0]}.json`
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
|
||||||
|
ElMessage.success('配置已导出')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImportConfig = (file) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const importedConfig = JSON.parse(e.target.result)
|
||||||
|
|
||||||
|
Object.keys(importedConfig).forEach(key => {
|
||||||
|
if (key in config.value) {
|
||||||
|
config.value[key] = importedConfig[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ElMessage.success('配置已导入')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('配置文件格式错误')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.readAsText(file.raw)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.font-inter {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content {
|
||||||
|
animation: fadeInUp 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(12px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
164
web/src/view/layout/setting/modules/layout/index.vue
Normal file
164
web/src/view/layout/setting/modules/layout/index.vue
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
<template>
|
||||||
|
<div class="font-inter">
|
||||||
|
<div class="mb-10">
|
||||||
|
<div class="flex items-center justify-center mb-6">
|
||||||
|
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
|
||||||
|
<span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">布局模式</span>
|
||||||
|
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-content">
|
||||||
|
<LayoutModeCard
|
||||||
|
v-model="config.side_mode"
|
||||||
|
@update:modelValue="appStore.toggleSideMode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-10">
|
||||||
|
<div class="flex items-center justify-center mb-6">
|
||||||
|
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
|
||||||
|
<span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">界面配置</span>
|
||||||
|
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-content">
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 shadow-sm">
|
||||||
|
<SettingItem label="显示标签页">
|
||||||
|
<template #suffix>
|
||||||
|
<span class="text-xs text-gray-400 dark:text-gray-500 ml-2">页面标签导航</span>
|
||||||
|
</template>
|
||||||
|
<el-switch
|
||||||
|
v-model="config.showTabs"
|
||||||
|
@change="appStore.toggleTabs"
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
<SettingItem label="页面切换动画">
|
||||||
|
<template #suffix>
|
||||||
|
<span class="text-xs text-gray-400 dark:text-gray-500 ml-2">页面过渡效果</span>
|
||||||
|
</template>
|
||||||
|
<el-select
|
||||||
|
v-model="config.transition_type"
|
||||||
|
@change="appStore.toggleTransition"
|
||||||
|
class="w-32"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<el-option value="fade" label="淡入淡出" />
|
||||||
|
<el-option value="slide" label="滑动" />
|
||||||
|
<el-option value="zoom" label="缩放" />
|
||||||
|
<el-option value="none" label="无动画" />
|
||||||
|
</el-select>
|
||||||
|
</SettingItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-10">
|
||||||
|
<div class="flex items-center justify-center mb-6">
|
||||||
|
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
|
||||||
|
<span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">尺寸配置</span>
|
||||||
|
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-content">
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 shadow-sm">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-medium text-gray-900 dark:text-white">侧边栏展开宽度</h4>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">侧边栏完全展开时的宽度</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<el-input-number
|
||||||
|
v-model="config.layout_side_width"
|
||||||
|
:min="150"
|
||||||
|
:max="400"
|
||||||
|
:step="10"
|
||||||
|
size="small"
|
||||||
|
class="w-24"
|
||||||
|
/>
|
||||||
|
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">px</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-medium text-gray-900 dark:text-white">侧边栏收缩宽度</h4>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">侧边栏收缩时的最小宽度</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<el-input-number
|
||||||
|
v-model="config.layout_side_collapsed_width"
|
||||||
|
:min="60"
|
||||||
|
:max="100"
|
||||||
|
size="small"
|
||||||
|
class="w-24"
|
||||||
|
/>
|
||||||
|
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">px</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-medium text-gray-900 dark:text-white">菜单项高度</h4>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">侧边栏菜单项的行高</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<el-input-number
|
||||||
|
v-model="config.layout_side_item_height"
|
||||||
|
:min="30"
|
||||||
|
:max="50"
|
||||||
|
size="small"
|
||||||
|
class="w-24"
|
||||||
|
/>
|
||||||
|
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">px</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useAppStore } from '@/pinia'
|
||||||
|
import LayoutModeCard from '../../components/layoutModeCard.vue'
|
||||||
|
import SettingItem from '../../components/settingItem.vue'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'LayoutSettings'
|
||||||
|
})
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const { config } = storeToRefs(appStore)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.font-inter {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content {
|
||||||
|
animation: fadeInUp 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(12px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@@ -1,34 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="title relative my-2">
|
|
||||||
<div class="flex-shrink-0 text-center text-xl text-gray-600">
|
|
||||||
{{ title }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
defineOptions({
|
|
||||||
name: 'layoutSettingTitle'
|
|
||||||
})
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
title: String
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title::before,
|
|
||||||
.title::after {
|
|
||||||
content: '';
|
|
||||||
height: 1px;
|
|
||||||
width: 100%;
|
|
||||||
background-color: #e3e3e3;
|
|
||||||
}
|
|
||||||
</style>
|
|
Reference in New Issue
Block a user