feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState, type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { IconButton, Tooltip } from '@coze-arch/coze-design';
|
||||
|
||||
import { reportNavClick } from '../utils';
|
||||
import { type LayoutButtonItem } from '../types';
|
||||
|
||||
export const GlobalLayoutActionBtn: FC<LayoutButtonItem> = ({
|
||||
icon,
|
||||
iconClass,
|
||||
onClick,
|
||||
tooltip,
|
||||
dataTestId,
|
||||
className,
|
||||
portal,
|
||||
renderButton,
|
||||
}) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const onButtonClick = () => {
|
||||
setVisible(false);
|
||||
reportNavClick(tooltip);
|
||||
onClick?.();
|
||||
};
|
||||
|
||||
const btn = renderButton ? (
|
||||
renderButton({
|
||||
onClick: onButtonClick,
|
||||
icon,
|
||||
dataTestId,
|
||||
})
|
||||
) : (
|
||||
<IconButton
|
||||
color="secondary"
|
||||
size="large"
|
||||
className={classNames(className, { '!h-full': !!iconClass })}
|
||||
icon={
|
||||
<div
|
||||
className={classNames(
|
||||
'text-[20px] coz-fg-primary h-[20px]',
|
||||
iconClass,
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
}
|
||||
onClick={onButtonClick}
|
||||
data-testid={dataTestId}
|
||||
/>
|
||||
);
|
||||
// 如果 tooltip 为空,则不显示 tooltip
|
||||
return (
|
||||
<>
|
||||
{tooltip ? (
|
||||
<Tooltip
|
||||
content={tooltip}
|
||||
position="right"
|
||||
clickToHide
|
||||
visible={visible}
|
||||
onVisibleChange={setVisible}
|
||||
>
|
||||
{btn}
|
||||
</Tooltip>
|
||||
) : (
|
||||
btn
|
||||
)}
|
||||
{portal}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
import { type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { reportNavClick } from '../utils';
|
||||
import { type LayoutMenuItem } from '../types';
|
||||
|
||||
const menuStyle = classNames(
|
||||
'w-[60px] h-[48px]',
|
||||
'flex flex-col items-center justify-center',
|
||||
'rounded-[6px]',
|
||||
'transition-all',
|
||||
'hover:coz-mg-primary-hovered',
|
||||
);
|
||||
|
||||
export const GLobalLayoutMenuItem: FC<LayoutMenuItem> = ({
|
||||
title,
|
||||
icon,
|
||||
activeIcon,
|
||||
path,
|
||||
dataTestId,
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
|
||||
let isActive = false;
|
||||
let newPath = '';
|
||||
// 如果 path 是数组,则取第一个匹配的路径
|
||||
if (Array.isArray(path)) {
|
||||
isActive = path.some(p => location.pathname.startsWith(p));
|
||||
newPath = path.find(p => location.pathname.startsWith(p)) || path[0];
|
||||
} else {
|
||||
isActive = location.pathname.startsWith(path);
|
||||
newPath = path;
|
||||
}
|
||||
|
||||
// cp-disable-next-line
|
||||
const isLink = newPath.startsWith('https://');
|
||||
|
||||
const navId = `primary-menu-${newPath.startsWith('/') ? newPath.slice(1) : newPath}`;
|
||||
return (
|
||||
<NavLink
|
||||
to={newPath}
|
||||
target={isLink ? '_blank' : undefined}
|
||||
className="no-underline"
|
||||
onClick={() => {
|
||||
reportNavClick(title);
|
||||
}}
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
menuStyle,
|
||||
isActive
|
||||
? 'coz-mg-primary coz-fg-plus'
|
||||
: 'coz-bg-max coz-fg-secondary',
|
||||
)}
|
||||
id={navId}
|
||||
>
|
||||
<div className="text-[20px] leading-none">
|
||||
{isActive ? activeIcon : icon}
|
||||
</div>
|
||||
<div className="mt-[2px] h-[14px] font-[500] flex items-center justify-center overflow-hidden leading-none overflow-hidden w-full">
|
||||
<span className="text-[20px] scale-50 whitespace-nowrap">
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</NavLink>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Divider, Space } from '@coze-arch/coze-design';
|
||||
import { IconMenuLogo } from '@coze-arch/bot-icons';
|
||||
import { useRouteConfig } from '@coze-arch/bot-hooks';
|
||||
|
||||
import { type LayoutProps } from '../types';
|
||||
import { SubMenu } from './sub-menu';
|
||||
import { GLobalLayoutMenuItem } from './menu-item';
|
||||
import { GlobalLayoutActionBtn } from './action-btn';
|
||||
|
||||
const siderStyle = classNames(
|
||||
'relative',
|
||||
'h-full',
|
||||
'border-[1px] border-solid coz-stroke-primary rounded-[14px]',
|
||||
'coz-bg-max',
|
||||
'flex flex-row items-stretch',
|
||||
);
|
||||
|
||||
const mainMenuStyle = classNames(
|
||||
'px-[6px] py-[16px]',
|
||||
'flex flex-col h-full items-center',
|
||||
);
|
||||
|
||||
export const GlobalLayoutSider: FC<Omit<LayoutProps, 'hasSider'>> = ({
|
||||
actions,
|
||||
menus,
|
||||
extras,
|
||||
onClickLogo,
|
||||
footer = null,
|
||||
}) => {
|
||||
const config = useRouteConfig();
|
||||
const { subMenu: SubMenuComponent } = config;
|
||||
const hasSubNav = Boolean(SubMenuComponent);
|
||||
|
||||
return (
|
||||
<div className="pl-8px py-8px h-full">
|
||||
<div className={siderStyle}>
|
||||
{/* 主导航 */}
|
||||
<div
|
||||
className={classNames(
|
||||
mainMenuStyle,
|
||||
hasSubNav &&
|
||||
'border-0 border-r-[1px] border-solid coz-stroke-primary',
|
||||
)}
|
||||
>
|
||||
<IconMenuLogo
|
||||
onClick={onClickLogo}
|
||||
className="cursor-pointer w-[40px] h-[40px]"
|
||||
/>
|
||||
<div className="mt-[16px]">
|
||||
{actions?.map((action, index) => (
|
||||
<GlobalLayoutActionBtn {...action} key={index} />
|
||||
))}
|
||||
</div>
|
||||
<Divider className="my-12px w-[24px]" />
|
||||
<Space spacing={4} vertical className="flex-1 overflow-auto">
|
||||
{menus?.map((menu, index) => (
|
||||
<GLobalLayoutMenuItem {...menu} key={index} />
|
||||
))}
|
||||
</Space>
|
||||
<Space spacing={4} vertical className="mt-[12px]">
|
||||
{extras?.map((extra, index) => (
|
||||
<GlobalLayoutActionBtn {...extra} key={index} />
|
||||
))}
|
||||
{footer}
|
||||
</Space>
|
||||
</div>
|
||||
{/* 二级导航 */}
|
||||
<SubMenu />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
GlobalLayoutSider.displayName = 'GlobalLayoutSider';
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC, Suspense, useState, useCallback } from 'react';
|
||||
|
||||
import { useRouteConfig } from '@coze-arch/bot-hooks';
|
||||
|
||||
import styles from '../side-sheet.module.less';
|
||||
|
||||
const STORAGE_KEY = 'submenu-width';
|
||||
const MIN_WIDTH = 200;
|
||||
const MAX_WIDTH = 380;
|
||||
|
||||
export const SubMenu: FC = () => {
|
||||
const config = useRouteConfig();
|
||||
const { subMenu: SubMenuComponent } = config;
|
||||
const [width, setWidth] = useState(() => {
|
||||
const savedWidth = localStorage.getItem(STORAGE_KEY);
|
||||
return savedWidth
|
||||
? Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Number(savedWidth)))
|
||||
: MIN_WIDTH;
|
||||
});
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
const startX = event.pageX;
|
||||
const startWidth = width;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const newWidth = Math.min(
|
||||
MAX_WIDTH,
|
||||
Math.max(MIN_WIDTH, startWidth + e.pageX - startX),
|
||||
);
|
||||
setWidth(newWidth);
|
||||
localStorage.setItem(STORAGE_KEY, String(newWidth));
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
},
|
||||
[width],
|
||||
);
|
||||
|
||||
if (!SubMenuComponent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-row">
|
||||
<div
|
||||
className="overflow-auto flex flex-col box-border px-[6px] py-[12px]"
|
||||
style={{ width: `${width}px` }}
|
||||
>
|
||||
<Suspense>
|
||||
<SubMenuComponent />
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className={styles['sub-menu-resize']} onMouseDown={handleMouseDown}>
|
||||
<div className={styles['sub-menu-resize-line']}></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user