💄 更改顶边ui和侧边栏ui

This commit is contained in:
MoeSnowyFox
2025-09-08 00:03:52 +08:00
parent ac182a7c77
commit cbba670eb2
3 changed files with 281 additions and 252 deletions

View File

@@ -1,78 +1,31 @@
<template> <template>
<a-layout style="height: 100vh; overflow: hidden" class="app-layout-collapsed"> <a-layout style="height: 100vh; overflow: hidden">
<a-layout-sider <a-layout-sider
v-model:collapsed="collapsed" :width="SIDER_WIDTH"
collapsible
:trigger="null"
:width="180"
:collapsed-width="60"
:theme="isDark ? 'dark' : 'light'" :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="sider-content">
<!-- &lt;!&ndash; 折叠按钮 &ndash;&gt;-->
<!-- <div class="collapse-trigger" @click="toggleCollapse">-->
<!-- <MenuFoldOutlined v-if="!collapsed" />-->
<!-- <MenuUnfoldOutlined v-else />-->
<!-- </div>-->
<!-- 主菜单容器 -->
<div class="main-menu-container">
<a-menu <a-menu
mode="inline" mode="inline"
:inline-collapsed="collapsed"
:theme="isDark ? 'dark' : 'light'" :theme="isDark ? 'dark' : 'light'"
class="main-menu"
v-model:selectedKeys="selectedKeys" v-model:selectedKeys="selectedKeys"
> :items="mainMenuItems"
<template v-for="item in mainMenuItems" :key="item.path"> @click="onMenuClick"
<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 <a-menu
mode="inline" mode="inline"
:inline-collapsed="collapsed"
:theme="isDark ? 'dark' : 'light'" :theme="isDark ? 'dark' : 'light'"
class="bottom-menu" class="bottom-menu"
v-model:selectedKeys="selectedKeys" v-model:selectedKeys="selectedKeys"
> :items="bottomMenuItems"
<template v-for="item in bottomMenuItems" :key="item.path"> @click="onMenuClick"
<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> </div>
</a-layout-sider> </a-layout-sider>
<!-- 主内容区 --> <a-layout :style="{ marginLeft: SIDER_WIDTH + 'px', height: 'calc(100vh - 32px)', transition: 'margin-left .2s' }">
<a-layout <a-layout-content class="content-area">
: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',
}"
>
<router-view /> <router-view />
</a-layout-content> </a-layout-content>
</a-layout> </a-layout>
@@ -88,202 +41,92 @@ import {
ControlOutlined, ControlOutlined,
HistoryOutlined, HistoryOutlined,
SettingOutlined, SettingOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
} from '@ant-design/icons-vue' } from '@ant-design/icons-vue'
import { ref, computed } from 'vue' import { computed, h } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useTheme } from '../composables/useTheme.ts' import { useTheme } from '../composables/useTheme.ts'
import type { MenuProps } from 'ant-design-vue'
const SIDER_WIDTH = 140
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const { isDark } = useTheme() const { isDark } = useTheme()
const collapsed = ref<boolean>(false) // 工具:生成菜单项
const icon = (Comp: any) => () => h(Comp)
// 菜单数据
const mainMenuItems = [ const mainMenuItems = [
{ path: '/home', label: '主页', icon: HomeOutlined }, { key: '/home', label: '主页', icon: icon(HomeOutlined) },
{ path: '/scripts', label: '脚本管理', icon: FileTextOutlined }, { key: '/scripts', label: '脚本管理', icon: icon(FileTextOutlined) },
{ path: '/plans', label: '计划管理', icon: CalendarOutlined }, { key: '/plans', label: '计划管理', icon: icon(CalendarOutlined) },
{ path: '/queue', label: '调度队列', icon: UnorderedListOutlined }, { key: '/queue', label: '调度队列', icon: icon(UnorderedListOutlined) },
{ path: '/scheduler', label: '调度中心', icon: ControlOutlined }, { key: '/scheduler', label: '调度中心', icon: icon(ControlOutlined) },
{ path: '/history', label: '历史记录', icon: HistoryOutlined }, { 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 selectedKeys = computed(() => {
const path = route.path const path = route.path
const allItems = [...mainMenuItems, ...bottomMenuItems] const matched = allItems.find(i => path.startsWith(String(i.key)))
const matched = allItems.find(item => path.startsWith(item.path)) return [matched?.key || '/home']
return [matched?.path || '/home']
}) })
const goTo = (path: string) => { const onMenuClick: MenuProps['onClick'] = info => {
router.push(path) const target = String(info.key)
} if (route.path !== target) router.push(target)
const toggleCollapse = () => {
collapsed.value = !collapsed.value
} }
</script> </script>
<style scoped> <style scoped>
.sider-content { .sider-content { height:100%; display:flex; flex-direction:column; padding:4px 0 8px 0; }
height: 100%; .sider-content :deep(.ant-menu) { border-inline-end: none !important; background: transparent !important; }
display: flex; /* 菜单项外框居中(左右留空),内容左对齐 */
flex-direction: column; .sider-content :deep(.ant-menu .ant-menu-item) {
padding-bottom: 4px; /* 关键添加3px底部内边距 */ color: var(--app-menu-text-color);
} margin: 2px auto; /* 水平居中 */
width: calc(100% - 16px); /* 两侧各留 8px 空隙 */
/* 折叠按钮 */ border-radius: 6px;
.collapse-trigger { padding: 0 10px !important; /* 左右内边距 */
height: 42px; line-height: 36px;
height: 36px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: flex-start; /* 左对齐图标与文字 */
margin: 4px; gap: 6px;
border-radius: 6px; transition: background .16s ease, color .16s ease;
cursor: pointer; text-align: left;
}
.sider-content :deep(.ant-menu .ant-menu-item .anticon) {
color: var(--app-menu-icon-color);
font-size: 16px; font-size: 16px;
transition: background-color 0.2s; line-height: 1;
transition: color .16s ease;
margin-right: 0;
} }
/* Hover */
.collapse-trigger:hover { .sider-content :deep(.ant-menu .ant-menu-item:hover) {
background-color: rgba(255, 255, 255, 0.1); background: var(--app-menu-item-hover-bg, var(--app-menu-item-hover-bg-hex));
color: var(--app-menu-item-hover-text-color);
} }
.sider-content :deep(.ant-menu .ant-menu-item:hover .anticon) { color: var(--app-menu-item-hover-text-color); }
:deep(.ant-layout-sider-light) .collapse-trigger:hover { /* Selected */
background-color: rgba(0, 0, 0, 0.04); .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;
:deep(.ant-layout-sider-dark) .collapse-trigger { font-weight: 500;
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-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>
<!-- 全局样式 --> <!-- 调整外框菜单项背景块水平居中文字与图标左对齐 -->
<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>

View File

@@ -3,6 +3,8 @@
<!-- 左侧Logo和软件名 --> <!-- 左侧Logo和软件名 -->
<div class="title-bar-left"> <div class="title-bar-left">
<div class="logo-section"> <div class="logo-section">
<!-- 新增虚化主题色圆形阴影 -->
<span class="logo-glow" aria-hidden="true"></span>
<img src="@/assets/AUTO-MAS.ico" alt="AUTO-MAS" class="title-logo" /> <img src="@/assets/AUTO-MAS.ico" alt="AUTO-MAS" class="title-logo" />
<span class="title-text">AUTO-MAS</span> <span class="title-text">AUTO-MAS</span>
</div> </div>
@@ -94,6 +96,7 @@ onMounted(async () => {
user-select: none; user-select: none;
position: relative; position: relative;
z-index: 1000; z-index: 1000;
overflow: hidden; /* 新增:裁剪超出顶栏的发光 */
} }
.title-bar-dark { .title-bar-dark {
@@ -112,17 +115,42 @@ onMounted(async () => {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; 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 { .title-logo {
width: 20px; width: 20px;
height: 20px; height: 20px;
position: relative;
z-index: 1; /* 确保在阴影上方 */
} }
.title-text { .title-text {
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
position: relative;
z-index: 1;
} }
.title-bar-dark .title-text { .title-bar-dark .title-text {

View File

@@ -71,15 +71,32 @@ const updateCSSVariables = () => {
const root = document.documentElement const root = document.documentElement
const primaryColor = themeColors[themeColor.value] 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) { if (isDark.value) {
// 深色模式变量
root.style.setProperty('--ant-color-primary', primaryColor) root.style.setProperty('--ant-color-primary', primaryColor)
root.style.setProperty('--ant-color-primary-hover', lightenColor(primaryColor, 10)) root.style.setProperty('--ant-color-primary-hover', hslLighten(primaryColor, 6))
root.style.setProperty('--ant-color-primary-bg', `${primaryColor}1a`) 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', '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-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-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-layout', '#000000')
root.style.setProperty('--ant-color-bg-elevated', '#1f1f1f') root.style.setProperty('--ant-color-bg-elevated', '#1f1f1f')
root.style.setProperty('--ant-color-border', '#424242') 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-success', '#52c41a')
root.style.setProperty('--ant-color-warning', '#faad14') root.style.setProperty('--ant-color-warning', '#faad14')
} else { } else {
// 浅色模式变量
root.style.setProperty('--ant-color-primary', primaryColor) root.style.setProperty('--ant-color-primary', primaryColor)
root.style.setProperty('--ant-color-primary-hover', darkenColor(primaryColor, 10)) root.style.setProperty('--ant-color-primary-hover', hslDarken(primaryColor, 6))
root.style.setProperty('--ant-color-primary-bg', `${primaryColor}1a`) 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', '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-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-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-layout', '#f5f5f5')
root.style.setProperty('--ant-color-bg-elevated', '#ffffff') root.style.setProperty('--ant-color-bg-elevated', '#ffffff')
root.style.setProperty('--ant-color-border', '#d9d9d9') 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-success', '#52c41a')
root.style.setProperty('--ant-color-warning', '#faad14') 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 hexToRgb = (hex: string) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result 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) 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) const rgb = hexToRgb(hex)
if (!rgb) return hex if (!rgb) return 'rgba(0,0,0,0)'
const a = alpha > 1 ? alpha / 100 : alpha
const { r, g, b } = rgb return `rgba(${rgb.r},${rgb.g},${rgb.b},${clamp01(a)})`
const amount = Math.round(2.55 * percent)
return rgbToHex(Math.min(255, r + amount), Math.min(255, g + amount), Math.min(255, b + amount))
} }
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) const rgb = hexToRgb(hex)
if (!rgb) return 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 hslLighten = (hex: string, percent: number) => hslAdjust(hex, percent/100)
const amount = Math.round(2.55 * percent) 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)
} }
// 监听系统主题变化 // 监听系统主题变化