├── .babelrc ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── favicon.ico ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── manifest.json ├── package.json └── prod.html ├── src ├── css │ ├── app.css │ ├── bootstrap.css │ ├── day.css │ └── night.css ├── index.js ├── js │ ├── App.jsx │ ├── App.test.jsx │ ├── config.jsx │ ├── grade.jsx │ ├── help.jsx │ ├── index.jsx │ ├── left.jsx │ ├── navbar.jsx │ ├── registerServiceWorker.js │ ├── right.jsx │ ├── storage.js │ ├── test.jsx │ ├── typing.jsx │ └── words.js └── prod.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react","env" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | debug.log 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 garfeng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | rm build/* -rf 3 | webpack 4 | cp public/package.json build/ 5 | 6 | test: 7 | npm run start 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 汉字输入法练习 2 | 3 | 网页版练习地址:[点击直达](https://garfeng.github.io/xnhe/) 4 | 5 | ## 写这个工具的原因: 6 | 7 | * 自己可随时练习小鹤双拼。 8 | * 现有的练习工具需要频繁地点击鼠标(比如每次练好一个小章节,弹窗口提示成绩,点击关闭才能继续练习)。或者重打的快捷键太远,这对于我有点难以忍受。我希望有个工具可以不停地练习,手不需要离开键盘,但又能及时显示成绩。 9 | * 我希望一个工具可以帮助我们进阶,自动添加新字,自动增加错误字的练习次数。而不是纯靠我们手动控制下一步练习什么,那个太费脑子。 10 | * …… 11 | 12 | 综上,一个字: 13 | 14 | # 懒! 15 | 16 | 除了打字,别的我什么都不想干。 17 | 18 | ## 为什么不提供编码提示…… 19 | 20 | 本来是有的。但本程序还没写完,我发现自己已经不需要编码提示(接触小鹤仅三天),又去掉了。 21 | 22 | 本程序只用来练习条反,肌肉记忆,不用于熟悉编码。所以我现在用它练习飞扬~ 23 | 24 | 熟悉编码可以到这里:[双拼在线练习](http://typing.sjz.io/) 25 | 26 | ## 练习方法 27 | 28 | 网页版练习地址:[点击直达](https://garfeng.github.io/xnhe/) 29 | 30 | 1. 在输入框输入闪动的汉字。 31 | 32 | 3. 第一个面板里的文字列表,会显示每个字的正确率,当所有文字正确率达到70%以上,且你的击键达到`目标击键`,程序会自动在列表里添加一个字给你练。 33 | 34 | 4. 点击右侧导航里的`设置`,设置你的`目标击键`,每一段的字数,要练习的文本,以及皮肤。有程序员需要的黑色皮肤呦! 35 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garfeng/xnhe/bfd64cfc3a9596e7f3596c992c4a07cde55a6f25/favicon.ico -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xnhe", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "echarts": "^4.0.2", 7 | "react": "^16.2.0", 8 | "react-dom": "^16.2.0", 9 | "react-router": "^4.2.0", 10 | "react-router-dom": "^4.2.2", 11 | "react-scripts": "1.0.17", 12 | "reactstrap": "^5.0.0-beta.2" 13 | }, 14 | "devDependencies": { 15 | "babel-loader": "^7.1.2", 16 | "babel-preset-react": "^6.24.1", 17 | "css-loader": "^0.28.8" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test --env=jsdom", 23 | "eject": "react-scripts eject" 24 | } 25 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garfeng/xnhe/bfd64cfc3a9596e7f3596c992c4a07cde55a6f25/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | 25 | 汉字输入法练习 26 | 27 | 28 | 29 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xnhe", 3 | "main": "index.html" 4 | } -------------------------------------------------------------------------------- /public/prod.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 23 | 32 | 汉字输入法练习 33 | 34 | 35 | 36 | 39 |
40 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/css/app.css: -------------------------------------------------------------------------------- 1 | .flex{ 2 | display: flex; 3 | flex-direction: row; 4 | flex-wrap: wrap; 5 | justify-content: flex-start; 6 | align-content: flex-start; 7 | } 8 | 9 | .one-character{ 10 | width:1.5rem; 11 | height:1.6rem; 12 | text-align:center; 13 | font-size: 1.2rem; 14 | line-height: 1.6rem; 15 | margin-bottom: 0.3rem; 16 | } 17 | 18 | @-webkit-keyframes progress-current { 19 | from { 20 | border-bottom: 2px solid #6CC3D5; 21 | } 22 | 50% { 23 | border-bottom: 2px solid transparent; 24 | } 25 | to { 26 | border-bottom: 2px solid #6CC3D5; 27 | } 28 | 29 | } 30 | 31 | @keyframes progress-current { 32 | from { 33 | border-bottom: 2px solid #6CC3D5; 34 | } 35 | 50% { 36 | border-bottom: 2px solid transparent; 37 | } 38 | to { 39 | border-bottom: 2px solid #6CC3D5; 40 | } 41 | } 42 | 43 | .current-chatacter{ 44 | background-color: transparent; 45 | background-image: none; 46 | /* 47 | border-radius: 0.4rem; 48 | border: 1px solid #6CC3D5;*/ 49 | 50 | -webkit-animation: progress-current 1s linear infinite; 51 | nimation: progress-current 1s linear infinite; 52 | } 53 | 54 | .keyboard-container{ 55 | 56 | } 57 | 58 | .keyboard:hover{ 59 | background-color: #5eb69d; 60 | cursor: pointer; 61 | } 62 | 63 | .key-line{ 64 | display: flex; 65 | flex-direction: row; 66 | flex-wrap: nowrap; 67 | justify-content: flex-start; 68 | align-content: flex-start; 69 | } 70 | 71 | .key-button { 72 | flex:2 1 4rem; 73 | margin:0.1rem; 74 | 75 | /*width: 3rem; 76 | height: 3rem; 77 | */ 78 | } 79 | 80 | .key-space { 81 | flex:1 1 2rem; 82 | margin:0rem; 83 | opacity: 0; 84 | } 85 | 86 | .height-100{ 87 | /*height: 100%;*/ 88 | } 89 | 90 | .flex-center{ 91 | display: flex; 92 | flex-direction: column; 93 | flex-wrap: nowrap; 94 | justify-content: center; 95 | align-content: center; 96 | overflow-y: auto; 97 | } 98 | 99 | html{ 100 | } -------------------------------------------------------------------------------- /src/css/day.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | .img-thumbnail, 4 | .table .table, 5 | .form-control, 6 | .form-control:focus, 7 | select.form-control:focus::-ms-value, 8 | .dropdown-menu, 9 | .custom-select:focus::-ms-value, 10 | .custom-file-label, 11 | .nav-tabs .nav-link.active, 12 | .nav-tabs .nav-item.show .nav-link, 13 | .card, 14 | .list-group-item, 15 | .list-group-item.disabled, 16 | .list-group-item:disabled, 17 | .modal-content, 18 | .popover, 19 | .carousel-indicators .active { 20 | background-color: #FFF; 21 | } -------------------------------------------------------------------------------- /src/css/night.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | background-color: #232930; 4 | } 5 | 6 | .img-thumbnail { 7 | background-color: #232930; 8 | } 9 | 10 | .table .table { 11 | background-color: #232930; 12 | } 13 | 14 | .form-control { 15 | background-color: #232930; 16 | } 17 | 18 | .form-control:focus { 19 | background-color: #232930; 20 | } 21 | 22 | select.form-control:focus::-ms-value { 23 | background-color: #232930; 24 | } 25 | 26 | .dropdown-menu { 27 | background-color: #232930; 28 | } 29 | 30 | .custom-select:focus::-ms-value { 31 | background-color: #232930; 32 | } 33 | 34 | .custom-file-label { 35 | background-color: #232930; 36 | } 37 | 38 | .nav-tabs .nav-link.active { 39 | background-color: #232930; 40 | } 41 | 42 | .nav-tabs .nav-item.show .nav-link { 43 | background-color: #232930; 44 | } 45 | 46 | .card { 47 | background-color: #232930; 48 | } 49 | 50 | .list-group-item { 51 | background-color: #232930; 52 | } 53 | 54 | .list-group-item.disabled { 55 | background-color: #232930; 56 | } 57 | 58 | .list-group-item:disabled { 59 | background-color: #232930; 60 | } 61 | 62 | .modal-content { 63 | background-color: #232930; 64 | } 65 | 66 | .popover { 67 | background-color: #232930; 68 | } 69 | 70 | .carousel-indicators .active { 71 | background-color: #232930; 72 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './js/index'; 2 | import './css/bootstrap.css'; 3 | import './css/app.css'; 4 | -------------------------------------------------------------------------------- /src/js/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class App extends Component { 4 | render() { 5 | return (
6 |
7 |

8 | Welcome to React 9 |

10 |
11 |

12 | To get started, edit src / App.js and save to reload. 13 |

14 |
15 | ); 16 | } 17 | } 18 | 19 | export default App; -------------------------------------------------------------------------------- /src/js/App.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/js/config.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Row, Col, Card, CardHeader, CardBody, Form, FormText, FormGroup, Label, Input, Button, CardBlock, InputGroup, InputGroupAddon, InputGroupText } from "reactstrap"; 3 | import db from './storage'; 4 | 5 | class SelectButton extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.onClick = this.onClick.bind(this); 9 | } 10 | 11 | onClick() { 12 | this.props.onClick(this.props.value); 13 | } 14 | render() { 15 | return (); 16 | } 17 | } 18 | 19 | class OneInputLine extends Component { 20 | render() { 21 | return 22 | 23 | 24 | 25 | {this.props.children} 26 | 27 | 28 | 29 | } 30 | } 31 | 32 | class Config extends Component { 33 | constructor(props) { 34 | super(props); 35 | this.state = { 36 | goalSpeed: parseInt(db.getItem("goalSpeed")) || 1, 37 | goalWordSpeed: parseInt(db.getItem("goalWordSpeed")) || 30, 38 | currentLen: parseInt(db.getItem("currentLen")) || 10, 39 | article: db.getItem("article") || "", 40 | night: parseInt(db.getItem("night")) || 0, 41 | simple: parseInt(db.getItem("simple")) || 0 42 | } 43 | this.selectSpeed = this.selectSpeed.bind(this); 44 | this.selectLen = this.selectLen.bind(this); 45 | this.onInputArticle = this.onInputArticle.bind(this); 46 | this.triggerNight = this.triggerNight.bind(this); 47 | this.triggerMode = this.triggerMode.bind(this) 48 | this.modeMap = ["可爱", "日间", "夜间", "木板墙"]; 49 | this.simpleMode = ["正常", "极简"]; 50 | this.onGoalWordSpeed = this.onGoalWordSpeed.bind(this); 51 | 52 | } 53 | 54 | 55 | triggerNight(d) { 56 | const index = Math.max(this.modeMap.indexOf(d), 0); 57 | this.setState({ night: index }); 58 | db.setItem("night", index.toString()); 59 | window.forceUpdate(); 60 | } 61 | 62 | onInputArticle(e) { 63 | this.setState({ article: e.target.value }); 64 | db.setItem("article", e.target.value); 65 | } 66 | 67 | selectLen(d) { 68 | this.setState({ currentLen: d }); 69 | db.setItem("currentLen", d.toString()) 70 | } 71 | 72 | selectSpeed(d) { 73 | if (this.state.goalSpeed < d) { 74 | db.setItem("wordsSelect", ""); 75 | } 76 | this.setState({ goalSpeed: d }); 77 | db.setItem("goalSpeed", d.toString()); 78 | } 79 | 80 | onGoalWordSpeed(e) { 81 | const speed = (parseInt(e.target.value) || 30); 82 | this.setState({ goalWordSpeed: speed }) 83 | db.setItem("goalWordSpeed", speed.toString()) 84 | } 85 | 86 | triggerMode(d) { 87 | const index = Math.max(this.simpleMode.indexOf(d), 0); 88 | this.setState({ simple: index }); 89 | db.setItem("simple", index.toString()); 90 | window.forceUpdate(); 91 | } 92 | 93 | render() { 94 | const speedMap = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 95 | const lenMap = [10, 20, 30, 50, 100, 200, 500]; 96 | 97 | return ( 98 | 设置 99 | 100 |
101 | 102 | {speedMap.map(d => )} 103 | 104 | 达到后,且正确率>70%,才会添加一个字继续练习。 105 | 106 | 107 | 108 | 109 | 110 | 111 | 字/分 112 | 113 | 114 | 115 | 116 | {lenMap.map(d => )} 117 | 118 | 119 | {this.modeMap.map(d => )} 120 | 121 | 122 | {this.simpleMode.map(d => )} 123 | 手机用户可选择极简 124 | 125 |
126 |
127 |
) 128 | } 129 | } 130 | 131 | 132 | export default Config; -------------------------------------------------------------------------------- /src/js/grade.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Card, CardHeader, CardBody, Table } from "reactstrap"; 3 | import db from './storage'; 4 | import echarts from "echarts"; 5 | 6 | class OneLine extends Component { 7 | render() { 8 | const d = this.props.data; 9 | const time = new Date(d.time) 10 | const dateS = time.toLocaleDateString(); 11 | const timeS = time.toLocaleTimeString(); 12 | return ( 13 | {dateS}{" "}{timeS} 14 | {parseInt(d.speed * 100) / 100} 15 | {parseInt(d.wordsSpeed * 100) / 100} 16 | {parseInt(d.error * 100)}% 17 | ) 18 | } 19 | } 20 | /* 21 | class XZoBn extends Component { 22 | constructor(props) { 23 | super(props); 24 | } 25 | 26 | render() { 27 | const width = this.props.width; 28 | const height = this.props.height; 29 | return (); 30 | } 31 | } 32 | 33 | class YZoBn extends Component { 34 | constructor(props) { 35 | super(props); 36 | } 37 | 38 | render() { 39 | const width = this.props.width; 40 | const height = this.props.height; 41 | return (); 42 | } 43 | } 44 | */ 45 | class Grade extends Component { 46 | constructor(props) { 47 | super(props); 48 | const str = db.getItem("history") || "[]"; 49 | const data = JSON.parse(str) || []; 50 | this.state = { 51 | data: data//.reverse(), 52 | }; 53 | } 54 | 55 | render() { 56 | return ( 57 |
58 | 59 | 成绩 60 | 61 | 62 | 63 | 64 | 65 |
66 | ); 67 | } 68 | } 69 | 70 | class AllInfo extends Component { 71 | constructor(props) { 72 | super(props); 73 | this.state = JSON.parse(db.getItem("all_info")) || { 74 | number: 0, 75 | time: 0 76 | }; 77 | this.OneLine = this.OneLine.bind(this); 78 | this.timeInfo = this.timeInfo.bind(this); 79 | } 80 | 81 | timeInfo({ time, number }) { 82 | const t = parseInt(time) / 100; 83 | let tShow = "" 84 | if (t < 60) { 85 | tShow = t.toString() + "秒"; 86 | } else if (t < 3600) { 87 | const tMinute = parseInt(t / 60); 88 | const tSecond = parseInt(t) % 60; 89 | tShow = tMinute.toString() + "分" + tSecond.toString() + "秒"; 90 | } else { 91 | let tMinute = parseInt(t / 60); 92 | const tHour = parseInt(tMinute / 60); 93 | tMinute = tMinute % 60; 94 | tShow = tHour.toString() + "时" + tMinute.toString() + "分"; 95 | } 96 | 97 | const zi = number; 98 | let ziShow = "" 99 | if (zi < 1000) { 100 | ziShow = zi 101 | } else if (zi < 10000) { 102 | const ziK = parseInt(zi / 10) / 100; 103 | ziShow = ziK.toString() + "K"; 104 | } else { 105 | const ziK = parseInt(zi / 100) / 100; 106 | ziShow = ziK.toString() + "万"; 107 | } 108 | const speed = parseInt(zi / Math.max(t, 1) * 60) 109 | const speedShow = speed.toString() + "字/分"; 110 | 111 | return { 112 | time: tShow, 113 | number: ziShow, 114 | speed: speedShow 115 | } 116 | } 117 | 118 | OneLine(d, i) { 119 | const info = this.timeInfo(d); 120 | return 121 | {d.date} 122 | {info.time} 123 | {info.number} 124 | {info.speed} 125 | 126 | } 127 | 128 | render() { 129 | 130 | const allInfo = this.timeInfo(this.state); 131 | 132 | const allDays = this.state.days || {}; 133 | let allDaysArr = []; 134 | 135 | console.log(allDays); 136 | 137 | for (let i in allDays) { 138 | allDaysArr.push(allDays[i]); 139 | } 140 | 141 | allDaysArr.reverse(); 142 | 143 | return ( 144 | 145 | 146 | 统计 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | {allDaysArr.map(this.OneLine)} 166 | 167 |
-总时间总字数均速
全部{allInfo.time}{allInfo.number}{allInfo.speed}
168 |
169 |
) 170 | } 171 | } 172 | 173 | class Speed extends Component { 174 | constructor(props) { 175 | super(props); 176 | this.draw = this.draw.bind(this); 177 | this.setTimeOut = this.setTimeOut.bind(this); 178 | this.tooltip = this.tooltip.bind(this); 179 | this.chart = null; 180 | } 181 | 182 | tooltip(params) { 183 | const time = new Date(this.props.data[params[0].name].time); 184 | const dateS = time.toLocaleDateString(); 185 | const timeS = time.toLocaleTimeString(); 186 | 187 | if (params.length == 2) { 188 | return dateS + " " + timeS + '
' 189 | + params[0].seriesName + ' : ' + params[0].value + ' (次/秒)
' 190 | + params[1].seriesName + ' : ' + params[1].value + ' (字/分)'; 191 | } else if (params.length == 1) { 192 | if (params[0].seriesName == "击键") { 193 | return dateS + " " + timeS + '
' 194 | + params[0].seriesName + ' : ' + params[0].value + " (次/秒)"; 195 | } else { 196 | return dateS + " " + timeS + '
' 197 | + params[0].seriesName + ' : ' + params[0].value + ' (字/分)'; 198 | } 199 | } else { 200 | return dateS + " " + timeS; 201 | } 202 | 203 | } 204 | 205 | ave(...data) { 206 | var d = 0; 207 | data.map(v => { d = d + v }); 208 | d = d / data.length; 209 | return d || 0; 210 | } 211 | 212 | draw() { 213 | this.chart = echarts.init(this.refs["chart"]); 214 | let timeList = []; 215 | let speedList = []; 216 | let speedWordsList = []; 217 | 218 | this.props.data.map((d, i) => { 219 | timeList.push(i); 220 | speedList.push(Math.floor(d.speed * 10) / 10); 221 | speedWordsList.push(Math.floor(d.wordsSpeed * 10) / 10); 222 | }); 223 | 224 | const maxSpeed = Math.ceil(Math.max(...speedList) / 2) * 2 || 2; 225 | const maxSpeedWords = Math.ceil(Math.max(...speedWordsList) / 20) * 20 || 20; 226 | 227 | const minSpeed = Math.floor(Math.min(...speedList) / 2) * 2 || 1; 228 | const minSpeedWords = Math.floor(Math.min(...speedWordsList) / 20) * 20 || 10; 229 | 230 | const aveSpeed = this.ave(...speedList); 231 | const aveSpeedWords = this.ave(...speedWordsList); 232 | 233 | const dData = Math.ceil((aveSpeed - aveSpeedWords / 30)); 234 | 235 | const interval = ((maxSpeedWords - minSpeedWords) / (maxSpeed - minSpeed)); 236 | this.chart.setOption({ 237 | title: { 238 | text: '' 239 | }, 240 | xAxis: { 241 | data: timeList, 242 | type: "category", 243 | boundaryGap: false, 244 | axisLine: { onZero: true } 245 | }, 246 | tooltip: { 247 | trigger: 'axis', 248 | formatter: this.tooltip 249 | }, 250 | toolbox: { 251 | show: true, 252 | feature: { 253 | mark: { show: true }, 254 | dataView: { show: true, readOnly: false }, 255 | restore: { show: true }, 256 | saveAsImage: { show: true } 257 | } 258 | }, 259 | legend: { 260 | data: ['击键', '打字'], 261 | x: 'left' 262 | }, 263 | yAxis: [ 264 | { 265 | name: '击键(次/秒)', 266 | type: 'value', 267 | max: maxSpeed,// 10 + dData, 268 | min: minSpeed,//dData, 269 | axisLabel: { 270 | formatter: function (value) { return value; } 271 | } 272 | }, 273 | { 274 | name: '打字速度(字/分)', 275 | type: 'value', 276 | max: maxSpeedWords, 277 | interval: interval, 278 | min: minSpeedWords, 279 | axisLabel: { 280 | formatter: function (value) { return Math.floor(value); } 281 | }, 282 | show: true 283 | } 284 | ], 285 | series: [{ 286 | name: '击键', 287 | type: 'line', 288 | smooth: 0.5, 289 | data: speedList 290 | 291 | }, { 292 | yAxisIndex: 1, 293 | name: '打字', 294 | type: 'line', 295 | smooth: 0.5, 296 | data: speedWordsList 297 | 298 | }] 299 | }) 300 | } 301 | 302 | setTimeOut() { 303 | if (this.chart != null) { 304 | this.chart.resize(); 305 | } 306 | } 307 | componentDidMount() { 308 | setTimeout(this.draw, 10); 309 | window.addEventListener("resize", this.setTimeOut); 310 | } 311 | 312 | componentWillUnmount() { 313 | window.removeEventListener("resize", this.setTimeOut); 314 | 315 | } 316 | render() { 317 | return (
); 318 | } 319 | } 320 | 321 | export default Grade; 322 | 323 | /* 324 | 325 | 326 | import React, {Component} from "react"; 327 | import Konva from "konva"; 328 | import {render} from "react-dom"; 329 | import {Stage, Layer, Rect, Text,Line } from "react-konva"; 330 | 331 | class ColoredRect extends React.Component { 332 | state = { 333 | color: "green" 334 | }; 335 | handleClick = () => { 336 | this.setState({ 337 | color: Konva.Util.getRandomColor() 338 | }); 339 | }; 340 | render() { 341 | return ( 342 | 343 | 345 | 346 | ); 347 | } 348 | } 349 | class App extends Component { 350 | render() { 351 | return ( 352 | 353 | 354 | 355 | 356 | 357 | 358 | ); 359 | } 360 | } 361 | */ 362 | /* 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | {this.state.data.map(d => )} 373 | 374 |
时间击键打字错误率
375 | */ 376 | -------------------------------------------------------------------------------- /src/js/help.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Col, Nav, Navbar, NavItem, NavbarBrand, Row, Container, Card, CardBody, CardHeader } from "reactstrap"; 3 | import { NavLink } from "react-router-dom"; 4 | 5 | class Help extends Component { 6 | 7 | render() { 8 | return ( 9 | 关于 10 | 11 |

一些你可能不会注意到的细节

12 |
13 |
    14 |
  • 本工具运行时无需联网,所有数据都存储在你的本地。加载完毕后,您可以在地铁等没有信号的位置继续练习,数据不会丢失。
  • 15 |
  • 每次提高目标击键后,已练习的字会被重置,请谨慎操作。
  • 16 |
  • 多余的空格和字母,在判断正误时会忽略。
  • 17 |
  • 请把嵌入模式设为「编码」或「空白」,不要设为「首选」,否则二码及二码以上的字都会被判断错误。
  • 18 |
19 |
20 |
); 21 | } 22 | } 23 | 24 | export default Help; -------------------------------------------------------------------------------- /src/js/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import { HashRouter as Router, Route, Link } from "react-router-dom"; 5 | import { Container, Row, Col } from "reactstrap"; 6 | import db from './storage'; 7 | 8 | import Navbar from "./navbar"; 9 | 10 | import Left from "./left"; 11 | import Right from "./right"; 12 | 13 | const simpleStyle = `.card-header { 14 | display: none; 15 | } 16 | .card-body { 17 | padding: 0.6rem; 18 | } 19 | .card { 20 | margin-bottom: 0.2rem; 21 | } 22 | .navbar-all { 23 | display: none; 24 | } 25 | hr { 26 | margin-top: 0.2rem; 27 | margin-bottom: 0.5rem; 28 | }`; 29 | const commonStyle = `.navbar-simple{ 30 | display: none; 31 | } 32 | .card{ 33 | margin-bottom:1rem; 34 | }`; 35 | 36 | const SimpleStyle = (props) => { 37 | if (props.simple) { 38 | return (); 41 | } else { 42 | return (); 45 | } 46 | } 47 | 48 | 49 | Date.prototype.toLocaleDateString = function () { 50 | const y = this.getFullYear(); 51 | const m = this.getMonth() + 1; 52 | const date = this.getDate(); 53 | return `${y}/${m}/${date}`; 54 | } 55 | 56 | Date.prototype.toLocaleTimeString = function () { 57 | const h = this.getHours(); 58 | const m = this.getMinutes(); 59 | const s = this.getSeconds(); 60 | 61 | return `${h}:${m}:${s}` 62 | } 63 | 64 | class Index extends Component { 65 | constructor(props) { 66 | super(props); 67 | this.state = { 68 | night: parseInt(db.getItem("night")) || 0, 69 | simple: parseInt(db.getItem("simple")) || 0, 70 | display: "none" 71 | }; 72 | this.updateMode = this.updateMode.bind(this); 73 | 74 | this.styleMap = [ 75 | "https://cdn.bootcss.com/bootswatch/4.0.0/minty/bootstrap.min.css", 76 | "https://cdn.bootcss.com/bootswatch/4.0.0/flatly/bootstrap.min.css", 77 | "https://cdn.bootcss.com/bootswatch/4.0.0/darkly/bootstrap.min.css", 78 | "https://cdn.bootcss.com/bootswatch/4.0.0/sketchy/bootstrap.min.css" 79 | ]; 80 | //window.onload = this.show; 81 | } 82 | 83 | updateMode() { 84 | this.setState({ 85 | night: parseInt(db.getItem("night")) || 0, 86 | simple: parseInt(db.getItem("simple")) || 0 87 | }); 88 | } 89 | 90 | render() { 91 | return ( 92 | 93 |
94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
108 |
109 | ); 110 | } 111 | } 112 | 113 | const reactRootId = ReactDOM.render(, document.getElementById('root')); 114 | 115 | window.forceUpdate = () => { 116 | reactRootId.updateMode(); 117 | } 118 | 119 | /* 120 | import registerServiceWorker from './registerServiceWorker'; 121 | registerServiceWorker(); 122 | */ -------------------------------------------------------------------------------- /src/js/left.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import Route from "react-router-dom/Route"; 3 | import Typing from "./typing"; 4 | import Grade from "./grade"; 5 | import Config from "./config"; 6 | import Help from "./help"; 7 | 8 | class Left extends Component { 9 | render() { 10 | return ( 11 |
12 | 13 | 14 | 15 | 16 |
17 | ); 18 | } 19 | } 20 | 21 | export default Left; -------------------------------------------------------------------------------- /src/js/navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Col, Nav, Navbar, NavItem, NavbarBrand, Row, Container, Card, CardBody } from "reactstrap"; 3 | import { NavLink } from "react-router-dom"; 4 | 5 | class GNavbar extends Component { 6 | render() { 7 | return ( 8 | 9 | 10 | 打字 {" "} 11 | 成绩{" "} 12 | 设置{" "} 13 | 关于{" "} 14 | 小鹤双拼 15 | 16 | 17 | ); 18 | return null; 19 | return ( 20 | 21 | 22 | 打字 23 | 24 | 34 | 35 | ); 36 | } 37 | } 38 | 39 | export default GNavbar; -------------------------------------------------------------------------------- /src/js/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | } else { 39 | // Is not local host. Just register service worker 40 | registerValidSW(swUrl); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | function registerValidSW(swUrl) { 47 | navigator.serviceWorker 48 | .register(swUrl) 49 | .then(registration => { 50 | registration.onupdatefound = () => { 51 | const installingWorker = registration.installing; 52 | installingWorker.onstatechange = () => { 53 | if (installingWorker.state === 'installed') { 54 | if (navigator.serviceWorker.controller) { 55 | // At this point, the old content will have been purged and 56 | // the fresh content will have been added to the cache. 57 | // It's the perfect time to display a "New content is 58 | // available; please refresh." message in your web app. 59 | console.log('New content is available; please refresh.'); 60 | } else { 61 | // At this point, everything has been precached. 62 | // It's the perfect time to display a 63 | // "Content is cached for offline use." message. 64 | console.log('Content is cached for offline use.'); 65 | } 66 | } 67 | }; 68 | }; 69 | }) 70 | .catch(error => { 71 | console.error('Error during service worker registration:', error); 72 | }); 73 | } 74 | 75 | function checkValidServiceWorker(swUrl) { 76 | // Check if the service worker can be found. If it can't reload the page. 77 | fetch(swUrl) 78 | .then(response => { 79 | // Ensure service worker exists, and that we really are getting a JS file. 80 | if ( 81 | response.status === 404 || 82 | response.headers.get('content-type').indexOf('javascript') === -1 83 | ) { 84 | // No service worker found. Probably a different app. Reload the page. 85 | navigator.serviceWorker.ready.then(registration => { 86 | registration.unregister().then(() => { 87 | window.location.reload(); 88 | }); 89 | }); 90 | } else { 91 | // Service worker found. Proceed as normal. 92 | registerValidSW(swUrl); 93 | } 94 | }) 95 | .catch(() => { 96 | console.log( 97 | 'No internet connection found. App is running in offline mode.' 98 | ); 99 | }); 100 | } 101 | 102 | export function unregister() { 103 | if ('serviceWorker' in navigator) { 104 | navigator.serviceWorker.ready.then(registration => { 105 | registration.unregister(); 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/js/right.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Card, CardHeader, CardBody, ListGroup, ListGroupItem, ListGroupItemHeading } from "reactstrap"; 3 | import { NavLink } from "react-router-dom"; 4 | 5 | class Right extends Component { 6 | render() { 7 | return ( 8 |
9 | 10 | 打字 11 | 成绩 12 | 设置 13 | 关于 14 | Github 15 | 小鹤双拼 16 | 17 |
18 | ); 19 | } 20 | } 21 | 22 | export default Right; -------------------------------------------------------------------------------- /src/js/storage.js: -------------------------------------------------------------------------------- 1 | class WindowStorage { 2 | constructor() { 3 | this.db = {}; 4 | } 5 | 6 | setItem(key, value) { 7 | Object.assign(this.db, value); 8 | } 9 | 10 | getItem(key) { 11 | return this.db[key] || null; 12 | } 13 | 14 | removeItem(key) { 15 | delete this.db[key]; 16 | } 17 | } 18 | 19 | const db = window.localStorage ? window.localStorage : new WindowStorage(); 20 | 21 | export default db; -------------------------------------------------------------------------------- /src/js/test.jsx: -------------------------------------------------------------------------------- 1 | isYm(s){ 2 | return s == "a" || s == "o" || s == "e" || s == "i" || s =="u" || s == "v"; 3 | } 4 | 5 | spxh(s){ 6 | if(s.length == 1){ 7 | return s.toUpperCase(); 8 | } else { 9 | return xnheMap[s]; 10 | } 11 | } 12 | 13 | parseXnhe(){ 14 | let dict_xnhe = {}; 15 | let dict_xnhe2 = { 16 | }; 17 | 18 | for (let key in Dict){ 19 | let s,m; 20 | if (key.length ==1){ 21 | s = key; 22 | m = key; 23 | } else if (key.length == 2) { 24 | s = key[0]; 25 | m = key[1]; 26 | } else { 27 | if(key[1] == "h"){ 28 | s = key.substr(0,2); 29 | m = key.substr(2); 30 | } else { 31 | s = key.substr(0,1); 32 | if(this.isYm(s)){ 33 | m = key; 34 | } else { 35 | m = key.substr(1); 36 | } 37 | } 38 | } 39 | dict_xnhe[key] = this.spxh(s)+this.spxh(m); 40 | } 41 | console.log(JSON.stringify(dict_xnhe)); 42 | } 43 | -------------------------------------------------------------------------------- /src/js/typing.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Button, Card, CardBody, CardHeader, FormText, UncontrolledTooltip, Input } from "reactstrap"; 3 | import words from './words'; 4 | import db from './storage'; 5 | 6 | class OneCharacter extends Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | render() { 12 | const c = this.props.c; 13 | let className = ""; 14 | 15 | if (c.ok == null) { 16 | className = "text-muted"; 17 | } else { 18 | className = c.ok ? "text-success" : "text-danger"; 19 | } 20 | 21 | if (this.props.current) { 22 | className += " current-chatacter"; 23 | } 24 | 25 | return ( 26 |
27 | 28 | {c.c} 29 | 30 |
); 31 | } 32 | } 33 | 34 | class Text extends Component { 35 | constructor(props) { 36 | super(props); 37 | this.state = { 38 | text: [], 39 | currentIndex: 0 40 | }; 41 | 42 | this.saveGrade = this.saveGrade.bind(this); 43 | this.OneCharacter = this.OneCharacter.bind(this); 44 | this.onInput = this.onInput.bind(this); 45 | this.OneProbC = this.OneProbC.bind(this); 46 | this.grade = { 47 | errorNumber: 0, 48 | length: 0 49 | }; 50 | this.wordsSelect = db.getItem("wordsSelect") || ""; 51 | 52 | this.everyWordsProb = JSON.parse(db.getItem("wordsProbs") || "{}") || {}; 53 | 54 | this.updateOldDataToNew(); 55 | 56 | this.everyWordsCount = {}; 57 | this.everyWordsError = []; 58 | 59 | this.currentIndexInArticle = 0; 60 | 61 | this.wordsSelectRandom = this.wordsSelect; 62 | 63 | this.allInfo = JSON.parse(db.getItem("all_info")) || { 64 | number: 0, 65 | time: 0, 66 | days: {} 67 | }; 68 | 69 | this.updateProb(); 70 | this.update(); 71 | } 72 | 73 | updateOldDataToNew() { 74 | for (let i in this.everyWordsProb) { 75 | if (typeof this.everyWordsProb[i] != "number") { 76 | this.everyWordsProb[i] = this.everyWordsProb[i][0] / (this.everyWordsProb[i][1] + 1); 77 | } 78 | } 79 | } 80 | 81 | updateProb() { 82 | let obj = {} 83 | const text = this.wordsSelect.split(""); 84 | text.map(d => obj[d] = 0.5); 85 | Object.assign(obj, this.everyWordsProb); 86 | this.everyWordsProb = obj; 87 | db.setItem("wordsProbs", JSON.stringify(obj)); 88 | //this.wordsSelectRandom = ""; 89 | 90 | if (text.length > 0) { 91 | this.wordsSelectRandom += text[text.length - 1] + text[text.length - 1]; 92 | } 93 | 94 | /* 95 | for (const index in text) { 96 | const key = text[index]; 97 | const needNum = Math.max(1, text.indexOf(key) + 6 - text.length); 98 | 99 | this.everyWordsCount[key] = 10 - needNum; 100 | 101 | for (let i = 0; i < needNum; i++) { 102 | this.wordsSelectRandom += key; 103 | } 104 | } 105 | */ 106 | } 107 | 108 | updateWordsSelect() { 109 | let len = Math.max(this.wordsSelect.length + 1, 5); 110 | len = Math.min(len, words.length); 111 | this.wordsSelect = words.substr(0, len); 112 | if (len <= 10) { 113 | this.wordsSelectRandom += this.wordsSelect; 114 | } 115 | db.setItem("wordsSelect", this.wordsSelect); 116 | console.log("updateWordsSelect") 117 | this.updateProb(); 118 | } 119 | 120 | isWordOk(c) { 121 | const dCount = this.everyWordsCount[c] || 0; 122 | const prob = this.everyWordsProb[c] || 0.5; 123 | return prob >= 0.7 && dCount >= 2; 124 | } 125 | 126 | selectFromWords() { 127 | let res = ""; 128 | let wdLen = this.wordsSelectRandom.length || 1; 129 | const num = this.props.wdLen || 10; 130 | let i = 0; 131 | 132 | console.log(this.wordsSelectRandom); 133 | 134 | while (i < num) { 135 | const index = Math.min(wdLen - 1, Math.floor(Math.random() * (1 + wdLen))); 136 | const c = this.wordsSelectRandom[index]; 137 | //if (!this.isWordOk(c)) { 138 | res = res + c; 139 | i++; 140 | //} 141 | } 142 | return res; 143 | } 144 | selectFromArticle() { 145 | const num = this.props.wdLen || 10; 146 | let selectNum = 0; 147 | let res = ""; 148 | let findNum = 0 149 | while (selectNum < num && findNum < this.props.article.length) { 150 | findNum++; 151 | this.currentIndexInArticle++; 152 | if (this.currentIndexInArticle >= this.props.article.length) { 153 | this.currentIndexInArticle = 0; 154 | } 155 | const c = this.props.article[this.currentIndexInArticle]; 156 | if (this.wordsSelect.indexOf(c) >= 0) { 157 | if (!this.isWordOk(c)) { 158 | res = res + c; 159 | selectNum++; 160 | } 161 | } 162 | } 163 | if (findNum >= this.props.article.length) { 164 | return this.selectFromWords(); 165 | } 166 | 167 | return res; 168 | } 169 | selectArticle() { 170 | return this.selectFromWords(); 171 | /* 172 | if (!this.props.article) { 173 | return this.selectFromWords(); 174 | } 175 | return this.selectFromArticle(); 176 | */ 177 | // return this.selectFromArticle(); 178 | } 179 | text() { 180 | // this.props.goalWordSpeed; 181 | const num = this.props.wdLen || 10; 182 | const now = new Date().getTime() / 10; 183 | const wordsSpeed = num * 100 * 60 / (now - this.startTime); 184 | 185 | console.log("current word speed = ", wordsSpeed, this.props.goalWordSpeed, this.props.currentGrade, this.props.goalSpeed); 186 | 187 | 188 | if ((this.props.currentGrade >= this.props.goalSpeed && wordsSpeed >= this.props.goalWordSpeed) || this.wordsSelect.length < 5) { 189 | let allOk = true; 190 | console.log("check ok"); 191 | for (let i = 0; i < this.wordsSelect.length; i++) { 192 | if (!this.isWordOk(this.wordsSelect[i])) { 193 | console.log("Error:", this.wordsSelect[i]) 194 | allOk = false; 195 | break; 196 | } 197 | } 198 | if (allOk) { 199 | this.updateWordsSelect(); 200 | } 201 | } 202 | return this.selectArticle(); 203 | } 204 | 205 | saveGrade() { 206 | 207 | } 208 | 209 | update() { 210 | const text = this.text(); 211 | this.everyWordsError = []; 212 | 213 | this.state.text = []; 214 | let index = 0; 215 | 216 | for (let i = 0; i < text.length; i++) { 217 | const element = text[i]; 218 | this.state.text.push({ c: element, index: index, ok: null }); 219 | index++; 220 | } 221 | this.state.currentIndex = 0; 222 | Object.assign(this.grade, { errorNumber: 0, length: text.length }); 223 | } 224 | 225 | OneCharacter(c) { 226 | return 227 | } 228 | 229 | calculateOk() { 230 | const text = Object.assign([], this.state.text); 231 | this.allInfo.number += text.length; 232 | const d = new Date(); 233 | const endTime = d.getTime() / 10; 234 | this.allInfo.time += (endTime - this.startTime); 235 | 236 | this.allInfo.days = this.allInfo.days || {}; 237 | const todayStr = d.toLocaleDateString() 238 | 239 | let today = this.allInfo.days[todayStr] || { 240 | time: 0, 241 | number: 0, 242 | date: todayStr 243 | }; 244 | 245 | today.time = today.time + (endTime - this.startTime); 246 | today.number = today.number + text.length; 247 | 248 | this.allInfo.days[todayStr] = today; 249 | 250 | db.setItem("all_info", JSON.stringify(this.allInfo)); 251 | 252 | for (let i = 0; i < text.length; i++) { 253 | const c = text[i].c; 254 | 255 | this.everyWordsCount[c] = (this.everyWordsCount[c] || 0) + 1; 256 | const currentProb = Math.max(0, Math.min(1.0, this.everyWordsProb[c] || 0.5)); 257 | 258 | if (this.everyWordsError[i]) { 259 | this.everyWordsProb[c] = currentProb * 0.9; 260 | this.wordsSelectRandom += c; 261 | } else { 262 | this.everyWordsProb[c] = currentProb * 0.9 + 0.1; 263 | } 264 | } 265 | 266 | for (let i = 0; i < this.wordsSelect.length; i++) { 267 | const c = this.wordsSelect[i]; 268 | if (this.isWordOk(c)) { 269 | console.log("OK:", c, this.wordsSelectRandom.indexOf(c), this.wordsSelectRandom.lastIndexOf(c)) 270 | 271 | if (this.wordsSelectRandom.indexOf(c) != this.wordsSelectRandom.lastIndexOf(c)) { 272 | this.wordsSelectRandom = this.wordsSelectRandom.replace(c, ""); 273 | } 274 | } else { 275 | this.wordsSelectRandom += c; 276 | } 277 | } 278 | db.setItem("wordsProbs", JSON.stringify(this.everyWordsProb)); 279 | 280 | console.log(this.everyWordsProb); 281 | } 282 | 283 | onStart() { 284 | this.startTime = new Date().getTime() / 10; 285 | } 286 | 287 | onInput(code) { 288 | const count = Math.min(code.length, this.state.text.length); 289 | let text = Object.assign([], this.state.text); 290 | let allOk = true; 291 | 292 | for (let i = 0; i < count; i++) { 293 | const c = this.state.text[i].c; 294 | const zi = { 295 | c: c, 296 | ok: c == code[i], 297 | index: i 298 | }; 299 | allOk = allOk && zi.ok; 300 | if (!zi.ok) { 301 | this.everyWordsError[i] = true; 302 | } 303 | 304 | text[i] = zi; 305 | } 306 | if (code.length == this.state.text.length && allOk) { 307 | this.grade.errorNumber = 0; 308 | for (let i in this.everyWordsError) { 309 | if (this.everyWordsError[i]) { 310 | this.grade.errorNumber += 1; 311 | } 312 | } 313 | 314 | this.calculateOk(); 315 | this.props.onReset(this.grade); 316 | this.update(); 317 | return; 318 | } 319 | this.setState({ text: text, currentIndex: count }); 320 | } 321 | 322 | OneProbC(c, i) { 323 | const dCount = this.everyWordsCount[c] || 0; 324 | const prob = this.everyWordsProb[c] || 0.5; 325 | let probPercent = Math.floor(prob * 255); 326 | 327 | if (probPercent < 0) { probPercent = 0 }; 328 | if (probPercent > 255) { probPercent = 255 }; 329 | 330 | const color = ((255 - probPercent) << 16) | (probPercent << 8); 331 | 332 | let colorString = `000000${color.toString(16)}`; 333 | 334 | colorString = "#" + colorString.substr(-6); 335 | const probPercentShow = parseInt(probPercent * 100 * 10 / 255) / 10; 336 | 337 | return ; 338 | } 339 | 340 | render() { 341 | // const helpInfo = this.props.article ? "从文章中选择以下文字:" : "随机选择以下文字:"; 342 | const helpInfo = "随机选择以下文字:" 343 | const text = this.wordsSelect.split(""); 344 | return 345 | 文本 346 | 347 | 348 | {helpInfo} {text.map(this.OneProbC)} 349 | 350 |
351 |
352 | {this.state.text.map(this.OneCharacter)} 353 |
354 |
355 |
356 | } 357 | } 358 | 359 | class OneProbC extends Component { 360 | constructor(props) { 361 | super(props); 362 | this.state = { show: true }; 363 | this.toggle = this.toggle.bind(this); 364 | } 365 | 366 | toggle() { 367 | this.setState({ 368 | state: !this.state.show 369 | }); 370 | } 371 | 372 | render() { 373 | return ( 374 | {this.props.c} 375 | 376 | 正确率:{this.props.percent}% 377 | 378 | ); 379 | } 380 | 381 | } 382 | 383 | class Keyboard extends Component { 384 | constructor(props) { 385 | super(props); 386 | this.onChange = this.onChange.bind(this); 387 | this.onKeyDown = this.onKeyDown.bind(this); 388 | this.reset = this.reset.bind(this); 389 | 390 | this.state = { 391 | value: "", 392 | simple: parseInt(db.getItem("simple")) || 0 393 | }; 394 | this.start = false; 395 | this.onBlur = this.onBlur.bind(this) 396 | } 397 | 398 | onKeyDown(e) { 399 | if (!this.start) { 400 | this.start = true; 401 | this.props.onStart(); 402 | } 403 | this.props.onKeyDown(e); 404 | } 405 | 406 | onChange(e) { 407 | this.setState({ value: e.target.value }) 408 | let v = e.target.value; 409 | v = v.replace(/[a-z|\ ]+/ig, ""); 410 | this.props.onInput(v); 411 | } 412 | 413 | reset() { 414 | this.start = false; 415 | this.setState({ value: "" }) 416 | } 417 | 418 | componentDidMount() { 419 | this.refs["input"].focus(); 420 | } 421 | 422 | onBlur() { 423 | console.log("Blur"); 424 | this.refs["input"].focus(); 425 | } 426 | 427 | render() { 428 | return
429 |