Skip to content

Commit d9a2b98

Browse files
authored
Merge pull request #1 from yecongling/dev
feat(组件优化): 更新依赖版本,添加KeepAlive组件,优化Header和Menu组件,增强用户体验
2 parents 6ba859f + ef89b20 commit d9a2b98

17 files changed

Lines changed: 1283 additions & 19 deletions

File tree

bun.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/KeepAlive/index.tsx

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import type React from 'react';
2+
import { useRef, useEffect, useState } from 'react';
3+
import { useTabStore } from '@/stores/tabStore';
4+
5+
interface KeepAliveProps {
6+
children: React.ReactNode;
7+
}
8+
9+
interface CacheItem {
10+
component: React.ReactElement;
11+
scrollTop: number;
12+
scrollLeft: number;
13+
}
14+
15+
const KeepAlive: React.FC<KeepAliveProps> = ({ children }) => {
16+
const { tabs, activeKey } = useTabStore();
17+
const cacheRef = useRef<Map<string, CacheItem>>(new Map());
18+
const [currentComponent, setCurrentComponent] = useState<React.ReactElement | null>(null);
19+
const containerRef = useRef<HTMLDivElement>(null);
20+
21+
// 保存当前页面的滚动位置
22+
const saveScrollPosition = (key: string) => {
23+
if (containerRef.current) {
24+
const scrollTop = containerRef.current.scrollTop;
25+
const scrollLeft = containerRef.current.scrollLeft;
26+
27+
const cached = cacheRef.current.get(key);
28+
if (cached) {
29+
cached.scrollTop = scrollTop;
30+
cached.scrollLeft = scrollLeft;
31+
}
32+
}
33+
};
34+
35+
// 恢复页面的滚动位置
36+
const restoreScrollPosition = (key: string) => {
37+
const cached = cacheRef.current.get(key);
38+
if (cached && containerRef.current) {
39+
// 使用setTimeout确保DOM更新后再设置滚动位置
40+
setTimeout(() => {
41+
if (containerRef.current) {
42+
containerRef.current.scrollTop = cached.scrollTop;
43+
containerRef.current.scrollLeft = cached.scrollLeft;
44+
}
45+
}, 0);
46+
}
47+
};
48+
49+
useEffect(() => {
50+
if (!activeKey) return;
51+
52+
// 保存之前页面的滚动位置
53+
const previousActiveKey = Object.keys(cacheRef.current).find(key => key !== activeKey);
54+
if (previousActiveKey) {
55+
saveScrollPosition(previousActiveKey);
56+
}
57+
58+
// 检查缓存中是否已有当前页面
59+
let cached = cacheRef.current.get(activeKey);
60+
61+
if (!cached) {
62+
// 如果没有缓存,创建新的缓存项
63+
cached = {
64+
component: children as React.ReactElement,
65+
scrollTop: 0,
66+
scrollLeft: 0
67+
};
68+
cacheRef.current.set(activeKey, cached);
69+
}
70+
71+
// 设置当前显示的组件
72+
setCurrentComponent(cached.component);
73+
74+
// 恢复滚动位置
75+
restoreScrollPosition(activeKey);
76+
77+
}, [activeKey, children]);
78+
79+
// 清理不存在的tab对应的缓存
80+
useEffect(() => {
81+
const tabKeys = tabs.map(tab => tab.key);
82+
const cacheKeys = Array.from(cacheRef.current.keys());
83+
84+
cacheKeys.forEach(key => {
85+
if (!tabKeys.includes(key)) {
86+
cacheRef.current.delete(key);
87+
}
88+
});
89+
}, [tabs]);
90+
91+
if (!currentComponent) {
92+
return null;
93+
}
94+
95+
return (
96+
<div
97+
ref={containerRef}
98+
className="keep-alive-container"
99+
style={{
100+
height: '100%',
101+
overflow: 'auto',
102+
display: 'flex',
103+
flexDirection: 'column',
104+
position: 'relative'
105+
}}
106+
>
107+
{currentComponent}
108+
</div>
109+
);
110+
};
111+
112+
export default KeepAlive;

src/components/TabBar/README.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# TabBar 组件
2+
3+
一个支持 keepalive 的标签页组件,基于 Ant Design 的 Tabs 组件实现。
4+
5+
## 功能特性
6+
7+
- ✅ 支持 keepalive 页面缓存
8+
- ✅ 自动根据路由变化添加/激活标签页
9+
- ✅ 右键菜单支持多种操作
10+
- ✅ 标签页固定/取消固定
11+
- ✅ 批量关闭标签页
12+
- ✅ 页面刷新后保持标签页状态
13+
- ✅ 左侧菜单与标签页联动
14+
- ✅ 响应式设计
15+
-**新增:右侧功能区域**
16+
- 下拉菜单(与右键菜单功能一致)
17+
- 全屏切换功能
18+
19+
## 使用方法
20+
21+
### 1. 在 Header 中添加 TabBar
22+
23+
```tsx
24+
import TabBar from '@/components/TabBar';
25+
26+
// 在 Header 组件中添加
27+
<HeaderMenu />
28+
<TabBar />
29+
<Space size="large" className="flex justify-end items-center toolbox">
30+
```
31+
32+
### 2. 在 Content 中添加 KeepAlive
33+
34+
```tsx
35+
import KeepAlive from '@/components/KeepAlive';
36+
37+
// 在 Content 组件中包装 Outlet
38+
<KeepAlive>
39+
<Outlet />
40+
</KeepAlive>
41+
```
42+
43+
## 功能区域
44+
45+
### 标签页区域
46+
- 支持右键菜单操作
47+
- 自动管理标签页的添加/删除
48+
- 当只有一个标签页时不显示关闭按钮
49+
50+
### 右侧功能区域
51+
- **下拉菜单按钮** (⬇️): 点击显示操作菜单
52+
- **全屏按钮** (⛶): 切换页面全屏状态
53+
54+
## 右键菜单功能
55+
56+
每个标签页都支持右键菜单,包含以下操作:
57+
58+
- **关闭**: 关闭当前标签页
59+
- **固定/取消固定**: 固定标签页(不可关闭)
60+
- **最大化**: 最大化标签页(待实现)
61+
- **重新加载**: 重新加载页面内容
62+
- **在新窗口打开**: 在新标签页中打开
63+
- **关闭左侧标签页**: 关闭当前标签页左侧的所有标签页
64+
- **关闭右侧标签页**: 关闭当前标签页右侧的所有标签页
65+
- **关闭其它标签页**: 只保留当前标签页
66+
- **关闭全部标签页**: 关闭所有标签页
67+
68+
## 右侧下拉菜单
69+
70+
右侧下拉菜单提供与右键菜单相同的功能,但操作的是当前激活的标签页:
71+
72+
- 所有右键菜单功能都可以通过右侧下拉菜单访问
73+
- 点击触发,更直观的操作方式
74+
- 适合键盘操作和触摸设备
75+
76+
## 状态管理
77+
78+
TabBar 使用 Zustand 进行状态管理,主要状态包括:
79+
80+
- `tabs`: 打开的标签页列表
81+
- `activeKey`: 当前激活的标签页
82+
- `addTab`: 添加新标签页
83+
- `removeTab`: 移除标签页
84+
- `setActiveKey`: 设置激活的标签页
85+
- `closeOtherTabs`: 关闭其他标签页
86+
- `closeLeftTabs`: 关闭左侧标签页
87+
- `closeRightTabs`: 关闭右侧标签页
88+
- `closeAllTabs`: 关闭所有标签页
89+
- `reloadTab`: 重新加载标签页
90+
- `pinTab`: 固定标签页
91+
- `unpinTab`: 取消固定标签页
92+
93+
## 样式定制
94+
95+
TabBar 组件使用 SCSS 进行样式定制,主要样式类:
96+
97+
- `.tab-bar`: 标签栏容器
98+
- `.tab-bar-content`: 标签栏内容区域(包含标签页和右侧功能)
99+
- `.tab-bar-tabs`: 标签页组件
100+
- `.tab-bar-actions`: 右侧功能区域
101+
- `.tab-action-btn`: 右侧功能按钮
102+
- `.ant-tabs-tab`: 单个标签页
103+
- `.ant-tabs-tab-active`: 激活状态的标签页
104+
- `.ant-tabs-tab-remove`: 关闭按钮
105+
106+
## 注意事项
107+
108+
1. 标签页会根据路由自动添加,无需手动管理
109+
2. 当只有一个标签页时,关闭按钮不会显示
110+
3. 关闭当前激活的标签页时,会自动激活前一个标签页
111+
4. 页面刷新后,标签页状态会从 localStorage 中恢复
112+
5. 标签页内容由 KeepAlive 组件缓存,支持页面状态保持
113+
6. 右侧下拉菜单操作的是当前激活的标签页
114+
7. 全屏功能使用浏览器原生 API
115+
116+
## 国际化支持
117+
118+
TabBar 组件支持中英文国际化,相关文案在以下文件中配置:
119+
120+
- `src/locales/zh-CN/common.ts`
121+
- `src/locales/en-US/common.ts`
122+
123+
## 依赖
124+
125+
- React 18+
126+
- Ant Design 5+
127+
- React Router 6+
128+
- Zustand
129+
- SCSS

0 commit comments

Comments
 (0)