@spacego/fe-components 0.0.1-alpha.2 → 0.0.1-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/assets/svg/chrome-bg-after.svg.js +3 -2
- package/lib/assets/svg/chrome-bg-before.svg.js +2 -1
- package/lib/assets/svg/icon-arrows-right.svg.js +3 -2
- package/lib/assets/svg/icon-bell.svg.js +3 -2
- package/lib/assets/svg/icon-custom.svg.js +3 -2
- package/lib/assets/svg/icon-sun-moon.svg.js +3 -2
- package/lib/assets/svg/icon-system.svg.js +3 -2
- package/lib/assets/svg/login-view.svg.js +871 -2
- package/lib/fe-layouts/auth-layout/index.js +8 -8
- package/lib/fe-layouts/basics-layout/components/basics-layout/tabs.js +5 -5
- package/package.json +9 -1
- package/src/assets/styles/animate.css +0 -62
- package/src/assets/styles/index.css +0 -49
- package/src/assets/svg/chrome-bg-after.svg +0 -1
- package/src/assets/svg/chrome-bg-before.svg +0 -1
- package/src/assets/svg/icon-arrows-right.svg +0 -1
- package/src/assets/svg/icon-bell.svg +0 -1
- package/src/assets/svg/icon-custom.svg +0 -1
- package/src/assets/svg/icon-sun-moon.svg +0 -1
- package/src/assets/svg/icon-system.svg +0 -1
- package/src/assets/svg/loading.svg +0 -13
- package/src/assets/svg/login-view.svg +0 -193
- package/src/config/constants.ts +0 -19
- package/src/config/index.ts +0 -2
- package/src/config/theme.ts +0 -20
- package/src/fe-layouts/auth-layout/index.scss +0 -34
- package/src/fe-layouts/auth-layout/index.tsx +0 -121
- package/src/fe-layouts/basics-layout/components/basics-layout/header.tsx +0 -148
- package/src/fe-layouts/basics-layout/components/basics-layout/setting-custom-color.tsx +0 -52
- package/src/fe-layouts/basics-layout/components/basics-layout/setting-drawer.tsx +0 -165
- package/src/fe-layouts/basics-layout/components/basics-layout/sidebar.tsx +0 -88
- package/src/fe-layouts/basics-layout/components/basics-layout/tabs.tsx +0 -94
- package/src/fe-layouts/basics-layout/components/utils/index.ts +0 -142
- package/src/fe-layouts/basics-layout/index.scss +0 -110
- package/src/fe-layouts/basics-layout/index.tsx +0 -207
- package/src/fe-layouts/blank-layout/index.tsx +0 -12
- package/src/fe-layouts/context/context.ts +0 -11
- package/src/fe-layouts/context/global-context.d.ts +0 -241
- package/src/fe-layouts/context/global-context.provider.tsx +0 -81
- package/src/fe-layouts/context/index.ts +0 -10
- package/src/fe-layouts/index.ts +0 -13
- package/src/fe-layouts/layout.tsx +0 -74
- package/src/hooks/index.ts +0 -1
- package/src/hooks/use-auth.hook.ts +0 -54
- package/src/index.ts +0 -24
- package/src/router/index.ts +0 -110
- package/src/router/permission.tsx +0 -134
- package/src/router/routes.tsx +0 -94
- package/src/router/utils.ts +0 -283
- package/src/store/index.ts +0 -9
- package/src/store/modules/layout-config.store.ts +0 -343
- package/src/store/modules/theme.store.ts +0 -99
- package/src/typings/index.d.ts +0 -59
- package/src/typings/shims-axios.d.ts +0 -38
- package/src/utils/icon.tsx +0 -32
- package/src/utils/index.ts +0 -9
- package/src/utils/theme.ts +0 -219
- package/tsconfig.json +0 -28
- package/vite.config.ts +0 -85
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* @Author: dushuai
|
|
3
|
-
* @Date: 2026-01-26 22:13:52
|
|
4
|
-
* @LastEditors: dushuai
|
|
5
|
-
* @LastEditTime: 2026-02-01 02:00:04
|
|
6
|
-
* @description: login-layout
|
|
7
|
-
*/
|
|
8
|
-
import { useSelector } from '@spacego/zustand';
|
|
9
|
-
import { useState } from 'react';
|
|
10
|
-
import { FaCheck } from 'react-icons/fa6';
|
|
11
|
-
import { IoMoon } from 'react-icons/io5';
|
|
12
|
-
import { LuPalette } from 'react-icons/lu';
|
|
13
|
-
import { MdWbSunny } from 'react-icons/md';
|
|
14
|
-
import { Outlet } from 'react-router-dom';
|
|
15
|
-
|
|
16
|
-
import LoginSvg from '@/assets/svg/login-view.svg?react';
|
|
17
|
-
import { applyThemeWithTransition, THEME_COLORS, useGlobal, useThemeStore } from '@/index';
|
|
18
|
-
|
|
19
|
-
import './index.scss';
|
|
20
|
-
|
|
21
|
-
export default function LoginLayout() {
|
|
22
|
-
|
|
23
|
-
const { appName, logo } = useGlobal();
|
|
24
|
-
|
|
25
|
-
const { theme, themeColor, TOGGLE_THEME, SET_THEME_COLOR } = useThemeStore(useSelector(['theme', 'themeColor', 'TOGGLE_THEME', 'SET_THEME_COLOR']));
|
|
26
|
-
// 记录调色板图标的 hover 状态,可在组件中使用 isPaletteHovered
|
|
27
|
-
const [isPaletteHovered, setIsPaletteHovered] = useState(false);
|
|
28
|
-
|
|
29
|
-
// 处理主题切换
|
|
30
|
-
const handleToggleTheme = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
31
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
32
|
-
const x = rect.left + rect.width / 2;
|
|
33
|
-
const y = rect.top + rect.height / 2;
|
|
34
|
-
|
|
35
|
-
// 使用带过渡的主题切换
|
|
36
|
-
const newTheme = theme === 'light' ? 'dark' : 'light';
|
|
37
|
-
applyThemeWithTransition(newTheme, x, y).then(() => {
|
|
38
|
-
TOGGLE_THEME();
|
|
39
|
-
});
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<div className="flex min-h-full flex-1 select-none overflow-x-hidden">
|
|
44
|
-
|
|
45
|
-
<div
|
|
46
|
-
className="bg-[#f4f4f5] dark:bg-[#2e3033] rounded-3xl px-3 py-1 flex-center absolute right-3 top-4 z-10 text-[#323639cc] dark:text-[#f2f2f2] flex justify-end transition-all duration-300 ease-in-out"
|
|
47
|
-
>
|
|
48
|
-
<div
|
|
49
|
-
className="flex overflow-hidden items-center gap-3 whitespace-nowrap origin-right"
|
|
50
|
-
style={{
|
|
51
|
-
width: isPaletteHovered ? '224px' : '0', // 7个颜色(20px) + 6个gap(12px) = 140 + 72 = 212px
|
|
52
|
-
transform: `scaleX(${isPaletteHovered ? 1 : 0})`,
|
|
53
|
-
opacity: isPaletteHovered ? 1 : 0,
|
|
54
|
-
transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), width 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
|
|
55
|
-
}}
|
|
56
|
-
onMouseEnter={() => setIsPaletteHovered(true)}
|
|
57
|
-
onMouseLeave={() => setIsPaletteHovered(false)}
|
|
58
|
-
>
|
|
59
|
-
{
|
|
60
|
-
THEME_COLORS.map((color) => (
|
|
61
|
-
<div
|
|
62
|
-
key={color}
|
|
63
|
-
className="w-5 h-5 rounded-full flex items-center justify-center cursor-pointer hover-scale-animation shrink-0"
|
|
64
|
-
style={{ backgroundColor: color }}
|
|
65
|
-
onClick={() => SET_THEME_COLOR(color)}
|
|
66
|
-
>
|
|
67
|
-
{themeColor === color && <FaCheck className="size-[10px] text-white" />}
|
|
68
|
-
</div>
|
|
69
|
-
))
|
|
70
|
-
}
|
|
71
|
-
</div>
|
|
72
|
-
|
|
73
|
-
<div className="flex relative z-10 shrink-0">
|
|
74
|
-
<div
|
|
75
|
-
className="w-8 h-8 flex items-center justify-center cursor-pointer hover-scale-animation"
|
|
76
|
-
onMouseEnter={() => setIsPaletteHovered(true)}
|
|
77
|
-
onMouseLeave={() => setIsPaletteHovered(false)}
|
|
78
|
-
>
|
|
79
|
-
<LuPalette className="h-[16px] w-[16px] text-(--color-primary)" />
|
|
80
|
-
</div>
|
|
81
|
-
|
|
82
|
-
<div className="w-8 h-8 flex items-center justify-center cursor-pointer hover-scale-animation" onClick={handleToggleTheme}>
|
|
83
|
-
{theme === 'dark' ? <MdWbSunny className="h-[18px] w-[18px]" /> : <IoMoon className="h-[18px] w-[18px]" />}
|
|
84
|
-
</div>
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
|
-
|
|
88
|
-
<div className="absolute left-0 top-0 z-10 flex flex-1">
|
|
89
|
-
<div className="text-foreground lg:text-foreground ml-4 mt-4 flex flex-1 items-center sm:left-6 sm:top-6">
|
|
90
|
-
<div className="m-0 text-xl font-medium flex items-center gap-2 text-[#333] dark:text-[#f2f2f2]">
|
|
91
|
-
{logo}
|
|
92
|
-
{appName}
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
|
|
97
|
-
<div className="relative hidden w-0 flex-1 lg:block">
|
|
98
|
-
<div className="bg-background-deep absolute inset-0 h-full w-full dark:bg-[#070709]!">
|
|
99
|
-
<div className="login-background absolute left-0 top-0 size-full" />
|
|
100
|
-
|
|
101
|
-
<div className="flex flex-col items-center justify-center mr-20 h-full -enter-x">
|
|
102
|
-
<LoginSvg className="animate-float h-64 w-2/5" />
|
|
103
|
-
|
|
104
|
-
<div className="text-1xl mt-6 font-sans lg:text-2xl text-[#333] dark:text-[#f2f2f2]">企业级的大型中后台管理系统</div>
|
|
105
|
-
<div className="mt-2 text-[#323639] dark:text-[#a1a1aa]">高效、安全、易用的管理系统</div>
|
|
106
|
-
</div>
|
|
107
|
-
</div>
|
|
108
|
-
</div>
|
|
109
|
-
|
|
110
|
-
<div className="login-side flex flex-col items-center justify-center relative px-6 py-10 lg:flex-initial lg:px-8 min-h-full w-2/5 flex-1 dark:bg-[#14161a]">
|
|
111
|
-
<Outlet />
|
|
112
|
-
|
|
113
|
-
<div className="text-[#71717a] absolute bottom-3 flex text-center text-xs">
|
|
114
|
-
<div className="text-md flex justify-center mt-4">
|
|
115
|
-
Copyright © 2024 <a href="https://dshuais.netlify.app/" className="text-(--color-primary)! hover:text-(--hover-primary-color)! mx-1" target="_blank">DuShuai</a>
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
</div>
|
|
119
|
-
</div>
|
|
120
|
-
);
|
|
121
|
-
}
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* @Author: dushuai
|
|
3
|
-
* @Date: 2026-01-27
|
|
4
|
-
* @LastEditors: dushuai
|
|
5
|
-
* @LastEditTime: 2026-02-01 02:00:08
|
|
6
|
-
* @description: 顶部导航栏组件
|
|
7
|
-
*/
|
|
8
|
-
import { LogoutOutlined } from '@ant-design/icons';
|
|
9
|
-
import { useSelector } from '@spacego/zustand';
|
|
10
|
-
import { Avatar, Badge, Breadcrumb, Dropdown, type MenuProps, Popover, Space } from 'antd';
|
|
11
|
-
import { useState } from 'react';
|
|
12
|
-
import { IoMoon } from 'react-icons/io5';
|
|
13
|
-
import { MdWbSunny } from 'react-icons/md';
|
|
14
|
-
import { Link } from 'react-router-dom';
|
|
15
|
-
|
|
16
|
-
import IconArrowsRightSvg from '@/assets/svg/icon-arrows-right.svg?react';
|
|
17
|
-
import IconBellSvg from '@/assets/svg/icon-bell.svg?react';
|
|
18
|
-
import IconSystemSvg from '@/assets/svg/icon-system.svg?react';
|
|
19
|
-
import { useAuth } from '@/hooks';
|
|
20
|
-
import { useThemeStore } from '@/store';
|
|
21
|
-
import { applyThemeWithTransition } from '@/utils';
|
|
22
|
-
|
|
23
|
-
import { useGlobal } from '../../../context';
|
|
24
|
-
import SettingDrawer from './setting-drawer';
|
|
25
|
-
|
|
26
|
-
interface HeaderProps {
|
|
27
|
-
breadcrumbItems: { title: string }[]
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export default function Header({ breadcrumbItems }: HeaderProps) {
|
|
31
|
-
const { logout } = useAuth();
|
|
32
|
-
const { appName, logo, headerActionsRender, userInfo, userMenuItems: _userMenuItems = [] } = useGlobal();
|
|
33
|
-
|
|
34
|
-
const { theme, TOGGLE_THEME } = useThemeStore(useSelector(['theme', 'TOGGLE_THEME']));
|
|
35
|
-
|
|
36
|
-
const [settingOpen, setSettingOpen] = useState(false);
|
|
37
|
-
|
|
38
|
-
// 处理主题切换
|
|
39
|
-
const handleToggleTheme = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
40
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
41
|
-
const x = rect.left + rect.width / 2;
|
|
42
|
-
const y = rect.top + rect.height / 2;
|
|
43
|
-
|
|
44
|
-
// 使用带过渡的主题切换
|
|
45
|
-
const newTheme = theme === 'light' ? 'dark' : 'light';
|
|
46
|
-
applyThemeWithTransition(newTheme, x, y).then(() => {
|
|
47
|
-
TOGGLE_THEME();
|
|
48
|
-
});
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
// 用户菜单
|
|
52
|
-
const userMenuItems: MenuProps['items'] = [
|
|
53
|
-
..._userMenuItems,
|
|
54
|
-
{
|
|
55
|
-
key: 'logout',
|
|
56
|
-
label: '退出登录',
|
|
57
|
-
icon: <LogoutOutlined />,
|
|
58
|
-
onClick: () => {
|
|
59
|
-
logout();
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<header className="layout-header h-[50px] px-4 flex items-center justify-between bg-(--global-background-color) border-b border-(--border-color) z-10">
|
|
66
|
-
<div className="flex items-center gap-2">
|
|
67
|
-
{/* Logo */}
|
|
68
|
-
<div className="min-w-[204px]">
|
|
69
|
-
<Link to="/" className="flex items-center gap-2 no-underline">
|
|
70
|
-
{logo}
|
|
71
|
-
<span className="text-lg font-semibold text-gray-800 dark:text-[#f2f2f2] tracking-wide">
|
|
72
|
-
{appName}
|
|
73
|
-
</span>
|
|
74
|
-
</Link>
|
|
75
|
-
</div>
|
|
76
|
-
|
|
77
|
-
{/* 面包屑 */}
|
|
78
|
-
<Breadcrumb
|
|
79
|
-
items={breadcrumbItems}
|
|
80
|
-
className="items-center basics-breadcrumb text-sm! hidden md:flex"
|
|
81
|
-
separator={<IconArrowsRightSvg className="w-4 h-4 text-gray-400" />}
|
|
82
|
-
/>
|
|
83
|
-
</div>
|
|
84
|
-
|
|
85
|
-
<div className="flex items-center">
|
|
86
|
-
<Space size={6}>
|
|
87
|
-
{/* 头部按钮组 */}
|
|
88
|
-
{headerActionsRender}
|
|
89
|
-
|
|
90
|
-
{/* 设置 */}
|
|
91
|
-
<div
|
|
92
|
-
className="cursor-pointer w-8 h-8 text-(--icon-text-color) hover:text-primary transition-colors p-2 rounded-full hover:bg-(--hover-background-color) hover-scale-animation"
|
|
93
|
-
onClick={() => setSettingOpen(true)}
|
|
94
|
-
>
|
|
95
|
-
<IconSystemSvg />
|
|
96
|
-
</div>
|
|
97
|
-
|
|
98
|
-
{/* 主题切换 */}
|
|
99
|
-
<div
|
|
100
|
-
onClick={handleToggleTheme}
|
|
101
|
-
className="cursor-pointer w-8 h-8 text-(--icon-text-color) hover:text-primary transition-colors p-2 rounded-full hover:bg-(--hover-background-color) hover-scale-animation flex items-center justify-center"
|
|
102
|
-
title={theme === 'light' ? '切换到暗色主题' : '切换到亮色主题'}
|
|
103
|
-
>
|
|
104
|
-
{theme === 'light' ? (
|
|
105
|
-
<IoMoon className="text-lg" />
|
|
106
|
-
) : (
|
|
107
|
-
<MdWbSunny className="text-lg" />
|
|
108
|
-
)}
|
|
109
|
-
</div>
|
|
110
|
-
|
|
111
|
-
{/* 通知 */}
|
|
112
|
-
<div className="cursor-pointer w-8 h-8 text-(--icon-text-color) hover:text-primary transition-colors p-2 rounded-full hover:bg-(--hover-background-color) hover-scale-animation">
|
|
113
|
-
<Popover
|
|
114
|
-
placement="bottom"
|
|
115
|
-
arrow={false}
|
|
116
|
-
trigger="click"
|
|
117
|
-
classNames={{
|
|
118
|
-
container: 'p-0!'
|
|
119
|
-
}}
|
|
120
|
-
content={1}
|
|
121
|
-
>
|
|
122
|
-
<Badge dot color="var(--color-primary)">
|
|
123
|
-
<IconBellSvg />
|
|
124
|
-
</Badge>
|
|
125
|
-
</Popover>
|
|
126
|
-
</div>
|
|
127
|
-
|
|
128
|
-
{/* 用户头像 */}
|
|
129
|
-
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight" arrow>
|
|
130
|
-
<div className="flex items-center gap-2 cursor-pointer hover:bg-(--hover-background-color) px-2 py-1 rounded-full transition-all border border-transparent">
|
|
131
|
-
<Avatar
|
|
132
|
-
size="default"
|
|
133
|
-
className="bg-primary/10 text-primary border border-primary/20"
|
|
134
|
-
src={userInfo?.avatar}
|
|
135
|
-
/>
|
|
136
|
-
<div className="hidden lg:flex flex-col items-start">
|
|
137
|
-
<span className="text-sm font-medium text-gray-700 dark:text-[#f2f2f2] leading-tight">{userInfo?.username}</span>
|
|
138
|
-
<span className="text-xs text-gray-400 dark:text-[#a1a1aa] leading-tight">{userInfo?.roleName}</span>
|
|
139
|
-
</div>
|
|
140
|
-
</div>
|
|
141
|
-
</Dropdown>
|
|
142
|
-
</Space>
|
|
143
|
-
</div>
|
|
144
|
-
|
|
145
|
-
<SettingDrawer open={settingOpen} onClose={() => setSettingOpen(false)} />
|
|
146
|
-
</header>
|
|
147
|
-
);
|
|
148
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* @Author: dushuai
|
|
3
|
-
* @Date: 2026-01-31 23:03:47
|
|
4
|
-
* @LastEditors: dushuai
|
|
5
|
-
* @LastEditTime: 2026-02-01 02:00:11
|
|
6
|
-
* @description: 自定义主题色
|
|
7
|
-
*/
|
|
8
|
-
import { useSelector } from '@spacego/zustand';
|
|
9
|
-
import { memo, useCallback, useRef } from 'react';
|
|
10
|
-
|
|
11
|
-
import IconCustomSvg from '@/assets/svg/icon-custom.svg?react';
|
|
12
|
-
import { useThemeStore } from '@/store';
|
|
13
|
-
import { applyThemeColor } from '@/utils';
|
|
14
|
-
|
|
15
|
-
function SettingCustomColor() {
|
|
16
|
-
const { themeColor, SET_THEME_COLOR } = useThemeStore(useSelector(['themeColor', 'SET_THEME_COLOR']));
|
|
17
|
-
|
|
18
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
19
|
-
const localValueRef = useRef<string>(themeColor);
|
|
20
|
-
|
|
21
|
-
const handleInput = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
22
|
-
const value = e.target.value;
|
|
23
|
-
localValueRef.current = value;
|
|
24
|
-
|
|
25
|
-
// 直接更新 CSS 变量,不更新 store,避免重新渲染
|
|
26
|
-
applyThemeColor(value);
|
|
27
|
-
}, []);
|
|
28
|
-
|
|
29
|
-
// onBlur 事件在颜色选择面板关闭时触发,确保最终值被保存
|
|
30
|
-
const handleBlur = useCallback(() => {
|
|
31
|
-
// 确保最终值被保存到 store
|
|
32
|
-
if(localValueRef.current) {
|
|
33
|
-
SET_THEME_COLOR(localValueRef.current);
|
|
34
|
-
}
|
|
35
|
-
}, []);
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<div className="w-5 h-5 relative z-10">
|
|
39
|
-
<IconCustomSvg />
|
|
40
|
-
<input
|
|
41
|
-
ref={inputRef}
|
|
42
|
-
type="color"
|
|
43
|
-
value={themeColor}
|
|
44
|
-
className="absolute w-full h-full z-10 opacity-0 cursor-pointer"
|
|
45
|
-
onInput={handleInput}
|
|
46
|
-
onBlur={handleBlur}
|
|
47
|
-
/>
|
|
48
|
-
</div>
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export default memo(SettingCustomColor);
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* @Author: dushuai
|
|
3
|
-
* @Date: 2026-01-29
|
|
4
|
-
* @LastEditors: dushuai
|
|
5
|
-
* @LastEditTime: 2026-01-30 21:13:40
|
|
6
|
-
* @description: 设置抽屉
|
|
7
|
-
*/
|
|
8
|
-
import { CloseOutlined } from '@ant-design/icons';
|
|
9
|
-
import { useSelector } from '@spacego/zustand';
|
|
10
|
-
import { Drawer } from 'antd';
|
|
11
|
-
import { IoMoon } from 'react-icons/io5';
|
|
12
|
-
import { MdWbSunny } from 'react-icons/md';
|
|
13
|
-
|
|
14
|
-
import IconSunMoonSvg from '@/assets/svg/icon-sun-moon.svg?react';
|
|
15
|
-
import { THEME_COLORS } from '@/config';
|
|
16
|
-
import { useThemeStore } from '@/store';
|
|
17
|
-
|
|
18
|
-
import SettingCustomColor from './setting-custom-color';
|
|
19
|
-
|
|
20
|
-
interface SettingDrawerProps {
|
|
21
|
-
open: boolean
|
|
22
|
-
onClose: () => void
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export default function SettingDrawer({ open, onClose }: SettingDrawerProps) {
|
|
26
|
-
const { theme, themeColor, isCustomThemeColor, SET_THEME_COLOR, SET_IS_CUSTOM_THEME_COLOR } =
|
|
27
|
-
useThemeStore(useSelector(['theme', 'themeColor', 'isCustomThemeColor', 'SET_THEME_COLOR', 'SET_IS_CUSTOM_THEME_COLOR']));
|
|
28
|
-
|
|
29
|
-
const list = [
|
|
30
|
-
{ mode: 'light', icon: MdWbSunny, label: '浅色' },
|
|
31
|
-
{ mode: 'dark', icon: IoMoon, label: '深色' },
|
|
32
|
-
{ mode: 'system', icon: IconSunMoonSvg, label: '跟随系统' }
|
|
33
|
-
] as const;
|
|
34
|
-
|
|
35
|
-
const colorNames = ['默认', '紫罗兰', '樱花粉', '柠檬黄', '天蓝色', '浅绿色', '中性色', '自定义'];
|
|
36
|
-
const themeColorList = [
|
|
37
|
-
...THEME_COLORS.map((color, index) => ({ color, name: colorNames[index], isCustom: false })),
|
|
38
|
-
{ color: themeColor, name: '自定义', isCustom: true }
|
|
39
|
-
] as const;
|
|
40
|
-
|
|
41
|
-
// 处理主题切换
|
|
42
|
-
const handleToggleTheme = (mode: 'light' | 'dark' | 'system') => {
|
|
43
|
-
if(theme === mode) return;
|
|
44
|
-
useThemeStore.getState().SET_THEME(mode);
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<Drawer
|
|
49
|
-
title={
|
|
50
|
-
<div className="flex items-end">
|
|
51
|
-
偏好设置
|
|
52
|
-
<span className="text-xs text-[#71717a] dark:text-[#a1a1aa] mb-[2px]">
|
|
53
|
-
自定义偏好设置 & 实时预览
|
|
54
|
-
</span>
|
|
55
|
-
</div>
|
|
56
|
-
}
|
|
57
|
-
placement="right"
|
|
58
|
-
onClose={onClose}
|
|
59
|
-
open={open}
|
|
60
|
-
width={380}
|
|
61
|
-
closable={false}
|
|
62
|
-
maskStyle={{ backdropFilter: 'none' }}
|
|
63
|
-
className="dark:bg-[#1a1c1f]!"
|
|
64
|
-
extra={
|
|
65
|
-
<div className="cursor-pointer hover:text-primary transition-colors" onClick={onClose}>
|
|
66
|
-
<CloseOutlined className="text-sm" />
|
|
67
|
-
</div>
|
|
68
|
-
}
|
|
69
|
-
styles={{
|
|
70
|
-
header: {
|
|
71
|
-
padding: '18px',
|
|
72
|
-
borderBottom: '1px solid var(--border-color)'
|
|
73
|
-
},
|
|
74
|
-
body: {
|
|
75
|
-
padding: '18px'
|
|
76
|
-
}
|
|
77
|
-
}}
|
|
78
|
-
>
|
|
79
|
-
{/* 主题模块 */}
|
|
80
|
-
<div className="mb-8">
|
|
81
|
-
<h3 className="text-base font-medium mb-4 text-(--text-color)">主题</h3>
|
|
82
|
-
<div className="grid grid-cols-3 gap-6">
|
|
83
|
-
{list.map(({ mode, icon: Icon, label }) => {
|
|
84
|
-
const isSelected = theme === mode;
|
|
85
|
-
|
|
86
|
-
return (
|
|
87
|
-
<div
|
|
88
|
-
key={mode}
|
|
89
|
-
className="group flex flex-col items-center gap-2 cursor-pointer"
|
|
90
|
-
onClick={() => handleToggleTheme(mode)}
|
|
91
|
-
>
|
|
92
|
-
<div className={`w-full aspect-[2] rounded-lg outline flex items-center justify-center transition-all relative overflow-hidden ${isSelected ? 'outline-(--color-primary) outline-2' : 'outline-[#e4e4e7] dark:outline-[#36363a]'}`}>
|
|
93
|
-
{
|
|
94
|
-
!isSelected && <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
|
95
|
-
<div className="w-full h-full rounded-lg border-2 border-(--color-primary) opacity-0 scale-0 transition-all duration-300 group-hover:opacity-100 group-hover:scale-100" />
|
|
96
|
-
</div>
|
|
97
|
-
}
|
|
98
|
-
<Icon className="text-xl text-[#2d2f32] dark:text-[#f0f0f0] relative z-10" />
|
|
99
|
-
</div>
|
|
100
|
-
<span className="text-xs text-[#71717a] dark:text-[#a1a1aa]">{label}</span>
|
|
101
|
-
</div>
|
|
102
|
-
);
|
|
103
|
-
})}
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
|
|
107
|
-
{/* 内置主题模块 */}
|
|
108
|
-
<div>
|
|
109
|
-
<h3 className="text-base font-medium mb-4 text-(--text-color)">内置主题</h3>
|
|
110
|
-
<div className="grid grid-cols-3 gap-6">
|
|
111
|
-
{themeColorList.map((item) => {
|
|
112
|
-
const isSelected = themeColor === item.color && !isCustomThemeColor;
|
|
113
|
-
|
|
114
|
-
return (
|
|
115
|
-
!item.isCustom ?
|
|
116
|
-
<div
|
|
117
|
-
key={item.color}
|
|
118
|
-
className="cursor-pointer group flex flex-col items-center gap-2"
|
|
119
|
-
onClick={() => {
|
|
120
|
-
SET_THEME_COLOR(item.color);
|
|
121
|
-
SET_IS_CUSTOM_THEME_COLOR(false);
|
|
122
|
-
}}
|
|
123
|
-
>
|
|
124
|
-
<div className={`w-full aspect-[2] rounded-lg outline flex items-center justify-center transition-all relative overflow-hidden ${isSelected ? 'outline-(--color-primary) outline-2' : 'outline-[#e4e4e7] dark:outline-[#36363a]'}`}>
|
|
125
|
-
{/* 未选中时显示hover效果 */}
|
|
126
|
-
{
|
|
127
|
-
!isSelected && <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
|
128
|
-
<div className="w-full h-full rounded-lg border-2 border-(--color-primary) opacity-0 scale-0 transition-all duration-300 group-hover:opacity-100 group-hover:scale-100" />
|
|
129
|
-
</div>
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
{/* 中心色块 */}
|
|
133
|
-
<div
|
|
134
|
-
className="w-5 h-5 rounded-md shadow-sm relative z-10"
|
|
135
|
-
style={{ backgroundColor: item.color }}
|
|
136
|
-
/>
|
|
137
|
-
</div>
|
|
138
|
-
<span className="text-xs text-[#71717a] dark:text-[#a1a1aa]">{item.name}</span>
|
|
139
|
-
</div>
|
|
140
|
-
:
|
|
141
|
-
<div
|
|
142
|
-
key={`custom-${item.color}`}
|
|
143
|
-
className="cursor-pointer group flex flex-col items-center gap-2"
|
|
144
|
-
onClick={() => SET_IS_CUSTOM_THEME_COLOR(true)}
|
|
145
|
-
>
|
|
146
|
-
<div className={`w-full aspect-[2] rounded-lg outline flex items-center justify-center transition-all relative overflow-hidden ${isCustomThemeColor ? 'outline-(--color-primary) outline-2' : 'outline-[#e4e4e7] dark:outline-[#36363a]'}`}>
|
|
147
|
-
{/* 未选中时显示hover效果 */}
|
|
148
|
-
{
|
|
149
|
-
!isCustomThemeColor && <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
|
150
|
-
<div className="w-full h-full rounded-lg border-2 border-(--color-primary) opacity-0 scale-0 transition-all duration-300 group-hover:opacity-100 group-hover:scale-100" />
|
|
151
|
-
</div>
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
{/* 中心色块 */}
|
|
155
|
-
<SettingCustomColor />
|
|
156
|
-
</div>
|
|
157
|
-
<span className="text-xs text-[#71717a] dark:text-[#a1a1aa]">{item.name}</span>
|
|
158
|
-
</div>
|
|
159
|
-
);
|
|
160
|
-
})}
|
|
161
|
-
</div>
|
|
162
|
-
</div>
|
|
163
|
-
</Drawer>
|
|
164
|
-
);
|
|
165
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* @Author: dushuai
|
|
3
|
-
* @Date: 2026-01-27
|
|
4
|
-
* @LastEditors: dushuai
|
|
5
|
-
* @LastEditTime: 2026-02-01 01:49:11
|
|
6
|
-
* @description: 侧边栏组件
|
|
7
|
-
*/
|
|
8
|
-
import { Layout, Menu, type MenuProps } from 'antd';
|
|
9
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
10
|
-
import { MdOutlineKeyboardDoubleArrowLeft, MdOutlineKeyboardDoubleArrowRight } from 'react-icons/md';
|
|
11
|
-
|
|
12
|
-
import type { MenuItem } from '@/router';
|
|
13
|
-
import { renderIcon } from '@/utils';
|
|
14
|
-
|
|
15
|
-
const { Sider } = Layout;
|
|
16
|
-
|
|
17
|
-
interface SidebarProps {
|
|
18
|
-
collapsed: boolean
|
|
19
|
-
menus: MenuItem[]
|
|
20
|
-
activeKey: string
|
|
21
|
-
openKeys?: string[]
|
|
22
|
-
onMenuClick: (key: string) => void
|
|
23
|
-
onCollapse: (collapsed: boolean) => void
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export default function Sidebar(props: SidebarProps) {
|
|
27
|
-
const { collapsed, menus, activeKey, openKeys: initialOpenKeys = [], onMenuClick, onCollapse } = props;
|
|
28
|
-
|
|
29
|
-
// 使用内部状态管理 openKeys,使其成为非受控变量
|
|
30
|
-
const [openKeys, setOpenKeys] = useState<string[]>(initialOpenKeys);
|
|
31
|
-
|
|
32
|
-
// 当初始 openKeys 变化时(如路由变化),更新内部状态
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
if(initialOpenKeys.length > 0) {
|
|
35
|
-
setOpenKeys(initialOpenKeys);
|
|
36
|
-
}
|
|
37
|
-
}, [initialOpenKeys]);
|
|
38
|
-
|
|
39
|
-
// 转换菜单数据为 antd Menu 格式
|
|
40
|
-
const menuItems: MenuProps['items'] = useMemo(() => {
|
|
41
|
-
const convertMenu = (menuList: MenuItem[]): MenuProps['items'] => {
|
|
42
|
-
return menuList.map(menu => ({
|
|
43
|
-
key: menu.key,
|
|
44
|
-
label: menu.label,
|
|
45
|
-
icon: renderIcon(menu.icon),
|
|
46
|
-
children: menu.children ? convertMenu(menu.children) : undefined
|
|
47
|
-
}));
|
|
48
|
-
};
|
|
49
|
-
return convertMenu(menus);
|
|
50
|
-
}, [menus]);
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<Sider
|
|
54
|
-
trigger={null}
|
|
55
|
-
collapsible
|
|
56
|
-
collapsed={collapsed}
|
|
57
|
-
width={224}
|
|
58
|
-
className="layout-sidebar border-r border-(--border-color) bg-(--global-background-color)! h-full relative"
|
|
59
|
-
theme="light"
|
|
60
|
-
>
|
|
61
|
-
<div className="h-full flex flex-col">
|
|
62
|
-
{/* 菜单区域 */}
|
|
63
|
-
<div className="flex-1 overflow-y-auto overflow-x-hidden py-2 custom-scrollbar">
|
|
64
|
-
<Menu
|
|
65
|
-
mode="inline"
|
|
66
|
-
selectedKeys={[activeKey]}
|
|
67
|
-
openKeys={openKeys}
|
|
68
|
-
onOpenChange={setOpenKeys}
|
|
69
|
-
items={menuItems}
|
|
70
|
-
onClick={({ key }) => onMenuClick(key)}
|
|
71
|
-
className="sidebar-menu border-none"
|
|
72
|
-
inlineIndent={16}
|
|
73
|
-
/>
|
|
74
|
-
</div>
|
|
75
|
-
|
|
76
|
-
{/* 底部折叠按钮 */}
|
|
77
|
-
<div className="flex items-center justify-start pl-4 mb-2">
|
|
78
|
-
<div
|
|
79
|
-
onClick={() => onCollapse(!collapsed)}
|
|
80
|
-
className="text-gray-500 dark:text-[#a1a1a2] text-xl hover:text-[#323639] dark:hover:text-[#f0f0f0] hover:bg-[#dfe3e4] dark:hover:bg-[#333538] transition-all duration-300 w-7 h-7 rounded-sm bg-[#f4f4f5] dark:bg-[#292a2d] cursor-pointer flex items-center justify-center"
|
|
81
|
-
>
|
|
82
|
-
{collapsed ? <MdOutlineKeyboardDoubleArrowRight /> : <MdOutlineKeyboardDoubleArrowLeft />}
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
</div>
|
|
86
|
-
</Sider>
|
|
87
|
-
);
|
|
88
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* @Author: dushuai
|
|
3
|
-
* @Date: 2026-01-27
|
|
4
|
-
* @LastEditors: dushuai
|
|
5
|
-
* @LastEditTime: 2026-02-01 02:00:18
|
|
6
|
-
* @description: 标签页组件
|
|
7
|
-
*/
|
|
8
|
-
import { classnames } from '@spacego/turbo-utils';
|
|
9
|
-
import { Divider } from 'antd';
|
|
10
|
-
import { useEffect, useRef, useState } from 'react';
|
|
11
|
-
import { Fragment } from 'react/jsx-runtime';
|
|
12
|
-
import { MdClear } from 'react-icons/md';
|
|
13
|
-
|
|
14
|
-
import ChromeBgAfter from '@/assets/svg/chrome-bg-after.svg?react';
|
|
15
|
-
import ChromeBgBefore from '@/assets/svg/chrome-bg-before.svg?react';
|
|
16
|
-
import type { TabItem } from '@/store/modules/layout-config.store';
|
|
17
|
-
|
|
18
|
-
interface TabsComponentProps {
|
|
19
|
-
tabs: TabItem[]
|
|
20
|
-
activeKey: string
|
|
21
|
-
onChange: (key: string) => void
|
|
22
|
-
onClose: (key: string) => void
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export default function TabsComponent(props: TabsComponentProps) {
|
|
26
|
-
const { tabs, activeKey, onChange, onClose } = props;
|
|
27
|
-
|
|
28
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
29
|
-
const [hasScroll, setHasScroll] = useState(false);
|
|
30
|
-
|
|
31
|
-
// 检查是否出现滚动条
|
|
32
|
-
const checkScroll = () => {
|
|
33
|
-
const container = containerRef.current;
|
|
34
|
-
if(container) {
|
|
35
|
-
// 如果 scrollWidth 大于 clientWidth,说明有内容溢出,出现了横向滚动条
|
|
36
|
-
const isScroll = container.scrollWidth > container.clientWidth;
|
|
37
|
-
setHasScroll(isScroll);
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
checkScroll();
|
|
43
|
-
window.addEventListener('resize', checkScroll);
|
|
44
|
-
return () => {
|
|
45
|
-
window.removeEventListener('resize', checkScroll);
|
|
46
|
-
};
|
|
47
|
-
}, [tabs]);
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<div
|
|
51
|
-
ref={containerRef}
|
|
52
|
-
className={classnames('layout-tabs bg-(--global-background-color) border-b border-(--border-color) overflow-x-auto overflow-y-hidden relative transition-all duration-300 shrink-0', {
|
|
53
|
-
'h-[38px]': !hasScroll,
|
|
54
|
-
// 出现滚动条时增加高度,留出滚动条的空间,防止挤压内容
|
|
55
|
-
'h-[46px]': hasScroll
|
|
56
|
-
})}
|
|
57
|
-
>
|
|
58
|
-
<div className="flex items-center h-[38px] px-2 pt-1 min-w-fit">
|
|
59
|
-
{
|
|
60
|
-
tabs.map((item, index) => {
|
|
61
|
-
return <Fragment key={`${item.key}-${index}`}>
|
|
62
|
-
<div
|
|
63
|
-
key={`${item.key}-${index}`}
|
|
64
|
-
className={classnames('layout-tabs-item group cursor-pointer px-[16px] h-full flex items-center justify-center rounded-tl-[8px] rounded-tr-[8px] transition-all duration-500 relative hover:bg-[#f4f4f5] dark:hover:bg-[#292a2d] shrink-0', {
|
|
65
|
-
'bg-(--background-primary-light)! text-(--color-primary) dark:text-[#f9f9f9]! is-active': activeKey === item.key
|
|
66
|
-
})}
|
|
67
|
-
onClick={() => onChange(item.key)}
|
|
68
|
-
>
|
|
69
|
-
<ChromeBgBefore />
|
|
70
|
-
{item.label}
|
|
71
|
-
<ChromeBgAfter />
|
|
72
|
-
|
|
73
|
-
{
|
|
74
|
-
item.closable !== false && <div
|
|
75
|
-
className="w-4 h-4 flex ml-1 mr-[-10px] items-center justify-center text-[#18181bcc] hover:text-[#18181b] hover:bg-[#f4f4f5] dark:hover:bg-transparent rounded-full transition-all duration-300"
|
|
76
|
-
onClick={e => {
|
|
77
|
-
e.stopPropagation();
|
|
78
|
-
onClose(item.key);
|
|
79
|
-
}}
|
|
80
|
-
>
|
|
81
|
-
<MdClear className="text-xs text-[#333333] dark:text-[#cfd0d0] hover:text-black dark:hover:text-white transition-all" />
|
|
82
|
-
</div>
|
|
83
|
-
}
|
|
84
|
-
</div>
|
|
85
|
-
{
|
|
86
|
-
index !== tabs.length - 1 && <Divider orientation="vertical" className="mx-1! shrink-0" />
|
|
87
|
-
}
|
|
88
|
-
</Fragment>;
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
</div>
|
|
92
|
-
</div>
|
|
93
|
-
);
|
|
94
|
-
}
|