写在前面
- 这是我练习React的练手作业,毕竟官方给的井字棋还是有些简单了
- 用到的图片来源是网络,主要是三国杀的素材,所以严禁用于商业用途
- 这个模拟器主要是主页、抽卡动画页与抽卡记录页三个部分,动画用的css动画,记录则用的是indexedDB,所以看我的代码前最好了解一下这些东西
- 用到图片可能存在图名不对应的情况,因为我也不知道图片画的是谁,就随便猜了个名字
- 代码中可能存在一些问题,这很正常,毕竟这是练习用的,有问题欢迎大家指出
- 样式基本没采用响应式与浏览器兼容,所以最好在最新版的谷歌浏览器上查看
- 本来想着打个包给大家看,没想到打完包后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
字体文件很大,可以不要,图片也能换自己喜欢的,多谢你看完我的文章