React项目

此文项目代码:https://github.com/bei-yang/I-want-to-be-an-architect
码字不易,辛苦点个star,感谢!

引言


此篇文章主要涉及以下内容:

  1. 创建页面布局
  2. Ant-design-pro应用
  3. 用户登录认证
  4. 创建商品页面
  5. 添加购物车

学习资源


  1. antd-pro

创建页面布局


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>
  )
}
  • 样式设置,index.css
.header {
 color: white;
}
.content {
 margin: 16px;
}
.box {
 padding: 24px;
 background: #fff;
 min-height: 500px;
}
.footer {
 text-align: center;
}
  • 配置式路由需要手动修改页面布局,config.js
{
   path: "/",
   component: "../layouts",
   routes: [
    // 移动之前路由配置到这里
  ]
}
  • 导航菜单移动至顶部,layouts/index.js
import { Menu } from "antd";
import Link from "umi/link";
export default function(props) {
 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" }}
    >
     <Menu.Item key="1">
      <Link to="/">商品</Link>
     </Menu.Item>
     <Menu.Item key="2">
      <Link to="/users">用户</Link>
     </Menu.Item>
     <Menu.Item key="3">
      <Link to="/about">关于</Link>
     </Menu.Item>
    </Menu>
   </Header>
   ...
  </Layout>
);
}
  • 样式设置,index.css
.logo {
 float: left;
 width: 160px;
 margin: 16px 16px 0 0;
}
  • 登录页面不需要布局,将登录路由配置移至顶层
{ path: "/login", component: "./login" },
{
 path: "/",
 component: "../layouts",
 ...
}
  • 解决导航页签在PrivateRoute重定向后未激活
const selectedKeys=[props.location.pathname];
<Menu selectedKeys={selectedKeys}>
    <Menu.Item key='/'></Menu.Item>
    <Menu.Item key='/users'></Menu.Item>
    <Menu.Item key='/about'></Menu.Item>
</Menu>

用户登录认证


  • 利用antd-design-proLogin/Exception/图表等业务组件加速开发进度
  • 引入ant-design-pro,安装:npm i ant-design-pro -S
  • 测试:修改404页面提示内容,404.js
// umi的配置,已经自动支持antd-pro的按需加载
import {Exception} from 'ant-design-pro'
export default function() {
 return (
  <Exception type="404" backText="返回首页"></Exception>
);
}
  • 登录页构建,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";
const { UserName, Password, Submit } = Login; // 通用的用户名、密码和提交组件
// 改为类形式组件,可持有状态
export default class extends Component {
 // let from = props.location.state.from || "/"; // 重定向地址
 onSubmit = (err, values) => {
  console.log(err, values);
};
 render() {
  return (
   <div className={styles.loginForm}>
   {/* logo */}
    <img className={styles.logo}
     src="https://img.kaikeba.com/logo-new.png"/>
   {/* 登录表单 */}
    <Login onSubmit={this.onSubmit}>
     <UserName
      name="username"
      placeholder="kaikeba"
      rules={[{ required: true, message: "请输入用户名" }]}
     />
     <Password
      name="password"
      placeholder="123"
      rules={[{ required: true, message: "请输入密码" }]}
     />
     <Submit>登录</Submit>
    </Login>
   </div>
 );
}
}

样式,login.css

.loginForm {
 width: 50%;
 margin: 100px auto;
 text-align: center;
}
.logo {
 margin-bottom: 30px;
}
  • 登录接口mock,创建./mock/login.js
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.json({
   code: -1,
   msg: "密码错误"
 });
}
};
  • 用户信息保存和登录动作编写,创建./src/models/user.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;
 }
}
};
  • 请求登录,login.js
import { connect } from "dva";
@connect()
export default class extends Component {
 onSubmit = (err, values) => {
  console.log("用户输入:", values);
  if (!err) {
   // 校验通过,提交登录
   this.props.dispatch({ type: "user/login", payload: values });
 }
};
 ...
}
  • 登录失败处理:
    设置响应状态码,./mock/login.js
// 设置401状态码
return res.status(401).json({
code: -1,
msg: "密码错误"
});

响应拦截,创建./src/interceptor.js

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

// 全局入口
import interceptor from './interceptor'

saga中异常处理,修改./src/models/user.js

*login({ payload }, { call, put }) {
   try {
    // 同之前,删除else部分
  } catch (error) {
    // 登录失败:错误信息已在拦截器实现,可执行其他业务
  }
}

商品列表


  • 数据mock,./mock/goods.js
  • 图片素材,./public/courses/*.png
  • 修改商品数据模型,src/pages/goods/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 };
 },
}
};
  • 显示课程分类页签,src/pages/goods.js
import { TagSelect } from "ant-design-pro";
@connect(
 state => ({
  courses: state.goods.courses, // 映射课程数据
  tags: state.goods.tags, // 映射标签数据
}),
{...}
)
class Goods extends 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>
 );
}
}
export default Goods;
  • 显示课程列表,src/pages/goods.js
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) {
  //  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;
  • TagSelect初始状态调整:默认应当全选
constructor(props) {
  super(props);
  this.state = {
   //...
   tags: [], // 默认未选中任何标签
 };
}
tagSelectChange = (tags, courses = this.props.courses) => {
  // 用户行为修改状态
  this.setState({ displayCourses, tags });  
};
// 组件受控
<TagSelect value={this.state.tags}>

添加购物车


  • 创建购物车模型,./src/models/cart.js
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 }];
  }
 }
}
};
  • 请求添加购物车,src/pages/goods.js
@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,新建./src/app.js,配置dva
export const dva = {
 config: {
  onStateChange(state) {
   if (localStorage) {
    localStorage.setItem("cart", JSON.stringify(state.cart));
  }
 }
}
};
  • 页头添加购物车信息,./src/layouts/index.js
export default class extends Component {
render(){
    ...
    <div style={{float:'right'}}>
      <Icon type="shopping-cart" style={{fontSize:18}}/>
      <span>我的购物车</span>
      <Badge count={5} offset={[-4,-18]}/>
    </div>
}
}

希望徽章小一点,添加全局样式,./src/global.css

.ant-badge-count{
  height: 16px;
  border-radius: 8px;
  min-width: 16px;
  line-height: 16px;
  padding: 0;
}

显示购物车数据

@connect(state => ({ // 连接购物车状态
 count: state.cart.length,
 cart: state.cart
}))
export default class extends Component {
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>
)
}
}

你的赞是我前进的动力

求赞,求评论,求分享...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容