💄 更改顶边ui和侧边栏ui
This commit is contained in:
@@ -1,78 +1,31 @@
|
||||
<template>
|
||||
<a-layout style="height: 100vh; overflow: hidden" class="app-layout-collapsed">
|
||||
<a-layout style="height: 100vh; overflow: hidden">
|
||||
<a-layout-sider
|
||||
v-model:collapsed="collapsed"
|
||||
collapsible
|
||||
:trigger="null"
|
||||
:width="180"
|
||||
:collapsed-width="60"
|
||||
:width="SIDER_WIDTH"
|
||||
:theme="isDark ? 'dark' : 'light'"
|
||||
style="height: calc(100vh - 32px); position: fixed; left: 0; top: 32px; z-index: 100"
|
||||
:style="{ height: 'calc(100vh - 32px)', position: 'fixed', left: '0', top: '32px', zIndex: 100, background: 'var(--app-sider-bg)', borderRight: '1px solid var(--app-sider-border-color)' }"
|
||||
>
|
||||
<div class="sider-content">
|
||||
<!-- <!– 折叠按钮 –>-->
|
||||
<!-- <div class="collapse-trigger" @click="toggleCollapse">-->
|
||||
<!-- <MenuFoldOutlined v-if="!collapsed" />-->
|
||||
<!-- <MenuUnfoldOutlined v-else />-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- 主菜单容器 -->
|
||||
<div class="main-menu-container">
|
||||
<a-menu
|
||||
mode="inline"
|
||||
:inline-collapsed="collapsed"
|
||||
:theme="isDark ? 'dark' : 'light'"
|
||||
class="main-menu"
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
>
|
||||
<template v-for="item in mainMenuItems" :key="item.path">
|
||||
<a-menu-item @click="goTo(item.path)" :data-title="item.label">
|
||||
<template #icon>
|
||||
<component :is="item.icon" />
|
||||
</template>
|
||||
<span v-if="!collapsed" class="menu-text">{{ item.label }}</span>
|
||||
</a-menu-item>
|
||||
</template>
|
||||
</a-menu>
|
||||
</div>
|
||||
|
||||
<!-- 底部菜单(带3px底部内边距) -->
|
||||
<a-menu
|
||||
mode="inline"
|
||||
:inline-collapsed="collapsed"
|
||||
:theme="isDark ? 'dark' : 'light'"
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
:items="mainMenuItems"
|
||||
@click="onMenuClick"
|
||||
/>
|
||||
<a-menu
|
||||
mode="inline"
|
||||
:theme="isDark ? 'dark' : 'light'"
|
||||
class="bottom-menu"
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
>
|
||||
<template v-for="item in bottomMenuItems" :key="item.path">
|
||||
<a-menu-item @click="goTo(item.path)" :data-title="item.label">
|
||||
<template #icon>
|
||||
<component :is="item.icon" />
|
||||
</template>
|
||||
<span v-if="!collapsed" class="menu-text">{{ item.label }}</span>
|
||||
</a-menu-item>
|
||||
</template>
|
||||
</a-menu>
|
||||
:items="bottomMenuItems"
|
||||
@click="onMenuClick"
|
||||
/>
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<a-layout
|
||||
:style="{
|
||||
marginLeft: collapsed ? '60px' : '180px',
|
||||
transition: 'margin-left 0.2s',
|
||||
height: 'calc(100vh - 32px)',
|
||||
}"
|
||||
>
|
||||
<a-layout-content
|
||||
class="content-area"
|
||||
:style="{
|
||||
padding: '24px',
|
||||
background: isDark ? '#141414' : '#ffffff',
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
}"
|
||||
>
|
||||
<a-layout :style="{ marginLeft: SIDER_WIDTH + 'px', height: 'calc(100vh - 32px)', transition: 'margin-left .2s' }">
|
||||
<a-layout-content class="content-area">
|
||||
<router-view />
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
@@ -88,202 +41,92 @@ import {
|
||||
ControlOutlined,
|
||||
HistoryOutlined,
|
||||
SettingOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { computed, h } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useTheme } from '../composables/useTheme.ts'
|
||||
import type { MenuProps } from 'ant-design-vue'
|
||||
|
||||
const SIDER_WIDTH = 140
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { isDark } = useTheme()
|
||||
|
||||
const collapsed = ref<boolean>(false)
|
||||
// 工具:生成菜单项
|
||||
const icon = (Comp: any) => () => h(Comp)
|
||||
|
||||
// 菜单数据
|
||||
const mainMenuItems = [
|
||||
{ path: '/home', label: '主页', icon: HomeOutlined },
|
||||
{ path: '/scripts', label: '脚本管理', icon: FileTextOutlined },
|
||||
{ path: '/plans', label: '计划管理', icon: CalendarOutlined },
|
||||
{ path: '/queue', label: '调度队列', icon: UnorderedListOutlined },
|
||||
{ path: '/scheduler', label: '调度中心', icon: ControlOutlined },
|
||||
{ path: '/history', label: '历史记录', icon: HistoryOutlined },
|
||||
{ key: '/home', label: '主页', icon: icon(HomeOutlined) },
|
||||
{ key: '/scripts', label: '脚本管理', icon: icon(FileTextOutlined) },
|
||||
{ key: '/plans', label: '计划管理', icon: icon(CalendarOutlined) },
|
||||
{ key: '/queue', label: '调度队列', icon: icon(UnorderedListOutlined) },
|
||||
{ key: '/scheduler', label: '调度中心', icon: icon(ControlOutlined) },
|
||||
{ key: '/history', label: '历史记录', icon: icon(HistoryOutlined) },
|
||||
]
|
||||
const bottomMenuItems = [
|
||||
{ key: '/settings', label: '设置', icon: icon(SettingOutlined) },
|
||||
]
|
||||
|
||||
const bottomMenuItems = [{ path: '/settings', label: '设置', icon: SettingOutlined }]
|
||||
const allItems = [...mainMenuItems, ...bottomMenuItems]
|
||||
|
||||
// 自动同步选中项
|
||||
// 选中项:根据当前路径前缀匹配
|
||||
const selectedKeys = computed(() => {
|
||||
const path = route.path
|
||||
const allItems = [...mainMenuItems, ...bottomMenuItems]
|
||||
const matched = allItems.find(item => path.startsWith(item.path))
|
||||
return [matched?.path || '/home']
|
||||
const matched = allItems.find(i => path.startsWith(String(i.key)))
|
||||
return [matched?.key || '/home']
|
||||
})
|
||||
|
||||
const goTo = (path: string) => {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
const toggleCollapse = () => {
|
||||
collapsed.value = !collapsed.value
|
||||
const onMenuClick: MenuProps['onClick'] = info => {
|
||||
const target = String(info.key)
|
||||
if (route.path !== target) router.push(target)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sider-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 4px; /* 关键:添加3px底部内边距 */
|
||||
}
|
||||
|
||||
/* 折叠按钮 */
|
||||
.collapse-trigger {
|
||||
height: 42px;
|
||||
.sider-content { height:100%; display:flex; flex-direction:column; padding:4px 0 8px 0; }
|
||||
.sider-content :deep(.ant-menu) { border-inline-end: none !important; background: transparent !important; }
|
||||
/* 菜单项外框居中(左右留空),内容左对齐 */
|
||||
.sider-content :deep(.ant-menu .ant-menu-item) {
|
||||
color: var(--app-menu-text-color);
|
||||
margin: 2px auto; /* 水平居中 */
|
||||
width: calc(100% - 16px); /* 两侧各留 8px 空隙 */
|
||||
border-radius: 6px;
|
||||
padding: 0 10px !important; /* 左右内边距 */
|
||||
line-height: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 4px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
justify-content: flex-start; /* 左对齐图标与文字 */
|
||||
gap: 6px;
|
||||
transition: background .16s ease, color .16s ease;
|
||||
text-align: left;
|
||||
}
|
||||
.sider-content :deep(.ant-menu .ant-menu-item .anticon) {
|
||||
color: var(--app-menu-icon-color);
|
||||
font-size: 16px;
|
||||
transition: background-color 0.2s;
|
||||
line-height: 1;
|
||||
transition: color .16s ease;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.collapse-trigger:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
/* Hover */
|
||||
.sider-content :deep(.ant-menu .ant-menu-item:hover) {
|
||||
background: var(--app-menu-item-hover-bg, var(--app-menu-item-hover-bg-hex));
|
||||
color: var(--app-menu-item-hover-text-color);
|
||||
}
|
||||
|
||||
:deep(.ant-layout-sider-light) .collapse-trigger:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
:deep(.ant-layout-sider-dark) .collapse-trigger {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
:deep(.ant-layout-sider-light) .collapse-trigger {
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
/* 主菜单容器 */
|
||||
.main-menu-container {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
/* 修复滚动条显示问题 */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
||||
}
|
||||
|
||||
.main-menu-container::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.main-menu-container::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.main-menu-container::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 底部菜单 */
|
||||
.bottom-menu {
|
||||
margin-top: auto;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
:deep(.ant-layout-sider-light .bottom-menu) {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* 菜单项文字 */
|
||||
.menu-text {
|
||||
margin-left: 36px;
|
||||
white-space: nowrap;
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
/* 主题颜色 */
|
||||
:deep(.ant-layout-sider-dark) .logo-text,
|
||||
:deep(.ant-layout-sider-dark) .menu-text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
:deep(.ant-layout-sider-light) .logo-text,
|
||||
:deep(.ant-layout-sider-light) .menu-text {
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
/* 菜单项统一样式 */
|
||||
:deep(.ant-menu-item),
|
||||
:deep(.ant-menu-item-selected) {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
line-height: 34px;
|
||||
margin: 0 6px;
|
||||
border-radius: 6px;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* 图标绝对定位 */
|
||||
:deep(.ant-menu-item .ant-menu-item-icon) {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 隐藏内容区滚动条 */
|
||||
.content-area {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.content-area::-webkit-scrollbar {
|
||||
display: none;
|
||||
.sider-content :deep(.ant-menu .ant-menu-item:hover .anticon) { color: var(--app-menu-item-hover-text-color); }
|
||||
/* Selected */
|
||||
.sider-content :deep(.ant-menu .ant-menu-item-selected) {
|
||||
background: var(--app-menu-item-selected-bg, var(--app-menu-item-selected-bg-hex));
|
||||
color: var(--app-menu-text-color) !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
.sider-content :deep(.ant-menu .ant-menu-item-selected .anticon) { color: var(--app-menu-icon-color); }
|
||||
.sider-content :deep(.ant-menu-light .ant-menu-item::after),
|
||||
.sider-content :deep(.ant-menu-dark .ant-menu-item::after) { display: none; }
|
||||
.bottom-menu { margin-top:auto; }
|
||||
.content-area { height:100%; overflow:auto; scrollbar-width:none; -ms-overflow-style:none; }
|
||||
.content-area::-webkit-scrollbar { display:none; }
|
||||
</style>
|
||||
|
||||
<!-- 全局样式 -->
|
||||
<style>
|
||||
/* 收缩状态下,通过 data-title 显示 Tooltip */
|
||||
.app-layout-collapsed .ant-menu-inline-collapsed .ant-menu-item:hover::before {
|
||||
content: attr(data-title);
|
||||
position: absolute;
|
||||
left: 60px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
z-index: 1000;
|
||||
opacity: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 修复底部菜单在折叠状态下的tooltip位置 */
|
||||
.app-layout-collapsed .ant-menu-inline-collapsed .bottom-menu .ant-menu-item:hover::before {
|
||||
left: 60px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
/* 确保底部菜单在收缩状态下也有3px间距 */
|
||||
.app-layout-collapsed .ant-menu-inline-collapsed .bottom-menu {
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
</style>
|
||||
<!-- 调整:外框(菜单项背景块)水平居中,文字与图标左对齐 -->
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
<!-- 左侧:Logo和软件名 -->
|
||||
<div class="title-bar-left">
|
||||
<div class="logo-section">
|
||||
<!-- 新增虚化主题色圆形阴影 -->
|
||||
<span class="logo-glow" aria-hidden="true"></span>
|
||||
<img src="@/assets/AUTO-MAS.ico" alt="AUTO-MAS" class="title-logo" />
|
||||
<span class="title-text">AUTO-MAS</span>
|
||||
</div>
|
||||
@@ -94,6 +96,7 @@ onMounted(async () => {
|
||||
user-select: none;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
overflow: hidden; /* 新增:裁剪超出顶栏的发光 */
|
||||
}
|
||||
|
||||
.title-bar-dark {
|
||||
@@ -112,17 +115,42 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
position: relative; /* 使阴影绝对定位基准 */
|
||||
}
|
||||
|
||||
/* 新增:主题色虚化圆形阴影 */
|
||||
.logo-glow {
|
||||
position: absolute;
|
||||
left: 55px; /* 调整:更贴近图标 */
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 200px; /* 缩小尺寸以适配 32px 高度 */
|
||||
height: 100px;
|
||||
pointer-events: none;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 50% 50%, var(--ant-color-primary) 0%, rgba(0,0,0,0) 70%);
|
||||
filter: blur(24px); /* 降低模糊避免越界过多 */
|
||||
opacity: 0.4;
|
||||
z-index: 0;
|
||||
}
|
||||
.title-bar-dark .logo-glow {
|
||||
opacity: 0.7;
|
||||
filter: blur(24px);
|
||||
}
|
||||
|
||||
.title-logo {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
z-index: 1; /* 确保在阴影上方 */
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.title-bar-dark .title-text {
|
||||
|
||||
@@ -71,15 +71,32 @@ const updateCSSVariables = () => {
|
||||
const root = document.documentElement
|
||||
const primaryColor = themeColors[themeColor.value]
|
||||
|
||||
// 基础背景(用于估算混合)
|
||||
const baseLightBg = '#ffffff'
|
||||
const baseDarkBg = '#141414'
|
||||
const baseMenuBg = isDark.value ? baseDarkBg : baseLightBg
|
||||
|
||||
// 改进:侧边栏背景使用 HSL 调整而不是简单线性混合,提高在不同主色下的可读性
|
||||
const siderBg = deriveSiderBg(primaryColor, baseMenuBg, isDark.value)
|
||||
const siderBorder = deriveSiderBorder(siderBg, primaryColor, isDark.value)
|
||||
|
||||
// 基础文字候选色
|
||||
const candidateTextLight = 'rgba(255,255,255,0.88)'
|
||||
const candidateTextDark = 'rgba(0,0,0,0.88)'
|
||||
|
||||
// 选菜单文字颜色:对 siderBg 计算对比度,优先满足 >=4.5
|
||||
const menuTextColor = pickAccessibleColor(candidateTextDark, candidateTextLight, siderBg)
|
||||
const iconColor = menuTextColor
|
||||
|
||||
// ===== AntD token 变量(保留) =====
|
||||
if (isDark.value) {
|
||||
// 深色模式变量
|
||||
root.style.setProperty('--ant-color-primary', primaryColor)
|
||||
root.style.setProperty('--ant-color-primary-hover', lightenColor(primaryColor, 10))
|
||||
root.style.setProperty('--ant-color-primary-bg', `${primaryColor}1a`)
|
||||
root.style.setProperty('--ant-color-primary-hover', hslLighten(primaryColor, 6))
|
||||
root.style.setProperty('--ant-color-primary-bg', addAlpha(primaryColor, 0.10))
|
||||
root.style.setProperty('--ant-color-text', 'rgba(255, 255, 255, 0.88)')
|
||||
root.style.setProperty('--ant-color-text-secondary', 'rgba(255, 255, 255, 0.65)')
|
||||
root.style.setProperty('--ant-color-text-tertiary', 'rgba(255, 255, 255, 0.45)')
|
||||
root.style.setProperty('--ant-color-bg-container', '#141414')
|
||||
root.style.setProperty('--ant-color-bg-container', baseDarkBg)
|
||||
root.style.setProperty('--ant-color-bg-layout', '#000000')
|
||||
root.style.setProperty('--ant-color-bg-elevated', '#1f1f1f')
|
||||
root.style.setProperty('--ant-color-border', '#424242')
|
||||
@@ -88,14 +105,13 @@ const updateCSSVariables = () => {
|
||||
root.style.setProperty('--ant-color-success', '#52c41a')
|
||||
root.style.setProperty('--ant-color-warning', '#faad14')
|
||||
} else {
|
||||
// 浅色模式变量
|
||||
root.style.setProperty('--ant-color-primary', primaryColor)
|
||||
root.style.setProperty('--ant-color-primary-hover', darkenColor(primaryColor, 10))
|
||||
root.style.setProperty('--ant-color-primary-bg', `${primaryColor}1a`)
|
||||
root.style.setProperty('--ant-color-primary-hover', hslDarken(primaryColor, 6))
|
||||
root.style.setProperty('--ant-color-primary-bg', addAlpha(primaryColor, 0.10))
|
||||
root.style.setProperty('--ant-color-text', 'rgba(0, 0, 0, 0.88)')
|
||||
root.style.setProperty('--ant-color-text-secondary', 'rgba(0, 0, 0, 0.65)')
|
||||
root.style.setProperty('--ant-color-text-tertiary', 'rgba(0, 0, 0, 0.45)')
|
||||
root.style.setProperty('--ant-color-bg-container', '#ffffff')
|
||||
root.style.setProperty('--ant-color-bg-container', baseLightBg)
|
||||
root.style.setProperty('--ant-color-bg-layout', '#f5f5f5')
|
||||
root.style.setProperty('--ant-color-bg-elevated', '#ffffff')
|
||||
root.style.setProperty('--ant-color-border', '#d9d9d9')
|
||||
@@ -104,9 +120,70 @@ const updateCSSVariables = () => {
|
||||
root.style.setProperty('--ant-color-success', '#52c41a')
|
||||
root.style.setProperty('--ant-color-warning', '#faad14')
|
||||
}
|
||||
|
||||
// ===== 自定义菜单配色 =====
|
||||
// 动态 Alpha:根据主色亮度调整透明度以保持区分度
|
||||
const lumPrim = getLuminance(primaryColor)
|
||||
const hoverAlphaBase = isDark.value ? 0.22 : 0.14
|
||||
const selectedAlphaBase = isDark.value ? 0.38 : 0.26
|
||||
const hoverAlpha = clamp01(hoverAlphaBase + (isDark.value ? (lumPrim > 0.65 ? -0.04 : 0) : (lumPrim < 0.30 ? 0.04 : 0)))
|
||||
const selectedAlpha = clamp01(selectedAlphaBase + (isDark.value ? (lumPrim > 0.65 ? -0.05 : 0) : (lumPrim < 0.30 ? 0.05 : 0)))
|
||||
|
||||
// 估算最终选中背景(混合算实际颜色用于对比度计算)
|
||||
const estimatedSelectedBg = blendColors(baseMenuBg, primaryColor, selectedAlpha)
|
||||
const selectedTextColor = pickAccessibleColor('rgba(0,0,0,0.90)', 'rgba(255,255,255,0.92)', estimatedSelectedBg)
|
||||
const hoverTextColor = menuTextColor
|
||||
|
||||
root.style.setProperty('--app-sider-bg', siderBg)
|
||||
root.style.setProperty('--app-sider-border-color', siderBorder)
|
||||
root.style.setProperty('--app-menu-text-color', menuTextColor)
|
||||
root.style.setProperty('--app-menu-icon-color', iconColor)
|
||||
root.style.setProperty('--app-menu-item-hover-text-color', hoverTextColor)
|
||||
root.style.setProperty('--app-menu-item-selected-text-color', selectedTextColor)
|
||||
|
||||
// 背景同时提供 rgba 与 hex alpha(兼容处理)
|
||||
const hoverRgba = hexToRgba(primaryColor, hoverAlpha)
|
||||
const selectedRgba = hexToRgba(primaryColor, selectedAlpha)
|
||||
root.style.setProperty('--app-menu-item-hover-bg', hoverRgba)
|
||||
root.style.setProperty('--app-menu-item-hover-bg-hex', addAlpha(primaryColor, hoverAlpha))
|
||||
root.style.setProperty('--app-menu-item-selected-bg', selectedRgba)
|
||||
root.style.setProperty('--app-menu-item-selected-bg-hex', addAlpha(primaryColor, selectedAlpha))
|
||||
}
|
||||
|
||||
// 颜色工具函数
|
||||
// ===== 新增缺失基础函数(从旧版本恢复) =====
|
||||
const addAlpha = (hex: string, alpha: number) => {
|
||||
const a = alpha > 1 ? alpha / 100 : alpha
|
||||
const clamped = Math.min(1, Math.max(0, a))
|
||||
const alphaHex = Math.round(clamped * 255).toString(16).padStart(2, '0')
|
||||
return `${hex}${alphaHex}`
|
||||
}
|
||||
|
||||
const blendColors = (color1: string, color2: string, ratio: number) => {
|
||||
const r1 = hexToRgb(color1)
|
||||
const r2 = hexToRgb(color2)
|
||||
if (!r1 || !r2) return color1
|
||||
const r = Math.round(r1.r * (1 - ratio) + r2.r * ratio)
|
||||
const g = Math.round(r1.g * (1 - ratio) + r2.g * ratio)
|
||||
const b = Math.round(r1.b * (1 - ratio) + r2.b * ratio)
|
||||
return rgbToHex(r, g, b)
|
||||
}
|
||||
|
||||
const getLuminance = (hex: string) => {
|
||||
const rgb = hexToRgb(hex)
|
||||
if (!rgb) return 0
|
||||
const transform = (v: number) => {
|
||||
const srgb = v / 255
|
||||
return srgb <= 0.03928 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4)
|
||||
}
|
||||
const r = transform(rgb.r)
|
||||
const g = transform(rgb.g)
|
||||
const b = transform(rgb.b)
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||
}
|
||||
|
||||
// ===== 新增/改进的颜色工具 =====
|
||||
const clamp01 = (v: number) => Math.min(1, Math.max(0, v))
|
||||
|
||||
const hexToRgb = (hex: string) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return result
|
||||
@@ -122,24 +199,105 @@ const rgbToHex = (r: number, g: number, b: number) => {
|
||||
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)
|
||||
}
|
||||
|
||||
const lightenColor = (hex: string, percent: number) => {
|
||||
// 新增:hex -> rgba 字符串
|
||||
const hexToRgba = (hex: string, alpha: number) => {
|
||||
const rgb = hexToRgb(hex)
|
||||
if (!rgb) return hex
|
||||
|
||||
const { r, g, b } = rgb
|
||||
const amount = Math.round(2.55 * percent)
|
||||
|
||||
return rgbToHex(Math.min(255, r + amount), Math.min(255, g + amount), Math.min(255, b + amount))
|
||||
if (!rgb) return 'rgba(0,0,0,0)'
|
||||
const a = alpha > 1 ? alpha / 100 : alpha
|
||||
return `rgba(${rgb.r},${rgb.g},${rgb.b},${clamp01(a)})`
|
||||
}
|
||||
|
||||
const darkenColor = (hex: string, percent: number) => {
|
||||
// HSL 转换(感知更平滑)
|
||||
const rgbToHsl = (r: number, g: number, b: number) => {
|
||||
r /= 255; g /= 255; b /= 255
|
||||
const max = Math.max(r, g, b), min = Math.min(r, g, b)
|
||||
let h = 0, s = 0
|
||||
const l = (max + min) / 2
|
||||
const d = max - min
|
||||
if (d !== 0) {
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
||||
switch (max) {
|
||||
case r: h = (g - b) / d + (g < b ? 6 : 0); break
|
||||
case g: h = (b - r) / d + 2; break
|
||||
case b: h = (r - g) / d + 4; break
|
||||
}
|
||||
h /= 6
|
||||
}
|
||||
return { h: h * 360, s, l }
|
||||
}
|
||||
|
||||
const hslToRgb = (h: number, s: number, l: number) => {
|
||||
h /= 360
|
||||
if (s === 0) {
|
||||
const val = Math.round(l * 255)
|
||||
return { r: val, g: val, b: val }
|
||||
}
|
||||
const hue2rgb = (p: number, q: number, t: number) => {
|
||||
if (t < 0) t += 1
|
||||
if (t > 1) t -= 1
|
||||
if (t < 1/6) return p + (q - p) * 6 * t
|
||||
if (t < 1/2) return q
|
||||
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6
|
||||
return p
|
||||
}
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s
|
||||
const p = 2 * l - q
|
||||
const r = hue2rgb(p, q, h + 1/3)
|
||||
const g = hue2rgb(p, q, h)
|
||||
const b = hue2rgb(p, q, h - 1/3)
|
||||
return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) }
|
||||
}
|
||||
|
||||
const hslAdjust = (hex: string, dl: number) => {
|
||||
const rgb = hexToRgb(hex)
|
||||
if (!rgb) return hex
|
||||
const { h, s, l } = rgbToHsl(rgb.r, rgb.g, rgb.b)
|
||||
const nl = clamp01(l + dl)
|
||||
const nrgb = hslToRgb(h, s, nl)
|
||||
return rgbToHex(nrgb.r, nrgb.g, nrgb.b)
|
||||
}
|
||||
|
||||
const { r, g, b } = rgb
|
||||
const amount = Math.round(2.55 * percent)
|
||||
const hslLighten = (hex: string, percent: number) => hslAdjust(hex, percent/100)
|
||||
const hslDarken = (hex: string, percent: number) => hslAdjust(hex, -percent/100)
|
||||
|
||||
return rgbToHex(Math.max(0, r - amount), Math.max(0, g - amount), Math.max(0, b - amount))
|
||||
// 对比度 (WCAG)
|
||||
const contrastRatio = (hex1: string, hex2: string) => {
|
||||
const L1 = getLuminance(hex1)
|
||||
const L2 = getLuminance(hex2)
|
||||
const light = Math.max(L1, L2)
|
||||
const dark = Math.min(L1, L2)
|
||||
return (light + 0.05) / (dark + 0.05)
|
||||
}
|
||||
|
||||
const rgbaExtractHex = (color: string) => {
|
||||
// 只支持 hex(#rrggbb) 直接返回;若 rgba 则忽略 alpha 并合成背景为黑假设
|
||||
if (color.startsWith('#') && (color.length === 7)) return color
|
||||
// 简化:返回黑或白占位
|
||||
return '#000000'
|
||||
}
|
||||
|
||||
const pickAccessibleColor = (c1: string, c2: string, bg: string, minRatio = 4.5) => {
|
||||
const hexBg = rgbaExtractHex(bg)
|
||||
const hex1 = rgbaExtractHex(c1 === 'rgba(255,255,255,0.88)' ? '#ffffff' : (c1.includes('255,255,255') ? '#ffffff' : '#000000'))
|
||||
const hex2 = rgbaExtractHex(c2 === 'rgba(255,255,255,0.88)' ? '#ffffff' : (c2.includes('255,255,255') ? '#ffffff' : '#000000'))
|
||||
const r1 = contrastRatio(hex1, hexBg)
|
||||
const r2 = contrastRatio(hex2, hexBg)
|
||||
// 优先满足 >= minRatio;都满足取更高;否则取更高
|
||||
if (r1 >= minRatio && r2 >= minRatio) return r1 >= r2 ? c1 : c2
|
||||
if (r1 >= minRatio) return c1
|
||||
if (r2 >= minRatio) return c2
|
||||
return r1 >= r2 ? c1 : c2
|
||||
}
|
||||
|
||||
// 改进侧栏背景:如果深色模式,降低亮度并略增饱和;浅色模式提高亮度轻度染色
|
||||
const deriveSiderBg = (primary: string, base: string, dark: boolean) => {
|
||||
const mixRatio = dark ? 0.22 : 0.18
|
||||
const mixed = blendColors(base, primary, mixRatio)
|
||||
return dark ? hslDarken(mixed, 8) : hslLighten(mixed, 6)
|
||||
}
|
||||
|
||||
const deriveSiderBorder = (siderBg: string, primary: string, dark: boolean) => {
|
||||
return dark ? blendColors(siderBg, primary, 0.30) : blendColors('#d9d9d9', primary, 0.25)
|
||||
}
|
||||
|
||||
// 监听系统主题变化
|
||||
|
||||
Reference in New Issue
Block a user