wip: 临时提交
This commit is contained in:
@@ -34,6 +34,6 @@ const theneList = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const handleDarkChange = (theme: ThemeMode) => {
|
const handleDarkChange = (theme: ThemeMode) => {
|
||||||
settingsStore.changeTheme(theme);
|
settingsStore.updateTheme(theme);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { ref, onMounted, onUnmounted, watch, getCurrentInstance } from "vue";
|
import { ref, onMounted, onUnmounted, watch, getCurrentInstance } from "vue";
|
||||||
import { useStomp } from "./useStomp";
|
import { useStomp } from "./useStomp";
|
||||||
import { ElMessage } from "element-plus";
|
|
||||||
import { registerWebSocketInstance } from "@/plugins/websocket";
|
import { registerWebSocketInstance } from "@/plugins/websocket";
|
||||||
import { Auth } from "@/utils/auth";
|
import { Auth } from "@/utils/auth";
|
||||||
|
|
||||||
|
|||||||
@@ -76,5 +76,21 @@ export default {
|
|||||||
showWatermark: "Show Watermark",
|
showWatermark: "Show Watermark",
|
||||||
classicBlue: "Classic Blue",
|
classicBlue: "Classic Blue",
|
||||||
minimalWhite: "Minimal White",
|
minimalWhite: "Minimal White",
|
||||||
|
copyConfig: "Copy Config",
|
||||||
|
resetConfig: "Reset Default",
|
||||||
|
copySuccess: "Configuration copied to clipboard",
|
||||||
|
resetSuccess: "Reset to default configuration",
|
||||||
|
copyDescription:
|
||||||
|
"Copy config will generate current settings code, reset will restore all settings to default",
|
||||||
|
confirmReset: "Are you sure to reset all settings to default? This operation cannot be undone.",
|
||||||
|
applyToFile: "Apply to File",
|
||||||
|
onlyCopy: "Only Copy",
|
||||||
|
leftLayout: "Left Mode",
|
||||||
|
topLayout: "Top Mode",
|
||||||
|
mixLayout: "Mix Mode",
|
||||||
|
configManagement: "Config Management",
|
||||||
|
copyConfigDescription:
|
||||||
|
"Generate current settings code and copy to clipboard, then overwrite src/settings.ts file",
|
||||||
|
resetConfigDescription: "Restore all settings to system default values",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -79,5 +79,19 @@ export default {
|
|||||||
showWatermark: "显示水印",
|
showWatermark: "显示水印",
|
||||||
classicBlue: "经典蓝",
|
classicBlue: "经典蓝",
|
||||||
minimalWhite: "极简白",
|
minimalWhite: "极简白",
|
||||||
|
copyConfig: "复制配置",
|
||||||
|
resetConfig: "重置默认",
|
||||||
|
copySuccess: "配置已复制到剪贴板",
|
||||||
|
resetSuccess: "已重置为默认配置",
|
||||||
|
copyDescription: "复制配置将生成当前设置的代码,重置将恢复所有设置为默认值",
|
||||||
|
confirmReset: "确定要重置所有设置为默认值吗?此操作不可恢复。",
|
||||||
|
applyToFile: "应用到文件",
|
||||||
|
onlyCopy: "仅复制",
|
||||||
|
leftLayout: "左侧模式",
|
||||||
|
topLayout: "顶部模式",
|
||||||
|
mixLayout: "混合模式",
|
||||||
|
configManagement: "配置管理",
|
||||||
|
copyConfigDescription: "生成当前设置的代码并复制到剪贴板,然后覆盖 src/settings.ts 文件",
|
||||||
|
resetConfigDescription: "恢复所有设置为系统默认值",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import variables from "@/styles/variables.module.scss";
|
|||||||
// 缓存页面集合
|
// 缓存页面集合
|
||||||
const cachedViews = computed(() => useTagsViewStore().cachedViews);
|
const cachedViews = computed(() => useTagsViewStore().cachedViews);
|
||||||
const appMainHeight = computed(() => {
|
const appMainHeight = computed(() => {
|
||||||
if (useSettingsStore().tagsView) {
|
if (useSettingsStore().showTagsView) {
|
||||||
return `calc(100vh - ${variables["navbar-height"]} - ${variables["tags-view-height"]})`;
|
return `calc(100vh - ${variables["navbar-height"]} - ${variables["tags-view-height"]})`;
|
||||||
} else {
|
} else {
|
||||||
return `calc(100vh - ${variables["navbar-height"]})`;
|
return `calc(100vh - ${variables["navbar-height"]})`;
|
||||||
@@ -32,5 +32,19 @@ const appMainHeight = computed(() => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: var(--el-bg-color-page);
|
background-color: var(--el-bg-color-page);
|
||||||
|
|
||||||
|
/* 布局切换动画优化 */
|
||||||
|
&.animate__animated {
|
||||||
|
animation-duration: 0.4s;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.animate__fadeOut {
|
||||||
|
animation-timing-function: ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.animate__fadeIn {
|
||||||
|
animation-timing-function: ease-out;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,102 +1,167 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-drawer
|
<el-drawer
|
||||||
v-model="drawerVisible"
|
v-model="drawerVisible"
|
||||||
size="300"
|
size="380"
|
||||||
:title="t('settings.project')"
|
:title="t('settings.project')"
|
||||||
:before-close="handleCloseDrawer"
|
:before-close="handleCloseDrawer"
|
||||||
|
class="settings-drawer"
|
||||||
>
|
>
|
||||||
<section class="config-section">
|
<div class="settings-content">
|
||||||
<el-divider>{{ t("settings.theme") }}</el-divider>
|
<section class="config-section">
|
||||||
|
<el-divider>{{ t("settings.theme") }}</el-divider>
|
||||||
|
|
||||||
<div class="flex-center">
|
<div class="flex-center">
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="isDark"
|
v-model="isDark"
|
||||||
active-icon="Moon"
|
active-icon="Moon"
|
||||||
inactive-icon="Sunny"
|
inactive-icon="Sunny"
|
||||||
@change="handleThemeChange"
|
class="theme-switch"
|
||||||
/>
|
@change="handleThemeChange"
|
||||||
</div>
|
/>
|
||||||
</section>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- 界面设置 -->
|
<!-- 界面设置 -->
|
||||||
<section class="config-section">
|
<section class="config-section">
|
||||||
<el-divider>{{ t("settings.interface") }}</el-divider>
|
<el-divider>{{ t("settings.interface") }}</el-divider>
|
||||||
|
|
||||||
<div class="config-item flex-x-between">
|
<div class="config-item flex-x-between">
|
||||||
<span class="text-xs">{{ t("settings.themeColor") }}</span>
|
<span class="text-xs">{{ t("settings.themeColor") }}</span>
|
||||||
<el-color-picker
|
<el-color-picker
|
||||||
v-model="selectedThemeColor"
|
v-model="selectedThemeColor"
|
||||||
:predefine="colorPresets"
|
:predefine="colorPresets"
|
||||||
popper-class="theme-picker-dropdown"
|
popper-class="theme-picker-dropdown"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="config-item flex-x-between">
|
<div class="config-item flex-x-between">
|
||||||
<span class="text-xs">{{ t("settings.showTagsView") }}</span>
|
<span class="text-xs">{{ t("settings.showTagsView") }}</span>
|
||||||
<el-switch v-model="settingsStore.showTagsView" />
|
<el-switch v-model="settingsStore.showTagsView" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="config-item flex-x-between">
|
<div class="config-item flex-x-between">
|
||||||
<span class="text-xs">{{ t("settings.showAppLogo") }}</span>
|
<span class="text-xs">{{ t("settings.showAppLogo") }}</span>
|
||||||
<el-switch v-model="settingsStore.showAppLogo" />
|
<el-switch v-model="settingsStore.showAppLogo" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="config-item flex-x-between">
|
<div class="config-item flex-x-between">
|
||||||
<span class="text-xs">{{ t("settings.showWatermark") }}</span>
|
<span class="text-xs">{{ t("settings.showWatermark") }}</span>
|
||||||
<el-switch v-model="settingsStore.showWatermark" />
|
<el-switch v-model="settingsStore.showWatermark" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isDark" class="config-item flex-x-between">
|
<div v-if="!isDark" class="config-item flex-x-between">
|
||||||
<span class="text-xs">{{ t("settings.sidebarColorScheme") }}</span>
|
<span class="text-xs">{{ t("settings.sidebarColorScheme") }}</span>
|
||||||
<el-radio-group v-model="sidebarColor" @change="changeSidebarColor">
|
<el-radio-group v-model="sidebarColor" @change="changeSidebarColor">
|
||||||
<el-radio :value="SidebarColor.CLASSIC_BLUE">
|
<el-radio :value="SidebarColor.CLASSIC_BLUE">
|
||||||
{{ t("settings.classicBlue") }}
|
{{ t("settings.classicBlue") }}
|
||||||
</el-radio>
|
</el-radio>
|
||||||
<el-radio :value="SidebarColor.MINIMAL_WHITE">
|
<el-radio :value="SidebarColor.MINIMAL_WHITE">
|
||||||
{{ t("settings.minimalWhite") }}
|
{{ t("settings.minimalWhite") }}
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 布局设置 -->
|
<!-- 布局设置 -->
|
||||||
<section class="config-section">
|
<section class="config-section">
|
||||||
<el-divider>{{ t("settings.navigation") }}</el-divider>
|
<el-divider>{{ t("settings.navigation") }}</el-divider>
|
||||||
|
|
||||||
<!-- 整合的布局选择器 -->
|
<!-- 整合的布局选择器 -->
|
||||||
<div class="layout-select">
|
<div class="layout-select">
|
||||||
<el-tooltip
|
<div class="layout-grid">
|
||||||
v-for="item in layoutOptions"
|
<el-tooltip
|
||||||
:key="item.value"
|
v-for="item in layoutOptions"
|
||||||
:content="item.label"
|
:key="item.value"
|
||||||
placement="bottom"
|
:content="item.label"
|
||||||
>
|
placement="bottom"
|
||||||
<div
|
>
|
||||||
role="button"
|
<div
|
||||||
tabindex="0"
|
role="button"
|
||||||
:class="[
|
tabindex="0"
|
||||||
'layout-item',
|
:class="[
|
||||||
item.className,
|
'layout-item',
|
||||||
{ 'is-active': settingsStore.layout === item.value },
|
item.className,
|
||||||
]"
|
{
|
||||||
@click="handleLayoutChange(item.value)"
|
'is-active': settingsStore.layout === item.value,
|
||||||
@keydown.enter.space="handleLayoutChange(item.value)"
|
},
|
||||||
>
|
]"
|
||||||
<div class="layout-item-part" />
|
@click="handleLayoutChange(item.value)"
|
||||||
<div class="layout-item-part" />
|
@keydown.enter.space="handleLayoutChange(item.value)"
|
||||||
|
>
|
||||||
|
<!-- 布局预览图标 -->
|
||||||
|
<div class="layout-preview">
|
||||||
|
<div v-if="item.value !== LayoutMode.LEFT" class="layout-header"></div>
|
||||||
|
<div v-if="item.value !== LayoutMode.TOP" class="layout-sidebar"></div>
|
||||||
|
<div class="layout-main"></div>
|
||||||
|
</div>
|
||||||
|
<!-- 布局名称 -->
|
||||||
|
<div class="layout-name">{{ item.label }}</div>
|
||||||
|
<!-- 选中状态指示器 -->
|
||||||
|
<div v-if="settingsStore.layout === item.value" class="layout-check">
|
||||||
|
<el-icon><Check /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</el-tooltip>
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮区域 - 固定到底部 -->
|
||||||
|
<div class="action-footer">
|
||||||
|
<div class="action-divider"></div>
|
||||||
|
<div class="action-card">
|
||||||
|
<div class="action-buttons">
|
||||||
|
<el-tooltip
|
||||||
|
content="复制配置将生成当前设置的代码,覆盖 src/settings.ts 下的 defaultSettings 变量"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="default"
|
||||||
|
:icon="copyIcon"
|
||||||
|
:loading="copyLoading"
|
||||||
|
class="action-btn"
|
||||||
|
@click="handleCopySettings"
|
||||||
|
>
|
||||||
|
{{ copyLoading ? "复制中..." : t("settings.copyConfig") }}
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="重置将恢复所有设置为默认值" placement="top">
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
size="default"
|
||||||
|
:icon="resetIcon"
|
||||||
|
:loading="resetLoading"
|
||||||
|
class="action-btn"
|
||||||
|
@click="handleResetSettings"
|
||||||
|
>
|
||||||
|
{{ resetLoading ? "重置中..." : t("settings.resetConfig") }}
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { DocumentCopy, RefreshLeft, Check } from "@element-plus/icons-vue";
|
||||||
|
import { markRaw } from "vue";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
import { LayoutMode } from "@/enums/settings/layout.enum";
|
import { LayoutMode } from "@/enums/settings/layout.enum";
|
||||||
import { ThemeMode } from "@/enums/settings/theme.enum";
|
import { ThemeMode } from "@/enums/settings/theme.enum";
|
||||||
import { SidebarColor } from "@/enums/settings/theme.enum";
|
import { SidebarColor } from "@/enums/settings/theme.enum";
|
||||||
import { useSettingsStore, usePermissionStore, useAppStore } from "@/store";
|
import { useSettingsStore, usePermissionStore, useAppStore } from "@/store";
|
||||||
|
|
||||||
|
// 按钮图标 - 使用markRaw避免响应式警告
|
||||||
|
const copyIcon = markRaw(DocumentCopy);
|
||||||
|
const resetIcon = markRaw(RefreshLeft);
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const copyLoading = ref(false);
|
||||||
|
const resetLoading = ref(false);
|
||||||
|
|
||||||
// 布局选项配置
|
// 布局选项配置
|
||||||
interface LayoutOption {
|
interface LayoutOption {
|
||||||
value: LayoutMode;
|
value: LayoutMode;
|
||||||
@@ -105,9 +170,9 @@ interface LayoutOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const layoutOptions: LayoutOption[] = [
|
const layoutOptions: LayoutOption[] = [
|
||||||
{ value: LayoutMode.LEFT, label: "左侧模式", className: "left" },
|
{ value: LayoutMode.LEFT, label: t("settings.leftLayout"), className: "left" },
|
||||||
{ value: LayoutMode.TOP, label: "顶部模式", className: "top" },
|
{ value: LayoutMode.TOP, label: t("settings.topLayout"), className: "top" },
|
||||||
{ value: LayoutMode.MIX, label: "混合模式", className: "mix" },
|
{ value: LayoutMode.MIX, label: t("settings.mixLayout"), className: "mix" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 颜色预设
|
// 颜色预设
|
||||||
@@ -165,7 +230,10 @@ const changeSidebarColor = (val: any) => {
|
|||||||
* @param layout - 布局模式
|
* @param layout - 布局模式
|
||||||
*/
|
*/
|
||||||
const handleLayoutChange = (layout: LayoutMode) => {
|
const handleLayoutChange = (layout: LayoutMode) => {
|
||||||
|
if (settingsStore.layout === layout) return;
|
||||||
|
|
||||||
settingsStore.updateLayout(layout);
|
settingsStore.updateLayout(layout);
|
||||||
|
|
||||||
if (layout === LayoutMode.MIX && route.name) {
|
if (layout === LayoutMode.MIX && route.name) {
|
||||||
const topLevelRoute = findTopLevelRoute(permissionStore.routes, route.name as string);
|
const topLevelRoute = findTopLevelRoute(permissionStore.routes, route.name as string);
|
||||||
if (appStore.activeTopMenuPath !== topLevelRoute.path) {
|
if (appStore.activeTopMenuPath !== topLevelRoute.path) {
|
||||||
@@ -174,6 +242,89 @@ const handleLayoutChange = (layout: LayoutMode) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制当前配置
|
||||||
|
*/
|
||||||
|
const handleCopySettings = async () => {
|
||||||
|
try {
|
||||||
|
copyLoading.value = true;
|
||||||
|
|
||||||
|
// 生成配置代码
|
||||||
|
const configCode = generateSettingsCode();
|
||||||
|
|
||||||
|
// 复制到剪贴板
|
||||||
|
await navigator.clipboard.writeText(configCode);
|
||||||
|
|
||||||
|
// 显示成功消息
|
||||||
|
ElMessage.success({
|
||||||
|
message: t("settings.copySuccess"),
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
ElMessage.error("复制配置失败");
|
||||||
|
} finally {
|
||||||
|
copyLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置为默认配置
|
||||||
|
*/
|
||||||
|
const handleResetSettings = async () => {
|
||||||
|
resetLoading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
settingsStore.resetSettings();
|
||||||
|
|
||||||
|
// 同步更新本地状态
|
||||||
|
isDark.value = settingsStore.theme === ThemeMode.DARK;
|
||||||
|
sidebarColor.value = settingsStore.sidebarColorScheme;
|
||||||
|
|
||||||
|
ElMessage.success(t("settings.resetSuccess"));
|
||||||
|
} catch {
|
||||||
|
ElMessage.error("重置配置失败");
|
||||||
|
} finally {
|
||||||
|
resetLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成配置代码字符串
|
||||||
|
*/
|
||||||
|
const generateSettingsCode = (): string => {
|
||||||
|
const settings = {
|
||||||
|
title: "pkg.name",
|
||||||
|
version: "pkg.version",
|
||||||
|
showSettings: true,
|
||||||
|
showTagsView: settingsStore.showTagsView,
|
||||||
|
showAppLogo: settingsStore.showAppLogo,
|
||||||
|
layout: `LayoutMode.${settingsStore.layout.toUpperCase()}`,
|
||||||
|
theme: `ThemeMode.${settingsStore.theme.toUpperCase()}`,
|
||||||
|
size: "ComponentSize.DEFAULT",
|
||||||
|
language: "LanguageEnum.ZH_CN",
|
||||||
|
themeColor: `"${settingsStore.themeColor}"`,
|
||||||
|
showWatermark: settingsStore.showWatermark,
|
||||||
|
watermarkContent: "pkg.name",
|
||||||
|
sidebarColorScheme: `SidebarColor.${settingsStore.sidebarColorScheme.toUpperCase().replace("-", "_")}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return `const defaultSettings: AppSettings = {
|
||||||
|
title: ${settings.title},
|
||||||
|
version: ${settings.version},
|
||||||
|
showSettings: ${settings.showSettings},
|
||||||
|
showTagsView: ${settings.showTagsView},
|
||||||
|
showAppLogo: ${settings.showAppLogo},
|
||||||
|
layout: ${settings.layout},
|
||||||
|
theme: ${settings.theme},
|
||||||
|
size: ${settings.size},
|
||||||
|
language: ${settings.language},
|
||||||
|
themeColor: ${settings.themeColor},
|
||||||
|
showWatermark: ${settings.showWatermark},
|
||||||
|
watermarkContent: ${settings.watermarkContent},
|
||||||
|
sidebarColorScheme: ${settings.sidebarColorScheme},
|
||||||
|
};`;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查找路由的顶层父路由
|
* 查找路由的顶层父路由
|
||||||
*
|
*
|
||||||
@@ -216,109 +367,300 @@ const handleCloseDrawer = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
/* 设置抽屉样式 */
|
||||||
|
.settings-drawer {
|
||||||
|
:deep(.el-drawer__body) {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 设置内容区域 */
|
||||||
|
.settings-content {
|
||||||
|
height: calc(100vh - 120px); /* 减去头部和底部按钮的高度 */
|
||||||
|
padding: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部操作区域样式 */
|
||||||
|
.action-footer {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
border-top: 1px solid var(--el-border-color-light);
|
||||||
|
|
||||||
|
.action-divider {
|
||||||
|
display: none; /* 移除重复的分割线 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card {
|
||||||
|
padding: 16px 20px;
|
||||||
|
margin: 0;
|
||||||
|
background: var(--el-fill-color-extra-light);
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主题切换器优化 */
|
||||||
|
.theme-switch {
|
||||||
|
transform: scale(1.2);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.config-section {
|
.config-section {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
|
||||||
.config-item {
|
.config-item {
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
border-bottom: 1px solid var(--el-border-color-light);
|
border-bottom: 1px solid var(--el-border-color-light);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
padding-right: 8px;
|
||||||
|
padding-left: 8px;
|
||||||
|
margin: 0 -8px;
|
||||||
|
background-color: var(--el-fill-color-light);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 布局选择器样式 */
|
/* 布局选择器样式优化 */
|
||||||
.layout-select {
|
.layout-select {
|
||||||
display: flex;
|
padding: 16px 8px;
|
||||||
gap: 10px;
|
|
||||||
justify-content: space-evenly;
|
.layout-grid {
|
||||||
padding: 10px 0;
|
display: grid;
|
||||||
--layout-primary: #1b2a47;
|
grid-template-columns: repeat(3, 1fr);
|
||||||
--layout-background: #f0f2f5;
|
gap: 12px;
|
||||||
--layout-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
|
justify-items: center;
|
||||||
--layout-hover: #e3f1f9;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-item {
|
.layout-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 18%;
|
width: 70px;
|
||||||
height: 50px;
|
height: 80px;
|
||||||
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: var(--layout-background);
|
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%);
|
||||||
border-radius: 8px;
|
border: 2px solid var(--el-border-color-light);
|
||||||
box-shadow: var(--layout-shadow);
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
transition:
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
transform 0.2s ease,
|
|
||||||
border-color 0.2s ease,
|
|
||||||
box-shadow 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--layout-hover);
|
background: linear-gradient(145deg, #ffffff 0%, var(--el-color-primary-light-9) 100%);
|
||||||
transform: scale(1.02);
|
border-color: var(--el-color-primary-light-3);
|
||||||
|
transform: translateY(-4px) scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:active {
|
||||||
outline: 2px solid var(--el-color-primary);
|
transform: translateY(-2px) scale(1.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
&-part {
|
.layout-preview {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
margin: 8px 0 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-header {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: var(--layout-primary);
|
top: 0;
|
||||||
border-radius: 4px;
|
right: 4px;
|
||||||
box-shadow: var(--layout-shadow);
|
left: 4px;
|
||||||
transition: all 0.3s ease;
|
height: 8px;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--el-color-primary) 0%,
|
||||||
|
var(--el-color-primary-light-3) 100%
|
||||||
|
);
|
||||||
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.layout-sidebar {
|
||||||
|
position: absolute;
|
||||||
|
left: 4px;
|
||||||
|
width: 12px;
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
var(--el-color-primary-dark-2) 0%,
|
||||||
|
var(--el-color-primary) 100%
|
||||||
|
);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-main {
|
||||||
|
position: absolute;
|
||||||
|
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-name {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 6px;
|
||||||
|
left: 0;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
text-align: center;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-check {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: white;
|
||||||
|
background: var(--el-color-success);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 左侧布局
|
||||||
&.left {
|
&.left {
|
||||||
.layout-item-part {
|
.layout-sidebar {
|
||||||
&:first-child {
|
top: 4px;
|
||||||
width: 30%;
|
bottom: 4px;
|
||||||
height: 100%;
|
}
|
||||||
border-radius: 4px 0 0 4px;
|
.layout-main {
|
||||||
}
|
top: 4px;
|
||||||
&:last-child {
|
right: 4px;
|
||||||
top: 0;
|
bottom: 4px;
|
||||||
right: 0;
|
left: 20px;
|
||||||
width: 70%;
|
|
||||||
height: 30%;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 0 4px 4px 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 顶部布局
|
||||||
&.top {
|
&.top {
|
||||||
.layout-item-part:first-child {
|
.layout-header {
|
||||||
width: 100%;
|
height: 12px;
|
||||||
height: 30%;
|
}
|
||||||
border-radius: 4px 4px 0 0;
|
.layout-main {
|
||||||
|
top: 16px;
|
||||||
|
right: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
left: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 混合布局
|
||||||
&.mix {
|
&.mix {
|
||||||
.layout-item-part {
|
.layout-header {
|
||||||
&:first-child {
|
height: 10px;
|
||||||
width: 100%;
|
}
|
||||||
height: 30%;
|
.layout-sidebar {
|
||||||
border-radius: 4px 4px 0 0;
|
top: 14px;
|
||||||
}
|
bottom: 4px;
|
||||||
&:last-child {
|
}
|
||||||
bottom: 0;
|
.layout-main {
|
||||||
left: 0;
|
top: 14px;
|
||||||
width: 30%;
|
right: 4px;
|
||||||
height: 70%;
|
bottom: 4px;
|
||||||
border-radius: 0 0 4px 4px;
|
left: 20px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background: linear-gradient(
|
||||||
|
145deg,
|
||||||
|
var(--el-color-primary-light-9) 0%,
|
||||||
|
var(--el-color-primary-light-8) 100%
|
||||||
|
);
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
transform: translateY(-2px) scale(1.08);
|
||||||
|
|
||||||
|
.layout-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-color-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-active {
|
/* 深色模式适配 */
|
||||||
background-color: var(--layout-hover);
|
.dark {
|
||||||
border: 2px solid var(--el-color-primary);
|
.action-footer {
|
||||||
transform: scale(1.05);
|
background: var(--el-bg-color);
|
||||||
|
border-top-color: var(--el-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card {
|
||||||
|
background: var(--el-fill-color-extra-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-item {
|
||||||
|
background: linear-gradient(145deg, var(--el-bg-color) 0%, var(--el-bg-color-page) 100%);
|
||||||
|
border-color: var(--el-border-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(
|
||||||
|
145deg,
|
||||||
|
var(--el-bg-color-page) 0%,
|
||||||
|
var(--el-color-primary-light-9) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background: linear-gradient(
|
||||||
|
145deg,
|
||||||
|
var(--el-color-primary-light-9) 0%,
|
||||||
|
var(--el-color-primary-light-8) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-main {
|
||||||
|
background: linear-gradient(135deg, var(--el-fill-color) 0%, var(--el-fill-color-light) 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 复制配置对话框样式 */
|
||||||
|
:deep(.copy-config-dialog) {
|
||||||
|
.el-message-box__content {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<component :is="currentLayoutComponent" />
|
<div class="layout-wrapper">
|
||||||
|
<component :is="currentLayoutComponent" />
|
||||||
|
|
||||||
|
<!-- 设置面板 - 独立于布局组件 -->
|
||||||
|
<Settings v-if="isShowSettings" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -8,7 +13,9 @@ import { useLayout } from "./composables/useLayout";
|
|||||||
import LeftLayout from "./views/LeftLayout.vue";
|
import LeftLayout from "./views/LeftLayout.vue";
|
||||||
import TopLayout from "./views/TopLayout.vue";
|
import TopLayout from "./views/TopLayout.vue";
|
||||||
import MixLayout from "./views/MixLayout.vue";
|
import MixLayout from "./views/MixLayout.vue";
|
||||||
|
import Settings from "./components/Settings/index.vue";
|
||||||
import { LayoutMode } from "@/enums/settings/layout.enum";
|
import { LayoutMode } from "@/enums/settings/layout.enum";
|
||||||
|
import defaultSettings from "@/settings";
|
||||||
|
|
||||||
const { currentLayout } = useLayout();
|
const { currentLayout } = useLayout();
|
||||||
|
|
||||||
@@ -24,4 +31,14 @@ const currentLayoutComponent = computed(() => {
|
|||||||
return LeftLayout;
|
return LeftLayout;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 是否显示设置面板
|
||||||
|
const isShowSettings = computed(() => defaultSettings.showSettings);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.layout-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -6,9 +6,6 @@
|
|||||||
<!-- 布局内容插槽 -->
|
<!-- 布局内容插槽 -->
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
|
||||||
<!-- 设置面板 -->
|
|
||||||
<Settings v-if="isShowSettings" />
|
|
||||||
|
|
||||||
<!-- 返回顶部按钮 -->
|
<!-- 返回顶部按钮 -->
|
||||||
<el-backtop target=".app-main">
|
<el-backtop target=".app-main">
|
||||||
<div class="i-svg:backtop w-6 h-6" />
|
<div class="i-svg:backtop w-6 h-6" />
|
||||||
@@ -19,10 +16,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useLayout } from "../composables/useLayout";
|
import { useLayout } from "../composables/useLayout";
|
||||||
import { useLayoutResponsive } from "../composables/useLayoutResponsive";
|
import { useLayoutResponsive } from "../composables/useLayoutResponsive";
|
||||||
import Settings from "../components/Settings/index.vue";
|
|
||||||
|
|
||||||
// 布局相关
|
// 布局相关
|
||||||
const { layoutClass, isShowSettings, isSidebarOpen, closeSidebar } = useLayout();
|
const { layoutClass, isSidebarOpen, closeSidebar } = useLayout();
|
||||||
|
|
||||||
// 响应式处理
|
// 响应式处理
|
||||||
const { isMobile } = useLayoutResponsive();
|
const { isMobile } = useLayoutResponsive();
|
||||||
|
|||||||
Reference in New Issue
Block a user