feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -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}
</>
);
};

View File

@@ -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>
);
};

View File

@@ -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';

View File

@@ -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>
);
};