Browse Source

init template

roxas 4 years ago
commit
7a92d75591
44 changed files with 929 additions and 0 deletions
  1. 16
    0
      .editorconfig
  2. 2
    0
      .env
  3. 3
    0
      .eslintrc
  4. 18
    0
      .gitignore
  5. 7
    0
      .prettierignore
  6. 11
    0
      .prettierrc
  7. 53
    0
      .umirc.ts
  8. 0
    0
      mock/.gitkeep
  9. 60
    0
      package.json
  10. 10
    0
      src/app.ts
  11. 8
    0
      src/global.css
  12. 16
    0
      src/layouts/__tests__/index.test.tsx
  13. 31
    0
      src/layouts/index.less
  14. 14
    0
      src/layouts/index.tsx
  15. 5
    0
      src/locales/en-US.ts
  16. 5
    0
      src/locales/zh-CN.ts
  17. 0
    0
      src/models/.gitkeep
  18. 10
    0
      src/models/global.ts
  19. 16
    0
      src/pages/__tests__/index.test.tsx
  20. 10
    0
      src/pages/game/index.tsx
  21. 5
    0
      src/pages/index.css
  22. 11
    0
      src/pages/index.tsx
  23. 3
    0
      src/pages/main/index.less
  24. 44
    0
      src/pages/main/index.tsx
  25. 10
    0
      src/pages/rank/index.tsx
  26. 18
    0
      src/utils/game_setting/api_config.ts
  27. 4
    0
      src/utils/game_setting/game_config.ts
  28. BIN
      src/utils/logic_tools/GameModal/CloseButton/icon_quit.png
  29. BIN
      src/utils/logic_tools/GameModal/CloseButton/icon_quit@2x.png
  30. 36
    0
      src/utils/logic_tools/GameModal/CloseButton/index.tsx
  31. 44
    0
      src/utils/logic_tools/GameModal/game_modal.less
  32. 110
    0
      src/utils/logic_tools/GameModal/game_modal.tsx
  33. 73
    0
      src/utils/logic_tools/count_down.ts
  34. 13
    0
      src/utils/logic_tools/question_reporter.ts
  35. 99
    0
      src/utils/logic_tools/voice_play.ts
  36. 16
    0
      src/utils/tools/cookie_helper.ts
  37. 17
    0
      src/utils/tools/intl_helper.ts
  38. 8
    0
      src/utils/tools/mobile_tool.ts
  39. 48
    0
      src/utils/tools/request_helper.ts
  40. 28
    0
      src/utils/tools/visiable_helper.ts
  41. 16
    0
      src/utils/types/interface.ts
  42. 17
    0
      tsconfig.json
  43. 11
    0
      tslint.yml
  44. 3
    0
      typings.d.ts

+ 16
- 0
.editorconfig View File

@@ -0,0 +1,16 @@
1
+# http://editorconfig.org
2
+root = true
3
+
4
+[*]
5
+indent_style = space
6
+indent_size = 2
7
+end_of_line = lf
8
+charset = utf-8
9
+trim_trailing_whitespace = true
10
+insert_final_newline = true
11
+
12
+[*.md]
13
+trim_trailing_whitespace = false
14
+
15
+[Makefile]
16
+indent_style = tab

+ 2
- 0
.env View File

@@ -0,0 +1,2 @@
1
+BROWSER=none
2
+ESLINT=1

+ 3
- 0
.eslintrc View File

@@ -0,0 +1,3 @@
1
+{
2
+  "extends": "eslint-config-umi"
3
+}

+ 18
- 0
.gitignore View File

@@ -0,0 +1,18 @@
1
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+# dependencies
4
+/node_modules
5
+/npm-debug.log*
6
+/yarn-error.log
7
+/yarn.lock
8
+/package-lock.json
9
+
10
+# production
11
+/dist
12
+
13
+# misc
14
+.DS_Store
15
+
16
+# umi
17
+.umi
18
+.umi-production

+ 7
- 0
.prettierignore View File

@@ -0,0 +1,7 @@
1
+**/*.md
2
+**/*.svg
3
+**/*.ejs
4
+**/*.html
5
+package.json
6
+.umi
7
+.umi-production

+ 11
- 0
.prettierrc View File

@@ -0,0 +1,11 @@
1
+{
2
+  "singleQuote": true,
3
+  "trailingComma": "all",
4
+  "printWidth": 100,
5
+  "overrides": [
6
+    {
7
+      "files": ".prettierrc",
8
+      "options": { "parser": "json" }
9
+    }
10
+  ]
11
+}

+ 53
- 0
.umirc.ts View File

@@ -0,0 +1,53 @@
1
+import * as path from 'path';
2
+import { IConfig } from 'umi-types';
3
+
4
+// ref: https://umijs.org/config/
5
+const config: IConfig =  {
6
+  history: 'hash',
7
+  treeShaking: true,
8
+  disableGlobalVariables: true,
9
+  "define": {
10
+    "process.env.API_ENV": "development"
11
+  },
12
+  devtool: 'cheap-module-eval-source-map',
13
+  plugins: [
14
+    // ref: https://umijs.org/plugin/umi-plugin-react.html
15
+    ['umi-plugin-react', {
16
+      antd: true,
17
+      dva: {
18
+        immer: true
19
+      },
20
+      dynamicImport: false,
21
+      title: 'game_name',
22
+      dll: false,
23
+      locale: {
24
+        default: 'zh-CN', //默认语言 zh-CN
25
+        baseNavigator: true, // 为true时,用navigator.language的值作为默认语言
26
+        antd: true, // 是否启用antd的<LocaleProvider />
27
+      },
28
+      routes: {
29
+        exclude: [
30
+          /models\//,
31
+          /services\//,
32
+          /model\.(t|j)sx?$/,
33
+          /service\.(t|j)sx?$/,
34
+          /components\//,
35
+        ],
36
+      },
37
+    }],
38
+  ],
39
+  routes: [
40
+    {
41
+      path: '/',
42
+      component: '../layouts/index',
43
+      routes: [
44
+        { path: '/', component: './main' },
45
+        { path: '/main', component: './main' },
46
+        { path: '/game', component: './game' },
47
+        { path: '/rank', component: './rank' },
48
+      ],
49
+    },
50
+  ],
51
+}
52
+
53
+export default config;

+ 0
- 0
mock/.gitkeep View File


+ 60
- 0
package.json View File

@@ -0,0 +1,60 @@
1
+{
2
+  "private": true,
3
+  "scripts": {
4
+    "start": "cross-env API_ENV=development umi dev",
5
+    "startTest": "cross-env API_ENV=startTest umi dev",
6
+    "build": "cross-env API_ENV=production umi build",
7
+    "buildTest": "cross-env API_ENV=buildTest umi build",
8
+    "test": "umi test",
9
+    "lint:es": "eslint --ext .js src mock tests",
10
+    "lint:ts": "tslint \"src/**/*.ts\" \"src/**/*.tsx\"",
11
+    "precommit": "lint-staged"
12
+  },
13
+  "dependencies": {
14
+    "antd": "^3.19.5",
15
+    "dva": "^2.6.0-beta.6",
16
+    "lodash": "^4.17.15",
17
+    "react": "^16.8.6",
18
+    "react-dom": "^16.8.6"
19
+  },
20
+  "devDependencies": {
21
+    "@types/classnames": "^2.2.9",
22
+    "@types/jest": "^23.3.12",
23
+    "@types/lodash": "^4.14.136",
24
+    "@types/node": "^12.0.10",
25
+    "@types/react": "^16.7.18",
26
+    "@types/react-dom": "^16.0.11",
27
+    "@types/react-test-renderer": "^16.0.3",
28
+    "babel-eslint": "^9.0.0",
29
+    "classnames": "^2.2.6",
30
+    "cross-env": "^5.2.0",
31
+    "eslint": "^5.4.0",
32
+    "eslint-config-umi": "^1.4.0",
33
+    "eslint-plugin-flowtype": "^2.50.0",
34
+    "eslint-plugin-import": "^2.14.0",
35
+    "eslint-plugin-jsx-a11y": "^5.1.1",
36
+    "eslint-plugin-react": "^7.11.1",
37
+    "husky": "^0.14.3",
38
+    "lint-staged": "^7.2.2",
39
+    "react-test-renderer": "^16.7.0",
40
+    "tslint": "^5.12.0",
41
+    "tslint-eslint-rules": "^5.4.0",
42
+    "tslint-react": "^3.6.0",
43
+    "umi": "^2.8.0",
44
+    "umi-plugin-react": "^1.8.0",
45
+    "umi-types": "^0.3.0"
46
+  },
47
+  "lint-staged": {
48
+    "*.{ts,tsx}": [
49
+      "tslint --fix",
50
+      "git add"
51
+    ],
52
+    "*.{js,jsx}": [
53
+      "eslint --fix",
54
+      "git add"
55
+    ]
56
+  },
57
+  "engines": {
58
+    "node": ">=8.0.0"
59
+  }
60
+}

+ 10
- 0
src/app.ts View File

@@ -0,0 +1,10 @@
1
+import '@babel/polyfill'
2
+
3
+export const dva = {
4
+  config: {
5
+    onError(err: ErrorEvent) {
6
+      err.preventDefault();
7
+      console.error(err.message);
8
+    },
9
+  },
10
+};

+ 8
- 0
src/global.css View File

@@ -0,0 +1,8 @@
1
+html, body, #root {
2
+  margin: 0;
3
+  padding: 0;
4
+}
5
+
6
+#root {
7
+  width: auto;
8
+}

+ 16
- 0
src/layouts/__tests__/index.test.tsx View File

@@ -0,0 +1,16 @@
1
+import 'jest';
2
+import BasicLayout from '..';
3
+import React from 'react';
4
+import renderer, { ReactTestInstance, ReactTestRenderer } from 'react-test-renderer';
5
+
6
+describe('Layout: BasicLayout', () => {
7
+  it('Render correctly', () => {
8
+    const wrapper: ReactTestRenderer = renderer.create(<BasicLayout />);
9
+    expect(wrapper.root.children.length).toBe(1);
10
+    const outerLayer = wrapper.root.children[0] as ReactTestInstance;
11
+    expect(outerLayer.type).toBe('div');
12
+    const title = outerLayer.children[0] as ReactTestInstance;
13
+    expect(title.type).toBe('h1');
14
+    expect(title.children[0]).toBe('Yay! Welcome to umi!');
15
+  });
16
+});

+ 31
- 0
src/layouts/index.less View File

@@ -0,0 +1,31 @@
1
+
2
+.game_content {
3
+  font-family: Georgia, sans-serif;
4
+  text-align: center;
5
+}
6
+
7
+
8
+/* 2k以上分辨率 */
9
+@media screen and (min-width: 2560px) {
10
+}
11
+/* 正常分辨率 */
12
+@media only screen and (min-width: 1400px) and (max-width: 1920px) {
13
+}
14
+
15
+/* iPad Pro 分辨率 */
16
+@media only screen and (min-width: 1025px) and (max-width: 1399px) {
17
+}
18
+@media only screen and (min-width: 1024px) and (max-width: 1399px) and (orientation: portrait) {
19
+}
20
+/* iPad 分辨率 */
21
+@media only screen and (orientation: landscape) and (min-device-width: 768px) and (max-device-width: 1024px) {
22
+}
23
+@media only screen and (orientation: portrait) and (min-device-width: 768px) and (max-device-width: 1024px) and (min-device-height: 768px) and (max-device-height: 1024px) {
24
+}
25
+/* 手机分辨率 */
26
+@media only screen and (max-device-width: 960px) and (max-device-height: 960px) {
27
+}
28
+@media only screen and (max-device-width: 960px) and (max-device-height: 960px) and (orientation: portrait) {
29
+}
30
+
31
+@import "../utils/logic_tools/GameModal/game_modal.less";

+ 14
- 0
src/layouts/index.tsx View File

@@ -0,0 +1,14 @@
1
+import React from 'react';
2
+import styles from './index.less';
3
+import classNames from 'classnames';
4
+import { game_container_id } from '@/utils/game_setting/game_config';
5
+
6
+const BasicLayout: React.FC = props => {
7
+  return (
8
+    <div id={game_container_id} className={classNames(styles.game_content, styles['override-ant-modal'])}>
9
+      {props.children}
10
+    </div>
11
+  );
12
+};
13
+
14
+export default BasicLayout;

+ 5
- 0
src/locales/en-US.ts View File

@@ -0,0 +1,5 @@
1
+const en_US: any = {
2
+
3
+}
4
+
5
+export default en_US;

+ 5
- 0
src/locales/zh-CN.ts View File

@@ -0,0 +1,5 @@
1
+const zh_CN: any = {
2
+
3
+}
4
+
5
+export default zh_CN;

+ 0
- 0
src/models/.gitkeep View File


+ 10
- 0
src/models/global.ts View File

@@ -0,0 +1,10 @@
1
+export default {
2
+  state: {
3
+    count: 1,
4
+  },
5
+  reducers: {
6
+    add: (state: any, { payload }: any) => {
7
+      state.count += 1;
8
+    }
9
+  }
10
+}

+ 16
- 0
src/pages/__tests__/index.test.tsx View File

@@ -0,0 +1,16 @@
1
+import 'jest';
2
+import Index from '..';
3
+import React from 'react';
4
+import renderer, { ReactTestInstance, ReactTestRenderer } from 'react-test-renderer';
5
+
6
+
7
+describe('Page: index', () => {
8
+  it('Render correctly', () => {
9
+    const wrapper: ReactTestRenderer = renderer.create(<Index />);
10
+    expect(wrapper.root.children.length).toBe(1);
11
+    const outerLayer = wrapper.root.children[0] as ReactTestInstance;
12
+    expect(outerLayer.type).toBe('div');
13
+    expect(outerLayer.children.length).toBe(2);
14
+    
15
+  });
16
+});

+ 10
- 0
src/pages/game/index.tsx View File

@@ -0,0 +1,10 @@
1
+import React from 'react';
2
+
3
+
4
+export default function() {
5
+  return (
6
+    <div>
7
+      game
8
+    </div>
9
+  );
10
+}

+ 5
- 0
src/pages/index.css View File

@@ -0,0 +1,5 @@
1
+.normal {
2
+  font-family: Georgia, sans-serif;
3
+  margin-top: 4em;
4
+  text-align: center;
5
+}

+ 11
- 0
src/pages/index.tsx View File

@@ -0,0 +1,11 @@
1
+import React from 'react';
2
+import router from 'umi/router';
3
+import styles from './index.css';
4
+
5
+export default function() {
6
+  return (
7
+    <div className={styles.normal}>
8
+      /
9
+    </div>
10
+  );
11
+}

+ 3
- 0
src/pages/main/index.less View File

@@ -0,0 +1,3 @@
1
+.normal {
2
+  color: blue;
3
+}

+ 44
- 0
src/pages/main/index.tsx View File

@@ -0,0 +1,44 @@
1
+import React from 'react';
2
+import { Link } from 'react-router-dom';
3
+import { connect } from 'dva';
4
+import { PageProps } from '@/utils/types/interface';
5
+import { GameModalController } from '@/utils/logic_tools/GameModal/game_modal';
6
+import styles from './index.less';
7
+
8
+class Main extends React.Component<PageProps> {
9
+  state = {}
10
+
11
+  componentDidMount() {
12
+    const { dispatch } = this.props;
13
+    dispatch({
14
+      type: 'global/add'
15
+    })
16
+  }
17
+
18
+  getDerivedStateFromProps(props: PageProps, state: any) {
19
+
20
+  }
21
+
22
+  render() {
23
+    const { global } = this.props;
24
+    return (
25
+      <div className={styles.normal}>
26
+        <Link to="/rank">
27
+          main{global.count}
28
+        </Link>
29
+        <div
30
+          onClick={() => {
31
+            GameModalController.showPauseModal({
32
+              content: '123',
33
+              okText: 'ok',
34
+            });
35
+          }}
36
+        >
37
+          Modal
38
+        </div>
39
+      </div>
40
+    );
41
+  }
42
+}
43
+
44
+export default connect(({ global }: any) => ({ global }))(Main);

+ 10
- 0
src/pages/rank/index.tsx View File

@@ -0,0 +1,10 @@
1
+import React from 'react';
2
+
3
+
4
+export default function() {
5
+  return (
6
+    <div>
7
+      rank
8
+    </div>
9
+  );
10
+}

+ 18
- 0
src/utils/game_setting/api_config.ts View File

@@ -0,0 +1,18 @@
1
+const protocol = document.location.protocol;
2
+
3
+const ExportURL = {
4
+  API_SERVER_URL: "",
5
+  AUDIO_BASE_URL: "",
6
+  SETTING_SERVER_URL: "",
7
+  STATIC_URL: ""
8
+};
9
+
10
+if (process.env.API_ENV === "development") {
11
+} else if (process.env.API_ENV === "startTest") {
12
+} else if (process.env.API_ENV === "buildTest") {
13
+} else {
14
+}
15
+
16
+export const { API_SERVER_URL, STATIC_URL, SETTING_SERVER_URL, AUDIO_BASE_URL } = ExportURL;
17
+
18
+export default ExportURL;

+ 4
- 0
src/utils/game_setting/game_config.ts View File

@@ -0,0 +1,4 @@
1
+// 配置游戏参数
2
+// export const init_count_down = 20;
3
+// export const init_count_down_total = 20;
4
+export const game_container_id = "xxx_game_content";

BIN
src/utils/logic_tools/GameModal/CloseButton/icon_quit.png View File


BIN
src/utils/logic_tools/GameModal/CloseButton/icon_quit@2x.png View File


+ 36
- 0
src/utils/logic_tools/GameModal/CloseButton/index.tsx View File

@@ -0,0 +1,36 @@
1
+import React, { ReactHTMLElement } from "react";
2
+import PropTypes from "prop-types";
3
+
4
+import closeBtnPng from "./icon_quit.png";
5
+import closeBtn2xPng from "./icon_quit@2x.png";
6
+
7
+const ModalCloseBtn = (props: any): JSX.Element => (
8
+  <div
9
+    role="button"
10
+    tabIndex={0}
11
+    onKeyPress={() => {}}
12
+    onClick={() => props.onClick()}
13
+    style={{
14
+      position: "absolute",
15
+      cursor: "pointer",
16
+      top: "9px",
17
+      right: "9px",
18
+      width: "18px",
19
+      height: "18px",
20
+      outline: "none"
21
+    }}
22
+  >
23
+    <img
24
+      style={{ width: "100%", height: "100%" }}
25
+      src={closeBtnPng}
26
+      srcSet={`${closeBtnPng} 1400w, ${closeBtn2xPng} 2800w`}
27
+      alt="close_modal"
28
+    />
29
+  </div>
30
+);
31
+
32
+ModalCloseBtn.propTypes = {
33
+  onClick: PropTypes.oneOfType([PropTypes.any]).isRequired
34
+};
35
+
36
+export default ModalCloseBtn;

+ 44
- 0
src/utils/logic_tools/GameModal/game_modal.less View File

@@ -0,0 +1,44 @@
1
+/* ant-modal 默认覆盖样式 */
2
+.override-ant-modal {
3
+  :global(.ant-modal):global(.game-status-modal) {
4
+    width: 290px !important;
5
+    height: 165px;
6
+    top: 50%;
7
+    left: 50%;
8
+    transform-origin: 0 0 0 !important;
9
+    margin: -82.5px 0 0 -145px;
10
+
11
+    :global(.ant-modal-content) {
12
+      position: relative;
13
+      background-color: rgba(255, 255, 255, 1) !important;
14
+      border-radius: 7px;
15
+      border: none;
16
+      background-clip: padding-box;
17
+      box-shadow: 0 10px 15px rgba(106, 113, 119, 0.17) !important;
18
+
19
+
20
+      :global(.ant-modal-body) {
21
+        :global(.ant-modal-confirm-body) {
22
+          & > :global(.ant-modal-confirm-content) {
23
+            margin-left: 0;
24
+          }
25
+        }
26
+      }
27
+
28
+      :global(.ant-modal-confirm-btns) {
29
+        display: flex;
30
+        justify-content: center;
31
+        float: none;
32
+      }
33
+    }
34
+
35
+    :global(.ant-btn-primary) {
36
+      background-color: #1A91EB;
37
+      border-color: #1A91EB;
38
+    }
39
+  }
40
+
41
+  :global(.ant-modal-mask), :global(.ant-modal-wrap) {
42
+    position: absolute;
43
+  }
44
+}

+ 110
- 0
src/utils/logic_tools/GameModal/game_modal.tsx View File

@@ -0,0 +1,110 @@
1
+import React from "react";
2
+import { Modal } from "antd";
3
+import ModalCloseBtn from '@/utils/logic_tools/GameModal/CloseButton';
4
+import { game_container_id } from '@/utils/game_setting/game_config';
5
+import { intl_get } from '@/utils/tools/intl_helper';
6
+
7
+export interface ModalItem {
8
+  key: any,
9
+  type: string,
10
+}
11
+
12
+
13
+export class GameModalController {
14
+  public static modal_list: Array<ModalItem> = [];
15
+  constructor() {}
16
+
17
+  static destoryAll() {
18
+    Modal.destroyAll();
19
+  }
20
+
21
+  static recordModal(key: any, type: string) {
22
+    this.modal_list.push({
23
+      key,
24
+      type,
25
+    });
26
+  }
27
+
28
+  static checkBeforeModal(type: string) {
29
+    const target = this.findTypeModal(type);
30
+    target.map(item => this.closeTypeModal(item.key, type));
31
+  }
32
+
33
+  static findTypeModal(type: string) {
34
+    return this.modal_list.filter(item => item.type === type);
35
+  }
36
+
37
+  static closeTypeModal(key: any, type: string) {
38
+    const targetIndex = this.modal_list.findIndex(item => (item.key === key && item.type === type));
39
+    if (targetIndex || targetIndex === 0) {
40
+      const target = this.modal_list[targetIndex];
41
+      target.key.destroy();
42
+      this.modal_list.splice(targetIndex, 1);
43
+    }
44
+  }
45
+
46
+  static showPauseModal({
47
+    mount = document.getElementById(game_container_id),
48
+    className = "game-status-modal",
49
+    content = intl_get({ id: "pause_modal_tip_text" }),
50
+    okText = intl_get({ id: "pause_modal_ok_text" }),
51
+    onOk = () => {},
52
+    onCancel = () => {},
53
+  }: any = {}) {
54
+    this.checkBeforeModal("pause");
55
+    const result = Modal.info({
56
+      icon: null,
57
+      iconType: undefined,
58
+      title: null,
59
+      content: (
60
+        <div style={{ textAlign: 'center' }}>
61
+          <ModalCloseBtn onClick={() => { onCancel(); }} />
62
+          {content}
63
+        </div>
64
+      ),
65
+      okText,
66
+      width: 290,
67
+      className,
68
+      getContainer: () => mount || document.body,
69
+      onOk
70
+    });
71
+    this.recordModal(result, "pause");
72
+    if (document.body.style.overflow === "hidden") {
73
+      document.body.style.overflow = "";
74
+    }
75
+    return result;
76
+  }
77
+
78
+  static showErrorModal({
79
+    mount = document.getElementById(game_container_id),
80
+    className = "game-status-modal",
81
+    type = "common",
82
+    okText = intl_get({ id: "error_loading_ok_text" }),
83
+    onOk = () => {},
84
+    onCancel = () => {},
85
+  }: any = {}) {
86
+    this.checkBeforeModal("error");
87
+    let content = intl_get({ id: `error_loading_${type}_tip_text` });
88
+    const result = Modal.error({
89
+      icon: null,
90
+      iconType: undefined,
91
+      title: null,
92
+      content: (
93
+        <div style={{ textAlign: 'center' }}>
94
+          <ModalCloseBtn onClick={() => { onCancel(); }} />
95
+          {content}
96
+        </div>
97
+      ),
98
+      okText,
99
+      width: 290,
100
+      className,
101
+      getContainer: () => mount || document.body,
102
+      onOk
103
+    });
104
+    this.recordModal(result, "error");
105
+    if (document.body.style.overflow === "hidden") {
106
+      document.body.style.overflow = "";
107
+    }
108
+    return result;
109
+  }
110
+}

+ 73
- 0
src/utils/logic_tools/count_down.ts View File

@@ -0,0 +1,73 @@
1
+
2
+import { init_count_down, init_count_interval } from '@/utils/game_config';
3
+
4
+const delay = (timeout: any) => new Promise(resolve => setTimeout(resolve, timeout));
5
+
6
+interface CountDownClockBase {
7
+  task_id: NodeJS.Timeout;
8
+  count: number;
9
+  record_start_time: Date|any;
10
+  record_stop_time: Date|any;
11
+  is_clock_stop: boolean;
12
+  is_restart_timeout: boolean;
13
+  render: Function;
14
+}
15
+
16
+export class CountDownClock<CountDownClockBase> {
17
+  static task_id: NodeJS.Timeout;
18
+  static count = init_count_down;
19
+  static record_start_time: Date|any;
20
+  static record_stop_time: Date|any;
21
+  static is_clock_stop: boolean = true;
22
+  static is_restart_timeout: boolean = false;
23
+  static render: Function = () => {};
24
+
25
+  static _check_is_end() {
26
+    if (this.count <= 0) {
27
+      this.count = 0;
28
+      this.render();
29
+      clearInterval(this.task_id);
30
+      return true;
31
+    }
32
+    return false;
33
+  }
34
+
35
+  static async start() {
36
+    if (!this.is_clock_stop || this.is_restart_timeout) return;
37
+    if (this.record_stop_time && this.record_start_time) {
38
+      // 是暂停的开始, 精确计算暂停时间。
39
+      const time_gap = (this.record_stop_time.getTime() - this.record_start_time.getTime()) % init_count_interval;
40
+      this.is_restart_timeout = true;
41
+      await delay(init_count_interval - time_gap);
42
+      this.is_restart_timeout = false;
43
+      if (this._check_is_end()) {
44
+        return ;
45
+      }
46
+      this.count--;
47
+      this.render();
48
+    }
49
+    this.is_clock_stop = false;
50
+    clearInterval(this.task_id);
51
+    this.task_id = setInterval(() => {
52
+      if (this._check_is_end()) {
53
+        return ;
54
+      }
55
+      this.count--;
56
+      this.render();
57
+    }, init_count_interval);
58
+    this.record_start_time = new Date();
59
+  }
60
+  static async stop() {
61
+    if (this.is_restart_timeout) { setTimeout(() => { this.stop() }, 1000); return; }
62
+    this.is_clock_stop = true;
63
+    clearInterval(this.task_id);
64
+    this.record_stop_time = new Date();
65
+  }
66
+  static destory() {
67
+    clearInterval(this.task_id);
68
+    this.is_clock_stop = true;
69
+    this.is_restart_timeout = false;
70
+    this.record_start_time = undefined;
71
+    this.record_stop_time = undefined;
72
+  }
73
+}

+ 13
- 0
src/utils/logic_tools/question_reporter.ts View File

@@ -0,0 +1,13 @@
1
+export class QuestionReporter {
2
+  static error_question_list: Array<any> = [];
3
+  static error_count: number = 0;
4
+
5
+  static record_error(error_item: Object|Array<any>) {
6
+    if (Array.isArray(error_item)) {
7
+      this.error_question_list = [...this.error_question_list, ...error_item];
8
+    } else {
9
+      this.error_question_list.push(error_item);
10
+    }
11
+    // 检验有效错误
12
+  }
13
+}

+ 99
- 0
src/utils/logic_tools/voice_play.ts View File

@@ -0,0 +1,99 @@
1
+import request from '../request';
2
+import { AUDIO_BASE_URL } from '@/api_config';
3
+let audio_url = "";
4
+
5
+export function* getAudioSrc({ word, language = "zh-CN", gender = "FEMALE" }: any): any {
6
+  if (audio_url === "") {
7
+    const req_data = yield request(AUDIO_BASE_URL);
8
+    if (!req_data || !req_data.audio_base_url) {
9
+      return false;
10
+    }
11
+    audio_url = req_data.audio_base_url;
12
+  }
13
+  const word_src = `${audio_url}?text=${word}&languageCode=${language}&gender=${gender}`;
14
+  return word_src;
15
+}
16
+
17
+class AudioPlayer {
18
+  public AudioInstance: HTMLAudioElement;
19
+  public play_list: Array<string>;
20
+  public play_word: Array<string>;
21
+  public play_index: number;
22
+  public is_pause: boolean;
23
+
24
+  constructor() {
25
+    this.AudioInstance = new Audio();
26
+    this.play_list = [];
27
+    this.play_word = [];
28
+    this.play_index = 0;
29
+    this.is_pause = false;
30
+    this.AudioInstance.addEventListener("ended", () => {
31
+      if (this.play_list[this.play_index + 1]) {
32
+        // 播放下一个音频
33
+        this.play_index += 1;
34
+        this.playAudio();
35
+      }
36
+    });
37
+  }
38
+
39
+  playAudio = () => {
40
+    if (this.is_pause) {
41
+      this.is_pause = false;
42
+      this.AudioInstance.src = this.play_list[this.play_index];
43
+      this.AudioInstance.play();
44
+      return;
45
+    }
46
+    this.AudioInstance.src = this.play_list[this.play_index];
47
+    this.AudioInstance.load();
48
+    this.AudioInstance.play();
49
+    return;
50
+  };
51
+
52
+  addAudio = (play_src: string, play_word: string) => {
53
+    this.play_word.push(play_word);
54
+    this.play_list.push(play_src);
55
+  };
56
+
57
+  pauseAudio = () => {
58
+    this.is_pause = true;
59
+    this.AudioInstance.pause();
60
+  };
61
+
62
+  preloadAudio = () => {
63
+    this.AudioInstance.src = "";
64
+    this.AudioInstance.load();
65
+  };
66
+
67
+  clearAudioInstance = () => {
68
+    this.is_pause = true;
69
+    this.AudioInstance.pause();
70
+    this.play_list = [];
71
+    this.play_word = [];
72
+    this.play_index = 0;
73
+  };
74
+
75
+}
76
+
77
+export const ExportAudioPlayer = new AudioPlayer();
78
+
79
+export function subscriptAudioCanPlay(action: any) {
80
+  if (typeof window !== "undefined") {
81
+    ExportAudioPlayer.AudioInstance.addEventListener("canplaythrough", () => {
82
+      action({
83
+        type: "can_play"
84
+      });
85
+    });
86
+    ExportAudioPlayer.AudioInstance.addEventListener("error", (err: any) => {
87
+      action({
88
+        type: "load_fail",
89
+      });
90
+      ExportAudioPlayer.clearAudioInstance();
91
+    });
92
+  }
93
+}
94
+
95
+export default {
96
+  ExportAudioPlayer,
97
+  getAudioSrc,
98
+  subscriptAudioCanPlay,
99
+}

+ 16
- 0
src/utils/tools/cookie_helper.ts View File

@@ -0,0 +1,16 @@
1
+function getCookie(name: string): null|string {
2
+  if (typeof window !== "undefined") {
3
+    const reg = new RegExp(`(^| )${name}=([^;]*)(;|$)`);
4
+    const arr = document && document.cookie.match(reg);
5
+    if (arr) {
6
+      return unescape(arr[2]);
7
+    }
8
+  }
9
+  return null;
10
+}
11
+
12
+export const ak = getCookie('ak');
13
+export const lnk_lang = getCookie('lnk_lang');
14
+export const authorizationHeaders = {
15
+  Authorization: ak ? `Bearer ${ak}` : null
16
+};

+ 17
- 0
src/utils/tools/intl_helper.ts View File

@@ -0,0 +1,17 @@
1
+import zh_CN from '@/locales/zh-CN';
2
+import en_US from '@/locales/en-US';
3
+import { lnk_lang } from './cookie_helper';
4
+
5
+export const intl_get = ({ id }: { id: string }): any => {
6
+  if (lnk_lang) {
7
+    switch (lnk_lang) {
8
+      case "en-US":
9
+        return en_US[id];
10
+      case "zh-CN":
11
+      default:
12
+        return zh_CN[id];
13
+    }
14
+  } else {
15
+    return zh_CN[id];
16
+  }
17
+}

+ 8
- 0
src/utils/tools/mobile_tool.ts View File

@@ -0,0 +1,8 @@
1
+export function is_mobile() {
2
+  if (window) {
3
+    return /Android|webOS|iPhone|iPod|iPad|BlackBerry/i.test(
4
+      window.navigator.userAgent
5
+    );
6
+  }
7
+  return false;
8
+}

+ 48
- 0
src/utils/tools/request_helper.ts View File

@@ -0,0 +1,48 @@
1
+function checkStatus(response: any) {
2
+  if (response.status >= 200 && response.status < 300) {
3
+    return response;
4
+  }
5
+
6
+  const error = new Error(response.statusText);
7
+  throw error;
8
+}
9
+
10
+export function parseUrl(obj: any): string {
11
+  let result:string = "?";
12
+  for (let key in obj) {
13
+    if (obj[key]) {
14
+      if (result === "?") {
15
+        result += `${key}=${obj[key]}`;
16
+      } else {
17
+        result += `&${key}=${obj[key]}`;
18
+      }
19
+    }
20
+  }
21
+  if (result === "?") return "";
22
+  return result;
23
+}
24
+
25
+/**
26
+ * Requests a URL, returning a promise.
27
+ *
28
+ * @param  {string} url       The URL we want to request
29
+ * @param  {object} [options] The options we want to pass to "fetch"
30
+ * @return {object}           An object containing either "data" or "err"
31
+ */
32
+export default async function request(url: string, options: any = {}): Promise<any> {
33
+  let data, response: any;
34
+  try {
35
+    response = await fetch(url, { credentials: 'include', ...options });
36
+
37
+    checkStatus(response);
38
+    if (!response.body) return true;
39
+    data = await response.clone().json();
40
+  } catch (err) {
41
+    if (response && response.clone) {
42
+      data = await response.clone().text();
43
+    }
44
+    return false;
45
+  }
46
+
47
+  return data;
48
+}

+ 28
- 0
src/utils/tools/visiable_helper.ts View File

@@ -0,0 +1,28 @@
1
+const hiddenProperty: string =
2
+  "hidden" in document
3
+    ? "hidden"
4
+    : "webkitHidden" in document
5
+    ? "webkitHidden"
6
+    : "mozHidden" in document
7
+    ? "mozHidden"
8
+    : "";
9
+const visibilityChangeEvent = hiddenProperty.replace(
10
+  /hidden/i,
11
+  "visibilitychange"
12
+);
13
+
14
+export default function subscriptVisibility(action: any) {
15
+  if (typeof window !== "undefined" && visibilityChangeEvent) {
16
+    document.addEventListener(visibilityChangeEvent, () => {
17
+      if (!(document as any)[hiddenProperty]) {
18
+        action({
19
+          type: "show"
20
+        });
21
+      } else {
22
+        action({
23
+          type: "hide"
24
+        });
25
+      }
26
+    });
27
+  }
28
+}

+ 16
- 0
src/utils/types/interface.ts View File

@@ -0,0 +1,16 @@
1
+export interface PageProps {
2
+  dispatch: any;
3
+  location: any;
4
+  message: any;
5
+  global: any;
6
+}
7
+
8
+export interface MainProps extends PageProps {
9
+  main: any;
10
+}
11
+export interface GameProps extends PageProps {
12
+  game: any;
13
+}
14
+export interface RankProps extends PageProps {
15
+  rank: any;
16
+}

+ 17
- 0
tsconfig.json View File

@@ -0,0 +1,17 @@
1
+{
2
+  "compilerOptions": {
3
+    "target": "esnext",
4
+    "module": "esnext",
5
+    "moduleResolution": "node",
6
+    "importHelpers": true,
7
+    "jsx": "react",
8
+    "esModuleInterop": true,
9
+    "sourceMap": true,
10
+    "baseUrl": ".",
11
+    "strict": true,
12
+    "paths": {
13
+      "@/*": ["src/*"]
14
+    },
15
+    "allowSyntheticDefaultImports": true
16
+  }
17
+}

+ 11
- 0
tslint.yml View File

@@ -0,0 +1,11 @@
1
+defaultSeverity: error
2
+extends:
3
+  - tslint-react
4
+  - tslint-eslint-rules
5
+jsRules:
6
+rules:
7
+  eofline: true
8
+  no-console: true
9
+  no-construct: true
10
+  no-debugger: true
11
+  no-reference: true

+ 3
- 0
typings.d.ts View File

@@ -0,0 +1,3 @@
1
+declare module '*.css';
2
+declare module '*.less';
3
+declare module "*.png";