Umi,中文发音为「乌米」,是可扩展的企业级前端应用框架。Umi 以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。
mkdir umi4project
cd umi4project
npm init -y
npm install @ant-design/pro-components ahooks jsonwebtoken @umijs/max --save
mdir src
max g
max g
{
"scripts": {
"dev": "max dev",
"build": "max build"
},
}
/node_modules
/src/.umi
/dist
tsconfig.json
{
"extends": "./src/.umi/tsconfig.json",
+ "compilerOptions": {
+ "noImplicitAny": false
+ }
}
npm run dev
config\config.ts
import { defineConfig } from '@umijs/max';
+import routes from "./routes";
export default defineConfig({
npmClient: 'npm',
+ routes
});
config\routes.ts
export default [
{ path: '/', redirect: '/home' },
{ icon: 'HomeOutlined', name: '首页', path: '/home', component: './home/index' },
{ icon: 'ProfileOutlined', name: '个人中心', path: '/profile', component: './profile/index' },
]
src\pages\home\index.tsx
import styles from './index.less';
+import { Link, history, useNavigate } from '@umijs/max';
+import { Button } from 'antd';
export default function Page() {
let navigate = useNavigate();
return (
<div>
+ <h1 className={styles.title}>首页</h1>
+ <Link to="/profile">个人中心</Link>
+ <Button type='primary' onClick={() => history.push('/profile')}>个人中心</Button>
+ <Button type='dashed' onClick={() => navigate('/profile')}>个人中心</Button>
</div>
);
}
max g
src\pages\home\index.tsx
import { Link, history, useNavigate } from '@umijs/max';
import { Button } from 'antd';
export default function Page() {
let navigate = useNavigate();
return (
<div>
+ <h1 className={`text-lg font-bold text-red-600`}>首页</h1>
<Link to="/profile">个人中心</Link>
<Button onClick={() => history.push('/profile')}>个人中心</Button>
<Button onClick={() => navigate('/profile')}>个人中心</Button>
</div>
);
}
src\pages\profile\index.tsx
export default function Page() {
return (
<div>
+ <h1 className={`text-lg font-bold text-green-600`}>个人中心</h1>
</div>
);
}
import { defineConfig } from "@umijs/max";
import routes from "./routes";
export default defineConfig({
npmClient: "pnpm",
routes,
tailwindcss: {},
+ layout: {
+ title: "UMI",
+ locale: false
+ },
+ antd: {},
});
config\routes.ts
export default [
{ path: '/', redirect: '/home' },
{ icon: 'HomeOutlined', name: '首页', path: '/home', component: './home/index' },
{ icon: 'ProfileOutlined', name: '个人中心', path: '/profile', component: './profile/index' },
+ {
+ icon: 'UserOutlined',
+ name: '用户管理',
+ path: '/user',
+ component: './user/index',
+ routes: [
+ { name: '添加用户', path: '/user/add', component: './user/add/index' },
+ { name: '用户列表', path: '/user/list', component: './user/list/index' },
+ { name: '用户详情', path: '/user/detail/:id', component: './user/detail/index', hideInMenu: true },
+ ],
+ },
]
src\pages\user\index.tsx
import { PageContainer } from '@ant-design/pro-components';
import { Outlet } from '@umijs/max';
export default function () {
return (
<PageContainer>
<Outlet />
</PageContainer>
);
}
src\pages\user\list\index.tsx
import { List } from 'antd';
import { Link } from '@umijs/max';
export default function () {
const data = {
list: [
{ id: 1, username: 'root' },
{ id: 2, username: 'admin' },
{ id: 3, username: 'member' }
]
}
return (
<List
header={<div>用户列表</div>}
footer={<div>共计{data?.list?.length}</div>}
bordered
dataSource={data?.list}
renderItem={(user: any) => (
<List.Item>
<Link to={`/user/detail/${user.id}`} state={user}> {user.username}</Link>
</List.Item>
)}
/>
);
}
src\pages\user\add\index.tsx
import { Form, Input, Button, Row, Col } from 'antd';
export default function () {
const onFinish = (values: any) => {
console.log(values);
};
return (
<Row >
<Col offset={8} span={8}>
<Form
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
onFinish={onFinish}
>
<Form.Item
label="用户名"
name="username"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input />
</Form.Item>
<Form.Item
label="密码"
name="password"
rules={[{ required: true, message: '请输入密码' }]}
>
<Input.Password />
</Form.Item>
<Form.Item
label="手机号"
name="phone"
rules={[{ required: true, message: '请输入手机号' }]}
>
<Input />
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit" >
提交
</Button>
</Form.Item>
</Form>
</Col>
</Row>
);
}
src\pages\user\detail\index.tsx
import { Descriptions } from 'antd';
import { useLocation } from '@umijs/max';
export default function (props: any) {
const location = useLocation();
let user = location.state as API.User;
return (
<Descriptions title="用户信息">
<Descriptions.Item label="用户名">{user?.username}</Descriptions.Item>
<Descriptions.Item label="手机号">{user?.phone}</Descriptions.Item>
</Descriptions>
);
}
import { defineConfig } from "@umijs/max";
import routes from "./routes";
export default defineConfig({
npmClient: "pnpm",
routes,
tailwindcss: {},
layout: {
title: "UMI",
locale: false
},
antd: {},
+ request: {},
+ model: {},
+ initialState: {},
+ proxy: {
+ '/api/': {
+ target: 'http://127.0.0.1:7001/',
+ changeOrigin: true
+ }
+ }
});
src\pages\user\list\index.tsx
import { List } from 'antd';
import { Link, useModel } from '@umijs/max';
export default function () {
+ const { data, loading } = useModel('user.model');
return (
<List
header={<div>用户列表</div>}
footer={<div>共计{data?.list?.length}</div>}
bordered
dataSource={data?.list}
renderItem={(user: any) => (
<List.Item>
<Link to={`/user/detail/${user.id}`} state={user}> {user.username}</Link>
</List.Item>
)}
/>
);
}
src\pages\user\model.ts
import { useRequest } from 'ahooks';
import { getUser } from '@/services/user';
export default () => {
const { data, loading, refresh } = useRequest(getUser);
return {
data,
refresh,
loading
};
};
src\services\typings.d.ts
declare namespace API {
export interface ListResponse<T> {
data?: ListData<T>;
errorCode: string;
errorMessage: string;
errorType: number;
success?: boolean;
}
export interface ListData<T> {
list?: T[];
total?: number;
}
export interface User {
id?: number;
password?: string;
phone?: string;
username?: string;
role?: string;
}
export interface SigninUser {
username?: string;
password?: string;
}
export interface SignupUser {
password?: string;
phone?: string;
username?: string;
role_id?: number;
}
}
src\services\user.ts
import { request } from '@umijs/max';
export async function getUser() {
return request<API.ListData<API.User>>('/api/user', {
method: 'GET'
});
}
src\app.ts
import { notification, message } from 'antd';
enum ErrorShowType {
SILENT = 0,
WARN_MESSAGE = 1,
ERROR_MESSAGE = 2,
NOTIFICATION = 3
}
const errorHandler = (error: any) => {
if (error.name === 'BizError') {
const errorInfo = error.info;
if (errorInfo) {
const { errorMessage, errorCode, showType } = errorInfo;
switch (errorInfo.showType) {
case ErrorShowType.SILENT:
break;
case ErrorShowType.WARN_MESSAGE:
message.warn(errorMessage);
break;
case ErrorShowType.ERROR_MESSAGE:
message.error(errorMessage);
break;
case ErrorShowType.NOTIFICATION:
notification.open({
description: errorMessage,
message: errorCode,
});
break;
default:
message.error(errorMessage);
}
}
} else if (error.response) {
message.error(`响应状态码:${error.response.status}`);
} else if (error.request) {
message.error('没有收到响应');
} else {
message.error('请求发送失败');
}
};
const errorThrower = (res: any) => {
const { success, data, errorCode, errorMessage } = res;
if (!success) {
const error: any = new Error(errorMessage);
error.name = 'BizError';
error.info = { errorCode, errorMessage, data };
throw error;
}
}
export const request = {
timeout: 3000,
headers: {
['Content-Type']: 'application/json',
['Accept']: 'application/json',
credentials: 'include'
},
errorConfig: {
errorThrower,
errorHandler
},
requestInterceptors: [(url, options) => {
return { url, options }
}],
responseInterceptors: [(response, options) => {
return response.data;
}]
};
export const layout = () => {
return {
title: 'UMI4',
logo: 'https://img.alicdn.com/tfs/TB1YHEpwUT1gK0jSZFhXXaAtVXa-28-27.svg',
};
};
src\pages\user\add\index.tsx
import { Form, Input, Button, Row, Col } from 'antd';
+import { useRequest } from 'ahooks';
+import { addUser } from '@/services/user';
+import { useNavigate, useModel } from '@umijs/max';
+import { useEffect } from 'react';
export default function () {
+ const navigate = useNavigate();
+ const { refresh } = useModel('user.model');
+ const { data, loading, run } = useRequest(addUser, {
+ manual: true,
+ onSuccess: refresh
+ });
const onFinish = (values: any) => {
+ run(values);
};
+ useEffect(() => {
+ if (data) {
+ navigate('/user/list');
+ }
+ }, [data]);
return (
<Row >
<Col offset={8} span={8}>
<Form
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
onFinish={onFinish}
>
<Form.Item
label="用户名"
name="username"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input />
</Form.Item>
<Form.Item
label="密码"
name="password"
rules={[{ required: true, message: '请输入密码' }]}
>
<Input.Password />
</Form.Item>
<Form.Item
label="手机号"
name="phone"
rules={[{ required: true, message: '请输入手机号' }]}
>
<Input />
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit" >
提交
</Button>
</Form.Item>
</Form>
</Col>
</Row>
);
}
src\services\user.ts
import { request } from '@umijs/max';
export async function getUser() {
return request<API.ListData<API.User>>('/api/user', {
method: 'GET'
});
}
+export async function addUser(
+ body?: API.User
+) {
+ return request<string>('/api/user', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ data: body
+ });
+}
config\routes.ts
export default [
{ path: '/', redirect: '/home' },
{ icon: 'HomeOutlined', name: '首页', path: '/home', component: './home/index' },
{ icon: 'ProfileOutlined', name: '个人中心', path: '/profile', component: './profile/index' },
{
icon: 'UserOutlined',
name: '用户管理',
path: '/user',
component: './user/index',
routes: [
{ name: '添加用户', path: '/user/add', component: './user/add/index' },
{ name: '用户列表', path: '/user/list', component: './user/list/index' },
{ name: '用户详情', path: '/user/detail/:id', component: './user/detail/index', hideInMenu: true },
],
},
+ {
+ name: '注册',
+ path: '/signup',
+ component: './signup/index',
+ hideInMenu: true,
+ layout: false
+ }
]
src\app.ts
import { notification, message } from 'antd';
+import { history } from '@umijs/max';
enum ErrorShowType {
SILENT = 0,
WARN_MESSAGE = 1,
ERROR_MESSAGE = 2,
NOTIFICATION = 3
}
const errorHandler = (error: any) => {
if (error.name === 'BizError') {
const errorInfo = error.info;
if (errorInfo) {
const { errorMessage, errorCode, showType } = errorInfo;
switch (errorInfo.showType) {
case ErrorShowType.SILENT:
break;
case ErrorShowType.WARN_MESSAGE:
message.warn(errorMessage);
break;
case ErrorShowType.ERROR_MESSAGE:
message.error(errorMessage);
break;
case ErrorShowType.NOTIFICATION:
notification.open({
description: errorMessage,
message: errorCode,
});
break;
default:
message.error(errorMessage);
}
}
} else if (error.response) {
message.error(`响应状态码:${error.response.status}`);
} else if (error.request) {
message.error('没有收到响应');
} else {
message.error('请求发送失败');
}
};
const errorThrower = (res: any) => {
const { success, data, errorCode, errorMessage } = res;
if (!success) {
const error: any = new Error(errorMessage);
error.name = 'BizError';
error.info = { errorCode, errorMessage, data };
throw error;
}
}
export const request = {
timeout: 3000,
headers: {
['Content-Type']: 'application/json',
['Accept']: 'application/json',
credentials: 'include'
},
errorConfig: {
errorThrower,
errorHandler
},
requestInterceptors: [(url, options) => {
return { url, options }
}],
responseInterceptors: [(response, options) => {
return response.data;
}]
};
+interface InitialState {
+ currentUser: API.User | null
+}
+
+export async function getInitialState() {
+ let initialState: InitialState = {
+ currentUser: null
+ }
+ return initialState;
+}
export const layout = ({ initialState }) => {
return {
title: 'UMI4',
logo: 'https://img.alicdn.com/tfs/TB1YHEpwUT1gK0jSZFhXXaAtVXa-28-27.svg',
+ onPageChange(location) {
+ const { currentUser } = initialState;
+ if (!currentUser && location.pathname !== '/signin') {
+ history.push('/signup');
+ }
+ }
};
};
src\constants\index.ts
enum RoleCodes {
root = 'root',
admin = 'admin',
member = 'member'
}
const ROLES = [
{
code: RoleCodes.root,
name: '超级管理员'
},
{
code: RoleCodes.admin,
name: '管理员'
},
{
code: RoleCodes.member,
name: '普通成员'
}
]
export {
RoleCodes,
ROLES
}
src\pages\signup\index.tsx
import { Form, Input, Button, Card, Row, Col, Spin, Select } from 'antd';
import { useRequest } from 'ahooks';
import { signup } from '@/services/auth';
import { history, Link } from '@umijs/max';
import { ROLES } from '@/constants';
export default function () {
const { loading, run } = useRequest(signup, {
manual: true,
onSuccess() {
history.push('/signin')
}
});
const onFinish = (values: any) => {
run(values);
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
return (
<Row className='h-screen bg-gray-200' align='middle'>
<Col offset={8} span={8} >
<Card title="请登录" extra={<Link to="/signin">去登录</Link>}>
<Spin spinning={loading}>
<Form
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
>
<Form.Item
label="用户名"
name="username"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input />
</Form.Item>
<Form.Item
label="密码"
name="password"
rules={[{ required: true, message: '请输入密码' }]}
>
<Input.Password />
</Form.Item>
<Form.Item
label="手机号"
name="phone"
rules={[{ required: true, message: '请输入手机号' }]}
>
<Input />
</Form.Item>
<Form.Item
label="角色"
name="role"
rules={[{ required: true, message: '请选择角色' }]}
>
<Select
>
{
ROLES.map(role => {
return (
<Select.Option value={role.code} key={role.code}>
{role.name}
</Select.Option>
)
})
}
</Select>
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit" >
提交
</Button>
</Form.Item>
</Form>
</Spin>
</Card>
</Col>
</Row>
);
}
src\services\auth.ts
import { request } from '@umijs/max';
export async function signup(
body?: API.SignupUser
) {
return request<string>('/api/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body
});
}
config\routes.ts
export default [
{ path: '/', redirect: '/home' },
{ icon: 'HomeOutlined', name: '首页', path: '/home', component: './home/index' },
{ icon: 'ProfileOutlined', name: '个人中心', path: '/profile', component: './profile/index' },
{
icon: 'UserOutlined',
name: '用户管理',
path: '/user',
component: './user/index',
routes: [
{ name: '添加用户', path: '/user/add', component: './user/add/index' },
{ name: '用户列表', path: '/user/list', component: './user/list/index' },
{ name: '用户详情', path: '/user/detail/:id', component: './user/detail/index', hideInMenu: true },
],
},
{
name: '注册',
path: '/signup',
component: './signup/index',
hideInMenu: true,
layout: false
},
+ {
+ name: '登录',
+ path: '/signin',
+ component: './signin/index',
+ hideInMenu: true,
+ layout: false
+ }
]
src\app.ts
import { notification, message } from 'antd';
import { history } from '@umijs/max';
+import { decode } from 'jsonwebtoken';
enum ErrorShowType {
SILENT = 0,
WARN_MESSAGE = 1,
ERROR_MESSAGE = 2,
NOTIFICATION = 3
}
const errorHandler = (error: any) => {
if (error.name === 'BizError') {
const errorInfo = error.info;
if (errorInfo) {
const { errorMessage, errorCode, showType } = errorInfo;
switch (errorInfo.showType) {
case ErrorShowType.SILENT:
break;
case ErrorShowType.WARN_MESSAGE:
message.warn(errorMessage);
break;
case ErrorShowType.ERROR_MESSAGE:
message.error(errorMessage);
break;
case ErrorShowType.NOTIFICATION:
notification.open({
description: errorMessage,
message: errorCode,
});
break;
default:
message.error(errorMessage);
}
}
} else if (error.response) {
message.error(`响应状态码:${error.response.status}`);
} else if (error.request) {
message.error('没有收到响应');
} else {
message.error('请求发送失败');
}
};
const errorThrower = (res: any) => {
const { success, data, errorCode, errorMessage } = res;
if (!success) {
const error: any = new Error(errorMessage);
error.name = 'BizError';
error.info = { errorCode, errorMessage, data };
throw error;
}
}
export const request = {
timeout: 3000,
headers: {
['Content-Type']: 'application/json',
['Accept']: 'application/json',
credentials: 'include'
},
errorConfig: {
errorThrower,
errorHandler
},
requestInterceptors: [(url, options) => {
+ const token = localStorage.getItem('token');
+ if (token) {
+ options.headers.authorization = token
+ }
+ return { url, options }
+ }],
responseInterceptors: [(response, options) => {
return response.data;
}]
};
interface InitialState {
currentUser: API.User | null
}
export async function getInitialState() {
let initialState: InitialState = {
currentUser: null
}
+ const token = localStorage.getItem('token');
+ if (token) {
+ initialState.currentUser = decode(token)
+ }
return initialState;
}
export const layout = ({ initialState }) => {
return {
title: 'UMI4',
logo: 'https://img.alicdn.com/tfs/TB1YHEpwUT1gK0jSZFhXXaAtVXa-28-27.svg',
onPageChange(location) {
const { currentUser } = initialState;
+ if (!currentUser && location.pathname !== '/signin') {
+ history.push('/signin');
+ }
}
};
};
src\pages\signin\index.tsx
import { Form, Input, Button, Card, Row, Col, Spin } from 'antd';
import { useRequest } from 'ahooks';
import { signin } from '@/services/auth';
import { useModel, history, Link } from '@umijs/max';
import { useEffect } from 'react';
import { decode } from 'jsonwebtoken';
export default function () {
const { initialState, setInitialState } = useModel('@@initialState');
const { loading, run } = useRequest(signin, {
manual: true,
onSuccess(result) {
localStorage.setItem('token', result);
const currentUser = decode(result);
setInitialState({ currentUser });
}
});
useEffect(() => {
if (initialState?.currentUser)
history.push('/')
}, [initialState]);
const onFinish = (values: any) => {
run(values);
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
return (
<Row className='h-screen bg-gray-200' align='middle'>
<Col offset={8} span={8} >
<Card title="请登录" extra={<Link to="/signup">去注册</Link>}>
<Spin spinning={loading}>
<Form
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
>
<Form.Item
label="用户名"
name="username"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input />
</Form.Item>
<Form.Item
label="密码"
name="password"
rules={[{ required: true, message: '请输入密码' }]}
>
<Input.Password />
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit" >
提交
</Button>
</Form.Item>
</Form>
</Spin>
</Card>
</Col>
</Row>
);
}
src\services\auth.ts
import { request } from '@umijs/max';
export async function signup(
body?: API.SignupUser
) {
return request<string>('/api/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body
});
}
+export async function signin(
+ body?: API.SigninUser
+) {
+ return request('/api/signin', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ data: body
+ });
+}
src\pages\profile\index.tsx
+import { PageContainer } from '@ant-design/pro-components';
+import HeaderMenu from '@/components/HeaderMenu';
export default function Page() {
return (
+ <PageContainer ghost>
+ <HeaderMenu />
+ </PageContainer>
);
}
src\components\HeaderMenu\index.tsx
import { Dropdown, Menu } from 'antd'
import { LoginOutlined } from '@ant-design/icons';
import { useModel, history } from '@umijs/max';
export default function HeaderMenu() {
const { initialState, setInitialState } = useModel('@@initialState');
const logoutClick = () => {
localStorage.removeItem('token');
setInitialState({ currentUser: null });
history.push('/signin');
}
const menu = (
<Menu items={[
{
key: 'logout',
label: (
<span>退出</span>
),
icon: <LoginOutlined />,
onClick: logoutClick
}
]} />
)
return (
<Dropdown.Button overlay={menu}>
{initialState?.currentUser?.username}
</Dropdown.Button>
)
}
config\config.ts
import { defineConfig } from "@umijs/max";
import routes from "./routes";
export default defineConfig({
npmClient: "pnpm",
routes,
tailwindcss: {},
layout: {
title: "UMI",
locale: false
},
antd: {},
request: {},
model: {},
initialState: {},
proxy: {
'/api/': {
target: 'http://127.0.0.1:7001/',
changeOrigin: true
}
},
+ access: {}
});
config\routes.ts
export default [
{ path: '/', redirect: '/home' },
{ icon: 'HomeOutlined', name: '首页', path: '/home', component: './home/index' },
{ icon: 'ProfileOutlined', name: '个人中心', path: '/profile', component: './profile/index' },
{
icon: 'UserOutlined',
name: '用户管理',
path: '/user',
component: './user/index',
routes: [
+ { name: '添加用户', path: '/user/add', component: './user/add/index', access: 'adminCan' },
+ { name: '用户列表', path: '/user/list', component: './user/list/index', access: 'memberCan' },
+ { name: '用户详情', path: '/user/detail/:id', component: './user/detail/index', hideInMenu: true, access: 'memberCan' },
],
},
{
name: '注册',
path: '/signup',
component: './signup/index',
hideInMenu: true,
layout: false
},
{
name: '登录',
path: '/signin',
component: './signin/index',
hideInMenu: true,
layout: false
},
]
src\access.ts
import { RoleCodes } from '@/constants';
export default function (initialState) {
const role = initialState?.currentUser?.role;
return {
rootCan: role === RoleCodes.root,
adminCan: [RoleCodes.root, RoleCodes.admin].includes(role),
memberCan: [RoleCodes.root, RoleCodes.admin, RoleCodes.member].includes(role),
};
}
src\pages\user\list\index.tsx
+import { List, Button } from 'antd';
+import { Link, useModel, useAccess } from '@umijs/max';
+import { useRequest } from 'ahooks';
+import { deleteUser } from '@/services/user';
export default function () {
const { data, loading, refresh } = useModel('user.model');
+ const { run } = useRequest(deleteUser, {
+ manual: true,
+ onSuccess: refresh
+ });
+ const access = useAccess();
return (
<List
className='w-1/3'
header={<div>用户列表</div>}
footer={<div>共计{data?.list?.length}</div>}
bordered
dataSource={data?.list}
renderItem={(user: any) => (
<List.Item>
<Link to={`/user/detail/${user.id}`} state={user}> {user.username}</Link>
+ <Button
+ onClick={() => run(user.id)}
+ size='small'
+ type="primary"
+ disabled={!access.adminCan}
+ loading={loading}>
+ 删除
+ </Button>
</List.Item>
)}
/>
);
}
src\services\user.ts
import { request } from '@umijs/max';
export async function getUser() {
return request<API.ListData<API.User>>('/api/user', {
method: 'GET'
});
}
export async function addUser(
body?: API.User
) {
return request<string>('/api/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body
});
}
+export async function deleteUser(id) {
+ return request<string>(`/api/user/${id}`, {
+ method: 'DELETE'
+ });
+}
config\routes.ts
[
+ {
+ icon:'UserOutlined',
+ name:'users',
+ path:'/users',
+ component:'./users/index'
+ },
]
src\services\user.ts
import { request } from '@umijs/max';
export async function getUser(params) {
return request('/api/user', {
method: 'GET',
params,
});
}
export async function addUser(user) {
return request('/api/user', {
method: 'POST',
data: user,
});
}
export async function updateUser(id, user) {
return request(`/api/user/${id}`, {
method: 'PUT',
data: user,
});
}
export async function deleteUser(ids) {
return request(`/api/user/${ids[0]}`, {
method: 'DELETE',
data:ids
});
}
src\pages\users\index.tsx
import { useState, useEffect } from 'react';
import { Table, Button, Modal, Form, Input, message, Space, Popconfirm, Select,Row,Col } from 'antd';
import { getUser, addUser, updateUser, deleteUser } from '@/services/user';
const { Option } = Select;
const Users = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [visible, setVisible] = useState(false);
const [current, setCurrent] = useState(null);
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const [searchUsername, setSearchUsername] = useState('');
const [searchPhone, setSearchPhone] = useState('');
const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
const [form] = Form.useForm();
const fetchUsers = async (pageNum = 1, pageSize = 10, username = '', phone = '') => {
setLoading(true);
try {
let params:any = { pageNum, pageSize };
if (username) {
params.username = username;
}
if (phone) {
params.phone = phone;
}
const response = await getUser(params);
setData(response.data.list);
setPagination({ ...pagination, total: response.data.total, current: pageNum, pageSize });
setLoading(false);
} catch (error) {
setLoading(false);
message.error('获取用户列表失败');
}
};
useEffect(() => {
fetchUsers();
}, []);
const handleTableChange = (pagination) => {
fetchUsers(pagination.current, pagination.pageSize);
};
const showModal = (record) => {
form.resetFields();
setCurrent(record);
if (record) {
form.setFieldsValue(record);
}
setVisible(true);
};
const handleOk = async () => {
const values = await form.validateFields();
if (current) {
try {
await updateUser(current.id, values);
message.success('更新成功');
} catch (error) {
message.error('更新失败');
}
} else {
try {
await addUser(values);
message.success('添加成功');
} catch (error) {
message.error('添加失败');
}
}
setVisible(false);
fetchUsers(pagination.current, pagination.pageSize);
};
const handleDelete = async (id) => {
try {
await deleteUser([id]);
message.success('删除成功');
} catch (error) {
message.error('删除失败');
}
fetchUsers(pagination.current, pagination.pageSize);
};
const handleMultiDelete = async () => {
try {
await deleteUser(selectedRowKeys);
message.success('删除成功');
} catch (error) {
message.error('删除失败');
}
setSelectedRowKeys([]);
fetchUsers(pagination.current, pagination.pageSize);
};
const handleSearch = () => {
fetchUsers(pagination.current, pagination.pageSize, searchUsername, searchPhone);
};
const columns = [
{
title: '用户名',
dataIndex: 'username',
},
{
title: '密码',
dataIndex: 'password',
},
{
title: '手机号',
dataIndex: 'phone',
},
{
title: '角色',
dataIndex: 'access',
render: (access) => {
switch (access) {
case 'root': return '超级管理员';
case 'admin': return '管理员';
default: return '普通用户';
}
}
},
{
title: '操作',
dataIndex: 'operation',
render: (_, record) => (
<Space size="middle">
<a onClick={() => showModal(record)}>编辑</a>
<Popconfirm title="确定要删除吗?" onConfirm={() => handleDelete(record.id)}>
<a>删除</a>
</Popconfirm>
</Space>
),
},
];
const rowSelection = {
selectedRowKeys,
onChange: setSelectedRowKeys,
};
return (
<div>
<Row>
<Col flex="auto">
<Input
placeholder="输入用户名搜索"
value={searchUsername}
onChange={e => setSearchUsername(e.target.value)}
style={{ width: 200, marginRight: 16 }}
/>
<Input
placeholder="输入手机号搜索"
value={searchPhone}
onChange={e => setSearchPhone(e.target.value)}
style={{ width: 200, marginRight: 16 }}
/>
<Button type="primary" onClick={handleSearch}>
搜索
</Button>
</Col>
<Col flex="none">
<Button type="primary" onClick={() => showModal(null)}>
新增用户
</Button>
<Button type="primary" onClick={handleMultiDelete} style={{ marginLeft: 16 }}>
批量删除
</Button>
</Col>
</Row>
<Table
rowKey="id"
columns={columns}
dataSource={data}
loading={loading}
rowSelection={rowSelection}
pagination={pagination}
onChange={handleTableChange}
/>
<Modal
title={current ? '编辑用户' : '新增用户'}
open={visible}
onOk={handleOk}
onCancel={() => setVisible(false)}
destroyOnClose
>
<Form form={form} initialValues={current} layout="vertical">
<Form.Item
name="username"
label="用户名"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input />
</Form.Item>
<Form.Item
name="password"
label="密码"
rules={[{ required: true, message: '请输入密码' }]}
>
<Input.Password />
</Form.Item>
<Form.Item
name="phone"
label="手机号"
rules={[
{ required: true, message: '请输入手机号' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确' },
]}
>
<Input />
</Form.Item>
<Form.Item
name="access"
label="角色"
rules={[{ required: true, message: '请选择角色' }]}
>
<Select>
<Option value="root">超级管理员</Option>
<Option value="admin">管理员</Option>
<Option value="member">普通用户</Option>
</Select>
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default Users;
fields.tsx
import { Table, Row, Col, Button, Modal, Form, Input, Select, message, Space, Popconfirm } from 'antd';
const fields = [
{
props: {
name: 'username',
label: '用户名',
rules: [{ required: true, message: '请输入用户名' }],
},
component: Input,
},
{
props: {
name: 'password',
label: '密码',
rules: [{ required: true, message: '请输入密码' }],
},
component: Input.Password,
},
{
props: {
name: 'email',
label: '邮箱',
rules: [{ required: true, message: '请输入邮箱' }],
},
component: Input,
},
{
props: {
name: 'phone',
label: '手机号',
rules: [{ required: true, message: '请输入手机号' }],
},
component: Input,
},
{
props: {
name: 'gender',
label: '性别',
rules: [{ required: true, message: '请选择性别' }],
},
component: Select,
children: [
<Select.Option key="1" value={1}>男</Select.Option>,
<Select.Option key="0" value={0}>女</Select.Option>,
],
},
{
props: {
name: 'access',
label: '角色',
rules: [{ required: true, message: '请输入角色' }],
},
component: Select,
children: [
<Select.Option key="root" value="root">超级管理员</Select.Option>,
<Select.Option key="admin" value="admin">管理员</Select.Option>,
<Select.Option key="member" value="member">普通会员</Select.Option>,
],
},
];
export default fields;
ConfigForm.tsx
import React, { useCallback, useState } from 'react';
import { Form, Button } from 'antd';
const toConfigForm = (Form) => {
return ({ form,handleSubmit, handleReset, fields, submitText = '提交', resetText = '重置', ...rest }) => {
const [submitting, setSubmitting] = useState(false);
const onSubmit = useCallback(async () => {
try {
setSubmitting(true);
const values = await form.validateFields();
handleSubmit?.(values);
} catch (error) {
console.error('Failed:', error);
// Add your custom user-friendly error message here
} finally {
setSubmitting(false);
}
}, [form, handleSubmit]);
const onReset = useCallback(() => {
form.resetFields();
handleReset?.();
}, [form, handleReset]);
return (
<Form form={form} onFinish={onSubmit} {...rest}>
{fields.map(({ props, component, componentProps, children }) => (
<Form.Item key={props.name} {...props}>
{React.createElement(component, componentProps, children)}
</Form.Item>
))}
<Form.Item>
<Button type="primary" htmlType="submit" loading={submitting}>{submitText}</Button>
<Button type="default" onClick={onReset}>{resetText}</Button>
</Form.Item>
</Form>
);
};
};
const ConfigForm = toConfigForm(Form);
export default ConfigForm;
Tailwind CSS 是一个高度可定制的、工具类优先的 CSS 框架,它使你可以在开发过程中迅速构建现代化的用户界面。与其他 CSS 框架(如 Bootstrap 和 Foundation)不同,Tailwind 不提供预设的 UI 组件,而是提供一套底层工具类,这些工具类可以通过组合来创建自定义的设计。
使用 npm 或 yarn 安装:
npm install tailwindcss
# or
yarn add tailwindcss
初始化配置文件
运行下面的命令来生成一个 tailwind.config.js
文件:
npx tailwindcss init
引入 Tailwind
在你的 CSS 文件中:
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
编写 HTML
使用工具类来定义元素的样式:
<button class="bg-blue-500 text-white rounded-lg px-4 py-2 hover:bg-blue-600">
Click Me
</button>
useRequest
是一个常见的 React Hook,用于简化 API 调用或异步操作的处理。尽管多个库提供了类似的功能,但 useRequest
通常与 UmiJS 和 ahooks 这样的库关联在一起。
useRequest
的主要目标是:
// 从 "ahooks" 库中引入 "useRequest" Hook。
import { useRequest } from 'ahooks';
// 从 "react" 库中引入 "React"。
import React from 'react';
// 定义一个函数 "getName",它返回一个 Promise。
// 这个 Promise 在1秒后解析为字符串 "zhufeng"。
function getName() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('zhufeng');
}, 1000);
});
}
// 定义一个 React 函数组件 "App"。
function App() {
// 使用 "useRequest" Hook 来调用 "getName" 函数。
// "data" 会在 Promise 解析后获得其值,而 "loading" 用于表示当前是否在加载状态。
const { data, loading } = useRequest(getName);
// 如果处于加载状态,显示 "加载中..."。
if (loading) {
return <div>加载中...</div>;
}
// 当数据加载完成时,显示 "用户名: " 后面跟着加载的数据。
return <div>用户名: {data}</div>;
};
// 导出 "App" 组件,使其可以在其他地方被引用。
export default App;
styled-components
是一种流行的 CSS-in-JS 库,允许你使用 JavaScript 中的模板字符串来定义组件的样式。使用 styled-components
可以将样式与组件逻辑紧密地结合在一起,从而使得组件样式更加模块化和可重用。
以下是 styled-components
的基础讲解:
安装
在你的项目中安装 styled-components
:
npm install styled-components
基本使用
创建一个 styled component:
import styled from 'styled-components';
const Button = styled.button`
background: palevioletred;
border-radius: 3px;
border: none;
color: white;
padding: 0.5rem 1rem;
`;
function App() {
return <Button>点击我</Button>;
}
在上述代码中,我们定义了一个名为 Button
的 styled component,该组件将渲染为一个带有特定样式的 <button>
元素。
传递属性
你可以根据组件的属性来调整样式:
const Button = styled.button`
background: ${props => props.primary ? 'palevioletred' : 'white'};
color: ${props => props.primary ? 'white' : 'palevioletred'};
/* ...其他样式... */
`;
function App() {
return (
<div>
<Button primary>主要按钮</Button>
<Button>次要按钮</Button>
</div>
);
}
扩展样式
你可以在一个 styled component 的基础上扩展样式:
const Button = styled.button`
background: palevioletred;
color: white;
/* ... */
`;
const LargeButton = styled(Button)`
padding: 1rem 2rem;
`;
function App() {
return <LargeButton>更大的按钮</LargeButton>;
}
全局样式
使用 createGlobalStyle
创建全局样式:
import styled, { createGlobalStyle } from 'styled-components';
const GlobalStyle = createGlobalStyle`
body {
margin: 0;
font-family: Arial, sans-serif;
}
`;
function App() {
return (
<>
<GlobalStyle />
{/* 其他组件 */}
</>
);
}
主题
使用 ThemeProvider
组件来为你的应用提供主题:
import styled, { ThemeProvider } from 'styled-components';
const theme = {
primaryColor: 'palevioletred',
secondaryColor: 'white'
};
const Button = styled.button`
background: ${props => props.theme.primaryColor};
color: ${props => props.theme.secondaryColor};
`;
function App() {
return (
<ThemeProvider theme={theme}>
<Button>使用主题的按钮</Button>
</ThemeProvider>
);
}
react-query
是一个数据获取库,用于 React。与 Redux 和 MobX 等状态管理库不同,react-query
主要关注远程数据的获取、缓存、同步和更新。它提供了自动数据同步、后台数据获取、缓存管理、无限查询等功能。
以下是 react-query
的核心概念:
安装
你首先需要安装 react-query
:
npm install react-query
基本使用
使用 useQuery
hook 来进行数据获取:
import { useQuery } from 'react-query';
function FetchData() {
const { data, error, isLoading } = useQuery('repoData', () =>
fetch('https://api.github.com/repos/tannerlinsley/react-query').then(res => res.json())
);
if (isLoading) return 'Loading...';
if (error) return `An error occurred: ${error.message}`;
return (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
</div>
);
}
在上述示例中,useQuery
接受两个参数:查询键和获取函数。查询键用于唯一标识此查询。
配置
你可以为 useQuery
提供配置对象以改变其默认行为:
const { data } = useQuery('repoData', fetchFunction, {
staleTime: 1000 * 60 * 5, // 5 minutes
cacheTime: 1000 * 60 * 60, // 1 hour
retry: 3
});
staleTime
: 数据在多少时间后被视为过时,此后将在背景中重新获取。cacheTime
: 未使用的数据在多长时间后从缓存中被清除。retry
: 如果查询失败,尝试的次数。QueryClient 和 QueryClientProvider
要使用 react-query
, 你需要创建一个 QueryClient
并使用 QueryClientProvider
将其提供给你的应用程序:
import { QueryClient, QueryClientProvider } from 'react-query';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<FetchData />
{/* 其他组件 */}
</QueryClientProvider>
);
}
现在,我们将上述概念组合在一起,创建一个完整的 react-query
示例:
import React from 'react';
import { useQuery, QueryClient, QueryClientProvider } from 'react-query';
// 数据获取函数
function fetchRepoData() {
return fetch('https://api.github.com/repos/tannerlinsley/react-query').then(res => res.json());
}
function RepoInfo() {
const { data, error, isLoading } = useQuery('repoData', fetchRepoData);
if (isLoading) return 'Loading...';
if (error) return `An error occurred: ${error.message}`;
return (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
</div>
);
}
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<RepoInfo />
{/* 其他组件 */}
</QueryClientProvider>
);
}
export default App;
valtio
是一个简单的状态管理库,用于 React。其主要思想是使状态管理变得简单而不牺牲反应性。与其他状态管理解决方案如 Redux 或 MobX 相比,valtio
提供了一个更直观和轻量级的方法来管理和响应状态变化。
以下是 valtio
的基础讲解:
安装
你首先需要安装 valtio
:
npm install valtio
创建状态
使用 proxy
函数创建一个代理状态:
import { proxy } from 'valtio';
const state = proxy({
count: 0,
});
上述代码创建了一个有 count
属性的状态对象。
在组件中使用状态
使用 useSnapshot
hook 来在组件中访问和响应状态变化:
import { useSnapshot } from 'valtio';
function Counter() {
const snapshot = useSnapshot(state);
return (
<div>
<p>Count: {snapshot.count}</p>
<button onClick={() => state.count++}>Increment</button>
</div>
);
}
当 state.count
改变时,组件将自动重新渲染。
现在,我们将上述概念组合在一起,创建一个完整的 valtio
示例:
import React from 'react';
import { proxy, useSnapshot } from 'valtio';
// 创建一个状态代理
const state = proxy({
count: 0,
});
function Counter() {
// 获取状态的快照
const snapshot = useSnapshot(state);
return (
<div>
<p>Count: {snapshot.count}</p>
<button onClick={() => state.count++}>Increment</button>
<button onClick={() => state.count--}>Decrement</button>
</div>
);
}
function App() {
return (
<div>
<h1>Valtio Counter</h1>
<Counter />
</div>
);
}
export default App;
当然可以。
UnoCSS 是什么?
UnoCSS 是一个实时 (JIT) 原子 CSS 框架。原子 CSS 的理念是将样式分解为原子性(小型、单一目的)的类名,每个类名只做一件事。这样,你可以通过组合这些原子类来构建复杂的用户界面。
UnoCSS 的主要特点:
实时编译:在开发过程中,UnoCSS 实时生成样式。这意味着你在 HTML 中实际使用的样式才会在最终的 CSS 输出中被编译和包含,从而产生更小的 CSS 包。
基于插件的架构:它基于一组插件来操作,这意味着你可以使用插件扩展其功能,或者编写自己的插件来生成自定义样式。
预设:UnoCSS 为常见的框架如 TailwindCSS 提供预设,允许你实现类似的实用程序为基础的工作流程,但可能提供更多的自定义和效率。
动态实用程序:除了静态实用程序,UnoCSS 还允许你创建动态实用程序。例如,你可以使用特定的类名语法动态生成颜色的阴影。
UnoCSS 如何工作?
扫描和清除:在构建过程中,UnoCSS 扫描你的文件(如 Vue 或 React 组件)以获取类名。然后处理这些类名,生成必要的样式。
生成样式:根据你使用的实用程序生成样式。例如,如果你使用了像 p-1
这样的实用程序(它可能代表 padding: 0.25rem
),UnoCSS 就会生成该特定样式。
输出:一旦生成了所有实际使用的样式,UnoCSS 就会输出一个包含你项目中实际使用的样式的 CSS 文件。
使用 UnoCSS 的优点:
性能:由于它只包括实际使用的样式,所以生成的 CSS 通常较小。
灵活性:借助于 UnoCSS 的插件扩展能力,你可以完美地定制框架来满足你的需求。
快速开发:一旦熟悉了实用程序类,开发人员可以在不频繁跳转 HTML 和 CSS 文件之间的情况下快速进行原型设计和构建用户界面。
可能的缺点:
学习曲线:如果你不熟悉原子/实用程序优先的 CSS,可能需要一段时间来熟悉。
冗长的 HTML:由于大量的实用程序类,你的 HTML 可能会变得很冗长。
自定义:尽管 UnoCSS 提供了广泛的自定义功能,但要实现某些设计可能需要额外的配置或自定义实用程序。
集成:
UnoCSS 可与 Vue、React、Vite 和 Webpack 等流行的框架和构建工具集成。
UMI4使用 UnoCSS
npm i unocss @unocss/cli
config\config.js
export default {
// 添加插件列表
plugins: [
// 使用 Umi 的 unoCSS 插件
require.resolve('@umijs/plugins/dist/unocss')
],
// unoCSS 的配置
unocss: {
// 观察并处理以下路径中的文件
watch: ['src/**/*.tsx']
},
};
// unocss.config.ts
// 导入来自'unocss'的相关方法和预设
import {defineConfig, presetAttributify, presetUno} from 'unocss';
// 定义一个函数,根据提供的参数来创建配置
export function createConfig({strict = true, dev = true} = {}) {
// 使用定义的配置,其中包括环境模式和所使用的预设
return defineConfig({
envMode: dev ? 'dev' : 'build',
presets: [presetAttributify({strict}), presetUno()],
});
}
// 默认导出创建的配置
export default createConfig();
presetUno
presetUno
是 UnoCSS
的默认预设。UnoCSS 的核心思想是为常用的 CSS 属性和值提供缩写,从而让开发者可以以更快、更简洁的方式书写样式。例如,你可以用 .m-1
来表示 margin: 0.25rem
,或 .bg-red
来表示背景色为红色。
这种方法与 TailwindCSS 类似,但 UnoCSS 提供了一种更自定义和轻量级的方法来实现这些效果。
使用 presetUno
,你就启用了这些原子类样式的功能。
presetAttributify
presetAttributify
是另一个功能强大的预设,允许你直接在 HTML 标签的属性中书写样式。
传统的 UnoCSS 或类似的工具类 CSS 方法要求你在 class
属性中列出所需的工具类。但使用 attributify
,你可以直接在元素上使用属性作为工具类。
举例来说:
传统的方式:
<div class="bg-red text-white">Hello, world!</div>
使用 attributify:
<div bg="red" text="white">Hello, world!</div>
如你所见,attributify
预设允许你使用属性来代替 class
来定义样式。这提供了一种更直观、更紧凑的方式来表示样式。
<div className="p-4 bg-blue-200 text-center">
<button className="px-4 py-2 bg-blue-500 text-white rounded">Click Me</button>
</div>
UI组件的二次封装是一个常见的开发模式,特别是在大型项目和团队中。这意味着我们基于一个现有的 UI 组件库(如 Ant Design、Material-UI、Bootstrap 等)创建自己的组件集,以满足项目特定的需求或设计规范。
二次封装有以下好处
确定需求:首先确定你需要哪些组件,并了解它们需要满足的特定需求和设计规范。
创建基础组件:基于原始组件库,创建你自己的基础组件。例如,你可以创建一个自定义的按钮,该按钮具有特定的大小、颜色和行为。
增加额外功能:为封装的组件增加额外的功能。例如,如果你封装了一个输入组件,你可以添加自动完成、字符计数等功能。
封装复杂组件:一些组件可能涉及多个子组件的组合。例如,一个带有标签、图标和工具提示的按钮。确保所有的子组件都能很好地一起工作。
测试:在使用封装的组件之前,确保对其进行充分的测试,包括单元测试、集成测试和可视化测试。
文档:为你的封装的组件编写文档是非常重要的。确保开发团队知道如何使用它们,以及它们的功能和限制。
示例:
假设我们要在 Ant Design 的基础上封装一个按钮:
import { Button as AntButton } from 'antd';
import PropTypes from 'prop-types';
const CustomButton = ({ label, variant, ...props }) => {
let color;
switch(variant) {
case 'primary':
color = 'blue';
break;
case 'secondary':
color = 'gray';
break;
default:
color = 'blue';
break;
}
return <AntButton style={{ backgroundColor: color }} {...props}>{label}</AntButton>;
}
CustomButton.propTypes = {
label: PropTypes.string.isRequired,
variant: PropTypes.oneOf(['primary', 'secondary'])
};
export default CustomButton;