Bootstrap

React--简单的抽卡模拟器

写在前面

  1. 这是我练习React的练手作业,毕竟官方给的井字棋还是有些简单了
  2. 用到的图片来源是网络,主要是三国杀的素材,所以严禁用于商业用途
  3. 这个模拟器主要是主页、抽卡动画页与抽卡记录页三个部分,动画用的css动画,记录则用的是indexedDB,所以看我的代码前最好了解一下这些东西
  4. 用到图片可能存在图名不对应的情况,因为我也不知道图片画的是谁,就随便猜了个名字
  5. 代码中可能存在一些问题,这很正常,毕竟这是练习用的,有问题欢迎大家指出
  6. 样式基本没采用响应式与浏览器兼容,所以最好在最新版的谷歌浏览器上查看
  7. 本来想着打个包给大家看,没想到打完包后indexedDB相关的代码一直在报错,改来改去改不好了就放弃了

功能介绍

1、主页

在这里插入图片描述
2、十连抽页

在这里插入图片描述
3、单抽页

在这里插入图片描述
4、抽卡记录页
在这里插入图片描述

代码结构

在这里插入图片描述

代码内容

1、heros.json

{
  "legend": [
    {
      "name": "步练师",
      "spell": "bu_lianshi"
    },
    {
      "name": "关羽",
      "spell": "guan_yu"
    },
    {
      "name": "郭嘉",
      "spell": "guo_jia"
    },
    {
      "name": "黄盖",
      "spell": "huang_gai"
    },
    {
      "name": "黄忠",
      "spell": "huang_zhong"
    },
    {
      "name": "鲁肃",
      "spell": "lu_su"
    },
    {
      "name": "陆逊",
      "spell": "lu_xun"
    },
    {
      "name": "司马懿",
      "spell": "si_mayi"
    },
    {
      "name": "孙尚香",
      "spell": "sun_shangxiang"
    },
    {
      "name": "严颜",
      "spell": "yan_yan"
    },
    {
      "name": "于禁",
      "spell": "yu_jin"
    },
    {
      "name": "周瑜",
      "spell": "zhou_yu"
    },
    {
      "name": "诸葛亮",
      "spell": "zhu_geliang"
    }
  ],
  "epic": [
    {
      "name": "步练师",
      "spell": "bu_lianshi"
    },
    {
      "name": "关羽",
      "spell": "guan_yu"
    },
    {
      "name": "郭嘉",
      "spell": "guo_jia"
    },
    {
      "name": "黄盖",
      "spell": "huang_gai"
    },
    {
      "name": "黄忠",
      "spell": "huang_zhong"
    },
    {
      "name": "鲁肃",
      "spell": "lu_su"
    },
    {
      "name": "陆逊",
      "spell": "lu_xun"
    },
    {
      "name": "司马懿",
      "spell": "si_mayi"
    },
    {
      "name": "孙尚香",
      "spell": "sun_shangxiang"
    },
    {
      "name": "严颜",
      "spell": "yan_yan"
    },
    {
      "name": "于禁",
      "spell": "yu_jin"
    },
    {
      "name": "周瑜",
      "spell": "zhou_yu"
    },
    {
      "name": "诸葛亮",
      "spell": "zhu_geliang"
    }
  ],
  "elite": [
    {
      "name": "步练师",
      "spell": "bu_lianshi"
    },
    {
      "name": "关羽",
      "spell": "guan_yu"
    },
    {
      "name": "郭嘉",
      "spell": "guo_jia"
    },
    {
      "name": "黄盖",
      "spell": "huang_gai"
    },
    {
      "name": "黄忠",
      "spell": "huang_zhong"
    },
    {
      "name": "鲁肃",
      "spell": "lu_su"
    },
    {
      "name": "陆逊",
      "spell": "lu_xun"
    },
    {
      "name": "司马懿",
      "spell": "si_mayi"
    },
    {
      "name": "孙尚香",
      "spell": "sun_shangxiang"
    },
    {
      "name": "严颜",
      "spell": "yan_yan"
    },
    {
      "name": "于禁",
      "spell": "yu_jin"
    },
    {
      "name": "周瑜",
      "spell": "zhou_yu"
    },
    {
      "name": "诸葛亮",
      "spell": "zhu_geliang"
    }
  ],
  "ordinary": [
    {
      "name": "步练师",
      "spell": "bu_lianshi"
    },
    {
      "name": "关羽",
      "spell": "guan_yu"
    },
    {
      "name": "郭嘉",
      "spell": "guo_jia"
    },
    {
      "name": "黄盖",
      "spell": "huang_gai"
    },
    {
      "name": "黄忠",
      "spell": "huang_zhong"
    },
    {
      "name": "鲁肃",
      "spell": "lu_su"
    },
    {
      "name": "陆逊",
      "spell": "lu_xun"
    },
    {
      "name": "司马懿",
      "spell": "si_mayi"
    },
    {
      "name": "孙尚香",
      "spell": "sun_shangxiang"
    },
    {
      "name": "严颜",
      "spell": "yan_yan"
    },
    {
      "name": "于禁",
      "spell": "yu_jin"
    },
    {
      "name": "周瑜",
      "spell": "zhou_yu"
    },
    {
      "name": "诸葛亮",
      "spell": "zhu_geliang"
    }
  ]
}

就是十三个武将与四种稀有度,分别是传奇、史诗、精英与普通,抽中概率分别是3%、7%、10%、80%,对应颜色分别是金色、红色、紫色、蓝色

2、index.js

import React from 'react';
import ReactDOM from 'react-dom';

// 抽卡相关
import './card.scss';
import CardArea from './components/card/CardArea';
import BtnArea from './components/card/BtnArea'

class Game extends React.Component {
  // 抽卡相关
  onRef = (ref) => {
    this.cardArea = ref;
  }

  render () {
    return (
      <div className="game" style={{ backgroundImage: `url(${require('./images/bg.jpg')})` }}>
        <BtnArea getHeros={ // 调用CardArea的方法
          () => {
            this.cardArea.updateCard(10);
          }
        } getHero={ // 调用CardArea的方法
          () => {
            this.cardArea.updateCard(1);
          }
        } />
        <CardArea onRef={this.onRef} />
      </div >
    );
  }
}

ReactDOM.render(
  <Game />,
  document.getElementById('root')
);

这个简单,除了兄弟组件互相调用对方的方法之外也没什么

3、BtnArea.js

import React from 'react';

import History from './History';

export default class BtnArea extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      times: 0
    }

    this.calculationTimes = this.calculationTimes.bind(this) // 方法绑定this的一种解决方案
  }

  onRef = (ref) => {
    this.history = ref
  }

  calculationTimes (subtractor) { // 抽奖次数的变化
    if (this.state.times < subtractor) {
      alert('抽奖系数不足');
      return false;
    } else {
      this.setState({
        times: this.state.times - subtractor
      })
    }
    return true;
  }

  render () { // 页面渲染,子组件History
    return (
      <div className="index_page">
        <History onRef={this.onRef} />
        <div className="btn_area">
          <div className="increase_times">
            <span>{this.state.times}</span>
            <img onClick={() => {
              this.setState({
                times: this.state.times + 10
              })
            }} src={require('./../../images/add.png')} alt="" />
          </div>
          <button className="single_btn" style={{ backgroundImage: `url(${require('./../../images/single.png')})` }} onClick={() => {
            if (this.calculationTimes(1)) {
              this.props.getHero();
            }
          }}>单抽</button>
          <button className="ten_btn" style={{ backgroundImage: `url(${require('./../../images/ten.png')})` }} onClick={() => {
            if (this.calculationTimes(10)) {
              this.props.getHeros();
            }
          }}>十连抽</button>
          <div className="history_btn" onClick={() => {
            this.history.getList();
          }}>抽卡记录</div>
        </div>
      </div>
    )
  }
}

这是十连抽与单抽按钮内容,还加上了抽卡记录

4、Card.js

import React from "react"

export default class Card extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      underway: false
    }
  }

  getRarity (rarity) { // 将英文转为中文
    switch (rarity) {
      case 'legend':
        return '传奇';
      case 'epic':
        return '史诗';
      case 'elite':
        return '精英';
      default:
        return '普通'
    }
  }

  render () { // 单个卡片
    return (
      <div className={['card', this.state.underway ? 'flip_card' : ''].join(' ')} onClick={() => {
        this.setState({
          underway: true,
        })
      }}>
        <div className={["card_face", `rarity_${this.props.cardInfo.rarity}`].join(' ')} style={{ backgroundImage: `url(${require('./../../images/' + this.props.cardInfo.hero.spell + '.jpg')})` }}>
          <div className="card_rarity">{this.getRarity(this.props.cardInfo.rarity)}</div>
          <div className="card_info">
            <div className="card_name">{this.props.cardInfo.hero.name}</div>
          </div>
        </div>
        <img src={require('./../../images/card_bg.jpg')} alt="" />
      </div>
    );
  }
}

单个卡片,翻转卡片动画用的是切换class再加上CSS动画功能实现的

5、CardArea.js

import React from 'react';
import Card from './Card';

import heros from './../../data/heros.json';

export default class CardArea extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      cardAreaStatus: false, // 抽卡状态
      cardArr: [], // 抽出的卡片
      indexedDB: null // 本地数据库
    }
    this.addHistory = this.addHistory.bind(this);
  }

  componentDidMount () {
    this.props.onRef && this.props.onRef(this);
    const request = window.indexedDB.open('History');
    request.onerror = () => {
      console.error('Error')
    }

    request.onsuccess = (e) => {
      this.setState({
        indexedDB: e.target.result
      })
    }

    request.onupgradeneeded = (event) => {
      const db = event.target.result;
      let objectStore;
      console.log(event, db);
      if (!db.objectStoreNames.contains('heros')) {
        objectStore = db.createObjectStore('heros', { autoIncrement: true });
        objectStore.createIndex('id', 'id', { unique: true });
        objectStore.createIndex('rarity', 'rarity', { unique: false });
        objectStore.createIndex('name', 'name', { unique: false });
      }
    }
  }

  addHistory (card) { // 点击抽卡将抽卡结果加入到indexedDB里
    console.log(this.state);
    const customerOS = this.state.indexedDB.transaction(['heros'], 'readwrite').objectStore('heros');
    const data = {
      id: Number(Math.random().toString().substr(3, 10) + Date.now()).toString(36),
      rarity: card.rarity || null,
      name: card.hero.name || null
    }
    const request = customerOS.add(data);
    request.onsuccess = () => {
      console.log(data, '数据已新增');
    }
    request.onerror = () => {
      console.error(data);
    }
  }

  // 更新卡池
  updateCard (num) {
    this.setState({
      cardAreaStatus: true,
      cardArr: this.randomHero(num)
    })
  }

  // 关闭抽卡
  closeCardArea () {
    this.setState({
      cardAreaStatus: false,
      cardArr: []
    })
  }


  getHeros () { // 获取单个英雄
    const rarityNum = Math.floor(Math.random() * 100); // 使用伪随机数(0 - 100之间的整数)获取随机稀有度
    let rarityArr = heros.ordinary;
    let rarity = "ordinary";
    if (rarityNum < 3) { // 此处配置抽卡几率
      rarityArr = heros.legend;
      rarity = "legend";
    } else if (rarityNum < 10) {
      rarityArr = heros.epic;
      rarity = "epic";
    } else if (rarityNum < 20) {
      rarityArr = heros.elite;
      rarity = "elite";
    }
    const heroNum = Math.floor(Math.random() * rarityArr.length); // 此处获取随机武将
    const hero = rarityArr[heroNum];
    const card = {
      rarity: rarity,
      hero: hero
    }
    this.addHistory(card);
    return card;
  }

  randomHero (num) { // 执行单抽与十连抽操作,按传入数字判断
    const heros = [];
    for (let i = 0; i < num; i++) {
      heros.push(this.getHeros());
    }
    return heros;
  }

  render () { // 生成页面
    const cardArea = this.state.cardArr.map((card, index) =>
      <Card key={index} cardInfo={card} />
    )
    return (
      <div className="card_main" style={{ display: this.state.cardAreaStatus ? "flex" : "none", backgroundImage: `url(${require('./../../images/bg_card.jpg')})` }}>
        <div className="close_btn" onClick={this.closeCardArea.bind(this)}>
          <img src={require('./../../images/close.png')} alt="" />
        </div>
        <div className="card_area">
          {cardArea}
        </div>
      </div>
    );
  }
}

这里是抽卡页,在此处随机获取不同稀有度的不同武将

6、History.js

import React from 'react';

export default class History extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      historyStatus: false,
      historyList: [],
      indexedDB: null,
      count: []
    }
    this.getList = this.getList.bind(this);
  }

  componentDidMount () {
    this.props.onRef && this.props.onRef(this);
    const request = window.indexedDB.open('History');
    request.onerror = () => {
      console.error('Error')
    }

    request.onsuccess = (e) => {
      this.setState({
        indexedDB: e.target.result
      })
    }
  }

  getList = () => { // 获取所有抽卡历史,使用箭头函数也可以确保this调用没问题
    const customerOS = this.state.indexedDB.transaction(['heros'], 'readwrite').objectStore('heros');
    let dataArr = [];
    customerOS.openCursor().onsuccess = (event) => {
      const result = event.target.result;
      if (result) {
        dataArr.push(result.value);
        result.continue();
      }
      this.setState({
        historyStatus: true,
        historyList: dataArr
      })
    }
    this.getCount();
  }

  getRarity (rarity) { // 同样的英文转中文
    switch (rarity) {
      case 'legend':
        return '传奇';
      case 'epic':
        return '史诗';
      case 'elite':
        return '精英';
      default:
        return '普通'
    }
  }

  getCount = () => { // 此处统计抽卡结果,用的是indexedDB自动统计的,当然你也可以用for循环一个一个统计
    const typeArr = ['legend', 'epic', 'elite', 'ordinary'];
    let count = [];
    // 执行事务,从对象仓库(表)中获取所有数据
    const request = this.state.indexedDB.transaction(['heros'], 'readonly').objectStore('heros');
    for (let index = 0; index < typeArr.length; index++) {
      const typeCount = request.index('rarity').count(typeArr[index]);
      //数据获取成功
      typeCount.onsuccess = () => {
        if (typeCount.result) {
          count.push(typeCount.result);
          this.setState({
            count: count
          })
        }
      };
    }
  }

  sumCount () { // 获取总计的抽奖次数,这里我是遍历了getCount的结果数组,当然用indexedDB的count方法也可以直接获取
    let total = 0;
    this.state.count.forEach(value => {
      total += value;
    });
    return total;
  }

  render () {
    const historyTab = this.state.historyList.map((history, index) =>
      <tr className={`rarity_${history.rarity}`} key={index}>
        <td>{index + 1}</td>
        <td>{this.getRarity(history.rarity)}</td>
        <td>{history.name}</td>
      </tr>
    )
    return (
      <div className="history_area" style={{ display: this.state.historyStatus ? "flex" : "none" }}>
        <div className="history_list">
          <table>
            <thead>
              <tr>
                <th>序号</th>
                <th>稀有度</th>
                <th>武将</th>
              </tr>
            </thead>
            <tbody>
              {historyTab}
            </tbody>
          </table>
        </div>
        <div className="history_count">
          <div className="rarity_legend">传奇:{this.state.count[0]}</div>
          <div className="rarity_epic">史诗:{this.state.count[1]}</div>
          <div className="rarity_elite">精英:{this.state.count[2]}</div>
          <div className="rarity_ordinary">普通:{this.state.count[3]}</div>
          <div className="rarity_total">总计:{this.sumCount()}</div>
          <button onClick={() => {
            this.setState({
              historyStatus: false
            })
          }}>关闭记录</button>
        </div>
      </div>
    )
  }
}

具体内容都在代码里注释出来了,当然不熟悉indexedDB也可以不要这个内容

7、card.scss

@import "./fonts/font";
html,
body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  margin: 0;
  padding: 0;
  /* 设置滚动条的样式 */
  ::-webkit-scrollbar {
    width: 5px;
  }
  /* 滚动槽 */
  ::-webkit-scrollbar-track {
    box-shadow: inset006pxrgba(0, 0, 0, 0.3);
    border-radius: 10px;
  }
  /* 滚动条滑块 */
  ::-webkit-scrollbar-thumb {
    border-radius: 5px;
    background: rgba(150, 150, 150, 0.5);
    box-shadow: inset006pxrgba(0, 0, 0, 0.5);
  }
  ::-webkit-scrollbar-thumb:window-inactive {
    background: rgba(255, 0, 0, 0.4);
  }
  #root {
    width: 100%;
    height: 100%;
    .game {
      width: 100%;
      height: 100%;
      background-size: 100% 100%;
      display: flex;
      align-items: center;
      justify-content: center;
      font-family: "AaDingCiShouShu";
      .rarity_legend {
        color: #ffd700;
        .card_rarity {
          background-color: rgba(255, 215, 0, 0.8);
        }
      }
      .rarity_epic {
        color: #ff0000;
        .card_rarity {
          background-color: rgba(255, 0, 0, 0.8);
        }
      }
      .rarity_elite {
        color: #800080;
        .card_rarity {
          background-color: rgba(128, 0, 128, 0.8);
        }
      }
      .rarity_ordinary {
        color: #87ceeb;
        .card_rarity {
          background-color: rgba(135, 206, 235, 0.8);
        }
      }
      .index_page {
        .btn_area {
          .increase_times {
            position: fixed;
            top: 2rem;
            right: 2rem;
            font-size: 2rem;
            background-color: rgba(0, 0, 0, 0.5);
            color: #fff;
            padding: 0.5rem 1rem;
            border-radius: 25px;
            display: flex;
            align-items: center;
            img {
              width: 1.8rem;
              padding-left: 0.5rem;
            }
          }
          button {
            width: 12rem;
            height: 4rem;
            font-size: 2rem;
            margin: 0 15rem;
            border-radius: 4px;
            color: #ffffff;
            border: 0;
            outline: 0;
            font-family: "AaDingCiShouShu";
            box-shadow: 10px 10px 10px rgba(0, 0, 0, 0.5);
            transition: all 0.5s;
            background-size: 100% 100%;
            &:hover {
              outline: 0;
            }
            &:active {
              box-shadow: none;
            }
          }
          .single_btn {
            background-color: #87ceeb;
          }
          .ten_btn {
            background-color: #ffd700;
          }
          .history_btn {
            position: fixed;
            bottom: 2rem;
            right: 2rem;
            color: #fff;
            text-decoration-line: underline;
            cursor: pointer;
          }
        }
        .history_area {
          position: fixed;
          width: 100%;
          top: 0;
          height: 100%;
          left: 0;
          bottom: 0;
          right: 0;
          margin: auto;
          padding: 2rem;
          background-color: rgba(0, 0, 0, 0.8);
          border-radius: 4px;
          z-index: 3;
          .history_list {
            width: calc(100% - 20rem);
            position: relative;
            table {
              width: 100%;
              height: 100%;
              color: #fff;
              tbody {
                display: block;
                height: calc(100% - 2rem);
                overflow-y: auto;
              }
              thead,
              tbody tr {
                display: table;
                width: 100%;
                table-layout: fixed;
              }
              thead {
                width: calc(100% - 5px);
              }
              th {
                font-size: 2rem;
              }
              td {
                font-size: 1.6rem;
                text-align: center;
                text-shadow: 0 0 1px #fff;
              }
            }
          }
          .history_count {
            width: 20rem;
            div {
              margin-left: 2rem;
              font-size: 2rem;
              height: 4rem;
              line-height: 4rem;
            }
            .rarity_total {
              color: #fff;
            }
            button {
              position: absolute;
              right: 2rem;
              bottom: 2rem;
              border: 0;
              padding: 0.5rem 1rem;
              border-radius: 4px;
              font-family: "AaDingCiShouShu";
            }
          }
        }
      }
      .card_main {
        width: 100%;
        height: 100%;
        display: flex;
        justify-content: center;
        align-items: center;
        background-size: inherit;
        position: absolute;
        .close_btn {
          width: 2rem;
          height: 2rem;
          position: fixed;
          top: 2rem;
          right: 2rem;
          img {
            width: 100%;
          }
        }
        .card_area {
          width: 80%;
          height: 80%;
          display: flex;
          flex-wrap: wrap;
          justify-content: space-between;
          align-items: center;
          .flip_card {
            animation: flipCard 1s forwards;
            img {
              animation: hideBg 0.5s forwards;
            }
          }
          @keyframes flipCard {
            0% {
              transform: rotateY(0);
            }
            50% {
              transform: rotateY(90deg);
            }
            100% {
              transform: rotateY(180deg);
            }
          }
          @keyframes hideBg {
            90% {
              opacity: 1;
            }
            100% {
              opacity: 0;
            }
          }
          .card {
            width: calc(20% - 2rem);
            height: calc(50% - 1rem);
            border-radius: 8px;
            margin: 1rem auto;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
            position: relative;
            img {
              width: 100%;
              height: 100%;
              border-radius: 8px;
              position: absolute;
              top: 0;
            }
            .card_face {
              width: 100%;
              height: 100%;
              padding: 0.5rem;
              transform: rotateY(180deg);
              background-size: cover;
              border-radius: 8px;
              text-shadow: 0 0 5px rgba(255, 255, 255, 0.6);
              .card_rarity {
                font-size: 1rem;
                float: right;
                font-weight: bold;
                color: #fff;
                padding: 4px 10px;
                border-radius: 4px;
              }
              .card_info {
                position: absolute;
                bottom: 1rem;
                text-align: center;
                font-weight: 800;
                left: 0;
                right: 0;
                margin: auto;
                .card_name {
                  text-shadow: 0 0 10px #000;
                }
              }
            }
          }
        }
      }
    }
  }
}
* {
  box-sizing: border-box;
}

这里是样式文件,内容很多就不一一介绍了

结尾

大体上的代码都贴出来了,也就是给大家当个例子看看,写完这个例子对React也就算入门了。
这里是字体文件与图片文件的下载地址链接:
链接: 百度云盘-静态文件 提取码: 68pi

字体文件很大,可以不要,图片也能换自己喜欢的,多谢你看完我的文章

;