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,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,55 @@
/*
* 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 {
useSetResponsiveBodyStyle,
useIsResponsive,
} from '@coze-arch/bot-hooks-adapter';
export {
useRouteConfig,
TRouteConfigGlobal,
useIsResponsiveByRouteConfig,
useLoggedIn,
useLineClamp,
useInitialValue,
useExposure,
UseExposureParams,
useComponentState,
ComponentStateUpdateFunc,
useDragAndPasteUpload,
UseDragAndPasteUploadParam,
useDefaultExPandCheck,
useResetLocationState,
PlacementEnum,
useLayoutContext,
LayoutContext,
usePageState,
PageStateUpdateFunc,
useUserSenderInfo,
useMessageReportEvent,
} from '@coze-arch/bot-hooks-base';
export {
usePageJumpService,
usePageJumpResponse,
SceneResponseType,
} from './page-jump';
export {
SceneType,
PageType,
WorkflowModalState,
OpenModeType,
} from './page-jump/config';

View File

@@ -0,0 +1,491 @@
/*
* 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 WorkflowMode,
type WorkFlowListStatus,
} from '@coze-arch/bot-api/workflow_api';
import type { PageJumpExecFunc } from '.';
/**
* workflow 弹窗打开模式,默认不传,或仅添加一次
*/
export enum OpenModeType {
OnlyOnceAdd = 'only_once_add',
}
/**
* 记录 workflow 弹窗的选中状态
*/
export interface WorkflowModalState {
/**
* @deprecated 是否已发布的状态
*/
status?: WorkFlowListStatus;
/**
* @deprecated 左边菜单栏选中的类型,注意这个 type 是前端翻译过的,与接口请求参数里的 type 不是同一个
*/
type?: number | string;
/**
* 弹窗状态 JSON 字符串
*/
statusStr?: string;
}
// #region ---------------------- step 1. 声明场景枚举,若涉及新页面则也声明一下页面枚举 ----------------------
// (添加了页面或场景枚举后,整个文件会有多处 ts 报错,这是预期内的。根据 step 指引一步一步完善配置即可)
/**
* 目标页面枚举,用于聚合【场景(scene)】,便于根据页面对当前场景做 narrowing
*
* e.g. 从 A 页跳转到 B 页,只需要定义 B 的页面枚举
*
*
* - Q: 为什么不使用默认的自增 enum 数值,每个页面手写两遍名字好麻烦
* - A: 便于调试时能直接看出含义,不用查代码对照,下同
*/
export const enum PageType {
BOT = 'bot',
WORKFLOW = 'workflow',
PLUGIN_MOCK_DATA = 'plugin_mock_data',
KNOWLEDGE = 'knowledge',
SOCIAL_SCENE = 'social_scene',
DOUYIN_BOT = 'douyin_bot',
}
/* eslint-disable @typescript-eslint/naming-convention -- 有必要 disable该场景需要不同的 enum 命名规范 */
/**
* 每种跳转场景的唯一枚举
*
* e.g. 比如 bot 编辑页创建 workflow 是一种场景,查看 workflow 是一种场景
*
* 枚举定义规范:
* 1. 最常见的场景:两个页面简单跳转可以按 `{源页面}__TO__{目标页面}` 的格式命名。
* (注意 TO 前后各有两个下划线,以便区分页面名为多个单词的场景,比如 BOT_LIST__TO__BOT_DETAIL后文不再赘述
* 2. 从源页面可能存在多种跳转到目标页面的场景,则可以按 `{源页面}__{行为}__{目标页面}` 格式命名。
* 3. 如果目标页面逻辑很简单,又有多处跳转来源,则可以按 `TO__{目标页面}` 格式命名。
* 4. 对于「跳转到目标页再返回」的特化逻辑,可以给已有的场景命名添加 `__{后缀}`,比如 BOT__CREATE__WORKFLOW__BACK
*
* - Q: 我觉得一个目标页面无脑使用 `TO__{目标页面}` 的格式就没问题啊,业务逻辑、来源判断什么我都可以在 参数(param) 和 响应值(response) 中完成
* - A: 的确,一个目标页面只声明一种场景就可以打遍天下,这里只是提供了逐级细化拆分场景的范式。
* 从 `TO__{目标页面}` 到 `{源页面}__TO__{目标页面}` 再到 `{源页面}__{行为}__{目标页面}` 场景是越来越细分的,业务方可以自行决定如何使用
*/
export const enum SceneType {
/** bot 详情页查看 workflow */
BOT__VIEW__WORKFLOW = 'botViewWorkflow',
/** bot 详情页查看 workflow或新建 workflow 但未发布,点击返回 */
WORKFLOW__BACK__BOT = 'workflowBackBot',
/** bot 详情页创建 workflow在 workflow 发布后返回 */
WORKFLOW_PUBLISHED__BACK__BOT = 'workflowPublishedBackBot',
/** 抖音 bot 详情查看 workflow */
DOUYIN_BOT__VIEW__WORKFLOW = 'douyinBotViewWorkflow',
/** 抖音 bot 详情页返回 */
WORKFLOW__BACK__DOUYIN_BOT = 'workflowBackDouyinBot',
/** 抖音 bot 详情页发布后返回 */
WORKFLOW_PUBLISHED__BACK__DOUYIN_BOT = 'workflowPulishedBackDouyinBot',
/** bot 详情页进入 mock data 页面 */
BOT__TO__PLUGIN_MOCK_DATA = 'botToPluginMockData',
/** workflow 详情页进入 mock data 页面 */
WORKFLOW__TO__PLUGIN_MOCK_DATA = 'workflowToPluginMockData',
/** mock set 页进入 mock data 页面 */
PLUGIN_MOCK_SET__TO__PLUGIN_MOCK_DATA = 'pluginMockSetToPluginMockData',
/** bot 详情页进入 knowledge 页面 */
BOT__VIEW__KNOWLEDGE = 'botViewKnowledge',
/** knowledge 页面点击退出返回 bot 详情页(未点击添加) */
KNOWLEDGE__BACK__BOT = 'knowledgeBackBot',
/** knowledge 页面点击返回 bot 详情页,并添加到 bot */
KNOWLEDGE__ADD_TO__BOT = 'knowledgeAddToBot',
/** bot 列表页进入bot 详情页,并查看发布结果 */
BOT_LIST__VIEW_PUBLISH_RESULT_IN__BOT_DETAIL = 'botListViewPublishResultInBotDetail',
/** bot 列表页进入bot 详情页,并查看发布结果 */
BOT_LIST__VIEW_PUBLISH_RESULT_IN__DOUYIN_DETAIL = 'botListViewPublishResultInDouyinDetail',
/** social scene 页面查看 workflow */
SOCIAL_SCENE__VIEW__WORKFLOW = 'socialSceneViewWorkflow',
/** social scene 详情页查看 workflow或新建 workflow 但未发布,点击返回 */
WORKFLOW__BACK__SOCIAL_SCENE = 'workflowBackSocialScene',
/** social scene 详情页创建或查看 workflow在 workflow 发布后返回 */
WORKFLOW_PUBLISHED__BACK__SOCIAL_SCENE = 'workflowPublishedBackSocialScene',
}
/* eslint-enable @typescript-eslint/naming-convention -- 恢复 enum 命名规范 */
// #endregion
// #region ---------------------- step 2. 将声明的场景枚举绑定至页面 ----------------------
/** 绑定某一页面可能包含的场景 */
export const PAGE_SCENE_MAP = {
[PageType.WORKFLOW]: [
SceneType.BOT__VIEW__WORKFLOW,
SceneType.SOCIAL_SCENE__VIEW__WORKFLOW,
SceneType.DOUYIN_BOT__VIEW__WORKFLOW,
],
[PageType.BOT]: [
SceneType.WORKFLOW__BACK__BOT,
SceneType.WORKFLOW_PUBLISHED__BACK__BOT,
SceneType.KNOWLEDGE__BACK__BOT,
SceneType.KNOWLEDGE__ADD_TO__BOT,
SceneType.BOT_LIST__VIEW_PUBLISH_RESULT_IN__BOT_DETAIL,
],
[PageType.DOUYIN_BOT]: [
SceneType.WORKFLOW__BACK__DOUYIN_BOT,
SceneType.WORKFLOW_PUBLISHED__BACK__DOUYIN_BOT,
SceneType.BOT_LIST__VIEW_PUBLISH_RESULT_IN__DOUYIN_DETAIL,
],
[PageType.PLUGIN_MOCK_DATA]: [
SceneType.BOT__TO__PLUGIN_MOCK_DATA,
SceneType.WORKFLOW__TO__PLUGIN_MOCK_DATA,
SceneType.PLUGIN_MOCK_SET__TO__PLUGIN_MOCK_DATA,
],
[PageType.KNOWLEDGE]: [SceneType.BOT__VIEW__KNOWLEDGE],
[PageType.SOCIAL_SCENE]: [
SceneType.WORKFLOW__BACK__SOCIAL_SCENE,
SceneType.WORKFLOW_PUBLISHED__BACK__SOCIAL_SCENE,
],
} satisfies Record<PageType, SceneType[]>;
// #endregion
// #region ---------------------- step 3. 声明新场景的参数类型 ----------------------
// 【参数(param)】表示从 A 页面跳转 B 页面时,需要 A 页面填写的数据。这份数据会作为 route state 传递
// B 页面会取得处理后的数据,称为响应值)
interface BotViewWorkflow {
spaceID: string;
workflowID: string;
botID?: string;
workflowModalState?: WorkflowModalState;
/** multi 模式下会有此项 */
agentID?: string;
/** @deprecated workflow弹窗打开模式默认和仅可添加一次 */
workflowOpenMode?: OpenModeType;
flowMode?: WorkflowMode;
/** 是否在新窗口打开 */
newWindow?: boolean;
/** 可选的工作流节点 ID */
workflowNodeID?: string;
/** 可选的工作流版本 */
workflowVersion?: string;
/** 可选的执行 ID */
executeID?: string;
/** 可选的子流程执行 ID */
subExecuteID?: string;
}
interface WorkflowBackBot {
spaceID: string;
botID: string;
workflowModalState?: WorkflowModalState;
/** multi 模式下会有此项 */
agentID?: string;
/** workflow弹窗打开模式默认和仅可添加一次 */
workflowOpenMode?: OpenModeType;
flowMode?: WorkflowMode;
}
interface WorkflowPulishedBackBot {
spaceID: string;
botID: string;
workflowID: string;
pluginID: string;
/** multi 模式下会有此项 */
agentID?: string;
/** workflow弹窗打开模式默认和仅可添加一次 */
workflowOpenMode?: OpenModeType;
flowMode?: WorkflowMode;
}
/** 绑定场景的参数类型 */
export type SceneParamTypeMap<T extends SceneType> = {
[SceneType.BOT__VIEW__WORKFLOW]: BotViewWorkflow;
[SceneType.DOUYIN_BOT__VIEW__WORKFLOW]: BotViewWorkflow;
[SceneType.WORKFLOW__BACK__BOT]: WorkflowBackBot;
[SceneType.WORKFLOW__BACK__DOUYIN_BOT]: WorkflowBackBot;
[SceneType.WORKFLOW_PUBLISHED__BACK__BOT]: WorkflowPulishedBackBot;
[SceneType.WORKFLOW_PUBLISHED__BACK__DOUYIN_BOT]: WorkflowPulishedBackBot;
[SceneType.BOT__TO__PLUGIN_MOCK_DATA]: {
spaceId: string;
pluginId: string;
pluginName?: string;
toolId: string;
toolName?: string;
mockSetId: string;
mockSetName?: string;
generationMode?: number;
bizCtx: string;
bindSubjectInfo: string;
};
[SceneType.WORKFLOW__TO__PLUGIN_MOCK_DATA]: {
spaceId: string;
pluginId: string;
pluginName?: string;
toolId: string;
toolName?: string;
mockSetId: string;
mockSetName?: string;
generationMode?: number;
bizCtx: string;
bindSubjectInfo: string;
};
[SceneType.PLUGIN_MOCK_SET__TO__PLUGIN_MOCK_DATA]: {
spaceId: string;
pluginId: string;
pluginName?: string;
toolId: string;
toolName?: string;
mockSetId: string;
mockSetName?: string;
generationMode?: number;
bizCtx?: string;
bindSubjectInfo?: string;
};
[SceneType.BOT__VIEW__KNOWLEDGE]: {
spaceID?: string;
botID?: string;
knowledgeID?: string;
};
[SceneType.KNOWLEDGE__BACK__BOT]: {
spaceID?: string;
botID?: string;
mode: 'bot' | 'douyin';
};
[SceneType.KNOWLEDGE__ADD_TO__BOT]: {
spaceID?: string;
botID?: string;
knowledgeID?: string;
};
[SceneType.BOT_LIST__VIEW_PUBLISH_RESULT_IN__BOT_DETAIL]: {
publishId: string;
commitVersion: string;
spaceId: string;
botId: string;
};
[SceneType.BOT_LIST__VIEW_PUBLISH_RESULT_IN__DOUYIN_DETAIL]: {
publishId: string;
commitVersion: string;
spaceId: string;
botId: string;
};
[SceneType.SOCIAL_SCENE__VIEW__WORKFLOW]: {
spaceID: string;
workflowID: string;
sceneID: string;
workflowModalState?: WorkflowModalState;
flowMode?: WorkflowMode;
/** 是否在新窗口打开 */
newWindow?: boolean;
};
[SceneType.WORKFLOW__BACK__SOCIAL_SCENE]: {
spaceID: string;
sceneID: string;
workflowModalState?: WorkflowModalState;
flowMode?: WorkflowMode;
};
[SceneType.WORKFLOW_PUBLISHED__BACK__SOCIAL_SCENE]: {
spaceID: string;
workflowID: string;
sceneID: string;
pluginID: string;
flowMode?: WorkflowMode;
};
}[T];
// #endregion
// #region ---------------------- step 4. 配置新场景的响应值 ----------------------
// 【响应值(response)】表示从 A 页面跳转 B 页面时B 页面获取的数据
// Q: 为什么 B 页面不能直接拿到 参数(param),还得转换一下?
// A: 1. route state 无法传递 不能 stringify 的参数,比如函数;
// 2. 静态配置response和动态配置param分离简化业务调用。
// 若未来这部分配置不断膨胀导致文件过长,则可以考虑进一步拆分文件
/** 绑定场景的响应值 */
export const SCENE_RESPONSE_MAP = {
// 临时修正类型推导问题,待有场景需要第二个参数 jump 时删掉此处 _
[SceneType.BOT__VIEW__WORKFLOW]: (params, _) => {
let url = `/work_flow?space_id=${params.spaceID}&workflow_id=${params.workflowID}`;
const queries = [
['bot_id', params.botID],
['node_id', params.workflowNodeID],
['version', params.workflowVersion],
['execute_id', params.executeID],
['sub_execute_id', params.subExecuteID],
];
queries.forEach(([key, value]) => {
if (value && value.length > 0) {
url += `&${key}=${value}`;
}
});
return {
url,
botID: params.botID,
workflowModalState: params.workflowModalState,
agentID: params.agentID,
workflowOpenMode: params.workflowOpenMode,
flowMode: params.flowMode,
};
},
[SceneType.DOUYIN_BOT__VIEW__WORKFLOW]: (params, _) => {
let url = `/work_flow?space_id=${params.spaceID}&workflow_id=${params.workflowID}`;
const queries = [
['bot_id', params.botID],
['node_id', params.workflowNodeID],
['version', params.workflowVersion],
['execute_id', params.executeID],
['sub_execute_id', params.subExecuteID],
];
queries.forEach(([key, value]) => {
if (value && value.length > 0) {
url += `&${key}=${value}`;
}
});
return {
url,
botID: params.botID,
workflowModalState: params.workflowModalState,
agentID: params.agentID,
workflowOpenMode: params.workflowOpenMode,
flowMode: params.flowMode,
};
},
[SceneType.WORKFLOW__BACK__BOT]: params => ({
url: `/space/${params.spaceID}/bot/${params.botID}`,
workflowModalState: params.workflowModalState,
agentID: params.agentID,
workflowOpenMode: params.workflowOpenMode,
flowMode: params.flowMode,
}),
[SceneType.WORKFLOW__BACK__DOUYIN_BOT]: params => ({
url: `/space/${params.spaceID}/douyin-bot/${params.botID}`,
workflowModalState: params.workflowModalState,
agentID: params.agentID,
workflowOpenMode: params.workflowOpenMode,
flowMode: params.flowMode,
}),
[SceneType.WORKFLOW_PUBLISHED__BACK__BOT]: params => ({
url: `/space/${params.spaceID}/bot/${params.botID}`,
workflowID: params.workflowID,
pluginID: params.pluginID,
agentID: params.agentID,
workflowOpenMode: params.workflowOpenMode,
flowMode: params.flowMode,
}),
[SceneType.WORKFLOW_PUBLISHED__BACK__DOUYIN_BOT]: params => ({
url: `/space/${params.spaceID}/douyin-bot/${params.botID}`,
workflowID: params.workflowID,
pluginID: params.pluginID,
agentID: params.agentID,
workflowOpenMode: params.workflowOpenMode,
flowMode: params.flowMode,
}),
[SceneType.BOT__TO__PLUGIN_MOCK_DATA]: params => {
const { spaceId, pluginId, toolId, mockSetId } = params;
return {
url: `/space/${spaceId}/plugin/${pluginId}/tool/${toolId}/plugin-mock-set/${mockSetId}?hideMenu=true`,
fromSource: 'bot',
...params,
};
},
[SceneType.WORKFLOW__TO__PLUGIN_MOCK_DATA]: params => {
const { spaceId, pluginId, toolId, mockSetId } = params;
return {
url: `/space/${spaceId}/plugin/${pluginId}/tool/${toolId}/plugin-mock-set/${mockSetId}?hideMenu=true&workflowPluginMockset=true`,
fromSource: 'workflow',
...params,
};
},
[SceneType.PLUGIN_MOCK_SET__TO__PLUGIN_MOCK_DATA]: params => {
const { spaceId, pluginId, toolId, mockSetId } = params;
return {
url: `/space/${spaceId}/plugin/${pluginId}/tool/${toolId}/plugin-mock-set/${mockSetId}`,
fromSource: 'mock_set',
back: undefined,
...params,
};
},
[SceneType.BOT__VIEW__KNOWLEDGE]: params => ({
url: `/space/${params.spaceID}/knowledge/${params.knowledgeID}?page_mode=modal&from=bot&bot_id=${params.botID}`,
botID: params.botID,
}),
[SceneType.KNOWLEDGE__BACK__BOT]: params => ({
url: `/space/${params.spaceID}/${params.mode === 'bot' ? 'bot' : 'douyin-bot'}/${params.botID}`,
}),
[SceneType.KNOWLEDGE__ADD_TO__BOT]: params => ({
url: `/space/${params.spaceID}/bot/${params.botID}`,
knowledgeID: params.knowledgeID,
}),
[SceneType.BOT_LIST__VIEW_PUBLISH_RESULT_IN__BOT_DETAIL]: params => ({
url: `/space/${params.spaceId}/bot/${params.botId}`,
publishId: params.publishId,
commitVersion: params.commitVersion,
}),
[SceneType.BOT_LIST__VIEW_PUBLISH_RESULT_IN__DOUYIN_DETAIL]: params => ({
url: `/space/${params.spaceId}/douyin-bot/${params.botId}`,
publishId: params.publishId,
commitVersion: params.commitVersion,
}),
[SceneType.SOCIAL_SCENE__VIEW__WORKFLOW]: (params, _) => ({
url: `/work_flow?scene_id=${params.sceneID}&space_id=${params.spaceID}&workflow_id=${params.workflowID}`,
sceneID: params.sceneID,
workflowModalState: params.workflowModalState,
flowMode: params.flowMode,
}),
[SceneType.WORKFLOW_PUBLISHED__BACK__SOCIAL_SCENE]: (params, _) => ({
url: `/space/${params.spaceID}/social-scene/${params.sceneID}`,
workflowID: params.workflowID,
pluginID: params.pluginID,
flowMode: params.flowMode,
}),
[SceneType.WORKFLOW__BACK__SOCIAL_SCENE]: params => ({
url: `/space/${params.spaceID}/social-scene/${params.sceneID}`,
workflowModalState: params.workflowModalState,
flowMode: params.flowMode,
}),
} satisfies SceneResponseConstraint;
// #endregion
// #region ---------------------- 业务方无需关注的部分 ----------------------
/**
* 辅助类型
*
* 该类型实现以下几件事:
* 1. 检查 SceneType 是否被遍历,有遗漏则会报错
* 2. 为回调方法注入参数类型
* 3. 约束响应值必须包含某些字段(比如 url否则报错
* 4. 正确推导响应值的具体类型,便于后续使用
*/
type SceneResponseConstraint = {
[K in SceneType]: (
param: SceneParamTypeMap<K>,
jump: PageJumpExecFunc,
) => {
url: string;
};
};
// #endregion

View File

@@ -0,0 +1,161 @@
/*
* 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 { isNil } from 'lodash-es';
import { useLocation, useNavigate } from 'react-router-dom';
import {
PAGE_SCENE_MAP,
type PageType,
SCENE_RESPONSE_MAP,
type SceneType,
type SceneParamTypeMap,
} from './config';
export { PageType, SceneType };
/**
* 页面跳转 hook
*
* @example
* const pageJump = usePageJump();
*
* pageJump.jump(SceneType.BOT_CREATE_WORKFLOW, { ...param })
*/
export function usePageJumpService(): {
jump: PageJumpExecFunc;
} {
const navigate = useNavigate();
return {
jump: <T extends SceneType>(sceneType: T, param?: SceneParamTypeMap<T>) => {
// eslint-disable-next-line max-len -- eslint 注释格式限制,不得不超出 max-len
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-function -- 1.内部类型难以推导,不影响外侧类型约束和推导 2.只是获取 url不会使用第二个参数空函数仅用于解决类型错误不影响使用更不影响调用侧类型约束和推导
const { url } = SCENE_RESPONSE_MAP[sceneType](param as any, () => {});
if (!url) {
return console.error('page jump error: no url provided');
}
if (
(param as SceneParamTypeMap<SceneType.BOT__VIEW__WORKFLOW>)?.newWindow
) {
window.open(url, '_blank');
} else {
navigate(url, { state: { ...param, scene: sceneType } });
}
},
};
}
/**
* 获取当前页面的响应值
*
* 如果当前页面可能有多种场景,那么返回值将是这些场景响应值的 union需要在业务代码中根据 `scene` 来做 type narrowing
*
* 当没接收到场景值 或接收到的场景值与页面不匹配时,返回 null
*
* 注意:即使页面刷新后也会保留该响应值,若不希望刷新后也保留,需要调用 clearScene() 方法
*
* @example
* const routeResponse = usePageResponse(PageType.WORKFLOW);
* // 此时只知道是 workflow 页面,但场景可能是 查看或创建
* if (routeResponse.scene === SceneType.BOT_CREATE_WORKFLOW) {
* // 此时 routeResponse 能被推导为 BOT_CREATE_WORKFLOW 场景的响应值
* }
*/
export function usePageJumpResponse<P extends PageType>(
pageType: P,
): SceneResponseType<PageSceneUnion<P>> | null {
const { jump } = usePageJumpService();
const navigate = useNavigate();
const location = useLocation();
const validScenes = PAGE_SCENE_MAP[pageType];
const param: SceneParamTypeMap<(typeof validScenes)[number]> & {
scene: (typeof validScenes)[number];
} = location.state;
if (isNil(param?.scene)) {
return null;
}
if (!(validScenes as SceneType[]).includes(param?.scene)) {
// route state 传来的场景枚举值,并不存在于调用方声明的页面中
console.error(
"got wrong route state: this page doesn't have the scene passed by route param",
);
return null;
}
return {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 内部类型难以推导,不影响调用侧类型推导
...SCENE_RESPONSE_MAP[param.scene](param as any, jump),
scene: param.scene,
clearScene: (forceRerender = false) => {
if (forceRerender) {
// 清除 history.state 之后 rerenderuseLocation 依然能取到清除前的值,应该是 react-router-dom 做了缓存
// 搜索发现做一次 replace navigate 可解,且测试发现并不会导致组件重新挂载,只会 rerender
navigate(location.pathname, { replace: true });
return;
}
history.replaceState({}, '');
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 内部类型难以推导,不影响调用侧类型推导
} as any;
}
/**
* usePageJumpResponse().jump 的类型
*
* 因为要复用,所以单独声明一下
*/
export interface PageJumpExecFunc {
/**
* @param sceneType 场景
* @param param 用户输入场景后,能推导出对应的 param 类型作为约束,若该场景无参数,则可不传 param
*/
<T extends SceneWithNoParam>(sceneType: T): void;
// eslint-disable-next-line @typescript-eslint/unified-signatures -- 报错有问题,不应该合并声明
<T extends SceneType>(sceneType: T, param: SceneParamTypeMap<T>): void;
}
/** 返回 P 页面下可能的场景 */
type PageSceneUnion<P extends PageType> = (typeof PAGE_SCENE_MAP)[P][number];
/**
* 获取场景的响应值类型
*
* 利用 distributive condition 特性,将返回的类型拆分为 union
* 以便业务中利用 discriminated union 特性通过判断 scene 来实现 type narrowing
*/
export type SceneResponseType<T extends SceneType> = T extends SceneType
? Omit<ReturnType<(typeof SCENE_RESPONSE_MAP)[T]>, 'url'> & {
scene: T;
/**
* 清除当前页面绑定的一切跳转数据
* @param forceRefresh 是否即时清空。
* 默认需要刷新才能清空(由于 react-router-dom 的原因,即使 rerender 也会获取到清空前的响应值);
* 传 true 时会调用一次 replace navigate触发 rerender 并且不会再获取到响应值(不会触发组件 unmount
*/
clearScene: (forceRefresh?: boolean) => void;
}
: never;
/** 筛选出没有参数的场景 */
type SceneWithNoParam = SceneType extends infer P
? P extends SceneType
? SceneParamTypeMap<P> extends undefined
? P
: never
: never
: never;