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,33 @@
/*
* 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 { useParams } from 'react-router-dom';
import { useEffect } from 'react';
import { messageReportEvent } from '@coze-arch/bot-utils';
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
export const useMessageReportEvent = () => {
const params = useParams<DynamicParams>();
useEffect(() => {
if (params.bot_id) {
messageReportEvent.start(params.bot_id);
}
return () => {
messageReportEvent.interrupt();
};
}, [params.bot_id]);
};

View File

@@ -0,0 +1,36 @@
/*
* 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 { userStoreService } from '@coze-studio/user-store';
import { type UserSenderInfo } from '@coze-common/chat-area';
export const useUserSenderInfo = () => {
const userLabel = userStoreService.useUserLabel();
const userInfo = userStoreService.useUserInfo();
if (!userInfo) {
return null;
}
const userSenderInfo: UserSenderInfo = {
url: userInfo?.avatar_url || '',
nickname: userInfo?.name || '',
id: userInfo?.user_id_str || '',
userUniqueName: userInfo?.app_user_info?.user_unique_name || '',
userLabel,
};
return userSenderInfo;
};

View File

@@ -0,0 +1,40 @@
/*
* 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.
*/
/**
* @description `LayoutContext`用于跨组件传递布局相关信息
* @since 2024.03.05
*/
import { createContext, useContext } from 'react';
export enum PlacementEnum {
LEFT = 'left',
CENTER = 'center',
RIGHT = 'right',
}
interface ILayoutContext {
placement: PlacementEnum;
}
const context = createContext<ILayoutContext>({
placement: PlacementEnum.CENTER,
});
export const useLayoutContext = () => useContext(context);
// eslint-disable-next-line @typescript-eslint/naming-convention
export const LayoutContext = context.Provider;

View File

@@ -0,0 +1,25 @@
/*
* 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' />
declare const ENABLE_COVERAGE: boolean;
interface Window {
/**
* tea 实例
*/
Tea?: any;
}

View File

@@ -0,0 +1,40 @@
/*
* 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 { useRouteConfig, TRouteConfigGlobal } from './use-route-config';
export { useIsResponsiveByRouteConfig } from './use-responsive';
export { useLoggedIn } from './use-loggedin';
export { useLineClamp } from './use-line-clamp';
export { useInitialValue } from './use-initial-value';
export { useExposure, UseExposureParams } from './use-exposure';
export {
useComponentState,
ComponentStateUpdateFunc,
} from './use-component-state';
export {
useDragAndPasteUpload,
UseDragAndPasteUploadParam,
} from './use-drag-and-paste-upload';
export { useDefaultExPandCheck } from './use-default-expand-check';
export { useResetLocationState } from './router/use-reset-location-state';
export {
PlacementEnum,
useLayoutContext,
LayoutContext,
} from './editor-layout';
export { usePageState, PageStateUpdateFunc } from './use-page-state';
export { useUserSenderInfo } from './bot/use-user-sender-info';
export { useMessageReportEvent } from './bot/use-message-report-event';

View File

@@ -0,0 +1,30 @@
/*
* 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';
/** 清空认证数据的路由参数 */
export const resetAuthLoginDataFromRoute = () => {
window.history.replaceState({}, '');
};
export function useResetLocationState() {
const location = useLocation();
return () => {
// 清空location的state
location.state = {};
resetAuthLoginDataFromRoute();
};
}

View File

@@ -0,0 +1,64 @@
/*
* 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 } from 'react';
import { type Obj } from '@coze-arch/bot-typings/common';
export interface ComponentStateUpdateFunc<State extends Obj> {
(freshState: State, replace: true): void;
(freshState: Partial<State>): void;
}
/**
* 对 state 一层封装,用途:
* 1. 默认增量更新
* 2. 支持重置
*
* @example
* const { state, resetState, setState } = useComponentState({ a: 1, b: 2 });
* console.log(state); // { a: 1, b: 2 }
* setState({ b: 3 }); // { a: 1, b: 3 }
*
* setState({ a: 2 }, true); // { a: 2 }
*
* resetState(); // { a: 1, b: 2 }
*
* @author lengfangbing
* @docs by zhanghaochen
*/
export function useComponentState<State extends Obj>(initState: State) {
const [state, customSetState] = useState(initState);
function setState(freshState: State, replace: true): void;
function setState(freshState: Partial<State>): void;
function setState(freshState: Partial<State> | State, replace?: true) {
if (replace) {
customSetState(freshState as State);
}
customSetState(prev => ({ ...prev, ...freshState }));
}
const resetState = () => {
customSetState(initState);
};
return {
state,
resetState,
setState: setState as ComponentStateUpdateFunc<State>,
};
}

View File

@@ -0,0 +1,92 @@
/*
* 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 { useMemo } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { size } from 'lodash-es';
import { type SkillKeyEnum } from '@coze-agent-ide/tool-config';
import { usePageRuntimeStore } from '@coze-studio/bot-detail-store/page-runtime';
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
import { skillKeyToApiStatusKeyTransformer } from '@coze-arch/bot-utils';
import {
TabStatus,
type TabDisplayItems,
} from '@coze-arch/bot-api/developer_api';
/**
* 用于校验当前模块默认展开收起状态
* @deprecated 改属性已经废弃不维护,请更换@coze-agent-ide/tool中的useToolContentBlockDefaultExpand
* @param blockKey 主键
* @param configured 是否有配置内容
* @param when 是否校验
*
*/
const useDefaultExPandCheck = (
$params: {
blockKey: SkillKeyEnum;
configured: boolean;
},
$when = true,
) => {
const { blockKey, configured = false } = $params;
const isReadonly = useBotDetailIsReadonly();
const { init, editable, botSkillBlockCollapsibleState } = usePageRuntimeStore(
useShallow(store => ({
init: store.init,
editable: store.editable,
botSkillBlockCollapsibleState: store.botSkillBlockCollapsibleState,
})),
);
return useMemo(() => {
// 不做校验
if (!$when) {
return undefined;
// 状态机未就绪
} else if (!init || size(botSkillBlockCollapsibleState) === 0) {
return undefined;
/**
* @description 仅在满足以下条件时用户行为记录才能生效
*
* 1. 用户有编辑权限
* 2. 不能是历史预览环境
* 3. 必须已配置
*/
} else if (editable && !isReadonly && configured) {
const transformerBlockKey = skillKeyToApiStatusKeyTransformer(blockKey);
const collapsibleState =
botSkillBlockCollapsibleState[
transformerBlockKey as keyof TabDisplayItems
];
if (collapsibleState === TabStatus.Open) {
return true;
} else if (collapsibleState === TabStatus.Close) {
return false;
}
}
return configured;
}, [
$when,
blockKey,
configured,
init,
isReadonly,
editable,
botSkillBlockCollapsibleState,
]);
};
export { useDefaultExPandCheck };

View File

@@ -0,0 +1,39 @@
/*
* 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 const getFileListByDragOrPaste = (
e: HTMLElementEventMap['drop'] | HTMLElementEventMap['paste'],
): File[] => {
let fileList: FileList | undefined;
if ('dataTransfer' in e) {
fileList = e.dataTransfer?.files;
} else {
fileList = e.clipboardData?.files;
}
if (!fileList) {
return [];
}
return formatTypeFileListToTypeArray(fileList);
};
export const formatTypeFileListToTypeArray = (fileList: FileList) => {
const fileLength = fileList.length;
const fileArray: (File | null)[] = [];
for (let i = 0; i < fileLength; i++) {
fileArray.push(fileList.item(i));
}
return fileArray.filter((file): file is File => Boolean(file));
};

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.
*/
export const isHasFileByDrag = (e: HTMLElementEventMap['drag']) =>
Boolean(e.dataTransfer?.types.includes('Files'));

View File

@@ -0,0 +1,223 @@
/*
* 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 RefObject, useEffect, useRef, useState } from 'react';
import { Toast } from '@coze-arch/bot-semi';
import { isHasFileByDrag } from './helper/is-has-file-by-drag';
import { getFileListByDragOrPaste } from './helper/get-file-list-by-drag';
export interface UseDragAndPasteUploadParam {
ref: RefObject<HTMLDivElement>;
/**
* 触发上传的回调
*/
onUpload: (fileList: File[]) => void;
/**
* 是否禁用拖拽上传
*/
disableDrag: boolean;
/**
* 是否禁用粘贴上传
*/
disablePaste: boolean;
/**
* 最大上传的文件数量
*/
fileLimit: number;
/**
* 文件大小, eg: 10MB = 10 * 1024 * 1024
*/
maxFileSize: number;
invalidSizeMessage: string | undefined;
invalidFormatMessage: string | undefined;
fileExceedsMessage: string | undefined;
/**
* 文件格式是否合法
*/
isFileFormatValid: (file: File) => boolean;
/**
* @returns 已存在文件的数量
*/
getExistingFileCount: () => number;
/**
* 用户离开拖拽区域时, state 变化的延迟
* @default 100
*/
closeDelay: number | undefined;
}
// eslint-disable-next-line max-lines-per-function, @coze-arch/max-line-per-function -- drag callback
export const useDragAndPasteUpload = ({
onUpload,
disableDrag,
disablePaste,
fileLimit,
isFileFormatValid,
maxFileSize,
getExistingFileCount,
closeDelay = 100,
invalidFormatMessage,
invalidSizeMessage,
fileExceedsMessage,
ref,
}: UseDragAndPasteUploadParam) => {
const [isDragOver, setIsDragOver] = useState(false);
/**
* drag 时, 指针从 parent dom 进入到 child dom 时会快速连续触发 onDragEnter onDragLeave 导致状态流转错误
* 在 onLeave 时给状态流转加上延时能够避免流转问题
* 触发 dragEnter dragLeave 时, event.target 不一定指向 parent dom, 所以也无法通过 target 来判断
*/
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
const clearTimer = () => {
if (!timer.current) {
return;
}
clearTimeout(timer.current);
timer.current = null;
};
const handleDropOrPaste = (
e: HTMLElementEventMap['paste'] | HTMLElementEventMap['drop'],
) => getFileListByDragOrPaste(e);
const handleUpload = (fileList: File[]) => {
if (!fileList.some(isFileFormatValid)) {
Toast.warning({
content: invalidFormatMessage,
showClose: false,
});
return;
}
if (!fileList.some(file => file.size <= maxFileSize)) {
Toast.warning({
content: invalidSizeMessage,
showClose: false,
});
return;
}
const remainingCount = fileLimit - getExistingFileCount();
if (fileList.length > remainingCount) {
Toast.warning({
content: fileExceedsMessage,
showClose: false,
});
return;
}
onUpload(fileList);
};
useEffect(() => {
const target = ref.current;
if (!target) {
return;
}
if (disableDrag) {
return;
}
const onDragEnter = (e: HTMLElementEventMap['dragenter']) => {
clearTimer();
if (!isHasFileByDrag(e)) {
return;
}
};
const onDragOver = (e: HTMLElementEventMap['dragover']) => {
/**
* {@link https://segmentfault.com/q/1010000011746669}
* 原理:
* 这里阻止的默认行为是开启可编辑模式具体就是document.designMode属性
* 该属性默认是off关闭的当开启之后就可以对网页进行编辑
* 开启的方式就是document.designMode = "on"; 开启之后就不用在监听dragover事件中阻止默认了
*/
e.preventDefault();
clearTimer();
if (!isHasFileByDrag(e)) {
return;
}
setIsDragOver(true);
};
const onDragLeave = (e: HTMLElementEventMap['dragleave']) => {
clearTimer();
timer.current = setTimeout(() => {
setIsDragOver(false);
}, closeDelay);
};
const onDragDrop = (e: HTMLElementEventMap['drop']) => {
clearTimer();
if (!isHasFileByDrag(e)) {
return;
}
setIsDragOver(false);
e.preventDefault();
handleUpload(handleDropOrPaste(e));
};
target.addEventListener('dragenter', onDragEnter);
target.addEventListener('dragover', onDragOver);
target.addEventListener('dragleave', onDragLeave);
target.addEventListener('drop', onDragDrop);
return () => {
clearTimer();
target.removeEventListener('dragenter', onDragEnter);
target.removeEventListener('dragover', onDragOver);
target.removeEventListener('dragleave', onDragLeave);
target.removeEventListener('drop', onDragDrop);
};
}, [ref.current, disableDrag]);
useEffect(() => {
const target = ref.current;
if (!target) {
return;
}
const onPaste = (e: HTMLElementEventMap['paste']) => {
const fileList = handleDropOrPaste(e);
if (!fileList.length) {
return;
}
e.preventDefault();
if (disablePaste) {
return;
}
handleUpload(fileList);
};
target.addEventListener('paste', onPaste);
return () => {
target.removeEventListener('paste', onPaste);
};
}, [ref.current, disablePaste]);
return { isDragOver };
};

View File

@@ -0,0 +1,60 @@
/*
* 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, useRef } from 'react';
import { type BasicTarget } from 'ahooks/lib/utils/domTarget';
import { type Options } from 'ahooks/lib/useInViewport';
import { useInViewport } from 'ahooks';
import { type EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
export interface UseExposureParams {
/** 曝光元素 */
target: BasicTarget;
/** Intersection observer参数 */
options?: Options;
/** 上报事件名称 */
eventName?: EVENT_NAMES;
/** 上报参数 */
reportParams?: Record<string, unknown>;
/** 是否进行上报 默认为true */
needReport?: boolean;
isReportOnce?: boolean;
}
/** 曝光埋点上报 */
export const useExposure = ({
target,
options,
eventName,
reportParams,
needReport = true,
isReportOnce = false,
}: UseExposureParams) => {
const [isInView] = useInViewport(target, options);
const refHasReport = useRef(false);
useEffect(() => {
if (isReportOnce && refHasReport.current) {
//已上报过数据,就直接返回
return;
}
if (needReport && isInView) {
sendTeaEvent(eventName, reportParams);
refHasReport.current = true;
}
}, [needReport, isInView, isReportOnce]);
};

View File

@@ -0,0 +1,22 @@
/*
* 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 { useRef } from 'react';
export function useInitialValue<T>(value: T): T {
const ref = useRef<T>(value);
return ref.current;
}

View File

@@ -0,0 +1,41 @@
/*
* 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, useEffect, useRef } from 'react';
export function useLineClamp() {
const contentRef = useRef<HTMLDivElement>(null);
const [isClamped, setIsClamped] = useState(false);
useEffect(() => {
const checkClamped = () => {
if (contentRef.current) {
setIsClamped(
contentRef.current.scrollHeight > contentRef.current.clientHeight,
);
}
};
checkClamped();
window.addEventListener('resize', checkClamped);
return () => {
window.removeEventListener('resize', checkClamped);
};
}, []);
return { contentRef, isClamped };
}

View File

@@ -0,0 +1,22 @@
/*
* 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 { userStoreService } from '@coze-studio/user-store';
/**
* 判断当前用户是否处于登陆状态
*/
export const useLoggedIn = () => userStoreService.useIsLogined();

View File

@@ -0,0 +1,68 @@
/*
* 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, useRef, useState } from 'react';
export interface PageStateUpdateFunc<State extends object = object> {
(freshState: State, replace: true): void;
(freshState: Partial<State>): void;
}
/**
* 对state一层封装包含更新state、重置state
*
* @deprecated 请使用 bot-hooks 的 useComponentStates
*/
export function usePageState<State extends object = object>(
initState: State,
autoResetWhenDestroy = false,
) {
const [state, customSetState] = useState(initState);
const destroyRef = useRef(false);
function setState(freshState: State, replace: true): void;
function setState(freshState: Partial<State>): void;
function setState(freshState: Partial<State> | State, replace?: true) {
if (replace) {
customSetState(freshState as State);
}
customSetState(prev => ({ ...prev, ...freshState }));
}
const resetState = () => {
customSetState(initState);
};
useEffect(() => {
destroyRef.current = autoResetWhenDestroy;
}, [autoResetWhenDestroy]);
useEffect(
() => () => {
// 自动重置状态
if (destroyRef.current) {
resetState();
}
},
[],
);
return {
state,
resetState,
setState: setState as PageStateUpdateFunc<State>,
};
}

View File

@@ -0,0 +1,39 @@
/*
* 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 { useMediaQuery, ScreenRange } from '@coze-arch/responsive-kit';
import { useRouteConfig } from './use-route-config';
export const useIsResponsiveByRouteConfig = () => {
const { responsive } = useRouteConfig();
const shouldResponsive = responsive !== undefined;
const { rangeMax, include = false } =
responsive === true
? { rangeMax: ScreenRange.LG, include: false }
: responsive || {};
const matches = useMediaQuery(
include
? {
rangeMax,
}
: {
rangeMin: rangeMax,
},
);
const isResponsive = include ? matches : !matches;
return shouldResponsive && isResponsive;
};

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 { useMatches, type NavigateFunction } from 'react-router-dom';
import { type FC, useMemo } from 'react';
import { type ScreenRange } from '@coze-arch/responsive-kit';
export interface TRouteConfigGlobal {
/**
* 展示小助手
* @default true
* @import 社区版不支持该字段
*/
showAssistant?: boolean;
/**
* 展示小助手引导提示
* @default false
* @import 社区版不支持该字段
*/
showAssistantGuideTip?: boolean;
/**
* 当企业ID发生变化时的回调函数。
* @import 社区版不支持该字段
* @param enterpriseId - 变化后的企业ID。
* @param params - 包含导航函数和当前路径名的对象。
*/
onEnterpriseChange?: (
enterpriseId: string,
params: {
navigate: NavigateFunction; // 导航函数,用于路由跳转。
pathname: string; // 当前路径名,用于构建新的路径。
},
) => void;
/**
* 是否展示侧边栏
* @default false
*/
hasSider?: boolean;
/**
* 展示移动端不适配提示文案
* @default false
*/
showMobileTips?: boolean;
/**
* 是否需要身份验证
* @default false
*/
requireAuth?: boolean;
/**
* 登录失效时的回退地址
* @default /sign
*/
loginFallbackPath?: string;
/**
* @deprecated
* 是否允许身份验证为可选
* @default false
*/
requireAuthOptional?: boolean;
/**
* 设置为 true 时自动应用缺省值 { rangeMax: ScreenRange.LG, include: false } 对应之前绝大多数支持响应式路由的配置
* @default false
*/
responsive?: { rangeMax: ScreenRange; include?: boolean } | true;
/**
* 子菜单组件
* @default undefined
*/
subMenu?: FC<Record<string, never>>;
/**
* 一级导航菜单项 key
* @default undefined
*/
menuKey?: string;
/**
* 二级导航菜单项 key
* @default undefined
*/
subMenuKey?: string;
/**
* 控制是否根据 query 中的 page_mode 字段判断页面模式: 默认侧边导航模式 or 全屏popover模式
* @default false
*/
pageModeByQuery?: boolean;
}
export const useRouteConfig = <
TConfig extends TRouteConfigGlobal = TRouteConfigGlobal,
>(
defaults?: TConfig,
// 强制所有字段可能为空
): Partial<TConfig> => {
const matches = useMatches();
return useMemo<Partial<TConfig>>(
() =>
matches.reduce(
(res, matchedRoute) => ({
...res,
...(matchedRoute.handle as Partial<TConfig>),
...(matchedRoute.data as Partial<TConfig>),
}),
defaults ?? {},
),
[matches],
);
};