Skip to content

Commit 08e4fc2

Browse files
committed
pref: Optimize Tabs
1 parent 6c82b91 commit 08e4fc2

4 files changed

Lines changed: 101 additions & 45 deletions

File tree

chat2db-client/src/components/Tabs/index.less

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
.tabsNav {
2626
display: flex;
2727
overflow-x: scroll;
28+
position: relative;
2829
height: 32px;
2930
flex-shrink: 0;
3031
background-color: var(--color-bg-base);
@@ -66,6 +67,9 @@
6667
cursor: pointer;
6768
user-select: none;
6869
border-right: 1px solid var(--color-border);
70+
&:last-child {
71+
border-right: 0;
72+
}
6973
.textBox {
7074
flex: 1;
7175
display: flex;
@@ -111,6 +115,29 @@
111115

112116
.rightBox {
113117
flex: 1;
118+
position: sticky;
119+
right: 0;
120+
top: 0;
121+
bottom: 0;
122+
background-color: var(--color-bg-base);
123+
border-left: 1px solid var(--color-border);
124+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
125+
display: flex;
126+
127+
128+
}
129+
130+
.moreTabs{
131+
width: 30px;
132+
height: 100%;
133+
display: flex;
134+
justify-content: center;
135+
align-items: center;
136+
cursor: pointer;
137+
&:hover {
138+
color: var(--color-primary);
139+
}
140+
114141
}
115142

116143
.addIcon {
@@ -120,6 +147,8 @@
120147
justify-content: center;
121148
align-items: center;
122149
cursor: pointer;
150+
border-left: 1px solid var(--color-border);
151+
123152
&:hover {
124153
color: var(--color-primary);
125154
}

chat2db-client/src/components/Tabs/index.tsx

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import React, { memo, useEffect, useState, useRef } from 'react';
22
import classnames from 'classnames';
33
import Iconfont from '@/components/Iconfont';
4-
import styles from './index.less';
5-
import { Popover, Dropdown } from 'antd';
4+
import { Popover, Dropdown, MenuProps } from 'antd';
65
import i18n from '@/i18n';
6+
import { isValid } from '@/utils/check';
7+
import _ from 'lodash';
8+
import styles from './index.less';
79

810
export interface ITabItem {
911
prefixIcon?: string | React.ReactNode;
@@ -55,28 +57,28 @@ export default memo<IProps>((props) => {
5557
const tabsNavRef = useRef<HTMLDivElement>(null);
5658

5759
useEffect(() => {
58-
if (activeKey !== null && activeKey !== undefined) {
59-
setInternalActiveTab(activeKey);
60+
if (isValid(activeKey)) {
61+
setInternalActiveTab(activeKey!);
6062
}
6163
}, [activeKey]);
6264

6365
useEffect(() => {
6466
setInternalTabs(items || []);
65-
if (items?.length && (internalActiveTab === undefined || internalActiveTab === null)) {
67+
if (items?.length && !isValid(internalActiveTab)) {
6668
setInternalActiveTab(items[0]?.key);
6769
}
6870
}, [items]);
6971

7072
useEffect(() => {
71-
const fn = (e) => {
73+
const fn = _.debounce((e) => {
7274
if (e.deltaY) {
7375
e.preventDefault();
7476
// 鼠标滚轮事件,让tab可以横向滚动
7577
if (tabsNavRef.current) {
7678
tabsNavRef.current.scrollLeft -= e.deltaY;
7779
}
7880
}
79-
};
81+
}, 30);
8082
tabsNavRef.current?.addEventListener('wheel', fn);
8183
return () => {
8284
tabsNavRef.current?.removeEventListener('wheel', fn);
@@ -88,14 +90,16 @@ export default memo<IProps>((props) => {
8890
if (tabListBoxRef.current) {
8991
const activeTab = tabListBoxRef.current.querySelector(`.${styles.activeTab}`);
9092
if (activeTab) {
91-
activeTab.scrollIntoView({ block: 'nearest' });
93+
setTimeout(() => {
94+
activeTab.scrollIntoView({ behavior: 'smooth', inline: 'start' });
95+
}, 50);
9296
}
9397
}
9498

9599
onChange?.(internalActiveTab);
96100
}, [internalActiveTab]);
97101

98-
function deleteTab(data: ITabItem) {
102+
const deleteTab = (data: ITabItem) => {
99103
const newInternalTabs = internalTabs?.filter((t) => t.key !== data.key);
100104
let activeKeyTemp = internalActiveTab;
101105
// 删掉的是当前激活的tab,那么就切换到前一个,如果前一个没有就切换到后一个
@@ -110,7 +114,7 @@ export default memo<IProps>((props) => {
110114
changeTab(activeKeyTemp);
111115
setInternalTabs(newInternalTabs);
112116
onEdit?.('remove', [data], newInternalTabs);
113-
}
117+
};
114118

115119
const deleteOtherTab = (data: ITabItem) => {
116120
const newInternalTabs = internalTabs?.filter((t) => t.key === data.key);
@@ -127,21 +131,21 @@ export default memo<IProps>((props) => {
127131
onEdit?.('remove', [...internalTabs]);
128132
};
129133

130-
function changeTab(key: string | number | null) {
134+
const changeTab = (key: string | number | null) => {
131135
setInternalActiveTab(key);
132-
}
136+
};
133137

134-
function handleAdd() {
138+
const handleAdd = () => {
135139
onEdit?.('add');
136-
}
140+
};
137141

138-
function onDoubleClick(t: ITabItem) {
142+
const onDoubleClick = (t: ITabItem) => {
139143
if (t.editableName) {
140144
setEditingTab(t.key);
141145
}
142-
}
146+
};
143147

144-
function renderTabItem(t: ITabItem, index: number) {
148+
const renderTabItem = (t: ITabItem, index: number) => {
145149
function inputOnChange(value: string) {
146150
internalTabs[index].label = value;
147151
setInternalTabs([...internalTabs]);
@@ -170,7 +174,7 @@ export default memo<IProps>((props) => {
170174
deleteTab(t);
171175
},
172176
},
173-
{
177+
{
174178
label: i18n('common.button.closeOthers'),
175179
key: 'closeOther',
176180
onClick: () => {
@@ -227,7 +231,18 @@ export default memo<IProps>((props) => {
227231
</Popover>
228232
</Dropdown>
229233
);
230-
}
234+
};
235+
236+
const moreTabsMenu: MenuProps['items'] = (internalTabs || []).map((t) => {
237+
return {
238+
label: t.label,
239+
key: t.key,
240+
// itemIcon: t.prefixIcon,
241+
// onClick: () => {
242+
// changeTab(t.key);
243+
// },
244+
};
245+
});
231246

232247
return (
233248
<div className={classnames(styles.tabBox, className)}>
@@ -242,6 +257,21 @@ export default memo<IProps>((props) => {
242257
)}
243258
{!hideAdd && (
244259
<div className={styles.rightBox}>
260+
<div className={styles.moreTabs}>
261+
<Dropdown
262+
menu={{
263+
items: moreTabsMenu,
264+
selectable: true,
265+
selectedKeys: [`${activeKey}`],
266+
onClick: (v) => changeTab(Number(v.key)),
267+
}}
268+
// trigger={['click']}
269+
>
270+
<a onClick={(e) => e.preventDefault()}>
271+
<Iconfont code="&#xe601;" />
272+
</a>
273+
</Dropdown>
274+
</div>
245275
<div className={styles.addIcon} onClick={handleAdd}>
246276
<Iconfont code="&#xe631;" />
247277
</div>

chat2db-client/src/typings/common.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { ConsoleOpenedStatus, ConsoleStatus, DatabaseTypeCode, WorkspaceTabType } from '@/constants';
22

3+
export type NonNullable<T> = T extends null | undefined ? never : T;
4+
35
export interface IPageResponse<T> {
46
data: T[];
57
pageNo: number;

chat2db-client/src/utils/check.ts

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,27 @@
1-
const toString = Object.prototype.toString
1+
// type NonNullable<T> = T extends null | undefined ? never : T;
22

3+
const toString = Object.prototype.toString;
34

45
const isType =
5-
<T>(type: string | string[]) =>
6-
(obj: unknown): obj is T =>
7-
getType(obj) === `[object ${type}]`
8-
export const getType = (obj: any) => toString.call(obj)
6+
<T>(type: string | string[]) =>
7+
(obj: unknown): obj is T =>
8+
getType(obj) === `[object ${type}]`;
9+
export const getType = (obj: any) => toString.call(obj);
910
export const isArr = Array.isArray;
1011
export const isValid = (val: any) => val !== null && val !== undefined;
11-
export const isFn = (val: any): val is Function => typeof val === 'function'
12-
export const isPlainObj = isType<object>('Object')
13-
export const isStr = isType<string>('String')
14-
export const isBool = isType<boolean>('Boolean')
15-
export const isNum = isType<number>('Number')
16-
export const isMap = (val: any): val is Map<any, any> =>
17-
val && val instanceof Map
18-
export const isSet = (val: any): val is Set<any> => val && val instanceof Set
19-
export const isWeakMap = (val: any): val is WeakMap<any, any> =>
20-
val && val instanceof WeakMap
21-
export const isWeakSet = (val: any): val is WeakSet<any> =>
22-
val && val instanceof WeakSet
23-
export const isNumberLike = (index: any): index is number =>
24-
isNum(index) || /^\d+$/.test(index)
25-
export const isObj = (val: unknown): val is object => typeof val === 'object'
26-
export const isRegExp = isType<RegExp>('RegExp')
27-
export const isReactElement = (obj: any): boolean =>
28-
obj && obj['$$typeof'] && obj['_owner']
12+
export const isFn = (val: any): val is Function => typeof val === 'function';
13+
export const isPlainObj = isType<object>('Object');
14+
export const isStr = isType<string>('String');
15+
export const isBool = isType<boolean>('Boolean');
16+
export const isNum = isType<number>('Number');
17+
export const isMap = (val: any): val is Map<any, any> => val && val instanceof Map;
18+
export const isSet = (val: any): val is Set<any> => val && val instanceof Set;
19+
export const isWeakMap = (val: any): val is WeakMap<any, any> => val && val instanceof WeakMap;
20+
export const isWeakSet = (val: any): val is WeakSet<any> => val && val instanceof WeakSet;
21+
export const isNumberLike = (index: any): index is number => isNum(index) || /^\d+$/.test(index);
22+
export const isObj = (val: unknown): val is object => typeof val === 'object';
23+
export const isRegExp = isType<RegExp>('RegExp');
24+
export const isReactElement = (obj: any): boolean => obj && obj['$$typeof'] && obj['_owner'];
2925
export const isHTMLElement = (target: any): target is EventTarget => {
30-
return Object.prototype.toString.call(target).indexOf('HTML') > -1
31-
}
32-
26+
return Object.prototype.toString.call(target).indexOf('HTML') > -1;
27+
};

0 commit comments

Comments
 (0)