react-umi_dva项目
29.9React课程
第11节:umi_Dva路由及项目实现
(第11节:umi-Dva路由及项目实现&)
第11节:umi-Dva路由及项目实现&
项目创建
创建页面
创建目录结构
router.js
import React from 'react';
import { Router as DefaultRouter, Route, Switch } from 'react-router-dom';
import dynamic from 'umi/dynamic';
import renderRoutes from 'umi/lib/renderRoutes';
import history from '@tmp/history';
import { routerRedux } from 'dva';
const Router = routerRedux.ConnectedRouter;
const routes = [
{
path: '/',
component: require('../../layouts/index.js').default,
routes: [
{
path: '/404',
exact: true,
component: require('../404.js').default,
},
{
path: '/about',
exact: true,
component: require('../about.js').default,
title: 'about page',
Routes: [
require('../../routes/PrivateRoute.js').default,
require('../../routes/Test.js').default,
],
},
{
path: '/goods',
exact: true,
component: require('../goods/index.js').default,
},
{
path: '/goods',
exact: true,
component: require('../goods.js').default,
},
{
path: '/',
exact: true,
component: require('../index.js').default,
},
{
path: '/login',
exact: true,
component: require('../login.js').default,
},
{
path: '/users',
exact: false,
component: require('../users/_layout.js').default,
routes: [
{
path: '/users',
exact: true,
component: require('../users/index.js').default,
},
{
path: '/users/:id',
exact: true,
component: require('../users/$id.js').default,
},
{
component: () =>
React.createElement(
require('/Users/kele/.config/yarn/global/node_modules/umi-build-dev/lib/plugins/404/NotFound.js')
.default,
{ pagesPath: 'pages', hasRoutesInConfig: false },
),
},
],
},
{
path: '/:post/commit',
exact: true,
component: require('../$post/commit.js').default,
},
{
path: '/:post',
exact: true,
component: require('../$post/index.js').default,
},
{
component: () =>
React.createElement(
require('/Users/kele/.config/yarn/global/node_modules/umi-build-dev/lib/plugins/404/NotFound.js')
.default,
{ pagesPath: 'pages', hasRoutesInConfig: false },
),
},
],
},
{
component: () =>
React.createElement(
require('/Users/kele/.config/yarn/global/node_modules/umi-build-dev/lib/plugins/404/NotFound.js')
.default,
{ pagesPath: 'pages', hasRoutesInConfig: false },
),
},
];
window.g_routes = routes;
const plugins = require('umi/_runtimePlugin');
plugins.applyForEach('patchRoutes', { initialValue: routes });
export { routes };
export default class RouterWrapper extends React.Component {
unListen() {}
constructor(props) {
super(props);
// route change handler
function routeChangeHandler(location, action) {
plugins.applyForEach('onRouteChange', {
initialValue: {
routes,
location,
action,
},
});
}
this.unListen = history.listen(routeChangeHandler);
routeChangeHandler(history.location);
}
componentWillUnmount() {
this.unListen();
}
render() {
const props = this.props || {};
return <Router history={history}>{renderRoutes(routes, props)}</Router>;
}
}
带$前缀的文件或路径为动态路由
创建users目录,页面是动态路由
$id.js
import styles from "./$id.css";
export default function(props) {
const { match } = props;
console.log(match);
return (
<div className={styles.normal}>
<h1>Page $id:: {match.params.id}</h1>
</div>
);
}
$post
commit.js
2号用户进入留言页
嵌套路由
_layout.js
props.children显示子内容插槽
import styles from './_layout.css';
export default function(props) {
return (
<div className={styles.normal}>
<h1>Layout for ./users</h1>
{
props.children
}
</div>
);
}
两个组件都会共享外面的layout
全局layout,约定src/layouts/index.js为全局路由,在pages文件夹外面创建layout
layouts/index.js,共享布局,login有自己的布局不要共享全局的布局
import styles from "./index.css";
import { Layout } from "antd";
import { Menu } from "antd";
const { Header, Footer, Content } = Layout;
import Link from "umi/link";
export default function(props) {
const { location } = props;
if (location.pathname === "/login") {
return <>{props.children}</>;
}
return (
<Layout>
{/* 页头 */}
<Header className={styles.header}>
{/* 新增内容 */}
<img
className={styles.logo}
src="https://img.kaikeba.com/logo-new.png"
/>
<Menu
theme="dark"
mode="horizontal"
defaultSelectedKeys={["2"]}
style={{ lineHeight: "64px", float: "left" }}
>
<Menu.Item key="/goods">
<Link to="/goods">商品</Link>
</Menu.Item>
<Menu.Item key="/users">
<Link to="/users">用户</Link>
</Menu.Item>
<Menu.Item key="/about">
<Link to="/about">关于</Link>
</Menu.Item>
</Menu>
</Header>
{/* 内容 */}
<Content className={styles.content}>
<div className={styles.box}>{props.children}</div>
</Content>
{/* 页脚 */}
<Footer className={styles.footer}>开课吧</Footer>
</Layout>
);
// const { location } = props;
// if (location.pathname === "/login") {
// return <>{props.children}</>;
// }
// return (
// <div className={styles.normal}>
// <h1>Layout for /</h1>
// {props.children}
// </div>
// );
}
全局layouts包含所有的页面
(404页面&)
404页面&
404.js
import { Exception } from "ant-design-pro";
export default function() {
return <Exception type="404" backText="返回首页"></Exception>;
}
扩展路由
多了一个title属性
配置式路由与约定式路由是冲突的
config/config.js ,根路径不再是index而是abouts
export default {
// routes: [{ path: "/", component: "./about" }]
plugins: [
[
"umi-plugin-react",
{
dva: true,
antd: true
}
]
]
};
routes的默认路径都在pages中
路由守卫,增加routes字段格式是一个数组,pages下所有约定式路由全部失效
pages/about.js yml 数组是-
/*
*
* title: "about page"
* Routes:
* - ./routes/PrivateRoute.js
* - ./routes/Test.js
*
*
*
*/
import styles from "./about.less";
export default function() {
return (
<div className={styles.normal}>
<h1>Page about</h1>
</div>
);
}
routes/PrivateRoute.js
export default props => {
return (
<div>
<h3>PrivateRoute Page</h3>
{props.children}
</div>
);
};
routes/Test.js
export default props => {
return <div>test page {props.children}</div>;
};
自动生成配置式路由,路由组件让about.js组件包含PrivateRoute组件,设置路由守卫
最外面是PrivateRoute,里面是testRoute,最里面是
(配置configu路由&)
配置configu路由&
pages/index.js
import styles from "./index.css";
import Link from "umi/link";
export default function() {
return (
<div className={styles.normal}>
<h1>Page index</h1>
<Link to="/login">登录</Link>
</div>
);
}
pages/login.js
import React, { Component } from "react";
// import { Button } from "antd";
import styles from "./login.css";
import router from "umi/router";
import { Login } from "ant-design-pro";
import { connect } from "dva";
const { UserName, Password, Submit } = Login; // 通用的用户名、密码和提交组件
export default connect()(function(props) {
// let from = props.location.state.from || "/"; // 重定向地址
const onSubmit = (err, values) => {
console.log("用户输入:", values);
if (!err) {
// 校验通过,提交登录
props.dispatch({ type: "user/login", payload: values });
}
};
return (
<div className={styles.loginForm}>
{/* logo */}
<img className={styles.logo} src="https://img.kaikeba.com/logo-new.png" />
{/* 登录表单 */}
<Login onSubmit={onSubmit}>
<UserName
name="username"
placeholder="kaikeba"
rules={[{ required: true, message: "请输入用户名" }]}
/>
<Password
name="password"
placeholder="123"
rules={[{ required: true, message: "请输入密码" }]}
/>
<Submit>登录</Submit>
</Login>
</div>
);
});
(mock数据&)
mock数据&
时候mock.js来模拟生成随机数据
import mockjs from "mockjs";
export default {
"GET /api/users": { users: [1, 2] },
"GET /api/users/create": (req, res) => {
setTimeout(() => {
res.send("Ok");
}, 1000);
},
"GET /api/tags": mockjs.mock({
"list|100": [{ name: "@city", "value|1-100": 50, "type|0-2": 1 }]
}),
//动态的mock
"/api/random": (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.send(
mockjs.mock({
// 每次请求均产生随机值
"number|1-100": 100
})
);
}
};
mock/goods.js
let data = [{ title: "web全栈" }, { title: "java架构师" }];
export default {
"GET /api/goods": function(req, res) {
setTimeout(() => {
res.json({ result: data });
}, 1000);
}
};
添加跨域请求头,在别的服务请求mock的数据
允许跨域操作
import mockjs from "mockjs";
export default {
"GET /api/users": { users: [1, 2] },
"GET /api/users/create": (req, res) => {
setTimeout(() => {
res.send("Ok");
}, 1000);
},
"GET /api/tags": mockjs.mock({
"list|100": [{ name: "@city", "value|1-100": 50, "type|0-2": 1 }]
}),
//动态的mock
"/api/random": (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.send(
mockjs.mock({
// 每次请求均产生随机值
"number|1-100": 100
})
);
}
};
umi插件集
config/config.js 开启插件,开启dva
export default {
// routes: [{ path: "/", component: "./about" }]
plugins: [
[
"umi-plugin-react",
{
dva: true,
antd: true
}
]
]
};
(Dva&)
Dva&
models/goods.js 通过namespace来区分,不能直接修改state,通过reducers来修改state
export default {
namespace: "goods",
state: {
// 初始状态包括课程和分类
courses: {}, // 课程
tags: [] // 分类
},
effects: {
*getList(action, { call, put }) {
// 解构出courseData并初始化状态
const {
data: { data: courseData }
} = yield call(getGoods);
yield put({ type: "initGoods", payload: courseData });
}
},
reducers: {
initGoods(state, { payload }) {
// 解构出tags和courses并返回
const { tags, data: courses } = payload;
return { ...state, tags, courses };
}
}
};
models/users.js
import axios from "axios";
import router from "umi/router";
// 初始状态:本地缓存或空值对象
const userinfo = JSON.parse(localStorage.getItem("userinfo")) || {
token: "",
role: "",
username: "",
balance: 0
};
// 登录请求方法
function login(payload) {
return axios.post("/api/login", payload);
}
export default {
namespace: "user", // 可省略
state: userinfo,
effects: {
// action: user/login
*login({ payload }, { call, put }) {
const {
data: { code, data: userinfo }
} = yield call(login, payload);
if (code == 0) {
// 登录成功: 缓存用户信息
localStorage.setItem("userinfo", JSON.stringify(userinfo));
yield put({ type: "init", payload: userinfo });
router.push("/");
} else {
// 登录失败:弹出提示信息,可以通过响应拦截器实现
}
}
},
reducers: {
init(state, action) {
// 覆盖旧状态
return action.payload;
}
}
};
组件里使用model通过connect
pages/goods.js goods页面,把状态映射到组件里
第一个参数映射属性返回一个state对象,通过state.goods(namespace)取到对应的state
function() goodsList已经映射到props中,直接解构{goodLists}
函数式组件在页面渲染前发送请求通过Hooks
import styles from "./goods.css";
import { connect } from "dva";
import { useEffect } from "react";
export default connect(
state => ({
goodList: state.goods,
usersList: state.users,
loading: state.loading
}),
{
addGood: title => {
return {
type: "goods/addGoods",
payload: { title }
};
},
getList: () => {
return {
type: "goods/getList"
};
}
}
)(function({ goodList, usersList, addGood, getList, loading }) {
// console.log(usersList[0].name);
useEffect(() => {
getList();
}, []);
if (loading.models.goods) {
return <div>加载中...</div>;
}
return (
<div className={styles.normal}>
<h1>Page goods</h1>
<ul>
{goodList.map((item, index) => {
return <li key={index}>{item.title}</li>;
})}
</ul>
<button
onClick={() => {
addGood("商品" + new Date().getTime());
}}
>
添加商品
</button>
</div>
);
});
mock/goods.js
let data = [{ title: "web全栈" }, { title: "java架构师" }];
export default {
"GET /api/goods": function(req, res) {
setTimeout(() => {
res.json({ result: data });
}, 1000);
}
};
actions返回promise
dva-loading加载状态,直接把loading映射到组件上
(Reat购物车项目实战&)
Reat购物车项目实战&
git clone -b step01 https://github.com/57code/umi-test.git
umi配置,开启dva和antd
antd布局组件
https://ant.design/components/layout-cn/
import { Layout } from "antd";
import styles from "./index.css";
const { Header, Footer, Content } = Layout;
export default function(props) {
return (
<Layout>
{/* ⻚头 */}
<Header className={styles.header}>导航</Header>
{/* 内容 */}
<Content className={styles.content}>
<div className={styles.box}>{props.children} </div>
</Content>
{/* ⻚脚 */}
<Footer className={styles.footer}>开课吧</Footer>
</Layout>
);
}
import styles from "./index.css";
import { Layout } from "antd";
import { Menu } from "antd";
const { Header, Footer, Content } = Layout;
import Link from "umi/link";
export default function(props) {
const { location } = props;
if (location.pathname === "/login") {
return <>{props.children}</>;
}
return (
<Layout>
{/* 页头 */}
<Header className={styles.header}>
{/* 新增内容 */}
<img
className={styles.logo}
src="https://img.kaikeba.com/logo-new.png"
/>
<Menu
theme="dark"
mode="horizontal"
defaultSelectedKeys={["2"]}
style={{ lineHeight: "64px", float: "left" }}
>
<Menu.Item key="/goods">
<Link to="/goods">商品</Link>
</Menu.Item>
<Menu.Item key="/users">
<Link to="/users">用户</Link>
</Menu.Item>
<Menu.Item key="/about">
<Link to="/about">关于</Link>
</Menu.Item>
</Menu>
</Header>
{/* 内容 */}
<Content className={styles.content}>
<div className={styles.box}>{props.children}</div>
</Content>
{/* 页脚 */}
<Footer className={styles.footer}>开课吧</Footer>
</Layout>
);
// const { location } = props;
// if (location.pathname === "/login") {
// return <>{props.children}</>;
// }
// return (
// <div className={styles.normal}>
// <h1>Layout for /</h1>
// {props.children}
// </div>
// );
}
样式设置,layouts/index.css
.header {
color: white; }
.content {
margin: 16px; }
.box {
锦绣课堂-React课程
padding: 24px;
background: #fff;
min-height: 500px; }
.footer {
text-align: center; }
.logo {
float: left;
width: 160px;
margin: 16px 16px 0 0; }
//登录⻚⾯不需要布局,将登录路由配置移⾄顶层
{ path: "/login", component: "./login" },
{
path: "/",
component: "../layouts",
routes: [
// 移动之前路由配置到这⾥
]
}
export default {
// routes: [{ path: "/", component: "./about" }]
plugins: [
[
"umi-plugin-react",
{
dva: true,
antd: true
}
]
]
};
导航菜单移动⾄顶部,layouts/index.js
import styles from "./index.css";
import { Layout } from "antd";
import { Menu } from "antd";
const { Header, Footer, Content } = Layout;
import Link from "umi/link";
export default function(props) {
const { location } = props;
if (location.pathname === "/login") {
return <>{props.children}</>;
}
return (
<Layout>
{/* 页头 */}
<Header className={styles.header}>
{/* 新增内容 */}
<img
className={styles.logo}
src="https://img.kaikeba.com/logo-new.png"
/>
<Menu
theme="dark"
mode="horizontal"
defaultSelectedKeys={["2"]}
style={{ lineHeight: "64px", float: "left" }}
>
<Menu.Item key="/goods">
<Link to="/goods">商品</Link>
</Menu.Item>
<Menu.Item key="/users">
<Link to="/users">用户</Link>
</Menu.Item>
<Menu.Item key="/about">
<Link to="/about">关于</Link>
</Menu.Item>
</Menu>
</Header>
{/* 内容 */}
<Content className={styles.content}>
<div className={styles.box}>{props.children}</div>
</Content>
{/* 页脚 */}
<Footer className={styles.footer}>开课吧</Footer>
</Layout>
);
// const { location } = props;
// if (location.pathname === "/login") {
// return <>{props.children}</>;
// }
// return (
// <div className={styles.normal}>
// <h1>Layout for /</h1>
// {props.children}
// </div>
// );
}
// umi的配置,已经⾃动⽀持antd-pro的按需加载
import { Exception } from "ant-design-pro";
export default function() {
return <Exception type="404" backText="返回首页"></Exception>;
}
import React, { Component } from "react";
// import { Button } from "antd";
import styles from "./login.css";
import router from "umi/router";
import { Login } from "ant-design-pro";
import { connect } from "dva";
const { UserName, Password, Submit } = Login; // 通用的用户名、密码和提交组件
export default connect()(function(props) {
// let from = props.location.state.from || "/"; // 重定向地址
const onSubmit = (err, values) => {
console.log("用户输入:", values);
if (!err) {
// 校验通过,提交登录
props.dispatch({ type: "user/login", payload: values });
}
};
return (
<div className={styles.loginForm}>
{/* logo */}
<img className={styles.logo} src="https://img.kaikeba.com/logo-new.png" />
{/* 登录表单 */}
<Login onSubmit={onSubmit}>
<UserName
name="username"
placeholder="kaikeba"
rules={[{ required: true, message: "请输入用户名" }]}
/>
<Password
name="password"
placeholder="123"
rules={[{ required: true, message: "请输入密码" }]}
/>
<Submit>登录</Submit>
</Login>
</div>
);
});
pages/login.css
.loginForm {
width: 50%;
margin: 100px auto;
text-align: center;
}
.logo {
margin-bottom: 30px;
}
export default {
"post /api/login"(req, res, next) {
const { username, password } = req.body;
console.log(username, password);
if (username == "kaikeba" && password == "123") {
return res.json({
code: 0,
data: {
token: "kaikebaisgood",
role: "admin",
balance: 1000,
username: "kaikeba"
}
});
}
if (username == "jerry" && password == "123") {
return res.json({
code: 0,
data: {
token: "kaikebaisgood",
role: "user",
balance: 100,
username: "jerry"
}
});
}
return res.status(401).json({
code: -1,
msg: "密码错误"
});
}
};
layouts/index.js 判断是/login,不让布局组件包裹login
import styles from "./index.css";
import { Layout } from "antd";
import { Menu } from "antd";
const { Header, Footer, Content } = Layout;
import Link from "umi/link";
export default function(props) {
const { location } = props;
if (location.pathname === "/login") {
return <>{props.children}</>;
}
return (
<Layout>
{/* 页头 */}
<Header className={styles.header}>
{/* 新增内容 */}
<img
className={styles.logo}
src="https://img.kaikeba.com/logo-new.png"
/>
<Menu
theme="dark"
mode="horizontal"
defaultSelectedKeys={["2"]}
style={{ lineHeight: "64px", float: "left" }}
>
<Menu.Item key="/goods">
<Link to="/goods">商品</Link>
</Menu.Item>
<Menu.Item key="/users">
<Link to="/users">用户</Link>
</Menu.Item>
<Menu.Item key="/about">
<Link to="/about">关于</Link>
</Menu.Item>
</Menu>
</Header>
{/* 内容 */}
<Content className={styles.content}>
<div className={styles.box}>{props.children}</div>
</Content>
{/* 页脚 */}
<Footer className={styles.footer}>开课吧</Footer>
</Layout>
);
// const { location } = props;
// if (location.pathname === "/login") {
// return <>{props.children}</>;
// }
// return (
// <div className={styles.normal}>
// <h1>Layout for /</h1>
// {props.children}
// </div>
// );
}
import axios from "axios";
import router from "umi/router";
// 初始状态:本地缓存或空值对象
const userinfo = JSON.parse(localStorage.getItem("userinfo")) || {
token: "",
role: "",
username: "",
balance: 0
};
// 登录请求方法
function login(payload) {
return axios.post("/api/login", payload);
}
export default {
namespace: "user", // 可省略
state: userinfo,
effects: {
// action: user/login
*login({ payload }, { call, put }) {
const {
data: { code, data: userinfo }
} = yield call(login, payload);
if (code == 0) {
// 登录成功: 缓存用户信息
localStorage.setItem("userinfo", JSON.stringify(userinfo));
yield put({ type: "init", payload: userinfo });
router.push("/");
} else {
// 登录失败:弹出提示信息,可以通过响应拦截器实现
}
}
},
reducers: {
init(state, action) {
// 覆盖旧状态
return action.payload;
}
}
};
pages/login.js
import React, { Component } from "react";
// import { Button } from "antd";
import styles from "./login.css";
import router from "umi/router";
import { Login } from "ant-design-pro";
import { connect } from "dva";
const { UserName, Password, Submit } = Login; // 通用的用户名、密码和提交组件
export default connect()(function(props) {
// let from = props.location.state.from || "/"; // 重定向地址
const onSubmit = (err, values) => {
console.log("用户输入:", values);
if (!err) {
// 校验通过,提交登录
props.dispatch({ type: "user/login", payload: values });
}
};
return (
<div className={styles.loginForm}>
{/* logo */}
<img className={styles.logo} src="https://img.kaikeba.com/logo-new.png" />
{/* 登录表单 */}
<Login onSubmit={onSubmit}>
<UserName
name="username"
placeholder="kaikeba"
rules={[{ required: true, message: "请输入用户名" }]}
/>
<Password
name="password"
placeholder="123"
rules={[{ required: true, message: "请输入密码" }]}
/>
<Submit>登录</Submit>
</Login>
</div>
);
});
~ 响应拦截,创建./src/interceptor.js,通过notification通知组件进行弹窗
import axios from "axios";
import { notification } from "antd";
const codeMessage = {
202: "一个请求已经进入后台排队(异步任务)。",
401: "用户没有权限(令牌、用户名、密码错误)。",
404: "发出的请求针对的是不存在的记录,服务器没有进行操作。",
500: "服务器发生错误,请检查服务器。"
};
// 仅拦截异常状态响应
axios.interceptors.response.use(null, ({ response }) => {
if (codeMessage[response.status]) {
notification.error({
message: `请求错误 ${response.status}: ${response.config.url}`,
description: codeMessage[response.status]
});
}
return Promise.reject(err);
});
src/global.js 执行拦截器带需要创建global
// 全局入口
import interceptor from "./interceptor";
商品列表
mock/goods.js
let data = [{ title: "web全栈" }, { title: "java架构师" }];
export default {
"GET /api/goods": function(req, res) {
setTimeout(() => {
res.json({ result: data });
}, 1000);
}
};
models/goods.js
export default {
namespace: "goods",
state: {
// 初始状态包括课程和分类
courses: {}, // 课程
tags: [] // 分类
},
effects: {
*getList(action, { call, put }) {
// 解构出courseData并初始化状态
const {
data: { data: courseData }
} = yield call(getGoods);
yield put({ type: "initGoods", payload: courseData });
}
},
reducers: {
initGoods(state, { payload }) {
// 解构出tags和courses并返回
const { tags, data: courses } = payload;
return { ...state, tags, courses };
}
}
};
pages/goods/index.js
import { TagSelect } from "ant-design-pro";
import { connect } from "dva";
import React from "react";
// @connect(
// state => ({
// courses: state.goods.courses, // 映射课程数据
// tags: state.goods.tags // 映射标签数据
// }),
// {}
// )
export default connect(
state => ({
courses: state.goods.courses, // 映射课程数据
tags: state.goods.tags // 映射标签数据
}),
{}
)(
class GoodsTest extends React.Component {
// 页签变更
tagSelectChange = tags => {
console.log(tags);
};
render() {
if (this.props.loading.models.goods) {
return <div>加载中...</div>;
}
return (
<div>
{/* 分类标签 */}
<TagSelect onChange={this.tagSelectChange}>
{this.props.tags.map(tag => {
return (
<TagSelect.Option key={tag} value={tag}>
{tag}
</TagSelect.Option>
);
})}
</TagSelect>
</div>
);
}
}
);
import styles from "./goods.css";
import { connect } from "dva";
import { useEffect } from "react";
export default connect(
state => ({
goodList: state.goods,
usersList: state.users,
loading: state.loading
}),
{
addGood: title => {
return {
type: "goods/addGoods",
payload: { title }
};
},
getList: () => {
return {
type: "goods/getList"
};
}
}
)(function({ goodList, usersList, addGood, getList, loading }) {
// console.log(usersList[0].name);
useEffect(() => {
getList();
}, []);
if (loading.models.goods) {
return <div>加载中...</div>;
}
return (
<div className={styles.normal}>
<h1>Page goods</h1>
<ul>
{goodList.map((item, index) => {
return <li key={index}>{item.title}</li>;
})}
</ul>
<button
onClick={() => {
addGood("商品" + new Date().getTime());
}}
>
添加商品
</button>
</div>
);
});
import { Card, Row, Col, Skeleton, Icon } from "antd";
class Goods extends Component {
constructor(props) {
super(props);
// displayCourses为需要显示的商品数组
this.state = {
displayCourses: new Array(8).fill({}) // 填充数组⽤
于⻣架屏展示
};
}
// 数据传⼊时执⾏⼀次tagSelectChange
componentWillReceiveProps(props){
if(props.tags.length){
this.tagSelectChange(props.tags, props.courses)
}
}
// 额外传⼊课程列表数据
tagSelectChange = (tags, courses = this.props.courses)
=> {
console.log(tags);
// 过滤出要显示的数据
let displayCourses = [];
tags.forEach(tag => {
displayCourses = [...displayCourses,
...courses[tag]];
});
this.setState({ displayCourses });
console.log(displayCourses);
};
render() {
// 使⽤⻣架屏做加载反馈,loading属性不再需要
// if (this.props.loading.models.goods) {
锦绣课堂-React课程
// return <div>加载中...</div>;
// }
return (
<div>
{/* 分类标签 */}
{/* 商品列表 */}
<Row type="flex" justify="start">
{this.state.displayCourses.map((item, index)
=> {
return (
<Col key={index} style={{ padding: 10 }}
span={6}>
{item.name ? (
<Card
hoverable
title={item.name}
cover={<img src={"/course/" +
item.img} />}
>
<Card.Meta
description={
<div>
<span>¥{item.price}</span>
<span style={{ float: "right"
}}>
<Icon type="user" />
{item.solded}
</span>
</div>
}
/>
<div />
</Card>
) : (
<Skeleton active={true} />
)}
</Col>
})}
</Row>
</div>
);
}
}
export default Goods;
constructor(props) {
super(props);
this.state = {
//...
tags: [], // 默认未选中任何标签
};
}
tagSelectChange = (tags, courses = this.props.courses)
=> {
// 用户行为修改状态
this.setState({ displayCourses, tags });
};
// 组件受控
<TagSelect value={this.state.tags}>
export default {
namespace: "cart", // 可省略
state: JSON.parse(localStorage.getItem("cart")) || [],
// 初始状态:缓存或空数组
reducers: {
addCart(cart, action) {
const good = action.payload;
const idx = cart.findIndex(v => v.id == good.id);
if (idx > -1) {
// 更新数量
const cartCopy = [...cart];
const itemCopy = { ...cartCopy[idx] };
itemCopy.count += 1;
cartCopy.splice(idx, 1, itemCopy);
return cartCopy;
} else {
// 新增
return [...cart, { ...good, count: 1 }];
}
}
}
};
@connect(
state => ({ ... }),
{
addCart: item => ({ // 加购⽅法
type: "cart/addCart",
payload: item
}),
}
)
class Goods extends Component {
addCart = (e, item) => {
e.stopPropagation();
this.props.addCart(item);
};
render() {
<Card extra={
<Icon onClick={e => this.addCart(e, item)}
type="shopping-cart"
style={{ fontSize: 18 }} />}>
}
}
购物车数据同步到localStorage
effects: {
*addCart({payload}, {put, select}) {
yield put({type: 'add', payload})
const cart = yield select(state => state.cart);
localStorage.setItem("cart",
JSON.stringify(cart));
}
},
reducers: {
add(cart, action) {...}
}
import {Badge, Icon} from 'antd';
export default function(props) {
render(){
...
<div style={{float:'right'}}>
<Icon type="shopping-cart" style=
{{fontSize:18}}/>
<span>我的购物⻋</span>
<Badge count={5} offset={[-4,-18]}/>
</div>
} }
.ant-badge-count{
height: 16px;
border-radius: 8px;
min-width: 16px;
line-height: 16px;
padding: 0; }
~ 显示购物车数据
import { Dropdown } from "antd";
export default connect(state => ({ // 连接购物⻋状态
count: state.cart.length,
cart: state.cart
}))(function(props) {
render(){
// 构造购物⻋列表菜单
const menu = (
<Menu>
{this.props.cart.map((item, index) => (
<Menu.Item key={index}>
{item.name}×{item.count} <span>¥ {item.count * item.price}</span>
</Menu.Item>
))}
</Menu>
);
...
return (
{/* 购物⻋信息放在Dropdown以便展示 */}
<Dropdown overlay={menu}
placement="bottomRight">
<div className={styles.cart}>
{/* 购物⻋项⽬数量 */}
<Badge count={this.props.count} offset=
{[-4, -18]} />
</div>
</Dropdown>
)
} })
上一篇: Spring中多配置文件以及寻找引用其他bean的方式 博客分类: Spring springbeanxmlref读取配置文件
下一篇: JAVA中常用IO流类:FileReader和FileWriter 博客分类: Java FileReader、FileWriter
推荐阅读
-
react-umi_dva项目
-
springboot部署到阿里云,配置https,springboot项目同时支持http和https请求,阿里云配置https
-
第三方服务之Bmob——答题系统小项目
-
百度地图API 安卓项目 实时获取地图中心点经纬度
-
java项目从后端直接访问其他项目或网站获取所需内容
-
视频分割项目预研 博客分类: 编程相关Java相关 javaffmpeg入门
-
视频分割项目预研 博客分类: 编程相关Java相关 javaffmpeg入门
-
python项目java重构各种问题 博客分类: javapython
-
Android项目结构和AndroidManifest.xml
-
python项目java重构各种问题 博客分类: javapython