refactor(settings): 将设置面板拆分为多个组件

- 新增 DisplayModeSettings、ThemeSettings 和 InterfaceSettings 组件
- 重构 SettingsPanel 组件,使用新组件替换原有的设置项
- 删除 ThemeSwitcher 组件
- 更新样式和布局,优化用户体验
This commit is contained in:
Lexcubia 2025-04-22 16:43:32 +08:00
parent f65dd67087
commit 995eb85166
6 changed files with 217 additions and 275 deletions

View File

@ -0,0 +1,82 @@
<template>
<div>
<h4>整体外观</h4>
<el-radio-group
v-model="currentDisplayMode"
@change="changeDisplayMode"
size="small"
class="display-mode-group"
>
<el-radio-button label="system">
<span class="icon system"></span> 默认
</el-radio-button>
<el-radio-button label="light">
<span class="icon light"></span> 浅色
</el-radio-button>
<el-radio-button label="dark">
<span class="icon dark"></span> 深色
</el-radio-button>
</el-radio-group>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useSettingsStore } from '@/store/settings';
import { ElRadioGroup, ElRadioButton } from 'element-plus';
const settingsStore = useSettingsStore();
const currentDisplayMode = ref(settingsStore.displayMode);
const changeDisplayMode = (newMode) => {
settingsStore.updateDisplayMode(newMode);
currentDisplayMode.value = newMode;
};
</script>
<style lang="scss" scoped>
.display-mode-group {
display: flex;
width: 100%;
margin-bottom: 5px;
gap: 8px;
.el-radio-button {
flex: 1;
:deep(.el-radio-button__inner) {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px !important;
border: 1px solid var(--el-border-color-lighter);
box-shadow: none;
padding: 8px 5px;
font-size: 12px;
.icon {
display: inline-block;
width: 14px;
height: 14px;
margin-right: 5px;
border-radius: 2px;
border: 1px solid var(--el-border-color);
&.system { background: linear-gradient(135deg, #fff 50%, #222 50%); border-color: #888; }
&.light { background-color: #fff; }
&.dark { background-color: #222; border-color: #555; }
}
}
&.is-active :deep(.el-radio-button__inner) {
background-color: var(--el-color-primary-light-9);
border-color: var(--el-color-primary);
color: var(--el-color-primary);
box-shadow: none;
}
}
.el-radio-button + .el-radio-button {
margin-left: 0;
}
}
</style>

View File

@ -0,0 +1,37 @@
<template>
<div>
<h4>界面设置</h4>
<div class="setting-item">
<span>启用动画效果</span>
<el-switch
v-model="animationsEnabled"
@change="toggleAnimationsSetting"
/>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useSettingsStore } from '@/store/settings';
import { ElSwitch } from 'element-plus';
const settingsStore = useSettingsStore();
const animationsEnabled = ref(settingsStore.animationsEnabled);
const toggleAnimationsSetting = (value) => {
settingsStore.toggleAnimations(value);
animationsEnabled.value = value;
};
</script>
<style lang="scss" scoped>
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
font-size: 13px;
color: var(--el-text-color-regular);
}
</style>

View File

@ -1,243 +1,35 @@
<template>
<div class="settings-popover">
<h4>整体外观</h4>
<el-radio-group
v-model="currentDisplayMode"
@change="changeDisplayMode"
size="small"
class="display-mode-group"
>
<el-radio-button label="system">
<span class="icon system"></span> 系统默认
</el-radio-button>
<el-radio-button label="light">
<span class="icon light"></span> 浅色
</el-radio-button>
<el-radio-button label="dark">
<span class="icon dark"></span> 深色
</el-radio-button>
</el-radio-group>
<DisplayModeSettings />
<el-divider />
<h4>主题</h4>
<div class="theme-grid">
<div
v-for="(theme, name) in filteredThemes"
:key="name"
class="theme-option"
:class="{ 'active': currentTheme === name }"
@click="changeTheme(name)"
>
<div class="theme-preview" :style="{ backgroundColor: theme.primary }"></div>
<span class="theme-label">{{ getThemeLabel(name) }}</span>
</div>
<!-- 可以添加 '选择一种颜色...' 的选项 -->
</div>
<ThemeSettings />
<el-divider />
<h4>界面设置</h4>
<div class="setting-item">
<span>启用动画效果</span>
<el-switch
v-model="animationsEnabled"
@change="toggleAnimationsSetting"
/>
</div>
<!-- 可以添加更多设置项 -->
<!--
<el-divider />
<h4>其他设置</h4>
<div class="setting-item">
<span>收起侧边栏</span>
<el-switch v-model="sidebarCollapsed" @change="toggleSidebarSetting" />
</div>
-->
<InterfaceSettings />
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useSettingsStore } from '@/store/settings';
// Import themes object and omit 'dark' theme from selection
import { themes as availableThemes } from '@/styles/theme';
import {
ElButton, ElDropdown, ElDropdownMenu, ElDropdownItem,
ElDivider, ElSwitch, ElIcon, ElRadioGroup, ElRadioButton
} from 'element-plus';
import { ArrowDown } from '@element-plus/icons-vue'; // Keep if needed elsewhere, remove if not
const settingsStore = useSettingsStore();
// --- Display Mode ---
const currentDisplayMode = ref(settingsStore.displayMode);
const changeDisplayMode = (newMode) => {
settingsStore.updateDisplayMode(newMode);
// currentDisplayMode will update reactively from the store if using computed,
// but direct update here ensures immediate visual feedback if needed
currentDisplayMode.value = newMode;
};
// --- Theme Settings ---
const currentTheme = ref(settingsStore.theme);
// Filter out the 'dark' theme as it's handled by displayMode
const filteredThemes = computed(() => {
const { dark, ...rest } = availableThemes;
return rest;
});
const themeLabels = {
default: '默认',
// dark: '', // No longer selectable as a base theme color palette
fresh: '清新',
warm: '暖色',
business: '商务',
// Add labels for other themes from your image if they exist in styles/theme.js
// e.g., cool: '', pink: '', bubblegum: '', sunny: '', etc.
// Make sure the keys match the keys in your `themes` object in theme.js
};
const getThemeLabel = (themeName) => {
return themeLabels[themeName] || themeName;
};
const changeTheme = (newTheme) => {
settingsStore.updateTheme(newTheme); // Use the action
currentTheme.value = newTheme; // Update local ref for immediate feedback
};
// --- Animation Settings ---
const animationsEnabled = ref(settingsStore.animationsEnabled);
const toggleAnimationsSetting = (value) => {
settingsStore.toggleAnimations(value); // Use the action
animationsEnabled.value = value; // Update local ref
};
// Watch for external store changes (e.g., reset button) if necessary
// settingsStore.$subscribe((mutation, state) => {
// currentDisplayMode.value = state.displayMode;
// currentTheme.value = state.theme;
// animationsEnabled.value = state.animationsEnabled;
// });
import { ElDivider } from 'element-plus';
import DisplayModeSettings from './DisplayModeSettings.vue';
import ThemeSettings from './ThemeSettings.vue';
import InterfaceSettings from './InterfaceSettings.vue';
</script>
<style lang="scss" scoped>
.settings-popover {
padding: 15px;
min-width: 240px; // Increased width for better layout
min-width: 240px;
h4 {
margin-top: 0;
margin-bottom: 12px;
font-size: 14px;
font-weight: 600; // Slightly bolder heading
font-weight: 600;
color: var(--el-text-color-primary);
}
.display-mode-group {
display: flex; // Use flex for equal spacing
width: 100%;
margin-bottom: 5px; // Add some space below
.el-radio-button {
flex: 1; // Make buttons fill the space equally
:deep(.el-radio-button__inner) {
width: 100%;
display: flex;
align-items: center;
justify-content: center; // Center content
border-radius: 4px !important; // Slightly round corners
border: 1px solid var(--el-border-color-lighter); // Use variable for border
box-shadow: none; // Remove default shadow
padding: 8px 5px; // Adjust padding
font-size: 12px; // Smaller font size
.icon {
display: inline-block;
width: 14px;
height: 14px;
margin-right: 5px;
border-radius: 2px;
border: 1px solid var(--el-border-color);
&.system { background: linear-gradient(135deg, #fff 50%, #222 50%); border-color: #888; }
&.light { background-color: #fff; }
&.dark { background-color: #222; border-color: #555; }
}
}
// Style for selected button
&.is-active :deep(.el-radio-button__inner) {
background-color: var(--el-color-primary-light-9);
border-color: var(--el-color-primary);
color: var(--el-color-primary);
box-shadow: none;
}
}
// Remove spacing between buttons created by inline-block nature
.el-radio-button + .el-radio-button {
margin-left: 0;
}
.el-divider {
margin: 18px 0;
}
.theme-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); // Responsive grid
gap: 10px; // Gap between items
margin-bottom: 5px;
}
.theme-option {
cursor: pointer;
border: 2px solid transparent;
padding: 4px;
border-radius: 6px;
text-align: center;
transition: border-color 0.2s ease-in-out;
&:hover {
border-color: var(--el-border-color-hover);
}
&.active {
border-color: var(--el-color-primary);
}
.theme-preview {
width: 100%;
height: 30px; // Adjust height as needed
border-radius: 4px;
margin-bottom: 5px;
border: 1px solid var(--el-border-color-lighter); // Add subtle border
}
.theme-label {
font-size: 12px;
color: var(--el-text-color-regular);
display: block; // Ensure label is on new line
}
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px; // Consistent spacing
font-size: 13px;
color: var(--el-text-color-regular);
}
.el-divider {
margin: 18px 0; // Increase divider margin
}
}
</style>

View File

@ -0,0 +1,87 @@
<template>
<div>
<h4>主题</h4>
<div class="theme-grid">
<div
v-for="(theme, name) in filteredThemes"
:key="name"
class="theme-option"
:class="{ 'active': currentTheme === name }"
@click="changeTheme(name)"
>
<div class="theme-preview" :style="{ backgroundColor: theme.primary }"></div>
<span class="theme-label">{{ getThemeLabel(name) }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useSettingsStore } from '@/store/settings';
import { themes as availableThemes } from '@/styles/theme';
const settingsStore = useSettingsStore();
const currentTheme = ref(settingsStore.theme);
const filteredThemes = computed(() => {
const { dark, ...rest } = availableThemes;
return rest;
});
const themeLabels = {
default: '默认',
fresh: '清新',
warm: '暖色',
business: '商务',
};
const getThemeLabel = (themeName) => {
return themeLabels[themeName] || themeName;
};
const changeTheme = (newTheme) => {
settingsStore.updateTheme(newTheme);
currentTheme.value = newTheme;
};
</script>
<style lang="scss" scoped>
.theme-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
gap: 10px;
margin-bottom: 5px;
}
.theme-option {
cursor: pointer;
border: 2px solid transparent;
padding: 4px;
border-radius: 6px;
text-align: center;
transition: border-color 0.2s ease-in-out;
&:hover {
border-color: var(--el-border-color-hover);
}
&.active {
border-color: var(--el-color-primary);
}
.theme-preview {
width: 100%;
height: 30px;
border-radius: 4px;
margin-bottom: 5px;
border: 1px solid var(--el-border-color-lighter);
}
.theme-label {
font-size: 12px;
color: var(--el-text-color-regular);
display: block;
}
}
</style>

View File

@ -1,56 +0,0 @@
<template>
<el-dropdown trigger="click" @command="handleCommand">
<el-button type="primary">
主题切换
<el-icon class="el-icon--right"><arrow-down /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="(theme, name) in themes" :key="name" :command="name">
<div class="theme-item">
<span class="theme-name">{{ name }}</span>
<div class="theme-colors">
<span v-for="(color, type) in theme" :key="type"
class="color-dot"
:style="{ backgroundColor: color }">
</span>
</div>
</div>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup>
import { ArrowDown } from '@element-plus/icons-vue'
import { themes, setTheme } from '@/styles/theme'
const handleCommand = (themeName) => {
setTheme(themeName)
}
</script>
<style scoped>
.theme-item {
display: flex;
align-items: center;
gap: 8px;
}
.theme-name {
text-transform: capitalize;
}
.theme-colors {
display: flex;
gap: 4px;
}
.color-dot {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
}
</style>