├── 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 | 
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
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/tab-content/folder/Folder.tsx:
--------------------------------------------------------------------------------
1 | import { Tabs } from 'antd'
2 |
3 | import { ApiTabContentWrapper } from '@/components/ApiTab/ApiTabContentWrapper'
4 |
5 | import { FolderApiList } from './FolderApiList'
6 | import { FolderSetting } from './FolderSetting'
7 |
8 | export function Folder() {
9 | return (
10 |
11 |
20 |
21 |
22 | ),
23 | },
24 | {
25 | key: 'apis',
26 | label: '全部接口',
27 | children: (
28 |
29 |
30 |
31 | ),
32 | },
33 | ]}
34 | />
35 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/UIBtn.tsx:
--------------------------------------------------------------------------------
1 | import { useStyles } from '@/hooks/useStyle'
2 |
3 | import { css } from '@emotion/css'
4 |
5 | interface UIBtnProps extends React.PropsWithChildren, React.ComponentProps<'button'> {
6 | primary?: boolean
7 | }
8 |
9 | export function UIButton(props: UIBtnProps) {
10 | const { children, primary, className = '', ...rest } = props
11 |
12 | const { styles } = useStyles(({ token }) => ({
13 | btn: css({
14 | padding: `${token.paddingXXS}px ${token.paddingXS}px`,
15 | backgroundColor: primary ? token.colorPrimaryBg : token.colorFillTertiary,
16 | borderRadius: token.borderRadiusSM,
17 | color: primary ? token.colorPrimary : token.colorTextSecondary,
18 |
19 | '&:hover': {
20 | backgroundColor: primary ? token.colorPrimaryBg : token.colorFillSecondary,
21 | },
22 | }),
23 | }))
24 |
25 | return (
26 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/ApiMenu/MenuActionButton.tsx:
--------------------------------------------------------------------------------
1 | import { forwardRef } from 'react'
2 |
3 | import { theme } from 'antd'
4 |
5 | import { useStyles } from '@/hooks/useStyle'
6 | import { UnsafeAny } from '@/types'
7 |
8 | import { css } from '@emotion/css'
9 |
10 | interface MenuActionButtonProps extends React.ComponentProps<'span'> {
11 | icon?: React.ReactNode
12 | }
13 |
14 | export const MenuActionButton = forwardRef(
15 | function MenuActionButton(props, ref) {
16 | const { token } = theme.useToken()
17 |
18 | const { icon, className = '', style, ...restSpanProps } = props
19 |
20 | const { styles } = useStyles(({ token }) => ({
21 | icon: css({
22 | '&:hover': {
23 | backgroundColor: token.colorFillSecondary,
24 | },
25 | }),
26 | }))
27 |
28 | return (
29 |
35 | {icon}
36 |
37 | )
38 | },
39 | )
40 |
--------------------------------------------------------------------------------
/src/components/InputUnderline.tsx:
--------------------------------------------------------------------------------
1 | import { ConfigProvider, Input, type InputProps, theme } from 'antd'
2 |
3 | import { useStyles } from '@/hooks/useStyle'
4 |
5 | import { css } from '@emotion/css'
6 |
7 | export function InputUnderline(props: InputProps) {
8 | const { token } = theme.useToken()
9 |
10 | const { styles } = useStyles(({ token }) => {
11 | return {
12 | nameInput: css({
13 | color: token.colorTextBase,
14 | borderBottom: '1px solid transparent',
15 | padding: `0 ${token.paddingXXS}px`,
16 |
17 | '&:hover': {
18 | borderColor: token.colorBorder,
19 | },
20 |
21 | '&:focus': {
22 | borderColor: token.colorPrimary,
23 | },
24 | }),
25 | }
26 | })
27 |
28 | return (
29 |
36 |
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 |