├── src ├── app │ ├── (main) │ │ ├── components │ │ │ ├── SideNav │ │ │ │ ├── index.ts │ │ │ │ ├── SideNav.tsx │ │ │ │ └── NavMenu.tsx │ │ │ ├── FooterBar │ │ │ │ ├── index.ts │ │ │ │ └── FooterBar.tsx │ │ │ └── PanelLayout.tsx │ │ ├── layout.tsx │ │ ├── settings │ │ │ └── page.tsx │ │ └── home │ │ │ └── page.tsx │ ├── page.tsx │ ├── layout.tsx │ └── test │ │ └── page.tsx ├── constants.ts ├── components │ ├── ApiTab │ │ ├── index.ts │ │ ├── ApiTab.enum.ts │ │ ├── ApiTabContentWrapper.tsx │ │ ├── ApiTab.type.ts │ │ ├── TabContentContext.tsx │ │ ├── ApiTabContent.tsx │ │ ├── ApiTabAction.tsx │ │ └── ApiTabLabel.tsx │ ├── MonacoEditor │ │ ├── index.ts │ │ └── MonacoEditor.tsx │ ├── ApiMenu │ │ ├── index.ts │ │ ├── AppMenuControls.tsx │ │ ├── FileAction.tsx │ │ ├── SwitcherIcon.tsx │ │ ├── MenuActionButton.tsx │ │ ├── FolderAction.tsx │ │ ├── ApiMenu.type.ts │ │ ├── ApiMenuContext.tsx │ │ └── ApiMenuTitle.tsx │ ├── JsonSchema │ │ ├── index.ts │ │ ├── JsonSchemaNodeWrapper.tsx │ │ ├── JsonSchema.context.tsx │ │ ├── JsonSchema.type.ts │ │ ├── constants.ts │ │ ├── utils.ts │ │ └── JsonSchemaEditor.tsx │ ├── ThemeEditor │ │ ├── index.tsx │ │ ├── ThemeEditor.type.ts │ │ ├── ThemeEditor.helper.ts │ │ ├── ThemePicker.tsx │ │ ├── ThemeRadiusPicker.tsx │ │ ├── ThemeColorPicker.tsx │ │ ├── ThemeEditor.tsx │ │ └── ThemeContext.tsx │ ├── tab-content │ │ ├── api │ │ │ ├── components │ │ │ │ ├── GroupTitle.tsx │ │ │ │ ├── ParamsEditableCell.tsx │ │ │ │ ├── InputDesc.tsx │ │ │ │ ├── PathInput.tsx │ │ │ │ └── BaseFormItems.tsx │ │ │ ├── ApiRemoveButton.tsx │ │ │ ├── ApiSidePanel.tsx │ │ │ ├── ModalNewResponse.tsx │ │ │ ├── params │ │ │ │ ├── ParamsBody.tsx │ │ │ │ └── ParamsTab.tsx │ │ │ └── Api.tsx │ │ ├── folder │ │ │ ├── Folder.tsx │ │ │ ├── FolderApiList.tsx │ │ │ └── FolderSetting.tsx │ │ ├── Overview.tsx │ │ ├── Blank.tsx │ │ ├── Schema.tsx │ │ ├── Recycle.tsx │ │ └── Doc.tsx │ ├── IconText.tsx │ ├── JsonViewer.tsx │ ├── icons │ │ ├── HttpMethodText.tsx │ │ ├── FolderIcon.tsx │ │ ├── FileIcon.tsx │ │ └── IconLogo.tsx │ ├── SelectorService.tsx │ ├── UIBtn.tsx │ ├── InputUnderline.tsx │ ├── AntdStyleProvider.tsx │ ├── InputSearch.tsx │ ├── modals │ │ ├── ModalRename.tsx │ │ ├── ModalMoveMenu.tsx │ │ ├── ModalNewCatalog.tsx │ │ └── ModalSettings.tsx │ ├── HeaderNav.tsx │ ├── DoubleCheckRemoveBtn.tsx │ ├── SelectorCatalog.tsx │ ├── MarkdownEditor.tsx │ ├── EditableTable备份.tsx │ └── DataTypeSelect.tsx ├── hooks │ ├── useCssVariable.tsx │ ├── useStyle.ts │ ├── useCatalog.ts │ └── useHelpers.ts ├── contexts │ ├── layout-settings.tsx │ ├── global.tsx │ └── menu-tab-settings.tsx ├── utils.ts ├── enums.ts ├── types.ts ├── helpers.ts ├── styles │ └── globals.css └── configs │ └── static.ts ├── .vscode └── settings.json ├── next.config.mjs ├── stylelint.config.mjs ├── postcss.config.cjs ├── public ├── manifest.webmanifest └── favicon.svg ├── eslint.config.mjs ├── tailwind.config.ts ├── .gitignore ├── README.md ├── tsconfig.json └── package.json /src/app/(main)/components/SideNav/index.ts: -------------------------------------------------------------------------------- 1 | export { SideNav } from './SideNav' 2 | -------------------------------------------------------------------------------- /src/app/(main)/components/FooterBar/index.ts: -------------------------------------------------------------------------------- 1 | export { FooterBar } from './FooterBar' 2 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const GitHubRepo = 'https://github.com/Codennnn/Apifox-UI' 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.preferences.importModuleSpecifier": "non-relative" 3 | } 4 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | 3 | export default { 4 | reactStrictMode: true, 5 | } 6 | -------------------------------------------------------------------------------- /src/components/ApiTab/index.ts: -------------------------------------------------------------------------------- 1 | export { ApiTab } from './ApiTab' 2 | export type { ApiTabItem, EditStatus } from './ApiTab.type' 3 | -------------------------------------------------------------------------------- /src/components/MonacoEditor/index.ts: -------------------------------------------------------------------------------- 1 | export { MonacoEditor, type MonacoEditorProps, type MonacoEditorRef } from './MonacoEditor' 2 | -------------------------------------------------------------------------------- /stylelint.config.mjs: -------------------------------------------------------------------------------- 1 | import stylelintPreset from 'prefer-code-style/stylelint' 2 | 3 | export default { 4 | extends: [stylelintPreset], 5 | } 6 | -------------------------------------------------------------------------------- /src/components/ApiMenu/index.ts: -------------------------------------------------------------------------------- 1 | export { ApiMenu } from './ApiMenu' 2 | export type { ApiMenuData, CatalogDataNode, CatalogId } from './ApiMenu.type' 3 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'tailwindcss/nesting': {}, 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /src/components/ApiTab/ApiTab.enum.ts: -------------------------------------------------------------------------------- 1 | export const enum PageTabStatus { 2 | /** 已修改,但未更新到数据库 */ 3 | Update, 4 | /** 新创建,且未保存到数据库 */ 5 | Create, 6 | } 7 | -------------------------------------------------------------------------------- /src/components/JsonSchema/index.ts: -------------------------------------------------------------------------------- 1 | export { SchemaType } from './constants' 2 | export type { JsonSchema } from './JsonSchema.type' 3 | export { JsonSchemaEditor, type JsonSchemaEditorProps } from './JsonSchemaEditor' 4 | -------------------------------------------------------------------------------- /src/components/ThemeEditor/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | export { ThemeProvider, ThemeProviderClient, useThemeContext } from './ThemeContext' 4 | export { ThemeEditor } from './ThemeEditor' 5 | export type { ThemeSetting } from './ThemeEditor.type' 6 | -------------------------------------------------------------------------------- /src/components/ApiMenu/AppMenuControls.tsx: -------------------------------------------------------------------------------- 1 | export function AppMenuControls(props: React.PropsWithChildren) { 2 | return ( 3 | 4 | {props.children} 5 | 6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /public/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "xxx", 3 | "name": "xxx", 4 | "description": "xxx", 5 | "theme_color": "#334155", 6 | "icons": [ 7 | { 8 | "src": "/favicon.svg", 9 | "type": "image/svg+xml", 10 | "sizes": "256x256" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import { redirect } from 'next/navigation' 3 | 4 | import { getPageTitle } from '../utils' 5 | 6 | export const metadata: Metadata = { 7 | title: getPageTitle(), 8 | } 9 | 10 | export default function RootPage() { 11 | return redirect('/home') 12 | } 13 | -------------------------------------------------------------------------------- /src/hooks/useCssVariable.tsx: -------------------------------------------------------------------------------- 1 | import { theme } from 'antd' 2 | 3 | export function useCssVariable(): React.CSSProperties { 4 | const { token } = theme.useToken() 5 | 6 | return { 7 | '--ui-tabs-hover-color': token.colorTextBase, 8 | '--ui-tabs-hover-bg': token.colorFillContent, 9 | } as React.CSSProperties 10 | } 11 | -------------------------------------------------------------------------------- /src/components/tab-content/api/components/GroupTitle.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from 'antd' 2 | 3 | export function GroupTitle(props: React.PropsWithChildren<{ className?: string }>) { 4 | return ( 5 |
6 | {props.children} 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/components/IconText.tsx: -------------------------------------------------------------------------------- 1 | interface IconTextProps { 2 | icon?: React.ReactNode 3 | text?: string 4 | } 5 | 6 | export function IconText({ icon, text }: IconTextProps) { 7 | return ( 8 | 9 | {icon} 10 | {text ? {text} : null} 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/components/ThemeEditor/ThemeEditor.type.ts: -------------------------------------------------------------------------------- 1 | import type { GlobalToken } from 'antd' 2 | 3 | export type ThemeMode = 'lightDefault' | 'darkDefault' | 'lark' 4 | 5 | export interface ThemeSetting { 6 | themeMode: ThemeMode 7 | colorPrimary: GlobalToken['colorPrimary'] 8 | borderRadius: GlobalToken['borderRadius'] 9 | spaceType: 'default' | 'compact' 10 | } 11 | -------------------------------------------------------------------------------- /src/hooks/useStyle.ts: -------------------------------------------------------------------------------- 1 | import { theme } from 'antd' 2 | 3 | import { css } from '@emotion/css' 4 | 5 | type Theme = ReturnType 6 | 7 | type StyleFunction = (theme: Theme, cssFn: typeof css) => T 8 | 9 | export function useStyles(fn: StyleFunction): { styles: ReturnType> } { 10 | return { styles: fn(theme.useToken(), css) } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/JsonViewer.tsx: -------------------------------------------------------------------------------- 1 | import JsonView from 'react18-json-view' 2 | 3 | import 'react18-json-view/src/style.css' 4 | 5 | interface JsonViewerProps { 6 | value?: string 7 | } 8 | 9 | export function JsonViewer(props: JsonViewerProps) { 10 | const { value } = props 11 | 12 | if (!value) { 13 | return null 14 | } 15 | 16 | return 17 | } 18 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import nextPreset from 'prefer-code-style/eslint/preset/next' 2 | 3 | export default [ 4 | ...nextPreset, 5 | 6 | { 7 | rules: { 8 | '@typescript-eslint/no-unsafe-assignment': 0, 9 | }, 10 | }, 11 | 12 | { 13 | settings: { 14 | tailwindcss: { 15 | whitelist: ['ant-tree-switcher-icon', 'ui-menu-controls', 'ui-tabs-tab-label'], 16 | }, 17 | }, 18 | }, 19 | ] 20 | -------------------------------------------------------------------------------- /src/components/JsonSchema/JsonSchemaNodeWrapper.tsx: -------------------------------------------------------------------------------- 1 | export function JsonSchemaNodeWrapper( 2 | props: React.PropsWithChildren<{ 3 | className?: string 4 | shouldExpand?: boolean 5 | }>, 6 | ) { 7 | const { children, className = '', shouldExpand } = props 8 | 9 | return ( 10 |
11 | {children} 12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/components/ApiTab/ApiTabContentWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { ConfigProvider, theme } from 'antd' 2 | 3 | export function ApiTabContentWrapper(props: React.PropsWithChildren<{ className?: string }>) { 4 | const { token } = theme.useToken() 5 | 6 | return ( 7 | 16 |
{props.children}
17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/ApiTab/ApiTab.type.ts: -------------------------------------------------------------------------------- 1 | import type { TabsProps } from 'antd' 2 | 3 | import type { TabContentType } from '@/types' 4 | 5 | import type { PageTabStatus } from './ApiTab.enum' 6 | 7 | export type EditStatus = 'changed' | 'saved' 8 | 9 | export type Tab = NonNullable[0] 10 | 11 | export interface ApiTabItem extends Pick { 12 | /** 页签内容类型。 */ 13 | contentType: TabContentType 14 | /** 页签附加数据。 */ 15 | data?: Record & { 16 | editStatus?: EditStatus 17 | tabStatus?: PageTabStatus 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | export default { 4 | content: ['./src/{app,components}/**/*.{js,jsx,ts,tsx}'], 5 | 6 | theme: { 7 | extend: { 8 | colors: {}, 9 | 10 | padding: { 11 | layoutHeader: 'var(--layout-header-height)', 12 | main: 'var(--p-main)', 13 | tabContent: 'var(--p-tab-content)', 14 | }, 15 | 16 | margin: { 17 | tabContent: 'var(--p-tab-content)', 18 | }, 19 | }, 20 | }, 21 | 22 | corePlugins: { 23 | preflight: false, 24 | }, 25 | } satisfies Config 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | next-env.d.ts 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | .pnpm-debug.log* 28 | 29 | # local env files 30 | .env.local 31 | .env.development.local 32 | .env.test.local 33 | .env.production.local 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | -------------------------------------------------------------------------------- /src/components/ApiTab/TabContentContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react' 2 | 3 | import type { ApiTabItem } from './ApiTab.type' 4 | 5 | interface TabContentContextData { 6 | tabData: ApiTabItem 7 | } 8 | 9 | const TabContentContext = createContext({} as TabContentContextData) 10 | 11 | export function TabContentProvider( 12 | props: React.PropsWithChildren>, 13 | ) { 14 | const { children, tabData } = props 15 | 16 | return {children} 17 | } 18 | 19 | export const useTabContentContext = () => useContext(TabContentContext) 20 | -------------------------------------------------------------------------------- /src/components/ThemeEditor/ThemeEditor.helper.ts: -------------------------------------------------------------------------------- 1 | import { defaultThemeSetting } from './theme-data' 2 | import type { ThemeSetting } from './ThemeEditor.type' 3 | 4 | export const storeThemeSetting = (autoSaveId: string, newThemeSetting: ThemeSetting): void => { 5 | window.localStorage.setItem(autoSaveId, JSON.stringify(newThemeSetting)) 6 | } 7 | 8 | export const restoreThemeSetting = (autoSaveId: string | undefined): ThemeSetting => { 9 | if (autoSaveId) { 10 | const storage = window.localStorage.getItem(autoSaveId) 11 | 12 | if (storage) { 13 | return JSON.parse(storage) as ThemeSetting 14 | } 15 | } 16 | 17 | return defaultThemeSetting 18 | } 19 | -------------------------------------------------------------------------------- /src/app/(main)/components/SideNav/SideNav.tsx: -------------------------------------------------------------------------------- 1 | import { theme } from 'antd' 2 | 3 | import { IconLogo } from '@/components/icons/IconLogo' 4 | 5 | import { NavMenu } from './NavMenu' 6 | 7 | export function SideNav() { 8 | const { token } = theme.useToken() 9 | 10 | return ( 11 |
12 |
16 | 17 |
18 | 19 | 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/components/ApiMenu/FileAction.tsx: -------------------------------------------------------------------------------- 1 | import { MoreHorizontalIcon } from 'lucide-react' 2 | 3 | import { DropdownActions } from '@/components/ApiMenu/DropdownActions' 4 | 5 | import type { ApiMenuData } from './ApiMenu.type' 6 | import { MenuActionButton } from './MenuActionButton' 7 | 8 | /** 9 | * 菜单项的文件操作。 10 | */ 11 | export function FileAction(props: { catalog: ApiMenuData }) { 12 | const { catalog } = props 13 | 14 | return ( 15 | 16 | } 18 | onClick={(ev) => { 19 | ev.stopPropagation() 20 | }} 21 | /> 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/components/icons/HttpMethodText.tsx: -------------------------------------------------------------------------------- 1 | import { HTTP_METHOD_CONFIG } from '@/configs/static' 2 | import type { HttpMethod } from '@/enums' 3 | 4 | interface HttpIconProps { 5 | method?: HttpMethod 6 | className?: string 7 | text?: string 8 | } 9 | 10 | export function HttpMethodText({ method, className = '', text }: HttpIconProps) { 11 | if (method) { 12 | try { 13 | const httpMethod = HTTP_METHOD_CONFIG[method] 14 | 15 | return ( 16 | 17 | {text ?? httpMethod.text} 18 | 19 | ) 20 | } 21 | catch { 22 | return null 23 | } 24 | } 25 | 26 | return null 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!CAUTION] 2 | > 该项目仍处于开发阶段,这表示: 3 | > 4 | > - **功能不完整:** 当前缺少一些关键功能,正在努力开发中。 5 | > - **代码变动:** 随着进一步开发,代码结构会有较大的变动。 6 | > - **文档暂缺:** 还没来得及编写完整的文档,如果您在使用中遇到问题,请通过 issue 反馈。 7 | 8 | # Apifox UI 9 | 10 | ## 介绍 11 | 12 | 这是一个精心仿制 Apifox 界面的纯前端项目,使用 Next + Antd + TypeScript + TailwindCSS 开发,源码融入了很多好的编码实践,能让你学习到如何组织和建设一个复杂的 React 项目,非常适合 React 新手学习! 13 | 14 | ![Apifox UI 界面展示](https://i.imgur.com/8UmNM9c.png) 15 | 16 | ## 动机 17 | 18 | 在日常工作中,我经常会使用 Antd 来构建页面,但大多数页面的结构和交互都是比较简单的。为了精进对 Next + Antd 的使用技巧,我选择了 Apifox 这个相对复杂的界面进行模仿,希望在实践中能够掌握使用 Antd 打造出高级的页面效果。 19 | 20 | 可能有很多小伙伴也抱有类似的学习动机,所以我将代码开源出来,希望能帮助各位,感兴趣的话不妨到点个 star⭐ 收藏一下噢~ 21 | 22 | ## 本地启动 23 | 24 | ```sh 25 | pnpm i # 安装项目依赖 26 | 27 | pnpm dev # 启动本地服务 28 | ``` 29 | -------------------------------------------------------------------------------- /src/components/JsonSchema/JsonSchema.context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react' 2 | 3 | import type { JsonSchemaEditorProps } from './JsonSchemaEditor' 4 | 5 | type JsonSchemaContextData = Pick< 6 | JsonSchemaEditorProps, 7 | 'readOnly' | 'expandedKeys' | 'onExpand' | 'extraColumns' 8 | > 9 | 10 | const JsonSchemaContext = createContext({} as JsonSchemaContextData) 11 | 12 | export function JsonSchemaContextProvider( 13 | props: React.PropsWithChildren<{ value: JsonSchemaContextData }>, 14 | ) { 15 | return ( 16 | {props.children} 17 | ) 18 | } 19 | 20 | export const useJsonSchemaContext = () => useContext(JsonSchemaContext) 21 | -------------------------------------------------------------------------------- /src/components/tab-content/api/ApiRemoveButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Popconfirm } from 'antd' 2 | 3 | import { useMenuHelpersContext } from '@/contexts/menu-helpers' 4 | import { useMenuTabHelpers } from '@/contexts/menu-tab-settings' 5 | 6 | export function ApiRemoveButton(props: { tabKey: string }) { 7 | const { tabKey } = props 8 | 9 | const { removeMenuItem } = useMenuHelpersContext() 10 | const { removeTabItem } = useMenuTabHelpers() 11 | 12 | return ( 13 | { 17 | removeTabItem({ key: tabKey }) 18 | removeMenuItem({ id: tabKey }) 19 | }} 20 | > 21 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/components/ApiMenu/SwitcherIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ChevronDownIcon } from 'lucide-react' 2 | 3 | import { useStyles } from '@/hooks/useStyle' 4 | 5 | import { css } from '@emotion/css' 6 | 7 | export function SwitcherIcon(props: Pick, 'onClick'>) { 8 | const { onClick } = props 9 | 10 | const { styles } = useStyles(({ token }) => ({ 11 | icon: css({ 12 | borderRadius: token.borderRadiusOuter, 13 | 14 | '&:hover': { 15 | backgroundColor: token.colorFillSecondary, 16 | }, 17 | }), 18 | })) 19 | 20 | return ( 21 | 25 | 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/components/icons/FolderIcon.tsx: -------------------------------------------------------------------------------- 1 | import { LayoutIcon, TrashIcon, UnplugIcon } from 'lucide-react' 2 | 3 | import { CatalogType } from '@/enums' 4 | 5 | import { FileIcon, type FileIconProps } from './FileIcon' 6 | 7 | type FolderIconProps = FileIconProps 8 | 9 | /** 10 | * 菜单目录文件夹的图标。 11 | */ 12 | export function FolderIcon(props: FolderIconProps) { 13 | const { type, size = 16, className, style } = props 14 | 15 | const iconProps: Pick = { size, className, style } 16 | 17 | switch (type) { 18 | case CatalogType.Overview: 19 | return 20 | 21 | case CatalogType.Http: 22 | return 23 | 24 | case CatalogType.Recycle: 25 | return 26 | 27 | default: 28 | return 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/contexts/layout-settings.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useRef, useState } from 'react' 2 | import type { ImperativePanelHandle } from 'react-resizable-panels' 3 | 4 | interface LayoutContextData { 5 | isSideMenuCollapsed: boolean 6 | setIsSideMenuCollapsed: React.Dispatch< 7 | React.SetStateAction 8 | > 9 | panelRef: React.RefObject 10 | } 11 | 12 | const LayoutContext = createContext({} as LayoutContextData) 13 | 14 | export function LayoutProvider(props: React.PropsWithChildren) { 15 | const panelRef = useRef(null) 16 | 17 | const [isSideMenuCollapsed, setIsSideMenuCollapsed] = useState(false) 18 | 19 | return ( 20 | 21 | {props.children} 22 | 23 | ) 24 | } 25 | 26 | export const useLayoutContext = () => useContext(LayoutContext) 27 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { AnyType, UnsafeAny } from '@/types' 2 | 3 | export function getPageTitle(title?: string): string { 4 | const mainTitle = 'Apifox UI' 5 | 6 | return title ? `${title} - ${mainTitle}` : mainTitle 7 | } 8 | 9 | /** 将 JS 序列化为 JSON 的超集,包括正则表达式,日期和函数。 */ 10 | export { default as serialize } from 'serialize-javascript' 11 | 12 | /** 反序列化,对应 serialize 方法。 */ 13 | export function deserialize(data: AnyType): unknown { 14 | // eslint-disable-next-line @typescript-eslint/no-implied-eval, @typescript-eslint/restrict-plus-operands 15 | return Function('"use strict";return (' + data + ')')() 16 | } 17 | 18 | /** 检查传入的值是否为简单的 JS 对象。 */ 19 | export function isPureObject(value: AnyType): value is Record { 20 | return Object.prototype.toString.call(value) === '[object Object]' 21 | } 22 | 23 | /** 移动数组元素。 */ 24 | export function moveArrayItem(arr: T[], fromIndex: number, toIndex: number) { 25 | // 先删除原位置上的元素。 26 | const element = arr.splice(fromIndex, 1)[0] 27 | 28 | // 然后在指定位置插入该元素。 29 | arr.splice(toIndex, 0, element) 30 | } 31 | -------------------------------------------------------------------------------- /src/components/tab-content/api/components/ParamsEditableCell.tsx: -------------------------------------------------------------------------------- 1 | import { useStyles } from '@/hooks/useStyle' 2 | 3 | import { css } from '@emotion/css' 4 | 5 | interface ParamsEditableCelllProps 6 | extends React.PropsWithChildren, 7 | Pick, 'className'> { 8 | validateError?: boolean 9 | } 10 | 11 | export function ParamsEditableCell(props: ParamsEditableCelllProps) { 12 | const { children, className = '', validateError } = props 13 | 14 | const { styles } = useStyles(({ token }) => { 15 | const editableCell = css({ 16 | height: '100%', 17 | minHeight: '32px', 18 | outline: '1px solid', 19 | outlineColor: validateError ? token.colorErrorText : 'transparent', 20 | 21 | '&:hover, &:focus-within': { 22 | outlineColor: validateError ? token.colorErrorText : token.colorPrimary, 23 | borderColor: 'transparent', 24 | }, 25 | }) 26 | 27 | return { editableCell } 28 | }) 29 | 30 | return
{children}
31 | } 32 | -------------------------------------------------------------------------------- /src/components/SelectorService.tsx: -------------------------------------------------------------------------------- 1 | import { Select, type SelectProps } from 'antd' 2 | 3 | const serviceOptions: SelectProps['options'] = [ 4 | { 5 | label: '默认设置', 6 | options: [ 7 | { 8 | label: ( 9 | 10 | 继承父级 11 | 跟随父级目录设置(推荐) 12 | 13 | ), 14 | value: '', 15 | }, 16 | ], 17 | }, 18 | { 19 | label: '手动指定', 20 | options: [ 21 | { 22 | label: ( 23 | 24 | 默认服务 25 | 26 | ), 27 | value: 'default', 28 | }, 29 | ], 30 | }, 31 | ] 32 | 33 | interface SelectorServiceProps { 34 | value?: string 35 | onChange?: (value: SelectorServiceProps['value']) => void 36 | } 37 | 38 | export function SelectorService(props: SelectorServiceProps) { 39 | return 42 | 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/components/AntdStyleProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from 'react' 4 | 5 | import { useServerInsertedHTML } from 'next/navigation' 6 | import { createCache, extractStyle, StyleProvider } from '@ant-design/cssinjs' 7 | 8 | /** 9 | * 这部分的代码参考自:https://github.com/ant-design/ant-design/issues/38555#issuecomment-1571203559 10 | */ 11 | function AntdStyleRegister(props: React.PropsWithChildren) { 12 | const [cache] = useState(() => createCache()) 13 | 14 | useServerInsertedHTML(() => { 15 | return ( 16 |