883 lines
16 KiB
Markdown
883 lines
16 KiB
Markdown
# 前端开发指南
|
||
|
||
## 1. 开发环境搭建
|
||
|
||
### 1.1 安装依赖
|
||
|
||
```bash
|
||
# 安装Node.js
|
||
# 推荐版本:18.x 或更高
|
||
|
||
# 进入frontend目录
|
||
cd frontend
|
||
|
||
# 安装依赖
|
||
npm install
|
||
```
|
||
|
||
### 1.2 环境变量配置
|
||
|
||
```bash
|
||
# 复制环境变量模板
|
||
cp .env.example .env
|
||
|
||
# 编辑.env文件
|
||
VITE_API_BASE_URL=http://localhost:5000/api/v1
|
||
VITE_APP_TITLE=海洋项目管理系统
|
||
```
|
||
|
||
### 1.3 启动开发服务器
|
||
|
||
```bash
|
||
# 启动开发服务器
|
||
npm run dev
|
||
|
||
# 访问 http://localhost:3000
|
||
```
|
||
|
||
### 1.4 常用命令
|
||
|
||
```bash
|
||
# 开发
|
||
npm run dev
|
||
|
||
# 构建
|
||
npm run build
|
||
|
||
# 预览构建
|
||
npm run preview
|
||
|
||
# 代码检查
|
||
npm run lint
|
||
|
||
# 代码格式化
|
||
npm run format
|
||
|
||
# 运行测试
|
||
npm run test
|
||
|
||
# 测试覆盖率
|
||
npm run test:coverage
|
||
```
|
||
|
||
## 2. 代码规范
|
||
|
||
### 2.1 ESLint配置
|
||
|
||
```javascript
|
||
// .eslintrc.js
|
||
module.exports = {
|
||
root: true,
|
||
env: {
|
||
browser: true,
|
||
es2021: true,
|
||
node: true,
|
||
},
|
||
extends: [
|
||
'eslint:recommended',
|
||
'plugin:react/recommended',
|
||
'plugin:react-hooks/recommended',
|
||
'plugin:react/jsx-runtime',
|
||
],
|
||
parserOptions: {
|
||
ecmaFeatures: {
|
||
jsx: true,
|
||
},
|
||
ecmaVersion: 'latest',
|
||
sourceType: 'module',
|
||
},
|
||
plugins: ['react', 'react-hooks'],
|
||
rules: {
|
||
'react/prop-types': 'off',
|
||
'react/react-in-jsx-scope': 'off',
|
||
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||
},
|
||
settings: {
|
||
react: {
|
||
version: 'detect',
|
||
},
|
||
},
|
||
};
|
||
```
|
||
|
||
### 2.2 Prettier配置
|
||
|
||
```javascript
|
||
// .prettierrc
|
||
{
|
||
"semi": true,
|
||
"singleQuote": true,
|
||
"tabWidth": 2,
|
||
"trailingComma": "es5",
|
||
"printWidth": 100,
|
||
"arrowParens": "avoid",
|
||
"endOfLine": "lf"
|
||
}
|
||
```
|
||
|
||
### 2.3 Git提交规范
|
||
|
||
```
|
||
格式:[类型] 简短描述
|
||
|
||
类型:
|
||
- feat: 新功能
|
||
- fix: 修复bug
|
||
- style: 代码格式调整
|
||
- refactor: 重构
|
||
- test: 测试
|
||
- docs: 文档
|
||
- chore: 构建/工具链
|
||
|
||
示例:
|
||
[frontend] feat: 添加项目列表页面
|
||
[frontend] fix: 修复登录后Token未保存的问题
|
||
[frontend] style: 统一代码格式
|
||
[frontend] refactor: 重构API服务模块
|
||
[frontend] test: 添加组件单元测试
|
||
[frontend] docs: 更新开发文档
|
||
[frontend] chore: 更新依赖版本
|
||
```
|
||
|
||
## 3. 目录结构说明
|
||
|
||
### 3.1 src目录
|
||
|
||
```
|
||
src/
|
||
├── assets/ # 静态资源
|
||
│ ├── images/ # 图片
|
||
│ ├── fonts/ # 字体
|
||
│ └── styles/ # 样式文件
|
||
├── components/ # 组件
|
||
│ ├── Layout/ # 布局组件
|
||
│ ├── Common/ # 通用组件
|
||
│ └── Business/ # 业务组件
|
||
├── pages/ # 页面组件
|
||
├── contexts/ # Context上下文
|
||
├── hooks/ # 自定义Hooks
|
||
├── services/ # API服务
|
||
├── utils/ # 工具函数
|
||
├── config/ # 配置文件
|
||
└── router/ # 路由配置
|
||
```
|
||
|
||
### 3.2 文件命名规范
|
||
|
||
```
|
||
组件:PascalCase
|
||
- Button.jsx
|
||
- ProjectList.jsx
|
||
- UserForm.jsx
|
||
|
||
Hooks:camelCase
|
||
- useAuth.js
|
||
- useTable.js
|
||
- useApi.js
|
||
|
||
工具函数:camelCase
|
||
- formatDate.js
|
||
- formatMoney.js
|
||
- validate.js
|
||
|
||
样式文件:kebab-case
|
||
- button.css
|
||
- project-list.css
|
||
- user-form.css
|
||
```
|
||
|
||
## 4. 组件开发规范
|
||
|
||
### 4.1 函数式组件
|
||
|
||
```jsx
|
||
// 推荐:使用函数式组件 + Hooks
|
||
import React, { useState, useEffect } from 'react';
|
||
|
||
const MyComponent = ({ title, data }) => {
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
useEffect(() => {
|
||
// 副作用逻辑
|
||
}, []);
|
||
|
||
return <div>{title}</div>;
|
||
};
|
||
|
||
export default MyComponent;
|
||
```
|
||
|
||
### 4.2 Props类型检查
|
||
|
||
```jsx
|
||
import PropTypes from 'prop-types';
|
||
|
||
const MyComponent = ({ title, count, onAdd }) => {
|
||
return <div>{title}: {count}</div>;
|
||
};
|
||
|
||
MyComponent.propTypes = {
|
||
title: PropTypes.string.isRequired,
|
||
count: PropTypes.number,
|
||
onAdd: PropTypes.func,
|
||
};
|
||
|
||
MyComponent.defaultProps = {
|
||
count: 0,
|
||
onAdd: () => {},
|
||
};
|
||
|
||
export default MyComponent;
|
||
```
|
||
|
||
### 4.3 事件处理
|
||
|
||
```jsx
|
||
// 推荐:使用箭头函数或bind绑定
|
||
const MyComponent = () => {
|
||
const handleClick = () => {
|
||
console.log('Clicked');
|
||
};
|
||
|
||
return <button onClick={handleClick}>Click me</button>;
|
||
};
|
||
|
||
// 不推荐:在JSX中直接定义函数
|
||
const MyComponent = () => {
|
||
return <button onClick={() => console.log('Clicked')}>Click me</button>;
|
||
};
|
||
```
|
||
|
||
## 5. Hooks使用规范
|
||
|
||
### 5.1 useState
|
||
|
||
```jsx
|
||
// 基本用法
|
||
const [count, setCount] = useState(0);
|
||
|
||
// 函数式更新
|
||
const [count, setCount] = useState(0);
|
||
const increment = () => setCount(prev => prev + 1);
|
||
|
||
// 对象更新
|
||
const [user, setUser] = useState({ name: '', age: 0 });
|
||
const updateName = (name) => setUser(prev => ({ ...prev, name }));
|
||
```
|
||
|
||
### 5.2 useEffect
|
||
|
||
```jsx
|
||
// 副作用
|
||
useEffect(() => {
|
||
// 执行副作用
|
||
return () => {
|
||
// 清理函数
|
||
};
|
||
}, [dependencies]);
|
||
|
||
// 空依赖数组:只在挂载时执行一次
|
||
useEffect(() => {
|
||
console.log('Component mounted');
|
||
}, []);
|
||
|
||
// 依赖数组为空时,不要在依赖数组中省略
|
||
// 错误示例
|
||
useEffect(() => {
|
||
fetchUser(userId);
|
||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||
|
||
// 正确示例
|
||
useEffect(() => {
|
||
fetchUser(userId);
|
||
}, [userId]);
|
||
```
|
||
|
||
### 5.3 useMemo和useCallback
|
||
|
||
```jsx
|
||
// useMemo:缓存计算结果
|
||
const expensiveValue = useMemo(() => {
|
||
return computeExpensiveValue(a, b);
|
||
}, [a, b]);
|
||
|
||
// useCallback:缓存函数
|
||
const handleClick = useCallback(() => {
|
||
doSomething(a, b);
|
||
}, [a, b]);
|
||
```
|
||
|
||
### 5.4 自定义Hooks
|
||
|
||
```jsx
|
||
// 自定义Hook以use开头
|
||
const useFetch = (url) => {
|
||
const [data, setData] = useState(null);
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState(null);
|
||
|
||
useEffect(() => {
|
||
const fetchData = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const response = await fetch(url);
|
||
setData(await response.json());
|
||
} catch (err) {
|
||
setError(err);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
fetchData();
|
||
}, [url]);
|
||
|
||
return { data, loading, error };
|
||
};
|
||
```
|
||
|
||
## 6. 样式开发规范
|
||
|
||
### 6.1 CSS模块
|
||
|
||
```jsx
|
||
// MyComponent.jsx
|
||
import styles from './MyComponent.module.css';
|
||
|
||
const MyComponent = () => {
|
||
return <div className={styles.container}>Content</div>;
|
||
};
|
||
```
|
||
|
||
### 6.2 CSS-in-JS(可选)
|
||
|
||
```jsx
|
||
// 使用styled-components或emotion
|
||
import styled from 'styled-components';
|
||
|
||
const StyledDiv = styled.div`
|
||
padding: 20px;
|
||
background: #f0f0f0;
|
||
|
||
&:hover {
|
||
background: #e0e0e0;
|
||
}
|
||
`;
|
||
|
||
const MyComponent = () => {
|
||
return <StyledDiv>Content</StyledDiv>;
|
||
};
|
||
```
|
||
|
||
### 6.3 Tailwind CSS(可选)
|
||
|
||
```jsx
|
||
// 使用Tailwind CSS工具类
|
||
const MyComponent = () => {
|
||
return (
|
||
<div className="p-5 bg-gray-100 hover:bg-gray-200">
|
||
Content
|
||
</div>
|
||
);
|
||
};
|
||
```
|
||
|
||
## 7. 状态管理规范
|
||
|
||
### 7.1 使用Context API
|
||
|
||
```jsx
|
||
// Context定义
|
||
import { createContext, useContext } from 'react';
|
||
|
||
const MyContext = createContext();
|
||
|
||
export const MyProvider = ({ children }) => {
|
||
const [state, setState] = useState(initialState);
|
||
|
||
return (
|
||
<MyContext.Provider value={{ state, setState }}>
|
||
{children}
|
||
</MyContext.Provider>
|
||
);
|
||
};
|
||
|
||
export const useMyContext = () => {
|
||
const context = useContext(MyContext);
|
||
if (!context) {
|
||
throw new Error('useMyContext must be used within MyProvider');
|
||
}
|
||
return context;
|
||
};
|
||
```
|
||
|
||
### 7.2 状态分类
|
||
|
||
```jsx
|
||
// 1. 本地状态:使用useState
|
||
const [value, setValue] = useState('');
|
||
|
||
// 2. 上下文状态:使用Context
|
||
const { user, setUser } = useAuth();
|
||
|
||
// 3. 表单状态:使用Ant Design Form
|
||
const [form] = Form.useForm();
|
||
|
||
// 4. URL状态:使用useSearchParams
|
||
const [searchParams, setSearchParams] = useSearchParams();
|
||
```
|
||
|
||
## 8. 路由开发规范
|
||
|
||
### 8.1 使用React Router
|
||
|
||
```jsx
|
||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||
|
||
const Router = () => {
|
||
return (
|
||
<Routes>
|
||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||
<Route path="/dashboard" element={<Dashboard />} />
|
||
<Route path="/projects" element={<ProjectList />} />
|
||
<Route path="/projects/:id" element={<ProjectDetail />} />
|
||
</Routes>
|
||
);
|
||
};
|
||
```
|
||
|
||
### 8.2 路由参数
|
||
|
||
```jsx
|
||
import { useParams, useSearchParams } from 'react-router-dom';
|
||
|
||
const ProjectDetail = () => {
|
||
const { id } = useParams();
|
||
const [searchParams] = useSearchParams();
|
||
|
||
const keyword = searchParams.get('keyword') || '';
|
||
|
||
return <div>Project ID: {id}</div>;
|
||
};
|
||
```
|
||
|
||
### 8.3 路由守卫
|
||
|
||
```jsx
|
||
const PrivateRoute = ({ children }) => {
|
||
const { isAuthenticated } = useAuth();
|
||
|
||
if (!isAuthenticated) {
|
||
return <Navigate to="/login" replace />;
|
||
}
|
||
|
||
return children;
|
||
};
|
||
```
|
||
|
||
## 9. API调用规范
|
||
|
||
### 9.1 使用Axios
|
||
|
||
```jsx
|
||
import { useEffect } from 'react';
|
||
import { projectAPI } from '../services/project';
|
||
|
||
const ProjectList = () => {
|
||
const [list, setList] = useState([]);
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
useEffect(() => {
|
||
const fetchList = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const data = await projectAPI.getList();
|
||
setList(data.items);
|
||
} catch (error) {
|
||
console.error('Fetch error:', error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
fetchList();
|
||
}, []);
|
||
|
||
return <div>{/* 渲染列表 */}</div>;
|
||
};
|
||
```
|
||
|
||
### 9.2 使用自定义Hooks
|
||
|
||
```jsx
|
||
import { useList } from '../hooks/useList';
|
||
import { projectAPI } from '../services/project';
|
||
|
||
const ProjectList = () => {
|
||
const { list, loading, pagination, handleTableChange } = useList(
|
||
projectAPI.getList
|
||
);
|
||
|
||
return <Table {...} />;
|
||
};
|
||
```
|
||
|
||
## 10. 错误处理规范
|
||
|
||
### 10.1 错误边界
|
||
|
||
```jsx
|
||
class ErrorBoundary extends React.Component {
|
||
state = { hasError: false, error: null };
|
||
|
||
static getDerivedStateFromError(error) {
|
||
return { hasError: true, error };
|
||
}
|
||
|
||
componentDidCatch(error, errorInfo) {
|
||
console.error('Error:', error, errorInfo);
|
||
}
|
||
|
||
render() {
|
||
if (this.state.hasError) {
|
||
return <div>Something went wrong.</div>;
|
||
}
|
||
|
||
return this.props.children;
|
||
}
|
||
}
|
||
|
||
// 使用
|
||
<ErrorBoundary>
|
||
<App />
|
||
</ErrorBoundary>
|
||
```
|
||
|
||
### 10.2 错误处理组件
|
||
|
||
```jsx
|
||
const ErrorFallback = ({ error, resetErrorBoundary }) => {
|
||
return (
|
||
<div role="alert">
|
||
<p>Something went wrong:</p>
|
||
<pre>{error.message}</pre>
|
||
<button onClick={resetErrorBoundary}>Try again</button>
|
||
</div>
|
||
);
|
||
};
|
||
```
|
||
|
||
## 11. 性能优化
|
||
|
||
### 11.1 代码分割
|
||
|
||
```jsx
|
||
import { lazy, Suspense } from 'react';
|
||
|
||
const ProjectList = lazy(() => import('./pages/Projects/ProjectList'));
|
||
const ProjectDetail = lazy(() => import('./pages/Projects/ProjectDetail'));
|
||
|
||
const App = () => {
|
||
return (
|
||
<Suspense fallback={<Spin />}>
|
||
<Routes>
|
||
<Route path="/projects" element={<ProjectList />} />
|
||
<Route path="/projects/:id" element={<ProjectDetail />} />
|
||
</Routes>
|
||
</Suspense>
|
||
);
|
||
};
|
||
```
|
||
|
||
### 11.2 React.memo
|
||
|
||
```jsx
|
||
const ExpensiveComponent = React.memo(({ data }) => {
|
||
// 渲染逻辑
|
||
});
|
||
|
||
// 自定义比较函数
|
||
const MyComponent = React.memo(({ a, b }) => {
|
||
// 渲染逻辑
|
||
}, (prevProps, nextProps) => {
|
||
return prevProps.a === nextProps.a && prevProps.b === nextProps.b;
|
||
});
|
||
```
|
||
|
||
### 11.3 useMemo和useCallback
|
||
|
||
```jsx
|
||
const MyComponent = ({ data, onUpdate }) => {
|
||
// 缓存计算结果
|
||
const sortedData = useMemo(() => {
|
||
return data.sort((a, b) => a.id - b.id);
|
||
}, [data]);
|
||
|
||
// 缓存函数
|
||
const handleClick = useCallback(() => {
|
||
onUpdate(sortedData);
|
||
}, [onUpdate, sortedData]);
|
||
|
||
return <div onClick={handleClick}>Content</div>;
|
||
};
|
||
```
|
||
|
||
## 12. 测试规范
|
||
|
||
### 12.1 组件测试
|
||
|
||
```jsx
|
||
import { render, screen, fireEvent } from '@testing-library/react';
|
||
import Button from './Button';
|
||
|
||
describe('Button Component', () => {
|
||
test('renders button with children', () => {
|
||
render(<Button>Click me</Button>);
|
||
expect(screen.getByText('Click me')).toBeInTheDocument();
|
||
});
|
||
|
||
test('calls onClick when clicked', () => {
|
||
const handleClick = jest.fn();
|
||
render(<Button onClick={handleClick}>Click</Button>);
|
||
fireEvent.click(screen.getByText('Click'));
|
||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||
});
|
||
});
|
||
```
|
||
|
||
### 12.2 Hooks测试
|
||
|
||
```jsx
|
||
import { renderHook, act } from '@testing-library/react';
|
||
import { useCounter } from './useCounter';
|
||
|
||
describe('useCounter', () => {
|
||
test('should increment counter', () => {
|
||
const { result } = renderHook(() => useCounter());
|
||
|
||
expect(result.current.count).toBe(0);
|
||
|
||
act(() => {
|
||
result.current.increment();
|
||
});
|
||
|
||
expect(result.current.count).toBe(1);
|
||
});
|
||
});
|
||
```
|
||
|
||
### 12.3 集成测试
|
||
|
||
```jsx
|
||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||
import { BrowserRouter } from 'react-router-dom';
|
||
import App from './App';
|
||
|
||
const renderWithRouter = (component) => {
|
||
return render(<BrowserRouter>{component}</BrowserRouter>);
|
||
};
|
||
|
||
test('user can login', async () => {
|
||
renderWithRouter(<App />);
|
||
|
||
fireEvent.change(screen.getByLabelText('Username'), {
|
||
target: { value: 'admin' },
|
||
});
|
||
fireEvent.change(screen.getByLabelText('Password'), {
|
||
target: { value: 'password' },
|
||
});
|
||
fireEvent.click(screen.getByText('Login'));
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getByText('Dashboard')).toBeInTheDocument();
|
||
});
|
||
});
|
||
```
|
||
|
||
## 13. 调试技巧
|
||
|
||
### 13.1 React DevTools
|
||
|
||
```jsx
|
||
// 安装React DevTools浏览器扩展
|
||
// 可以查看组件树、props、state、Hooks等
|
||
```
|
||
|
||
### 13.2 Console日志
|
||
|
||
```jsx
|
||
// 开发环境下的调试日志
|
||
if (import.meta.env.DEV) {
|
||
console.log('Debug:', { data, loading });
|
||
}
|
||
|
||
// 生产环境下自动移除
|
||
```
|
||
|
||
### 13.3 性能分析
|
||
|
||
```jsx
|
||
// 使用React Profiler分析性能
|
||
import { Profiler } from 'react';
|
||
|
||
const onRenderCallback = (id, phase, actualDuration) => {
|
||
console.log(`${id} ${phase} took ${actualDuration}ms`);
|
||
};
|
||
|
||
<Profiler id="MyComponent" onRender={onRenderCallback}>
|
||
<MyComponent />
|
||
</Profiler>
|
||
```
|
||
|
||
## 14. 部署规范
|
||
|
||
### 14.1 构建配置
|
||
|
||
```javascript
|
||
// vite.config.js
|
||
import { defineConfig } from 'vite';
|
||
import react from '@vitejs/plugin-react';
|
||
|
||
export default defineConfig({
|
||
plugins: [react()],
|
||
base: '/app/', // 基础路径
|
||
build: {
|
||
outDir: 'dist',
|
||
sourcemap: false, // 生产环境关闭sourcemap
|
||
chunkSizeWarningLimit: 1000,
|
||
},
|
||
server: {
|
||
port: 3000,
|
||
proxy: {
|
||
'/api': {
|
||
target: 'http://localhost:5000',
|
||
changeOrigin: true,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
```
|
||
|
||
### 14.2 环境变量
|
||
|
||
```bash
|
||
# .env.development
|
||
VITE_API_BASE_URL=http://localhost:5000/api/v1
|
||
|
||
# .env.production
|
||
VITE_API_BASE_URL=https://api.example.com/api/v1
|
||
```
|
||
|
||
### 14.3 Docker部署
|
||
|
||
```dockerfile
|
||
# Dockerfile
|
||
FROM node:18-alpine as builder
|
||
WORKDIR /app
|
||
COPY package*.json ./
|
||
RUN npm ci
|
||
COPY . .
|
||
RUN npm run build
|
||
|
||
FROM nginx:alpine
|
||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||
EXPOSE 80
|
||
CMD ["nginx", "-g", "daemon off;"]
|
||
```
|
||
|
||
## 15. 最佳实践
|
||
|
||
### 15.1 组件设计
|
||
|
||
- 单一职责原则:每个组件只做一件事
|
||
- 组件大小控制在300行以内
|
||
- 合理拆分组件,提高复用性
|
||
- 使用组合而非继承
|
||
|
||
### 15.2 性能优化
|
||
|
||
- 使用React.memo避免不必要的渲染
|
||
- 使用useMemo缓存计算结果
|
||
- 使用useCallback缓存函数
|
||
- 使用懒加载减少初始加载时间
|
||
|
||
### 15.3 代码质量
|
||
|
||
- 遵循ESLint和Prettier规范
|
||
- 编写单元测试
|
||
- 添加必要的注释
|
||
- 保持代码简洁清晰
|
||
|
||
### 15.4 安全性
|
||
|
||
- 避免直接渲染用户输入
|
||
- 使用HTTPS
|
||
- Token存储安全
|
||
- 设置适当的CORS策略
|
||
|
||
## 16. 常见问题
|
||
|
||
### 16.1 如何处理跨域?
|
||
|
||
```javascript
|
||
// vite.config.js
|
||
export default defineConfig({
|
||
server: {
|
||
proxy: {
|
||
'/api': {
|
||
target: 'http://localhost:5000',
|
||
changeOrigin: true,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
```
|
||
|
||
### 16.2 如何优化首屏加载?
|
||
|
||
```jsx
|
||
// 使用路由懒加载
|
||
const Dashboard = lazy(() => import('./pages/Dashboard'));
|
||
|
||
// 使用Suspense包裹
|
||
<Suspense fallback={<Loading />}>
|
||
<Dashboard />
|
||
</Suspense>
|
||
```
|
||
|
||
### 16.3 如何处理表单验证?
|
||
|
||
```jsx
|
||
import { Form, Input, Button } from 'antd';
|
||
|
||
const MyForm = () => {
|
||
const [form] = Form.useForm();
|
||
|
||
const onFinish = (values) => {
|
||
console.log('Form values:', values);
|
||
};
|
||
|
||
return (
|
||
<Form form={form} onFinish={onFinish} layout="vertical">
|
||
<Form.Item
|
||
name="username"
|
||
label="用户名"
|
||
rules={[{ required: true, message: '请输入用户名' }]}
|
||
>
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item>
|
||
<Button type="primary" htmlType="submit">提交</Button>
|
||
</Form.Item>
|
||
</Form>
|
||
);
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
**文档维护**: 前端程序员
|
||
**文档类型**: 前端开发指南
|
||
**最后更新**: 2026-01-25
|