React项目说明
一.通过脚手架创建项目
https://umijs.org/zh-CN/docs/getting-started
cnpm i yarn tyarn -g
mkdir admin-app
cd admin-app
yarn create @umijs/umi-app
npx @umijs/create-umi-app
yarn
yarn start
二.熟悉目录和文件
1.mock
-
模拟数据 http://mockjs.com/
生成随机数据,拦截 Ajax 请求
-
前后端分离
让前端攻城师独立于后端进行开发
-
开发无侵入
不需要修改既有代码,就可以拦截 Ajax 请求,返回模拟的响应数据
-
数据类型丰富
支持生成随机的文本、数字、布尔值、日期、邮箱、链接、图片、颜色等。
-
增加单元测试的真实性
通过随机数据,模拟各种场景。
-
用法简单
符合直觉的接口。
-
方便扩展
支持扩展更多数据类型,支持自定义函数和正则
-
2.src
-
pages 页面 --- 等同于vue脚手架中views,存放的就是页面组件
-
index.less
-
index.tsx 注意引入的less是局部生效的,使用
:global
放成全局的
-
3.editorconfig 编辑器配置
4.gitignore git上传的忽略文件
5.prettierignore 格式化忽略配置文件
6.prettierrc 格式化配置文件
7.umirc.ts 项目配置文件,类似vue的vue.config.js
8.package.json 项目记录文件
9.README.md 项目说明书
10.tsconfig.json ts的配置文件
11.typings.d.ts ts的声明文件,不声明不可以引入
如果代码提示:无法使用 JSX,除非提供了 "--jsx" 标志。修改tsconfig.json文件中的
"jsx": "react-jsx", 为 ‘"jsx": "react"
三.搭建项目基本结构
查看umi提供的umi插件
https://umijs.org/zh-CN/plugins/plugin-layout
1.构建时配置
可以通过配置文件配置 layout
的主题等配置, 在 config/config.ts
或者 umirc.ts
中这样写:
import { defineConfig } from 'umi'; export default defineConfig({ nodeModulesTransform: { type: 'none', }, routes: [ { path: '/', component: '@/pages/index' }, ], fastRefresh: {}, layout: { name: '嗨购管理系统', logo: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fwww.lgstatic.com%2Fthumbnail_300x300%2Fi%2Fimage%2FM00%2F1C%2F95%2FCgpEMlkBrbuANDoAAABOTfWdJhc845.png&refer=http%3A%2F%2Fwww.lgstatic.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg' } });
2.运行时配置
https://umijs.org/zh-CN/plugins/plugin-layout#%E8%BF%90%E8%A1%8C%E6%97%B6%E9%85%8D%E7%BD%AE
src文件夹下创建 app.tsx
import React from 'react'; import { BasicLayoutProps } from '@ant-design/pro-layout'; export const layout = (): BasicLayoutProps => { return { rightContentRender: () => <header>header</header>, footerRender: () => <footer>footer</footer> } }
3.扩展路由配置
config/route.ts
Pages/home/index.tsx
import * as React from 'react'; export interface IHomeProps { } export default class Home extends React.PureComponent<IHomeProps> { public render() { return ( <div> 首页 </div> ); } }
pages/login/index.tsx
import * as React from 'react'; export interface ILoginProps { } export default function Login (props: ILoginProps) { return ( <div> 登录 </div> ); }
// config/route.ts
export interface IBestAFSRoute { path: string component: string name?: string // 兼容此写法 icon?: string // 更多功能查看 // https://beta-pro.ant.design/docs/advanced-menu // --- // 新页面打开 target?: string // 不展示顶栏 headerRender?: boolean // 不展示页脚 footerRender?: boolean // 不展示菜单 menuRender?: boolean // 不展示菜单顶栏 menuHeaderRender?: boolean // 权限配置,需要与 plugin-access 插件配合使用 access?: string // 隐藏子菜单 hideChildrenInMenu?: boolean // 隐藏自己和子菜单 hideInMenu?: boolean // 在面包屑中隐藏 hideInBreadcrumb?: boolean // 子项往上提,仍旧展示, flatMenu?: boolean } const routes: IBestAFSRoute[] = [ { path: '/', component: '@/pages/index' }, { path: '/home', name: '首页', component: '@/pages/home/index' }, { path: '/login', name: '登录', component: '@/pages/login/index' } ] export default routes
如果要使用图标
cnpm install --save @ant-design/icons赋值ant design的图标即可
umirc.ts中配置路由
import { defineConfig } from 'umi'; import routes from './config/route' export default defineConfig({ nodeModulesTransform: { type: 'none', }, // routes: [ // { path: '/', component: '@/pages/index' }, // ], routes, fastRefresh: {}, layout: { name: '嗨购管理系统', logo: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fwww.lgstatic.com%2Fthumbnail_300x300%2Fi%2Fimage%2FM00%2F1C%2F95%2FCgpEMlkBrbuANDoAAABOTfWdJhc845.png&refer=http%3A%2F%2Fwww.lgstatic.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg' } });
四.配置基本路由
1.轮播图管理
src/pages/banner-manager/list.tsx
import * as React from 'react'; export interface IBannerListProps { } export default function BannerList (props: IBannerListProps) { return ( <div> 轮播图列表 </div> ); }
二级路由:子路由https://umijs.org/zh-CN/docs/routing#routes
export interface IChildRoute { // ***************** path: string name: string component: string } export interface IBestAFSRoute { // ***************** routes?: IChildRoute[] // Array<IChildRoute> path: string component?: string // ***************** name?: string // 兼容此写法 icon?: string // 更多功能查看 // https://beta-pro.ant.design/docs/advanced-menu // --- // 新页面打开 target?: string // 不展示顶栏 headerRender?: boolean // 不展示页脚 footerRender?: boolean // 不展示菜单 menuRender?: boolean // 不展示菜单顶栏 menuHeaderRender?: boolean // 权限配置,需要与 plugin-access 插件配合使用 access?: string // 隐藏子菜单 hideChildrenInMenu?: boolean // 隐藏自己和子菜单 hideInMenu?: boolean // 在面包屑中隐藏 hideInBreadcrumb?: boolean // 子项往上提,仍旧展示, flatMenu?: boolean } const routes: IBestAFSRoute[] = [ { path: '/', component: '@/pages/index' }, { path: '/home', icon: 'HomeOutlined', name: '首页', // 如果需要出现在左侧的菜单栏 component: '@/pages/home/index' }, { // 登录页面不需要侧边菜单栏等 path: '/login', // name: '登录', component: '@/pages/login/index', // 不展示顶栏 headerRender: false, // 不展示页脚 footerRender: false, // 不展示菜单 menuRender: false }, { path: '/banner', name: '轮播图管理', icon: 'FileImageOutlined', routes: [ { path: '/banner/list', name: '轮播图列表', component: '@/pages/banner-manager/list' } ] } ] export default routes
2.产品管理
src/pages/pro-manager/list.tsx
import * as React from 'react'; export interface IProListProps { } export default function ProList (props: IProListProps) { return ( <div> 产品列表 </div> ); }
src/pages/pro-manager/recommend.tsx
import * as React from 'react'; export interface IRecommendListProps { } export default function RecommendList (props: IRecommendListProps) { return ( <div> 推荐列表 </div> ); }
src/pages/pro-manager/seckill.tsx
import * as React from 'react'; export interface ISeclillListProps { } export default function SeclillList (props: ISeclillListProps) { return ( <div> 秒杀列表 </div> ); }
export interface IChildRoute { path: string name: string component: string } export interface IBestAFSRoute { routes?: IChildRoute[] // Array<IChildRoute> path: string component?: string name?: string // 兼容此写法 icon?: string // 更多功能查看 // https://beta-pro.ant.design/docs/advanced-menu // --- // 新页面打开 target?: string // 不展示顶栏 headerRender?: boolean // 不展示页脚 footerRender?: boolean // 不展示菜单 menuRender?: boolean // 不展示菜单顶栏 menuHeaderRender?: boolean // 权限配置,需要与 plugin-access 插件配合使用 access?: string // 隐藏子菜单 hideChildrenInMenu?: boolean // 隐藏自己和子菜单 hideInMenu?: boolean // 在面包屑中隐藏 hideInBreadcrumb?: boolean // 子项往上提,仍旧展示, flatMenu?: boolean } const routes: IBestAFSRoute[] = [ { path: '/', component: '@/pages/index' }, { path: '/home', icon: 'HomeOutlined', name: '首页', // 如果需要出现在左侧的菜单栏 component: '@/pages/home/index' }, { // 登录页面不需要侧边菜单栏等 path: '/login', // name: '登录', component: '@/pages/login/index', // 不展示顶栏 headerRender: false, // 不展示页脚 footerRender: false, // 不展示菜单 menuRender: false }, { path: '/banner', name: '轮播图管理', icon: 'FileImageOutlined', routes: [ { path: '/banner/list', name: '轮播图列表', component: '@/pages/banner-manager/list' } ] }, { // ********************************** path: '/pro', name: '产品管理', icon: 'UnorderedListOutlined', routes: [ { path: '/pro/list', name: '产品列表', component: '@/pages/pro-manager/list' }, { path: '/pro/recommend', name: '推荐列表', component: '@/pages/pro-manager/recommend' }, { path: '/pro/seckill', name: '秒杀列表', component: '@/pages/pro-manager/seckill' } ] } ] export default routes
五.登录页面实现
https://ant.design/components/form-cn/#components-form-demo-normal-login
https://procomponents.ant.design/components/form/#%E7%99%BB%E5%BD%95
移动端接口地址:http://121.89.205.189/apidoc/
pc后台管理系统:http://121.89.205.189/admindoc/
复制UI组件库的ts版本的代码,删除了 记住密码以及 忘记密码还有注册,删除了初始化的数据
根据接口文档修改了 字段的名称,修改了提示的语句
import React from 'react' import { Form, Input, Button, Checkbox } from 'antd'; import { UserOutlined, LockOutlined } from '@ant-design/icons'; const Login = () => { const onFinish = (values) => { console.log('Received values of form: ', values); }; return ( <Form name="normal_login" className="login-form" onFinish={onFinish} > <Form.Item name="adminname" rules={[ { required: true, message: '请输入管理员账号!', },x ]} > <Input prefix={<UserOutlined className="site-form-item-icon" />} placeholder="管理员账号" /> </Form.Item> <Form.Item name="password" rules={[ { required: true, message: '请输入管理员密码!', }, ]} > <Input prefix={<LockOutlined className="site-form-item-icon" />} type="password" placeholder="管理员密码" /> </Form.Item> <Form.Item> <Button type="primary" htmlType="submit" className="login-form-button"> 登录 </Button> </Form.Item> </Form> ); }; export default Login
构建登录页面的样式
同级目录创建index.less
修改样式方法一
#root { // 为了自定义的样式生效 height: 100%; .ant-design-pro { height: 100%; .ant-layout { height: 100%; .ant-layout { height: 100%; .ant-layout-content { height: 100%; .login_container { height: 100%; } } } } } } .login_container { width: 100%; // min-height: 500px; // height: 100%; background-color: cornflowerblue; display: flex; flex-direction: column; justify-content: center; align-items: center; .login_title { color: #fff; transform: translateY(-100px); } .login-form { transform: translateY(-100px); width: 30%; background-color: #fff; padding: 30px 20px } } @media (max-width: 960px) { .login_container { .login-form { width: 70%; } } }
修改样式方法二
src目录下创建global.less
https://umijs.org/zh-CN/docs/assets-css
Umi 中约定 src/global.css
为全局样式,如果存在此文件,会被自动引入到入口文件最前面。
#root { width: 100%; height: 100%; }
然而并没有生效,目标是 引入到入口文件最前面,既然自动失效,手动操作
app.tsx中引入less
import './global.less' ....
发现其实背景的高度应该是填充满整个屏幕,css控制不到,可以使用js控制
cnpm i ahooks -S目标:登录页面的背景填充满整个屏幕,实际上没有,审查元素得知 root 那一个层级就没有填充满整个屏幕
方式一:通过一层一层的css控制
方式二:压根不关心root,只需要获取到屏幕的高度,通过js设置登录容器的高度为屏幕的高度
import React from 'react' import { Form, Input, Button, Checkbox } from 'antd'; import { UserOutlined, LockOutlined } from '@ant-design/icons'; import { useMount } from 'ahooks' import './index.less' const Login = () => { const onFinish = (values) => { console.log('Received values of form: ', values); }; // js控制容器的高度 useMount(() => { // 获取屏幕的高度 let height: number = document.documentElement.offsetHeight console.log(height) // 修改样式 let login_container: any = document.querySelector('.login_container') login_container.style.height = height + 'px' }) return ( <div className="login_container"> <h1 className="login_title">嗨购后台管理系统</h1> <Form name="normal_login" className="login-form" onFinish={onFinish} > <Form.Item name="adminname" rules={[ { required: true, message: '请输入管理员账号!', }, ]} > <Input prefix={<UserOutlined className="site-form-item-icon" />} placeholder="管理员账号" /> </Form.Item> <Form.Item name="password" rules={[ { required: true, message: '请输入管理员密码!', }, ]} > <Input prefix={<LockOutlined className="site-form-item-icon" />} type="password" placeholder="管理员密码" /> </Form.Item> <Form.Item> <Button type="primary" htmlType="submit" className="login-form-button"> 登录 </Button> </Form.Item> </Form> </div> ); }; export default Login
如何给 表单添加验证规则
import React from 'react' import { Form, Input, Button, Checkbox } from 'antd'; import { UserOutlined, LockOutlined } from '@ant-design/icons'; import { useMount } from 'ahooks' import './index.less' const Login = () => { const onFinish = (values) => { console.log('Received values of form: ', values); }; // js控制容器的高度 useMount(() => { // 获取屏幕的高度 let height: number = document.documentElement.offsetHeight console.log(height) // 修改样式 let login_container: any = document.querySelector('.login_container') login_container.style.height = height + 'px' }) return ( <div className="login_container"> <h1 className="login_title">嗨购后台管理系统</h1> <Form name="normal_login" className="login-form" onFinish={onFinish} > <Form.Item name="adminname" rules={[ { required: true, pattern:/^[a-zA-Z0-9_-]{4,16}$/, message: '请输入4-16位有效字符,包含字母,数字,下划线以及减号!', }, ]} > <Input prefix={<UserOutlined className="site-form-item-icon" />} placeholder="管理员账号" /> </Form.Item> <Form.Item name="password" rules={[ { required: true, len: 6, // pattern: /^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])(?=\S*[!@#$%^&*? ])\S*$/, message: '请输入6位字符的密码!', }, ]} > <Input prefix={<LockOutlined className="site-form-item-icon" />} type="password" placeholder="管理员密码" /> </Form.Item> <Form.Item> <Button type="primary" htmlType="submit" className="login-form-button"> 登录 </Button> </Form.Item> </Form> </div> ); }; export default Login
六.封装axios
umi中必须使用axios请求数据吗?
不是,可以使用umi的插件: https://umijs.org/zh-CN/plugins/plugin-request
cnpm i axios -S
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, Method, AxiosError } from 'axios' const isDev: boolean = process.env.NODE_ENV === 'development' const ins: AxiosInstance = axios.create({ baseURL: isDev ? 'http://121.89.205.189/admin' : 'http://121.89.205.189/admin', timeout: 6000 }) // 请求拦截器 ins.interceptors.request.use(function (config: AxiosRequestConfig): AxiosRequestConfig { // 在发送请求之前做些什么 // 设定加载的进度条 / 统一传递token 信息(从本地获取) config.headers.common['token'] = '' return config; }, function (error: any): Promise<never> { // 对请求错误做些什么 return Promise.reject(error); }); // 添加响应拦截器 ins.interceptors.response.use(function (response: AxiosResponse<any>): AxiosResponse<any> { // 对响应数据做点什么 // 进度条消失 / 验证token的有效性 // if (response.data.code === '10119') { // 我的接口token失效 { code: '10119', message: 'token无效'} // //跳转到登录页面 // } return response; }, function (error: any): Promise<never> { // 对响应错误做点什么 return Promise.reject(error); }); // http://www.axios-js.com/zh-cn/docs/#axios-config // 自定义各种数据请求 axios({}) export default function request(config: AxiosRequestConfig): Promise<AxiosResponse<any>>{ let { url = '', method = 'GET', data = {}, headers = '' } = config // url = url || '' // method = method || 'get' // data = data || {} // headers = headers || '' // method 转换为大写 switch (method.toUpperCase()) { case 'GET': return ins.get(url, { params: data }) case 'POST': // 表单提交 application/x-www-form-url-encoded if (headers['content-type'] === 'application/x-www-form-url-encoded') { // 转参数 URLSearchParams/第三方库qs const p = new URLSearchParams() for(let key in data) { p.append(key, data[key]) } return ins.post(url, p, {headers}) } // 文件提交 multipart/form-data if (headers['content-type'] === 'multipart/form-data') { const p = new FormData() for(let key in data) { p.append(key, data[key]) } return ins.post(url, p, {headers}) } // 默认 application/json return ins.post(url, data) case 'PUT': // 修改数据 --- 所有的数据的更新 return ins.put(url, data) case 'DELETE': // 删除数据 return ins.delete(url, {data}) case 'PATCH': // 更新局部资源 return ins.patch(url, data) default: return ins(config) } }
封装数据请求 src/services/admin.ts
import request from '@/utils/request' export interface IAdminLogin { adminname: string password: string } export function login (params: IAdminLogin) { return request({ url: '/admin/login', method: 'POST', data: params }) }
七.dva数据流 - 状态管理器
-
内置 dva,默认版本是
^2.6.0-beta.20
,如果项目中有依赖,会优先使用项目中依赖的版本。 -
约定式的 model 组织方式,不用手动注册 model
-
文件名即 namespace,model 内如果没有声明 namespace,会以文件名作为 namespace
-
内置 dva-loading,直接 connect
loading
字段使用即可 -
支持 immer,通过配置
immer
开启
约定式的 model 组织方式
符合以下规则的文件会被认为是 model 文件,
-
src/models
下的文件 -
src/pages
下,子目录中 models 目录下的文件 -
src/pages
下,所有 model.ts 文件(不区分任何字母大小写)
1.配置
https://umijs.org/zh-CN/plugins/plugin-dva#%E9%85%8D%E7%BD%AE
// umirc.ts
import { defineConfig } from 'umi'; import routes from './config/route' export default defineConfig({ nodeModulesTransform: { type: 'none', }, // routes: [ // { path: '/', component: '@/pages/index' }, // ], routes, fastRefresh: {}, layout: { name: '嗨购管理系统', logo: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fwww.lgstatic.com%2Fthumbnail_300x300%2Fi%2Fimage%2FM00%2F1C%2F95%2FCgpEMlkBrbuANDoAAABOTfWdJhc845.png&refer=http%3A%2F%2Fwww.lgstatic.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg' }, dva: { immer: true, hmr: false } });
2.dva数据流引入
约定式的 model 组织方式
符合以下规则的文件会被认为是 model 文件,
-
src/models
下的文件 - 常用 -- 全局性的redux数据 -
src/pages
下,子目录中 models 目录下的文件 -
src/pages
下,所有 model.ts 文件(不区分任何字母大小写) - 常用 - 表示某个模块下的redux数据
运行时配置 - 通过 src/app.tsx
文件配置 dva 创建时的参数。
https://umijs.org/zh-CN/plugins/plugin-dva#dva-%E8%BF%90%E8%A1%8C%E6%97%B6%E9%85%8D%E7%BD%AE
cnpm i redux-logger @types/redux-logger -S // @types/redux-logger 为声明文件,声明文件其实就是 用来提示代码(数据类型)
import React from 'react'; import { BasicLayoutProps } from '@ant-design/pro-layout'; import './global.less' import { createLogger } from 'redux-logger'; import { message } from 'antd'; // 其实不需要运行时配置了,配置了会报错 // export const dva = { // config: { // onAction: createLogger(), // onError(e: Error) { // message.error(e.message, 3); // }, // }, // }; export const layout = (): BasicLayoutProps => { return { rightContentRender: () => <header>header</header>, footerRender: () => <footer>footer</footer> } }
3.配置登录模块dva
使用第一种使用dva的方式
src/models/admin.ts
import { Effect, ImmerReducer } from 'umi' export interface ILoginState { adminname: string token: string role: number } export interface ILoginModelInterface { namespace: 'admin' // 接口中的namespace 直接写模块的名称即可 state: ILoginState, effects: { loginReq: Effect }, reducers: { changeAdminName: ImmerReducer<ILoginState>, changeToken: ImmerReducer<ILoginState>, changeRole: ImmerReducer<ILoginState> } } const LoginModel: ILoginModelInterface = { namespace: 'admin', // 命名空间 --- 区分模块 state: { // 初始化的状态 adminname: '', token: '', role: 1 }, effects: { // 可以看作是 vuex中actions, 必须写成 generator 的写法 // payload 解构赋值 说明传入过来的参数是以对象形式传递的 // call 代表调用 异步操作的方法 // put 代表 类似于之前的 dispatch, 也可以看成vuex中acitons中的 commit *loginReq ({ payload }: any, { call, put }: any) {} }, reducers: { // 类似于vuex中的 mutations changeAdminName (state: ILoginState, action: any) { // 出发reducers 必须是以 对象的形式触发 state.adminname = action.payload }, changeToken (state: ILoginState, action: any) { state.token = action.payload }, changeRole (state: ILoginState, action: any) { state.role = action.payload } } } export default LoginModel
在effects中请求数据
import { Effect, ImmerReducer } from 'umi' import { login } from '@/services/admin' // ******************* export interface ILoginState { adminname: string token: string role: number } export interface ILoginModelInterface { namespace: 'admin' // 接口中的namespace 直接写模块的名称即可 state: ILoginState, effects: { loginReq: Effect }, reducers: { changeAdminName: ImmerReducer<ILoginState>, changeToken: ImmerReducer<ILoginState>, changeRole: ImmerReducer<ILoginState> } } const LoginModel: ILoginModelInterface = { namespace: 'admin', // 命名空间 --- 区分模块 state: { // 初始化的状态 adminname: '', token: '', role: 1 }, effects: { // 可以看作是 vuex中actions, 必须写成 generator 的写法 // payload 解构赋值 说明传入过来的参数是以对象形式传递的 // call 代表调用 异步操作的方法 // put 代表 类似于之前的 dispatch *loginReq ({ payload }: any, { call, put }: any) { console.log(11111) // 异步操作数据 必须通过yield 执行 // call(异步函数, 函数需要的参数) const res = yield call(login, payload) console.log(res) // 修改状态 ---- 必须使用 yield 表示代码的继续执行 // 使用时必须是对象形式 yield put({ type: 'changeAdminName', payload: res.data.data.adminname}) yield put({ type: 'changeToken', payload: res.data.data.token}) yield put({ type: 'changeRole', payload: res.data.data.role}) } }, reducers: { // 类似于vuex中的 mutations changeAdminName (state: ILoginState, action: any) { state.adminname = action.payload }, changeToken (state: ILoginState, action: any) { state.token = action.payload }, changeRole (state: ILoginState, action: any) { state.role = action.payload } } } export default LoginModel
登录页面测试登录接口
import React from 'react' import { Form, Input, Button, Checkbox } from 'antd'; import { UserOutlined, LockOutlined } from '@ant-design/icons'; import { useMount } from 'ahooks' import './index.less' import { IAdminLogin } from '@/services/admin'; // ********需要状态再引入********* import { connect } from 'umi' // 通过connect可以直接给组件提供状态,类似于redux的mapStateToProps const Login = ( props ) => { console.log(props) const onFinish = (values: IAdminLogin) => { console.log('Received values of form: ', values); // props属性中含有dispatch 函数,可以用来触发 相应。effects props.dispatch({ type: 'admin/loginReq', payload: values }) }; // js控制容器的高度 useMount(() => { // 获取屏幕的高度 let height: number = document.documentElement.offsetHeight console.log(height) // 修改样式 let login_container: any = document.querySelector('.login_container') login_container.style.height = height + 'px' }) return ( <div className="login_container"> <h1 className="login_title">嗨购后台管理系统</h1> ..... </Form> </div> ); }; export default connect((state) => { // 测试状态有没有被修改 console.log(state) return {} })(Login)
得到服务器的响应,如果登录成功,应该将信息保存到本地并且跳转到系统的首页
4.封装本地存储
原生cookie --- 回顾
// src/utils/cookie.ts interface SetCookieType { // :前的是函数参数的类型 // :后的是函数返回值的类型 (name: string, value: string, days: number): void } interface GetCookieType { // :前的是函数参数的类型 // :后的是函数返回值的类型 (name: string): undefined | string } interface RemoveCookieType { // :前的是函数参数的类型 // :后的是函数返回值的类型 (name: string): void } export const setCookie: SetCookieType = function (name, value, days) { let d = new Date(); d.setDate(d.getDate() + days) document.cookie = `${name}=${encodeURIComponent(value)};expires=${d};path=/` } export const getCookie: GetCookieType = function (name) { let arr = decodeURIComponent(document.cookie).split('; ') // 注意;后面的空格不能省略 for(let i = 0; i < arr.length; i++) { let newArr = arr[i].split('=') if (name === newArr[0]) { return newArr[1] } } } export const removeCookie: RemoveCookieType = function (name) { setCookie(name, '', -1) }
js-cookie模块 - 了解
https://www.npmjs.com/package/js-cookie
npm install js-cookie --save
src/utils/cookie.ts
import Cookie from 'js-cookie' // days 有效期以天为单位 // export const setCookie: (key: string, value: string, days: number) => void // = (key: string, value: string, days: number): void => { // Cookie.set(key, value, { expires: days }) // } export function setCookie (key: string, value: string, days: number): void { Cookie.set(key, value, { expires: days }) } // export const getCookie: (key: string) => undefined | string // = (key: string): undefined | string => { // return Cookie.get(key) // } export function getCookie (key: string): undefined | string { return Cookie.get(key) } // export const removeCookie: (key: string) => void // = (key: string): void => { // Cookie.remove(key) // } export function removeCookie (key: string): void { Cookie.remove(key) }
兼容性封装,如果可以使用localStorage,那么就用localStorage否则使用cookie
Src/utils/cookie.ts
import Cookie from 'js-cookie' export function setItem(key: string, value: string, days: number): void { if (window.localStorage) { localStorage.setItem(key, value) } else { Cookie.set(key, value, { expires: days }) } } export function getItem(key: string): string | undefined { if (window.localStorage) { return localStorage.getItem(key) || '' } else { return Cookie.get(key) } } export function removeItem(key: string): void { if (window.localStorage) { localStorage.removeItem(key) } else { Cookie.set(key, '', { expires: -1 }) } } export function clear(): void { if (window.localStorage) { localStorage.clear() } else { // 使用原生 var cookies = document.cookie.split(";"); for (var i = 0; i < cookies.length; i++) { var cookie = cookies[i]; var eqPos = cookie.indexOf("="); var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie; document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/"; } if (cookies.length > 0) { for (var i = 0; i < cookies.length; i++) { var cookie = cookies[i]; var eqPos = cookie.indexOf("="); var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie; var domain = location.host.substr(location.host.indexOf('.')); document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=" + domain; } } } }
5.保存状态跳转到系统首页
import { Effect, ImmerReducer, history } from 'umi' import { login } from '@/services/admin' import { message } from 'antd' import { setItem, getItem } from '@/utils/cookie' export interface ILoginState { adminname: string token: string role: string // ***************************** } export interface ILoginModelInterface { namespace: 'admin' // 接口中的namespace 直接写模块的名称即可 state: ILoginState, effects: { loginReq: Effect }, reducers: { changeAdminName: ImmerReducer<ILoginState>, changeToken: ImmerReducer<ILoginState>, changeRole: ImmerReducer<ILoginState> } } const LoginModel: ILoginModelInterface = { namespace: 'admin', // 命名空间 --- 区分模块 // // ***************************** state: { // 初始化的状态 --- 从本地存储提取,防止页面刷新状态丢失 adminname: getItem('adminname') || '', token: getItem('token') || '', role: getItem('role') || '1' }, effects: { // 可以看作是 vuex中actions, 必须写成 generator 的写法 // payload 解构赋值 说明传入过来的参数是以对象形式传递的 // call 代表调用 异步操作的方法 // put 代表 类似于之前的 dispatch *loginReq ({ payload }: any, { call, put }: any) { console.log(11111) // 异步操作数据 const res = yield call(login, payload) console.log(res) if (res.data.code === '10003') { // ***************************** message.error('密码错误') } else if (res.data.code === '10005') { message.error('没有此账号,与管理员联系') } else { // 修改状态 yield put({ type: 'changeAdminName', payload: res.data.data.adminname}) yield put({ type: 'changeToken', payload: res.data.data.token}) yield put({ type: 'changeRole', payload: res.data.data.role}) // 保存数据到本地 setItem('adminname', res.data.data.adminname, 7) setItem('token', res.data.data.token, 7) setItem('role', res.data.data.role, 7) // 跳转到系统首页 history.push('/home') } } }, reducers: { // 类似于vuex中的 mutations changeAdminName (state: ILoginState, action: any) { state.adminname = action.payload }, changeToken (state: ILoginState, action: any) { state.token = action.payload }, changeRole (state: ILoginState, action: any) { state.role = action.payload } } } export default LoginModel
7.首页测试状态
import * as React from 'react'; import { connect, ILoginState } from 'umi'; export interface IHomeProps { adminname: string } class Home extends React.PureComponent<IHomeProps> { public render() { return ( <div> 首页 - { this.props.adminname } </div> ); } } export default connect(({ admin }: any) => { return { adminname: admin.adminname } })(Home)
八.头部 - 退出
// components/RightHeader.tsx import * as React from 'react'; import { Button } from 'antd' import { removeItem } from '@/utils/cookie'; import { history, connect } from 'umi' interface IRightHeaderProps { adminname: string } const RightHeader: React.FunctionComponent<IRightHeaderProps> = (props) => { return ( <div> 欢迎您:{ props.adminname } <Button onClick = { () => { removeItem('adminname') removeItem('token') removeItem('role') history.replace('/login') }}> 退出 </Button> </div> ); }; export default connect(({admin}: any) => ({ adminname: admin.adminname }))(RightHeader);
import React from 'react'; import { BasicLayoutProps } from '@ant-design/pro-layout'; import RightHeader from './components/RightHeader' import './global.less' import { createLogger } from 'redux-logger'; import { message } from 'antd'; // 其实不需要运行时配置了,配置了会报错 // export const dva = { // config: { // onAction: createLogger(), // onError(e: Error) { // message.error(e.message, 3); // }, // }, // }; export const layout = (): BasicLayoutProps => { return { rightContentRender: () => <RightHeader />, footerRender: () => <footer>footer</footer> } }
优化登陆组件
import React from 'react' import { Form, Input, Button, Checkbox } from 'antd'; import { UserOutlined, LockOutlined } from '@ant-design/icons'; import { useMount } from 'ahooks' import './index.less' import { IAdminLogin } from '@/services/admin'; import { connect, ConnectRC } from 'umi' interface PageProps { } // ************************************ const Login: ConnectRC<PageProps> = ( { dispatch } ) => { // console.log(props) const onFinish = (values: IAdminLogin) => { console.log('Received values of form: ', values); // 可以直接dispatch effects 也可以直接 dispatch reducers dispatch({ type: 'admin/loginReq', payload: values }) }; // js控制容器的高度 useMount(() => { // 获取屏幕的高度 let height: number = document.documentElement.offsetHeight console.log(height) // 修改样式 let login_container: any = document.querySelector('.login_container') login_container.style.height = height + 'px' }) return ( <div className="login_container"> <h1 className="login_title">嗨购后台管理系统</h1> <Form name="normal_login" className="login-form" onFinish={onFinish} > <Form.Item name="adminname" rules={[ { required: true, pattern: /^[a-zA-Z0-9_-]{4,16}$/, message: '请输入4-16位有效字符,包含字母,数字,下划线以及减号!', }, ]} > <Input prefix={<UserOutlined className="site-form-item-icon" />} placeholder="管理员账号" /> </Form.Item> <Form.Item name="password" rules={[ { required: true, len: 6, // pattern: /^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])(?=\S*[!@#$%^&*? ])\S*$/, message: '请输入6位字符的密码!', }, ]} > <Input prefix={<LockOutlined className="site-form-item-icon" />} type="password" placeholder="管理员密码" /> </Form.Item> <Form.Item> <Button type="primary" htmlType="submit" className="login-form-button"> 登录 </Button> </Form.Item> </Form> </div> ); }; export default connect((state) => { console.log(state) return {} })(Login)
九.产品列表
1.新增一个声明文件 pro.d.ts
*.d.ts 称之为 ts中的声明文件, 写代码时可以自动提示 数据类型
Src/pages/pro-manager/pro.d.ts
/** * /** * { "banners": [], "proid": "pro_e374dae5-22d7-40d8-ade2-5aa6449624c3", "category": "手机", "brand": "Apple", "proname": "Apple***", "originprice": 4999, "sales": 4070000, "stock": 50000, "desc": "商品名称***", "issale": 0, "isrecommend": 0, "discount": 9, "isseckill": 1, "img1": "https://*.jpg", "img2": "https://*.jpg", "img3": "https://*.jpg", "img4": "https://*.jpg" }, */ export interface IPro { banners: Array<string> proid: string category: string brand: string proname: string originprice: number sales: number stock: number desc: string issale: number isrecommend: number discount: number isseckill: number img1: string img2: string img3: string img4: string }
2.创建 数据表格组件
// pro-manager/TableList.tsx import * as React from 'react'; import { ConnectRC } from 'umi'; import { Table } from 'antd' interface IAppProps { } const TableList = () => { const dataSource = [ { key: '1', name: '胡彦斌', age: 32, address: '西湖区湖底公园1号', }, { key: '2', name: '胡彦祖', age: 42, address: '西湖区湖底公园1号', }, ]; const columns = [ { title: '姓名', dataIndex: 'name', key: 'name', }, { title: '年龄', dataIndex: 'age', key: 'age', }, { title: '住址', dataIndex: 'address', key: 'address', }, ]; return ( <Table dataSource={dataSource} columns={columns} /> ); }; export default TableList;
// /pro-manager/list.tsx import * as React from 'react'; import TableList from './TableList' export interface IProListProps { } export default function ProList (props: IProListProps) { return ( <div> <TableList /> </div> ); }
先设定一下表格的表头
import * as React from 'react'; import { Table } from 'antd' import { IPro } from './pro'; interface IAppProps { dataSource: Array<IPro> } const TableList = (props: IAppProps) => { const { dataSource } = props const columns = [ { title: '序号' }, { title: '产品分类', dataIndex: 'category' // 产品对应的字段 }, { title: '产品品牌', dataIndex: 'brand' // 产品对应的字段 }, { title: '产品名称', dataIndex: 'proname' // 产品对应的字段 }, { title: '产品图片', dataIndex: 'img1' }, { title: '产品原价', dataIndex: 'originprice' }, { title: '折扣', dataIndex: 'discount' }, { title: '销量', dataIndex: 'sales' }, { title: '库存', dataIndex: 'stock' }, { title: '是否售卖', dataIndex: 'issale' }, { title: '是否推荐', dataIndex: 'isrecommend' }, { title: '是否秒杀', dataIndex: 'isseckill' }, { title: '操作', }, ]; return ( <Table dataSource={dataSource} columns={columns} /> ); }; export default TableList;
3.列表使用表格组件
import * as React from 'react'; import TableList from './TableList' export interface IProListProps { } export default function ProList (props: IProListProps) { return ( <div> <TableList dataSource = { [] }/> </div> ); }
4.构建关于产品管理的dva数据流
dva数据流有三种使用方式
-
Src/models/*.ts. * 要和定义的模块的namespace保持一致。 ------ 登录数据流
-
src/pages/models/*.ts
//src/pages/pro-manager/models/.ts
import { IPro } from '../pro' import { Effect, ImmerReducer } from 'umi' export interface IProState { proList: IPro[], recommendList: IPro[], seckillList: IPro[] } export interface IProModelInterface { namespace: 'pro', state: IProState, effects: { getProListEffect: Effect, getRecommendListEffect: Effect, getSeckillListEffect: Effect }, reducers: { changeProList: ImmerReducer<IProState>, changeRecommendList: ImmerReducer<IProState>, changeSeckillList: ImmerReducer<IProState>, } } const proModel: IProModelInterface = { namespace: 'pro', state: { proList: [], recommendList: [], seckillList: [] }, effects: { * getProListEffect ({ payload } : any, { call, put}: any) { }, * getRecommendListEffect ({ payload } : any, { call, put}: any) { }, * getSeckillListEffect ({ payload } : any, { call, put}: any) { } }, reducers: { changeProList (state, action) { state.proList = action.payload }, changeRecommendList (state, action) { state.recommendList = action.payload }, changeSeckillList (state, action) { state.seckillList = action.payload } } } export default proModel
5.封装产品相关的数据请求
//src/services/pro.ts
import request from './../utils/request' export interface IProListParams{ count: number limitNum: number } export function getProListReq (params: IProListParams) { return request({ url: '/pro/list', data: params }) } export function getRecommendListReq () { return request({ url: '/pro/showdata', data: { type: 'isrecommend', flag: 1 } }) } export function getSeckillListReq () { return request({ url: '/pro/showdata', data: { type: 'isseckill', flag: 1 } }) }
进一步添加dva数据流的操作
import { IPro } from '../pro' import { Effect, ImmerReducer } from 'umi' import { getProListReq, getRecommendListReq, getSeckillListReq } from '@/services/pro' export interface IProState { proList: IPro[], recommendList: IPro[], seckillList: IPro[] } export interface IProModelInterface { namespace: 'pro', state: IProState, effects: { getProListEffect: Effect, getRecommendListEffect: Effect, getSeckillListEffect: Effect }, reducers: { changeProList: ImmerReducer<IProState>, changeRecommendList: ImmerReducer<IProState>, changeSeckillList: ImmerReducer<IProState>, } } const proModel: IProModelInterface = { namespace: 'pro', state: { proList: [], recommendList: [], seckillList: [] }, effects: { * getProListEffect ({ payload } : any, { call, put}: any) { const res = yield call(getProListReq, payload) yield put({ type: 'changeProList', payload: res.data.data }) }, * getRecommendListEffect ({ payload } : any, { call, put}: any) { const res = yield call(getRecommendListReq, payload) yield put({ type: 'changeRecommendList', payload: res.data.data }) }, * getSeckillListEffect ({ payload } : any, { call, put}: any) { const res = yield call(getSeckillListReq, payload) yield put({ type: 'changeSeckillList', payload: res.data.data }) } }, reducers: { changeProList (state, action) { state.proList = action.payload }, changeRecommendList (state, action) { state.recommendList = action.payload }, changeSeckillList (state, action) { state.seckillList = action.payload } } } export default proModel
6.产品列表页面请求数据并且展示
// src/pages/pro-manager/list.tsx
import * as React from 'react'; import TableList from './TableList' import { useMount } from 'ahooks'; import { connect, ConnectRC } from 'umi'; import { IPro } from './pro'; export interface IProListProps { proList: IPro[] } const ProList: ConnectRC<IProListProps> = (props) => { useMount(() => { props.dispatch({ type: 'pro/getProListEffect', payload: { limitNum: 200 } }) }) return ( <div> <TableList dataSource = { props.proList }/> </div> ); } export default connect((state: any) => { return { proList: state.pro.proList } })(ProList)
7.调整数据表格
render | 生成复杂数据的渲染函数,参数分别为当前行的值,当前行数据,行索引,@return 里面可以设置表格行/列合并 | function(text, record, index) {} | |
---|---|---|---|
import * as React from 'react'; import { Table, Image, Space, Button, Switch } from 'antd' import { IPro } from './pro'; interface IAppProps { dataSource: Array<IPro> } const TableList = (props: IAppProps) => { const { dataSource } = props const columns = [ { title: '序号', // 注意render 函数返回的是 React的Node节点 render: (text: any, record: IPro, index: number): React.ReactNode => { return <span>{ index + 1 }</span> } }, { title: '产品分类', dataIndex: 'category' // 产品对应的字段 }, { title: '产品品牌', dataIndex: 'brand' // 产品对应的字段 }, { title: '产品名称', dataIndex: 'proname' // 产品对应的字段 }, { title: '产品图片', dataIndex: 'img1', render: (text: string, record: IPro, index: number): React.ReactNode => { return ( <Image width = { 100 } src = { text } /> ) } }, { title: '产品原价', dataIndex: 'originprice' }, { title: '折扣', dataIndex: 'discount' }, { title: '销量', dataIndex: 'sales' }, { title: '库存', dataIndex: 'stock' }, { title: '是否售卖', dataIndex: 'issale', render (text: number, record: IPro, index: number): React.ReactNode { return ( <Switch checked = { text === 1 } disabled = { text === 0 }/> ) } }, { title: '是否推荐', dataIndex: 'isrecommend', render (text: number, record: IPro, index: number): React.ReactNode { return ( <Switch checked = { text === 1 } disabled = { text === 0 }/> ) } }, { title: '是否秒杀', dataIndex: 'isseckill', render (text: number, record: IPro, index: number): React.ReactNode { return ( <Switch checked = { text === 1 } disabled = { text === 0 }/> ) } }, { title: '操作', render: (text: any, record: IPro, index: number): React.ReactNode => { return ( <Space> <Button type="primary">编辑</Button> <Button danger>删除</Button> </Space> ) } }, ]; return ( <Table dataSource={dataSource} columns={columns} rowKey = "proid"/> ); }; export default TableList;
优化数据表格 --- 固定头和列
import * as React from 'react'; import { Table, Image, Space, Button, Switch } from 'antd' import { IPro } from './pro'; interface IAppProps { dataSource: Array<IPro> } const TableList = (props: IAppProps) => { const { dataSource } = props const columns: any = [ { title: '序号', fixed: 'left', width: 100, align: 'center', // 注意render 函数返回的是 React的Node节点 render: (text: any, record: IPro, index: number): React.ReactNode => { return <span>{ index + 1 }</span> } }, { title: '产品名称', fixed: 'left', width: 150, align: 'center', dataIndex: 'proname' // 产品对应的字段 }, { title: '产品图片', dataIndex: 'img1', fixed: 'left', width: 180, align: 'center', render: (text: string, record: IPro, index: number): React.ReactNode => { return ( <Image width = { 100 } src = { text } /> ) } }, { title: '产品分类', width: 150, align: 'center', dataIndex: 'category' // 产品对应的字段 }, { title: '产品品牌', width: 150, align: 'center', dataIndex: 'brand' // 产品对应的字段 }, { title: '产品原价', align: 'center', dataIndex: 'originprice' }, { title: '折扣', align: 'center', dataIndex: 'discount' }, { title: '销量', align: 'center', dataIndex: 'sales' }, { title: '库存', align: 'center', dataIndex: 'stock' }, { title: '是否售卖', fixed: 'right', width: 100, align: 'center', dataIndex: 'issale', render (text: number, record: IPro, index: number): React.ReactNode { return ( <Switch checked = { text === 1 } disabled = { text === 0 }/> ) } }, { title: '是否推荐', fixed: 'right', width: 100, align: 'center', dataIndex: 'isrecommend', render (text: number, record: IPro, index: number): React.ReactNode { return ( <Switch checked = { text === 1 } disabled = { text === 0 }/> ) } }, { title: '是否秒杀', fixed: 'right', width: 100, align: 'center', dataIndex: 'isseckill', render (text: number, record: IPro, index: number): React.ReactNode { return ( <Switch checked = { text === 1 } disabled = { text === 0 }/> ) } }, { title: '操作', fixed: 'right', width: 200, align: 'center', render: (text: any, record: IPro, index: number): React.ReactNode => { return ( <Space> <Button type="primary">编辑</Button> <Button danger>删除</Button> </Space> ) } }, ]; return ( <Table dataSource={dataSource} columns={columns} rowKey = "proid" scroll={{ x: 1800, y: 800 }}/> ); }; export default TableList;
8.数据的排序
https://ant.design/components/table-cn/#components-table-demo-head
主要思想就是给列的属性添加 sorter 函数
import * as React from 'react'; import { Table, Image, Space, Button, Switch } from 'antd' import { IPro } from './pro'; interface IAppProps { dataSource: Array<IPro> } const TableList = (props: IAppProps) => { const { dataSource } = props const columns: any = [ { title: '序号', fixed: 'left', width: 100, align: 'center', // 注意render 函数返回的是 React的Node节点 render: (text: any, record: IPro, index: number): React.ReactNode => { return <span>{ index + 1 }</span> } }, { title: '产品名称', fixed: 'left', width: 150, align: 'center', dataIndex: 'proname' // 产品对应的字段 }, { title: '产品图片', dataIndex: 'img1', fixed: 'left', width: 180, align: 'center', render: (text: string, record: IPro, index: number): React.ReactNode => { return ( <Image width = { 100 } src = { text } /> ) } }, { title: '产品分类', width: 150, align: 'center', dataIndex: 'category' // 产品对应的字段 }, { title: '产品品牌', width: 150, align: 'center', dataIndex: 'brand' // 产品对应的字段 }, { title: '产品原价', align: 'center', sorter (a: IPro, b: IPro) { return a.originprice - b.originprice }, dataIndex: 'originprice' }, { title: '折扣', align: 'center', sorter (a: IPro, b: IPro) { return a.discount - b.discount }, dataIndex: 'discount' }, { title: '销量', align: 'center', sorter (a: IPro, b: IPro) { return a.sales - b.sales }, dataIndex: 'sales' }, { title: '库存', align: 'center', sorter (a: IPro, b: IPro) { return a.stock - b.stock }, dataIndex: 'stock' }, { title: '是否售卖', fixed: 'right', width: 100, align: 'center', dataIndex: 'issale', render (text: number, record: IPro, index: number): React.ReactNode { return ( <Switch checked = { text === 1 } disabled = { text === 0 }/> ) } }, { title: '是否推荐', fixed: 'right', width: 100, align: 'center', dataIndex: 'isrecommend', render (text: number, record: IPro, index: number): React.ReactNode { return ( <Switch checked = { text === 1 } disabled = { text === 0 }/> ) } }, { title: '是否秒杀', fixed: 'right', width: 100, align: 'center', dataIndex: 'isseckill', render (text: number, record: IPro, index: number): React.ReactNode { return ( <Switch checked = { text === 1 } disabled = { text === 0 }/> ) } }, { title: '操作', fixed: 'right', width: 200, align: 'center', render: (text: any, record: IPro, index: number): React.ReactNode => { return ( <Space> <Button type="primary">编辑</Button> <Button danger>删除</Button> </Space> ) } }, ]; return ( <Table dataSource={dataSource} columns={columns} rowKey = "proid" scroll={{ x: 1800, y: 800 }}/> ); }; export default TableList;
自定义排序
s r c/pages/pro-manager/list.tsx
import * as React from 'react'; import TableList from './TableList' import { useMount } from 'ahooks'; import { connect, ConnectRC } from 'umi'; import { IPro } from './pro'; import { Button } from 'antd' export interface IProListProps { proList: IPro[] } const ProList: ConnectRC<IProListProps> = (props) => { useMount(() => { props.dispatch({ type: 'pro/getProListEffect', payload: { limitNum: 200 } }) }) return ( <div> <Button onClick = { () => { // 深拷贝数据 ********************************* const arr = JSON.parse(JSON.stringify(props.proList)) arr.sort((a: IPro, b: IPro) => { return a.originprice - b.originprice }) // 修改状态管理器数据 props.dispatch({ type: 'pro/changeProList', payload: arr }) }}>价格升序</Button> <Button onClick = { () => { // 深拷贝数据 const arr = JSON.parse(JSON.stringify(props.proList)) arr.sort((a: IPro, b: IPro) => { return b.originprice - a.originprice }) // 修改状态管理器数据 props.dispatch({ type: 'pro/changeProList', payload: arr }) }}>价格降序</Button> <TableList dataSource = { props.proList }/> </div> ); } export default connect((state: any) => { return { proList: state.pro.proList } })(ProList)
9.修改滑动开关的样式
Src/globle.less
#root { height: 100%; } .ant-table-wrapper { padding: 20px; } .switch_sales.ant-switch-checked { background-color: red; } .switch_recommend.ant-switch-checked { background-color: yellow; } .switch_seckill.ant-switch-checked { background-color: blue; }
10.配置页码以及显示的序号
注意table组件的 分页器选项以及 分页器组件的属性表
https://ant.design/components/pagination-cn/
import * as React from 'react'; import { Table, Image, Space, Button, Switch } from 'antd' import { IPro } from './pro'; interface IAppProps { dataSource: Array<IPro> } const TableList = (props: IAppProps) => { const { dataSource } = props const [ current, setCurrent ] = React.useState(1) const [ pageSize, setPageSize ] = React.useState(10) const columns: any = [ { title: '序号', fixed: 'left', width: 100, align: 'center', // 注意render 函数返回的是 React的Node节点 render: (text: any, record: IPro, index: number): React.ReactNode => { // 根据页码和每页显示个数计算出 序号 *********************************** return <span>{ (current - 1) * pageSize + index + 1 }</span> } }, { title: '产品名称', fixed: 'left', width: 150, align: 'center', dataIndex: 'proname' // 产品对应的字段 }, { title: '产品图片', dataIndex: 'img1', fixed: 'left', width: 180, align: 'center', render: (text: string, record: IPro, index: number): React.ReactNode => { return ( <Image width = { 100 } src = { text } /> ) } }, { title: '产品分类', width: 150, align: 'center', dataIndex: 'category' // 产品对应的字段 }, { title: '产品品牌', width: 150, align: 'center', dataIndex: 'brand' // 产品对应的字段 }, { title: '产品原价', align: 'center', sorter (a: IPro, b: IPro) { return a.originprice - b.originprice }, dataIndex: 'originprice' }, { title: '折扣', align: 'center', sorter (a: IPro, b: IPro) { return a.discount - b.discount }, dataIndex: 'discount' }, { title: '销量', align: 'center', sorter (a: IPro, b: IPro) { return a.sales - b.sales }, dataIndex: 'sales' }, { title: '库存', align: 'center', sorter (a: IPro, b: IPro) { return a.stock - b.stock }, dataIndex: 'stock' }, { title: '是否售卖', fixed: 'right', width: 100, align: 'center', dataIndex: 'issale', render (text: number, record: IPro, index: number): React.ReactNode { return ( <Switch className = "switch_sales" checked = { text === 1 } disabled = { text === 0 }/> ) } }, { title: '是否推荐', fixed: 'right', width: 100, align: 'center', dataIndex: 'isrecommend', render (text: number, record: IPro, index: number): React.ReactNode { return ( <Switch className = "switch_recommend" checked = { text === 1 } disabled = { text === 0 }/> ) } }, { title: '是否秒杀', fixed: 'right', width: 100, align: 'center', dataIndex: 'isseckill', render (text: number, record: IPro, index: number): React.ReactNode { return ( <Switch className = "switch_seckill" checked = { text === 1 } disabled = { text === 0 }/> ) } }, { title: '操作', fixed: 'right', width: 200, align: 'center', render: (text: any, record: IPro, index: number): React.ReactNode => { return ( <Space> <Button type="primary">编辑</Button> <Button danger>删除</Button> </Space> ) } }, ]; return ( <Table dataSource={dataSource} columns={columns} rowKey = "proid" scroll={{ x: 1800, y: 700 }} pagination = { { // **************************** total: dataSource.length, position: ["topRight", 'bottomLeft'], current, pageSize, showQuickJumper: true, hideOnSinglePage: true, pageSizeOptions: ['10', '30', '60', '100', '150'], onChange (page: number): void { // 页码变化 setCurrent(page) }, onShowSizeChange (current, size) { // 每页显示个数的变化 setPageSize(size) }, showTotal (total) { return '共' + total + '条数据' } } } /> ); }; export default TableList;
11.给产品列表添加搜索表单
import * as React from 'react'; import TableList from './TableList' import { useMount } from 'ahooks'; import { connect, ConnectRC } from 'umi'; import { IPro } from './pro'; import { Button, Select, Input } from 'antd' const { Option } = Select export interface IProListProps { proList: IPro[] } const ProList: ConnectRC<IProListProps> = (props) => { useMount(() => { props.dispatch({ type: 'pro/getProListEffect', payload: { limitNum: 200 } }) }) const searchPro = () => { } return ( <div> <Button onClick = { () => { // 深拷贝数据 const arr = JSON.parse(JSON.stringify(props.proList)) arr.sort((a: IPro, b: IPro) => { return a.originprice - b.originprice }) // 修改状态管理器数据 props.dispatch({ type: 'pro/changeProList', payload: arr }) }}>价格升序</Button> <Button onClick = { () => { // 深拷贝数据 const arr = JSON.parse(JSON.stringify(props.proList)) arr.sort((a: IPro, b: IPro) => { return b.originprice - a.originprice }) // 修改状态管理器数据 props.dispatch({ type: 'pro/changeProList', payload: arr }) }}>价格降序</Button> <div className = "fiterdata"> <Select defaultValue="" style={{ width: 120 }} allowClear> <Option value="">全部</Option> </Select> <Input style={{ width: 300}} placeholder="请输入关键词"></Input> <Button type="primary" onClick = { searchPro }>搜索</Button> </div> <TableList dataSource = { props.proList }/> </div> ); } export default connect((state: any) => { return { proList: state.pro.proList } })(ProList)
12.筛选商品
基本不使用状态管理器,使用组件自己的状态
封装接口
import request from './../utils/request' export interface IProListParams{ count: number limitNum: number } export function getProListReq (params: IProListParams) { return request({ url: '/pro/list', data: params }) } export function getRecommendListReq () { return request({ url: '/pro/showdata', data: { type: 'isrecommend', flag: 1 } }) } export function getSeckillListReq () { return request({ url: '/pro/showdata', data: { type: 'isseckill', flag: 1 } }) } export function getCategoryReq () { return request({ url: '/pro/getCategory' }) } export function getSearchListReq (params: { category: string, search: string}) { return request({ url: '/pro/searchPro', data: params, method: 'POST' }) }
实现效果
import * as React from 'react'; import TableList from './TableList' import { useMount } from 'ahooks'; import { connect, ConnectRC } from 'umi'; import { IPro } from './pro'; import { Button, Select, Input } from 'antd' import { getCategoryReq, getSearchListReq } from '@/services/pro'; const { Option } = Select export interface IProListProps { proList: IPro[] } const ProList: ConnectRC<IProListProps> = (props) => { useMount(() => { props.dispatch({ type: 'pro/getProListEffect', payload: { limitNum: 200 } }) }) // 设定下拉选择框的数据 const [list, setList] = React.useState([]) // 数据库中的分类数据 const [category, setCategory] = React.useState('') // 用户选中的分类数据 const [search, setSearch] = React.useState('') // 用户输入的关键词 // 请求分类的数据 useMount(() => { getCategoryReq().then(res => { setList(res.data.data) }) }) const searchPro = () => { getSearchListReq({category, search}).then(res => { // 产品列表数据在dva数据流 props.dispatch({ type: 'pro/changeProList', payload: res.data.data }) }) } return ( <div> <Button onClick = { () => { // 深拷贝数据 const arr = JSON.parse(JSON.stringify(props.proList)) arr.sort((a: IPro, b: IPro) => { return a.originprice - b.originprice }) // 修改状态管理器数据 props.dispatch({ type: 'pro/changeProList', payload: arr }) }}>价格升序</Button> <Button onClick = { () => { // 深拷贝数据 const arr = JSON.parse(JSON.stringify(props.proList)) arr.sort((a: IPro, b: IPro) => { return b.originprice - a.originprice }) // 修改状态管理器数据 props.dispatch({ type: 'pro/changeProList', payload: arr }) }}>价格降序</Button> <div className = "fiterdata"> <Select defaultValue="" style={{ width: 120 }} allowClear onChange = { (value) => { console.log(value) setCategory(value) }} value={ category }> <Option value="">全部</Option> { list.map((item,index) => { return <Option value={item} key = { index }>{item}</Option> }) } </Select> <Input value = { search } onChange = { (e) => { setSearch(e.target.value) }} style={{ width: 300}} placeholder="请输入关键词"></Input> <Button type="primary" onClick = { searchPro }>搜索</Button> </div> <TableList dataSource = { props.proList }/> </div> ); } export default connect((state: any) => { return { proList: state.pro.proList } })(ProList)
十.推荐列表以及秒杀列表实现
开关设置
十一.权限管理
设置项目的重定向
export interface IChildRoute { path: string name: string component: string } export interface IBestAFSRoute { routes?: IChildRoute[] // Array<IChildRoute> path: string redirect?: string, exact?: boolean, component?: string name?: string // 兼容此写法 icon?: string // 更多功能查看 // https://beta-pro.ant.design/docs/advanced-menu // --- // 新页面打开 target?: string // 不展示顶栏 headerRender?: boolean // 不展示页脚 footerRender?: boolean // 不展示菜单 menuRender?: boolean // 不展示菜单顶栏 menuHeaderRender?: boolean // 权限配置,需要与 plugin-access 插件配合使用 access?: string // 隐藏子菜单 hideChildrenInMenu?: boolean // 隐藏自己和子菜单 hideInMenu?: boolean // 在面包屑中隐藏 hideInBreadcrumb?: boolean // 子项往上提,仍旧展示, flatMenu?: boolean } const routes: IBestAFSRoute[] = [ { path: '/', exact: true, redirect: '/home' }, { path: '/home', icon: 'HomeOutlined', name: '首页', // 如果需要出现在左侧的菜单栏 component: '@/pages/home/index' }, { // 登录页面不需要侧边菜单栏等 path: '/login', // name: '登录', component: '@/pages/login/index', // 不展示顶栏 headerRender: false, // 不展示页脚 footerRender: false, // 不展示菜单 menuRender: false }, { path: '/banner', name: '轮播图管理', icon: 'FileImageOutlined', routes: [ { path: '/banner/list', name: '轮播图列表', component: '@/pages/banner-manager/list' } ] }, { path: '/pro', name: '产品管理', icon: 'UnorderedListOutlined', routes: [ { path: '/pro/list', name: '产品列表', component: '@/pages/pro-manager/list' }, { path: '/pro/recommend', name: '推荐列表', component: '@/pages/pro-manager/recommend' }, { path: '/pro/seckill', name: '秒杀列表', component: '@/pages/pro-manager/seckill' } ] } ] export default routes
1.请求管理员的登录状态接口
本接口文档没有专门的验证用户登录状态的接口,使用 查看管理员信息的接口代替专门的接口
请求的数据的长度大于0 表示用户登录的
//service/index.ts
import request from '@/utils/request' import { getItem } from '@/utils/cookie' // 不要使用常量或者变量,只会执行一次 // const adminname = getItem('adminname') || 'safjkhsdgjhsdjhsdgfjdshgfjhdsgfds' export const checkAuthReq = () => { // 通过获取管理员的信息接口去判断有无登录 // 真实情况应该是 调用 获取管理员登录状态的 接口去判断 // 如果输出的数据的长度大于0 认为是登录的 return request({ url: '/admin/detail', method: 'GET', data: { adminname: getItem('adminname') || 'safjkhsdgjhsdjhsdgfjdshgfjhdsgfds' } }) }
2.封装验证用户的登录状态的hooks
// src/hooks/useAuth.tsx
import React from 'react' import { useRequest } from 'umi' import { checkAuthReq } from '@/services/index' export default () => { const test = useRequest(() => checkAuthReq()) console.log('11111', test) const { loading, error, data } = test if (loading) { return 1 } if (error) { return 2 } return data }
3.设定权限校验的组件 auth.tsx
Src/wrappers/auth.tsx
import React from 'react' import { Redirect } from 'umi' import useAuth from '@/hooks/useAuth' export default (props: any) => { const data = useAuth(); console.log('222', data) if (data === 1) { return <div>正在加载...</div>; } else if (data === 2) { return <div>出错了...</div>; } else { console.log('333', data) if (data.data.length !== 0) { return <div>{ props.children }</div>; } else { return <Redirect to="/login" />; } } }
4.配置路由的权限
export interface IChildRoute { path: string name: string component: string, wrappers?: string[] } export interface IBestAFSRoute { routes?: IChildRoute[] // Array<IChildRoute> path: string redirect?: string, exact?: boolean, component?: string name?: string // 兼容此写法 icon?: string // 更多功能查看 // https://beta-pro.ant.design/docs/advanced-menu // --- // 新页面打开 target?: string // 不展示顶栏 headerRender?: boolean // 不展示页脚 footerRender?: boolean // 不展示菜单 menuRender?: boolean // 不展示菜单顶栏 menuHeaderRender?: boolean // 权限配置,需要与 plugin-access 插件配合使用 access?: string // 隐藏子菜单 hideChildrenInMenu?: boolean // 隐藏自己和子菜单 hideInMenu?: boolean // 在面包屑中隐藏 hideInBreadcrumb?: boolean // 子项往上提,仍旧展示, flatMenu?: boolean, wrappers?: string[] } const routes: IBestAFSRoute[] = [ { path: '/', exact: true, redirect: '/home' }, { path: '/home', icon: 'HomeOutlined', name: '首页', // 如果需要出现在左侧的菜单栏 component: '@/pages/home/index' }, { // 登录页面不需要侧边菜单栏等 path: '/login', // name: '登录', component: '@/pages/login/index', // 不展示顶栏 headerRender: false, // 不展示页脚 footerRender: false, // 不展示菜单 menuRender: false }, { path: '/banner', name: '轮播图管理', icon: 'FileImageOutlined', routes: [ { path: '/banner/list', name: '轮播图列表', wrappers: [ '@/wrappers/auth', ], component: '@/pages/banner-manager/list' } ] }, { path: '/pro', name: '产品管理', icon: 'UnorderedListOutlined', routes: [ { path: '/pro/list', name: '产品列表', wrappers: [ '@/wrappers/auth', ], component: '@/pages/pro-manager/list' }, { path: '/pro/recommend', name: '推荐列表', wrappers: [ '@/wrappers/auth', ], component: '@/pages/pro-manager/recommend' }, { path: '/pro/seckill', name: '秒杀列表', wrappers: [ '@/wrappers/auth', ], component: '@/pages/pro-manager/seckill' } ] } ] export default routes
5.如果某些页面需要超级管理员才能访问
页面权限
s r c/wrappers/role.tsx
import React from 'react' import { Redirect } from 'umi' import useAuth from '@/hooks/useAuth' export default (props: any) => { const data = useAuth(); console.log('222', data) if (data === 1) { return <div>正在加载...</div>; } else if (data === 2) { return <div>出错了...</div>; } else { console.log('333', data) if (data.data.length !== 0) { if (data.data[0].role > 1) { // ****************** return <div>{ props.children }</div>; } else { // return <Redirect to="/nopermission" />; return <div>无权限</div> } } else { return <Redirect to="/login" />; } } }
export interface IChildRoute { path: string name: string component: string, wrappers?: string[] } export interface IBestAFSRoute { routes?: IChildRoute[] // Array<IChildRoute> path: string redirect?: string, exact?: boolean, component?: string name?: string // 兼容此写法 icon?: string // 更多功能查看 // https://beta-pro.ant.design/docs/advanced-menu // --- // 新页面打开 target?: string // 不展示顶栏 headerRender?: boolean // 不展示页脚 footerRender?: boolean // 不展示菜单 menuRender?: boolean // 不展示菜单顶栏 menuHeaderRender?: boolean // 权限配置,需要与 plugin-access 插件配合使用 access?: string // 隐藏子菜单 hideChildrenInMenu?: boolean // 隐藏自己和子菜单 hideInMenu?: boolean // 在面包屑中隐藏 hideInBreadcrumb?: boolean // 子项往上提,仍旧展示, flatMenu?: boolean, wrappers?: string[], role?: number } const routes: IBestAFSRoute[] = [ { path: '/', exact: true, redirect: '/home' }, { path: '/home', icon: 'HomeOutlined', name: '首页', // 如果需要出现在左侧的菜单栏 component: '@/pages/home/index' }, { // 登录页面不需要侧边菜单栏等 path: '/login', // name: '登录', component: '@/pages/login/index', // 不展示顶栏 headerRender: false, // 不展示页脚 footerRender: false, // 不展示菜单 menuRender: false }, { path: '/banner', name: '轮播图管理', icon: 'FileImageOutlined', routes: [ { path: '/banner/list', name: '轮播图列表', wrappers: [ '@/wrappers/auth', ], component: '@/pages/banner-manager/list' } ] }, { path: '/pro', name: '产品管理', icon: 'UnorderedListOutlined', routes: [ { path: '/pro/list', name: '产品列表', wrappers: [ '@/wrappers/role', ], component: '@/pages/pro-manager/list' }, { path: '/pro/recommend', name: '推荐列表', wrappers: [ '@/wrappers/role', ], component: '@/pages/pro-manager/recommend' }, { path: '/pro/seckill', name: '秒杀列表', wrappers: [ '@/wrappers/role', ], component: '@/pages/pro-manager/seckill' } ] } ] export default routes
如果在设置权限是,可以在wrappers下写多个文件,在定义路由规则时使用不同文件即可
十二.轮播图相关
Src/pages/banner-manager/banner.d.ts
export interface IBanner { bannerid: string img: string alt: string link: string flag: boolean }
Src/pages/banner-manager/list.tsx
import * as React from 'react'; import { Table, Button, Image, Space } from 'antd' import { IBanner } from './banner'; export interface IBannerListProps { bannerList: IBanner[] } export default function BannerList (props: IBannerListProps) { const { bannerList } = props const columns = [ { title: '序号', render (text: any, record: IBanner, index: number) { return <span>{ index + 1}</span> } }, { title: '链接', dataIndex: 'link' }, { title: '描述', dataIndex: 'alt' }, { title: '图片', dataIndex: 'img', render (text: string, record: IBanner, index: number) { return ( <Image src = { text} width="200" /> ) } }, { title: '操作', render (text: any, record: IBanner, index: number) { return ( <Space> <Button type="primary">编辑</Button> <Button danger>删除</Button> </Space> ) } } ] return ( <div> <Button onClick = { () => { }}>添加轮播图</Button> <Table dataSource = { bannerList } columns = { columns } rowKey = "bannerid"/> </div> ); }
1.构建以及请求轮播图的数据
// services/banner.ts
import request from '@/utils/request' export const getBannerList = () => { return request({ url: '/banner/list' }) } export const addBanner = (params: { img: string, alt: string, link: string }) => { return request({ url: '/banner/add', method: 'POST', data: params }) } export const removeBanner = (params: { bannerid: string }) => { return request({ url: '/banner/delete', data: params }) } export const removeAllBanner = () => { return request({ url: '/banner/removeAll' }) }
2.构建数据流
Dva 数据流的使用方式
src/models/*.ts
src/pages/models/*.ts
src/pages/model.ts
// src/pages/banner-manager/banner.d.ts
export interface IBanner { bannerid: string img: string alt: string link: string flag: boolean }
// banner-manager/model.ts
import { getBannerList } from "@/services/banner" import { IBanner } from './banner' import { Effect, ImmerReducer } from 'umi' export interface IBannerState { bannerList: IBanner[] } export interface IBannerModel { namespace: 'banner', state: IBannerState, effects: { getBannerListReq: Effect }, reducers: { changeBannerList: ImmerReducer<IBannerState> } } const bannerModel: IBannerModel = { namespace: 'banner', state: { bannerList: [] }, effects: { *getBannerListReq ({ payload }: any, { call, put }: any) { const res = yield call(getBannerList, payload) yield put({ type: 'changeBannerList', payload: res.data.data }) } }, reducers: { changeBannerList (state, action) { state.bannerList = action.payload } } } export default bannerModel
3.列表页面请求数据
import * as React from 'react'; import { Table, Button, Image, Space } from 'antd' import { IBanner } from './banner'; import { useMount } from 'ahooks'; import { ConnectRC, connect } from 'umi'; export interface IBannerListProps { bannerList: IBanner[] } const BannerList: ConnectRC<IBannerListProps> = (props) => { useMount(() => { props.dispatch({ type: 'banner/getBannerListReq' }) }) const { bannerList } = props const columns = [ { title: '序号', render (text: any, record: IBanner, index: number) { return <span>{ index + 1}</span> } }, { title: '链接', dataIndex: 'link' }, { title: '描述', dataIndex: 'alt' }, { title: '图片', dataIndex: 'img', render (text: string, record: IBanner, index: number) { return ( <Image src = { text} width={200} /> ) } }, { title: '操作', render (text: any, record: IBanner, index: number) { return ( <Space> <Button type="primary">编辑</Button> <Button danger>删除</Button> </Space> ) } } ] return ( <div> <Button onClick = { () => { }}>添加轮播图</Button> <Table dataSource = { bannerList } columns = { columns } rowKey = "bannerid"/> </div> ); } export default connect(({ banner }: any) => ({ bannerList: banner.bannerList}))(BannerList)
###
5.数据的删除
import * as React from 'react'; import { Table, Button, Image, Space, Popconfirm } from 'antd' import { IBanner } from './banner'; import { useMount } from 'ahooks'; import { ConnectRC, connect } from 'umi'; import { removeBanner } from '@/services/banner'; export interface IBannerListProps { bannerList: IBanner[] } const BannerList: ConnectRC<IBannerListProps> = (props) => { useMount(() => { props.dispatch({ type: 'banner/getBannerListReq' }) }) const { bannerList } = props const columns = [ { title: '序号', render (text: any, record: IBanner, index: number) { return <span>{ index + 1}</span> } }, { title: '链接', dataIndex: 'link' }, { title: '描述', dataIndex: 'alt' }, { title: '图片', dataIndex: 'img', render (text: string, record: IBanner, index: number) { return ( <Image src = { text} width={200} /> ) } }, { title: '操作', render (text: any, record: IBanner, index: number) { return ( <Space> <Button type="primary">编辑</Button> <Popconfirm title="确认删除此轮播图吗?" onConfirm={ () => { removeBanner({ bannerid: record.bannerid }).then(() => { // 删除此数据成功 // 重新获取一次数据 props.dispatch({ type: 'banner/getBannerListReq' }) }) }} onCancel={ () => {}} > <Button danger >删除</Button> </Popconfirm> </Space> ) } } ] return ( <div> <Button onClick = { () => { }}>添加轮播图</Button> <Table dataSource = { bannerList } columns = { columns } rowKey = "bannerid"/> </div> ); } export default connect(({ banner }: any) => ({ bannerList: banner.bannerList}))(BannerList)
6.添加轮播图数据
// src/pages/banner_manager/add.tsx
import * as React from 'react'; import { Input,Button } from 'antd' import { addBanner } from '@/services/banner'; import { history } from 'umi' export interface IBannerAddProps { } function BannerAdd (props: IBannerAddProps) { const linkRef = React.useRef('link') const altRef = React.useRef('alt') const hideRef = React.useRef('hide') const fileRef = React.useRef('file') const imgRef = React.useRef('img') return ( <div> <input ref = {linkRef} style={{ width: 200 }} placeholder = "请输入link" /> <input ref = {altRef} style={{ width: 200 }} placeholder = "请输入alt" /> <input ref = {fileRef} type="file" multiple onChange = {() => { const file = fileRef.current.files[0] const img = imgRef.current const reader = new FileReader() reader.readAsDataURL(file) reader.onload = function () { img.src = this.result hideRef.current.value = this.result } }}/> <input type="text" ref = { hideRef } hidden/> <img src = "" ref = {imgRef}></img> <Button onClick = { () => { const link = linkRef.current.value const alt = altRef.current.value const img = hideRef.current.value console.log({ link, alt, img }) addBanner({ link, alt, img }).then(() => { history.push('/banner/list') }) } }>上传</Button> </div> ); } export default BannerAdd
// 配置路由
export interface IChildRoute { path: string name?: string component: string, wrappers?: string[] } export interface IBestAFSRoute { routes?: IChildRoute[] // Array<IChildRoute> path: string redirect?: string, exact?: boolean, component?: string name?: string // 兼容此写法 icon?: string // 更多功能查看 // https://beta-pro.ant.design/docs/advanced-menu // --- // 新页面打开 target?: string // 不展示顶栏 headerRender?: boolean // 不展示页脚 footerRender?: boolean // 不展示菜单 menuRender?: boolean // 不展示菜单顶栏 menuHeaderRender?: boolean // 权限配置,需要与 plugin-access 插件配合使用 access?: string // 隐藏子菜单 hideChildrenInMenu?: boolean // 隐藏自己和子菜单 hideInMenu?: boolean // 在面包屑中隐藏 hideInBreadcrumb?: boolean // 子项往上提,仍旧展示, flatMenu?: boolean, wrappers?: string[], role?: number } const routes: IBestAFSRoute[] = [ { path: '/', exact: true, redirect: '/home' }, { path: '/home', icon: 'HomeOutlined', name: '首页', // 如果需要出现在左侧的菜单栏 component: '@/pages/home/index' }, { // 登录页面不需要侧边菜单栏等 path: '/login', // name: '登录', component: '@/pages/login/index', // 不展示顶栏 headerRender: false, // 不展示页脚 footerRender: false, // 不展示菜单 menuRender: false }, { path: '/banner', name: '轮播图管理', icon: 'FileImageOutlined', routes: [ { path: '/banner/list', name: '轮播图列表', wrappers: [ '@/wrappers/auth', ], component: '@/pages/banner-manager/list' }, { path: '/banner/add', // name: '添加轮播图', wrappers: [ '@/wrappers/auth', ], component: '@/pages/banner-manager/add' } ] }, { path: '/pro', name: '产品管理', icon: 'UnorderedListOutlined', routes: [ { path: '/pro/list', name: '产品列表', wrappers: [ '@/wrappers/role', ], component: '@/pages/pro-manager/list' }, { path: '/pro/recommend', name: '推荐列表', wrappers: [ '@/wrappers/role', ], component: '@/pages/pro-manager/recommend' }, { path: '/pro/seckill', name: '秒杀列表', wrappers: [ '@/wrappers/role', ], component: '@/pages/pro-manager/seckill' } ] } ] export default routes
十三.面包屑导航
// src/components/MyBreadcrumb/index.tsx
import * as React from 'react'; import { Breadcrumb } from 'antd' import { useMount } from 'ahooks'; import { useLocation } from 'umi'; interface IMyBreadcrumbProps { } const MyBreadcrumb: React.FunctionComponent<IMyBreadcrumbProps> = (props) => { const { pathname } = useLocation() console.log(pathname) const [ title, setTitle ] = React.useState('') const [ subTitle, setSubTitle ] = React.useState('') const breadcrumbList = { '/banner/list': { title: '轮播图管理', subTitle: '轮播题列表' }, '/pro/list': { title: '产品管理', subTitle: '产品列表' }, '/pro/recommend': { title: '产品管理', subTitle: '推荐列表' }, '/pro/seckill': { title: '产品管理', subTitle: '秒杀列表' } } useMount(() => { // 获取地址栏的 pathname setTitle(breadcrumbList[pathname].title) setSubTitle(breadcrumbList[pathname].subTitle) }) return ( <Breadcrumb> <Breadcrumb.Item>首页</Breadcrumb.Item> <Breadcrumb.Item> <a href="">{ title }</a> </Breadcrumb.Item> <Breadcrumb.Item> <a href=""> { subTitle }</a> </Breadcrumb.Item> </Breadcrumb> ); }; export default MyBreadcrumb;
// 页面中使用 以产品列表为例
import React from 'react'; import {MyBreadcrumb} from '@/components/myBreadcrumb' type ProListProps = { }; const index:React.FC<ProListProps> = () => { return ( <> <MyBreadcrumb /> <div>产品列表</div> </> ) } export default index;
十四、管理员操作
详情见代码
编辑使用抽屉,添加使用对话框
token的使用 --- 拦截器
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, Method, AxiosError } from 'axios' import { getItem } from './cookie'; import { history } from 'umi' const isDev: boolean = process.env.NODE_ENV === 'development' const ins: AxiosInstance = axios.create({ baseURL: isDev ? 'http://121.89.205.189/admin' : 'http://121.89.205.189/admin', timeout: 6000 }) // 请求拦截器 ins.interceptors.request.use(function (config: AxiosRequestConfig): AxiosRequestConfig { // 在发送请求之前做些什么 // 设定加载的进度条 / 统一传递token 信息(从本地获取) ********************************** config.headers.common['token'] = getItem('token') || '' return config; }, function (error: any): Promise<never> { // 对请求错误做些什么 return Promise.reject(error); }); // 添加响应拦截器 ins.interceptors.response.use(function (response: AxiosResponse<any>): AxiosResponse<any> { // 对响应数据做点什么 // 进度条消失 / 验证token的有效性 ********************************** if (response.data.code === '10119') { // 我的接口token失效 { code: '10119', message: 'token无效'} //跳转到登录页面 window.location.href = "/login" return response; } return response; }, function (error: any): Promise<never> { // 对响应错误做点什么 return Promise.reject(error); }); // http://www.axios-js.com/zh-cn/docs/#axios-config // 自定义各种数据请求 axios({}) export default function request(config: AxiosRequestConfig): Promise<AxiosResponse<any>>{ let { url = '', method = 'GET', data = {}, headers = '' } = config // url = url || '' // method = method || 'get' // data = data || {} // headers = headers || '' // method 转换为大写 switch (method.toUpperCase()) { case 'GET': return ins.get(url, { params: data }) case 'POST': // 表单提交 application/x-www-form-url-encoded if (headers['content-type'] === 'application/x-www-form-url-encoded') { // 转参数 URLSearchParams/第三方库qs const p = new URLSearchParams() for(let key in data) { p.append(key, data[key]) } return ins.post(url, p, {headers}) } // 文件提交 multipart/form-data if (headers['content-type'] === 'multipart/form-data') { const p = new FormData() for(let key in data) { p.append(key, data[key]) } return ins.post(url, p, {headers}) } // 默认 application/json return ins.post(url, data) case 'PUT': // 修改数据 --- 所有的数据的更新 return ins.put(url, data) case 'DELETE': // 删除数据 return ins.delete(url, {data}) case 'PATCH': // 更新局部资源 return ins.patch(url, data) default: return ins(config) } }
十三作业:
需要自主完成管理员列表的相关功能
adminname
password
role 2 超级管理员。1管理员
##
十五.富文本编辑器的使用
https://braft.margox.cn/demos/basic
cnpm i braft-editor -S
创建页面以及路由
1.创建表单页面
Src/pages/editor/Rich.tsx
import * as React from 'react'; // 引入编辑器组件 import BraftEditor from 'braft-editor' // 引入编辑器样式 import 'braft-editor/dist/index.css' interface IRichProps { } const Rich: React.FunctionComponent<IRichProps> = (props) => { const [ editorState, setEditorState ] = React.useState('') const [ text, setText ] = React.useState('') const handleEditorChange = (editorState) => { console.log('change', editorState ) setEditorState(editorState) } const submitContent = () => { const html = editorState.toHTML() setText(html) console.log(html) } return ( <div> <BraftEditor value={editorState} onChange={handleEditorChange} onSave={submitContent} /> asdasdasdsads <div dangerouslySetInnerHTML = {{ __html: text}} style={{ height: 600}}> </div> </div> ); }; export default Rich;
{ path: '/rich', icon: 'HomeOutlined', name: '富文本编辑器', // 如果需要出现在左侧的菜单栏 component: '@/pages/editor/Rich' },
可以使用markdown编辑器
https://gitee.com/uiw/react-markdown-editor#https://github.com/jaywcjlove/react-monacoeditor
s r c/pages/editor/MarkDown.tsx
import * as React from 'react'; import MarkdownEditor from '@uiw/react-markdown-editor'; interface IMarkDownProps { } const MarkDown: React.FunctionComponent<IMarkDownProps> = (props) => { const [markdown, setMarkdown] = React.useState('') const updateMarkdown = (editor,data,value) => { console.log(editor) console.log(data) console.log(value) setMarkdown(value) } return ( <div> markdown编辑器 <MarkdownEditor value={markdown} onChange={updateMarkdown} height = { 600 } /> { markdown } </div> ) }; export default MarkDown;
export interface IChildRoute { path: string name?: string component: string, wrappers?: string[] } export interface IBestAFSRoute { routes?: IChildRoute[] // Array<IChildRoute> path: string redirect?: string, exact?: boolean, component?: string name?: string // 兼容此写法 icon?: string // 更多功能查看 // https://beta-pro.ant.design/docs/advanced-menu // --- // 新页面打开 target?: string // 不展示顶栏 headerRender?: boolean // 不展示页脚 footerRender?: boolean // 不展示菜单 menuRender?: boolean // 不展示菜单顶栏 menuHeaderRender?: boolean // 权限配置,需要与 plugin-access 插件配合使用 access?: string // 隐藏子菜单 hideChildrenInMenu?: boolean // 隐藏自己和子菜单 hideInMenu?: boolean // 在面包屑中隐藏 hideInBreadcrumb?: boolean // 子项往上提,仍旧展示, flatMenu?: boolean, wrappers?: string[], role?: number } const routes: IBestAFSRoute[] = [ .... { path: '/mk', icon: 'HomeOutlined', name: 'markdown编辑器', // 如果需要出现在左侧的菜单栏 component: '@/pages/editor/MarkDown' }, ] export default routes
十六.数据可视化
ECharts https://echarts.apache.org/zh/index.html
1.html中使用echarts
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>ECharts</title> <!-- 引入 echarts.js --> <script src="echarts.min.js"></script> </head> <body> <!-- 为ECharts准备一个具备大小(宽高)的Dom --> <div id="main" style="width: 600px;height:400px;"></div> <script type="text/javascript"> // 基于准备好的dom,初始化echarts实例 var myChart = echarts.init(document.getElementById('main')); // 指定图表的配置项和数据 var data = genData(50); option = { title: { text: '同名数量统计', subtext: '纯属虚构', left: 'center' }, tooltip: { trigger: 'item', formatter: '{a} <br/>{b} : {c} ({d}%)' }, legend: { type: 'scroll', orient: 'vertical', right: 10, top: 20, bottom: 20, data: data.legendData, selected: data.selected }, series: [ { name: '姓名', type: 'pie', radius: '55%', center: ['40%', '50%'], data: data.seriesData, emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } } } ] }; function genData(count) { var nameList = [ '赵', '钱', '孙', '李', '周', '吴', '郑', '王', '冯', '陈', '褚', '卫', '蒋', '沈', '韩', '杨', '朱', '秦', '尤', '许', '何', '吕', '施', '张', '孔', '曹', '严', '华', '金', '魏', '陶', '姜', '戚', '谢', '邹', '喻', '柏', '水', '窦', '章', '云', '苏', '潘', '葛', '奚', '范', '彭', '郎', '鲁', '韦', '昌', '马', '苗', '凤', '花', '方', '俞', '任', '袁', '柳', '酆', '鲍', '史', '唐', '费', '廉', '岑', '薛', '雷', '贺', '倪', '汤', '滕', '殷', '罗', '毕', '郝', '邬', '安', '常', '乐', '于', '时', '傅', '皮', '卞', '齐', '康', '伍', '余', '元', '卜', '顾', '孟', '平', '黄', '和', '穆', '萧', '尹', '姚', '邵', '湛', '汪', '祁', '毛', '禹', '狄', '米', '贝', '明', '臧', '计', '伏', '成', '戴', '谈', '宋', '茅', '庞', '熊', '纪', '舒', '屈', '项', '祝', '董', '梁', '杜', '阮', '蓝', '闵', '席', '季', '麻', '强', '贾', '路', '娄', '危' ]; var legendData = []; var seriesData = []; for (var i = 0; i < count; i++) { var name = Math.random() > 0.65 ? makeWord(4, 1) + '·' + makeWord(3, 0) : makeWord(2, 1); legendData.push(name); seriesData.push({ name: name, value: Math.round(Math.random() * 100000) }); } return { legendData: legendData, seriesData: seriesData }; function makeWord(max, min) { var nameLen = Math.ceil(Math.random() * max + min); var name = []; for (var i = 0; i < nameLen; i++) { name.push(nameList[Math.round(Math.random() * nameList.length - 1)]); } return name.join(''); } } // 使用刚指定的配置项和数据显示图表。 myChart.setOption(option); </script> </body> </html>
2.在react中使用echarts
cnpm install echarts --save
Src/pages/data-manager/echarts.tsx
import React from 'react'; import * as echarts from 'echarts'; export interface IAppProps { } export interface IAppState { myChart: any, option: any } export default class App extends React.Component<IAppProps, IAppState> { constructor(props: IAppProps) { super(props); this.state = { myChart: null, option: {} } } componentDidMount () { this.setState({ myChart: echarts.init((document.getElementById('box') as any)), option: { // https://echarts.apache.org/zh/option.html#title title: { text: 'ECharts 入门示例', subtext: 'echarts简单', left: '50%', textAlign: 'center', // show: false link: 'https://www.baidu.com', textStyle: { color: '#f66' } }, legend: { bottom: 10 }, grid: { show: true }, brush: { xAxisIndex: 'all', brushLink: 'all', outOfBrush: { colorAlpha: 0.1 } }, tooltip: {}, axisPointer: { show: true, link: {xAxisIndex: 'all'}, label: { backgroundColor: '#777' } }, toolbox: { show: true, showTitle: false, // 隐藏默认文字,否则两者位置会重叠 feature: { saveAsImage: { show: true, title: 'Save As Image' }, dataView: { show: true, title: 'Data View' }, }, tooltip: { // 和 option.tooltip 的配置项相同 show: true, formatter: function (param) { return '<div>' + param.title + '</div>'; // 自定义的 DOM 结构 }, backgroundColor: '#222', textStyle: { fontSize: 12, }, extraCssText: 'box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);' // 自定义的 CSS 样式 } }, xAxis: { data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'] }, dataZoom: {}, yAxis: {}, // darkMode: 'dark', // backgroundColor: 'rgba(0,0,0, 0.9)', // polar: {}, series: [{ name: '销量', type: 'bar', data: [5, 20, 36, 10, 10, 20] }, { name: '库存', type: 'line', data: [15, 120, 136, 110, 110, 120] }] } }, () => { this.state.myChart.setOption(this.state.option) }) } changeLine = () => { this.setState({ option: { title: { text: 'ECharts 入门示例' }, tooltip: {}, xAxis: { data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'] }, yAxis: {}, series: [{ name: '销量', type: 'line', data: [5, 20, 36, 10, 10, 20] }] } }, () => { this.state.myChart.setOption(this.state.option) }) } changeLineAndData = () => { this.setState({ option: { title: { text: 'ECharts 入门示例' }, tooltip: {}, xAxis: { data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'] }, yAxis: {}, series: [{ name: '销量', type: 'line', data: [15, 25, 16, 50, 70, 120] }] } }, () => { this.state.myChart.setOption(this.state.option) }) } public render() { return ( <div> <h1>echarts案例</h1> <button onClick = { this.changeLine }>折线图</button> <button onClick = { this.changeLineAndData }>折线图-改变数据</button> <div id="box" style={{ width: 1000, height: 800, backgroundColor: '#fff'}}></div> </div> ); } }
{ path: '/data', name: '数据可视化', icon: 'PictureOutlined', routes: [ { path: '/data/echarts', name: 'echarts数据可视化', component: '@/pages/data-manager/echarts', } ] }
import React from 'react' import * as echarts from 'echarts' import { useMount } from 'ahooks'; export default function Echarts() { let myChart = '' const [option, setOption] = React.useState({ xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] }, yAxis: { type: 'value' }, series: [{ data: [150, 230, 224, 218, 135, 147, 260], type: 'line' }] }) useMount(() => { draw() }) function draw (){ myChart = echarts.init(document.getElementById('main')) myChart.setOption(option) } function changeData1 () { setOption({ xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] }, yAxis: { type: 'value' }, series: [{ data: [150, 230, 224, 218, 135, 147, 260], type: 'bar' }] }) draw() } function changeData2 () { setOption({ xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] }, yAxis: { type: 'value' }, series: [{ data: [150, 230, 224, 218, 135, 147, 260], type: 'pie' }] }) draw() } return ( <div> <button onClick = { changeData1 }>改变数据-柱状图</button> <button onClick = { changeData2 }>改变数据-饼状图</button> <div id="main" style={{ width: '600px',height: '500px'}}></div> </div> ); }
3.其余的数据可视化工具
highcharts https://www.highcharts.com.cn/ 使用方法类似于echarts,但是。。。。
antv https://antv.vision/ https://antv.gitee.io/zh/
react - https://charts.ant.design/
在 React / Vue / Angular 中使用 G2
基于 AntV 技术栈还有许多优秀的项目,在 React 环境下使用 G2,我们推荐使用 Ant Design Charts,BizCharts 和 Viser。这三个产品都是基于 G2 的 React 版本封装,使用体验更符合 React 技术栈的习惯,他们都与 AntV 有着紧密的协同,他们很快也将同步开源和发布基于 G2 4.0 的版本。Viser 除了 React 外,还提供了 Vue 和 Angular 不同的分发版本。
-
Ant Design Charts 地址:https://charts.ant.design
-
BizCharts 地址:https://bizcharts.net
-
Viser 地址:https://viserjs.github.io/
自己写
D3.js自定义数据可视化
Access-token. Refresh-token
-
vue里面有一个MVVM的模型,对此怎么理解?它有什么作用?
-
双向数据绑定怎么实现?
-
立即反映到视图层或模型层是怎么做到的?
-
简单实现一下双向数据绑定,用JS去实现,这个要怎么做?
-
它可以监听哪些事件?
-
有自己封装过vue组件吗?
-
Vue组件之间参数传递
-
遇到跨域问题,是怎么处理的?
-
页面上有比较复杂的数据结构,页面的表格可能比较复杂,当我要修改其中内容提交的时候,需要将这些数据提交上去,这个表格可能是一个list,里面是个对象,对象里可能又有一些数组,这样多层嵌套的数据结构,页面处理的时候需要怎么做?需要注意些什么?(比如调一些接口,服务端接口去查询,然后给返回了一个这样的列表,你要再页面上去展示,需要做些什么?)
-
正常显示一个列表是怎么显示的?通过什么组件?
-
开发过程中对组件库不熟,平时用到一个组件的时候怎么办呢?去哪找这个组件?
-
平常你在工作当中你是怎么去找我现在要用一个什么组件来完成现在的工作,因为你说你没封装过组件,那工作当中就是找现成的组件用,那你是从哪找的?
-
举个例子讲讲你平常工作中某一个业务的开发过程,当时你拿到了一个什么样的需求,你是怎么去理解这个需求的?怎么去跟后端开发人员以及其他相关人员交互的,最后怎么去完成这个工作?
-
这些接口是你告诉后端你需要什么接口,还是后端人员告诉你他有什么接口,然后你来想页面上怎么用?
-
需求给过来是个什么样子,是文档还是原型图,还是一些其他的形式呢?
-
从原型图效果图到实际的页面,这中间切图的工作也是你们做的吗?
-
你的首页和详情页是两个不同的页面,你们是单页面模式还是多页面模式?(我回答的单页面)
-
那首页跟详情页你是怎么路由的
-
假如在首页定义了一个定时器,(你是单页面),跳转到详情页面去了后,定时器还会继续生效吗?是还在后台继续运行还是已经被销毁了?
-
会在生命周期哪个阶段销毁?
-
destroy这个阶段其实有两个方法会被调用,beforeDestroy和destroyed,beforeDestroy里面一般会做什么事情? 说一下你工作中处理过最难的一个前端的问题是什么?