├── .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 |
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 | {allInfo.time} |
162 | {allInfo.number} |
163 | {allInfo.speed} |
164 |
165 | {allDaysArr.map(this.OneLine)}
166 |
167 |
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
432 | }
433 | }
434 |
435 | class Typing extends Component {
436 | constructor(props) {
437 | super(props);
438 | this.onInput = this.onInput.bind(this);
439 | this.onStart = this.onStart.bind(this);
440 | this.onKeyDown = this.onKeyDown.bind(this);
441 | this.onReset = this.onReset.bind(this);
442 | this.updateGrade = this.updateGrade.bind(this);
443 |
444 | this.state = {
445 | currentGrade: 0,
446 | goalSpeed: parseInt(db.getItem("goalSpeed")) || 1,
447 | goalWordSpeed: parseInt(db.getItem("goalWordSpeed")) || 30,
448 | wdLen: parseInt(db.getItem("currentLen")) || 10,
449 | article: db.getItem("article") || ""
450 | }
451 | }
452 |
453 | onInput(e) {
454 | this.refs["text"].onInput(e);
455 | }
456 |
457 | onStart() {
458 | this.refs.grade.onStart();
459 | this.refs.text.onStart();
460 | }
461 |
462 | onKeyDown(e) {
463 | this.refs.grade.onKeyDown(e);
464 | }
465 |
466 | onReset(grade) {
467 | this.refs.input.reset();
468 | this.refs.grade.onEnd(grade);
469 | }
470 |
471 | updateGrade(grade) {
472 | this.setState({ currentGrade: grade });
473 | }
474 |
475 | updateConfig() {
476 | this.onReset();
477 | this.refs.text.update();
478 | }
479 |
480 | render() {
481 | return (
482 |
483 |
484 |
485 |
486 |
487 | );
488 | }
489 | }
490 |
491 | class CurrentGrade extends Component {
492 | constructor(props) {
493 | super(props);
494 | this.state = {
495 | speed: parseFloat(db.getItem("speed") || "0"),
496 | error: parseFloat(db.getItem("error") || "0"),
497 | wordsSpeed: parseFloat(db.getItem("wordsSpeed") || "0"),
498 | eachWordKey: parseFloat(db.getItem("eachWordKey") || "0")
499 | };
500 | this.currentGrade = {
501 | startTime: 0,
502 | endTime: 0,
503 | keyNum: 0,
504 | speed: 0
505 | }
506 |
507 | this.onStart = this.onStart.bind(this);
508 | this.onKeyDown = this.onKeyDown.bind(this);
509 | this.onEnd = this.onEnd.bind(this);
510 | this.currentTime = this.currentTime.bind(this);
511 | this.testId = 0;
512 | }
513 |
514 | currentTime() {
515 | return new Date().getTime();
516 | }
517 |
518 | onStart() {
519 | this.currentGrade.startTime = this.currentTime();
520 | }
521 |
522 | onKeyDown(e) {
523 | const current = this.currentTime();
524 | var keyNum = this.currentGrade.keyNum;
525 | var speed;
526 | keyNum++;
527 |
528 | /*
529 | if (e) {
530 | this.testId++;
531 | console.log(e.key, this.testId)
532 | }
533 |
534 | if (typeof e != "undefined" && typeof e.key != "undefined") {
535 | const validKey = "qwertyuiopasdfghjklzxcvbnm1234567890";
536 | if (validKey.indexOf(e.key) >= 0) {
537 | console.log("++")
538 | }
539 |
540 | }
541 | */
542 | speed = keyNum * 1000 / (current - this.currentGrade.startTime);
543 | Object.assign(this.currentGrade, {
544 | endTime: current,
545 | keyNum: keyNum,
546 | speed: speed
547 | });
548 |
549 | this.props.onGradeUpdate(speed);
550 | }
551 |
552 | saveGradeList(data) {
553 | const str = db.getItem("history") || "[]";
554 | const saveData = Object.assign({ time: this.currentTime() }, data);
555 | let arr = JSON.parse(str) || [];
556 | arr.push(saveData);
557 | if (arr.length >= 200) {
558 | const newArr = [];
559 | for (let i = 0; i < arr.length; i += 2) {
560 | let newObj = {};
561 | const i1 = i;
562 | const i2 = i1 + 1 > arr.length - 1 ? i1 : i1 + 1;
563 |
564 | for (var key in arr[i]) {
565 | newObj[key] = (arr[i1][key] + arr[i2][key]) / 2;
566 | }
567 |
568 | newObj.time = Math.floor(newObj.time);
569 |
570 | newArr.push(newObj);
571 | }
572 | arr = newArr;
573 | }
574 |
575 | const output = JSON.stringify(arr);
576 |
577 | db.setItem("history", output);
578 | }
579 |
580 | onEnd(grade) {
581 | this.onKeyDown();
582 | const error = grade.errorNumber / grade.length;
583 | const current = this.currentTime();
584 |
585 | const wordsSpeed = grade.length * 1000 * 60 / (current - this.currentGrade.startTime);
586 |
587 | const eachWordKey = Math.floor((this.currentGrade.speed * 60) / wordsSpeed * 100) / 100;
588 |
589 | const data = {
590 | speed: this.currentGrade.speed,
591 | error: error,
592 | wordsSpeed: wordsSpeed,
593 | eachWordKey: eachWordKey
594 | };
595 |
596 | this.setState(data);
597 |
598 | db.setItem("speed", this.currentGrade.speed.toString());
599 | db.setItem("error", error.toString());
600 | db.setItem("wordsSpeed", data.wordsSpeed.toString());
601 | db.setItem("eachWordKey", data.eachWordKey.toString());
602 |
603 | this.saveGradeList(data);
604 |
605 | Object.assign(this.currentGrade, {
606 | startTime: 0,
607 | endTime: 0,
608 | keyNum: 0
609 | });
610 | }
611 |
612 | render() {
613 | return
614 | 成绩
615 |
616 | 击键:{parseInt(this.state.speed * 100) / 100} {" | "}
617 | 打字:{parseInt(this.state.wordsSpeed * 100) / 100} {" | "}
618 | 码长:{this.state.eachWordKey} {" | "}
619 | 错误:{parseInt(this.state.error * 100)}%
620 |
621 |
622 | }
623 | }
624 |
625 | export default Typing;
--------------------------------------------------------------------------------
/src/js/words.js:
--------------------------------------------------------------------------------
1 | const words = "的一是在不了有和人这中大为上个国我以要他时来用们己笑生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海地口东导器压志世金增争济阶油思术极交受联什认六共权收证改清已美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细魔匆引听该铁价严首底液官德调随病苏失尔死讲配女黄推显谈罪神艺呢席含企望密批营项防举球英氧势告李台落木帮轮破亚师围注远字材排供河态封另施减树溶怎止案言士均武固叶鱼波视仅费紧爱左章早朝害续轻服试食充兵源判护司足某练差致板田降黑犯负击范继兴似余坚曲输修的故城夫够送笔船占右财吃富春职觉汉画功巴跟虽杂飞检吸助升阳互初创抗考投坏策古径换未跑留钢曾端责站简述钱副尽帝射草冲承独令限阿宣环双请超微让控州良轴找否纪益依优顶础载倒房突坐粉敌略客袁冷胜绝析块剂测丝协重诉念陈仍罗盐友洋错苦夜刑移频逐靠混母短皮终聚汽村云哪既距卫停烈央察烧行迅境若印洲刻括激孔搞甚室待核校散侵吧甲游久菜味旧模湖货损预阻毫普稳乙妈植息扩银语挥酒守拿序纸医缺雨吗针刘啊急唱误训愿审附获茶鲜粮斤孩脱硫肥善龙演父渐血欢械掌歌沙著刚攻谓盾讨晚粒乱燃矛乎杀药宁鲁贵钟煤读班伯香介迫句丰培握兰担弦蛋沉假穿执答乐谁顺烟缩征脸喜松脚困异免背星福买染井概慢怕磁倍祖皇促静补评翻肉践尼衣宽扬棉希伤操垂秋宜氢套笔督振架亮末宪庆编牛触映雷销诗座居抓裂胞呼娘景威绿晶厚盟衡鸡孙延危胶还屋乡临陆顾掉呀灯岁措束耐剧玉赵跳哥季课凯胡额款绍卷齐伟蒸殖永宗苗川炉岩弱零杨奏沿露杆探滑镇饭浓航怀赶库夺伊灵税了途灭赛归召鼓播盘裁险康唯录菌纯借糖盖横符私努堂域枪润幅哈竟熟虫泽脑壤碳欧遍侧寨敢彻虑斜薄庭都纳弹饲伸折麦湿暗荷瓦塞床筑恶户访塔奇透梁刀旋迹卡氯遇份毒泥退洗摆灰彩卖耗夏择忙铜献硬予繁圈雪函亦抽篇阵阴丁尺追堆雄迎泛爸楼避谋吨野猪旗累偏典馆索秦脂潮爷豆忽托惊塑遗愈朱替纤粗倾尚痛楚谢奋购磨君池旁碎骨监捕弟暴割贯殊释词亡壁顿宝午尘闻揭炮残冬桥妇警综招吴付浮遭徐您摇谷赞箱隔订男吹乐园纷唐败宋玻巨耕坦荣闭湾键凡驻锅救恩剥凝碱齿截炼麻纺禁废盛版缓净睛昌婚涉筒嘴插岸朗庄街藏姑贸腐奴啦惯乘伙恢匀纱扎辩耳彪臣亿璃抵脉秀萨俄网舞店喷纵寸汗挂洪着贺闪柬爆烯津稻墙软勇像滚厘蒙芳肯坡柱荡腿仪旅尾轧冰贡登黎削钻勒逃障氨郭峰币港伏轨亩毕擦莫刺浪秘援株健售股岛甘泡睡童铸汤阀休汇舍牧绕炸哲磷绩朋淡尖启陷柴呈徒颜泪稍忘泵蓝拖洞授镜辛壮锋贫虚弯摩泰幼廷尊窗纲弄隶疑氏宫姐震瑞怪尤琴循描膜违夹腰缘珠穷森枝竹沟催绳忆邦剩幸浆栏拥牙贮礼滤钠纹弹罢拍咱喊袖埃勤罚焦潜伍墨欲缝姓刊饱仿奖铝鬼丽跨默挖链扫喝袋炭污幕诸弧励梅奶洁灾舟鉴苯讼抱毁率懂寒智埔寄届跃渡挑丹艰贝碰拔爹戴码梦芽熔赤渔哭敬颗奔藏铅熟仲虎稀妹乏珍申桌遵允隆螺仓魏锐晓氮兼隐碍赫拨忠肃缸牵抢博巧壳兄杜讯诚碧祥柯页巡矩悲灌龄伦票寻桂铺圣恐恰郑趣抬荒腾贴柔滴猛阔辆妻填撤储签闹扰紫砂递戏吊陶伐喂疗瓶婆抚臂摸忍虾蜡邻胸巩挤偶弃槽劲乳邓吉仁烂砖租乌舰伴瓜浅丙暂燥橡柳迷暖牌纤秧胆详簧踏瓷谱呆宾糊洛辉愤竞隙怒粘乃绪肩籍敏涂熙皆侦悬掘享纠醒狂锁淀恨牲霸爬赏逆玩陵祝秒浙貌役彼悉鸭着趋凤晨畜辈秩卵署梯炎滩棋驱筛峡冒啥寿译浸泉帽迟硅疆贷漏稿冠嫩胁芯牢叛蚀奥鸣岭羊凭串塘绘酵融盆锡庙筹冻辅摄袭筋拒僚旱钾鸟漆沈眉疏添棒穗硝韩逼扭侨凉挺碗栽炒杯患馏劝豪辽勃鸿旦吏拜狗埋辊掩饮搬骂辞勾扣估蒋绒雾丈朵姆拟宇辑陕雕偿蓄崇剪倡厅咬驶薯刷斥番赋奉佛浇漫曼扇钙桃扶仔返俗亏腔鞋棱覆框悄叔撞骗勘旺沸孤粘吐孟渠屈疾妙惜仰狠胀谐抛霉桑岗嘛衰盗渗脏赖涌甜曹阅肌哩厉烃纬毅昨伪症煮叹钉搭茎笼酷偷弓锥恒杰坑鼻翼纶叙狱逮罐络棚抑膨蔬寺骤穆冶枯册尸凸绅坯牺焰轰欣晋瘦御锭锦丧旬锻垄搜佛扑邀亭酯迈舒脆酶闲忧酚顽羽涨卸仗陪薄辟惩杭姚肚捉飘漂昆欺吾郎烷汁呵饰萧雅邮迁燕撒姻赴宴烦削债帐斑铃旨醇董饼雏姿拌傅腹妥揉贤拆歪葡胺丢浩徽昂垫挡览贪慰缴汪慌冯诺姜谊凶劣诬耀昏躺盈骑乔溪丛卢抹易闷咨刮驾缆悟摘铒掷颇幻柄惠惨佳仇腊窝涤剑瞧堡泼葱罩霍捞胎苍滨俩捅湘砍霞邵萄疯淮遂熊粪烘宿档戈驳嫂裕徙箭捐肠撑晒辨殿莲摊搅酱屏疫哀蔡堵沫皱畅叠阁莱敲辖钩痕坝巷饿祸丘玄溜曰逻彭尝卿妨艇吞韦怨矮歇郊禄捻漠粹颠宏冤肪饥呵仙押挨醛娃拾没佩勿吓讹侯恋夕锌篡戚淋蓬岂釉兆泊魂拘亡杠摧氟颂浑凌铀诱犁谴颁舶扯嘉萌犹滋焊舌匹媳肺掠酿烹疲驰鸦窄辱狭朴遣菲奸韧辣拳秆卧醉竭茅墓矣哎艳敦舆缔雇尿葬履契禽渣衬躲赔咸溉贼醋堤抖妃裤廉晴挽掀茫丑亥拦悠阐慧佐奇竖孝柜麟绣遥逝愁肖昭芬逢窑捷圜盲闸宙辐披账狼幽绸蜂慎餐酬誓惟叉弥址帜芝砌唉仆涛臭翠盒劫慨炳阖寂椒倘拓畏喉巾颈垦拚兽蔽芦乾爽窃谭挣崩模褐传翅儒伞晃谬胚剖凑眠浊霜礁蔑抄闯洒碑蓉耶猜蹲壶唤澳锯郡玲绵纽梳掏吁锤鼠穴椅殷遮吵萍厌畜俱夸吕囊捧雌闽饶瞬郁哨凿朝俺浒茂肝勋盯籽耻菊滥稼戒奈帅鞭蚕镁询跌烤坛宅笛鄂蛮颤棍睁鼎岌降侍藩嚷匪岳糟缠迪泄卑氛堪萝盛碘缚悦澄甫攀屠溢拱晰携朽吟菱谦凹俊芒盼婶艘酰趁唇挫羞浴疼萎肴愚肿刨绞枢嫁慕舱铲苹豫谕迭潘顷翁榜匠欠茬畴胃沾踪弊哼鹏歧桐沃悼惑溃蔗荐潭孢露诊庸聪嫌厨庞祁钳肆梭赠崖篮颖甸藻捣且撕诏贞赐慈炕胖兹差琼锈汛卓棵馈挠灶婴蒂肤衫沥仑勉沪逸蜜浦嗓晕膏祭赢艾扮鹅怜蒲兔孕呖蘖挪淑谣惧廊缅俘骄膀陡宰诞峻";
2 |
3 | export default words;
--------------------------------------------------------------------------------
/src/prod.js:
--------------------------------------------------------------------------------
1 | import './js/index';
2 | import './css/app.css';
3 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 | const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
6 | const autoprefixer = require('autoprefixer');
7 |
8 | const publicPath = path.resolve(__dirname);
9 | // Some apps do not use client-side routing with pushState.
10 | // For these, "homepage" can be set to "." to enable relative asset paths.
11 | const shouldUseRelativeAssetPaths = publicPath === './';
12 |
13 | const cssFilename = 'static/css/[name].[contenthash:8].css';
14 | const extractTextPluginOptions = shouldUseRelativeAssetPaths ? {
15 | publicPath: Array(cssFilename.split('/').length).join('../')
16 | } : {};
17 |
18 | const shouldUseSourceMap = true;
19 |
20 | /*
21 | new webpack.optimize.UglifyJsPlugin({
22 | compress: {
23 | warnings: false,
24 | drop_debugger: true,
25 | drop_console: true
26 | }
27 | })
28 | */
29 |
30 | module.exports = {
31 | entry: {
32 | index: "./src/prod.js"
33 | },
34 | output: {
35 | // The build folder.
36 | path: path.resolve(publicPath, "build"),
37 | // Generated JS file names (with nested folders).
38 | // There will be one main bundle, and one file per asynchronous chunk.
39 | // We don't currently advertise code splitting but Webpack supports it.
40 | filename: 'static/js/[name].[chunkhash:8].js',
41 | chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js',
42 | // We inferred the "public path" (such as / or /my-project) from homepage.
43 | publicPath: "./",
44 | // Point sourcemap entries to original disk location (format as URL on Windows)
45 | devtoolModuleFilenameTemplate: info =>
46 | path
47 | .relative("./src", info.absoluteResourcePath)
48 | .replace(/\\/g, '/'),
49 | },
50 | externals: {
51 | 'react': 'React',
52 | 'react-dom': 'ReactDOM',
53 | "react-router": "ReactRouter",
54 | "react-router-dom": "ReactRouterDOM",
55 | "reactstrap": "Reactstrap",
56 | "echarts": "echarts"
57 | },
58 | resolve: {
59 | extensions: ['.web.js', '.mjs', '.js', '.json', '.web.jsx', '.jsx']
60 | },
61 | module: {
62 | // Process JS with Babel.
63 | rules: [{
64 | test: /\.(js|jsx|mjs)$/,
65 | include: path.resolve(publicPath, "src"),
66 | loader: require.resolve('babel-loader')
67 | },
68 | // The notation here is somewhat confusing.
69 | // "postcss" loader applies autoprefixer to our CSS.
70 | // "css" loader resolves paths in CSS and adds assets as dependencies.
71 | // "style" loader normally turns CSS into JS modules injecting