@hsu-react/ui 2.0.2 → 2.2.0
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/es/layout/Header/index.d.ts +8 -0
- package/es/layout/Header/index.js +4 -2
- package/es/layout/NavTabBar/_hooks/useKeepTabs.d.ts +18 -0
- package/es/layout/NavTabBar/_hooks/useKeepTabs.js +45 -0
- package/es/layout/NavTabBar/index.d.ts +21 -0
- package/es/layout/NavTabBar/index.js +4 -1
- package/es/layout/index.d.ts +7 -0
- package/es/layout/index.js +6 -0
- package/lib/layout/Header/index.d.ts +8 -0
- package/lib/layout/Header/index.js +3 -2
- package/lib/layout/NavTabBar/_hooks/useKeepTabs.d.ts +18 -0
- package/lib/layout/NavTabBar/_hooks/useKeepTabs.js +48 -0
- package/lib/layout/NavTabBar/index.d.ts +21 -0
- package/lib/layout/NavTabBar/index.js +4 -1
- package/lib/layout/index.d.ts +7 -0
- package/lib/layout/index.js +13 -0
- package/package.json +1 -1
- package/src/layout/Header/index.md +5 -0
- package/src/layout/Header/index.tsx +22 -11
- package/src/layout/Menu/index.md +30 -0
- package/src/layout/NavTabBar/_hooks/useKeepTabs.ts +46 -0
- package/src/layout/NavTabBar/index.md +26 -0
- package/src/layout/NavTabBar/index.tsx +22 -0
- package/src/layout/index.ts +7 -0
|
@@ -34,6 +34,14 @@ export interface HeaderProps {
|
|
|
34
34
|
title?: React.ReactNode;
|
|
35
35
|
/** 侧栏收起时用的短标题;不传则回落到 title */
|
|
36
36
|
smallTitle?: React.ReactNode;
|
|
37
|
+
/**
|
|
38
|
+
* 是否显示语言切换。
|
|
39
|
+
*
|
|
40
|
+
* 只有中文一种文案的项目把它关掉——留着会给用户一个切了之后只有 antd 内建文案变化、
|
|
41
|
+
* 业务文案纹丝不动的开关,比没有更糟。关掉后 `Layout.I18n` 仍可照常用(日期、分页
|
|
42
|
+
* 这些 antd 内建文案还是要它)。
|
|
43
|
+
*/
|
|
44
|
+
showLocale?: boolean;
|
|
37
45
|
}
|
|
38
46
|
declare const Header: React.FC<HeaderProps>;
|
|
39
47
|
export default Header;
|
|
@@ -40,7 +40,9 @@ var Header = observer(function (props) {
|
|
|
40
40
|
menu = props.menu,
|
|
41
41
|
user = props.user,
|
|
42
42
|
title = props.title,
|
|
43
|
-
smallTitle = props.smallTitle
|
|
43
|
+
smallTitle = props.smallTitle,
|
|
44
|
+
_props$showLocale = props.showLocale,
|
|
45
|
+
showLocale = _props$showLocale === void 0 ? true : _props$showLocale;
|
|
44
46
|
var layout = ThemeStore.layout,
|
|
45
47
|
headerTheme = ThemeStore.headerTheme,
|
|
46
48
|
appearance = ThemeStore.appearance,
|
|
@@ -118,7 +120,7 @@ var Header = observer(function (props) {
|
|
|
118
120
|
return setAppearance(v);
|
|
119
121
|
}
|
|
120
122
|
})]
|
|
121
|
-
}), /*#__PURE__*/_jsxs("div", {
|
|
123
|
+
}), showLocale && /*#__PURE__*/_jsxs("div", {
|
|
122
124
|
className: styles.settingRow,
|
|
123
125
|
children: [/*#__PURE__*/_jsx("span", {
|
|
124
126
|
className: styles.settingLabel,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { TabType } from "..";
|
|
2
|
+
/**
|
|
3
|
+
* 按业务规则在地址变化时收掉一批页签。
|
|
4
|
+
*
|
|
5
|
+
* 起因是消费方(writer-assistant)的一条规则:进出某部作品时,把上一部作品的所有页签
|
|
6
|
+
* 一起清掉——「一次只专注一部书」。这种规则依赖业务对 URL 的解读(哪一段是作品 id),
|
|
7
|
+
* 组件库不可能内建,但**收页签这件事本身**必须留在库里做:光从 openKeys 里删掉不够,
|
|
8
|
+
* 还要 `drop` 掉 react-activation 的缓存,否则组件树留在内存里,下次同名路由回来还会
|
|
9
|
+
* 复用旧实例。
|
|
10
|
+
*
|
|
11
|
+
* 所以对外只暴露一个判定函数,删除与丢缓存由这里统一执行。
|
|
12
|
+
*
|
|
13
|
+
* 判定按**当前地址**做,不需要消费方自己记「上一个是什么」:每次地址变化重新评估每个
|
|
14
|
+
* 页签「在现在这个位置还该不该留着」,语义更直白,也不会因为漏更新 ref 而出错。
|
|
15
|
+
*/
|
|
16
|
+
export declare const useKeepTabs: (shouldKeepTab: ((tab: TabType, info: {
|
|
17
|
+
pathname: string;
|
|
18
|
+
}) => boolean) | undefined, setOpenkeys: React.Dispatch<React.SetStateAction<TabType[]>>) => void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useLocation } from "react-router";
|
|
3
|
+
import { useAliveController } from "react-activation";
|
|
4
|
+
/**
|
|
5
|
+
* 按业务规则在地址变化时收掉一批页签。
|
|
6
|
+
*
|
|
7
|
+
* 起因是消费方(writer-assistant)的一条规则:进出某部作品时,把上一部作品的所有页签
|
|
8
|
+
* 一起清掉——「一次只专注一部书」。这种规则依赖业务对 URL 的解读(哪一段是作品 id),
|
|
9
|
+
* 组件库不可能内建,但**收页签这件事本身**必须留在库里做:光从 openKeys 里删掉不够,
|
|
10
|
+
* 还要 `drop` 掉 react-activation 的缓存,否则组件树留在内存里,下次同名路由回来还会
|
|
11
|
+
* 复用旧实例。
|
|
12
|
+
*
|
|
13
|
+
* 所以对外只暴露一个判定函数,删除与丢缓存由这里统一执行。
|
|
14
|
+
*
|
|
15
|
+
* 判定按**当前地址**做,不需要消费方自己记「上一个是什么」:每次地址变化重新评估每个
|
|
16
|
+
* 页签「在现在这个位置还该不该留着」,语义更直白,也不会因为漏更新 ref 而出错。
|
|
17
|
+
*/
|
|
18
|
+
export var useKeepTabs = function useKeepTabs(shouldKeepTab, setOpenkeys) {
|
|
19
|
+
var _useLocation = useLocation(),
|
|
20
|
+
pathname = _useLocation.pathname;
|
|
21
|
+
var _useAliveController = useAliveController(),
|
|
22
|
+
drop = _useAliveController.drop;
|
|
23
|
+
useEffect(function () {
|
|
24
|
+
if (!shouldKeepTab) return;
|
|
25
|
+
setOpenkeys(function (prev) {
|
|
26
|
+
var kept = prev.filter(function (tab) {
|
|
27
|
+
return shouldKeepTab(tab, {
|
|
28
|
+
pathname: pathname
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
if (kept.length === prev.length) return prev;
|
|
32
|
+
prev.filter(function (tab) {
|
|
33
|
+
return !kept.includes(tab);
|
|
34
|
+
})
|
|
35
|
+
// 缓存是按不带 query 的路径存的
|
|
36
|
+
.forEach(function (tab) {
|
|
37
|
+
return drop(tab.key.split("?")[0] || "");
|
|
38
|
+
});
|
|
39
|
+
return kept;
|
|
40
|
+
});
|
|
41
|
+
// shouldKeepTab 多半是行内箭头函数,放进依赖会每次渲染都重跑。
|
|
42
|
+
// 这个 effect 的触发时机就是「地址变了」,按 pathname 走即可。
|
|
43
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
44
|
+
}, [pathname]);
|
|
45
|
+
};
|
|
@@ -16,6 +16,27 @@ export interface NavTabBarProps {
|
|
|
16
16
|
basePath?: string;
|
|
17
17
|
/** 页签栏最前面的「刷新当前页」按钮 */
|
|
18
18
|
showReload?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* 地址变化时决定每个页签还留不留。返回 false 的会被关掉,并连同 react-activation 的
|
|
21
|
+
* 缓存一起丢弃。
|
|
22
|
+
*
|
|
23
|
+
* 给的是「在当前地址下这个页签还该不该在」这种判定,不需要自己记上一个位置是什么。
|
|
24
|
+
*
|
|
25
|
+
* @example 一次只专注一部作品:进入另一部书时,把上一部的页签全收掉
|
|
26
|
+
* ```ts
|
|
27
|
+
* const workId = (p: string) => p.match(/\/work\/([0-9a-f-]{36})/)?.[1] ?? null;
|
|
28
|
+
*
|
|
29
|
+
* <Layout.NavTabBar
|
|
30
|
+
* shouldKeepTab={(tab, { pathname }) => {
|
|
31
|
+
* const tabWork = workId(tab.key);
|
|
32
|
+
* return !tabWork || tabWork === workId(pathname);
|
|
33
|
+
* }}
|
|
34
|
+
* />
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
shouldKeepTab?: (tab: TabType, info: {
|
|
38
|
+
pathname: string;
|
|
39
|
+
}) => boolean;
|
|
19
40
|
}
|
|
20
41
|
declare const NavTabBar: React.FC<NavTabBarProps>;
|
|
21
42
|
export default NavTabBar;
|
|
@@ -24,6 +24,7 @@ import { useTabPath } from "./_hooks/useTabPath";
|
|
|
24
24
|
import { useTabContextMenu } from "./_hooks/useTabContextMenu";
|
|
25
25
|
import { useDropTabKey } from "./_hooks/useDropTabKey";
|
|
26
26
|
import { useTabTitle } from "./_hooks/useTabTitle";
|
|
27
|
+
import { useKeepTabs } from "./_hooks/useKeepTabs";
|
|
27
28
|
import TabLabel from "./_components/TabLabel";
|
|
28
29
|
import DraggableTabNode from "./_components/SortableTab";
|
|
29
30
|
import TabDragOverlay from "./_components/TabDragOverlay";
|
|
@@ -40,7 +41,8 @@ var NavTabBar = function NavTabBar(props) {
|
|
|
40
41
|
_props$basePath = props.basePath,
|
|
41
42
|
basePath = _props$basePath === void 0 ? "/" : _props$basePath,
|
|
42
43
|
_props$showReload = props.showReload,
|
|
43
|
-
showReload = _props$showReload === void 0 ? true : _props$showReload
|
|
44
|
+
showReload = _props$showReload === void 0 ? true : _props$showReload,
|
|
45
|
+
shouldKeepTab = props.shouldKeepTab;
|
|
44
46
|
var navigate = useNavigate();
|
|
45
47
|
var _useAliveController = useAliveController(),
|
|
46
48
|
drop = _useAliveController.drop,
|
|
@@ -62,6 +64,7 @@ var NavTabBar = function NavTabBar(props) {
|
|
|
62
64
|
setOpen = _useTabContextMenu.setOpen;
|
|
63
65
|
useDropTabKey(setOpenkeys);
|
|
64
66
|
useTabTitle(setOpenkeys);
|
|
67
|
+
useKeepTabs(shouldKeepTab, setOpenkeys);
|
|
65
68
|
|
|
66
69
|
// ID of the tab currently being dragged
|
|
67
70
|
var _useState = useState(null),
|
package/es/layout/index.d.ts
CHANGED
|
@@ -26,6 +26,13 @@ export type { RouteType, MetaType } from "./types";
|
|
|
26
26
|
*/
|
|
27
27
|
export type { AccountAction, HeaderProps } from "./Header";
|
|
28
28
|
export type { MenuType, MenuProps } from "./Menu";
|
|
29
|
+
/**
|
|
30
|
+
* 次级菜单的默认头部。`Menu` 在切到次级菜单时会自己渲染它,一般不用管;
|
|
31
|
+
* 需要在返回入口下面塞点东西(详情内检索框之类)时,把它的 `extra` 用起来、
|
|
32
|
+
* 整块作为 `secondaryHeader` 传给 `Menu`。
|
|
33
|
+
*/
|
|
34
|
+
export { default as SecondaryHeader } from "./Menu/_components/SecondaryHeader";
|
|
35
|
+
export type { SecondaryHeaderProps } from "./Menu/_components/SecondaryHeader";
|
|
29
36
|
export type { TabType, NavTabBarProps } from "./NavTabBar";
|
|
30
37
|
export type { BreadcrumbType, BreadcrumbProps } from "./Breadcrumb";
|
|
31
38
|
export type { InternationalizationProps } from "./I18n";
|
package/es/layout/index.js
CHANGED
|
@@ -25,6 +25,12 @@ import ThemeStore from "./Theme/ThemeStore";
|
|
|
25
25
|
* 目录结构变成公开契约。统一从这里出。
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* 次级菜单的默认头部。`Menu` 在切到次级菜单时会自己渲染它,一般不用管;
|
|
30
|
+
* 需要在返回入口下面塞点东西(详情内检索框之类)时,把它的 `extra` 用起来、
|
|
31
|
+
* 整块作为 `secondaryHeader` 传给 `Menu`。
|
|
32
|
+
*/
|
|
33
|
+
export { default as SecondaryHeader } from "./Menu/_components/SecondaryHeader";
|
|
28
34
|
export { default as useSetTabTitle, NavTabBarTitleContent } from "./_hooks/useSetTabTitle";
|
|
29
35
|
export { default as useReload, ReloadContent } from "./_hooks/useReload";
|
|
30
36
|
export { default as useDropTab, NavTabBarContent } from "./_hooks/useDropTab";
|
|
@@ -34,6 +34,14 @@ export interface HeaderProps {
|
|
|
34
34
|
title?: React.ReactNode;
|
|
35
35
|
/** 侧栏收起时用的短标题;不传则回落到 title */
|
|
36
36
|
smallTitle?: React.ReactNode;
|
|
37
|
+
/**
|
|
38
|
+
* 是否显示语言切换。
|
|
39
|
+
*
|
|
40
|
+
* 只有中文一种文案的项目把它关掉——留着会给用户一个切了之后只有 antd 内建文案变化、
|
|
41
|
+
* 业务文案纹丝不动的开关,比没有更糟。关掉后 `Layout.I18n` 仍可照常用(日期、分页
|
|
42
|
+
* 这些 antd 内建文案还是要它)。
|
|
43
|
+
*/
|
|
44
|
+
showLocale?: boolean;
|
|
37
45
|
}
|
|
38
46
|
declare const Header: React.FC<HeaderProps>;
|
|
39
47
|
export default Header;
|
|
@@ -43,7 +43,8 @@ const Header = (0, _mobxReactLite.observer)(props => {
|
|
|
43
43
|
menu,
|
|
44
44
|
user,
|
|
45
45
|
title,
|
|
46
|
-
smallTitle
|
|
46
|
+
smallTitle,
|
|
47
|
+
showLocale = true
|
|
47
48
|
} = props;
|
|
48
49
|
const {
|
|
49
50
|
layout,
|
|
@@ -126,7 +127,7 @@ const Header = (0, _mobxReactLite.observer)(props => {
|
|
|
126
127
|
options: appearanceOptions,
|
|
127
128
|
onChange: v => setAppearance(v)
|
|
128
129
|
})]
|
|
129
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
|
|
130
|
+
}), showLocale && /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
|
|
130
131
|
className: _indexModule.default.settingRow,
|
|
131
132
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
|
|
132
133
|
className: _indexModule.default.settingLabel,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { TabType } from "..";
|
|
2
|
+
/**
|
|
3
|
+
* 按业务规则在地址变化时收掉一批页签。
|
|
4
|
+
*
|
|
5
|
+
* 起因是消费方(writer-assistant)的一条规则:进出某部作品时,把上一部作品的所有页签
|
|
6
|
+
* 一起清掉——「一次只专注一部书」。这种规则依赖业务对 URL 的解读(哪一段是作品 id),
|
|
7
|
+
* 组件库不可能内建,但**收页签这件事本身**必须留在库里做:光从 openKeys 里删掉不够,
|
|
8
|
+
* 还要 `drop` 掉 react-activation 的缓存,否则组件树留在内存里,下次同名路由回来还会
|
|
9
|
+
* 复用旧实例。
|
|
10
|
+
*
|
|
11
|
+
* 所以对外只暴露一个判定函数,删除与丢缓存由这里统一执行。
|
|
12
|
+
*
|
|
13
|
+
* 判定按**当前地址**做,不需要消费方自己记「上一个是什么」:每次地址变化重新评估每个
|
|
14
|
+
* 页签「在现在这个位置还该不该留着」,语义更直白,也不会因为漏更新 ref 而出错。
|
|
15
|
+
*/
|
|
16
|
+
export declare const useKeepTabs: (shouldKeepTab: ((tab: TabType, info: {
|
|
17
|
+
pathname: string;
|
|
18
|
+
}) => boolean) | undefined, setOpenkeys: React.Dispatch<React.SetStateAction<TabType[]>>) => void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.useKeepTabs = void 0;
|
|
7
|
+
var _react = require("react");
|
|
8
|
+
var _reactRouter = require("react-router");
|
|
9
|
+
var _reactActivation = require("react-activation");
|
|
10
|
+
/**
|
|
11
|
+
* 按业务规则在地址变化时收掉一批页签。
|
|
12
|
+
*
|
|
13
|
+
* 起因是消费方(writer-assistant)的一条规则:进出某部作品时,把上一部作品的所有页签
|
|
14
|
+
* 一起清掉——「一次只专注一部书」。这种规则依赖业务对 URL 的解读(哪一段是作品 id),
|
|
15
|
+
* 组件库不可能内建,但**收页签这件事本身**必须留在库里做:光从 openKeys 里删掉不够,
|
|
16
|
+
* 还要 `drop` 掉 react-activation 的缓存,否则组件树留在内存里,下次同名路由回来还会
|
|
17
|
+
* 复用旧实例。
|
|
18
|
+
*
|
|
19
|
+
* 所以对外只暴露一个判定函数,删除与丢缓存由这里统一执行。
|
|
20
|
+
*
|
|
21
|
+
* 判定按**当前地址**做,不需要消费方自己记「上一个是什么」:每次地址变化重新评估每个
|
|
22
|
+
* 页签「在现在这个位置还该不该留着」,语义更直白,也不会因为漏更新 ref 而出错。
|
|
23
|
+
*/
|
|
24
|
+
const useKeepTabs = (shouldKeepTab, setOpenkeys) => {
|
|
25
|
+
const {
|
|
26
|
+
pathname
|
|
27
|
+
} = (0, _reactRouter.useLocation)();
|
|
28
|
+
const {
|
|
29
|
+
drop
|
|
30
|
+
} = (0, _reactActivation.useAliveController)();
|
|
31
|
+
(0, _react.useEffect)(() => {
|
|
32
|
+
if (!shouldKeepTab) return;
|
|
33
|
+
setOpenkeys(prev => {
|
|
34
|
+
const kept = prev.filter(tab => shouldKeepTab(tab, {
|
|
35
|
+
pathname
|
|
36
|
+
}));
|
|
37
|
+
if (kept.length === prev.length) return prev;
|
|
38
|
+
prev.filter(tab => !kept.includes(tab))
|
|
39
|
+
// 缓存是按不带 query 的路径存的
|
|
40
|
+
.forEach(tab => drop(tab.key.split("?")[0] || ""));
|
|
41
|
+
return kept;
|
|
42
|
+
});
|
|
43
|
+
// shouldKeepTab 多半是行内箭头函数,放进依赖会每次渲染都重跑。
|
|
44
|
+
// 这个 effect 的触发时机就是「地址变了」,按 pathname 走即可。
|
|
45
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
46
|
+
}, [pathname]);
|
|
47
|
+
};
|
|
48
|
+
exports.useKeepTabs = useKeepTabs;
|
|
@@ -16,6 +16,27 @@ export interface NavTabBarProps {
|
|
|
16
16
|
basePath?: string;
|
|
17
17
|
/** 页签栏最前面的「刷新当前页」按钮 */
|
|
18
18
|
showReload?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* 地址变化时决定每个页签还留不留。返回 false 的会被关掉,并连同 react-activation 的
|
|
21
|
+
* 缓存一起丢弃。
|
|
22
|
+
*
|
|
23
|
+
* 给的是「在当前地址下这个页签还该不该在」这种判定,不需要自己记上一个位置是什么。
|
|
24
|
+
*
|
|
25
|
+
* @example 一次只专注一部作品:进入另一部书时,把上一部的页签全收掉
|
|
26
|
+
* ```ts
|
|
27
|
+
* const workId = (p: string) => p.match(/\/work\/([0-9a-f-]{36})/)?.[1] ?? null;
|
|
28
|
+
*
|
|
29
|
+
* <Layout.NavTabBar
|
|
30
|
+
* shouldKeepTab={(tab, { pathname }) => {
|
|
31
|
+
* const tabWork = workId(tab.key);
|
|
32
|
+
* return !tabWork || tabWork === workId(pathname);
|
|
33
|
+
* }}
|
|
34
|
+
* />
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
shouldKeepTab?: (tab: TabType, info: {
|
|
38
|
+
pathname: string;
|
|
39
|
+
}) => boolean;
|
|
19
40
|
}
|
|
20
41
|
declare const NavTabBar: React.FC<NavTabBarProps>;
|
|
21
42
|
export default NavTabBar;
|
|
@@ -18,6 +18,7 @@ var _useTabPath = require("./_hooks/useTabPath");
|
|
|
18
18
|
var _useTabContextMenu = require("./_hooks/useTabContextMenu");
|
|
19
19
|
var _useDropTabKey = require("./_hooks/useDropTabKey");
|
|
20
20
|
var _useTabTitle = require("./_hooks/useTabTitle");
|
|
21
|
+
var _useKeepTabs = require("./_hooks/useKeepTabs");
|
|
21
22
|
var _TabLabel = _interopRequireDefault(require("./_components/TabLabel"));
|
|
22
23
|
var _SortableTab = _interopRequireDefault(require("./_components/SortableTab"));
|
|
23
24
|
var _TabDragOverlay = _interopRequireDefault(require("./_components/TabDragOverlay"));
|
|
@@ -33,7 +34,8 @@ const NavTabBar = props => {
|
|
|
33
34
|
router,
|
|
34
35
|
affixRouter = [],
|
|
35
36
|
basePath = "/",
|
|
36
|
-
showReload = true
|
|
37
|
+
showReload = true,
|
|
38
|
+
shouldKeepTab
|
|
37
39
|
} = props;
|
|
38
40
|
const navigate = (0, _reactRouter.useNavigate)();
|
|
39
41
|
const {
|
|
@@ -59,6 +61,7 @@ const NavTabBar = props => {
|
|
|
59
61
|
} = (0, _useTabContextMenu.useTabContextMenu)();
|
|
60
62
|
(0, _useDropTabKey.useDropTabKey)(setOpenkeys);
|
|
61
63
|
(0, _useTabTitle.useTabTitle)(setOpenkeys);
|
|
64
|
+
(0, _useKeepTabs.useKeepTabs)(shouldKeepTab, setOpenkeys);
|
|
62
65
|
|
|
63
66
|
// ID of the tab currently being dragged
|
|
64
67
|
const [activeId, setActiveId] = (0, _react.useState)(null);
|
package/lib/layout/index.d.ts
CHANGED
|
@@ -26,6 +26,13 @@ export type { RouteType, MetaType } from "./types";
|
|
|
26
26
|
*/
|
|
27
27
|
export type { AccountAction, HeaderProps } from "./Header";
|
|
28
28
|
export type { MenuType, MenuProps } from "./Menu";
|
|
29
|
+
/**
|
|
30
|
+
* 次级菜单的默认头部。`Menu` 在切到次级菜单时会自己渲染它,一般不用管;
|
|
31
|
+
* 需要在返回入口下面塞点东西(详情内检索框之类)时,把它的 `extra` 用起来、
|
|
32
|
+
* 整块作为 `secondaryHeader` 传给 `Menu`。
|
|
33
|
+
*/
|
|
34
|
+
export { default as SecondaryHeader } from "./Menu/_components/SecondaryHeader";
|
|
35
|
+
export type { SecondaryHeaderProps } from "./Menu/_components/SecondaryHeader";
|
|
29
36
|
export type { TabType, NavTabBarProps } from "./NavTabBar";
|
|
30
37
|
export type { BreadcrumbType, BreadcrumbProps } from "./Breadcrumb";
|
|
31
38
|
export type { InternationalizationProps } from "./I18n";
|
package/lib/layout/index.js
CHANGED
|
@@ -21,6 +21,12 @@ Object.defineProperty(exports, "ReloadContent", {
|
|
|
21
21
|
return _useReload.ReloadContent;
|
|
22
22
|
}
|
|
23
23
|
});
|
|
24
|
+
Object.defineProperty(exports, "SecondaryHeader", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
get: function () {
|
|
27
|
+
return _SecondaryHeader.default;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
24
30
|
exports.default = void 0;
|
|
25
31
|
Object.defineProperty(exports, "useDropTab", {
|
|
26
32
|
enumerable: true,
|
|
@@ -48,6 +54,7 @@ var _NavTabBar = _interopRequireDefault(require("./NavTabBar"));
|
|
|
48
54
|
var _Theme = _interopRequireDefault(require("./Theme"));
|
|
49
55
|
var _I18nStore = _interopRequireDefault(require("./I18n/I18nStore"));
|
|
50
56
|
var _ThemeStore = _interopRequireDefault(require("./Theme/ThemeStore"));
|
|
57
|
+
var _SecondaryHeader = _interopRequireDefault(require("./Menu/_components/SecondaryHeader"));
|
|
51
58
|
var _useSetTabTitle = _interopRequireWildcard(require("./_hooks/useSetTabTitle"));
|
|
52
59
|
var _useReload = _interopRequireWildcard(require("./_hooks/useReload"));
|
|
53
60
|
var _useDropTab = _interopRequireWildcard(require("./_hooks/useDropTab"));
|
|
@@ -73,6 +80,12 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
|
|
|
73
80
|
* 目录结构变成公开契约。统一从这里出。
|
|
74
81
|
*/
|
|
75
82
|
|
|
83
|
+
/**
|
|
84
|
+
* 次级菜单的默认头部。`Menu` 在切到次级菜单时会自己渲染它,一般不用管;
|
|
85
|
+
* 需要在返回入口下面塞点东西(详情内检索框之类)时,把它的 `extra` 用起来、
|
|
86
|
+
* 整块作为 `secondaryHeader` 传给 `Menu`。
|
|
87
|
+
*/
|
|
88
|
+
|
|
76
89
|
/**
|
|
77
90
|
* 显式标注这个命名空间的类型,而不是让 TS 去推断。
|
|
78
91
|
*
|
package/package.json
CHANGED
|
@@ -116,5 +116,10 @@ import type {
|
|
|
116
116
|
| onToggleCollapsed | 点击收起按钮 | `() => void` | - |
|
|
117
117
|
| onChildItems | mixed 布局下把顶部菜单的子项回传给左侧栏 | `(items: MenuType[]) => void` | - |
|
|
118
118
|
| menu | 账号下拉里的动作项(改密、退出等) | `AccountAction[]` | - |
|
|
119
|
+
| showLocale | 是否显示语言切换 | `boolean` | `true` |
|
|
120
|
+
|
|
121
|
+
只有中文一种文案的项目请把 `showLocale` 关掉:留着会给用户一个「切了之后只有 antd 内建
|
|
122
|
+
文案变化、业务文案纹丝不动」的开关,比没有更糟。关掉后 [`Layout.I18n`](/layouts/i18n) 仍
|
|
123
|
+
可照常用 —— 日期选择器、分页这些 antd 内建文案还是要它。
|
|
119
124
|
|
|
120
125
|
> `user` 与 `title` 原本分别读应用的鉴权模块与全局 `Config`,移入库时改成了属性 —— 组件不该认识消费方的这两样东西。
|
|
@@ -53,6 +53,14 @@ export interface HeaderProps {
|
|
|
53
53
|
title?: React.ReactNode;
|
|
54
54
|
/** 侧栏收起时用的短标题;不传则回落到 title */
|
|
55
55
|
smallTitle?: React.ReactNode;
|
|
56
|
+
/**
|
|
57
|
+
* 是否显示语言切换。
|
|
58
|
+
*
|
|
59
|
+
* 只有中文一种文案的项目把它关掉——留着会给用户一个切了之后只有 antd 内建文案变化、
|
|
60
|
+
* 业务文案纹丝不动的开关,比没有更糟。关掉后 `Layout.I18n` 仍可照常用(日期、分页
|
|
61
|
+
* 这些 antd 内建文案还是要它)。
|
|
62
|
+
*/
|
|
63
|
+
showLocale?: boolean;
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
const Header: React.FC<HeaderProps> = observer((props) => {
|
|
@@ -65,6 +73,7 @@ const Header: React.FC<HeaderProps> = observer((props) => {
|
|
|
65
73
|
user,
|
|
66
74
|
title,
|
|
67
75
|
smallTitle,
|
|
76
|
+
showLocale = true,
|
|
68
77
|
} = props;
|
|
69
78
|
const { layout, headerTheme, appearance, setAppearance } = ThemeStore;
|
|
70
79
|
const { locale, setLocale } = I18nStore;
|
|
@@ -148,17 +157,19 @@ const Header: React.FC<HeaderProps> = observer((props) => {
|
|
|
148
157
|
onChange={(v) => setAppearance(v as typeof appearance)}
|
|
149
158
|
/>
|
|
150
159
|
</div>
|
|
151
|
-
|
|
152
|
-
<
|
|
153
|
-
{
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
{showLocale && (
|
|
161
|
+
<div className={styles.settingRow}>
|
|
162
|
+
<span className={styles.settingLabel}>
|
|
163
|
+
{isEn ? "Language" : "语言"}
|
|
164
|
+
</span>
|
|
165
|
+
<Segmented
|
|
166
|
+
size="small"
|
|
167
|
+
value={locale}
|
|
168
|
+
options={languageOptions}
|
|
169
|
+
onChange={(v) => setLocale(v as string)}
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
162
173
|
|
|
163
174
|
<div className={styles.settingDivider} />
|
|
164
175
|
|
package/src/layout/Menu/index.md
CHANGED
|
@@ -41,3 +41,33 @@ import Layout from "@hsu-react/ui/es/layout";
|
|
|
41
41
|
| secondaryItemFilter | 次级菜单项的额外过滤,按 key(绝对路径)判断 | `(key: string) => boolean` | - |
|
|
42
42
|
|
|
43
43
|
> 其余属性透传给 antd 的 [Menu](https://ant.design/components/menu-cn)。
|
|
44
|
+
|
|
45
|
+
### 自定义次级菜单头部
|
|
46
|
+
|
|
47
|
+
不传 `secondaryHeader` 时组件会渲染一个默认头部(返回入口 + 当前实体名)。要在它上面加东西
|
|
48
|
+
——比如详情内检索框——不用整块重写,把 `SecondaryHeader` 拿出来用它的 `extra` 插槽:
|
|
49
|
+
|
|
50
|
+
```tsx | pure
|
|
51
|
+
import Layout, { SecondaryHeader } from "@hsu-react/ui/es/layout";
|
|
52
|
+
|
|
53
|
+
<Layout.Menu
|
|
54
|
+
router={router}
|
|
55
|
+
secondaryHeader={
|
|
56
|
+
<SecondaryHeader
|
|
57
|
+
collapsed={collapsed}
|
|
58
|
+
theme="dark"
|
|
59
|
+
title={work.name}
|
|
60
|
+
extra={<SearchInWork workId={work.id} />}
|
|
61
|
+
/>
|
|
62
|
+
}
|
|
63
|
+
/>;
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| SecondaryHeader 属性 | 说明 | 类型 |
|
|
67
|
+
| --- | --- | --- |
|
|
68
|
+
| collapsed | 侧栏是否收起,收起时只剩箭头 | `boolean` |
|
|
69
|
+
| theme | 明暗,跟随侧栏 | `"light" \| "dark"` |
|
|
70
|
+
| backText | 返回入口文案 | `string` |
|
|
71
|
+
| onBack | 自定义返回行为,不给则退回上一页 | `() => void` |
|
|
72
|
+
| title | 当前实体名 | `string` |
|
|
73
|
+
| extra | 返回入口与标题之间的插槽 | `ReactNode` |
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useLocation } from "react-router";
|
|
3
|
+
import { useAliveController } from "react-activation";
|
|
4
|
+
|
|
5
|
+
import { TabType } from "..";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 按业务规则在地址变化时收掉一批页签。
|
|
9
|
+
*
|
|
10
|
+
* 起因是消费方(writer-assistant)的一条规则:进出某部作品时,把上一部作品的所有页签
|
|
11
|
+
* 一起清掉——「一次只专注一部书」。这种规则依赖业务对 URL 的解读(哪一段是作品 id),
|
|
12
|
+
* 组件库不可能内建,但**收页签这件事本身**必须留在库里做:光从 openKeys 里删掉不够,
|
|
13
|
+
* 还要 `drop` 掉 react-activation 的缓存,否则组件树留在内存里,下次同名路由回来还会
|
|
14
|
+
* 复用旧实例。
|
|
15
|
+
*
|
|
16
|
+
* 所以对外只暴露一个判定函数,删除与丢缓存由这里统一执行。
|
|
17
|
+
*
|
|
18
|
+
* 判定按**当前地址**做,不需要消费方自己记「上一个是什么」:每次地址变化重新评估每个
|
|
19
|
+
* 页签「在现在这个位置还该不该留着」,语义更直白,也不会因为漏更新 ref 而出错。
|
|
20
|
+
*/
|
|
21
|
+
export const useKeepTabs = (
|
|
22
|
+
shouldKeepTab: ((tab: TabType, info: { pathname: string }) => boolean) | undefined,
|
|
23
|
+
setOpenkeys: React.Dispatch<React.SetStateAction<TabType[]>>
|
|
24
|
+
) => {
|
|
25
|
+
const { pathname } = useLocation();
|
|
26
|
+
const { drop } = useAliveController();
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!shouldKeepTab) return;
|
|
30
|
+
|
|
31
|
+
setOpenkeys((prev) => {
|
|
32
|
+
const kept = prev.filter((tab) => shouldKeepTab(tab, { pathname }));
|
|
33
|
+
if (kept.length === prev.length) return prev;
|
|
34
|
+
|
|
35
|
+
prev
|
|
36
|
+
.filter((tab) => !kept.includes(tab))
|
|
37
|
+
// 缓存是按不带 query 的路径存的
|
|
38
|
+
.forEach((tab) => drop(tab.key.split("?")[0] || ""));
|
|
39
|
+
|
|
40
|
+
return kept;
|
|
41
|
+
});
|
|
42
|
+
// shouldKeepTab 多半是行内箭头函数,放进依赖会每次渲染都重跑。
|
|
43
|
+
// 这个 effect 的触发时机就是「地址变了」,按 pathname 走即可。
|
|
44
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
45
|
+
}, [pathname]);
|
|
46
|
+
};
|
|
@@ -62,6 +62,32 @@ export default () => (
|
|
|
62
62
|
| affixRouter | 额外固定的路由路径(等价于给这些路由加 `meta.affix`) | `string[]` | `[]` |
|
|
63
63
|
| basePath | 关掉最后一个页签后回退到的路径 | `string` | `"/"` |
|
|
64
64
|
| showReload | 页签栏最前面的「刷新当前页」按钮 | `boolean` | `true` |
|
|
65
|
+
| shouldKeepTab | 地址变化时决定每个页签留不留,返回 `false` 的会被关掉并丢弃缓存 | `(tab, { pathname }) => boolean` | - |
|
|
66
|
+
|
|
67
|
+
## 按业务规则批量收页签
|
|
68
|
+
|
|
69
|
+
`shouldKeepTab` 用来表达「在当前这个位置,这个页签还该不该留着」。典型场景是同一时间只
|
|
70
|
+
围绕一个对象工作,切到另一个对象时把上一个的页签一起收掉:
|
|
71
|
+
|
|
72
|
+
```tsx | pure
|
|
73
|
+
const workId = (p: string) => p.match(/\/work\/([0-9a-f-]{36})/)?.[1] ?? null;
|
|
74
|
+
|
|
75
|
+
<Layout.NavTabBar
|
|
76
|
+
router={router}
|
|
77
|
+
shouldKeepTab={(tab, { pathname }) => {
|
|
78
|
+
const tabWork = workId(tab.key);
|
|
79
|
+
// 不属于任何作品的页签(总览、设置…)一律留着;属于某部作品的,只在还在那部作品里时留
|
|
80
|
+
return !tabWork || tabWork === workId(pathname);
|
|
81
|
+
}}
|
|
82
|
+
/>;
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
判定按**当前地址**做,不用自己记「上一个位置是什么」—— 每次地址变化重新评估一遍,语义更
|
|
86
|
+
直白,也不会因为漏更新而残留。
|
|
87
|
+
|
|
88
|
+
> 关页签这件事必须由组件来做:光把它从列表里去掉不够,还要丢掉 react-activation 的缓存,
|
|
89
|
+
> 否则那棵组件树留在内存里,下次同名路由回来会复用旧实例。所以这里只让你给判定,删除与
|
|
90
|
+
> 丢缓存由组件统一执行。
|
|
65
91
|
|
|
66
92
|
## 配套 Hook
|
|
67
93
|
|
|
@@ -27,6 +27,7 @@ import { useTabPath } from "./_hooks/useTabPath";
|
|
|
27
27
|
import { useTabContextMenu } from "./_hooks/useTabContextMenu";
|
|
28
28
|
import { useDropTabKey } from "./_hooks/useDropTabKey";
|
|
29
29
|
import { useTabTitle } from "./_hooks/useTabTitle";
|
|
30
|
+
import { useKeepTabs } from "./_hooks/useKeepTabs";
|
|
30
31
|
import TabLabel from "./_components/TabLabel";
|
|
31
32
|
import DraggableTabNode from "./_components/SortableTab";
|
|
32
33
|
import TabDragOverlay from "./_components/TabDragOverlay";
|
|
@@ -51,6 +52,25 @@ export interface NavTabBarProps {
|
|
|
51
52
|
basePath?: string;
|
|
52
53
|
/** 页签栏最前面的「刷新当前页」按钮 */
|
|
53
54
|
showReload?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* 地址变化时决定每个页签还留不留。返回 false 的会被关掉,并连同 react-activation 的
|
|
57
|
+
* 缓存一起丢弃。
|
|
58
|
+
*
|
|
59
|
+
* 给的是「在当前地址下这个页签还该不该在」这种判定,不需要自己记上一个位置是什么。
|
|
60
|
+
*
|
|
61
|
+
* @example 一次只专注一部作品:进入另一部书时,把上一部的页签全收掉
|
|
62
|
+
* ```ts
|
|
63
|
+
* const workId = (p: string) => p.match(/\/work\/([0-9a-f-]{36})/)?.[1] ?? null;
|
|
64
|
+
*
|
|
65
|
+
* <Layout.NavTabBar
|
|
66
|
+
* shouldKeepTab={(tab, { pathname }) => {
|
|
67
|
+
* const tabWork = workId(tab.key);
|
|
68
|
+
* return !tabWork || tabWork === workId(pathname);
|
|
69
|
+
* }}
|
|
70
|
+
* />
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
shouldKeepTab?: (tab: TabType, info: { pathname: string }) => boolean;
|
|
54
74
|
}
|
|
55
75
|
|
|
56
76
|
const NavTabBar: React.FC<NavTabBarProps> = (props) => {
|
|
@@ -59,6 +79,7 @@ const NavTabBar: React.FC<NavTabBarProps> = (props) => {
|
|
|
59
79
|
affixRouter = [],
|
|
60
80
|
basePath = "/",
|
|
61
81
|
showReload = true,
|
|
82
|
+
shouldKeepTab,
|
|
62
83
|
} = props;
|
|
63
84
|
const navigate = useNavigate();
|
|
64
85
|
const { drop, refresh } = useAliveController();
|
|
@@ -78,6 +99,7 @@ const NavTabBar: React.FC<NavTabBarProps> = (props) => {
|
|
|
78
99
|
|
|
79
100
|
useDropTabKey(setOpenkeys);
|
|
80
101
|
useTabTitle(setOpenkeys);
|
|
102
|
+
useKeepTabs(shouldKeepTab, setOpenkeys);
|
|
81
103
|
|
|
82
104
|
// ID of the tab currently being dragged
|
|
83
105
|
const [activeId, setActiveId] = useState<string | null>(null);
|
package/src/layout/index.ts
CHANGED
|
@@ -29,6 +29,13 @@ export type { RouteType, MetaType } from "./types";
|
|
|
29
29
|
*/
|
|
30
30
|
export type { AccountAction, HeaderProps } from "./Header";
|
|
31
31
|
export type { MenuType, MenuProps } from "./Menu";
|
|
32
|
+
/**
|
|
33
|
+
* 次级菜单的默认头部。`Menu` 在切到次级菜单时会自己渲染它,一般不用管;
|
|
34
|
+
* 需要在返回入口下面塞点东西(详情内检索框之类)时,把它的 `extra` 用起来、
|
|
35
|
+
* 整块作为 `secondaryHeader` 传给 `Menu`。
|
|
36
|
+
*/
|
|
37
|
+
export { default as SecondaryHeader } from "./Menu/_components/SecondaryHeader";
|
|
38
|
+
export type { SecondaryHeaderProps } from "./Menu/_components/SecondaryHeader";
|
|
32
39
|
export type { TabType, NavTabBarProps } from "./NavTabBar";
|
|
33
40
|
export type { BreadcrumbType, BreadcrumbProps } from "./Breadcrumb";
|
|
34
41
|
export type { InternationalizationProps } from "./I18n";
|