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

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -0,0 +1,13 @@
@common-box-shadow: 0px 2px 8px 0px rgba(31, 35, 41, 0.02),
0px 2px 4px 0px rgba(31, 35, 41, 0.02), 0px 2px 2px 0px rgba(31, 35, 41, 0.02);
.common-svg-icon(@size:14px, @color:#3370ff) {
> svg {
width: @size;
height: @size;
> path {
fill: @color;
}
}
}

View File

@@ -0,0 +1,8 @@
.menu {
:global {
.coz-item-text {
overflow: hidden;
flex: 1
}
}
}

View File

@@ -0,0 +1,148 @@
/*
* 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,
isValidElement,
type ReactNode,
type PropsWithChildren,
} from 'react';
import classNames from 'classnames';
import { Avatar, Badge, Dropdown } from '@coze-arch/coze-design';
import { useUserInfo } from '@coze-foundation/account-adapter';
import { reportNavClick } from '../global-layout/utils';
import { type LayoutAccountMenuItem } from '../global-layout/types';
import style from './index.module.less';
function isReactNode(value: unknown): value is ReactNode {
if (
value === null ||
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean' ||
isValidElement(value) ||
Array.isArray(value)
) {
return true;
}
return false;
}
export const GlobalLayoutAccountDropdown: FC<
PropsWithChildren<{
menus?: LayoutAccountMenuItem[];
userBadge?: ReactNode;
userTips?: ReactNode;
disableVisibleChange?: boolean;
visible?: boolean;
onVisibleChange?: (visible: boolean) => void;
}>
> = ({
menus,
userBadge = null,
userTips = null,
children,
disableVisibleChange,
visible,
onVisibleChange,
}) => {
const userInfo = useUserInfo();
if (!userInfo) {
return null;
}
return (
<>
<Dropdown
trigger="custom"
position={'rightBottom'}
visible={visible}
onVisibleChange={onVisibleChange}
onClickOutSide={() => {
if (!disableVisibleChange) {
onVisibleChange?.(false);
}
}}
render={
<Dropdown.Menu
className={classNames(style.menu, 'w-[250px]')}
mode="menu"
>
{menus?.map(item =>
isReactNode(item) ? (
item
) : (
<Dropdown.Item
key={item.title}
onClick={e => {
reportNavClick(item.title);
onVisibleChange?.(false);
item.onClick();
}}
data-testid={item.dataTestId}
>
<div className="flex items-center justify-between">
<div className="flex items-center">
<div className="mr-[8px] flex items-center">
{item.prefixIcon}
</div>
<div>{item.title}</div>
</div>
<div className="flex items-center">{item.extra}</div>
</div>
</Dropdown.Item>
),
)}
</Dropdown.Menu>
}
>
<div
className={classNames(
'relative',
'p-[4px] rounded-[8px] transition-colors hover:coz-mg-secondary-hovered',
'leading-none',
visible && 'coz-mg-secondary-hovered',
)}
onClick={() => {
if (!disableVisibleChange) {
onVisibleChange?.(!visible);
}
}}
data-testid="layout_avatar-menu-button"
>
<Badge
position="rightBottom"
countStyle={{
right: 6,
bottom: 6,
}}
count={userBadge}
>
<Avatar
src={userInfo.avatar_url}
className={classNames('w-[32px] h-[32px] rounded-full')}
/>
</Badge>
{userTips}
</div>
</Dropdown>
{children}
</>
);
};

View File

@@ -0,0 +1,9 @@
.bot-exit-btn {
:global {
.semi-button.semi-button-with-icon-only {
width: 32px;
padding: 4px;
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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 React from 'react';
import { type BackButtonProps } from '@coze-arch/foundation-sdk';
import { IconButton } from '@coze-arch/coze-design';
import { IconArrowLeft } from '@coze-arch/bot-icons';
import s from './index.module.less';
export const BackButton = ({ onClickBack }: BackButtonProps) => (
<div className={s['bot-exit-btn']}>
<IconButton
color="secondary"
icon={<IconArrowLeft />}
onClick={onClickBack}
data-testid="bot-exit-button"
/>
</div>
);

View File

@@ -0,0 +1,30 @@
/* stylelint-disable declaration-no-important -- 历史代码为避免引入新BUG暂不修复 */
.wrapper {
width: 100%;
height: 100%;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
margin: auto;
padding-top: 35.39vh;
}
.title {
margin-top: 24px !important;
margin-bottom: 4px !important;
font-size: 16px !important;
font-weight: 600 !important;
line-height: 22px !important;
}
.paragraph {
margin-bottom: 8px;
font-size: 12px;
line-height: 16px;
color: #1d1c2399;
}

View File

@@ -0,0 +1,121 @@
/*
* 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 { useNavigate, useRouteError } from 'react-router-dom';
import { useMemo, useState, type FC } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { escape } from 'lodash-es';
import { BaseEnum } from '@coze-arch/web-context';
import { getSlardarInstance } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import { Typography, UIButton } from '@coze-arch/bot-semi';
import { useRouteConfig } from '@coze-arch/bot-hooks';
import { isCustomError, useRouteErrorCatch } from '@coze-arch/bot-error';
import { IllustrationNoAccess } from '@douyinfe/semi-illustrations';
import { useSpaceStore, useSpaceApp } from '@coze-foundation/space-store';
import s from './index.module.less';
// i18n 的配置,对齐 starling 文案后再替换
export const GlobalError: FC = () => {
const navigate = useNavigate();
const spaceApp = useSpaceApp();
const { menuKey: base } = useRouteConfig();
const { id, getPersonalSpaceID } = useSpaceStore(
useShallow(spaceStore => ({
id: spaceStore.space.id,
getPersonalSpaceID: spaceStore.getPersonalSpaceID,
})),
);
const error = useRouteError();
useRouteErrorCatch(error);
const isLazyLoadError = useMemo(() => {
if (hasErrorMessage(error)) {
return /Minified\sReact\serror\s\#306/i.test(error.message);
}
}, [error]);
const customGlobalErrorConfig = useMemo(() => {
if (isCustomError(error)) {
return error.ext?.customGlobalErrorConfig;
}
}, [error]);
const [sessionId] = useState(() => getSlardarInstance()?.config()?.sessionId);
return (
<div className={s.wrapper}>
<div className={s.content}>
<IllustrationNoAccess width={140} height={140} />
<Typography.Title className={s.title}>
{customGlobalErrorConfig?.title ??
I18n.t('errorpage_bot_title', {}, `Failed to view the ${spaceApp}`)}
</Typography.Title>
<Typography.Paragraph className={s.paragraph}>
{customGlobalErrorConfig?.subtitle ??
I18n.t(
'errorpage_subtitle',
{},
"Please check your link or try again after joining the bot's team.",
)}
</Typography.Paragraph>
{!!sessionId && (
<div className="leading-[12px] mb-[24px] text-[12px] text-gray-400">
{sessionId}
</div>
)}
<UIButton
theme="solid"
onClick={() => {
let url = '';
if (BaseEnum.Space === base) {
const spaceId =
id ??
getPersonalSpaceID() ??
// 企业下无个人空间,缺省跳转到第一个空间
useSpaceStore.getState().spaceList[0]?.id;
url = spaceId ? `/space/${spaceId}/${spaceApp}` : '/space';
} else if (base && base in BaseEnum) {
url = `/${base}`;
} else {
url = '/';
}
if (!isLazyLoadError) {
navigate(url);
} else {
window.location.href = escape(url);
}
}}
>
{I18n.t('errorpage_bot_btn', {}, 'Go to Bot Platform')}
</UIButton>
</div>
</div>
);
};
function hasErrorMessage(e: unknown): e is { message: string } {
if (!e || typeof e !== 'object') {
return false;
}
if ('message' in e && typeof e.message === 'string') {
return true;
}
return false;
}

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

View File

@@ -0,0 +1,29 @@
/*
* 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 { createContext, useContext } from 'react';
import { noop } from 'lodash-es';
import { type GlobalLayoutContext } from './types';
export const globalLayoutContext = createContext<GlobalLayoutContext>({
sideSheetVisible: false,
setSideSheetVisible: noop,
});
export const GlobalLayoutProvider = globalLayoutContext.Provider;
export const useGlobalLayoutContext = () => useContext(globalLayoutContext);

View File

@@ -0,0 +1,63 @@
/*
* 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 { useEffect } from 'react';
import { isMobile, setMobileBody, setPCBody } from '@coze-arch/bot-utils';
import {
useIsResponsive,
useIsResponsiveByRouteConfig,
useRouteConfig,
} from '@coze-arch/bot-hooks';
import { useSignMobileStore } from '../../store';
import { useMobileTips } from '../../hooks';
import { useGlobalLayoutContext } from './context';
export const useLayoutResponsive = () => {
const { mobileTips, setMobileTips } = useSignMobileStore();
const { node: mobileTipsModal, open: openMobileTipsModal } = useMobileTips();
const config = useRouteConfig();
const isResponsiveOld = useIsResponsive();
const isResponsiveByRouteConfig = useIsResponsiveByRouteConfig();
const isResponsive = isResponsiveOld || isResponsiveByRouteConfig;
useEffect(() => {
if (config.showMobileTips) {
if (!mobileTips && isMobile()) {
openMobileTipsModal(); // 不适配移动端弹窗提示
setMobileTips(true);
}
if (isResponsive) {
setMobileBody();
} else {
setPCBody();
}
}
}, [config.showMobileTips, isResponsive]);
return {
isResponsive,
mobileTipsModal: config.showMobileTips ? mobileTipsModal : null,
};
};
export const useOpenGlobalLayoutSideSheet = () => {
const { setSideSheetVisible } = useGlobalLayoutContext();
return () => {
setSideSheetVisible(true);
};
};

View File

@@ -0,0 +1,80 @@
/*
* 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 { useLocation } from 'react-router-dom';
import { type FC, type PropsWithChildren, useState, useEffect } from 'react';
import cls from 'classnames';
import { Layout, SideSheet } from '@coze-arch/coze-design';
import { type LayoutProps } from './types';
import { useLayoutResponsive } from './hooks';
import { GlobalLayoutProvider } from './context';
import { GlobalLayoutSider } from './component/sider';
import sideSheetStyle from './side-sheet.module.less';
export const GlobalLayout: FC<PropsWithChildren<LayoutProps>> = ({
hasSider,
children,
banner,
...props
}) => {
const [sideSheetVisible, setSideSheetVisible] = useState(false);
const { isResponsive, mobileTipsModal } = useLayoutResponsive();
const location = useLocation();
useEffect(() => {
setSideSheetVisible(false);
}, [location.pathname, location.search, isResponsive]);
const siderContent = isResponsive ? (
<SideSheet
placement="left"
visible={sideSheetVisible}
className={sideSheetStyle['side-sheet']}
closeOnEsc
onCancel={() => {
setSideSheetVisible(false);
}}
>
<GlobalLayoutSider {...props} key="GlobalLayoutSider" />
</SideSheet>
) : (
<GlobalLayoutSider {...props} key="GlobalLayoutSider" />
);
return (
<GlobalLayoutProvider
value={{
sideSheetVisible,
setSideSheetVisible,
}}
>
{banner}
<Layout
className={cls(
'flex !flex-row items-stretch w-full coz-bg-plus',
banner ? 'h-[calc(100%_-_30px)]' : 'h-full',
)}
>
{hasSider ? siderContent : null}
<Layout className="flex-1 relative flex flex-col overflow-x-hidden coz-bg-plus">
{children}
</Layout>
{mobileTipsModal}
</Layout>
</GlobalLayoutProvider>
);
};

View File

@@ -0,0 +1,45 @@
.side-sheet {
:global {
.semi-sidesheet-inner {
width: fit-content;
background-color: transparent;
}
.semi-sidesheet-header {
display: none;
}
.semi-sidesheet-body {
width: fit-content;
padding: 0;
}
}
}
.sub-menu-resize {
cursor: col-resize;
position: absolute;
z-index: 1;
top: 0;
right: 0;
width: 12px;
height: 100%;
margin-right: -6px;
.sub-menu-resize-line {
position: absolute;
top: 12px;
left: 6px;
width: 1px;
height: calc(100% - 24px);
}
&:hover {
.sub-menu-resize-line {
background-color: var(--coz-stroke-plus);
}
}
}

View File

@@ -0,0 +1,70 @@
/*
* 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 ReactNode } from 'react';
export interface RenderButtonProps {
onClick?: () => void;
icon: ReactNode;
dataTestId?: string;
}
export interface LayoutButtonItem {
icon: ReactNode;
tooltip: string;
portal?: ReactNode;
onClick?: () => void;
dataTestId?: string;
className?: string;
iconClass?: string;
renderButton?: (props: RenderButtonProps) => ReactNode;
}
export interface LayoutMenuItem {
title: string;
icon: ReactNode;
activeIcon: ReactNode;
path: string | string[];
dataTestId?: string;
}
export type LayoutAccountMenuItem =
| {
prefixIcon?: ReactNode;
title: string;
extra?: ReactNode;
onClick: () => void;
dataTestId?: string;
}
| ReactNode;
export interface LayoutOverrides {
feedbackUrl?: string;
}
export interface LayoutProps {
hasSider: boolean;
actions?: LayoutButtonItem[];
menus?: LayoutMenuItem[];
extras?: LayoutButtonItem[];
onClickLogo?: () => void;
banner?: ReactNode;
footer?: ReactNode;
}
export interface GlobalLayoutContext {
sideSheetVisible: boolean;
setSideSheetVisible: (visible: boolean) => void;
}

View File

@@ -0,0 +1,27 @@
/*
* 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 { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
export const reportNavClick = (title: string) => {
sendTeaEvent(EVENT_NAMES.tab_click, { content: title });
sendTeaEvent(EVENT_NAMES.coze_space_sidenavi_ck, {
item: title,
navi_type: 'prime',
need_login: true,
have_access: true,
});
};

View File

@@ -0,0 +1,35 @@
/*
* 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 { IconButton } from '@coze-arch/coze-design';
import { IconSideFoldOutlined } from '@coze-arch/bot-icons';
import { useOpenGlobalLayoutSideSheet } from './global-layout/hooks';
// 用于在移动端模式开启侧边栏
export const SideSheetMenu = () => {
const open = useOpenGlobalLayoutSideSheet();
return (
<IconButton
color="secondary"
icon={<IconSideFoldOutlined className="coz-fg-primary text-base" />}
onClick={open}
/>
);
};
export default SideSheetMenu;

View File

@@ -0,0 +1,18 @@
/*
* 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.
*/
/**store */
export { useMobileTips } from './use-mobile-tips';

View File

@@ -0,0 +1,9 @@
.mobile-tips-span {
font-size: 0.75rem;
font-weight: 400;
line-height: 1rem;
color: rgb(29 28 35 / 80%);
text-align: center;
/* 133.333% */
}

View File

@@ -0,0 +1,56 @@
/*
* 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 React from 'react';
import { I18n } from '@coze-arch/i18n';
import { useUIModal } from '@coze-arch/bot-semi';
import s from './index.module.less';
export interface UseMobileTipsReturnType {
open: () => void;
close: () => void;
node: JSX.Element;
}
export const useMobileTips = (): UseMobileTipsReturnType => {
const { open, close, modal } = useUIModal({
title: I18n.t('landing_mobile_popup_title'),
okText: I18n.t('landing_mobile_popup_button'),
// width: 456,
centered: true,
hideCancelButton: true,
isMobile: true,
onOk: () => {
close();
},
});
return {
node: modal(
<span className={s['mobile-tips-span']}>
{I18n.t('landing_mobile_popup_context')}
</span>,
),
open: () => {
open();
},
close: () => {
close();
},
};
};

View File

@@ -0,0 +1,23 @@
/*
* 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.
*/
/** 布局框架 */
export { SideSheetMenu } from './components/side-sheet-menu';
export { GlobalError } from './components/global-error';
export { BackButton } from './components/back-button';
export { GlobalLayout } from './components/global-layout';
export { GlobalLayoutAccountDropdown } from './components/account-dropdown';
export { reportNavClick } from './components/global-layout/utils';

View File

@@ -0,0 +1,42 @@
/*
* 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 { devtools } from 'zustand/middleware';
import { create } from 'zustand';
interface SignMobileStore {
/** 标识有没有弹出过提示 */
mobileTips: boolean;
}
interface SignMobileAction {
setMobileTips: (tipsFlag: boolean) => void;
}
export const useSignMobileStore = create<SignMobileStore & SignMobileAction>()(
devtools(
set => ({
mobileTips: false,
setMobileTips: flag => {
set({ mobileTips: flag });
},
}),
{
enabled: IS_DEV_MODE,
name: 'botStudio.signMobile',
},
),
);

View File

@@ -0,0 +1,17 @@
/*
* 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.
*/
export { useSignMobileStore } from './bot-mobile';

View File

@@ -0,0 +1,17 @@
/*
* 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.
*/
/// <reference types='@coze-arch/bot-typings' />